From 1cbd8d1b325d2766cdce02d9e20f4fc5ffd51ad6 Mon Sep 17 00:00:00 2001 From: Santiago Castro Date: Thu, 6 May 2021 22:10:03 -0400 Subject: [PATCH 001/846] Fix an Sphinx/RST code block in the docs --- docs/overview/about.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/overview/about.rst b/docs/overview/about.rst index cfda991c1..7a58f31ff 100644 --- a/docs/overview/about.rst +++ b/docs/overview/about.rst @@ -34,9 +34,10 @@ Bring your own FFmpeg PyAV can also be compiled against your own build of FFmpeg. While it must be built for the specific FFmpeg version installed it does not require a specific version. You can force installing PyAV from source by running: -``` -pip install av --no-binary av -``` +.. code-block:: bash + + pip install av --no-binary av + We automatically detect the differences that we depended on at build time. This is a fairly trial-and-error process, so please let us know if something won't compile due to missing functions or members. From c34dfce047b9bd34453bec3096840abb3c53780a Mon Sep 17 00:00:00 2001 From: Santiago Castro Date: Thu, 6 May 2021 21:07:38 -0400 Subject: [PATCH 002/846] Fix the Conda install link in README --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 961b5ff17..b9fe5ce98 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Another way of installing PyAV is via [conda-forge][conda-forge]: conda install av -c conda-forge ``` -See the [Conda quick install][conda-install] docs to get started with (mini)Conda. +See the [Conda install][conda-install] docs to get started with (mini)Conda. And if you want to build from the absolute source (for development or testing): @@ -78,5 +78,4 @@ Have fun, [read the docs][docs], [come chat with us][gitter], and good luck! [ffmpeg]: http://ffmpeg.org/ [conda-forge]: https://conda-forge.github.io/ -[conda-install]: https://conda.io/docs/install/quick.html - +[conda-install]: https://docs.conda.io/projects/conda/en/latest/user-guide/install/index.html From 89836a4e038173c1595843a3d4a8ee446708553d Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Mon, 12 Apr 2021 14:06:33 +0200 Subject: [PATCH 003/846] Export AudioStream --- av/audio/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/av/audio/__init__.py b/av/audio/__init__.py index 2986dd37c..74ddf6964 100644 --- a/av/audio/__init__.py +++ b/av/audio/__init__.py @@ -1 +1,2 @@ from .frame import AudioFrame +from .stream import AudioStream From 73d3ced8499e29104d11f0ba47e00bfa23f3116c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Wed, 29 Dec 2021 09:29:38 +0100 Subject: [PATCH 004/846] [build] update FFmpeg to 4.3.2 for binary wheels Mention Python 3.10 compatibility, drop Python 3.6. --- .github/workflows/tests.yml | 3 ++- scripts/fetch-vendor.json | 2 +- setup.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ad4287a3c..1e38e5e64 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -217,13 +217,14 @@ jobs: brew install pkg-config - name: Build wheels env: + CIBW_ARCHS_WINDOWS: AMD64 CIBW_BEFORE_BUILD: pip install cython && python scripts/fetch-vendor /tmp/vendor CIBW_BEFORE_BUILD_WINDOWS: pip install cython && python scripts\fetch-vendor C:\cibw\vendor CIBW_ENVIRONMENT_LINUX: LD_LIBRARY_PATH=/tmp/vendor/lib:$LD_LIBRARY_PATH PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig CIBW_ENVIRONMENT_MACOS: PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig LDFLAGS=-headerpad_max_install_names CIBW_ENVIRONMENT_WINDOWS: INCLUDE=C:\\cibw\\vendor\\include LIB=C:\\cibw\\vendor\\lib PYAV_SKIP_TESTS=unicode_filename CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: python scripts/inject-dll {wheel} {dest_dir} C:\cibw\vendor\bin - CIBW_SKIP: cp27-* pp27-* pp36-win* + CIBW_SKIP: cp36-* pp36-* *-musllinux* CIBW_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m unittest discover -t {project} -s tests && mv {project}/av.disabled {project}/av # disable test suite on OS X, the SSL config seems broken CIBW_TEST_COMMAND_MACOS: true diff --git a/scripts/fetch-vendor.json b/scripts/fetch-vendor.json index 95829f1eb..4cddc2f3b 100644 --- a/scripts/fetch-vendor.json +++ b/scripts/fetch-vendor.json @@ -1,3 +1,3 @@ { - "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/4.3.1-1/ffmpeg-{platform}.tar.gz"] + "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/4.3.2-1/ffmpeg-{platform}.tar.gz"] } diff --git a/setup.py b/setup.py index e4135425f..c89b94caa 100644 --- a/setup.py +++ b/setup.py @@ -526,11 +526,11 @@ def run(self): 'Operating System :: Unix', 'Operating System :: Microsoft :: Windows', 'Programming Language :: Cython', - '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', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Multimedia :: Sound/Audio', 'Topic :: Multimedia :: Sound/Audio :: Conversion', From b1ff8a7f7c2759f943f95dae3c7f74822e8b06e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Wed, 29 Dec 2021 10:31:52 +0100 Subject: [PATCH 005/846] Don't build PyPy 3.8 / Windows wheels There seems to be some Cython issue with Windows: src\av\buffer.c(2928): error C2078: too many initializers --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1e38e5e64..58484bb77 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -224,7 +224,7 @@ jobs: CIBW_ENVIRONMENT_MACOS: PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig LDFLAGS=-headerpad_max_install_names CIBW_ENVIRONMENT_WINDOWS: INCLUDE=C:\\cibw\\vendor\\include LIB=C:\\cibw\\vendor\\lib PYAV_SKIP_TESTS=unicode_filename CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: python scripts/inject-dll {wheel} {dest_dir} C:\cibw\vendor\bin - CIBW_SKIP: cp36-* pp36-* *-musllinux* + CIBW_SKIP: cp36-* pp36-* pp38-win* *-musllinux* CIBW_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m unittest discover -t {project} -s tests && mv {project}/av.disabled {project}/av # disable test suite on OS X, the SSL config seems broken CIBW_TEST_COMMAND_MACOS: true From f20e2c58b57fd3fd19bd83119154dca9b6ef4f51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Wed, 29 Dec 2021 19:05:38 +0100 Subject: [PATCH 006/846] Do not set max_analyze_duration at container init (fixes #801) libavformat seems to set its own default values, see: https://github.com/FFmpeg/FFmpeg/blob/8ff3fbf6bca0ee897e458fc27e5f967cdcbc16c7/libavformat/demux.c#L2393 --- av/container/core.pyx | 1 - 1 file changed, 1 deletion(-) diff --git a/av/container/core.pyx b/av/container/core.pyx index afbe2f6f6..80c92bde0 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -165,7 +165,6 @@ cdef class Container(object): self.ptr.interrupt_callback.opaque = &self.interrupt_callback_info self.ptr.flags |= lib.AVFMT_FLAG_GENPTS - self.ptr.max_analyze_duration = 10000000 # Setup Python IO. if self.file is not None: From f4a9df04dc08d28d1198af7b5550ad1e37b99aa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Wed, 29 Dec 2021 19:28:21 +0100 Subject: [PATCH 007/846] Release v8.1.0 --- CHANGELOG.rst | 19 ++++++++++++++++++- VERSION.txt | 2 +- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2fa11f678..9d4536e9b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,9 +16,26 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch, currently "develop"). -v8.0.4.dev0 +v8.1.0 ------ +Minor: + +- Update FFmpeg to 4.3.2 for the binary wheels. +- Provide binary wheels for Python 3.10 (:issue:`820`). +- Stop providing binary wheels for end-of-life Python 3.6. +- Fix args order in Frame.__repr__ (:issue:`749`). +- Fix documentation to remove unavailable QUIET log level (:issue:`719`). +- Expose codec_context.codec_tag (:issue:`741`). +- Add example for encoding with a custom PTS (:issue:`725`). +- Use av_packet_rescale_ts in Packet._rebase_time() (:issue:`737`). +- Do not hardcode errno values in test suite (:issue:`729`). +- Use av_guess_format for output container format (:issue:`691`). +- Fix setting CodecContext.extradata (:issue:`658`, :issue:`740`). +- Fix documentation code block indentation (:issue:`783`). +- Fix link to Conda installation instructions (:issue:`782`). +- Export AudioStream from av.audio (:issue:`775`). +- Fix setting CodecContext.extradata (:issue:`801`). v8.0.3 ------ diff --git a/VERSION.txt b/VERSION.txt index b6c32c94b..8104cabd3 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -8.0.4.dev0 +8.1.0 From f1a535a1ae643d8102dc095d6b25c694fcf01410 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Wed, 29 Dec 2021 19:37:34 +0100 Subject: [PATCH 008/846] Bump to next dev version. --- CHANGELOG.rst | 4 ++++ VERSION.txt | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9d4536e9b..dd1aae32f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,10 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch, currently "develop"). +v8.1.1.dev0 +------ + + v8.1.0 ------ diff --git a/VERSION.txt b/VERSION.txt index 8104cabd3..7e4c9637a 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -8.1.0 +8.1.1.dev0 From f7f5483d71db10ff81fe00b4f5c5bdc0652900c4 Mon Sep 17 00:00:00 2001 From: Julian Schweizer Date: Thu, 1 Apr 2021 13:07:50 +0200 Subject: [PATCH 009/846] Reflect the updated FFMPEG version in docs PyAV v8.1.0 ships with FFMPEG 4.3.2, which should be reflected in the documentation. --- docs/overview/about.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/overview/about.rst b/docs/overview/about.rst index 7a58f31ff..995aa450e 100644 --- a/docs/overview/about.rst +++ b/docs/overview/about.rst @@ -4,7 +4,7 @@ More About PyAV Binary wheels ------------- -Since release 8.0.0 binary wheels are provided on PyPI for Linux, Mac and Windows linked against FFmpeg. Currently FFmpeg 4.2.2 is used with the following features enabled for all platforms: +Since release 8.0.0 binary wheels are provided on PyPI for Linux, Mac and Windows linked against FFmpeg. Currently FFmpeg 4.3.2 is used with the following features enabled for all platforms: - fontconfig - libaom From 0728a0853577a7385424730d0eb300f8c2d9fe5f Mon Sep 17 00:00:00 2001 From: Santiago Castro Date: Thu, 30 Dec 2021 06:00:05 +0200 Subject: [PATCH 010/846] Add code highlighting in the README --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b9fe5ce98..2bf36844e 100644 --- a/README.md +++ b/README.md @@ -19,13 +19,13 @@ Installation Due to the complexity of the dependencies, PyAV is not always the easiest Python package to install from source. Since release 8.0.0 binary wheels are provided on [PyPI][pypi] for Linux, Mac and Windows linked against a modern FFmpeg. You can install these wheels by running: -``` +```bash pip install av ``` If you want to use your existing FFmpeg/Libav, the C-source version of PyAV is on [PyPI][pypi] too: -``` +```bash pip install av --no-binary av ``` @@ -34,7 +34,7 @@ Alternative installation methods Another way of installing PyAV is via [conda-forge][conda-forge]: -``` +```bash conda install av -c conda-forge ``` @@ -42,7 +42,7 @@ See the [Conda install][conda-install] docs to get started with (mini)Conda. And if you want to build from the absolute source (for development or testing): -``` +```bash git clone git@github.com:PyAV-Org/PyAV cd PyAV source scripts/activate.sh From a1af07efb064a9cfbb831247e4fcb72cbd7de520 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon-Martin=20Schr=C3=B6der?= Date: Fri, 31 Dec 2021 08:14:39 +0100 Subject: [PATCH 011/846] Validate pixel format in VideoCodecContext.pix_fmt setter (fixes #815) --- av/video/codeccontext.pyx | 10 +++++++--- av/video/format.pxd | 2 ++ av/video/format.pyx | 14 +++++++++++--- av/video/frame.pyx | 6 ++---- tests/test_codec_context.py | 13 +++++++++++++ tests/test_videoformat.py | 4 ++++ tests/test_videoframe.py | 4 ++++ 7 files changed, 43 insertions(+), 10 deletions(-) diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.pyx index f5eb9d07b..8dac3b3fe 100644 --- a/av/video/codeccontext.pyx +++ b/av/video/codeccontext.pyx @@ -6,7 +6,7 @@ from av.error cimport err_check from av.frame cimport Frame from av.packet cimport Packet from av.utils cimport avrational_to_fraction, to_avrational -from av.video.format cimport VideoFormat, get_video_format +from av.video.format cimport VideoFormat, get_pix_fmt, get_video_format from av.video.frame cimport VideoFrame, alloc_video_frame from av.video.reformatter cimport VideoReformatter @@ -93,13 +93,17 @@ cdef class VideoCodecContext(CodecContext): self.ptr.height = value self._build_format() - # TODO: Replace with `format`. property pix_fmt: + """ + The pixel format's name. + + :type: str + """ def __get__(self): return self._format.name def __set__(self, value): - self.ptr.pix_fmt = lib.av_get_pix_fmt(value) + self.ptr.pix_fmt = get_pix_fmt(value) self._build_format() property framerate: diff --git a/av/video/format.pxd b/av/video/format.pxd index f9110ae56..372821666 100644 --- a/av/video/format.pxd +++ b/av/video/format.pxd @@ -23,3 +23,5 @@ cdef class VideoFormatComponent(object): cdef VideoFormat get_video_format(lib.AVPixelFormat c_format, unsigned int width, unsigned int height) + +cdef lib.AVPixelFormat get_pix_fmt(const char *name) except lib.AV_PIX_FMT_NONE \ No newline at end of file diff --git a/av/video/format.pyx b/av/video/format.pyx index fc7002579..b96658272 100644 --- a/av/video/format.pyx +++ b/av/video/format.pyx @@ -8,6 +8,16 @@ cdef VideoFormat get_video_format(lib.AVPixelFormat c_format, unsigned int width format._init(c_format, width, height) return format +cdef lib.AVPixelFormat get_pix_fmt(const char *name) except lib.AV_PIX_FMT_NONE: + """Wrapper for lib.av_get_pix_fmt with error checking.""" + + cdef lib.AVPixelFormat pix_fmt = lib.av_get_pix_fmt(name) + + if pix_fmt == lib.AV_PIX_FMT_NONE: + raise ValueError('not a pixel format: %r' % name) + + return pix_fmt + cdef class VideoFormat(object): """ @@ -29,9 +39,7 @@ cdef class VideoFormat(object): self._init(other.pix_fmt, width or other.width, height or other.height) return - cdef lib.AVPixelFormat pix_fmt = lib.av_get_pix_fmt(name) - if pix_fmt < 0: - raise ValueError('not a pixel format: %r' % name) + cdef lib.AVPixelFormat pix_fmt = get_pix_fmt(name) self._init(pix_fmt, width, height) cdef _init(self, lib.AVPixelFormat pix_fmt, unsigned int width, unsigned int height): diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 62601ce0e..b140b0c9b 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -2,7 +2,7 @@ from libc.stdint cimport uint8_t from av.enum cimport define_enum from av.error cimport err_check -from av.video.format cimport VideoFormat, get_video_format +from av.video.format cimport VideoFormat, get_pix_fmt, get_video_format from av.video.plane cimport VideoPlane from av.deprecation import renamed_attr @@ -71,9 +71,7 @@ cdef class VideoFrame(Frame): if width is _cinit_bypass_sentinel: return - cdef lib.AVPixelFormat c_format = lib.av_get_pix_fmt(format) - if c_format < 0: - raise ValueError('invalid format %r' % format) + cdef lib.AVPixelFormat c_format = get_pix_fmt(format) self._init(c_format, width, height) diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 9f68785bf..77049b1c5 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -88,6 +88,19 @@ def test_encoder_extradata(self): ctx.extradata = b"123" self.assertEqual(str(cm.exception), "Can only set extradata for decoders.") + def test_encoder_pix_fmt(self): + ctx = av.codec.Codec('h264', 'w').create() + + # valid format + ctx.pix_fmt = "yuv420p" + self.assertEqual(ctx.pix_fmt, "yuv420p") + + # invalid format + with self.assertRaises(ValueError) as cm: + ctx.pix_fmt = "__unknown_pix_fmt" + self.assertEqual(str(cm.exception), "not a pixel format: '__unknown_pix_fmt'") + self.assertEqual(ctx.pix_fmt, "yuv420p") + def test_parse(self): # This one parses into a single packet. diff --git a/tests/test_videoformat.py b/tests/test_videoformat.py index c46b2de34..4c72fdb00 100644 --- a/tests/test_videoformat.py +++ b/tests/test_videoformat.py @@ -4,6 +4,10 @@ class TestVideoFormats(TestCase): + def test_invalid_pixel_format(self): + with self.assertRaises(ValueError) as cm: + VideoFormat("__unknown_pix_fmt", 640, 480) + self.assertEqual(str(cm.exception), "not a pixel format: '__unknown_pix_fmt'") def test_rgb24_inspection(self): fmt = VideoFormat('rgb24', 640, 480) diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index a016f81b4..c746b7cf5 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -10,6 +10,10 @@ class TestVideoFrameConstructors(TestCase): + def test_invalid_pixel_format(self): + with self.assertRaises(ValueError) as cm: + VideoFrame(640, 480, "__unknown_pix_fmt") + self.assertEqual(str(cm.exception), "not a pixel format: '__unknown_pix_fmt'") def test_null_constructor(self): frame = VideoFrame() From 9785425285b8c28d0b0af603eb5692de726ba9e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Thu, 30 Dec 2021 18:22:00 +0100 Subject: [PATCH 012/846] [build] add pyproject.toml to ensure Cython is present --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..e9f294dd4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[build-system] +requires = ["setuptools", "wheel", "cython"] From 95475cc19e5e43b56225ae00e253adbaf5d3a35d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Thu, 30 Dec 2021 10:45:28 +0100 Subject: [PATCH 013/846] [build] simplify setup.py - remove `distutils` import, it's deprecated - remove msvc-specific hacks --- setup.py | 117 ++----------------------------------------------------- 1 file changed, 4 insertions(+), 113 deletions(-) diff --git a/setup.py b/setup.py index c89b94caa..9ec50402f 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,5 @@ -from distutils.ccompiler import new_compiler as _new_compiler -from distutils.command.clean import clean, log -from distutils.core import Command -from distutils.dir_util import remove_tree -from distutils.errors import DistutilsExecError -from distutils.msvccompiler import MSVCCompiler -from setuptools import setup, find_packages, Extension, Distribution -from setuptools.command.build_ext import build_ext from shlex import quote -from subprocess import Popen, PIPE +from subprocess import PIPE, Popen import argparse import errno import os @@ -16,15 +8,9 @@ import shlex import sys -try: - # This depends on _winreg, which is not available on not-Windows. - from distutils.msvc9compiler import MSVCCompiler as MSVC9Compiler -except ImportError: - MSVC9Compiler = None -try: - from distutils._msvccompiler import MSVCCompiler as MSVC14Compiler -except ImportError: - MSVC14Compiler = None +from setuptools import Command, Extension, find_packages, setup +from setuptools.command.build_ext import build_ext + try: from Cython import __version__ as cython_version @@ -170,67 +156,6 @@ def dump_config(): print('\t%s=%s' % x) -# Monkey-patch for CCompiler to be silent. -def _CCompiler_spawn_silent(cmd, dry_run=None): - """Spawn a process, and eat the stdio.""" - proc = Popen(cmd, stdout=PIPE, stderr=PIPE) - out, err = proc.communicate() - if proc.returncode: - raise DistutilsExecError(err) - -def new_compiler(*args, **kwargs): - """Create a C compiler. - - :param bool silent: Eat all stdio? Defaults to ``True``. - - All other arguments passed to ``distutils.ccompiler.new_compiler``. - - """ - make_silent = kwargs.pop('silent', True) - cc = _new_compiler(*args, **kwargs) - # If MSVC10, initialize the compiler here and add /MANIFEST to linker flags. - # See Python issue 4431 (https://bugs.python.org/issue4431) - if is_msvc(cc): - from distutils.msvc9compiler import get_build_version - if get_build_version() == 10: - cc.initialize() - for ldflags in [cc.ldflags_shared, cc.ldflags_shared_debug]: - unique_extend(ldflags, ['/MANIFEST']) - # If MSVC14, do not silence. As msvc14 requires some custom - # steps before the process is spawned, we can't monkey-patch this. - elif get_build_version() == 14: - make_silent = False - # monkey-patch compiler to suppress stdout and stderr. - if make_silent: - cc.spawn = _CCompiler_spawn_silent - return cc - - -_msvc_classes = tuple(filter(None, (MSVCCompiler, MSVC9Compiler, MSVC14Compiler))) -def is_msvc(cc=None): - cc = _new_compiler() if cc is None else cc - return isinstance(cc, _msvc_classes) - - -if os.name == 'nt': - - if is_msvc(): - config_macros['inline'] = '__inline' - - # Since we're shipping a self contained unit on Windows, we need to mark - # the package as such. On other systems, let it be universal. - class BinaryDistribution(Distribution): - def is_pure(self): - return False - - distclass = BinaryDistribution - -else: - - # Nothing to see here. - distclass = Distribution - - # Monkey-patch Cython to not overwrite embedded signatures. if cythonize: @@ -320,13 +245,6 @@ def run(self): os.environ[name] = unknown update_extend(extension_extra, known) - if is_msvc(new_compiler(compiler=self.compiler)): - # Assume we have to disable /OPT:REF for MSVC with ffmpeg - config = { - 'extra_link_args': ['/OPT:NOREF'], - } - update_extend(extension_extra, config) - # Check if we're using pkg-config or not if self.no_pkg_config: # Simply assume we have everything we need! @@ -375,27 +293,6 @@ def run(self): setattr(ext, key, value) -class CleanCommand(clean): - - user_options = clean.user_options + [ - ('sources', None, - "remove Cython build output (C sources)")] - - boolean_options = clean.boolean_options + ['sources'] - - def initialize_options(self): - clean.initialize_options(self) - self.sources = None - - def run(self): - clean.run(self) - if self.sources: - if os.path.exists('src'): - remove_tree('src', dry_run=self.dry_run) - else: - log.info("'%s' does not exist -- can't clean it", 'src') - - class CythonizeCommand(Command): user_options = [] @@ -410,8 +307,6 @@ def run(self): # the existing extension instead of replacing them all. for i, ext in enumerate(self.distribution.ext_modules): if any(s.endswith('.pyx') for s in ext.sources): - if is_msvc(): - ext.define_macros.append(('inline', '__inline')) new_ext = cythonize( ext, compiler_directives=dict( @@ -503,7 +398,6 @@ def run(self): cmdclass={ 'build_ext': BuildExtCommand, - 'clean': CleanCommand, 'config': ConfigCommand, 'cythonize': CythonizeCommand, }, @@ -537,7 +431,4 @@ def run(self): 'Topic :: Multimedia :: Video', 'Topic :: Multimedia :: Video :: Conversion', ], - - distclass=distclass, - ) From 8707fea24c7163b99a2bac7c2115932d7cf22f69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Mon, 24 Jan 2022 15:40:43 +0100 Subject: [PATCH 014/846] Fix VideoFrame.to_image with height & width (fixes #878) If the user passes height / width to VideoFrame.to_image(), the output of reformat() may have a different size than the original frame, so we cannot rely on `self.height` or `self.width`. --- av/video/frame.pyx | 2 +- tests/test_videoframe.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/av/video/frame.pyx b/av/video/frame.pyx index b140b0c9b..5b0253a89 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -233,7 +233,7 @@ cdef class VideoFrame(Frame): i_pos += i_stride o_pos += o_stride - return Image.frombytes("RGB", (self.width, self.height), bytes(o_buf), "raw", "RGB", 0, 1) + return Image.frombytes("RGB", (plane.width, plane.height), bytes(o_buf), "raw", "RGB", 0, 1) def to_ndarray(self, **kwargs): """Get a numpy array of this frame. diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index c746b7cf5..31022eed9 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -149,6 +149,12 @@ def test_to_image_rgb24(self): self.assertEqual(img.size, (width, height)) self.assertEqual(img.tobytes(), expected) + def test_to_image_with_dimensions(self): + frame = VideoFrame(640, 480, format='rgb24') + + img = frame.to_image(width=320, height=240) + self.assertEqual(img.size, (320, 240)) + class TestVideoFrameNdarray(TestCase): From 6443f55e7d07948a93cc03f417e3cb66227fd6bf Mon Sep 17 00:00:00 2001 From: Justin Wong <46082645+uvjustin@users.noreply.github.com> Date: Wed, 5 Jan 2022 23:45:34 +0800 Subject: [PATCH 015/846] Replace deprecated av_init_packet with av_packet_alloc --- av/codec/context.pyx | 6 ++--- av/container/input.pyx | 8 +++--- av/container/output.pyx | 11 ++++---- av/packet.pxd | 2 +- av/packet.pyx | 48 +++++++++++++++++----------------- av/stream.pyx | 2 +- av/subtitles/codeccontext.pyx | 2 +- include/libavcodec/avcodec.pxd | 6 ++--- 8 files changed, 41 insertions(+), 44 deletions(-) diff --git a/av/codec/context.pyx b/av/codec/context.pyx index d93fb64f5..ec27d02ed 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -376,7 +376,7 @@ cdef class CodecContext(object): # ... but this results in corruption. packet = Packet(out_size) - memcpy(packet.struct.data, out_data, out_size) + memcpy(packet.ptr.data, out_data, out_size) packets.append(packet) @@ -417,7 +417,7 @@ cdef class CodecContext(object): cdef int res with nogil: - res = lib.avcodec_send_packet(self.ptr, &packet.struct if packet is not None else NULL) + res = lib.avcodec_send_packet(self.ptr, packet.ptr if packet is not None else NULL) err_check(res) out = [] @@ -459,7 +459,7 @@ cdef class CodecContext(object): cdef int res with nogil: - res = lib.avcodec_receive_packet(self.ptr, &packet.struct) + res = lib.avcodec_receive_packet(self.ptr, packet.ptr) if res == -EAGAIN or res == lib.AVERROR_EOF: return err_check(res) diff --git a/av/container/input.pyx b/av/container/input.pyx index 64612d84e..4b3a5ea5e 100644 --- a/av/container/input.pyx +++ b/av/container/input.pyx @@ -138,18 +138,18 @@ cdef class InputContainer(Container): try: self.start_timeout() with nogil: - ret = lib.av_read_frame(self.ptr, &packet.struct) + ret = lib.av_read_frame(self.ptr, packet.ptr) self.err_check(ret) except EOFError: break - if include_stream[packet.struct.stream_index]: + if include_stream[packet.ptr.stream_index]: # If AVFMTCTX_NOHEADER is set in ctx_flags, then new streams # may also appear in av_read_frame(). # http://ffmpeg.org/doxygen/trunk/structAVFormatContext.html # TODO: find better way to handle this - if packet.struct.stream_index < len(self.streams): - packet._stream = self.streams[packet.struct.stream_index] + if packet.ptr.stream_index < len(self.streams): + packet._stream = self.streams[packet.ptr.stream_index] # Keep track of this so that remuxing is easier. packet._time_base = packet._stream._stream.time_base yield packet diff --git a/av/container/output.pyx b/av/container/output.pyx index 9569c3e9d..c1a47b5bf 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -210,18 +210,17 @@ cdef class OutputContainer(Container): self.start_encoding() # Assert the packet is in stream time. - if packet.struct.stream_index < 0 or packet.struct.stream_index >= self.ptr.nb_streams: + if packet.ptr.stream_index < 0 or packet.ptr.stream_index >= self.ptr.nb_streams: raise ValueError('Bad Packet stream_index.') - cdef lib.AVStream *stream = self.ptr.streams[packet.struct.stream_index] + cdef lib.AVStream *stream = self.ptr.streams[packet.ptr.stream_index] packet._rebase_time(stream.time_base) # Make another reference to the packet, as av_interleaved_write_frame # takes ownership of it. - cdef lib.AVPacket packet_ref - lib.av_init_packet(&packet_ref) - self.err_check(lib.av_packet_ref(&packet_ref, &packet.struct)) + cdef lib.AVPacket *packet_ptr = lib.av_packet_alloc() + self.err_check(lib.av_packet_ref(packet_ptr, packet.ptr)) cdef int ret with nogil: - ret = lib.av_interleaved_write_frame(self.ptr, &packet_ref) + ret = lib.av_interleaved_write_frame(self.ptr, packet_ptr) self.err_check(ret) diff --git a/av/packet.pxd b/av/packet.pxd index 317443fde..ca21e6b76 100644 --- a/av/packet.pxd +++ b/av/packet.pxd @@ -7,7 +7,7 @@ from av.stream cimport Stream cdef class Packet(Buffer): - cdef lib.AVPacket struct + cdef lib.AVPacket* ptr cdef Stream _stream diff --git a/av/packet.pyx b/av/packet.pyx index 2ffbcca62..fae970ee3 100644 --- a/av/packet.pyx +++ b/av/packet.pyx @@ -18,7 +18,7 @@ cdef class Packet(Buffer): def __cinit__(self, input=None): with nogil: - lib.av_init_packet(&self.struct) + self.ptr = lib.av_packet_alloc() def __init__(self, input=None): @@ -35,7 +35,7 @@ cdef class Packet(Buffer): size = source.length if size: - err_check(lib.av_new_packet(&self.struct, size)) + err_check(lib.av_new_packet(self.ptr, size)) if source is not None: self.update(source) @@ -45,7 +45,7 @@ cdef class Packet(Buffer): def __dealloc__(self): with nogil: - lib.av_packet_unref(&self.struct) + lib.av_packet_free(&self.ptr) def __repr__(self): return '' % ( @@ -53,15 +53,15 @@ cdef class Packet(Buffer): self._stream.index if self._stream else 0, self.dts, self.pts, - self.struct.size, + self.ptr.size, id(self), ) # Buffer protocol. cdef size_t _buffer_size(self): - return self.struct.size + return self.ptr.size cdef void* _buffer_ptr(self): - return self.struct.data + return self.ptr.data cdef _rebase_time(self, lib.AVRational dst): @@ -75,7 +75,7 @@ cdef class Packet(Buffer): if self._time_base.num == dst.num and self._time_base.den == dst.den: return - lib.av_packet_rescale_ts(&self.struct, self._time_base, dst) + lib.av_packet_rescale_ts(self.ptr, self._time_base, dst) self._time_base = dst @@ -101,7 +101,7 @@ cdef class Packet(Buffer): property stream_index: def __get__(self): - return self.struct.stream_index + return self.ptr.stream_index property stream: """ @@ -112,7 +112,7 @@ cdef class Packet(Buffer): def __set__(self, Stream stream): self._stream = stream - self.struct.stream_index = stream._stream.index + self.ptr.stream_index = stream._stream.index property time_base: """ @@ -135,14 +135,14 @@ cdef class Packet(Buffer): :type: int """ def __get__(self): - if self.struct.pts != lib.AV_NOPTS_VALUE: - return self.struct.pts + if self.ptr.pts != lib.AV_NOPTS_VALUE: + return self.ptr.pts def __set__(self, v): if v is None: - self.struct.pts = lib.AV_NOPTS_VALUE + self.ptr.pts = lib.AV_NOPTS_VALUE else: - self.struct.pts = v + self.ptr.pts = v property dts: """ @@ -151,14 +151,14 @@ cdef class Packet(Buffer): :type: int """ def __get__(self): - if self.struct.dts != lib.AV_NOPTS_VALUE: - return self.struct.dts + if self.ptr.dts != lib.AV_NOPTS_VALUE: + return self.ptr.dts def __set__(self, v): if v is None: - self.struct.dts = lib.AV_NOPTS_VALUE + self.ptr.dts = lib.AV_NOPTS_VALUE else: - self.struct.dts = v + self.ptr.dts = v property pos: """ @@ -169,8 +169,8 @@ cdef class Packet(Buffer): :type: int """ def __get__(self): - if self.struct.pos != -1: - return self.struct.pos + if self.ptr.pos != -1: + return self.ptr.pos property size: """ @@ -179,7 +179,7 @@ cdef class Packet(Buffer): :type: int """ def __get__(self): - return self.struct.size + return self.ptr.size property duration: """ @@ -190,11 +190,11 @@ cdef class Packet(Buffer): :type: int """ def __get__(self): - if self.struct.duration != lib.AV_NOPTS_VALUE: - return self.struct.duration + if self.ptr.duration != lib.AV_NOPTS_VALUE: + return self.ptr.duration property is_keyframe: - def __get__(self): return bool(self.struct.flags & lib.AV_PKT_FLAG_KEY) + def __get__(self): return bool(self.ptr.flags & lib.AV_PKT_FLAG_KEY) property is_corrupt: - def __get__(self): return bool(self.struct.flags & lib.AV_PKT_FLAG_CORRUPT) + def __get__(self): return bool(self.ptr.flags & lib.AV_PKT_FLAG_CORRUPT) diff --git a/av/stream.pyx b/av/stream.pyx index f24912d5f..c20c92a34 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -156,7 +156,7 @@ cdef class Stream(object): cdef Packet packet for packet in packets: packet._stream = self - packet.struct.stream_index = self._stream.index + packet.ptr.stream_index = self._stream.index return packets def decode(self, packet=None): diff --git a/av/subtitles/codeccontext.pyx b/av/subtitles/codeccontext.pyx index c4d83a256..a120fc3a5 100644 --- a/av/subtitles/codeccontext.pyx +++ b/av/subtitles/codeccontext.pyx @@ -16,7 +16,7 @@ cdef class SubtitleCodecContext(CodecContext): self.ptr, &proxy.struct, &got_frame, - &packet.struct if packet else NULL)) + packet.ptr if packet else NULL)) if got_frame: return [SubtitleSet(proxy)] else: diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 7e921e847..8e5752ae5 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -338,8 +338,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: int64_t pos - void (*destruct)(AVPacket*) - cdef int avcodec_fill_audio_frame( AVFrame *frame, @@ -352,10 +350,10 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef void avcodec_free_frame(AVFrame **frame) - cdef void av_init_packet(AVPacket*) + cdef AVPacket* av_packet_alloc() + cdef void av_packet_free(AVPacket **) cdef int av_new_packet(AVPacket*, int) cdef int av_packet_ref(AVPacket *dst, const AVPacket *src) - cdef void av_packet_unref(AVPacket *pkt) cdef void av_packet_rescale_ts(AVPacket *pkt, AVRational src_tb, AVRational dst_tb) cdef enum AVSubtitleType: From 8efcb4cd75b0b6e74b547dc64ced81c01a614511 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Tue, 25 Jan 2022 13:17:09 +0100 Subject: [PATCH 016/846] [package] build wheels for arm64 on macos --- .github/workflows/tests.yml | 20 ++++++++++++---- scripts/fetch-vendor.json | 2 +- scripts/{fetch-vendor => fetch-vendor.py} | 29 ++++++++++++++--------- 3 files changed, 34 insertions(+), 17 deletions(-) rename scripts/{fetch-vendor => fetch-vendor.py} (64%) mode change 100755 => 100644 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 58484bb77..6378f9677 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -190,7 +190,7 @@ jobs: - name: Build source package run: | pip install cython - python scripts/fetch-vendor /tmp/vendor + python scripts/fetch-vendor.py /tmp/vendor PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig make build python setup.py sdist - name: Upload source package @@ -204,7 +204,17 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + include: + - os: macos-latest + arch: arm64 + - os: macos-latest + arch: x86_64 + - os: ubuntu-latest + arch: i686 + - os: ubuntu-latest + arch: x86_64 + - os: windows-latest + arch: AMD64 steps: - uses: actions/checkout@v2 - uses: actions/setup-python@v1 @@ -217,9 +227,9 @@ jobs: brew install pkg-config - name: Build wheels env: - CIBW_ARCHS_WINDOWS: AMD64 - CIBW_BEFORE_BUILD: pip install cython && python scripts/fetch-vendor /tmp/vendor - CIBW_BEFORE_BUILD_WINDOWS: pip install cython && python scripts\fetch-vendor C:\cibw\vendor + CIBW_ARCHS: ${{ matrix.arch }} + CIBW_BEFORE_BUILD: pip install cython && python scripts/fetch-vendor.py /tmp/vendor + CIBW_BEFORE_BUILD_WINDOWS: pip install cython && python scripts\fetch-vendor.py C:\cibw\vendor CIBW_ENVIRONMENT_LINUX: LD_LIBRARY_PATH=/tmp/vendor/lib:$LD_LIBRARY_PATH PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig CIBW_ENVIRONMENT_MACOS: PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig LDFLAGS=-headerpad_max_install_names CIBW_ENVIRONMENT_WINDOWS: INCLUDE=C:\\cibw\\vendor\\include LIB=C:\\cibw\\vendor\\lib PYAV_SKIP_TESTS=unicode_filename diff --git a/scripts/fetch-vendor.json b/scripts/fetch-vendor.json index 4cddc2f3b..d87885e4b 100644 --- a/scripts/fetch-vendor.json +++ b/scripts/fetch-vendor.json @@ -1,3 +1,3 @@ { - "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/4.3.2-1/ffmpeg-{platform}.tar.gz"] + "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/4.3.2-2/ffmpeg-{platform}.tar.gz"] } diff --git a/scripts/fetch-vendor b/scripts/fetch-vendor.py old mode 100755 new mode 100644 similarity index 64% rename from scripts/fetch-vendor rename to scripts/fetch-vendor.py index aa354aaba..fcfa8d3c8 --- a/scripts/fetch-vendor +++ b/scripts/fetch-vendor.py @@ -1,30 +1,37 @@ -#!/usr/bin/env python - import argparse import logging import json import os +import platform import shutil import struct import subprocess -import sys def get_platform(): - if sys.platform == "linux": - return "manylinux_%s" % os.uname().machine - elif sys.platform == "darwin": - return "macosx_%s" % os.uname().machine - elif sys.platform == "win32": - return "win%s" % (struct.calcsize("P") * 8) + system = platform.system() + machine = platform.machine() + if system == "Linux": + return f"manylinux_{machine}" + elif system == "Darwin": + # cibuildwheel sets ARCHFLAGS: + # https://github.com/pypa/cibuildwheel/blob/5255155bc57eb6224354356df648dc42e31a0028/cibuildwheel/macos.py#L207-L220 + if "ARCHFLAGS" in os.environ: + machine = os.environ["ARCHFLAGS"].split()[1] + return f"macosx_{machine}" + elif system == "Windows": + if struct.calcsize("P") * 8 == 64: + return "win_amd64" + else: + return "win32" else: - raise Exception("Unsupported platfom %s" % sys.platform) + raise Exception(f"Unsupported system {system}") parser = argparse.ArgumentParser(description="Fetch and extract tarballs") parser.add_argument("destination_dir") parser.add_argument("--cache-dir", default="tarballs") -parser.add_argument("--config-file", default=__file__ + ".json") +parser.add_argument("--config-file", default=os.path.splitext(__file__)[0] + ".json") args = parser.parse_args() logging.basicConfig(level=logging.INFO) From f941e17b1c07295f6a00226f76c0561acc82b4c2 Mon Sep 17 00:00:00 2001 From: Justin Wong <46082645+uvjustin@users.noreply.github.com> Date: Tue, 4 Jan 2022 13:07:26 +0800 Subject: [PATCH 017/846] Fix setting Stream time_base (fixes #784) --- av/stream.pxd | 2 ++ av/stream.pyx | 25 ++++++++++++++++++------- tests/test_encode.py | 12 ++++++++++++ 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/av/stream.pxd b/av/stream.pxd index fa0ffb2d3..4a3cab488 100644 --- a/av/stream.pxd +++ b/av/stream.pxd @@ -24,6 +24,8 @@ cdef class Stream(object): # Private API. cdef _init(self, Container, lib.AVStream*) cdef _finalize_for_output(self) + cdef _set_time_base(self, value) + cdef _set_id(self, value) cdef Stream wrap_stream(Container, lib.AVStream*) diff --git a/av/stream.pyx b/av/stream.pyx index c20c92a34..c2a108e2a 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -126,7 +126,12 @@ cdef class Stream(object): raise AttributeError(name) def __setattr__(self, name, value): + if name == "id": + self._set_id(value) + return setattr(self.codec_context, name, value) + if name == "time_base": + self._set_time_base(value) cdef _finalize_for_output(self): @@ -193,11 +198,14 @@ cdef class Stream(object): def __get__(self): return self._stream.id - def __set__(self, v): - if v is None: - self._stream.id = 0 - else: - self._stream.id = v + cdef _set_id(self, value): + """ + Setter used by __setattr__ for the id property. + """ + if value is None: + self._stream.id = 0 + else: + self._stream.id = value property profile: """ @@ -229,8 +237,11 @@ cdef class Stream(object): def __get__(self): return avrational_to_fraction(&self._stream.time_base) - def __set__(self, value): - to_avrational(value, &self._stream.time_base) + cdef _set_time_base(self, value): + """ + Setter used by __setattr__ for the time_base property. + """ + to_avrational(value, &self._stream.time_base) property average_rate: """ diff --git a/tests/test_encode.py b/tests/test_encode.py index 2a58044d6..3e7ec2c0e 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -208,3 +208,15 @@ def test_stream_index(self): self.assertIs(apacket.stream, astream) self.assertEqual(apacket.stream_index, 1) + + def test_audio_set_time_base_and_id(self): + output = av.open(self.sandboxed('output.mov'), 'w') + + stream = output.add_stream('mp2') + self.assertEqual(stream.rate, 48000) + self.assertEqual(stream.time_base, None) + stream.time_base = Fraction(1, 48000) + self.assertEqual(stream.time_base, Fraction(1, 48000)) + self.assertEqual(stream.id, 0) + stream.id = 1 + self.assertEqual(stream.id, 1) From cd6595aa75159da14adc966e6d781d073481f803 Mon Sep 17 00:00:00 2001 From: Felix Vollmer Date: Sat, 27 Mar 2021 15:37:48 +0100 Subject: [PATCH 018/846] Better time_base support with filters --- av/filter/context.pyx | 2 ++ av/filter/graph.pyx | 12 ++++++++++-- tests/test_filters.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/av/filter/context.pyx b/av/filter/context.pyx index bd027aa34..30481eb21 100644 --- a/av/filter/context.pyx +++ b/av/filter/context.pyx @@ -6,6 +6,7 @@ from av.dictionary import Dictionary from av.error cimport err_check from av.filter.pad cimport alloc_filter_pads from av.frame cimport Frame +from av.utils cimport avrational_to_fraction from av.video.frame cimport VideoFrame, alloc_video_frame @@ -106,4 +107,5 @@ cdef class FilterContext(object): err_check(lib.av_buffersink_get_frame(self.ptr, frame.ptr)) frame._init_user_attributes() + frame.time_base = avrational_to_fraction(&self.ptr.inputs[0].time_base) return frame diff --git a/av/filter/graph.pyx b/av/filter/graph.pyx index 110da0e52..20c76c7de 100644 --- a/av/filter/graph.pyx +++ b/av/filter/graph.pyx @@ -1,4 +1,5 @@ from fractions import Fraction +import warnings from av.audio.format cimport AudioFormat from av.audio.frame cimport AudioFrame @@ -122,7 +123,7 @@ cdef class Graph(object): self._register_context(py_ctx) self._nb_filters_seen = self.ptr.nb_filters - def add_buffer(self, template=None, width=None, height=None, format=None, name=None): + def add_buffer(self, template=None, width=None, height=None, format=None, name=None, time_base=None): if template is not None: if width is None: @@ -131,6 +132,8 @@ cdef class Graph(object): height = template.height if format is None: format = template.format + if time_base is None: + time_base = template.time_base if width is None: raise ValueError('missing width') @@ -138,13 +141,18 @@ cdef class Graph(object): raise ValueError('missing height') if format is None: raise ValueError('missing format') + if time_base is None: + warnings.warn('missing time_base. Guessing 1/1000 time base. ' + 'This is deprecated and may be removed in future releases.', + DeprecationWarning) + time_base = Fraction(1, 1000) return self.add( 'buffer', name=name, video_size=f'{width}x{height}', pix_fmt=str(int(VideoFormat(format))), - time_base='1/1000', + time_base=str(time_base), pixel_aspect='1/1', ) diff --git a/tests/test_filters.py b/tests/test_filters.py index df4febb2f..659ffc708 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -7,6 +7,7 @@ from av import AudioFrame, VideoFrame from av.audio.frame import format_dtypes from av.filter import Filter, Graph +import av from .common import Image, TestCase, fate_suite @@ -34,6 +35,17 @@ def generate_audio_frame(frame_num, input_format='s16', layout='stereo', sample_ return frame +def pull_until_blocked(graph): + frames = [] + while True: + try: + frames.append(graph.pull()) + except av.utils.AVError as e: + if e.errno != errno.EAGAIN: + raise + return frames + + class TestFilters(TestCase): def test_filter_descriptor(self): @@ -213,3 +225,33 @@ def test_audio_buffer_volume_filter(self): output_data = out_frame.to_ndarray() self.assertTrue(np.allclose(input_data * 0.5, output_data), "Check that volume is reduced") + + def test_video_buffer(self): + input_container = av.open(format="lavfi", file="color=c=pink:duration=1:r=30") + input_video_stream = input_container.streams.video[0] + + graph = av.filter.Graph() + buffer = graph.add_buffer(template=input_video_stream) + bwdif = graph.add("bwdif", "send_field:tff:all") + buffersink = graph.add("buffersink") + buffer.link_to(bwdif) + bwdif.link_to(buffersink) + graph.configure() + + for frame in input_container.decode(): + self.assertEqual(frame.time_base, Fraction(1, 30)) + graph.push(frame) + filtered_frames = pull_until_blocked(graph) + + if frame.pts == 0: + # no output for the first input frame + self.assertEqual(len(filtered_frames), 0) + else: + # we expect two filtered frames per input frame + self.assertEqual(len(filtered_frames), 2) + + self.assertEqual(filtered_frames[0].pts, (frame.pts - 1) * 2) + self.assertEqual(filtered_frames[0].time_base, Fraction(1, 60)) + + self.assertEqual(filtered_frames[1].pts, (frame.pts - 1) * 2 + 1) + self.assertEqual(filtered_frames[1].time_base, Fraction(1, 60)) From 95b4f818c0b53c18af8a1ae0d5d9044df1586545 Mon Sep 17 00:00:00 2001 From: Felix Vollmer Date: Sun, 28 Mar 2021 14:22:21 +0200 Subject: [PATCH 019/846] Implement resampler and fifo with aformat and buffersink This simplifies the code and matches the behaviour to the ffmpeg tool. --- av/audio/codeccontext.pyx | 30 ++--- av/audio/resampler.pxd | 11 +- av/audio/resampler.pyx | 193 ++++++++++--------------------- include/libavfilter/avfilter.pxd | 4 + tests/test_audioresampler.py | 43 ++----- tests/test_codec_context.py | 21 ++-- tests/test_encode.py | 7 +- 7 files changed, 102 insertions(+), 207 deletions(-) diff --git a/av/audio/codeccontext.pyx b/av/audio/codeccontext.pyx index 0109f95ff..c81a49dd8 100644 --- a/av/audio/codeccontext.pyx +++ b/av/audio/codeccontext.pyx @@ -28,30 +28,22 @@ cdef class AudioCodecContext(CodecContext): cdef AudioFrame frame = input_frame - # Resample. A None frame will flush the resampler, and then the fifo (if used). + cdef bint allow_var_frame_size = self.ptr.codec.capabilities & lib.AV_CODEC_CAP_VARIABLE_FRAME_SIZE + # Note that the resampler will simply return an input frame if there is # no resampling to be done. The control flow was just a little easier this way. if not self.resampler: self.resampler = AudioResampler( - self.format, - self.layout, - self.ptr.sample_rate + format=self.format, + layout=self.layout, + rate=self.ptr.sample_rate, + frame_size=None if allow_var_frame_size else self.ptr.frame_size ) - frame = self.resampler.resample(frame) - - cdef bint is_flushing = input_frame is None - cdef bint use_fifo = not (self.ptr.codec.capabilities & lib.AV_CODEC_CAP_VARIABLE_FRAME_SIZE) - - if use_fifo: - if not self.fifo: - self.fifo = AudioFifo() - if frame is not None: - self.fifo.write(frame) - frames = self.fifo.read_many(self.ptr.frame_size, partial=is_flushing) - if is_flushing: - frames.append(None) - else: - frames = [frame] + frames = self.resampler.resample(frame) + + # flush if input frame is None + if input_frame is None: + frames.append(None) return frames diff --git a/av/audio/resampler.pxd b/av/audio/resampler.pxd index 4a2d9ceaf..b1c72e2a5 100644 --- a/av/audio/resampler.pxd +++ b/av/audio/resampler.pxd @@ -4,6 +4,7 @@ cimport libav as lib from av.audio.format cimport AudioFormat from av.audio.frame cimport AudioFrame from av.audio.layout cimport AudioLayout +from av.filter.graph cimport Graph cdef class AudioResampler(object): @@ -14,18 +15,12 @@ cdef class AudioResampler(object): cdef AudioFrame template - # Source descriptors; not for public consumption. - cdef unsigned int template_rate - # Destination descriptors cdef readonly AudioFormat format cdef readonly AudioLayout layout cdef readonly int rate + cdef readonly unsigned int frame_size - # Retiming. - cdef readonly uint64_t samples_in - cdef readonly double pts_per_sample_in - cdef readonly uint64_t samples_out - cdef readonly bint simple_pts_out + cdef Graph graph cpdef resample(self, AudioFrame) diff --git a/av/audio/resampler.pyx b/av/audio/resampler.pyx index bc24f4703..416b1183c 100644 --- a/av/audio/resampler.pyx +++ b/av/audio/resampler.pyx @@ -1,13 +1,12 @@ from libc.stdint cimport int64_t, uint8_t cimport libav as lib -from av.audio.fifo cimport AudioFifo -from av.audio.format cimport get_audio_format -from av.audio.frame cimport alloc_audio_frame -from av.audio.layout cimport get_audio_layout -from av.error cimport err_check +from av.filter.context cimport FilterContext + +import errno from av.error import FFmpegError +import av.filter cdef class AudioResampler(object): @@ -23,17 +22,16 @@ cdef class AudioResampler(object): """ - def __cinit__(self, format=None, layout=None, rate=None): + def __cinit__(self, format=None, layout=None, rate=None, frame_size=None): if format is not None: self.format = format if isinstance(format, AudioFormat) else AudioFormat(format) if layout is not None: self.layout = layout if isinstance(layout, AudioLayout) else AudioLayout(layout) self.rate = int(rate) if rate else 0 - def __dealloc__(self): - if self.ptr: - lib.swr_close(self.ptr) - lib.swr_free(&self.ptr) + self.frame_size = int(frame_size) if frame_size else 0 + + self.graph = None cpdef resample(self, AudioFrame frame): """resample(frame) @@ -42,149 +40,74 @@ cdef class AudioResampler(object): a :class:`~.AudioFrame`. :param AudioFrame frame: The frame to convert. - :returns: A new :class:`AudioFrame` in new parameters, or the same frame - if there is nothing to be done. - :raises: ``ValueError`` if ``Frame.pts`` is set and non-simple. + :returns: A list of :class:`AudioFrame` in new parameters. If the nothing is to be done return the same frame + as a single element list. """ - if self.is_passthrough: - return frame + return [frame] - # Take source settings from the first frame. - if not self.ptr: + # We don't have any input, so don't bother even setting up. + if not frame: + return [] - # We don't have any input, so don't bother even setting up. - if not frame: - return - - # Hold onto a copy of the attributes of the first frame to populate - # output frames with. - self.template = alloc_audio_frame() - self.template._copy_internal_attributes(frame) - self.template._init_user_attributes() + # Take source settings from the first frame. + if not self.graph: + self.template = frame # Set some default descriptors. - self.format = self.format or self.template.format - self.layout = self.layout or self.template.layout - self.rate = self.rate or self.template.ptr.sample_rate + self.format = self.format or frame.format + self.layout = self.layout or frame.layout + self.rate = self.rate or frame.sample_rate - # Check if there is actually work to do. + # Check if we can passthrough or if there is actually work to do. if ( - self.template.format.sample_fmt == self.format.sample_fmt and - self.template.layout.layout == self.layout.layout and - self.template.ptr.sample_rate == self.rate + frame.format.sample_fmt == self.format.sample_fmt and + frame.layout.layout == self.layout.layout and + frame.sample_rate == self.rate and + self.frame_size == 0 ): self.is_passthrough = True - return frame - - # Figure out our time bases. - if frame._time_base.num and frame.ptr.sample_rate: - self.pts_per_sample_in = frame._time_base.den / float(frame._time_base.num) - self.pts_per_sample_in /= self.template.ptr.sample_rate - - # We will only provide outgoing PTS if the time_base is trivial. - if frame._time_base.num == 1 and frame._time_base.den == frame.ptr.sample_rate: - self.simple_pts_out = True - - self.ptr = lib.swr_alloc() - if not self.ptr: - raise RuntimeError('Could not allocate SwrContext.') - - # Configure it! - try: - err_check(lib.av_opt_set_int(self.ptr, 'in_sample_fmt', self.template.format.sample_fmt, 0)) - err_check(lib.av_opt_set_int(self.ptr, 'out_sample_fmt', self.format.sample_fmt, 0)) - err_check(lib.av_opt_set_int(self.ptr, 'in_channel_layout', self.template.layout.layout, 0)) - err_check(lib.av_opt_set_int(self.ptr, 'out_channel_layout', self.layout.layout, 0)) - err_check(lib.av_opt_set_int(self.ptr, 'in_sample_rate', self.template.ptr.sample_rate, 0)) - err_check(lib.av_opt_set_int(self.ptr, 'out_sample_rate', self.rate, 0)) - err_check(lib.swr_init(self.ptr)) - except FFmpegError: - self.ptr = NULL - raise + return [frame] + + # handle resampling with aformat filter + # (similar to configure_output_audio_filter from ffmpeg) + self.graph = av.filter.Graph() + abuffer = self.graph.add("abuffer", + sample_rate=str(frame.sample_rate), + sample_fmt=AudioFormat(frame.format).name, + channel_layout=frame.layout.name) + aformat = self.graph.add("aformat", + sample_rates=str(self.rate), + sample_fmts=self.format.name, + channel_layouts=str(self.layout.layout)) + abuffersink = self.graph.add("abuffersink") + abuffer.link_to(aformat) + aformat.link_to(abuffersink) + self.graph.configure() + + if self.frame_size > 0: + lib.av_buffersink_set_frame_size((abuffersink).ptr, self.frame_size) elif frame: # Assert the settings are the same on consecutive frames. if ( - frame.ptr.format != self.template.format.sample_fmt or - frame.ptr.channel_layout != self.template.layout.layout or - frame.ptr.sample_rate != self.template.ptr.sample_rate + frame.format.sample_fmt != self.template.format.sample_fmt or + frame.layout.layout != self.template.layout.layout or + frame.sample_rate != self.template.rate ): raise ValueError('Frame does not match AudioResampler setup.') - # Assert that the PTS are what we expect. - cdef int64_t expected_pts - if frame is not None and frame.ptr.pts != lib.AV_NOPTS_VALUE: - expected_pts = (self.pts_per_sample_in * self.samples_in) - if frame.ptr.pts != expected_pts: - raise ValueError('Input frame pts %d != expected %d; fix or set to None.' % (frame.ptr.pts, expected_pts)) - self.samples_in += frame.ptr.nb_samples - - # The example "loop" as given in the FFmpeg documentation looks like: - # uint8_t **input; - # int in_samples; - # while (get_input(&input, &in_samples)) { - # uint8_t *output; - # int out_samples = av_rescale_rnd(swr_get_delay(swr, 48000) + - # in_samples, 44100, 48000, AV_ROUND_UP); - # av_samples_alloc(&output, NULL, 2, out_samples, - # AV_SAMPLE_FMT_S16, 0); - # out_samples = swr_convert(swr, &output, out_samples, - # input, in_samples); - # handle_output(output, out_samples); - # av_freep(&output); - # } - - # Estimate out how many samples this will create; it will be high. - # My investigations say that this swr_get_delay is not required, but - # it is in the example loop, and avresample (as opposed to swresample) - # may require it. - cdef int output_nb_samples = lib.av_rescale_rnd( - lib.swr_get_delay(self.ptr, self.rate) + frame.ptr.nb_samples, - self.rate, - self.template.ptr.sample_rate, - lib.AV_ROUND_UP, - ) if frame else lib.swr_get_delay(self.ptr, self.rate) - - # There aren't any frames coming, so no new frame pops out. - if not output_nb_samples: - return - - cdef AudioFrame output = alloc_audio_frame() - output._copy_internal_attributes(self.template) - output.ptr.sample_rate = self.rate - output._init( - self.format.sample_fmt, - self.layout.layout, - output_nb_samples, - 1, # Align? - ) - - output.ptr.nb_samples = err_check(lib.swr_convert( - self.ptr, - output.ptr.extended_data, - output_nb_samples, - # Cast for const-ness, because Cython isn't expressive enough. - (frame.ptr.extended_data if frame else NULL), - frame.ptr.nb_samples if frame else 0 - )) - - # Empty frame. - if output.ptr.nb_samples <= 0: - return - - # Create new PTSes in simple cases. - if self.simple_pts_out: - output._time_base.num = 1 - output._time_base.den = self.rate - output.ptr.pts = self.samples_out - else: - output._time_base.num = 0 - output._time_base.den = 1 - output.ptr.pts = lib.AV_NOPTS_VALUE - - self.samples_out += output.ptr.nb_samples + self.graph.push(frame) + + output = [] + while True: + try: + output.append(self.graph.pull()) + except av.utils.AVError as e: + if e.errno != errno.EAGAIN: + raise + break return output diff --git a/include/libavfilter/avfilter.pxd b/include/libavfilter/avfilter.pxd index d6832e364..41e0e1d1f 100644 --- a/include/libavfilter/avfilter.pxd +++ b/include/libavfilter/avfilter.pxd @@ -75,3 +75,7 @@ cdef extern from "libavfilter/avfilter.h" nogil: # custom cdef set pyav_get_available_filters() + + +cdef extern from "libavfilter/buffersink.h" nogil: + cdef void av_buffersink_set_frame_size(AVFilterContext *ctx, unsigned frame_size) diff --git a/tests/test_audioresampler.py b/tests/test_audioresampler.py index 683d70376..42236a4c3 100644 --- a/tests/test_audioresampler.py +++ b/tests/test_audioresampler.py @@ -1,3 +1,5 @@ +from fractions import Fraction + from av import AudioFrame, AudioResampler from .common import TestCase @@ -12,7 +14,7 @@ def test_identity_passthrough(self): resampler = AudioResampler() iframe = AudioFrame('s16', 'stereo', 1024) - oframe = resampler.resample(iframe) + oframe = resampler.resample(iframe)[0] self.assertIs(iframe, oframe) @@ -23,7 +25,7 @@ def test_matching_passthrough(self): resampler = AudioResampler('s16', 'stereo') iframe = AudioFrame('s16', 'stereo', 1024) - oframe = resampler.resample(iframe) + oframe = resampler.resample(iframe)[0] self.assertIs(iframe, oframe) @@ -36,21 +38,21 @@ def test_pts_assertion_same_rate(self): iframe.time_base = '1/48000' iframe.pts = 0 - oframe = resampler.resample(iframe) + oframe = resampler.resample(iframe)[0] self.assertEqual(oframe.pts, 0) self.assertEqual(oframe.time_base, iframe.time_base) self.assertEqual(oframe.sample_rate, iframe.sample_rate) iframe.pts = 1024 - oframe = resampler.resample(iframe) + oframe = resampler.resample(iframe)[0] self.assertEqual(oframe.pts, 1024) self.assertEqual(oframe.time_base, iframe.time_base) self.assertEqual(oframe.sample_rate, iframe.sample_rate) iframe.pts = 9999 - self.assertRaises(ValueError, resampler.resample, iframe) + resampler.resample(iframe) # resampler should handle this without an exception def test_pts_assertion_new_rate(self): @@ -61,20 +63,11 @@ def test_pts_assertion_new_rate(self): iframe.time_base = '1/48000' iframe.pts = 0 - oframe = resampler.resample(iframe) + oframe = resampler.resample(iframe)[0] self.assertEqual(oframe.pts, 0) self.assertEqual(str(oframe.time_base), '1/44100') self.assertEqual(oframe.sample_rate, 44100) - samples_out = resampler.samples_out - self.assertTrue(samples_out > 0) - - iframe.pts = 1024 - oframe = resampler.resample(iframe) - self.assertEqual(oframe.pts, samples_out) - self.assertEqual(str(oframe.time_base), '1/44100') - self.assertEqual(oframe.sample_rate, 44100) - def test_pts_missing_time_base(self): resampler = AudioResampler('s16', 'mono', 44100) @@ -83,21 +76,7 @@ def test_pts_missing_time_base(self): iframe.sample_rate = 48000 iframe.pts = 0 - oframe = resampler.resample(iframe) - self.assertIs(oframe.pts, None) - self.assertIs(oframe.time_base, None) - self.assertEqual(oframe.sample_rate, 44100) - - def test_pts_complex_time_base(self): - - resampler = AudioResampler('s16', 'mono', 44100) - - iframe = AudioFrame('s16', 'stereo', 1024) - iframe.sample_rate = 48000 - iframe.time_base = '1/96000' - iframe.pts = 0 - - oframe = resampler.resample(iframe) - self.assertIs(oframe.pts, None) - self.assertIs(oframe.time_base, None) + oframe = resampler.resample(iframe)[0] + self.assertEqual(oframe.pts, 0) + self.assertEqual(oframe.time_base, Fraction(1, 44100)) self.assertEqual(oframe.sample_rate, 44100) diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 77049b1c5..91449b8f0 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -376,18 +376,15 @@ def audio_encoding(self, codec_name): next(encoder.encode(bad_frame)) """ - resampled_frame = resampler.resample(frame) - samples += resampled_frame.samples - - for packet in ctx.encode(resampled_frame): - # bytearray because python can - # freaks out if the first byte is NULL - f.write(bytearray(packet)) - packet_sizes.append(packet.size) - - for packet in ctx.encode(None): - packet_sizes.append(packet.size) - f.write(bytearray(packet)) + resampled_frames = resampler.resample(frame) + for resampled_frame in resampled_frames: + samples += resampled_frame.samples + + for packet in ctx.encode(resampled_frame): + # bytearray because python can + # freaks out if the first byte is NULL + f.write(bytearray(packet)) + packet_sizes.append(packet.size) ctx = Codec(codec_name, 'r').create() ctx.time_base = Fraction(1) / sample_rate diff --git a/tests/test_encode.py b/tests/test_encode.py index 3e7ec2c0e..f4664e5b2 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -199,7 +199,12 @@ def test_stream_index(self): self.assertEqual(vpacket.stream_index, 0) for i in range(10): - aframe = AudioFrame('s16', 'stereo', samples=astream.frame_size) + if astream.frame_size != 0: + frame_size = astream.frame_size + else: + # decoder didn't indicate constant frame size + frame_size = 1000 + aframe = AudioFrame('s16', 'stereo', samples=frame_size) aframe.rate = 48000 apackets = astream.encode(aframe) if apackets: From 3ae7fe4c2120f235abcf415a27e6ec86302641b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Fri, 25 Feb 2022 11:40:00 +0100 Subject: [PATCH 020/846] [audio frame] fix ndarray conversion endianness (fixes: #833) We assumed that sample data is always stored in little-endian order, but that is not the case according to the FFmpeg documentation: The data described by the sample format is always in native-endian order. --- av/audio/frame.pyx | 16 ++++++++-------- tests/test_audioframe.py | 36 ++++++++++++++++++------------------ 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/av/audio/frame.pyx b/av/audio/frame.pyx index c9a7e6fef..7a71c749e 100644 --- a/av/audio/frame.pyx +++ b/av/audio/frame.pyx @@ -9,14 +9,14 @@ cdef object _cinit_bypass_sentinel format_dtypes = { - 'dbl': ' Date: Fri, 25 Feb 2022 14:19:04 +0100 Subject: [PATCH 021/846] [frames] make from_ndarray ValueError instead of AssertionError When checking an input array's dtype and ndim, raise a ValueError with a helpful message instead of an AssertionError. --- av/audio/frame.pyx | 9 +++++---- av/utils.pxd | 2 ++ av/utils.pyx | 19 +++++++++++++++++++ av/video/frame.pyx | 40 ++++++++++++++++++---------------------- tests/test_audioframe.py | 19 +++++++++++++++++++ 5 files changed, 63 insertions(+), 26 deletions(-) diff --git a/av/audio/frame.pyx b/av/audio/frame.pyx index 7a71c749e..549b91d86 100644 --- a/av/audio/frame.pyx +++ b/av/audio/frame.pyx @@ -3,6 +3,7 @@ from av.audio.layout cimport get_audio_layout from av.audio.plane cimport AudioPlane from av.deprecation import renamed_attr from av.error cimport err_check +from av.utils cimport check_ndarray, check_ndarray_shape cdef object _cinit_bypass_sentinel @@ -116,14 +117,14 @@ cdef class AudioFrame(Frame): except KeyError: raise ValueError('Conversion from numpy array with format `%s` is not yet supported' % format) + # check input format nb_channels = len(AudioLayout(layout).channels) - assert array.dtype == dtype - assert array.ndim == 2 + check_ndarray(array, dtype, 2) if AudioFormat(format).is_planar: - assert array.shape[0] == nb_channels + check_ndarray_shape(array, array.shape[0] == nb_channels) samples = array.shape[1] else: - assert array.shape[0] == 1 + check_ndarray_shape(array, array.shape[0] == 1) samples = array.shape[1] // nb_channels frame = AudioFrame(format=format, layout=layout, samples=samples) diff --git a/av/utils.pxd b/av/utils.pxd index 1f4a254f6..0c943de81 100644 --- a/av/utils.pxd +++ b/av/utils.pxd @@ -10,4 +10,6 @@ cdef object avrational_to_fraction(const lib.AVRational *input) cdef object to_avrational(object value, lib.AVRational *input) +cdef check_ndarray(object array, object dtype, int ndim) +cdef check_ndarray_shape(object array, bint ok) cdef flag_in_bitfield(uint64_t bitfield, uint64_t flag) diff --git a/av/utils.pyx b/av/utils.pyx index 85b71a281..1894ce7ab 100644 --- a/av/utils.pyx +++ b/av/utils.pyx @@ -61,6 +61,25 @@ cdef object to_avrational(object value, lib.AVRational *input): # === OTHER === # ============= + +cdef check_ndarray(object array, object dtype, int ndim): + """ + Check a numpy array has the expected data type and number of dimensions. + """ + if array.dtype != dtype: + raise ValueError(f"Expected numpy array with dtype `{dtype}` but got `{array.dtype}`") + if array.ndim != ndim: + raise ValueError(f"Expected numpy array with ndim `{ndim}` but got `{array.ndim}`") + + +cdef check_ndarray_shape(object array, bint ok): + """ + Check a numpy array has the expected shape. + """ + if not ok: + raise ValueError(f"Unexpected numpy array shape `{array.shape}`") + + cdef flag_in_bitfield(uint64_t bitfield, uint64_t flag): # Not every flag exists in every version of FFMpeg, so we define them to 0. if not flag: diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 5b0253a89..311ab9c9f 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -2,6 +2,7 @@ from libc.stdint cimport uint8_t from av.enum cimport define_enum from av.error cimport err_check +from av.utils cimport check_ndarray, check_ndarray_shape from av.video.format cimport VideoFormat, get_pix_fmt, get_video_format from av.video.plane cimport VideoPlane @@ -301,20 +302,19 @@ cdef class VideoFrame(Frame): """ if format == 'pal8': array, palette = array - assert array.dtype == 'uint8' - assert array.ndim == 2 - assert palette.dtype == 'uint8' - assert palette.shape == (256, 4) + check_ndarray(array, 'uint8', 2) + check_ndarray(palette, 'uint8', 2) + check_ndarray_shape(palette, palette.shape == (256, 4)) + frame = VideoFrame(array.shape[1], array.shape[0], format) copy_array_to_plane(array, frame.planes[0], 1) frame.planes[1].update(palette.view('>i4').astype('i4').tobytes()) return frame + elif format in ('yuv420p', 'yuvj420p'): + check_ndarray(array, 'uint8', 2) + check_ndarray_shape(array, array.shape[0] % 3 == 0) + check_ndarray_shape(array, array.shape[1] % 2 == 0) - if format in ('yuv420p', 'yuvj420p'): - assert array.dtype == 'uint8' - assert array.ndim == 2 - assert array.shape[0] % 3 == 0 - assert array.shape[1] % 2 == 0 frame = VideoFrame(array.shape[1], (array.shape[0] * 2) // 3, format) u_start = frame.width * frame.height v_start = 5 * u_start // 4 @@ -324,22 +324,18 @@ cdef class VideoFrame(Frame): copy_array_to_plane(flat[v_start:], frame.planes[2], 1) return frame elif format == 'yuyv422': - assert array.dtype == 'uint8' - assert array.ndim == 3 - assert array.shape[0] % 2 == 0 - assert array.shape[1] % 2 == 0 - assert array.shape[2] == 2 + check_ndarray(array, 'uint8', 3) + check_ndarray_shape(array, array.shape[0] % 2 == 0) + check_ndarray_shape(array, array.shape[1] % 2 == 0) + check_ndarray_shape(array, array.shape[2] == 2) elif format in ('rgb24', 'bgr24'): - assert array.dtype == 'uint8' - assert array.ndim == 3 - assert array.shape[2] == 3 + check_ndarray(array, 'uint8', 3) + check_ndarray_shape(array, array.shape[2] == 3) elif format in ('argb', 'rgba', 'abgr', 'bgra'): - assert array.dtype == 'uint8' - assert array.ndim == 3 - assert array.shape[2] == 4 + check_ndarray(array, 'uint8', 3) + check_ndarray_shape(array, array.shape[2] == 4) elif format in ('gray', 'gray8', 'rgb8', 'bgr8'): - assert array.dtype == 'uint8' - assert array.ndim == 2 + check_ndarray(array, 'uint8', 2) else: raise ValueError('Conversion from numpy array with format `%s` is not yet supported' % format) diff --git a/tests/test_audioframe.py b/tests/test_audioframe.py index 7d3d7f72a..9468641c1 100644 --- a/tests/test_audioframe.py +++ b/tests/test_audioframe.py @@ -114,6 +114,25 @@ def test_ndarray_dbl(self): self.assertEqual(frame.samples, 160) self.assertTrue((frame.to_ndarray() == array).all()) + def test_from_ndarray_value_error(self): + # incorrect dtype + array = numpy.ndarray(shape=(1, 160), dtype="f2") + with self.assertRaises(ValueError) as cm: + AudioFrame.from_ndarray(array, format="flt", layout="mono") + self.assertEqual(str(cm.exception), "Expected numpy array with dtype `float32` but got `float16`") + + # incorrect number of dimensions + array = numpy.ndarray(shape=(1, 160, 2), dtype="f4") + with self.assertRaises(ValueError) as cm: + AudioFrame.from_ndarray(array, format="flt", layout="mono") + self.assertEqual(str(cm.exception), "Expected numpy array with ndim `2` but got `3`") + + # incorrect shape + array = numpy.ndarray(shape=(2, 160), dtype="f4") + with self.assertRaises(ValueError) as cm: + AudioFrame.from_ndarray(array, format="flt", layout="mono") + self.assertEqual(str(cm.exception), "Unexpected numpy array shape `(2, 160)`") + def test_ndarray_flt(self): layouts = [ ('flt', 'mono', 'f4', (1, 160)), From 04d5cdd78b5a7dd6711636660ad7daea243d2713 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Fri, 25 Feb 2022 14:47:14 +0100 Subject: [PATCH 022/846] [qa] reformat code using `black` --- .github/workflows/tests.yml | 3 +- av/__init__.py | 7 +- av/__main__.py | 28 ++-- av/codec/__init__.py | 8 +- av/datasets.py | 36 +++-- av/deprecation.py | 34 +++-- examples/basics/parse.py | 31 ++-- examples/basics/remux.py | 4 +- examples/basics/save_keyframes.py | 6 +- examples/basics/thread_type.py | 10 +- examples/numpy/barcode.py | 14 +- examples/numpy/generate_video.py | 9 +- examples/numpy/generate_video_with_pts.py | 14 +- scripts/test | 4 + setup.cfg | 10 +- tests/common.py | 79 +++++----- tests/test_audiofifo.py | 13 +- tests/test_audioformat.py | 15 +- tests/test_audioframe.py | 110 +++++++------- tests/test_audiolayout.py | 19 ++- tests/test_audioresampler.py | 25 ++-- tests/test_codec.py | 46 +++--- tests/test_codec_context.py | 141 +++++++++--------- tests/test_container.py | 12 +- tests/test_containerformat.py | 29 ++-- tests/test_decode.py | 23 +-- tests/test_deprecation.py | 26 ++-- tests/test_dictionary.py | 13 +- tests/test_doctests.py | 28 ++-- tests/test_encode.py | 119 ++++++++------- tests/test_enums.py | 94 ++++++------ tests/test_errors.py | 29 ++-- tests/test_file_probing.py | 171 +++++++++++++--------- tests/test_filters.py | 101 +++++++------ tests/test_logging.py | 63 ++++---- tests/test_options.py | 7 +- tests/test_python_io.py | 35 +++-- tests/test_seek.py | 21 ++- tests/test_streams.py | 11 +- tests/test_subtitles.py | 18 ++- tests/test_timeout.py | 18 ++- tests/test_videoformat.py | 16 +- tests/test_videoframe.py | 127 ++++++++-------- 43 files changed, 878 insertions(+), 749 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6378f9677..994b158ab 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,8 +13,9 @@ jobs: strategy: matrix: config: - - {suite: isort} + - {suite: black} - {suite: flake8} + - {suite: isort} env: PYAV_PYTHON: python3 diff --git a/av/__init__.py b/av/__init__.py index 7713215b7..1eed5f27b 100644 --- a/av/__init__.py +++ b/av/__init__.py @@ -1,8 +1,11 @@ # Add the native FFMPEG and MinGW libraries to executable path, so that the # AV pyd files can find them. import os -if os.name == 'nt': - os.environ['PATH'] = os.path.abspath(os.path.dirname(__file__)) + os.pathsep + os.environ['PATH'] + +if os.name == "nt": + os.environ["PATH"] = ( + os.path.abspath(os.path.dirname(__file__)) + os.pathsep + os.environ["PATH"] + ) # MUST import the core before anything else in order to initalize the underlying # library that is being wrapped. diff --git a/av/__main__.py b/av/__main__.py index a5873a5f9..32a1db565 100644 --- a/av/__main__.py +++ b/av/__main__.py @@ -4,8 +4,8 @@ def main(): parser = argparse.ArgumentParser() - parser.add_argument('--codecs', action='store_true') - parser.add_argument('--version', action='store_true') + parser.add_argument("--codecs", action="store_true") + parser.add_argument("--version", action="store_true") args = parser.parse_args() # --- @@ -14,29 +14,31 @@ def main(): import av._core - print('PyAV v' + av._core.pyav_version) - print('git origin: git@github.com:PyAV-Org/PyAV') - print('git commit:', av._core.pyav_commit) + print("PyAV v" + av._core.pyav_version) + print("git origin: git@github.com:PyAV-Org/PyAV") + print("git commit:", av._core.pyav_commit) by_config = {} for libname, config in sorted(av._core.library_meta.items()): - version = config['version'] + version = config["version"] if version[0] >= 0: by_config.setdefault( - (config['configuration'], config['license']), - [] + (config["configuration"], config["license"]), [] ).append((libname, config)) for (config, license), libs in sorted(by_config.items()): - print('library configuration:', config) - print('library license:', license) + print("library configuration:", config) + print("library license:", license) for libname, config in libs: - version = config['version'] - print('%-13s %3d.%3d.%3d' % (libname, version[0], version[1], version[2])) + version = config["version"] + print( + "%-13s %3d.%3d.%3d" % (libname, version[0], version[1], version[2]) + ) if args.codecs: from av.codec.codec import dump_codecs + dump_codecs() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/av/codec/__init__.py b/av/codec/__init__.py index c8c693a43..7a775ed5a 100644 --- a/av/codec/__init__.py +++ b/av/codec/__init__.py @@ -1,8 +1,2 @@ -from .codec import ( - Capabilities, - Codec, - Properties, - codec_descriptor, - codecs_available -) +from .codec import Capabilities, Codec, Properties, codec_descriptor, codecs_available from .context import CodecContext diff --git a/av/datasets.py b/av/datasets.py index 7ef768a2a..a38268341 100644 --- a/av/datasets.py +++ b/av/datasets.py @@ -18,27 +18,27 @@ def iter_data_dirs(check_writable=False): try: - yield os.environ['PYAV_TESTDATA_DIR'] + yield os.environ["PYAV_TESTDATA_DIR"] except KeyError: pass - if os.name == 'nt': - yield os.path.join(sys.prefix, 'pyav', 'datasets') + if os.name == "nt": + yield os.path.join(sys.prefix, "pyav", "datasets") return bases = [ - '/usr/local/share', - '/usr/local/lib', - '/usr/share', - '/usr/lib', + "/usr/local/share", + "/usr/local/lib", + "/usr/share", + "/usr/lib", ] # Prefer the local virtualenv. - if hasattr(sys, 'real_prefix'): + if hasattr(sys, "real_prefix"): bases.insert(0, sys.prefix) for base in bases: - dir_ = os.path.join(base, 'pyav', 'datasets') + dir_ = os.path.join(base, "pyav", "datasets") if check_writable: if os.path.exists(dir_): if not os.access(dir_, os.W_OK): @@ -48,7 +48,7 @@ def iter_data_dirs(check_writable=False): continue yield dir_ - yield os.path.join(os.path.expanduser('~'), '.pyav', 'datasets') + yield os.path.join(os.path.expanduser("~"), ".pyav", "datasets") def cached_download(url, name): @@ -92,8 +92,8 @@ def cached_download(url, name): if e.errno != errno.EEXIST: raise - tmp_path = path + '.tmp' - with open(tmp_path, 'wb') as fh: + tmp_path = path + ".tmp" + with open(tmp_path, "wb") as fh: while True: chunk = response.read(8196) if chunk: @@ -114,8 +114,10 @@ def fate(name): See the `FFmpeg Automated Test Environment `_ """ - return cached_download('http://fate.ffmpeg.org/fate-suite/' + name, - os.path.join('fate-suite', name.replace('/', os.path.sep))) + return cached_download( + "http://fate.ffmpeg.org/fate-suite/" + name, + os.path.join("fate-suite", name.replace("/", os.path.sep)), + ) def curated(name): @@ -124,5 +126,7 @@ def curated(name): Data is handled by :func:`cached_download`. """ - return cached_download('https://pyav.org/datasets/' + name, - os.path.join('pyav-curated', name.replace('/', os.path.sep))) + return cached_download( + "https://pyav.org/datasets/" + name, + os.path.join("pyav-curated", name.replace("/", os.path.sep)), + ) diff --git a/av/deprecation.py b/av/deprecation.py index b62723ade..1e0cbb317 100644 --- a/av/deprecation.py +++ b/av/deprecation.py @@ -18,7 +18,7 @@ class MethodDeprecationWarning(AVDeprecationWarning): # really want these to be seen, but also to use the "correct" base classes. # So we're putting a filter in place to show our warnings. The users can # turn them back off if they want. -warnings.filterwarnings('default', '', AVDeprecationWarning) +warnings.filterwarnings("default", "", AVDeprecationWarning) class renamed_attr(object): @@ -43,27 +43,39 @@ def old_name(self, cls): def __get__(self, instance, cls): old_name = self.old_name(cls) - warnings.warn('{0}.{1} is deprecated; please use {0}.{2}.'.format( - cls.__name__, old_name, self.new_name, - ), AttributeRenamedWarning, stacklevel=2) + warnings.warn( + "{0}.{1} is deprecated; please use {0}.{2}.".format( + cls.__name__, + old_name, + self.new_name, + ), + AttributeRenamedWarning, + stacklevel=2, + ) return getattr(instance if instance is not None else cls, self.new_name) def __set__(self, instance, value): old_name = self.old_name(instance.__class__) - warnings.warn('{0}.{1} is deprecated; please use {0}.{2}.'.format( - instance.__class__.__name__, old_name, self.new_name, - ), AttributeRenamedWarning, stacklevel=2) + warnings.warn( + "{0}.{1} is deprecated; please use {0}.{2}.".format( + instance.__class__.__name__, + old_name, + self.new_name, + ), + AttributeRenamedWarning, + stacklevel=2, + ) setattr(instance, self.new_name, value) class method(object): - def __init__(self, func): - functools.update_wrapper(self, func, ('__name__', '__doc__')) + functools.update_wrapper(self, func, ("__name__", "__doc__")) self.func = func def __get__(self, instance, cls): - warning = MethodDeprecationWarning('{}.{} is deprecated.'.format( - cls.__name__, self.func.__name__)) + warning = MethodDeprecationWarning( + "{}.{} is deprecated.".format(cls.__name__, self.func.__name__) + ) warnings.warn(warning, stacklevel=2) return self.func.__get__(instance, cls) diff --git a/examples/basics/parse.py b/examples/basics/parse.py index 8e000b8d2..2d313cb3f 100644 --- a/examples/basics/parse.py +++ b/examples/basics/parse.py @@ -7,21 +7,26 @@ # We want an H.264 stream in the Annex B byte-stream format. # We haven't exposed bitstream filters yet, so we're gonna use the `ffmpeg` CLI. -h264_path = 'night-sky.h264' +h264_path = "night-sky.h264" if not os.path.exists(h264_path): - subprocess.check_call([ - 'ffmpeg', - '-i', av.datasets.curated('pexels/time-lapse-video-of-night-sky-857195.mp4'), - '-vcodec', 'copy', - '-an', - '-bsf:v', 'h264_mp4toannexb', - h264_path, - ]) + subprocess.check_call( + [ + "ffmpeg", + "-i", + av.datasets.curated("pexels/time-lapse-video-of-night-sky-857195.mp4"), + "-vcodec", + "copy", + "-an", + "-bsf:v", + "h264_mp4toannexb", + h264_path, + ] + ) -fh = open(h264_path, 'rb') +fh = open(h264_path, "rb") -codec = av.CodecContext.create('h264', 'r') +codec = av.CodecContext.create("h264", "r") while True: @@ -32,11 +37,11 @@ for packet in packets: - print(' ', packet) + print(" ", packet) frames = codec.decode(packet) for frame in frames: - print(' ', frame) + print(" ", frame) # We wait until the end to bail so that the last empty `buf` flushes # the parser. diff --git a/examples/basics/remux.py b/examples/basics/remux.py index d12266fb5..c4779f4e9 100644 --- a/examples/basics/remux.py +++ b/examples/basics/remux.py @@ -2,8 +2,8 @@ import av.datasets -input_ = av.open(av.datasets.curated('pexels/time-lapse-video-of-night-sky-857195.mp4')) -output = av.open('remuxed.mkv', 'w') +input_ = av.open(av.datasets.curated("pexels/time-lapse-video-of-night-sky-857195.mp4")) +output = av.open("remuxed.mkv", "w") # Make an output stream using the input as a template. This copies the stream # setup from one to the other. diff --git a/examples/basics/save_keyframes.py b/examples/basics/save_keyframes.py index 2af00dc55..1169c153a 100644 --- a/examples/basics/save_keyframes.py +++ b/examples/basics/save_keyframes.py @@ -2,11 +2,11 @@ import av.datasets -content = av.datasets.curated('pexels/time-lapse-video-of-night-sky-857195.mp4') +content = av.datasets.curated("pexels/time-lapse-video-of-night-sky-857195.mp4") with av.open(content) as container: # Signal that we only want to look at keyframes. stream = container.streams.video[0] - stream.codec_context.skip_frame = 'NONKEY' + stream.codec_context.skip_frame = "NONKEY" for frame in container.decode(stream): @@ -14,6 +14,6 @@ # We use `frame.pts` as `frame.index` won't make must sense with the `skip_frame`. frame.to_image().save( - 'night-sky.{:04d}.jpg'.format(frame.pts), + "night-sky.{:04d}.jpg".format(frame.pts), quality=80, ) diff --git a/examples/basics/thread_type.py b/examples/basics/thread_type.py index 665717e85..2fa7562c9 100644 --- a/examples/basics/thread_type.py +++ b/examples/basics/thread_type.py @@ -6,7 +6,9 @@ print("Decoding with default (slice) threading...") -container = av.open(av.datasets.curated('pexels/time-lapse-video-of-night-sky-857195.mp4')) +container = av.open( + av.datasets.curated("pexels/time-lapse-video-of-night-sky-857195.mp4") +) start_time = time.time() for packet in container.demux(): @@ -20,10 +22,12 @@ print("Decoding with auto threading...") -container = av.open(av.datasets.curated('pexels/time-lapse-video-of-night-sky-857195.mp4')) +container = av.open( + av.datasets.curated("pexels/time-lapse-video-of-night-sky-857195.mp4") +) # !!! This is the only difference. -container.streams.video[0].thread_type = 'AUTO' +container.streams.video[0].thread_type = "AUTO" start_time = time.time() for packet in container.demux(): diff --git a/examples/numpy/barcode.py b/examples/numpy/barcode.py index 45300a87c..64c13e198 100644 --- a/examples/numpy/barcode.py +++ b/examples/numpy/barcode.py @@ -5,20 +5,22 @@ import av.datasets -container = av.open(av.datasets.curated('pexels/time-lapse-video-of-sunset-by-the-sea-854400.mp4')) -container.streams.video[0].thread_type = 'AUTO' # Go faster! +container = av.open( + av.datasets.curated("pexels/time-lapse-video-of-sunset-by-the-sea-854400.mp4") +) +container.streams.video[0].thread_type = "AUTO" # Go faster! columns = [] for frame in container.decode(video=0): print(frame) - array = frame.to_ndarray(format='rgb24') + array = frame.to_ndarray(format="rgb24") # Collapse down to a column. column = array.mean(axis=1) # Convert to bytes, as the `mean` turned our array into floats. - column = column.clip(0, 255).astype('uint8') + column = column.clip(0, 255).astype("uint8") # Get us in the right shape for the `hstack` below. column = column.reshape(-1, 1, 3) @@ -29,6 +31,6 @@ container.close() full_array = np.hstack(columns) -full_img = Image.fromarray(full_array, 'RGB') +full_img = Image.fromarray(full_array, "RGB") full_img = full_img.resize((800, 200)) -full_img.save('barcode.jpg', quality=85) +full_img.save("barcode.jpg", quality=85) diff --git a/examples/numpy/generate_video.py b/examples/numpy/generate_video.py index 9fc3a604a..50e8f52c9 100644 --- a/examples/numpy/generate_video.py +++ b/examples/numpy/generate_video.py @@ -1,4 +1,3 @@ - from __future__ import division import numpy as np @@ -10,12 +9,12 @@ fps = 24 total_frames = duration * fps -container = av.open('test.mp4', mode='w') +container = av.open("test.mp4", mode="w") -stream = container.add_stream('mpeg4', rate=fps) +stream = container.add_stream("mpeg4", rate=fps) stream.width = 480 stream.height = 320 -stream.pix_fmt = 'yuv420p' +stream.pix_fmt = "yuv420p" for frame_i in range(total_frames): @@ -27,7 +26,7 @@ img = np.round(255 * img).astype(np.uint8) img = np.clip(img, 0, 255) - frame = av.VideoFrame.from_ndarray(img, format='rgb24') + frame = av.VideoFrame.from_ndarray(img, format="rgb24") for packet in stream.encode(frame): container.mux(packet) diff --git a/examples/numpy/generate_video_with_pts.py b/examples/numpy/generate_video_with_pts.py index 2ac835f66..58ca547e2 100644 --- a/examples/numpy/generate_video_with_pts.py +++ b/examples/numpy/generate_video_with_pts.py @@ -12,12 +12,12 @@ total_frames = 20 fps = 30 -container = av.open('generate_video_with_pts.mp4', mode='w') +container = av.open("generate_video_with_pts.mp4", mode="w") -stream = container.add_stream('mpeg4', rate=fps) # alibi frame rate +stream = container.add_stream("mpeg4", rate=fps) # alibi frame rate stream.width = width stream.height = height -stream.pix_fmt = 'yuv420p' +stream.pix_fmt = "yuv420p" # ffmpeg time is complicated # more at https://github.com/PyAV-Org/PyAV/blob/main/docs/api/time.rst @@ -51,9 +51,11 @@ # draw blocks of a progress bar cx = int(width / total_frames * (frame_i + 0.5)) cy = int(height / 2) - the_canvas[cy-block_h2: cy+block_h2, cx-block_w2: cx+block_w2] = nice_color + the_canvas[ + cy - block_h2 : cy + block_h2, cx - block_w2 : cx + block_w2 + ] = nice_color - frame = av.VideoFrame.from_ndarray(the_canvas, format='rgb24') + frame = av.VideoFrame.from_ndarray(the_canvas, format="rgb24") # seconds -> counts of time_base frame.pts = int(round(my_pts / stream.codec_context.time_base)) @@ -70,7 +72,7 @@ # this black frame will probably be shown for 1/fps time # at least, that is the analysis of ffprobe the_canvas[:] = 0 -frame = av.VideoFrame.from_ndarray(the_canvas, format='rgb24') +frame = av.VideoFrame.from_ndarray(the_canvas, format="rgb24") frame.pts = int(round(my_pts / stream.codec_context.time_base)) for packet in stream.encode(frame): container.mux(packet) diff --git a/scripts/test b/scripts/test index e7503a0d7..6277bdbf6 100755 --- a/scripts/test +++ b/scripts/test @@ -18,6 +18,10 @@ istest() { return $? } +if istest black; then + $PYAV_PYTHON -m black av examples tests +fi + if istest flake8; then # Settings are in setup.cfg $PYAV_PYTHON -m flake8 av examples tests diff --git a/setup.cfg b/setup.cfg index f4980c82b..1de247b85 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,16 +1,18 @@ [flake8] filename = *.py,*.pyx,*.pxd +ignore = E203,W503 max-line-length = 142 per-file-ignores = __init__.py: E402,F401 *.pyx,*.pxd: E211,E225,E227,E402,E999 [isort] -sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER -lines_after_imports = 2 -skip = av/__init__.py -known_first_party = av default_section = THIRDPARTY from_first = 1 +known_first_party = av +line_length = 88 +lines_after_imports = 2 multi_line_output = 3 +sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER +skip = av/__init__.py [metadata] license = BSD diff --git a/tests/common.py b/tests/common.py index 2bb13d5f7..bb3851a5f 100644 --- a/tests/common.py +++ b/tests/common.py @@ -18,7 +18,7 @@ Image = ImageFilter = None -is_windows = os.name == 'nt' +is_windows = os.name == "nt" skip_tests = frozenset(os.environ.get("PYAV_SKIP_TESTS", "").split(",")) @@ -34,15 +34,16 @@ def makedirs(path): def _sandbox(timed=False): - root = os.path.abspath(os.path.join( - __file__, '..', '..', - 'sandbox' - )) - - sandbox = os.path.join( - root, - _start_time.strftime('%Y%m%d-%H%M%S'), - ) if timed else root + root = os.path.abspath(os.path.join(__file__, "..", "..", "sandbox")) + + sandbox = ( + os.path.join( + root, + _start_time.strftime("%Y%m%d-%H%M%S"), + ) + if timed + else root + ) if not os.path.exists(sandbox): os.makedirs(sandbox) return sandbox @@ -50,23 +51,23 @@ def _sandbox(timed=False): def asset(*args): adir = os.path.dirname(__file__) - return os.path.abspath(os.path.join(adir, 'assets', *args)) + return os.path.abspath(os.path.join(adir, "assets", *args)) # Store all of the sample data here. -os.environ['PYAV_TESTDATA_DIR'] = asset() +os.environ["PYAV_TESTDATA_DIR"] = asset() def fate_png(): - return fate_suite('png1/55c99e750a5fd6_50314226.png') + return fate_suite("png1/55c99e750a5fd6_50314226.png") def sandboxed(*args, **kwargs): - do_makedirs = kwargs.pop('makedirs', True) - base = kwargs.pop('sandbox', None) - timed = kwargs.pop('timed', False) + do_makedirs = kwargs.pop("makedirs", True) + base = kwargs.pop("sandbox", None) + timed = kwargs.pop("timed", False) if kwargs: - raise TypeError('extra kwargs: %s' % ', '.join(sorted(kwargs))) + raise TypeError("extra kwargs: %s" % ", ".join(sorted(kwargs))) path = os.path.join(_sandbox(timed=timed) if base is None else base, *args) if do_makedirs: makedirs(os.path.dirname(path)) @@ -74,17 +75,24 @@ def sandboxed(*args, **kwargs): class MethodLogger(object): - def __init__(self, obj): self._obj = obj self._log = [] def __getattr__(self, name): value = getattr(self._obj, name) - if isinstance(value, (types.MethodType, types.FunctionType, types.BuiltinFunctionType, types.BuiltinMethodType)): + if isinstance( + value, + ( + types.MethodType, + types.FunctionType, + types.BuiltinFunctionType, + types.BuiltinMethodType, + ), + ): return functools.partial(self._method, name, value) else: - self._log.append(('__getattr__', (name, ), {})) + self._log.append(("__getattr__", (name,), {})) return value def _method(self, name, meth, *args, **kwargs): @@ -96,7 +104,6 @@ def _filter(self, type_): class TestCase(_Base): - @classmethod def _sandbox(cls, timed=True): path = os.path.join(_sandbox(timed=timed), cls.__name__) @@ -108,50 +115,56 @@ def sandbox(self): return self._sandbox(timed=True) def sandboxed(self, *args, **kwargs): - kwargs.setdefault('sandbox', self.sandbox) - kwargs.setdefault('timed', True) + kwargs.setdefault("sandbox", self.sandbox) + kwargs.setdefault("timed", True) return sandboxed(*args, **kwargs) def assertImagesAlmostEqual(self, a, b, epsilon=0.1, *args): - self.assertEqual(a.size, b.size, 'sizes dont match') + self.assertEqual(a.size, b.size, "sizes dont match") a = a.filter(ImageFilter.BLUR).getdata() b = b.filter(ImageFilter.BLUR).getdata() for i, ax, bx in zip(range(len(a)), a, b): diff = sum(abs(ac / 256 - bc / 256) for ac, bc in zip(ax, bx)) / 3 if diff > epsilon: - self.fail('images differed by %s at index %d; %s %s' % (diff, i, ax, bx)) + self.fail( + "images differed by %s at index %d; %s %s" % (diff, i, ax, bx) + ) # Add some of the unittest methods that we love from 2.7. if sys.version_info < (2, 7): def assertIs(self, a, b, msg=None): if a is not b: - self.fail(msg or '%r at 0x%x is not %r at 0x%x; %r is not %r' % (type(a), id(a), type(b), id(b), a, b)) + self.fail( + msg + or "%r at 0x%x is not %r at 0x%x; %r is not %r" + % (type(a), id(a), type(b), id(b), a, b) + ) def assertIsNot(self, a, b, msg=None): if a is b: - self.fail(msg or 'both are %r at 0x%x; %r' % (type(a), id(a), a)) + self.fail(msg or "both are %r at 0x%x; %r" % (type(a), id(a), a)) def assertIsNone(self, x, msg=None): if x is not None: - self.fail(msg or 'is not None; %r' % x) + self.fail(msg or "is not None; %r" % x) def assertIsNotNone(self, x, msg=None): if x is None: - self.fail(msg or 'is None; %r' % x) + self.fail(msg or "is None; %r" % x) def assertIn(self, a, b, msg=None): if a not in b: - self.fail(msg or '%r not in %r' % (a, b)) + self.fail(msg or "%r not in %r" % (a, b)) def assertNotIn(self, a, b, msg=None): if a in b: - self.fail(msg or '%r in %r' % (a, b)) + self.fail(msg or "%r in %r" % (a, b)) def assertIsInstance(self, instance, types, msg=None): if not isinstance(instance, types): - self.fail(msg or 'not an instance of %r; %r' % (types, instance)) + self.fail(msg or "not an instance of %r; %r" % (types, instance)) def assertNotIsInstance(self, instance, types, msg=None): if isinstance(instance, types): - self.fail(msg or 'is an instance of %r; %r' % (types, instance)) + self.fail(msg or "is an instance of %r; %r" % (types, instance)) diff --git a/tests/test_audiofifo.py b/tests/test_audiofifo.py index 8f67fbe72..c29090647 100644 --- a/tests/test_audiofifo.py +++ b/tests/test_audiofifo.py @@ -4,10 +4,9 @@ class TestAudioFifo(TestCase): - def test_data(self): - container = av.open(fate_suite('audio-reference/chorusnoise_2ch_44kHz_s16.wav')) + container = av.open(fate_suite("audio-reference/chorusnoise_2ch_44kHz_s16.wav")) stream = container.streams.audio[0] fifo = av.AudioFifo() @@ -24,8 +23,8 @@ def test_data(self): if i == 10: break - input_ = b''.join(input_) - output = b''.join(output) + input_ = b"".join(input_) + output = b"".join(output) min_len = min(len(input_), len(output)) self.assertTrue(min_len > 10 * 512 * 2 * 2) @@ -38,7 +37,7 @@ def test_pts_simple(self): iframe = av.AudioFrame(samples=1024) iframe.pts = 0 iframe.sample_rate = 48000 - iframe.time_base = '1/48000' + iframe.time_base = "1/48000" fifo.write(iframe) @@ -68,7 +67,7 @@ def test_pts_complex(self): iframe = av.AudioFrame(samples=1024) iframe.pts = 0 iframe.sample_rate = 48000 - iframe.time_base = '1/96000' + iframe.time_base = "1/96000" fifo.write(iframe) iframe.pts = 2048 @@ -85,7 +84,7 @@ def test_missing_sample_rate(self): iframe = av.AudioFrame(samples=1024) iframe.pts = 0 - iframe.time_base = '1/48000' + iframe.time_base = "1/48000" fifo.write(iframe) diff --git a/tests/test_audioformat.py b/tests/test_audioformat.py index ed87496fe..5d4eb7871 100644 --- a/tests/test_audioformat.py +++ b/tests/test_audioformat.py @@ -5,24 +5,23 @@ from .common import TestCase -postfix = 'le' if sys.byteorder == 'little' else 'be' +postfix = "le" if sys.byteorder == "little" else "be" class TestAudioFormats(TestCase): - def test_s16_inspection(self): - fmt = AudioFormat('s16') - self.assertEqual(fmt.name, 's16') + fmt = AudioFormat("s16") + self.assertEqual(fmt.name, "s16") self.assertFalse(fmt.is_planar) self.assertEqual(fmt.bits, 16) self.assertEqual(fmt.bytes, 2) - self.assertEqual(fmt.container_name, 's16' + postfix) - self.assertEqual(fmt.planar.name, 's16p') + self.assertEqual(fmt.container_name, "s16" + postfix) + self.assertEqual(fmt.planar.name, "s16p") self.assertIs(fmt.packed, fmt) def test_s32p_inspection(self): - fmt = AudioFormat('s32p') - self.assertEqual(fmt.name, 's32p') + fmt = AudioFormat("s32p") + self.assertEqual(fmt.name, "s32p") self.assertTrue(fmt.is_planar) self.assertEqual(fmt.bits, 32) self.assertEqual(fmt.bytes, 4) diff --git a/tests/test_audioframe.py b/tests/test_audioframe.py index 9468641c1..626c37e99 100644 --- a/tests/test_audioframe.py +++ b/tests/test_audioframe.py @@ -9,67 +9,66 @@ class TestAudioFrameConstructors(TestCase): - def test_null_constructor(self): frame = AudioFrame() - self.assertEqual(frame.format.name, 's16') - self.assertEqual(frame.layout.name, 'stereo') + self.assertEqual(frame.format.name, "s16") + self.assertEqual(frame.layout.name, "stereo") self.assertEqual(len(frame.planes), 0) self.assertEqual(frame.samples, 0) def test_manual_flt_mono_constructor(self): - frame = AudioFrame(format='flt', layout='mono', samples=160) - self.assertEqual(frame.format.name, 'flt') - self.assertEqual(frame.layout.name, 'mono') + frame = AudioFrame(format="flt", layout="mono", samples=160) + self.assertEqual(frame.format.name, "flt") + self.assertEqual(frame.layout.name, "mono") self.assertEqual(len(frame.planes), 1) self.assertEqual(frame.planes[0].buffer_size, 640) self.assertEqual(frame.samples, 160) def test_manual_flt_stereo_constructor(self): - frame = AudioFrame(format='flt', layout='stereo', samples=160) - self.assertEqual(frame.format.name, 'flt') - self.assertEqual(frame.layout.name, 'stereo') + frame = AudioFrame(format="flt", layout="stereo", samples=160) + self.assertEqual(frame.format.name, "flt") + self.assertEqual(frame.layout.name, "stereo") self.assertEqual(len(frame.planes), 1) self.assertEqual(frame.planes[0].buffer_size, 1280) self.assertEqual(frame.samples, 160) def test_manual_fltp_stereo_constructor(self): - frame = AudioFrame(format='fltp', layout='stereo', samples=160) - self.assertEqual(frame.format.name, 'fltp') - self.assertEqual(frame.layout.name, 'stereo') + frame = AudioFrame(format="fltp", layout="stereo", samples=160) + self.assertEqual(frame.format.name, "fltp") + self.assertEqual(frame.layout.name, "stereo") self.assertEqual(len(frame.planes), 2) self.assertEqual(frame.planes[0].buffer_size, 640) self.assertEqual(frame.planes[1].buffer_size, 640) self.assertEqual(frame.samples, 160) def test_manual_s16_mono_constructor(self): - frame = AudioFrame(format='s16', layout='mono', samples=160) - self.assertEqual(frame.format.name, 's16') - self.assertEqual(frame.layout.name, 'mono') + frame = AudioFrame(format="s16", layout="mono", samples=160) + self.assertEqual(frame.format.name, "s16") + self.assertEqual(frame.layout.name, "mono") self.assertEqual(len(frame.planes), 1) self.assertEqual(frame.planes[0].buffer_size, 320) self.assertEqual(frame.samples, 160) def test_manual_s16_mono_constructor_align_8(self): - frame = AudioFrame(format='s16', layout='mono', samples=159, align=8) - self.assertEqual(frame.format.name, 's16') - self.assertEqual(frame.layout.name, 'mono') + frame = AudioFrame(format="s16", layout="mono", samples=159, align=8) + self.assertEqual(frame.format.name, "s16") + self.assertEqual(frame.layout.name, "mono") self.assertEqual(len(frame.planes), 1) self.assertEqual(frame.planes[0].buffer_size, 320) self.assertEqual(frame.samples, 159) def test_manual_s16_stereo_constructor(self): - frame = AudioFrame(format='s16', layout='stereo', samples=160) - self.assertEqual(frame.format.name, 's16') - self.assertEqual(frame.layout.name, 'stereo') + frame = AudioFrame(format="s16", layout="stereo", samples=160) + self.assertEqual(frame.format.name, "s16") + self.assertEqual(frame.layout.name, "stereo") self.assertEqual(len(frame.planes), 1) self.assertEqual(frame.planes[0].buffer_size, 640) self.assertEqual(frame.samples, 160) def test_manual_s16p_stereo_constructor(self): - frame = AudioFrame(format='s16p', layout='stereo', samples=160) - self.assertEqual(frame.format.name, 's16p') - self.assertEqual(frame.layout.name, 'stereo') + frame = AudioFrame(format="s16p", layout="stereo", samples=160) + self.assertEqual(frame.format.name, "s16p") + self.assertEqual(frame.layout.name, "stereo") self.assertEqual(len(frame.planes), 2) self.assertEqual(frame.planes[0].buffer_size, 320) self.assertEqual(frame.planes[1].buffer_size, 320) @@ -77,15 +76,14 @@ def test_manual_s16p_stereo_constructor(self): class TestAudioFrameConveniences(TestCase): - def test_basic_to_ndarray(self): - frame = AudioFrame(format='s16p', layout='stereo', samples=160) + frame = AudioFrame(format="s16p", layout="stereo", samples=160) array = frame.to_ndarray() - self.assertEqual(array.dtype, 'i2') + self.assertEqual(array.dtype, "i2") self.assertEqual(array.shape, (2, 160)) def test_basic_to_nd_array(self): - frame = AudioFrame(format='s16p', layout='stereo', samples=160) + frame = AudioFrame(format="s16p", layout="stereo", samples=160) with warnings.catch_warnings(record=True) as recorded: array = frame.to_nd_array() self.assertEqual(array.shape, (2, 160)) @@ -95,14 +93,15 @@ def test_basic_to_nd_array(self): self.assertEqual(recorded[0].category, AttributeRenamedWarning) self.assertEqual( str(recorded[0].message), - 'AudioFrame.to_nd_array is deprecated; please use AudioFrame.to_ndarray.') + "AudioFrame.to_nd_array is deprecated; please use AudioFrame.to_ndarray.", + ) def test_ndarray_dbl(self): layouts = [ - ('dbl', 'mono', 'f8', (1, 160)), - ('dbl', 'stereo', 'f8', (1, 320)), - ('dblp', 'mono', 'f8', (1, 160)), - ('dblp', 'stereo', 'f8', (2, 160)), + ("dbl", "mono", "f8", (1, 160)), + ("dbl", "stereo", "f8", (1, 320)), + ("dblp", "mono", "f8", (1, 160)), + ("dblp", "stereo", "f8", (2, 160)), ] for format, layout, dtype, size in layouts: array = numpy.ndarray(shape=size, dtype=dtype) @@ -119,13 +118,18 @@ def test_from_ndarray_value_error(self): array = numpy.ndarray(shape=(1, 160), dtype="f2") with self.assertRaises(ValueError) as cm: AudioFrame.from_ndarray(array, format="flt", layout="mono") - self.assertEqual(str(cm.exception), "Expected numpy array with dtype `float32` but got `float16`") + self.assertEqual( + str(cm.exception), + "Expected numpy array with dtype `float32` but got `float16`", + ) # incorrect number of dimensions array = numpy.ndarray(shape=(1, 160, 2), dtype="f4") with self.assertRaises(ValueError) as cm: AudioFrame.from_ndarray(array, format="flt", layout="mono") - self.assertEqual(str(cm.exception), "Expected numpy array with ndim `2` but got `3`") + self.assertEqual( + str(cm.exception), "Expected numpy array with ndim `2` but got `3`" + ) # incorrect shape array = numpy.ndarray(shape=(2, 160), dtype="f4") @@ -135,10 +139,10 @@ def test_from_ndarray_value_error(self): def test_ndarray_flt(self): layouts = [ - ('flt', 'mono', 'f4', (1, 160)), - ('flt', 'stereo', 'f4', (1, 320)), - ('fltp', 'mono', 'f4', (1, 160)), - ('fltp', 'stereo', 'f4', (2, 160)), + ("flt", "mono", "f4", (1, 160)), + ("flt", "stereo", "f4", (1, 320)), + ("fltp", "mono", "f4", (1, 160)), + ("fltp", "stereo", "f4", (2, 160)), ] for format, layout, dtype, size in layouts: array = numpy.ndarray(shape=size, dtype=dtype) @@ -152,10 +156,10 @@ def test_ndarray_flt(self): def test_ndarray_s16(self): layouts = [ - ('s16', 'mono', 'i2', (1, 160)), - ('s16', 'stereo', 'i2', (1, 320)), - ('s16p', 'mono', 'i2', (1, 160)), - ('s16p', 'stereo', 'i2', (2, 160)), + ("s16", "mono", "i2", (1, 160)), + ("s16", "stereo", "i2", (1, 320)), + ("s16p", "mono", "i2", (1, 160)), + ("s16p", "stereo", "i2", (2, 160)), ] for format, layout, dtype, size in layouts: array = numpy.random.randint(0, 256, size=size, dtype=dtype) @@ -166,17 +170,17 @@ def test_ndarray_s16(self): self.assertTrue((frame.to_ndarray() == array).all()) def test_ndarray_s16p_align_8(self): - frame = AudioFrame(format='s16p', layout='stereo', samples=159, align=8) + frame = AudioFrame(format="s16p", layout="stereo", samples=159, align=8) array = frame.to_ndarray() - self.assertEqual(array.dtype, 'i2') + self.assertEqual(array.dtype, "i2") self.assertEqual(array.shape, (2, 159)) def test_ndarray_s32(self): layouts = [ - ('s32', 'mono', 'i4', (1, 160)), - ('s32', 'stereo', 'i4', (1, 320)), - ('s32p', 'mono', 'i4', (1, 160)), - ('s32p', 'stereo', 'i4', (2, 160)), + ("s32", "mono", "i4", (1, 160)), + ("s32", "stereo", "i4", (1, 320)), + ("s32p", "mono", "i4", (1, 160)), + ("s32p", "stereo", "i4", (2, 160)), ] for format, layout, dtype, size in layouts: array = numpy.random.randint(0, 256, size=size, dtype=dtype) @@ -188,10 +192,10 @@ def test_ndarray_s32(self): def test_ndarray_u8(self): layouts = [ - ('u8', 'mono', 'u1', (1, 160)), - ('u8', 'stereo', 'u1', (1, 320)), - ('u8p', 'mono', 'u1', (1, 160)), - ('u8p', 'stereo', 'u1', (2, 160)), + ("u8", "mono", "u1", (1, 160)), + ("u8", "stereo", "u1", (1, 320)), + ("u8p", "mono", "u1", (1, 160)), + ("u8p", "stereo", "u1", (2, 160)), ] for format, layout, dtype, size in layouts: array = numpy.random.randint(0, 256, size=size, dtype=dtype) diff --git a/tests/test_audiolayout.py b/tests/test_audiolayout.py index 966bd4894..0d13a2a18 100644 --- a/tests/test_audiolayout.py +++ b/tests/test_audiolayout.py @@ -4,9 +4,8 @@ class TestAudioLayout(TestCase): - def test_stereo_properties(self): - layout = AudioLayout('stereo') + layout = AudioLayout("stereo") self._test_stereo(layout) def test_2channel_properties(self): @@ -18,18 +17,23 @@ def test_channel_counts(self): self.assertRaises(ValueError, AudioLayout, 9) def _test_stereo(self, layout): - self.assertEqual(layout.name, 'stereo') + self.assertEqual(layout.name, "stereo") self.assertEqual(len(layout.channels), 2) self.assertEqual(repr(layout), "") self.assertEqual(layout.channels[0].name, "FL") self.assertEqual(layout.channels[0].description, "front left") - self.assertEqual(repr(layout.channels[0]), "") + self.assertEqual( + repr(layout.channels[0]), "" + ) self.assertEqual(layout.channels[1].name, "FR") self.assertEqual(layout.channels[1].description, "front right") - self.assertEqual(repr(layout.channels[1]), "") + self.assertEqual( + repr(layout.channels[1]), "" + ) def test_defaults(self): - for i, name in enumerate(''' + for i, name in enumerate( + """ mono stereo 2.1 @@ -38,7 +42,8 @@ def test_defaults(self): 5.1 6.1 7.1 - '''.strip().split()): + """.strip().split() + ): layout = AudioLayout(i + 1) self.assertEqual(layout.name, name) self.assertEqual(len(layout.channels), i + 1) diff --git a/tests/test_audioresampler.py b/tests/test_audioresampler.py index 42236a4c3..3558923aa 100644 --- a/tests/test_audioresampler.py +++ b/tests/test_audioresampler.py @@ -6,14 +6,13 @@ class TestAudioResampler(TestCase): - def test_identity_passthrough(self): # If we don't ask it to do anything, it won't. resampler = AudioResampler() - iframe = AudioFrame('s16', 'stereo', 1024) + iframe = AudioFrame("s16", "stereo", 1024) oframe = resampler.resample(iframe)[0] self.assertIs(iframe, oframe) @@ -22,20 +21,20 @@ def test_matching_passthrough(self): # If the frames match, it won't do anything. - resampler = AudioResampler('s16', 'stereo') + resampler = AudioResampler("s16", "stereo") - iframe = AudioFrame('s16', 'stereo', 1024) + iframe = AudioFrame("s16", "stereo", 1024) oframe = resampler.resample(iframe)[0] self.assertIs(iframe, oframe) def test_pts_assertion_same_rate(self): - resampler = AudioResampler('s16', 'mono') + resampler = AudioResampler("s16", "mono") - iframe = AudioFrame('s16', 'stereo', 1024) + iframe = AudioFrame("s16", "stereo", 1024) iframe.sample_rate = 48000 - iframe.time_base = '1/48000' + iframe.time_base = "1/48000" iframe.pts = 0 oframe = resampler.resample(iframe)[0] @@ -56,23 +55,23 @@ def test_pts_assertion_same_rate(self): def test_pts_assertion_new_rate(self): - resampler = AudioResampler('s16', 'mono', 44100) + resampler = AudioResampler("s16", "mono", 44100) - iframe = AudioFrame('s16', 'stereo', 1024) + iframe = AudioFrame("s16", "stereo", 1024) iframe.sample_rate = 48000 - iframe.time_base = '1/48000' + iframe.time_base = "1/48000" iframe.pts = 0 oframe = resampler.resample(iframe)[0] self.assertEqual(oframe.pts, 0) - self.assertEqual(str(oframe.time_base), '1/44100') + self.assertEqual(str(oframe.time_base), "1/44100") self.assertEqual(oframe.sample_rate, 44100) def test_pts_missing_time_base(self): - resampler = AudioResampler('s16', 'mono', 44100) + resampler = AudioResampler("s16", "mono", 44100) - iframe = AudioFrame('s16', 'stereo', 1024) + iframe = AudioFrame("s16", "stereo", 1024) iframe.sample_rate = 48000 iframe.pts = 0 diff --git a/tests/test_codec.py b/tests/test_codec.py index a5026864b..b7af5eedf 100644 --- a/tests/test_codec.py +++ b/tests/test_codec.py @@ -8,7 +8,7 @@ # some older ffmpeg versions have no native opus encoder try: - opus_c = Codec('opus', 'w') + opus_c = Codec("opus", "w") opus_encoder_missing = False except UnknownCodecError: opus_encoder_missing = True @@ -17,16 +17,16 @@ class TestCodecs(TestCase): def test_codec_bogus(self): with self.assertRaises(UnknownCodecError): - Codec('bogus123') + Codec("bogus123") with self.assertRaises(UnknownCodecError): - Codec('bogus123', 'w') + Codec("bogus123", "w") def test_codec_mpeg4_decoder(self): - c = Codec('mpeg4') + c = Codec("mpeg4") - self.assertEqual(c.name, 'mpeg4') - self.assertEqual(c.long_name, 'MPEG-4 part 2') - self.assertEqual(c.type, 'video') + self.assertEqual(c.name, "mpeg4") + self.assertEqual(c.long_name, "MPEG-4 part 2") + self.assertEqual(c.type, "video") self.assertIn(c.id, (12, 13)) self.assertTrue(c.is_decoder) self.assertFalse(c.is_encoder) @@ -39,15 +39,15 @@ def test_codec_mpeg4_decoder(self): formats = c.video_formats self.assertTrue(formats) self.assertIsInstance(formats[0], VideoFormat) - self.assertTrue(any(f.name == 'yuv420p' for f in formats)) + self.assertTrue(any(f.name == "yuv420p" for f in formats)) self.assertIsNone(c.frame_rates) def test_codec_mpeg4_encoder(self): - c = Codec('mpeg4', 'w') - self.assertEqual(c.name, 'mpeg4') - self.assertEqual(c.long_name, 'MPEG-4 part 2') - self.assertEqual(c.type, 'video') + c = Codec("mpeg4", "w") + self.assertEqual(c.name, "mpeg4") + self.assertEqual(c.long_name, "MPEG-4 part 2") + self.assertEqual(c.type, "video") self.assertIn(c.id, (12, 13)) self.assertTrue(c.is_encoder) self.assertFalse(c.is_decoder) @@ -60,16 +60,16 @@ def test_codec_mpeg4_encoder(self): formats = c.video_formats self.assertTrue(formats) self.assertIsInstance(formats[0], VideoFormat) - self.assertTrue(any(f.name == 'yuv420p' for f in formats)) + self.assertTrue(any(f.name == "yuv420p" for f in formats)) self.assertIsNone(c.frame_rates) def test_codec_opus_decoder(self): - c = Codec('opus') + c = Codec("opus") - self.assertEqual(c.name, 'opus') - self.assertEqual(c.long_name, 'Opus') - self.assertEqual(c.type, 'audio') + self.assertEqual(c.name, "opus") + self.assertEqual(c.long_name, "Opus") + self.assertEqual(c.type, "audio") self.assertTrue(c.is_decoder) self.assertFalse(c.is_encoder) @@ -81,12 +81,12 @@ def test_codec_opus_decoder(self): self.assertIsNone(c.video_formats) self.assertIsNone(c.frame_rates) - @unittest.skipIf(opus_encoder_missing, 'Opus encoder is not available') + @unittest.skipIf(opus_encoder_missing, "Opus encoder is not available") def test_codec_opus_encoder(self): - c = Codec('opus', 'w') - self.assertIn(c.name, ('opus', 'libopus')) - self.assertIn(c.long_name, ('Opus', 'libopus Opus')) - self.assertEqual(c.type, 'audio') + c = Codec("opus", "w") + self.assertIn(c.name, ("opus", "libopus")) + self.assertIn(c.long_name, ("Opus", "libopus Opus")) + self.assertEqual(c.type, "audio") self.assertTrue(c.is_encoder) self.assertFalse(c.is_decoder) @@ -94,7 +94,7 @@ def test_codec_opus_encoder(self): formats = c.audio_formats self.assertTrue(formats) self.assertIsInstance(formats[0], AudioFormat) - self.assertTrue(any(f.name in ['flt', 'fltp'] for f in formats)) + self.assertTrue(any(f.name in ["flt", "fltp"] for f in formats)) self.assertIsNotNone(c.audio_rates) self.assertIn(48000, c.audio_rates) diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 91449b8f0..72df49db5 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -16,7 +16,7 @@ def iter_frames(container, stream): def iter_raw_frames(path, packet_sizes, ctx): - with open(path, 'rb') as f: + with open(path, "rb") as f: for i, size in enumerate(packet_sizes): packet = Packet(size) read_size = f.readinto(packet) @@ -38,32 +38,31 @@ def iter_raw_frames(path, packet_sizes, ctx): class TestCodecContext(TestCase): - def test_skip_frame_default(self): - ctx = Codec('png', 'w').create() - self.assertEqual(ctx.skip_frame.name, 'DEFAULT') + ctx = Codec("png", "w").create() + self.assertEqual(ctx.skip_frame.name, "DEFAULT") def test_codec_tag(self): - ctx = Codec('mpeg4', 'w').create() - self.assertEqual(ctx.codec_tag, '\x00\x00\x00\x00') - ctx.codec_tag = 'xvid' - self.assertEqual(ctx.codec_tag, 'xvid') + ctx = Codec("mpeg4", "w").create() + self.assertEqual(ctx.codec_tag, "\x00\x00\x00\x00") + ctx.codec_tag = "xvid" + self.assertEqual(ctx.codec_tag, "xvid") # wrong length with self.assertRaises(ValueError) as cm: - ctx.codec_tag = 'bob' - self.assertEqual(str(cm.exception), 'Codec tag should be a 4 character string.') + ctx.codec_tag = "bob" + self.assertEqual(str(cm.exception), "Codec tag should be a 4 character string.") # wrong type with self.assertRaises(ValueError) as cm: ctx.codec_tag = 123 - self.assertEqual(str(cm.exception), 'Codec tag should be a 4 character string.') + self.assertEqual(str(cm.exception), "Codec tag should be a 4 character string.") - with av.open(fate_suite('h264/interlaced_crop.mp4')) as container: - self.assertEqual(container.streams[0].codec_tag, 'avc1') + with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: + self.assertEqual(container.streams[0].codec_tag, "avc1") def test_decoder_extradata(self): - ctx = av.codec.Codec('h264', 'r').create() + ctx = av.codec.Codec("h264", "r").create() self.assertEqual(ctx.extradata, None) self.assertEqual(ctx.extradata_size, 0) @@ -80,7 +79,7 @@ def test_decoder_extradata(self): self.assertEqual(ctx.extradata_size, 0) def test_encoder_extradata(self): - ctx = av.codec.Codec('h264', 'w').create() + ctx = av.codec.Codec("h264", "w").create() self.assertEqual(ctx.extradata, None) self.assertEqual(ctx.extradata_size, 0) @@ -89,7 +88,7 @@ def test_encoder_extradata(self): self.assertEqual(str(cm.exception), "Can only set extradata for decoders.") def test_encoder_pix_fmt(self): - ctx = av.codec.Codec('h264', 'w').create() + ctx = av.codec.Codec("h264", "w").create() # valid format ctx.pix_fmt = "yuv420p" @@ -104,10 +103,10 @@ def test_encoder_pix_fmt(self): def test_parse(self): # This one parses into a single packet. - self._assert_parse('mpeg4', fate_suite('h264/interlaced_crop.mp4')) + self._assert_parse("mpeg4", fate_suite("h264/interlaced_crop.mp4")) # This one parses into many small packets. - self._assert_parse('mpeg2video', fate_suite('mpeg2/mpeg2_field_encoding.ts')) + self._assert_parse("mpeg2video", fate_suite("mpeg2/mpeg2_field_encoding.ts")) def _assert_parse(self, codec_name, path): @@ -116,7 +115,7 @@ def _assert_parse(self, codec_name, path): for packet in fh.demux(video=0): packets.append(packet) - full_source = b''.join(p.to_bytes() for p in packets) + full_source = b"".join(p.to_bytes() for p in packets) for size in 1024, 8192, 65535: @@ -124,34 +123,33 @@ def _assert_parse(self, codec_name, path): packets = [] for i in range(0, len(full_source), size): - block = full_source[i:i + size] + block = full_source[i : i + size] packets.extend(ctx.parse(block)) packets.extend(ctx.parse()) - parsed_source = b''.join(p.to_bytes() for p in packets) + parsed_source = b"".join(p.to_bytes() for p in packets) self.assertEqual(len(parsed_source), len(full_source)) self.assertEqual(full_source, parsed_source) class TestEncoding(TestCase): - def test_encoding_png(self): - self.image_sequence_encode('png') + self.image_sequence_encode("png") def test_encoding_mjpeg(self): - self.image_sequence_encode('mjpeg') + self.image_sequence_encode("mjpeg") def test_encoding_tiff(self): - self.image_sequence_encode('tiff') + self.image_sequence_encode("tiff") def image_sequence_encode(self, codec_name): try: - codec = Codec(codec_name, 'w') + codec = Codec(codec_name, "w") except UnknownCodecError: raise SkipTest() - container = av.open(fate_suite('h264/interlaced_crop.mp4')) + container = av.open(fate_suite("h264/interlaced_crop.mp4")) video_stream = container.streams.video[0] width = 640 @@ -177,22 +175,25 @@ def image_sequence_encode(self, codec_name): self.assertEqual(len(new_packets), 1) new_packet = new_packets[0] - path = self.sandboxed('%s/encoder.%04d.%s' % ( - codec_name, - frame_count, - codec_name if codec_name != 'mjpeg' else 'jpg', - )) + path = self.sandboxed( + "%s/encoder.%04d.%s" + % ( + codec_name, + frame_count, + codec_name if codec_name != "mjpeg" else "jpg", + ) + ) path_list.append(path) - with open(path, 'wb') as f: + with open(path, "wb") as f: f.write(new_packet) frame_count += 1 if frame_count > 5: break - ctx = av.Codec(codec_name, 'r').create() + ctx = av.Codec(codec_name, "r").create() for path in path_list: - with open(path, 'rb') as f: + with open(path, "rb") as f: size = os.fstat(f.fileno()).st_size packet = Packet(size) size = f.readinto(packet) @@ -202,47 +203,47 @@ def image_sequence_encode(self, codec_name): self.assertEqual(frame.format.name, pix_fmt) def test_encoding_h264(self): - self.video_encoding('libx264', {'crf': '19'}) + self.video_encoding("libx264", {"crf": "19"}) def test_encoding_mpeg4(self): - self.video_encoding('mpeg4') + self.video_encoding("mpeg4") def test_encoding_xvid(self): - self.video_encoding('mpeg4', codec_tag='xvid') + self.video_encoding("mpeg4", codec_tag="xvid") def test_encoding_mpeg1video(self): - self.video_encoding('mpeg1video') + self.video_encoding("mpeg1video") def test_encoding_dvvideo(self): - options = {'pix_fmt': 'yuv411p', - 'width': 720, - 'height': 480} - self.video_encoding('dvvideo', options) + options = {"pix_fmt": "yuv411p", "width": 720, "height": 480} + self.video_encoding("dvvideo", options) def test_encoding_dnxhd(self): - options = {'b': '90M', # bitrate - 'pix_fmt': 'yuv422p', - 'width': 1920, - 'height': 1080, - 'time_base': '1001/30000', - 'max_frames': 5} - self.video_encoding('dnxhd', options) + options = { + "b": "90M", # bitrate + "pix_fmt": "yuv422p", + "width": 1920, + "height": 1080, + "time_base": "1001/30000", + "max_frames": 5, + } + self.video_encoding("dnxhd", options) def video_encoding(self, codec_name, options={}, codec_tag=None): try: - codec = Codec(codec_name, 'w') + codec = Codec(codec_name, "w") except UnknownCodecError: raise SkipTest() - container = av.open(fate_suite('h264/interlaced_crop.mp4')) + container = av.open(fate_suite("h264/interlaced_crop.mp4")) video_stream = container.streams.video[0] - pix_fmt = options.pop('pix_fmt', 'yuv420p') - width = options.pop('width', 640) - height = options.pop('height', 480) - max_frames = options.pop('max_frames', 50) - time_base = options.pop('time_base', video_stream.codec_context.time_base) + pix_fmt = options.pop("pix_fmt", "yuv420p") + width = options.pop("width", 640) + height = options.pop("height", 480) + max_frames = options.pop("max_frames", 50) + time_base = options.pop("time_base", video_stream.codec_context.time_base) ctx = codec.create() ctx.width = width @@ -255,11 +256,11 @@ def video_encoding(self, codec_name, options={}, codec_tag=None): ctx.codec_tag = codec_tag ctx.open() - path = self.sandboxed('encoder.%s' % codec_name) + path = self.sandboxed("encoder.%s" % codec_name) packet_sizes = [] frame_count = 0 - with open(path, 'wb') as f: + with open(path, "wb") as f: for frame in iter_frames(container, video_stream): @@ -293,10 +294,10 @@ def video_encoding(self, codec_name, options={}, codec_tag=None): f.write(packet) dec_codec_name = codec_name - if codec_name == 'libx264': - dec_codec_name = 'h264' + if codec_name == "libx264": + dec_codec_name = "h264" - ctx = av.Codec(dec_codec_name, 'r').create() + ctx = av.Codec(dec_codec_name, "r").create() ctx.open() decoded_frame_count = 0 @@ -309,18 +310,18 @@ def video_encoding(self, codec_name, options={}, codec_tag=None): self.assertEqual(frame_count, decoded_frame_count) def test_encoding_pcm_s24le(self): - self.audio_encoding('pcm_s24le') + self.audio_encoding("pcm_s24le") def test_encoding_aac(self): - self.audio_encoding('aac') + self.audio_encoding("aac") def test_encoding_mp2(self): - self.audio_encoding('mp2') + self.audio_encoding("mp2") def audio_encoding(self, codec_name): try: - codec = Codec(codec_name, 'w') + codec = Codec(codec_name, "w") except UnknownCodecError: raise SkipTest() @@ -343,15 +344,15 @@ def audio_encoding(self, codec_name): resampler = AudioResampler(sample_fmt, channel_layout, sample_rate) - container = av.open(fate_suite('audio-reference/chorusnoise_2ch_44kHz_s16.wav')) + container = av.open(fate_suite("audio-reference/chorusnoise_2ch_44kHz_s16.wav")) audio_stream = container.streams.audio[0] - path = self.sandboxed('encoder.%s' % codec_name) + path = self.sandboxed("encoder.%s" % codec_name) samples = 0 packet_sizes = [] - with open(path, 'wb') as f: + with open(path, "wb") as f: for frame in iter_frames(container, audio_stream): # We need to let the encoder retime. @@ -386,7 +387,7 @@ def audio_encoding(self, codec_name): f.write(bytearray(packet)) packet_sizes.append(packet.size) - ctx = Codec(codec_name, 'r').create() + ctx = Codec(codec_name, "r").create() ctx.time_base = Fraction(1) / sample_rate ctx.sample_rate = sample_rate ctx.format = sample_fmt diff --git a/tests/test_container.py b/tests/test_container.py index fa7052395..5aef83d11 100644 --- a/tests/test_container.py +++ b/tests/test_container.py @@ -15,13 +15,15 @@ class TestContainers(TestCase): - def test_context_manager(self): - with av.open(fate_suite('h264/interlaced_crop.mp4')) as container: - self.assertEqual(container.format.long_name, 'QuickTime / MOV') + with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: + self.assertEqual(container.format.long_name, "QuickTime / MOV") self.assertEqual(len(container.streams), 1) - @unittest.skipIf(broken_unicode or 'unicode_filename' in skip_tests, 'Unicode filename handling is broken') + @unittest.skipIf( + broken_unicode or "unicode_filename" in skip_tests, + "Unicode filename handling is broken", + ) def test_unicode_filename(self): - av.open(self.sandboxed(u'¢∞§¶•ªº.mov'), 'w') + av.open(self.sandboxed("¢∞§¶•ªº.mov"), "w") diff --git a/tests/test_containerformat.py b/tests/test_containerformat.py index e5e5a9698..dea3d29dc 100644 --- a/tests/test_containerformat.py +++ b/tests/test_containerformat.py @@ -4,40 +4,39 @@ class TestContainerFormats(TestCase): - def test_matroska(self): - fmt = ContainerFormat('matroska') + fmt = ContainerFormat("matroska") self.assertTrue(fmt.is_input) self.assertTrue(fmt.is_output) - self.assertEqual(fmt.name, 'matroska') - self.assertEqual(fmt.long_name, 'Matroska') - self.assertIn('mkv', fmt.extensions) + self.assertEqual(fmt.name, "matroska") + self.assertEqual(fmt.long_name, "Matroska") + self.assertIn("mkv", fmt.extensions) self.assertFalse(fmt.no_file) def test_mov(self): - fmt = ContainerFormat('mov') + fmt = ContainerFormat("mov") self.assertTrue(fmt.is_input) self.assertTrue(fmt.is_output) - self.assertEqual(fmt.name, 'mov') - self.assertEqual(fmt.long_name, 'QuickTime / MOV') - self.assertIn('mov', fmt.extensions) + self.assertEqual(fmt.name, "mov") + self.assertEqual(fmt.long_name, "QuickTime / MOV") + self.assertIn("mov", fmt.extensions) self.assertFalse(fmt.no_file) def test_stream_segment(self): # This format goes by two names, check both. - fmt = ContainerFormat('stream_segment') + fmt = ContainerFormat("stream_segment") self.assertFalse(fmt.is_input) self.assertTrue(fmt.is_output) - self.assertEqual(fmt.name, 'stream_segment') - self.assertEqual(fmt.long_name, 'streaming segment muxer') + self.assertEqual(fmt.name, "stream_segment") + self.assertEqual(fmt.long_name, "streaming segment muxer") self.assertEqual(fmt.extensions, set()) self.assertTrue(fmt.no_file) - fmt = ContainerFormat('ssegment') + fmt = ContainerFormat("ssegment") self.assertFalse(fmt.is_input) self.assertTrue(fmt.is_output) - self.assertEqual(fmt.name, 'ssegment') - self.assertEqual(fmt.long_name, 'streaming segment muxer') + self.assertEqual(fmt.name, "ssegment") + self.assertEqual(fmt.long_name, "streaming segment muxer") self.assertEqual(fmt.extensions, set()) self.assertTrue(fmt.no_file) diff --git a/tests/test_decode.py b/tests/test_decode.py index 5627f55f2..525577f29 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -4,11 +4,10 @@ class TestDecode(TestCase): - def test_decoded_video_frame_count(self): - container = av.open(fate_suite('h264/interlaced_crop.mp4')) - video_stream = next(s for s in container.streams if s.type == 'video') + container = av.open(fate_suite("h264/interlaced_crop.mp4")) + video_stream = next(s for s in container.streams if s.type == "video") self.assertIs(video_stream, container.streams.video[0]) @@ -22,8 +21,8 @@ def test_decoded_video_frame_count(self): def test_decode_audio_sample_count(self): - container = av.open(fate_suite('audio-reference/chorusnoise_2ch_44kHz_s16.wav')) - audio_stream = next(s for s in container.streams if s.type == 'audio') + container = av.open(fate_suite("audio-reference/chorusnoise_2ch_44kHz_s16.wav")) + audio_stream = next(s for s in container.streams if s.type == "audio") self.assertIs(audio_stream, container.streams.audio[0]) @@ -33,12 +32,14 @@ def test_decode_audio_sample_count(self): for frame in packet.decode(): sample_count += frame.samples - total_samples = (audio_stream.duration * audio_stream.rate.numerator) / audio_stream.time_base.denominator + total_samples = ( + audio_stream.duration * audio_stream.rate.numerator + ) / audio_stream.time_base.denominator self.assertEqual(sample_count, total_samples) def test_decoded_time_base(self): - container = av.open(fate_suite('h264/interlaced_crop.mp4')) + container = av.open(fate_suite("h264/interlaced_crop.mp4")) stream = container.streams.video[0] codec_context = stream.codec_context @@ -52,14 +53,14 @@ def test_decoded_time_base(self): def test_decoded_motion_vectors(self): - container = av.open(fate_suite('h264/interlaced_crop.mp4')) + container = av.open(fate_suite("h264/interlaced_crop.mp4")) stream = container.streams.video[0] codec_context = stream.codec_context codec_context.options = {"flags2": "+export_mvs"} for packet in container.demux(stream): for frame in packet.decode(): - vectors = frame.side_data.get('MOTION_VECTORS') + vectors = frame.side_data.get("MOTION_VECTORS") if frame.key_frame: # Key frame don't have motion vectors assert vectors is None @@ -69,12 +70,12 @@ def test_decoded_motion_vectors(self): def test_decoded_motion_vectors_no_flag(self): - container = av.open(fate_suite('h264/interlaced_crop.mp4')) + container = av.open(fate_suite("h264/interlaced_crop.mp4")) stream = container.streams.video[0] for packet in container.demux(stream): for frame in packet.decode(): - vectors = frame.side_data.get('MOTION_VECTORS') + vectors = frame.side_data.get("MOTION_VECTORS") if not frame.key_frame: assert vectors is None return diff --git a/tests/test_deprecation.py b/tests/test_deprecation.py index 30270146c..abdc79f8e 100644 --- a/tests/test_deprecation.py +++ b/tests/test_deprecation.py @@ -6,11 +6,8 @@ class TestDeprecations(TestCase): - def test_method(self): - class Example(object): - def __init__(self, x=100): self.x = x @@ -22,30 +19,33 @@ def foo(self, a, b): with warnings.catch_warnings(record=True) as captured: self.assertEqual(obj.foo(20, b=3), 123) - self.assertIn('Example.foo is deprecated', captured[0].message.args[0]) + self.assertIn("Example.foo is deprecated", captured[0].message.args[0]) def test_renamed_attr(self): - class Example(object): - new_value = 'foo' - old_value = deprecation.renamed_attr('new_value') + new_value = "foo" + old_value = deprecation.renamed_attr("new_value") def new_func(self, a, b): return a + b - old_func = deprecation.renamed_attr('new_func') + old_func = deprecation.renamed_attr("new_func") obj = Example() with warnings.catch_warnings(record=True) as captured: - self.assertEqual(obj.old_value, 'foo') - self.assertIn('Example.old_value is deprecated', captured[0].message.args[0]) + self.assertEqual(obj.old_value, "foo") + self.assertIn( + "Example.old_value is deprecated", captured[0].message.args[0] + ) - obj.old_value = 'bar' - self.assertIn('Example.old_value is deprecated', captured[1].message.args[0]) + obj.old_value = "bar" + self.assertIn( + "Example.old_value is deprecated", captured[1].message.args[0] + ) with warnings.catch_warnings(record=True) as captured: self.assertEqual(obj.old_func(1, 2), 3) - self.assertIn('Example.old_func is deprecated', captured[0].message.args[0]) + self.assertIn("Example.old_func is deprecated", captured[0].message.args[0]) diff --git a/tests/test_dictionary.py b/tests/test_dictionary.py index e27173e28..4e2c4995e 100644 --- a/tests/test_dictionary.py +++ b/tests/test_dictionary.py @@ -4,17 +4,16 @@ class TestDictionary(TestCase): - def test_basics(self): d = Dictionary() - d['key'] = 'value' + d["key"] = "value" - self.assertEqual(d['key'], 'value') - self.assertIn('key', d) + self.assertEqual(d["key"], "value") + self.assertIn("key", d) self.assertEqual(len(d), 1) - self.assertEqual(list(d), ['key']) + self.assertEqual(list(d), ["key"]) - self.assertEqual(d.pop('key'), 'value') - self.assertRaises(KeyError, d.pop, 'key') + self.assertEqual(d.pop("key"), "value") + self.assertRaises(KeyError, d.pop, "key") self.assertEqual(len(d), 0) diff --git a/tests/test_doctests.py b/tests/test_doctests.py index 34ee969e4..c2144eab1 100644 --- a/tests/test_doctests.py +++ b/tests/test_doctests.py @@ -12,14 +12,16 @@ def fix_doctests(suite): # Add some more flags. case._dt_optionflags = ( - (case._dt_optionflags or 0) | - doctest.IGNORE_EXCEPTION_DETAIL | - doctest.ELLIPSIS | - doctest.NORMALIZE_WHITESPACE + (case._dt_optionflags or 0) + | doctest.IGNORE_EXCEPTION_DETAIL + | doctest.ELLIPSIS + | doctest.NORMALIZE_WHITESPACE ) - case._dt_test.globs['av'] = av - case._dt_test.globs['video_path'] = av.datasets.curated('pexels/time-lapse-video-of-night-sky-857195.mp4') + case._dt_test.globs["av"] = av + case._dt_test.globs["video_path"] = av.datasets.curated( + "pexels/time-lapse-video-of-night-sky-857195.mp4" + ) for example in case._dt_test.examples: @@ -31,7 +33,7 @@ def fix_doctests(suite): def register_doctests(mod): if isinstance(mod, str): - mod = __import__(mod, fromlist=['']) + mod = __import__(mod, fromlist=[""]) try: suite = doctest.DocTestSuite(mod) @@ -40,13 +42,15 @@ def register_doctests(mod): fix_doctests(suite) - cls_name = 'Test' + ''.join(x.title() for x in mod.__name__.split('.')) - cls = type(cls_name, (TestCase, ), {}) + cls_name = "Test" + "".join(x.title() for x in mod.__name__.split(".")) + cls = type(cls_name, (TestCase,), {}) for test in suite._tests: + def func(self): return test.runTest() - name = str('test_' + re.sub('[^a-zA-Z0-9]+', '_', test.id()).strip('_')) + + name = str("test_" + re.sub("[^a-zA-Z0-9]+", "_", test.id()).strip("_")) func.__name__ = name setattr(cls, name, func) @@ -54,8 +58,6 @@ def func(self): for importer, mod_name, ispkg in pkgutil.walk_packages( - path=av.__path__, - prefix=av.__name__ + '.', - onerror=lambda x: None + path=av.__path__, prefix=av.__name__ + ".", onerror=lambda x: None ): register_doctests(mod_name) diff --git a/tests/test_encode.py b/tests/test_encode.py index f4664e5b2..f10ef3b5c 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -22,8 +22,8 @@ def write_rgb_rotate(output): if not Image: raise SkipTest() - output.metadata['title'] = 'container' - output.metadata['key'] = 'value' + output.metadata["title"] = "container" + output.metadata["key"] = "value" stream = output.add_stream("mpeg4", 24) stream.width = WIDTH @@ -32,12 +32,30 @@ def write_rgb_rotate(output): for frame_i in range(DURATION): - frame = VideoFrame(WIDTH, HEIGHT, 'rgb24') - image = Image.new('RGB', (WIDTH, HEIGHT), ( - int(255 * (0.5 + 0.5 * math.sin(frame_i / DURATION * 2 * math.pi))), - int(255 * (0.5 + 0.5 * math.sin(frame_i / DURATION * 2 * math.pi + 2 / 3 * math.pi))), - int(255 * (0.5 + 0.5 * math.sin(frame_i / DURATION * 2 * math.pi + 4 / 3 * math.pi))), - )) + frame = VideoFrame(WIDTH, HEIGHT, "rgb24") + image = Image.new( + "RGB", + (WIDTH, HEIGHT), + ( + int(255 * (0.5 + 0.5 * math.sin(frame_i / DURATION * 2 * math.pi))), + int( + 255 + * ( + 0.5 + + 0.5 + * math.sin(frame_i / DURATION * 2 * math.pi + 2 / 3 * math.pi) + ) + ), + int( + 255 + * ( + 0.5 + + 0.5 + * math.sin(frame_i / DURATION * 2 * math.pi + 4 / 3 * math.pi) + ) + ), + ), + ) frame.planes[0].update(image.tobytes()) for packet in stream.encode(frame): @@ -54,42 +72,43 @@ def assert_rgb_rotate(self, input_): # Now inspect it a little. self.assertEqual(len(input_.streams), 1) - self.assertEqual(input_.metadata.get('title'), 'container', input_.metadata) - self.assertEqual(input_.metadata.get('key'), None) + self.assertEqual(input_.metadata.get("title"), "container", input_.metadata) + self.assertEqual(input_.metadata.get("key"), None) stream = input_.streams[0] self.assertIsInstance(stream, VideoStream) - self.assertEqual(stream.type, 'video') - self.assertEqual(stream.name, 'mpeg4') - self.assertEqual(stream.average_rate, 24) # Only because we constructed is precisely. + self.assertEqual(stream.type, "video") + self.assertEqual(stream.name, "mpeg4") + self.assertEqual( + stream.average_rate, 24 + ) # Only because we constructed is precisely. self.assertEqual(stream.rate, Fraction(24, 1)) self.assertEqual(stream.time_base * stream.duration, 2) - self.assertEqual(stream.format.name, 'yuv420p') + self.assertEqual(stream.format.name, "yuv420p") self.assertEqual(stream.format.width, WIDTH) self.assertEqual(stream.format.height, HEIGHT) class TestBasicVideoEncoding(TestCase): - def test_rgb_rotate(self): - path = self.sandboxed('rgb_rotate.mov') - output = av.open(path, 'w') + path = self.sandboxed("rgb_rotate.mov") + output = av.open(path, "w") write_rgb_rotate(output) assert_rgb_rotate(self, av.open(path)) def test_encoding_with_pts(self): - path = self.sandboxed('video_with_pts.mov') - output = av.open(path, 'w') + path = self.sandboxed("video_with_pts.mov") + output = av.open(path, "w") - stream = output.add_stream('libx264', 24) + stream = output.add_stream("libx264", 24) stream.width = WIDTH stream.height = HEIGHT stream.pix_fmt = "yuv420p" for i in range(DURATION): - frame = VideoFrame(WIDTH, HEIGHT, 'rgb24') + frame = VideoFrame(WIDTH, HEIGHT, "rgb24") frame.pts = i * 2000 frame.time_base = Fraction(1, 48000) @@ -105,20 +124,19 @@ def test_encoding_with_pts(self): class TestBasicAudioEncoding(TestCase): - def test_audio_transcode(self): - path = self.sandboxed('audio_transcode.mov') - output = av.open(path, 'w') - output.metadata['title'] = 'container' - output.metadata['key'] = 'value' + path = self.sandboxed("audio_transcode.mov") + output = av.open(path, "w") + output.metadata["title"] = "container" + output.metadata["key"] = "value" sample_rate = 48000 - channel_layout = 'stereo' + channel_layout = "stereo" channels = 2 - sample_fmt = 's16' + sample_fmt = "s16" - stream = output.add_stream('mp2', sample_rate) + stream = output.add_stream("mp2", sample_rate) ctx = stream.codec_context ctx.time_base = sample_rate @@ -127,7 +145,7 @@ def test_audio_transcode(self): ctx.layout = channel_layout ctx.channels = channels - src = av.open(fate_suite('audio-reference/chorusnoise_2ch_44kHz_s16.wav')) + src = av.open(fate_suite("audio-reference/chorusnoise_2ch_44kHz_s16.wav")) for frame in src.decode(audio=0): frame.pts = None for packet in stream.encode(frame): @@ -140,59 +158,60 @@ def test_audio_transcode(self): container = av.open(path) self.assertEqual(len(container.streams), 1) - self.assertEqual(container.metadata.get('title'), 'container', container.metadata) - self.assertEqual(container.metadata.get('key'), None) + self.assertEqual( + container.metadata.get("title"), "container", container.metadata + ) + self.assertEqual(container.metadata.get("key"), None) stream = container.streams[0] self.assertIsInstance(stream, AudioStream) self.assertEqual(stream.codec_context.sample_rate, sample_rate) - self.assertEqual(stream.codec_context.format.name, 's16p') + self.assertEqual(stream.codec_context.format.name, "s16p") self.assertEqual(stream.codec_context.channels, channels) class TestEncodeStreamSemantics(TestCase): - def test_audio_default_options(self): - output = av.open(self.sandboxed('output.mov'), 'w') + output = av.open(self.sandboxed("output.mov"), "w") - stream = output.add_stream('mp2') + stream = output.add_stream("mp2") self.assertEqual(stream.bit_rate, 128000) - self.assertEqual(stream.format.name, 's16') + self.assertEqual(stream.format.name, "s16") self.assertEqual(stream.rate, 48000) self.assertEqual(stream.ticks_per_frame, 1) self.assertEqual(stream.time_base, None) def test_video_default_options(self): - output = av.open(self.sandboxed('output.mov'), 'w') + output = av.open(self.sandboxed("output.mov"), "w") - stream = output.add_stream('mpeg4') + stream = output.add_stream("mpeg4") self.assertEqual(stream.bit_rate, 1024000) self.assertEqual(stream.format.height, 480) - self.assertEqual(stream.format.name, 'yuv420p') + self.assertEqual(stream.format.name, "yuv420p") self.assertEqual(stream.format.width, 640) self.assertEqual(stream.height, 480) - self.assertEqual(stream.pix_fmt, 'yuv420p') + self.assertEqual(stream.pix_fmt, "yuv420p") self.assertEqual(stream.rate, Fraction(24, 1)) self.assertEqual(stream.ticks_per_frame, 1) self.assertEqual(stream.time_base, None) self.assertEqual(stream.width, 640) def test_stream_index(self): - output = av.open(self.sandboxed('output.mov'), 'w') + output = av.open(self.sandboxed("output.mov"), "w") - vstream = output.add_stream('mpeg4', 24) - vstream.pix_fmt = 'yuv420p' + vstream = output.add_stream("mpeg4", 24) + vstream.pix_fmt = "yuv420p" vstream.width = 320 vstream.height = 240 - astream = output.add_stream('mp2', 48000) + astream = output.add_stream("mp2", 48000) astream.channels = 2 - astream.format = 's16' + astream.format = "s16" self.assertEqual(vstream.index, 0) self.assertEqual(astream.index, 1) - vframe = VideoFrame(320, 240, 'yuv420p') + vframe = VideoFrame(320, 240, "yuv420p") vpacket = vstream.encode(vframe)[0] self.assertIs(vpacket.stream, vstream) @@ -204,7 +223,7 @@ def test_stream_index(self): else: # decoder didn't indicate constant frame size frame_size = 1000 - aframe = AudioFrame('s16', 'stereo', samples=frame_size) + aframe = AudioFrame("s16", "stereo", samples=frame_size) aframe.rate = 48000 apackets = astream.encode(aframe) if apackets: @@ -215,9 +234,9 @@ def test_stream_index(self): self.assertEqual(apacket.stream_index, 1) def test_audio_set_time_base_and_id(self): - output = av.open(self.sandboxed('output.mov'), 'w') + output = av.open(self.sandboxed("output.mov"), "w") - stream = output.add_stream('mp2') + stream = output.add_stream("mp2") self.assertEqual(stream.rate, 48000) self.assertEqual(stream.time_base, None) stream.time_base = Fraction(1, 48000) diff --git a/tests/test_enums.py b/tests/test_enums.py index a45bb44a4..c22e659fb 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -6,16 +6,20 @@ # This must be at the top-level. -PickleableFooBar = define_enum('PickleableFooBar', __name__, [('FOO', 1)]) +PickleableFooBar = define_enum("PickleableFooBar", __name__, [("FOO", 1)]) class TestEnums(TestCase): - def define_foobar(self, **kwargs): - return define_enum('Foobar', __name__, ( - ('FOO', 1), - ('BAR', 2), - ), **kwargs) + return define_enum( + "Foobar", + __name__, + ( + ("FOO", 1), + ("BAR", 2), + ), + **kwargs + ) def test_basics(self): @@ -26,7 +30,7 @@ def test_basics(self): foo = cls.FOO self.assertIsInstance(foo, cls) - self.assertEqual(foo.name, 'FOO') + self.assertEqual(foo.name, "FOO") self.assertEqual(foo.value, 1) self.assertNotIsInstance(foo, PickleableFooBar) @@ -35,7 +39,7 @@ def test_access(self): cls = self.define_foobar() foo1 = cls.FOO - foo2 = cls['FOO'] + foo2 = cls["FOO"] foo3 = cls[1] foo4 = cls[foo1] self.assertIs(foo1, foo2) @@ -43,26 +47,26 @@ def test_access(self): self.assertIs(foo1, foo4) self.assertIn(foo1, cls) - self.assertIn('FOO', cls) + self.assertIn("FOO", cls) self.assertIn(1, cls) - self.assertRaises(KeyError, lambda: cls['not a foo']) + self.assertRaises(KeyError, lambda: cls["not a foo"]) self.assertRaises(KeyError, lambda: cls[10]) self.assertRaises(TypeError, lambda: cls[()]) - self.assertEqual(cls.get('FOO'), foo1) - self.assertIs(cls.get('not a foo'), None) + self.assertEqual(cls.get("FOO"), foo1) + self.assertIs(cls.get("not a foo"), None) def test_casting(self): cls = self.define_foobar() foo = cls.FOO - self.assertEqual(repr(foo), '') + self.assertEqual(repr(foo), "") str_foo = str(foo) self.assertIsInstance(str_foo, str) - self.assertEqual(str_foo, 'FOO') + self.assertEqual(str_foo, "FOO") int_foo = int(foo) self.assertIsInstance(int_foo, int) @@ -78,14 +82,14 @@ def test_equality(self): foo = cls.FOO bar = cls.BAR - self.assertEqual(foo, 'FOO') + self.assertEqual(foo, "FOO") self.assertEqual(foo, 1) self.assertEqual(foo, foo) - self.assertNotEqual(foo, 'BAR') + self.assertNotEqual(foo, "BAR") self.assertNotEqual(foo, 2) self.assertNotEqual(foo, bar) - self.assertRaises(ValueError, lambda: foo == 'not a foo') + self.assertRaises(ValueError, lambda: foo == "not a foo") self.assertRaises(ValueError, lambda: foo == 10) self.assertRaises(TypeError, lambda: foo == ()) @@ -94,9 +98,9 @@ def test_as_key(self): cls = self.define_foobar() foo = cls.FOO - d = {foo: 'value'} - self.assertEqual(d[foo], 'value') - self.assertIs(d.get('FOO'), None) + d = {foo: "value"} + self.assertEqual(d[foo], "value") + self.assertIs(d.get("FOO"), None) self.assertIs(d.get(1), None) def test_pickleable(self): @@ -115,32 +119,41 @@ def test_create_unknown(self): cls = self.define_foobar() baz = cls.get(3, create=True) - self.assertEqual(baz.name, 'FOOBAR_3') + self.assertEqual(baz.name, "FOOBAR_3") self.assertEqual(baz.value, 3) def test_multiple_names(self): - cls = define_enum('FFooBBar', __name__, ( - ('FOO', 1), - ('F', 1), - ('BAR', 2), - ('B', 2), - )) + cls = define_enum( + "FFooBBar", + __name__, + ( + ("FOO", 1), + ("F", 1), + ("BAR", 2), + ("B", 2), + ), + ) self.assertIs(cls.F, cls.FOO) - self.assertEqual(cls.F.name, 'FOO') - self.assertNotEqual(cls.F.name, 'F') # This is actually the string. + self.assertEqual(cls.F.name, "FOO") + self.assertNotEqual(cls.F.name, "F") # This is actually the string. - self.assertEqual(cls.F, 'FOO') - self.assertEqual(cls.F, 'F') - self.assertNotEqual(cls.F, 'BAR') - self.assertNotEqual(cls.F, 'B') - self.assertRaises(ValueError, lambda: cls.F == 'x') + self.assertEqual(cls.F, "FOO") + self.assertEqual(cls.F, "F") + self.assertNotEqual(cls.F, "BAR") + self.assertNotEqual(cls.F, "B") + self.assertRaises(ValueError, lambda: cls.F == "x") def test_flag_basics(self): - cls = define_enum('FoobarAllFlags', __name__, dict(FOO=1, BAR=2, FOOBAR=3).items(), is_flags=True) + cls = define_enum( + "FoobarAllFlags", + __name__, + dict(FOO=1, BAR=2, FOOBAR=3).items(), + is_flags=True, + ) foo = cls.FOO bar = cls.BAR @@ -171,7 +184,7 @@ def test_multi_flags_basics(self): foo = cls.FOO bar = cls.BAR foobar = foo | bar - self.assertEqual(foobar.name, 'FOO|BAR') + self.assertEqual(foobar.name, "FOO|BAR") self.assertEqual(foobar.value, 3) self.assertEqual(foobar.flags, (foo, bar)) @@ -183,7 +196,7 @@ def test_multi_flags_basics(self): self.assertIs(foobar, foobar3) self.assertIs(foobar, foobar4) - self.assertRaises(KeyError, lambda: cls['FOO|BAR']) + self.assertRaises(KeyError, lambda: cls["FOO|BAR"]) self.assertEqual(len(cls), 2) # It didn't get bigger self.assertEqual(list(cls), [foo, bar]) @@ -204,7 +217,6 @@ def test_properties(self): foobar = Flags.FOO | Flags.BAR class Class(object): - def __init__(self, value): self.value = Flags[value].value @@ -216,10 +228,10 @@ def flags(self): def flags(self, value): self.value = value - foo = flags.flag_property('FOO') - bar = flags.flag_property('BAR') + foo = flags.flag_property("FOO") + bar = flags.flag_property("BAR") - obj = Class('FOO') + obj = Class("FOO") self.assertIs(obj.flags, Flags.FOO) self.assertTrue(obj.foo) diff --git a/tests/test_errors.py b/tests/test_errors.py index 924fdbed0..55d969999 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -7,29 +7,28 @@ class TestErrorBasics(TestCase): - def test_stringify(self): for cls in (av.ValueError, av.FileNotFoundError, av.DecoderNotFoundError): - e = cls(1, 'foo') - self.assertEqual(str(e), '[Errno 1] foo') + e = cls(1, "foo") + self.assertEqual(str(e), "[Errno 1] foo") self.assertEqual(repr(e), "{}(1, 'foo')".format(cls.__name__)) self.assertEqual( traceback.format_exception_only(cls, e)[-1], - '{}{}: [Errno 1] foo\n'.format( - 'av.error.', + "{}{}: [Errno 1] foo\n".format( + "av.error.", cls.__name__, ), ) for cls in (av.ValueError, av.FileNotFoundError, av.DecoderNotFoundError): - e = cls(1, 'foo', 'bar.txt') + e = cls(1, "foo", "bar.txt") self.assertEqual(str(e), "[Errno 1] foo: 'bar.txt'") self.assertEqual(repr(e), "{}(1, 'foo', 'bar.txt')".format(cls.__name__)) self.assertEqual( traceback.format_exception_only(cls, e)[-1], "{}{}: [Errno 1] foo: 'bar.txt'\n".format( - 'av.error.', + "av.error.", cls.__name__, ), ) @@ -46,17 +45,19 @@ def test_bases(self): def test_filenotfound(self): """Catch using builtin class on Python 3.3""" try: - av.open('does not exist') + av.open("does not exist") except FileNotFoundError as e: self.assertEqual(e.errno, errno.ENOENT) if is_windows: - self.assertTrue(e.strerror in ['Error number -2 occurred', - 'No such file or directory']) + self.assertTrue( + e.strerror + in ["Error number -2 occurred", "No such file or directory"] + ) else: - self.assertEqual(e.strerror, 'No such file or directory') - self.assertEqual(e.filename, 'does not exist') + self.assertEqual(e.strerror, "No such file or directory") + self.assertEqual(e.filename, "does not exist") else: - self.fail('no exception raised') + self.fail("no exception raised") def test_buffertoosmall(self): """Throw an exception from an enum.""" @@ -65,4 +66,4 @@ def test_buffertoosmall(self): except av.BufferTooSmallError as e: self.assertEqual(e.errno, av.error.BUFFER_TOO_SMALL.value) else: - self.fail('no exception raised') + self.fail("no exception raised") diff --git a/tests/test_file_probing.py b/tests/test_file_probing.py index e40a1c55f..a6582ac47 100644 --- a/tests/test_file_probing.py +++ b/tests/test_file_probing.py @@ -15,12 +15,14 @@ class TestAudioProbe(TestCase): def setUp(self): - self.file = av.open(fate_suite('aac/latm_stereo_to_51.ts')) + self.file = av.open(fate_suite("aac/latm_stereo_to_51.ts")) def test_container_probing(self): self.assertEqual(str(self.file.format), "") - self.assertEqual(self.file.format.name, 'mpegts') - self.assertEqual(self.file.format.long_name, "MPEG-TS (MPEG-2 Transport Stream)") + self.assertEqual(self.file.format.name, "mpegts") + self.assertEqual( + self.file.format.long_name, "MPEG-TS (MPEG-2 Transport Stream)" + ) self.assertEqual(self.file.size, 207740) # This is a little odd, but on OS X with FFmpeg we get a different value. @@ -41,61 +43,79 @@ def test_stream_probing(self): self.assertEqual(stream.frames, 0) self.assertEqual(stream.id, 256) self.assertEqual(stream.index, 0) - self.assertEqual(stream.language, 'eng') - self.assertEqual(stream.metadata, { - 'language': 'eng', - }) - self.assertEqual(stream.profile, 'LC') + self.assertEqual(stream.language, "eng") + self.assertEqual( + stream.metadata, + { + "language": "eng", + }, + ) + self.assertEqual(stream.profile, "LC") self.assertEqual(stream.start_time, 126000) self.assertEqual(stream.time_base, Fraction(1, 90000)) - self.assertEqual(stream.type, 'audio') + self.assertEqual(stream.type, "audio") # codec properties - self.assertEqual(stream.name, 'aac_latm') - self.assertEqual(stream.long_name, 'AAC LATM (Advanced Audio Coding LATM syntax)') + self.assertEqual(stream.name, "aac_latm") + self.assertEqual( + stream.long_name, "AAC LATM (Advanced Audio Coding LATM syntax)" + ) # codec context properties self.assertEqual(stream.bit_rate, None) self.assertEqual(stream.channels, 2) self.assertEqual(stream.format.bits, 32) - self.assertEqual(stream.format.name, 'fltp') - self.assertEqual(stream.layout.name, 'stereo') + self.assertEqual(stream.format.name, "fltp") + self.assertEqual(stream.layout.name, "stereo") self.assertEqual(stream.max_bit_rate, None) self.assertEqual(stream.rate, 48000) class TestDataProbe(TestCase): - def setUp(self): - self.file = av.open(fate_suite('mxf/track_01_v02.mxf')) + self.file = av.open(fate_suite("mxf/track_01_v02.mxf")) def test_container_probing(self): self.assertEqual(str(self.file.format), "") - self.assertEqual(self.file.format.name, 'mxf') - self.assertEqual(self.file.format.long_name, 'MXF (Material eXchange Format)') + self.assertEqual(self.file.format.name, "mxf") + self.assertEqual(self.file.format.long_name, "MXF (Material eXchange Format)") self.assertEqual(self.file.size, 1453153) - self.assertEqual(self.file.bit_rate, 8 * self.file.size * av.time_base // self.file.duration) + self.assertEqual( + self.file.bit_rate, 8 * self.file.size * av.time_base // self.file.duration + ) self.assertEqual(self.file.duration, 417083) self.assertEqual(len(self.file.streams), 4) for key, value, min_version in ( - ('application_platform', 'AAFSDK (MacOS X)', None), - ('comment_Comments', 'example comment', None), - ('comment_UNC Path', '/Users/mark/Desktop/dnxhr_tracknames_export.aaf', None), - ('company_name', 'Avid Technology, Inc.', None), - ('generation_uid', 'b6bcfcab-70ff-7331-c592-233869de11d2', None), - ('material_package_name', 'Example.new.04', None), - ('material_package_umid', '0x060A2B340101010101010F001300000057E19D16BA8202DB060E2B347F7F2A80', None), - ('modification_date', '2016-09-20T20:33:26.000000Z', None), + ("application_platform", "AAFSDK (MacOS X)", None), + ("comment_Comments", "example comment", None), + ( + "comment_UNC Path", + "/Users/mark/Desktop/dnxhr_tracknames_export.aaf", + None, + ), + ("company_name", "Avid Technology, Inc.", None), + ("generation_uid", "b6bcfcab-70ff-7331-c592-233869de11d2", None), + ("material_package_name", "Example.new.04", None), + ( + "material_package_umid", + "0x060A2B340101010101010F001300000057E19D16BA8202DB060E2B347F7F2A80", + None, + ), + ("modification_date", "2016-09-20T20:33:26.000000Z", None), # Next one is FFmpeg >= 4.2. - ('operational_pattern_ul', '060e2b34.04010102.0d010201.10030000', {'libavformat': (58, 29)}), - ('product_name', 'Avid Media Composer 8.6.3.43955', None), - ('product_uid', 'acfbf03a-4f42-a231-d0b7-c06ecd3d4ad7', None), - ('product_version', 'Unknown version', None), - ('project_name', 'UHD', None), - ('uid', '4482d537-4203-ea40-9e4e-08a22900dd39', None), + ( + "operational_pattern_ul", + "060e2b34.04010102.0d010201.10030000", + {"libavformat": (58, 29)}, + ), + ("product_name", "Avid Media Composer 8.6.3.43955", None), + ("product_uid", "acfbf03a-4f42-a231-d0b7-c06ecd3d4ad7", None), + ("product_version", "Unknown version", None), + ("project_name", "UHD", None), + ("uid", "4482d537-4203-ea40-9e4e-08a22900dd39", None), ): if min_version and any( av.library_versions[name] < version @@ -116,15 +136,18 @@ def test_stream_probing(self): self.assertEqual(stream.id, 1) self.assertEqual(stream.index, 0) self.assertEqual(stream.language, None) - self.assertEqual(stream.metadata, { - 'data_type': 'video', - 'file_package_umid': '0x060A2B340101010101010F001300000057E19D16BA8302DB060E2B347F7F2A80', - 'track_name': 'Base', - }) + self.assertEqual( + stream.metadata, + { + "data_type": "video", + "file_package_umid": "0x060A2B340101010101010F001300000057E19D16BA8302DB060E2B347F7F2A80", + "track_name": "Base", + }, + ) self.assertEqual(stream.profile, None) self.assertEqual(stream.start_time, 0) self.assertEqual(stream.time_base, Fraction(1, 90000)) - self.assertEqual(stream.type, 'data') + self.assertEqual(stream.type, "data") # codec properties self.assertEqual(stream.name, None) @@ -133,23 +156,30 @@ def test_stream_probing(self): class TestSubtitleProbe(TestCase): def setUp(self): - self.file = av.open(fate_suite('sub/MovText_capability_tester.mp4')) + self.file = av.open(fate_suite("sub/MovText_capability_tester.mp4")) def test_container_probing(self): - self.assertEqual(str(self.file.format), "") - self.assertEqual(self.file.format.name, 'mov,mp4,m4a,3gp,3g2,mj2') - self.assertEqual(self.file.format.long_name, 'QuickTime / MOV') + self.assertEqual( + str(self.file.format), "" + ) + self.assertEqual(self.file.format.name, "mov,mp4,m4a,3gp,3g2,mj2") + self.assertEqual(self.file.format.long_name, "QuickTime / MOV") self.assertEqual(self.file.size, 825) - self.assertEqual(self.file.bit_rate, 8 * self.file.size * av.time_base // self.file.duration) + self.assertEqual( + self.file.bit_rate, 8 * self.file.size * av.time_base // self.file.duration + ) self.assertEqual(self.file.duration, 8140000) self.assertEqual(len(self.file.streams), 1) - self.assertEqual(self.file.metadata, { - 'compatible_brands': 'isom', - 'creation_time': '2012-07-04T05:10:41.000000Z', - 'major_brand': 'isom', - 'minor_version': '1', - }) + self.assertEqual( + self.file.metadata, + { + "compatible_brands": "isom", + "creation_time": "2012-07-04T05:10:41.000000Z", + "major_brand": "isom", + "minor_version": "1", + }, + ) def test_stream_probing(self): stream = self.file.streams[0] @@ -160,36 +190,43 @@ def test_stream_probing(self): self.assertEqual(stream.frames, 6) self.assertEqual(stream.id, 1) self.assertEqual(stream.index, 0) - self.assertEqual(stream.language, 'und') - self.assertEqual(stream.metadata, { - 'creation_time': '2012-07-04T05:10:41.000000Z', - 'handler_name': 'reference.srt - Imported with GPAC 0.4.6-DEV-rev4019', - 'language': 'und' - }) + self.assertEqual(stream.language, "und") + self.assertEqual( + stream.metadata, + { + "creation_time": "2012-07-04T05:10:41.000000Z", + "handler_name": "reference.srt - Imported with GPAC 0.4.6-DEV-rev4019", + "language": "und", + }, + ) self.assertEqual(stream.profile, None) self.assertEqual(stream.start_time, None) self.assertEqual(stream.time_base, Fraction(1, 1000)) - self.assertEqual(stream.type, 'subtitle') + self.assertEqual(stream.type, "subtitle") # codec properties - self.assertEqual(stream.name, 'mov_text') - self.assertEqual(stream.long_name, '3GPP Timed Text subtitle') + self.assertEqual(stream.name, "mov_text") + self.assertEqual(stream.long_name, "3GPP Timed Text subtitle") class TestVideoProbe(TestCase): def setUp(self): - self.file = av.open(fate_suite('mpeg2/mpeg2_field_encoding.ts')) + self.file = av.open(fate_suite("mpeg2/mpeg2_field_encoding.ts")) def test_container_probing(self): self.assertEqual(str(self.file.format), "") - self.assertEqual(self.file.format.name, 'mpegts') - self.assertEqual(self.file.format.long_name, "MPEG-TS (MPEG-2 Transport Stream)") + self.assertEqual(self.file.format.name, "mpegts") + self.assertEqual( + self.file.format.long_name, "MPEG-TS (MPEG-2 Transport Stream)" + ) self.assertEqual(self.file.size, 800000) # This is a little odd, but on OS X with FFmpeg we get a different value. self.assertIn(self.file.duration, (1620000, 1580000)) - self.assertEqual(self.file.bit_rate, 8 * self.file.size * av.time_base // self.file.duration) + self.assertEqual( + self.file.bit_rate, 8 * self.file.size * av.time_base // self.file.duration + ) self.assertEqual(len(self.file.streams), 1) self.assertEqual(self.file.start_time, long(22953408322)) self.assertEqual(self.file.metadata, {}) @@ -205,19 +242,19 @@ def test_stream_probing(self): self.assertEqual(stream.index, 0) self.assertEqual(stream.language, None) self.assertEqual(stream.metadata, {}) - self.assertEqual(stream.profile, 'Simple') + self.assertEqual(stream.profile, "Simple") self.assertEqual(stream.start_time, 2065806749) self.assertEqual(stream.time_base, Fraction(1, 90000)) - self.assertEqual(stream.type, 'video') + self.assertEqual(stream.type, "video") # codec properties - self.assertEqual(stream.long_name, 'MPEG-2 video') - self.assertEqual(stream.name, 'mpeg2video') + self.assertEqual(stream.long_name, "MPEG-2 video") + self.assertEqual(stream.name, "mpeg2video") # codec context properties self.assertEqual(stream.bit_rate, 3364800) self.assertEqual(stream.display_aspect_ratio, Fraction(4, 3)) - self.assertEqual(stream.format.name, 'yuv420p') + self.assertEqual(stream.format.name, "yuv420p") self.assertFalse(stream.has_b_frames) self.assertEqual(stream.gop_size, 12) self.assertEqual(stream.height, 576) diff --git a/tests/test_filters.py b/tests/test_filters.py index 659ffc708..2f3d6985f 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -12,7 +12,9 @@ from .common import Image, TestCase, fate_suite -def generate_audio_frame(frame_num, input_format='s16', layout='stereo', sample_rate=44100, frame_size=1024): +def generate_audio_frame( + frame_num, input_format="s16", layout="stereo", sample_rate=44100, frame_size=1024 +): """ Generate audio frame representing part of the sinusoidal wave :param input_format: default: s16 @@ -47,22 +49,21 @@ def pull_until_blocked(graph): class TestFilters(TestCase): - def test_filter_descriptor(self): - f = Filter('testsrc') - self.assertEqual(f.name, 'testsrc') - self.assertEqual(f.description, 'Generate test pattern.') + f = Filter("testsrc") + self.assertEqual(f.name, "testsrc") + self.assertEqual(f.description, "Generate test pattern.") self.assertFalse(f.dynamic_inputs) self.assertEqual(len(f.inputs), 0) self.assertFalse(f.dynamic_outputs) self.assertEqual(len(f.outputs), 1) - self.assertEqual(f.outputs[0].name, 'default') - self.assertEqual(f.outputs[0].type, 'video') + self.assertEqual(f.outputs[0].name, "default") + self.assertEqual(f.outputs[0].type, "video") def test_dynamic_filter_descriptor(self): - f = Filter('split') + f = Filter("split") self.assertFalse(f.dynamic_inputs) self.assertEqual(len(f.inputs), 1) self.assertTrue(f.dynamic_outputs) @@ -71,9 +72,13 @@ def test_dynamic_filter_descriptor(self): def test_generator_graph(self): graph = Graph() - src = graph.add('testsrc') - lutrgb = graph.add('lutrgb', "r=maxval+minval-val:g=maxval+minval-val:b=maxval+minval-val", name='invert') - sink = graph.add('buffersink') + src = graph.add("testsrc") + lutrgb = graph.add( + "lutrgb", + "r=maxval+minval-val:g=maxval+minval-val:b=maxval+minval-val", + name="invert", + ) + sink = graph.add("buffersink") src.link_to(lutrgb) lutrgb.link_to(sink) @@ -85,31 +90,31 @@ def test_generator_graph(self): self.assertIsInstance(frame, VideoFrame) if Image: - frame.to_image().save(self.sandboxed('mandelbrot2.png')) + frame.to_image().save(self.sandboxed("mandelbrot2.png")) def test_auto_find_sink(self): graph = Graph() - src = graph.add('testsrc') - src.link_to(graph.add('buffersink')) + src = graph.add("testsrc") + src.link_to(graph.add("buffersink")) graph.configure() frame = graph.pull() if Image: - frame.to_image().save(self.sandboxed('mandelbrot3.png')) + frame.to_image().save(self.sandboxed("mandelbrot3.png")) def test_delegate_sink(self): graph = Graph() - src = graph.add('testsrc') - src.link_to(graph.add('buffersink')) + src = graph.add("testsrc") + src.link_to(graph.add("buffersink")) graph.configure() frame = src.pull() if Image: - frame.to_image().save(self.sandboxed('mandelbrot4.png')) + frame.to_image().save(self.sandboxed("mandelbrot4.png")) def test_haldclut_graph(self): @@ -117,17 +122,17 @@ def test_haldclut_graph(self): graph = Graph() - img = Image.open(fate_suite('png1/lena-rgb24.png')) + img = Image.open(fate_suite("png1/lena-rgb24.png")) frame = VideoFrame.from_image(img) img_source = graph.add_buffer(frame) - hald_img = Image.open('hald_7.png') + hald_img = Image.open("hald_7.png") hald_frame = VideoFrame.from_image(hald_img) hald_source = graph.add_buffer(hald_frame) - hald_filter = graph.add('haldclut') + hald_filter = graph.add("haldclut") - sink = graph.add('buffersink') + sink = graph.add("buffersink") img_source.link(0, hald_filter, 0) hald_source.link(0, hald_filter, 1) @@ -144,17 +149,17 @@ def test_haldclut_graph(self): frame = sink.pull() self.assertIsInstance(frame, VideoFrame) - frame.to_image().save(self.sandboxed('filtered.png')) + frame.to_image().save(self.sandboxed("filtered.png")) def test_audio_buffer_sink(self): graph = Graph() audio_buffer = graph.add_abuffer( - format='fltp', + format="fltp", sample_rate=48000, - layout='stereo', - time_base=Fraction(1, 48000) + layout="stereo", + time_base=Fraction(1, 48000), ) - audio_buffer.link_to(graph.add('abuffersink')) + audio_buffer.link_to(graph.add("abuffersink")) graph.configure() try: @@ -173,58 +178,58 @@ def test_audio_buffer_resample(self): graph = Graph() self.link_nodes( graph.add_abuffer( - format='fltp', + format="fltp", sample_rate=48000, - layout='stereo', - time_base=Fraction(1, 48000) + layout="stereo", + time_base=Fraction(1, 48000), ), graph.add( - 'aformat', - 'sample_fmts=s16:sample_rates=44100:channel_layouts=stereo' + "aformat", "sample_fmts=s16:sample_rates=44100:channel_layouts=stereo" ), - graph.add('abuffersink') + graph.add("abuffersink"), ) graph.configure() graph.push( generate_audio_frame( - 0, - input_format='fltp', - layout='stereo', - sample_rate=48000 + 0, input_format="fltp", layout="stereo", sample_rate=48000 ) ) out_frame = graph.pull() - self.assertEqual(out_frame.format.name, 's16') - self.assertEqual(out_frame.layout.name, 'stereo') + self.assertEqual(out_frame.format.name, "s16") + self.assertEqual(out_frame.layout.name, "stereo") self.assertEqual(out_frame.sample_rate, 44100) def test_audio_buffer_volume_filter(self): graph = Graph() self.link_nodes( graph.add_abuffer( - format='fltp', + format="fltp", sample_rate=48000, - layout='stereo', - time_base=Fraction(1, 48000) + layout="stereo", + time_base=Fraction(1, 48000), ), - graph.add('volume', volume='0.5'), - graph.add('abuffersink') + graph.add("volume", volume="0.5"), + graph.add("abuffersink"), ) graph.configure() - input_frame = generate_audio_frame(0, input_format='fltp', layout='stereo', sample_rate=48000) + input_frame = generate_audio_frame( + 0, input_format="fltp", layout="stereo", sample_rate=48000 + ) graph.push(input_frame) out_frame = graph.pull() - self.assertEqual(out_frame.format.name, 'fltp') - self.assertEqual(out_frame.layout.name, 'stereo') + self.assertEqual(out_frame.format.name, "fltp") + self.assertEqual(out_frame.layout.name, "stereo") self.assertEqual(out_frame.sample_rate, 48000) input_data = input_frame.to_ndarray() output_data = out_frame.to_ndarray() - self.assertTrue(np.allclose(input_data * 0.5, output_data), "Check that volume is reduced") + self.assertTrue( + np.allclose(input_data * 0.5, output_data), "Check that volume is reduced" + ) def test_video_buffer(self): input_container = av.open(format="lavfi", file="color=c=pink:duration=1:r=30") diff --git a/tests/test_logging.py b/tests/test_logging.py index 1747f40ee..839d60ae5 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -11,70 +11,65 @@ def do_log(message): - av.logging.log(av.logging.INFO, 'test', message) + av.logging.log(av.logging.INFO, "test", message) class TestLogging(TestCase): - def test_adapt_level(self): - self.assertEqual( - av.logging.adapt_level(av.logging.ERROR), - logging.ERROR - ) - self.assertEqual( - av.logging.adapt_level(av.logging.WARNING), - logging.WARNING - ) + self.assertEqual(av.logging.adapt_level(av.logging.ERROR), logging.ERROR) + self.assertEqual(av.logging.adapt_level(av.logging.WARNING), logging.WARNING) self.assertEqual( av.logging.adapt_level((av.logging.WARNING + av.logging.ERROR) // 2), - logging.WARNING + logging.WARNING, ) def test_threaded_captures(self): with av.logging.Capture(local=True) as logs: - do_log('main') - thread = threading.Thread(target=do_log, args=('thread', )) + do_log("main") + thread = threading.Thread(target=do_log, args=("thread",)) thread.start() thread.join() - self.assertIn((av.logging.INFO, 'test', 'main'), logs) + self.assertIn((av.logging.INFO, "test", "main"), logs) def test_global_captures(self): with av.logging.Capture(local=False) as logs: - do_log('main') - thread = threading.Thread(target=do_log, args=('thread', )) + do_log("main") + thread = threading.Thread(target=do_log, args=("thread",)) thread.start() thread.join() - self.assertIn((av.logging.INFO, 'test', 'main'), logs) - self.assertIn((av.logging.INFO, 'test', 'thread'), logs) + self.assertIn((av.logging.INFO, "test", "main"), logs) + self.assertIn((av.logging.INFO, "test", "thread"), logs) def test_repeats(self): with av.logging.Capture() as logs: - do_log('foo') - do_log('foo') - do_log('bar') - do_log('bar') - do_log('bar') - do_log('baz') - - logs = [log for log in logs if log[1] == 'test'] + do_log("foo") + do_log("foo") + do_log("bar") + do_log("bar") + do_log("bar") + do_log("baz") - self.assertEqual(logs, [ - (av.logging.INFO, 'test', 'foo'), - (av.logging.INFO, 'test', 'foo'), - (av.logging.INFO, 'test', 'bar'), - (av.logging.INFO, 'test', 'bar (repeated 2 more times)'), - (av.logging.INFO, 'test', 'baz'), + logs = [log for log in logs if log[1] == "test"] - ]) + self.assertEqual( + logs, + [ + (av.logging.INFO, "test", "foo"), + (av.logging.INFO, "test", "foo"), + (av.logging.INFO, "test", "bar"), + (av.logging.INFO, "test", "bar (repeated 2 more times)"), + (av.logging.INFO, "test", "baz"), + ], + ) def test_error(self): - log = (av.logging.ERROR, 'test', 'This is a test.') + log = (av.logging.ERROR, "test", "This is a test.") av.logging.log(*log) try: av.error.err_check(-errno.EPERM) diff --git a/tests/test_options.py b/tests/test_options.py index ff161e8a1..cf76252d9 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -5,17 +5,16 @@ class TestOptions(TestCase): - def test_mov_options(self): - mov = ContainerFormat('mov') + mov = ContainerFormat("mov") options = mov.descriptor.options by_name = {opt.name: opt for opt in options} - opt = by_name.get('use_absolute_path') + opt = by_name.get("use_absolute_path") self.assertIsInstance(opt, Option) - self.assertEqual(opt.name, 'use_absolute_path') + self.assertEqual(opt.name, "use_absolute_path") # This was not a good option to actually test. self.assertIn(opt.type, (OptionType.BOOL, OptionType.INT)) diff --git a/tests/test_python_io.py b/tests/test_python_io.py index f622bdb65..71530f622 100644 --- a/tests/test_python_io.py +++ b/tests/test_python_io.py @@ -23,26 +23,27 @@ def read(self, n): class TestPythonIO(TestCase): - def test_reading(self): - with open(fate_suite('mpeg2/mpeg2_field_encoding.ts'), 'rb') as fh: + with open(fate_suite("mpeg2/mpeg2_field_encoding.ts"), "rb") as fh: wrapped = MethodLogger(fh) container = av.open(wrapped) - self.assertEqual(container.format.name, 'mpegts') - self.assertEqual(container.format.long_name, "MPEG-TS (MPEG-2 Transport Stream)") + self.assertEqual(container.format.name, "mpegts") + self.assertEqual( + container.format.long_name, "MPEG-TS (MPEG-2 Transport Stream)" + ) self.assertEqual(len(container.streams), 1) self.assertEqual(container.size, 800000) self.assertEqual(container.metadata, {}) # Make sure it did actually call "read". - reads = wrapped._filter('read') + reads = wrapped._filter("read") self.assertTrue(reads) def test_reading_no_seek(self): - with open(fate_suite('mpeg2/mpeg2_field_encoding.ts'), 'rb') as fh: + with open(fate_suite("mpeg2/mpeg2_field_encoding.ts"), "rb") as fh: data = fh.read() buf = NonSeekableBuffer(data) @@ -50,32 +51,34 @@ def test_reading_no_seek(self): container = av.open(wrapped) - self.assertEqual(container.format.name, 'mpegts') - self.assertEqual(container.format.long_name, "MPEG-TS (MPEG-2 Transport Stream)") + self.assertEqual(container.format.name, "mpegts") + self.assertEqual( + container.format.long_name, "MPEG-TS (MPEG-2 Transport Stream)" + ) self.assertEqual(len(container.streams), 1) self.assertEqual(container.metadata, {}) # Make sure it did actually call "read". - reads = wrapped._filter('read') + reads = wrapped._filter("read") self.assertTrue(reads) def test_basic_errors(self): self.assertRaises(Exception, av.open, None) - self.assertRaises(Exception, av.open, None, 'w') + self.assertRaises(Exception, av.open, None, "w") def test_writing(self): - path = self.sandboxed('writing.mov') - with open(path, 'wb') as fh: + path = self.sandboxed("writing.mov") + with open(path, "wb") as fh: wrapped = MethodLogger(fh) - output = av.open(wrapped, 'w', 'mov') + output = av.open(wrapped, "w", "mov") write_rgb_rotate(output) output.close() fh.close() # Make sure it did actually write. - writes = wrapped._filter('write') + writes = wrapped._filter("write") self.assertTrue(writes) # Standard assertions. @@ -85,10 +88,10 @@ def test_buffer_read_write(self): buffer_ = StringIO() wrapped = MethodLogger(buffer_) - write_rgb_rotate(av.open(wrapped, 'w', 'mp4')) + write_rgb_rotate(av.open(wrapped, "w", "mp4")) # Make sure it did actually write. - writes = wrapped._filter('write') + writes = wrapped._filter("write") self.assertTrue(writes) self.assertTrue(buffer_.tell()) diff --git a/tests/test_seek.py b/tests/test_seek.py index e482906df..559e93b0b 100644 --- a/tests/test_seek.py +++ b/tests/test_seek.py @@ -24,20 +24,19 @@ def step_forward(container, stream): class TestSeek(TestCase): - def test_seek_float(self): - container = av.open(fate_suite('h264/interlaced_crop.mp4')) + container = av.open(fate_suite("h264/interlaced_crop.mp4")) self.assertRaises(TypeError, container.seek, 1.0) self.assertRaises(TypeError, container.streams.video[0].seek, 1.0) def test_seek_int64(self): # Assert that it accepts large values. # Issue 251 pointed this out. - container = av.open(fate_suite('h264/interlaced_crop.mp4')) + container = av.open(fate_suite("h264/interlaced_crop.mp4")) container.seek(2**32) def test_seek_start(self): - container = av.open(fate_suite('h264/interlaced_crop.mp4')) + container = av.open(fate_suite("h264/interlaced_crop.mp4")) # count all the packets total_packet_count = 0 @@ -55,7 +54,7 @@ def test_seek_start(self): self.assertEqual(total_packet_count, seek_packet_count) def test_seek_middle(self): - container = av.open(fate_suite('h264/interlaced_crop.mp4')) + container = av.open(fate_suite("h264/interlaced_crop.mp4")) # count all the packets total_packet_count = 0 @@ -72,7 +71,7 @@ def test_seek_middle(self): self.assertTrue(seek_packet_count < total_packet_count) def test_seek_end(self): - container = av.open(fate_suite('h264/interlaced_crop.mp4')) + container = av.open(fate_suite("h264/interlaced_crop.mp4")) # seek to middle container.seek(container.duration // 2) @@ -94,9 +93,9 @@ def test_seek_end(self): def test_decode_half(self): - container = av.open(fate_suite('h264/interlaced_crop.mp4')) + container = av.open(fate_suite("h264/interlaced_crop.mp4")) - video_stream = next(s for s in container.streams if s.type == 'video') + video_stream = next(s for s in container.streams if s.type == "video") total_frame_count = 0 # Count number of frames in video @@ -131,9 +130,9 @@ def test_decode_half(self): def test_stream_seek(self, use_deprecated_api=False): - container = av.open(fate_suite('h264/interlaced_crop.mp4')) + container = av.open(fate_suite("h264/interlaced_crop.mp4")) - video_stream = next(s for s in container.streams if s.type == 'video') + video_stream = next(s for s in container.streams if s.type == "video") total_frame_count = 0 # Count number of frames in video @@ -153,7 +152,7 @@ def test_stream_seek(self, use_deprecated_api=False): with warnings.catch_warnings(record=True) as captured: video_stream.seek(target_timestamp) self.assertEqual(len(captured), 1) - self.assertIn('Stream.seek is deprecated.', captured[0].message.args[0]) + self.assertIn("Stream.seek is deprecated.", captured[0].message.args[0]) else: container.seek(target_timestamp, stream=video_stream) diff --git a/tests/test_streams.py b/tests/test_streams.py index c108a8447..beab831ba 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -4,27 +4,26 @@ class TestStreams(TestCase): - def test_stream_tuples(self): - for fate_name in ('h264/interlaced_crop.mp4', ): + for fate_name in ("h264/interlaced_crop.mp4",): container = av.open(fate_suite(fate_name)) - video_streams = tuple([s for s in container.streams if s.type == 'video']) + video_streams = tuple([s for s in container.streams if s.type == "video"]) self.assertEqual(video_streams, container.streams.video) - audio_streams = tuple([s for s in container.streams if s.type == 'audio']) + audio_streams = tuple([s for s in container.streams if s.type == "audio"]) self.assertEqual(audio_streams, container.streams.audio) def test_selection(self): - container = av.open(fate_suite('h264/interlaced_crop.mp4')) + container = av.open(fate_suite("h264/interlaced_crop.mp4")) video = container.streams.video[0] # audio_stream = container.streams.audio[0] # audio_streams = list(container.streams.audio[0:2]) self.assertEqual([video], container.streams.get(video=0)) - self.assertEqual([video], container.streams.get(video=(0, ))) + self.assertEqual([video], container.streams.get(video=(0,))) # TODO: Find something in the fate suite with video, audio, and subtitles. diff --git a/tests/test_subtitles.py b/tests/test_subtitles.py index 5f7352430..5f8f8cf41 100644 --- a/tests/test_subtitles.py +++ b/tests/test_subtitles.py @@ -5,10 +5,9 @@ class TestSubtitle(TestCase): - def test_movtext(self): - path = fate_suite('sub/MovText_capability_tester.mp4') + path = fate_suite("sub/MovText_capability_tester.mp4") fh = av.open(path) subs = [] @@ -19,12 +18,17 @@ def test_movtext(self): self.assertIsInstance(subs[0][0], AssSubtitle) # The format FFmpeg gives us changed at one point. - self.assertIn(subs[0][0].ass, ('Dialogue: 0,0:00:00.97,0:00:02.54,Default,- Test 1.\\N- Test 2.\r\n', - 'Dialogue: 0,0:00:00.97,0:00:02.54,Default,,0,0,0,,- Test 1.\\N- Test 2.\r\n')) + self.assertIn( + subs[0][0].ass, + ( + "Dialogue: 0,0:00:00.97,0:00:02.54,Default,- Test 1.\\N- Test 2.\r\n", + "Dialogue: 0,0:00:00.97,0:00:02.54,Default,,0,0,0,,- Test 1.\\N- Test 2.\r\n", + ), + ) def test_vobsub(self): - path = fate_suite('sub/vobsub.sub') + path = fate_suite("sub/vobsub.sub") fh = av.open(path) subs = [] @@ -42,7 +46,7 @@ def test_vobsub(self): bms = sub.planes self.assertEqual(len(bms), 1) - if hasattr(__builtins__, 'buffer'): + if hasattr(__builtins__, "buffer"): self.assertEqual(len(buffer(bms[0])), 4800) # noqa - if hasattr(__builtins__, 'memoryview'): + if hasattr(__builtins__, "memoryview"): self.assertEqual(len(memoryview(bms[0])), 4800) # noqa diff --git a/tests/test_timeout.py b/tests/test_timeout.py index a5e8bb21f..7463c0b18 100644 --- a/tests/test_timeout.py +++ b/tests/test_timeout.py @@ -9,8 +9,7 @@ PORT = 8002 -CONTENT = open(fate_suite('mpeg2/mpeg2_field_encoding.ts'), 'rb').read()\ - +CONTENT = open(fate_suite("mpeg2/mpeg2_field_encoding.ts"), "rb").read() # Needs to be long enough for all host OSes to deal. TIMEOUT = 0.25 DELAY = 4 * TIMEOUT @@ -27,7 +26,7 @@ class SlowRequestHandler(BaseHTTPRequestHandler): def do_GET(self): time.sleep(DELAY) self.send_response(200) - self.send_header('Content-Length', str(len(CONTENT))) + self.send_header("Content-Length", str(len(CONTENT))) self.end_headers() self.wfile.write(CONTENT) @@ -37,7 +36,7 @@ def log_message(self, format, *args): class TestTimeout(TestCase): def setUp(cls): - cls._server = HttpServer(('', PORT), SlowRequestHandler) + cls._server = HttpServer(("", PORT), SlowRequestHandler) cls._thread = threading.Thread(target=cls._server.handle_request) cls._thread.daemon = True # Make sure the tests will exit. cls._thread.start() @@ -48,20 +47,25 @@ def tearDown(cls): def test_no_timeout(self): start = time.time() - av.open('http://localhost:%d/mpeg2_field_encoding.ts' % PORT) + av.open("http://localhost:%d/mpeg2_field_encoding.ts" % PORT) duration = time.time() - start self.assertGreater(duration, DELAY) def test_open_timeout(self): with self.assertRaises(av.ExitError): start = time.time() - av.open('http://localhost:%d/mpeg2_field_encoding.ts' % PORT, timeout=TIMEOUT) + av.open( + "http://localhost:%d/mpeg2_field_encoding.ts" % PORT, timeout=TIMEOUT + ) duration = time.time() - start self.assertLess(duration, DELAY) def test_open_timeout_2(self): with self.assertRaises(av.ExitError): start = time.time() - av.open('http://localhost:%d/mpeg2_field_encoding.ts' % PORT, timeout=(TIMEOUT, None)) + av.open( + "http://localhost:%d/mpeg2_field_encoding.ts" % PORT, + timeout=(TIMEOUT, None), + ) duration = time.time() - start self.assertLess(duration, DELAY) diff --git a/tests/test_videoformat.py b/tests/test_videoformat.py index 4c72fdb00..61b9ca0fc 100644 --- a/tests/test_videoformat.py +++ b/tests/test_videoformat.py @@ -10,8 +10,8 @@ def test_invalid_pixel_format(self): self.assertEqual(str(cm.exception), "not a pixel format: '__unknown_pix_fmt'") def test_rgb24_inspection(self): - fmt = VideoFormat('rgb24', 640, 480) - self.assertEqual(fmt.name, 'rgb24') + fmt = VideoFormat("rgb24", 640, 480) + self.assertEqual(fmt.name, "rgb24") self.assertEqual(len(fmt.components), 3) self.assertFalse(fmt.is_planar) self.assertFalse(fmt.has_palette) @@ -31,8 +31,8 @@ def test_rgb24_inspection(self): self.assertEqual(comp.height, 480) def test_yuv420p_inspection(self): - fmt = VideoFormat('yuv420p', 640, 480) - self.assertEqual(fmt.name, 'yuv420p') + fmt = VideoFormat("yuv420p", 640, 480) + self.assertEqual(fmt.name, "yuv420p") self.assertEqual(len(fmt.components), 3) self._test_yuv420(fmt) @@ -62,15 +62,15 @@ def _test_yuv420(self, fmt): self.assertEqual(fmt.components[2].width, 320) def test_yuva420p_inspection(self): - fmt = VideoFormat('yuva420p', 640, 480) + fmt = VideoFormat("yuva420p", 640, 480) self.assertEqual(len(fmt.components), 4) self._test_yuv420(fmt) self.assertFalse(fmt.components[3].is_chroma) self.assertEqual(fmt.components[3].width, 640) def test_gray16be_inspection(self): - fmt = VideoFormat('gray16be', 640, 480) - self.assertEqual(fmt.name, 'gray16be') + fmt = VideoFormat("gray16be", 640, 480) + self.assertEqual(fmt.name, "gray16be") self.assertEqual(len(fmt.components), 1) self.assertFalse(fmt.is_planar) self.assertFalse(fmt.has_palette) @@ -89,6 +89,6 @@ def test_gray16be_inspection(self): self.assertFalse(comp.is_alpha) def test_pal8_inspection(self): - fmt = VideoFormat('pal8', 640, 480) + fmt = VideoFormat("pal8", 640, 480) self.assertEqual(len(fmt.components), 1) self.assertTrue(fmt.has_palette) diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 31022eed9..3e15f1f5c 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -19,29 +19,28 @@ def test_null_constructor(self): frame = VideoFrame() self.assertEqual(frame.width, 0) self.assertEqual(frame.height, 0) - self.assertEqual(frame.format.name, 'yuv420p') + self.assertEqual(frame.format.name, "yuv420p") def test_manual_yuv_constructor(self): - frame = VideoFrame(640, 480, 'yuv420p') + frame = VideoFrame(640, 480, "yuv420p") self.assertEqual(frame.width, 640) self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, 'yuv420p') + self.assertEqual(frame.format.name, "yuv420p") def test_manual_rgb_constructor(self): - frame = VideoFrame(640, 480, 'rgb24') + frame = VideoFrame(640, 480, "rgb24") self.assertEqual(frame.width, 640) self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, 'rgb24') + self.assertEqual(frame.format.name, "rgb24") class TestVideoFramePlanes(TestCase): - def test_null_planes(self): frame = VideoFrame() # yuv420p self.assertEqual(len(frame.planes), 0) def test_yuv420p_planes(self): - frame = VideoFrame(640, 480, 'yuv420p') + frame = VideoFrame(640, 480, "yuv420p") self.assertEqual(len(frame.planes), 3) self.assertEqual(frame.planes[0].width, 640) self.assertEqual(frame.planes[0].height, 480) @@ -56,7 +55,7 @@ def test_yuv420p_planes(self): def test_yuv420p_planes_align(self): # If we request 8-byte alignment for a width which is not a multiple of 8, # the line sizes are larger than the plane width. - frame = VideoFrame(318, 238, 'yuv420p') + frame = VideoFrame(318, 238, "yuv420p") self.assertEqual(len(frame.planes), 3) self.assertEqual(frame.planes[0].width, 318) self.assertEqual(frame.planes[0].height, 238) @@ -69,7 +68,7 @@ def test_yuv420p_planes_align(self): self.assertEqual(frame.planes[i].buffer_size, 160 * 119) def test_rgb24_planes(self): - frame = VideoFrame(640, 480, 'rgb24') + frame = VideoFrame(640, 480, "rgb24") self.assertEqual(len(frame.planes), 1) self.assertEqual(frame.planes[0].width, 640) self.assertEqual(frame.planes[0].height, 480) @@ -78,35 +77,33 @@ def test_rgb24_planes(self): class TestVideoFrameBuffers(TestCase): - def test_buffer(self): - if not hasattr(__builtins__, 'buffer'): + if not hasattr(__builtins__, "buffer"): raise SkipTest() - frame = VideoFrame(640, 480, 'rgb24') - frame.planes[0].update(b'01234' + (b'x' * (640 * 480 * 3 - 5))) + frame = VideoFrame(640, 480, "rgb24") + frame.planes[0].update(b"01234" + (b"x" * (640 * 480 * 3 - 5))) buf = buffer(frame.planes[0]) # noqa - self.assertEqual(buf[1], b'1') - self.assertEqual(buf[:7], b'01234xx') + self.assertEqual(buf[1], b"1") + self.assertEqual(buf[:7], b"01234xx") def test_memoryview_read(self): - if not hasattr(__builtins__, 'memoryview'): + if not hasattr(__builtins__, "memoryview"): raise SkipTest() - frame = VideoFrame(640, 480, 'rgb24') - frame.planes[0].update(b'01234' + (b'x' * (640 * 480 * 3 - 5))) + frame = VideoFrame(640, 480, "rgb24") + frame.planes[0].update(b"01234" + (b"x" * (640 * 480 * 3 - 5))) mem = memoryview(frame.planes[0]) # noqa self.assertEqual(mem.ndim, 1) - self.assertEqual(mem.shape, (640 * 480 * 3, )) + self.assertEqual(mem.shape, (640 * 480 * 3,)) self.assertFalse(mem.readonly) self.assertEqual(mem[1], 49) - self.assertEqual(mem[:7], b'01234xx') + self.assertEqual(mem[:7], b"01234xx") mem[1] = 46 - self.assertEqual(mem[:7], b'0.234xx') + self.assertEqual(mem[:7], b"0.234xx") class TestVideoFrameImage(TestCase): - def setUp(self): if not Image: raise SkipTest() @@ -115,7 +112,7 @@ def test_roundtrip(self): image = Image.open(fate_png()) frame = VideoFrame.from_image(image) img = frame.to_image() - img.save(self.sandboxed('roundtrip-high.jpg')) + img.save(self.sandboxed("roundtrip-high.jpg")) self.assertImagesAlmostEqual(image, img) def test_to_image_rgb24(self): @@ -125,7 +122,7 @@ def test_to_image_rgb24(self): (500, 500), ] for width, height in sizes: - frame = VideoFrame(width, height, format='rgb24') + frame = VideoFrame(width, height, format="rgb24") # fill video frame data for plane in frame.planes: @@ -150,21 +147,20 @@ def test_to_image_rgb24(self): self.assertEqual(img.tobytes(), expected) def test_to_image_with_dimensions(self): - frame = VideoFrame(640, 480, format='rgb24') + frame = VideoFrame(640, 480, format="rgb24") img = frame.to_image(width=320, height=240) self.assertEqual(img.size, (320, 240)) class TestVideoFrameNdarray(TestCase): - def test_basic_to_ndarray(self): - frame = VideoFrame(640, 480, 'rgb24') + frame = VideoFrame(640, 480, "rgb24") array = frame.to_ndarray() self.assertEqual(array.shape, (480, 640, 3)) def test_basic_to_nd_array(self): - frame = VideoFrame(640, 480, 'rgb24') + frame = VideoFrame(640, 480, "rgb24") with warnings.catch_warnings(record=True) as recorded: array = frame.to_nd_array() self.assertEqual(array.shape, (480, 640, 3)) @@ -174,29 +170,30 @@ def test_basic_to_nd_array(self): self.assertEqual(recorded[0].category, AttributeRenamedWarning) self.assertEqual( str(recorded[0].message), - 'VideoFrame.to_nd_array is deprecated; please use VideoFrame.to_ndarray.') + "VideoFrame.to_nd_array is deprecated; please use VideoFrame.to_ndarray.", + ) def test_ndarray_gray(self): array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) - for format in ['gray', 'gray8']: + for format in ["gray", "gray8"]: frame = VideoFrame.from_ndarray(array, format=format) self.assertEqual(frame.width, 640) self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, 'gray') + self.assertEqual(frame.format.name, "gray") self.assertTrue((frame.to_ndarray() == array).all()) def test_ndarray_gray_align(self): array = numpy.random.randint(0, 256, size=(238, 318), dtype=numpy.uint8) - for format in ['gray', 'gray8']: + for format in ["gray", "gray8"]: frame = VideoFrame.from_ndarray(array, format=format) self.assertEqual(frame.width, 318) self.assertEqual(frame.height, 238) - self.assertEqual(frame.format.name, 'gray') + self.assertEqual(frame.format.name, "gray") self.assertTrue((frame.to_ndarray() == array).all()) def test_ndarray_rgb(self): array = numpy.random.randint(0, 256, size=(480, 640, 3), dtype=numpy.uint8) - for format in ['rgb24', 'bgr24']: + for format in ["rgb24", "bgr24"]: frame = VideoFrame.from_ndarray(array, format=format) self.assertEqual(frame.width, 640) self.assertEqual(frame.height, 480) @@ -205,7 +202,7 @@ def test_ndarray_rgb(self): def test_ndarray_rgb_align(self): array = numpy.random.randint(0, 256, size=(238, 318, 3), dtype=numpy.uint8) - for format in ['rgb24', 'bgr24']: + for format in ["rgb24", "bgr24"]: frame = VideoFrame.from_ndarray(array, format=format) self.assertEqual(frame.width, 318) self.assertEqual(frame.height, 238) @@ -214,7 +211,7 @@ def test_ndarray_rgb_align(self): def test_ndarray_rgba(self): array = numpy.random.randint(0, 256, size=(480, 640, 4), dtype=numpy.uint8) - for format in ['argb', 'rgba', 'abgr', 'bgra']: + for format in ["argb", "rgba", "abgr", "bgra"]: frame = VideoFrame.from_ndarray(array, format=format) self.assertEqual(frame.width, 640) self.assertEqual(frame.height, 480) @@ -223,7 +220,7 @@ def test_ndarray_rgba(self): def test_ndarray_rgba_align(self): array = numpy.random.randint(0, 256, size=(238, 318, 4), dtype=numpy.uint8) - for format in ['argb', 'rgba', 'abgr', 'bgra']: + for format in ["argb", "rgba", "abgr", "bgra"]: frame = VideoFrame.from_ndarray(array, format=format) self.assertEqual(frame.width, 318) self.assertEqual(frame.height, 238) @@ -232,67 +229,67 @@ def test_ndarray_rgba_align(self): def test_ndarray_yuv420p(self): array = numpy.random.randint(0, 256, size=(720, 640), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray(array, format='yuv420p') + frame = VideoFrame.from_ndarray(array, format="yuv420p") self.assertEqual(frame.width, 640) self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, 'yuv420p') + self.assertEqual(frame.format.name, "yuv420p") self.assertTrue((frame.to_ndarray() == array).all()) def test_ndarray_yuv420p_align(self): array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray(array, format='yuv420p') + frame = VideoFrame.from_ndarray(array, format="yuv420p") self.assertEqual(frame.width, 318) self.assertEqual(frame.height, 238) - self.assertEqual(frame.format.name, 'yuv420p') + self.assertEqual(frame.format.name, "yuv420p") self.assertTrue((frame.to_ndarray() == array).all()) def test_ndarray_yuvj420p(self): array = numpy.random.randint(0, 256, size=(720, 640), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray(array, format='yuvj420p') + frame = VideoFrame.from_ndarray(array, format="yuvj420p") self.assertEqual(frame.width, 640) self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, 'yuvj420p') + self.assertEqual(frame.format.name, "yuvj420p") self.assertTrue((frame.to_ndarray() == array).all()) def test_ndarray_yuyv422(self): array = numpy.random.randint(0, 256, size=(480, 640, 2), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray(array, format='yuyv422') + frame = VideoFrame.from_ndarray(array, format="yuyv422") self.assertEqual(frame.width, 640) self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, 'yuyv422') + self.assertEqual(frame.format.name, "yuyv422") self.assertTrue((frame.to_ndarray() == array).all()) def test_ndarray_yuyv422_align(self): array = numpy.random.randint(0, 256, size=(238, 318, 2), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray(array, format='yuyv422') + frame = VideoFrame.from_ndarray(array, format="yuyv422") self.assertEqual(frame.width, 318) self.assertEqual(frame.height, 238) - self.assertEqual(frame.format.name, 'yuyv422') + self.assertEqual(frame.format.name, "yuyv422") self.assertTrue((frame.to_ndarray() == array).all()) def test_ndarray_rgb8(self): array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray(array, format='rgb8') + frame = VideoFrame.from_ndarray(array, format="rgb8") self.assertEqual(frame.width, 640) self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, 'rgb8') + self.assertEqual(frame.format.name, "rgb8") self.assertTrue((frame.to_ndarray() == array).all()) def test_ndarray_bgr8(self): array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray(array, format='bgr8') + frame = VideoFrame.from_ndarray(array, format="bgr8") self.assertEqual(frame.width, 640) self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, 'bgr8') + self.assertEqual(frame.format.name, "bgr8") self.assertTrue((frame.to_ndarray() == array).all()) def test_ndarray_pal8(self): array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) palette = numpy.random.randint(0, 256, size=(256, 4), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray((array, palette), format='pal8') + frame = VideoFrame.from_ndarray((array, palette), format="pal8") self.assertEqual(frame.width, 640) self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, 'pal8') + self.assertEqual(frame.format.name, "pal8") returned = frame.to_ndarray() self.assertTrue((type(returned) is tuple) and len(returned) == 2) self.assertTrue((returned[0] == array).all()) @@ -300,46 +297,44 @@ def test_ndarray_pal8(self): class TestVideoFrameTiming(TestCase): - def test_reformat_pts(self): - frame = VideoFrame(640, 480, 'rgb24') + frame = VideoFrame(640, 480, "rgb24") frame.pts = 123 - frame.time_base = '456/1' # Just to be different. + frame.time_base = "456/1" # Just to be different. frame = frame.reformat(320, 240) self.assertEqual(frame.pts, 123) self.assertEqual(frame.time_base, 456) class TestVideoFrameReformat(TestCase): - def test_reformat_identity(self): - frame1 = VideoFrame(640, 480, 'rgb24') - frame2 = frame1.reformat(640, 480, 'rgb24') + frame1 = VideoFrame(640, 480, "rgb24") + frame2 = frame1.reformat(640, 480, "rgb24") self.assertIs(frame1, frame2) def test_reformat_colourspace(self): # This is allowed. - frame = VideoFrame(640, 480, 'rgb24') - frame.reformat(src_colorspace=None, dst_colorspace='smpte240') + frame = VideoFrame(640, 480, "rgb24") + frame.reformat(src_colorspace=None, dst_colorspace="smpte240") # I thought this was not allowed, but it seems to be. - frame = VideoFrame(640, 480, 'yuv420p') - frame.reformat(src_colorspace=None, dst_colorspace='smpte240') + frame = VideoFrame(640, 480, "yuv420p") + frame.reformat(src_colorspace=None, dst_colorspace="smpte240") def test_reformat_pixel_format_align(self): height = 480 for width in range(2, 258, 2): - frame_yuv = VideoFrame(width, height, 'yuv420p') + frame_yuv = VideoFrame(width, height, "yuv420p") for plane in frame_yuv.planes: - plane.update(b'\xff' * plane.buffer_size) + plane.update(b"\xff" * plane.buffer_size) expected_rgb = numpy.zeros(shape=(height, width, 3), dtype=numpy.uint8) expected_rgb[:, :, 0] = 255 expected_rgb[:, :, 1] = 124 expected_rgb[:, :, 2] = 255 - frame_rgb = frame_yuv.reformat(format='rgb24') + frame_rgb = frame_yuv.reformat(format="rgb24") array_rgb = frame_rgb.to_ndarray() self.assertEqual(array_rgb.shape, (height, width, 3)) self.assertTrue((array_rgb == expected_rgb).all()) From 01b89d980fc9202ef23d72cce89a9c4c6a9423b5 Mon Sep 17 00:00:00 2001 From: FirefoxMetzger Date: Thu, 24 Feb 2022 21:22:29 +0100 Subject: [PATCH 023/846] [filters] allow flushing by sending `None` (fixes: #886) --- av/filter/context.pyx | 5 ++++- av/filter/graph.pyx | 6 ++++-- tests/test_filters.py | 24 ++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/av/filter/context.pyx b/av/filter/context.pyx index 30481eb21..9c9a1fa20 100644 --- a/av/filter/context.pyx +++ b/av/filter/context.pyx @@ -77,7 +77,10 @@ cdef class FilterContext(object): def push(self, Frame frame): - if self.filter.name in ('abuffer', 'buffer'): + if frame is None: + err_check(lib.av_buffersrc_write_frame(self.ptr, NULL)) + return + elif self.filter.name in ('abuffer', 'buffer'): err_check(lib.av_buffersrc_write_frame(self.ptr, frame.ptr)) return diff --git a/av/filter/graph.pyx b/av/filter/graph.pyx index 20c76c7de..bcb49f788 100644 --- a/av/filter/graph.pyx +++ b/av/filter/graph.pyx @@ -196,12 +196,14 @@ cdef class Graph(object): def push(self, frame): - if isinstance(frame, VideoFrame): + if frame is None: + contexts = self._context_by_type.get('buffer', []) + self._context_by_type.get('abuffer', []) + elif isinstance(frame, VideoFrame): contexts = self._context_by_type.get('buffer', []) elif isinstance(frame, AudioFrame): contexts = self._context_by_type.get('abuffer', []) else: - raise ValueError('can only push VideoFrame or AudioFrame', type(frame)) + raise ValueError('can only AudioFrame, VideoFrame or None; got %s' % type(frame)) if len(contexts) != 1: raise ValueError('can only auto-push with single buffer; found %s' % len(contexts)) diff --git a/tests/test_filters.py b/tests/test_filters.py index 2f3d6985f..f73bf4cc8 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -260,3 +260,27 @@ def test_video_buffer(self): self.assertEqual(filtered_frames[1].pts, (frame.pts - 1) * 2 + 1) self.assertEqual(filtered_frames[1].time_base, Fraction(1, 60)) + + def test_EOF(self): + input_container = av.open(format="lavfi", file="color=c=pink:duration=1:r=30") + video_stream = input_container.streams.video[0] + + graph = av.filter.Graph() + video_in = graph.add_buffer(template=video_stream) + palette_gen_filter = graph.add("palettegen") + video_out = graph.add("buffersink") + video_in.link_to(palette_gen_filter) + palette_gen_filter.link_to(video_out) + graph.configure() + + for frame in input_container.decode(video=0): + graph.push(frame) + + graph.push(None) + + # if we do not push None, we get a BlockingIOError + palette_frame = graph.pull() + + self.assertIsInstance(palette_frame, av.VideoFrame) + self.assertEqual(palette_frame.width, 16) + self.assertEqual(palette_frame.height, 16) From 82ac9ac23e5a17eca728edf6a5c011c58bdf291e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Fri, 25 Feb 2022 16:22:42 +0100 Subject: [PATCH 024/846] [tests] make ndarray comparisons more explicit Show the differences between the arrays to make debugging easier. --- tests/common.py | 18 ++++++++++++++++++ tests/test_audioframe.py | 10 +++++----- tests/test_videoframe.py | 34 ++++++++++++++++------------------ 3 files changed, 39 insertions(+), 23 deletions(-) diff --git a/tests/common.py b/tests/common.py index bb3851a5f..4322038bb 100644 --- a/tests/common.py +++ b/tests/common.py @@ -119,6 +119,24 @@ def sandboxed(self, *args, **kwargs): kwargs.setdefault("timed", True) return sandboxed(*args, **kwargs) + def assertNdarraysEqual(self, a, b): + import numpy + + self.assertEqual(a.shape, b.shape) + + comparison = a == b + if not comparison.all(): + it = numpy.nditer(comparison, flags=["multi_index"]) + msg = "" + for equal in it: + if not equal: + msg += "- arrays differ at index %s; %s %s\n" % ( + it.multi_index, + a[it.multi_index], + b[it.multi_index], + ) + self.fail("ndarrays contents differ\n%s" % msg) + def assertImagesAlmostEqual(self, a, b, epsilon=0.1, *args): self.assertEqual(a.size, b.size, "sizes dont match") a = a.filter(ImageFilter.BLUR).getdata() diff --git a/tests/test_audioframe.py b/tests/test_audioframe.py index 626c37e99..a76526e62 100644 --- a/tests/test_audioframe.py +++ b/tests/test_audioframe.py @@ -111,7 +111,7 @@ def test_ndarray_dbl(self): self.assertEqual(frame.format.name, format) self.assertEqual(frame.layout.name, layout) self.assertEqual(frame.samples, 160) - self.assertTrue((frame.to_ndarray() == array).all()) + self.assertNdarraysEqual(frame.to_ndarray(), array) def test_from_ndarray_value_error(self): # incorrect dtype @@ -152,7 +152,7 @@ def test_ndarray_flt(self): self.assertEqual(frame.format.name, format) self.assertEqual(frame.layout.name, layout) self.assertEqual(frame.samples, 160) - self.assertTrue((frame.to_ndarray() == array).all()) + self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_s16(self): layouts = [ @@ -167,7 +167,7 @@ def test_ndarray_s16(self): self.assertEqual(frame.format.name, format) self.assertEqual(frame.layout.name, layout) self.assertEqual(frame.samples, 160) - self.assertTrue((frame.to_ndarray() == array).all()) + self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_s16p_align_8(self): frame = AudioFrame(format="s16p", layout="stereo", samples=159, align=8) @@ -188,7 +188,7 @@ def test_ndarray_s32(self): self.assertEqual(frame.format.name, format) self.assertEqual(frame.layout.name, layout) self.assertEqual(frame.samples, 160) - self.assertTrue((frame.to_ndarray() == array).all()) + self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_u8(self): layouts = [ @@ -203,4 +203,4 @@ def test_ndarray_u8(self): self.assertEqual(frame.format.name, format) self.assertEqual(frame.layout.name, layout) self.assertEqual(frame.samples, 160) - self.assertTrue((frame.to_ndarray() == array).all()) + self.assertNdarraysEqual(frame.to_ndarray(), array) diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 3e15f1f5c..2f02cebb8 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -180,7 +180,7 @@ def test_ndarray_gray(self): self.assertEqual(frame.width, 640) self.assertEqual(frame.height, 480) self.assertEqual(frame.format.name, "gray") - self.assertTrue((frame.to_ndarray() == array).all()) + self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_gray_align(self): array = numpy.random.randint(0, 256, size=(238, 318), dtype=numpy.uint8) @@ -189,7 +189,7 @@ def test_ndarray_gray_align(self): self.assertEqual(frame.width, 318) self.assertEqual(frame.height, 238) self.assertEqual(frame.format.name, "gray") - self.assertTrue((frame.to_ndarray() == array).all()) + self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_rgb(self): array = numpy.random.randint(0, 256, size=(480, 640, 3), dtype=numpy.uint8) @@ -198,7 +198,7 @@ def test_ndarray_rgb(self): self.assertEqual(frame.width, 640) self.assertEqual(frame.height, 480) self.assertEqual(frame.format.name, format) - self.assertTrue((frame.to_ndarray() == array).all()) + self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_rgb_align(self): array = numpy.random.randint(0, 256, size=(238, 318, 3), dtype=numpy.uint8) @@ -207,7 +207,7 @@ def test_ndarray_rgb_align(self): self.assertEqual(frame.width, 318) self.assertEqual(frame.height, 238) self.assertEqual(frame.format.name, format) - self.assertTrue((frame.to_ndarray() == array).all()) + self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_rgba(self): array = numpy.random.randint(0, 256, size=(480, 640, 4), dtype=numpy.uint8) @@ -216,7 +216,7 @@ def test_ndarray_rgba(self): self.assertEqual(frame.width, 640) self.assertEqual(frame.height, 480) self.assertEqual(frame.format.name, format) - self.assertTrue((frame.to_ndarray() == array).all()) + self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_rgba_align(self): array = numpy.random.randint(0, 256, size=(238, 318, 4), dtype=numpy.uint8) @@ -225,7 +225,7 @@ def test_ndarray_rgba_align(self): self.assertEqual(frame.width, 318) self.assertEqual(frame.height, 238) self.assertEqual(frame.format.name, format) - self.assertTrue((frame.to_ndarray() == array).all()) + self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_yuv420p(self): array = numpy.random.randint(0, 256, size=(720, 640), dtype=numpy.uint8) @@ -233,7 +233,7 @@ def test_ndarray_yuv420p(self): self.assertEqual(frame.width, 640) self.assertEqual(frame.height, 480) self.assertEqual(frame.format.name, "yuv420p") - self.assertTrue((frame.to_ndarray() == array).all()) + self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_yuv420p_align(self): array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) @@ -241,7 +241,7 @@ def test_ndarray_yuv420p_align(self): self.assertEqual(frame.width, 318) self.assertEqual(frame.height, 238) self.assertEqual(frame.format.name, "yuv420p") - self.assertTrue((frame.to_ndarray() == array).all()) + self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_yuvj420p(self): array = numpy.random.randint(0, 256, size=(720, 640), dtype=numpy.uint8) @@ -249,7 +249,7 @@ def test_ndarray_yuvj420p(self): self.assertEqual(frame.width, 640) self.assertEqual(frame.height, 480) self.assertEqual(frame.format.name, "yuvj420p") - self.assertTrue((frame.to_ndarray() == array).all()) + self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_yuyv422(self): array = numpy.random.randint(0, 256, size=(480, 640, 2), dtype=numpy.uint8) @@ -257,7 +257,7 @@ def test_ndarray_yuyv422(self): self.assertEqual(frame.width, 640) self.assertEqual(frame.height, 480) self.assertEqual(frame.format.name, "yuyv422") - self.assertTrue((frame.to_ndarray() == array).all()) + self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_yuyv422_align(self): array = numpy.random.randint(0, 256, size=(238, 318, 2), dtype=numpy.uint8) @@ -265,7 +265,7 @@ def test_ndarray_yuyv422_align(self): self.assertEqual(frame.width, 318) self.assertEqual(frame.height, 238) self.assertEqual(frame.format.name, "yuyv422") - self.assertTrue((frame.to_ndarray() == array).all()) + self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_rgb8(self): array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) @@ -273,7 +273,7 @@ def test_ndarray_rgb8(self): self.assertEqual(frame.width, 640) self.assertEqual(frame.height, 480) self.assertEqual(frame.format.name, "rgb8") - self.assertTrue((frame.to_ndarray() == array).all()) + self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_bgr8(self): array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) @@ -281,7 +281,7 @@ def test_ndarray_bgr8(self): self.assertEqual(frame.width, 640) self.assertEqual(frame.height, 480) self.assertEqual(frame.format.name, "bgr8") - self.assertTrue((frame.to_ndarray() == array).all()) + self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_pal8(self): array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) @@ -292,8 +292,8 @@ def test_ndarray_pal8(self): self.assertEqual(frame.format.name, "pal8") returned = frame.to_ndarray() self.assertTrue((type(returned) is tuple) and len(returned) == 2) - self.assertTrue((returned[0] == array).all()) - self.assertTrue((returned[1] == palette).all()) + self.assertNdarraysEqual(returned[0], array) + self.assertNdarraysEqual(returned[1], palette) class TestVideoFrameTiming(TestCase): @@ -335,6 +335,4 @@ def test_reformat_pixel_format_align(self): expected_rgb[:, :, 2] = 255 frame_rgb = frame_yuv.reformat(format="rgb24") - array_rgb = frame_rgb.to_ndarray() - self.assertEqual(array_rgb.shape, (height, width, 3)) - self.assertTrue((array_rgb == expected_rgb).all()) + self.assertNdarraysEqual(frame_rgb.to_ndarray(), expected_rgb) From f7178e3067c00a8123d4d0f17c09406138493ed6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Fri, 25 Feb 2022 18:44:49 +0100 Subject: [PATCH 025/846] [wheels] skip test suite on some platforms When there are no binary wheels of numpy, the test suite takes ages to run because numpy needs to be built from source. There are no binary wheels of numpy: - for Python 3.7 - for PyPy on some platforms - for i686 --- .github/workflows/tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 994b158ab..cbe7f789d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -240,6 +240,8 @@ jobs: # disable test suite on OS X, the SSL config seems broken CIBW_TEST_COMMAND_MACOS: true CIBW_TEST_REQUIRES: numpy + # skip tests when there are no binary wheels of numpy + CIBW_TEST_SKIP: cp37-* pp* *-i686 run: | pip install cibuildwheel cibuildwheel --output-dir dist From 34d7f4840bba0f959382288b7d231fb3a32f0328 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Fri, 25 Feb 2022 18:47:04 +0100 Subject: [PATCH 026/846] [wheels] re-enable wheel tests on macos --- .github/workflows/tests.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cbe7f789d..36ed8c12f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -237,11 +237,9 @@ jobs: CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: python scripts/inject-dll {wheel} {dest_dir} C:\cibw\vendor\bin CIBW_SKIP: cp36-* pp36-* pp38-win* *-musllinux* CIBW_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m unittest discover -t {project} -s tests && mv {project}/av.disabled {project}/av - # disable test suite on OS X, the SSL config seems broken - CIBW_TEST_COMMAND_MACOS: true CIBW_TEST_REQUIRES: numpy # skip tests when there are no binary wheels of numpy - CIBW_TEST_SKIP: cp37-* pp* *-i686 + CIBW_TEST_SKIP: cp37-* pp* *_i686 run: | pip install cibuildwheel cibuildwheel --output-dir dist From 020312667268e322c5caaa33ae35453204a46992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Sun, 14 Feb 2021 00:13:02 +0100 Subject: [PATCH 027/846] [package] update ffmpeg to 4.3.3, build wheels for aarch64 --- .github/workflows/tests.yml | 5 +++++ docs/overview/about.rst | 4 +++- scripts/fetch-vendor.json | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 36ed8c12f..706a70245 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -210,6 +210,8 @@ jobs: arch: arm64 - os: macos-latest arch: x86_64 + - os: ubuntu-latest + arch: aarch64 - os: ubuntu-latest arch: i686 - os: ubuntu-latest @@ -221,6 +223,9 @@ jobs: - uses: actions/setup-python@v1 with: python-version: 3.7 + - name: Set up QEMU + if: matrix.os == 'ubuntu-latest' + uses: docker/setup-qemu-action@v1 - name: Install packages if: matrix.os == 'macos-latest' run: | diff --git a/docs/overview/about.rst b/docs/overview/about.rst index 995aa450e..2fa2f8dd5 100644 --- a/docs/overview/about.rst +++ b/docs/overview/about.rst @@ -4,9 +4,11 @@ More About PyAV Binary wheels ------------- -Since release 8.0.0 binary wheels are provided on PyPI for Linux, Mac and Windows linked against FFmpeg. Currently FFmpeg 4.3.2 is used with the following features enabled for all platforms: +Since release 8.0.0 binary wheels are provided on PyPI for Linux, Mac and Windows linked against FFmpeg. Currently FFmpeg 4.3.3 is used with the following features enabled for all platforms: - fontconfig +- gmp +- gnutls - libaom - libass - libbluray diff --git a/scripts/fetch-vendor.json b/scripts/fetch-vendor.json index d87885e4b..30c99fa35 100644 --- a/scripts/fetch-vendor.json +++ b/scripts/fetch-vendor.json @@ -1,3 +1,3 @@ { - "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/4.3.2-2/ffmpeg-{platform}.tar.gz"] + "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/4.3.3-1/ffmpeg-{platform}.tar.gz"] } From 8f51049dc11705013c61979ca8a820982e8bd114 Mon Sep 17 00:00:00 2001 From: mephi42 Date: Sun, 23 Jan 2022 18:41:29 +0100 Subject: [PATCH 028/846] Avoid unnecessary vsnprintf() calls in log_callback() mov_read_trak() and mov_read_trun() generate a lot of AV_LOG_TRACE logs, which are ultimately thrown away. However, log_callback() still formats and processes them. This causes av_log() to - at least in some cases I observed with perf record - to take 97% of avformat_open_input() run time. Improve this by returning from log_callback() early when a log is obviously not needed. --- av/logging.pyx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/av/logging.pyx b/av/logging.pyx index 399e8159e..1ae5d2df6 100644 --- a/av/logging.pyx +++ b/av/logging.pyx @@ -245,6 +245,12 @@ cdef void log_callback(void *ptr, int level, const char *format, lib.va_list arg if not inited and not print_after_shutdown: return + # Fast path: avoid logging overhead. This should match the + # log_callback_gil() checks that result in ignoring the message. + with gil: + if level > level_threshold and level != lib.AV_LOG_ERROR: + return + # Format the message. cdef char message[1024] lib.vsnprintf(message, 1023, format, args) From fb0f7faab4804c8c605fcf3ff4fdb5f8e535a3ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Sun, 6 Mar 2022 23:43:07 +0100 Subject: [PATCH 029/846] [package] move version information to av/about.py (see: #844) We can also simplify setup.py because Cython is guaranteed to be present. While we are at it, reformat setup.py using `black`. --- README.md | 3 +- VERSION.txt | 1 - av/__init__.py | 3 +- av/__main__.py | 5 +- av/_core.pyx | 3 - av/about.py | 1 + docs/conf.py | 6 +- include/libav.pxd | 8 - include/libav.pyav.h | 0 setup.py | 354 +++++++++++++++++-------------------------- 10 files changed, 146 insertions(+), 238 deletions(-) delete mode 100644 VERSION.txt create mode 100644 av/about.py delete mode 100644 include/libav.pyav.h diff --git a/README.md b/README.md index 2bf36844e..627d6336f 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ PyAV [![GitHub Test Status][github-tests-badge]][github-tests] \ [![Gitter Chat][gitter-badge]][gitter] [![Documentation][docs-badge]][docs] \ -[![GitHub][github-badge]][github] [![Python Package Index][pypi-badge]][pypi] [![Conda Forge][conda-badge]][conda] +[![Python Package Index][pypi-badge]][pypi] [![Conda Forge][conda-badge]][conda] PyAV is a Pythonic binding for the [FFmpeg][ffmpeg] libraries. We aim to provide all of the power and control of the underlying library, but manage the gritty details as much as possible. @@ -73,7 +73,6 @@ Have fun, [read the docs][docs], [come chat with us][gitter], and good luck! [github-tests-badge]: https://github.com/PyAV-Org/PyAV/workflows/tests/badge.svg [github-tests]: https://github.com/PyAV-Org/PyAV/actions?workflow=tests -[github-badge]: https://img.shields.io/badge/dynamic/xml.svg?label=github&url=https%3A%2F%2Fraw.githubusercontent.com%2FPyAV-Org%2FPyAV%2Fdevelop%2FVERSION.txt&query=.&colorB=CCB39A&prefix=v [github]: https://github.com/PyAV-Org/PyAV [ffmpeg]: http://ffmpeg.org/ diff --git a/VERSION.txt b/VERSION.txt deleted file mode 100644 index 7e4c9637a..000000000 --- a/VERSION.txt +++ /dev/null @@ -1 +0,0 @@ -8.1.1.dev0 diff --git a/av/__init__.py b/av/__init__.py index 1eed5f27b..237cb8b94 100644 --- a/av/__init__.py +++ b/av/__init__.py @@ -9,12 +9,13 @@ # MUST import the core before anything else in order to initalize the underlying # library that is being wrapped. -from av._core import time_base, pyav_version as __version__, library_versions +from av._core import time_base, library_versions # Capture logging (by importing it). from av import logging # For convenience, IMPORT ALL OF THE THINGS (that are constructable by the user). +from av.about import __version__ from av.audio.fifo import AudioFifo from av.audio.format import AudioFormat from av.audio.frame import AudioFrame diff --git a/av/__main__.py b/av/__main__.py index 32a1db565..8c57e2dd9 100644 --- a/av/__main__.py +++ b/av/__main__.py @@ -12,11 +12,10 @@ def main(): if args.version: + import av import av._core - print("PyAV v" + av._core.pyav_version) - print("git origin: git@github.com:PyAV-Org/PyAV") - print("git commit:", av._core.pyav_commit) + print("PyAV v" + av.__version__) by_config = {} for libname, config in sorted(av._core.library_meta.items()): diff --git a/av/_core.pyx b/av/_core.pyx index e126580d7..b2a6e83bd 100644 --- a/av/_core.pyx +++ b/av/_core.pyx @@ -8,9 +8,6 @@ lib.avdevice_register_all() # Exports. time_base = lib.AV_TIME_BASE -pyav_version = lib.PYAV_VERSION_STR -pyav_commit = lib.PYAV_COMMIT_STR - cdef decode_version(v): if v < 0: diff --git a/av/about.py b/av/about.py new file mode 100644 index 000000000..777de5c43 --- /dev/null +++ b/av/about.py @@ -0,0 +1 @@ +__version__ = "8.1.1.dev0" diff --git a/docs/conf.py b/docs/conf.py index 795627b83..6575a3530 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -79,8 +79,12 @@ # |version| and |release|, also used in various other places throughout the # built documents. # +about = {} +with open('../av/about.py') as fp: + exec(fp.read(), about) + # The full version, including alpha/beta/rc tags. -release = open('../VERSION.txt').read().strip() +release = about['__version__'] # The short X.Y version. version = release.split('-')[0] diff --git a/include/libav.pxd b/include/libav.pxd index 60099df04..b9bfe3943 100644 --- a/include/libav.pxd +++ b/include/libav.pxd @@ -1,11 +1,3 @@ - -# This file is built by setup.py and contains macros telling us which libraries -# and functions we have (of those which are different between FFMpeg and LibAV). -cdef extern from "pyav/config.h" nogil: - - char* PYAV_VERSION_STR - char* PYAV_COMMIT_STR - include "libavutil/avutil.pxd" include "libavutil/channel_layout.pxd" include "libavutil/dict.pxd" diff --git a/include/libav.pyav.h b/include/libav.pyav.h deleted file mode 100644 index e69de29bb..000000000 diff --git a/setup.py b/setup.py index 9ec50402f..f244150f6 100644 --- a/setup.py +++ b/setup.py @@ -1,54 +1,53 @@ -from shlex import quote -from subprocess import PIPE, Popen import argparse -import errno import os import platform import re import shlex +import subprocess import sys +from Cython.Build import cythonize +from Cython.Compiler.AutoDocTransforms import EmbedSignature from setuptools import Command, Extension, find_packages, setup from setuptools.command.build_ext import build_ext -try: - from Cython import __version__ as cython_version - from Cython.Build import cythonize -except ImportError: - cythonize = None -else: - # We depend upon some features in Cython 0.27; reject older ones. - if tuple(map(int, cython_version.split('.'))) < (0, 27): - print("Cython {} is too old for PyAV; ignoring it.".format(cython_version)) - cythonize = None - - -# We will embed this metadata into the package so it can be recalled for debugging. -version = open('VERSION.txt').read().strip() -try: - git_commit, _ = Popen(['git', 'describe', '--tags'], stdout=PIPE, stderr=PIPE).communicate() -except OSError: - git_commit = None -else: - git_commit = git_commit.decode().strip() +FFMPEG_DIR = None +FFMPEG_LIBRARIES = [ + "avformat", + "avcodec", + "avdevice", + "avutil", + "avfilter", + "swscale", + "swresample", +] + +# Read package metadata +about = {} +about_file = os.path.join(os.path.dirname(__file__), "av", "about.py") +with open(about_file, encoding="utf-8") as fp: + exec(fp.read(), about) _cflag_parser = argparse.ArgumentParser(add_help=False) -_cflag_parser.add_argument('-I', dest='include_dirs', action='append') -_cflag_parser.add_argument('-L', dest='library_dirs', action='append') -_cflag_parser.add_argument('-l', dest='libraries', action='append') -_cflag_parser.add_argument('-D', dest='define_macros', action='append') -_cflag_parser.add_argument('-R', dest='runtime_library_dirs', action='append') +_cflag_parser.add_argument("-I", dest="include_dirs", action="append") +_cflag_parser.add_argument("-L", dest="library_dirs", action="append") +_cflag_parser.add_argument("-l", dest="libraries", action="append") +_cflag_parser.add_argument("-D", dest="define_macros", action="append") +_cflag_parser.add_argument("-R", dest="runtime_library_dirs", action="append") + + def parse_cflags(raw_cflags): raw_args = shlex.split(raw_cflags.strip()) args, unknown = _cflag_parser.parse_known_args(raw_args) config = {k: v or [] for k, v in args.__dict__.items()} - for i, x in enumerate(config['define_macros']): - parts = x.split('=', 1) + for i, x in enumerate(config["define_macros"]): + parts = x.split("=", 1) value = x[1] or None if len(x) == 2 else None - config['define_macros'][i] = (parts[0], value) - return config, ' '.join(quote(x) for x in unknown) + config["define_macros"][i] = (parts[0], value) + return config, " ".join(shlex.quote(x) for x in unknown) + def get_library_config(name): """Get distutils-compatible extension extras for the given library. @@ -57,16 +56,15 @@ def get_library_config(name): """ try: - proc = Popen(['pkg-config', '--cflags', '--libs', name], stdout=PIPE, stderr=PIPE) - except OSError: - print('pkg-config is required for building PyAV') + raw_cflags = subprocess.check_output(["pkg-config", "--cflags", "--libs", name]) + except FileNotFoundError: + print("pkg-config is required for building PyAV") + exit(1) + except subprocess.CalledProcessError: + print("pkg-config could not find library {}".format(name)) exit(1) - raw_cflags, err = proc.communicate() - if proc.wait(): - return - - known, unknown = parse_cflags(raw_cflags.decode('utf8')) + known, unknown = parse_cflags(raw_cflags.decode("utf-8")) if unknown: print("pkg-config returned flags we don't understand: {}".format(unknown)) exit(1) @@ -92,28 +90,27 @@ def unique_extend(a, *args): # Obtain the ffmpeg dir from the "--ffmpeg-dir=" argument -FFMPEG_DIR = None for i, arg in enumerate(sys.argv): - if arg.startswith('--ffmpeg-dir='): - FFMPEG_DIR = arg.split('=')[1] + if arg.startswith("--ffmpeg-dir="): + FFMPEG_DIR = arg.split("=")[1] break if FFMPEG_DIR is not None: # delete the --ffmpeg-dir arg so that distutils does not see it del sys.argv[i] if not os.path.isdir(FFMPEG_DIR): - print('The specified ffmpeg directory does not exist') + print("The specified ffmpeg directory does not exist") exit(1) else: # Check the environment variable FFMPEG_DIR - FFMPEG_DIR = os.environ.get('FFMPEG_DIR') + FFMPEG_DIR = os.environ.get("FFMPEG_DIR") if FFMPEG_DIR is not None: if not os.path.isdir(FFMPEG_DIR): FFMPEG_DIR = None if FFMPEG_DIR is not None: - ffmpeg_lib = os.path.join(FFMPEG_DIR, 'lib') - ffmpeg_include = os.path.join(FFMPEG_DIR, 'include') + ffmpeg_lib = os.path.join(FFMPEG_DIR, "lib") + ffmpeg_include = os.path.join(FFMPEG_DIR, "include") if os.path.exists(ffmpeg_lib): ffmpeg_lib = [ffmpeg_lib] else: @@ -130,64 +127,53 @@ def unique_extend(a, *args): # The "extras" to be supplied to every one of our modules. # This is expanded heavily by the `config` command. extension_extra = { - 'include_dirs': ['include'] + ffmpeg_include, # The first are PyAV's includes. - 'libraries' : [], - 'library_dirs': ffmpeg_lib, -} - -# The macros which describe the current PyAV version. -config_macros = { - "PYAV_VERSION": version, - "PYAV_VERSION_STR": '"%s"' % version, - "PYAV_COMMIT_STR": '"%s"' % (git_commit or 'unknown-commit'), + "include_dirs": ["include"] + ffmpeg_include, # The first are PyAV's includes. + "libraries": [], + "library_dirs": ffmpeg_lib, } def dump_config(): """Print out all the config information we have so far (for debugging).""" - print('PyAV:', version, git_commit or '(unknown commit)') - print('Python:', sys.version.encode('unicode_escape').decode()) - print('platform:', platform.platform()) - print('extension_extra:') + print("PyAV:", about["__version__"]) + print("Python:", sys.version) + print("platform:", platform.platform()) + print("extension_extra:") for k, vs in extension_extra.items(): - print('\t%s: %s' % (k, [x.encode('utf8') for x in vs])) - print('config_macros:') - for x in sorted(config_macros.items()): - print('\t%s=%s' % x) + print("\t%s: %s" % (k, vs)) # Monkey-patch Cython to not overwrite embedded signatures. -if cythonize: +old_embed_signature = EmbedSignature._embed_signature - from Cython.Compiler.AutoDocTransforms import EmbedSignature - old_embed_signature = EmbedSignature._embed_signature - def new_embed_signature(self, sig, doc): +def new_embed_signature(self, sig, doc): - # Strip any `self` parameters from the front. - sig = re.sub(r'\(self(,\s+)?', '(', sig) + # Strip any `self` parameters from the front. + sig = re.sub(r"\(self(,\s+)?", "(", sig) - # If they both start with the same signature; skip it. - if sig and doc: - new_name = sig.split('(')[0].strip() - old_name = doc.split('(')[0].strip() - if new_name == old_name: - return doc - if new_name.endswith('.' + old_name): - return doc + # If they both start with the same signature; skip it. + if sig and doc: + new_name = sig.split("(")[0].strip() + old_name = doc.split("(")[0].strip() + if new_name == old_name: + return doc + if new_name.endswith("." + old_name): + return doc - return old_embed_signature(self, sig, doc) + return old_embed_signature(self, sig, doc) - EmbedSignature._embed_signature = new_embed_signature + +EmbedSignature._embed_signature = new_embed_signature # Construct the modules that we find in the "av" directory. ext_modules = [] -for dirname, dirnames, filenames in os.walk('av'): +for dirname, dirnames, filenames in os.walk("av"): for filename in filenames: # We are looing for Cython sources. - if filename.startswith('.') or os.path.splitext(filename)[1] != '.pyx': + if filename.startswith(".") or os.path.splitext(filename)[1] != ".pyx": continue pyx_path = os.path.join(dirname, filename) @@ -195,99 +181,57 @@ def new_embed_signature(self, sig, doc): # Need to be a little careful because Windows will accept / or \ # (where os.sep will be \ on Windows). - mod_name = base.replace('/', '.').replace(os.sep, '.') - - c_path = os.path.join('src', base + '.c') + mod_name = base.replace("/", ".").replace(os.sep, ".") - # We go with the C sources if Cython is not installed, and fail if - # those also don't exist. We can't `cythonize` here though, since the - # `pyav/include.h` must be generated (by `build_ext`) first. - if not cythonize and not os.path.exists(c_path): - print('Cython is required to build PyAV from raw sources.') - print('Please `pip install Cython`.') - exit(3) - ext_modules.append(Extension( - mod_name, - sources=[c_path if not cythonize else pyx_path], - )) + ext_modules.append(Extension(mod_name, sources=[pyx_path])) class ConfigCommand(Command): user_options = [ - ('no-pkg-config', None, - "do not use pkg-config to configure dependencies"), - ('verbose', None, - "dump out configuration"), - ('compiler=', 'c', - "specify the compiler type"), ] + ("no-pkg-config", None, "do not use pkg-config to configure dependencies"), + ("verbose", None, "dump out configuration"), + ("compiler=", "c", "specify the compiler type"), + ] - boolean_options = ['no-pkg-config'] + boolean_options = ["no-pkg-config"] def initialize_options(self): self.compiler = None self.no_pkg_config = None def finalize_options(self): - self.set_undefined_options('build', - ('compiler', 'compiler'),) - self.set_undefined_options('build_ext', - ('no_pkg_config', 'no_pkg_config'),) + self.set_undefined_options("build", ("compiler", "compiler")) + self.set_undefined_options("build_ext", ("no_pkg_config", "no_pkg_config")) def run(self): # For some reason we get the feeling that CFLAGS is not respected, so we parse # it here. TODO: Leave any arguments that we can't figure out. - for name in 'CFLAGS', 'LDFLAGS': - known, unknown = parse_cflags(os.environ.pop(name, '')) + for name in "CFLAGS", "LDFLAGS": + known, unknown = parse_cflags(os.environ.pop(name, "")) if unknown: - print("Warning: We don't understand some of {} (and will leave it in the envvar): {}".format(name, unknown)) + print( + "Warning: We don't understand some of {} (and will leave it in the envvar): {}".format( + name, unknown + ) + ) os.environ[name] = unknown update_extend(extension_extra, known) # Check if we're using pkg-config or not if self.no_pkg_config: # Simply assume we have everything we need! - config = { - 'libraries': ['avformat', 'avcodec', 'avdevice', 'avutil', 'avfilter', - 'swscale', 'swresample'], - 'library_dirs': [], - 'include_dirs': [] - } - update_extend(extension_extra, config) - for ext in self.distribution.ext_modules: - for key, value in extension_extra.items(): - setattr(ext, key, value) - return - - # We're using pkg-config: - errors = [] - - # Get the config for the libraries that we require. - for name in 'libavformat', 'libavcodec', 'libavdevice', 'libavutil', 'libavfilter', 'libswscale', 'libswresample': - config = get_library_config(name) - if config: - update_extend(extension_extra, config) - # We don't need macros for these, since they all must exist. - else: - errors.append('Could not find ' + name + ' with pkg-config.') + update_extend(extension_extra, {"libraries": FFMPEG_LIBRARIES}) + else: + # Get the config for the libraries that we require. + for name in FFMPEG_LIBRARIES: + update_extend(extension_extra, get_library_config("lib" + name)) if self.verbose: dump_config() - # Don't continue if we have errors. - # TODO: Warn Ubuntu 12 users that they can't satisfy requirements with the - # default package sources. - if errors: - print('\n'.join(errors)) - exit(1) - - # Normalize the extras. - extension_extra.update( - dict((k, sorted(set(v))) for k, v in extension_extra.items()) - ) - - # Apply them. + # Apply configuration to all modules. for ext in self.distribution.ext_modules: for key, value in extension_extra.items(): setattr(ext, key, value) @@ -296,8 +240,10 @@ def run(self): class CythonizeCommand(Command): user_options = [] + def initialize_options(self): pass + def finalize_options(self): pass @@ -306,16 +252,16 @@ def run(self): # Cythonize, if required. We do it individually since we must update # the existing extension instead of replacing them all. for i, ext in enumerate(self.distribution.ext_modules): - if any(s.endswith('.pyx') for s in ext.sources): + if any(s.endswith(".pyx") for s in ext.sources): new_ext = cythonize( ext, compiler_directives=dict( - c_string_type='str', - c_string_encoding='ascii', + c_string_type="str", + c_string_encoding="ascii", embedsignature=True, language_level=2, ), - build_dir='src', + build_dir="src", include_path=ext.include_dirs, )[0] ext.sources = new_ext.sources @@ -323,112 +269,82 @@ def run(self): class BuildExtCommand(build_ext): - if os.name != 'nt': + if os.name != "nt": user_options = build_ext.user_options + [ - ('no-pkg-config', None, - "do not use pkg-config to configure dependencies")] + ("no-pkg-config", None, "do not use pkg-config to configure dependencies") + ] - boolean_options = build_ext.boolean_options + ['no-pkg-config'] + boolean_options = build_ext.boolean_options + ["no-pkg-config"] def initialize_options(self): build_ext.initialize_options(self) self.no_pkg_config = None + else: no_pkg_config = 1 def run(self): # Propagate build options to config - obj = self.distribution.get_command_obj('config') + obj = self.distribution.get_command_obj("config") obj.compiler = self.compiler obj.no_pkg_config = self.no_pkg_config obj.include_dirs = self.include_dirs obj.libraries = self.libraries obj.library_dirs = self.library_dirs - self.run_command('config') - - # We write a header file containing everything we have discovered by - # inspecting the libraries which exist. This is the main mechanism we - # use to detect differenced between FFmpeg and Libav. - - include_dir = os.path.join(self.build_temp, 'include') - pyav_dir = os.path.join(include_dir, 'pyav') - try: - os.makedirs(pyav_dir) - except OSError as e: - if e.errno != errno.EEXIST: - raise - header_path = os.path.join(pyav_dir, 'config.h') - print('writing', header_path) - with open(header_path, 'w') as fh: - fh.write('#ifndef PYAV_COMPAT_H\n') - fh.write('#define PYAV_COMPAT_H\n') - for k, v in sorted(config_macros.items()): - fh.write('#define %s %s\n' % (k, v)) - fh.write('#endif\n') - - self.include_dirs = self.include_dirs or [] - self.include_dirs.append(include_dir) + self.run_command("config") + # Propagate config to cythonize. for i, ext in enumerate(self.distribution.ext_modules): unique_extend(ext.include_dirs, self.include_dirs) unique_extend(ext.library_dirs, self.library_dirs) unique_extend(ext.libraries, self.libraries) - self.run_command('cythonize') + self.run_command("cythonize") build_ext.run(self) setup( - - name='av', - version=version, + name="av", + version=about["__version__"], description="Pythonic bindings for FFmpeg's libraries.", - author="Mike Boers", author_email="pyav@mikeboers.com", - url="https://github.com/PyAV-Org/PyAV", - - packages=find_packages(exclude=['build*', 'examples*', 'scratchpad*', 'tests*']), - + packages=find_packages(exclude=["build*", "examples*", "scratchpad*", "tests*"]), zip_safe=False, ext_modules=ext_modules, - cmdclass={ - 'build_ext': BuildExtCommand, - 'config': ConfigCommand, - 'cythonize': CythonizeCommand, + "build_ext": BuildExtCommand, + "config": ConfigCommand, + "cythonize": CythonizeCommand, }, - - test_suite='tests', - + test_suite="tests", entry_points={ - 'console_scripts': [ - 'pyav = av.__main__:main', + "console_scripts": [ + "pyav = av.__main__:main", ], }, - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Natural Language :: English', - 'Operating System :: MacOS :: MacOS X', - 'Operating System :: POSIX', - 'Operating System :: Unix', - 'Operating System :: Microsoft :: Windows', - 'Programming Language :: Cython', - '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', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Topic :: Multimedia :: Sound/Audio', - 'Topic :: Multimedia :: Sound/Audio :: Conversion', - 'Topic :: Multimedia :: Video', - 'Topic :: Multimedia :: Video :: Conversion', - ], + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Natural Language :: English", + "Operating System :: MacOS :: MacOS X", + "Operating System :: POSIX", + "Operating System :: Unix", + "Operating System :: Microsoft :: Windows", + "Programming Language :: Cython", + "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", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Multimedia :: Sound/Audio", + "Topic :: Multimedia :: Sound/Audio :: Conversion", + "Topic :: Multimedia :: Video", + "Topic :: Multimedia :: Video :: Conversion", + ], ) From fc9f927cedbb1637426725db490385ba58e06890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Mon, 7 Mar 2022 10:32:58 +0100 Subject: [PATCH 030/846] [docs] restructure documentation - remove obsolete instructions for old Ubuntu and libav - move "Unsupported features" to the "caveats" section - merge the "about" into "installation" --- docs/overview/about.rst | 75 ------------------------ docs/overview/caveats.rst | 21 ++++++- docs/overview/installation.rst | 101 ++++++++++++++++----------------- 3 files changed, 68 insertions(+), 129 deletions(-) delete mode 100644 docs/overview/about.rst diff --git a/docs/overview/about.rst b/docs/overview/about.rst deleted file mode 100644 index 2fa2f8dd5..000000000 --- a/docs/overview/about.rst +++ /dev/null @@ -1,75 +0,0 @@ -More About PyAV -=============== - -Binary wheels -------------- - -Since release 8.0.0 binary wheels are provided on PyPI for Linux, Mac and Windows linked against FFmpeg. Currently FFmpeg 4.3.3 is used with the following features enabled for all platforms: - -- fontconfig -- gmp -- gnutls -- libaom -- libass -- libbluray -- libdav1d -- libfreetype -- libmp3lame -- libopencore-amrnb -- libopencore-amrwb -- libopenjpeg -- libopus -- libspeex -- libtheora -- libtwolame -- libvorbis -- libwavpack -- libx264 -- libx265 -- libxml2 -- libxvid -- lzma -- zlib - -Bring your own FFmpeg ---------------------- - -PyAV can also be compiled against your own build of FFmpeg. While it must be built for the specific FFmpeg version installed it does not require a specific version. You can force installing PyAV from source by running: - -.. code-block:: bash - - pip install av --no-binary av - - -We automatically detect the differences that we depended on at build time. This is a fairly trial-and-error process, so please let us know if something won't compile due to missing functions or members. - -Additionally, we are far from wrapping the full extents of the libraries. There are many functions and C struct members which are currently unexposed. - - -Dropping Libav --------------- - -Until mid-2018 PyAV supported either FFmpeg_ or Libav_. The split support in the community essentially required we do so. That split has largely been resolved as distributions have returned to shipping FFmpeg instead of Libav. - -While we could have theoretically continued to support both, it has been years since automated testing of PyAV with Libav passed, and we received zero complaints. Supporting both also restricted us to using the subset of both, which was starting to erode at the cleanliness of PyAV. - -Many Libav-isms remain in PyAV, and we will slowly scrub them out to clean up PyAV as we come across them again. - - -Unsupported Features --------------------- - -Our goal is to provide all of the features that make sense for the contexts that PyAV would be used in. If there is something missing, please reach out on Gitter_ or open a feature request on GitHub_ (or even better a pull request). Your request will be more likely to be addressed if you can point to the relevant `FFmpeg API documentation `__. - -There are some features we may elect to not implement because we don't believe they fit the PyAV ethos. The only one that we've encountered so far is hardware decoding. The `FFmpeg man page `__ discusses the drawback of ``-hwaccel``: - - Note that most acceleration methods are intended for playback and will not be faster than software decoding on modern CPUs. Additionally, ``ffmpeg`` will usually need to copy the decoded frames from the GPU memory into the system memory, resulting in further performance loss. - -Since PyAV is not expected to be used in a high performance playback loop, we do not find the added code complexity worth the benefits of supporting this feature - - -.. _FFmpeg: https://ffmpeg.org/ -.. _Libav: https://libav.org/ - -.. _Gitter: https://gitter.im/PyAV-Org -.. _GitHub: https://github.com/PyAV-Org/pyav diff --git a/docs/overview/caveats.rst b/docs/overview/caveats.rst index 5a8eb32d5..5ccac3ffb 100644 --- a/docs/overview/caveats.rst +++ b/docs/overview/caveats.rst @@ -6,13 +6,23 @@ Caveats Authority of Documentation -------------------------- -FFmpeg is extremely complex, and the PyAV developers have not been successful in making it 100% clear to themselves in all aspects. Our understanding of how it works and how to work with it is via reading the docs, digging through the source, perfoming experiments, and hearing from users where PyAV isn't doing the right thing. +FFmpeg_ is extremely complex, and the PyAV developers have not been successful in making it 100% clear to themselves in all aspects. Our understanding of how it works and how to work with it is via reading the docs, digging through the source, perfoming experiments, and hearing from users where PyAV isn't doing the right thing. Only where this documentation is about the mechanics of PyAV can it be considered authoritative. Anywhere that we discuss something that is actually about the underlying FFmpeg libraries comes with the caveat that we can not always be 100% on it. -It is, unfortunately, often on the user the understand and deal with the edge cases. We encourage you to bring them to our attension via GitHub_ so that we can try to make PyAV deal with it, but we can't always make it work. +It is, unfortunately, often on the user the understand and deal with the edge cases. We encourage you to bring them to our attention via GitHub_ so that we can try to make PyAV deal with it, but we can't always make it work. -.. _GitHub: https://github.com/PyAv-Org/PyAV/issues + +Unsupported Features +-------------------- + +Our goal is to provide all of the features that make sense for the contexts that PyAV would be used in. If there is something missing, please reach out on Gitter_ or open a feature request on GitHub_ (or even better a pull request). Your request will be more likely to be addressed if you can point to the relevant `FFmpeg API documentation `__. + +There are some features we may elect to not implement because we don't believe they fit the PyAV ethos. The only one that we've encountered so far is hardware decoding. The `FFmpeg man page `__ discusses the drawback of ``-hwaccel``: + + Note that most acceleration methods are intended for playback and will not be faster than software decoding on modern CPUs. Additionally, ``ffmpeg`` will usually need to copy the decoded frames from the GPU memory into the system memory, resulting in further performance loss. + +Since PyAV is not expected to be used in a high performance playback loop, we do not find the added code complexity worth the benefits of supporting this feature. Sub-Interpeters @@ -40,3 +50,8 @@ Until we resolve this issue, you should explicitly call :meth:`.Container.close` with av.open(path) as fh: # Do stuff with it. + + +.. _FFmpeg: https://ffmpeg.org/ +.. _Gitter: https://gitter.im/PyAV-Org +.. _GitHub: https://github.com/PyAV-Org/pyav diff --git a/docs/overview/installation.rst b/docs/overview/installation.rst index 5a96b8bc1..74428a76c 100644 --- a/docs/overview/installation.rst +++ b/docs/overview/installation.rst @@ -1,20 +1,64 @@ Installation ============ +Binary wheels +------------- + +Since release 8.0.0 binary wheels are provided on PyPI for Linux, Mac and Windows linked against FFmpeg. The most straight-forward way to install PyAV is to run: + +.. code-block:: bash + + pip install av + + +Currently FFmpeg 4.3.3 is used with the following features enabled for all platforms: + +- fontconfig +- gmp +- gnutls +- libaom +- libass +- libbluray +- libdav1d +- libfreetype +- libmp3lame +- libopencore-amrnb +- libopencore-amrwb +- libopenjpeg +- libopus +- libspeex +- libtheora +- libtwolame +- libvorbis +- libwavpack +- libx264 +- libx265 +- libxml2 +- libxvid +- lzma +- zlib + + Conda ----- -Due to the complexity of the dependencies, PyAV is not always the easiest Python package to install. The most straight-foward install is via `conda-forge `_:: +Another way to install PyAV is via `conda-forge `_:: conda install av -c conda-forge See the `Conda quick install `_ docs to get started with (mini)Conda. -Dependencies ------------- +Bring your own FFmpeg +--------------------- + +PyAV can also be compiled against your own build of FFmpeg ((version ``4.0`` or higher). You can force installing PyAV from source by running: -PyAV depends upon several libraries from FFmpeg (version ``4.0`` or higher): +.. code-block:: bash + + pip install av --no-binary av + +PyAV depends upon several libraries from FFmpeg: - ``libavcodec`` - ``libavdevice`` @@ -54,39 +98,6 @@ On **Ubuntu 18.04 LTS** everything can come from the default sources:: libavutil-dev libswscale-dev libswresample-dev libavfilter-dev -Ubuntu < 18.04 LTS -^^^^^^^^^^^^^^^^^^ - -On older Ubuntu releases you will be unable to satisfy these requirements with the default package sources. We recommend compiling and installing FFmpeg from source. For FFmpeg:: - - sudo apt install \ - autoconf \ - automake \ - build-essential \ - cmake \ - libass-dev \ - libfreetype6-dev \ - libjpeg-dev \ - libtheora-dev \ - libtool \ - libvorbis-dev \ - libx264-dev \ - pkg-config \ - wget \ - yasm \ - zlib1g-dev - - wget http://ffmpeg.org/releases/ffmpeg-3.2.tar.bz2 - tar -xjf ffmpeg-3.2.tar.bz2 - cd ffmpeg-3.2 - - ./configure --disable-static --enable-shared --disable-doc - make - sudo make install - -`See this script `_ for a very detailed installation of all dependencies. - - Windows ^^^^^^^ @@ -95,20 +106,8 @@ It is possible to build PyAV on Windows without Conda by installing FFmpeg yours Unpack them somewhere (like ``C:\ffmpeg``), and then :ref:`tell PyAV where they are located `. - -PyAV ----- - - -Via PyPI/CheeseShop -^^^^^^^^^^^^^^^^^^^ -:: - - pip install av - - -Via Source -^^^^^^^^^^ +Building from the latest source +------------------------------- :: From 27a3bff0b6578b4db9c83ceaa9eb32b837e8fd94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Mon, 7 Mar 2022 09:37:14 +0100 Subject: [PATCH 031/846] [package] update ffmpeg binaries to enable xcb on Linux (fixes: #885) --- scripts/fetch-vendor.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/fetch-vendor.json b/scripts/fetch-vendor.json index 30c99fa35..26cf77d90 100644 --- a/scripts/fetch-vendor.json +++ b/scripts/fetch-vendor.json @@ -1,3 +1,3 @@ { - "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/4.3.3-1/ffmpeg-{platform}.tar.gz"] + "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/4.3.3-2/ffmpeg-{platform}.tar.gz"] } From 033a9e8ba5b3af07a07a373409de1e2728ffe2e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Mon, 7 Mar 2022 17:20:57 +0100 Subject: [PATCH 032/846] [audio] make resampler consistently flush buffer The previous behaviour when passing `None` to AudioResampler.resample() was not consistent: - the "passthrough" resampler returned `[None]` - actual resamplers were never flushed, dropping the final frames Now: - AudioResampler.resample() never returns `None` frames - actual resamplers are correctly flushed --- av/audio/resampler.pyx | 15 ++-- tests/test_audioresampler.py | 138 +++++++++++++++++++++++++++++++---- 2 files changed, 133 insertions(+), 20 deletions(-) diff --git a/av/audio/resampler.pyx b/av/audio/resampler.pyx index 416b1183c..bf4f33ac7 100644 --- a/av/audio/resampler.pyx +++ b/av/audio/resampler.pyx @@ -39,18 +39,19 @@ cdef class AudioResampler(object): Convert the ``sample_rate``, ``channel_layout`` and/or ``format`` of a :class:`~.AudioFrame`. - :param AudioFrame frame: The frame to convert. + :param AudioFrame frame: The frame to convert or `None` to flush. :returns: A list of :class:`AudioFrame` in new parameters. If the nothing is to be done return the same frame as a single element list. """ - if self.is_passthrough: - return [frame] - # We don't have any input, so don't bother even setting up. - if not frame: + if not self.graph and frame is None: return [] + # Shortcut for passthrough. + if self.is_passthrough: + return [frame] + # Take source settings from the first frame. if not self.graph: self.template = frame @@ -89,7 +90,7 @@ cdef class AudioResampler(object): if self.frame_size > 0: lib.av_buffersink_set_frame_size((abuffersink).ptr, self.frame_size) - elif frame: + elif frame is not None: # Assert the settings are the same on consecutive frames. if ( @@ -105,6 +106,8 @@ cdef class AudioResampler(object): while True: try: output.append(self.graph.pull()) + except EOFError: + break except av.utils.AVError as e: if e.errno != errno.EAGAIN: raise diff --git a/tests/test_audioresampler.py b/tests/test_audioresampler.py index 3558923aa..fe1907c14 100644 --- a/tests/test_audioresampler.py +++ b/tests/test_audioresampler.py @@ -6,76 +6,186 @@ class TestAudioResampler(TestCase): - def test_identity_passthrough(self): + def test_flush_immediately(self): + """ + If we flush the resampler before passing any input, it returns + a `None` frame without setting up the graph. + """ + + resampler = AudioResampler() - # If we don't ask it to do anything, it won't. + # flush + oframes = resampler.resample(None) + self.assertEqual(len(oframes), 0) + + def test_identity_passthrough(self): + """ + If we don't ask it to do anything, it won't. + """ resampler = AudioResampler() + # resample one frame iframe = AudioFrame("s16", "stereo", 1024) - oframe = resampler.resample(iframe)[0] - self.assertIs(iframe, oframe) + oframes = resampler.resample(iframe) + self.assertEqual(len(oframes), 1) + self.assertIs(iframe, oframes[0]) - def test_matching_passthrough(self): + # resample another frame + iframe.pts = 1024 + + oframes = resampler.resample(iframe) + self.assertEqual(len(oframes), 1) + self.assertIs(iframe, oframes[0]) - # If the frames match, it won't do anything. + # flush + oframes = resampler.resample(None) + self.assertEqual(len(oframes), 0) + + def test_matching_passthrough(self): + """ + If the frames match, it won't do anything. + """ resampler = AudioResampler("s16", "stereo") + # resample one frame iframe = AudioFrame("s16", "stereo", 1024) - oframe = resampler.resample(iframe)[0] - self.assertIs(iframe, oframe) + oframes = resampler.resample(iframe) + self.assertEqual(len(oframes), 1) + self.assertIs(iframe, oframes[0]) + + # resample another frame + iframe.pts = 1024 + + oframes = resampler.resample(iframe) + self.assertEqual(len(oframes), 1) + self.assertIs(iframe, oframes[0]) + + # flush + oframes = resampler.resample(None) + self.assertEqual(len(oframes), 0) def test_pts_assertion_same_rate(self): resampler = AudioResampler("s16", "mono") + # resample one frame iframe = AudioFrame("s16", "stereo", 1024) iframe.sample_rate = 48000 iframe.time_base = "1/48000" iframe.pts = 0 - oframe = resampler.resample(iframe)[0] + oframes = resampler.resample(iframe) + self.assertEqual(len(oframes), 1) + oframe = oframes[0] self.assertEqual(oframe.pts, 0) self.assertEqual(oframe.time_base, iframe.time_base) self.assertEqual(oframe.sample_rate, iframe.sample_rate) + self.assertEqual(oframe.samples, iframe.samples) + # resample another frame iframe.pts = 1024 - oframe = resampler.resample(iframe)[0] + oframes = resampler.resample(iframe) + self.assertEqual(len(oframes), 1) + + oframe = oframes[0] self.assertEqual(oframe.pts, 1024) self.assertEqual(oframe.time_base, iframe.time_base) self.assertEqual(oframe.sample_rate, iframe.sample_rate) + self.assertEqual(oframe.samples, iframe.samples) + # resample another frame with a pts gap, do not raise exception iframe.pts = 9999 - resampler.resample(iframe) # resampler should handle this without an exception + oframes = resampler.resample(iframe) + self.assertEqual(len(oframes), 1) + + oframe = oframes[0] + self.assertEqual(oframe.pts, 9999) + self.assertEqual(oframe.time_base, iframe.time_base) + self.assertEqual(oframe.sample_rate, iframe.sample_rate) + self.assertEqual(oframe.samples, iframe.samples) + + # flush + oframes = resampler.resample(None) + self.assertEqual(len(oframes), 0) def test_pts_assertion_new_rate(self): resampler = AudioResampler("s16", "mono", 44100) + # resample one frame iframe = AudioFrame("s16", "stereo", 1024) iframe.sample_rate = 48000 iframe.time_base = "1/48000" iframe.pts = 0 - oframe = resampler.resample(iframe)[0] + oframes = resampler.resample(iframe) + self.assertEqual(len(oframes), 1) + + oframe = oframes[0] self.assertEqual(oframe.pts, 0) - self.assertEqual(str(oframe.time_base), "1/44100") + self.assertEqual(oframe.time_base, Fraction(1, 44100)) self.assertEqual(oframe.sample_rate, 44100) + self.assertEqual(oframe.samples, 925) + + # flush + oframes = resampler.resample(None) + self.assertEqual(len(oframes), 1) + + oframe = oframes[0] + self.assertEqual(oframe.pts, 925) + self.assertEqual(oframe.time_base, Fraction(1, 44100)) + self.assertEqual(oframe.sample_rate, 44100) + self.assertEqual(oframe.samples, 16) def test_pts_missing_time_base(self): resampler = AudioResampler("s16", "mono", 44100) + # resample one frame iframe = AudioFrame("s16", "stereo", 1024) iframe.sample_rate = 48000 iframe.pts = 0 - oframe = resampler.resample(iframe)[0] + oframes = resampler.resample(iframe) + self.assertEqual(len(oframes), 1) + + oframe = oframes[0] self.assertEqual(oframe.pts, 0) self.assertEqual(oframe.time_base, Fraction(1, 44100)) self.assertEqual(oframe.sample_rate, 44100) + + # flush + oframes = resampler.resample(None) + self.assertEqual(len(oframes), 1) + + oframe = oframes[0] + self.assertEqual(oframe.pts, 925) + self.assertEqual(oframe.time_base, Fraction(1, 44100)) + self.assertEqual(oframe.sample_rate, 44100) + self.assertEqual(oframe.samples, 16) + + def test_mismatched_input(self): + """ + Consecutive frames must have the same layout, sample format and sample rate. + """ + resampler = AudioResampler("s16", "mono", 44100) + + # resample one frame + iframe = AudioFrame("s16", "stereo", 1024) + iframe.sample_rate = 48000 + resampler.resample(iframe) + + # resample another frame with a sample format + iframe = AudioFrame("s16", "mono", 1024) + iframe.sample_rate = 48000 + with self.assertRaises(ValueError) as cm: + resampler.resample(iframe) + self.assertEqual( + str(cm.exception), "Frame does not match AudioResampler setup." + ) From b9eb2683a26e1e0abbcb571b857acd342c09f7a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Mon, 7 Mar 2022 18:19:16 +0100 Subject: [PATCH 033/846] [audio] prune some useless imports and members --- av/audio/codeccontext.pxd | 2 -- av/audio/resampler.pxd | 5 ----- av/audio/resampler.pyx | 2 -- 3 files changed, 9 deletions(-) diff --git a/av/audio/codeccontext.pxd b/av/audio/codeccontext.pxd index 23d1a62a9..277d47780 100644 --- a/av/audio/codeccontext.pxd +++ b/av/audio/codeccontext.pxd @@ -1,5 +1,4 @@ -from av.audio.fifo cimport AudioFifo from av.audio.frame cimport AudioFrame from av.audio.resampler cimport AudioResampler from av.codec.context cimport CodecContext @@ -12,4 +11,3 @@ cdef class AudioCodecContext(CodecContext): # For encoding. cdef AudioResampler resampler - cdef AudioFifo fifo diff --git a/av/audio/resampler.pxd b/av/audio/resampler.pxd index b1c72e2a5..4fe78b54a 100644 --- a/av/audio/resampler.pxd +++ b/av/audio/resampler.pxd @@ -1,6 +1,3 @@ -from libc.stdint cimport uint64_t -cimport libav as lib - from av.audio.format cimport AudioFormat from av.audio.frame cimport AudioFrame from av.audio.layout cimport AudioLayout @@ -11,8 +8,6 @@ cdef class AudioResampler(object): cdef readonly bint is_passthrough - cdef lib.SwrContext *ptr - cdef AudioFrame template # Destination descriptors diff --git a/av/audio/resampler.pyx b/av/audio/resampler.pyx index bf4f33ac7..b1c6c0aad 100644 --- a/av/audio/resampler.pyx +++ b/av/audio/resampler.pyx @@ -1,11 +1,9 @@ -from libc.stdint cimport int64_t, uint8_t cimport libav as lib from av.filter.context cimport FilterContext import errno -from av.error import FFmpegError import av.filter From 9a7b7b0ca643786676c1b87f320509c14dc5d40e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Mon, 7 Mar 2022 19:50:53 +0100 Subject: [PATCH 034/846] Remove deprecated methods The following deprecated methods are removed: - AudioFrame.to_nd_array - VideoFrame.to_nd_array - Stream.seek --- av/audio/frame.pyx | 3 --- av/stream.pyx | 15 --------------- av/video/frame.pyx | 4 ---- tests/test_audioframe.py | 17 ----------------- tests/test_seek.py | 16 ++-------------- tests/test_videoframe.py | 16 ---------------- 6 files changed, 2 insertions(+), 69 deletions(-) diff --git a/av/audio/frame.pyx b/av/audio/frame.pyx index 549b91d86..97de3cc53 100644 --- a/av/audio/frame.pyx +++ b/av/audio/frame.pyx @@ -1,7 +1,6 @@ from av.audio.format cimport get_audio_format from av.audio.layout cimport get_audio_layout from av.audio.plane cimport AudioPlane -from av.deprecation import renamed_attr from av.error cimport err_check from av.utils cimport check_ndarray, check_ndarray_shape @@ -195,5 +194,3 @@ cdef class AudioFrame(Frame): # convert and return data return np.vstack([np.frombuffer(x, dtype=dtype, count=count) for x in self.planes]) - - to_nd_array = renamed_attr('to_ndarray') diff --git a/av/stream.pyx b/av/stream.pyx index c2a108e2a..4ac5d0ee9 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -13,8 +13,6 @@ from av.utils cimport ( to_avrational ) -from av import deprecation - cdef object _cinit_bypass_sentinel = object() @@ -175,19 +173,6 @@ cdef class Stream(object): """ return self.codec_context.decode(packet) - @deprecation.method - def seek(self, offset, **kwargs): - """ - .. seealso:: :meth:`.InputContainer.seek` for documentation on parameters. - The only difference is that ``offset`` will be interpreted in - :attr:`.Stream.time_base` when ``whence == 'time'``. - - .. deprecated:: 6.1.0 - Use :meth:`.InputContainer.seek` with ``stream`` argument instead. - - """ - self.container.seek(offset, stream=self, **kwargs) - property id: """ The format-specific ID of this stream. diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 311ab9c9f..09cbe98e5 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -6,8 +6,6 @@ from av.utils cimport check_ndarray, check_ndarray_shape from av.video.format cimport VideoFormat, get_pix_fmt, get_video_format from av.video.plane cimport VideoPlane -from av.deprecation import renamed_attr - cdef object _cinit_bypass_sentinel @@ -276,8 +274,6 @@ cdef class VideoFrame(Frame): else: raise ValueError('Conversion to numpy array with format `%s` is not yet supported' % frame.format.name) - to_nd_array = renamed_attr('to_ndarray') - @staticmethod def from_image(img): """ diff --git a/tests/test_audioframe.py b/tests/test_audioframe.py index a76526e62..f4ffeb661 100644 --- a/tests/test_audioframe.py +++ b/tests/test_audioframe.py @@ -1,9 +1,6 @@ -import warnings - import numpy from av import AudioFrame -from av.deprecation import AttributeRenamedWarning from .common import TestCase @@ -82,20 +79,6 @@ def test_basic_to_ndarray(self): self.assertEqual(array.dtype, "i2") self.assertEqual(array.shape, (2, 160)) - def test_basic_to_nd_array(self): - frame = AudioFrame(format="s16p", layout="stereo", samples=160) - with warnings.catch_warnings(record=True) as recorded: - array = frame.to_nd_array() - self.assertEqual(array.shape, (2, 160)) - - # check deprecation warning - self.assertEqual(len(recorded), 1) - self.assertEqual(recorded[0].category, AttributeRenamedWarning) - self.assertEqual( - str(recorded[0].message), - "AudioFrame.to_nd_array is deprecated; please use AudioFrame.to_ndarray.", - ) - def test_ndarray_dbl(self): layouts = [ ("dbl", "mono", "f8", (1, 160)), diff --git a/tests/test_seek.py b/tests/test_seek.py index 559e93b0b..75d6e4a09 100644 --- a/tests/test_seek.py +++ b/tests/test_seek.py @@ -1,7 +1,6 @@ from __future__ import division import unittest -import warnings import av @@ -27,7 +26,6 @@ class TestSeek(TestCase): def test_seek_float(self): container = av.open(fate_suite("h264/interlaced_crop.mp4")) self.assertRaises(TypeError, container.seek, 1.0) - self.assertRaises(TypeError, container.streams.video[0].seek, 1.0) def test_seek_int64(self): # Assert that it accepts large values. @@ -128,7 +126,7 @@ def test_decode_half(self): self.assertEqual(frame_count, total_frame_count - target_frame) - def test_stream_seek(self, use_deprecated_api=False): + def test_stream_seek(self): container = av.open(fate_suite("h264/interlaced_crop.mp4")) @@ -147,14 +145,7 @@ def test_stream_seek(self, use_deprecated_api=False): target_sec = target_frame * 1 / rate target_timestamp = int(target_sec / time_base) + video_stream.start_time - - if use_deprecated_api: - with warnings.catch_warnings(record=True) as captured: - video_stream.seek(target_timestamp) - self.assertEqual(len(captured), 1) - self.assertIn("Stream.seek is deprecated.", captured[0].message.args[0]) - else: - container.seek(target_timestamp, stream=video_stream) + container.seek(target_timestamp, stream=video_stream) current_frame = None frame_count = 0 @@ -173,9 +164,6 @@ def test_stream_seek(self, use_deprecated_api=False): self.assertEqual(frame_count, total_frame_count - target_frame) - def test_deprecated_stream_seek(self): - self.test_stream_seek(use_deprecated_api=True) - if __name__ == "__main__": unittest.main() diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 2f02cebb8..2ad81b2bd 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -1,10 +1,8 @@ from unittest import SkipTest -import warnings import numpy from av import VideoFrame -from av.deprecation import AttributeRenamedWarning from .common import Image, TestCase, fate_png @@ -159,20 +157,6 @@ def test_basic_to_ndarray(self): array = frame.to_ndarray() self.assertEqual(array.shape, (480, 640, 3)) - def test_basic_to_nd_array(self): - frame = VideoFrame(640, 480, "rgb24") - with warnings.catch_warnings(record=True) as recorded: - array = frame.to_nd_array() - self.assertEqual(array.shape, (480, 640, 3)) - - # check deprecation warning - self.assertEqual(len(recorded), 1) - self.assertEqual(recorded[0].category, AttributeRenamedWarning) - self.assertEqual( - str(recorded[0].message), - "VideoFrame.to_nd_array is deprecated; please use VideoFrame.to_ndarray.", - ) - def test_ndarray_gray(self): array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) for format in ["gray", "gray8"]: From 6d2d9aab4893491c7fecdd9923ab0fa014aef72e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Mon, 7 Mar 2022 15:44:29 +0100 Subject: [PATCH 035/846] Release v9.0.0 --- CHANGELOG.rst | 22 +++++++++++++++++++++- av/about.py | 2 +- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index dd1aae32f..fc8474573 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,9 +16,29 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch, currently "develop"). -v8.1.1.dev0 +v9.0.0 ------ +Major: + +- Re-implement AudioResampler with aformat and buffersink (:issue:`761`). + AudioResampler.resample() now returns a list of frames. +- Remove deprecated methods: AudioFrame.to_nd_array, VideoFrame.to_nd_array and Stream.seek. + +Minor: + +- Provide binary wheels for macOS/arm64 and Linux/aarch64. +- Simplify setup.py, require Cython. +- Update the installation instructions in favor of PyPI. +- Fix VideoFrame.to_image with height & width (:issue:`878`). +- Fix setting Stream time_base (:issue:`784`). +- Replace deprecated av_init_packet with av_packet_alloc (:issue:`872`). +- Validate pixel format in VideoCodecContext.pix_fmt setter (:issue:`815`). +- Fix AudioFrame ndarray conversion endianness (:issue:`833`). +- Improve time_base support with filters (:issue:`765`). +- Allow flushing filters by sending `None` (:issue:`886`). +- Avoid unnecessary vsnprintf() calls in log_callback() (:issue:`877`). +- Make Frame.from_ndarray raise ValueError instead of AssertionError. v8.1.0 ------ diff --git a/av/about.py b/av/about.py index 777de5c43..6dcf770e3 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "8.1.1.dev0" +__version__ = "9.0.0" From 6bf8b1f486d03c9742e81a97b0e3099597f47b07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Tue, 8 Mar 2022 08:25:39 +0100 Subject: [PATCH 036/846] Bump to next dev version. --- CHANGELOG.rst | 6 +++++- README.md | 2 +- av/about.py | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fc8474573..f707238ba 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,7 +14,11 @@ We are operating with `semantic versioning `_. . Note that they these tags will not actually close the issue/PR until they - are merged into the "default" branch, currently "develop"). + are merged into the "default" branch. + +v9.0.1.dev0 +------ + v9.0.0 ------ diff --git a/README.md b/README.md index 627d6336f..74eecefcb 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Due to the complexity of the dependencies, PyAV is not always the easiest Python pip install av ``` -If you want to use your existing FFmpeg/Libav, the C-source version of PyAV is on [PyPI][pypi] too: +If you want to use your existing FFmpeg, the source version of PyAV is on [PyPI][pypi] too: ```bash pip install av --no-binary av diff --git a/av/about.py b/av/about.py index 6dcf770e3..efedf53db 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "9.0.0" +__version__ = "9.0.1.dev0" From 0e3ff4cc51f7fdc9ee5184305df948c4da559103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Thu, 10 Mar 2022 07:36:07 +0100 Subject: [PATCH 037/846] [tests] test against FFmpeg 4.3, re-enable macOS tests --- .github/workflows/tests.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 706a70245..342606949 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -19,7 +19,7 @@ jobs: env: PYAV_PYTHON: python3 - PYAV_LIBRARY: ffmpeg-4.2 # doesn't matter + PYAV_LIBRARY: ffmpeg-4.3 # doesn't matter steps: @@ -55,11 +55,12 @@ jobs: strategy: matrix: config: - - {os: ubuntu-latest, python: 3.7, ffmpeg: "4.2", extras: true} + - {os: ubuntu-latest, python: 3.7, ffmpeg: "4.3", extras: true} + - {os: ubuntu-latest, python: 3.7, ffmpeg: "4.2"} - {os: ubuntu-latest, python: 3.7, ffmpeg: "4.1"} - {os: ubuntu-latest, python: 3.7, ffmpeg: "4.0"} - - {os: ubuntu-latest, python: pypy3, ffmpeg: "4.2"} - #- {os: macos-latest, python: 3.7, ffmpeg: "4.2"} + - {os: ubuntu-latest, python: pypy3, ffmpeg: "4.3"} + - {os: macos-latest, python: 3.7, ffmpeg: "4.3"} env: PYAV_PYTHON: python${{ matrix.config.python }} @@ -144,6 +145,7 @@ jobs: strategy: matrix: config: + - {os: windows-latest, python: 3.7, ffmpeg: "4.3"} - {os: windows-latest, python: 3.7, ffmpeg: "4.2"} - {os: windows-latest, python: 3.7, ffmpeg: "4.1"} - {os: windows-latest, python: 3.7, ffmpeg: "4.0"} From 210e61804660c626ea636699ff095fb04de48d33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Thu, 10 Mar 2022 11:21:53 +0100 Subject: [PATCH 038/846] [streams] prune some dead code - the `average_rate` is already defined on the base Stream class, we do not need to redefine it in VideoStream - prune unused imports from AudioStream and VideoStream - fix a typo in the `guessed_rate` docstring --- av/audio/stream.pyx | 1 - av/stream.pyx | 2 +- av/video/stream.pxd | 1 - av/video/stream.pyx | 11 ----------- 4 files changed, 1 insertion(+), 14 deletions(-) diff --git a/av/audio/stream.pyx b/av/audio/stream.pyx index a20356bb6..4e8f929ae 100644 --- a/av/audio/stream.pyx +++ b/av/audio/stream.pyx @@ -1,4 +1,3 @@ - cdef class AudioStream(Stream): def __repr__(self): diff --git a/av/stream.pyx b/av/stream.pyx index 4ac5d0ee9..cb083468b 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -260,7 +260,7 @@ cdef class Stream(object): """The guessed frame rate of this stream. This is a wrapper around :ffmpeg:`av_guess_frame_rate`, and uses multiple - huristics to decide what is "the" frame rate. + heuristics to decide what is "the" frame rate. :type: :class:`~fractions.Fraction` or ``None`` diff --git a/av/video/stream.pxd b/av/video/stream.pxd index 582cee22a..01b8d9d41 100644 --- a/av/video/stream.pxd +++ b/av/video/stream.pxd @@ -1,4 +1,3 @@ - from av.stream cimport Stream diff --git a/av/video/stream.pyx b/av/video/stream.pyx index 94b3cf369..70b8f3209 100644 --- a/av/video/stream.pyx +++ b/av/video/stream.pyx @@ -1,10 +1,3 @@ -from libc.stdint cimport int64_t -cimport libav as lib - -from av.container.core cimport Container -from av.utils cimport avrational_to_fraction - - cdef class VideoStream(Stream): def __repr__(self): @@ -17,7 +10,3 @@ cdef class VideoStream(Stream): self._codec_context.height, id(self), ) - - property average_rate: - def __get__(self): - return avrational_to_fraction(&self._stream.avg_frame_rate) From 4991d8780dae21e19503679a7d87291a650da5c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Thu, 10 Mar 2022 12:15:08 +0100 Subject: [PATCH 039/846] [tests] explicitly run memoryview() tests We *know* we are using Python 3, so drop the tests using buffer() and always run those using memoryview(). --- tests/test_subtitles.py | 5 +---- tests/test_videoframe.py | 15 +-------------- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/tests/test_subtitles.py b/tests/test_subtitles.py index 5f8f8cf41..6ec95b852 100644 --- a/tests/test_subtitles.py +++ b/tests/test_subtitles.py @@ -46,7 +46,4 @@ def test_vobsub(self): bms = sub.planes self.assertEqual(len(bms), 1) - if hasattr(__builtins__, "buffer"): - self.assertEqual(len(buffer(bms[0])), 4800) # noqa - if hasattr(__builtins__, "memoryview"): - self.assertEqual(len(memoryview(bms[0])), 4800) # noqa + self.assertEqual(len(memoryview(bms[0])), 4800) diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 2ad81b2bd..eba38e4f9 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -75,23 +75,10 @@ def test_rgb24_planes(self): class TestVideoFrameBuffers(TestCase): - def test_buffer(self): - if not hasattr(__builtins__, "buffer"): - raise SkipTest() - - frame = VideoFrame(640, 480, "rgb24") - frame.planes[0].update(b"01234" + (b"x" * (640 * 480 * 3 - 5))) - buf = buffer(frame.planes[0]) # noqa - self.assertEqual(buf[1], b"1") - self.assertEqual(buf[:7], b"01234xx") - def test_memoryview_read(self): - if not hasattr(__builtins__, "memoryview"): - raise SkipTest() - frame = VideoFrame(640, 480, "rgb24") frame.planes[0].update(b"01234" + (b"x" * (640 * 480 * 3 - 5))) - mem = memoryview(frame.planes[0]) # noqa + mem = memoryview(frame.planes[0]) self.assertEqual(mem.ndim, 1) self.assertEqual(mem.shape, (640 * 480 * 3,)) self.assertFalse(mem.readonly) From 7548d7b42f1a34033a6ee4cbe74d16652baefaab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Fri, 11 Mar 2022 00:19:38 +0100 Subject: [PATCH 040/846] [package] update FFmpeg binaries for wheels (fixes: #901) This updates several packages to fix security vulnerabilities. --- scripts/fetch-vendor.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/fetch-vendor.json b/scripts/fetch-vendor.json index 26cf77d90..0ea0a7bef 100644 --- a/scripts/fetch-vendor.json +++ b/scripts/fetch-vendor.json @@ -1,3 +1,3 @@ { - "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/4.3.3-2/ffmpeg-{platform}.tar.gz"] + "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/4.3.3-3/ffmpeg-{platform}.tar.gz"] } From 7a71cad474dfccd54421395a0f8ebd88f0230297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Fri, 11 Mar 2022 11:23:16 +0100 Subject: [PATCH 041/846] Release v9.0.1 --- CHANGELOG.rst | 5 ++++- av/about.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f707238ba..afdaddb4a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,9 +16,12 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. -v9.0.1.dev0 +v9.0.1 ------ +Minor: + +- Update binary wheels to fix security vulnerabilities (:issue:`901`). v9.0.0 ------ diff --git a/av/about.py b/av/about.py index efedf53db..ed588be5f 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "9.0.1.dev0" +__version__ = "9.0.1" From 37915a338c7bd8df8e757c3c98231ebb3b3c5eca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Fri, 11 Mar 2022 11:30:54 +0100 Subject: [PATCH 042/846] Bump to next dev version. --- CHANGELOG.rst | 4 ++++ av/about.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index afdaddb4a..c73b13c98 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,10 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. +v9.0.2.dev0 +------ + + v9.0.1 ------ diff --git a/av/about.py b/av/about.py index ed588be5f..2237cf396 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "9.0.1" +__version__ = "9.0.2.dev0" From 25a744c10bd54fabdab84d769a912e9b76e07d13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Fri, 11 Mar 2022 12:18:44 +0100 Subject: [PATCH 043/846] [package] simplify setup.py down to bare minimum (fixes: #844) --- .github/workflows/tests.yml | 3 +- setup.py | 337 +++++++++++------------------------- 2 files changed, 100 insertions(+), 240 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 342606949..6de5036b8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -194,8 +194,7 @@ jobs: run: | pip install cython python scripts/fetch-vendor.py /tmp/vendor - PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig make build - python setup.py sdist + PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig python setup.py sdist - name: Upload source package uses: actions/upload-artifact@v1 with: diff --git a/setup.py b/setup.py index f244150f6..02010f537 100644 --- a/setup.py +++ b/setup.py @@ -8,11 +8,9 @@ from Cython.Build import cythonize from Cython.Compiler.AutoDocTransforms import EmbedSignature -from setuptools import Command, Extension, find_packages, setup -from setuptools.command.build_ext import build_ext +from setuptools import Extension, find_packages, setup -FFMPEG_DIR = None FFMPEG_LIBRARIES = [ "avformat", "avcodec", @@ -23,45 +21,67 @@ "swresample", ] -# Read package metadata -about = {} -about_file = os.path.join(os.path.dirname(__file__), "av", "about.py") -with open(about_file, encoding="utf-8") as fp: - exec(fp.read(), about) +# Monkey-patch Cython to not overwrite embedded signatures. +old_embed_signature = EmbedSignature._embed_signature -_cflag_parser = argparse.ArgumentParser(add_help=False) -_cflag_parser.add_argument("-I", dest="include_dirs", action="append") -_cflag_parser.add_argument("-L", dest="library_dirs", action="append") -_cflag_parser.add_argument("-l", dest="libraries", action="append") -_cflag_parser.add_argument("-D", dest="define_macros", action="append") -_cflag_parser.add_argument("-R", dest="runtime_library_dirs", action="append") +def new_embed_signature(self, sig, doc): + + # Strip any `self` parameters from the front. + sig = re.sub(r"\(self(,\s+)?", "(", sig) + + # If they both start with the same signature; skip it. + if sig and doc: + new_name = sig.split("(")[0].strip() + old_name = doc.split("(")[0].strip() + if new_name == old_name: + return doc + if new_name.endswith("." + old_name): + return doc + + return old_embed_signature(self, sig, doc) -def parse_cflags(raw_cflags): - raw_args = shlex.split(raw_cflags.strip()) - args, unknown = _cflag_parser.parse_known_args(raw_args) - config = {k: v or [] for k, v in args.__dict__.items()} - for i, x in enumerate(config["define_macros"]): - parts = x.split("=", 1) - value = x[1] or None if len(x) == 2 else None - config["define_macros"][i] = (parts[0], value) - return config, " ".join(shlex.quote(x) for x in unknown) +EmbedSignature._embed_signature = new_embed_signature + + +def get_config_from_directory(ffmpeg_dir): + """ + Get distutils-compatible extension arguments for a specific directory. + """ + if not os.path.isdir(ffmpeg_dir): + print("The specified ffmpeg directory does not exist") + exit(1) + + include_dir = os.path.join(FFMPEG_DIR, "include") + library_dir = os.path.join(FFMPEG_DIR, "lib") + if not os.path.exists(include_dir): + include_dir = FFMPEG_DIR + if not os.path.exists(library_dir): + library_dir = FFMPEG_DIR -def get_library_config(name): - """Get distutils-compatible extension extras for the given library. + return { + "include_dirs": [include_dir], + "libraries": FFMPEG_LIBRARIES, + "library_dirs": [library_dir], + } - This requires ``pkg-config``. +def get_config_from_pkg_config(): + """ + Get distutils-compatible extension arguments using pkg-config. """ try: - raw_cflags = subprocess.check_output(["pkg-config", "--cflags", "--libs", name]) + raw_cflags = subprocess.check_output( + ["pkg-config", "--cflags", "--libs"] + + ["lib" + name for name in FFMPEG_LIBRARIES] + ) except FileNotFoundError: print("pkg-config is required for building PyAV") exit(1) except subprocess.CalledProcessError: - print("pkg-config could not find library {}".format(name)) + print("pkg-config could not find libraries {}".format(FFMPEG_LIBRARIES)) exit(1) known, unknown = parse_cflags(raw_cflags.decode("utf-8")) @@ -72,107 +92,48 @@ def get_library_config(name): return known -def update_extend(dst, src): - """Update the `dst` with the `src`, extending values where lists. - - Primiarily useful for integrating results from `get_library_config`. - - """ - for k, v in src.items(): - existing = dst.setdefault(k, []) - for x in v: - if x not in existing: - existing.append(x) +def parse_cflags(raw_flags): + parser = argparse.ArgumentParser(add_help=False) + parser.add_argument("-I", dest="include_dirs", action="append") + parser.add_argument("-L", dest="library_dirs", action="append") + parser.add_argument("-l", dest="libraries", action="append") + parser.add_argument("-D", dest="define_macros", action="append") + parser.add_argument("-R", dest="runtime_library_dirs", action="append") - -def unique_extend(a, *args): - a[:] = list(set().union(a, *args)) + raw_args = shlex.split(raw_flags.strip()) + args, unknown = parser.parse_known_args(raw_args) + config = {k: v or [] for k, v in args.__dict__.items()} + for i, x in enumerate(config["define_macros"]): + parts = x.split("=", 1) + value = x[1] or None if len(x) == 2 else None + config["define_macros"][i] = (parts[0], value) + return config, " ".join(shlex.quote(x) for x in unknown) -# Obtain the ffmpeg dir from the "--ffmpeg-dir=" argument +# Parse command-line arguments. +FFMPEG_DIR = None for i, arg in enumerate(sys.argv): if arg.startswith("--ffmpeg-dir="): FFMPEG_DIR = arg.split("=")[1] - break - -if FFMPEG_DIR is not None: - # delete the --ffmpeg-dir arg so that distutils does not see it - del sys.argv[i] - if not os.path.isdir(FFMPEG_DIR): - print("The specified ffmpeg directory does not exist") - exit(1) -else: - # Check the environment variable FFMPEG_DIR - FFMPEG_DIR = os.environ.get("FFMPEG_DIR") - if FFMPEG_DIR is not None: - if not os.path.isdir(FFMPEG_DIR): - FFMPEG_DIR = None + del sys.argv[i] +# Locate FFmpeg libraries and headers. if FFMPEG_DIR is not None: - ffmpeg_lib = os.path.join(FFMPEG_DIR, "lib") - ffmpeg_include = os.path.join(FFMPEG_DIR, "include") - if os.path.exists(ffmpeg_lib): - ffmpeg_lib = [ffmpeg_lib] - else: - ffmpeg_lib = [FFMPEG_DIR] - if os.path.exists(ffmpeg_include): - ffmpeg_include = [ffmpeg_include] - else: - ffmpeg_include = [FFMPEG_DIR] + extension_extra = get_config_from_directory(FFMPEG_DIR) +elif platform.system() != "Windows": + extension_extra = get_config_from_pkg_config() else: - ffmpeg_lib = [] - ffmpeg_include = [] - - -# The "extras" to be supplied to every one of our modules. -# This is expanded heavily by the `config` command. -extension_extra = { - "include_dirs": ["include"] + ffmpeg_include, # The first are PyAV's includes. - "libraries": [], - "library_dirs": ffmpeg_lib, -} - - -def dump_config(): - """Print out all the config information we have so far (for debugging).""" - print("PyAV:", about["__version__"]) - print("Python:", sys.version) - print("platform:", platform.platform()) - print("extension_extra:") - for k, vs in extension_extra.items(): - print("\t%s: %s" % (k, vs)) - - -# Monkey-patch Cython to not overwrite embedded signatures. -old_embed_signature = EmbedSignature._embed_signature - - -def new_embed_signature(self, sig, doc): - - # Strip any `self` parameters from the front. - sig = re.sub(r"\(self(,\s+)?", "(", sig) - - # If they both start with the same signature; skip it. - if sig and doc: - new_name = sig.split("(")[0].strip() - old_name = doc.split("(")[0].strip() - if new_name == old_name: - return doc - if new_name.endswith("." + old_name): - return doc - - return old_embed_signature(self, sig, doc) - - -EmbedSignature._embed_signature = new_embed_signature - + extension_extra = { + "include_dirs": [], + "libraries": FFMPEG_LIBRARIES, + "library_dirs": [], + } # Construct the modules that we find in the "av" directory. ext_modules = [] for dirname, dirnames, filenames in os.walk("av"): for filename in filenames: - - # We are looing for Cython sources. + # We are looking for Cython sources. if filename.startswith(".") or os.path.splitext(filename)[1] != ".pyx": continue @@ -183,126 +144,31 @@ def new_embed_signature(self, sig, doc): # (where os.sep will be \ on Windows). mod_name = base.replace("/", ".").replace(os.sep, ".") - ext_modules.append(Extension(mod_name, sources=[pyx_path])) - - -class ConfigCommand(Command): - - user_options = [ - ("no-pkg-config", None, "do not use pkg-config to configure dependencies"), - ("verbose", None, "dump out configuration"), - ("compiler=", "c", "specify the compiler type"), - ] + # Cythonize the module. + ext_modules += cythonize( + Extension( + mod_name, + include_dirs=extension_extra["include_dirs"], + libraries=extension_extra["libraries"], + library_dirs=extension_extra["library_dirs"], + sources=[pyx_path], + ), + compiler_directives=dict( + c_string_type="str", + c_string_encoding="ascii", + embedsignature=True, + language_level=2, + ), + build_dir="src", + include_path=["include"], + ) - boolean_options = ["no-pkg-config"] - def initialize_options(self): - self.compiler = None - self.no_pkg_config = None - - def finalize_options(self): - self.set_undefined_options("build", ("compiler", "compiler")) - self.set_undefined_options("build_ext", ("no_pkg_config", "no_pkg_config")) - - def run(self): - - # For some reason we get the feeling that CFLAGS is not respected, so we parse - # it here. TODO: Leave any arguments that we can't figure out. - for name in "CFLAGS", "LDFLAGS": - known, unknown = parse_cflags(os.environ.pop(name, "")) - if unknown: - print( - "Warning: We don't understand some of {} (and will leave it in the envvar): {}".format( - name, unknown - ) - ) - os.environ[name] = unknown - update_extend(extension_extra, known) - - # Check if we're using pkg-config or not - if self.no_pkg_config: - # Simply assume we have everything we need! - update_extend(extension_extra, {"libraries": FFMPEG_LIBRARIES}) - else: - # Get the config for the libraries that we require. - for name in FFMPEG_LIBRARIES: - update_extend(extension_extra, get_library_config("lib" + name)) - - if self.verbose: - dump_config() - - # Apply configuration to all modules. - for ext in self.distribution.ext_modules: - for key, value in extension_extra.items(): - setattr(ext, key, value) - - -class CythonizeCommand(Command): - - user_options = [] - - def initialize_options(self): - pass - - def finalize_options(self): - pass - - def run(self): - - # Cythonize, if required. We do it individually since we must update - # the existing extension instead of replacing them all. - for i, ext in enumerate(self.distribution.ext_modules): - if any(s.endswith(".pyx") for s in ext.sources): - new_ext = cythonize( - ext, - compiler_directives=dict( - c_string_type="str", - c_string_encoding="ascii", - embedsignature=True, - language_level=2, - ), - build_dir="src", - include_path=ext.include_dirs, - )[0] - ext.sources = new_ext.sources - - -class BuildExtCommand(build_ext): - - if os.name != "nt": - user_options = build_ext.user_options + [ - ("no-pkg-config", None, "do not use pkg-config to configure dependencies") - ] - - boolean_options = build_ext.boolean_options + ["no-pkg-config"] - - def initialize_options(self): - build_ext.initialize_options(self) - self.no_pkg_config = None - - else: - no_pkg_config = 1 - - def run(self): - - # Propagate build options to config - obj = self.distribution.get_command_obj("config") - obj.compiler = self.compiler - obj.no_pkg_config = self.no_pkg_config - obj.include_dirs = self.include_dirs - obj.libraries = self.libraries - obj.library_dirs = self.library_dirs - - self.run_command("config") - - # Propagate config to cythonize. - for i, ext in enumerate(self.distribution.ext_modules): - unique_extend(ext.include_dirs, self.include_dirs) - unique_extend(ext.library_dirs, self.library_dirs) - unique_extend(ext.libraries, self.libraries) - - self.run_command("cythonize") - build_ext.run(self) +# Read package metadata +about = {} +about_file = os.path.join(os.path.dirname(__file__), "av", "about.py") +with open(about_file, encoding="utf-8") as fp: + exec(fp.read(), about) setup( @@ -315,11 +181,6 @@ def run(self): packages=find_packages(exclude=["build*", "examples*", "scratchpad*", "tests*"]), zip_safe=False, ext_modules=ext_modules, - cmdclass={ - "build_ext": BuildExtCommand, - "config": ConfigCommand, - "cythonize": CythonizeCommand, - }, test_suite="tests", entry_points={ "console_scripts": [ From 831040033b9f03bb0b6d881a248909c8b4b358c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Mon, 24 Jan 2022 23:25:10 +0100 Subject: [PATCH 044/846] [tests] test against FFmpeg 4.4 --- .github/workflows/tests.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6de5036b8..fa86aecd8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -19,7 +19,7 @@ jobs: env: PYAV_PYTHON: python3 - PYAV_LIBRARY: ffmpeg-4.3 # doesn't matter + PYAV_LIBRARY: ffmpeg-4.0 # doesn't matter steps: @@ -53,14 +53,16 @@ jobs: runs-on: ${{ matrix.config.os }} strategy: + fail-fast: false matrix: config: - - {os: ubuntu-latest, python: 3.7, ffmpeg: "4.3", extras: true} + - {os: ubuntu-latest, python: 3.7, ffmpeg: "4.4", extras: true} + - {os: ubuntu-latest, python: 3.7, ffmpeg: "4.3"} - {os: ubuntu-latest, python: 3.7, ffmpeg: "4.2"} - {os: ubuntu-latest, python: 3.7, ffmpeg: "4.1"} - {os: ubuntu-latest, python: 3.7, ffmpeg: "4.0"} - - {os: ubuntu-latest, python: pypy3, ffmpeg: "4.3"} - - {os: macos-latest, python: 3.7, ffmpeg: "4.3"} + - {os: ubuntu-latest, python: pypy3, ffmpeg: "4.4"} + - {os: macos-latest, python: 3.7, ffmpeg: "4.4"} env: PYAV_PYTHON: python${{ matrix.config.python }} @@ -143,6 +145,7 @@ jobs: runs-on: ${{ matrix.config.os }} strategy: + fail-fast: false matrix: config: - {os: windows-latest, python: 3.7, ffmpeg: "4.3"} From 17f26d4deb605bfc865f1d9e2c592b5b2a276a75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Wed, 16 Mar 2022 11:15:42 +0100 Subject: [PATCH 045/846] [streams] initialise stream avg_frame_rate (fixes: #876) We need to initialise an output video stream's average frame rate from the codec context's framerate, otherwise the duration and FPS calculations are wrong when writing video with FFmpeg >= 4.4. --- av/container/output.pyx | 1 + 1 file changed, 1 insertion(+) diff --git a/av/container/output.pyx b/av/container/output.pyx index c1a47b5bf..9910dc930 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -107,6 +107,7 @@ cdef class OutputContainer(Container): codec_context.framerate.num = rate.numerator codec_context.framerate.den = rate.denominator + stream.avg_frame_rate = codec_context.framerate stream.time_base = codec_context.time_base # Some sane audio defaults From 7289675495285ac1f3eb4a0c60c4174197a15d9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Wed, 16 Mar 2022 12:50:57 +0100 Subject: [PATCH 046/846] [tests] get rid of "long", it doesn't exist in Python 3 --- tests/test_file_probing.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/test_file_probing.py b/tests/test_file_probing.py index a6582ac47..a61f623c4 100644 --- a/tests/test_file_probing.py +++ b/tests/test_file_probing.py @@ -7,12 +7,6 @@ from .common import TestCase, fate_suite -try: - long -except NameError: - long = int - - class TestAudioProbe(TestCase): def setUp(self): self.file = av.open(fate_suite("aac/latm_stereo_to_51.ts")) @@ -29,7 +23,7 @@ def test_container_probing(self): self.assertIn(self.file.bit_rate, (269558, 270494)) self.assertEqual(len(self.file.streams), 1) - self.assertEqual(self.file.start_time, long(1400000)) + self.assertEqual(self.file.start_time, 1400000) self.assertEqual(self.file.metadata, {}) def test_stream_probing(self): @@ -228,7 +222,7 @@ def test_container_probing(self): self.file.bit_rate, 8 * self.file.size * av.time_base // self.file.duration ) self.assertEqual(len(self.file.streams), 1) - self.assertEqual(self.file.start_time, long(22953408322)) + self.assertEqual(self.file.start_time, 22953408322) self.assertEqual(self.file.metadata, {}) def test_stream_probing(self): From 34796664e9b44cb390bab90d41019d1bc3ed98d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Mon, 21 Mar 2022 10:04:34 +0100 Subject: [PATCH 047/846] [package] update ffmpeg to 4.4.1 --- docs/overview/installation.rst | 3 +-- scripts/fetch-vendor.json | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/overview/installation.rst b/docs/overview/installation.rst index 74428a76c..b511375e9 100644 --- a/docs/overview/installation.rst +++ b/docs/overview/installation.rst @@ -11,7 +11,7 @@ Since release 8.0.0 binary wheels are provided on PyPI for Linux, Mac and Window pip install av -Currently FFmpeg 4.3.3 is used with the following features enabled for all platforms: +Currently FFmpeg 4.4.1 is used with the following features enabled for all platforms: - fontconfig - gmp @@ -30,7 +30,6 @@ Currently FFmpeg 4.3.3 is used with the following features enabled for all platf - libtheora - libtwolame - libvorbis -- libwavpack - libx264 - libx265 - libxml2 diff --git a/scripts/fetch-vendor.json b/scripts/fetch-vendor.json index 0ea0a7bef..a39f9c113 100644 --- a/scripts/fetch-vendor.json +++ b/scripts/fetch-vendor.json @@ -1,3 +1,3 @@ { - "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/4.3.3-3/ffmpeg-{platform}.tar.gz"] + "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/4.4.1-1/ffmpeg-{platform}.tar.gz"] } From 507d348e2d0660fbcde06e815f959918693061b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Mon, 21 Mar 2022 14:04:04 +0100 Subject: [PATCH 048/846] Remove useless `from __future__` imports We only support Python >= 3.6, so we can prune these imports. --- av/datasets.py | 9 +- av/dictionary.pyx | 5 +- av/enum.pyx | 7 +- av/logging.pyx | 14 +-- av/sidedata/motionvectors.pyx | 5 +- av/sidedata/sidedata.pyx | 8 +- examples/numpy/generate_video.py | 2 - scratchpad/audio.py | 1 - scratchpad/audio_player.py | 1 - scratchpad/average.py | 1 - scratchpad/cctx_decode.py | 1 - scratchpad/cctx_encode.py | 1 - scratchpad/decode.py | 1 - scratchpad/encode.py | 1 - scratchpad/encode_frames.py | 1 - scratchpad/filter_audio.py | 2 - scratchpad/frame_seek_example.py | 1 - scratchpad/graph.py | 1 - scratchpad/player.py | 1 - scratchpad/remux.py | 1 - scratchpad/resource_use.py | 3 - scratchpad/save_subtitles.py | 1 - scratchpad/second_seek_example.py | 1 - scratchpad/seekmany.py | 1 - scripts/autolint | 194 ------------------------------ tests/common.py | 2 - tests/test_encode.py | 2 - tests/test_file_probing.py | 2 - tests/test_logging.py | 2 - tests/test_python_io.py | 10 +- tests/test_seek.py | 2 - 31 files changed, 10 insertions(+), 274 deletions(-) delete mode 100755 scripts/autolint diff --git a/av/datasets.py b/av/datasets.py index a38268341..3324ce0bc 100644 --- a/av/datasets.py +++ b/av/datasets.py @@ -1,17 +1,10 @@ -from __future__ import absolute_import - +from urllib.request import urlopen import errno import logging import os import sys -try: - from urllib.request import urlopen -except ImportError: - from urllib2 import urlopen - - log = logging.getLogger(__name__) diff --git a/av/dictionary.pyx b/av/dictionary.pyx index 6cc199612..d88ccebcd 100644 --- a/av/dictionary.pyx +++ b/av/dictionary.pyx @@ -1,7 +1,4 @@ -try: - from collections.abc import MutableMapping -except ImportError: - from collections import MutableMapping +from collections.abc import MutableMapping from av.error cimport err_check diff --git a/av/enum.pyx b/av/enum.pyx index e54bf67ea..0dd46ab01 100644 --- a/av/enum.pyx +++ b/av/enum.pyx @@ -10,15 +10,10 @@ integers for names and values respectively. """ from collections import OrderedDict +import copyreg import sys -try: - import copyreg -except ImportError: - import copy_reg as copyreg - - cdef sentinel = object() diff --git a/av/logging.pyx b/av/logging.pyx index 1ae5d2df6..1bdb7fab7 100644 --- a/av/logging.pyx +++ b/av/logging.pyx @@ -36,22 +36,12 @@ from libc.stdio cimport fprintf, printf, stderr from libc.stdlib cimport free, malloc cimport libav as lib -from threading import Lock +from threading import Lock, get_ident import logging import os import sys -try: - from threading import get_ident -except ImportError: - from thread import get_ident - - -cdef bint is_py35 = sys.version_info[:2] >= (3, 5) -cdef str decode_error_handler = 'backslashreplace' if is_py35 else 'replace' - - # Library levels. # QUIET = lib.AV_LOG_QUIET # -8; not really a level. PANIC = lib.AV_LOG_PANIC # 0 @@ -289,7 +279,7 @@ cdef log_callback_gil(int level, const char *c_name, const char *c_message): global last_error name = c_name if c_name is not NULL else '' - message = (c_message).decode('utf8', decode_error_handler) + message = (c_message).decode('utf8', 'backslashreplace') log = (level, name, message) # We have to filter it ourselves, but we will still process it in general so diff --git a/av/sidedata/motionvectors.pyx b/av/sidedata/motionvectors.pyx index 40b6717e4..35d0e2f33 100644 --- a/av/sidedata/motionvectors.pyx +++ b/av/sidedata/motionvectors.pyx @@ -1,7 +1,4 @@ -try: - from collections.abc import Sequence -except ImportError: - from collections import Sequence +from collections.abc import Sequence cdef object _cinit_bypass_sentinel = object() diff --git a/av/sidedata/sidedata.pyx b/av/sidedata/sidedata.pyx index 8f5040657..c09f4c6e9 100644 --- a/av/sidedata/sidedata.pyx +++ b/av/sidedata/sidedata.pyx @@ -1,12 +1,8 @@ from av.enum cimport define_enum -from av.sidedata.motionvectors import MotionVectors - +from collections.abc import Mapping -try: - from collections.abc import Mapping -except ImportError: - from collections import Mapping +from av.sidedata.motionvectors import MotionVectors cdef object _cinit_bypass_sentinel = object() diff --git a/examples/numpy/generate_video.py b/examples/numpy/generate_video.py index 50e8f52c9..e0c9a9997 100644 --- a/examples/numpy/generate_video.py +++ b/examples/numpy/generate_video.py @@ -1,5 +1,3 @@ -from __future__ import division - import numpy as np import av diff --git a/scratchpad/audio.py b/scratchpad/audio.py index 950a7951c..a85085d48 100644 --- a/scratchpad/audio.py +++ b/scratchpad/audio.py @@ -1,4 +1,3 @@ -from __future__ import print_function import array import argparse import sys diff --git a/scratchpad/audio_player.py b/scratchpad/audio_player.py index 7a06aba4d..1d86b388e 100644 --- a/scratchpad/audio_player.py +++ b/scratchpad/audio_player.py @@ -1,4 +1,3 @@ -from __future__ import print_function import array import argparse import sys diff --git a/scratchpad/average.py b/scratchpad/average.py index d32c40bb5..f72297f08 100644 --- a/scratchpad/average.py +++ b/scratchpad/average.py @@ -1,4 +1,3 @@ -from __future__ import print_function import argparse import os import sys diff --git a/scratchpad/cctx_decode.py b/scratchpad/cctx_decode.py index 67d70855e..bdb6724f4 100644 --- a/scratchpad/cctx_decode.py +++ b/scratchpad/cctx_decode.py @@ -1,4 +1,3 @@ -from __future__ import print_function import logging logging.basicConfig() diff --git a/scratchpad/cctx_encode.py b/scratchpad/cctx_encode.py index 03bd8ef8f..7885c578e 100644 --- a/scratchpad/cctx_encode.py +++ b/scratchpad/cctx_encode.py @@ -1,4 +1,3 @@ -from __future__ import print_function import logging from PIL import Image, ImageFont, ImageDraw diff --git a/scratchpad/decode.py b/scratchpad/decode.py index 0dfbf2df9..e0bc3e30a 100644 --- a/scratchpad/decode.py +++ b/scratchpad/decode.py @@ -1,4 +1,3 @@ -from __future__ import print_function import array import argparse import logging diff --git a/scratchpad/encode.py b/scratchpad/encode.py index 5efa06ac6..099ac4a14 100644 --- a/scratchpad/encode.py +++ b/scratchpad/encode.py @@ -1,4 +1,3 @@ -from __future__ import print_function import argparse import logging import os diff --git a/scratchpad/encode_frames.py b/scratchpad/encode_frames.py index 19d1e28dc..642a1c2c6 100644 --- a/scratchpad/encode_frames.py +++ b/scratchpad/encode_frames.py @@ -1,4 +1,3 @@ -from __future__ import print_function import argparse import os import sys diff --git a/scratchpad/filter_audio.py b/scratchpad/filter_audio.py index 660996779..092aba3ae 100644 --- a/scratchpad/filter_audio.py +++ b/scratchpad/filter_audio.py @@ -2,8 +2,6 @@ Simple audio filtering example ported from C code: https://github.com/FFmpeg/FFmpeg/blob/master/doc/examples/filter_audio.c """ -from __future__ import division, print_function - from fractions import Fraction import hashlib import sys diff --git a/scratchpad/frame_seek_example.py b/scratchpad/frame_seek_example.py index 021148990..25b2d27d0 100644 --- a/scratchpad/frame_seek_example.py +++ b/scratchpad/frame_seek_example.py @@ -1,4 +1,3 @@ -from __future__ import print_function """ Note this example only really works accurately on constant frame rate media. """ diff --git a/scratchpad/graph.py b/scratchpad/graph.py index fc45b686c..d1a209c37 100644 --- a/scratchpad/graph.py +++ b/scratchpad/graph.py @@ -1,4 +1,3 @@ -from __future__ import print_function from av.filter.graph import Graph g = Graph() diff --git a/scratchpad/player.py b/scratchpad/player.py index c6ad3e51a..e3a7898dc 100644 --- a/scratchpad/player.py +++ b/scratchpad/player.py @@ -1,4 +1,3 @@ -from __future__ import print_function import argparse import ctypes import os diff --git a/scratchpad/remux.py b/scratchpad/remux.py index 3db1b264c..5a2946977 100644 --- a/scratchpad/remux.py +++ b/scratchpad/remux.py @@ -1,4 +1,3 @@ -from __future__ import print_function import array import argparse import logging diff --git a/scratchpad/resource_use.py b/scratchpad/resource_use.py index 4b2791aa9..b61fc3930 100644 --- a/scratchpad/resource_use.py +++ b/scratchpad/resource_use.py @@ -1,6 +1,3 @@ -from __future__ import division, print_function -from __future__ import division - import argparse import resource import gc diff --git a/scratchpad/save_subtitles.py b/scratchpad/save_subtitles.py index 97f8f415f..8666501d8 100644 --- a/scratchpad/save_subtitles.py +++ b/scratchpad/save_subtitles.py @@ -1,4 +1,3 @@ -from __future__ import print_function """ As you can see, the subtitle API needs some work. diff --git a/scratchpad/second_seek_example.py b/scratchpad/second_seek_example.py index a9dc11698..58b3c3811 100644 --- a/scratchpad/second_seek_example.py +++ b/scratchpad/second_seek_example.py @@ -1,4 +1,3 @@ -from __future__ import print_function """ Note this example only really works accurately on constant frame rate media. """ diff --git a/scratchpad/seekmany.py b/scratchpad/seekmany.py index c1ca758e6..f117e658e 100644 --- a/scratchpad/seekmany.py +++ b/scratchpad/seekmany.py @@ -1,4 +1,3 @@ -from __future__ import print_function import sys import av diff --git a/scripts/autolint b/scripts/autolint deleted file mode 100755 index a2e9b01fc..000000000 --- a/scripts/autolint +++ /dev/null @@ -1,194 +0,0 @@ -#!/usr/bin/env python - -import argparse -import fnmatch -import os -import re -import sys - -import autopep8 -import editorconfig -import lib2to3.refactor - - -SOURCE_ROOTS = ['av', 'docs', 'examples', 'include', 'scratchpad', 'scripts', 'tests'] -SOURCE_EXTS = set('.py .pyx .pxd .rst'.split()) - - -def iter_source_paths(): - for root in SOURCE_ROOTS: - for dir_path, _, file_names in os.walk(root): - for file_name in file_names: - base, ext = os.path.splitext(file_name) - if base.startswith('.'): - continue - if ext not in SOURCE_EXTS: - continue - yield os.path.abspath(os.path.join(dir_path, file_name)) - - -def apply_editorconfig(source, path): - - config = editorconfig.get_properties(path) - - soft_indent = config.get('indent_style', 'space') == 'space' - indent_size = int(config.get('indent_size', 4)) - do_trim = config['trim_trailing_whitespace'] == 'true' - do_final_newline = config['insert_final_newline'] == 'true' - - spaced_indent = ' ' * indent_size - - output = [] - - for line in source.splitlines(): - - # Apply trim_trailing_whitespace. - if do_trim: - line = line.rstrip() - - # Adapt tabs to/from spaces. - m = re.match(r'(\s+)(.*)', line) - if m: - indent, content = m.groups() - if soft_indent: - indent.replace('\t', spaced_indent) - else: - indent.replace(indent, '\t') - line = indent + content - - output.append(line) - - while output and not output[-1]: - output.pop() - - if do_final_newline: - output.append('') - - return '\n'.join(output) - - -pep8_fixes_by_ext = { - pattern: tuple(filter(None, (x.split('#')[0].split('-')[0].strip() for x in value.splitlines()))) - for pattern, value in { - '*': ''' - E121 - Fix indentation to be a multiple of four. - E122 - Add absent indentation for hanging indentation. - ''', - '.py': ''' - E226 - Fix missing whitespace around arithmetic operator. - E227 - Fix missing whitespace around bitwise/shift operator. - E228 - Fix missing whitespace around modulo operator. - E231 - Add missing whitespace. - E242 - Remove extraneous whitespace around operator. - W603 - Use "!=" instead of "<>" - W604 - Use "repr()" instead of backticks. - - E22 - Fix extraneous whitespace around keywords. # Messes with Cython. - E241 - Fix extraneous whitespace around keywords. # Messes with Cython. - - ''', - '.py*': ''' - E224 - Remove extraneous whitespace around operator. - # E301 - Add missing blank line. - # E302 - Add missing 2 blank lines. - # E303 - Remove extra blank lines. - E251 - Remove whitespace around parameter '=' sign. - E304 - Remove blank line following function decorator. - E401 - Put imports on separate lines. - E20 - Remove extraneous whitespace. - E211 - Remove extraneous whitespace. - ''', - }.items() -} - -def apply_autopep8(source, path): - - fixes = set() - ext = os.path.splitext(path)[1] - for pattern in pep8_fixes_by_ext: - if fnmatch.fnmatch(ext, pattern): - fixes.update(pep8_fixes_by_ext[pattern]) - - source = autopep8.fix_code(source, options=dict( - select=filter(None, fixes), - )) - - return source - - - -def apply_future(source, path): - - if os.path.splitext(path)[1] not in ('.py', ): - return source - - m = re.search(r'^from __future__ import ([\w, \t]+)', source, flags=re.MULTILINE) - if m: - features = set(x.strip() for x in m.group(1).split(',')) - else: - features = set() - - fixes = [] - - if 'print_function' not in features and re.search(r'^\s*print\s+', source, flags=re.MULTILINE): - fixes.append('lib2to3.fixes.fix_print') - - if not fixes: - # Nothing to do. - return source - - # The parser chokes if the last line is not empty. - if not source.endswith('\n'): - source += '\n' - - tool = lib2to3.refactor.RefactoringTool(fixes) - tree = tool.refactor_string(source, path) - source = str(tree) - - if 'print' in source: - features.add('print_function') - - source = 'from __future__ import {}\n{}'.format(', '.join(sorted(features)), source) - - return source - - - - -def main(): - - parser = argparse.ArgumentParser() - parser.add_argument('-a', '--all', action='store_true') - parser.add_argument('-e', '--editorconfig', action='store_true') - parser.add_argument('-p', '--pep8', action='store_true') - parser.add_argument('-f', '--future', action='store_true') - args = parser.parse_args() - - if not (args.all or args.editorconfig or args.pep8 or args.future): - print("Nothing to do.", file=sys.stderr) - parser.print_usage() - exit(1) - - for path in iter_source_paths(): - - before = after = open(path).read() - print(path) - - if args.all or args.pep8: - after = apply_autopep8(after, path) - - if args.all or args.future: - after = apply_future(after, path) - - if args.all or args.editorconfig: - after = apply_editorconfig(after, path) - - if before == after: - continue - - with open(path, 'w') as fh: - fh.write(after) - - -if __name__ == '__main__': - main() diff --git a/tests/common.py b/tests/common.py index 4322038bb..3707cdf5d 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1,5 +1,3 @@ -from __future__ import division - from unittest import TestCase as _Base import datetime import errno diff --git a/tests/test_encode.py b/tests/test_encode.py index f10ef3b5c..4468e8b17 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -1,5 +1,3 @@ -from __future__ import division - from fractions import Fraction from unittest import SkipTest import math diff --git a/tests/test_file_probing.py b/tests/test_file_probing.py index a61f623c4..e24d67713 100644 --- a/tests/test_file_probing.py +++ b/tests/test_file_probing.py @@ -1,5 +1,3 @@ -from __future__ import division - from fractions import Fraction import av diff --git a/tests/test_logging.py b/tests/test_logging.py index 839d60ae5..7a8e94d3d 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -1,5 +1,3 @@ -from __future__ import division - import errno import logging import threading diff --git a/tests/test_python_io.py b/tests/test_python_io.py index 71530f622..7d3efbd9f 100644 --- a/tests/test_python_io.py +++ b/tests/test_python_io.py @@ -1,4 +1,4 @@ -from __future__ import division +from io import BytesIO import av @@ -6,12 +6,6 @@ from .test_encode import assert_rgb_rotate, write_rgb_rotate -try: - from cStringIO import StringIO -except ImportError: - from io import BytesIO as StringIO - - class NonSeekableBuffer: def __init__(self, data): self.data = data @@ -86,7 +80,7 @@ def test_writing(self): def test_buffer_read_write(self): - buffer_ = StringIO() + buffer_ = BytesIO() wrapped = MethodLogger(buffer_) write_rgb_rotate(av.open(wrapped, "w", "mp4")) diff --git a/tests/test_seek.py b/tests/test_seek.py index 75d6e4a09..6f0f55c1e 100644 --- a/tests/test_seek.py +++ b/tests/test_seek.py @@ -1,5 +1,3 @@ -from __future__ import division - import unittest import av From b65f5a9f93144d2eadbb3e460bb45d869f5ce2fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Mon, 21 Mar 2022 14:14:52 +0100 Subject: [PATCH 049/846] [scripts] remove references to Travis, we use GitHub Actions --- scripts/activate.sh | 13 ++----------- scripts/build | 6 ------ scripts/build-deps | 5 ----- scripts/test | 4 ---- 4 files changed, 2 insertions(+), 26 deletions(-) diff --git a/scripts/activate.sh b/scripts/activate.sh index 1fdd6ee93..bbb440185 100755 --- a/scripts/activate.sh +++ b/scripts/activate.sh @@ -8,10 +8,6 @@ fi export PYAV_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.."; pwd)" -if [[ "$TRAVIS" ]]; then - PYAV_LIBRARY=$LIBRARY -fi - if [[ ! "$PYAV_LIBRARY" ]]; then # Pull from command line argument. @@ -40,16 +36,11 @@ fi export PYAV_PYTHON export PYAV_PIP="${PYAV_PIP-$PYAV_PYTHON -m pip}" -if [[ "$GITHUB_ACTION" || "$TRAVIS" ]]; then +if [[ "$GITHUB_ACTION" ]]; then - # GitHub/Travis as a very self-contained environment. Lets just work in that. + # GitHub has a very self-contained environment. Lets just work in that. echo "We're on CI, so not setting up another virtualenv." - if [[ "$TRAVIS_PYTHON_VERSION" = "2.7" || "$TRAVIS_PYTHON_VERSION" = "pypy" ]]; then - PYAV_PYTHON=python - PYAV_PIP=pip - fi - else export PYAV_VENV_NAME="$(uname -s).$(uname -r).$("$PYAV_PYTHON" -c ' diff --git a/scripts/build b/scripts/build index d860c65a3..8d7e3b06e 100755 --- a/scripts/build +++ b/scripts/build @@ -1,11 +1,5 @@ #!/bin/bash -if [[ "$TRAVIS" && ("$TESTSUITE" == "isort" || "$TESTSUITE" == "flake8") ]]; then - echo "We don't need to build PyAV for source linting." - exit 0 -fi - - if [[ ! "$_PYAV_ACTIVATED" ]]; then export here="$(cd "$(dirname "${BASH_SOURCE[0]}")"; pwd)" source "$here/activate.sh" diff --git a/scripts/build-deps b/scripts/build-deps index e941d0c6b..212fe5144 100755 --- a/scripts/build-deps +++ b/scripts/build-deps @@ -11,11 +11,6 @@ cd "$PYAV_ROOT" $PYAV_PIP install --upgrade -r tests/requirements.txt -if [[ "$TRAVIS" && ("$TESTSUITE" == "isort" || "$TESTSUITE" == "flake8") ]]; then - echo "We don't need to build dependencies for source linting." - exit 0 -fi - # Skip the rest of the build if it already exists. if [[ -e "$PYAV_LIBRARY_PREFIX/bin/ffmpeg" ]]; then echo "We have a cached build of $PYAV_LIBRARY; skipping re-build." diff --git a/scripts/test b/scripts/test index 6277bdbf6..0ed7eb862 100755 --- a/scripts/test +++ b/scripts/test @@ -39,10 +39,6 @@ fi if istest sdist; then $PYAV_PYTHON setup.py build_ext $PYAV_PYTHON setup.py sdist - if [[ "$TRAVIS_TAG" ]]; then - $PYAV_PIP install twine - $PYAV_PYTHON -m twine upload --skip-existing dist/* - fi fi if istest doctest; then From 5f067bd69ae249c235271ff980010adc312a6026 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Wed, 23 Mar 2022 08:26:57 +0100 Subject: [PATCH 050/846] Release v9.0.2 --- CHANGELOG.rst | 6 +++++- av/about.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c73b13c98..f735edc71 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,9 +16,13 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. -v9.0.2.dev0 +v9.0.2 ------ +Minor: + +- Update FFmpeg to 4.4.1 for the binary wheels. +- Fix framerate when writing video with FFmpeg 4.4 (:issue:`876`). v9.0.1 ------ diff --git a/av/about.py b/av/about.py index 2237cf396..357b1f58b 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "9.0.2.dev0" +__version__ = "9.0.2" From dc2cd763515b142c848e8eccd8a398dc64503d72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Thu, 24 Mar 2022 08:16:01 +0100 Subject: [PATCH 051/846] Bump to next dev version. --- CHANGELOG.rst | 4 ++++ av/about.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f735edc71..a30c95dac 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,10 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. +v9.0.3.dev0 +------ + + v9.0.2 ------ diff --git a/av/about.py b/av/about.py index 357b1f58b..7e05bb723 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "9.0.2" +__version__ = "9.0.3.dev0" From 924edd0883c5b8493a905884ea7a50a943118824 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Wed, 16 Mar 2022 00:13:37 +0100 Subject: [PATCH 052/846] [container] remove flags which are gone in FFmpeg 5 --- av/container/core.pyx | 6 ------ include/libavformat/avformat.pxd | 2 -- 2 files changed, 8 deletions(-) diff --git a/av/container/core.pyx b/av/container/core.pyx index 80c92bde0..32f622377 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -79,14 +79,10 @@ Flags = define_enum('Flags', __name__, ( This includes any random IDs, real-time timestamps/dates, muxer version, etc. This flag is mainly intended for testing."""), - ('MP4A_LATM', lib.AVFMT_FLAG_MP4A_LATM, - "Enable RTP MP4A-LATM payload"), ('SORT_DTS', lib.AVFMT_FLAG_SORT_DTS, "Try to interleave outputted packets by dts (using this flag can slow demuxing down)."), ('PRIV_OPT', lib.AVFMT_FLAG_PRIV_OPT, "Enable use of private options by delaying codec open (this could be made default once all code is converted)."), - ('KEEP_SIDE_DATA', lib.AVFMT_FLAG_KEEP_SIDE_DATA, - "Deprecated, does nothing."), ('FAST_SEEK', lib.AVFMT_FLAG_FAST_SEEK, "Enable fast, but inaccurate seeks for some formats."), ('SHORTEST', lib.AVFMT_FLAG_SHORTEST, @@ -293,10 +289,8 @@ cdef class Container(object): discard_corrupt = flags.flag_property('DISCARD_CORRUPT') flush_packets = flags.flag_property('FLUSH_PACKETS') bit_exact = flags.flag_property('BITEXACT') - mp4a_latm = flags.flag_property('MP4A_LATM') sort_dts = flags.flag_property('SORT_DTS') priv_opt = flags.flag_property('PRIV_OPT') - keep_side_data = flags.flag_property('KEEP_SIDE_DATA') fast_seek = flags.flag_property('FAST_SEEK') shortest = flags.flag_property('SHORTEST') auto_bsf = flags.flag_property('AUTO_BSF') diff --git a/include/libavformat/avformat.pxd b/include/libavformat/avformat.pxd index 7a2285171..37e9f1c01 100644 --- a/include/libavformat/avformat.pxd +++ b/include/libavformat/avformat.pxd @@ -145,10 +145,8 @@ cdef extern from "libavformat/avformat.h" nogil: AVFMT_FLAG_DISCARD_CORRUPT AVFMT_FLAG_FLUSH_PACKETS AVFMT_FLAG_BITEXACT - AVFMT_FLAG_MP4A_LATM AVFMT_FLAG_SORT_DTS AVFMT_FLAG_PRIV_OPT - AVFMT_FLAG_KEEP_SIDE_DATA # deprecated; does nothing AVFMT_FLAG_FAST_SEEK AVFMT_FLAG_SHORTEST AVFMT_FLAG_AUTO_BSF From f69b19daa62f968104746c4e224ec1c8f89d60cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Thu, 24 Mar 2022 09:07:55 +0100 Subject: [PATCH 053/846] [tests] reduce the number of warnings - flush audio encoders - reset the picture type for video encoding - stop gratuitously clearing pts --- tests/test_codec_context.py | 51 ++++++++----------------------------- tests/test_encode.py | 1 - 2 files changed, 10 insertions(+), 42 deletions(-) diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 72df49db5..1ad0ac130 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -4,6 +4,7 @@ from av import AudioResampler, Codec, Packet from av.codec.codec import UnknownCodecError +from av.video.frame import PictureType import av from .common import TestCase, fate_suite @@ -264,28 +265,16 @@ def video_encoding(self, codec_name, options={}, codec_tag=None): for frame in iter_frames(container, video_stream): - """ - bad_frame = frame.reformat(width, 100, pix_fmt) - with self.assertRaises(ValueError): - ctx.encode(bad_frame) + new_frame = frame.reformat(width, height, pix_fmt) - bad_frame = frame.reformat(100, height, pix_fmt) - with self.assertRaises(ValueError): - ctx.encode(bad_frame) + # reset the picture type + new_frame.pict_type = PictureType.NONE - bad_frame = frame.reformat(width, height, "rgb24") - with self.assertRaises(ValueError): - ctx.encode(bad_frame) - """ - - if frame: - frame_count += 1 - - new_frame = frame.reformat(width, height, pix_fmt) if frame else None for packet in ctx.encode(new_frame): packet_sizes.append(packet.size) f.write(packet) + frame_count += 1 if frame_count >= max_frames: break @@ -355,37 +344,17 @@ def audio_encoding(self, codec_name): with open(path, "wb") as f: for frame in iter_frames(container, audio_stream): - # We need to let the encoder retime. - frame.pts = None - - """ - bad_resampler = AudioResampler(sample_fmt, "mono", sample_rate) - bad_frame = bad_resampler.resample(frame) - with self.assertRaises(ValueError): - next(encoder.encode(bad_frame)) - - bad_resampler = AudioResampler(sample_fmt, channel_layout, 3000) - bad_frame = bad_resampler.resample(frame) - - with self.assertRaises(ValueError): - next(encoder.encode(bad_frame)) - - bad_resampler = AudioResampler('u8', channel_layout, 3000) - bad_frame = bad_resampler.resample(frame) - - with self.assertRaises(ValueError): - next(encoder.encode(bad_frame)) - """ - resampled_frames = resampler.resample(frame) for resampled_frame in resampled_frames: samples += resampled_frame.samples for packet in ctx.encode(resampled_frame): - # bytearray because python can - # freaks out if the first byte is NULL - f.write(bytearray(packet)) packet_sizes.append(packet.size) + f.write(packet) + + for packet in ctx.encode(None): + packet_sizes.append(packet.size) + f.write(packet) ctx = Codec(codec_name, "r").create() ctx.time_base = Fraction(1) / sample_rate diff --git a/tests/test_encode.py b/tests/test_encode.py index 4468e8b17..ab963a72f 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -145,7 +145,6 @@ def test_audio_transcode(self): src = av.open(fate_suite("audio-reference/chorusnoise_2ch_44kHz_s16.wav")) for frame in src.decode(audio=0): - frame.pts = None for packet in stream.encode(frame): output.mux(packet) From 345302fadb1638a499b76fe4079c6d96743e05a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Thu, 24 Mar 2022 10:26:28 +0100 Subject: [PATCH 054/846] [tests] refactor Python I/O tests --- tests/test_python_io.py | 105 ++++++++++++++++++---------------------- 1 file changed, 47 insertions(+), 58 deletions(-) diff --git a/tests/test_python_io.py b/tests/test_python_io.py index 7d3efbd9f..2873600ee 100644 --- a/tests/test_python_io.py +++ b/tests/test_python_io.py @@ -17,79 +17,68 @@ def read(self, n): class TestPythonIO(TestCase): - def test_reading(self): + def test_basic_errors(self): + self.assertRaises(Exception, av.open, None) + self.assertRaises(Exception, av.open, None, "w") + def test_reading_from_buffer(self): with open(fate_suite("mpeg2/mpeg2_field_encoding.ts"), "rb") as fh: - wrapped = MethodLogger(fh) - - container = av.open(wrapped) - - self.assertEqual(container.format.name, "mpegts") - self.assertEqual( - container.format.long_name, "MPEG-TS (MPEG-2 Transport Stream)" - ) - self.assertEqual(len(container.streams), 1) - self.assertEqual(container.size, 800000) - self.assertEqual(container.metadata, {}) - - # Make sure it did actually call "read". - reads = wrapped._filter("read") - self.assertTrue(reads) + buf = BytesIO(fh.read()) + self.read(buf, seekable=True) - def test_reading_no_seek(self): + def test_reading_from_buffer_no_seek(self): with open(fate_suite("mpeg2/mpeg2_field_encoding.ts"), "rb") as fh: - data = fh.read() + buf = NonSeekableBuffer(fh.read()) + self.read(buf, seekable=False) - buf = NonSeekableBuffer(data) - wrapped = MethodLogger(buf) - - container = av.open(wrapped) + def test_reading_from_file(self): + with open(fate_suite("mpeg2/mpeg2_field_encoding.ts"), "rb") as fh: + self.read(fh, seekable=True) - self.assertEqual(container.format.name, "mpegts") - self.assertEqual( - container.format.long_name, "MPEG-TS (MPEG-2 Transport Stream)" - ) - self.assertEqual(len(container.streams), 1) - self.assertEqual(container.metadata, {}) + def test_writing_to_buffer(self): + fh = BytesIO() - # Make sure it did actually call "read". - reads = wrapped._filter("read") - self.assertTrue(reads) + self.write(fh) - def test_basic_errors(self): - self.assertRaises(Exception, av.open, None) - self.assertRaises(Exception, av.open, None, "w") + # Check contents. + self.assertTrue(fh.tell()) + fh.seek(0) + assert_rgb_rotate(self, av.open(fh)) - def test_writing(self): + def test_writing_to_file(self): + path = self.sandboxed("writing.mp4") - path = self.sandboxed("writing.mov") with open(path, "wb") as fh: - wrapped = MethodLogger(fh) - - output = av.open(wrapped, "w", "mov") - write_rgb_rotate(output) - output.close() - fh.close() + self.write(fh) - # Make sure it did actually write. - writes = wrapped._filter("write") - self.assertTrue(writes) + # Check contents. + with av.open(path) as container: + assert_rgb_rotate(self, container) - # Standard assertions. - assert_rgb_rotate(self, av.open(path)) + def read(self, fh, seekable=True): + wrapped = MethodLogger(fh) - def test_buffer_read_write(self): + with av.open(wrapped) as container: + self.assertEqual(container.format.name, "mpegts") + self.assertEqual( + container.format.long_name, "MPEG-TS (MPEG-2 Transport Stream)" + ) + self.assertEqual(len(container.streams), 1) + if seekable: + self.assertEqual(container.size, 800000) + self.assertEqual(container.metadata, {}) - buffer_ = BytesIO() - wrapped = MethodLogger(buffer_) - write_rgb_rotate(av.open(wrapped, "w", "mp4")) + # Check method calls. + self.assertTrue(wrapped._filter("read")) + if seekable: + self.assertTrue(wrapped._filter("seek")) - # Make sure it did actually write. - writes = wrapped._filter("write") - self.assertTrue(writes) + def write(self, fh): + wrapped = MethodLogger(fh) - self.assertTrue(buffer_.tell()) + with av.open(wrapped, "w", "mp4") as container: + write_rgb_rotate(container) - # Standard assertions. - buffer_.seek(0) - assert_rgb_rotate(self, av.open(buffer_)) + # Check method calls. + self.assertTrue(wrapped._filter("write")) + self.assertTrue(wrapped._filter("seek")) From 9a313d8f47ec2214011863b120940b0ee5083fa8 Mon Sep 17 00:00:00 2001 From: Philip de Nier Date: Thu, 21 May 2020 12:32:25 +0100 Subject: [PATCH 055/846] Move Python I/O code to a PyIOFile class --- av/container/core.pxd | 14 ++------ av/container/core.pyx | 54 +++--------------------------- av/container/pyio.pxd | 16 +++++++++ av/container/pyio.pyx | 76 ++++++++++++++++++++++++++++++++++++++----- 4 files changed, 90 insertions(+), 70 deletions(-) diff --git a/av/container/core.pxd b/av/container/core.pxd index 01a9dfa0d..cd2ba117a 100644 --- a/av/container/core.pxd +++ b/av/container/core.pxd @@ -1,5 +1,6 @@ cimport libav as lib +from av.container.pyio cimport PyIOFile from av.container.streams cimport StreamContainer from av.dictionary cimport _Dictionary from av.format cimport ContainerFormat @@ -21,18 +22,7 @@ cdef class Container(object): cdef readonly str metadata_encoding cdef readonly str metadata_errors - # File-like source. - cdef readonly object file - cdef object fread - cdef object fwrite - cdef object fseek - cdef object ftell - - # Custom IO for above. - cdef lib.AVIOContext *iocontext - cdef unsigned char *buffer - cdef long pos - cdef bint pos_is_valid + cdef readonly PyIOFile file cdef bint input_was_opened cdef readonly ContainerFormat format diff --git a/av/container/core.pyx b/av/container/core.pyx index 32f622377..5e0c9e7d7 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -10,10 +10,10 @@ cimport libav as lib from av.container.core cimport timeout_info from av.container.input cimport InputContainer from av.container.output cimport OutputContainer -from av.container.pyio cimport pyio_read, pyio_seek, pyio_write from av.enum cimport define_enum from av.error cimport err_check, stash_exception from av.format cimport build_container_format +from av.utils cimport avdict_to_dict from av.dictionary import Dictionary from av.logging import Capture as LogCapture @@ -112,7 +112,6 @@ cdef class Container(object): self.name = getattr(file_, 'name', '') if not isinstance(self.name, str): raise TypeError("File's name attribute must be string-like.") - self.file = file_ self.options = dict(options or ()) self.container_options = dict(container_options or ()) @@ -163,42 +162,9 @@ cdef class Container(object): self.ptr.flags |= lib.AVFMT_FLAG_GENPTS # Setup Python IO. - if self.file is not None: - - self.fread = getattr(self.file, 'read', None) - self.fwrite = getattr(self.file, 'write', None) - self.fseek = getattr(self.file, 'seek', None) - self.ftell = getattr(self.file, 'tell', None) - - if self.writeable: - if self.fwrite is None: - raise ValueError("File object has no write method.") - else: - if self.fread is None: - raise ValueError("File object has no read method.") - - if self.fseek is not None and self.ftell is not None: - seek_func = pyio_seek - - self.pos = 0 - self.pos_is_valid = True - - # This is effectively the maximum size of reads. - self.buffer = lib.av_malloc(buffer_size) - - self.iocontext = lib.avio_alloc_context( - self.buffer, buffer_size, - self.writeable, # Writeable. - self, # User data. - pyio_read, - pyio_write, - seek_func - ) - - if seek_func: - self.iocontext.seekable = lib.AVIO_SEEKABLE_NORMAL - self.iocontext.max_packet_size = buffer_size - self.ptr.pb = self.iocontext + if not isinstance(file_, basestring): + self.file = PyIOFile(file_, buffer_size, self.writeable) + self.ptr.pb = self.file.iocontext cdef lib.AVInputFormat *ifmt cdef _Dictionary c_options @@ -226,18 +192,6 @@ cdef class Container(object): def __dealloc__(self): with nogil: - # FFmpeg will not release custom input, so it's up to us to free it. - # Do not touch our original buffer as it may have been freed and replaced. - if self.iocontext: - lib.av_freep(&self.iocontext.buffer) - lib.av_freep(&self.iocontext) - - # We likely errored badly if we got here, and so are still - # responsible for our buffer. - else: - lib.av_freep(&self.buffer) - - # Finish releasing the whole structure. lib.avformat_free_context(self.ptr) def __enter__(self): diff --git a/av/container/pyio.pxd b/av/container/pyio.pxd index 1292d2c71..b7597e9b3 100644 --- a/av/container/pyio.pxd +++ b/av/container/pyio.pxd @@ -1,4 +1,5 @@ from libc.stdint cimport int64_t, uint8_t +cimport libav as lib cdef int pyio_read(void *opaque, uint8_t *buf, int buf_size) nogil @@ -6,3 +7,18 @@ cdef int pyio_read(void *opaque, uint8_t *buf, int buf_size) nogil cdef int pyio_write(void *opaque, uint8_t *buf, int buf_size) nogil cdef int64_t pyio_seek(void *opaque, int64_t offset, int whence) nogil + +cdef class PyIOFile(object): + + # File-like source. + cdef readonly object file + cdef object fread + cdef object fwrite + cdef object fseek + cdef object ftell + + # Custom IO for above. + cdef lib.AVIOContext *iocontext + cdef unsigned char *buffer + cdef long pos + cdef bint pos_is_valid diff --git a/av/container/pyio.pyx b/av/container/pyio.pyx index 62629313d..ef844a3dc 100644 --- a/av/container/pyio.pyx +++ b/av/container/pyio.pyx @@ -1,19 +1,80 @@ from libc.string cimport memcpy cimport libav as lib -from av.container.core cimport Container from av.error cimport stash_exception +ctypedef int64_t (*seek_func_t)(void *opaque, int64_t offset, int whence) nogil + + +cdef class PyIOFile(object): + + def __cinit__(self, file, buffer_size, writeable=None): + + self.file = file + + cdef seek_func_t seek_func = NULL + + self.fread = getattr(self.file, 'read', None) + self.fwrite = getattr(self.file, 'write', None) + self.fseek = getattr(self.file, 'seek', None) + self.ftell = getattr(self.file, 'tell', None) + + if self.fseek is not None and self.ftell is not None: + seek_func = pyio_seek + + if writeable is None: + writeable = self.fwrite is not None + + if writeable: + if self.fwrite is None: + raise ValueError("File object has no write method.") + else: + if self.fread is None: + raise ValueError("File object has no read method.") + + self.pos = 0 + self.pos_is_valid = True + + # This is effectively the maximum size of reads. + self.buffer = lib.av_malloc(buffer_size) + + self.iocontext = lib.avio_alloc_context( + self.buffer, buffer_size, + writeable, + self, # User data. + pyio_read, + pyio_write, + seek_func + ) + + if seek_func: + self.iocontext.seekable = lib.AVIO_SEEKABLE_NORMAL + self.iocontext.max_packet_size = buffer_size + + def __dealloc__(self): + with nogil: + # FFmpeg will not release custom input, so it's up to us to free it. + # Do not touch our original buffer as it may have been freed and replaced. + if self.iocontext: + lib.av_freep(&self.iocontext.buffer) + lib.av_freep(&self.iocontext) + + # We likely errored badly if we got here, and so are still + # responsible for our buffer. + else: + lib.av_freep(&self.buffer) + + cdef int pyio_read(void *opaque, uint8_t *buf, int buf_size) nogil: with gil: return pyio_read_gil(opaque, buf, buf_size) cdef int pyio_read_gil(void *opaque, uint8_t *buf, int buf_size): - cdef Container self + cdef PyIOFile self cdef bytes res try: - self = opaque + self = opaque res = self.fread(buf_size) memcpy(buf, res, len(res)) self.pos += len(res) @@ -29,11 +90,11 @@ cdef int pyio_write(void *opaque, uint8_t *buf, int buf_size) nogil: return pyio_write_gil(opaque, buf, buf_size) cdef int pyio_write_gil(void *opaque, uint8_t *buf, int buf_size): - cdef Container self + cdef PyIOFile self cdef bytes bytes_to_write cdef int bytes_written try: - self = opaque + self = opaque bytes_to_write = buf[:buf_size] ret_value = self.fwrite(bytes_to_write) bytes_written = ret_value if isinstance(ret_value, int) else buf_size @@ -53,9 +114,9 @@ cdef int64_t pyio_seek(void *opaque, int64_t offset, int whence) nogil: return pyio_seek_gil(opaque, offset, whence) cdef int64_t pyio_seek_gil(void *opaque, int64_t offset, int whence): - cdef Container self + cdef PyIOFile self try: - self = opaque + self = opaque res = self.fseek(offset, whence) # Track the position for the user. @@ -71,6 +132,5 @@ cdef int64_t pyio_seek_gil(void *opaque, int64_t offset, int whence): else: res = self.ftell() return res - except Exception as e: return stash_exception() From 864fa981183f5972ee723af7b81a5ac402a656d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Fri, 25 Mar 2022 09:56:32 +0100 Subject: [PATCH 056/846] [docs] fix build by pinning sphinx < 4.4 Also remove a reference to the obsolete Stream.seek() method. See: #913 --- av/stream.pyx | 4 ++-- docs/api/codec.rst | 6 +++--- docs/api/stream.rst | 2 -- docs/conf.py | 2 +- tests/requirements.txt | 2 +- 5 files changed, 7 insertions(+), 9 deletions(-) diff --git a/av/stream.pyx b/av/stream.pyx index cb083468b..0889813dc 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -298,7 +298,7 @@ cdef class Stream(object): Returns ``0`` if it is not known. - :type: int + :type: :class:`int` """ def __get__(self): return self._stream.nb_frames @@ -306,7 +306,7 @@ cdef class Stream(object): """ The language of the stream. - :type: :class:``str`` or ``None`` + :type: :class:`str` or ``None`` """ def __get__(self): return self.metadata.get('language') diff --git a/docs/api/codec.rst b/docs/api/codec.rst index 44fead746..ebc147c30 100644 --- a/docs/api/codec.rst +++ b/docs/api/codec.rst @@ -12,10 +12,10 @@ Descriptors .. automethod:: Codec.create +.. autoattribute:: Codec.is_decoder .. autoattribute:: Codec.is_encoder -.. autoattribute:: Codec.is_encoder -.. - .. autoattribute:: Codec.descriptor + +.. autoattribute:: Codec.descriptor .. autoattribute:: Codec.name .. autoattribute:: Codec.long_name .. autoattribute:: Codec.type diff --git a/docs/api/stream.rst b/docs/api/stream.rst index bcfa64896..49ff05ac8 100644 --- a/docs/api/stream.rst +++ b/docs/api/stream.rst @@ -111,8 +111,6 @@ Whenever possible, we advise that you use raw timing instead of frame rates. Others ~~~~~~ -.. automethod:: Stream.seek - .. autoattribute:: Stream.profile .. autoattribute:: Stream.language diff --git a/docs/conf.py b/docs/conf.py index 6575a3530..cc97c4397 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -482,7 +482,7 @@ def _doxylink_handler(name, rawtext, text, lineno, inliner, options={}, content= def setup(app): - app.add_stylesheet('custom.css') + app.add_css_file('custom.css') app.add_directive('flagtable', EnumTable) app.add_directive('enumtable', EnumTable) diff --git a/tests/requirements.txt b/tests/requirements.txt index f2a15b027..2a321a28d 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -5,4 +5,4 @@ flake8 isort numpy Pillow -sphinx +sphinx < 4.4 From ed84e8e9251759086af852b0ad16a40df3261005 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Fri, 25 Mar 2022 10:49:27 +0100 Subject: [PATCH 057/846] [buffer] deprecate Buffer.to_bytes() This method is no longer needed since with Python 3 we can simply use bytes(buf). --- av/buffer.pyx | 2 ++ scratchpad/audio.py | 4 ++-- scratchpad/audio_player.py | 2 +- scratchpad/decode.py | 4 ++-- tests/test_audiofifo.py | 4 ++-- tests/test_codec_context.py | 4 ++-- 6 files changed, 11 insertions(+), 9 deletions(-) diff --git a/av/buffer.pyx b/av/buffer.pyx index 94f1c54d7..8176e1565 100644 --- a/av/buffer.pyx +++ b/av/buffer.pyx @@ -1,6 +1,7 @@ from cpython cimport PyBUF_WRITABLE, PyBuffer_FillInfo from libc.string cimport memcpy +from av import deprecation from av.bytesource cimport ByteSource, bytesource @@ -35,6 +36,7 @@ cdef class Buffer(object): """The memory address of the buffer.""" return self._buffer_ptr() + @deprecation.method def to_bytes(self): """Return the contents of this buffer as ``bytes``. diff --git a/scratchpad/audio.py b/scratchpad/audio.py index a85085d48..c5a79f9c0 100644 --- a/scratchpad/audio.py +++ b/scratchpad/audio.py @@ -10,7 +10,7 @@ def print_data(frame): for i, plane in enumerate(frame.planes or ()): - data = plane.to_bytes() + data = bytes(plane) print('\tPLANE %d, %d bytes' % (i, len(data))) data = data.encode('hex') for i in xrange(0, len(data), 128): @@ -91,7 +91,7 @@ def print_data(frame): ffplay = subprocess.Popen(cmd, stdin=subprocess.PIPE) try: for frame in frames: - ffplay.stdin.write(frame.planes[0].to_bytes()) + ffplay.stdin.write(bytes(frame.planes[0])) except IOError as e: print(e) exit() diff --git a/scratchpad/audio_player.py b/scratchpad/audio_player.py index 1d86b388e..8322a3206 100644 --- a/scratchpad/audio_player.py +++ b/scratchpad/audio_player.py @@ -60,7 +60,7 @@ def decode_iter(): print('pts: %.3f, played: %.3f, buffered: %.3f' % (frame.time or 0, us_processed / 1000000.0, us_buffered / 1000000.0)) - data = frame.planes[0].to_bytes() + data = bytes(frame.planes[0]) while data: written = device.write(data) if written: diff --git a/scratchpad/decode.py b/scratchpad/decode.py index e0bc3e30a..d2dfcc580 100644 --- a/scratchpad/decode.py +++ b/scratchpad/decode.py @@ -144,7 +144,7 @@ def format_time(time, time_base): ] proc = subprocess.Popen(cmd, stdin=subprocess.PIPE) try: - proc.stdin.write(frame.planes[0].to_bytes()) + proc.stdin.write(bytes(frame.planes[0])) except IOError as e: print(e) exit() @@ -152,7 +152,7 @@ def format_time(time, time_base): if args.dump_planes: print('\t\tplanes') for i, plane in enumerate(frame.planes or ()): - data = plane.to_bytes() + data = bytes(plane) print('\t\t\tPLANE %d, %d bytes' % (i, len(data))) data = data.encode('hex') for i in xrange(0, len(data), 128): diff --git a/tests/test_audiofifo.py b/tests/test_audiofifo.py index c29090647..f04995b89 100644 --- a/tests/test_audiofifo.py +++ b/tests/test_audiofifo.py @@ -16,10 +16,10 @@ def test_data(self): for i, packet in enumerate(container.demux(stream)): for frame in packet.decode(): - input_.append(frame.planes[0].to_bytes()) + input_.append(bytes(frame.planes[0])) fifo.write(frame) for frame in fifo.read_many(512, partial=i == 10): - output.append(frame.planes[0].to_bytes()) + output.append(bytes(frame.planes[0])) if i == 10: break diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 1ad0ac130..ca9433678 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -116,7 +116,7 @@ def _assert_parse(self, codec_name, path): for packet in fh.demux(video=0): packets.append(packet) - full_source = b"".join(p.to_bytes() for p in packets) + full_source = b"".join(bytes(p) for p in packets) for size in 1024, 8192, 65535: @@ -128,7 +128,7 @@ def _assert_parse(self, codec_name, path): packets.extend(ctx.parse(block)) packets.extend(ctx.parse()) - parsed_source = b"".join(p.to_bytes() for p in packets) + parsed_source = b"".join(bytes(p) for p in packets) self.assertEqual(len(parsed_source), len(full_source)) self.assertEqual(full_source, parsed_source) From e9d87b01ee412ca50180416c5cdf3fb996964a4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Fri, 25 Mar 2022 16:37:22 +0100 Subject: [PATCH 058/846] [stream] check self.codec_context is valid before using (fixes: #689) If self.codec_context is None, we get a segmentation fault when calling any of its methods. --- av/stream.pyx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/av/stream.pyx b/av/stream.pyx index 0889813dc..cbab9dde1 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -127,7 +127,10 @@ cdef class Stream(object): if name == "id": self._set_id(value) return - setattr(self.codec_context, name, value) + + if self.codec_context is not None: + setattr(self.codec_context, name, value) + if name == "time_base": self._set_time_base(value) @@ -155,6 +158,9 @@ cdef class Stream(object): .. seealso:: This is mostly a passthrough to :meth:`.CodecContext.encode`. """ + if self.codec_context is None: + raise RuntimeError("Stream.encode requires a valid CodecContext") + packets = self.codec_context.encode(frame) cdef Packet packet for packet in packets: @@ -171,6 +177,9 @@ cdef class Stream(object): .. seealso:: This is mostly a passthrough to :meth:`.CodecContext.decode`. """ + if self.codec_context is None: + raise RuntimeError("Stream.decode requires a valid CodecContext") + return self.codec_context.decode(packet) property id: From 4fa70f8073708a56fa816c08ef6fae74611d3df0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Thu, 24 Mar 2022 18:13:52 +0100 Subject: [PATCH 059/846] Make it possible to read from a pipe (fixes: #738) --- av/container/core.pyx | 4 +-- av/container/pyio.pyx | 19 +++++++--- tests/test_python_io.py | 77 ++++++++++++++++++++++++++++++++++++++--- 3 files changed, 88 insertions(+), 12 deletions(-) diff --git a/av/container/core.pyx b/av/container/core.pyx index 5e0c9e7d7..4ec03e487 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -109,9 +109,7 @@ cdef class Container(object): if isinstance(file_, str): self.name = file_ else: - self.name = getattr(file_, 'name', '') - if not isinstance(self.name, str): - raise TypeError("File's name attribute must be string-like.") + self.name = str(getattr(file_, 'name', '')) self.options = dict(options or ()) self.container_options = dict(container_options or ()) diff --git a/av/container/pyio.pyx b/av/container/pyio.pyx index ef844a3dc..a9e441225 100644 --- a/av/container/pyio.pyx +++ b/av/container/pyio.pyx @@ -15,23 +15,32 @@ cdef class PyIOFile(object): cdef seek_func_t seek_func = NULL + readable = getattr(self.file, 'readable', None) + writable = getattr(self.file, 'writable', None) + seekable = getattr(self.file, 'seekable', None) self.fread = getattr(self.file, 'read', None) self.fwrite = getattr(self.file, 'write', None) self.fseek = getattr(self.file, 'seek', None) self.ftell = getattr(self.file, 'tell', None) - if self.fseek is not None and self.ftell is not None: + # To be seekable the file object must have `seek` and `tell` methods. + # If it also has a `seekable` method, it must return True. + if ( + self.fseek is not None + and self.ftell is not None + and (seekable is None or seekable()) + ): seek_func = pyio_seek if writeable is None: writeable = self.fwrite is not None if writeable: - if self.fwrite is None: - raise ValueError("File object has no write method.") + if self.fwrite is None or (writable is not None and not writable()): + raise ValueError("File object has no write() method, or writable() returned False.") else: - if self.fread is None: - raise ValueError("File object has no read method.") + if self.fread is None or (readable is not None and not readable()): + raise ValueError("File object has no read() method, or readable() returned False.") self.pos = 0 self.pos_is_valid = True diff --git a/tests/test_python_io.py b/tests/test_python_io.py index 2873600ee..0716429c8 100644 --- a/tests/test_python_io.py +++ b/tests/test_python_io.py @@ -6,7 +6,11 @@ from .test_encode import assert_rgb_rotate, write_rgb_rotate -class NonSeekableBuffer: +class ReadOnlyBuffer: + """ + Minimal buffer which *only* implements the read() method. + """ + def __init__(self, data): self.data = data @@ -16,6 +20,38 @@ def read(self, n): return data +class ReadOnlyPipe(BytesIO): + """ + Buffer which behaves like a readable pipe. + """ + + @property + def name(self): + return 123 + + def seekable(self): + return False + + def writable(self): + return False + + +class WriteOnlyPipe(BytesIO): + """ + Buffer which behaves like a writable pipe. + """ + + @property + def name(self): + return 123 + + def readable(self): + return False + + def seekable(self): + return False + + class TestPythonIO(TestCase): def test_basic_errors(self): self.assertRaises(Exception, av.open, None) @@ -24,17 +60,32 @@ def test_basic_errors(self): def test_reading_from_buffer(self): with open(fate_suite("mpeg2/mpeg2_field_encoding.ts"), "rb") as fh: buf = BytesIO(fh.read()) - self.read(buf, seekable=True) + self.read(buf, seekable=True) def test_reading_from_buffer_no_seek(self): with open(fate_suite("mpeg2/mpeg2_field_encoding.ts"), "rb") as fh: - buf = NonSeekableBuffer(fh.read()) - self.read(buf, seekable=False) + buf = ReadOnlyBuffer(fh.read()) + self.read(buf, seekable=False) def test_reading_from_file(self): with open(fate_suite("mpeg2/mpeg2_field_encoding.ts"), "rb") as fh: self.read(fh, seekable=True) + def test_reading_from_pipe_readonly(self): + with open(fate_suite("mpeg2/mpeg2_field_encoding.ts"), "rb") as fh: + buf = ReadOnlyPipe(fh.read()) + self.read(buf, seekable=False) + + def test_reading_from_write_readonly(self): + with open(fate_suite("mpeg2/mpeg2_field_encoding.ts"), "rb") as fh: + buf = WriteOnlyPipe(fh.read()) + with self.assertRaises(ValueError) as cm: + self.read(buf, seekable=False) + self.assertEqual( + str(cm.exception), + "File object has no read() method, or readable() returned False.", + ) + def test_writing_to_buffer(self): fh = BytesIO() @@ -55,6 +106,24 @@ def test_writing_to_file(self): with av.open(path) as container: assert_rgb_rotate(self, container) + def test_writing_to_pipe_readonly(self): + buf = ReadOnlyPipe() + with self.assertRaises(ValueError) as cm: + self.write(buf) + self.assertEqual( + str(cm.exception), + "File object has no write() method, or writable() returned False.", + ) + + def test_writing_to_pipe_writeonly(self): + buf = WriteOnlyPipe() + with self.assertRaises(ValueError) as cm: + self.write(buf) + self.assertIn( + "[mp4] muxer does not support non seekable output", + str(cm.exception), + ) + def read(self, fh, seekable=True): wrapped = MethodLogger(fh) From fd9c9cc0e593b7ffd23bfe33778632fcf5e68d35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Fri, 25 Mar 2022 17:38:30 +0100 Subject: [PATCH 060/846] Ensure probing a corrupt file does not crash (fixes: #590) - make get_audio_format() return None for invalid formats - fix AudioStream.__repr__ to deal with this situation - make InputContainer.duration and InputContainer.start_time return None when "no value" is encountered --- av/audio/format.pyx | 4 ++ av/audio/stream.pyx | 2 +- av/container/input.pyx | 8 ++- tests/test_decode.py | 36 ++++++++++++ tests/test_file_probing.py | 117 +++++++++++++++++++++++++++++++++++++ 5 files changed, 164 insertions(+), 3 deletions(-) diff --git a/av/audio/format.pyx b/av/audio/format.pyx index 8a2aa20a2..f2eb72b5b 100644 --- a/av/audio/format.pyx +++ b/av/audio/format.pyx @@ -8,6 +8,10 @@ cdef object _cinit_bypass_sentinel cdef AudioFormat get_audio_format(lib.AVSampleFormat c_format): """Get an AudioFormat without going through a string.""" + + if c_format < 0: + return None + cdef AudioFormat format = AudioFormat.__new__(AudioFormat, _cinit_bypass_sentinel) format._init(c_format) return format diff --git a/av/audio/stream.pyx b/av/audio/stream.pyx index 4e8f929ae..0a4f1523c 100644 --- a/av/audio/stream.pyx +++ b/av/audio/stream.pyx @@ -7,6 +7,6 @@ cdef class AudioStream(Stream): self.name, self.rate, self.layout.name, - self.format.name, + self.format.name if self.format else None, id(self), ) diff --git a/av/container/input.pyx b/av/container/input.pyx index 4b3a5ea5e..e0c7dcc22 100644 --- a/av/container/input.pyx +++ b/av/container/input.pyx @@ -73,10 +73,14 @@ cdef class InputContainer(Container): close_input(self) property start_time: - def __get__(self): return self.ptr.start_time + def __get__(self): + if self.ptr.start_time != lib.AV_NOPTS_VALUE: + return self.ptr.start_time property duration: - def __get__(self): return self.ptr.duration + def __get__(self): + if self.ptr.duration != lib.AV_NOPTS_VALUE: + return self.ptr.duration property bit_rate: def __get__(self): return self.ptr.bit_rate diff --git a/tests/test_decode.py b/tests/test_decode.py index 525577f29..b4e13c183 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -19,6 +19,24 @@ def test_decoded_video_frame_count(self): self.assertEqual(frame_count, video_stream.frames) + def test_decode_audio_corrupt(self): + # write an empty file + path = self.sandboxed("empty.flac") + with open(path, "wb"): + pass + + packet_count = 0 + frame_count = 0 + + with av.open(path) as container: + for packet in container.demux(audio=0): + for frame in packet.decode(): + frame_count += 1 + packet_count += 1 + + self.assertEqual(packet_count, 1) + self.assertEqual(frame_count, 0) + def test_decode_audio_sample_count(self): container = av.open(fate_suite("audio-reference/chorusnoise_2ch_44kHz_s16.wav")) @@ -79,3 +97,21 @@ def test_decoded_motion_vectors_no_flag(self): if not frame.key_frame: assert vectors is None return + + def test_decode_video_corrupt(self): + # write an empty file + path = self.sandboxed("empty.h264") + with open(path, "wb"): + pass + + packet_count = 0 + frame_count = 0 + + with av.open(path) as container: + for packet in container.demux(video=0): + for frame in packet.decode(): + frame_count += 1 + packet_count += 1 + + self.assertEqual(packet_count, 1) + self.assertEqual(frame_count, 0) diff --git a/tests/test_file_probing.py b/tests/test_file_probing.py index e24d67713..1d7b5a033 100644 --- a/tests/test_file_probing.py +++ b/tests/test_file_probing.py @@ -63,6 +63,65 @@ def test_stream_probing(self): self.assertEqual(stream.rate, 48000) +class TestAudioProbeCorrupt(TestCase): + def setUp(self): + # write an empty file + path = self.sandboxed("empty.flac") + with open(path, "wb"): + pass + + self.file = av.open(path) + + def test_container_probing(self): + self.assertEqual(str(self.file.format), "") + self.assertEqual(self.file.format.name, "flac") + self.assertEqual(self.file.format.long_name, "raw FLAC") + self.assertEqual(self.file.size, 0) + self.assertEqual(self.file.bit_rate, 0) + self.assertEqual(self.file.duration, None) + + self.assertEqual(len(self.file.streams), 1) + self.assertEqual(self.file.start_time, None) + self.assertEqual(self.file.metadata, {}) + + def test_stream_probing(self): + stream = self.file.streams[0] + + # ensure __repr__ does not crash + self.assertTrue( + str(stream).startswith( + "") + self.assertEqual(self.file.format.name, "h264") + self.assertEqual(self.file.format.long_name, "raw H.264 video") + self.assertEqual(self.file.size, 0) + self.assertEqual(self.file.bit_rate, 0) + self.assertEqual(self.file.duration, None) + + self.assertEqual(len(self.file.streams), 1) + self.assertEqual(self.file.start_time, None) + self.assertEqual(self.file.metadata, {}) + + def test_stream_probing(self): + stream = self.file.streams[0] + + # ensure __repr__ does not crash + self.assertTrue(str(stream).startswith(" Date: Sat, 26 Mar 2022 00:34:12 +0100 Subject: [PATCH 061/846] [setup] detect static build, require FFmpeg to clean Fixes: #120 Fixes: #915 --- setup.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 02010f537..e7ef99bb3 100644 --- a/setup.py +++ b/setup.py @@ -87,6 +87,8 @@ def get_config_from_pkg_config(): known, unknown = parse_cflags(raw_cflags.decode("utf-8")) if unknown: print("pkg-config returned flags we don't understand: {}".format(unknown)) + if "-pthread" in unknown: + print("Building PyAV against static FFmpeg libraries is not supported.") exit(1) return known @@ -117,10 +119,16 @@ def parse_cflags(raw_flags): FFMPEG_DIR = arg.split("=")[1] del sys.argv[i] +# Do not cythonize or use pkg-config when cleaning. +use_pkg_config = platform.system() != "Windows" +if len(sys.argv) > 1 and sys.argv[1] == "clean": + cythonize = lambda ext, **kwargs: [ext] + use_pkg_config = False + # Locate FFmpeg libraries and headers. if FFMPEG_DIR is not None: extension_extra = get_config_from_directory(FFMPEG_DIR) -elif platform.system() != "Windows": +elif use_pkg_config: extension_extra = get_config_from_pkg_config() else: extension_extra = { From 1c68f225b03cefae2b31acd441f6703468a7fdac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Sat, 26 Mar 2022 01:09:46 +0100 Subject: [PATCH 062/846] [issues] add an action to flag and close stale issues --- .github/workflows/issues.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/workflows/issues.yml diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml new file mode 100644 index 000000000..1c9a67887 --- /dev/null +++ b/.github/workflows/issues.yml @@ -0,0 +1,17 @@ +name: issues +on: + schedule: + - cron: '30 1 * * *' + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v5 + with: + stale-issue-label: stale + stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.' + days-before-stale: 120 + days-before-close: 14 + days-before-pr-stale: -1 + days-before-pr-close: -1 From 1505cd017179124eff4ebb03cd85cc8f3cb96c51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Sat, 26 Mar 2022 02:05:00 +0100 Subject: [PATCH 063/846] [tests] prefer using `with av.open(...)` context manager when writing --- tests/test_container.py | 29 ----- tests/test_encode.py | 267 ++++++++++++++++++++-------------------- tests/test_python_io.py | 11 +- 3 files changed, 140 insertions(+), 167 deletions(-) delete mode 100644 tests/test_container.py diff --git a/tests/test_container.py b/tests/test_container.py deleted file mode 100644 index 5aef83d11..000000000 --- a/tests/test_container.py +++ /dev/null @@ -1,29 +0,0 @@ -import sys -import unittest - -import av - -from .common import TestCase, fate_suite, is_windows, skip_tests - - -# On Windows, Python 3.0 - 3.5 have issues handling unicode filenames. -# Starting with Python 3.6 the situation is saner thanks to PEP 529: -# -# https://www.python.org/dev/peps/pep-0529/ - -broken_unicode = is_windows and sys.version_info < (3, 6) - - -class TestContainers(TestCase): - def test_context_manager(self): - with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: - self.assertEqual(container.format.long_name, "QuickTime / MOV") - self.assertEqual(len(container.streams), 1) - - @unittest.skipIf( - broken_unicode or "unicode_filename" in skip_tests, - "Unicode filename handling is broken", - ) - def test_unicode_filename(self): - - av.open(self.sandboxed("¢∞§¶•ªº.mov"), "w") diff --git a/tests/test_encode.py b/tests/test_encode.py index ab963a72f..e18c34330 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -62,9 +62,6 @@ def write_rgb_rotate(output): for packet in stream.encode(None): output.mux(packet) - # Done! - output.close() - def assert_rgb_rotate(self, input_): @@ -87,157 +84,161 @@ def assert_rgb_rotate(self, input_): class TestBasicVideoEncoding(TestCase): - def test_rgb_rotate(self): - + def test_default_options(self): + with av.open(self.sandboxed("output.mov"), "w") as output: + stream = output.add_stream("mpeg4") + self.assertEqual(stream.bit_rate, 1024000) + self.assertEqual(stream.format.height, 480) + self.assertEqual(stream.format.name, "yuv420p") + self.assertEqual(stream.format.width, 640) + self.assertEqual(stream.height, 480) + self.assertEqual(stream.pix_fmt, "yuv420p") + self.assertEqual(stream.rate, Fraction(24, 1)) + self.assertEqual(stream.ticks_per_frame, 1) + self.assertEqual(stream.time_base, None) + self.assertEqual(stream.width, 640) + + def test_encoding(self): path = self.sandboxed("rgb_rotate.mov") - output = av.open(path, "w") - write_rgb_rotate(output) - assert_rgb_rotate(self, av.open(path)) + with av.open(path, "w") as output: + write_rgb_rotate(output) + with av.open(path) as input: + assert_rgb_rotate(self, input) def test_encoding_with_pts(self): - path = self.sandboxed("video_with_pts.mov") - output = av.open(path, "w") - stream = output.add_stream("libx264", 24) - stream.width = WIDTH - stream.height = HEIGHT - stream.pix_fmt = "yuv420p" + with av.open(path, "w") as output: + stream = output.add_stream("libx264", 24) + stream.width = WIDTH + stream.height = HEIGHT + stream.pix_fmt = "yuv420p" + + for i in range(DURATION): + frame = VideoFrame(WIDTH, HEIGHT, "rgb24") + frame.pts = i * 2000 + frame.time_base = Fraction(1, 48000) - for i in range(DURATION): - frame = VideoFrame(WIDTH, HEIGHT, "rgb24") - frame.pts = i * 2000 - frame.time_base = Fraction(1, 48000) + for packet in stream.encode(frame): + self.assertEqual(packet.time_base, Fraction(1, 24)) + output.mux(packet) - for packet in stream.encode(frame): + for packet in stream.encode(None): self.assertEqual(packet.time_base, Fraction(1, 24)) output.mux(packet) - for packet in stream.encode(None): - self.assertEqual(packet.time_base, Fraction(1, 24)) - output.mux(packet) + def test_encoding_with_unicode_filename(self): + path = self.sandboxed("¢∞§¶•ªº.mov") - output.close() + with av.open(path, "w") as output: + write_rgb_rotate(output) + with av.open(path) as input: + assert_rgb_rotate(self, input) class TestBasicAudioEncoding(TestCase): - def test_audio_transcode(self): - + def test_default_options(self): + with av.open(self.sandboxed("output.mov"), "w") as output: + stream = output.add_stream("mp2") + self.assertEqual(stream.bit_rate, 128000) + self.assertEqual(stream.format.name, "s16") + self.assertEqual(stream.rate, 48000) + self.assertEqual(stream.ticks_per_frame, 1) + self.assertEqual(stream.time_base, None) + + def test_transcode(self): path = self.sandboxed("audio_transcode.mov") - output = av.open(path, "w") - output.metadata["title"] = "container" - output.metadata["key"] = "value" - - sample_rate = 48000 - channel_layout = "stereo" - channels = 2 - sample_fmt = "s16" - - stream = output.add_stream("mp2", sample_rate) - - ctx = stream.codec_context - ctx.time_base = sample_rate - ctx.sample_rate = sample_rate - ctx.format = sample_fmt - ctx.layout = channel_layout - ctx.channels = channels - - src = av.open(fate_suite("audio-reference/chorusnoise_2ch_44kHz_s16.wav")) - for frame in src.decode(audio=0): - for packet in stream.encode(frame): - output.mux(packet) - for packet in stream.encode(None): - output.mux(packet) + with av.open(path, "w") as output: + output.metadata["title"] = "container" + output.metadata["key"] = "value" - output.close() + sample_rate = 48000 + channel_layout = "stereo" + channels = 2 + sample_fmt = "s16" - container = av.open(path) - self.assertEqual(len(container.streams), 1) - self.assertEqual( - container.metadata.get("title"), "container", container.metadata - ) - self.assertEqual(container.metadata.get("key"), None) + stream = output.add_stream("mp2", sample_rate) - stream = container.streams[0] - self.assertIsInstance(stream, AudioStream) - self.assertEqual(stream.codec_context.sample_rate, sample_rate) - self.assertEqual(stream.codec_context.format.name, "s16p") - self.assertEqual(stream.codec_context.channels, channels) + ctx = stream.codec_context + ctx.time_base = sample_rate + ctx.sample_rate = sample_rate + ctx.format = sample_fmt + ctx.layout = channel_layout + ctx.channels = channels + with av.open( + fate_suite("audio-reference/chorusnoise_2ch_44kHz_s16.wav") + ) as src: + for frame in src.decode(audio=0): + for packet in stream.encode(frame): + output.mux(packet) -class TestEncodeStreamSemantics(TestCase): - def test_audio_default_options(self): - output = av.open(self.sandboxed("output.mov"), "w") - - stream = output.add_stream("mp2") - self.assertEqual(stream.bit_rate, 128000) - self.assertEqual(stream.format.name, "s16") - self.assertEqual(stream.rate, 48000) - self.assertEqual(stream.ticks_per_frame, 1) - self.assertEqual(stream.time_base, None) - - def test_video_default_options(self): - output = av.open(self.sandboxed("output.mov"), "w") - - stream = output.add_stream("mpeg4") - self.assertEqual(stream.bit_rate, 1024000) - self.assertEqual(stream.format.height, 480) - self.assertEqual(stream.format.name, "yuv420p") - self.assertEqual(stream.format.width, 640) - self.assertEqual(stream.height, 480) - self.assertEqual(stream.pix_fmt, "yuv420p") - self.assertEqual(stream.rate, Fraction(24, 1)) - self.assertEqual(stream.ticks_per_frame, 1) - self.assertEqual(stream.time_base, None) - self.assertEqual(stream.width, 640) + for packet in stream.encode(None): + output.mux(packet) + + with av.open(path) as container: + self.assertEqual(len(container.streams), 1) + self.assertEqual( + container.metadata.get("title"), "container", container.metadata + ) + self.assertEqual(container.metadata.get("key"), None) + stream = container.streams[0] + self.assertIsInstance(stream, AudioStream) + self.assertEqual(stream.codec_context.sample_rate, sample_rate) + self.assertEqual(stream.codec_context.format.name, "s16p") + self.assertEqual(stream.codec_context.channels, channels) + + +class TestEncodeStreamSemantics(TestCase): def test_stream_index(self): - output = av.open(self.sandboxed("output.mov"), "w") - - vstream = output.add_stream("mpeg4", 24) - vstream.pix_fmt = "yuv420p" - vstream.width = 320 - vstream.height = 240 - - astream = output.add_stream("mp2", 48000) - astream.channels = 2 - astream.format = "s16" - - self.assertEqual(vstream.index, 0) - self.assertEqual(astream.index, 1) - - vframe = VideoFrame(320, 240, "yuv420p") - vpacket = vstream.encode(vframe)[0] - - self.assertIs(vpacket.stream, vstream) - self.assertEqual(vpacket.stream_index, 0) - - for i in range(10): - if astream.frame_size != 0: - frame_size = astream.frame_size - else: - # decoder didn't indicate constant frame size - frame_size = 1000 - aframe = AudioFrame("s16", "stereo", samples=frame_size) - aframe.rate = 48000 - apackets = astream.encode(aframe) - if apackets: - apacket = apackets[0] - break - - self.assertIs(apacket.stream, astream) - self.assertEqual(apacket.stream_index, 1) - - def test_audio_set_time_base_and_id(self): - output = av.open(self.sandboxed("output.mov"), "w") - - stream = output.add_stream("mp2") - self.assertEqual(stream.rate, 48000) - self.assertEqual(stream.time_base, None) - stream.time_base = Fraction(1, 48000) - self.assertEqual(stream.time_base, Fraction(1, 48000)) - self.assertEqual(stream.id, 0) - stream.id = 1 - self.assertEqual(stream.id, 1) + with av.open(self.sandboxed("output.mov"), "w") as output: + vstream = output.add_stream("mpeg4", 24) + vstream.pix_fmt = "yuv420p" + vstream.width = 320 + vstream.height = 240 + + astream = output.add_stream("mp2", 48000) + astream.channels = 2 + astream.format = "s16" + + self.assertEqual(vstream.index, 0) + self.assertEqual(astream.index, 1) + + vframe = VideoFrame(320, 240, "yuv420p") + vpacket = vstream.encode(vframe)[0] + + self.assertIs(vpacket.stream, vstream) + self.assertEqual(vpacket.stream_index, 0) + + for i in range(10): + if astream.frame_size != 0: + frame_size = astream.frame_size + else: + # decoder didn't indicate constant frame size + frame_size = 1000 + aframe = AudioFrame("s16", "stereo", samples=frame_size) + aframe.rate = 48000 + apackets = astream.encode(aframe) + if apackets: + apacket = apackets[0] + break + + self.assertIs(apacket.stream, astream) + self.assertEqual(apacket.stream_index, 1) + + def test_set_id_and_time_base(self): + with av.open(self.sandboxed("output.mov"), "w") as output: + stream = output.add_stream("mp2") + + # set id + self.assertEqual(stream.id, 0) + stream.id = 1 + self.assertEqual(stream.id, 1) + + # set time_base + self.assertEqual(stream.time_base, None) + stream.time_base = Fraction(1, 48000) + self.assertEqual(stream.time_base, Fraction(1, 48000)) diff --git a/tests/test_python_io.py b/tests/test_python_io.py index 0716429c8..13e495103 100644 --- a/tests/test_python_io.py +++ b/tests/test_python_io.py @@ -87,14 +87,15 @@ def test_reading_from_write_readonly(self): ) def test_writing_to_buffer(self): - fh = BytesIO() + buf = BytesIO() - self.write(fh) + self.write(buf) # Check contents. - self.assertTrue(fh.tell()) - fh.seek(0) - assert_rgb_rotate(self, av.open(fh)) + self.assertTrue(buf.tell()) + buf.seek(0) + with av.open(buf) as container: + assert_rgb_rotate(self, container) def test_writing_to_file(self): path = self.sandboxed("writing.mp4") From 4c68897c27e06f072d938434d0d4651f8a4c2817 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Sat, 26 Mar 2022 11:15:45 +0100 Subject: [PATCH 064/846] Ensure av_write_trailer is only called once (fixes: #613) We must only ever call av_write_trailer *once*, otherwise we get a segmentation fault. Therefore no matter whether it succeeds or not we must absolutely set self._done. Also we need to be more careful when closing the streams' CodecContext: - It's too late to iterate over the streams in __dealloc__ as Python object destruction may have already started. - We must not error if there is not CodecContext or if the CodecContext has already been closed by the user. --- av/container/output.pyx | 24 +++++++++++++----------- tests/test_python_io.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 11 deletions(-) diff --git a/av/container/output.pyx b/av/container/output.pyx index 9910dc930..4be328446 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -17,18 +17,16 @@ log = logging.getLogger(__name__) cdef close_output(OutputContainer self): - cdef Stream stream - if self._started and not self._done: - self.err_check(lib.av_write_trailer(self.ptr)) - - for stream in self.streams: - stream.codec_context.close() - - if self.file is None and not self.ptr.oformat.flags & lib.AVFMT_NOFILE: - lib.avio_closep(&self.ptr.pb) - - self._done = True + # We must only ever call av_write_trailer *once*, otherwise we get a + # segmentation fault. Therefore no matter whether it succeeds or not + # we must absolutely set self._done. + try: + self.err_check(lib.av_write_trailer(self.ptr)) + finally: + if self.file is None and not (self.ptr.oformat.flags & lib.AVFMT_NOFILE): + lib.avio_closep(&self.ptr.pb) + self._done = True cdef class OutputContainer(Container): @@ -195,6 +193,10 @@ cdef class OutputContainer(Container): self._started = True def close(self): + for stream in self.streams: + if stream.codec_context: + stream.codec_context.close(strict=False) + close_output(self) def mux(self, packets): diff --git a/tests/test_python_io.py b/tests/test_python_io.py index 13e495103..9eb1d9e25 100644 --- a/tests/test_python_io.py +++ b/tests/test_python_io.py @@ -6,6 +6,20 @@ from .test_encode import assert_rgb_rotate, write_rgb_rotate +class BrokenBuffer(BytesIO): + """ + Buffer which can be "broken" to simulate an I/O error. + """ + + broken = False + + def write(self, data): + if self.broken: + raise OSError("It's broken") + else: + return super().write(data) + + class ReadOnlyBuffer: """ Minimal buffer which *only* implements the read() method. @@ -97,6 +111,29 @@ def test_writing_to_buffer(self): with av.open(buf) as container: assert_rgb_rotate(self, container) + def test_writing_to_buffer_broken(self): + buf = BrokenBuffer() + + with self.assertRaises(OSError): + with av.open(buf, "w", "mp4") as container: + write_rgb_rotate(container) + + # break I/O + buf.broken = True + + def test_writing_to_buffer_broken_with_close(self): + buf = BrokenBuffer() + + with av.open(buf, "w", "mp4") as container: + write_rgb_rotate(container) + + # break I/O + buf.broken = True + + # try to close file + with self.assertRaises(OSError): + container.close() + def test_writing_to_file(self): path = self.sandboxed("writing.mp4") From 0299577c3335614e835a5c9bbbcf47a8e324aa1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Sat, 26 Mar 2022 15:18:57 +0100 Subject: [PATCH 065/846] Add VideoFrame ndarray operations for rgb48be, rgb48le, rgb64be, rgb64le --- av/video/frame.pyx | 35 +++++++++++++++++++++++++++-- tests/test_videoframe.py | 48 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 09cbe98e5..499b32af1 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -1,3 +1,5 @@ +import sys + from libc.stdint cimport uint8_t from av.enum cimport define_enum @@ -31,6 +33,13 @@ PictureType = define_enum('PictureType', __name__, ( )) +cdef byteswap_array(array, bint big_endian): + if (sys.byteorder == 'big') != big_endian: + return array.byteswap() + else: + return array + + cdef copy_array_to_plane(array, VideoPlane plane, unsigned int bytes_per_pixel): cdef bytes imgbytes = array.tobytes() cdef const uint8_t[:] i_buf = imgbytes @@ -48,7 +57,7 @@ cdef copy_array_to_plane(array, VideoPlane plane, unsigned int bytes_per_pixel): o_pos += o_stride -cdef useful_array(VideoPlane plane, unsigned int bytes_per_pixel=1): +cdef useful_array(VideoPlane plane, unsigned int bytes_per_pixel=1, str dtype='uint8'): """ Return the useful part of the VideoPlane as a single dimensional array. @@ -57,7 +66,7 @@ cdef useful_array(VideoPlane plane, unsigned int bytes_per_pixel=1): import numpy as np cdef size_t total_line_size = abs(plane.line_size) cdef size_t useful_line_size = plane.width * bytes_per_pixel - arr = np.frombuffer(plane, np.uint8) + arr = np.frombuffer(plane, np.dtype(dtype)) if total_line_size != useful_line_size: arr = arr.reshape(-1, total_line_size)[:, 0:useful_line_size].reshape(-1) return arr @@ -265,6 +274,16 @@ cdef class VideoFrame(Frame): return useful_array(frame.planes[0], 3).reshape(frame.height, frame.width, -1) elif frame.format.name in ('argb', 'rgba', 'abgr', 'bgra'): return useful_array(frame.planes[0], 4).reshape(frame.height, frame.width, -1) + elif frame.format.name in ('rgb48be', 'rgb48le'): + return byteswap_array( + useful_array(frame.planes[0], 6, 'uint16').reshape(frame.height, frame.width, -1), + frame.format.name == 'rgb48be', + ) + elif frame.format.name in ('rgba64be', 'rgba64le'): + return byteswap_array( + useful_array(frame.planes[0], 8, 'uint16').reshape(frame.height, frame.width, -1), + frame.format.name == 'rgba64be', + ) elif frame.format.name in ('gray', 'gray8', 'rgb8', 'bgr8'): return useful_array(frame.planes[0]).reshape(frame.height, frame.width) elif frame.format.name == 'pal8': @@ -332,6 +351,18 @@ cdef class VideoFrame(Frame): check_ndarray_shape(array, array.shape[2] == 4) elif format in ('gray', 'gray8', 'rgb8', 'bgr8'): check_ndarray(array, 'uint8', 2) + elif format in ('rgb48be', 'rgb48le'): + check_ndarray(array, 'uint16', 3) + check_ndarray_shape(array, array.shape[2] == 3) + frame = VideoFrame(array.shape[1], array.shape[0], format) + copy_array_to_plane(byteswap_array(array, format == 'rgb48be'), frame.planes[0], 6) + return frame + elif format in ('rgba64be', 'rgba64le'): + check_ndarray(array, 'uint16', 3) + check_ndarray_shape(array, array.shape[2] == 4) + frame = VideoFrame(array.shape[1], array.shape[0], format) + copy_array_to_plane(byteswap_array(array, format == 'rgba64be'), frame.planes[0], 8) + return frame else: raise ValueError('Conversion from numpy array with format `%s` is not yet supported' % format) diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index eba38e4f9..aa03ae9dc 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -238,6 +238,54 @@ def test_ndarray_yuyv422_align(self): self.assertEqual(frame.format.name, "yuyv422") self.assertNdarraysEqual(frame.to_ndarray(), array) + def test_ndarray_rgb48be(self): + array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="rgb48be") + self.assertEqual(frame.width, 640) + self.assertEqual(frame.height, 480) + self.assertEqual(frame.format.name, "rgb48be") + self.assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining red value of first pixel + self.assertEqual(memoryview(frame.planes[0])[0], (array[0][0][0] >> 8) & 0xFF) + self.assertEqual(memoryview(frame.planes[0])[1], array[0][0][0] & 0xFF) + + def test_ndarray_rgb48le(self): + array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="rgb48le") + self.assertEqual(frame.width, 640) + self.assertEqual(frame.height, 480) + self.assertEqual(frame.format.name, "rgb48le") + self.assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining red value of first pixel + self.assertEqual(memoryview(frame.planes[0])[0], array[0][0][0] & 0xFF) + self.assertEqual(memoryview(frame.planes[0])[1], (array[0][0][0] >> 8) & 0xFF) + + def test_ndarray_rgba64be(self): + array = numpy.random.randint(0, 65536, size=(480, 640, 4), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="rgba64be") + self.assertEqual(frame.width, 640) + self.assertEqual(frame.height, 480) + self.assertEqual(frame.format.name, "rgba64be") + self.assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining red value of first pixel + self.assertEqual(memoryview(frame.planes[0])[0], (array[0][0][0] >> 8) & 0xFF) + self.assertEqual(memoryview(frame.planes[0])[1], array[0][0][0] & 0xFF) + + def test_ndarray_rgba64le(self): + array = numpy.random.randint(0, 65536, size=(480, 640, 4), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="rgba64le") + self.assertEqual(frame.width, 640) + self.assertEqual(frame.height, 480) + self.assertEqual(frame.format.name, "rgba64le") + self.assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining red value of first pixel + self.assertEqual(memoryview(frame.planes[0])[0], array[0][0][0] & 0xFF) + self.assertEqual(memoryview(frame.planes[0])[1], (array[0][0][0] >> 8) & 0xFF) + def test_ndarray_rgb8(self): array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="rgb8") From 76afdefb119d4adbf356f9330943507f97a7f38d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Sat, 26 Mar 2022 21:32:37 +0100 Subject: [PATCH 066/846] Add VideoFrame ndarray operations for gray16be, gray16le (fixes: #674) --- av/video/frame.pyx | 20 +++++++++++++++++-- tests/test_videoframe.py | 43 ++++++++++++++++++++++++++++++++-------- 2 files changed, 53 insertions(+), 10 deletions(-) diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 499b32af1..6a3add3e1 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -250,6 +250,9 @@ cdef class VideoFrame(Frame): .. note:: Numpy must be installed. + .. note:: For formats which return an array of ``uint16`, the samples + will be in the system's native byte order. + .. note:: For ``pal8``, an ``(image, palette)`` tuple will be returned, with the palette being in ARGB (PyAV will swap bytes if needed). @@ -274,6 +277,13 @@ cdef class VideoFrame(Frame): return useful_array(frame.planes[0], 3).reshape(frame.height, frame.width, -1) elif frame.format.name in ('argb', 'rgba', 'abgr', 'bgra'): return useful_array(frame.planes[0], 4).reshape(frame.height, frame.width, -1) + elif frame.format.name in ('gray', 'gray8', 'rgb8', 'bgr8'): + return useful_array(frame.planes[0]).reshape(frame.height, frame.width) + elif frame.format.name in ('gray16be', 'gray16le'): + return byteswap_array( + useful_array(frame.planes[0], 2, 'uint16').reshape(frame.height, frame.width), + frame.format.name == 'gray16be', + ) elif frame.format.name in ('rgb48be', 'rgb48le'): return byteswap_array( useful_array(frame.planes[0], 6, 'uint16').reshape(frame.height, frame.width, -1), @@ -284,8 +294,6 @@ cdef class VideoFrame(Frame): useful_array(frame.planes[0], 8, 'uint16').reshape(frame.height, frame.width, -1), frame.format.name == 'rgba64be', ) - elif frame.format.name in ('gray', 'gray8', 'rgb8', 'bgr8'): - return useful_array(frame.planes[0]).reshape(frame.height, frame.width) elif frame.format.name == 'pal8': image = useful_array(frame.planes[0]).reshape(frame.height, frame.width) palette = np.frombuffer(frame.planes[1], 'i4').astype('>i4').reshape(-1, 1).view(np.uint8) @@ -311,6 +319,9 @@ cdef class VideoFrame(Frame): """ Construct a frame from a numpy array. + .. note:: For formats which expect an array of ``uint16``, the samples + must be in the system's native byte order. + .. note:: for ``pal8``, an ``(image, palette)`` pair must be passed. `palette` must have shape (256, 4) and is given in ARGB format (PyAV will swap bytes if needed). @@ -351,6 +362,11 @@ cdef class VideoFrame(Frame): check_ndarray_shape(array, array.shape[2] == 4) elif format in ('gray', 'gray8', 'rgb8', 'bgr8'): check_ndarray(array, 'uint8', 2) + elif format in ('gray16be', 'gray16le'): + check_ndarray(array, 'uint16', 2) + frame = VideoFrame(array.shape[1], array.shape[0], format) + copy_array_to_plane(byteswap_array(array, format == 'gray16be'), frame.planes[0], 2) + return frame elif format in ('rgb48be', 'rgb48le'): check_ndarray(array, 'uint16', 3) check_ndarray_shape(array, array.shape[2] == 3) diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index aa03ae9dc..09ab06b13 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -139,6 +139,15 @@ def test_to_image_with_dimensions(self): class TestVideoFrameNdarray(TestCase): + def assertPixelValue16(self, plane, expected, byteorder: str): + view = memoryview(plane) + if byteorder == "big": + self.assertEqual(view[0], (expected >> 8) & 0xFF) + self.assertEqual(view[1], expected & 0xFF) + else: + self.assertEqual(view[0], expected & 0xFF) + self.assertEqual(view[1], (expected >> 8) & 0xFF) + def test_basic_to_ndarray(self): frame = VideoFrame(640, 480, "rgb24") array = frame.to_ndarray() @@ -238,6 +247,28 @@ def test_ndarray_yuyv422_align(self): self.assertEqual(frame.format.name, "yuyv422") self.assertNdarraysEqual(frame.to_ndarray(), array) + def test_ndarray_gray16be(self): + array = numpy.random.randint(0, 65536, size=(480, 640), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="gray16be") + self.assertEqual(frame.width, 640) + self.assertEqual(frame.height, 480) + self.assertEqual(frame.format.name, "gray16be") + self.assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining value of first pixel + self.assertPixelValue16(frame.planes[0], array[0][0], "big") + + def test_ndarray_gray16le(self): + array = numpy.random.randint(0, 65536, size=(480, 640), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="gray16le") + self.assertEqual(frame.width, 640) + self.assertEqual(frame.height, 480) + self.assertEqual(frame.format.name, "gray16le") + self.assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining value of first pixel + self.assertPixelValue16(frame.planes[0], array[0][0], "little") + def test_ndarray_rgb48be(self): array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="rgb48be") @@ -247,8 +278,7 @@ def test_ndarray_rgb48be(self): self.assertNdarraysEqual(frame.to_ndarray(), array) # check endianness by examining red value of first pixel - self.assertEqual(memoryview(frame.planes[0])[0], (array[0][0][0] >> 8) & 0xFF) - self.assertEqual(memoryview(frame.planes[0])[1], array[0][0][0] & 0xFF) + self.assertPixelValue16(frame.planes[0], array[0][0][0], "big") def test_ndarray_rgb48le(self): array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) @@ -259,8 +289,7 @@ def test_ndarray_rgb48le(self): self.assertNdarraysEqual(frame.to_ndarray(), array) # check endianness by examining red value of first pixel - self.assertEqual(memoryview(frame.planes[0])[0], array[0][0][0] & 0xFF) - self.assertEqual(memoryview(frame.planes[0])[1], (array[0][0][0] >> 8) & 0xFF) + self.assertPixelValue16(frame.planes[0], array[0][0][0], "little") def test_ndarray_rgba64be(self): array = numpy.random.randint(0, 65536, size=(480, 640, 4), dtype=numpy.uint16) @@ -271,8 +300,7 @@ def test_ndarray_rgba64be(self): self.assertNdarraysEqual(frame.to_ndarray(), array) # check endianness by examining red value of first pixel - self.assertEqual(memoryview(frame.planes[0])[0], (array[0][0][0] >> 8) & 0xFF) - self.assertEqual(memoryview(frame.planes[0])[1], array[0][0][0] & 0xFF) + self.assertPixelValue16(frame.planes[0], array[0][0][0], "big") def test_ndarray_rgba64le(self): array = numpy.random.randint(0, 65536, size=(480, 640, 4), dtype=numpy.uint16) @@ -283,8 +311,7 @@ def test_ndarray_rgba64le(self): self.assertNdarraysEqual(frame.to_ndarray(), array) # check endianness by examining red value of first pixel - self.assertEqual(memoryview(frame.planes[0])[0], array[0][0][0] & 0xFF) - self.assertEqual(memoryview(frame.planes[0])[1], (array[0][0][0] >> 8) & 0xFF) + self.assertPixelValue16(frame.planes[0], array[0][0][0], "little") def test_ndarray_rgb8(self): array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) From 3c01e4ee5b8a8d241ed8d92c25a168dc421a6d88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Sun, 27 Mar 2022 11:00:10 +0200 Subject: [PATCH 067/846] [scratchpad] update the "remux" example --- scratchpad/remux.py | 66 +++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 38 deletions(-) diff --git a/scratchpad/remux.py b/scratchpad/remux.py index 5a2946977..99de07c2c 100644 --- a/scratchpad/remux.py +++ b/scratchpad/remux.py @@ -1,73 +1,63 @@ -import array import argparse import logging -import sys -import pprint -import subprocess -from PIL import Image - -from av import open, time_base +import av logging.basicConfig(level=logging.DEBUG) -def format_time(time, time_base): - if time is None: - return 'None' - return '%.3fs (%s or %s/%s)' % (time_base * time, time_base * time, time_base.numerator * time, time_base.denominator) - - arg_parser = argparse.ArgumentParser() -arg_parser.add_argument('input') -arg_parser.add_argument('output') -arg_parser.add_argument('-F', '--iformat') -arg_parser.add_argument('-O', '--ioption', action='append', default=[]) -arg_parser.add_argument('-f', '--oformat') -arg_parser.add_argument('-o', '--ooption', action='append', default=[]) -arg_parser.add_argument('-a', '--noaudio', action='store_true') -arg_parser.add_argument('-v', '--novideo', action='store_true') -arg_parser.add_argument('-s', '--nosubs', action='store_true') -arg_parser.add_argument('-d', '--nodata', action='store_true') -arg_parser.add_argument('-c', '--count', type=int, default=0) +arg_parser.add_argument("input") +arg_parser.add_argument("output") +arg_parser.add_argument("-F", "--iformat") +arg_parser.add_argument("-O", "--ioption", action="append", default=[]) +arg_parser.add_argument("-f", "--oformat") +arg_parser.add_argument("-o", "--ooption", action="append", default=[]) +arg_parser.add_argument("-a", "--noaudio", action="store_true") +arg_parser.add_argument("-v", "--novideo", action="store_true") +arg_parser.add_argument("-s", "--nosubs", action="store_true") +arg_parser.add_argument("-d", "--nodata", action="store_true") +arg_parser.add_argument("-c", "--count", type=int, default=0) args = arg_parser.parse_args() -input_ = open(args.input, +input_ = av.open( + args.input, format=args.iformat, - options=dict(x.split('=') for x in args.ioption), + options=dict(x.split("=") for x in args.ioption), ) -output = open(args.output, 'w', +output = av.open( + args.output, + "w", format=args.oformat, - options=dict(x.split('=') for x in args.ooption), + options=dict(x.split("=") for x in args.ooption), ) in_to_out = {} for i, stream in enumerate(input_.streams): - if ( - (stream.type == b'audio' and not args.noaudio) or - (stream.type == b'video' and not args.novideo) or - (stream.type == b'subtitle' and not args.nosubtitle) or - (stream.type == b'data' and not args.nodata) + (stream.type == "audio" and not args.noaudio) + or (stream.type == "video" and not args.novideo) + or (stream.type == "subtitle" and not args.nosubtitle) + or (stream.type == "data" and not args.nodata) ): - in_to_out[stream] = ostream = output.add_stream(template=stream) + in_to_out[stream] = output.add_stream(template=stream) -for i, packet in enumerate(input_.demux(in_to_out.keys())): +for i, packet in enumerate(input_.demux(list(in_to_out.keys()))): if args.count and i >= args.count: break - print('%02d %r' % (i, packet)) - print('\tin: ', packet.stream) + print("%02d %r" % (i, packet)) + print("\tin: ", packet.stream) if packet.dts is None: continue packet.stream = in_to_out[packet.stream] - print('\tout:', packet.stream) + print("\tout:", packet.stream) output.mux(packet) From 9cbe441d637be15d5b4b57211a7df3958c3c0a02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Sun, 27 Mar 2022 07:30:07 +0200 Subject: [PATCH 068/846] [package] update FFmpeg binaries for wheels (fixes: #921) This updates several packages to fix security vulnerabilities and adds support for vpx. --- docs/overview/installation.rst | 1 + scripts/fetch-vendor.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/overview/installation.rst b/docs/overview/installation.rst index b511375e9..b019e1e30 100644 --- a/docs/overview/installation.rst +++ b/docs/overview/installation.rst @@ -30,6 +30,7 @@ Currently FFmpeg 4.4.1 is used with the following features enabled for all platf - libtheora - libtwolame - libvorbis +- libvpx - libx264 - libx265 - libxml2 diff --git a/scripts/fetch-vendor.json b/scripts/fetch-vendor.json index a39f9c113..c11036e23 100644 --- a/scripts/fetch-vendor.json +++ b/scripts/fetch-vendor.json @@ -1,3 +1,3 @@ { - "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/4.4.1-1/ffmpeg-{platform}.tar.gz"] + "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/4.4.1-2/ffmpeg-{platform}.tar.gz"] } From 841debd91f2735d4b54ec77bef5296e2aab06b15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Sun, 27 Mar 2022 10:49:33 +0200 Subject: [PATCH 069/846] [filters] release GIL when pushing/pulling frames (fixes: #527) --- av/filter/context.pyx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/av/filter/context.pyx b/av/filter/context.pyx index 9c9a1fa20..4b7eaed08 100644 --- a/av/filter/context.pyx +++ b/av/filter/context.pyx @@ -76,12 +76,17 @@ cdef class FilterContext(object): err_check(lib.avfilter_link(self.ptr, output_idx, input_.ptr, input_idx)) def push(self, Frame frame): + cdef int res if frame is None: - err_check(lib.av_buffersrc_write_frame(self.ptr, NULL)) + with nogil: + res = lib.av_buffersrc_write_frame(self.ptr, NULL) + err_check(res) return elif self.filter.name in ('abuffer', 'buffer'): - err_check(lib.av_buffersrc_write_frame(self.ptr, frame.ptr)) + with nogil: + res = lib.av_buffersrc_write_frame(self.ptr, frame.ptr) + err_check(res) return # Delegate to the input. @@ -92,8 +97,9 @@ cdef class FilterContext(object): self.inputs[0].linked.context.push(frame) def pull(self): - cdef Frame frame + cdef int res + if self.filter.name == 'buffersink': frame = alloc_video_frame() elif self.filter.name == 'abuffersink': @@ -108,7 +114,10 @@ cdef class FilterContext(object): self.graph.configure() - err_check(lib.av_buffersink_get_frame(self.ptr, frame.ptr)) + with nogil: + res = lib.av_buffersink_get_frame(self.ptr, frame.ptr) + err_check(res) + frame._init_user_attributes() frame.time_base = avrational_to_fraction(&self.ptr.inputs[0].time_base) return frame From 485221eb8badd271f9b89bebfa48039edaf8ac5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Sun, 27 Mar 2022 12:22:48 +0200 Subject: [PATCH 070/846] [issues] raise number of operations per run --- .github/workflows/issues.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml index 1c9a67887..d3ea994cf 100644 --- a/.github/workflows/issues.yml +++ b/.github/workflows/issues.yml @@ -15,3 +15,4 @@ jobs: days-before-close: 14 days-before-pr-stale: -1 days-before-pr-close: -1 + operations-per-run: 60 From 95372bfbfe38e98447982d801749c6acff36181b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Sun, 27 Mar 2022 12:29:16 +0200 Subject: [PATCH 071/846] [thread type] improve documentation for AUTO (fixes: #365) --- av/codec/context.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/codec/context.pyx b/av/codec/context.pyx index ec27d02ed..0b3845a0d 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -48,7 +48,7 @@ ThreadType = define_enum('ThreadType', __name__, ( ('SLICE', lib.FF_THREAD_SLICE, """Decode more than one part of a single frame at once"""), ('AUTO', lib.FF_THREAD_SLICE | lib.FF_THREAD_FRAME, - """Either method."""), + """Decode using both FRAME and SLICE methods."""), ), is_flags=True) SkipType = define_enum('SkipType', __name__, ( From f74bcb21e838dadcb75b5dd8ee6abf1cf2a8bf6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Sun, 27 Mar 2022 16:07:09 +0200 Subject: [PATCH 072/846] [tests] uniformize file probing tests - test all attributes for every media type - sort tested attributes alphabetically - move multi-valued tests --- tests/test_file_probing.py | 53 ++++++++++++++------------------------ 1 file changed, 20 insertions(+), 33 deletions(-) diff --git a/tests/test_file_probing.py b/tests/test_file_probing.py index 1d7b5a033..69b356cb5 100644 --- a/tests/test_file_probing.py +++ b/tests/test_file_probing.py @@ -10,19 +10,17 @@ def setUp(self): self.file = av.open(fate_suite("aac/latm_stereo_to_51.ts")) def test_container_probing(self): + self.assertEqual(self.file.bit_rate, 269558) + self.assertEqual(self.file.duration, 6165333) self.assertEqual(str(self.file.format), "") self.assertEqual(self.file.format.name, "mpegts") self.assertEqual( self.file.format.long_name, "MPEG-TS (MPEG-2 Transport Stream)" ) + self.assertEqual(self.file.metadata, {}) self.assertEqual(self.file.size, 207740) - - # This is a little odd, but on OS X with FFmpeg we get a different value. - self.assertIn(self.file.bit_rate, (269558, 270494)) - - self.assertEqual(len(self.file.streams), 1) self.assertEqual(self.file.start_time, 1400000) - self.assertEqual(self.file.metadata, {}) + self.assertEqual(len(self.file.streams), 1) def test_stream_probing(self): stream = self.file.streams[0] @@ -73,16 +71,15 @@ def setUp(self): self.file = av.open(path) def test_container_probing(self): + self.assertEqual(self.file.bit_rate, 0) + self.assertEqual(self.file.duration, None) self.assertEqual(str(self.file.format), "") self.assertEqual(self.file.format.name, "flac") self.assertEqual(self.file.format.long_name, "raw FLAC") + self.assertEqual(self.file.metadata, {}) self.assertEqual(self.file.size, 0) - self.assertEqual(self.file.bit_rate, 0) - self.assertEqual(self.file.duration, None) - - self.assertEqual(len(self.file.streams), 1) self.assertEqual(self.file.start_time, None) - self.assertEqual(self.file.metadata, {}) + self.assertEqual(len(self.file.streams), 1) def test_stream_probing(self): stream = self.file.streams[0] @@ -127,16 +124,13 @@ def setUp(self): self.file = av.open(fate_suite("mxf/track_01_v02.mxf")) def test_container_probing(self): - + self.assertEqual(self.file.bit_rate, 27872687) + self.assertEqual(self.file.duration, 417083) self.assertEqual(str(self.file.format), "") self.assertEqual(self.file.format.name, "mxf") self.assertEqual(self.file.format.long_name, "MXF (Material eXchange Format)") self.assertEqual(self.file.size, 1453153) - - self.assertEqual( - self.file.bit_rate, 8 * self.file.size * av.time_base // self.file.duration - ) - self.assertEqual(self.file.duration, 417083) + self.assertEqual(self.file.start_time, 0) self.assertEqual(len(self.file.streams), 4) for key, value, min_version in ( @@ -210,18 +204,13 @@ def setUp(self): self.file = av.open(fate_suite("sub/MovText_capability_tester.mp4")) def test_container_probing(self): + self.assertEqual(self.file.bit_rate, 810) + self.assertEqual(self.file.duration, 8140000) self.assertEqual( str(self.file.format), "" ) self.assertEqual(self.file.format.name, "mov,mp4,m4a,3gp,3g2,mj2") self.assertEqual(self.file.format.long_name, "QuickTime / MOV") - self.assertEqual(self.file.size, 825) - - self.assertEqual( - self.file.bit_rate, 8 * self.file.size * av.time_base // self.file.duration - ) - self.assertEqual(self.file.duration, 8140000) - self.assertEqual(len(self.file.streams), 1) self.assertEqual( self.file.metadata, { @@ -231,6 +220,9 @@ def test_container_probing(self): "minor_version": "1", }, ) + self.assertEqual(self.file.size, 825) + self.assertEqual(self.file.start_time, None) + self.assertEqual(len(self.file.streams), 1) def test_stream_probing(self): stream = self.file.streams[0] @@ -265,22 +257,17 @@ def setUp(self): self.file = av.open(fate_suite("mpeg2/mpeg2_field_encoding.ts")) def test_container_probing(self): + self.assertEqual(self.file.bit_rate, 3950617) + self.assertEqual(self.file.duration, 1620000) self.assertEqual(str(self.file.format), "") self.assertEqual(self.file.format.name, "mpegts") self.assertEqual( self.file.format.long_name, "MPEG-TS (MPEG-2 Transport Stream)" ) + self.assertEqual(self.file.metadata, {}) self.assertEqual(self.file.size, 800000) - - # This is a little odd, but on OS X with FFmpeg we get a different value. - self.assertIn(self.file.duration, (1620000, 1580000)) - - self.assertEqual( - self.file.bit_rate, 8 * self.file.size * av.time_base // self.file.duration - ) - self.assertEqual(len(self.file.streams), 1) self.assertEqual(self.file.start_time, 22953408322) - self.assertEqual(self.file.metadata, {}) + self.assertEqual(len(self.file.streams), 1) def test_stream_probing(self): stream = self.file.streams[0] From 52536516be2471057216b2c051de850dcace08b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Sun, 27 Mar 2022 16:30:59 +0200 Subject: [PATCH 073/846] [subtitles] remove Python 2 buffer, improve tests --- av/subtitles/subtitle.pyx | 22 --------------------- tests/test_subtitles.py | 41 ++++++++++++++++++++++++--------------- 2 files changed, 25 insertions(+), 38 deletions(-) diff --git a/av/subtitles/subtitle.pyx b/av/subtitles/subtitle.pyx index 88e5e2a90..2f22bb0be 100644 --- a/av/subtitles/subtitle.pyx +++ b/av/subtitles/subtitle.pyx @@ -145,28 +145,6 @@ cdef class BitmapSubtitlePlane(object): self.buffer_size = subtitle.ptr.w * subtitle.ptr.h self._buffer = subtitle.ptr.data[index] - # PyBuffer_FromMemory(self.ptr.data[i], self.width * self.height) - - # Legacy buffer support. For `buffer` and PIL. - # See: http://docs.python.org/2/c-api/typeobj.html#PyBufferProcs - - def __getsegcount__(self, Py_ssize_t *len_out): - if len_out != NULL: - len_out[0] = self.buffer_size - return 1 - - def __getreadbuffer__(self, Py_ssize_t index, void **data): - if index: - raise RuntimeError("accessing non-existent buffer segment") - data[0] = self._buffer - return self.buffer_size - - def __getwritebuffer__(self, Py_ssize_t index, void **data): - if index: - raise RuntimeError("accessing non-existent buffer segment") - data[0] = self._buffer - return self.buffer_size - # New-style buffer support. def __getbuffer__(self, Py_buffer *view, int flags): diff --git a/tests/test_subtitles.py b/tests/test_subtitles.py index 6ec95b852..5dfe91cef 100644 --- a/tests/test_subtitles.py +++ b/tests/test_subtitles.py @@ -9,35 +9,44 @@ def test_movtext(self): path = fate_suite("sub/MovText_capability_tester.mp4") - fh = av.open(path) subs = [] - for packet in fh.demux(): - subs.extend(packet.decode()) + with av.open(path) as container: + for packet in container.demux(): + subs.extend(packet.decode()) self.assertEqual(len(subs), 3) - self.assertIsInstance(subs[0][0], AssSubtitle) - - # The format FFmpeg gives us changed at one point. - self.assertIn( - subs[0][0].ass, - ( - "Dialogue: 0,0:00:00.97,0:00:02.54,Default,- Test 1.\\N- Test 2.\r\n", - "Dialogue: 0,0:00:00.97,0:00:02.54,Default,,0,0,0,,- Test 1.\\N- Test 2.\r\n", - ), + + subset = subs[0] + self.assertEqual(subset.format, 1) + self.assertEqual(subset.pts, 970000) + self.assertEqual(subset.start_display_time, 0) + self.assertEqual(subset.end_display_time, 1570) + + sub = subset[0] + self.assertIsInstance(sub, AssSubtitle) + self.assertEqual( + sub.ass, + "Dialogue: 0,0:00:00.97,0:00:02.54,Default,,0,0,0,,- Test 1.\\N- Test 2.\r\n", ) def test_vobsub(self): path = fate_suite("sub/vobsub.sub") - fh = av.open(path) subs = [] - for packet in fh.demux(): - subs.extend(packet.decode()) + with av.open(path) as container: + for packet in container.demux(): + subs.extend(packet.decode()) self.assertEqual(len(subs), 43) - sub = subs[0][0] + subset = subs[0] + self.assertEqual(subset.format, 0) + self.assertEqual(subset.pts, 132499044) + self.assertEqual(subset.start_display_time, 0) + self.assertEqual(subset.end_display_time, 4960) + + sub = subset[0] self.assertIsInstance(sub, BitmapSubtitle) self.assertEqual(sub.x, 259) self.assertEqual(sub.y, 379) From 0271719b85b3981accca7e7cfaaa13bb1ae31db9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Sun, 27 Mar 2022 18:47:12 +0200 Subject: [PATCH 074/846] [subtitles] use the "ass" format for subtitles, not "ass_with_timings" The default "ass_with_timings" format has been deprecated for a long time, but it remained the default before FFmpeg 5.0. We explicitly opt into the new format to have consistent behaviour across all versions. --- av/codec/context.pyx | 4 ++++ include/libavcodec/avcodec.pxd | 3 +++ tests/test_subtitles.py | 5 +---- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 0b3845a0d..fd3b26fe7 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -166,6 +166,10 @@ cdef class CodecContext(object): self.ptr.thread_count = 0 self.ptr.thread_type = 2 + # Use "ass" format for subtitles (default as of FFmpeg 5.0), not the + # deprecated "ass_with_timings" formats. + self.ptr.sub_text_format = 0 + def _get_flags(self): return self.ptr.flags diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 8e5752ae5..c02274318 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -215,6 +215,9 @@ cdef extern from "libavcodec/avcodec.h" nogil: int frame_size int channel_layout + # Subtitles. + int sub_text_format + #: .. todo:: ``get_buffer`` is deprecated for get_buffer2 in newer versions of FFmpeg. int get_buffer(AVCodecContext *ctx, AVFrame *frame) void release_buffer(AVCodecContext *ctx, AVFrame *frame) diff --git a/tests/test_subtitles.py b/tests/test_subtitles.py index 5dfe91cef..04981a938 100644 --- a/tests/test_subtitles.py +++ b/tests/test_subtitles.py @@ -24,10 +24,7 @@ def test_movtext(self): sub = subset[0] self.assertIsInstance(sub, AssSubtitle) - self.assertEqual( - sub.ass, - "Dialogue: 0,0:00:00.97,0:00:02.54,Default,,0,0,0,,- Test 1.\\N- Test 2.\r\n", - ) + self.assertEqual(sub.ass, "0,0,Default,,0,0,0,,- Test 1.\\N- Test 2.") def test_vobsub(self): From 6c45f71c4f850ff1610e49ba23b403291aa61f01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Mon, 28 Mar 2022 00:45:29 +0200 Subject: [PATCH 075/846] [package] update FFmpeg binaries to enable ALSA on Linux (fixes: #941) --- .github/workflows/tests.yml | 1 + scripts/fetch-vendor.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fa86aecd8..366c2c7b3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -238,6 +238,7 @@ jobs: - name: Build wheels env: CIBW_ARCHS: ${{ matrix.arch }} + CIBW_BEFORE_ALL_LINUX: yum install -y alsa-lib libxcb CIBW_BEFORE_BUILD: pip install cython && python scripts/fetch-vendor.py /tmp/vendor CIBW_BEFORE_BUILD_WINDOWS: pip install cython && python scripts\fetch-vendor.py C:\cibw\vendor CIBW_ENVIRONMENT_LINUX: LD_LIBRARY_PATH=/tmp/vendor/lib:$LD_LIBRARY_PATH PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig diff --git a/scripts/fetch-vendor.json b/scripts/fetch-vendor.json index c11036e23..1f935c104 100644 --- a/scripts/fetch-vendor.json +++ b/scripts/fetch-vendor.json @@ -1,3 +1,3 @@ { - "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/4.4.1-2/ffmpeg-{platform}.tar.gz"] + "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/4.4.1-4/ffmpeg-{platform}.tar.gz"] } From 75b7d9688157a1f2f064f0e72d7898477d28ac36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Mon, 28 Mar 2022 08:25:17 +0200 Subject: [PATCH 076/846] Release v9.1.0. --- CHANGELOG.rst | 17 ++++++++++++++++- av/about.py | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a30c95dac..ede2f4a90 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,9 +16,24 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. -v9.0.3.dev0 +v9.1.0 ------ +Features: + +- Add VideoFrame ndarray operations for rgb48be, rgb48le, rgb64be, rgb64le pixel formats. +- Add VideoFrame ndarray operations for gray16be, gray16le pixel formats (:issue:`674`). +- Make it possible to use av.open() on a pipe (:issue:`738`). +- Use the "ASS without timings" format when decoding subtitles. + +Fixes: + +- Update binary wheels to fix security vulnerabilities (:issue:`921`) and enable ALSA on Linux (:issue:`941`). +- Fix crash when closing an output container an encountering an I/O error (:issue:`613`). +- Fix crash when probing corrupt raw format files (:issue:`590`). +- Fix crash when manipulating streams with an unknown codec (:issue:`689`). +- Remove obsolete KEEP_SIDE_DATA and MP4A_LATM flags which are gone in FFmpeg 5.0. +- Deprecate `to_bytes()` method of Packet, Plane and SideData, use `bytes(packet)` instead. v9.0.2 ------ diff --git a/av/about.py b/av/about.py index 7e05bb723..ba2807394 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "9.0.3.dev0" +__version__ = "9.1.0" From fecc0d544a9ce28b3f6c8456fa83e88a71de24e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Wed, 30 Mar 2022 08:43:11 +0200 Subject: [PATCH 077/846] [package] update FFmpeg binaries, use delvewheel - the Linux build disables ALSA again, it's not working - the Windows build is now built from source - use `delvewheel` to delocate Windows wheels --- .github/workflows/tests.yml | 4 ++-- scripts/fetch-vendor.json | 2 +- scripts/inject-dll | 37 ------------------------------------- 3 files changed, 3 insertions(+), 40 deletions(-) delete mode 100755 scripts/inject-dll diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 366c2c7b3..b5e62d012 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -244,14 +244,14 @@ jobs: CIBW_ENVIRONMENT_LINUX: LD_LIBRARY_PATH=/tmp/vendor/lib:$LD_LIBRARY_PATH PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig CIBW_ENVIRONMENT_MACOS: PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig LDFLAGS=-headerpad_max_install_names CIBW_ENVIRONMENT_WINDOWS: INCLUDE=C:\\cibw\\vendor\\include LIB=C:\\cibw\\vendor\\lib PYAV_SKIP_TESTS=unicode_filename - CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: python scripts/inject-dll {wheel} {dest_dir} C:\cibw\vendor\bin + CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: delvewheel repair --add-path C:\cibw\vendor\bin -w {dest_dir} {wheel} CIBW_SKIP: cp36-* pp36-* pp38-win* *-musllinux* CIBW_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m unittest discover -t {project} -s tests && mv {project}/av.disabled {project}/av CIBW_TEST_REQUIRES: numpy # skip tests when there are no binary wheels of numpy CIBW_TEST_SKIP: cp37-* pp* *_i686 run: | - pip install cibuildwheel + pip install cibuildwheel delvewheel cibuildwheel --output-dir dist shell: bash - name: Upload wheels diff --git a/scripts/fetch-vendor.json b/scripts/fetch-vendor.json index 1f935c104..6e0f6a50b 100644 --- a/scripts/fetch-vendor.json +++ b/scripts/fetch-vendor.json @@ -1,3 +1,3 @@ { - "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/4.4.1-4/ffmpeg-{platform}.tar.gz"] + "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/4.4.1-5/ffmpeg-{platform}.tar.gz"] } diff --git a/scripts/inject-dll b/scripts/inject-dll deleted file mode 100755 index b382b4547..000000000 --- a/scripts/inject-dll +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env python - -import argparse -import logging -import os -import shutil -import zipfile - -parser = argparse.ArgumentParser(description="Inject DLLs into a Windows binary wheel") -parser.add_argument( - "wheel", type=str, help="the source wheel to which DLLs should be added", -) -parser.add_argument( - "dest_dir", type=str, help="the directory where to create the repaired wheel", -) -parser.add_argument( - "dll_dir", type=str, help="the directory containing the DLLs", -) - -args = parser.parse_args() -wheel_name = os.path.basename(args.wheel) -package_name = wheel_name.split("-")[0] -repaired_wheel = os.path.join(args.dest_dir, wheel_name) - -logging.basicConfig(level=logging.INFO) -logging.info("Copying '%s' to '%s'", args.wheel, repaired_wheel) -shutil.copy(args.wheel, repaired_wheel) - -logging.info("Adding DLLs from '%s' to package '%s'", args.dll_dir, package_name) -with zipfile.ZipFile(repaired_wheel, mode="a", compression=zipfile.ZIP_DEFLATED) as wheel: - for name in sorted(os.listdir(args.dll_dir)): - if name.lower().endswith(".dll"): - local_path = os.path.join(args.dll_dir, name) - archive_path = os.path.join(package_name, name) - if archive_path not in wheel.namelist(): - logging.info("Adding '%s' as '%s'", local_path, archive_path) - wheel.write(local_path, archive_path) From 9638adc183acf9e1f1886990d9642e2f13055c5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Wed, 30 Mar 2022 22:08:36 +0200 Subject: [PATCH 078/846] [package] remove Python 3.6 from classifiers --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index e7ef99bb3..6dc0cc4c3 100644 --- a/setup.py +++ b/setup.py @@ -205,7 +205,6 @@ def parse_cflags(raw_flags): "Operating System :: Unix", "Operating System :: Microsoft :: Windows", "Programming Language :: Cython", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", From 6c3cf721e1a842a1ca04203652a3e25779dbe472 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Thu, 31 Mar 2022 23:22:00 +0200 Subject: [PATCH 079/846] Release v9.1.1. --- CHANGELOG.rst | 7 +++++++ av/about.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ede2f4a90..d64fe2b7f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,13 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. +v9.1.1 +------ + +Fixes: + +- Update binary wheels to update dependencies on Windows, disable ALSA on Linux. + v9.1.0 ------ diff --git a/av/about.py b/av/about.py index ba2807394..ee502a28b 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "9.1.0" +__version__ = "9.1.1" From 5c31a26105e129aa939e40fff132a750905801b8 Mon Sep 17 00:00:00 2001 From: Philip de Nier Date: Thu, 21 May 2020 12:32:25 +0100 Subject: [PATCH 080/846] Implement custom io_open/io_close support in container --- av/container/core.pxd | 3 ++ av/container/core.pyx | 92 ++++++++++++++++++++++++++++++-- av/container/pyio.pxd | 6 +++ av/container/pyio.pyx | 24 +++++++++ include/libavformat/avformat.pxd | 16 ++++++ tests/test_python_io.py | 63 ++++++++++++++++++++++ 6 files changed, 200 insertions(+), 4 deletions(-) diff --git a/av/container/core.pxd b/av/container/core.pxd index cd2ba117a..198c96fa8 100644 --- a/av/container/core.pxd +++ b/av/container/core.pxd @@ -23,7 +23,10 @@ cdef class Container(object): cdef readonly str metadata_errors cdef readonly PyIOFile file + cdef int buffer_size cdef bint input_was_opened + cdef readonly object io_open + cdef readonly object open_files cdef readonly ContainerFormat format diff --git a/av/container/core.pyx b/av/container/core.pyx index 4ec03e487..3155f5536 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -10,6 +10,7 @@ cimport libav as lib from av.container.core cimport timeout_info from av.container.input cimport InputContainer from av.container.output cimport OutputContainer +from av.container.pyio cimport pyio_close_custom_gil, pyio_close_gil from av.enum cimport define_enum from av.error cimport err_check, stash_exception from av.format cimport build_container_format @@ -51,6 +52,77 @@ cdef int interrupt_cb (void *p) nogil: return 0 +cdef int pyav_io_open(lib.AVFormatContext *s, + lib.AVIOContext **pb, + const char *url, + int flags, + lib.AVDictionary **options) nogil: + with gil: + return pyav_io_open_gil(s, pb, url, flags, options) + + +cdef int pyav_io_open_gil(lib.AVFormatContext *s, + lib.AVIOContext **pb, + const char *url, + int flags, + lib.AVDictionary **options): + cdef Container container + cdef object file + cdef PyIOFile pyio_file + try: + container = dereference(s).opaque + + file = container.io_open( + url if url is not NULL else "", + flags, + avdict_to_dict( + dereference(options), + encoding=container.metadata_encoding, + errors=container.metadata_errors + ) + ) + + pyio_file = PyIOFile( + file, + container.buffer_size, + (flags & lib.AVIO_FLAG_WRITE) != 0 + ) + + # Add it to the container to avoid it being deallocated + container.open_files[pyio_file.iocontext.opaque] = pyio_file + + pb[0] = pyio_file.iocontext + return 0 + + except Exception as e: + return stash_exception() + + +cdef void pyav_io_close(lib.AVFormatContext *s, + lib.AVIOContext *pb) nogil: + with gil: + pyav_io_close_gil(s, pb) + + +cdef void pyav_io_close_gil(lib.AVFormatContext *s, + lib.AVIOContext *pb): + cdef Container container + try: + container = dereference(s).opaque + + if container.open_files is not None and pb.opaque in container.open_files: + pyio_close_custom_gil(pb) + + # Remove it from the container so that it can be deallocated + del container.open_files[pb.opaque] + pb.opaque = NULL + else: + pyio_close_gil(pb) + + except Exception as e: + stash_exception() + + Flags = define_enum('Flags', __name__, ( ('GENPTS', lib.AVFMT_FLAG_GENPTS, "Generate missing pts even if it requires parsing future frames."), @@ -97,7 +169,8 @@ cdef class Container(object): def __cinit__(self, sentinel, file_, format_name, options, container_options, stream_options, metadata_encoding, metadata_errors, - buffer_size, open_timeout, read_timeout): + buffer_size, open_timeout, read_timeout, + io_open): if sentinel is not _cinit_sentinel: raise RuntimeError('cannot construct base Container') @@ -121,6 +194,9 @@ cdef class Container(object): self.open_timeout = open_timeout self.read_timeout = read_timeout + self.buffer_size = buffer_size + self.io_open = io_open + if format_name is not None: self.format = ContainerFormat(format_name) @@ -158,12 +234,18 @@ cdef class Container(object): self.ptr.interrupt_callback.opaque = &self.interrupt_callback_info self.ptr.flags |= lib.AVFMT_FLAG_GENPTS + self.ptr.opaque = self # Setup Python IO. + self.open_files = {} if not isinstance(file_, basestring): self.file = PyIOFile(file_, buffer_size, self.writeable) self.ptr.pb = self.file.iocontext + if io_open is not None: + self.ptr.io_open = pyav_io_open + self.ptr.io_close = pyav_io_close + cdef lib.AVInputFormat *ifmt cdef _Dictionary c_options if not self.writeable: @@ -251,7 +333,7 @@ cdef class Container(object): def open(file, mode=None, format=None, options=None, container_options=None, stream_options=None, metadata_encoding='utf-8', metadata_errors='strict', - buffer_size=32768, timeout=None): + buffer_size=32768, timeout=None, io_open=None): """open(file, mode='r', **kwargs) Main entrypoint to opening files/streams. @@ -301,7 +383,8 @@ def open(file, mode=None, format=None, options=None, _cinit_sentinel, file, format, options, container_options, stream_options, metadata_encoding, metadata_errors, - buffer_size, open_timeout, read_timeout + buffer_size, open_timeout, read_timeout, + io_open ) if mode.startswith('w'): if stream_options: @@ -310,6 +393,7 @@ def open(file, mode=None, format=None, options=None, _cinit_sentinel, file, format, options, container_options, stream_options, metadata_encoding, metadata_errors, - buffer_size, open_timeout, read_timeout + buffer_size, open_timeout, read_timeout, + io_open ) raise ValueError("mode must be 'r' or 'w'; got %r" % mode) diff --git a/av/container/pyio.pxd b/av/container/pyio.pxd index b7597e9b3..b2a593b14 100644 --- a/av/container/pyio.pxd +++ b/av/container/pyio.pxd @@ -8,6 +8,11 @@ cdef int pyio_write(void *opaque, uint8_t *buf, int buf_size) nogil cdef int64_t pyio_seek(void *opaque, int64_t offset, int whence) nogil +cdef void pyio_close_gil(lib.AVIOContext *pb) + +cdef void pyio_close_custom_gil(lib.AVIOContext *pb) + + cdef class PyIOFile(object): # File-like source. @@ -16,6 +21,7 @@ cdef class PyIOFile(object): cdef object fwrite cdef object fseek cdef object ftell + cdef object fclose # Custom IO for above. cdef lib.AVIOContext *iocontext diff --git a/av/container/pyio.pyx b/av/container/pyio.pyx index a9e441225..17d977f3e 100644 --- a/av/container/pyio.pyx +++ b/av/container/pyio.pyx @@ -22,6 +22,7 @@ cdef class PyIOFile(object): self.fwrite = getattr(self.file, 'write', None) self.fseek = getattr(self.file, 'seek', None) self.ftell = getattr(self.file, 'tell', None) + self.fclose = getattr(self.file, 'close', None) # To be seekable the file object must have `seek` and `tell` methods. # If it also has a `seekable` method, it must return True. @@ -143,3 +144,26 @@ cdef int64_t pyio_seek_gil(void *opaque, int64_t offset, int whence): return res except Exception as e: return stash_exception() + + +cdef void pyio_close_gil(lib.AVIOContext *pb): + try: + lib.avio_close(pb) + + except Exception as e: + stash_exception() + + +cdef void pyio_close_custom_gil(lib.AVIOContext *pb): + cdef PyIOFile self + try: + self = pb.opaque + + # Flush bytes in the AVIOContext buffers to the custom I/O + lib.avio_flush(pb) + + if self.fclose is not None: + self.fclose() + + except Exception as e: + stash_exception() diff --git a/include/libavformat/avformat.pxd b/include/libavformat/avformat.pxd index 37e9f1c01..0a33cf9f6 100644 --- a/include/libavformat/avformat.pxd +++ b/include/libavformat/avformat.pxd @@ -57,6 +57,7 @@ cdef extern from "libavformat/avformat.h" nogil: int direct int seekable int max_packet_size + void *opaque # http://ffmpeg.org/doxygen/trunk/structAVIOInterruptCB.html cdef struct AVIOInterruptCB: @@ -185,6 +186,19 @@ cdef extern from "libavformat/avformat.h" nogil: int flags int64_t max_analyze_duration + void *opaque + + int (*io_open)( + AVFormatContext *s, + AVIOContext **pb, + const char *url, + int flags, + AVDictionary **options + ) + void (*io_close)( + AVFormatContext *s, + AVIOContext *pb + ) cdef AVFormatContext* avformat_alloc_context() @@ -249,6 +263,8 @@ cdef extern from "libavformat/avformat.h" nogil: int std_compliance ) + cdef void avio_flush(AVIOContext *s) + cdef int avio_close(AVIOContext *s) cdef int avio_closep(AVIOContext **s) diff --git a/tests/test_python_io.py b/tests/test_python_io.py index 9eb1d9e25..23773cc56 100644 --- a/tests/test_python_io.py +++ b/tests/test_python_io.py @@ -66,6 +66,41 @@ def seekable(self): return False +CUSTOM_IO_PROTOCOL = "pyavtest://" +CUSTOM_IO_FILENAME = "custom_io_output.mpd" + + +class CustomIOLogger(object): + """Log calls to open a file as well as method calls on the files""" + + def __init__(self, sandboxed): + self._sandboxed = sandboxed + self._log = [] + self._method_log = [] + + def __call__(self, *args, **kwargs): + self._log.append((args, kwargs)) + self._method_log.append(self.io_open(*args, **kwargs)) + return self._method_log[-1] + + def io_open(self, url, flags, options): + # Remove the protocol prefix to reveal the local filename + if CUSTOM_IO_PROTOCOL in url: + url = url.split(CUSTOM_IO_PROTOCOL, 1)[1] + path = self._sandboxed(url) + + if (flags & 3) == 3: + mode = "r+b" + elif (flags & 1) == 1: + mode = "rb" + elif (flags & 2) == 2: + mode = "wb" + else: + raise RuntimeError("Unsupported io open mode {}".format(flags)) + + return MethodLogger(open(path, mode)) + + class TestPythonIO(TestCase): def test_basic_errors(self): self.assertRaises(Exception, av.open, None) @@ -134,6 +169,34 @@ def test_writing_to_buffer_broken_with_close(self): with self.assertRaises(OSError): container.close() + def test_writing_to_custom_io(self): + + # Custom I/O that opens file in the sandbox and logs calls + wrapped_custom_io = CustomIOLogger(self.sandboxed) + + # Write a DASH package using the custom IO + with av.open( + CUSTOM_IO_PROTOCOL + CUSTOM_IO_FILENAME, "w", io_open=wrapped_custom_io + ) as container: + write_rgb_rotate(container) + + # Check that at least 3 files were opened using the custom IO: + # "CUSTOM_IO_FILENAME", init-stream0.m4s and chunk-stream-0x.m4s + self.assertGreaterEqual(len(wrapped_custom_io._log), 3) + self.assertGreaterEqual(len(wrapped_custom_io._method_log), 3) + + # Check that all files were written to + all_write = all( + method_log._filter("write") for method_log in wrapped_custom_io._method_log + ) + self.assertTrue(all_write) + + # Check that all files were closed + all_closed = all( + method_log._filter("close") for method_log in wrapped_custom_io._method_log + ) + self.assertTrue(all_closed) + def test_writing_to_file(self): path = self.sandboxed("writing.mp4") From cf434f6fdcac067c2ce2b3bb71cec3110fb4eaa4 Mon Sep 17 00:00:00 2001 From: Philip de Nier Date: Tue, 29 Mar 2022 10:48:21 +0000 Subject: [PATCH 081/846] Remove redundant custom IO AVIOContext opaque set to NULL It can result in a segmentation fault if Python garbage collected the PyIOFile immediately after removing it from the open_files map. --- av/container/core.pyx | 1 - 1 file changed, 1 deletion(-) diff --git a/av/container/core.pyx b/av/container/core.pyx index 3155f5536..983d4bc3a 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -115,7 +115,6 @@ cdef void pyav_io_close_gil(lib.AVFormatContext *s, # Remove it from the container so that it can be deallocated del container.open_files[pb.opaque] - pb.opaque = NULL else: pyio_close_gil(pb) From f956e3ec10e0ab021f67e358eea16e65f3ddc1bf Mon Sep 17 00:00:00 2001 From: Philip de Nier Date: Tue, 29 Mar 2022 11:36:10 +0000 Subject: [PATCH 082/846] Use AVFMT_FLAG_CUSTOM_IO to stop ffmpeg closing it avformat_open_input would otherwise call avio_closep when there is an error opening a custom IO file. --- av/container/core.pyx | 1 + 1 file changed, 1 insertion(+) diff --git a/av/container/core.pyx b/av/container/core.pyx index 983d4bc3a..387eb54f9 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -244,6 +244,7 @@ cdef class Container(object): if io_open is not None: self.ptr.io_open = pyav_io_open self.ptr.io_close = pyav_io_close + self.ptr.flags |= lib.AVFMT_FLAG_CUSTOM_IO cdef lib.AVInputFormat *ifmt cdef _Dictionary c_options From b7248f3ee5259214addb61cff2580ca456d888d5 Mon Sep 17 00:00:00 2001 From: Philip de Nier Date: Tue, 29 Mar 2022 13:02:28 +0000 Subject: [PATCH 083/846] Enable dash demuxer in ffmpeg build --- scripts/build-deps | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/build-deps b/scripts/build-deps index 212fe5144..33c64727e 100755 --- a/scripts/build-deps +++ b/scripts/build-deps @@ -44,6 +44,7 @@ echo ./configure --enable-debug=3 \ --enable-gpl \ --enable-libx264 \ + --enable-libxml2 \ --enable-shared \ --prefix="$PYAV_LIBRARY_PREFIX" \ || exit 2 From 3f241d0673df4da405a76ecf4bf8a3b102a3a5f0 Mon Sep 17 00:00:00 2001 From: Philip de Nier Date: Tue, 29 Mar 2022 11:38:47 +0000 Subject: [PATCH 084/846] Add content check to custom IO test --- tests/common.py | 12 +++++++++ tests/test_encode.py | 14 ++++++++-- tests/test_python_io.py | 58 +++++++++++++++++++++++------------------ 3 files changed, 56 insertions(+), 28 deletions(-) diff --git a/tests/common.py b/tests/common.py index 3707cdf5d..4235976b6 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1,3 +1,4 @@ +from contextlib import contextmanager from unittest import TestCase as _Base import datetime import errno @@ -72,6 +73,17 @@ def sandboxed(*args, **kwargs): return path +# Context manager for running a test in a directory, e.g. path is the sandbox +@contextmanager +def run_in_directory(path): + current_dir = os.getcwd() + try: + os.chdir(path) + yield + finally: + os.chdir(current_dir) + + class MethodLogger(object): def __init__(self, obj): self._obj = obj diff --git a/tests/test_encode.py b/tests/test_encode.py index e18c34330..13f73285b 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -67,7 +67,12 @@ def assert_rgb_rotate(self, input_): # Now inspect it a little. self.assertEqual(len(input_.streams), 1) - self.assertEqual(input_.metadata.get("title"), "container", input_.metadata) + self.assertEqual( + # Fallback to "Title" for the DASH format + input_.metadata.get("title", input_.metadata.get("Title")), + "container", + input_.metadata + ) self.assertEqual(input_.metadata.get("key"), None) stream = input_.streams[0] self.assertIsInstance(stream, VideoStream) @@ -77,7 +82,12 @@ def assert_rgb_rotate(self, input_): stream.average_rate, 24 ) # Only because we constructed is precisely. self.assertEqual(stream.rate, Fraction(24, 1)) - self.assertEqual(stream.time_base * stream.duration, 2) + if stream.duration is not None: + self.assertEqual(stream.time_base * stream.duration, 2) + else: + # The DASH format doesn't provide a duration for the stream + # and the container duration (micro seconds) is checked instead + self.assertEqual(input_.duration, 2000000) self.assertEqual(stream.format.name, "yuv420p") self.assertEqual(stream.format.width, WIDTH) self.assertEqual(stream.format.height, HEIGHT) diff --git a/tests/test_python_io.py b/tests/test_python_io.py index 23773cc56..998602748 100644 --- a/tests/test_python_io.py +++ b/tests/test_python_io.py @@ -2,7 +2,7 @@ import av -from .common import MethodLogger, TestCase, fate_suite +from .common import MethodLogger, TestCase, fate_suite, run_in_directory from .test_encode import assert_rgb_rotate, write_rgb_rotate @@ -73,8 +73,7 @@ def seekable(self): class CustomIOLogger(object): """Log calls to open a file as well as method calls on the files""" - def __init__(self, sandboxed): - self._sandboxed = sandboxed + def __init__(self): self._log = [] self._method_log = [] @@ -87,7 +86,6 @@ def io_open(self, url, flags, options): # Remove the protocol prefix to reveal the local filename if CUSTOM_IO_PROTOCOL in url: url = url.split(CUSTOM_IO_PROTOCOL, 1)[1] - path = self._sandboxed(url) if (flags & 3) == 3: mode = "r+b" @@ -98,7 +96,7 @@ def io_open(self, url, flags, options): else: raise RuntimeError("Unsupported io open mode {}".format(flags)) - return MethodLogger(open(path, mode)) + return MethodLogger(open(url, mode)) class TestPythonIO(TestCase): @@ -171,31 +169,39 @@ def test_writing_to_buffer_broken_with_close(self): def test_writing_to_custom_io(self): - # Custom I/O that opens file in the sandbox and logs calls - wrapped_custom_io = CustomIOLogger(self.sandboxed) + # Run the test in the sandbox directory to workaround the limitation of the DASH demuxer + # whe dealing with relative files in the manifest. + with run_in_directory(self.sandbox): + # Custom I/O that opens file and logs calls + wrapped_custom_io = CustomIOLogger() - # Write a DASH package using the custom IO - with av.open( - CUSTOM_IO_PROTOCOL + CUSTOM_IO_FILENAME, "w", io_open=wrapped_custom_io - ) as container: - write_rgb_rotate(container) + # Write a DASH package using the custom IO + with av.open( + CUSTOM_IO_PROTOCOL + CUSTOM_IO_FILENAME, "w", io_open=wrapped_custom_io + ) as container: + write_rgb_rotate(container) - # Check that at least 3 files were opened using the custom IO: - # "CUSTOM_IO_FILENAME", init-stream0.m4s and chunk-stream-0x.m4s - self.assertGreaterEqual(len(wrapped_custom_io._log), 3) - self.assertGreaterEqual(len(wrapped_custom_io._method_log), 3) + # Check that at least 3 files were opened using the custom IO: + # "CUSTOM_IO_FILENAME", init-stream0.m4s and chunk-stream-0x.m4s + self.assertGreaterEqual(len(wrapped_custom_io._log), 3) + self.assertGreaterEqual(len(wrapped_custom_io._method_log), 3) - # Check that all files were written to - all_write = all( - method_log._filter("write") for method_log in wrapped_custom_io._method_log - ) - self.assertTrue(all_write) + # Check that all files were written to + all_write = all( + method_log._filter("write") for method_log in wrapped_custom_io._method_log + ) + self.assertTrue(all_write) - # Check that all files were closed - all_closed = all( - method_log._filter("close") for method_log in wrapped_custom_io._method_log - ) - self.assertTrue(all_closed) + # Check that all files were closed + all_closed = all( + method_log._filter("close") for method_log in wrapped_custom_io._method_log + ) + self.assertTrue(all_closed) + + # Check contents. + # Note that the dash demuxer doesn't support custom I/O. + with av.open(CUSTOM_IO_FILENAME, "r") as container: + assert_rgb_rotate(self, container) def test_writing_to_file(self): path = self.sandboxed("writing.mp4") From 23547638c97a44350d03c9f918e0fc14f768cdb7 Mon Sep 17 00:00:00 2001 From: Philip de Nier Date: Tue, 29 Mar 2022 15:42:33 +0000 Subject: [PATCH 085/846] Add add_dash to expose dash only checks in assert_rgb_rotate --- tests/test_encode.py | 21 ++++++++++----------- tests/test_python_io.py | 2 +- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/tests/test_encode.py b/tests/test_encode.py index 13f73285b..5a73508d4 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -63,16 +63,15 @@ def write_rgb_rotate(output): output.mux(packet) -def assert_rgb_rotate(self, input_): +def assert_rgb_rotate(self, input_, is_dash=False): # Now inspect it a little. self.assertEqual(len(input_.streams), 1) - self.assertEqual( - # Fallback to "Title" for the DASH format - input_.metadata.get("title", input_.metadata.get("Title")), - "container", - input_.metadata - ) + if is_dash: + # The "title" metadata is named "Title" in the DASH format + self.assertEqual(input_.metadata.get("Title"), "container", input_.metadata) + else: + self.assertEqual(input_.metadata.get("title"), "container", input_.metadata) self.assertEqual(input_.metadata.get("key"), None) stream = input_.streams[0] self.assertIsInstance(stream, VideoStream) @@ -82,12 +81,12 @@ def assert_rgb_rotate(self, input_): stream.average_rate, 24 ) # Only because we constructed is precisely. self.assertEqual(stream.rate, Fraction(24, 1)) - if stream.duration is not None: - self.assertEqual(stream.time_base * stream.duration, 2) - else: + if is_dash: # The DASH format doesn't provide a duration for the stream - # and the container duration (micro seconds) is checked instead + # and so the container duration (micro seconds) is checked instead self.assertEqual(input_.duration, 2000000) + else: + self.assertEqual(stream.time_base * stream.duration, 2) self.assertEqual(stream.format.name, "yuv420p") self.assertEqual(stream.format.width, WIDTH) self.assertEqual(stream.format.height, HEIGHT) diff --git a/tests/test_python_io.py b/tests/test_python_io.py index 998602748..e422be5c0 100644 --- a/tests/test_python_io.py +++ b/tests/test_python_io.py @@ -201,7 +201,7 @@ def test_writing_to_custom_io(self): # Check contents. # Note that the dash demuxer doesn't support custom I/O. with av.open(CUSTOM_IO_FILENAME, "r") as container: - assert_rgb_rotate(self, container) + assert_rgb_rotate(self, container, is_dash=True) def test_writing_to_file(self): path = self.sandboxed("writing.mp4") From d180657b3ce8509501045c20ae84e04fc2ce04ec Mon Sep 17 00:00:00 2001 From: Philip de Nier Date: Tue, 29 Mar 2022 16:05:30 +0000 Subject: [PATCH 086/846] Use a decorator to run test in the sandbox directory --- tests/common.py | 21 ++++++++-------- tests/test_python_io.py | 56 ++++++++++++++++++++--------------------- 2 files changed, 38 insertions(+), 39 deletions(-) diff --git a/tests/common.py b/tests/common.py index 4235976b6..5d1bf74cc 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1,4 +1,3 @@ -from contextlib import contextmanager from unittest import TestCase as _Base import datetime import errno @@ -73,15 +72,17 @@ def sandboxed(*args, **kwargs): return path -# Context manager for running a test in a directory, e.g. path is the sandbox -@contextmanager -def run_in_directory(path): - current_dir = os.getcwd() - try: - os.chdir(path) - yield - finally: - os.chdir(current_dir) +# Decorator for running a test in the sandbox directory +def run_in_sandbox(func): + @functools.wraps(func) + def _inner(self, *args, **kwargs): + current_dir = os.getcwd() + try: + os.chdir(self.sandbox) + return func(self, *args, **kwargs) + finally: + os.chdir(current_dir) + return _inner class MethodLogger(object): diff --git a/tests/test_python_io.py b/tests/test_python_io.py index e422be5c0..5ae86a9da 100644 --- a/tests/test_python_io.py +++ b/tests/test_python_io.py @@ -2,7 +2,7 @@ import av -from .common import MethodLogger, TestCase, fate_suite, run_in_directory +from .common import MethodLogger, TestCase, fate_suite, run_in_sandbox from .test_encode import assert_rgb_rotate, write_rgb_rotate @@ -167,41 +167,39 @@ def test_writing_to_buffer_broken_with_close(self): with self.assertRaises(OSError): container.close() + @run_in_sandbox def test_writing_to_custom_io(self): - # Run the test in the sandbox directory to workaround the limitation of the DASH demuxer - # whe dealing with relative files in the manifest. - with run_in_directory(self.sandbox): - # Custom I/O that opens file and logs calls - wrapped_custom_io = CustomIOLogger() + # Custom I/O that opens file and logs calls + wrapped_custom_io = CustomIOLogger() - # Write a DASH package using the custom IO - with av.open( - CUSTOM_IO_PROTOCOL + CUSTOM_IO_FILENAME, "w", io_open=wrapped_custom_io - ) as container: - write_rgb_rotate(container) + # Write a DASH package using the custom IO + with av.open( + CUSTOM_IO_PROTOCOL + CUSTOM_IO_FILENAME, "w", io_open=wrapped_custom_io + ) as container: + write_rgb_rotate(container) - # Check that at least 3 files were opened using the custom IO: - # "CUSTOM_IO_FILENAME", init-stream0.m4s and chunk-stream-0x.m4s - self.assertGreaterEqual(len(wrapped_custom_io._log), 3) - self.assertGreaterEqual(len(wrapped_custom_io._method_log), 3) + # Check that at least 3 files were opened using the custom IO: + # "CUSTOM_IO_FILENAME", init-stream0.m4s and chunk-stream-0x.m4s + self.assertGreaterEqual(len(wrapped_custom_io._log), 3) + self.assertGreaterEqual(len(wrapped_custom_io._method_log), 3) - # Check that all files were written to - all_write = all( - method_log._filter("write") for method_log in wrapped_custom_io._method_log - ) - self.assertTrue(all_write) + # Check that all files were written to + all_write = all( + method_log._filter("write") for method_log in wrapped_custom_io._method_log + ) + self.assertTrue(all_write) - # Check that all files were closed - all_closed = all( - method_log._filter("close") for method_log in wrapped_custom_io._method_log - ) - self.assertTrue(all_closed) + # Check that all files were closed + all_closed = all( + method_log._filter("close") for method_log in wrapped_custom_io._method_log + ) + self.assertTrue(all_closed) - # Check contents. - # Note that the dash demuxer doesn't support custom I/O. - with av.open(CUSTOM_IO_FILENAME, "r") as container: - assert_rgb_rotate(self, container, is_dash=True) + # Check contents. + # Note that the dash demuxer doesn't support custom I/O. + with av.open(CUSTOM_IO_FILENAME, "r") as container: + assert_rgb_rotate(self, container, is_dash=True) def test_writing_to_file(self): path = self.sandboxed("writing.mp4") From 7e6bbb51b6294168ca72501afe6a387a2e2138d6 Mon Sep 17 00:00:00 2001 From: Philip de Nier Date: Tue, 29 Mar 2022 16:16:32 +0000 Subject: [PATCH 087/846] Check DASH title metadata for ffmpeg version >= 4.2 --- tests/test_encode.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_encode.py b/tests/test_encode.py index 5a73508d4..018c6ac31 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -68,8 +68,9 @@ def assert_rgb_rotate(self, input_, is_dash=False): # Now inspect it a little. self.assertEqual(len(input_.streams), 1) if is_dash: - # The "title" metadata is named "Title" in the DASH format - self.assertEqual(input_.metadata.get("Title"), "container", input_.metadata) + # FFmpeg 4.2 added parsing of the programme information and it is named "Title" + if av.library_versions["libavformat"] >= (58, 28): + self.assertTrue(input_.metadata.get("Title") == "container", input_.metadata) else: self.assertEqual(input_.metadata.get("title"), "container", input_.metadata) self.assertEqual(input_.metadata.get("key"), None) From e0ff557a9a346ac8a77f8f47d467b0b7ef18aa9f Mon Sep 17 00:00:00 2001 From: Philip de Nier Date: Wed, 30 Mar 2022 10:08:33 +0000 Subject: [PATCH 088/846] Fix custom io open if when options is NULL The image muxer set the options to NULL and that resulted in a segmentation fault when attempting to dereference it. --- av/container/core.pyx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/av/container/core.pyx b/av/container/core.pyx index 387eb54f9..4c855181f 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -72,14 +72,19 @@ cdef int pyav_io_open_gil(lib.AVFormatContext *s, try: container = dereference(s).opaque - file = container.io_open( - url if url is not NULL else "", - flags, - avdict_to_dict( + if options is not NULL: + options_dict = avdict_to_dict( dereference(options), encoding=container.metadata_encoding, errors=container.metadata_errors ) + else: + options_dict = {} + + file = container.io_open( + url if url is not NULL else "", + flags, + options_dict ) pyio_file = PyIOFile( From e3d5dde8caf407896fbecf313bef5b8a6a4a8387 Mon Sep 17 00:00:00 2001 From: Philip de Nier Date: Wed, 30 Mar 2022 10:12:24 +0000 Subject: [PATCH 089/846] Add a custom IO test for a image2 PNG sequence --- tests/test_python_io.py | 72 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 65 insertions(+), 7 deletions(-) diff --git a/tests/test_python_io.py b/tests/test_python_io.py index 5ae86a9da..182d1c02f 100644 --- a/tests/test_python_io.py +++ b/tests/test_python_io.py @@ -2,7 +2,7 @@ import av -from .common import MethodLogger, TestCase, fate_suite, run_in_sandbox +from .common import Image, MethodLogger, TestCase, fate_png, fate_suite, run_in_sandbox from .test_encode import assert_rgb_rotate, write_rgb_rotate @@ -66,8 +66,9 @@ def seekable(self): return False +# Using a custom protocol will avoid the DASH muxer detecting or defaulting to a +# file: protocol and enabling the use of temporary files and renaming. CUSTOM_IO_PROTOCOL = "pyavtest://" -CUSTOM_IO_FILENAME = "custom_io_output.mpd" class CustomIOLogger(object): @@ -168,19 +169,22 @@ def test_writing_to_buffer_broken_with_close(self): container.close() @run_in_sandbox - def test_writing_to_custom_io(self): + def test_writing_to_custom_io_dash(self): # Custom I/O that opens file and logs calls wrapped_custom_io = CustomIOLogger() - # Write a DASH package using the custom IO + output_filename = "custom_io_output.mpd" + + # Write a DASH package using the custom IO. Prefix the name with CUSTOM_IO_PROTOCOL to + # avoid temporary file and renaming. with av.open( - CUSTOM_IO_PROTOCOL + CUSTOM_IO_FILENAME, "w", io_open=wrapped_custom_io + CUSTOM_IO_PROTOCOL + output_filename, "w", io_open=wrapped_custom_io ) as container: write_rgb_rotate(container) # Check that at least 3 files were opened using the custom IO: - # "CUSTOM_IO_FILENAME", init-stream0.m4s and chunk-stream-0x.m4s + # "output_filename", init-stream0.m4s and chunk-stream-0x.m4s self.assertGreaterEqual(len(wrapped_custom_io._log), 3) self.assertGreaterEqual(len(wrapped_custom_io._method_log), 3) @@ -198,9 +202,63 @@ def test_writing_to_custom_io(self): # Check contents. # Note that the dash demuxer doesn't support custom I/O. - with av.open(CUSTOM_IO_FILENAME, "r") as container: + with av.open(output_filename, "r") as container: assert_rgb_rotate(self, container, is_dash=True) + def test_writing_to_custom_io_image2(self): + + # Custom I/O that opens file and logs calls + wrapped_custom_io = CustomIOLogger() + + image = Image.open(fate_png()) + input_frame = av.VideoFrame.from_image(image) + + frame_count = 10 + sequence_filename = self.sandboxed("test%d.png") + width = 160 + height = 90 + + # Write a PNG image sequence using the custom IO + with av.open( + sequence_filename, "w", "image2", io_open=wrapped_custom_io + ) as output: + stream = output.add_stream("png") + stream.width = width + stream.height = height + stream.pix_fmt = "rgb24" + + for frame_i in range(frame_count): + for packet in stream.encode(input_frame): + output.mux(packet) + + # Check that "frame_count" files were opened using the custom IO + self.assertEqual(len(wrapped_custom_io._log), frame_count) + self.assertEqual(len(wrapped_custom_io._method_log), frame_count) + + # Check that all files were written to + all_write = all( + method_log._filter("write") for method_log in wrapped_custom_io._method_log + ) + self.assertTrue(all_write) + + # Check that all files were closed + all_closed = all( + method_log._filter("close") for method_log in wrapped_custom_io._method_log + ) + self.assertTrue(all_closed) + + # Check contents. + with av.open(sequence_filename, "r", "image2") as container: + self.assertEqual(len(container.streams), 1) + stream = container.streams[0] + self.assertIsInstance(stream, av.video.stream.VideoStream) + self.assertEqual(stream.type, "video") + self.assertEqual(stream.name, "png") + self.assertEqual(stream.duration, frame_count) + self.assertEqual(stream.format.name, "rgb24") + self.assertEqual(stream.format.width, width) + self.assertEqual(stream.format.height, height) + def test_writing_to_file(self): path = self.sandboxed("writing.mp4") From fd339eef867f7da7ee066eb7b9624b343b2624b9 Mon Sep 17 00:00:00 2001 From: Philip de Nier Date: Wed, 30 Mar 2022 12:38:11 +0000 Subject: [PATCH 090/846] Add docstring for io_open argument --- av/container/core.pyx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/av/container/core.pyx b/av/container/core.pyx index 4c855181f..d21893c43 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -358,6 +358,13 @@ def open(file, mode=None, format=None, options=None, :param timeout: How many seconds to wait for data before giving up, as a float, or a :ref:`(open timeout, read timeout) ` tuple. :type timeout: float or tuple + :param callable io_open: Custom I/O callable for opening files/streams. + This option is intended for formats that need to open additional + file-like objects to ``file`` using custom I/O. + The callable signature is ``io_open(url: str, flags: int, options: dict)``, where + ``url`` is the url to open, ``flags`` is a combination of AVIO_FLAG_* and + ``options`` is a dictionary of additional options. The callable should return a + file-like object. For devices (via ``libavdevice``), pass the name of the device to ``format``, e.g.:: @@ -365,6 +372,13 @@ def open(file, mode=None, format=None, options=None, >>> # Open webcam on OS X. >>> av.open(format='avfoundation', file='0') # doctest: +SKIP + For DASH and custom I/O using ``io_open``, add a protocol prefix to the ``file`` to + prevent the DASH encoder defaulting to the file protocol and using temporary files. + The custom I/O callable can be used to remove the protocol prefix to reveal the actual + name for creating the file-like object. E.g.:: + + >>> av.open("customprotocol://manifest.mpd", "w", io_open=custom_io) # doctest: +SKIP + .. seealso:: :ref:`garbage_collection` More information on using input and output devices is available on the From 15be68f8b242835adfa77c97dddb408755de8021 Mon Sep 17 00:00:00 2001 From: Philip de Nier Date: Wed, 30 Mar 2022 14:52:34 +0000 Subject: [PATCH 091/846] Skip test if Image is None, ie. PIL not available --- tests/test_python_io.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_python_io.py b/tests/test_python_io.py index 182d1c02f..eb6e97554 100644 --- a/tests/test_python_io.py +++ b/tests/test_python_io.py @@ -1,4 +1,5 @@ from io import BytesIO +from unittest import SkipTest import av @@ -207,6 +208,9 @@ def test_writing_to_custom_io_dash(self): def test_writing_to_custom_io_image2(self): + if not Image: + raise SkipTest() + # Custom I/O that opens file and logs calls wrapped_custom_io = CustomIOLogger() From d399d6f539b620b3a42decd01dfe4bf82886d692 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Mon, 4 Apr 2022 09:42:22 +0200 Subject: [PATCH 092/846] [wheels] fix creating temporary directory for dependencies cibuildwheel seems to have changed the temporary directory on Windows, so C:\cibw is not guaranteed to exist. --- scripts/fetch-vendor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/fetch-vendor.py b/scripts/fetch-vendor.py index fcfa8d3c8..3ea3a0c6d 100644 --- a/scripts/fetch-vendor.py +++ b/scripts/fetch-vendor.py @@ -43,7 +43,7 @@ def get_platform(): logging.info("Creating directory %s" % args.destination_dir) if os.path.exists(args.destination_dir): shutil.rmtree(args.destination_dir) -os.mkdir(args.destination_dir) +os.makedirs(args.destination_dir) for url_template in config["urls"]: tarball_url = url_template.replace("{platform}", get_platform()) From 4a916dbbb1d205ab82b5a28c075bb16d9ff5a769 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Tue, 5 Apr 2022 12:31:12 +0200 Subject: [PATCH 093/846] [package] update FFmpeg binaries - VPX is enabled on all platforms - the Windows build disable mediafoundation support as it drags in a dependency on MFPlat.DLL --- scripts/fetch-vendor.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/fetch-vendor.json b/scripts/fetch-vendor.json index 6e0f6a50b..36ec3473b 100644 --- a/scripts/fetch-vendor.json +++ b/scripts/fetch-vendor.json @@ -1,3 +1,3 @@ { - "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/4.4.1-5/ffmpeg-{platform}.tar.gz"] + "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/4.4.1-6/ffmpeg-{platform}.tar.gz"] } From 792e0fa511693924f25f295bcc25c6e10c95d994 Mon Sep 17 00:00:00 2001 From: Pablo Prietz Date: Fri, 30 Oct 2020 16:03:25 +0100 Subject: [PATCH 094/846] setup.py: Include *.pxd files as package_data --- setup.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/setup.py b/setup.py index 6dc0cc4c3..ec234f4a0 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,6 @@ import argparse import os +import pathlib import platform import re import shlex @@ -178,6 +179,9 @@ def parse_cflags(raw_flags): with open(about_file, encoding="utf-8") as fp: exec(fp.read(), about) +package_folders = pathlib.Path("av").glob("**/") +package_data = {".".join(pckg.parts): ["*.pxd"] for pckg in package_folders} + setup( name="av", @@ -187,6 +191,7 @@ def parse_cflags(raw_flags): author_email="pyav@mikeboers.com", url="https://github.com/PyAV-Org/PyAV", packages=find_packages(exclude=["build*", "examples*", "scratchpad*", "tests*"]), + package_data=package_data, zip_safe=False, ext_modules=ext_modules, test_suite="tests", From 1b709194a40a0285068260f240d7bf3efc720ea7 Mon Sep 17 00:00:00 2001 From: Pablo Prietz Date: Fri, 30 Oct 2020 16:04:42 +0100 Subject: [PATCH 095/846] Add __init__.pxd files --- av/__init__.pxd | 0 av/audio/__init__.pxd | 0 av/codec/__init__.pxd | 0 av/container/__init__.pxd | 0 av/data/__init__.pxd | 0 av/filter/__init__.pxd | 0 av/sidedata/__init__.pxd | 0 av/subtitles/__init__.pxd | 0 av/video/__init__.pxd | 0 9 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 av/__init__.pxd create mode 100644 av/audio/__init__.pxd create mode 100644 av/codec/__init__.pxd create mode 100644 av/container/__init__.pxd create mode 100644 av/data/__init__.pxd create mode 100644 av/filter/__init__.pxd create mode 100644 av/sidedata/__init__.pxd create mode 100644 av/subtitles/__init__.pxd create mode 100644 av/video/__init__.pxd diff --git a/av/__init__.pxd b/av/__init__.pxd new file mode 100644 index 000000000..e69de29bb diff --git a/av/audio/__init__.pxd b/av/audio/__init__.pxd new file mode 100644 index 000000000..e69de29bb diff --git a/av/codec/__init__.pxd b/av/codec/__init__.pxd new file mode 100644 index 000000000..e69de29bb diff --git a/av/container/__init__.pxd b/av/container/__init__.pxd new file mode 100644 index 000000000..e69de29bb diff --git a/av/data/__init__.pxd b/av/data/__init__.pxd new file mode 100644 index 000000000..e69de29bb diff --git a/av/filter/__init__.pxd b/av/filter/__init__.pxd new file mode 100644 index 000000000..e69de29bb diff --git a/av/sidedata/__init__.pxd b/av/sidedata/__init__.pxd new file mode 100644 index 000000000..e69de29bb diff --git a/av/subtitles/__init__.pxd b/av/subtitles/__init__.pxd new file mode 100644 index 000000000..e69de29bb diff --git a/av/video/__init__.pxd b/av/video/__init__.pxd new file mode 100644 index 000000000..e69de29bb From 8196baa5f0cede30a09c6a6e67ef7a8677b862a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Chr=C3=A9tien?= <2742231+bchretien@users.noreply.github.com> Date: Thu, 24 Mar 2022 16:31:56 +0100 Subject: [PATCH 096/846] Support AV_FRAME_DATA_SEI_UNREGISTERED (fixes: #723) This requires FFmpeg >= 4.4. --- av/enum.pyx | 3 ++- av/sidedata/sidedata.pyx | 2 ++ include/libavcodec/avcodec.pxd | 9 +++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/av/enum.pyx b/av/enum.pyx index 0dd46ab01..85f1b0748 100644 --- a/av/enum.pyx +++ b/av/enum.pyx @@ -384,6 +384,7 @@ cpdef define_enum(name, module, items, bint is_flags=False): else: base_cls = EnumItem - cls = EnumType(name, (base_cls, ), {'__module__': module}, items) + # Some items may be None if they correspond to an unsupported FFmpeg feature + cls = EnumType(name, (base_cls, ), {'__module__': module}, [i for i in items if i is not None]) return cls diff --git a/av/sidedata/sidedata.pyx b/av/sidedata/sidedata.pyx index c09f4c6e9..ec7de5997 100644 --- a/av/sidedata/sidedata.pyx +++ b/av/sidedata/sidedata.pyx @@ -25,6 +25,8 @@ Type = define_enum('Type', __name__, ( ('SPHERICAL', lib.AV_FRAME_DATA_SPHERICAL), ('CONTENT_LIGHT_LEVEL', lib.AV_FRAME_DATA_CONTENT_LIGHT_LEVEL), ('ICC_PROFILE', lib.AV_FRAME_DATA_ICC_PROFILE), + # SEI_UNREGISTERED available since version 56.54.100 of libavutil (FFmpeg >= 4.4) + ('SEI_UNREGISTERED', lib.AV_FRAME_DATA_SEI_UNREGISTERED) if lib.AV_FRAME_DATA_SEI_UNREGISTERED != -1 else None, # These are deprecated. See https://github.com/PyAV-Org/PyAV/issues/607 # ('QP_TABLE_PROPERTIES', lib.AV_FRAME_DATA_QP_TABLE_PROPERTIES), diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index c02274318..8c0a9685b 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -7,6 +7,14 @@ from libc.stdint cimport ( cdef extern from "libavcodec/avcodec.h" nogil: + """ + // AV_FRAME_DATA_SEI_UNREGISTERED available since version 56.54.100 of libavutil (FFmpeg >= 4.4) + #define HAS_AV_FRAME_DATA_SEI_UNREGISTERED (LIBAVUTIL_VERSION_INT >= 3683940) + + #if !HAS_AV_FRAME_DATA_SEI_UNREGISTERED + #define AV_FRAME_DATA_SEI_UNREGISTERED -1 + #endif + """ # custom cdef set pyav_get_available_codecs() @@ -283,6 +291,7 @@ cdef extern from "libavcodec/avcodec.h" nogil: AV_FRAME_DATA_ICC_PROFILE AV_FRAME_DATA_QP_TABLE_PROPERTIES AV_FRAME_DATA_QP_TABLE_DATA + AV_FRAME_DATA_SEI_UNREGISTERED cdef struct AVFrameSideData: AVFrameSideDataType type From e5ae31d25bdb4bf99593e8c1ce4e2e323431b8ae Mon Sep 17 00:00:00 2001 From: Justin Wong <46082645+uvjustin@users.noreply.github.com> Date: Tue, 12 Apr 2022 13:56:49 +1000 Subject: [PATCH 097/846] Allocate packet in OutputContainer to use for muxing Fixes a memory leak introduced in 6443f55 when moving from av_init_packet to av_packet_alloc. Updated comment to make clear that av_interleaved_write_frame takes ownership of the reference inside the packet and not the packet itself. --- av/container/output.pxd | 1 + av/container/output.pyx | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/av/container/output.pxd b/av/container/output.pxd index f65dba657..a4299891c 100644 --- a/av/container/output.pxd +++ b/av/container/output.pxd @@ -8,5 +8,6 @@ cdef class OutputContainer(Container): cdef bint _started cdef bint _done + cdef lib.AVPacket *packet_ptr cpdef start_encoding(self) diff --git a/av/container/output.pyx b/av/container/output.pyx index 4be328446..621ac8f18 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -34,9 +34,13 @@ cdef class OutputContainer(Container): def __cinit__(self, *args, **kwargs): self.streams = StreamContainer() self.metadata = {} + with nogil: + self.packet_ptr = lib.av_packet_alloc() def __dealloc__(self): close_output(self) + with nogil: + lib.av_packet_free(&self.packet_ptr) def add_stream(self, codec_name=None, object rate=None, Stream template=None, options=None, **kwargs): """add_stream(codec_name, rate=None) @@ -219,11 +223,10 @@ cdef class OutputContainer(Container): packet._rebase_time(stream.time_base) # Make another reference to the packet, as av_interleaved_write_frame - # takes ownership of it. - cdef lib.AVPacket *packet_ptr = lib.av_packet_alloc() - self.err_check(lib.av_packet_ref(packet_ptr, packet.ptr)) + # takes ownership of the reference. + self.err_check(lib.av_packet_ref(self.packet_ptr, packet.ptr)) cdef int ret with nogil: - ret = lib.av_interleaved_write_frame(self.ptr, packet_ptr) + ret = lib.av_interleaved_write_frame(self.ptr, self.packet_ptr) self.err_check(ret) From b1701f5758ef6d057940e0c53d55133e5955277e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Mon, 18 Apr 2022 16:47:25 +0200 Subject: [PATCH 098/846] [package] update FFmpeg binaries Fix vpx flags for macos, see https://github.com/PyAV-Org/pyav-ffmpeg/pull/63 --- scripts/fetch-vendor.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/fetch-vendor.json b/scripts/fetch-vendor.json index 36ec3473b..709aaabb6 100644 --- a/scripts/fetch-vendor.json +++ b/scripts/fetch-vendor.json @@ -1,3 +1,3 @@ { - "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/4.4.1-6/ffmpeg-{platform}.tar.gz"] + "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/4.4.1-7/ffmpeg-{platform}.tar.gz"] } From af1493cbad7faef70f39bc7cbaa843141d9fefd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Tue, 19 Apr 2022 21:03:04 +0200 Subject: [PATCH 099/846] [package] work around Conda bug which breaks DLL loading (fixes: #952) --- av/__init__.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/av/__init__.py b/av/__init__.py index 237cb8b94..8c87e17cc 100644 --- a/av/__init__.py +++ b/av/__init__.py @@ -1,10 +1,18 @@ -# Add the native FFMPEG and MinGW libraries to executable path, so that the -# AV pyd files can find them. import os +import sys -if os.name == "nt": +# Some Python versions distributed by Conda have a buggy `os.add_dll_directory` +# which prevents binary wheels from finding the FFmpeg DLLs in the `av.libs` +# directory. We work around this by adding `av.libs` to the PATH. +if ( + os.name == "nt" + and sys.version_info[:2] in ((3, 8), (3, 9)) + and os.path.exists(os.path.join(sys.base_prefix, "conda-meta")) +): os.environ["PATH"] = ( - os.path.abspath(os.path.dirname(__file__)) + os.pathsep + os.environ["PATH"] + os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, "av.libs")) + + os.pathsep + + os.environ["PATH"] ) # MUST import the core before anything else in order to initalize the underlying From 4cf095390d5537f49abe83dcf97f4f8525ecbf1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Wed, 20 Apr 2022 08:30:28 +0200 Subject: [PATCH 100/846] Release v9.2.0. --- CHANGELOG.rst | 15 +++++++++++++++ av/about.py | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d64fe2b7f..114551fcc 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,21 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. +v9.2.0 +------ + +Features: + +- Update binary wheels to enable libvpx support. +- Add an `io_open` argument to `av.open` for multi-file custom I/O. +- Add support for AV_FRAME_DATA_SEI_UNREGISTERED (:issue:`723`). +- Ship .pxd files to allow other libraries to `cimport av` (:issue:`716`). + +Fixes: + +- Fix an `ImportError` when using Python 3.8/3.9 via Conda (:issue:`952`). +- Fix a muxing memory leak which was introduced in v9.1.0 (:issue:`959`). + v9.1.1 ------ diff --git a/av/about.py b/av/about.py index ee502a28b..62ad3ee97 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "9.1.1" +__version__ = "9.2.0" From 88f286cb874324bb298bf85567f701cf5e3fb424 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Mon, 28 Mar 2022 19:33:15 +0200 Subject: [PATCH 101/846] [codec context] deprecate CodecContext.time_base for decoders The FFmpeg API docs state that using AVCodecContext.time_base for decoders is deprecated and AVCodecContext.framerate should be used instead. --- av/codec/context.pyx | 17 +++++++++++++++-- docs/api/time.rst | 3 --- tests/test_codec_context.py | 19 ++++++++++++++++++- tests/test_decode.py | 5 +++-- 4 files changed, 36 insertions(+), 8 deletions(-) diff --git a/av/codec/context.pyx b/av/codec/context.pyx index fd3b26fe7..c9f5177c1 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -1,3 +1,5 @@ +import warnings + from libc.errno cimport EAGAIN from libc.stdint cimport int64_t, uint8_t from libc.string cimport memcpy @@ -11,6 +13,7 @@ from av.error cimport err_check from av.packet cimport Packet from av.utils cimport avrational_to_fraction, to_avrational +from av.deprecation import AVDeprecationWarning from av.dictionary import Dictionary @@ -282,8 +285,8 @@ cdef class CodecContext(object): cdef _Dictionary options = Dictionary() options.update(self.options or {}) - # Assert we have a time_base. - if not self.ptr.time_base.num: + # Assert we have a time_base for encoders. + if not self.ptr.time_base.num and self.is_encoder: self._set_default_time_base() err_check(lib.avcodec_open2(self.ptr, self.codec.ptr, &options.ptr)) @@ -551,9 +554,19 @@ cdef class CodecContext(object): property time_base: def __get__(self): + if self.is_decoder: + warnings.warn( + "Using CodecContext.time_base for decoders is deprecated.", + AVDeprecationWarning + ) return avrational_to_fraction(&self.ptr.time_base) def __set__(self, value): + if self.is_decoder: + warnings.warn( + "Using CodecContext.time_base for decoders is deprecated.", + AVDeprecationWarning + ) to_avrational(value, &self.ptr.time_base) property codec_tag: diff --git a/docs/api/time.rst b/docs/api/time.rst index c0234e0f6..35e4cfc85 100644 --- a/docs/api/time.rst +++ b/docs/api/time.rst @@ -29,9 +29,6 @@ Time is expressed as integer multiples of arbitrary units of time called a ``tim >>> video.time_base Fraction(1, 25) - >>> video.codec_context.time_base - Fraction(1, 50) - Attributes that represent time on those objects will be in that object's ``time_base``: .. doctest:: diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index ca9433678..a62c05c4e 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -1,6 +1,7 @@ from fractions import Fraction from unittest import SkipTest import os +import warnings from av import AudioResampler, Codec, Packet from av.codec.codec import UnknownCodecError @@ -79,6 +80,23 @@ def test_decoder_extradata(self): self.assertEqual(ctx.extradata, None) self.assertEqual(ctx.extradata_size, 0) + def test_decoder_timebase(self): + ctx = av.codec.Codec("h264", "r").create() + + with warnings.catch_warnings(record=True) as captured: + self.assertIsNone(ctx.time_base) + self.assertEqual( + captured[0].message.args[0], + "Using CodecContext.time_base for decoders is deprecated.", + ) + + with warnings.catch_warnings(record=True) as captured: + ctx.time_base = Fraction(1, 25) + self.assertEqual( + captured[0].message.args[0], + "Using CodecContext.time_base for decoders is deprecated.", + ) + def test_encoder_extradata(self): ctx = av.codec.Codec("h264", "w").create() self.assertEqual(ctx.extradata, None) @@ -357,7 +375,6 @@ def audio_encoding(self, codec_name): f.write(packet) ctx = Codec(codec_name, "r").create() - ctx.time_base = Fraction(1) / sample_rate ctx.sample_rate = sample_rate ctx.format = sample_fmt ctx.layout = channel_layout diff --git a/tests/test_decode.py b/tests/test_decode.py index b4e13c183..709010196 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -1,3 +1,5 @@ +from fractions import Fraction + import av from .common import TestCase, fate_suite @@ -59,9 +61,8 @@ def test_decoded_time_base(self): container = av.open(fate_suite("h264/interlaced_crop.mp4")) stream = container.streams.video[0] - codec_context = stream.codec_context - self.assertNotEqual(stream.time_base, codec_context.time_base) + self.assertEqual(stream.time_base, Fraction(1, 25)) for packet in container.demux(stream): for frame in packet.decode(): From a3af704c36e78cc341bf19e8408d1cc0913ae476 Mon Sep 17 00:00:00 2001 From: Jonathan Drolet Date: Sun, 8 May 2022 20:56:46 -0400 Subject: [PATCH 102/846] Fix useful_array when unaligned and dtype > 1 byte --- av/video/frame.pyx | 4 ++-- tests/test_videoframe.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 6a3add3e1..c2a6134be 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -66,10 +66,10 @@ cdef useful_array(VideoPlane plane, unsigned int bytes_per_pixel=1, str dtype='u import numpy as np cdef size_t total_line_size = abs(plane.line_size) cdef size_t useful_line_size = plane.width * bytes_per_pixel - arr = np.frombuffer(plane, np.dtype(dtype)) + arr = np.frombuffer(plane, np.uint8) if total_line_size != useful_line_size: arr = arr.reshape(-1, total_line_size)[:, 0:useful_line_size].reshape(-1) - return arr + return arr.view(np.dtype(dtype)) cdef class VideoFrame(Frame): diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 09ab06b13..6b9a99a4b 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -291,6 +291,17 @@ def test_ndarray_rgb48le(self): # check endianness by examining red value of first pixel self.assertPixelValue16(frame.planes[0], array[0][0][0], "little") + def test_ndarray_rgb48le_align(self): + array = numpy.random.randint(0, 65536, size=(238, 318, 3), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="rgb48le") + self.assertEqual(frame.width, 318) + self.assertEqual(frame.height, 238) + self.assertEqual(frame.format.name, "rgb48le") + self.assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining red value of first pixel + self.assertPixelValue16(frame.planes[0], array[0][0][0], "little") + def test_ndarray_rgba64be(self): array = numpy.random.randint(0, 65536, size=(480, 640, 4), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="rgba64be") From 01280b2324e7946905e033677adfb95cd7dd40eb Mon Sep 17 00:00:00 2001 From: Jonathan Drolet Date: Thu, 19 May 2022 09:21:05 -0400 Subject: [PATCH 103/846] Add support in VideoFrame ndarray for gbrp formats --- av/video/frame.pyx | 30 ++++++++++++++ tests/test_videoframe.py | 88 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) diff --git a/av/video/frame.pyx b/av/video/frame.pyx index c2a6134be..42ec9dea2 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -273,6 +273,18 @@ cdef class VideoFrame(Frame): assert frame.width % 2 == 0 assert frame.height % 2 == 0 return useful_array(frame.planes[0], 2).reshape(frame.height, frame.width, -1) + elif frame.format.name == 'gbrp': + array = np.empty((frame.height, frame.width, 3), dtype="uint8") + array[:, :, 0] = useful_array(frame.planes[2], 1).reshape(-1, frame.width) + array[:, :, 1] = useful_array(frame.planes[0], 1).reshape(-1, frame.width) + array[:, :, 2] = useful_array(frame.planes[1], 1).reshape(-1, frame.width) + return array + elif frame.format.name in ('gbrp10be', 'gbrp12be', 'gbrp14be', 'gbrp16be', 'gbrp10le', 'gbrp12le', 'gbrp14le', 'gbrp16le'): + array = np.empty((frame.height, frame.width, 3), dtype="uint16") + array[:, :, 0] = useful_array(frame.planes[2], 2, "uint16").reshape(-1, frame.width) + array[:, :, 1] = useful_array(frame.planes[0], 2, "uint16").reshape(-1, frame.width) + array[:, :, 2] = useful_array(frame.planes[1], 2, "uint16").reshape(-1, frame.width) + return byteswap_array(array, frame.format.name.endswith('be')) elif frame.format.name in ('rgb24', 'bgr24'): return useful_array(frame.planes[0], 3).reshape(frame.height, frame.width, -1) elif frame.format.name in ('argb', 'rgba', 'abgr', 'bgra'): @@ -354,6 +366,24 @@ cdef class VideoFrame(Frame): check_ndarray_shape(array, array.shape[0] % 2 == 0) check_ndarray_shape(array, array.shape[1] % 2 == 0) check_ndarray_shape(array, array.shape[2] == 2) + elif format == 'gbrp': + check_ndarray(array, 'uint8', 3) + check_ndarray_shape(array, array.shape[2] == 3) + + frame = VideoFrame(array.shape[1], array.shape[0], format) + copy_array_to_plane(array[:, :, 1], frame.planes[0], 1) + copy_array_to_plane(array[:, :, 2], frame.planes[1], 1) + copy_array_to_plane(array[:, :, 0], frame.planes[2], 1) + return frame + elif format in ('gbrp10be', 'gbrp12be', 'gbrp14be', 'gbrp16be', 'gbrp10le', 'gbrp12le', 'gbrp14le', 'gbrp16le'): + check_ndarray(array, 'uint16', 3) + check_ndarray_shape(array, array.shape[2] == 3) + + frame = VideoFrame(array.shape[1], array.shape[0], format) + copy_array_to_plane(byteswap_array(array[:, :, 1], format.endswith('be')), frame.planes[0], 2) + copy_array_to_plane(byteswap_array(array[:, :, 2], format.endswith('be')), frame.planes[1], 2) + copy_array_to_plane(byteswap_array(array[:, :, 0], format.endswith('be')), frame.planes[2], 2) + return frame elif format in ('rgb24', 'bgr24'): check_ndarray(array, 'uint8', 3) check_ndarray_shape(array, array.shape[2] == 3) diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 6b9a99a4b..f037326db 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -207,6 +207,94 @@ def test_ndarray_rgba_align(self): self.assertEqual(frame.format.name, format) self.assertNdarraysEqual(frame.to_ndarray(), array) + def test_ndarray_gbrp(self): + array = numpy.random.randint(0, 256, size=(480, 640, 3), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="gbrp") + self.assertEqual(frame.width, 640) + self.assertEqual(frame.height, 480) + self.assertEqual(frame.format.name, "gbrp") + self.assertNdarraysEqual(frame.to_ndarray(), array) + + def test_ndarray_gbrp_align(self): + array = numpy.random.randint(0, 256, size=(238, 318, 3), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="gbrp") + self.assertEqual(frame.width, 318) + self.assertEqual(frame.height, 238) + self.assertEqual(frame.format.name, "gbrp") + self.assertNdarraysEqual(frame.to_ndarray(), array) + + def test_ndarray_gbrp10(self): + array = numpy.random.randint(0, 1024, size=(480, 640, 3), dtype=numpy.uint16) + for format in ["gbrp10be", "gbrp10le"]: + frame = VideoFrame.from_ndarray(array, format=format) + self.assertEqual(frame.width, 640) + self.assertEqual(frame.height, 480) + self.assertEqual(frame.format.name, format) + self.assertNdarraysEqual(frame.to_ndarray(), array) + + def test_ndarray_gbrp10_align(self): + array = numpy.random.randint(0, 1024, size=(238, 318, 3), dtype=numpy.uint16) + for format in ["gbrp10be", "gbrp10le"]: + frame = VideoFrame.from_ndarray(array, format=format) + self.assertEqual(frame.width, 318) + self.assertEqual(frame.height, 238) + self.assertEqual(frame.format.name, format) + self.assertNdarraysEqual(frame.to_ndarray(), array) + + def test_ndarray_gbrp12(self): + array = numpy.random.randint(0, 4096, size=(480, 640, 3), dtype=numpy.uint16) + for format in ["gbrp12be", "gbrp12le"]: + frame = VideoFrame.from_ndarray(array, format=format) + self.assertEqual(frame.width, 640) + self.assertEqual(frame.height, 480) + self.assertEqual(frame.format.name, format) + self.assertNdarraysEqual(frame.to_ndarray(), array) + + def test_ndarray_gbrp12_align(self): + array = numpy.random.randint(0, 4096, size=(238, 318, 3), dtype=numpy.uint16) + for format in ["gbrp12be", "gbrp12le"]: + frame = VideoFrame.from_ndarray(array, format=format) + self.assertEqual(frame.width, 318) + self.assertEqual(frame.height, 238) + self.assertEqual(frame.format.name, format) + self.assertNdarraysEqual(frame.to_ndarray(), array) + + def test_ndarray_gbrp14(self): + array = numpy.random.randint(0, 16384, size=(480, 640, 3), dtype=numpy.uint16) + for format in ["gbrp14be", "gbrp14le"]: + frame = VideoFrame.from_ndarray(array, format=format) + self.assertEqual(frame.width, 640) + self.assertEqual(frame.height, 480) + self.assertEqual(frame.format.name, format) + self.assertNdarraysEqual(frame.to_ndarray(), array) + + def test_ndarray_gbrp14_align(self): + array = numpy.random.randint(0, 16384, size=(238, 318, 3), dtype=numpy.uint16) + for format in ["gbrp14be", "gbrp14le"]: + frame = VideoFrame.from_ndarray(array, format=format) + self.assertEqual(frame.width, 318) + self.assertEqual(frame.height, 238) + self.assertEqual(frame.format.name, format) + self.assertNdarraysEqual(frame.to_ndarray(), array) + + def test_ndarray_gbrp16(self): + array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) + for format in ["gbrp16be", "gbrp16le"]: + frame = VideoFrame.from_ndarray(array, format=format) + self.assertEqual(frame.width, 640) + self.assertEqual(frame.height, 480) + self.assertEqual(frame.format.name, format) + self.assertNdarraysEqual(frame.to_ndarray(), array) + + def test_ndarray_gbrp16_align(self): + array = numpy.random.randint(0, 65536, size=(238, 318, 3), dtype=numpy.uint16) + for format in ["gbrp16be", "gbrp16le"]: + frame = VideoFrame.from_ndarray(array, format=format) + self.assertEqual(frame.width, 318) + self.assertEqual(frame.height, 238) + self.assertEqual(frame.format.name, format) + self.assertNdarraysEqual(frame.to_ndarray(), array) + def test_ndarray_yuv420p(self): array = numpy.random.randint(0, 256, size=(720, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuv420p") From 138eae8bc2ea4891eff36e9053f8f3228554ff45 Mon Sep 17 00:00:00 2001 From: Justin Wong <46082645+uvjustin@users.noreply.github.com> Date: Sun, 19 Jun 2022 17:36:15 +1000 Subject: [PATCH 104/846] Add FFmpeg 4.4 to Windows tests --- .github/workflows/tests.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b5e62d012..b218933bb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -148,6 +148,7 @@ jobs: fail-fast: false matrix: config: + - {os: windows-latest, python: 3.7, ffmpeg: "4.4"} - {os: windows-latest, python: 3.7, ffmpeg: "4.3"} - {os: windows-latest, python: 3.7, ffmpeg: "4.2"} - {os: windows-latest, python: 3.7, ffmpeg: "4.1"} @@ -166,17 +167,24 @@ jobs: conda config --add channels conda-forge conda create -q -n pyav \ cython \ - ffmpeg=${{ matrix.config.ffmpeg }} \ numpy \ pillow \ python=${{ matrix.config.python }} \ setuptools + if [[ "${{ matrix.config.ffmpeg }}" == "4.4" ]]; then + curl -L -o ffmpeg.tar.gz https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/4.4.1-7/ffmpeg-win_amd64.tar.gz + else + conda install -q -n pyav ffmpeg=${{ matrix.config.ffmpeg }} + fi - name: Build shell: bash run: | . $CONDA/etc/profile.d/conda.sh conda activate pyav + if [[ "${{ matrix.config.ffmpeg }}" == "4.4" ]]; then + tar -xf ffmpeg.tar.gz -C $CONDA_PREFIX/Library/ + fi python setup.py build_ext --inplace --ffmpeg-dir=$CONDA_PREFIX/Library - name: Test From 972f3ca096ef30c063744bdcd3c3380325408ec3 Mon Sep 17 00:00:00 2001 From: Justin Wong <46082645+uvjustin@users.noreply.github.com> Date: Tue, 21 Jun 2022 20:46:14 +0800 Subject: [PATCH 105/846] Use pad count in alloc_filter_pads --- av/filter/pad.pyx | 10 ++++++++-- include/libavfilter/avfilter.pxd | 12 +++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/av/filter/pad.pyx b/av/filter/pad.pyx index 9b2c6b853..64ec9a6b1 100644 --- a/av/filter/pad.pyx +++ b/av/filter/pad.pyx @@ -80,10 +80,16 @@ cdef tuple alloc_filter_pads(Filter filter, const lib.AVFilterPad *ptr, bint is_ # We need to be careful and check our bounds if we know what they are, # since the arrays on a AVFilterContext are not NULL terminated. cdef int i = 0 - cdef int count = (context.ptr.nb_inputs if is_input else context.ptr.nb_outputs) if context is not None else -1 + cdef int count + if context is None: + # This is a custom function defined using a macro in avfilter.pxd. Its usage + # can be changed after we stop supporting FFmpeg < 5.0. + count = lib.pyav_get_num_pads(filter.ptr, not is_input, ptr) + else: + count = (context.ptr.nb_inputs if is_input else context.ptr.nb_outputs) cdef FilterPad pad - while (i < count or count < 0) and lib.avfilter_pad_get_name(ptr, i): + while (i < count): pad = FilterPad(_cinit_sentinel) if context is None else FilterContextPad(_cinit_sentinel) pads.append(pad) pad.filter = filter diff --git a/include/libavfilter/avfilter.pxd b/include/libavfilter/avfilter.pxd index 41e0e1d1f..e1fd42f45 100644 --- a/include/libavfilter/avfilter.pxd +++ b/include/libavfilter/avfilter.pxd @@ -1,6 +1,14 @@ cdef extern from "libavfilter/avfilter.h" nogil: - + """ + #if (LIBAVFILTER_VERSION_INT >= 525156) + // avfilter_filter_pad_count is available since version 8.3.100 of libavfilter (FFmpeg 5.0) + #define _avfilter_get_num_pads(filter, is_output, pads) (avfilter_filter_pad_count(filter, is_output)) + #else + // avfilter_filter_pad_count has been deprecated as of version 8.3.100 of libavfilter (FFmpeg 5.0) + #define _avfilter_get_num_pads(filter, is_output, pads) (avfilter_pad_count(pads)) + #endif + """ cdef int avfilter_version() cdef char* avfilter_configuration() cdef char* avfilter_license() @@ -12,6 +20,8 @@ cdef extern from "libavfilter/avfilter.h" nogil: const char* avfilter_pad_get_name(const AVFilterPad *pads, int index) AVMediaType avfilter_pad_get_type(const AVFilterPad *pads, int index) + int pyav_get_num_pads "_avfilter_get_num_pads" (const AVFilter *filter, int is_output, const AVFilterPad *pads) + cdef struct AVFilter: AVClass *priv_class From db3358d834001d76addb3ad806f12da1f1f7f307 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Wed, 16 Mar 2022 00:17:08 +0100 Subject: [PATCH 106/846] =?UTF-8?q?[tests]=C2=A0run=20tests=20against=20FF?= =?UTF-8?q?mpeg=205.0,=20drop=204.0=20-=204.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/tests.yml | 11 +++-------- docs/overview/installation.rst | 2 +- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b218933bb..f0bfaa5c7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -19,7 +19,7 @@ jobs: env: PYAV_PYTHON: python3 - PYAV_LIBRARY: ffmpeg-4.0 # doesn't matter + PYAV_LIBRARY: ffmpeg-4.3 # doesn't matter steps: @@ -56,11 +56,9 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-latest, python: 3.7, ffmpeg: "4.4", extras: true} + - {os: ubuntu-latest, python: 3.7, ffmpeg: "5.0", extras: true} + - {os: ubuntu-latest, python: 3.7, ffmpeg: "4.4"} - {os: ubuntu-latest, python: 3.7, ffmpeg: "4.3"} - - {os: ubuntu-latest, python: 3.7, ffmpeg: "4.2"} - - {os: ubuntu-latest, python: 3.7, ffmpeg: "4.1"} - - {os: ubuntu-latest, python: 3.7, ffmpeg: "4.0"} - {os: ubuntu-latest, python: pypy3, ffmpeg: "4.4"} - {os: macos-latest, python: 3.7, ffmpeg: "4.4"} @@ -150,9 +148,6 @@ jobs: config: - {os: windows-latest, python: 3.7, ffmpeg: "4.4"} - {os: windows-latest, python: 3.7, ffmpeg: "4.3"} - - {os: windows-latest, python: 3.7, ffmpeg: "4.2"} - - {os: windows-latest, python: 3.7, ffmpeg: "4.1"} - - {os: windows-latest, python: 3.7, ffmpeg: "4.0"} steps: diff --git a/docs/overview/installation.rst b/docs/overview/installation.rst index b019e1e30..b7f9c1840 100644 --- a/docs/overview/installation.rst +++ b/docs/overview/installation.rst @@ -52,7 +52,7 @@ See the `Conda quick install `_ docs t Bring your own FFmpeg --------------------- -PyAV can also be compiled against your own build of FFmpeg ((version ``4.0`` or higher). You can force installing PyAV from source by running: +PyAV can also be compiled against your own build of FFmpeg ((version ``4.3`` or higher). You can force installing PyAV from source by running: .. code-block:: bash From 18704658487ea25e5202ac18438d836dfe65b9d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Fri, 11 Mar 2022 16:30:43 +0100 Subject: [PATCH 107/846] [streams] stop using deprecated Stream.codec, it's gone in FFmpeg 5 We now allocate and populate an AVCodecContext ourselves. avcodec_copy_context is also gone, so stop using it. We relax the Stream.average_rate tests for older FFmpeg, as the videos output by these older FFmpeg's seem to give a slightly wrong FPS since the switch to our own AVCodecContext. --- av/codec/context.pxd | 5 +- av/codec/context.pyx | 7 ++- av/container/input.pyx | 38 +++++++++----- av/container/output.pyx | 28 ++++------- av/container/streams.pyx | 10 ++-- av/data/stream.pyx | 2 +- av/packet.pyx | 2 +- av/stream.pxd | 10 ++-- av/stream.pyx | 85 +++++++++++++------------------- av/video/stream.pyx | 4 +- include/libavcodec/avcodec.pxd | 14 ++++-- include/libavformat/avformat.pxd | 1 - tests/common.py | 1 + tests/test_codec_context.py | 4 +- tests/test_encode.py | 50 +++++++++++++++---- 15 files changed, 143 insertions(+), 118 deletions(-) diff --git a/av/codec/context.pxd b/av/codec/context.pxd index d9b6906f9..387cb7de4 100644 --- a/av/codec/context.pxd +++ b/av/codec/context.pxd @@ -11,9 +11,6 @@ cdef class CodecContext(object): cdef lib.AVCodecContext *ptr - # Whether the AVCodecContext should be de-allocated upon destruction. - cdef bint allocated - # Whether AVCodecContext.extradata should be de-allocated upon destruction. cdef bint extradata_set @@ -64,4 +61,4 @@ cdef class CodecContext(object): cdef Frame _alloc_next_frame(self) -cdef CodecContext wrap_codec_context(lib.AVCodecContext*, const lib.AVCodec*, bint allocated) +cdef CodecContext wrap_codec_context(lib.AVCodecContext*, const lib.AVCodec*) diff --git a/av/codec/context.pyx b/av/codec/context.pyx index c9f5177c1..5c8314615 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -20,7 +20,7 @@ from av.dictionary import Dictionary cdef object _cinit_sentinel = object() -cdef CodecContext wrap_codec_context(lib.AVCodecContext *c_ctx, const lib.AVCodec *c_codec, bint allocated): +cdef CodecContext wrap_codec_context(lib.AVCodecContext *c_ctx, const lib.AVCodec *c_codec): """Build an av.CodecContext for an existing AVCodecContext.""" cdef CodecContext py_ctx @@ -38,7 +38,6 @@ cdef CodecContext wrap_codec_context(lib.AVCodecContext *c_ctx, const lib.AVCode else: py_ctx = CodecContext(_cinit_sentinel) - py_ctx.allocated = allocated py_ctx._init(c_ctx, c_codec) return py_ctx @@ -147,7 +146,7 @@ cdef class CodecContext(object): def create(codec, mode=None): cdef Codec cy_codec = codec if isinstance(codec, Codec) else Codec(codec, mode) cdef lib.AVCodecContext *c_ctx = lib.avcodec_alloc_context3(cy_codec.ptr) - return wrap_codec_context(c_ctx, cy_codec.ptr, True) + return wrap_codec_context(c_ctx, cy_codec.ptr) def __cinit__(self, sentinel=None, *args, **kwargs): if sentinel is not _cinit_sentinel: @@ -307,7 +306,7 @@ cdef class CodecContext(object): def __dealloc__(self): if self.ptr and self.extradata_set: lib.av_freep(&self.ptr.extradata) - if self.ptr and self.allocated: + if self.ptr: lib.avcodec_close(self.ptr) lib.avcodec_free_context(&self.ptr) if self.parser: diff --git a/av/container/input.pyx b/av/container/input.pyx index e0c7dcc22..e508f16f4 100644 --- a/av/container/input.pyx +++ b/av/container/input.pyx @@ -1,6 +1,7 @@ from libc.stdint cimport int64_t from libc.stdlib cimport free, malloc +from av.codec.context cimport CodecContext, wrap_codec_context from av.container.streams cimport StreamContainer from av.dictionary cimport _Dictionary from av.error cimport err_check @@ -22,7 +23,11 @@ cdef class InputContainer(Container): def __cinit__(self, *args, **kwargs): + cdef CodecContext py_codec_context cdef unsigned int i + cdef lib.AVStream *stream + cdef lib.AVCodec *codec + cdef lib.AVCodecContext *codec_context # If we have either the global `options`, or a `stream_options`, prepare # a mashup of those options for each stream. @@ -65,7 +70,18 @@ cdef class InputContainer(Container): self.streams = StreamContainer() for i in range(self.ptr.nb_streams): - self.streams.add_stream(wrap_stream(self, self.ptr.streams[i])) + stream = self.ptr.streams[i] + codec = lib.avcodec_find_decoder(stream.codecpar.codec_id) + if codec: + # allocate and initialise decoder + codec_context = lib.avcodec_alloc_context3(codec) + err_check(lib.avcodec_parameters_to_context(codec_context, stream.codecpar)) + codec_context.pkt_timebase = stream.time_base + py_codec_context = wrap_codec_context(codec_context, codec) + else: + # no decoder is available + py_codec_context = None + self.streams.add_stream(wrap_stream(self, stream, py_codec_context)) self.metadata = avdict_to_dict(self.ptr.metadata, self.metadata_encoding, self.metadata_errors) @@ -155,7 +171,7 @@ cdef class InputContainer(Container): if packet.ptr.stream_index < len(self.streams): packet._stream = self.streams[packet.ptr.stream_index] # Keep track of this so that remuxing is easier. - packet._time_base = packet._stream._stream.time_base + packet._time_base = packet._stream.ptr.time_base yield packet # Flush! @@ -163,7 +179,7 @@ cdef class InputContainer(Container): if include_stream[i]: packet = Packet() packet._stream = self.streams[i] - packet._time_base = packet._stream._stream.time_base + packet._time_base = packet._stream.ptr.time_base yield packet finally: @@ -254,11 +270,11 @@ cdef class InputContainer(Container): self.flush_buffers() cdef flush_buffers(self): - cdef unsigned int i - cdef lib.AVStream *stream - - with nogil: - for i in range(self.ptr.nb_streams): - stream = self.ptr.streams[i] - if stream.codec and stream.codec.codec and stream.codec.codec_id != lib.AV_CODEC_ID_NONE: - lib.avcodec_flush_buffers(stream.codec) + cdef Stream stream + cdef CodecContext codec_context + + for stream in self.streams: + codec_context = stream.codec_context + if codec_context and codec_context.is_open: + with nogil: + lib.avcodec_flush_buffers(codec_context.ptr) diff --git a/av/container/output.pyx b/av/container/output.pyx index 621ac8f18..a454e121e 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -3,12 +3,13 @@ import logging import os from av.codec.codec cimport Codec +from av.codec.context cimport CodecContext, wrap_codec_context from av.container.streams cimport StreamContainer from av.dictionary cimport _Dictionary from av.error cimport err_check from av.packet cimport Packet from av.stream cimport Stream, wrap_stream -from av.utils cimport dict_to_avdict +from av.utils cimport dict_to_avdict, to_avrational from av.dictionary import Dictionary @@ -64,14 +65,11 @@ cdef class OutputContainer(Container): if codec_name is not None: codec_obj = codec_name if isinstance(codec_name, Codec) else Codec(codec_name, 'w') - codec = codec_obj.ptr - else: - if not template._codec: - raise ValueError("template has no codec") - if not template._codec_context: + if not template.codec_context: raise ValueError("template has no codec context") - codec = template._codec + codec_obj = template.codec_context.codec + codec = codec_obj.ptr # Assert that this format supports the requested codec. if not lib.avformat_query_codec( @@ -82,16 +80,13 @@ cdef class OutputContainer(Container): raise ValueError("%r format does not support %r codec" % (self.format.name, codec_name)) # Create new stream in the AVFormatContext, set AVCodecContext values. - # As of last check, avformat_new_stream only calls avcodec_alloc_context3 to create - # the context, but doesn't modify it in any other way. Ergo, we can allow CodecContext - # to finish initializing it. lib.avformat_new_stream(self.ptr, codec) cdef lib.AVStream *stream = self.ptr.streams[self.ptr.nb_streams - 1] - cdef lib.AVCodecContext *codec_context = stream.codec # For readability. + cdef lib.AVCodecContext *codec_context = lib.avcodec_alloc_context3(codec) # Copy from the template. if template is not None: - lib.avcodec_copy_context(codec_context, template._codec_context) + err_check(lib.avcodec_parameters_to_context(codec_context, template.ptr.codecpar)) # Reset the codec tag assuming we are remuxing. codec_context.codec_tag = 0 @@ -103,11 +98,7 @@ cdef class OutputContainer(Container): codec_context.bit_rate = 1024000 codec_context.bit_rate_tolerance = 128000 codec_context.ticks_per_frame = 1 - - rate = Fraction(rate or 24) - - codec_context.framerate.num = rate.numerator - codec_context.framerate.den = rate.denominator + to_avrational(rate or 24, &codec_context.framerate) stream.avg_frame_rate = codec_context.framerate stream.time_base = codec_context.time_base @@ -126,7 +117,8 @@ cdef class OutputContainer(Container): codec_context.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER # Construct the user-land stream - cdef Stream py_stream = wrap_stream(self, stream) + cdef CodecContext py_codec_context = wrap_codec_context(codec_context, codec) + cdef Stream py_stream = wrap_stream(self, stream, py_codec_context) self.streams.add_stream(py_stream) if options: diff --git a/av/container/streams.pyx b/av/container/streams.pyx index 4ed2223d4..eb85d9ff3 100644 --- a/av/container/streams.pyx +++ b/av/container/streams.pyx @@ -37,16 +37,16 @@ cdef class StreamContainer(object): cdef add_stream(self, Stream stream): - assert stream._stream.index == len(self._streams) + assert stream.ptr.index == len(self._streams) self._streams.append(stream) - if stream._codec_context.codec_type == lib.AVMEDIA_TYPE_VIDEO: + if stream.ptr.codecpar.codec_type == lib.AVMEDIA_TYPE_VIDEO: self.video = self.video + (stream, ) - elif stream._codec_context.codec_type == lib.AVMEDIA_TYPE_AUDIO: + elif stream.ptr.codecpar.codec_type == lib.AVMEDIA_TYPE_AUDIO: self.audio = self.audio + (stream, ) - elif stream._codec_context.codec_type == lib.AVMEDIA_TYPE_SUBTITLE: + elif stream.ptr.codecpar.codec_type == lib.AVMEDIA_TYPE_SUBTITLE: self.subtitles = self.subtitles + (stream, ) - elif stream._codec_context.codec_type == lib.AVMEDIA_TYPE_DATA: + elif stream.ptr.codecpar.codec_type == lib.AVMEDIA_TYPE_DATA: self.data = self.data + (stream, ) else: self.other = self.other + (stream, ) diff --git a/av/data/stream.pyx b/av/data/stream.pyx index 698242c51..c019961d0 100644 --- a/av/data/stream.pyx +++ b/av/data/stream.pyx @@ -20,7 +20,7 @@ cdef class DataStream(Stream): property name: def __get__(self): - cdef const lib.AVCodecDescriptor *desc = lib.avcodec_descriptor_get(self._codec_context.codec_id) + cdef const lib.AVCodecDescriptor *desc = lib.avcodec_descriptor_get(self.ptr.codecpar.codec_id) if desc == NULL: return None return desc.name diff --git a/av/packet.pyx b/av/packet.pyx index fae970ee3..0687b2237 100644 --- a/av/packet.pyx +++ b/av/packet.pyx @@ -112,7 +112,7 @@ cdef class Packet(Buffer): def __set__(self, Stream stream): self._stream = stream - self.ptr.stream_index = stream._stream.index + self.ptr.stream_index = stream.ptr.index property time_base: """ diff --git a/av/stream.pxd b/av/stream.pxd index 4a3cab488..5ad3b965e 100644 --- a/av/stream.pxd +++ b/av/stream.pxd @@ -8,24 +8,20 @@ from av.packet cimport Packet cdef class Stream(object): + cdef lib.AVStream *ptr # Stream attributes. cdef readonly Container container - - cdef lib.AVStream *_stream cdef readonly dict metadata # CodecContext attributes. - cdef lib.AVCodecContext *_codec_context - cdef const lib.AVCodec *_codec - cdef readonly CodecContext codec_context # Private API. - cdef _init(self, Container, lib.AVStream*) + cdef _init(self, Container, lib.AVStream*, CodecContext) cdef _finalize_for_output(self) cdef _set_time_base(self, value) cdef _set_id(self, value) -cdef Stream wrap_stream(Container, lib.AVStream*) +cdef Stream wrap_stream(Container, lib.AVStream*, CodecContext) diff --git a/av/stream.pyx b/av/stream.pyx index cbab9dde1..73cb3504d 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -17,7 +17,7 @@ from av.utils cimport ( cdef object _cinit_bypass_sentinel = object() -cdef Stream wrap_stream(Container container, lib.AVStream *c_stream): +cdef Stream wrap_stream(Container container, lib.AVStream *c_stream, CodecContext codec_context): """Build an av.Stream for an existing AVStream. The AVStream MUST be fully constructed and ready for use before this is @@ -30,22 +30,22 @@ cdef Stream wrap_stream(Container container, lib.AVStream *c_stream): cdef Stream py_stream - if c_stream.codec.codec_type == lib.AVMEDIA_TYPE_VIDEO: + if c_stream.codecpar.codec_type == lib.AVMEDIA_TYPE_VIDEO: from av.video.stream import VideoStream py_stream = VideoStream.__new__(VideoStream, _cinit_bypass_sentinel) - elif c_stream.codec.codec_type == lib.AVMEDIA_TYPE_AUDIO: + elif c_stream.codecpar.codec_type == lib.AVMEDIA_TYPE_AUDIO: from av.audio.stream import AudioStream py_stream = AudioStream.__new__(AudioStream, _cinit_bypass_sentinel) - elif c_stream.codec.codec_type == lib.AVMEDIA_TYPE_SUBTITLE: + elif c_stream.codecpar.codec_type == lib.AVMEDIA_TYPE_SUBTITLE: from av.subtitles.stream import SubtitleStream py_stream = SubtitleStream.__new__(SubtitleStream, _cinit_bypass_sentinel) - elif c_stream.codec.codec_type == lib.AVMEDIA_TYPE_DATA: + elif c_stream.codecpar.codec_type == lib.AVMEDIA_TYPE_DATA: from av.data.stream import DataStream py_stream = DataStream.__new__(DataStream, _cinit_bypass_sentinel) else: py_stream = Stream.__new__(Stream, _cinit_bypass_sentinel) - py_stream._init(container, c_stream) + py_stream._init(container, c_stream, codec_context) return py_stream @@ -69,14 +69,15 @@ cdef class Stream(object): def __cinit__(self, name): if name is _cinit_bypass_sentinel: return - raise RuntimeError('cannot manually instatiate Stream') - - cdef _init(self, Container container, lib.AVStream *stream): + raise RuntimeError('cannot manually instantiate Stream') + cdef _init(self, Container container, lib.AVStream *stream, CodecContext codec_context): self.container = container - self._stream = stream + self.ptr = stream - self._codec_context = stream.codec + self.codec_context = codec_context + if self.codec_context: + self.codec_context.stream_index = stream.index self.metadata = avdict_to_dict( stream.metadata, @@ -84,23 +85,6 @@ cdef class Stream(object): errors=self.container.metadata_errors, ) - # This is an input container! - if self.container.ptr.iformat: - - # Find the codec. - self._codec = lib.avcodec_find_decoder(self._codec_context.codec_id) - if not self._codec: - # TODO: Setup a dummy CodecContext. - self.codec_context = None - return - - # This is an output container! - else: - self._codec = self._codec_context.codec - - self.codec_context = wrap_codec_context(self._codec_context, self._codec, False) - self.codec_context.stream_index = stream.index - def __repr__(self): return '' % ( self.__class__.__name__, @@ -137,17 +121,17 @@ cdef class Stream(object): cdef _finalize_for_output(self): dict_to_avdict( - &self._stream.metadata, self.metadata, + &self.ptr.metadata, self.metadata, encoding=self.container.metadata_encoding, errors=self.container.metadata_errors, ) - if not self._stream.time_base.num: - self._stream.time_base = self._codec_context.time_base + if not self.ptr.time_base.num: + self.ptr.time_base = self.codec_context.ptr.time_base # It prefers if we pass it parameters via this other object. # Lets just copy what we want. - err_check(lib.avcodec_parameters_from_context(self._stream.codecpar, self._stream.codec)) + err_check(lib.avcodec_parameters_from_context(self.ptr.codecpar, self.codec_context.ptr)) def encode(self, frame=None): """ @@ -165,7 +149,7 @@ cdef class Stream(object): cdef Packet packet for packet in packets: packet._stream = self - packet.ptr.stream_index = self._stream.index + packet.ptr.stream_index = self.ptr.index return packets def decode(self, packet=None): @@ -190,16 +174,16 @@ cdef class Stream(object): """ def __get__(self): - return self._stream.id + return self.ptr.id cdef _set_id(self, value): """ Setter used by __setattr__ for the id property. """ if value is None: - self._stream.id = 0 + self.ptr.id = 0 else: - self._stream.id = value + self.ptr.id = value property profile: """ @@ -208,8 +192,8 @@ cdef class Stream(object): :type: str """ def __get__(self): - if self._codec and lib.av_get_profile_name(self._codec, self._codec_context.profile): - return lib.av_get_profile_name(self._codec, self._codec_context.profile) + if self.codec_context: + return self.codec_context.profile else: return None @@ -219,7 +203,7 @@ cdef class Stream(object): :type: int """ - def __get__(self): return self._stream.index + def __get__(self): return self.ptr.index property time_base: """ @@ -229,13 +213,13 @@ cdef class Stream(object): """ def __get__(self): - return avrational_to_fraction(&self._stream.time_base) + return avrational_to_fraction(&self.ptr.time_base) cdef _set_time_base(self, value): """ Setter used by __setattr__ for the time_base property. """ - to_avrational(value, &self._stream.time_base) + to_avrational(value, &self.ptr.time_base) property average_rate: """ @@ -249,7 +233,7 @@ cdef class Stream(object): """ def __get__(self): - return avrational_to_fraction(&self._stream.avg_frame_rate) + return avrational_to_fraction(&self.ptr.avg_frame_rate) property base_rate: """ @@ -263,7 +247,7 @@ cdef class Stream(object): """ def __get__(self): - return avrational_to_fraction(&self._stream.r_frame_rate) + return avrational_to_fraction(&self.ptr.r_frame_rate) property guessed_rate: """The guessed frame rate of this stream. @@ -276,7 +260,7 @@ cdef class Stream(object): """ def __get__(self): # The two NULL arguments aren't used in FFmpeg >= 4.0 - cdef lib.AVRational val = lib.av_guess_frame_rate(NULL, self._stream, NULL) + cdef lib.AVRational val = lib.av_guess_frame_rate(NULL, self.ptr, NULL) return avrational_to_fraction(&val) property start_time: @@ -287,8 +271,8 @@ cdef class Stream(object): :type: :class:`int` or ``None`` """ def __get__(self): - if self._stream.start_time != lib.AV_NOPTS_VALUE: - return self._stream.start_time + if self.ptr.start_time != lib.AV_NOPTS_VALUE: + return self.ptr.start_time property duration: """ @@ -298,8 +282,8 @@ cdef class Stream(object): """ def __get__(self): - if self._stream.duration != lib.AV_NOPTS_VALUE: - return self._stream.duration + if self.ptr.duration != lib.AV_NOPTS_VALUE: + return self.ptr.duration property frames: """ @@ -309,7 +293,8 @@ cdef class Stream(object): :type: :class:`int` """ - def __get__(self): return self._stream.nb_frames + def __get__(self): + return self.ptr.nb_frames property language: """ @@ -329,4 +314,4 @@ cdef class Stream(object): :type: str """ - return lib.av_get_media_type_string(self._codec_context.codec_type) + return lib.av_get_media_type_string(self.ptr.codecpar.codec_type) diff --git a/av/video/stream.pyx b/av/video/stream.pyx index 70b8f3209..8694b63ba 100644 --- a/av/video/stream.pyx +++ b/av/video/stream.pyx @@ -6,7 +6,7 @@ cdef class VideoStream(Stream): self.index, self.name, self.format.name if self.format else None, - self._codec_context.width, - self._codec_context.height, + self.codec_context.width, + self.codec_context.height, id(self), ) diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 8c0a9685b..1e6111808 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -194,6 +194,7 @@ cdef extern from "libavcodec/avcodec.h" nogil: float rc_min_vbv_overflow_use AVRational framerate + AVRational pkt_timebase AVRational time_base int ticks_per_frame @@ -237,7 +238,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef void avcodec_free_context(AVCodecContext **ctx) cdef AVClass* avcodec_get_class() - cdef int avcodec_copy_context(AVCodecContext *dst, const AVCodecContext *src) cdef struct AVCodecDescriptor: AVCodecID id @@ -455,10 +455,18 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef struct AVCodecParameters: - pass + AVMediaType codec_type + AVCodecID codec_id + cdef int avcodec_parameters_copy( + AVCodecParameters *dst, + const AVCodecParameters *src + ) cdef int avcodec_parameters_from_context( AVCodecParameters *par, const AVCodecContext *codec, ) - + cdef int avcodec_parameters_to_context( + AVCodecContext *codec, + const AVCodecParameters *par + ) diff --git a/include/libavformat/avformat.pxd b/include/libavformat/avformat.pxd index 0a33cf9f6..ed3e503f5 100644 --- a/include/libavformat/avformat.pxd +++ b/include/libavformat/avformat.pxd @@ -33,7 +33,6 @@ cdef extern from "libavformat/avformat.h" nogil: int index int id - AVCodecContext *codec AVCodecParameters *codecpar AVRational time_base diff --git a/tests/common.py b/tests/common.py index 5d1bf74cc..a49b7bec2 100644 --- a/tests/common.py +++ b/tests/common.py @@ -82,6 +82,7 @@ def _inner(self, *args, **kwargs): return func(self, *args, **kwargs) finally: os.chdir(current_dir) + return _inner diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index a62c05c4e..7087804f7 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -180,7 +180,7 @@ def image_sequence_encode(self, codec_name): ctx.width = width ctx.height = height - ctx.time_base = video_stream.codec_context.time_base + ctx.time_base = video_stream.time_base ctx.pix_fmt = pix_fmt ctx.open() @@ -262,7 +262,7 @@ def video_encoding(self, codec_name, options={}, codec_tag=None): width = options.pop("width", 640) height = options.pop("height", 480) max_frames = options.pop("max_frames", 50) - time_base = options.pop("time_base", video_stream.codec_context.time_base) + time_base = options.pop("time_base", video_stream.time_base) ctx = codec.create() ctx.width = width diff --git a/tests/test_encode.py b/tests/test_encode.py index 018c6ac31..7c5d0353f 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -70,27 +70,59 @@ def assert_rgb_rotate(self, input_, is_dash=False): if is_dash: # FFmpeg 4.2 added parsing of the programme information and it is named "Title" if av.library_versions["libavformat"] >= (58, 28): - self.assertTrue(input_.metadata.get("Title") == "container", input_.metadata) + self.assertTrue( + input_.metadata.get("Title") == "container", input_.metadata + ) else: self.assertEqual(input_.metadata.get("title"), "container", input_.metadata) self.assertEqual(input_.metadata.get("key"), None) + stream = input_.streams[0] - self.assertIsInstance(stream, VideoStream) - self.assertEqual(stream.type, "video") - self.assertEqual(stream.name, "mpeg4") - self.assertEqual( - stream.average_rate, 24 - ) # Only because we constructed is precisely. - self.assertEqual(stream.rate, Fraction(24, 1)) + if is_dash: # The DASH format doesn't provide a duration for the stream # and so the container duration (micro seconds) is checked instead self.assertEqual(input_.duration, 2000000) + expected_average_rate = 24 + expected_duration = None + expected_frames = 0 + expected_id = 0 else: - self.assertEqual(stream.time_base * stream.duration, 2) + if av.library_versions["libavformat"] < (58, 76): + # FFmpeg < 4.4 + expected_average_rate = Fraction(1152, 47) + expected_duration = 24064 + else: + # FFmpeg >= 4.4 + expected_average_rate = 24 + expected_duration = 24576 + expected_frames = 48 + expected_id = 1 + + # actual stream properties + self.assertIsInstance(stream, VideoStream) + self.assertEqual(stream.average_rate, expected_average_rate) + self.assertEqual(stream.base_rate, 24) + self.assertEqual(stream.duration, expected_duration) + self.assertEqual(stream.guessed_rate, 24) + self.assertEqual(stream.frames, expected_frames) + self.assertEqual(stream.id, expected_id) + self.assertEqual(stream.index, 0) + self.assertEqual(stream.profile, "Simple Profile") + self.assertEqual(stream.start_time, 0) + self.assertEqual(stream.time_base, Fraction(1, 12288)) + self.assertEqual(stream.type, "video") + + # codec properties + self.assertEqual(stream.name, "mpeg4") + self.assertEqual(stream.long_name, "MPEG-4 part 2") + + # codec context properties self.assertEqual(stream.format.name, "yuv420p") self.assertEqual(stream.format.width, WIDTH) self.assertEqual(stream.format.height, HEIGHT) + self.assertEqual(stream.rate, None) + self.assertEqual(stream.ticks_per_frame, 1) class TestBasicVideoEncoding(TestCase): From bb6d61dca8089cce297061c490c82ce65b958943 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Wed, 20 Jul 2022 15:38:13 +0200 Subject: [PATCH 108/846] [tests] run Windows tests against FFmpeg 5.0 too --- .github/workflows/tests.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f0bfaa5c7..1c735ee2f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -146,6 +146,7 @@ jobs: fail-fast: false matrix: config: + - {os: windows-latest, python: 3.7, ffmpeg: "5.0"} - {os: windows-latest, python: 3.7, ffmpeg: "4.4"} - {os: windows-latest, python: 3.7, ffmpeg: "4.3"} @@ -166,8 +167,10 @@ jobs: pillow \ python=${{ matrix.config.python }} \ setuptools - if [[ "${{ matrix.config.ffmpeg }}" == "4.4" ]]; then - curl -L -o ffmpeg.tar.gz https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/4.4.1-7/ffmpeg-win_amd64.tar.gz + if [[ "${{ matrix.config.ffmpeg }}" == "5.0" ]]; then + curl -L -o ffmpeg.tar.gz https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.0.1-1/ffmpeg-win_amd64.tar.gz + elif [[ "${{ matrix.config.ffmpeg }}" == "4.4" ]]; then + curl -L -o ffmpeg.tar.gz https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/4.4.1-8/ffmpeg-win_amd64.tar.gz else conda install -q -n pyav ffmpeg=${{ matrix.config.ffmpeg }} fi @@ -177,9 +180,9 @@ jobs: run: | . $CONDA/etc/profile.d/conda.sh conda activate pyav - if [[ "${{ matrix.config.ffmpeg }}" == "4.4" ]]; then - tar -xf ffmpeg.tar.gz -C $CONDA_PREFIX/Library/ - fi + if [[ "${{ matrix.config.ffmpeg }}" != "4.3" ]]; then + tar -xf ffmpeg.tar.gz -C $CONDA_PREFIX/Library/ + fi python setup.py build_ext --inplace --ffmpeg-dir=$CONDA_PREFIX/Library - name: Test From 6bf6d29700e4b9e88ebf867a3f6bcea4e93be213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Thu, 21 Jul 2022 01:16:47 +0200 Subject: [PATCH 109/846] [package] compile wheels against FFmpeg 5.0.1 --- scripts/fetch-vendor.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/fetch-vendor.json b/scripts/fetch-vendor.json index 709aaabb6..41969df2c 100644 --- a/scripts/fetch-vendor.json +++ b/scripts/fetch-vendor.json @@ -1,3 +1,3 @@ { - "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/4.4.1-7/ffmpeg-{platform}.tar.gz"] + "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.0.1-1/ffmpeg-{platform}.tar.gz"] } From 1d892a7ba638c5c708da57f72afe204011c8e6e9 Mon Sep 17 00:00:00 2001 From: Maxime Desroches Date: Sat, 18 Jun 2022 01:09:26 -0700 Subject: [PATCH 110/846] Add VideoFrame ndarray operations for nv12 --- av/video/frame.pyx | 16 ++++++++++++++++ tests/test_videoframe.py | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 42ec9dea2..67cf55382 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -310,6 +310,11 @@ cdef class VideoFrame(Frame): image = useful_array(frame.planes[0]).reshape(frame.height, frame.width) palette = np.frombuffer(frame.planes[1], 'i4').astype('>i4').reshape(-1, 1).view(np.uint8) return image, palette + elif frame.format.name == 'nv12': + return np.hstack(( + useful_array(frame.planes[0]), + useful_array(frame.planes[1], 2) + )).reshape(-1, frame.width) else: raise ValueError('Conversion to numpy array with format `%s` is not yet supported' % frame.format.name) @@ -409,6 +414,17 @@ cdef class VideoFrame(Frame): frame = VideoFrame(array.shape[1], array.shape[0], format) copy_array_to_plane(byteswap_array(array, format == 'rgba64be'), frame.planes[0], 8) return frame + elif format == 'nv12': + check_ndarray(array, 'uint8', 2) + check_ndarray_shape(array, array.shape[0] % 3 == 0) + check_ndarray_shape(array, array.shape[1] % 2 == 0) + + frame = VideoFrame(array.shape[1], (array.shape[0] * 2) // 3, format) + uv_start = frame.width * frame.height + flat = array.reshape(-1) + copy_array_to_plane(flat[:uv_start], frame.planes[0], 1) + copy_array_to_plane(flat[uv_start:], frame.planes[1], 2) + return frame else: raise ValueError('Conversion from numpy array with format `%s` is not yet supported' % format) diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index f037326db..a322c1e99 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -440,6 +440,22 @@ def test_ndarray_pal8(self): self.assertNdarraysEqual(returned[0], array) self.assertNdarraysEqual(returned[1], palette) + def test_ndarray_nv12(self): + array = numpy.random.randint(0, 256, size=(720, 640), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="nv12") + self.assertEqual(frame.width, 640) + self.assertEqual(frame.height, 480) + self.assertEqual(frame.format.name, "nv12") + self.assertNdarraysEqual(frame.to_ndarray(), array) + + def test_ndarray_nv12_align(self): + array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="nv12") + self.assertEqual(frame.width, 318) + self.assertEqual(frame.height, 238) + self.assertEqual(frame.format.name, "nv12") + self.assertNdarraysEqual(frame.to_ndarray(), array) + class TestVideoFrameTiming(TestCase): def test_reformat_pts(self): From 54ada44d38eeb1b141db92df065000379ce25e1a Mon Sep 17 00:00:00 2001 From: Jonathan Drolet Date: Tue, 13 Sep 2022 14:38:29 -0400 Subject: [PATCH 111/846] Add support in VideoFrame ndarray for gbrpf32 format --- av/video/frame.pyx | 15 +++++++++++++++ tests/test_videoframe.py | 18 ++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 67cf55382..ac226fa0f 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -285,6 +285,12 @@ cdef class VideoFrame(Frame): array[:, :, 1] = useful_array(frame.planes[0], 2, "uint16").reshape(-1, frame.width) array[:, :, 2] = useful_array(frame.planes[1], 2, "uint16").reshape(-1, frame.width) return byteswap_array(array, frame.format.name.endswith('be')) + elif frame.format.name in ('gbrpf32be', 'gbrpf32le'): + array = np.empty((frame.height, frame.width, 3), dtype="float32") + array[:, :, 0] = useful_array(frame.planes[2], 4, "float32").reshape(-1, frame.width) + array[:, :, 1] = useful_array(frame.planes[0], 4, "float32").reshape(-1, frame.width) + array[:, :, 2] = useful_array(frame.planes[1], 4, "float32").reshape(-1, frame.width) + return byteswap_array(array, frame.format.name.endswith('be')) elif frame.format.name in ('rgb24', 'bgr24'): return useful_array(frame.planes[0], 3).reshape(frame.height, frame.width, -1) elif frame.format.name in ('argb', 'rgba', 'abgr', 'bgra'): @@ -389,6 +395,15 @@ cdef class VideoFrame(Frame): copy_array_to_plane(byteswap_array(array[:, :, 2], format.endswith('be')), frame.planes[1], 2) copy_array_to_plane(byteswap_array(array[:, :, 0], format.endswith('be')), frame.planes[2], 2) return frame + elif format in ('gbrpf32be', 'gbrpf32le'): + check_ndarray(array, 'float32', 3) + check_ndarray_shape(array, array.shape[2] == 3) + + frame = VideoFrame(array.shape[1], array.shape[0], format) + copy_array_to_plane(byteswap_array(array[:, :, 1], format.endswith('be')), frame.planes[0], 4) + copy_array_to_plane(byteswap_array(array[:, :, 2], format.endswith('be')), frame.planes[1], 4) + copy_array_to_plane(byteswap_array(array[:, :, 0], format.endswith('be')), frame.planes[2], 4) + return frame elif format in ('rgb24', 'bgr24'): check_ndarray(array, 'uint8', 3) check_ndarray_shape(array, array.shape[2] == 3) diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index a322c1e99..3d354f0d6 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -295,6 +295,24 @@ def test_ndarray_gbrp16_align(self): self.assertEqual(frame.format.name, format) self.assertNdarraysEqual(frame.to_ndarray(), array) + def test_ndarray_gbrpf32(self): + array = numpy.random.random_sample(size=(480, 640, 3)).astype(numpy.float32) + for format in ["gbrpf32be", "gbrpf32le"]: + frame = VideoFrame.from_ndarray(array, format=format) + self.assertEqual(frame.width, 640) + self.assertEqual(frame.height, 480) + self.assertEqual(frame.format.name, format) + self.assertNdarraysEqual(frame.to_ndarray(), array) + + def test_ndarray_gbrpf32_align(self): + array = numpy.random.random_sample(size=(238, 318, 3)).astype(numpy.float32) + for format in ["gbrpf32be", "gbrpf32le"]: + frame = VideoFrame.from_ndarray(array, format=format) + self.assertEqual(frame.width, 318) + self.assertEqual(frame.height, 238) + self.assertEqual(frame.format.name, format) + self.assertNdarraysEqual(frame.to_ndarray(), array) + def test_ndarray_yuv420p(self): array = numpy.random.randint(0, 256, size=(720, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuv420p") From 2a747c7fe9c3b428cffc385e63a10ae2b1c5e989 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Sun, 16 Oct 2022 11:48:04 +0200 Subject: [PATCH 112/846] =?UTF-8?q?[package]=C2=A0advertise=20Python=203.1?= =?UTF-8?q?1=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cibuildwheel v2.10.0 and later will automatically build wheels for Python 3.11. --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index ec234f4a0..dcd9962d4 100644 --- a/setup.py +++ b/setup.py @@ -214,6 +214,7 @@ def parse_cflags(raw_flags): "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Multimedia :: Sound/Audio", "Topic :: Multimedia :: Sound/Audio :: Conversion", From c107b33a09b637f6f779468e3156cbd37415dea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Mon, 17 Oct 2022 06:59:20 +0200 Subject: [PATCH 113/846] Do not pull codec_context.codec properties up to the stream The `Stream` class has a confusing behaviour which proxies: - both codec context *and* codec properties for read access - only codec context properties for write access Start simplifying things by completely removing access to codec properties. --- av/stream.pyx | 13 ++------ tests/test_codec_context.py | 2 +- tests/test_decode.py | 2 +- tests/test_encode.py | 29 +++++++++-------- tests/test_file_probing.py | 65 ++++++++++++++++++++++--------------- tests/test_python_io.py | 6 ++-- tests/test_seek.py | 6 ++-- 7 files changed, 68 insertions(+), 55 deletions(-) diff --git a/av/stream.pyx b/av/stream.pyx index 73cb3504d..5a4b5b436 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -95,23 +95,16 @@ cdef class Stream(object): ) def __getattr__(self, name): - # avoid an infinite loop for unsupported codecs - if self.codec_context is None: - return - - try: + # Convenience getter for codec context properties. + if self.codec_context is not None: return getattr(self.codec_context, name) - except AttributeError: - try: - return getattr(self.codec_context.codec, name) - except AttributeError: - raise AttributeError(name) def __setattr__(self, name, value): if name == "id": self._set_id(value) return + # Convenience setter for codec context properties. if self.codec_context is not None: setattr(self.codec_context, name, value) diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 7087804f7..c94945a54 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -388,5 +388,5 @@ def audio_encoding(self, codec_name): # so can really use checksums for frame in iter_raw_frames(path, packet_sizes, ctx): result_samples += frame.samples - self.assertEqual(frame.rate, sample_rate) + self.assertEqual(frame.sample_rate, sample_rate) self.assertEqual(len(frame.layout.channels), channels) diff --git a/tests/test_decode.py b/tests/test_decode.py index 709010196..185b7ec8e 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -53,7 +53,7 @@ def test_decode_audio_sample_count(self): sample_count += frame.samples total_samples = ( - audio_stream.duration * audio_stream.rate.numerator + audio_stream.duration * audio_stream.sample_rate.numerator ) / audio_stream.time_base.denominator self.assertEqual(sample_count, total_samples) diff --git a/tests/test_encode.py b/tests/test_encode.py index 7c5d0353f..c570e6a30 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -113,15 +113,12 @@ def assert_rgb_rotate(self, input_, is_dash=False): self.assertEqual(stream.time_base, Fraction(1, 12288)) self.assertEqual(stream.type, "video") - # codec properties - self.assertEqual(stream.name, "mpeg4") - self.assertEqual(stream.long_name, "MPEG-4 part 2") - # codec context properties + self.assertEqual(stream.codec.name, "mpeg4") + self.assertEqual(stream.codec.long_name, "MPEG-4 part 2") self.assertEqual(stream.format.name, "yuv420p") self.assertEqual(stream.format.width, WIDTH) self.assertEqual(stream.format.height, HEIGHT) - self.assertEqual(stream.rate, None) self.assertEqual(stream.ticks_per_frame, 1) @@ -129,15 +126,17 @@ class TestBasicVideoEncoding(TestCase): def test_default_options(self): with av.open(self.sandboxed("output.mov"), "w") as output: stream = output.add_stream("mpeg4") + self.assertEqual(stream.average_rate, Fraction(24, 1)) + self.assertEqual(stream.time_base, None) + + # codec context properties self.assertEqual(stream.bit_rate, 1024000) self.assertEqual(stream.format.height, 480) self.assertEqual(stream.format.name, "yuv420p") self.assertEqual(stream.format.width, 640) self.assertEqual(stream.height, 480) self.assertEqual(stream.pix_fmt, "yuv420p") - self.assertEqual(stream.rate, Fraction(24, 1)) self.assertEqual(stream.ticks_per_frame, 1) - self.assertEqual(stream.time_base, None) self.assertEqual(stream.width, 640) def test_encoding(self): @@ -183,11 +182,13 @@ class TestBasicAudioEncoding(TestCase): def test_default_options(self): with av.open(self.sandboxed("output.mov"), "w") as output: stream = output.add_stream("mp2") + self.assertEqual(stream.time_base, None) + + # codec context properties self.assertEqual(stream.bit_rate, 128000) self.assertEqual(stream.format.name, "s16") - self.assertEqual(stream.rate, 48000) + self.assertEqual(stream.sample_rate, 48000) self.assertEqual(stream.ticks_per_frame, 1) - self.assertEqual(stream.time_base, None) def test_transcode(self): path = self.sandboxed("audio_transcode.mov") @@ -229,9 +230,11 @@ def test_transcode(self): stream = container.streams[0] self.assertIsInstance(stream, AudioStream) - self.assertEqual(stream.codec_context.sample_rate, sample_rate) - self.assertEqual(stream.codec_context.format.name, "s16p") - self.assertEqual(stream.codec_context.channels, channels) + + # codec context properties + self.assertEqual(stream.channels, channels) + self.assertEqual(stream.format.name, "s16p") + self.assertEqual(stream.sample_rate, sample_rate) class TestEncodeStreamSemantics(TestCase): @@ -262,7 +265,7 @@ def test_stream_index(self): # decoder didn't indicate constant frame size frame_size = 1000 aframe = AudioFrame("s16", "stereo", samples=frame_size) - aframe.rate = 48000 + aframe.sample_rate = 48000 apackets = astream.encode(aframe) if apackets: apacket = apackets[0] diff --git a/tests/test_file_probing.py b/tests/test_file_probing.py index 69b356cb5..29710a1b0 100644 --- a/tests/test_file_probing.py +++ b/tests/test_file_probing.py @@ -25,6 +25,13 @@ def test_container_probing(self): def test_stream_probing(self): stream = self.file.streams[0] + # check __repr__ + self.assertTrue( + str(stream).startswith( + " at ")) + # actual stream properties self.assertEqual(stream.average_rate, None) self.assertEqual(stream.base_rate, None) @@ -194,9 +200,8 @@ def test_stream_probing(self): self.assertEqual(stream.time_base, Fraction(1, 90000)) self.assertEqual(stream.type, "data") - # codec properties - self.assertEqual(stream.name, None) - self.assertEqual(stream.long_name, None) + # codec context properties + self.assertEqual(stream.codec, None) class TestSubtitleProbe(TestCase): @@ -227,6 +232,11 @@ def test_container_probing(self): def test_stream_probing(self): stream = self.file.streams[0] + # check __repr__ + self.assertTrue( + str(stream).startswith(" Date: Mon, 17 Oct 2022 08:54:54 +0200 Subject: [PATCH 114/846] [package] compile wheels against FFmpeg 5.1.2 We also update our test matrix to explicitly test against FFmpeg 5.1. --- .github/workflows/tests.yml | 8 ++++++-- docs/overview/installation.rst | 8 ++++++-- scripts/fetch-vendor.json | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1c735ee2f..ee99d13b8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -56,7 +56,8 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-latest, python: 3.7, ffmpeg: "5.0", extras: true} + - {os: ubuntu-latest, python: 3.7, ffmpeg: "5.1", extras: true} + - {os: ubuntu-latest, python: 3.7, ffmpeg: "5.0"} - {os: ubuntu-latest, python: 3.7, ffmpeg: "4.4"} - {os: ubuntu-latest, python: 3.7, ffmpeg: "4.3"} - {os: ubuntu-latest, python: pypy3, ffmpeg: "4.4"} @@ -146,6 +147,7 @@ jobs: fail-fast: false matrix: config: + - {os: windows-latest, python: 3.7, ffmpeg: "5.1"} - {os: windows-latest, python: 3.7, ffmpeg: "5.0"} - {os: windows-latest, python: 3.7, ffmpeg: "4.4"} - {os: windows-latest, python: 3.7, ffmpeg: "4.3"} @@ -167,7 +169,9 @@ jobs: pillow \ python=${{ matrix.config.python }} \ setuptools - if [[ "${{ matrix.config.ffmpeg }}" == "5.0" ]]; then + if [[ "${{ matrix.config.ffmpeg }}" == "5.1" ]]; then + curl -L -o ffmpeg.tar.gz https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.1.2-1/ffmpeg-win_amd64.tar.gz + elif [[ "${{ matrix.config.ffmpeg }}" == "5.0" ]]; then curl -L -o ffmpeg.tar.gz https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.0.1-1/ffmpeg-win_amd64.tar.gz elif [[ "${{ matrix.config.ffmpeg }}" == "4.4" ]]; then curl -L -o ffmpeg.tar.gz https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/4.4.1-8/ffmpeg-win_amd64.tar.gz diff --git a/docs/overview/installation.rst b/docs/overview/installation.rst index b7f9c1840..9ac4dc078 100644 --- a/docs/overview/installation.rst +++ b/docs/overview/installation.rst @@ -11,11 +11,10 @@ Since release 8.0.0 binary wheels are provided on PyPI for Linux, Mac and Window pip install av -Currently FFmpeg 4.4.1 is used with the following features enabled for all platforms: +Currently FFmpeg 5.1.2 is used with the following features enabled for all platforms: - fontconfig - gmp -- gnutls - libaom - libass - libbluray @@ -38,6 +37,11 @@ Currently FFmpeg 4.4.1 is used with the following features enabled for all platf - lzma - zlib +The following additional features are also enabled on Linux: + +- gnutls +- libxcb + Conda ----- diff --git a/scripts/fetch-vendor.json b/scripts/fetch-vendor.json index 41969df2c..2a02f285b 100644 --- a/scripts/fetch-vendor.json +++ b/scripts/fetch-vendor.json @@ -1,3 +1,3 @@ { - "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.0.1-1/ffmpeg-{platform}.tar.gz"] + "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.1.2-1/ffmpeg-{platform}.tar.gz"] } From 71afd210ae42b4b13789726d788e8ff2a9ccc996 Mon Sep 17 00:00:00 2001 From: Justin Wong <46082645+uvjustin@users.noreply.github.com> Date: Mon, 17 Oct 2022 13:12:14 +0800 Subject: [PATCH 115/846] Deprecate VideoStream.framerate and VideoStream.rate (fixes: #1005) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These properties are only set for codecs that store the frame rate value in the bitstream. To obtain an estimated frame rate, it is better to use `VideoStream.average_rate`. See: https://ffmpeg.org/doxygen/trunk/structAVCodecContext.html#a4d08b297e97eefd66c714df4fff493c8 Co-authored-by: Jeremy Lainé --- av/stream.pyx | 12 ++++++++++++ tests/test_file_probing.py | 15 +++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/av/stream.pyx b/av/stream.pyx index 5a4b5b436..971eaded1 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -1,3 +1,5 @@ +import warnings + from cpython cimport PyWeakref_NewRef from libc.stdint cimport int64_t, uint8_t from libc.string cimport memcpy @@ -13,6 +15,8 @@ from av.utils cimport ( to_avrational ) +from av.deprecation import AVDeprecationWarning + cdef object _cinit_bypass_sentinel = object() @@ -95,6 +99,14 @@ cdef class Stream(object): ) def __getattr__(self, name): + # Deprecate framerate pass-through as it is not always set. + # See: https://github.com/PyAV-Org/PyAV/issues/1005 + if self.ptr.codecpar.codec_type == lib.AVMEDIA_TYPE_VIDEO and name in ("framerate", "rate"): + warnings.warn( + "VideoStream.%s is deprecated as it is not always set; please use VideoStream.average_rate." % name, + AVDeprecationWarning + ) + # Convenience getter for codec context properties. if self.codec_context is not None: return getattr(self.codec_context, name) diff --git a/tests/test_file_probing.py b/tests/test_file_probing.py index 29710a1b0..c67f7fb8e 100644 --- a/tests/test_file_probing.py +++ b/tests/test_file_probing.py @@ -1,4 +1,5 @@ from fractions import Fraction +import warnings import av @@ -319,6 +320,20 @@ def test_stream_probing(self): self.assertIn(stream.coded_width, (720, 0)) self.assertIn(stream.coded_height, (576, 0)) + # Deprecated properties. + with warnings.catch_warnings(record=True) as captured: + self.assertIsNone(stream.framerate) + self.assertEqual( + captured[0].message.args[0], + "VideoStream.framerate is deprecated as it is not always set; please use VideoStream.average_rate.", + ) + with warnings.catch_warnings(record=True) as captured: + self.assertIsNone(stream.rate) + self.assertEqual( + captured[0].message.args[0], + "VideoStream.rate is deprecated as it is not always set; please use VideoStream.average_rate.", + ) + class TestVideoProbeCorrupt(TestCase): def setUp(self): From bc4eedd5fc474e0f25b22102b2771fe5a42bb1c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Mon, 17 Oct 2022 18:46:55 +0200 Subject: [PATCH 116/846] Release v10.0.0. --- CHANGELOG.rst | 24 ++++++++++++++++++++++++ av/about.py | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 114551fcc..af5416bce 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,30 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. +v10.0.0 +------- + +Major: + +- Add support for FFmpeg 5.0 and 5.1 (:issue:`817`). +- Drop support for FFmpeg < 4.3. +- Deprecate `CodecContext.time_base` for decoders (:issue:`966`). +- Deprecate `VideoStream.framerate` and `VideoStream.rate` (:issue:`1005`). +- Stop proxying `Codec` from `Stream` instances (:issue:`1037`). + +Features: + +- Update FFmpeg to 5.1.2 for the binary wheels. +- Provide binary wheels for Python 3.11 (:issue:`1019`). +- Add VideoFrame ndarray operations for gbrp formats (:issue:`986`). +- Add VideoFrame ndarray operations for gbrpf32 formats (:issue:`1028`). +- Add VideoFrame ndarray operations for nv12 format (:issue:`996`). + +Fixes: + +- Fix conversion to numpy array for multi-byte formats (:issue:`981`). +- Safely iterate over filter pads (:issue:`1000`). + v9.2.0 ------ diff --git a/av/about.py b/av/about.py index 62ad3ee97..9158871f9 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "9.2.0" +__version__ = "10.0.0" From c0ec6adc8b09ba0c0bb388db6e25d2578f996c71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Mon, 17 Oct 2022 22:55:55 +0200 Subject: [PATCH 117/846] [tests] update all github actions to latest version This should get rid of the warnings related to Node.js 12 actions. --- .github/workflows/tests.yml | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ee99d13b8..ded1052b5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -23,11 +23,11 @@ jobs: steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 name: Checkout - name: Python - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: 3.7 @@ -60,7 +60,7 @@ jobs: - {os: ubuntu-latest, python: 3.7, ffmpeg: "5.0"} - {os: ubuntu-latest, python: 3.7, ffmpeg: "4.4"} - {os: ubuntu-latest, python: 3.7, ffmpeg: "4.3"} - - {os: ubuntu-latest, python: pypy3, ffmpeg: "4.4"} + - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "4.4"} - {os: macos-latest, python: 3.7, ffmpeg: "4.4"} env: @@ -69,11 +69,11 @@ jobs: steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 name: Checkout - name: Python ${{ matrix.config.python }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.config.python }} @@ -155,7 +155,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Set up Conda shell: bash @@ -199,8 +199,8 @@ jobs: package-source: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v1 + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 with: python-version: 3.7 - name: Build source package @@ -209,7 +209,7 @@ jobs: python scripts/fetch-vendor.py /tmp/vendor PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig python setup.py sdist - name: Upload source package - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v3 with: name: dist path: dist/ @@ -233,13 +233,13 @@ jobs: - os: windows-latest arch: AMD64 steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v1 + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 with: python-version: 3.7 - name: Set up QEMU if: matrix.os == 'ubuntu-latest' - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v2 - name: Install packages if: matrix.os == 'macos-latest' run: | @@ -265,7 +265,7 @@ jobs: cibuildwheel --output-dir dist shell: bash - name: Upload wheels - uses: actions/upload-artifact@v1 + uses: actions/upload-artifact@v3 with: name: dist path: dist/ @@ -274,8 +274,8 @@ jobs: runs-on: ubuntu-latest needs: [package-source, package-wheel] steps: - - uses: actions/checkout@v2 - - uses: actions/download-artifact@v1 + - uses: actions/checkout@v3 + - uses: actions/download-artifact@v3 with: name: dist path: dist/ From 218a62bef67d35513646a228b762f51641daad68 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sat, 21 Jan 2023 20:14:42 +0100 Subject: [PATCH 118/846] [scratchpad] xrange() was removed in Python 3 --- scratchpad/audio.py | 2 +- scratchpad/decode.py | 2 +- scratchpad/resource_use.py | 4 ++-- scratchpad/seekmany.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/scratchpad/audio.py b/scratchpad/audio.py index c5a79f9c0..addae0915 100644 --- a/scratchpad/audio.py +++ b/scratchpad/audio.py @@ -13,7 +13,7 @@ def print_data(frame): data = bytes(plane) print('\tPLANE %d, %d bytes' % (i, len(data))) data = data.encode('hex') - for i in xrange(0, len(data), 128): + for i in range(0, len(data), 128): print('\t\t\t%s' % data[i:i + 128]) diff --git a/scratchpad/decode.py b/scratchpad/decode.py index d2dfcc580..edfb413b7 100644 --- a/scratchpad/decode.py +++ b/scratchpad/decode.py @@ -155,7 +155,7 @@ def format_time(time, time_base): data = bytes(plane) print('\t\t\tPLANE %d, %d bytes' % (i, len(data))) data = data.encode('hex') - for i in xrange(0, len(data), 128): + for i in range(0, len(data), 128): print('\t\t\t%s' % data[i:i + 128]) if args.count and frame_count >= args.count: diff --git a/scratchpad/resource_use.py b/scratchpad/resource_use.py index b61fc3930..3387b0d70 100644 --- a/scratchpad/resource_use.py +++ b/scratchpad/resource_use.py @@ -24,7 +24,7 @@ def format_bytes(n): usage = [] -for round_ in xrange(args.count): +for round_ in range(args.count): print('Round %d/%d:' % (round_ + 1, args.count)) @@ -55,7 +55,7 @@ def format_bytes(n): usage.append(resource.getrusage(resource.RUSAGE_SELF)) -for i in xrange(len(usage) - 1): +for i in range(len(usage) - 1): before = usage[i] after = usage[i + 1] print('%s (%s)' % (format_bytes(after.ru_maxrss), format_bytes(after.ru_maxrss - before.ru_maxrss))) diff --git a/scratchpad/seekmany.py b/scratchpad/seekmany.py index f117e658e..0b37e5118 100644 --- a/scratchpad/seekmany.py +++ b/scratchpad/seekmany.py @@ -27,7 +27,7 @@ def iter_frames(): for frame in packet.decode(): yield frame -for i in xrange(steps): +for i in range(steps): time = real_duration * i / steps min_time = time - tolerance From ee1d6f62806f8fbb712a7a6103cae85ec445fdf9 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Mon, 23 Jan 2023 12:09:30 +0100 Subject: [PATCH 119/846] [scratchpad] buffer() was removed in Python 3 --- scratchpad/cctx_encode.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/scratchpad/cctx_encode.py b/scratchpad/cctx_encode.py index 7885c578e..165203c1c 100644 --- a/scratchpad/cctx_encode.py +++ b/scratchpad/cctx_encode.py @@ -1,16 +1,15 @@ import logging -from PIL import Image, ImageFont, ImageDraw +from PIL import Image, ImageDraw, ImageFont -logging.basicConfig() - -import av from av.codec import CodecContext from av.video import VideoFrame - from tests.common import fate_suite +logging.basicConfig() + + cc = CodecContext.create('flv', 'w') print(cc) @@ -18,8 +17,7 @@ font = ImageFont.truetype("/System/Library/Fonts/Menlo.ttc", 15) - -fh = open('test.flv', 'w') +fh = open('test.flv', 'wb') for i in range(30): @@ -35,7 +33,7 @@ packet = cc.encode(frame) print(' ', packet) - fh.write(str(buffer(packet))) + fh.write(bytes(packet)) print('Flushing...') @@ -44,6 +42,6 @@ if not packet: break print(' ', packet) - fh.write(str(buffer(packet))) + fh.write(bytes(packet)) print('Done!') From aad8b1b955b94ccd752be25eebf78fccf7a04308 Mon Sep 17 00:00:00 2001 From: Mark Harfouche Date: Fri, 23 Dec 2022 20:59:00 -0500 Subject: [PATCH 120/846] Remove unecessary clip in numpy example By the time you got to that line, the number is already a uint8, and cannot take any values outside of 0, 255. --- examples/numpy/generate_video.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/numpy/generate_video.py b/examples/numpy/generate_video.py index e0c9a9997..80145d514 100644 --- a/examples/numpy/generate_video.py +++ b/examples/numpy/generate_video.py @@ -22,7 +22,6 @@ img[:, :, 2] = 0.5 + 0.5 * np.sin(2 * np.pi * (2 / 3 + frame_i / total_frames)) img = np.round(255 * img).astype(np.uint8) - img = np.clip(img, 0, 255) frame = av.VideoFrame.from_ndarray(img, format="rgb24") for packet in stream.encode(frame): From 7250b2f27d8d38661b492915fb6bf69ec79acdd3 Mon Sep 17 00:00:00 2001 From: Mark Harfouche Date: Mon, 2 Jan 2023 20:43:16 +0530 Subject: [PATCH 121/846] Add the flag ENCODER_FLUSH --- av/codec/codec.pyx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/av/codec/codec.pyx b/av/codec/codec.pyx index 4bbbcf369..ad3198fd8 100644 --- a/av/codec/codec.pyx +++ b/av/codec/codec.pyx @@ -130,6 +130,10 @@ Capabilities = define_enum('Capabilities', 'av.codec', ( """This codec takes the reordered_opaque field from input AVFrames and returns it in the corresponding field in AVCodecContext after encoding."""), + ('ENCODER_FLUSH', 1 << 21, # lib.AV_CODEC_CAP_ENCODER_FLUSH # FFmpeg 4.3 + """This encoder can be flushed using avcodec_flush_buffers(). If this + flag is not set, the encoder must be closed and reopened to ensure that + no frames remain pending."""), ), is_flags=True) @@ -328,6 +332,7 @@ cdef class Codec(object): hardware = capabilities.flag_property('HARDWARE') hybrid = capabilities.flag_property('HYBRID') encoder_reordered_opaque = capabilities.flag_property('ENCODER_REORDERED_OPAQUE') + encoder_flush = capabilities.flag_property('ENCODER_FLUSH') cdef get_codec_names(): From 1ab3bf6bd0bfe60eddd22c8ff90cde4a2dc6d86f Mon Sep 17 00:00:00 2001 From: Mark Harfouche Date: Fri, 23 Dec 2022 22:15:48 -0500 Subject: [PATCH 122/846] Add python_requires to help users not install this with python 2 Works toward https://github.com/PyAV-Org/PyAV/issues/1057 You might need to yank the release and release 10.0.1 --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index dcd9962d4..58fc307b1 100644 --- a/setup.py +++ b/setup.py @@ -192,6 +192,7 @@ def parse_cflags(raw_flags): url="https://github.com/PyAV-Org/PyAV", packages=find_packages(exclude=["build*", "examples*", "scratchpad*", "tests*"]), package_data=package_data, + python_requires='>=3.7', zip_safe=False, ext_modules=ext_modules, test_suite="tests", From 6982d818080716b39bc3f333cf19daa0a3e0b56a Mon Sep 17 00:00:00 2001 From: Hanz <40712686+HanzCEO@users.noreply.github.com> Date: Sun, 25 Dec 2022 13:21:52 +0000 Subject: [PATCH 123/846] [streams] populate added stream with codec param MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit avformat_new_stream(AVFormatContext *s, const AVCodec *c) does not use its second parameter [1], so the codec type for the stream is not populated until avcodec_parameters_from_context() [2] is called. [1]: https://ffmpeg.org/doxygen/trunk/group__lavf__core.html#gadcb0fd3e507d9b58fe78f61f8ad39827 [2]: https://ffmpeg.org/doxygen/trunk/codec__par_8c_source.html#l00099 Co-authored-by: Jeremy Lainé --- av/container/output.pyx | 6 ++++++ tests/test_encode.py | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/av/container/output.pyx b/av/container/output.pyx index a454e121e..c4a7b5a33 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -116,6 +116,12 @@ cdef class OutputContainer(Container): if self.ptr.oformat.flags & lib.AVFMT_GLOBALHEADER: codec_context.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER + # Initialise stream codec parameters to populate the codec type. + # + # Subsequent changes to the codec context will be applied just before + # encoding starts in `start_encoding()`. + err_check(lib.avcodec_parameters_from_context(stream.codecpar, codec_context)) + # Construct the user-land stream cdef CodecContext py_codec_context = wrap_codec_context(codec_context, codec) cdef Stream py_stream = wrap_stream(self, stream, py_codec_context) diff --git a/tests/test_encode.py b/tests/test_encode.py index c570e6a30..4f942354a 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -126,6 +126,7 @@ class TestBasicVideoEncoding(TestCase): def test_default_options(self): with av.open(self.sandboxed("output.mov"), "w") as output: stream = output.add_stream("mpeg4") + self.assertIn(stream, output.streams.video) self.assertEqual(stream.average_rate, Fraction(24, 1)) self.assertEqual(stream.time_base, None) @@ -152,6 +153,7 @@ def test_encoding_with_pts(self): with av.open(path, "w") as output: stream = output.add_stream("libx264", 24) + self.assertIn(stream, output.streams.video) stream.width = WIDTH stream.height = HEIGHT stream.pix_fmt = "yuv420p" @@ -182,6 +184,7 @@ class TestBasicAudioEncoding(TestCase): def test_default_options(self): with av.open(self.sandboxed("output.mov"), "w") as output: stream = output.add_stream("mp2") + self.assertIn(stream, output.streams.audio) self.assertEqual(stream.time_base, None) # codec context properties @@ -203,6 +206,7 @@ def test_transcode(self): sample_fmt = "s16" stream = output.add_stream("mp2", sample_rate) + self.assertIn(stream, output.streams.audio) ctx = stream.codec_context ctx.time_base = sample_rate @@ -241,11 +245,13 @@ class TestEncodeStreamSemantics(TestCase): def test_stream_index(self): with av.open(self.sandboxed("output.mov"), "w") as output: vstream = output.add_stream("mpeg4", 24) + self.assertIn(vstream, output.streams.video) vstream.pix_fmt = "yuv420p" vstream.width = 320 vstream.height = 240 astream = output.add_stream("mp2", 48000) + self.assertIn(astream, output.streams.audio) astream.channels = 2 astream.format = "s16" @@ -277,6 +283,7 @@ def test_stream_index(self): def test_set_id_and_time_base(self): with av.open(self.sandboxed("output.mov"), "w") as output: stream = output.add_stream("mp2") + self.assertIn(stream, output.streams.audio) # set id self.assertEqual(stream.id, 0) From 00cf38fb0a11823dc171246d2fb115c144056d87 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 28 Oct 2023 12:32:56 -0400 Subject: [PATCH 124/846] Update CI/CL, drop Python 3.7 (#1195) Python 3.7 is EOL, let's at least use 3.8 as the minimum. --- .github/workflows/tests.yml | 55 +++++++++++++++++-------------------- av/__main__.py | 2 -- av/datasets.py | 2 -- setup.py | 5 ++-- 4 files changed, 27 insertions(+), 37 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ded1052b5..dc5db3b5b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,15 +1,19 @@ name: tests - -on: [push, pull_request] - +on: + push: + paths-ignore: + - '**.md' + - '**.rst' + - '**.txt' + pull_request: + paths-ignore: + - '**.md' + - '**.rst' + - '**.txt' jobs: - - style: - name: "${{ matrix.config.suite }}" runs-on: ubuntu-latest - strategy: matrix: config: @@ -22,14 +26,13 @@ jobs: PYAV_LIBRARY: ffmpeg-4.3 # doesn't matter steps: - - uses: actions/checkout@v3 name: Checkout - name: Python uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.8 - name: Environment run: env | sort @@ -45,30 +48,25 @@ jobs: . scripts/activate.sh ./scripts/test ${{ matrix.config.suite }} - nix: - name: "py-${{ matrix.config.python }} lib-${{ matrix.config.ffmpeg }} ${{matrix.config.os}}" - runs-on: ${{ matrix.config.os }} - strategy: fail-fast: false matrix: config: - - {os: ubuntu-latest, python: 3.7, ffmpeg: "5.1", extras: true} - - {os: ubuntu-latest, python: 3.7, ffmpeg: "5.0"} - - {os: ubuntu-latest, python: 3.7, ffmpeg: "4.4"} - - {os: ubuntu-latest, python: 3.7, ffmpeg: "4.3"} + - {os: ubuntu-latest, python: 3.8, ffmpeg: "5.1", extras: true} + - {os: ubuntu-latest, python: 3.8, ffmpeg: "5.0"} + - {os: ubuntu-latest, python: 3.8, ffmpeg: "4.4"} + - {os: ubuntu-latest, python: 3.8, ffmpeg: "4.3"} - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "4.4"} - - {os: macos-latest, python: 3.7, ffmpeg: "4.4"} + - {os: macos-latest, python: 3.8, ffmpeg: "4.4"} env: PYAV_PYTHON: python${{ matrix.config.python }} PYAV_LIBRARY: ffmpeg-${{ matrix.config.ffmpeg }} steps: - - uses: actions/checkout@v3 name: Checkout @@ -138,22 +136,19 @@ jobs: scripts/test sdist windows: - name: "py-${{ matrix.config.python }} lib-${{ matrix.config.ffmpeg }} ${{matrix.config.os}}" - runs-on: ${{ matrix.config.os }} strategy: fail-fast: false matrix: config: - - {os: windows-latest, python: 3.7, ffmpeg: "5.1"} - - {os: windows-latest, python: 3.7, ffmpeg: "5.0"} - - {os: windows-latest, python: 3.7, ffmpeg: "4.4"} - - {os: windows-latest, python: 3.7, ffmpeg: "4.3"} + - {os: windows-latest, python: 3.8, ffmpeg: "5.1"} + - {os: windows-latest, python: 3.8, ffmpeg: "5.0"} + - {os: windows-latest, python: 3.8, ffmpeg: "4.4"} + - {os: windows-latest, python: 3.8, ffmpeg: "4.3"} steps: - - name: Checkout uses: actions/checkout@v3 @@ -202,7 +197,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.8 - name: Build source package run: | pip install cython @@ -236,7 +231,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.8 - name: Set up QEMU if: matrix.os == 'ubuntu-latest' uses: docker/setup-qemu-action@v2 @@ -259,7 +254,7 @@ jobs: CIBW_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m unittest discover -t {project} -s tests && mv {project}/av.disabled {project}/av CIBW_TEST_REQUIRES: numpy # skip tests when there are no binary wheels of numpy - CIBW_TEST_SKIP: cp37-* pp* *_i686 + CIBW_TEST_SKIP: pp* *_i686 run: | pip install cibuildwheel delvewheel cibuildwheel --output-dir dist @@ -281,7 +276,7 @@ jobs: path: dist/ - name: Publish to PyPI if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/') - uses: pypa/gh-action-pypi-publish@master + uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.PYPI_TOKEN }} diff --git a/av/__main__.py b/av/__main__.py index 8c57e2dd9..b5718ba8b 100644 --- a/av/__main__.py +++ b/av/__main__.py @@ -2,7 +2,6 @@ def main(): - parser = argparse.ArgumentParser() parser.add_argument("--codecs", action="store_true") parser.add_argument("--version", action="store_true") @@ -11,7 +10,6 @@ def main(): # --- if args.version: - import av import av._core diff --git a/av/datasets.py b/av/datasets.py index 3324ce0bc..b3a5eed85 100644 --- a/av/datasets.py +++ b/av/datasets.py @@ -9,7 +9,6 @@ def iter_data_dirs(check_writable=False): - try: yield os.environ["PYAV_TESTDATA_DIR"] except KeyError: @@ -45,7 +44,6 @@ def iter_data_dirs(check_writable=False): def cached_download(url, name): - """Download the data at a URL, and cache it under the given name. The file is stored under `pyav/test` with the given name in the directory diff --git a/setup.py b/setup.py index 58fc307b1..b9e9ba2d4 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,6 @@ def new_embed_signature(self, sig, doc): - # Strip any `self` parameters from the front. sig = re.sub(r"\(self(,\s+)?", "(", sig) @@ -192,7 +191,7 @@ def parse_cflags(raw_flags): url="https://github.com/PyAV-Org/PyAV", packages=find_packages(exclude=["build*", "examples*", "scratchpad*", "tests*"]), package_data=package_data, - python_requires='>=3.7', + python_requires=">=3.8", zip_safe=False, ext_modules=ext_modules, test_suite="tests", @@ -211,11 +210,11 @@ def parse_cflags(raw_flags): "Operating System :: Unix", "Operating System :: Microsoft :: Windows", "Programming Language :: Cython", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Multimedia :: Sound/Audio", "Topic :: Multimedia :: Sound/Audio :: Conversion", From ad05d90c255bda3f56bbc2a5147c941103acb94a Mon Sep 17 00:00:00 2001 From: Mark Harfouche Date: Sat, 28 Oct 2023 13:26:21 -0400 Subject: [PATCH 125/846] Small updates to enable Cython 3 (#1145) --- av/container/core.pyx | 12 ++++++------ av/container/pyio.pxd | 6 +++--- av/container/pyio.pyx | 12 ++++++------ av/logging.pyx | 4 ++-- av/video/format.pxd | 2 +- docs/development/includes.py | 13 ++++++++++--- 6 files changed, 28 insertions(+), 21 deletions(-) diff --git a/av/container/core.pyx b/av/container/core.pyx index d21893c43..493576f16 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -20,7 +20,7 @@ from av.dictionary import Dictionary from av.logging import Capture as LogCapture -ctypedef int64_t (*seek_func_t)(void *opaque, int64_t offset, int whence) nogil +ctypedef int64_t (*seek_func_t)(void *opaque, int64_t offset, int whence) noexcept nogil cdef object _cinit_sentinel = object() @@ -29,7 +29,7 @@ cdef object _cinit_sentinel = object() # We want to use the monotonic clock if it is available. cdef object clock = getattr(time, 'monotonic', time.time) -cdef int interrupt_cb (void *p) nogil: +cdef int interrupt_cb (void *p) noexcept nogil: cdef timeout_info info = dereference( p) if info.timeout < 0: # timeout < 0 means no timeout @@ -56,7 +56,7 @@ cdef int pyav_io_open(lib.AVFormatContext *s, lib.AVIOContext **pb, const char *url, int flags, - lib.AVDictionary **options) nogil: + lib.AVDictionary **options) noexcept nogil: with gil: return pyav_io_open_gil(s, pb, url, flags, options) @@ -65,7 +65,7 @@ cdef int pyav_io_open_gil(lib.AVFormatContext *s, lib.AVIOContext **pb, const char *url, int flags, - lib.AVDictionary **options): + lib.AVDictionary **options) noexcept: cdef Container container cdef object file cdef PyIOFile pyio_file @@ -104,13 +104,13 @@ cdef int pyav_io_open_gil(lib.AVFormatContext *s, cdef void pyav_io_close(lib.AVFormatContext *s, - lib.AVIOContext *pb) nogil: + lib.AVIOContext *pb) noexcept nogil: with gil: pyav_io_close_gil(s, pb) cdef void pyav_io_close_gil(lib.AVFormatContext *s, - lib.AVIOContext *pb): + lib.AVIOContext *pb) noexcept: cdef Container container try: container = dereference(s).opaque diff --git a/av/container/pyio.pxd b/av/container/pyio.pxd index b2a593b14..0faeea4f1 100644 --- a/av/container/pyio.pxd +++ b/av/container/pyio.pxd @@ -2,11 +2,11 @@ from libc.stdint cimport int64_t, uint8_t cimport libav as lib -cdef int pyio_read(void *opaque, uint8_t *buf, int buf_size) nogil +cdef int pyio_read(void *opaque, uint8_t *buf, int buf_size) noexcept nogil -cdef int pyio_write(void *opaque, uint8_t *buf, int buf_size) nogil +cdef int pyio_write(void *opaque, uint8_t *buf, int buf_size) noexcept nogil -cdef int64_t pyio_seek(void *opaque, int64_t offset, int whence) nogil +cdef int64_t pyio_seek(void *opaque, int64_t offset, int whence) noexcept nogil cdef void pyio_close_gil(lib.AVIOContext *pb) diff --git a/av/container/pyio.pyx b/av/container/pyio.pyx index 17d977f3e..07224cd91 100644 --- a/av/container/pyio.pyx +++ b/av/container/pyio.pyx @@ -4,7 +4,7 @@ cimport libav as lib from av.error cimport stash_exception -ctypedef int64_t (*seek_func_t)(void *opaque, int64_t offset, int whence) nogil +ctypedef int64_t (*seek_func_t)(void *opaque, int64_t offset, int whence) noexcept nogil cdef class PyIOFile(object): @@ -76,11 +76,11 @@ cdef class PyIOFile(object): lib.av_freep(&self.buffer) -cdef int pyio_read(void *opaque, uint8_t *buf, int buf_size) nogil: +cdef int pyio_read(void *opaque, uint8_t *buf, int buf_size) noexcept nogil: with gil: return pyio_read_gil(opaque, buf, buf_size) -cdef int pyio_read_gil(void *opaque, uint8_t *buf, int buf_size): +cdef int pyio_read_gil(void *opaque, uint8_t *buf, int buf_size) noexcept: cdef PyIOFile self cdef bytes res try: @@ -95,11 +95,11 @@ cdef int pyio_read_gil(void *opaque, uint8_t *buf, int buf_size): return stash_exception() -cdef int pyio_write(void *opaque, uint8_t *buf, int buf_size) nogil: +cdef int pyio_write(void *opaque, uint8_t *buf, int buf_size) noexcept nogil: with gil: return pyio_write_gil(opaque, buf, buf_size) -cdef int pyio_write_gil(void *opaque, uint8_t *buf, int buf_size): +cdef int pyio_write_gil(void *opaque, uint8_t *buf, int buf_size) noexcept: cdef PyIOFile self cdef bytes bytes_to_write cdef int bytes_written @@ -114,7 +114,7 @@ cdef int pyio_write_gil(void *opaque, uint8_t *buf, int buf_size): return stash_exception() -cdef int64_t pyio_seek(void *opaque, int64_t offset, int whence) nogil: +cdef int64_t pyio_seek(void *opaque, int64_t offset, int whence) noexcept nogil: # Seek takes the standard flags, but also a ad-hoc one which means that # the library wants to know how large the file is. We are generally # allowed to ignore this. diff --git a/av/logging.pyx b/av/logging.pyx index 1bdb7fab7..2253560ad 100644 --- a/av/logging.pyx +++ b/av/logging.pyx @@ -208,7 +208,7 @@ cdef struct log_context: lib.AVClass *class_ const char *name -cdef const char *log_context_name(void *ptr) nogil: +cdef const char *log_context_name(void *ptr) noexcept nogil: cdef log_context *obj = ptr return obj.name @@ -229,7 +229,7 @@ cpdef log(int level, str name, str message): free(obj) -cdef void log_callback(void *ptr, int level, const char *format, lib.va_list args) nogil: +cdef void log_callback(void *ptr, int level, const char *format, lib.va_list args) noexcept nogil: cdef bint inited = lib.Py_IsInitialized() if not inited and not print_after_shutdown: diff --git a/av/video/format.pxd b/av/video/format.pxd index 372821666..923f05c44 100644 --- a/av/video/format.pxd +++ b/av/video/format.pxd @@ -24,4 +24,4 @@ cdef class VideoFormatComponent(object): cdef VideoFormat get_video_format(lib.AVPixelFormat c_format, unsigned int width, unsigned int height) -cdef lib.AVPixelFormat get_pix_fmt(const char *name) except lib.AV_PIX_FMT_NONE \ No newline at end of file +cdef lib.AVPixelFormat get_pix_fmt(const char *name) except lib.AV_PIX_FMT_NONE diff --git a/docs/development/includes.py b/docs/development/includes.py index b60629fda..8f350a81e 100644 --- a/docs/development/includes.py +++ b/docs/development/includes.py @@ -5,7 +5,7 @@ import xml.etree.ElementTree as etree -from Cython.Compiler.Main import compile_single, CompilationOptions +from Cython.Compiler.Main import CompilationOptions, Context from Cython.Compiler.TreeFragment import parse_from_strings from Cython.Compiler.Visitor import TreeVisitor from Cython.Compiler import Nodes @@ -107,9 +107,16 @@ def extract(path, **kwargs): c_string_encoding='ascii', ) - context = options.create_context() + context = Context( + options.include_path, + options.compiler_directives, + options.cplus, + options.language_level, + options=options, + ) - tree = parse_from_strings(name, open(path).read(), context, + tree = parse_from_strings( + name, open(path).read(), context, level='module_pxd' if path.endswith('.pxd') else None, **kwargs) From 297ff015da1c7d181967ee01c115219217adcf19 Mon Sep 17 00:00:00 2001 From: Mattias Wadman Date: Sat, 28 Oct 2023 19:42:15 +0200 Subject: [PATCH 126/846] Update to ffmpeg 6.0 (#1110) --- av/codec/codec.pyx | 12 ++++-------- av/codec/context.pyx | 11 ----------- av/container/core.pyx | 3 --- include/libavcodec/avcodec.pxd | 10 +--------- include/libavformat/avformat.pxd | 1 - scripts/activate.sh | 2 +- 6 files changed, 6 insertions(+), 33 deletions(-) diff --git a/av/codec/codec.pyx b/av/codec/codec.pyx index ad3198fd8..978d42775 100644 --- a/av/codec/codec.pyx +++ b/av/codec/codec.pyx @@ -52,7 +52,6 @@ Capabilities = define_enum('Capabilities', 'av.codec', ( """Codec uses get_buffer() for allocating buffers and supports custom allocators. If not set, it might not use get_buffer() at all or use operations that assume the buffer was allocated by avcodec_default_get_buffer."""), - ('TRUNCATED', lib.AV_CODEC_CAP_TRUNCATED), ('HWACCEL', 1 << 4), ('DELAY', lib.AV_CODEC_CAP_DELAY, """Encoder or decoder requires flushing with NULL input at the end in order to @@ -102,8 +101,10 @@ Capabilities = define_enum('Capabilities', 'av.codec', ( """Codec supports slice-based (or partition-based) multithreading."""), ('PARAM_CHANGE', lib.AV_CODEC_CAP_PARAM_CHANGE, """Codec supports changed parameters at any point."""), - ('AUTO_THREADS', lib.AV_CODEC_CAP_AUTO_THREADS, - """Codec supports avctx->thread_count == 0 (auto)."""), + ('AUTO_THREADS', lib.AV_CODEC_CAP_OTHER_THREADS, + """Codec supports multithreading through a method other than slice- or + frame-level multithreading. Typically this marks wrappers around + multithreading-capable external libraries."""), ('VARIABLE_FRAME_SIZE', lib.AV_CODEC_CAP_VARIABLE_FRAME_SIZE, """Audio encoder supports receiving a different number of samples in each call."""), ('AVOID_PROBING', lib.AV_CODEC_CAP_AVOID_PROBING, @@ -114,10 +115,6 @@ Capabilities = define_enum('Capabilities', 'av.codec', ( the stream. A decoder marked with this flag should only be used as last resort choice for probing."""), - ('INTRA_ONLY', lib.AV_CODEC_CAP_INTRA_ONLY, - """Codec is intra only."""), - ('LOSSLESS', lib.AV_CODEC_CAP_LOSSLESS, - """Codec is lossless."""), ('HARDWARE', lib.AV_CODEC_CAP_HARDWARE, """Codec is backed by a hardware implementation. Typically used to identify a non-hwaccel hardware decoder. For information about hwaccels, use @@ -312,7 +309,6 @@ cdef class Codec(object): draw_horiz_band = capabilities.flag_property('DRAW_HORIZ_BAND') dr1 = capabilities.flag_property('DR1') - truncated = capabilities.flag_property('TRUNCATED') hwaccel = capabilities.flag_property('HWACCEL') delay = capabilities.flag_property('DELAY') small_last_frame = capabilities.flag_property('SMALL_LAST_FRAME') diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 5c8314615..2cdf7ef5d 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -96,9 +96,6 @@ Flags = define_enum('Flags', __name__, ( """Only decode/encode grayscale."""), ('PSNR', lib.AV_CODEC_FLAG_PSNR, """error[?] variables will be set during encoding."""), - ('TRUNCATED', lib.AV_CODEC_FLAG_TRUNCATED, - """Input bitstream might be truncated at a random location - instead of only at frame boundaries."""), ('INTERLACED_DCT', lib.AV_CODEC_FLAG_INTERLACED_DCT, """Use interlaced DCT."""), ('LOW_DELAY', lib.AV_CODEC_FLAG_LOW_DELAY, @@ -122,8 +119,6 @@ Flags2 = define_enum('Flags2', __name__, ( """Skip bitstream encoding."""), ('LOCAL_HEADER', lib.AV_CODEC_FLAG2_LOCAL_HEADER, """Place global headers at every keyframe instead of in extradata."""), - ('DROP_FRAME_TIMECODE', lib.AV_CODEC_FLAG2_DROP_FRAME_TIMECODE, - """Timecode is in drop frame format. DEPRECATED!!!!"""), ('CHUNKS', lib.AV_CODEC_FLAG2_CHUNKS, """Input bitstream might be truncated at a packet boundaries instead of only at frame boundaries."""), @@ -168,10 +163,6 @@ cdef class CodecContext(object): self.ptr.thread_count = 0 self.ptr.thread_type = 2 - # Use "ass" format for subtitles (default as of FFmpeg 5.0), not the - # deprecated "ass_with_timings" formats. - self.ptr.sub_text_format = 0 - def _get_flags(self): return self.ptr.flags @@ -195,7 +186,6 @@ cdef class CodecContext(object): loop_filter = flags.flag_property('LOOP_FILTER') gray = flags.flag_property('GRAY') psnr = flags.flag_property('PSNR') - truncated = flags.flag_property('TRUNCATED') interlaced_dct = flags.flag_property('INTERLACED_DCT') low_delay = flags.flag_property('LOW_DELAY') global_header = flags.flag_property('GLOBAL_HEADER') @@ -219,7 +209,6 @@ cdef class CodecContext(object): fast = flags2.flag_property('FAST') no_output = flags2.flag_property('NO_OUTPUT') local_header = flags2.flag_property('LOCAL_HEADER') - drop_frame_timecode = flags2.flag_property('DROP_FRAME_TIMECODE') chunks = flags2.flag_property('CHUNKS') ignore_crop = flags2.flag_property('IGNORE_CROP') show_all = flags2.flag_property('SHOW_ALL') diff --git a/av/container/core.pyx b/av/container/core.pyx index 493576f16..4b629b36e 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -157,8 +157,6 @@ Flags = define_enum('Flags', __name__, ( This flag is mainly intended for testing."""), ('SORT_DTS', lib.AVFMT_FLAG_SORT_DTS, "Try to interleave outputted packets by dts (using this flag can slow demuxing down)."), - ('PRIV_OPT', lib.AVFMT_FLAG_PRIV_OPT, - "Enable use of private options by delaying codec open (this could be made default once all code is converted)."), ('FAST_SEEK', lib.AVFMT_FLAG_FAST_SEEK, "Enable fast, but inaccurate seeks for some formats."), ('SHORTEST', lib.AVFMT_FLAG_SHORTEST, @@ -329,7 +327,6 @@ cdef class Container(object): flush_packets = flags.flag_property('FLUSH_PACKETS') bit_exact = flags.flag_property('BITEXACT') sort_dts = flags.flag_property('SORT_DTS') - priv_opt = flags.flag_property('PRIV_OPT') fast_seek = flags.flag_property('FAST_SEEK') shortest = flags.flag_property('SHORTEST') auto_bsf = flags.flag_property('AUTO_BSF') diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 1e6111808..0334b18e4 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -39,7 +39,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef enum: AV_CODEC_CAP_DRAW_HORIZ_BAND AV_CODEC_CAP_DR1 - AV_CODEC_CAP_TRUNCATED # AV_CODEC_CAP_HWACCEL AV_CODEC_CAP_DELAY AV_CODEC_CAP_SMALL_LAST_FRAME @@ -51,11 +50,9 @@ cdef extern from "libavcodec/avcodec.h" nogil: AV_CODEC_CAP_FRAME_THREADS AV_CODEC_CAP_SLICE_THREADS AV_CODEC_CAP_PARAM_CHANGE - AV_CODEC_CAP_AUTO_THREADS + AV_CODEC_CAP_OTHER_THREADS AV_CODEC_CAP_VARIABLE_FRAME_SIZE AV_CODEC_CAP_AVOID_PROBING - AV_CODEC_CAP_INTRA_ONLY - AV_CODEC_CAP_LOSSLESS AV_CODEC_CAP_HARDWARE AV_CODEC_CAP_HYBRID AV_CODEC_CAP_ENCODER_REORDERED_OPAQUE @@ -76,7 +73,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: AV_CODEC_FLAG_LOOP_FILTER AV_CODEC_FLAG_GRAY AV_CODEC_FLAG_PSNR - AV_CODEC_FLAG_TRUNCATED AV_CODEC_FLAG_INTERLACED_DCT AV_CODEC_FLAG_LOW_DELAY AV_CODEC_FLAG_GLOBAL_HEADER @@ -89,7 +85,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: AV_CODEC_FLAG2_FAST AV_CODEC_FLAG2_NO_OUTPUT AV_CODEC_FLAG2_LOCAL_HEADER - AV_CODEC_FLAG2_DROP_FRAME_TIMECODE AV_CODEC_FLAG2_CHUNKS AV_CODEC_FLAG2_IGNORE_CROP AV_CODEC_FLAG2_SHOW_ALL @@ -224,9 +219,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: int frame_size int channel_layout - # Subtitles. - int sub_text_format - #: .. todo:: ``get_buffer`` is deprecated for get_buffer2 in newer versions of FFmpeg. int get_buffer(AVCodecContext *ctx, AVFrame *frame) void release_buffer(AVCodecContext *ctx, AVFrame *frame) diff --git a/include/libavformat/avformat.pxd b/include/libavformat/avformat.pxd index ed3e503f5..06029d9f9 100644 --- a/include/libavformat/avformat.pxd +++ b/include/libavformat/avformat.pxd @@ -146,7 +146,6 @@ cdef extern from "libavformat/avformat.h" nogil: AVFMT_FLAG_FLUSH_PACKETS AVFMT_FLAG_BITEXACT AVFMT_FLAG_SORT_DTS - AVFMT_FLAG_PRIV_OPT AVFMT_FLAG_FAST_SEEK AVFMT_FLAG_SHORTEST AVFMT_FLAG_AUTO_BSF diff --git a/scripts/activate.sh b/scripts/activate.sh index bbb440185..167266549 100755 --- a/scripts/activate.sh +++ b/scripts/activate.sh @@ -14,7 +14,7 @@ if [[ ! "$PYAV_LIBRARY" ]]; then if [[ "$1" ]]; then PYAV_LIBRARY="$1" else - PYAV_LIBRARY=ffmpeg-4.2 + PYAV_LIBRARY=ffmpeg-6.0 echo "No \$PYAV_LIBRARY set; defaulting to $PYAV_LIBRARY" fi fi From 1d41e370b2f046233c0b14bedbf30771e697bd49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Sun, 29 Oct 2023 21:26:20 +0100 Subject: [PATCH 127/846] Fix running doctests We use `av.datasets` so explicitly import it --- tests/test_doctests.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_doctests.py b/tests/test_doctests.py index c2144eab1..96ed0dad3 100644 --- a/tests/test_doctests.py +++ b/tests/test_doctests.py @@ -4,6 +4,7 @@ import re import av +import av.datasets def fix_doctests(suite): From 4e5d7b71f451ca334e01c1be8cf400b91f861251 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Sun, 29 Oct 2023 21:55:43 +0100 Subject: [PATCH 128/846] Explicitly drop support for FFmpeg < 5.0 Our code now depends on FFmpeg 5.0 or better, so update the test matrix. --- .github/workflows/tests.yml | 28 ++++++++++------------------ docs/overview/installation.rst | 2 +- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index dc5db3b5b..7d1b1a455 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -57,10 +57,8 @@ jobs: config: - {os: ubuntu-latest, python: 3.8, ffmpeg: "5.1", extras: true} - {os: ubuntu-latest, python: 3.8, ffmpeg: "5.0"} - - {os: ubuntu-latest, python: 3.8, ffmpeg: "4.4"} - - {os: ubuntu-latest, python: 3.8, ffmpeg: "4.3"} - - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "4.4"} - - {os: macos-latest, python: 3.8, ffmpeg: "4.4"} + - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "5.0"} + - {os: macos-latest, python: 3.8, ffmpeg: "5.0"} env: PYAV_PYTHON: python${{ matrix.config.python }} @@ -145,8 +143,6 @@ jobs: config: - {os: windows-latest, python: 3.8, ffmpeg: "5.1"} - {os: windows-latest, python: 3.8, ffmpeg: "5.0"} - - {os: windows-latest, python: 3.8, ffmpeg: "4.4"} - - {os: windows-latest, python: 3.8, ffmpeg: "4.3"} steps: - name: Checkout @@ -164,24 +160,20 @@ jobs: pillow \ python=${{ matrix.config.python }} \ setuptools - if [[ "${{ matrix.config.ffmpeg }}" == "5.1" ]]; then - curl -L -o ffmpeg.tar.gz https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.1.2-1/ffmpeg-win_amd64.tar.gz - elif [[ "${{ matrix.config.ffmpeg }}" == "5.0" ]]; then - curl -L -o ffmpeg.tar.gz https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.0.1-1/ffmpeg-win_amd64.tar.gz - elif [[ "${{ matrix.config.ffmpeg }}" == "4.4" ]]; then - curl -L -o ffmpeg.tar.gz https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/4.4.1-8/ffmpeg-win_amd64.tar.gz - else - conda install -q -n pyav ffmpeg=${{ matrix.config.ffmpeg }} - fi + if [[ "${{ matrix.config.ffmpeg }}" == "5.1" ]]; then + curl -L -o ffmpeg.tar.gz https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.1.2-1/ffmpeg-win_amd64.tar.gz + elif [[ "${{ matrix.config.ffmpeg }}" == "5.0" ]]; then + curl -L -o ffmpeg.tar.gz https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.0.1-1/ffmpeg-win_amd64.tar.gz + else + exit 1 + fi - name: Build shell: bash run: | . $CONDA/etc/profile.d/conda.sh conda activate pyav - if [[ "${{ matrix.config.ffmpeg }}" != "4.3" ]]; then - tar -xf ffmpeg.tar.gz -C $CONDA_PREFIX/Library/ - fi + tar -xf ffmpeg.tar.gz -C $CONDA_PREFIX/Library/ python setup.py build_ext --inplace --ffmpeg-dir=$CONDA_PREFIX/Library - name: Test diff --git a/docs/overview/installation.rst b/docs/overview/installation.rst index 9ac4dc078..72afe8d52 100644 --- a/docs/overview/installation.rst +++ b/docs/overview/installation.rst @@ -56,7 +56,7 @@ See the `Conda quick install `_ docs t Bring your own FFmpeg --------------------- -PyAV can also be compiled against your own build of FFmpeg ((version ``4.3`` or higher). You can force installing PyAV from source by running: +PyAV can also be compiled against your own build of FFmpeg ((version ``5.0`` or higher). You can force installing PyAV from source by running: .. code-block:: bash From 1f991fa776d2f20b6ce5db9efb700673fb7989dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Sun, 29 Oct 2023 22:54:42 +0100 Subject: [PATCH 129/846] Add FFmpeg 6.0 to test matrix --- .github/workflows/tests.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7d1b1a455..3cd077ade 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -55,7 +55,8 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-latest, python: 3.8, ffmpeg: "5.1", extras: true} + - {os: ubuntu-latest, python: 3.8, ffmpeg: "6.0", extras: true} + - {os: ubuntu-latest, python: 3.8, ffmpeg: "5.1"} - {os: ubuntu-latest, python: 3.8, ffmpeg: "5.0"} - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "5.0"} - {os: macos-latest, python: 3.8, ffmpeg: "5.0"} From d2e750fb466a49bc638a5e466c827b81a153d65e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Mon, 30 Oct 2023 11:30:47 +0100 Subject: [PATCH 130/846] Reformat Python code with black, and enforce it in CI --- examples/basics/parse.py | 2 -- examples/basics/remux.py | 1 - examples/basics/save_keyframes.py | 1 - examples/numpy/barcode.py | 1 - examples/numpy/generate_video.py | 1 - examples/numpy/generate_video_with_pts.py | 1 - scripts/test | 2 +- tests/test_audiofifo.py | 5 ----- tests/test_audioresampler.py | 3 --- tests/test_codec_context.py | 10 ---------- tests/test_decode.py | 5 ----- tests/test_deprecation.py | 2 -- tests/test_dictionary.py | 1 - tests/test_doctests.py | 4 ---- tests/test_encode.py | 3 --- tests/test_enums.py | 12 ------------ tests/test_errors.py | 2 -- tests/test_filters.py | 6 ------ tests/test_logging.py | 4 ---- tests/test_options.py | 1 - tests/test_python_io.py | 2 -- tests/test_seek.py | 2 -- tests/test_streams.py | 3 --- tests/test_subtitles.py | 2 -- tests/test_videoframe.py | 1 - 25 files changed, 1 insertion(+), 76 deletions(-) diff --git a/examples/basics/parse.py b/examples/basics/parse.py index 2d313cb3f..42a8af160 100644 --- a/examples/basics/parse.py +++ b/examples/basics/parse.py @@ -29,14 +29,12 @@ codec = av.CodecContext.create("h264", "r") while True: - chunk = fh.read(1 << 16) packets = codec.parse(chunk) print("Parsed {} packets from {} bytes:".format(len(packets), len(chunk))) for packet in packets: - print(" ", packet) frames = codec.decode(packet) diff --git a/examples/basics/remux.py b/examples/basics/remux.py index c4779f4e9..feb25cbcd 100644 --- a/examples/basics/remux.py +++ b/examples/basics/remux.py @@ -11,7 +11,6 @@ out_stream = output.add_stream(template=in_stream) for packet in input_.demux(in_stream): - print(packet) # We need to skip the "flushing" packets that `demux` generates. diff --git a/examples/basics/save_keyframes.py b/examples/basics/save_keyframes.py index 1169c153a..8d31aa621 100644 --- a/examples/basics/save_keyframes.py +++ b/examples/basics/save_keyframes.py @@ -9,7 +9,6 @@ stream.codec_context.skip_frame = "NONKEY" for frame in container.decode(stream): - print(frame) # We use `frame.pts` as `frame.index` won't make must sense with the `skip_frame`. diff --git a/examples/numpy/barcode.py b/examples/numpy/barcode.py index 64c13e198..2a3dfff0d 100644 --- a/examples/numpy/barcode.py +++ b/examples/numpy/barcode.py @@ -12,7 +12,6 @@ columns = [] for frame in container.decode(video=0): - print(frame) array = frame.to_ndarray(format="rgb24") diff --git a/examples/numpy/generate_video.py b/examples/numpy/generate_video.py index 80145d514..250a0bcc1 100644 --- a/examples/numpy/generate_video.py +++ b/examples/numpy/generate_video.py @@ -15,7 +15,6 @@ stream.pix_fmt = "yuv420p" for frame_i in range(total_frames): - img = np.empty((480, 320, 3)) img[:, :, 0] = 0.5 + 0.5 * np.sin(2 * np.pi * (0 / 3 + frame_i / total_frames)) img[:, :, 1] = 0.5 + 0.5 * np.sin(2 * np.pi * (1 / 3 + frame_i / total_frames)) diff --git a/examples/numpy/generate_video_with_pts.py b/examples/numpy/generate_video_with_pts.py index 58ca547e2..c570a07d2 100644 --- a/examples/numpy/generate_video_with_pts.py +++ b/examples/numpy/generate_video_with_pts.py @@ -43,7 +43,6 @@ block_h2 = int(0.5 * height / 4) for frame_i in range(total_frames): - # move around the color wheel (hue) nice_color = colorsys.hsv_to_rgb(frame_i / total_frames, 1.0, 1.0) nice_color = (np.array(nice_color) * 255).astype(np.uint8) diff --git a/scripts/test b/scripts/test index 0ed7eb862..806011859 100755 --- a/scripts/test +++ b/scripts/test @@ -19,7 +19,7 @@ istest() { } if istest black; then - $PYAV_PYTHON -m black av examples tests + $PYAV_PYTHON -m black --check av examples tests fi if istest flake8; then diff --git a/tests/test_audiofifo.py b/tests/test_audiofifo.py index f04995b89..30862f2bb 100644 --- a/tests/test_audiofifo.py +++ b/tests/test_audiofifo.py @@ -5,7 +5,6 @@ class TestAudioFifo(TestCase): def test_data(self): - container = av.open(fate_suite("audio-reference/chorusnoise_2ch_44kHz_s16.wav")) stream = container.streams.audio[0] @@ -31,7 +30,6 @@ def test_data(self): self.assertTrue(input_[:min_len] == output[:min_len]) def test_pts_simple(self): - fifo = av.AudioFifo() iframe = av.AudioFrame(samples=1024) @@ -61,7 +59,6 @@ def test_pts_simple(self): self.assertRaises(ValueError, fifo.write, iframe) def test_pts_complex(self): - fifo = av.AudioFifo() iframe = av.AudioFrame(samples=1024) @@ -79,7 +76,6 @@ def test_pts_complex(self): self.assertEqual(fifo.pts_per_sample, 2.0) def test_missing_sample_rate(self): - fifo = av.AudioFifo() iframe = av.AudioFrame(samples=1024) @@ -96,7 +92,6 @@ def test_missing_sample_rate(self): self.assertEqual(oframe.time_base, iframe.time_base) def test_missing_time_base(self): - fifo = av.AudioFifo() iframe = av.AudioFrame(samples=1024) diff --git a/tests/test_audioresampler.py b/tests/test_audioresampler.py index fe1907c14..9b66968c1 100644 --- a/tests/test_audioresampler.py +++ b/tests/test_audioresampler.py @@ -69,7 +69,6 @@ def test_matching_passthrough(self): self.assertEqual(len(oframes), 0) def test_pts_assertion_same_rate(self): - resampler = AudioResampler("s16", "mono") # resample one frame @@ -115,7 +114,6 @@ def test_pts_assertion_same_rate(self): self.assertEqual(len(oframes), 0) def test_pts_assertion_new_rate(self): - resampler = AudioResampler("s16", "mono", 44100) # resample one frame @@ -144,7 +142,6 @@ def test_pts_assertion_new_rate(self): self.assertEqual(oframe.samples, 16) def test_pts_missing_time_base(self): - resampler = AudioResampler("s16", "mono", 44100) # resample one frame diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index c94945a54..ee9193c81 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -120,7 +120,6 @@ def test_encoder_pix_fmt(self): self.assertEqual(ctx.pix_fmt, "yuv420p") def test_parse(self): - # This one parses into a single packet. self._assert_parse("mpeg4", fate_suite("h264/interlaced_crop.mp4")) @@ -128,7 +127,6 @@ def test_parse(self): self._assert_parse("mpeg2video", fate_suite("mpeg2/mpeg2_field_encoding.ts")) def _assert_parse(self, codec_name, path): - fh = av.open(path) packets = [] for packet in fh.demux(video=0): @@ -137,7 +135,6 @@ def _assert_parse(self, codec_name, path): full_source = b"".join(bytes(p) for p in packets) for size in 1024, 8192, 65535: - ctx = Codec(codec_name).create() packets = [] @@ -162,7 +159,6 @@ def test_encoding_tiff(self): self.image_sequence_encode("tiff") def image_sequence_encode(self, codec_name): - try: codec = Codec(codec_name, "w") except UnknownCodecError: @@ -187,7 +183,6 @@ def image_sequence_encode(self, codec_name): frame_count = 1 path_list = [] for frame in iter_frames(container, video_stream): - new_frame = frame.reformat(width, height, pix_fmt) new_packets = ctx.encode(new_frame) @@ -249,7 +244,6 @@ def test_encoding_dnxhd(self): self.video_encoding("dnxhd", options) def video_encoding(self, codec_name, options={}, codec_tag=None): - try: codec = Codec(codec_name, "w") except UnknownCodecError: @@ -280,9 +274,7 @@ def video_encoding(self, codec_name, options={}, codec_tag=None): frame_count = 0 with open(path, "wb") as f: - for frame in iter_frames(container, video_stream): - new_frame = frame.reformat(width, height, pix_fmt) # reset the picture type @@ -326,7 +318,6 @@ def test_encoding_mp2(self): self.audio_encoding("mp2") def audio_encoding(self, codec_name): - try: codec = Codec(codec_name, "w") except UnknownCodecError: @@ -361,7 +352,6 @@ def audio_encoding(self, codec_name): with open(path, "wb") as f: for frame in iter_frames(container, audio_stream): - resampled_frames = resampler.resample(frame) for resampled_frame in resampled_frames: samples += resampled_frame.samples diff --git a/tests/test_decode.py b/tests/test_decode.py index 185b7ec8e..564ea24cd 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -7,7 +7,6 @@ class TestDecode(TestCase): def test_decoded_video_frame_count(self): - container = av.open(fate_suite("h264/interlaced_crop.mp4")) video_stream = next(s for s in container.streams if s.type == "video") @@ -40,7 +39,6 @@ def test_decode_audio_corrupt(self): self.assertEqual(frame_count, 0) def test_decode_audio_sample_count(self): - container = av.open(fate_suite("audio-reference/chorusnoise_2ch_44kHz_s16.wav")) audio_stream = next(s for s in container.streams if s.type == "audio") @@ -58,7 +56,6 @@ def test_decode_audio_sample_count(self): self.assertEqual(sample_count, total_samples) def test_decoded_time_base(self): - container = av.open(fate_suite("h264/interlaced_crop.mp4")) stream = container.streams.video[0] @@ -71,7 +68,6 @@ def test_decoded_time_base(self): return def test_decoded_motion_vectors(self): - container = av.open(fate_suite("h264/interlaced_crop.mp4")) stream = container.streams.video[0] codec_context = stream.codec_context @@ -88,7 +84,6 @@ def test_decoded_motion_vectors(self): return def test_decoded_motion_vectors_no_flag(self): - container = av.open(fate_suite("h264/interlaced_crop.mp4")) stream = container.streams.video[0] diff --git a/tests/test_deprecation.py b/tests/test_deprecation.py index abdc79f8e..d728c330d 100644 --- a/tests/test_deprecation.py +++ b/tests/test_deprecation.py @@ -23,7 +23,6 @@ def foo(self, a, b): def test_renamed_attr(self): class Example(object): - new_value = "foo" old_value = deprecation.renamed_attr("new_value") @@ -35,7 +34,6 @@ def new_func(self, a, b): obj = Example() with warnings.catch_warnings(record=True) as captured: - self.assertEqual(obj.old_value, "foo") self.assertIn( "Example.old_value is deprecated", captured[0].message.args[0] diff --git a/tests/test_dictionary.py b/tests/test_dictionary.py index 4e2c4995e..a1c2f80d8 100644 --- a/tests/test_dictionary.py +++ b/tests/test_dictionary.py @@ -5,7 +5,6 @@ class TestDictionary(TestCase): def test_basics(self): - d = Dictionary() d["key"] = "value" diff --git a/tests/test_doctests.py b/tests/test_doctests.py index 96ed0dad3..1449355bd 100644 --- a/tests/test_doctests.py +++ b/tests/test_doctests.py @@ -8,9 +8,7 @@ def fix_doctests(suite): - for case in suite._tests: - # Add some more flags. case._dt_optionflags = ( (case._dt_optionflags or 0) @@ -25,14 +23,12 @@ def fix_doctests(suite): ) for example in case._dt_test.examples: - # Remove b prefix from strings. if example.want.startswith("b'"): example.want = example.want[1:] def register_doctests(mod): - if isinstance(mod, str): mod = __import__(mod, fromlist=[""]) diff --git a/tests/test_encode.py b/tests/test_encode.py index 4f942354a..a293cbf83 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -16,7 +16,6 @@ def write_rgb_rotate(output): - if not Image: raise SkipTest() @@ -29,7 +28,6 @@ def write_rgb_rotate(output): stream.pix_fmt = "yuv420p" for frame_i in range(DURATION): - frame = VideoFrame(WIDTH, HEIGHT, "rgb24") image = Image.new( "RGB", @@ -64,7 +62,6 @@ def write_rgb_rotate(output): def assert_rgb_rotate(self, input_, is_dash=False): - # Now inspect it a little. self.assertEqual(len(input_.streams), 1) if is_dash: diff --git a/tests/test_enums.py b/tests/test_enums.py index c22e659fb..d8b727dab 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -22,7 +22,6 @@ def define_foobar(self, **kwargs): ) def test_basics(self): - cls = self.define_foobar() self.assertIsInstance(cls, EnumType) @@ -36,7 +35,6 @@ def test_basics(self): self.assertNotIsInstance(foo, PickleableFooBar) def test_access(self): - cls = self.define_foobar() foo1 = cls.FOO foo2 = cls["FOO"] @@ -58,7 +56,6 @@ def test_access(self): self.assertIs(cls.get("not a foo"), None) def test_casting(self): - cls = self.define_foobar() foo = cls.FOO @@ -77,7 +74,6 @@ def test_iteration(self): self.assertEqual(list(cls), [cls.FOO, cls.BAR]) def test_equality(self): - cls = self.define_foobar() foo = cls.FOO bar = cls.BAR @@ -94,7 +90,6 @@ def test_equality(self): self.assertRaises(TypeError, lambda: foo == ()) def test_as_key(self): - cls = self.define_foobar() foo = cls.FOO @@ -104,7 +99,6 @@ def test_as_key(self): self.assertIs(d.get(1), None) def test_pickleable(self): - cls = PickleableFooBar foo = cls.FOO @@ -115,7 +109,6 @@ def test_pickleable(self): self.assertIs(foo, foo2) def test_create_unknown(self): - cls = self.define_foobar() baz = cls.get(3, create=True) @@ -123,7 +116,6 @@ def test_create_unknown(self): self.assertEqual(baz.value, 3) def test_multiple_names(self): - cls = define_enum( "FFooBBar", __name__, @@ -147,7 +139,6 @@ def test_multiple_names(self): self.assertRaises(ValueError, lambda: cls.F == "x") def test_flag_basics(self): - cls = define_enum( "FoobarAllFlags", __name__, @@ -178,7 +169,6 @@ def test_flag_basics(self): self.assertIs(x, cls.FOO) def test_multi_flags_basics(self): - cls = self.define_foobar(is_flags=True) foo = cls.FOO @@ -202,7 +192,6 @@ def test_multi_flags_basics(self): self.assertEqual(list(cls), [foo, bar]) def test_multi_flags_create_missing(self): - cls = self.define_foobar(is_flags=True) foobar = cls[3] @@ -212,7 +201,6 @@ def test_multi_flags_create_missing(self): self.assertRaises(KeyError, lambda: cls[7]) # FOO and BAR and missing flag. def test_properties(self): - Flags = self.define_foobar(is_flags=True) foobar = Flags.FOO | Flags.BAR diff --git a/tests/test_errors.py b/tests/test_errors.py index 55d969999..c7dc1729e 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -8,7 +8,6 @@ class TestErrorBasics(TestCase): def test_stringify(self): - for cls in (av.ValueError, av.FileNotFoundError, av.DecoderNotFoundError): e = cls(1, "foo") self.assertEqual(str(e), "[Errno 1] foo") @@ -34,7 +33,6 @@ def test_stringify(self): ) def test_bases(self): - self.assertTrue(issubclass(av.ValueError, ValueError)) self.assertTrue(issubclass(av.ValueError, av.FFmpegError)) diff --git a/tests/test_filters.py b/tests/test_filters.py index f73bf4cc8..6201f59ba 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -50,7 +50,6 @@ def pull_until_blocked(graph): class TestFilters(TestCase): def test_filter_descriptor(self): - f = Filter("testsrc") self.assertEqual(f.name, "testsrc") self.assertEqual(f.description, "Generate test pattern.") @@ -62,7 +61,6 @@ def test_filter_descriptor(self): self.assertEqual(f.outputs[0].type, "video") def test_dynamic_filter_descriptor(self): - f = Filter("split") self.assertFalse(f.dynamic_inputs) self.assertEqual(len(f.inputs), 1) @@ -70,7 +68,6 @@ def test_dynamic_filter_descriptor(self): self.assertEqual(len(f.outputs), 0) def test_generator_graph(self): - graph = Graph() src = graph.add("testsrc") lutrgb = graph.add( @@ -93,7 +90,6 @@ def test_generator_graph(self): frame.to_image().save(self.sandboxed("mandelbrot2.png")) def test_auto_find_sink(self): - graph = Graph() src = graph.add("testsrc") src.link_to(graph.add("buffersink")) @@ -105,7 +101,6 @@ def test_auto_find_sink(self): frame.to_image().save(self.sandboxed("mandelbrot3.png")) def test_delegate_sink(self): - graph = Graph() src = graph.add("testsrc") src.link_to(graph.add("buffersink")) @@ -117,7 +112,6 @@ def test_delegate_sink(self): frame.to_image().save(self.sandboxed("mandelbrot4.png")) def test_haldclut_graph(self): - raise SkipTest() graph = Graph() diff --git a/tests/test_logging.py b/tests/test_logging.py index 7a8e94d3d..2e35879e1 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -22,7 +22,6 @@ def test_adapt_level(self): ) def test_threaded_captures(self): - with av.logging.Capture(local=True) as logs: do_log("main") thread = threading.Thread(target=do_log, args=("thread",)) @@ -32,7 +31,6 @@ def test_threaded_captures(self): self.assertIn((av.logging.INFO, "test", "main"), logs) def test_global_captures(self): - with av.logging.Capture(local=False) as logs: do_log("main") thread = threading.Thread(target=do_log, args=("thread",)) @@ -43,7 +41,6 @@ def test_global_captures(self): self.assertIn((av.logging.INFO, "test", "thread"), logs) def test_repeats(self): - with av.logging.Capture() as logs: do_log("foo") do_log("foo") @@ -66,7 +63,6 @@ def test_repeats(self): ) def test_error(self): - log = (av.logging.ERROR, "test", "This is a test.") av.logging.log(*log) try: diff --git a/tests/test_options.py b/tests/test_options.py index cf76252d9..790780b20 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -6,7 +6,6 @@ class TestOptions(TestCase): def test_mov_options(self): - mov = ContainerFormat("mov") options = mov.descriptor.options by_name = {opt.name: opt for opt in options} diff --git a/tests/test_python_io.py b/tests/test_python_io.py index 8c341ab2d..4e7d61f59 100644 --- a/tests/test_python_io.py +++ b/tests/test_python_io.py @@ -171,7 +171,6 @@ def test_writing_to_buffer_broken_with_close(self): @run_in_sandbox def test_writing_to_custom_io_dash(self): - # Custom I/O that opens file and logs calls wrapped_custom_io = CustomIOLogger() @@ -207,7 +206,6 @@ def test_writing_to_custom_io_dash(self): assert_rgb_rotate(self, container, is_dash=True) def test_writing_to_custom_io_image2(self): - if not Image: raise SkipTest() diff --git a/tests/test_seek.py b/tests/test_seek.py index 4a753bb0e..c29b3c9d6 100644 --- a/tests/test_seek.py +++ b/tests/test_seek.py @@ -88,7 +88,6 @@ def test_seek_end(self): self.assertTrue(seek_packet_count < middle_packet_count) def test_decode_half(self): - container = av.open(fate_suite("h264/interlaced_crop.mp4")) video_stream = next(s for s in container.streams if s.type == "video") @@ -127,7 +126,6 @@ def test_decode_half(self): self.assertEqual(frame_count, total_frame_count - target_frame) def test_stream_seek(self): - container = av.open(fate_suite("h264/interlaced_crop.mp4")) video_stream = next(s for s in container.streams if s.type == "video") diff --git a/tests/test_streams.py b/tests/test_streams.py index beab831ba..b2871d43d 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -5,9 +5,7 @@ class TestStreams(TestCase): def test_stream_tuples(self): - for fate_name in ("h264/interlaced_crop.mp4",): - container = av.open(fate_suite(fate_name)) video_streams = tuple([s for s in container.streams if s.type == "video"]) @@ -17,7 +15,6 @@ def test_stream_tuples(self): self.assertEqual(audio_streams, container.streams.audio) def test_selection(self): - container = av.open(fate_suite("h264/interlaced_crop.mp4")) video = container.streams.video[0] # audio_stream = container.streams.audio[0] diff --git a/tests/test_subtitles.py b/tests/test_subtitles.py index 04981a938..04e613203 100644 --- a/tests/test_subtitles.py +++ b/tests/test_subtitles.py @@ -6,7 +6,6 @@ class TestSubtitle(TestCase): def test_movtext(self): - path = fate_suite("sub/MovText_capability_tester.mp4") subs = [] @@ -27,7 +26,6 @@ def test_movtext(self): self.assertEqual(sub.ass, "0,0,Default,,0,0,0,,- Test 1.\\N- Test 2.") def test_vobsub(self): - path = fate_suite("sub/vobsub.sub") subs = [] diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 3d354f0d6..283343bb6 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -492,7 +492,6 @@ def test_reformat_identity(self): self.assertIs(frame1, frame2) def test_reformat_colourspace(self): - # This is allowed. frame = VideoFrame(640, 480, "rgb24") frame.reformat(src_colorspace=None, dst_colorspace="smpte240") From 7f3f213c3a4f62da13213e0fc49adc9af3356177 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon-Martin=20Schr=C3=B6der?= Date: Wed, 15 Mar 2023 09:36:53 +0100 Subject: [PATCH 131/846] Add documentation Closes #1004 --- av/container/core.pyx | 1 + av/container/output.pyx | 3 +++ 2 files changed, 4 insertions(+) diff --git a/av/container/core.pyx b/av/container/core.pyx index 4b629b36e..19675d5e0 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -362,6 +362,7 @@ def open(file, mode=None, format=None, options=None, ``url`` is the url to open, ``flags`` is a combination of AVIO_FLAG_* and ``options`` is a dictionary of additional options. The callable should return a file-like object. + :rtype: Container For devices (via ``libavdevice``), pass the name of the device to ``format``, e.g.:: diff --git a/av/container/output.pyx b/av/container/output.pyx index c4a7b5a33..0107ff17e 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -53,6 +53,9 @@ cdef class OutputContainer(Container): Examples for video include ``24``, ``23.976``, and ``Fraction(30000,1001)``. Examples for audio include ``48000`` and ``44100``. + :param template: Copy codec from another :class:`~av.stream.Stream` instance. + :param dict options: Stream options. + :param \\**kwargs: Set attributes of the stream. :returns: The new :class:`~av.stream.Stream`. """ From 29ef1129a5f4fc9572dc0af2144416bab1148287 Mon Sep 17 00:00:00 2001 From: Ryan Huang Date: Fri, 21 May 2021 08:50:05 +0000 Subject: [PATCH 132/846] Add VideoFrame ndarray operations for yuv444p/yuvj444p formats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jeremy Lainé --- av/video/frame.pyx | 16 ++++++++++++++++ tests/test_videoframe.py | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/av/video/frame.pyx b/av/video/frame.pyx index ac226fa0f..972abb779 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -269,6 +269,12 @@ cdef class VideoFrame(Frame): useful_array(frame.planes[1]), useful_array(frame.planes[2]) )).reshape(-1, frame.width) + elif frame.format.name in ('yuv444p', 'yuvj444p'): + return np.hstack(( + useful_array(frame.planes[0]), + useful_array(frame.planes[1]), + useful_array(frame.planes[2]) + )).reshape(-1, frame.height, frame.width) elif frame.format.name == 'yuyv422': assert frame.width % 2 == 0 assert frame.height % 2 == 0 @@ -372,6 +378,16 @@ cdef class VideoFrame(Frame): copy_array_to_plane(flat[u_start:v_start], frame.planes[1], 1) copy_array_to_plane(flat[v_start:], frame.planes[2], 1) return frame + elif format in ('yuv444p', 'yuvj444p'): + check_ndarray(array, 'uint8', 3) + check_ndarray_shape(array, array.shape[0] == 3) + + frame = VideoFrame(array.shape[2], array.shape[1], format) + array = array.reshape(3, -1) + copy_array_to_plane(array[0], frame.planes[0], 1) + copy_array_to_plane(array[1], frame.planes[1], 1) + copy_array_to_plane(array[2], frame.planes[2], 1) + return frame elif format == 'yuyv422': check_ndarray(array, 'uint8', 3) check_ndarray_shape(array, array.shape[0] % 2 == 0) diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 283343bb6..0d36616e1 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -345,6 +345,22 @@ def test_ndarray_yuyv422(self): self.assertEqual(frame.format.name, "yuyv422") self.assertNdarraysEqual(frame.to_ndarray(), array) + def test_ndarray_yuv444p(self): + array = numpy.random.randint(0, 256, size=(3, 480, 640), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="yuv444p") + self.assertEqual(frame.width, 640) + self.assertEqual(frame.height, 480) + self.assertEqual(frame.format.name, "yuv444p") + self.assertNdarraysEqual(frame.to_ndarray(), array) + + def test_ndarray_yuvj444p(self): + array = numpy.random.randint(0, 256, size=(3, 480, 640), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="yuvj444p") + self.assertEqual(frame.width, 640) + self.assertEqual(frame.height, 480) + self.assertEqual(frame.format.name, "yuvj444p") + self.assertNdarraysEqual(frame.to_ndarray(), array) + def test_ndarray_yuyv422_align(self): array = numpy.random.randint(0, 256, size=(238, 318, 2), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuyv422") From 71cda77e56ec18b7880e3fcbec444ef643b8e66a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Mon, 30 Oct 2023 16:36:34 +0100 Subject: [PATCH 133/846] Remove inheritance from `object` class, it's not needed --- av/audio/format.pxd | 2 +- av/audio/format.pyx | 2 +- av/audio/layout.pxd | 4 ++-- av/audio/layout.pyx | 4 ++-- av/audio/resampler.pxd | 2 +- av/audio/resampler.pyx | 2 +- av/buffer.pxd | 2 +- av/buffer.pyx | 2 +- av/bytesource.pxd | 2 +- av/bytesource.pyx | 2 +- av/codec/codec.pxd | 2 +- av/codec/codec.pyx | 2 +- av/codec/context.pxd | 2 +- av/codec/context.pyx | 2 +- av/container/core.pxd | 2 +- av/container/core.pyx | 2 +- av/container/pyio.pxd | 2 +- av/container/pyio.pyx | 2 +- av/container/streams.pxd | 2 +- av/container/streams.pyx | 2 +- av/deprecation.py | 4 ++-- av/descriptor.pxd | 2 +- av/descriptor.pyx | 2 +- av/dictionary.pxd | 2 +- av/dictionary.pyx | 2 +- av/enum.pyx | 4 ++-- av/filter/context.pxd | 2 +- av/filter/context.pyx | 2 +- av/filter/filter.pxd | 2 +- av/filter/filter.pyx | 2 +- av/filter/graph.pxd | 2 +- av/filter/graph.pyx | 2 +- av/filter/link.pxd | 2 +- av/filter/link.pyx | 2 +- av/filter/pad.pxd | 2 +- av/filter/pad.pyx | 2 +- av/format.pxd | 2 +- av/format.pyx | 2 +- av/frame.pxd | 2 +- av/frame.pyx | 2 +- av/logging.pyx | 2 +- av/option.pxd | 2 +- av/option.pyx | 2 +- av/sidedata/motionvectors.pxd | 2 +- av/sidedata/motionvectors.pyx | 2 +- av/sidedata/sidedata.pxd | 2 +- av/sidedata/sidedata.pyx | 2 +- av/stream.pxd | 2 +- av/stream.pyx | 2 +- av/subtitles/subtitle.pxd | 8 ++++---- av/subtitles/subtitle.pyx | 8 ++++---- av/video/format.pxd | 4 ++-- av/video/format.pyx | 4 ++-- av/video/reformatter.pxd | 2 +- av/video/reformatter.pyx | 2 +- tests/common.py | 2 +- tests/test_deprecation.py | 4 ++-- tests/test_enums.py | 2 +- tests/test_python_io.py | 2 +- 59 files changed, 72 insertions(+), 72 deletions(-) diff --git a/av/audio/format.pxd b/av/audio/format.pxd index 5090f6886..4160aa85b 100644 --- a/av/audio/format.pxd +++ b/av/audio/format.pxd @@ -1,7 +1,7 @@ cimport libav as lib -cdef class AudioFormat(object): +cdef class AudioFormat: cdef lib.AVSampleFormat sample_fmt diff --git a/av/audio/format.pyx b/av/audio/format.pyx index f2eb72b5b..4c7cd1cdc 100644 --- a/av/audio/format.pyx +++ b/av/audio/format.pyx @@ -17,7 +17,7 @@ cdef AudioFormat get_audio_format(lib.AVSampleFormat c_format): return format -cdef class AudioFormat(object): +cdef class AudioFormat: """Descriptor of audio formats.""" diff --git a/av/audio/layout.pxd b/av/audio/layout.pxd index 60c8c953d..c46b9c741 100644 --- a/av/audio/layout.pxd +++ b/av/audio/layout.pxd @@ -1,7 +1,7 @@ from libc.stdint cimport uint64_t -cdef class AudioLayout(object): +cdef class AudioLayout: # The layout for FFMpeg; this is essentially a bitmask of channels. cdef uint64_t layout @@ -17,7 +17,7 @@ cdef class AudioLayout(object): cdef _init(self, uint64_t layout) -cdef class AudioChannel(object): +cdef class AudioChannel: # The channel for FFmpeg. cdef uint64_t channel diff --git a/av/audio/layout.pyx b/av/audio/layout.pyx index d4871553b..84801f47f 100644 --- a/av/audio/layout.pyx +++ b/av/audio/layout.pyx @@ -64,7 +64,7 @@ cdef dict channel_descriptions = { } -cdef class AudioLayout(object): +cdef class AudioLayout: def __init__(self, layout): @@ -105,7 +105,7 @@ cdef class AudioLayout(object): return out -cdef class AudioChannel(object): +cdef class AudioChannel: def __cinit__(self, AudioLayout layout, int index): self.channel = lib.av_channel_layout_extract_channel(layout.layout, index) diff --git a/av/audio/resampler.pxd b/av/audio/resampler.pxd index 4fe78b54a..d3601403d 100644 --- a/av/audio/resampler.pxd +++ b/av/audio/resampler.pxd @@ -4,7 +4,7 @@ from av.audio.layout cimport AudioLayout from av.filter.graph cimport Graph -cdef class AudioResampler(object): +cdef class AudioResampler: cdef readonly bint is_passthrough diff --git a/av/audio/resampler.pyx b/av/audio/resampler.pyx index b1c6c0aad..1214da317 100644 --- a/av/audio/resampler.pyx +++ b/av/audio/resampler.pyx @@ -7,7 +7,7 @@ import errno import av.filter -cdef class AudioResampler(object): +cdef class AudioResampler: """AudioResampler(format=None, layout=None, rate=None) diff --git a/av/buffer.pxd b/av/buffer.pxd index 199d2cc8b..cfab07ca0 100644 --- a/av/buffer.pxd +++ b/av/buffer.pxd @@ -1,5 +1,5 @@ -cdef class Buffer(object): +cdef class Buffer: cdef size_t _buffer_size(self) cdef void* _buffer_ptr(self) diff --git a/av/buffer.pyx b/av/buffer.pyx index 8176e1565..5affc81f0 100644 --- a/av/buffer.pyx +++ b/av/buffer.pyx @@ -5,7 +5,7 @@ from av import deprecation from av.bytesource cimport ByteSource, bytesource -cdef class Buffer(object): +cdef class Buffer: """A base class for PyAV objects which support the buffer protocol, such as :class:`.Packet` and :class:`.Plane`. diff --git a/av/bytesource.pxd b/av/bytesource.pxd index 68a6cca0f..050baab35 100644 --- a/av/bytesource.pxd +++ b/av/bytesource.pxd @@ -1,7 +1,7 @@ from cpython.buffer cimport Py_buffer -cdef class ByteSource(object): +cdef class ByteSource: cdef object owner diff --git a/av/bytesource.pyx b/av/bytesource.pyx index fd29bcf06..ec4138e72 100644 --- a/av/bytesource.pyx +++ b/av/bytesource.pyx @@ -6,7 +6,7 @@ from cpython.buffer cimport ( ) -cdef class ByteSource(object): +cdef class ByteSource: def __cinit__(self, owner): self.owner = owner diff --git a/av/codec/codec.pxd b/av/codec/codec.pxd index 173f0ef18..b9925df13 100644 --- a/av/codec/codec.pxd +++ b/av/codec/codec.pxd @@ -1,7 +1,7 @@ cimport libav as lib -cdef class Codec(object): +cdef class Codec: cdef const lib.AVCodec *ptr cdef const lib.AVCodecDescriptor *desc diff --git a/av/codec/codec.pyx b/av/codec/codec.pyx index 978d42775..46fafee15 100644 --- a/av/codec/codec.pyx +++ b/av/codec/codec.pyx @@ -138,7 +138,7 @@ class UnknownCodecError(ValueError): pass -cdef class Codec(object): +cdef class Codec: """Codec(name, mode='r') diff --git a/av/codec/context.pxd b/av/codec/context.pxd index 387cb7de4..6cc8bd899 100644 --- a/av/codec/context.pxd +++ b/av/codec/context.pxd @@ -7,7 +7,7 @@ from av.frame cimport Frame from av.packet cimport Packet -cdef class CodecContext(object): +cdef class CodecContext: cdef lib.AVCodecContext *ptr diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 2cdf7ef5d..98b017f66 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -135,7 +135,7 @@ Flags2 = define_enum('Flags2', __name__, ( ), is_flags=True) -cdef class CodecContext(object): +cdef class CodecContext: @staticmethod def create(codec, mode=None): diff --git a/av/container/core.pxd b/av/container/core.pxd index 198c96fa8..fb7c3b511 100644 --- a/av/container/core.pxd +++ b/av/container/core.pxd @@ -13,7 +13,7 @@ ctypedef struct timeout_info: double timeout -cdef class Container(object): +cdef class Container: cdef readonly bint writeable cdef lib.AVFormatContext *ptr diff --git a/av/container/core.pyx b/av/container/core.pyx index 19675d5e0..aae294c17 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -166,7 +166,7 @@ Flags = define_enum('Flags', __name__, ( ), is_flags=True) -cdef class Container(object): +cdef class Container: def __cinit__(self, sentinel, file_, format_name, options, container_options, stream_options, diff --git a/av/container/pyio.pxd b/av/container/pyio.pxd index 0faeea4f1..e93a11dc8 100644 --- a/av/container/pyio.pxd +++ b/av/container/pyio.pxd @@ -13,7 +13,7 @@ cdef void pyio_close_gil(lib.AVIOContext *pb) cdef void pyio_close_custom_gil(lib.AVIOContext *pb) -cdef class PyIOFile(object): +cdef class PyIOFile: # File-like source. cdef readonly object file diff --git a/av/container/pyio.pyx b/av/container/pyio.pyx index 07224cd91..ed79b44f8 100644 --- a/av/container/pyio.pyx +++ b/av/container/pyio.pyx @@ -7,7 +7,7 @@ from av.error cimport stash_exception ctypedef int64_t (*seek_func_t)(void *opaque, int64_t offset, int whence) noexcept nogil -cdef class PyIOFile(object): +cdef class PyIOFile: def __cinit__(self, file, buffer_size, writeable=None): diff --git a/av/container/streams.pxd b/av/container/streams.pxd index 2ae69d84b..bf217d7c6 100644 --- a/av/container/streams.pxd +++ b/av/container/streams.pxd @@ -1,7 +1,7 @@ from av.stream cimport Stream -cdef class StreamContainer(object): +cdef class StreamContainer: cdef list _streams diff --git a/av/container/streams.pyx b/av/container/streams.pyx index eb85d9ff3..8de28a13e 100644 --- a/av/container/streams.pyx +++ b/av/container/streams.pyx @@ -11,7 +11,7 @@ def _flatten(input_): yield x -cdef class StreamContainer(object): +cdef class StreamContainer: """ diff --git a/av/deprecation.py b/av/deprecation.py index 1e0cbb317..f36d2fe6f 100644 --- a/av/deprecation.py +++ b/av/deprecation.py @@ -21,7 +21,7 @@ class MethodDeprecationWarning(AVDeprecationWarning): warnings.filterwarnings("default", "", AVDeprecationWarning) -class renamed_attr(object): +class renamed_attr: """Proxy for renamed attributes (or methods) on classes. Getting and setting values will be redirected to the provided name, @@ -68,7 +68,7 @@ def __set__(self, instance, value): setattr(instance, self.new_name, value) -class method(object): +class method: def __init__(self, func): functools.update_wrapper(self, func, ("__name__", "__doc__")) self.func = func diff --git a/av/descriptor.pxd b/av/descriptor.pxd index 98b039c5d..404f646af 100644 --- a/av/descriptor.pxd +++ b/av/descriptor.pxd @@ -1,7 +1,7 @@ cimport libav as lib -cdef class Descriptor(object): +cdef class Descriptor: # These are present as: # - AVCodecContext.av_class (same as avcodec_get_class()) diff --git a/av/descriptor.pyx b/av/descriptor.pyx index d945b0ac6..8debfa35e 100644 --- a/av/descriptor.pyx +++ b/av/descriptor.pyx @@ -13,7 +13,7 @@ cdef Descriptor wrap_avclass(const lib.AVClass *ptr): return obj -cdef class Descriptor(object): +cdef class Descriptor: def __cinit__(self, sentinel): if sentinel is not _cinit_sentinel: diff --git a/av/dictionary.pxd b/av/dictionary.pxd index 84cb24068..1c59df448 100644 --- a/av/dictionary.pxd +++ b/av/dictionary.pxd @@ -1,7 +1,7 @@ cimport libav as lib -cdef class _Dictionary(object): +cdef class _Dictionary: cdef lib.AVDictionary *ptr diff --git a/av/dictionary.pyx b/av/dictionary.pyx index d88ccebcd..3ebc09b89 100644 --- a/av/dictionary.pyx +++ b/av/dictionary.pyx @@ -3,7 +3,7 @@ from collections.abc import MutableMapping from av.error cimport err_check -cdef class _Dictionary(object): +cdef class _Dictionary: def __cinit__(self, *args, **kwargs): for arg in args: diff --git a/av/enum.pyx b/av/enum.pyx index 85f1b0748..19a5b6d46 100644 --- a/av/enum.pyx +++ b/av/enum.pyx @@ -135,7 +135,7 @@ def _unpickle(mod_name, cls_name, item_name): copyreg.constructor(_unpickle) -cdef class EnumItem(object): +cdef class EnumItem: """ Enumerations are when an attribute may only take on a single value at once, and @@ -322,7 +322,7 @@ cdef class EnumFlag(EnumItem): return bool(self.value) -cdef class EnumProperty(object): +cdef class EnumProperty: cdef object enum cdef object fget diff --git a/av/filter/context.pxd b/av/filter/context.pxd index 3c69185b2..18954fbdd 100644 --- a/av/filter/context.pxd +++ b/av/filter/context.pxd @@ -4,7 +4,7 @@ from av.filter.filter cimport Filter from av.filter.graph cimport Graph -cdef class FilterContext(object): +cdef class FilterContext: cdef lib.AVFilterContext *ptr cdef readonly Graph graph diff --git a/av/filter/context.pyx b/av/filter/context.pyx index 4b7eaed08..c97718f32 100644 --- a/av/filter/context.pyx +++ b/av/filter/context.pyx @@ -21,7 +21,7 @@ cdef FilterContext wrap_filter_context(Graph graph, Filter filter, lib.AVFilterC return self -cdef class FilterContext(object): +cdef class FilterContext: def __cinit__(self, sentinel): if sentinel is not _cinit_sentinel: diff --git a/av/filter/filter.pxd b/av/filter/filter.pxd index e3d937e7c..27501ae57 100644 --- a/av/filter/filter.pxd +++ b/av/filter/filter.pxd @@ -3,7 +3,7 @@ cimport libav as lib from av.descriptor cimport Descriptor -cdef class Filter(object): +cdef class Filter: cdef const lib.AVFilter *ptr diff --git a/av/filter/filter.pyx b/av/filter/filter.pyx index f5b5f1eee..57090a47d 100644 --- a/av/filter/filter.pyx +++ b/av/filter/filter.pyx @@ -21,7 +21,7 @@ cpdef enum FilterFlags: SUPPORT_TIMELINE_INTERNAL = lib.AVFILTER_FLAG_SUPPORT_TIMELINE_INTERNAL -cdef class Filter(object): +cdef class Filter: def __cinit__(self, name): if name is _cinit_sentinel: diff --git a/av/filter/graph.pxd b/av/filter/graph.pxd index e01536527..c9226749c 100644 --- a/av/filter/graph.pxd +++ b/av/filter/graph.pxd @@ -3,7 +3,7 @@ cimport libav as lib from av.filter.context cimport FilterContext -cdef class Graph(object): +cdef class Graph: cdef lib.AVFilterGraph *ptr diff --git a/av/filter/graph.pyx b/av/filter/graph.pyx index bcb49f788..bf71fac9e 100644 --- a/av/filter/graph.pyx +++ b/av/filter/graph.pyx @@ -11,7 +11,7 @@ from av.video.format cimport VideoFormat from av.video.frame cimport VideoFrame -cdef class Graph(object): +cdef class Graph: def __cinit__(self): diff --git a/av/filter/link.pxd b/av/filter/link.pxd index 699838c38..a6a4b1c09 100644 --- a/av/filter/link.pxd +++ b/av/filter/link.pxd @@ -4,7 +4,7 @@ from av.filter.graph cimport Graph from av.filter.pad cimport FilterContextPad -cdef class FilterLink(object): +cdef class FilterLink: cdef readonly Graph graph cdef lib.AVFilterLink *ptr diff --git a/av/filter/link.pyx b/av/filter/link.pyx index 62e6ff8bc..c99e27f3b 100644 --- a/av/filter/link.pyx +++ b/av/filter/link.pyx @@ -6,7 +6,7 @@ from av.filter.graph cimport Graph cdef _cinit_sentinel = object() -cdef class FilterLink(object): +cdef class FilterLink: def __cinit__(self, sentinel): if sentinel is not _cinit_sentinel: diff --git a/av/filter/pad.pxd b/av/filter/pad.pxd index 088c0b0c0..15ac950fc 100644 --- a/av/filter/pad.pxd +++ b/av/filter/pad.pxd @@ -5,7 +5,7 @@ from av.filter.filter cimport Filter from av.filter.link cimport FilterLink -cdef class FilterPad(object): +cdef class FilterPad: cdef readonly Filter filter cdef readonly FilterContext context diff --git a/av/filter/pad.pyx b/av/filter/pad.pyx index 64ec9a6b1..482b2fc36 100644 --- a/av/filter/pad.pyx +++ b/av/filter/pad.pyx @@ -4,7 +4,7 @@ from av.filter.link cimport wrap_filter_link cdef object _cinit_sentinel = object() -cdef class FilterPad(object): +cdef class FilterPad: def __cinit__(self, sentinel): if sentinel is not _cinit_sentinel: diff --git a/av/format.pxd b/av/format.pxd index 8165daa4e..31cac50aa 100644 --- a/av/format.pxd +++ b/av/format.pxd @@ -1,7 +1,7 @@ cimport libav as lib -cdef class ContainerFormat(object): +cdef class ContainerFormat: cdef readonly str name diff --git a/av/format.pyx b/av/format.pyx index 05a5402a8..7afe4416f 100644 --- a/av/format.pyx +++ b/av/format.pyx @@ -59,7 +59,7 @@ Flags = define_enum('Flags', __name__, ( ), is_flags=True) -cdef class ContainerFormat(object): +cdef class ContainerFormat: """Descriptor of a container format. diff --git a/av/frame.pxd b/av/frame.pxd index e0d5b4280..34c302536 100644 --- a/av/frame.pxd +++ b/av/frame.pxd @@ -4,7 +4,7 @@ from av.packet cimport Packet from av.sidedata.sidedata cimport _SideDataContainer -cdef class Frame(object): +cdef class Frame: cdef lib.AVFrame *ptr diff --git a/av/frame.pyx b/av/frame.pyx index 3717ddc81..cab930334 100644 --- a/av/frame.pyx +++ b/av/frame.pyx @@ -5,7 +5,7 @@ from fractions import Fraction from av.sidedata.sidedata import SideDataContainer -cdef class Frame(object): +cdef class Frame: """ Base class for audio and video frames. diff --git a/av/logging.pyx b/av/logging.pyx index 2253560ad..4330da08a 100644 --- a/av/logging.pyx +++ b/av/logging.pyx @@ -168,7 +168,7 @@ cpdef get_last_error(): cdef global_captures = [] cdef thread_captures = {} -cdef class Capture(object): +cdef class Capture: """A context manager for capturing logs. diff --git a/av/option.pxd b/av/option.pxd index e455bff03..9087b811c 100644 --- a/av/option.pxd +++ b/av/option.pxd @@ -1,7 +1,7 @@ cimport libav as lib -cdef class BaseOption(object): +cdef class BaseOption: cdef const lib.AVOption *ptr diff --git a/av/option.pyx b/av/option.pyx index 24f945bf2..731a6d508 100644 --- a/av/option.pyx +++ b/av/option.pyx @@ -59,7 +59,7 @@ OptionFlags = define_enum('OptionFlags', __name__, ( ('FILTERING_PARAM', lib.AV_OPT_FLAG_FILTERING_PARAM), ), is_flags=True) -cdef class BaseOption(object): +cdef class BaseOption: def __cinit__(self, sentinel): if sentinel is not _cinit_sentinel: diff --git a/av/sidedata/motionvectors.pxd b/av/sidedata/motionvectors.pxd index 3b7f88bc1..993c5e283 100644 --- a/av/sidedata/motionvectors.pxd +++ b/av/sidedata/motionvectors.pxd @@ -10,7 +10,7 @@ cdef class _MotionVectors(SideData): cdef int _len -cdef class MotionVector(object): +cdef class MotionVector: cdef _MotionVectors parent cdef lib.AVMotionVector *ptr diff --git a/av/sidedata/motionvectors.pyx b/av/sidedata/motionvectors.pyx index 35d0e2f33..f1e12d56f 100644 --- a/av/sidedata/motionvectors.pyx +++ b/av/sidedata/motionvectors.pyx @@ -53,7 +53,7 @@ class MotionVectors(_MotionVectors, Sequence): pass -cdef class MotionVector(object): +cdef class MotionVector: def __init__(self, sentinel, _MotionVectors parent, int index): if sentinel is not _cinit_bypass_sentinel: diff --git a/av/sidedata/sidedata.pxd b/av/sidedata/sidedata.pxd index f30d8fef7..ac58f1477 100644 --- a/av/sidedata/sidedata.pxd +++ b/av/sidedata/sidedata.pxd @@ -15,7 +15,7 @@ cdef class SideData(Buffer): cdef SideData wrap_side_data(Frame frame, int index) -cdef class _SideDataContainer(object): +cdef class _SideDataContainer: cdef Frame frame diff --git a/av/sidedata/sidedata.pyx b/av/sidedata/sidedata.pyx index ec7de5997..17bb869cc 100644 --- a/av/sidedata/sidedata.pyx +++ b/av/sidedata/sidedata.pyx @@ -70,7 +70,7 @@ cdef class SideData(Buffer): return Type.get(self.ptr.type) or self.ptr.type -cdef class _SideDataContainer(object): +cdef class _SideDataContainer: def __init__(self, Frame frame): diff --git a/av/stream.pxd b/av/stream.pxd index 5ad3b965e..0edd9c83b 100644 --- a/av/stream.pxd +++ b/av/stream.pxd @@ -7,7 +7,7 @@ from av.frame cimport Frame from av.packet cimport Packet -cdef class Stream(object): +cdef class Stream: cdef lib.AVStream *ptr # Stream attributes. diff --git a/av/stream.pyx b/av/stream.pyx index 971eaded1..56127ccb2 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -53,7 +53,7 @@ cdef Stream wrap_stream(Container container, lib.AVStream *c_stream, CodecContex return py_stream -cdef class Stream(object): +cdef class Stream: """ A single stream of audio, video or subtitles within a :class:`.Container`. diff --git a/av/subtitles/subtitle.pxd b/av/subtitles/subtitle.pxd index ae7bb44b9..e9003ab9b 100644 --- a/av/subtitles/subtitle.pxd +++ b/av/subtitles/subtitle.pxd @@ -3,19 +3,19 @@ cimport libav as lib from av.packet cimport Packet -cdef class SubtitleProxy(object): +cdef class SubtitleProxy: cdef lib.AVSubtitle struct -cdef class SubtitleSet(object): +cdef class SubtitleSet: cdef readonly Packet packet cdef SubtitleProxy proxy cdef readonly tuple rects -cdef class Subtitle(object): +cdef class Subtitle: cdef SubtitleProxy proxy cdef lib.AVSubtitleRect *ptr @@ -31,7 +31,7 @@ cdef class BitmapSubtitle(Subtitle): cdef readonly planes -cdef class BitmapSubtitlePlane(object): +cdef class BitmapSubtitlePlane: cdef readonly BitmapSubtitle subtitle cdef readonly int index diff --git a/av/subtitles/subtitle.pyx b/av/subtitles/subtitle.pyx index 2f22bb0be..1f0e4319a 100644 --- a/av/subtitles/subtitle.pyx +++ b/av/subtitles/subtitle.pyx @@ -1,12 +1,12 @@ from cpython cimport PyBuffer_FillInfo -cdef class SubtitleProxy(object): +cdef class SubtitleProxy: def __dealloc__(self): lib.avsubtitle_free(&self.struct) -cdef class SubtitleSet(object): +cdef class SubtitleSet: def __cinit__(self, SubtitleProxy proxy): self.proxy = proxy @@ -63,7 +63,7 @@ cdef Subtitle build_subtitle(SubtitleSet subtitle, int index): raise ValueError('unknown subtitle type %r' % ptr.type) -cdef class Subtitle(object): +cdef class Subtitle: def __cinit__(self, SubtitleSet subtitle, int index): if index < 0 or index >= subtitle.proxy.struct.num_rects: @@ -131,7 +131,7 @@ cdef class BitmapSubtitle(Subtitle): return self.planes[i] -cdef class BitmapSubtitlePlane(object): +cdef class BitmapSubtitlePlane: def __cinit__(self, BitmapSubtitle subtitle, int index): diff --git a/av/video/format.pxd b/av/video/format.pxd index 923f05c44..a2efa9d1d 100644 --- a/av/video/format.pxd +++ b/av/video/format.pxd @@ -1,7 +1,7 @@ cimport libav as lib -cdef class VideoFormat(object): +cdef class VideoFormat: cdef lib.AVPixelFormat pix_fmt cdef const lib.AVPixFmtDescriptor *ptr @@ -15,7 +15,7 @@ cdef class VideoFormat(object): cpdef chroma_height(self, int luma_height=?) -cdef class VideoFormatComponent(object): +cdef class VideoFormatComponent: cdef VideoFormat format cdef readonly unsigned int index diff --git a/av/video/format.pyx b/av/video/format.pyx index b96658272..602ec5275 100644 --- a/av/video/format.pyx +++ b/av/video/format.pyx @@ -19,7 +19,7 @@ cdef lib.AVPixelFormat get_pix_fmt(const char *name) except lib.AV_PIX_FMT_NONE: return pix_fmt -cdef class VideoFormat(object): +cdef class VideoFormat: """ >>> format = VideoFormat('rgb24') @@ -118,7 +118,7 @@ cdef class VideoFormat(object): return -((-luma_height) >> self.ptr.log2_chroma_h) if luma_height else 0 -cdef class VideoFormatComponent(object): +cdef class VideoFormatComponent: def __cinit__(self, VideoFormat format, size_t index): self.format = format diff --git a/av/video/reformatter.pxd b/av/video/reformatter.pxd index 25135c27a..ee7467898 100644 --- a/av/video/reformatter.pxd +++ b/av/video/reformatter.pxd @@ -3,7 +3,7 @@ cimport libav as lib from av.video.frame cimport VideoFrame -cdef class VideoReformatter(object): +cdef class VideoReformatter: cdef lib.SwsContext *ptr diff --git a/av/video/reformatter.pyx b/av/video/reformatter.pyx index 3ad995fb9..1d3f08065 100644 --- a/av/video/reformatter.pyx +++ b/av/video/reformatter.pyx @@ -42,7 +42,7 @@ Colorspace = define_enum('Colorspace', __name__, ( )) -cdef class VideoReformatter(object): +cdef class VideoReformatter: """An object for reformatting size and pixel format of :class:`.VideoFrame`. diff --git a/tests/common.py b/tests/common.py index a49b7bec2..8431ba911 100644 --- a/tests/common.py +++ b/tests/common.py @@ -86,7 +86,7 @@ def _inner(self, *args, **kwargs): return _inner -class MethodLogger(object): +class MethodLogger: def __init__(self, obj): self._obj = obj self._log = [] diff --git a/tests/test_deprecation.py b/tests/test_deprecation.py index d728c330d..f8857ab73 100644 --- a/tests/test_deprecation.py +++ b/tests/test_deprecation.py @@ -7,7 +7,7 @@ class TestDeprecations(TestCase): def test_method(self): - class Example(object): + class Example: def __init__(self, x=100): self.x = x @@ -22,7 +22,7 @@ def foo(self, a, b): self.assertIn("Example.foo is deprecated", captured[0].message.args[0]) def test_renamed_attr(self): - class Example(object): + class Example: new_value = "foo" old_value = deprecation.renamed_attr("new_value") diff --git a/tests/test_enums.py b/tests/test_enums.py index d8b727dab..bc8385f5e 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -204,7 +204,7 @@ def test_properties(self): Flags = self.define_foobar(is_flags=True) foobar = Flags.FOO | Flags.BAR - class Class(object): + class Class: def __init__(self, value): self.value = Flags[value].value diff --git a/tests/test_python_io.py b/tests/test_python_io.py index 4e7d61f59..84e94d7a3 100644 --- a/tests/test_python_io.py +++ b/tests/test_python_io.py @@ -72,7 +72,7 @@ def seekable(self): CUSTOM_IO_PROTOCOL = "pyavtest://" -class CustomIOLogger(object): +class CustomIOLogger: """Log calls to open a file as well as method calls on the files""" def __init__(self): From 1a8b2a8a524c96875b28577e648ea6fbecd69a2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Mon, 30 Oct 2023 17:02:42 +0100 Subject: [PATCH 134/846] Prune some useless imports from Cython code --- av/audio/codeccontext.pyx | 1 - av/audio/fifo.pyx | 2 -- av/audio/frame.pyx | 1 - av/audio/plane.pyx | 2 -- av/codec/codec.pyx | 2 +- av/codec/context.pyx | 2 +- av/container/core.pyx | 5 ----- av/container/input.pyx | 2 +- av/container/output.pyx | 1 - av/enum.pyx | 2 -- av/filter/context.pyx | 6 ++---- av/frame.pyx | 2 -- av/logging.pyx | 2 +- av/stream.pxd | 1 - av/stream.pyx | 6 +----- av/subtitles/codeccontext.pyx | 1 - av/utils.pxd | 2 +- av/utils.pyx | 2 +- av/video/codeccontext.pyx | 1 - av/video/frame.pxd | 2 +- av/video/frame.pyx | 2 +- include/libavcodec/avcodec.pxd | 7 +------ include/libavutil/motion_vector.pxd | 7 +------ include/libswresample/swresample.pxd | 1 + 24 files changed, 14 insertions(+), 48 deletions(-) diff --git a/av/audio/codeccontext.pyx b/av/audio/codeccontext.pyx index c81a49dd8..8446fbcd0 100644 --- a/av/audio/codeccontext.pyx +++ b/av/audio/codeccontext.pyx @@ -3,7 +3,6 @@ cimport libav as lib from av.audio.format cimport AudioFormat, get_audio_format from av.audio.frame cimport AudioFrame, alloc_audio_frame from av.audio.layout cimport AudioLayout, get_audio_layout -from av.error cimport err_check from av.frame cimport Frame from av.packet cimport Packet diff --git a/av/audio/fifo.pyx b/av/audio/fifo.pyx index 6d1d17bd7..9b95d5770 100644 --- a/av/audio/fifo.pyx +++ b/av/audio/fifo.pyx @@ -1,6 +1,4 @@ -from av.audio.format cimport get_audio_format from av.audio.frame cimport alloc_audio_frame -from av.audio.layout cimport get_audio_layout from av.error cimport err_check diff --git a/av/audio/frame.pyx b/av/audio/frame.pyx index 97de3cc53..b97d5e043 100644 --- a/av/audio/frame.pyx +++ b/av/audio/frame.pyx @@ -56,7 +56,6 @@ cdef class AudioFrame(Frame): # Audio filters need AVFrame.channels to match number of channels from layout. self.ptr.channels = self.layout.nb_channels - cdef size_t buffer_size if self.layout.channels and nb_samples: # Cleanup the old buffer. diff --git a/av/audio/plane.pyx b/av/audio/plane.pyx index 50fe0aa59..92c508cbd 100644 --- a/av/audio/plane.pyx +++ b/av/audio/plane.pyx @@ -1,5 +1,3 @@ -cimport libav as lib - from av.audio.frame cimport AudioFrame diff --git a/av/codec/codec.pyx b/av/codec/codec.pyx index 46fafee15..a7002acc2 100644 --- a/av/codec/codec.pyx +++ b/av/codec/codec.pyx @@ -1,7 +1,7 @@ from av.audio.format cimport get_audio_format from av.descriptor cimport wrap_avclass from av.enum cimport define_enum -from av.utils cimport avrational_to_fraction, flag_in_bitfield +from av.utils cimport avrational_to_fraction from av.video.format cimport get_video_format diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 98b017f66..bc8b35d57 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -1,7 +1,7 @@ import warnings from libc.errno cimport EAGAIN -from libc.stdint cimport int64_t, uint8_t +from libc.stdint cimport uint8_t from libc.string cimport memcpy cimport libav as lib diff --git a/av/container/core.pyx b/av/container/core.pyx index aae294c17..5b1711a53 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -1,6 +1,5 @@ from cython.operator cimport dereference from libc.stdint cimport int64_t -from libc.stdlib cimport free, malloc import os import time @@ -20,9 +19,6 @@ from av.dictionary import Dictionary from av.logging import Capture as LogCapture -ctypedef int64_t (*seek_func_t)(void *opaque, int64_t offset, int whence) noexcept nogil - - cdef object _cinit_sentinel = object() @@ -207,7 +203,6 @@ cdef class Container: cdef bytes name_obj = os.fsencode(self.name) cdef char *name = name_obj - cdef seek_func_t seek_func = NULL cdef lib.AVOutputFormat *ofmt if self.writeable: diff --git a/av/container/input.pyx b/av/container/input.pyx index e508f16f4..80cd8f783 100644 --- a/av/container/input.pyx +++ b/av/container/input.pyx @@ -79,7 +79,7 @@ cdef class InputContainer(Container): codec_context.pkt_timebase = stream.time_base py_codec_context = wrap_codec_context(codec_context, codec) else: - # no decoder is available + # no decoder is available py_codec_context = None self.streams.add_stream(wrap_stream(self, stream, py_codec_context)) diff --git a/av/container/output.pyx b/av/container/output.pyx index 0107ff17e..788b3214d 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -1,4 +1,3 @@ -from fractions import Fraction import logging import os diff --git a/av/enum.pyx b/av/enum.pyx index 19a5b6d46..522948bbb 100644 --- a/av/enum.pyx +++ b/av/enum.pyx @@ -9,9 +9,7 @@ integers for names and values respectively. """ -from collections import OrderedDict import copyreg -import sys cdef sentinel = object() diff --git a/av/filter/context.pyx b/av/filter/context.pyx index c97718f32..4505c7cd3 100644 --- a/av/filter/context.pyx +++ b/av/filter/context.pyx @@ -1,13 +1,11 @@ -from libc.string cimport memcpy - -from av.audio.frame cimport AudioFrame, alloc_audio_frame +from av.audio.frame cimport alloc_audio_frame from av.dictionary cimport _Dictionary from av.dictionary import Dictionary from av.error cimport err_check from av.filter.pad cimport alloc_filter_pads from av.frame cimport Frame from av.utils cimport avrational_to_fraction -from av.video.frame cimport VideoFrame, alloc_video_frame +from av.video.frame cimport alloc_video_frame cdef object _cinit_sentinel = object() diff --git a/av/frame.pyx b/av/frame.pyx index cab930334..b98624cc5 100644 --- a/av/frame.pyx +++ b/av/frame.pyx @@ -1,7 +1,5 @@ from av.utils cimport avrational_to_fraction, to_avrational -from fractions import Fraction - from av.sidedata.sidedata import SideDataContainer diff --git a/av/logging.pyx b/av/logging.pyx index 4330da08a..131b9a69f 100644 --- a/av/logging.pyx +++ b/av/logging.pyx @@ -32,7 +32,7 @@ API Reference from __future__ import absolute_import -from libc.stdio cimport fprintf, printf, stderr +from libc.stdio cimport fprintf, stderr from libc.stdlib cimport free, malloc cimport libav as lib diff --git a/av/stream.pxd b/av/stream.pxd index 0edd9c83b..c847f641e 100644 --- a/av/stream.pxd +++ b/av/stream.pxd @@ -1,4 +1,3 @@ -from libc.stdint cimport int64_t cimport libav as lib from av.codec.context cimport CodecContext diff --git a/av/stream.pyx b/av/stream.pyx index 56127ccb2..f9b6d7ec5 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -1,11 +1,7 @@ import warnings -from cpython cimport PyWeakref_NewRef -from libc.stdint cimport int64_t, uint8_t -from libc.string cimport memcpy cimport libav as lib -from av.codec.context cimport wrap_codec_context from av.error cimport err_check from av.packet cimport Packet from av.utils cimport ( @@ -100,7 +96,7 @@ cdef class Stream: def __getattr__(self, name): # Deprecate framerate pass-through as it is not always set. - # See: https://github.com/PyAV-Org/PyAV/issues/1005 + # See: https://github.com/PyAV-Org/PyAV/issues/1005 if self.ptr.codecpar.codec_type == lib.AVMEDIA_TYPE_VIDEO and name in ("framerate", "rate"): warnings.warn( "VideoStream.%s is deprecated as it is not always set; please use VideoStream.average_rate." % name, diff --git a/av/subtitles/codeccontext.pyx b/av/subtitles/codeccontext.pyx index a120fc3a5..c3f433abe 100644 --- a/av/subtitles/codeccontext.pyx +++ b/av/subtitles/codeccontext.pyx @@ -1,7 +1,6 @@ cimport libav as lib from av.error cimport err_check -from av.frame cimport Frame from av.packet cimport Packet from av.subtitles.subtitle cimport SubtitleProxy, SubtitleSet diff --git a/av/utils.pxd b/av/utils.pxd index 0c943de81..bc5d56927 100644 --- a/av/utils.pxd +++ b/av/utils.pxd @@ -1,4 +1,4 @@ -from libc.stdint cimport int64_t, uint8_t, uint64_t +from libc.stdint cimport uint64_t cimport libav as lib diff --git a/av/utils.pyx b/av/utils.pyx index 1894ce7ab..1b25a38fe 100644 --- a/av/utils.pyx +++ b/av/utils.pyx @@ -1,4 +1,4 @@ -from libc.stdint cimport int64_t, uint8_t, uint64_t +from libc.stdint cimport uint64_t from fractions import Fraction diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.pyx index 8dac3b3fe..33efc5d54 100644 --- a/av/video/codeccontext.pyx +++ b/av/video/codeccontext.pyx @@ -2,7 +2,6 @@ from libc.stdint cimport int64_t cimport libav as lib from av.codec.context cimport CodecContext -from av.error cimport err_check from av.frame cimport Frame from av.packet cimport Packet from av.utils cimport avrational_to_fraction, to_avrational diff --git a/av/video/frame.pxd b/av/video/frame.pxd index a08da1ecc..709775e55 100644 --- a/av/video/frame.pxd +++ b/av/video/frame.pxd @@ -1,4 +1,4 @@ -from libc.stdint cimport int16_t, int32_t, uint8_t, uint16_t, uint64_t +from libc.stdint cimport uint8_t cimport libav as lib from av.frame cimport Frame diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 972abb779..971b88ccc 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -5,7 +5,7 @@ from libc.stdint cimport uint8_t from av.enum cimport define_enum from av.error cimport err_check from av.utils cimport check_ndarray, check_ndarray_shape -from av.video.format cimport VideoFormat, get_pix_fmt, get_video_format +from av.video.format cimport get_pix_fmt, get_video_format from av.video.plane cimport VideoPlane diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 0334b18e4..11d968b1a 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -1,9 +1,4 @@ -from libc.stdint cimport ( - uint8_t, int8_t, - uint16_t, int16_t, - uint32_t, int32_t, - uint64_t, int64_t -) +from libc.stdint cimport int8_t, int64_t, uint16_t, uint32_t cdef extern from "libavcodec/avcodec.h" nogil: diff --git a/include/libavutil/motion_vector.pxd b/include/libavutil/motion_vector.pxd index bcf61c0d1..457d7149e 100644 --- a/include/libavutil/motion_vector.pxd +++ b/include/libavutil/motion_vector.pxd @@ -1,9 +1,4 @@ -from libc.stdint cimport ( - uint8_t, int8_t, - uint16_t, int16_t, - uint32_t, int32_t, - uint64_t, int64_t -) +from libc.stdint cimport int16_t, int32_t, uint8_t, uint16_t, uint64_t cdef extern from "libavutil/motion_vector.h" nogil: diff --git a/include/libswresample/swresample.pxd b/include/libswresample/swresample.pxd index 703310139..d76b777a3 100644 --- a/include/libswresample/swresample.pxd +++ b/include/libswresample/swresample.pxd @@ -1,5 +1,6 @@ from libc.stdint cimport int64_t, uint8_t + cdef extern from "libswresample/swresample.h" nogil: cdef int swresample_version() From 24bcd1fdbbba11f476eb6da3c3fe12ca63ae806f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Mon, 30 Oct 2023 17:28:13 +0100 Subject: [PATCH 135/846] Remove backports of `unittest` matchers This is unnecessary since we only support Python 3. --- tests/common.py | 40 ---------------------------------------- 1 file changed, 40 deletions(-) diff --git a/tests/common.py b/tests/common.py index 8431ba911..5b12ca0d9 100644 --- a/tests/common.py +++ b/tests/common.py @@ -3,7 +3,6 @@ import errno import functools import os -import sys import types from av.datasets import fate as fate_suite @@ -159,42 +158,3 @@ def assertImagesAlmostEqual(self, a, b, epsilon=0.1, *args): self.fail( "images differed by %s at index %d; %s %s" % (diff, i, ax, bx) ) - - # Add some of the unittest methods that we love from 2.7. - if sys.version_info < (2, 7): - - def assertIs(self, a, b, msg=None): - if a is not b: - self.fail( - msg - or "%r at 0x%x is not %r at 0x%x; %r is not %r" - % (type(a), id(a), type(b), id(b), a, b) - ) - - def assertIsNot(self, a, b, msg=None): - if a is b: - self.fail(msg or "both are %r at 0x%x; %r" % (type(a), id(a), a)) - - def assertIsNone(self, x, msg=None): - if x is not None: - self.fail(msg or "is not None; %r" % x) - - def assertIsNotNone(self, x, msg=None): - if x is None: - self.fail(msg or "is None; %r" % x) - - def assertIn(self, a, b, msg=None): - if a not in b: - self.fail(msg or "%r not in %r" % (a, b)) - - def assertNotIn(self, a, b, msg=None): - if a in b: - self.fail(msg or "%r in %r" % (a, b)) - - def assertIsInstance(self, instance, types, msg=None): - if not isinstance(instance, types): - self.fail(msg or "not an instance of %r; %r" % (types, instance)) - - def assertNotIsInstance(self, instance, types, msg=None): - if isinstance(instance, types): - self.fail(msg or "is an instance of %r; %r" % (types, instance)) From a26cda17c297867e3c75f314bd98071165ef7ee6 Mon Sep 17 00:00:00 2001 From: Eric Kalosa-Kenyon Date: Tue, 6 Jun 2023 18:24:30 -0400 Subject: [PATCH 136/846] Catching AttributeError when repr(fifo) that hasn't had fifo.write(frame) called yet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jeremy Lainé --- av/audio/fifo.pyx | 23 +++++++++++++++-------- tests/test_audiofifo.py | 14 ++++++++++++++ 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/av/audio/fifo.pyx b/av/audio/fifo.pyx index 9b95d5770..9f92f1ff2 100644 --- a/av/audio/fifo.pyx +++ b/av/audio/fifo.pyx @@ -7,14 +7,21 @@ cdef class AudioFifo: """A simple audio sample FIFO (First In First Out) buffer.""" def __repr__(self): - return '' % ( - self.__class__.__name__, - self.samples, - self.sample_rate, - self.layout, - self.format, - id(self), - ) + try: + result = '' % ( + self.__class__.__name__, + self.samples, + self.sample_rate, + self.layout, + self.format, + id(self), + ) + except AttributeError: + result = '' % ( + self.__class__.__name__, + id(self), + ) + return result def __dealloc__(self): if self.ptr: diff --git a/tests/test_audiofifo.py b/tests/test_audiofifo.py index 30862f2bb..0cbb4acc4 100644 --- a/tests/test_audiofifo.py +++ b/tests/test_audiofifo.py @@ -32,6 +32,13 @@ def test_data(self): def test_pts_simple(self): fifo = av.AudioFifo() + # ensure __repr__ does not crash + self.assertTrue( + str(fifo).startswith( + " at 0x" + ) + ) + oframe = fifo.read(512) self.assertTrue(oframe is not None) self.assertEqual(oframe.pts, 0) From 9b32621446efe301231f89c04cbbf74de0924d03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Tue, 31 Oct 2023 10:53:29 +0100 Subject: [PATCH 137/846] Update github actions to their latest versions --- .github/workflows/issues.yml | 2 +- .github/workflows/tests.yml | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml index d3ea994cf..c88cccec4 100644 --- a/.github/workflows/issues.yml +++ b/.github/workflows/issues.yml @@ -7,7 +7,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v5 + - uses: actions/stale@v8 with: stale-issue-label: stale stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3cd077ade..7931282cf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -26,7 +26,7 @@ jobs: PYAV_LIBRARY: ffmpeg-4.3 # doesn't matter steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 name: Checkout - name: Python @@ -66,7 +66,7 @@ jobs: PYAV_LIBRARY: ffmpeg-${{ matrix.config.ffmpeg }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 name: Checkout - name: Python ${{ matrix.config.python }} @@ -147,7 +147,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Conda shell: bash @@ -187,7 +187,7 @@ jobs: package-source: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: 3.8 @@ -221,13 +221,13 @@ jobs: - os: windows-latest arch: AMD64 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: 3.8 - name: Set up QEMU if: matrix.os == 'ubuntu-latest' - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Install packages if: matrix.os == 'macos-latest' run: | @@ -262,7 +262,7 @@ jobs: runs-on: ubuntu-latest needs: [package-source, package-wheel] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/download-artifact@v3 with: name: dist From f1b1c0ac8c395ce6737669845e95ab009f4e8991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Wed, 1 Nov 2023 19:01:51 +0100 Subject: [PATCH 138/846] Update our binaries for FFmpeg 5.1 In addition to updating FFmpeg to version 5.1.3, this updates libvpx to fix the CVE-2023-5217 buffer overflow vulnerability. --- .github/workflows/tests.yml | 2 +- docs/overview/installation.rst | 3 +-- scripts/fetch-vendor.json | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7931282cf..996a60bf4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -162,7 +162,7 @@ jobs: python=${{ matrix.config.python }} \ setuptools if [[ "${{ matrix.config.ffmpeg }}" == "5.1" ]]; then - curl -L -o ffmpeg.tar.gz https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.1.2-1/ffmpeg-win_amd64.tar.gz + curl -L -o ffmpeg.tar.gz https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.1.3-1/ffmpeg-win_amd64.tar.gz elif [[ "${{ matrix.config.ffmpeg }}" == "5.0" ]]; then curl -L -o ffmpeg.tar.gz https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.0.1-1/ffmpeg-win_amd64.tar.gz else diff --git a/docs/overview/installation.rst b/docs/overview/installation.rst index 72afe8d52..e97da901e 100644 --- a/docs/overview/installation.rst +++ b/docs/overview/installation.rst @@ -11,7 +11,7 @@ Since release 8.0.0 binary wheels are provided on PyPI for Linux, Mac and Window pip install av -Currently FFmpeg 5.1.2 is used with the following features enabled for all platforms: +Currently FFmpeg 5.1.3 is used with the following features enabled for all platforms: - fontconfig - gmp @@ -26,7 +26,6 @@ Currently FFmpeg 5.1.2 is used with the following features enabled for all platf - libopenjpeg - libopus - libspeex -- libtheora - libtwolame - libvorbis - libvpx diff --git a/scripts/fetch-vendor.json b/scripts/fetch-vendor.json index 2a02f285b..75e4b8eca 100644 --- a/scripts/fetch-vendor.json +++ b/scripts/fetch-vendor.json @@ -1,3 +1,3 @@ { - "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.1.2-1/ffmpeg-{platform}.tar.gz"] + "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.1.3-1/ffmpeg-{platform}.tar.gz"] } From af760ec32a073fb9eaa4c1e4244ab7f775bff331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Wed, 1 Nov 2023 18:42:09 +0100 Subject: [PATCH 139/846] Guard input container properties and methods again use after close Calling `close()` on an input container calls `avformat_close_input` which will set the `self.ptr` AVFormatContext pointer to NULL. We need to ensure `self.ptr` is not NULL before using it. Fixes: #1137 --- av/container/core.pxd | 2 ++ av/container/core.pyx | 7 +++++++ av/container/input.pyx | 16 ++++++++++++++-- tests/test_decode.py | 13 +++++++++++++ 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/av/container/core.pxd b/av/container/core.pxd index fb7c3b511..8ca5ded16 100644 --- a/av/container/core.pxd +++ b/av/container/core.pxd @@ -37,6 +37,8 @@ cdef class Container: cdef readonly StreamContainer streams cdef readonly dict metadata + # Private API. + cdef _assert_open(self) cdef int err_check(self, int value) except -1 # Timeouts diff --git a/av/container/core.pyx b/av/container/core.pyx index 5b1711a53..61a201274 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -285,6 +285,7 @@ cdef class Container: return err_check(value, filename=self.name) def dumps_format(self): + self._assert_open() with LogCapture() as logs: lib.av_dump_format(self.ptr, 0, "", isinstance(self, OutputContainer)) return ''.join(log[2] for log in logs) @@ -298,10 +299,16 @@ cdef class Container: cdef start_timeout(self): self.interrupt_callback_info.start_time = clock() + cdef _assert_open(self): + if self.ptr == NULL: + raise AssertionError("Container is not open") + def _get_flags(self): + self._assert_open() return self.ptr.flags def _set_flags(self, value): + self._assert_open() self.ptr.flags = value flags = Flags.property( diff --git a/av/container/input.pyx b/av/container/input.pyx index 80cd8f783..e73fcc29f 100644 --- a/av/container/input.pyx +++ b/av/container/input.pyx @@ -15,6 +15,7 @@ from av.dictionary import Dictionary cdef close_input(InputContainer self): if self.input_was_opened: with nogil: + # This causes `self.ptr` to be set to NULL. lib.avformat_close_input(&self.ptr) self.input_was_opened = False @@ -90,19 +91,25 @@ cdef class InputContainer(Container): property start_time: def __get__(self): + self._assert_open() if self.ptr.start_time != lib.AV_NOPTS_VALUE: return self.ptr.start_time property duration: def __get__(self): + self._assert_open() if self.ptr.duration != lib.AV_NOPTS_VALUE: return self.ptr.duration property bit_rate: - def __get__(self): return self.ptr.bit_rate + def __get__(self): + self._assert_open() + return self.ptr.bit_rate property size: - def __get__(self): return lib.avio_size(self.ptr.pb) + def __get__(self): + self._assert_open() + return lib.avio_size(self.ptr.pb) def close(self): close_input(self) @@ -123,6 +130,7 @@ cdef class InputContainer(Container): .. note:: The last packets are dummy packets that when decoded will flush the buffers. """ + self._assert_open() # For whatever reason, Cython does not like us directly passing kwargs # from one method to another. Without kwargs, it ends up passing a @@ -198,6 +206,7 @@ cdef class InputContainer(Container): the arguments. """ + self._assert_open() id(kwargs) # Avoid Cython bug; see demux(). for packet in self.demux(*args, **kwargs): for frame in packet.decode(): @@ -234,6 +243,7 @@ cdef class InputContainer(Container): .. seealso:: :ffmpeg:`avformat_seek_file` for discussion of the flags. """ + self._assert_open() # We used to take floats here and assume they were in seconds. This # was super confusing, so lets go in the complete opposite direction @@ -270,6 +280,8 @@ cdef class InputContainer(Container): self.flush_buffers() cdef flush_buffers(self): + self._assert_open() + cdef Stream stream cdef CodecContext codec_context diff --git a/tests/test_decode.py b/tests/test_decode.py index 564ea24cd..bc9c96e58 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -111,3 +111,16 @@ def test_decode_video_corrupt(self): self.assertEqual(packet_count, 1) self.assertEqual(frame_count, 0) + + def test_decode_close_then_use(self): + container = av.open(fate_suite("h264/interlaced_crop.mp4")) + container.close() + + # Check accessing every attribute either works or raises + # an `AssertionError`. + for attr in dir(container): + with self.subTest(attr=attr): + try: + getattr(container, attr) + except AssertionError: + pass From 9954397036d48caec74a951459bb40a7fc5c6749 Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Tue, 17 Oct 2023 16:58:24 -0600 Subject: [PATCH 140/846] Add __set__ for is_keyframe and is_corrupt --- av/packet.pyx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/av/packet.pyx b/av/packet.pyx index 0687b2237..af77e2c10 100644 --- a/av/packet.pyx +++ b/av/packet.pyx @@ -196,5 +196,17 @@ cdef class Packet(Buffer): property is_keyframe: def __get__(self): return bool(self.ptr.flags & lib.AV_PKT_FLAG_KEY) + def __set__(self, v): + if v: + self.ptr.flags |= lib.AV_PKT_FLAG_KEY + else: + self.ptr.flags &= ~(lib.AV_PKT_FLAG_KEY) + property is_corrupt: def __get__(self): return bool(self.ptr.flags & lib.AV_PKT_FLAG_CORRUPT) + + def __set__(self, v): + if v: + self.ptr.flags |= lib.AV_PKT_FLAG_CORRUPT + else: + self.ptr.flags &= ~(lib.AV_PKT_FLAG_CORRUPT) From f9a5e7e879e6af36d4d1abb439192d90f10c8e28 Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Fri, 20 Oct 2023 14:00:49 -0600 Subject: [PATCH 141/846] Add __set__ for dts in AVFrame --- av/frame.pyx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/av/frame.pyx b/av/frame.pyx index b98624cc5..352b68157 100644 --- a/av/frame.pyx +++ b/av/frame.pyx @@ -75,6 +75,12 @@ cdef class Frame: return None return self.ptr.pkt_dts + def __set__(self, value): + if value is None: + self.ptr.pkt_dts = lib.AV_NOPTS_VALUE + else: + self.ptr.pkt_dts = value + property pts: """ The presentation timestamp in :attr:`time_base` units for this frame. From 30e3ef2c9be03f0966bf3bd61b38010c0439d2fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Wed, 1 Nov 2023 23:27:32 +0100 Subject: [PATCH 142/846] Simplify linter tests --- .github/workflows/tests.yml | 29 +++++------------------------ Makefile | 5 +++-- scripts/test | 14 -------------- tests/requirements.txt | 5 +++-- 4 files changed, 11 insertions(+), 42 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 996a60bf4..edca189f9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,41 +12,22 @@ on: - '**.txt' jobs: style: - name: "${{ matrix.config.suite }}" runs-on: ubuntu-latest - strategy: - matrix: - config: - - {suite: black} - - {suite: flake8} - - {suite: isort} - - env: - PYAV_PYTHON: python3 - PYAV_LIBRARY: ffmpeg-4.3 # doesn't matter steps: - - uses: actions/checkout@v4 - name: Checkout + - name: Checkout + uses: actions/checkout@v4 - name: Python uses: actions/setup-python@v4 with: python-version: 3.8 - - name: Environment - run: env | sort - - name: Packages - run: | - . scripts/activate.sh - # A bit of a hack that we can get away with this. - python -m pip install ${{ matrix.config.suite }} + run: pip install -r tests/requirements.txt - - name: "${{ matrix.config.suite }}" - run: | - . scripts/activate.sh - ./scripts/test ${{ matrix.config.suite }} + - name: Linters + run: make lint nix: name: "py-${{ matrix.config.python }} lib-${{ matrix.config.ffmpeg }} ${{matrix.config.os}}" diff --git a/Makefile b/Makefile index 6cb954dea..fd4d4ed51 100644 --- a/Makefile +++ b/Makefile @@ -34,8 +34,9 @@ fate-suite: rsync -vrltLW rsync://fate-suite.ffmpeg.org/fate-suite/ tests/assets/fate-suite/ lint: - TESTSUITE=flake8 scripts/test - TESTSUITE=isort scripts/test + black --check av examples tests + flake8 av examples tests + isort --check-only --diff av examples tests test: $(PYTHON) setup.py test diff --git a/scripts/test b/scripts/test index 806011859..3eca2feb7 100755 --- a/scripts/test +++ b/scripts/test @@ -18,20 +18,6 @@ istest() { return $? } -if istest black; then - $PYAV_PYTHON -m black --check av examples tests -fi - -if istest flake8; then - # Settings are in setup.cfg - $PYAV_PYTHON -m flake8 av examples tests -fi - -if istest isort; then - # More settings in setup.cfg - $PYAV_PYTHON -m isort --check-only --diff av examples tests -fi - if istest main; then $PYAV_PYTHON setup.py test fi diff --git a/tests/requirements.txt b/tests/requirements.txt index 2a321a28d..36d21ecf9 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,8 +1,9 @@ autopep8 -Cython +black +cython editorconfig flake8 isort numpy -Pillow +pillow sphinx < 4.4 From 0236c52f621a7f1b3cc7571a504e0811810bca8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Thu, 2 Nov 2023 00:03:54 +0100 Subject: [PATCH 143/846] Prune Makefile targets, drop unused scripts Remove some unused Makefile targets, as well as scripts: - build-debug-python - clean-branches - vagrant-test We can also remove the `sdist` step as this is exercised by the `package-source` job. --- .github/workflows/tests.yml | 6 -- .gitignore | 1 - Makefile | 56 +++-------------- scripts/build-debug-python | 33 ---------- scripts/clean-branches | 120 ------------------------------------ scripts/test | 5 -- scripts/vagrant-test | 7 --- 7 files changed, 8 insertions(+), 220 deletions(-) delete mode 100755 scripts/build-debug-python delete mode 100755 scripts/clean-branches delete mode 100755 scripts/vagrant-test diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index edca189f9..70af7ea14 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -109,12 +109,6 @@ jobs: . scripts/activate.sh ffmpeg-${{ matrix.config.ffmpeg }} scripts/test examples - - name: Source Distribution - if: matrix.config.extras - run: | - . scripts/activate.sh ffmpeg-${{ matrix.config.ffmpeg }} - scripts/test sdist - windows: name: "py-${{ matrix.config.python }} lib-${{ matrix.config.ffmpeg }} ${{matrix.config.os}}" runs-on: ${{ matrix.config.os }} diff --git a/.gitignore b/.gitignore index bc6c09077..6e5a2b48e 100644 --- a/.gitignore +++ b/.gitignore @@ -34,7 +34,6 @@ # Testing. *.spyderproject .idea -/.vagrant /sandbox /tests/assets /tests/samples diff --git a/Makefile b/Makefile index fd4d4ed51..e8ad38277 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ PYAV_PYTHON ?= python PYTHON := $(PYAV_PYTHON) -.PHONY: default build cythonize clean clean-all info lint test fate-suite test-assets docs +.PHONY: default build clean docs fate-suite lint test default: build @@ -13,21 +13,13 @@ default: build build: CFLAGS=$(CFLAGS) LDFLAGS=$(LDFLAGS) $(PYTHON) setup.py build_ext --inplace --debug -cythonize: - $(PYTHON) setup.py cythonize - - - -wheel: build-mingw32 - $(PYTHON) setup.py bdist_wheel - -build-mingw32: - # before running, set PKG_CONFIG_PATH to the pkgconfig dir of the ffmpeg build. - # set PKG_CONFIG_PATH=D:\dev\3rd\media-autobuild_suite\local32\bin-video\ffmpegSHARED\lib\pkgconfig - CFLAGS=$(CFLAGS) LDFLAGS=$(LDFLAGS) $(PYTHON) setup.py build_ext --inplace -c mingw32 - mv *.pyd av - - +clean: + - find av -name '*.so' -delete + - rm -rf build + - rm -rf sandbox + - rm -rf src + - rm -rf tmp + - make -C docs clean fate-suite: # Grab ALL of the samples from the ffmpeg site. @@ -41,16 +33,6 @@ lint: test: $(PYTHON) setup.py test - - -vagrant: - vagrant box list | grep -q precise32 || vagrant box add precise32 http://files.vagrantup.com/precise32.box - -vtest: - vagrant ssh -c /vagrant/scripts/vagrant-test - - - tmp/ffmpeg-git: @ mkdir -p tmp/ffmpeg-git git clone --depth=1 git://source.ffmpeg.org/ffmpeg.git tmp/ffmpeg-git @@ -67,25 +49,3 @@ docs: tmp/tagfile.xml deploy-docs: docs ./docs/upload docs - - - - -clean-build: - - rm -rf build - - find av -name '*.so' -delete - -clean-sandbox: - - rm -rf sandbox/201* - - rm sandbox/last - -clean-src: - - rm -rf src - -clean-docs: - - rm tmp/Doxyfile - - rm tmp/tagfile.xml - - make -C docs clean - -clean: clean-build clean-sandbox clean-src -clean-all: clean-build clean-sandbox clean-src clean-docs diff --git a/scripts/build-debug-python b/scripts/build-debug-python deleted file mode 100755 index 757946f14..000000000 --- a/scripts/build-debug-python +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash - -export PYAV_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.."; pwd)" - -export PYAV_PYTHON_VERSION="2.7.13" -export PYAV_PLATFORM_SLUG="$(uname -s).$(uname -r)" -export PYAV_VENV_NAME="$PYAV_PLATFORM_SLUG.cpython-$PYAV_PYTHON_VERSION-debug" -export PYAV_VENV="$PYAV_ROOT/venvs/$PYAV_VENV_NAME" - - -export PYAV_PYTHON_SRC="$PYAV_ROOT/vendor/Python-$PYAV_PYTHON_VERSION" - -if [[ ! -d "$PYAV_PYTHON_SRC" ]]; then - url="https://www.python.org/ftp/python/$PYAV_PYTHON_VERSION/Python-$PYAV_PYTHON_VERSION.tgz" - echo "Downloading $url" - wget -O "$PYAV_PYTHON_SRC.tgz" "$url" || exit 2 - tar -C "$PYAV_ROOT/vendor" -xvzf "$PYAV_PYTHON_SRC.tgz" || exit 3 -fi - -cd "$PYAV_PYTHON_SRC" || exit 4 - -# TODO: Make generic. -export CPPFLAGS="-I$(brew --prefix openssl)/include" -export LDFLAGS="-L$(brew --prefix openssl)/lib" - -# --with-pymalloc \ -./configure \ - --with-pydebug \ - --prefix "$PYAV_VENV" \ - || exit 5 - -make -j 12 || exit 6 - diff --git a/scripts/clean-branches b/scripts/clean-branches deleted file mode 100755 index 83f82f6fb..000000000 --- a/scripts/clean-branches +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/env python - -import subprocess - - -# These are remote branches that got rebased or something, but I don't have -# control over. -ignored_hashes = set(''' - 078daa1148a84849ea0890b388353acd7333fcea - 0832d2bdaa048fca1393f3d3810b8b8810535f9f - 08e86996736d77df7d694d0a67a126fc7eac7e94 - 097631535fa4cdb87204980541d3f9bb9c7a9ffb - 16ce34ba1d1f1ed4327326a10565f1a9f07107b0 - 183cf1447571aa48baf0665d17d41be1e48f2cc6 - 245a4dca69cf0fff674c6ad5bcfb7929c7347bf6 - 28b4b3988981471ff173679db3643418ff3f5aaa - 3977b5d5be22922f2eb4288e2682f1de8fad8e12 - 5b9f192165855942918f9bd957c30e918b97cbeb - 6091e89de0ae4aff2ba76d21b1110409ef174b78 - 636afe3f0b5b07233edae8e333db35c044c36b30 - 74f79ef74ec281f5e0da51bcfd0b1051aa53edbf - 7737ef6e9e7307c40f326e61cc9291047540bc49 - 8618940d333f44ff960d561dda34167d4dbb81d4 - a2fb55e97788809b5f33b1b0c9241fc77312f606 - aa044b3f62a6d7bf4dde18cba91b1d0dd8a0816a - aa7d01ba458025ede1757e56b638002375bb864a - aafe064e209b667f565c4f57a94b098474d0b184 - afac2d8f89673c012d1f4b845b006911f55d1d86 - b115786b950c87ef9c422752e014297903bca393 - b737c6ceb6750d00f62dfdaa40fee3e757c680a3 - b7bf427a485736e6e1c71605bdce101214bae09f - ba02afa7ea160328b5a3be111c7e276fb9d3c961 - bc5ffe456345286a64ce33ffe5ce6a2ee8b63f40 - c45a337fe49875b1cc28a0501a704890be444765 - c6b1a5ac03e775ea46bffac7bbfea9d73cd03b87 - c9c0d63b09c450d494fba1c4073fbe18851dfaff - cc270d6790c02e6c5e93313d1e6499ce534350b9 - cdd8e4c085a55e258bd551f7bcf4fee60474aa05 - eac71881c24d42f801e9c18e448855a402333960 - efd12926b1f446c32f5a239c0b2d351fa2d78101 - f04dce0e80b4f290482eba4fb3c3ec68f353bf01 - f0d1e82dee788085cf4afad7656a90966e40f7a0 - f518f6e7bf47e00fe0c73a5098ae40813920400f - f779c4371fdace76ee572053b4acb3999ffd4107 -'''.strip().split()) - - -def get_branches(*args): - cmd = ['git', 'branch', '-v', '--abbrev=40'] - cmd.extend(args) - res = {} - for line in subprocess.check_output(cmd).decode().splitlines(): - parts = line[2:].strip().split() - name = parts[0] - hash_ = parts[1] - res[name] = hash_ - return res - -def rm(*args): - subprocess.check_call(('git', 'branch', '-D') + args) - - -# Clean up everything that was merged -for line in subprocess.check_output(['git', 'branch', '--merged']).decode().splitlines(): - line = line.strip() - if not line: - continue - parts = line.split() - if parts[0] == '*': - continue - if parts[-1] in ('develop', 'master'): - continue - rm(parts[-1]) - -for line in subprocess.check_output(['git', 'branch', '-r', '--merged']).decode().splitlines(): - name = line.strip().split()[-1] - if not name: - continue - if name.split('/', 1)[-1] in ('develop', 'master'): - continue - rm('-r', name) - - -our_branches = get_branches() -for name, hash_ in get_branches('-r').items(): - - if hash_ in ignored_hashes: - print("Removing ignored", name) - rm('-r', name) - continue - - if name.startswith('origin/'): - our_branches[name] = hash_ - - -for name in get_branches('-r', '--merged'): - if name.startswith('origin/'): - continue - print("Removing merged", name) - rm('-r', name) - -for name, hash_ in get_branches('-r', '--no-merged').items(): - - remote, branch = name.split('/', 1) - if remote == 'origin': - continue - - for prefix in '', 'origin/': - our_name = prefix + branch - if our_branches.get(our_name) == hash_: - print("Removing identical", name) - rm('-r', name) - break - -# Anything that doesn't root at the same place as us. -for name in get_branches('-r', '--no-contains', 'e105c0b4e64a0471f3f5375a86342c33cb942e23'): - rm('-r', name) - - - diff --git a/scripts/test b/scripts/test index 3eca2feb7..c34734456 100755 --- a/scripts/test +++ b/scripts/test @@ -22,11 +22,6 @@ if istest main; then $PYAV_PYTHON setup.py test fi -if istest sdist; then - $PYAV_PYTHON setup.py build_ext - $PYAV_PYTHON setup.py sdist -fi - if istest doctest; then make -C docs test fi diff --git a/scripts/vagrant-test b/scripts/vagrant-test deleted file mode 100755 index dbdab607a..000000000 --- a/scripts/vagrant-test +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash - -cd /vagrant - -./scripts/build-deps -./scripts/build -./scripts/test From 4a72a8b40cb2bd15fbb9b5291c3f6a2fa83afa39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Thu, 2 Nov 2023 09:18:28 +0100 Subject: [PATCH 144/846] Simplify documentation generation Do not depend on how FFmpeg was built, just fetch a fresh clone of the FFmpeg repository to generate doxygen tags. --- .github/workflows/tests.yml | 2 +- Makefile | 20 +----------------- docs/Makefile | 6 ++++-- docs/generate-tagfile | 42 +++++++++---------------------------- scripts/test | 4 ---- 5 files changed, 16 insertions(+), 58 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 70af7ea14..8037ba891 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -101,7 +101,7 @@ jobs: if: matrix.config.extras run: | . scripts/activate.sh ffmpeg-${{ matrix.config.ffmpeg }} - scripts/test doctest + make -C docs test - name: Examples if: matrix.config.extras diff --git a/Makefile b/Makefile index e8ad38277..9a0b0a95f 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ PYAV_PYTHON ?= python PYTHON := $(PYAV_PYTHON) -.PHONY: default build clean docs fate-suite lint test +.PHONY: default build clean fate-suite lint test default: build @@ -18,7 +18,6 @@ clean: - rm -rf build - rm -rf sandbox - rm -rf src - - rm -rf tmp - make -C docs clean fate-suite: @@ -32,20 +31,3 @@ lint: test: $(PYTHON) setup.py test - -tmp/ffmpeg-git: - @ mkdir -p tmp/ffmpeg-git - git clone --depth=1 git://source.ffmpeg.org/ffmpeg.git tmp/ffmpeg-git - -tmp/Doxyfile: tmp/ffmpeg-git - cp tmp/ffmpeg-git/doc/Doxyfile $@ - echo "GENERATE_TAGFILE = ../tagfile.xml" >> $@ - -tmp/tagfile.xml: tmp/Doxyfile - cd tmp/ffmpeg-git; doxygen ../Doxyfile - -docs: tmp/tagfile.xml - PYTHONPATH=.. make -C docs html - -deploy-docs: docs - ./docs/upload docs diff --git a/docs/Makefile b/docs/Makefile index 65a1e540e..f26ef7dc3 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -2,6 +2,7 @@ SPHINXOPTS = SPHINXBUILD = sphinx-build BUILDDIR = _build +FFMPEGDIR = _ffmpeg ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(SPHINXOPTS) . @@ -12,7 +13,8 @@ default: html TAGFILE := _build/doxygen/tagfile.xml $(TAGFILE) : - ./generate-tagfile -o $(TAGFILE) + git clone --depth=1 git://source.ffmpeg.org/ffmpeg.git $(FFMPEGDIR) + ./generate-tagfile --library $(FFMPEGDIR) -o $(TAGFILE) TEMPLATES := $(wildcard api/*.py development/*.py) @@ -24,7 +26,7 @@ _build/rst/%.rst: %.py $(TAGFILE) $(shell find ../include ../av -name '*.pyx' -o clean: - - rm -rf $(BUILDDIR)/* + rm -rf $(BUILDDIR) $(FFMPEGDIR) html: $(RENDERED) $(TAGFILE) $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html diff --git a/docs/generate-tagfile b/docs/generate-tagfile index f00ed7ad4..1f729de5c 100755 --- a/docs/generate-tagfile +++ b/docs/generate-tagfile @@ -1,46 +1,23 @@ #!/usr/bin/env python +import argparse import os import subprocess -import argparse parser = argparse.ArgumentParser() -parser.add_argument('-l', '--library', default=os.environ.get('PYAV_LIBRARY')) -parser.add_argument('-o', '--output', default=os.path.abspath(os.path.join( - __file__, - '..', - '_build', - 'doxygen', - 'tagfile.xml', -))) +parser.add_argument("-l", "--library", required=True) +parser.add_argument("-o", "--output", required=True) args = parser.parse_args() - -if not args.library: - print("Please provide --library or set $PYAV_LIBRARY") - exit(1) - -library = os.path.abspath(os.path.join( - __file__, - '..', '..', - 'vendor', - args.library, -)) - -if not os.path.exists(library): - print("Library does not exist:", library) - exit(2) - - output = os.path.abspath(args.output) outdir = os.path.dirname(output) if not os.path.exists(outdir): os.makedirs(outdir) - -proc = subprocess.Popen(['doxygen', '-'], stdin=subprocess.PIPE, cwd=library) -proc.communicate(''' +proc = subprocess.Popen(["doxygen", "-"], stdin=subprocess.PIPE, cwd=args.library) +proc.communicate( + """ #@INCLUDE = doc/Doxyfile GENERATE_TAGFILE = {} @@ -49,6 +26,7 @@ GENERATE_LATEX = no CASE_SENSE_NAMES = yes INPUT = libavcodec libavdevice libavfilter libavformat libavresample libavutil libswresample libswscale -'''.format(output).encode()) - - +""".format( + output + ).encode() +) diff --git a/scripts/test b/scripts/test index c34734456..01991f5a6 100755 --- a/scripts/test +++ b/scripts/test @@ -22,10 +22,6 @@ if istest main; then $PYAV_PYTHON setup.py test fi -if istest doctest; then - make -C docs test -fi - if istest examples; then for name in $(find examples -name '*.py'); do echo From d892dcf1c80a3c597d8cdf894ef431dead3967f8 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 3 Nov 2023 02:13:53 -0400 Subject: [PATCH 145/846] Remove `flags.txt` This doc file is outdated, hasn't been touched in years, and is not needed for anything. Just remove it. --- flags.txt | 53 ----------------------------------------------------- 1 file changed, 53 deletions(-) delete mode 100644 flags.txt diff --git a/flags.txt b/flags.txt deleted file mode 100644 index a2d730711..000000000 --- a/flags.txt +++ /dev/null @@ -1,53 +0,0 @@ - - -Objects with flags -=== -√ AVCodec.capabilities -√ AVCodecDescriptor.props -√ AVCodecContext.flags and flags2 -AVOutputFormat.flags - - - -Thoughts -=== - -- Having both individual properties AND the flags objects is kinda nice. -- I want lowercase flag/enum names, but to also work with the upper ones for b/c. - - -Option: av.enum flags. - - context.flags2 & 'EXPORT_MVS' - - context.flags2 |= 'EXPORT_MVS' - - new APIs: - - 'export_mvs' in context.flags2 - - context.flags2.export_mvs = True - - context.flags2['export_mvs'] = True - -Option: object which represents all flags, but can't work with integer values - - context.flags merges flags and flags2 - - this is really only handy on AVCodecContext, so... fuckit? - -Option: all exposed as individual properties - - context.export_mvs - - - This polutes the attribute space a lot. - - This feels the most "pythonic". - - If you can set multiple in constructors, then NBD if you want to do many. - - I don't like how I have to pick names. - - - - - -How to name -=== - -If a prefix is required, one of: - - is - - has - - can - - use - - do - - From ca1e7f9d824aceeea2c11f74b54b1019b77dec88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Thu, 2 Nov 2023 07:40:31 +0100 Subject: [PATCH 146/846] Build our wheels against FFmpeg 6.0 --- .github/workflows/tests.yml | 18 ++++++------------ scripts/fetch-vendor.py | 7 +++---- scripts/ffmpeg-5.0.json | 3 +++ scripts/{fetch-vendor.json => ffmpeg-5.1.json} | 0 scripts/ffmpeg-6.0.json | 3 +++ 5 files changed, 15 insertions(+), 16 deletions(-) create mode 100644 scripts/ffmpeg-5.0.json rename scripts/{fetch-vendor.json => ffmpeg-5.1.json} (100%) create mode 100644 scripts/ffmpeg-6.0.json diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8037ba891..9f880a6bb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -117,6 +117,7 @@ jobs: fail-fast: false matrix: config: + - {os: windows-latest, python: 3.8, ffmpeg: "6.0"} - {os: windows-latest, python: 3.8, ffmpeg: "5.1"} - {os: windows-latest, python: 3.8, ffmpeg: "5.0"} @@ -136,21 +137,14 @@ jobs: pillow \ python=${{ matrix.config.python }} \ setuptools - if [[ "${{ matrix.config.ffmpeg }}" == "5.1" ]]; then - curl -L -o ffmpeg.tar.gz https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.1.3-1/ffmpeg-win_amd64.tar.gz - elif [[ "${{ matrix.config.ffmpeg }}" == "5.0" ]]; then - curl -L -o ffmpeg.tar.gz https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.0.1-1/ffmpeg-win_amd64.tar.gz - else - exit 1 - fi - name: Build shell: bash run: | . $CONDA/etc/profile.d/conda.sh conda activate pyav - tar -xf ffmpeg.tar.gz -C $CONDA_PREFIX/Library/ - python setup.py build_ext --inplace --ffmpeg-dir=$CONDA_PREFIX/Library + python scripts\\fetch-vendor.py --config-file scripts\\ffmpeg-${{ matrix.config.ffmpeg }}.json $CONDA_PREFIX\\Library + python setup.py build_ext --inplace --ffmpeg-dir=$CONDA_PREFIX\\Library - name: Test shell: bash @@ -169,7 +163,7 @@ jobs: - name: Build source package run: | pip install cython - python scripts/fetch-vendor.py /tmp/vendor + python scripts/fetch-vendor.py --config-file scripts/ffmpeg-6.0.json /tmp/vendor PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig python setup.py sdist - name: Upload source package uses: actions/upload-artifact@v3 @@ -212,8 +206,8 @@ jobs: env: CIBW_ARCHS: ${{ matrix.arch }} CIBW_BEFORE_ALL_LINUX: yum install -y alsa-lib libxcb - CIBW_BEFORE_BUILD: pip install cython && python scripts/fetch-vendor.py /tmp/vendor - CIBW_BEFORE_BUILD_WINDOWS: pip install cython && python scripts\fetch-vendor.py C:\cibw\vendor + CIBW_BEFORE_BUILD: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-6.0.json /tmp/vendor + CIBW_BEFORE_BUILD_WINDOWS: python scripts\fetch-vendor.py --config-file scripts\ffmpeg-6.0.json C:\cibw\vendor CIBW_ENVIRONMENT_LINUX: LD_LIBRARY_PATH=/tmp/vendor/lib:$LD_LIBRARY_PATH PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig CIBW_ENVIRONMENT_MACOS: PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig LDFLAGS=-headerpad_max_install_names CIBW_ENVIRONMENT_WINDOWS: INCLUDE=C:\\cibw\\vendor\\include LIB=C:\\cibw\\vendor\\lib PYAV_SKIP_TESTS=unicode_filename diff --git a/scripts/fetch-vendor.py b/scripts/fetch-vendor.py index 3ea3a0c6d..9ea329eb3 100644 --- a/scripts/fetch-vendor.py +++ b/scripts/fetch-vendor.py @@ -39,11 +39,10 @@ def get_platform(): with open(args.config_file, "r") as fp: config = json.load(fp) -# create fresh destination directory +# ensure destination directory exists logging.info("Creating directory %s" % args.destination_dir) -if os.path.exists(args.destination_dir): - shutil.rmtree(args.destination_dir) -os.makedirs(args.destination_dir) +if not os.path.exists(args.destination_dir): + os.makedirs(args.destination_dir) for url_template in config["urls"]: tarball_url = url_template.replace("{platform}", get_platform()) diff --git a/scripts/ffmpeg-5.0.json b/scripts/ffmpeg-5.0.json new file mode 100644 index 000000000..41969df2c --- /dev/null +++ b/scripts/ffmpeg-5.0.json @@ -0,0 +1,3 @@ +{ + "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.0.1-1/ffmpeg-{platform}.tar.gz"] +} diff --git a/scripts/fetch-vendor.json b/scripts/ffmpeg-5.1.json similarity index 100% rename from scripts/fetch-vendor.json rename to scripts/ffmpeg-5.1.json diff --git a/scripts/ffmpeg-6.0.json b/scripts/ffmpeg-6.0.json new file mode 100644 index 000000000..0eb2034ee --- /dev/null +++ b/scripts/ffmpeg-6.0.json @@ -0,0 +1,3 @@ +{ + "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/6.0.0-1/ffmpeg-{platform}.tar.gz"] +} From de5a3b365fe6e0f5dd7398f1c34a4bfb736d5f4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Fri, 3 Nov 2023 17:01:38 +0100 Subject: [PATCH 147/846] Release v11.0.0. --- CHANGELOG.rst | 24 ++++++++++++++++++++++++ av/about.py | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index af5416bce..a6d4950c5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,30 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. +v11.0.0 +------- + +Major: + +- Add support for FFmpeg 6.0, drop support for FFmpeg < 5.0. +- Add support for Python 3.12, drop support for Python < 3.8. +- Build binary wheels against libvpx 1.13.1 to fix CVE-2023-5217. +- Build binary wheels against FFmpeg 6.0. + +Features: + +- Add support for the `ENCODER_FLUSH` encoder flag (:issue:`1067`). +- Add VideoFrame ndarray operations for yuv444p/yuvj444p formats (:issue:`788`). +- Add setters for `AVFrame.dts`, `AVPacket.is_keyframe` and `AVPacket.is_corrupt` (:issue:`1179`). + +Fixes: + +- Fix build using Cython 3 (:issue:`1140`). +- Populate new streams with codec parameters (:issue:`1044`). +- Explicitly set `python_requires` to avoid installing on incompatible Python (:issue:`1057`). +- Make `AudioFifo.__repr__` safe before the first frame (:issue:`1130`). +- Guard input container members against use after closes (:issue:`1137`). + v10.0.0 ------- diff --git a/av/about.py b/av/about.py index 9158871f9..5b461163e 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "10.0.0" +__version__ = "11.0.0" From 2454816fd9e52d0253f57c4d564fe1cafb335aa2 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 5 Nov 2023 12:55:21 -0500 Subject: [PATCH 148/846] Remove obsolete comments We only support ffmpeg 5 or greater. --- av/sidedata/sidedata.pyx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/av/sidedata/sidedata.pyx b/av/sidedata/sidedata.pyx index 17bb869cc..49ea4ad6a 100644 --- a/av/sidedata/sidedata.pyx +++ b/av/sidedata/sidedata.pyx @@ -25,13 +25,7 @@ Type = define_enum('Type', __name__, ( ('SPHERICAL', lib.AV_FRAME_DATA_SPHERICAL), ('CONTENT_LIGHT_LEVEL', lib.AV_FRAME_DATA_CONTENT_LIGHT_LEVEL), ('ICC_PROFILE', lib.AV_FRAME_DATA_ICC_PROFILE), - # SEI_UNREGISTERED available since version 56.54.100 of libavutil (FFmpeg >= 4.4) ('SEI_UNREGISTERED', lib.AV_FRAME_DATA_SEI_UNREGISTERED) if lib.AV_FRAME_DATA_SEI_UNREGISTERED != -1 else None, - - # These are deprecated. See https://github.com/PyAV-Org/PyAV/issues/607 - # ('QP_TABLE_PROPERTIES', lib.AV_FRAME_DATA_QP_TABLE_PROPERTIES), - # ('QP_TABLE_DATA', lib.AV_FRAME_DATA_QP_TABLE_DATA), - )) From a4bde60c714eeef9cc4b67f1d50b789a3d442978 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 5 Nov 2023 22:08:36 -0500 Subject: [PATCH 149/846] Change gitter url --- .github/ISSUE_TEMPLATE/build-bug-report.md | 2 +- .github/ISSUE_TEMPLATE/runtime-bug-report.md | 2 +- .github/ISSUE_TEMPLATE/user-help.md | 2 +- README.md | 2 +- docs/overview/caveats.rst | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/build-bug-report.md b/.github/ISSUE_TEMPLATE/build-bug-report.md index 9f327e5de..298424330 100644 --- a/.github/ISSUE_TEMPLATE/build-bug-report.md +++ b/.github/ISSUE_TEMPLATE/build-bug-report.md @@ -65,7 +65,7 @@ I have done the following: - [ ] Searched on [Google](https://www.google.com/search?q=pyav+how+do+I+foo) - [ ] Searched on [Stack Overflow](https://stackoverflow.com/search?q=pyav) - [ ] Looked through [old GitHub issues](https://github.com/PyAV-Org/PyAV/issues?&q=is%3Aissue) -- [ ] Asked on [PyAV Gitter](https://gitter.im/PyAV-Org) +- [ ] Asked on [PyAV Gitter](https://app.gitter.im/#/room/#PyAV-Org_User-Help:gitter.im) - [ ] ... and waited 72 hours for a response. diff --git a/.github/ISSUE_TEMPLATE/runtime-bug-report.md b/.github/ISSUE_TEMPLATE/runtime-bug-report.md index b29b11266..49fec3088 100644 --- a/.github/ISSUE_TEMPLATE/runtime-bug-report.md +++ b/.github/ISSUE_TEMPLATE/runtime-bug-report.md @@ -65,7 +65,7 @@ I have done the following: - [ ] Searched on [Google](https://www.google.com/search?q=pyav+how+do+I+foo) - [ ] Searched on [Stack Overflow](https://stackoverflow.com/search?q=pyav) - [ ] Looked through [old GitHub issues](https://github.com/PyAV-Org/PyAV/issues?&q=is%3Aissue) -- [ ] Asked on [PyAV Gitter](https://gitter.im/PyAV-Org) +- [ ] Asked on [PyAV Gitter](https://app.gitter.im/#/room/#PyAV-Org_User-Help:gitter.im) - [ ] ... and waited 72 hours for a response. diff --git a/.github/ISSUE_TEMPLATE/user-help.md b/.github/ISSUE_TEMPLATE/user-help.md index 62c3e5c16..46d6776f6 100644 --- a/.github/ISSUE_TEMPLATE/user-help.md +++ b/.github/ISSUE_TEMPLATE/user-help.md @@ -43,7 +43,7 @@ I have done the following: - [ ] Searched on [Google](https://www.google.com/search?q=pyav+how+do+I+foo) - [ ] Searched on [Stack Overflow](https://stackoverflow.com/search?q=pyav) - [ ] Looked through [old GitHub issues](https://github.com/PyAV-Org/PyAV/issues?&q=is%3Aissue) -- [ ] Asked on [PyAV Gitter](https://gitter.im/PyAV-Org) +- [ ] Asked on [PyAV Gitter](https://app.gitter.im/#/room/#PyAV-Org_User-Help:gitter.im) - [ ] ... and waited 72 hours for a response. diff --git a/README.md b/README.md index 74eecefcb..b6bbe56c9 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ Have fun, [read the docs][docs], [come chat with us][gitter], and good luck! [docs-badge]: https://img.shields.io/badge/docs-on%20pyav.org-blue.svg [docs]: http://pyav.org/docs [gitter-badge]: https://img.shields.io/gitter/room/nwjs/nw.js.svg?logo=gitter&colorB=cc2b5e -[gitter]: https://gitter.im/PyAV-Org +[gitter]: https://app.gitter.im/#/room/#PyAV-Org_User-Help:gitter.im [pypi-badge]: https://img.shields.io/pypi/v/av.svg?colorB=CCB39A [pypi]: https://pypi.org/project/av diff --git a/docs/overview/caveats.rst b/docs/overview/caveats.rst index 5ccac3ffb..093d9bb1a 100644 --- a/docs/overview/caveats.rst +++ b/docs/overview/caveats.rst @@ -53,5 +53,5 @@ Until we resolve this issue, you should explicitly call :meth:`.Container.close` .. _FFmpeg: https://ffmpeg.org/ -.. _Gitter: https://gitter.im/PyAV-Org +.. _Gitter: https://app.gitter.im/#/room/#PyAV-Org_User-Help:gitter.im .. _GitHub: https://github.com/PyAV-Org/pyav From 040cb505525cfa6b499e149f195fb2bda7ff41f3 Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Sun, 12 Nov 2023 22:29:06 -0700 Subject: [PATCH 150/846] Fix issues identified by cython-lint and import AVError directly --- av/audio/resampler.pyx | 2 +- av/error.pyx | 29 +++++++++++------------ include/libavcodec/avcodec.pxd | 33 ++++++++++++--------------- include/libavfilter/avfilter.pxd | 2 +- include/libavfilter/avfiltergraph.pxd | 1 - include/libavformat/avformat.pxd | 11 ++++----- include/libavutil/avutil.pxd | 14 ++++-------- include/libavutil/error.pxd | 1 - include/libavutil/samplefmt.pxd | 3 +-- include/libswresample/swresample.pxd | 3 +-- tests/test_filters.py | 2 +- 11 files changed, 42 insertions(+), 59 deletions(-) diff --git a/av/audio/resampler.pyx b/av/audio/resampler.pyx index 1214da317..bd5085704 100644 --- a/av/audio/resampler.pyx +++ b/av/audio/resampler.pyx @@ -106,7 +106,7 @@ cdef class AudioResampler: output.append(self.graph.pull()) except EOFError: break - except av.utils.AVError as e: + except av.AVError as e: if e.errno != errno.EAGAIN: raise break diff --git a/av/error.pyx b/av/error.pyx index cde4fec7f..e6257731e 100644 --- a/av/error.pyx +++ b/av/error.pyx @@ -200,23 +200,20 @@ for enum in ErrorType: # Mimick the builtin exception types. # See https://www.python.org/dev/peps/pep-3151/#new-exception-classes # Use the named ones we have, otherwise default to OSError for anything in errno. -r''' -See this command for the count of POSIX codes used: - - egrep -IR 'AVERROR\(E[A-Z]+\)' vendor/ffmpeg-4.2 |\ - sed -E 's/.*AVERROR\((E[A-Z]+)\).*/\1/' | \ - sort | uniq -c - -The biggest ones that don't map to PEP 3151 builtins: - - 2106 EINVAL -> ValueError - 649 EIO -> IOError (if it is distinct from OSError) - 4080 ENOMEM -> MemoryError - 340 ENOSYS -> NotImplementedError - 35 ERANGE -> OverflowError - -''' +# See this command for the count of POSIX codes used: +# +# egrep -IR 'AVERROR\(E[A-Z]+\)' vendor/ffmpeg-4.2 |\ +# sed -E 's/.*AVERROR\((E[A-Z]+)\).*/\1/' | \ +# sort | uniq -c +# +# The biggest ones that don't map to PEP 3151 builtins: +# +# 2106 EINVAL -> ValueError +# 649 EIO -> IOError (if it is distinct from OSError) +# 4080 ENOMEM -> MemoryError +# 340 ENOSYS -> NotImplementedError +# 35 ERANGE -> OverflowError classes = {} diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 11d968b1a..e11e9c115 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -30,7 +30,7 @@ cdef extern from "libavcodec/avcodec.h" nogil: AV_CODEC_PROP_BITMAP_SUB AV_CODEC_PROP_TEXT_SUB - #AVCodec.capabilities + # AVCodec.capabilities cdef enum: AV_CODEC_CAP_DRAW_HORIZ_BAND AV_CODEC_CAP_DR1 @@ -131,7 +131,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVClass *priv_class - cdef int av_codec_is_encoder(AVCodec*) cdef int av_codec_is_decoder(AVCodec*) @@ -144,7 +143,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVCodecDescriptor* avcodec_descriptor_get(AVCodecID) - cdef struct AVCodecContext: AVClass *av_class @@ -203,7 +201,7 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVPixelFormat pix_fmt AVRational sample_aspect_ratio - int gop_size # The number of pictures in a group of pictures, or 0 for intra_only. + int gop_size # The number of pictures in a group of pictures, or 0 for intra_only. int max_b_frames int has_b_frames @@ -288,15 +286,15 @@ cdef extern from "libavcodec/avcodec.h" nogil: # See: http://ffmpeg.org/doxygen/trunk/structAVFrame.html cdef struct AVFrame: - uint8_t *data[4]; - int linesize[4]; + uint8_t *data[4] + int linesize[4] uint8_t **extended_data - int format # Should be AVPixelFormat or AVSampleFormat - int key_frame # 0 or 1. + int format # Should be AVPixelFormat or AVSampleFormat + int key_frame # 0 or 1. AVPictureType pict_type - int interlaced_frame # 0 or 1. + int interlaced_frame # 0 or 1. int width int height @@ -304,10 +302,10 @@ cdef extern from "libavcodec/avcodec.h" nogil: int nb_side_data AVFrameSideData **side_data - int nb_samples # Audio samples - int sample_rate # Audio Sample rate - int channels # Number of audio channels - int channel_layout # Audio channel_layout + int nb_samples # Audio samples + int sample_rate # Audio Sample rate + int channels # Number of audio channels + int channel_layout # Audio channel_layout int64_t pts int64_t pkt_dts @@ -320,7 +318,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: int flags int decode_error_flags - cdef AVFrame* avcodec_alloc_frame() cdef struct AVPacket: @@ -337,7 +334,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: int64_t pos - cdef int avcodec_fill_audio_frame( AVFrame *frame, int nb_channels, @@ -367,8 +363,8 @@ cdef extern from "libavcodec/avcodec.h" nogil: int w int h int nb_colors - uint8_t *data[4]; - int linesize[4]; + uint8_t *data[4] + int linesize[4] AVSubtitleType type char *text char *ass @@ -402,7 +398,7 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef void avcodec_flush_buffers(AVCodecContext *ctx) - # TODO: avcodec_default_get_buffer is deprecated for avcodec_default_get_buffer2 in newer versions of FFmpeg + # TODO: avcodec_default_get_buffer is deprecated for avcodec_default_get_buffer2 in newer versions of FFmpeg cdef int avcodec_default_get_buffer(AVCodecContext *ctx, AVFrame *frame) cdef void avcodec_default_release_buffer(AVCodecContext *ctx, AVFrame *frame) @@ -440,7 +436,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: ) cdef void av_parser_close(AVCodecParserContext *s) - cdef struct AVCodecParameters: AVMediaType codec_type AVCodecID codec_id diff --git a/include/libavfilter/avfilter.pxd b/include/libavfilter/avfilter.pxd index e1fd42f45..dd3e91ddf 100644 --- a/include/libavfilter/avfilter.pxd +++ b/include/libavfilter/avfilter.pxd @@ -45,7 +45,7 @@ cdef extern from "libavfilter/avfilter.h" nogil: cdef AVFilter* avfilter_get_by_name(const char *name) cdef const AVFilter* av_filter_iterate(void **opaque) - cdef struct AVFilterLink # Defined later. + cdef struct AVFilterLink # Defined later. cdef struct AVFilterContext: diff --git a/include/libavfilter/avfiltergraph.pxd b/include/libavfilter/avfiltergraph.pxd index db9717a50..b773063f9 100644 --- a/include/libavfilter/avfiltergraph.pxd +++ b/include/libavfilter/avfiltergraph.pxd @@ -11,7 +11,6 @@ cdef extern from "libavfilter/avfilter.h" nogil: int pad_idx AVFilterInOut *next - cdef AVFilterGraph* avfilter_graph_alloc() cdef void avfilter_graph_free(AVFilterGraph **ptr) diff --git a/include/libavformat/avformat.pxd b/include/libavformat/avformat.pxd index 06029d9f9..a4d5991d8 100644 --- a/include/libavformat/avformat.pxd +++ b/include/libavformat/avformat.pxd @@ -16,7 +16,6 @@ cdef extern from "libavformat/avformat.h" nogil: cdef int AVSEEK_FLAG_ANY cdef int AVSEEK_FLAG_FRAME - cdef int AVIO_FLAG_WRITE cdef enum AVMediaType: @@ -207,10 +206,10 @@ cdef extern from "libavformat/avformat.h" nogil: # .. seealso:: FFmpeg's docs: :ffmpeg:`avformat_open_input` # cdef int avformat_open_input( - AVFormatContext **ctx, # NULL will allocate for you. + AVFormatContext **ctx, # NULL will allocate for you. char *filename, - AVInputFormat *format, # Can be NULL. - AVDictionary **options # Can be NULL. + AVInputFormat *format, # Can be NULL. + AVDictionary **options # Can be NULL. ) cdef int avformat_close_input(AVFormatContext **ctx) @@ -224,7 +223,7 @@ cdef extern from "libavformat/avformat.h" nogil: # cdef int avformat_write_header( AVFormatContext *ctx, - AVDictionary **options # Can be NULL + AVDictionary **options # Can be NULL ) cdef int av_write_trailer(AVFormatContext *ctx) @@ -269,7 +268,7 @@ cdef extern from "libavformat/avformat.h" nogil: cdef int avformat_find_stream_info( AVFormatContext *ctx, - AVDictionary **options, # Can be NULL. + AVDictionary **options, # Can be NULL. ) cdef AVStream* avformat_new_stream( diff --git a/include/libavutil/avutil.pxd b/include/libavutil/avutil.pxd index 50e6bfffd..d93016d3b 100644 --- a/include/libavutil/avutil.pxd +++ b/include/libavutil/avutil.pxd @@ -39,7 +39,6 @@ cdef extern from "libavutil/avutil.h" nogil: # This is nice, but only in FFMpeg: # AV_ROUND_PASS_MINMAX - cdef double M_PI cdef void* av_malloc(size_t size) @@ -67,9 +66,9 @@ cdef extern from "libavutil/avutil.h" nogil: # Rescales from one time base to another cdef int64_t av_rescale_q( - int64_t a, # time stamp - AVRational bq, # source time base - AVRational cq # target time base + int64_t a, # time stamp + AVRational bq, # source time base + AVRational cq # target time base ) # Rescale a 64-bit integer with specified rounding. @@ -78,14 +77,14 @@ cdef extern from "libavutil/avutil.h" nogil: int64_t a, int64_t b, int64_t c, - int r # should be AVRounding, but then we can't use bitwise logic. + int r # should be AVRounding, but then we can't use bitwise logic. ) cdef int64_t av_rescale_q_rnd( int64_t a, AVRational bq, AVRational cq, - int r # should be AVRounding, but then we can't use bitwise logic. + int r # should be AVRounding, but then we can't use bitwise logic. ) cdef int64_t av_rescale( @@ -146,7 +145,6 @@ cdef extern from "libavutil/pixdesc.h" nogil: int av_get_padded_bits_per_pixel(AVPixFmtDescriptor *pixdesc) - cdef extern from "libavutil/channel_layout.h" nogil: # Layouts. @@ -166,8 +164,6 @@ cdef extern from "libavutil/channel_layout.h" nogil: cdef char* av_get_channel_description(uint64_t channel) - - cdef extern from "libavutil/audio_fifo.h" nogil: cdef struct AVAudioFifo: diff --git a/include/libavutil/error.pxd b/include/libavutil/error.pxd index 8919c54b7..2122772e3 100644 --- a/include/libavutil/error.pxd +++ b/include/libavutil/error.pxd @@ -4,7 +4,6 @@ cdef extern from "libavutil/error.h" nogil: cdef int ENOMEM cdef int EAGAIN - cdef int AVERROR_BSF_NOT_FOUND cdef int AVERROR_BUG cdef int AVERROR_BUFFER_TOO_SMALL diff --git a/include/libavutil/samplefmt.pxd b/include/libavutil/samplefmt.pxd index 867367ee1..a26c6ecfd 100644 --- a/include/libavutil/samplefmt.pxd +++ b/include/libavutil/samplefmt.pxd @@ -12,7 +12,7 @@ cdef extern from "libavutil/samplefmt.h" nogil: AV_SAMPLE_FMT_S32P AV_SAMPLE_FMT_FLTP AV_SAMPLE_FMT_DBLP - AV_SAMPLE_FMT_NB # Number. + AV_SAMPLE_FMT_NB # Number. # Find by name. cdef AVSampleFormat av_get_sample_fmt(char* name) @@ -43,7 +43,6 @@ cdef extern from "libavutil/samplefmt.h" nogil: int align ) - cdef int av_samples_fill_arrays( uint8_t **audio_data, int *linesize, diff --git a/include/libswresample/swresample.pxd b/include/libswresample/swresample.pxd index d76b777a3..65b8314df 100644 --- a/include/libswresample/swresample.pxd +++ b/include/libswresample/swresample.pxd @@ -7,7 +7,6 @@ cdef extern from "libswresample/swresample.h" nogil: cdef char* swresample_configuration() cdef char* swresample_license() - cdef struct SwrContext: pass @@ -20,7 +19,7 @@ cdef extern from "libswresample/swresample.h" nogil: AVSampleFormat in_sample_fmt, int in_sample_rate, int log_offset, - void *log_ctx #logging context, can be NULL + void *log_ctx # logging context, can be NULL ) cdef int swr_convert( diff --git a/tests/test_filters.py b/tests/test_filters.py index 6201f59ba..31cbb46f6 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -42,7 +42,7 @@ def pull_until_blocked(graph): while True: try: frames.append(graph.pull()) - except av.utils.AVError as e: + except av.AVError as e: if e.errno != errno.EAGAIN: raise return frames From 8bfb5f6d6d9de874c1dd9ad5e3acdc0cbfb8247a Mon Sep 17 00:00:00 2001 From: David Plowman Date: Wed, 12 Apr 2023 16:13:26 +0100 Subject: [PATCH 151/846] Add max_b_frames property to codec context Allows an application to control the maximum number of B frames, irrespective of what other settings or preset is used. A test is also added, to check that it works as intended. --- av/video/codeccontext.pyx | 12 ++++++ tests/test_encode.py | 84 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.pyx index 33efc5d54..51103b787 100644 --- a/av/video/codeccontext.pyx +++ b/av/video/codeccontext.pyx @@ -161,3 +161,15 @@ cdef class VideoCodecContext(CodecContext): property coded_height: def __get__(self): return self.ptr.coded_height + + property max_b_frames: + """ + The maximum run of consecutive B frames when encoding a video. + + :type: int + """ + def __get__(self): + return self.ptr.max_b_frames + + def __set__(self, value): + self.ptr.max_b_frames = value diff --git a/tests/test_encode.py b/tests/test_encode.py index a293cbf83..e79bf539f 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -1,7 +1,10 @@ from fractions import Fraction from unittest import SkipTest +import io import math +import numpy as np + from av import AudioFrame, VideoFrame from av.audio.stream import AudioStream from av.video.stream import VideoStream @@ -291,3 +294,84 @@ def test_set_id_and_time_base(self): self.assertEqual(stream.time_base, None) stream.time_base = Fraction(1, 48000) self.assertEqual(stream.time_base, Fraction(1, 48000)) + + +def encode_file_with_max_b_frames(max_b_frames): + """ + Create an encoded video file (or file-like object) with the given + maximum run of B frames. + + max_b_frames: non-negative integer which is the maximum allowed run + of consecutive B frames. + + Returns: a file-like object. + """ + # Create a video file that is entirely arbitrary, but with the passed + # max_b_frames parameter. + file = io.BytesIO() + container = av.open(file, mode="w", format="mp4") + stream = container.add_stream("h264", rate=30) + stream.width = 640 + stream.height = 480 + stream.pix_fmt = "yuv420p" + stream.codec_context.gop_size = 15 + stream.codec_context.max_b_frames = max_b_frames + + for i in range(50): + array = np.empty((stream.height, stream.width, 3), dtype=np.uint8) + # This appears to hit a complexity "sweet spot" that makes the codec + # want to use B frames. + array[:, :] = (i, 0, 255 - i) + frame = av.VideoFrame.from_ndarray(array, format="rgb24") + for packet in stream.encode(frame): + container.mux(packet) + + for packet in stream.encode(): + container.mux(packet) + + container.close() + file.seek(0) + + return file + + +def max_b_frame_run_in_file(file): + """ + Count the maximum run of B frames in a file (or file-like object). + + file: the file or file-like object in which to count the maximum run + of B frames. The file should contain just one video stream. + + Returns: non-negative integer which is the maximum B frame run length. + """ + container = av.open(file) + stream = container.streams.video[0] + + max_b_frame_run = 0 + b_frame_run = 0 + for packet in container.demux(stream): + for frame in packet.decode(): + if frame.pict_type == av.video.frame.PictureType.B: + b_frame_run += 1 + else: + max_b_frame_run = max(max_b_frame_run, b_frame_run) + b_frame_run = 0 + + # Outside chance that the longest run was at the end of the file. + max_b_frame_run = max(max_b_frame_run, b_frame_run) + + container.close() + + return max_b_frame_run + + +class TestMaxBFrameEncoding(TestCase): + def test_max_b_frames(self): + """ + Test that we never get longer runs of B frames than we asked for with + the max_b_frames property. + """ + for max_b_frames in range(4): + file = encode_file_with_max_b_frames(max_b_frames) + actual_max_b_frames = max_b_frame_run_in_file(file) + self.assertTrue(actual_max_b_frames <= max_b_frames) From cf8a5c39c0a03ef9c99ed3da1ab1121eb5633996 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 16 Nov 2023 05:11:25 -0500 Subject: [PATCH 152/846] Prevent duplicate test runs on new PRs --- .github/workflows/tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9f880a6bb..c40b09460 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,11 +1,13 @@ name: tests on: push: + branches: main paths-ignore: - '**.md' - '**.rst' - '**.txt' pull_request: + branches: main paths-ignore: - '**.md' - '**.rst' From 39832c4f9eacc0319eb3dea8c48814cf678f53a6 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 20 Nov 2023 02:00:37 -0500 Subject: [PATCH 153/846] Allow using pathlib.Path for av.open --- av/container/core.pyx | 67 ++++++++++++++++++++++++++----------------- tests/test_open.py | 35 ++++++++++++++++++++++ 2 files changed, 76 insertions(+), 26 deletions(-) create mode 100644 tests/test_open.py diff --git a/av/container/core.pyx b/av/container/core.pyx index 61a201274..854aaff35 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -1,6 +1,7 @@ from cython.operator cimport dereference from libc.stdint cimport int64_t +from pathlib import Path import os import time @@ -334,10 +335,19 @@ cdef class Container: auto_bsf = flags.flag_property('AUTO_BSF') -def open(file, mode=None, format=None, options=None, - container_options=None, stream_options=None, - metadata_encoding='utf-8', metadata_errors='strict', - buffer_size=32768, timeout=None, io_open=None): +def open( + file, + mode=None, + format=None, + options=None, + container_options=None, + stream_options=None, + metadata_encoding="utf-8", + metadata_errors="strict", + buffer_size=32768, + timeout=None, + io_open=None, +): """open(file, mode='r', **kwargs) Main entrypoint to opening files/streams. @@ -385,34 +395,39 @@ def open(file, mode=None, format=None, options=None, `FFmpeg website `_. """ + if not (mode is None or (isinstance(mode, str) and mode == "r" or mode == "w")): + raise ValueError(f"mode must be 'r', 'w', or None, got: {mode}") + + if isinstance(file, str): + pass + elif isinstance(file, Path): + file = f"{file}" + elif mode is None: + mode = getattr(file, "mode", None) + if mode is None: - mode = getattr(file, 'mode', None) - if mode is None: - mode = 'r' + mode = "r" if isinstance(timeout, tuple): - open_timeout = timeout[0] - read_timeout = timeout[1] + if not len(timeout) == 2: + raise ValueError("timeout must be `float` or `tuple[float, float]`") + + open_timeout, read_timeout = timeout else: open_timeout = timeout read_timeout = timeout - if mode.startswith('r'): - return InputContainer( - _cinit_sentinel, file, format, options, - container_options, stream_options, - metadata_encoding, metadata_errors, - buffer_size, open_timeout, read_timeout, - io_open + if mode.startswith("r"): + return InputContainer(_cinit_sentinel, file, format, options, + container_options, stream_options, metadata_encoding, metadata_errors, + buffer_size, open_timeout, read_timeout, io_open, ) - if mode.startswith('w'): - if stream_options: - raise ValueError("Provide stream options via Container.add_stream(..., options={}).") - return OutputContainer( - _cinit_sentinel, file, format, options, - container_options, stream_options, - metadata_encoding, metadata_errors, - buffer_size, open_timeout, read_timeout, - io_open + + if stream_options: + raise ValueError( + "Provide stream options via Container.add_stream(..., options={})." ) - raise ValueError("mode must be 'r' or 'w'; got %r" % mode) + return OutputContainer(_cinit_sentinel, file, format, options, + container_options, stream_options, metadata_encoding, metadata_errors, + buffer_size, open_timeout, read_timeout, io_open, + ) diff --git a/tests/test_open.py b/tests/test_open.py new file mode 100644 index 000000000..b77312203 --- /dev/null +++ b/tests/test_open.py @@ -0,0 +1,35 @@ +from pathlib import Path + +import av + +from .common import TestCase, fate_suite + + +class TestOpen(TestCase): + def test_path_input(self): + path = Path(fate_suite("h264/interlaced_crop.mp4")) + self.assertIsInstance(path, Path) + + container = av.open(path) + self.assertIs(type(container), av.container.InputContainer) + + def test_str_input(self): + path = fate_suite("h264/interlaced_crop.mp4") + self.assertIs(type(path), str) + + container = av.open(path) + self.assertIs(type(container), av.container.InputContainer) + + def test_path_output(self): + path = Path(fate_suite("h264/interlaced_crop.mp4")) + self.assertIsInstance(path, Path) + + container = av.open(path, "w") + self.assertIs(type(container), av.container.OutputContainer) + + def test_str_output(self): + path = fate_suite("h264/interlaced_crop.mp4") + self.assertIs(type(path), str) + + container = av.open(path, "w") + self.assertIs(type(container), av.container.OutputContainer) From 895630bcae7c25a083c584ef0c08460e6d934fb4 Mon Sep 17 00:00:00 2001 From: zzjjbb <31069326+zzjjbb@users.noreply.github.com> Date: Tue, 21 Mar 2023 00:23:48 -0400 Subject: [PATCH 154/846] fix the doc of `time` page and `av.frame.Frame.dts` according to https://ffmpeg.org/doxygen/5.1/structAVFrame.html#aa52951f35ec9e303d3dfeb4b3e44248a --- av/frame.pyx | 4 +++- docs/api/time.rst | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/av/frame.pyx b/av/frame.pyx index 352b68157..50be5b8c9 100644 --- a/av/frame.pyx +++ b/av/frame.pyx @@ -66,7 +66,9 @@ cdef class Frame: property dts: """ - The decoding timestamp in :attr:`time_base` units for this frame. + The decoding timestamp copied from the :class:`~av.packet.Packet` that triggered returning this frame in :attr:`time_base` units. + + (if frame threading isn't used) This is also the Presentation time of this frame calculated from only :attr:`.Packet.dts` values without pts values. :type: int """ diff --git a/docs/api/time.rst b/docs/api/time.rst index 35e4cfc85..fd65de1a2 100644 --- a/docs/api/time.rst +++ b/docs/api/time.rst @@ -38,7 +38,7 @@ Attributes that represent time on those objects will be in that object's ``time_ >>> float(video.duration * video.time_base) 6.72 -:class:`.Packet` has a :attr:`.Packet.pts` ("presentation" time stamp), and :class:`.Frame` has a :attr:`.Frame.pts` and :attr:`.Frame.dts` ("presentation" and "decode" time stamps). Both have a ``time_base`` attribute, but it defaults to the time base of the object that handles them. For packets that is streams. For frames it is streams when decoding, and codec contexts when encoding (which is strange, but it is what it is). +:class:`.Packet` has a :attr:`.Packet.pts` and :attr:`.Packet.dts` ("presentation" and "decode" time stamps), and :class:`.Frame` has a :attr:`.Frame.pts` ("presentation" time stamp). Both have a ``time_base`` attribute, but it defaults to the time base of the object that handles them. For packets that is streams. For frames it is streams when decoding, and codec contexts when encoding (which is strange, but it is what it is). In many cases a stream has a time base of ``1 / frame_rate``, and then its frames have incrementing integers for times (0, 1, 2, etc.). Those frames take place at ``pts * time_base`` or ``0 / frame_rate``, ``1 / frame_rate``, ``2 / frame_rate``, etc.. From cfd46bb5a1324c3ee53b7aee84fa63b7f7a188d9 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 23 Nov 2023 21:05:29 -0500 Subject: [PATCH 155/846] Remove .editorconfig Random config files should not be in the project root. No one uses this anyware. --- .editorconfig | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index ea077de8c..000000000 --- a/.editorconfig +++ /dev/null @@ -1,16 +0,0 @@ -root = true - -[*] -charset = utf-8 -end_of_line = lf -indent_size = 4 -indent_style = space -insert_final_newline = true -trim_trailing_whitespace = true - -[*.yml] -indent_size = 2 - -[Makefile] -indent_size = unset -indent_style = tab From ad3f6965bdf9ab4720d64d315bfc2dce5beb7ad4 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 23 Nov 2023 20:54:26 -0500 Subject: [PATCH 156/846] Move isort config to pyproject.toml --- pyproject.toml | 10 ++++++++++ setup.cfg | 10 ---------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e9f294dd4..2d748e35f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,12 @@ [build-system] requires = ["setuptools", "wheel", "cython"] + +[tool.isort] +default_section = "THIRDPARTY" +from_first = true +known_first_party = ["av"] +line_length = 88 +lines_after_imports = 2 +multi_line_output = 3 +sections = ["FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] +skip = ["av/__init__.py"] diff --git a/setup.cfg b/setup.cfg index 1de247b85..c96fb624e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,16 +4,6 @@ ignore = E203,W503 max-line-length = 142 per-file-ignores = __init__.py: E402,F401 *.pyx,*.pxd: E211,E225,E227,E402,E999 -[isort] -default_section = THIRDPARTY -from_first = 1 -known_first_party = av -line_length = 88 -lines_after_imports = 2 -multi_line_output = 3 -sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER -skip = av/__init__.py - [metadata] license = BSD long_description = file: README.md From bdacaac87902eeb4401e0fe5ed4f822e435071d4 Mon Sep 17 00:00:00 2001 From: Ulrik Mikaelsson Date: Sat, 25 Nov 2023 10:26:10 +0100 Subject: [PATCH 157/846] Add encode_lazy method to CodecContext Some codecs (VP9) can both buffer _many_ frames, and take a long time encoding each frame. Accumulated, the last `encode(None)`-flush can end taking a long time >30s, without detectable progress. FFmpeg and many encoders themselves output one frame at a time, but PyAV currently buffer them all up into lists returned. This change adds a `encode_lazy` yielding frames as they are made ready. The change was benchmarked to also yield a net performance improvement. For both `encode()` and `encode_lazy` encoding really small (24x18) frames using the `mpeg4` encoder seem to take ~11% less time. --- av/codec/context.pxd | 2 +- av/codec/context.pyx | 28 ++++++++++++++++------------ tests/test_encode.py | 4 ++-- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/av/codec/context.pxd b/av/codec/context.pxd index 6cc8bd899..4a59b106e 100644 --- a/av/codec/context.pxd +++ b/av/codec/context.pxd @@ -40,6 +40,7 @@ cdef class CodecContext: # Used by both transcode APIs to setup user-land objects. # TODO: Remove the `Packet` from `_setup_decoded_frame` (because flushing # packets are bogus). It should take all info it needs from the context and/or stream. + cdef _prepare_and_time_rebase_frames_for_encode(self, Frame frame) cdef _prepare_frames_for_encode(self, Frame frame) cdef _setup_encoded_packet(self, Packet) cdef _setup_decoded_frame(self, Frame, Packet) @@ -50,7 +51,6 @@ cdef class CodecContext: # resampling audio to a higher rate but with fixed size frames), and the # send/recv buffer may be limited to a single frame. Ergo, we need to flush # the buffer as often as possible. - cdef _send_frame_and_recv(self, Frame frame) cdef _recv_packet(self) cdef _send_packet_and_recv(self, Packet packet) cdef _recv_frame(self) diff --git a/av/codec/context.pyx b/av/codec/context.pyx index bc8b35d57..112ab32e7 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -388,7 +388,7 @@ cdef class CodecContext: return packets - cdef _send_frame_and_recv(self, Frame frame): + def _send_frame_and_recv(self, Frame frame): cdef Packet packet @@ -397,14 +397,10 @@ cdef class CodecContext: res = lib.avcodec_send_frame(self.ptr, frame.ptr if frame is not None else NULL) err_check(res) - out = [] - while True: + packet = self._recv_packet() + while packet: + yield packet packet = self._recv_packet() - if packet: - out.append(packet) - else: - break - return out cdef _send_packet_and_recv(self, Packet packet): @@ -462,9 +458,7 @@ cdef class CodecContext: if not res: return packet - cpdef encode(self, Frame frame=None): - """Encode a list of :class:`.Packet` from the given :class:`.Frame`.""" - + cdef _prepare_and_time_rebase_frames_for_encode(self, Frame frame): if self.ptr.codec_type not in [lib.AVMEDIA_TYPE_VIDEO, lib.AVMEDIA_TYPE_AUDIO]: raise NotImplementedError('Encoding is only supported for audio and video.') @@ -478,13 +472,23 @@ cdef class CodecContext: if frame is not None: frame._rebase_time(self.ptr.time_base) + return frames + + cpdef encode(self, Frame frame=None): + """Encode a list of :class:`.Packet` from the given :class:`.Frame`.""" res = [] - for frame in frames: + for frame in self._prepare_and_time_rebase_frames_for_encode(frame): for packet in self._send_frame_and_recv(frame): self._setup_encoded_packet(packet) res.append(packet) return res + def encode_lazy(self, Frame frame=None): + for frame in self._prepare_and_time_rebase_frames_for_encode(frame): + for packet in self._send_frame_and_recv(frame): + self._setup_encoded_packet(packet) + yield packet + cdef _setup_encoded_packet(self, Packet packet): # We coerced the frame's time_base into the CodecContext's during encoding, # and FFmpeg copied the frame's pts/dts to the packet, so keep track of diff --git a/tests/test_encode.py b/tests/test_encode.py index e79bf539f..a9690bb5e 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -57,10 +57,10 @@ def write_rgb_rotate(output): ) frame.planes[0].update(image.tobytes()) - for packet in stream.encode(frame): + for packet in stream.encode_lazy(frame): output.mux(packet) - for packet in stream.encode(None): + for packet in stream.encode_lazy(None): output.mux(packet) From 1de6a50ee2fa6a79e4b4ee55390ffe453d1b2209 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 27 Nov 2023 11:44:55 -0500 Subject: [PATCH 158/846] Remove references to Libav --- av/audio/layout.pyx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/av/audio/layout.pyx b/av/audio/layout.pyx index 84801f47f..7c8a8c373 100644 --- a/av/audio/layout.pyx +++ b/av/audio/layout.pyx @@ -12,7 +12,6 @@ cdef AudioLayout get_audio_layout(int channels, uint64_t c_layout): return layout -# These are the defaults given by FFmpeg; Libav is different. # TODO: What about av_get_default_channel_layout(...)? cdef uint64_t default_layouts[17] default_layouts[0] = 0 @@ -31,10 +30,9 @@ default_layouts[12] = 0x0FFF default_layouts[13] = 0x1FFF default_layouts[14] = 0x3FFF default_layouts[15] = 0x7FFF -default_layouts[16] = 0xFFFF # FFmpeg has one here. +default_layouts[16] = 0xFFFF -# These are the descriptions as given by FFmpeg; Libav does not have them. cdef dict channel_descriptions = { 'FL': 'front left', 'FR': 'front right', From 8aa5fe74d5a581980dd6fde8167adfc75222495e Mon Sep 17 00:00:00 2001 From: Johan Jeppsson Karlin Date: Tue, 28 Nov 2023 10:35:58 +0100 Subject: [PATCH 159/846] Add color_range to CodecContext/Frame --- av/video/codeccontext.pyx | 13 ++++++++ av/video/frame.pyx | 26 ++++++++++++++++ av/video/reformatter.pxd | 3 +- av/video/reformatter.pyx | 55 +++++++++++++++++++++++----------- include/libavcodec/avcodec.pxd | 3 ++ include/libavutil/avutil.pxd | 20 +++++++++++++ tests/test_colorspace.py | 20 +++++++++++++ 7 files changed, 121 insertions(+), 19 deletions(-) create mode 100644 tests/test_colorspace.py diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.pyx index 51103b787..9d4ec13a8 100644 --- a/av/video/codeccontext.pyx +++ b/av/video/codeccontext.pyx @@ -162,6 +162,19 @@ cdef class VideoCodecContext(CodecContext): def __get__(self): return self.ptr.coded_height + @property + def color_range(self): + """ + Color range of context. + + Wraps :ffmpeg:`AVFrame.color_range`. + """ + def __get__(self): + return self.ptr.color_range + + def __set__(self, value): + self.ptr.color_range = value + property max_b_frames: """ The maximum run of consecutive B frames when encoding a video. diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 971b88ccc..3e4b11419 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -190,6 +190,32 @@ cdef class VideoFrame(Frame): def pict_type(self, value): self.ptr.pict_type = PictureType[value].value + @property + def colorspace(self): + """Colorspace of frame. + + Wraps :ffmpeg:`AVFrame.colorspace`. + + """ + return self.ptr.colorspace + + @colorspace.setter + def colorspace(self, value): + self.ptr.colorspace = value + + @property + def color_range(self): + """Color range of frame. + + Wraps :ffmpeg:`AVFrame.color_range`. + + """ + return self.ptr.color_range + + @color_range.setter + def color_range(self, value): + self.ptr.color_range = value + def reformat(self, *args, **kwargs): """reformat(width=None, height=None, format=None, src_colorspace=None, dst_colorspace=None, interpolation=None) diff --git a/av/video/reformatter.pxd b/av/video/reformatter.pxd index ee7467898..7682fab6d 100644 --- a/av/video/reformatter.pxd +++ b/av/video/reformatter.pxd @@ -9,4 +9,5 @@ cdef class VideoReformatter: cdef _reformat(self, VideoFrame frame, int width, int height, lib.AVPixelFormat format, int src_colorspace, - int dst_colorspace, int interpolation) + int dst_colorspace, int interpolation, + int src_color_range, int dst_color_range) diff --git a/av/video/reformatter.pyx b/av/video/reformatter.pyx index 1d3f08065..68208c220 100644 --- a/av/video/reformatter.pyx +++ b/av/video/reformatter.pyx @@ -22,7 +22,6 @@ Interpolation = define_enum('Interpolation', __name__, ( )) Colorspace = define_enum('Colorspace', __name__, ( - ('ITU709', lib.SWS_CS_ITU709), ('FCC', lib.SWS_CS_FCC), ('ITU601', lib.SWS_CS_ITU601), @@ -41,9 +40,14 @@ Colorspace = define_enum('Colorspace', __name__, ( )) +ColorRange = define_enum('ColorRange', __name__, ( + ('UNSPECIFIED', lib.AVCOL_RANGE_UNSPECIFIED, "Unspecified"), + ('MPEG', lib.AVCOL_RANGE_MPEG, "MPEG (limited) YUV range, 219*2^(n-8)"), + ('JPEG', lib.AVCOL_RANGE_JPEG, "JPEG (full) YUV range, 2^n-1"), + ('NB', lib.AVCOL_RANGE_NB, "Not part of ABI"), +)) cdef class VideoReformatter: - """An object for reformatting size and pixel format of :class:`.VideoFrame`. It is most efficient to have a reformatter object for each set of parameters @@ -57,7 +61,8 @@ cdef class VideoReformatter: def reformat(self, VideoFrame frame not None, width=None, height=None, format=None, src_colorspace=None, dst_colorspace=None, - interpolation=None): + interpolation=None, src_color_range=None, + dst_color_range=None): """Create a new :class:`VideoFrame` with the given width/height/format/colorspace. Returns the same frame untouched if nothing needs to be done to it. @@ -66,19 +71,25 @@ cdef class VideoReformatter: :param int height: New height, or ``None`` for the same height. :param format: New format, or ``None`` for the same format. :type format: :class:`.VideoFormat` or ``str`` - :param src_colorspace: Current colorspace, or ``None`` for ``DEFAULT``. + :param src_colorspace: Current colorspace, or ``None`` for the frame colorspace. :type src_colorspace: :class:`Colorspace` or ``str`` - :param dst_colorspace: Desired colorspace, or ``None`` for ``DEFAULT``. + :param dst_colorspace: Desired colorspace, or ``None`` for the frame colorspace. :type dst_colorspace: :class:`Colorspace` or ``str`` :param interpolation: The interpolation method to use, or ``None`` for ``BILINEAR``. :type interpolation: :class:`Interpolation` or ``str`` + :param src_color_range: Current color range, or ``None`` for the frame color range. + :type src_color_range: :class:`color range` or ``str`` + :param dst_color_range: Desired color range, or ``None`` for the frame color range. + :type dst_color_range: :class:`color range` or ``str`` """ cdef VideoFormat video_format = VideoFormat(format if format is not None else frame.format) - cdef int c_src_colorspace = (Colorspace[src_colorspace] if src_colorspace is not None else Colorspace.DEFAULT).value - cdef int c_dst_colorspace = (Colorspace[dst_colorspace] if dst_colorspace is not None else Colorspace.DEFAULT).value + cdef int c_src_colorspace = (Colorspace[src_colorspace].value if src_colorspace is not None else frame.colorspace) + cdef int c_dst_colorspace = (Colorspace[dst_colorspace].value if dst_colorspace is not None else frame.colorspace) cdef int c_interpolation = (Interpolation[interpolation] if interpolation is not None else Interpolation.BILINEAR).value + cdef int c_src_color_range = (ColorRange[src_color_range].value if src_color_range is not None else frame.color_range) + cdef int c_dst_color_range = (ColorRange[dst_color_range].value if dst_color_range is not None else frame.color_range) return self._reformat( frame, @@ -88,11 +99,14 @@ cdef class VideoReformatter: c_src_colorspace, c_dst_colorspace, c_interpolation, + c_src_color_range, + c_dst_color_range, ) cdef _reformat(self, VideoFrame frame, int width, int height, lib.AVPixelFormat dst_format, int src_colorspace, - int dst_colorspace, int interpolation): + int dst_colorspace, int interpolation, + int src_color_range, int dst_color_range): if frame.ptr.format < 0: raise ValueError("Frame does not have format set.") @@ -104,7 +118,8 @@ cdef class VideoReformatter: dst_format == src_format and width == frame.ptr.width and height == frame.ptr.height and - dst_colorspace == src_colorspace + dst_colorspace == src_colorspace and + src_color_range == dst_color_range ): return frame @@ -126,24 +141,28 @@ cdef class VideoReformatter: NULL ) - # We want to change the colorspace transforms. We do that by grabbing - # all of the current settings, changing a couple, and setting them all. - # We need a lot of state here. + # We want to change the colorspace/color_range transforms. + # We do that by grabbing all of the current settings, changing a + # couple, and setting them all. We need a lot of state here. cdef const int *inv_tbl cdef const int *tbl - cdef int src_range, dst_range, brightness, contrast, saturation + cdef int src_colorspace_range, dst_colorspace_range + cdef int brightness, contrast, saturation cdef int ret - if src_colorspace != dst_colorspace: + if ( + src_colorspace != dst_colorspace or + src_color_range != dst_color_range + ): with nogil: # Casts for const-ness, because Cython isn't expressive enough. ret = lib.sws_getColorspaceDetails( self.ptr, &inv_tbl, - &src_range, + &src_colorspace_range, &tbl, - &dst_range, + &dst_colorspace_range, &brightness, &contrast, &saturation @@ -164,9 +183,9 @@ cdef class VideoReformatter: ret = lib.sws_setColorspaceDetails( self.ptr, inv_tbl, - src_range, + src_color_range, tbl, - dst_range, + dst_color_range, brightness, contrast, saturation diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index e11e9c115..af7fe6150 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -204,6 +204,7 @@ cdef extern from "libavcodec/avcodec.h" nogil: int gop_size # The number of pictures in a group of pictures, or 0 for intra_only. int max_b_frames int has_b_frames + AVColorRange color_range # Audio. AVSampleFormat sample_fmt @@ -317,6 +318,8 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVDictionary *metadata int flags int decode_error_flags + AVColorRange color_range + AVColorSpace colorspace cdef AVFrame* avcodec_alloc_frame() diff --git a/include/libavutil/avutil.pxd b/include/libavutil/avutil.pxd index d93016d3b..09210bc0c 100644 --- a/include/libavutil/avutil.pxd +++ b/include/libavutil/avutil.pxd @@ -39,6 +39,26 @@ cdef extern from "libavutil/avutil.h" nogil: # This is nice, but only in FFMpeg: # AV_ROUND_PASS_MINMAX + cdef enum AVColorSpace: + AVCOL_SPC_RGB + AVCOL_SPC_BT709 + AVCOL_SPC_UNSPECIFIED + AVCOL_SPC_RESERVED + AVCOL_SPC_FCC + AVCOL_SPC_BT470BG + AVCOL_SPC_SMPTE170M + AVCOL_SPC_SMPTE240M + AVCOL_SPC_YCOCG + AVCOL_SPC_BT2020_NCL + AVCOL_SPC_BT2020_CL + AVCOL_SPC_NB + + cdef enum AVColorRange: + AVCOL_RANGE_UNSPECIFIED + AVCOL_RANGE_MPEG + AVCOL_RANGE_JPEG + AVCOL_RANGE_NB + cdef double M_PI cdef void* av_malloc(size_t size) diff --git a/tests/test_colorspace.py b/tests/test_colorspace.py new file mode 100644 index 000000000..13784fec3 --- /dev/null +++ b/tests/test_colorspace.py @@ -0,0 +1,20 @@ +from av.video.reformatter import ColorRange, Colorspace +import av + +from .common import TestCase, fate_suite + + +class TestColorSpace(TestCase): + def test_color_range(self): + container = av.open( + fate_suite("amv/MTV_high_res_320x240_sample_Penguin_Joke_MTV_from_WMV.amv") + ) + stream = container.streams.video[0] + + self.assertEqual(stream.color_range, None) + + for packet in container.demux(stream): + for frame in packet.decode(): + self.assertEqual(frame.color_range, ColorRange.JPEG) # a.k.a "pc" + self.assertEqual(frame.colorspace, Colorspace.ITU601) + return From 298b3b47c8ba169865229715181df0832e3e2e79 Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Sat, 2 Dec 2023 01:09:32 -0700 Subject: [PATCH 160/846] Set time_base for AudioResampler --- av/audio/resampler.pyx | 6 +- tests/test_audioresampler.py | 98 ++++++++++++++++++++++++++++++- tests/test_codec_context.py | 108 +++++++++++++++++++++++++++++++++++ tests/test_encode.py | 31 ++++++++++ 4 files changed, 239 insertions(+), 4 deletions(-) diff --git a/av/audio/resampler.pyx b/av/audio/resampler.pyx index bd5085704..29b6fb3f6 100644 --- a/av/audio/resampler.pyx +++ b/av/audio/resampler.pyx @@ -72,10 +72,14 @@ cdef class AudioResampler: # handle resampling with aformat filter # (similar to configure_output_audio_filter from ffmpeg) self.graph = av.filter.Graph() + extra_args = {} + if frame.time_base is not None: + extra_args["time_base"] = str(frame.time_base) abuffer = self.graph.add("abuffer", sample_rate=str(frame.sample_rate), sample_fmt=AudioFormat(frame.format).name, - channel_layout=frame.layout.name) + channel_layout=frame.layout.name, + **extra_args) aformat = self.graph.add("aformat", sample_rates=str(self.rate), sample_fmts=self.format.name, diff --git a/tests/test_audioresampler.py b/tests/test_audioresampler.py index 9b66968c1..2a9ae25c0 100644 --- a/tests/test_audioresampler.py +++ b/tests/test_audioresampler.py @@ -113,7 +113,7 @@ def test_pts_assertion_same_rate(self): oframes = resampler.resample(None) self.assertEqual(len(oframes), 0) - def test_pts_assertion_new_rate(self): + def test_pts_assertion_new_rate_up(self): resampler = AudioResampler("s16", "mono", 44100) # resample one frame @@ -131,15 +131,107 @@ def test_pts_assertion_new_rate(self): self.assertEqual(oframe.sample_rate, 44100) self.assertEqual(oframe.samples, 925) + iframe = AudioFrame("s16", "stereo", 1024) + iframe.sample_rate = 48000 + iframe.time_base = "1/48000" + iframe.pts = 1024 + + oframes = resampler.resample(iframe) + self.assertEqual(len(oframes), 1) + + oframe = oframes[0] + self.assertEqual(oframe.pts, 925) + self.assertEqual(oframe.time_base, Fraction(1, 44100)) + self.assertEqual(oframe.sample_rate, 44100) + self.assertEqual(oframe.samples, 941) + # flush oframes = resampler.resample(None) self.assertEqual(len(oframes), 1) oframe = oframes[0] - self.assertEqual(oframe.pts, 925) + self.assertEqual(oframe.pts, 941 + 925) self.assertEqual(oframe.time_base, Fraction(1, 44100)) self.assertEqual(oframe.sample_rate, 44100) - self.assertEqual(oframe.samples, 16) + self.assertEqual(oframe.samples, 15) + + def test_pts_assertion_new_rate_down(self): + resampler = AudioResampler("s16", "mono", 48000) + + # resample one frame + iframe = AudioFrame("s16", "stereo", 1024) + iframe.sample_rate = 44100 + iframe.time_base = "1/44100" + iframe.pts = 0 + + oframes = resampler.resample(iframe) + self.assertEqual(len(oframes), 1) + + oframe = oframes[0] + self.assertEqual(oframe.pts, 0) + self.assertEqual(oframe.time_base, Fraction(1, 48000)) + self.assertEqual(oframe.sample_rate, 48000) + self.assertEqual(oframe.samples, 1098) + + iframe = AudioFrame("s16", "stereo", 1024) + iframe.sample_rate = 44100 + iframe.time_base = "1/44100" + iframe.pts = 1024 + + oframes = resampler.resample(iframe) + self.assertEqual(len(oframes), 1) + + oframe = oframes[0] + self.assertEqual(oframe.pts, 1098) + self.assertEqual(oframe.time_base, Fraction(1, 48000)) + self.assertEqual(oframe.sample_rate, 48000) + self.assertEqual(oframe.samples, 1114) + + # flush + oframes = resampler.resample(None) + self.assertEqual(len(oframes), 1) + + oframe = oframes[0] + self.assertEqual(oframe.pts, 1114 + 1098) + self.assertEqual(oframe.time_base, Fraction(1, 48000)) + self.assertEqual(oframe.sample_rate, 48000) + self.assertEqual(oframe.samples, 18) + + def test_pts_assertion_new_rate_fltp(self): + resampler = AudioResampler("fltp", "mono", 8000, 1024) + + # resample one frame + iframe = AudioFrame("s16", "mono", 1024) + iframe.sample_rate = 8000 + iframe.time_base = "1/1000" + iframe.pts = 0 + + oframes = resampler.resample(iframe) + self.assertEqual(len(oframes), 1) + + oframe = oframes[0] + self.assertEqual(oframe.pts, 0) + self.assertEqual(oframe.time_base, Fraction(1, 8000)) + self.assertEqual(oframe.sample_rate, 8000) + self.assertEqual(oframe.samples, 1024) + + iframe = AudioFrame("s16", "mono", 1024) + iframe.sample_rate = 8000 + iframe.time_base = "1/1000" + iframe.pts = 8192 + + oframes = resampler.resample(iframe) + self.assertEqual(len(oframes), 1) + + oframe = oframes[0] + self.assertEqual(oframe.pts, 65536) + self.assertEqual(oframe.time_base, Fraction(1, 8000)) + self.assertEqual(oframe.sample_rate, 8000) + self.assertEqual(oframe.samples, 1024) + + # flush + oframes = resampler.resample(None) + self.assertEqual(len(oframes), 0) def test_pts_missing_time_base(self): resampler = AudioResampler("s16", "mono", 44100) diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index ee9193c81..d4ba2fdd2 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -317,6 +317,8 @@ def test_encoding_aac(self): def test_encoding_mp2(self): self.audio_encoding("mp2") + maxDiff = None + def audio_encoding(self, codec_name): try: codec = Codec(codec_name, "w") @@ -350,17 +352,123 @@ def audio_encoding(self, codec_name): samples = 0 packet_sizes = [] + pts_expected = [ + 0, + 1098, + 2212, + 3327, + 4441, + 5556, + 6670, + 7785, + 8900, + 10014, + 11129, + 12243, + 13358, + 14472, + 15587, + 16701, + 17816, + 18931, + 20045, + 21160, + 22274, + 23389, + 24503, + 25618, + 26732, + 27847, + 28962, + 30076, + 31191, + 32305, + 33420, + 34534, + 35649, + 36763, + 37878, + 38993, + 40107, + 41222, + 42336, + 43451, + 44565, + 45680, + 46795, + 47909, + 49024, + 50138, + 51253, + 52367, + 53482, + 54596, + 55711, + 56826, + 57940, + 59055, + 60169, + 61284, + 62398, + 63513, + 64627, + 65742, + 66857, + 67971, + 69086, + 70200, + 71315, + 72429, + 73544, + 74658, + 75773, + 76888, + 78002, + 79117, + 80231, + 81346, + 82460, + 83575, + 84689, + 85804, + 86919, + 88033, + 89148, + 90262, + 91377, + 92491, + 93606, + 94720, + 95835, + 96950, + 98064, + 99179, + 100293, + 101408, + ] + if codec_name == "aac": + pts_expected_encoded = list((-1024 + n * 1024 for n in range(101))) + elif codec_name == "mp2": + pts_expected_encoded = list((-481 + n * 1152 for n in range(89))) + else: + pts_expected_encoded = pts_expected.copy() with open(path, "wb") as f: for frame in iter_frames(container, audio_stream): resampled_frames = resampler.resample(frame) for resampled_frame in resampled_frames: + self.assertEqual(resampled_frame.pts, pts_expected.pop(0)) + self.assertEqual(resampled_frame.time_base, Fraction(1, 48000)) samples += resampled_frame.samples for packet in ctx.encode(resampled_frame): + self.assertEqual(packet.pts, pts_expected_encoded.pop(0)) + self.assertEqual(packet.time_base, Fraction(1, 48000)) packet_sizes.append(packet.size) f.write(packet) for packet in ctx.encode(None): + self.assertEqual(packet.pts, pts_expected_encoded.pop(0)) + self.assertEqual(packet.time_base, Fraction(1, 48000)) packet_sizes.append(packet.size) f.write(packet) diff --git a/tests/test_encode.py b/tests/test_encode.py index a9690bb5e..e0a90db18 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -280,6 +280,37 @@ def test_stream_index(self): self.assertIs(apacket.stream, astream) self.assertEqual(apacket.stream_index, 1) + def test_stream_audio_resample(self): + with av.open(self.sandboxed("output.mov"), "w") as output: + vstream = output.add_stream("mpeg4", 24) + vstream.pix_fmt = "yuv420p" + vstream.width = 320 + vstream.height = 240 + + astream = output.add_stream("aac", sample_rate=8000, layout="mono") + frame_size = 512 + + pts_expected = [-1024, 0, 512, 1024, 1536, 2048, 2560] + pts = 0 + for i in range(15): + aframe = AudioFrame("s16", "mono", samples=frame_size) + aframe.sample_rate = 8000 + aframe.time_base = Fraction(1, 1000) + aframe.pts = pts + aframe.dts = pts + pts += 32 + apackets = astream.encode(aframe) + if apackets: + apacket = apackets[0] + self.assertEqual(apacket.pts, pts_expected.pop(0)) + self.assertEqual(apacket.time_base, Fraction(1, 8000)) + + apackets = astream.encode(None) + if apackets: + apacket = apackets[0] + self.assertEqual(apacket.pts, pts_expected.pop(0)) + self.assertEqual(apacket.time_base, Fraction(1, 8000)) + def test_set_id_and_time_base(self): with av.open(self.sandboxed("output.mov"), "w") as output: stream = output.add_stream("mp2") From c8b535a5fdd893944b3545c490b8964fe16d375c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 14 Dec 2023 02:15:45 -0500 Subject: [PATCH 161/846] Update authors --- AUTHORS.rst | 106 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 72 insertions(+), 34 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 94601edd3..ea72e7cf6 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -6,41 +6,79 @@ All contributors (by number of commits): - Mike Boers ; `@mikeboers `_ * Jeremy Lainé ; `@jlaine `_ -* Mark Reid ; `@markreidvfx `_ -- Vidar Tonaas Fauske ; `@vidartf `_ -- Billy Shambrook ; `@billyshambrook `_ -- Casper van der Wel -- Tadas Dailyda +- Mark Reid ; `@markreidvfx `_ -* Xinran Xu ; `@xxr3376 `_ -* Dan Allan ; `@danielballan `_ -* Alireza Davoudi ; `@adavoudi `_ -* Moritz Kassner ; `@mkassner `_ -* Thomas A Caswell ; `@tacaswell `_ -* Ulrik Mikaelsson ; `@rawler `_ -* Wel C. van der -* Will Patera ; `@willpatera `_ +* Vidar Tonaas Fauske ; `@vidartf `_ +* Billy Shambrook ; `@billyshambrook `_ +* Casper van der Wel +* Philip de Nier +* Tadas Dailyda +* WyattBlue +* Justin Wong <46082645+uvjustin@users.noreply.github.com> -- rutsh +- Alba Mendez +- Xinran Xu ; `@xxr3376 `_ +- Dan Allan ; `@danielballan `_ +- Dave Johansen +- Mark Harfouche - Christoph Rackwitz -- Johannes Erdfelt -- Karl Litterfeldt ; `@litterfeldt `_ -- Martin Larralde -- Miles Kaufmann -- Radek Senfeld ; `@radek-senfeld `_ -- Ian Lee -- Arthur Barros -- Gemfield -- mephi42 -- Manuel Goacolou -- Ömer Sezgin Uğurlu -- Orivej Desh -- Brendan Long ; `@brendanlong `_ -- Tom Flanagan -- Tim O'Shea -- Tim Ahpee -- Jonas Tingeborn -- Vasiliy Kotov -- Koichi Akabe -- David Joy +- Alireza Davoudi ; `@adavoudi `_ +- Jonathan Drolet +- Moritz Kassner ; `@mkassner `_ +- Thomas A Caswell ; `@tacaswell `_ +- Ulrik Mikaelsson ; `@rawler `_ +- Wel C. van der +- Will Patera ; `@willpatera `_ + +* rutsh +* Felix Vollmer +* Santiago Castro +* Christian Clauss +* Ihor Liubymov +* Johannes Erdfelt +* Karl Litterfeldt ; `@litterfeldt `_ +* Martin Larralde +* Simon-Martin Schröder +* mephi42 +* Miles Kaufmann +* Pablo Prietz +* Radek Senfeld ; `@radek-senfeld `_ +* Benjamin Chrétien <2742231+bchretien@users.noreply.github.com> +* Marc Mueller <30130371+cdce8p@users.noreply.github.com> +* zzjjbb <31069326+zzjjbb@users.noreply.github.com> +* Hanz <40712686+HanzCEO@users.noreply.github.com> +* Ian Lee +* Ryan Huang +* Arthur Barros +* Carlos Ruiz +* David Plowman +* Maxime Desroches +* egao1980 +* Eric Kalosa-Kenyon +* Gemfield +* Jonathan Martin +* Johan Jeppsson Karlin +* Philipp Klaus +* Mattias Wadman +* Manuel Goacolou +* Julian Schweizer +* Ömer Sezgin Uğurlu +* Orivej Desh +* Philipp Krähenbühl +* ramoncaldeira +* Santiago Castro +* Kengo Sawatsu +* FirefoxMetzger +* Brendan Long ; `@brendanlong `_ +* Семён Марьясин +* Stephen.Y +* Tom Flanagan +* Tim O'Shea +* Tim Ahpee +* Jonas Tingeborn +* Pino Toscano +* Ulrik Mikaelsson +* Vasiliy Kotov +* Koichi Akabe +* David Joy From 11142a40de23921f26302c43d24cc67a2893d412 Mon Sep 17 00:00:00 2001 From: hyenal Date: Wed, 20 Dec 2023 03:45:37 +0000 Subject: [PATCH 162/846] Expose DISPLAYMATRIX side data Co-authored-by: Sebastien Ehrhardt Co-authored-by: WyattBlue --- av/stream.pxd | 3 +++ av/stream.pyx | 29 +++++++++++++++++++++++ include/libavcodec/avcodec.pxd | 40 ++++++++++++++++++++++++++++++++ include/libavformat/avformat.pxd | 4 ++++ include/libavutil/avutil.pxd | 5 +++- tests/test_streams.py | 13 +++++++++++ 6 files changed, 93 insertions(+), 1 deletion(-) diff --git a/av/stream.pxd b/av/stream.pxd index c847f641e..8ebda5704 100644 --- a/av/stream.pxd +++ b/av/stream.pxd @@ -12,6 +12,8 @@ cdef class Stream: # Stream attributes. cdef readonly Container container cdef readonly dict metadata + cdef readonly int nb_side_data + cdef readonly dict side_data # CodecContext attributes. cdef readonly CodecContext codec_context @@ -19,6 +21,7 @@ cdef class Stream: # Private API. cdef _init(self, Container, lib.AVStream*, CodecContext) cdef _finalize_for_output(self) + cdef _get_side_data(self, lib.AVStream *stream) cdef _set_time_base(self, value) cdef _set_id(self, value) diff --git a/av/stream.pyx b/av/stream.pyx index f9b6d7ec5..f65cee609 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -1,7 +1,9 @@ import warnings +from libc.stdint cimport int32_t cimport libav as lib +from av.enum cimport define_enum from av.error cimport err_check from av.packet cimport Packet from av.utils cimport ( @@ -17,6 +19,12 @@ from av.deprecation import AVDeprecationWarning cdef object _cinit_bypass_sentinel = object() +# If necessary more can be added from +# https://ffmpeg.org/doxygen/trunk/group__lavc__packet.html#ga9a80bfcacc586b483a973272800edb97 +SideData = define_enum("SideData", __name__, ( + ("DISPLAYMATRIX", lib.AV_PKT_DATA_DISPLAYMATRIX, "Display Matrix"), +)) + cdef Stream wrap_stream(Container container, lib.AVStream *c_stream, CodecContext codec_context): """Build an av.Stream for an existing AVStream. @@ -79,6 +87,8 @@ cdef class Stream: if self.codec_context: self.codec_context.stream_index = stream.index + self.nb_side_data, self.side_data = self._get_side_data(stream) + self.metadata = avdict_to_dict( stream.metadata, encoding=self.container.metadata_encoding, @@ -103,6 +113,11 @@ cdef class Stream: AVDeprecationWarning ) + if name == "side_data": + return self.side_data + elif name == "nb_side_data": + return self.nb_side_data + # Convenience getter for codec context properties. if self.codec_context is not None: return getattr(self.codec_context, name) @@ -167,6 +182,20 @@ cdef class Stream: return self.codec_context.decode(packet) + cdef _get_side_data(self, lib.AVStream *stream): + # Get DISPLAYMATRIX SideDate from a lib.AVStream object. + # Returns: tuple[int, dict[str, Any]] + + nb_side_data = stream.nb_side_data + side_data = {} + + for i in range(nb_side_data): + # Based on: https://www.ffmpeg.org/doxygen/trunk/dump_8c_source.html#l00430 + if stream.side_data[i].type == lib.AV_PKT_DATA_DISPLAYMATRIX: + side_data["DISPLAYMATRIX"] = lib.av_display_rotation_get(stream.side_data[i].data) + + return nb_side_data, side_data + property id: """ The format-specific ID of this stream. diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index af7fe6150..1daeac15b 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -258,6 +258,46 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef int AV_NUM_DATA_POINTERS + cdef enum AVPacketSideDataType: + AV_PKT_DATA_PALETTE + AV_PKT_DATA_NEW_EXTRADATA + AV_PKT_DATA_PARAM_CHANGE + AV_PKT_DATA_H263_MB_INFO + AV_PKT_DATA_REPLAYGAIN + AV_PKT_DATA_DISPLAYMATRIX + AV_PKT_DATA_STEREO3D + AV_PKT_DATA_AUDIO_SERVICE_TYPE + AV_PKT_DATA_QUALITY_STATS + AV_PKT_DATA_FALLBACK_TRACK + AV_PKT_DATA_CPB_PROPERTIES + AV_PKT_DATA_SKIP_SAMPLES + AV_PKT_DATA_JP_DUALMONO + AV_PKT_DATA_STRINGS_METADATA + AV_PKT_DATA_SUBTITLE_POSITION + AV_PKT_DATA_MATROSKA_BLOCKADDITIONAL + AV_PKT_DATA_WEBVTT_IDENTIFIER + AV_PKT_DATA_WEBVTT_SETTINGS + AV_PKT_DATA_METADATA_UPDATE + AV_PKT_DATA_MPEGTS_STREAM_ID + AV_PKT_DATA_MASTERING_DISPLAY_METADATA + AV_PKT_DATA_SPHERICAL + AV_PKT_DATA_CONTENT_LIGHT_LEVEL + AV_PKT_DATA_A53_CC + AV_PKT_DATA_ENCRYPTION_INIT_INFO + AV_PKT_DATA_ENCRYPTION_INFO + AV_PKT_DATA_AFD + AV_PKT_DATA_PRFT + AV_PKT_DATA_ICC_PROFILE + AV_PKT_DATA_DOVI_CONF + AV_PKT_DATA_S12M_TIMECODE + AV_PKT_DATA_DYNAMIC_HDR10_PLUS + AV_PKT_DATA_NB + + cdef struct AVPacketSideData: + uint8_t *data; + size_t size; + AVPacketSideDataType type; + cdef enum AVFrameSideDataType: AV_FRAME_DATA_PANSCAN AV_FRAME_DATA_A53_CC diff --git a/include/libavformat/avformat.pxd b/include/libavformat/avformat.pxd index a4d5991d8..224e76b4e 100644 --- a/include/libavformat/avformat.pxd +++ b/include/libavformat/avformat.pxd @@ -47,6 +47,10 @@ cdef extern from "libavformat/avformat.h" nogil: AVRational r_frame_rate AVRational sample_aspect_ratio + int nb_side_data + AVPacketSideData *side_data + + # http://ffmpeg.org/doxygen/trunk/structAVIOContext.html cdef struct AVIOContext: unsigned char* buffer diff --git a/include/libavutil/avutil.pxd b/include/libavutil/avutil.pxd index 09210bc0c..08f973cac 100644 --- a/include/libavutil/avutil.pxd +++ b/include/libavutil/avutil.pxd @@ -1,9 +1,12 @@ -from libc.stdint cimport int64_t, uint8_t, uint64_t +from libc.stdint cimport int64_t, uint8_t, uint64_t, int32_t cdef extern from "libavutil/mathematics.h" nogil: pass +cdef extern from "libavutil/display.h" nogil: + cdef double av_display_rotation_get(const int32_t matrix[9]) + cdef extern from "libavutil/rational.h" nogil: cdef int av_reduce(int *dst_num, int *dst_den, int64_t num, int64_t den, int64_t max) diff --git a/tests/test_streams.py b/tests/test_streams.py index b2871d43d..a4e3eae5e 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -24,3 +24,16 @@ def test_selection(self): self.assertEqual([video], container.streams.get(video=(0,))) # TODO: Find something in the fate suite with video, audio, and subtitles. + + def test_noside_data(self): + container = av.open(fate_suite("h264/interlaced_crop.mp4")) + video = container.streams.video[0] + + self.assertEqual(video.nb_side_data, 0) + + def test_side_data(self): + container = av.open(fate_suite("mov/displaymatrix.mov")) + video = container.streams.video[0] + + self.assertEqual(video.nb_side_data, 1) + self.assertEqual(video.side_data["DISPLAYMATRIX"], -90.0) From 16e2710e9daac54fec60d44f9071908d37f51e06 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 22 Dec 2023 18:21:47 -0500 Subject: [PATCH 163/846] Acknowledge recent changes in docs Update documentation to what we do now. Point to active url --- README.md | 4 ++-- docs/Makefile | 2 +- docs/conf.py | 4 ++-- docs/overview/installation.rst | 15 ++++++--------- 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index b6bbe56c9..b4dedceed 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ Have fun, [read the docs][docs], [come chat with us][gitter], and good luck! [conda-badge]: https://img.shields.io/conda/vn/conda-forge/av.svg?colorB=CCB39A [conda]: https://anaconda.org/conda-forge/av [docs-badge]: https://img.shields.io/badge/docs-on%20pyav.org-blue.svg -[docs]: http://pyav.org/docs +[docs]: https://pyav.basswood-io.com [gitter-badge]: https://img.shields.io/gitter/room/nwjs/nw.js.svg?logo=gitter&colorB=cc2b5e [gitter]: https://app.gitter.im/#/room/#PyAV-Org_User-Help:gitter.im [pypi-badge]: https://img.shields.io/pypi/v/av.svg?colorB=CCB39A @@ -75,6 +75,6 @@ Have fun, [read the docs][docs], [come chat with us][gitter], and good luck! [github-tests]: https://github.com/PyAV-Org/PyAV/actions?workflow=tests [github]: https://github.com/PyAV-Org/PyAV -[ffmpeg]: http://ffmpeg.org/ +[ffmpeg]: https://ffmpeg.org/ [conda-forge]: https://conda-forge.github.io/ [conda-install]: https://docs.conda.io/projects/conda/en/latest/user-guide/install/index.html diff --git a/docs/Makefile b/docs/Makefile index f26ef7dc3..b1791159f 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -38,5 +38,5 @@ open: open _build/html/index.html upload: - rsync -avxP --delete _build/html/ pyav.org:/srv/pyav.org/www/httpdocs/docs/develop/ + rsync -avxP --delete _build/html/ root@basswood-io.com:/var/www/pyav diff --git a/docs/conf.py b/docs/conf.py index cc97c4397..11cc7cd2e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -72,8 +72,8 @@ master_doc = 'index' # General information about the project. -project = u'PyAV' -copyright = u'2017, Mike Boers' +project = 'PyAV' +copyright = '2023, The PyAV Team' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/docs/overview/installation.rst b/docs/overview/installation.rst index e97da901e..67b2655d6 100644 --- a/docs/overview/installation.rst +++ b/docs/overview/installation.rst @@ -11,7 +11,7 @@ Since release 8.0.0 binary wheels are provided on PyPI for Linux, Mac and Window pip install av -Currently FFmpeg 5.1.3 is used with the following features enabled for all platforms: +Currently FFmpeg 6.0 is used with the following features enabled for all platforms: - fontconfig - gmp @@ -77,10 +77,10 @@ and a few other tools in general: - Python's development headers -Mac OS X -^^^^^^^^ +MacOS +^^^^^ -On **Mac OS X**, Homebrew_ saves the day:: +On **MacOS**, Homebrew_ saves the day:: brew install ffmpeg pkg-config @@ -115,7 +115,7 @@ Building from the latest source :: # Get PyAV from GitHub. - git clone git@github.com:PyAV-Org/PyAV.git + git clone https://github.com/PyAV-Org/PyAV.git cd PyAV # Prep a virtualenv. @@ -129,11 +129,8 @@ Building from the latest source # Build PyAV. make - # or - python setup.py build_ext --inplace - -On **Mac OS X** you may have issues with regards to Python expecting gcc but finding clang. Try to export the following before installation:: +On **MacOS** you may have issues with regards to Python expecting gcc but finding clang. Try to export the following before installation:: export ARCHFLAGS=-Wno-error=unused-command-line-argument-hard-error-in-future From 0afc238a59ab7f7d966c3d0be6d63ed0af702a52 Mon Sep 17 00:00:00 2001 From: Mark Harfouche Date: Mon, 25 Dec 2023 01:43:37 -0500 Subject: [PATCH 164/846] Create a path toward sharing memory from a numpy buffer This avoids the use of hstack, which inevitably copies the data to a new memory location. The speed up is small, but measurable. I can go from 185 fps to 200 fps when decoding a ~3000x2000 video --- av/video/frame.pxd | 1 + av/video/frame.pyx | 105 ++++++++++++++++++++++++++++++++--- av/video/plane.pxd | 4 ++ av/video/plane.pyx | 23 ++++++-- include/libavutil/avutil.pxd | 12 ++++ tests/test_videoframe.py | 98 ++++++++++++++++++++++++++++++++ 6 files changed, 232 insertions(+), 11 deletions(-) diff --git a/av/video/frame.pxd b/av/video/frame.pxd index 709775e55..9e97d946f 100644 --- a/av/video/frame.pxd +++ b/av/video/frame.pxd @@ -11,6 +11,7 @@ cdef class VideoFrame(Frame): # This is the buffer that is used to back everything in the AVFrame. # We don't ever actually access it directly. cdef uint8_t *_buffer + cdef object _np_buffer cdef VideoReformatter reformatter diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 3e4b11419..9498938e6 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -6,7 +6,7 @@ from av.enum cimport define_enum from av.error cimport err_check from av.utils cimport check_ndarray, check_ndarray_shape from av.video.format cimport get_pix_fmt, get_video_format -from av.video.plane cimport VideoPlane +from av.video.plane cimport VideoPlane, YUVPlanes cdef object _cinit_bypass_sentinel @@ -118,6 +118,8 @@ cdef class VideoFrame(Frame): # The `self._buffer` member is only set if *we* allocated the buffer in `_init`, # as opposed to a buffer allocated by a decoder. lib.av_freep(&self._buffer) + # Let go of the reference from the numpy buffers if we made one + self._np_buffer = None def __repr__(self): return '' % ( @@ -150,7 +152,6 @@ cdef class VideoFrame(Frame): cdef int plane_count = 0 while plane_count < max_plane_count and self.ptr.extended_data[plane_count]: plane_count += 1 - return tuple([VideoPlane(self, i) for i in range(plane_count)]) property width: @@ -290,11 +291,21 @@ cdef class VideoFrame(Frame): if frame.format.name in ('yuv420p', 'yuvj420p'): assert frame.width % 2 == 0 assert frame.height % 2 == 0 - return np.hstack(( - useful_array(frame.planes[0]), - useful_array(frame.planes[1]), - useful_array(frame.planes[2]) - )).reshape(-1, frame.width) + # Fast path for the case that the entire YUV data is contiguous + if ( + frame.planes[0].line_size == frame.planes[0].width and + frame.planes[1].line_size == frame.planes[1].width and + frame.planes[2].line_size == frame.planes[2].width + ): + yuv_planes = YUVPlanes(frame, 0) + return useful_array(yuv_planes).reshape(frame.height * 3 // 2, frame.width) + else: + # Otherwise, we need to copy the data through the use of np.hstack + return np.hstack(( + useful_array(frame.planes[0]), + useful_array(frame.planes[1]), + useful_array(frame.planes[2]) + )).reshape(-1, frame.width) elif frame.format.name in ('yuv444p', 'yuvj444p'): return np.hstack(( useful_array(frame.planes[0]), @@ -369,6 +380,86 @@ cdef class VideoFrame(Frame): return frame + @staticmethod + def from_numpy_buffer(array, format="rgb24"): + if format in ("rgb24", "bgr24"): + check_ndarray(array, 'uint8', 3) + check_ndarray_shape(array, array.shape[2] == 3) + height, width = array.shape[:2] + elif format in ("gray", "gray8", "rgb8", "bgr8"): + check_ndarray(array, "uint8", 2) + height, width = array.shape[:2] + elif format in ("yuv420p", "yuvj420p", "nv12"): + check_ndarray(array, "uint8", 2) + check_ndarray_shape(array, array.shape[0] % 3 == 0) + check_ndarray_shape(array, array.shape[1] % 2 == 0) + height, width = array.shape[:2] + height = height // 6 * 4 + else: + raise ValueError(f"Conversion from numpy array with format `{format}` is not yet supported") + + if not array.flags["C_CONTIGUOUS"]: + raise ValueError("provided array must be C_CONTIGUOUS") + + frame = alloc_video_frame() + frame._image_fill_pointers_numpy(array, width, height, format) + return frame + + def _image_fill_pointers_numpy(self, buffer, width, height, format): + cdef lib.AVPixelFormat c_format + cdef uint8_t * c_ptr + cdef size_t c_data + + # If you want to use the numpy notation + # then you need to include the following two lines at the top of the file + # cimport numpy as cnp + # cnp.import_array() + # And add the numpy include directories to the setup.py files + # hint np.get_include() + # cdef cnp.ndarray[ + # dtype=cnp.uint8_t, ndim=1, + # negative_indices=False, mode='c'] c_buffer + # c_buffer = buffer.reshape(-1) + # c_ptr = &c_buffer[0] + # c_ptr = ((buffer.ctypes.data)) + + # Using buffer.ctypes.data helps avoid any kind of + # usage of the c-api from numpy, which avoid the need to add numpy + # as a compile time dependency + # Without this double cast, you get an error that looks like + # c_ptr = (buffer.ctypes.data) + # TypeError: expected bytes, int found + c_data = buffer.ctypes.data + c_ptr = (c_data) + c_format = get_pix_fmt(format) + lib.av_freep(&self._buffer) + + # Hold on to a reference for the numpy buffer + # so that it doesn't get accidentally garbage collected + self._np_buffer = buffer + self.ptr.format = c_format + self.ptr.width = width + self.ptr.height = height + res = lib.av_image_fill_linesizes( + self.ptr.linesize, + self.ptr.format, + width, + ) + if res: + err_check(res) + + res = lib.av_image_fill_pointers( + self.ptr.data, + self.ptr.format, + self.ptr.height, + c_ptr, + self.ptr.linesize, + ) + + if res: + err_check(res) + self._init_user_attributes() + @staticmethod def from_ndarray(array, format='rgb24'): """ diff --git a/av/video/plane.pxd b/av/video/plane.pxd index f9abf22b6..a74eea206 100644 --- a/av/video/plane.pxd +++ b/av/video/plane.pxd @@ -6,3 +6,7 @@ cdef class VideoPlane(Plane): cdef readonly size_t buffer_size cdef readonly unsigned int width, height + + +cdef class YUVPlanes(VideoPlane): + pass diff --git a/av/video/plane.pyx b/av/video/plane.pyx index 6f1286ca3..de8959e67 100644 --- a/av/video/plane.pyx +++ b/av/video/plane.pyx @@ -2,11 +2,9 @@ from av.video.frame cimport VideoFrame cdef class VideoPlane(Plane): - def __cinit__(self, VideoFrame frame, int index): - # The palette plane has no associated component or linesize; set fields manually - if frame.format.name == 'pal8' and index == 1: + if frame.format.name == "pal8" and index == 1: self.width = 256 self.height = 1 self.buffer_size = 256 * 4 @@ -19,7 +17,7 @@ cdef class VideoPlane(Plane): self.height = component.height break else: - raise RuntimeError('could not find plane %d of %r' % (index, frame.format)) + raise RuntimeError("could not find plane %d of %r" % (index, frame.format)) # Sometimes, linesize is negative (and that is meaningful). We are only # insisting that the buffer size be based on the extent of linesize, and @@ -37,3 +35,20 @@ cdef class VideoPlane(Plane): """ def __get__(self): return self.frame.ptr.linesize[self.index] + + +cdef class YUVPlanes(VideoPlane): + def __cinit__(self, VideoFrame frame, int index): + if index != 0: + raise RuntimeError("YUVPlanes only supports index 0") + if frame.format.name not in ("yuvj420p", "yuv420p"): + raise RuntimeError("YUVPlane only supports yuv420p and yuvj420p") + if frame.ptr.linesize[0] < 0: + raise RuntimeError("YUVPlane only supports positive linesize") + self.width = frame.width + self.height = frame.height * 3 // 2 + self.buffer_size = self.height * abs(self.frame.ptr.linesize[0]) + self.frame = frame + + cdef void* _buffer_ptr(self): + return self.frame.ptr.extended_data[self.index] diff --git a/include/libavutil/avutil.pxd b/include/libavutil/avutil.pxd index 08f973cac..47941b2cf 100644 --- a/include/libavutil/avutil.pxd +++ b/include/libavutil/avutil.pxd @@ -297,6 +297,18 @@ cdef extern from "libavutil/imgutils.h" nogil: AVPixelFormat pix_fmt, int align ) + cdef int av_image_fill_pointers( + uint8_t *pointers[4], + AVPixelFormat pix_fmt, + int height, + uint8_t *ptr, + const int linesizes[4] + ) + cdef int av_image_fill_linesizes( + int linesizes[4], + AVPixelFormat pix_fmt, + int width, + ) cdef extern from "libavutil/log.h" nogil: diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 0d36616e1..32f6c6cd3 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -491,6 +491,104 @@ def test_ndarray_nv12_align(self): self.assertNdarraysEqual(frame.to_ndarray(), array) +class TestVideoFrameNumpyBuffer(TestCase): + def test_shares_memory_gray(self): + array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + frame = VideoFrame.from_numpy_buffer(array, "gray") + self.assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + # Make sure the frame reflects that + self.assertNdarraysEqual(frame.to_ndarray(), array) + + def test_shares_memory_gray8(self): + array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + frame = VideoFrame.from_numpy_buffer(array, "gray8") + self.assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + # Make sure the frame reflects that + self.assertNdarraysEqual(frame.to_ndarray(), array) + + def test_shares_memory_rgb8(self): + array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + frame = VideoFrame.from_numpy_buffer(array, "rgb8") + self.assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + # Make sure the frame reflects that + self.assertNdarraysEqual(frame.to_ndarray(), array) + + def test_shares_memory_bgr8(self): + array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + frame = VideoFrame.from_numpy_buffer(array, "bgr8") + self.assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + # Make sure the frame reflects that + self.assertNdarraysEqual(frame.to_ndarray(), array) + + def test_shares_memory_rgb24(self): + array = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8) + frame = VideoFrame.from_numpy_buffer(array, "rgb24") + self.assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8) + # Make sure the frame reflects that + self.assertNdarraysEqual(frame.to_ndarray(), array) + + def test_shares_memory_yuv420p(self): + array = numpy.random.randint( + 0, 256, size=(512 * 6 // 4, 256), dtype=numpy.uint8 + ) + frame = VideoFrame.from_numpy_buffer(array, "yuv420p") + self.assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) + # Make sure the frame reflects that + self.assertNdarraysEqual(frame.to_ndarray(), array) + + def test_shares_memory_yuvj420p(self): + array = numpy.random.randint( + 0, 256, size=(512 * 6 // 4, 256), dtype=numpy.uint8 + ) + frame = VideoFrame.from_numpy_buffer(array, "yuvj420p") + self.assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) + # Make sure the frame reflects that + self.assertNdarraysEqual(frame.to_ndarray(), array) + + def test_shares_memory_nv12(self): + array = numpy.random.randint( + 0, 256, size=(512 * 6 // 4, 256), dtype=numpy.uint8 + ) + frame = VideoFrame.from_numpy_buffer(array, "nv12") + self.assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) + # Make sure the frame reflects that + self.assertNdarraysEqual(frame.to_ndarray(), array) + + def test_shares_memory_bgr24(self): + array = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8) + frame = VideoFrame.from_numpy_buffer(array, "bgr24") + self.assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8) + # Make sure the frame reflects that + self.assertNdarraysEqual(frame.to_ndarray(), array) + + class TestVideoFrameTiming(TestCase): def test_reformat_pts(self): frame = VideoFrame(640, 480, "rgb24") From bf87dc2f60aadb0f703ae78fd695dce00ad20325 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 25 Dec 2023 20:23:41 -0500 Subject: [PATCH 165/846] Use double quotes Prefer double quotes like black does using black's rules. Single quotes in docstrings are left untouched. --- av/_core.pyx | 16 ++-- av/audio/fifo.pyx | 12 +-- av/audio/format.pyx | 20 ++-- av/audio/frame.pyx | 28 +++--- av/audio/layout.pyx | 60 ++++++------ av/audio/resampler.pyx | 2 +- av/audio/stream.pyx | 2 +- av/buffer.pyx | 6 +- av/bytesource.pyx | 3 +- av/codec/codec.pyx | 154 +++++++++++++++---------------- av/codec/context.pyx | 164 ++++++++++++++++----------------- av/container/core.pyx | 74 +++++++-------- av/container/input.pyx | 10 +- av/container/output.pyx | 8 +- av/container/pyio.pyx | 16 ++-- av/container/streams.pyx | 4 +- av/data/stream.pyx | 6 +- av/descriptor.pyx | 4 +- av/dictionary.pyx | 2 +- av/enum.pyx | 10 +- av/error.pyx | 117 ++++++++++++------------ av/filter/context.pyx | 26 +++--- av/filter/filter.pyx | 4 +- av/filter/graph.pyx | 50 +++++----- av/filter/link.pyx | 8 +- av/filter/pad.pyx | 10 +- av/format.pyx | 98 +++++++++----------- av/frame.pyx | 4 +- av/logging.pyx | 12 +-- av/option.pyx | 70 +++++++------- av/packet.pyx | 4 +- av/plane.pyx | 2 +- av/sidedata/motionvectors.pyx | 28 +++--- av/sidedata/sidedata.pyx | 40 ++++---- av/subtitles/subtitle.pyx | 30 +++--- av/video/format.pyx | 7 +- av/video/frame.pyx | 166 +++++++++++++++++----------------- av/video/reformatter.pyx | 62 ++++++------- av/video/stream.pyx | 2 +- 39 files changed, 663 insertions(+), 678 deletions(-) diff --git a/av/_core.pyx b/av/_core.pyx index b2a6e83bd..1c3e4816a 100644 --- a/av/_core.pyx +++ b/av/_core.pyx @@ -18,41 +18,41 @@ cdef decode_version(v): return (major, minor, micro) library_meta = { - 'libavutil': dict( + "libavutil": dict( version=decode_version(lib.avutil_version()), configuration=lib.avutil_configuration(), license=lib.avutil_license() ), - 'libavcodec': dict( + "libavcodec": dict( version=decode_version(lib.avcodec_version()), configuration=lib.avcodec_configuration(), license=lib.avcodec_license() ), - 'libavformat': dict( + "libavformat": dict( version=decode_version(lib.avformat_version()), configuration=lib.avformat_configuration(), license=lib.avformat_license() ), - 'libavdevice': dict( + "libavdevice": dict( version=decode_version(lib.avdevice_version()), configuration=lib.avdevice_configuration(), license=lib.avdevice_license() ), - 'libavfilter': dict( + "libavfilter": dict( version=decode_version(lib.avfilter_version()), configuration=lib.avfilter_configuration(), license=lib.avfilter_license() ), - 'libswscale': dict( + "libswscale": dict( version=decode_version(lib.swscale_version()), configuration=lib.swscale_configuration(), license=lib.swscale_license() ), - 'libswresample': dict( + "libswresample": dict( version=decode_version(lib.swresample_version()), configuration=lib.swresample_configuration(), license=lib.swresample_license() ), } -library_versions = {name: meta['version'] for name, meta in library_meta.items()} +library_versions = {name: meta["version"] for name, meta in library_meta.items()} diff --git a/av/audio/fifo.pyx b/av/audio/fifo.pyx index 9f92f1ff2..605f7644d 100644 --- a/av/audio/fifo.pyx +++ b/av/audio/fifo.pyx @@ -8,7 +8,7 @@ cdef class AudioFifo: def __repr__(self): try: - result = '' % ( + result = "" % ( self.__class__.__name__, self.samples, self.sample_rate, @@ -17,7 +17,7 @@ cdef class AudioFifo: id(self), ) except AttributeError: - result = '' % ( + result = "" % ( self.__class__.__name__, id(self), ) @@ -44,7 +44,7 @@ cdef class AudioFifo: """ if frame is None: - raise TypeError('AudioFifo must be given an AudioFrame.') + raise TypeError("AudioFifo must be given an AudioFrame.") if not frame.ptr.nb_samples: return @@ -71,7 +71,7 @@ cdef class AudioFifo: ) if not self.ptr: - raise RuntimeError('Could not allocate AVAudioFifo.') + raise RuntimeError("Could not allocate AVAudioFifo.") # Make sure nothing changed. elif ( @@ -83,14 +83,14 @@ cdef class AudioFifo: frame._time_base.den != self.template._time_base.den )) ): - raise ValueError('Frame does not match AudioFifo parameters.') + raise ValueError("Frame does not match AudioFifo parameters.") # Assert that the PTS are what we expect. cdef int64_t expected_pts if self.pts_per_sample and frame.ptr.pts != lib.AV_NOPTS_VALUE: expected_pts = (self.pts_per_sample * self.samples_written) if frame.ptr.pts != expected_pts: - raise ValueError('Frame.pts (%d) != expected (%d); fix or set to None.' % (frame.ptr.pts, expected_pts)) + raise ValueError("Frame.pts (%d) != expected (%d); fix or set to None." % (frame.ptr.pts, expected_pts)) err_check(lib.av_audio_fifo_write( self.ptr, diff --git a/av/audio/format.pyx b/av/audio/format.pyx index 4c7cd1cdc..4cecc7cf1 100644 --- a/av/audio/format.pyx +++ b/av/audio/format.pyx @@ -1,7 +1,7 @@ import sys -cdef str container_format_postfix = 'le' if sys.byteorder == 'little' else 'be' +cdef str container_format_postfix = "le" if sys.byteorder == "little" else "be" cdef object _cinit_bypass_sentinel @@ -33,7 +33,7 @@ cdef class AudioFormat: sample_fmt = lib.av_get_sample_fmt(name) if sample_fmt < 0: - raise ValueError('Not a sample format: %r' % name) + raise ValueError("Not a sample format: %r" % name) self._init(sample_fmt) @@ -41,7 +41,7 @@ cdef class AudioFormat: self.sample_fmt = sample_fmt def __repr__(self): - return '' % (self.name) + return "" % (self.name) property name: """Canonical name of the sample format. @@ -130,17 +130,17 @@ cdef class AudioFormat: def __get__(self): if self.is_planar: - raise ValueError('no planar container formats') + raise ValueError("no planar container formats") if self.sample_fmt == lib.AV_SAMPLE_FMT_U8: - return 'u8' + return "u8" elif self.sample_fmt == lib.AV_SAMPLE_FMT_S16: - return 's16' + container_format_postfix + return "s16" + container_format_postfix elif self.sample_fmt == lib.AV_SAMPLE_FMT_S32: - return 's32' + container_format_postfix + return "s32" + container_format_postfix elif self.sample_fmt == lib.AV_SAMPLE_FMT_FLT: - return 'f32' + container_format_postfix + return "f32" + container_format_postfix elif self.sample_fmt == lib.AV_SAMPLE_FMT_DBL: - return 'f64' + container_format_postfix + return "f64" + container_format_postfix - raise ValueError('unknown layout') + raise ValueError("unknown layout") diff --git a/av/audio/frame.pyx b/av/audio/frame.pyx index b97d5e043..aa6562259 100644 --- a/av/audio/frame.pyx +++ b/av/audio/frame.pyx @@ -9,16 +9,16 @@ cdef object _cinit_bypass_sentinel format_dtypes = { - 'dbl': 'f8', - 'dblp': 'f8', - 'flt': 'f4', - 'fltp': 'f4', - 's16': 'i2', - 's16p': 'i2', - 's32': 'i4', - 's32p': 'i4', - 'u8': 'u1', - 'u8p': 'u1', + "dbl": "f8", + "dblp": "f8", + "flt": "f4", + "fltp": "f4", + "s16": "i2", + "s16p": "i2", + "s32": "i4", + "s32p": "i4", + "u8": "u1", + "u8p": "u1", } @@ -36,7 +36,7 @@ cdef class AudioFrame(Frame): """A frame of audio.""" - def __cinit__(self, format='s16', layout='stereo', samples=0, align=1): + def __cinit__(self, format="s16", layout="stereo", samples=0, align=1): if format is _cinit_bypass_sentinel: return @@ -91,7 +91,7 @@ cdef class AudioFrame(Frame): self.format = get_audio_format(self.ptr.format) def __repr__(self): - return '' % ( + return "" % ( self.__class__.__name__, self.index, self.pts, @@ -103,7 +103,7 @@ cdef class AudioFrame(Frame): ) @staticmethod - def from_ndarray(array, format='s16', layout='stereo'): + def from_ndarray(array, format="s16", layout="stereo"): """ Construct a frame from a numpy array. """ @@ -113,7 +113,7 @@ cdef class AudioFrame(Frame): try: dtype = np.dtype(format_dtypes[format]) except KeyError: - raise ValueError('Conversion from numpy array with format `%s` is not yet supported' % format) + raise ValueError("Conversion from numpy array with format `%s` is not yet supported" % format) # check input format nb_channels = len(AudioLayout(layout).channels) diff --git a/av/audio/layout.pyx b/av/audio/layout.pyx index 7c8a8c373..2f9f69815 100644 --- a/av/audio/layout.pyx +++ b/av/audio/layout.pyx @@ -34,31 +34,31 @@ default_layouts[16] = 0xFFFF cdef dict channel_descriptions = { - 'FL': 'front left', - 'FR': 'front right', - 'FC': 'front center', - 'LFE': 'low frequency', - 'BL': 'back left', - 'BR': 'back right', - 'FLC': 'front left-of-center', - 'FRC': 'front right-of-center', - 'BC': 'back center', - 'SL': 'side left', - 'SR': 'side right', - 'TC': 'top center', - 'TFL': 'top front left', - 'TFC': 'top front center', - 'TFR': 'top front right', - 'TBL': 'top back left', - 'TBC': 'top back center', - 'TBR': 'top back right', - 'DL': 'downmix left', - 'DR': 'downmix right', - 'WL': 'wide left', - 'WR': 'wide right', - 'SDL': 'surround direct left', - 'SDR': 'surround direct right', - 'LFE2': 'low frequency 2', + "FL": "front left", + "FR": "front right", + "FC": "front center", + "LFE": "low frequency", + "BL": "back left", + "BR": "back right", + "FLC": "front left-of-center", + "FRC": "front right-of-center", + "BC": "back center", + "SL": "side left", + "SR": "side right", + "TC": "top center", + "TFL": "top front left", + "TFC": "top front center", + "TFR": "top front right", + "TBL": "top back left", + "TBC": "top back center", + "TBR": "top back right", + "DL": "downmix left", + "DR": "downmix right", + "WL": "wide left", + "WR": "wide right", + "SDL": "surround direct left", + "SDR": "surround direct right", + "LFE2": "low frequency 2", } @@ -72,17 +72,17 @@ cdef class AudioLayout: cdef uint64_t c_layout if isinstance(layout, int): if layout < 0 or layout > 8: - raise ValueError('no layout with %d channels' % layout) + raise ValueError("no layout with %d channels" % layout) c_layout = default_layouts[layout] elif isinstance(layout, str): c_layout = lib.av_get_channel_layout(layout) elif isinstance(layout, AudioLayout): c_layout = layout.layout else: - raise TypeError('layout must be str or int') + raise TypeError("layout must be str or int") if not c_layout: - raise ValueError('invalid channel layout %r' % layout) + raise ValueError("invalid channel layout %r" % layout) self._init(c_layout) @@ -92,7 +92,7 @@ cdef class AudioLayout: self.channels = tuple(AudioChannel(self, i) for i in range(self.nb_channels)) def __repr__(self): - return '' % (self.__class__.__name__, self.name) + return "" % (self.__class__.__name__, self.name) property name: """The canonical name of the audio layout.""" @@ -109,7 +109,7 @@ cdef class AudioChannel: self.channel = lib.av_channel_layout_extract_channel(layout.layout, index) def __repr__(self): - return '' % (self.__class__.__name__, self.name, self.description) + return "" % (self.__class__.__name__, self.name, self.description) property name: """The canonical name of the audio channel.""" diff --git a/av/audio/resampler.pyx b/av/audio/resampler.pyx index 29b6fb3f6..89a5428e1 100644 --- a/av/audio/resampler.pyx +++ b/av/audio/resampler.pyx @@ -100,7 +100,7 @@ cdef class AudioResampler: frame.layout.layout != self.template.layout.layout or frame.sample_rate != self.template.rate ): - raise ValueError('Frame does not match AudioResampler setup.') + raise ValueError("Frame does not match AudioResampler setup.") self.graph.push(frame) diff --git a/av/audio/stream.pyx b/av/audio/stream.pyx index 0a4f1523c..e40659212 100644 --- a/av/audio/stream.pyx +++ b/av/audio/stream.pyx @@ -1,7 +1,7 @@ cdef class AudioStream(Stream): def __repr__(self): - return '' % ( + return "" % ( self.__class__.__name__, self.index, self.name, diff --git a/av/buffer.pyx b/av/buffer.pyx index 5affc81f0..d5e49fe96 100644 --- a/av/buffer.pyx +++ b/av/buffer.pyx @@ -23,7 +23,7 @@ cdef class Buffer: def __getbuffer__(self, Py_buffer *view, int flags): if flags & PyBUF_WRITABLE and not self._buffer_writable(): - raise ValueError('buffer is not writable') + raise ValueError("buffer is not writable") PyBuffer_FillInfo(view, self, self._buffer_ptr(), self._buffer_size(), 0, flags) @property @@ -58,9 +58,9 @@ cdef class Buffer: """ if not self._buffer_writable(): - raise ValueError('buffer is not writable') + raise ValueError("buffer is not writable") cdef ByteSource source = bytesource(input) cdef size_t size = self._buffer_size() if source.length != size: - raise ValueError('got %d bytes; need %d bytes' % (source.length, size)) + raise ValueError("got %d bytes; need %d bytes" % (source.length, size)) memcpy(self._buffer_ptr(), source.ptr, size) diff --git a/av/bytesource.pyx b/av/bytesource.pyx index ec4138e72..447df55d3 100644 --- a/av/bytesource.pyx +++ b/av/bytesource.pyx @@ -7,7 +7,6 @@ from cpython.buffer cimport ( cdef class ByteSource: - def __cinit__(self, owner): self.owner = owner @@ -28,7 +27,7 @@ cdef class ByteSource: self.length = self.view.len return - raise TypeError('expected bytes, bytearray or memoryview') + raise TypeError("expected bytes, bytearray or memoryview") def __dealloc__(self): if self.has_view: diff --git a/av/codec/codec.pyx b/av/codec/codec.pyx index a7002acc2..069df5059 100644 --- a/av/codec/codec.pyx +++ b/av/codec/codec.pyx @@ -16,19 +16,19 @@ cdef Codec wrap_codec(const lib.AVCodec *ptr): return codec -Properties = define_enum('Properties', 'av.codec', ( - ('NONE', 0), - ('INTRA_ONLY', lib.AV_CODEC_PROP_INTRA_ONLY, +Properties = define_enum("Properties", "av.codec", ( + ("NONE", 0), + ("INTRA_ONLY", lib.AV_CODEC_PROP_INTRA_ONLY, """Codec uses only intra compression. Video and audio codecs only."""), - ('LOSSY', lib.AV_CODEC_PROP_LOSSY, + ("LOSSY", lib.AV_CODEC_PROP_LOSSY, """Codec supports lossy compression. Audio and video codecs only. Note: A codec may support both lossy and lossless compression modes."""), - ('LOSSLESS', lib.AV_CODEC_PROP_LOSSLESS, + ("LOSSLESS", lib.AV_CODEC_PROP_LOSSLESS, """Codec supports lossless compression. Audio and video codecs only."""), - ('REORDER', lib.AV_CODEC_PROP_REORDER, + ("REORDER", lib.AV_CODEC_PROP_REORDER, """Codec supports frame reordering. That is, the coded order (the order in which the encoded packets are output by the encoders / stored / input to the decoders) may be different from the presentation order of the corresponding @@ -36,24 +36,24 @@ Properties = define_enum('Properties', 'av.codec', ( For codecs that do not have this property set, PTS and DTS should always be equal."""), - ('BITMAP_SUB', lib.AV_CODEC_PROP_BITMAP_SUB, + ("BITMAP_SUB", lib.AV_CODEC_PROP_BITMAP_SUB, """Subtitle codec is bitmap based Decoded AVSubtitle data can be read from the AVSubtitleRect->pict field."""), - ('TEXT_SUB', lib.AV_CODEC_PROP_TEXT_SUB, + ("TEXT_SUB", lib.AV_CODEC_PROP_TEXT_SUB, """Subtitle codec is text based. Decoded AVSubtitle data can be read from the AVSubtitleRect->ass field."""), ), is_flags=True) -Capabilities = define_enum('Capabilities', 'av.codec', ( - ('NONE', 0), - ('DRAW_HORIZ_BAND', lib.AV_CODEC_CAP_DRAW_HORIZ_BAND, +Capabilities = define_enum("Capabilities", "av.codec", ( + ("NONE", 0), + ("DRAW_HORIZ_BAND", lib.AV_CODEC_CAP_DRAW_HORIZ_BAND, """Decoder can use draw_horiz_band callback."""), - ('DR1', lib.AV_CODEC_CAP_DR1, + ("DR1", lib.AV_CODEC_CAP_DR1, """Codec uses get_buffer() for allocating buffers and supports custom allocators. If not set, it might not use get_buffer() at all or use operations that assume the buffer was allocated by avcodec_default_get_buffer."""), - ('HWACCEL', 1 << 4), - ('DELAY', lib.AV_CODEC_CAP_DELAY, + ("HWACCEL", 1 << 4), + ("DELAY", lib.AV_CODEC_CAP_DELAY, """Encoder or decoder requires flushing with NULL input at the end in order to give the complete and correct output. @@ -75,11 +75,11 @@ Capabilities = define_enum('Capabilities', 'av.codec', ( flag also means that the encoder must set the pts and duration for each output packet. If this flag is not set, the pts and duration will be determined by libavcodec from the input frame."""), - ('SMALL_LAST_FRAME', lib.AV_CODEC_CAP_SMALL_LAST_FRAME, + ("SMALL_LAST_FRAME", lib.AV_CODEC_CAP_SMALL_LAST_FRAME, """Codec can be fed a final frame with a smaller size. This can be used to prevent truncation of the last audio samples."""), - ('HWACCEL_VDPAU', 1 << 7), - ('SUBFRAMES', lib.AV_CODEC_CAP_SUBFRAMES, + ("HWACCEL_VDPAU", 1 << 7), + ("SUBFRAMES", lib.AV_CODEC_CAP_SUBFRAMES, """Codec can output multiple frames per AVPacket Normally demuxers return one frame at a time, demuxers which do not do are connected to a parser to split what they return into proper frames. @@ -89,25 +89,25 @@ Capabilities = define_enum('Capabilities', 'av.codec', ( may return multiple frames in a packet. This has many disadvantages like prohibiting stream copy in many cases thus it should only be considered as a last resort."""), - ('EXPERIMENTAL', lib.AV_CODEC_CAP_EXPERIMENTAL, + ("EXPERIMENTAL", lib.AV_CODEC_CAP_EXPERIMENTAL, """Codec is experimental and is thus avoided in favor of non experimental encoders"""), - ('CHANNEL_CONF', lib.AV_CODEC_CAP_CHANNEL_CONF, + ("CHANNEL_CONF", lib.AV_CODEC_CAP_CHANNEL_CONF, """Codec should fill in channel configuration and samplerate instead of container"""), - ('NEG_LINESIZES', 1 << 11), - ('FRAME_THREADS', lib.AV_CODEC_CAP_FRAME_THREADS, + ("NEG_LINESIZES", 1 << 11), + ("FRAME_THREADS", lib.AV_CODEC_CAP_FRAME_THREADS, """Codec supports frame-level multithreading""",), - ('SLICE_THREADS', lib.AV_CODEC_CAP_SLICE_THREADS, + ("SLICE_THREADS", lib.AV_CODEC_CAP_SLICE_THREADS, """Codec supports slice-based (or partition-based) multithreading."""), - ('PARAM_CHANGE', lib.AV_CODEC_CAP_PARAM_CHANGE, + ("PARAM_CHANGE", lib.AV_CODEC_CAP_PARAM_CHANGE, """Codec supports changed parameters at any point."""), - ('AUTO_THREADS', lib.AV_CODEC_CAP_OTHER_THREADS, + ("AUTO_THREADS", lib.AV_CODEC_CAP_OTHER_THREADS, """Codec supports multithreading through a method other than slice- or frame-level multithreading. Typically this marks wrappers around multithreading-capable external libraries."""), - ('VARIABLE_FRAME_SIZE', lib.AV_CODEC_CAP_VARIABLE_FRAME_SIZE, + ("VARIABLE_FRAME_SIZE", lib.AV_CODEC_CAP_VARIABLE_FRAME_SIZE, """Audio encoder supports receiving a different number of samples in each call."""), - ('AVOID_PROBING', lib.AV_CODEC_CAP_AVOID_PROBING, + ("AVOID_PROBING", lib.AV_CODEC_CAP_AVOID_PROBING, """Decoder is not a preferred choice for probing. This indicates that the decoder is not a good choice for probing. It could for example be an expensive to spin up hardware decoder, @@ -115,19 +115,19 @@ Capabilities = define_enum('Capabilities', 'av.codec', ( the stream. A decoder marked with this flag should only be used as last resort choice for probing."""), - ('HARDWARE', lib.AV_CODEC_CAP_HARDWARE, + ("HARDWARE", lib.AV_CODEC_CAP_HARDWARE, """Codec is backed by a hardware implementation. Typically used to identify a non-hwaccel hardware decoder. For information about hwaccels, use avcodec_get_hw_config() instead."""), - ('HYBRID', lib.AV_CODEC_CAP_HYBRID, + ("HYBRID", lib.AV_CODEC_CAP_HYBRID, """Codec is potentially backed by a hardware implementation, but not necessarily. This is used instead of AV_CODEC_CAP_HARDWARE, if the implementation provides some sort of internal fallback."""), - ('ENCODER_REORDERED_OPAQUE', 1 << 20, # lib.AV_CODEC_CAP_ENCODER_REORDERED_OPAQUE, # FFmpeg 4.2 + ("ENCODER_REORDERED_OPAQUE", 1 << 20, # lib.AV_CODEC_CAP_ENCODER_REORDERED_OPAQUE, # FFmpeg 4.2 """This codec takes the reordered_opaque field from input AVFrames and returns it in the corresponding field in AVCodecContext after encoding."""), - ('ENCODER_FLUSH', 1 << 21, # lib.AV_CODEC_CAP_ENCODER_FLUSH # FFmpeg 4.3 + ("ENCODER_FLUSH", 1 << 21, # lib.AV_CODEC_CAP_ENCODER_FLUSH # FFmpeg 4.3 """This encoder can be flushed using avcodec_flush_buffers(). If this flag is not set, the encoder must be closed and reopened to ensure that no frames remain pending."""), @@ -160,19 +160,19 @@ cdef class Codec: """ - def __cinit__(self, name, mode='r'): + def __cinit__(self, name, mode="r"): if name is _cinit_sentinel: return - if mode == 'w': + if mode == "w": self.ptr = lib.avcodec_find_encoder_by_name(name) if not self.ptr: self.desc = lib.avcodec_descriptor_get_by_name(name) if self.desc: self.ptr = lib.avcodec_find_encoder(self.desc.id) - elif mode == 'r': + elif mode == "r": self.ptr = lib.avcodec_find_decoder_by_name(name) if not self.ptr: self.desc = lib.avcodec_descriptor_get_by_name(name) @@ -185,7 +185,7 @@ cdef class Codec: self._init(name) # Sanity check. - if (mode == 'w') != self.is_encoder: + if (mode == "w") != self.is_encoder: raise RuntimeError("Found codec does not match mode.", name, mode) cdef _init(self, name=None): @@ -196,13 +196,13 @@ cdef class Codec: if not self.desc: self.desc = lib.avcodec_descriptor_get(self.ptr.id) if not self.desc: - raise RuntimeError('No codec descriptor for %r.' % name) + raise RuntimeError("No codec descriptor for %r." % name) self.is_encoder = lib.av_codec_is_encoder(self.ptr) # Sanity check. if self.is_encoder and lib.av_codec_is_decoder(self.ptr): - raise RuntimeError('%s is both encoder and decoder.') + raise RuntimeError("%s is both encoder and decoder.") def create(self): """Create a :class:`.CodecContext` for this codec.""" @@ -217,9 +217,9 @@ cdef class Codec: def __get__(self): return wrap_avclass(self.ptr.priv_class) property name: - def __get__(self): return self.ptr.name or '' + def __get__(self): return self.ptr.name or "" property long_name: - def __get__(self): return self.ptr.long_name or '' + def __get__(self): return self.ptr.long_name or "" @property def type(self): @@ -295,40 +295,40 @@ cdef class Codec: """Flag property of :class:`.Properties`""" return self.desc.props - intra_only = properties.flag_property('INTRA_ONLY') - lossy = properties.flag_property('LOSSY') # Defer to capability. - lossless = properties.flag_property('LOSSLESS') # Defer to capability. - reorder = properties.flag_property('REORDER') - bitmap_sub = properties.flag_property('BITMAP_SUB') - text_sub = properties.flag_property('TEXT_SUB') + intra_only = properties.flag_property("INTRA_ONLY") + lossy = properties.flag_property("LOSSY") # Defer to capability. + lossless = properties.flag_property("LOSSLESS") # Defer to capability. + reorder = properties.flag_property("REORDER") + bitmap_sub = properties.flag_property("BITMAP_SUB") + text_sub = properties.flag_property("TEXT_SUB") @Capabilities.property def capabilities(self): """Flag property of :class:`.Capabilities`""" return self.ptr.capabilities - draw_horiz_band = capabilities.flag_property('DRAW_HORIZ_BAND') - dr1 = capabilities.flag_property('DR1') - hwaccel = capabilities.flag_property('HWACCEL') - delay = capabilities.flag_property('DELAY') - small_last_frame = capabilities.flag_property('SMALL_LAST_FRAME') - hwaccel_vdpau = capabilities.flag_property('HWACCEL_VDPAU') - subframes = capabilities.flag_property('SUBFRAMES') - experimental = capabilities.flag_property('EXPERIMENTAL') - channel_conf = capabilities.flag_property('CHANNEL_CONF') - neg_linesizes = capabilities.flag_property('NEG_LINESIZES') - frame_threads = capabilities.flag_property('FRAME_THREADS') - slice_threads = capabilities.flag_property('SLICE_THREADS') - param_change = capabilities.flag_property('PARAM_CHANGE') - auto_threads = capabilities.flag_property('AUTO_THREADS') - variable_frame_size = capabilities.flag_property('VARIABLE_FRAME_SIZE') - avoid_probing = capabilities.flag_property('AVOID_PROBING') - # intra_only = capabilities.flag_property('INTRA_ONLY') # Dupes. - # lossless = capabilities.flag_property('LOSSLESS') # Dupes. - hardware = capabilities.flag_property('HARDWARE') - hybrid = capabilities.flag_property('HYBRID') - encoder_reordered_opaque = capabilities.flag_property('ENCODER_REORDERED_OPAQUE') - encoder_flush = capabilities.flag_property('ENCODER_FLUSH') + draw_horiz_band = capabilities.flag_property("DRAW_HORIZ_BAND") + dr1 = capabilities.flag_property("DR1") + hwaccel = capabilities.flag_property("HWACCEL") + delay = capabilities.flag_property("DELAY") + small_last_frame = capabilities.flag_property("SMALL_LAST_FRAME") + hwaccel_vdpau = capabilities.flag_property("HWACCEL_VDPAU") + subframes = capabilities.flag_property("SUBFRAMES") + experimental = capabilities.flag_property("EXPERIMENTAL") + channel_conf = capabilities.flag_property("CHANNEL_CONF") + neg_linesizes = capabilities.flag_property("NEG_LINESIZES") + frame_threads = capabilities.flag_property("FRAME_THREADS") + slice_threads = capabilities.flag_property("SLICE_THREADS") + param_change = capabilities.flag_property("PARAM_CHANGE") + auto_threads = capabilities.flag_property("AUTO_THREADS") + variable_frame_size = capabilities.flag_property("VARIABLE_FRAME_SIZE") + avoid_probing = capabilities.flag_property("AVOID_PROBING") + # intra_only = capabilities.flag_property("INTRA_ONLY") # Dupes. + # lossless = capabilities.flag_property("LOSSLESS") # Dupes. + hardware = capabilities.flag_property("HARDWARE") + hybrid = capabilities.flag_property("HYBRID") + encoder_reordered_opaque = capabilities.flag_property("ENCODER_REORDERED_OPAQUE") + encoder_flush = capabilities.flag_property("ENCODER_FLUSH") cdef get_codec_names(): @@ -352,7 +352,7 @@ codec_descriptor = wrap_avclass(lib.avcodec_get_class()) def dump_codecs(): """Print information about available codecs.""" - print '''Codecs: + print """Codecs: D..... = Decoding supported .E.... = Encoding supported ..V... = Video codec @@ -361,17 +361,17 @@ def dump_codecs(): ...I.. = Intra frame-only codec ....L. = Lossy compression .....S = Lossless compression - ------''' + ------""" for name in sorted(codecs_available): try: - e_codec = Codec(name, 'w') + e_codec = Codec(name, "w") except ValueError: e_codec = None try: - d_codec = Codec(name, 'r') + d_codec = Codec(name, "r") except ValueError: d_codec = None @@ -379,15 +379,15 @@ def dump_codecs(): codec = e_codec or d_codec try: - print ' %s%s%s%s%s%s %-18s %s' % ( - '.D'[bool(d_codec)], - '.E'[bool(e_codec)], + print " %s%s%s%s%s%s %-18s %s" % ( + ".D"[bool(d_codec)], + ".E"[bool(e_codec)], codec.type[0].upper(), - '.I'[codec.intra_only], - '.L'[codec.lossy], - '.S'[codec.lossless], + ".I"[codec.intra_only], + ".L"[codec.lossy], + ".S"[codec.lossless], codec.name, codec.long_name ) except Exception as e: - print '...... %-18s ERROR: %s' % (codec.name, e) + print "...... %-18s ERROR: %s" % (codec.name, e) diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 112ab32e7..bf87b4391 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -43,94 +43,94 @@ cdef CodecContext wrap_codec_context(lib.AVCodecContext *c_ctx, const lib.AVCode return py_ctx -ThreadType = define_enum('ThreadType', __name__, ( - ('NONE', 0), - ('FRAME', lib.FF_THREAD_FRAME, +ThreadType = define_enum("ThreadType", __name__, ( + ("NONE", 0), + ("FRAME", lib.FF_THREAD_FRAME, """Decode more than one frame at once"""), - ('SLICE', lib.FF_THREAD_SLICE, + ("SLICE", lib.FF_THREAD_SLICE, """Decode more than one part of a single frame at once"""), - ('AUTO', lib.FF_THREAD_SLICE | lib.FF_THREAD_FRAME, + ("AUTO", lib.FF_THREAD_SLICE | lib.FF_THREAD_FRAME, """Decode using both FRAME and SLICE methods."""), ), is_flags=True) -SkipType = define_enum('SkipType', __name__, ( - ('NONE', lib.AVDISCARD_NONE, +SkipType = define_enum("SkipType", __name__, ( + ("NONE", lib.AVDISCARD_NONE, """Discard nothing"""), - ('DEFAULT', lib.AVDISCARD_DEFAULT, + ("DEFAULT", lib.AVDISCARD_DEFAULT, """Discard useless packets like 0 size packets in AVI"""), - ('NONREF', lib.AVDISCARD_NONREF, + ("NONREF", lib.AVDISCARD_NONREF, """Discard all non reference"""), - ('BIDIR', lib.AVDISCARD_BIDIR, + ("BIDIR", lib.AVDISCARD_BIDIR, """Discard all bidirectional frames"""), - ('NONINTRA', lib.AVDISCARD_NONINTRA, + ("NONINTRA", lib.AVDISCARD_NONINTRA, """Discard all non intra frames"""), - ('NONKEY', lib.AVDISCARD_NONKEY, + ("NONKEY", lib.AVDISCARD_NONKEY, """Discard all frames except keyframes"""), - ('ALL', lib.AVDISCARD_ALL, + ("ALL", lib.AVDISCARD_ALL, """Discard all"""), )) -Flags = define_enum('Flags', __name__, ( - ('NONE', 0), - ('UNALIGNED', lib.AV_CODEC_FLAG_UNALIGNED, +Flags = define_enum("Flags", __name__, ( + ("NONE", 0), + ("UNALIGNED", lib.AV_CODEC_FLAG_UNALIGNED, """Allow decoders to produce frames with data planes that are not aligned to CPU requirements (e.g. due to cropping)."""), - ('QSCALE', lib.AV_CODEC_FLAG_QSCALE, + ("QSCALE", lib.AV_CODEC_FLAG_QSCALE, """Use fixed qscale."""), - ('4MV', lib.AV_CODEC_FLAG_4MV, + ("4MV", lib.AV_CODEC_FLAG_4MV, """4 MV per MB allowed / advanced prediction for H.263."""), - ('OUTPUT_CORRUPT', lib.AV_CODEC_FLAG_OUTPUT_CORRUPT, + ("OUTPUT_CORRUPT", lib.AV_CODEC_FLAG_OUTPUT_CORRUPT, """Output even those frames that might be corrupted."""), - ('QPEL', lib.AV_CODEC_FLAG_QPEL, + ("QPEL", lib.AV_CODEC_FLAG_QPEL, """Use qpel MC."""), - ('DROPCHANGED', 1 << 5, + ("DROPCHANGED", 1 << 5, """Don't output frames whose parameters differ from first decoded frame in stream."""), - ('PASS1', lib.AV_CODEC_FLAG_PASS1, + ("PASS1", lib.AV_CODEC_FLAG_PASS1, """Use internal 2pass ratecontrol in first pass mode."""), - ('PASS2', lib.AV_CODEC_FLAG_PASS2, + ("PASS2", lib.AV_CODEC_FLAG_PASS2, """Use internal 2pass ratecontrol in second pass mode."""), - ('LOOP_FILTER', lib.AV_CODEC_FLAG_LOOP_FILTER, + ("LOOP_FILTER", lib.AV_CODEC_FLAG_LOOP_FILTER, """loop filter."""), - ('GRAY', lib.AV_CODEC_FLAG_GRAY, + ("GRAY", lib.AV_CODEC_FLAG_GRAY, """Only decode/encode grayscale."""), - ('PSNR', lib.AV_CODEC_FLAG_PSNR, + ("PSNR", lib.AV_CODEC_FLAG_PSNR, """error[?] variables will be set during encoding."""), - ('INTERLACED_DCT', lib.AV_CODEC_FLAG_INTERLACED_DCT, + ("INTERLACED_DCT", lib.AV_CODEC_FLAG_INTERLACED_DCT, """Use interlaced DCT."""), - ('LOW_DELAY', lib.AV_CODEC_FLAG_LOW_DELAY, + ("LOW_DELAY", lib.AV_CODEC_FLAG_LOW_DELAY, """Force low delay."""), - ('GLOBAL_HEADER', lib.AV_CODEC_FLAG_GLOBAL_HEADER, + ("GLOBAL_HEADER", lib.AV_CODEC_FLAG_GLOBAL_HEADER, """Place global headers in extradata instead of every keyframe."""), - ('BITEXACT', lib.AV_CODEC_FLAG_BITEXACT, + ("BITEXACT", lib.AV_CODEC_FLAG_BITEXACT, """Use only bitexact stuff (except (I)DCT)."""), - ('AC_PRED', lib.AV_CODEC_FLAG_AC_PRED, + ("AC_PRED", lib.AV_CODEC_FLAG_AC_PRED, """H.263 advanced intra coding / MPEG-4 AC prediction"""), - ('INTERLACED_ME', lib.AV_CODEC_FLAG_INTERLACED_ME, + ("INTERLACED_ME", lib.AV_CODEC_FLAG_INTERLACED_ME, """Interlaced motion estimation"""), - ('CLOSED_GOP', lib.AV_CODEC_FLAG_CLOSED_GOP), + ("CLOSED_GOP", lib.AV_CODEC_FLAG_CLOSED_GOP), ), is_flags=True) -Flags2 = define_enum('Flags2', __name__, ( - ('NONE', 0), - ('FAST', lib.AV_CODEC_FLAG2_FAST, +Flags2 = define_enum("Flags2", __name__, ( + ("NONE", 0), + ("FAST", lib.AV_CODEC_FLAG2_FAST, """Allow non spec compliant speedup tricks."""), - ('NO_OUTPUT', lib.AV_CODEC_FLAG2_NO_OUTPUT, + ("NO_OUTPUT", lib.AV_CODEC_FLAG2_NO_OUTPUT, """Skip bitstream encoding."""), - ('LOCAL_HEADER', lib.AV_CODEC_FLAG2_LOCAL_HEADER, + ("LOCAL_HEADER", lib.AV_CODEC_FLAG2_LOCAL_HEADER, """Place global headers at every keyframe instead of in extradata."""), - ('CHUNKS', lib.AV_CODEC_FLAG2_CHUNKS, + ("CHUNKS", lib.AV_CODEC_FLAG2_CHUNKS, """Input bitstream might be truncated at a packet boundaries instead of only at frame boundaries."""), - ('IGNORE_CROP', lib.AV_CODEC_FLAG2_IGNORE_CROP, + ("IGNORE_CROP", lib.AV_CODEC_FLAG2_IGNORE_CROP, """Discard cropping information from SPS."""), - ('SHOW_ALL', lib.AV_CODEC_FLAG2_SHOW_ALL, + ("SHOW_ALL", lib.AV_CODEC_FLAG2_SHOW_ALL, """Show all frames before the first keyframe"""), - ('EXPORT_MVS', lib.AV_CODEC_FLAG2_EXPORT_MVS, + ("EXPORT_MVS", lib.AV_CODEC_FLAG2_EXPORT_MVS, """Export motion vectors through frame side data"""), - ('SKIP_MANUAL', lib.AV_CODEC_FLAG2_SKIP_MANUAL, + ("SKIP_MANUAL", lib.AV_CODEC_FLAG2_SKIP_MANUAL, """Do not skip samples and export skip information as frame side data"""), - ('RO_FLUSH_NOOP', lib.AV_CODEC_FLAG2_RO_FLUSH_NOOP, + ("RO_FLUSH_NOOP", lib.AV_CODEC_FLAG2_RO_FLUSH_NOOP, """Do not reset ASS ReadOrder field on flush (subtitles decoding)"""), ), is_flags=True) @@ -145,7 +145,7 @@ cdef class CodecContext: def __cinit__(self, sentinel=None, *args, **kwargs): if sentinel is not _cinit_sentinel: - raise RuntimeError('Cannot instantiate CodecContext') + raise RuntimeError("Cannot instantiate CodecContext") self.options = {} self.stream_index = -1 # This is set by the container immediately. @@ -154,7 +154,7 @@ cdef class CodecContext: self.ptr = ptr if self.ptr.codec and codec and self.ptr.codec != codec: - raise RuntimeError('Wrapping CodecContext with mismatched codec.') + raise RuntimeError("Wrapping CodecContext with mismatched codec.") self.codec = wrap_codec(codec if codec != NULL else self.ptr.codec) # Set reasonable threading defaults. @@ -175,24 +175,24 @@ cdef class CodecContext: """Flag property of :class:`.Flags`.""" ) - unaligned = flags.flag_property('UNALIGNED') - qscale = flags.flag_property('QSCALE') - four_mv = flags.flag_property('4MV') - output_corrupt = flags.flag_property('OUTPUT_CORRUPT') - qpel = flags.flag_property('QPEL') - drop_changed = flags.flag_property('DROPCHANGED') - pass1 = flags.flag_property('PASS1') - pass2 = flags.flag_property('PASS2') - loop_filter = flags.flag_property('LOOP_FILTER') - gray = flags.flag_property('GRAY') - psnr = flags.flag_property('PSNR') - interlaced_dct = flags.flag_property('INTERLACED_DCT') - low_delay = flags.flag_property('LOW_DELAY') - global_header = flags.flag_property('GLOBAL_HEADER') - bitexact = flags.flag_property('BITEXACT') - ac_pred = flags.flag_property('AC_PRED') - interlaced_me = flags.flag_property('INTERLACED_ME') - closed_gop = flags.flag_property('CLOSED_GOP') + unaligned = flags.flag_property("UNALIGNED") + qscale = flags.flag_property("QSCALE") + four_mv = flags.flag_property("4MV") + output_corrupt = flags.flag_property("OUTPUT_CORRUPT") + qpel = flags.flag_property("QPEL") + drop_changed = flags.flag_property("DROPCHANGED") + pass1 = flags.flag_property("PASS1") + pass2 = flags.flag_property("PASS2") + loop_filter = flags.flag_property("LOOP_FILTER") + gray = flags.flag_property("GRAY") + psnr = flags.flag_property("PSNR") + interlaced_dct = flags.flag_property("INTERLACED_DCT") + low_delay = flags.flag_property("LOW_DELAY") + global_header = flags.flag_property("GLOBAL_HEADER") + bitexact = flags.flag_property("BITEXACT") + ac_pred = flags.flag_property("AC_PRED") + interlaced_me = flags.flag_property("INTERLACED_ME") + closed_gop = flags.flag_property("CLOSED_GOP") def _get_flags2(self): return self.ptr.flags2 @@ -206,15 +206,15 @@ cdef class CodecContext: """Flag property of :class:`.Flags2`.""" ) - fast = flags2.flag_property('FAST') - no_output = flags2.flag_property('NO_OUTPUT') - local_header = flags2.flag_property('LOCAL_HEADER') - chunks = flags2.flag_property('CHUNKS') - ignore_crop = flags2.flag_property('IGNORE_CROP') - show_all = flags2.flag_property('SHOW_ALL') - export_mvs = flags2.flag_property('EXPORT_MVS') - skip_manual = flags2.flag_property('SKIP_MANUAL') - ro_flush_noop = flags2.flag_property('RO_FLUSH_NOOP') + fast = flags2.flag_property("FAST") + no_output = flags2.flag_property("NO_OUTPUT") + local_header = flags2.flag_property("LOCAL_HEADER") + chunks = flags2.flag_property("CHUNKS") + ignore_crop = flags2.flag_property("IGNORE_CROP") + show_all = flags2.flag_property("SHOW_ALL") + export_mvs = flags2.flag_property("EXPORT_MVS") + skip_manual = flags2.flag_property("SKIP_MANUAL") + ro_flush_noop = flags2.flag_property("RO_FLUSH_NOOP") property extradata: def __get__(self): @@ -259,7 +259,7 @@ cdef class CodecContext: if lib.avcodec_is_open(self.ptr): if strict: - raise ValueError('CodecContext is already open.') + raise ValueError("CodecContext is already open.") return # We might pass partial frames. @@ -288,7 +288,7 @@ cdef class CodecContext: cpdef close(self, bint strict=True): if not lib.avcodec_is_open(self.ptr): if strict: - raise ValueError('CodecContext is already closed.') + raise ValueError("CodecContext is already closed.") return err_check(lib.avcodec_close(self.ptr)) @@ -302,10 +302,10 @@ cdef class CodecContext: lib.av_parser_close(self.parser) def __repr__(self): - return '' % ( + return "" % ( self.__class__.__name__, - self.type or '', - self.name or '', + self.type or "", + self.name or "", id(self), ) @@ -329,7 +329,7 @@ cdef class CodecContext: if not self.parser: self.parser = lib.av_parser_init(self.codec.ptr.id) if not self.parser: - raise ValueError('No parser for %s' % self.codec.name) + raise ValueError("No parser for %s" % self.codec.name) cdef ByteSource source = bytesource(raw_input, allow_none=True) @@ -424,7 +424,7 @@ cdef class CodecContext: return [frame] cdef Frame _alloc_next_frame(self): - raise NotImplementedError('Base CodecContext cannot decode.') + raise NotImplementedError("Base CodecContext cannot decode.") cdef _recv_frame(self): @@ -460,7 +460,7 @@ cdef class CodecContext: cdef _prepare_and_time_rebase_frames_for_encode(self, Frame frame): if self.ptr.codec_type not in [lib.AVMEDIA_TYPE_VIDEO, lib.AVMEDIA_TYPE_AUDIO]: - raise NotImplementedError('Encoding is only supported for audio and video.') + raise NotImplementedError("Encoding is only supported for audio and video.") self.open(strict=False) @@ -509,7 +509,7 @@ cdef class CodecContext: """ if not self.codec.ptr: - raise ValueError('cannot decode unknown codec') + raise ValueError("cannot decode unknown codec") self.open(strict=False) diff --git a/av/container/core.pyx b/av/container/core.pyx index 854aaff35..e7bb8f4b9 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -24,7 +24,7 @@ cdef object _cinit_sentinel = object() # We want to use the monotonic clock if it is available. -cdef object clock = getattr(time, 'monotonic', time.time) +cdef object clock = getattr(time, "monotonic", time.time) cdef int interrupt_cb (void *p) noexcept nogil: @@ -124,41 +124,41 @@ cdef void pyav_io_close_gil(lib.AVFormatContext *s, stash_exception() -Flags = define_enum('Flags', __name__, ( - ('GENPTS', lib.AVFMT_FLAG_GENPTS, +Flags = define_enum("Flags", __name__, ( + ("GENPTS", lib.AVFMT_FLAG_GENPTS, "Generate missing pts even if it requires parsing future frames."), - ('IGNIDX', lib.AVFMT_FLAG_IGNIDX, + ("IGNIDX", lib.AVFMT_FLAG_IGNIDX, "Ignore index."), - ('NONBLOCK', lib.AVFMT_FLAG_NONBLOCK, + ("NONBLOCK", lib.AVFMT_FLAG_NONBLOCK, "Do not block when reading packets from input."), - ('IGNDTS', lib.AVFMT_FLAG_IGNDTS, + ("IGNDTS", lib.AVFMT_FLAG_IGNDTS, "Ignore DTS on frames that contain both DTS & PTS."), - ('NOFILLIN', lib.AVFMT_FLAG_NOFILLIN, + ("NOFILLIN", lib.AVFMT_FLAG_NOFILLIN, "Do not infer any values from other values, just return what is stored in the container."), - ('NOPARSE', lib.AVFMT_FLAG_NOPARSE, + ("NOPARSE", lib.AVFMT_FLAG_NOPARSE, """Do not use AVParsers, you also must set AVFMT_FLAG_NOFILLIN as the fillin code works on frames and no parsing -> no frames. Also seeking to frames can not work if parsing to find frame boundaries has been disabled."""), - ('NOBUFFER', lib.AVFMT_FLAG_NOBUFFER, + ("NOBUFFER", lib.AVFMT_FLAG_NOBUFFER, "Do not buffer frames when possible."), - ('CUSTOM_IO', lib.AVFMT_FLAG_CUSTOM_IO, + ("CUSTOM_IO", lib.AVFMT_FLAG_CUSTOM_IO, "The caller has supplied a custom AVIOContext, don't avio_close() it."), - ('DISCARD_CORRUPT', lib.AVFMT_FLAG_DISCARD_CORRUPT, + ("DISCARD_CORRUPT", lib.AVFMT_FLAG_DISCARD_CORRUPT, "Discard frames marked corrupted."), - ('FLUSH_PACKETS', lib.AVFMT_FLAG_FLUSH_PACKETS, + ("FLUSH_PACKETS", lib.AVFMT_FLAG_FLUSH_PACKETS, "Flush the AVIOContext every packet."), - ('BITEXACT', lib.AVFMT_FLAG_BITEXACT, + ("BITEXACT", lib.AVFMT_FLAG_BITEXACT, """When muxing, try to avoid writing any random/volatile data to the output. This includes any random IDs, real-time timestamps/dates, muxer version, etc. This flag is mainly intended for testing."""), - ('SORT_DTS', lib.AVFMT_FLAG_SORT_DTS, + ("SORT_DTS", lib.AVFMT_FLAG_SORT_DTS, "Try to interleave outputted packets by dts (using this flag can slow demuxing down)."), - ('FAST_SEEK', lib.AVFMT_FLAG_FAST_SEEK, + ("FAST_SEEK", lib.AVFMT_FLAG_FAST_SEEK, "Enable fast, but inaccurate seeks for some formats."), - ('SHORTEST', lib.AVFMT_FLAG_SHORTEST, + ("SHORTEST", lib.AVFMT_FLAG_SHORTEST, "Stop muxing when the shortest stream stops."), - ('AUTO_BSF', lib.AVFMT_FLAG_AUTO_BSF, + ("AUTO_BSF", lib.AVFMT_FLAG_AUTO_BSF, "Add bitstream filters as requested by the muxer."), ), is_flags=True) @@ -172,16 +172,16 @@ cdef class Container: io_open): if sentinel is not _cinit_sentinel: - raise RuntimeError('cannot construct base Container') + raise RuntimeError("cannot construct base Container") self.writeable = isinstance(self, OutputContainer) if not self.writeable and not isinstance(self, InputContainer): - raise RuntimeError('Container cannot be directly extended.') + raise RuntimeError("Container cannot be directly extended.") if isinstance(file_, str): self.name = file_ else: - self.name = str(getattr(file_, 'name', '')) + self.name = str(getattr(file_, "name", "")) self.options = dict(options or ()) self.container_options = dict(container_options or ()) @@ -280,7 +280,7 @@ cdef class Container: self.close() def __repr__(self): - return '' % (self.__class__.__name__, self.file or self.name) + return "" % (self.__class__.__name__, self.file or self.name) cdef int err_check(self, int value) except -1: return err_check(value, filename=self.name) @@ -289,7 +289,7 @@ cdef class Container: self._assert_open() with LogCapture() as logs: lib.av_dump_format(self.ptr, 0, "", isinstance(self, OutputContainer)) - return ''.join(log[2] for log in logs) + return "".join(log[2] for log in logs) cdef set_timeout(self, timeout): if timeout is None: @@ -318,21 +318,21 @@ cdef class Container: """Flags property of :class:`.Flags`""" ) - gen_pts = flags.flag_property('GENPTS') - ign_idx = flags.flag_property('IGNIDX') - non_block = flags.flag_property('NONBLOCK') - ign_dts = flags.flag_property('IGNDTS') - no_fill_in = flags.flag_property('NOFILLIN') - no_parse = flags.flag_property('NOPARSE') - no_buffer = flags.flag_property('NOBUFFER') - custom_io = flags.flag_property('CUSTOM_IO') - discard_corrupt = flags.flag_property('DISCARD_CORRUPT') - flush_packets = flags.flag_property('FLUSH_PACKETS') - bit_exact = flags.flag_property('BITEXACT') - sort_dts = flags.flag_property('SORT_DTS') - fast_seek = flags.flag_property('FAST_SEEK') - shortest = flags.flag_property('SHORTEST') - auto_bsf = flags.flag_property('AUTO_BSF') + gen_pts = flags.flag_property("GENPTS") + ign_idx = flags.flag_property("IGNIDX") + non_block = flags.flag_property("NONBLOCK") + ign_dts = flags.flag_property("IGNDTS") + no_fill_in = flags.flag_property("NOFILLIN") + no_parse = flags.flag_property("NOPARSE") + no_buffer = flags.flag_property("NOBUFFER") + custom_io = flags.flag_property("CUSTOM_IO") + discard_corrupt = flags.flag_property("DISCARD_CORRUPT") + flush_packets = flags.flag_property("FLUSH_PACKETS") + bit_exact = flags.flag_property("BITEXACT") + sort_dts = flags.flag_property("SORT_DTS") + fast_seek = flags.flag_property("FAST_SEEK") + shortest = flags.flag_property("SHORTEST") + auto_bsf = flags.flag_property("AUTO_BSF") def open( diff --git a/av/container/input.pyx b/av/container/input.pyx index e73fcc29f..eed457a2c 100644 --- a/av/container/input.pyx +++ b/av/container/input.pyx @@ -157,7 +157,7 @@ cdef class InputContainer(Container): for stream in streams: i = stream.index if i >= self.ptr.nb_streams: - raise ValueError('stream index %d out of range' % i) + raise ValueError("stream index %d out of range" % i) include_stream[i] = True while True: @@ -212,7 +212,7 @@ cdef class InputContainer(Container): for frame in packet.decode(): yield frame - def seek(self, offset, *, str whence='time', bint backward=True, + def seek(self, offset, *, str whence="time", bint backward=True, bint any_frame=False, Stream stream=None, bint unsupported_frame_offset=False, bint unsupported_byte_offset=False): @@ -249,16 +249,16 @@ cdef class InputContainer(Container): # was super confusing, so lets go in the complete opposite direction # and reject non-ints. if not isinstance(offset, (int, long)): - raise TypeError('Container.seek only accepts integer offset.', type(offset)) + raise TypeError("Container.seek only accepts integer offset.", type(offset)) cdef int64_t c_offset = offset cdef int flags = 0 cdef int ret # We used to support whence in 'time', 'frame', and 'byte', but later - # realized that FFmpged doens't implement the frame or byte ones. + # realized that FFmpeg doens't implement the frame or byte ones. # We don't even document this anymore, but do allow 'time' to pass through. - if whence != 'time': + if whence != "time": raise ValueError("whence != 'time' is no longer supported") if backward: diff --git a/av/container/output.pyx b/av/container/output.pyx index 788b3214d..8f71b446e 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -60,13 +60,13 @@ cdef class OutputContainer(Container): """ if (codec_name is None and template is None) or (codec_name is not None and template is not None): - raise ValueError('needs one of codec_name or template') + raise ValueError("needs one of codec_name or template") cdef const lib.AVCodec *codec cdef Codec codec_obj if codec_name is not None: - codec_obj = codec_name if isinstance(codec_name, Codec) else Codec(codec_name, 'w') + codec_obj = codec_name if isinstance(codec_name, Codec) else Codec(codec_name, "w") else: if not template.codec_context: raise ValueError("template has no codec context") @@ -192,7 +192,7 @@ cdef class OutputContainer(Container): # ... and warn if any weren't used. unused_options = {k: v for k, v in self.options.items() if k not in used_options} if unused_options: - log.warning('Some options were not used: %s' % unused_options) + log.warning("Some options were not used: %s" % unused_options) self._started = True @@ -218,7 +218,7 @@ cdef class OutputContainer(Container): # Assert the packet is in stream time. if packet.ptr.stream_index < 0 or packet.ptr.stream_index >= self.ptr.nb_streams: - raise ValueError('Bad Packet stream_index.') + raise ValueError("Bad Packet stream_index.") cdef lib.AVStream *stream = self.ptr.streams[packet.ptr.stream_index] packet._rebase_time(stream.time_base) diff --git a/av/container/pyio.pyx b/av/container/pyio.pyx index ed79b44f8..d68a69944 100644 --- a/av/container/pyio.pyx +++ b/av/container/pyio.pyx @@ -15,14 +15,14 @@ cdef class PyIOFile: cdef seek_func_t seek_func = NULL - readable = getattr(self.file, 'readable', None) - writable = getattr(self.file, 'writable', None) - seekable = getattr(self.file, 'seekable', None) - self.fread = getattr(self.file, 'read', None) - self.fwrite = getattr(self.file, 'write', None) - self.fseek = getattr(self.file, 'seek', None) - self.ftell = getattr(self.file, 'tell', None) - self.fclose = getattr(self.file, 'close', None) + readable = getattr(self.file, "readable", None) + writable = getattr(self.file, "writable", None) + seekable = getattr(self.file, "seekable", None) + self.fread = getattr(self.file, "read", None) + self.fwrite = getattr(self.file, "write", None) + self.fseek = getattr(self.file, "seek", None) + self.ftell = getattr(self.file, "tell", None) + self.fclose = getattr(self.file, "close", None) # To be seekable the file object must have `seek` and `tell` methods. # If it also has a `seekable` method, it must return True. diff --git a/av/container/streams.pyx b/av/container/streams.pyx index 8de28a13e..6875d6715 100644 --- a/av/container/streams.pyx +++ b/av/container/streams.pyx @@ -107,7 +107,7 @@ cdef class StreamContainer: elif isinstance(x, dict): for type_, indices in x.items(): - if type_ == 'streams': # For compatibility with the pseudo signature + if type_ == "streams": # For compatibility with the pseudo signature streams = self._streams else: streams = getattr(self, type_) @@ -117,6 +117,6 @@ cdef class StreamContainer: selection.append(streams[i]) else: - raise TypeError('Argument must be Stream or int.', type(x)) + raise TypeError("Argument must be Stream or int.", type(x)) return selection or self._streams[:] diff --git a/av/data/stream.pyx b/av/data/stream.pyx index c019961d0..097777999 100644 --- a/av/data/stream.pyx +++ b/av/data/stream.pyx @@ -4,11 +4,11 @@ cimport libav as lib cdef class DataStream(Stream): def __repr__(self): - return '' % ( + return "" % ( self.__class__.__name__, self.index, - self.type or '', - self.name or '', + self.type or "", + self.name or "", id(self), ) diff --git a/av/descriptor.pyx b/av/descriptor.pyx index 8debfa35e..5840d2d80 100644 --- a/av/descriptor.pyx +++ b/av/descriptor.pyx @@ -17,7 +17,7 @@ cdef class Descriptor: def __cinit__(self, sentinel): if sentinel is not _cinit_sentinel: - raise RuntimeError('Cannot construct av.Descriptor') + raise RuntimeError("Cannot construct av.Descriptor") property name: def __get__(self): return self.ptr.class_name if self.ptr.class_name else None @@ -56,4 +56,4 @@ cdef class Descriptor: return self._options def __repr__(self): - return '<%s %s at 0x%x>' % (self.__class__.__name__, self.name, id(self)) + return "<%s %s at 0x%x>" % (self.__class__.__name__, self.name, id(self)) diff --git a/av/dictionary.pyx b/av/dictionary.pyx index 3ebc09b89..27a265f77 100644 --- a/av/dictionary.pyx +++ b/av/dictionary.pyx @@ -40,7 +40,7 @@ cdef class _Dictionary: yield element.key def __repr__(self): - return 'av.Dictionary(%r)' % dict(self) + return "av.Dictionary(%r)" % dict(self) cpdef _Dictionary copy(self): cdef _Dictionary other = Dictionary() diff --git a/av/enum.pyx b/av/enum.pyx index 522948bbb..bc704a11d 100644 --- a/av/enum.pyx +++ b/av/enum.pyx @@ -85,7 +85,7 @@ class EnumType(type): if not create: return - return self._create('{}_{}'.format(self.__name__.upper(), value), value, by_value_only=True) + return self._create("{}_{}".format(self.__name__.upper(), value), value, by_value_only=True) def _get_multi_flags(self, long value): @@ -106,7 +106,7 @@ class EnumType(type): if to_find: raise KeyError(value) - name = '|'.join(f.name for f in flags) + name = "|".join(f.name for f in flags) cdef EnumFlag combo = self._create(name, value, by_value_only=True) combo.flags = tuple(flags) @@ -125,7 +125,7 @@ class EnumType(type): def _unpickle(mod_name, cls_name, item_name): - mod = __import__(mod_name, fromlist=['.']) + mod = __import__(mod_name, fromlist=["."]) cls = getattr(mod, cls_name) return cls[item_name] @@ -190,7 +190,7 @@ cdef class EnumItem: self._hash = hash_ def __repr__(self): - return '<{}.{}:{}(0x{:x})>'.format( + return "<{}.{}:{}(0x{:x})>".format( self.__class__.__module__, self.__class__.__name__, self.name, @@ -383,6 +383,6 @@ cpdef define_enum(name, module, items, bint is_flags=False): base_cls = EnumItem # Some items may be None if they correspond to an unsupported FFmpeg feature - cls = EnumType(name, (base_cls, ), {'__module__': module}, [i for i in items if i is not None]) + cls = EnumType(name, (base_cls, ), {"__module__": module}, [i for i in items if i is not None]) return cls diff --git a/av/error.pyx b/av/error.pyx index e6257731e..249be7d53 100644 --- a/av/error.pyx +++ b/av/error.pyx @@ -13,8 +13,8 @@ from av.enum import define_enum # Will get extended with all of the exceptions. __all__ = [ - 'ErrorType', 'FFmpegError', 'LookupError', 'HTTPError', 'HTTPClientError', - 'UndefinedError', + "ErrorType", "FFmpegError", "LookupError", "HTTPError", "HTTPClientError", + "UndefinedError", ] @@ -108,21 +108,20 @@ class FFmpegError(Exception): pass def __str__(self): - - msg = f'[Errno {self.errno}] {self.strerror}' + msg = f"[Errno {self.errno}] {self.strerror}" if self.filename: - msg = f'{msg}: {self.filename!r}' + msg = f"{msg}: {self.filename!r}" if self.log: - msg = f'{msg}; last error log: [{self.log[1].strip()}] {self.log[2].strip()}' + msg = f"{msg}; last error log: [{self.log[1].strip()}] {self.log[2].strip()}" return msg # Our custom error, used in callbacks. -cdef int c_PYAV_STASHED_ERROR = tag_to_code(b'PyAV') -cdef str PYAV_STASHED_ERROR_message = 'Error in PyAV callback' +cdef int c_PYAV_STASHED_ERROR = tag_to_code(b"PyAV") +cdef str PYAV_STASHED_ERROR_message = "Error in PyAV callback" # Bases for the FFmpeg-based exceptions. @@ -140,32 +139,32 @@ class HTTPClientError(FFmpegError): # Tuples of (enum_name, enum_value, exc_name, exc_base). _ffmpeg_specs = ( - ('BSF_NOT_FOUND', -lib.AVERROR_BSF_NOT_FOUND, 'BSFNotFoundError', LookupError), - ('BUG', -lib.AVERROR_BUG, None, RuntimeError), - ('BUFFER_TOO_SMALL', -lib.AVERROR_BUFFER_TOO_SMALL, None, ValueError), - ('DECODER_NOT_FOUND', -lib.AVERROR_DECODER_NOT_FOUND, None, LookupError), - ('DEMUXER_NOT_FOUND', -lib.AVERROR_DEMUXER_NOT_FOUND, None, LookupError), - ('ENCODER_NOT_FOUND', -lib.AVERROR_ENCODER_NOT_FOUND, None, LookupError), - ('EOF', -lib.AVERROR_EOF, 'EOFError', EOFError), - ('EXIT', -lib.AVERROR_EXIT, None, None), - ('EXTERNAL', -lib.AVERROR_EXTERNAL, None, None), - ('FILTER_NOT_FOUND', -lib.AVERROR_FILTER_NOT_FOUND, None, LookupError), - ('INVALIDDATA', -lib.AVERROR_INVALIDDATA, 'InvalidDataError', ValueError), - ('MUXER_NOT_FOUND', -lib.AVERROR_MUXER_NOT_FOUND, None, LookupError), - ('OPTION_NOT_FOUND', -lib.AVERROR_OPTION_NOT_FOUND, None, LookupError), - ('PATCHWELCOME', -lib.AVERROR_PATCHWELCOME, 'PatchWelcomeError', None), - ('PROTOCOL_NOT_FOUND', -lib.AVERROR_PROTOCOL_NOT_FOUND, None, LookupError), - ('UNKNOWN', -lib.AVERROR_UNKNOWN, None, None), - ('EXPERIMENTAL', -lib.AVERROR_EXPERIMENTAL, None, None), - ('INPUT_CHANGED', -lib.AVERROR_INPUT_CHANGED, None, None), - ('OUTPUT_CHANGED', -lib.AVERROR_OUTPUT_CHANGED, None, None), - ('HTTP_BAD_REQUEST', -lib.AVERROR_HTTP_BAD_REQUEST, 'HTTPBadRequestError', HTTPClientError), - ('HTTP_UNAUTHORIZED', -lib.AVERROR_HTTP_UNAUTHORIZED, 'HTTPUnauthorizedError', HTTPClientError), - ('HTTP_FORBIDDEN', -lib.AVERROR_HTTP_FORBIDDEN, 'HTTPForbiddenError', HTTPClientError), - ('HTTP_NOT_FOUND', -lib.AVERROR_HTTP_NOT_FOUND, 'HTTPNotFoundError', HTTPClientError), - ('HTTP_OTHER_4XX', -lib.AVERROR_HTTP_OTHER_4XX, 'HTTPOtherClientError', HTTPClientError), - ('HTTP_SERVER_ERROR', -lib.AVERROR_HTTP_SERVER_ERROR, 'HTTPServerError', HTTPError), - ('PYAV_CALLBACK', c_PYAV_STASHED_ERROR, 'PyAVCallbackError', RuntimeError), + ("BSF_NOT_FOUND", -lib.AVERROR_BSF_NOT_FOUND, "BSFNotFoundError", LookupError), + ("BUG", -lib.AVERROR_BUG, None, RuntimeError), + ("BUFFER_TOO_SMALL", -lib.AVERROR_BUFFER_TOO_SMALL, None, ValueError), + ("DECODER_NOT_FOUND", -lib.AVERROR_DECODER_NOT_FOUND, None, LookupError), + ("DEMUXER_NOT_FOUND", -lib.AVERROR_DEMUXER_NOT_FOUND, None, LookupError), + ("ENCODER_NOT_FOUND", -lib.AVERROR_ENCODER_NOT_FOUND, None, LookupError), + ("EOF", -lib.AVERROR_EOF, "EOFError", EOFError), + ("EXIT", -lib.AVERROR_EXIT, None, None), + ("EXTERNAL", -lib.AVERROR_EXTERNAL, None, None), + ("FILTER_NOT_FOUND", -lib.AVERROR_FILTER_NOT_FOUND, None, LookupError), + ("INVALIDDATA", -lib.AVERROR_INVALIDDATA, "InvalidDataError", ValueError), + ("MUXER_NOT_FOUND", -lib.AVERROR_MUXER_NOT_FOUND, None, LookupError), + ("OPTION_NOT_FOUND", -lib.AVERROR_OPTION_NOT_FOUND, None, LookupError), + ("PATCHWELCOME", -lib.AVERROR_PATCHWELCOME, "PatchWelcomeError", None), + ("PROTOCOL_NOT_FOUND", -lib.AVERROR_PROTOCOL_NOT_FOUND, None, LookupError), + ("UNKNOWN", -lib.AVERROR_UNKNOWN, None, None), + ("EXPERIMENTAL", -lib.AVERROR_EXPERIMENTAL, None, None), + ("INPUT_CHANGED", -lib.AVERROR_INPUT_CHANGED, None, None), + ("OUTPUT_CHANGED", -lib.AVERROR_OUTPUT_CHANGED, None, None), + ("HTTP_BAD_REQUEST", -lib.AVERROR_HTTP_BAD_REQUEST, "HTTPBadRequestError", HTTPClientError), + ("HTTP_UNAUTHORIZED", -lib.AVERROR_HTTP_UNAUTHORIZED, "HTTPUnauthorizedError", HTTPClientError), + ("HTTP_FORBIDDEN", -lib.AVERROR_HTTP_FORBIDDEN, "HTTPForbiddenError", HTTPClientError), + ("HTTP_NOT_FOUND", -lib.AVERROR_HTTP_NOT_FOUND, "HTTPNotFoundError", HTTPClientError), + ("HTTP_OTHER_4XX", -lib.AVERROR_HTTP_OTHER_4XX, "HTTPOtherClientError", HTTPClientError), + ("HTTP_SERVER_ERROR", -lib.AVERROR_HTTP_SERVER_ERROR, "HTTPServerError", HTTPError), + ("PYAV_CALLBACK", c_PYAV_STASHED_ERROR, "PyAVCallbackError", RuntimeError), ) @@ -234,35 +233,35 @@ def _extend_builtin(name, codes): # PEP 3151 builtins. -_extend_builtin('PermissionError', (errno.EACCES, errno.EPERM)) -_extend_builtin('BlockingIOError', (errno.EAGAIN, errno.EALREADY, errno.EINPROGRESS, errno.EWOULDBLOCK)) -_extend_builtin('ChildProcessError', (errno.ECHILD, )) -_extend_builtin('ConnectionAbortedError', (errno.ECONNABORTED, )) -_extend_builtin('ConnectionRefusedError', (errno.ECONNREFUSED, )) -_extend_builtin('ConnectionResetError', (errno.ECONNRESET, )) -_extend_builtin('FileExistsError', (errno.EEXIST, )) -_extend_builtin('InterruptedError', (errno.EINTR, )) -_extend_builtin('IsADirectoryError', (errno.EISDIR, )) -_extend_builtin('FileNotFoundError', (errno.ENOENT, )) -_extend_builtin('NotADirectoryError', (errno.ENOTDIR, )) -_extend_builtin('BrokenPipeError', (errno.EPIPE, errno.ESHUTDOWN)) -_extend_builtin('ProcessLookupError', (errno.ESRCH, )) -_extend_builtin('TimeoutError', (errno.ETIMEDOUT, )) +_extend_builtin("PermissionError", (errno.EACCES, errno.EPERM)) +_extend_builtin("BlockingIOError", (errno.EAGAIN, errno.EALREADY, errno.EINPROGRESS, errno.EWOULDBLOCK)) +_extend_builtin("ChildProcessError", (errno.ECHILD, )) +_extend_builtin("ConnectionAbortedError", (errno.ECONNABORTED, )) +_extend_builtin("ConnectionRefusedError", (errno.ECONNREFUSED, )) +_extend_builtin("ConnectionResetError", (errno.ECONNRESET, )) +_extend_builtin("FileExistsError", (errno.EEXIST, )) +_extend_builtin("InterruptedError", (errno.EINTR, )) +_extend_builtin("IsADirectoryError", (errno.EISDIR, )) +_extend_builtin("FileNotFoundError", (errno.ENOENT, )) +_extend_builtin("NotADirectoryError", (errno.ENOTDIR, )) +_extend_builtin("BrokenPipeError", (errno.EPIPE, errno.ESHUTDOWN)) +_extend_builtin("ProcessLookupError", (errno.ESRCH, )) +_extend_builtin("TimeoutError", (errno.ETIMEDOUT, )) # Other obvious ones. -_extend_builtin('ValueError', (errno.EINVAL, )) -_extend_builtin('MemoryError', (errno.ENOMEM, )) -_extend_builtin('NotImplementedError', (errno.ENOSYS, )) -_extend_builtin('OverflowError', (errno.ERANGE, )) +_extend_builtin("ValueError", (errno.EINVAL, )) +_extend_builtin("MemoryError", (errno.ENOMEM, )) +_extend_builtin("NotImplementedError", (errno.ENOSYS, )) +_extend_builtin("OverflowError", (errno.ERANGE, )) if IOError is not OSError: - _extend_builtin('IOError', (errno.EIO, )) + _extend_builtin("IOError", (errno.EIO, )) # The rest of them (for now) -_extend_builtin('OSError', [code for code in errno.errorcode if code not in classes]) +_extend_builtin("OSError", [code for code in errno.errorcode if code not in classes]) # Classes for the FFmpeg errors. for enum_name, code, name, base in _ffmpeg_specs: - name = name or enum_name.title().replace('_', '') + 'Error' + name = name or enum_name.title().replace("_", "") + "Error" if base is None: bases = (FFmpegError, ) @@ -289,9 +288,9 @@ cdef int stash_exception(exc_info=None): global _err_count - existing = getattr(_local, 'exc_info', None) + existing = getattr(_local, "exc_info", None) if existing is not None: - print >> sys.stderr, 'PyAV library exception being dropped:' + print >> sys.stderr, "PyAV library exception being dropped:" traceback.print_exception(*existing) _err_count -= 1 # Balance out the +=1 that is coming. @@ -313,7 +312,7 @@ cpdef int err_check(int res, filename=None) except -1: # Check for stashed exceptions. if _err_count: - exc_info = getattr(_local, 'exc_info', None) + exc_info = getattr(_local, "exc_info", None) if exc_info is not None: _err_count -= 1 _local.exc_info = None @@ -354,7 +353,7 @@ cpdef make_error(int res, filename=None, log=None): c_buffer = py_buffer lib.av_strerror(res, c_buffer, lib.AV_ERROR_MAX_STRING_SIZE) py_buffer = c_buffer - message = py_buffer.decode('latin1') + message = py_buffer.decode("latin1") # Default to the OS if we have no message; this should not get called. message = message or os.strerror(code) diff --git a/av/filter/context.pyx b/av/filter/context.pyx index 4505c7cd3..1a1522d40 100644 --- a/av/filter/context.pyx +++ b/av/filter/context.pyx @@ -23,11 +23,11 @@ cdef class FilterContext: def __cinit__(self, sentinel): if sentinel is not _cinit_sentinel: - raise RuntimeError('cannot construct FilterContext') + raise RuntimeError("cannot construct FilterContext") def __repr__(self): - return '' % ( - (repr(self.ptr.name) if self.ptr.name != NULL else '') if self.ptr != NULL else 'None', + return "" % ( + (repr(self.ptr.name) if self.ptr.name != NULL else "") if self.ptr != NULL else "None", self.filter.ptr.name if self.filter and self.filter.ptr != NULL else None, id(self), ) @@ -52,9 +52,9 @@ cdef class FilterContext: def init(self, args=None, **kwargs): if self.inited: - raise ValueError('already inited') + raise ValueError("already inited") if args and kwargs: - raise ValueError('cannot init from args and kwargs') + raise ValueError("cannot init from args and kwargs") cdef _Dictionary dict_ = None cdef char *c_args = NULL @@ -68,7 +68,7 @@ cdef class FilterContext: self.inited = True if dict_: - raise ValueError('unused config: %s' % ', '.join(sorted(dict_))) + raise ValueError("unused config: %s" % ", ".join(sorted(dict_))) def link_to(self, FilterContext input_, int output_idx=0, int input_idx=0): err_check(lib.avfilter_link(self.ptr, output_idx, input_.ptr, input_idx)) @@ -81,7 +81,7 @@ cdef class FilterContext: res = lib.av_buffersrc_write_frame(self.ptr, NULL) err_check(res) return - elif self.filter.name in ('abuffer', 'buffer'): + elif self.filter.name in ("abuffer", "buffer"): with nogil: res = lib.av_buffersrc_write_frame(self.ptr, frame.ptr) err_check(res) @@ -89,25 +89,25 @@ cdef class FilterContext: # Delegate to the input. if len(self.inputs) != 1: - raise ValueError('cannot delegate push without single input; found %d' % len(self.inputs)) + raise ValueError("cannot delegate push without single input; found %d" % len(self.inputs)) if not self.inputs[0].link: - raise ValueError('cannot delegate push without linked input') + raise ValueError("cannot delegate push without linked input") self.inputs[0].linked.context.push(frame) def pull(self): cdef Frame frame cdef int res - if self.filter.name == 'buffersink': + if self.filter.name == "buffersink": frame = alloc_video_frame() - elif self.filter.name == 'abuffersink': + elif self.filter.name == "abuffersink": frame = alloc_audio_frame() else: # Delegate to the output. if len(self.outputs) != 1: - raise ValueError('cannot delegate pull without single output; found %d' % len(self.outputs)) + raise ValueError("cannot delegate pull without single output; found %d" % len(self.outputs)) if not self.outputs[0].link: - raise ValueError('cannot delegate pull without linked output') + raise ValueError("cannot delegate pull without linked output") return self.outputs[0].linked.context.pull() self.graph.configure() diff --git a/av/filter/filter.pyx b/av/filter/filter.pyx index 57090a47d..611dfd81e 100644 --- a/av/filter/filter.pyx +++ b/av/filter/filter.pyx @@ -27,10 +27,10 @@ cdef class Filter: if name is _cinit_sentinel: return if not isinstance(name, str): - raise TypeError('takes a filter name as a string') + raise TypeError("takes a filter name as a string") self.ptr = lib.avfilter_get_by_name(name) if not self.ptr: - raise ValueError('no filter %s' % name) + raise ValueError("no filter %s" % name) property descriptor: def __get__(self): diff --git a/av/filter/graph.pyx b/av/filter/graph.pyx index bf71fac9e..7253c25b4 100644 --- a/av/filter/graph.pyx +++ b/av/filter/graph.pyx @@ -33,7 +33,7 @@ cdef class Graph: count = self._name_counts.get(name, 0) self._name_counts[name] = count + 1 if count: - return '%s_%s' % (name, count) + return "%s_%s" % (name, count) else: return name @@ -45,7 +45,7 @@ cdef class Graph: # for ctx in self._context_by_ptr.itervalues(): # for in_ in ctx.inputs: # if not in_.link: - # if in_.type == 'video': + # if in_.type == "video": # pass err_check(lib.avfilter_graph_config(self.ptr, NULL)) @@ -84,7 +84,7 @@ cdef class Graph: else: raise TypeError("filter must be a string or Filter") - cdef str name = self._get_unique_name(kwargs.pop('name', None) or cy_filter.name) + cdef str name = self._get_unique_name(kwargs.pop("name", None) or cy_filter.name) cdef lib.AVFilterContext *ptr = lib.avfilter_graph_alloc_filter(self.ptr, cy_filter.ptr, name) if not ptr: @@ -136,24 +136,24 @@ cdef class Graph: time_base = template.time_base if width is None: - raise ValueError('missing width') + raise ValueError("missing width") if height is None: - raise ValueError('missing height') + raise ValueError("missing height") if format is None: - raise ValueError('missing format') + raise ValueError("missing format") if time_base is None: - warnings.warn('missing time_base. Guessing 1/1000 time base. ' - 'This is deprecated and may be removed in future releases.', + warnings.warn("missing time_base. Guessing 1/1000 time base. " + "This is deprecated and may be removed in future releases.", DeprecationWarning) time_base = Fraction(1, 1000) return self.add( - 'buffer', + "buffer", name=name, - video_size=f'{width}x{height}', + video_size=f"{width}x{height}", pix_fmt=str(int(VideoFormat(format))), time_base=str(time_base), - pixel_aspect='1/1', + pixel_aspect="1/1", ) def add_abuffer(self, template=None, sample_rate=None, format=None, layout=None, channels=None, name=None, time_base=None): @@ -174,11 +174,11 @@ cdef class Graph: time_base = template.time_base if sample_rate is None: - raise ValueError('missing sample_rate') + raise ValueError("missing sample_rate") if format is None: - raise ValueError('missing format') + raise ValueError("missing format") if layout is None and channels is None: - raise ValueError('missing layout or channels') + raise ValueError("missing layout or channels") if time_base is None: time_base = Fraction(1, sample_rate) @@ -188,35 +188,35 @@ cdef class Graph: time_base=str(time_base), ) if layout: - kwargs['channel_layout'] = AudioLayout(layout).name + kwargs["channel_layout"] = AudioLayout(layout).name if channels: - kwargs['channels'] = str(channels) + kwargs["channels"] = str(channels) - return self.add('abuffer', name=name, **kwargs) + return self.add("abuffer", name=name, **kwargs) def push(self, frame): if frame is None: - contexts = self._context_by_type.get('buffer', []) + self._context_by_type.get('abuffer', []) + contexts = self._context_by_type.get("buffer", []) + self._context_by_type.get("abuffer", []) elif isinstance(frame, VideoFrame): - contexts = self._context_by_type.get('buffer', []) + contexts = self._context_by_type.get("buffer", []) elif isinstance(frame, AudioFrame): - contexts = self._context_by_type.get('abuffer', []) + contexts = self._context_by_type.get("abuffer", []) else: - raise ValueError('can only AudioFrame, VideoFrame or None; got %s' % type(frame)) + raise ValueError("can only AudioFrame, VideoFrame or None; got %s" % type(frame)) if len(contexts) != 1: - raise ValueError('can only auto-push with single buffer; found %s' % len(contexts)) + raise ValueError("can only auto-push with single buffer; found %s" % len(contexts)) contexts[0].push(frame) def pull(self): - vsinks = self._context_by_type.get('buffersink', []) - asinks = self._context_by_type.get('abuffersink', []) + vsinks = self._context_by_type.get("buffersink", []) + asinks = self._context_by_type.get("abuffersink", []) nsinks = len(vsinks) + len(asinks) if nsinks != 1: - raise ValueError('can only auto-pull with single sink; found %s' % nsinks) + raise ValueError("can only auto-pull with single sink; found %s" % nsinks) return (vsinks or asinks)[0].pull() diff --git a/av/filter/link.pyx b/av/filter/link.pyx index c99e27f3b..b9e28e082 100644 --- a/av/filter/link.pyx +++ b/av/filter/link.pyx @@ -10,7 +10,7 @@ cdef class FilterLink: def __cinit__(self, sentinel): if sentinel is not _cinit_sentinel: - raise RuntimeError('cannot instantiate FilterLink') + raise RuntimeError("cannot instantiate FilterLink") property input: def __get__(self): @@ -22,7 +22,7 @@ cdef class FilterLink: if self.ptr == cctx.outputs[i]: break else: - raise RuntimeError('could not find link in context') + raise RuntimeError("could not find link in context") ctx = self.graph._context_by_ptr[cctx] self._input = ctx.outputs[i] return self._input @@ -37,11 +37,11 @@ cdef class FilterLink: if self.ptr == cctx.inputs[i]: break else: - raise RuntimeError('could not find link in context') + raise RuntimeError("could not find link in context") try: ctx = self.graph._context_by_ptr[cctx] except KeyError: - raise RuntimeError('could not find context in graph', (cctx.name, cctx.filter.name)) + raise RuntimeError("could not find context in graph", (cctx.name, cctx.filter.name)) self._output = ctx.inputs[i] return self._output diff --git a/av/filter/pad.pyx b/av/filter/pad.pyx index 482b2fc36..66915dcf4 100644 --- a/av/filter/pad.pyx +++ b/av/filter/pad.pyx @@ -8,12 +8,12 @@ cdef class FilterPad: def __cinit__(self, sentinel): if sentinel is not _cinit_sentinel: - raise RuntimeError('cannot construct FilterPad') + raise RuntimeError("cannot construct FilterPad") def __repr__(self): - return '' % ( + return "" % ( self.filter.name, - 'inputs' if self.is_input else 'outputs', + "inputs" if self.is_input else "outputs", self.index, self.name, self.type, @@ -43,9 +43,9 @@ cdef class FilterContextPad(FilterPad): def __repr__(self): - return '' % ( + return "" % ( self.filter.name, - 'inputs' if self.is_input else 'outputs', + "inputs" if self.is_input else "outputs", self.index, self.context.name, self.name, diff --git a/av/format.pyx b/av/format.pyx index 7afe4416f..07f883cdc 100644 --- a/av/format.pyx +++ b/av/format.pyx @@ -8,7 +8,7 @@ cdef object _cinit_bypass_sentinel = object() cdef ContainerFormat build_container_format(lib.AVInputFormat* iptr, lib.AVOutputFormat* optr): if not iptr and not optr: - raise ValueError('needs input format or output format') + raise ValueError("needs input format or output format") cdef ContainerFormat format = ContainerFormat.__new__(ContainerFormat, _cinit_bypass_sentinel) format.iptr = iptr format.optr = optr @@ -16,46 +16,36 @@ cdef ContainerFormat build_container_format(lib.AVInputFormat* iptr, lib.AVOutpu return format -Flags = define_enum('Flags', __name__, ( - ('NOFILE', lib.AVFMT_NOFILE), - ('NEEDNUMBER', lib.AVFMT_NEEDNUMBER, - """Needs '%d' in filename."""), - ('SHOW_IDS', lib.AVFMT_SHOW_IDS, - """Show format stream IDs numbers."""), - ('GLOBALHEADER', lib.AVFMT_GLOBALHEADER, - """Format wants global header."""), - ('NOTIMESTAMPS', lib.AVFMT_NOTIMESTAMPS, - """Format does not need / have any timestamps."""), - ('GENERIC_INDEX', lib.AVFMT_GENERIC_INDEX, - """Use generic index building code."""), - ('TS_DISCONT', lib.AVFMT_TS_DISCONT, +Flags = define_enum("Flags", __name__, ( + ("NOFILE", lib.AVFMT_NOFILE), + ("NEEDNUMBER", lib.AVFMT_NEEDNUMBER, "Needs '%d' in filename."), + ("SHOW_IDS", lib.AVFMT_SHOW_IDS, "Show format stream IDs numbers."), + ("GLOBALHEADER", lib.AVFMT_GLOBALHEADER, "Format wants global header."), + ("NOTIMESTAMPS", lib.AVFMT_NOTIMESTAMPS, "Format does not need / have any timestamps."), + ("GENERIC_INDEX", lib.AVFMT_GENERIC_INDEX, "Use generic index building code."), + ("TS_DISCONT", lib.AVFMT_TS_DISCONT, """Format allows timestamp discontinuities. Note, muxers always require valid (monotone) timestamps"""), - ('VARIABLE_FPS', lib.AVFMT_VARIABLE_FPS, - """Format allows variable fps."""), - ('NODIMENSIONS', lib.AVFMT_NODIMENSIONS, - """Format does not need width/height"""), - ('NOSTREAMS', lib.AVFMT_NOSTREAMS, - """Format does not require any streams"""), - ('NOBINSEARCH', lib.AVFMT_NOBINSEARCH, - """Format does not allow to fall back on binary search via read_timestamp"""), - ('NOGENSEARCH', lib.AVFMT_NOGENSEARCH, - """Format does not allow to fall back on generic search"""), - ('NO_BYTE_SEEK', lib.AVFMT_NO_BYTE_SEEK, - """Format does not allow seeking by bytes"""), - ('ALLOW_FLUSH', lib.AVFMT_ALLOW_FLUSH, + ("VARIABLE_FPS", lib.AVFMT_VARIABLE_FPS, "Format allows variable fps."), + ("NODIMENSIONS", lib.AVFMT_NODIMENSIONS, "Format does not need width/height"), + ("NOSTREAMS", lib.AVFMT_NOSTREAMS, "Format does not require any streams"), + ("NOBINSEARCH", lib.AVFMT_NOBINSEARCH, + "Format does not allow to fall back on binary search via read_timestamp"), + ("NOGENSEARCH", lib.AVFMT_NOGENSEARCH, + "Format does not allow to fall back on generic search"), + ("NO_BYTE_SEEK", lib.AVFMT_NO_BYTE_SEEK, "Format does not allow seeking by bytes"), + ("ALLOW_FLUSH", lib.AVFMT_ALLOW_FLUSH, """Format allows flushing. If not set, the muxer will not receive a NULL packet in the write_packet function."""), - ('TS_NONSTRICT', lib.AVFMT_TS_NONSTRICT, + ("TS_NONSTRICT", lib.AVFMT_TS_NONSTRICT, """Format does not require strictly increasing timestamps, but they must still be monotonic."""), - ('TS_NEGATIVE', lib.AVFMT_TS_NEGATIVE, + ("TS_NEGATIVE", lib.AVFMT_TS_NEGATIVE, """Format allows muxing negative timestamps. If not set the timestamp will be shifted in av_write_frame and av_interleaved_write_frame so they start from 0. The user or muxer can override this through AVFormatContext.avoid_negative_ts"""), - ('SEEK_TO_PTS', lib.AVFMT_SEEK_TO_PTS, - """Seeking is based on PTS"""), + ("SEEK_TO_PTS", lib.AVFMT_SEEK_TO_PTS, "Seeking is based on PTS"), ), is_flags=True) @@ -80,17 +70,17 @@ cdef class ContainerFormat: self.name = name # Searches comma-seperated names. - if mode is None or mode == 'r': + if mode is None or mode == "r": self.iptr = lib.av_find_input_format(name) - if mode is None or mode == 'w': + if mode is None or mode == "w": self.optr = lib.av_guess_format(name, NULL, NULL) if not self.iptr and not self.optr: - raise ValueError('no container format %r' % name) + raise ValueError("no container format %r" % name) def __repr__(self): - return '' % (self.__class__.__name__, self.name) + return "" % (self.__class__.__name__, self.name) property descriptor: def __get__(self): @@ -141,9 +131,9 @@ cdef class ContainerFormat: def __get__(self): cdef set exts = set() if self.iptr and self.iptr.extensions: - exts.update(self.iptr.extensions.split(',')) + exts.update(self.iptr.extensions.split(",")) if self.optr and self.optr.extensions: - exts.update(self.optr.extensions.split(',')) + exts.update(self.optr.extensions.split(",")) return exts @Flags.property @@ -153,23 +143,23 @@ cdef class ContainerFormat: (self.optr.flags if self.optr else 0) ) - no_file = flags.flag_property('NOFILE') - need_number = flags.flag_property('NEEDNUMBER') - show_ids = flags.flag_property('SHOW_IDS') - global_header = flags.flag_property('GLOBALHEADER') - no_timestamps = flags.flag_property('NOTIMESTAMPS') - generic_index = flags.flag_property('GENERIC_INDEX') - ts_discont = flags.flag_property('TS_DISCONT') - variable_fps = flags.flag_property('VARIABLE_FPS') - no_dimensions = flags.flag_property('NODIMENSIONS') - no_streams = flags.flag_property('NOSTREAMS') - no_bin_search = flags.flag_property('NOBINSEARCH') - no_gen_search = flags.flag_property('NOGENSEARCH') - no_byte_seek = flags.flag_property('NO_BYTE_SEEK') - allow_flush = flags.flag_property('ALLOW_FLUSH') - ts_nonstrict = flags.flag_property('TS_NONSTRICT') - ts_negative = flags.flag_property('TS_NEGATIVE') - seek_to_pts = flags.flag_property('SEEK_TO_PTS') + no_file = flags.flag_property("NOFILE") + need_number = flags.flag_property("NEEDNUMBER") + show_ids = flags.flag_property("SHOW_IDS") + global_header = flags.flag_property("GLOBALHEADER") + no_timestamps = flags.flag_property("NOTIMESTAMPS") + generic_index = flags.flag_property("GENERIC_INDEX") + ts_discont = flags.flag_property("TS_DISCONT") + variable_fps = flags.flag_property("VARIABLE_FPS") + no_dimensions = flags.flag_property("NODIMENSIONS") + no_streams = flags.flag_property("NOSTREAMS") + no_bin_search = flags.flag_property("NOBINSEARCH") + no_gen_search = flags.flag_property("NOGENSEARCH") + no_byte_seek = flags.flag_property("NO_BYTE_SEEK") + allow_flush = flags.flag_property("ALLOW_FLUSH") + ts_nonstrict = flags.flag_property("TS_NONSTRICT") + ts_negative = flags.flag_property("TS_NEGATIVE") + seek_to_pts = flags.flag_property("SEEK_TO_PTS") cdef get_output_format_names(): diff --git a/av/frame.pyx b/av/frame.pyx index 50be5b8c9..d81fd6acb 100644 --- a/av/frame.pyx +++ b/av/frame.pyx @@ -21,7 +21,7 @@ cdef class Frame: lib.av_frame_free(&self.ptr) def __repr__(self): - return 'av.%s #%d pts=%s at 0x%x>' % ( + return "av.%s #%d pts=%s at 0x%x>" % ( self.__class__.__name__, self.index, self.pts, @@ -47,7 +47,7 @@ cdef class Frame: cdef _rebase_time(self, lib.AVRational dst): if not dst.num: - raise ValueError('Cannot rebase to zero time.') + raise ValueError("Cannot rebase to zero time.") if not self._time_base.num: self._time_base = dst diff --git a/av/logging.pyx b/av/logging.pyx index 131b9a69f..fa2ced823 100644 --- a/av/logging.pyx +++ b/av/logging.pyx @@ -82,8 +82,8 @@ cpdef adapt_level(int level): cdef int level_threshold = lib.AV_LOG_VERBOSE # ... but lets limit ourselves to WARNING (assuming nobody already did this). -if 'libav' not in logging.Logger.manager.loggerDict: - logging.getLogger('libav').setLevel(logging.WARNING) +if "libav" not in logging.Logger.manager.loggerDict: + logging.getLogger("libav").setLevel(logging.WARNING) def get_level(): @@ -278,8 +278,8 @@ cdef log_callback_gil(int level, const char *c_name, const char *c_message): global last_log global last_error - name = c_name if c_name is not NULL else '' - message = (c_message).decode('utf8', 'backslashreplace') + name = c_name if c_name is not NULL else "" + message = (c_message).decode("utf8", "backslashreplace") log = (level, name, message) # We have to filter it ourselves, but we will still process it in general so @@ -339,7 +339,7 @@ cdef log_callback_emit(log): py_level = adapt_level(lib_level) - logger_name = 'libav.' + name if name else 'libav.generic' + logger_name = "libav." + name if name else "libav.generic" logger = logging.getLogger(logger_name) logger.log(py_level, message.strip()) @@ -347,5 +347,5 @@ cdef log_callback_emit(log): # Start the magic! # We allow the user to fully disable the logging system as it will not play # nicely with subinterpreters due to FFmpeg-created threads. -if os.environ.get('PYAV_LOGGING') != 'off': +if os.environ.get("PYAV_LOGGING") != "off": lib.av_log_set_callback(log_callback) diff --git a/av/option.pyx b/av/option.pyx index 731a6d508..02bc8e83a 100644 --- a/av/option.pyx +++ b/av/option.pyx @@ -15,26 +15,26 @@ cdef Option wrap_option(tuple choices, const lib.AVOption *ptr): return obj -OptionType = define_enum('OptionType', __name__, ( - ('FLAGS', lib.AV_OPT_TYPE_FLAGS), - ('INT', lib.AV_OPT_TYPE_INT), - ('INT64', lib.AV_OPT_TYPE_INT64), - ('DOUBLE', lib.AV_OPT_TYPE_DOUBLE), - ('FLOAT', lib.AV_OPT_TYPE_FLOAT), - ('STRING', lib.AV_OPT_TYPE_STRING), - ('RATIONAL', lib.AV_OPT_TYPE_RATIONAL), - ('BINARY', lib.AV_OPT_TYPE_BINARY), - ('DICT', lib.AV_OPT_TYPE_DICT), - # ('UINT64', lib.AV_OPT_TYPE_UINT64), # Added recently, and not yet used AFAICT. - ('CONST', lib.AV_OPT_TYPE_CONST), - ('IMAGE_SIZE', lib.AV_OPT_TYPE_IMAGE_SIZE), - ('PIXEL_FMT', lib.AV_OPT_TYPE_PIXEL_FMT), - ('SAMPLE_FMT', lib.AV_OPT_TYPE_SAMPLE_FMT), - ('VIDEO_RATE', lib.AV_OPT_TYPE_VIDEO_RATE), - ('DURATION', lib.AV_OPT_TYPE_DURATION), - ('COLOR', lib.AV_OPT_TYPE_COLOR), - ('CHANNEL_LAYOUT', lib.AV_OPT_TYPE_CHANNEL_LAYOUT), - ('BOOL', lib.AV_OPT_TYPE_BOOL), +OptionType = define_enum("OptionType", __name__, ( + ("FLAGS", lib.AV_OPT_TYPE_FLAGS), + ("INT", lib.AV_OPT_TYPE_INT), + ("INT64", lib.AV_OPT_TYPE_INT64), + ("DOUBLE", lib.AV_OPT_TYPE_DOUBLE), + ("FLOAT", lib.AV_OPT_TYPE_FLOAT), + ("STRING", lib.AV_OPT_TYPE_STRING), + ("RATIONAL", lib.AV_OPT_TYPE_RATIONAL), + ("BINARY", lib.AV_OPT_TYPE_BINARY), + ("DICT", lib.AV_OPT_TYPE_DICT), + # ("UINT64", lib.AV_OPT_TYPE_UINT64), # Added recently, and not yet used AFAICT. + ("CONST", lib.AV_OPT_TYPE_CONST), + ("IMAGE_SIZE", lib.AV_OPT_TYPE_IMAGE_SIZE), + ("PIXEL_FMT", lib.AV_OPT_TYPE_PIXEL_FMT), + ("SAMPLE_FMT", lib.AV_OPT_TYPE_SAMPLE_FMT), + ("VIDEO_RATE", lib.AV_OPT_TYPE_VIDEO_RATE), + ("DURATION", lib.AV_OPT_TYPE_DURATION), + ("COLOR", lib.AV_OPT_TYPE_COLOR), + ("CHANNEL_LAYOUT", lib.AV_OPT_TYPE_CHANNEL_LAYOUT), + ("BOOL", lib.AV_OPT_TYPE_BOOL), )) cdef tuple _INT_TYPES = ( @@ -48,22 +48,21 @@ cdef tuple _INT_TYPES = ( lib.AV_OPT_TYPE_BOOL, ) -OptionFlags = define_enum('OptionFlags', __name__, ( - ('ENCODING_PARAM', lib.AV_OPT_FLAG_ENCODING_PARAM), - ('DECODING_PARAM', lib.AV_OPT_FLAG_DECODING_PARAM), - ('AUDIO_PARAM', lib.AV_OPT_FLAG_AUDIO_PARAM), - ('VIDEO_PARAM', lib.AV_OPT_FLAG_VIDEO_PARAM), - ('SUBTITLE_PARAM', lib.AV_OPT_FLAG_SUBTITLE_PARAM), - ('EXPORT', lib.AV_OPT_FLAG_EXPORT), - ('READONLY', lib.AV_OPT_FLAG_READONLY), - ('FILTERING_PARAM', lib.AV_OPT_FLAG_FILTERING_PARAM), +OptionFlags = define_enum("OptionFlags", __name__, ( + ("ENCODING_PARAM", lib.AV_OPT_FLAG_ENCODING_PARAM), + ("DECODING_PARAM", lib.AV_OPT_FLAG_DECODING_PARAM), + ("AUDIO_PARAM", lib.AV_OPT_FLAG_AUDIO_PARAM), + ("VIDEO_PARAM", lib.AV_OPT_FLAG_VIDEO_PARAM), + ("SUBTITLE_PARAM", lib.AV_OPT_FLAG_SUBTITLE_PARAM), + ("EXPORT", lib.AV_OPT_FLAG_EXPORT), + ("READONLY", lib.AV_OPT_FLAG_READONLY), + ("FILTERING_PARAM", lib.AV_OPT_FLAG_FILTERING_PARAM), ), is_flags=True) cdef class BaseOption: - def __cinit__(self, sentinel): if sentinel is not _cinit_sentinel: - raise RuntimeError('Cannot construct av.%s' % self.__class__.__name__) + raise RuntimeError(f"Cannot construct av.{self.__class__.__name__}") property name: def __get__(self): @@ -71,7 +70,7 @@ cdef class BaseOption: property help: def __get__(self): - return self.ptr.help if self.ptr.help != NULL else '' + return self.ptr.help if self.ptr.help != NULL else "" property flags: def __get__(self): @@ -105,7 +104,6 @@ cdef class BaseOption: cdef class Option(BaseOption): - property type: def __get__(self): return OptionType._get(self.ptr.type, create=True) @@ -128,7 +126,7 @@ cdef class Option(BaseOption): if self.ptr.type in (lib.AV_OPT_TYPE_STRING, lib.AV_OPT_TYPE_BINARY, lib.AV_OPT_TYPE_IMAGE_SIZE, lib.AV_OPT_TYPE_VIDEO_RATE, lib.AV_OPT_TYPE_COLOR): - return self.ptr.default_val.str if self.ptr.default_val.str != NULL else '' + return self.ptr.default_val.str if self.ptr.default_val.str != NULL else "" def _norm_range(self, value): if self.ptr.type in _INT_TYPES: @@ -144,7 +142,7 @@ cdef class Option(BaseOption): return self._norm_range(self.ptr.max) def __repr__(self): - return '' % ( + return "" % ( self.__class__.__name__, self.name, self.type, @@ -173,4 +171,4 @@ cdef class OptionChoice(BaseOption): return self.ptr.default_val.i64 def __repr__(self): - return '' % (self.__class__.__name__, self.name, id(self)) + return "" % (self.__class__.__name__, self.name, id(self)) diff --git a/av/packet.pyx b/av/packet.pyx index af77e2c10..b5736facd 100644 --- a/av/packet.pyx +++ b/av/packet.pyx @@ -48,7 +48,7 @@ cdef class Packet(Buffer): lib.av_packet_free(&self.ptr) def __repr__(self): - return '' % ( + return "" % ( self.__class__.__name__, self._stream.index if self._stream else 0, self.dts, @@ -66,7 +66,7 @@ cdef class Packet(Buffer): cdef _rebase_time(self, lib.AVRational dst): if not dst.num: - raise ValueError('Cannot rebase to zero time.') + raise ValueError("Cannot rebase to zero time.") if not self._time_base.num: self._time_base = dst diff --git a/av/plane.pyx b/av/plane.pyx index e3ec291a6..5d1320b1a 100644 --- a/av/plane.pyx +++ b/av/plane.pyx @@ -11,7 +11,7 @@ cdef class Plane(Buffer): self.index = index def __repr__(self): - return '' % ( + return "" % ( self.__class__.__name__, self.buffer_size, self.buffer_ptr, diff --git a/av/sidedata/motionvectors.pyx b/av/sidedata/motionvectors.pyx index f1e12d56f..af83132c8 100644 --- a/av/sidedata/motionvectors.pyx +++ b/av/sidedata/motionvectors.pyx @@ -14,7 +14,7 @@ cdef class _MotionVectors(SideData): self._len = self.ptr.size // sizeof(lib.AVMotionVector) def __repr__(self): - return f'self.ptr.data:0x}' + return f"self.ptr.data:0x}" def __getitem__(self, int index): @@ -35,17 +35,17 @@ cdef class _MotionVectors(SideData): def to_ndarray(self): import numpy as np return np.frombuffer(self, dtype=np.dtype([ - ('source', 'int32'), - ('w', 'uint8'), - ('h', 'uint8'), - ('src_x', 'int16'), - ('src_y', 'int16'), - ('dst_x', 'int16'), - ('dst_y', 'int16'), - ('flags', 'uint64'), - ('motion_x', 'int32'), - ('motion_y', 'int32'), - ('motion_scale', 'uint16'), + ("source", "int32"), + ("w", "uint8"), + ("h", "uint8"), + ("src_x", "int16"), + ("src_y", "int16"), + ("dst_x", "int16"), + ("dst_y", "int16"), + ("flags", "uint64"), + ("motion_x", "int32"), + ("motion_y", "int32"), + ("motion_scale", "uint16"), ], align=True)) @@ -57,13 +57,13 @@ cdef class MotionVector: def __init__(self, sentinel, _MotionVectors parent, int index): if sentinel is not _cinit_bypass_sentinel: - raise RuntimeError('cannot manually instatiate MotionVector') + raise RuntimeError("cannot manually instatiate MotionVector") self.parent = parent cdef lib.AVMotionVector *base = parent.ptr.data self.ptr = base + index def __repr__(self): - return f'' + return f"" @property def source(self): diff --git a/av/sidedata/sidedata.pyx b/av/sidedata/sidedata.pyx index 49ea4ad6a..201e14cfe 100644 --- a/av/sidedata/sidedata.pyx +++ b/av/sidedata/sidedata.pyx @@ -8,24 +8,24 @@ from av.sidedata.motionvectors import MotionVectors cdef object _cinit_bypass_sentinel = object() -Type = define_enum('Type', __name__, ( - ('PANSCAN', lib.AV_FRAME_DATA_PANSCAN), - ('A53_CC', lib.AV_FRAME_DATA_A53_CC), - ('STEREO3D', lib.AV_FRAME_DATA_STEREO3D), - ('MATRIXENCODING', lib.AV_FRAME_DATA_MATRIXENCODING), - ('DOWNMIX_INFO', lib.AV_FRAME_DATA_DOWNMIX_INFO), - ('REPLAYGAIN', lib.AV_FRAME_DATA_REPLAYGAIN), - ('DISPLAYMATRIX', lib.AV_FRAME_DATA_DISPLAYMATRIX), - ('AFD', lib.AV_FRAME_DATA_AFD), - ('MOTION_VECTORS', lib.AV_FRAME_DATA_MOTION_VECTORS), - ('SKIP_SAMPLES', lib.AV_FRAME_DATA_SKIP_SAMPLES), - ('AUDIO_SERVICE_TYPE', lib.AV_FRAME_DATA_AUDIO_SERVICE_TYPE), - ('MASTERING_DISPLAY_METADATA', lib.AV_FRAME_DATA_MASTERING_DISPLAY_METADATA), - ('GOP_TIMECODE', lib.AV_FRAME_DATA_GOP_TIMECODE), - ('SPHERICAL', lib.AV_FRAME_DATA_SPHERICAL), - ('CONTENT_LIGHT_LEVEL', lib.AV_FRAME_DATA_CONTENT_LIGHT_LEVEL), - ('ICC_PROFILE', lib.AV_FRAME_DATA_ICC_PROFILE), - ('SEI_UNREGISTERED', lib.AV_FRAME_DATA_SEI_UNREGISTERED) if lib.AV_FRAME_DATA_SEI_UNREGISTERED != -1 else None, +Type = define_enum("Type", __name__, ( + ("PANSCAN", lib.AV_FRAME_DATA_PANSCAN), + ("A53_CC", lib.AV_FRAME_DATA_A53_CC), + ("STEREO3D", lib.AV_FRAME_DATA_STEREO3D), + ("MATRIXENCODING", lib.AV_FRAME_DATA_MATRIXENCODING), + ("DOWNMIX_INFO", lib.AV_FRAME_DATA_DOWNMIX_INFO), + ("REPLAYGAIN", lib.AV_FRAME_DATA_REPLAYGAIN), + ("DISPLAYMATRIX", lib.AV_FRAME_DATA_DISPLAYMATRIX), + ("AFD", lib.AV_FRAME_DATA_AFD), + ("MOTION_VECTORS", lib.AV_FRAME_DATA_MOTION_VECTORS), + ("SKIP_SAMPLES", lib.AV_FRAME_DATA_SKIP_SAMPLES), + ("AUDIO_SERVICE_TYPE", lib.AV_FRAME_DATA_AUDIO_SERVICE_TYPE), + ("MASTERING_DISPLAY_METADATA", lib.AV_FRAME_DATA_MASTERING_DISPLAY_METADATA), + ("GOP_TIMECODE", lib.AV_FRAME_DATA_GOP_TIMECODE), + ("SPHERICAL", lib.AV_FRAME_DATA_SPHERICAL), + ("CONTENT_LIGHT_LEVEL", lib.AV_FRAME_DATA_CONTENT_LIGHT_LEVEL), + ("ICC_PROFILE", lib.AV_FRAME_DATA_ICC_PROFILE), + ("SEI_UNREGISTERED", lib.AV_FRAME_DATA_SEI_UNREGISTERED) if lib.AV_FRAME_DATA_SEI_UNREGISTERED != -1 else None, )) @@ -42,7 +42,7 @@ cdef class SideData(Buffer): def __init__(self, sentinel, Frame frame, int index): if sentinel is not _cinit_bypass_sentinel: - raise RuntimeError('cannot manually instatiate SideData') + raise RuntimeError("cannot manually instatiate SideData") self.frame = frame self.ptr = frame.ptr.side_data[index] self.metadata = wrap_dictionary(self.ptr.metadata) @@ -57,7 +57,7 @@ cdef class SideData(Buffer): return False def __repr__(self): - return f'self.ptr.data:0x}>' + return f"self.ptr.data:0x}>" @property def type(self): diff --git a/av/subtitles/subtitle.pyx b/av/subtitles/subtitle.pyx index 1f0e4319a..5d6905cd8 100644 --- a/av/subtitles/subtitle.pyx +++ b/av/subtitles/subtitle.pyx @@ -14,7 +14,7 @@ cdef class SubtitleSet: self.rects = tuple(build_subtitle(self, i) for i in range(self.proxy.struct.num_rects)) def __repr__(self): - return '<%s.%s at 0x%x>' % ( + return "<%s.%s at 0x%x>" % ( self.__class__.__module__, self.__class__.__name__, id(self), @@ -48,7 +48,7 @@ cdef Subtitle build_subtitle(SubtitleSet subtitle, int index): """ if index < 0 or index >= subtitle.proxy.struct.num_rects: - raise ValueError('subtitle rect index out of range') + raise ValueError("subtitle rect index out of range") cdef lib.AVSubtitleRect *ptr = subtitle.proxy.struct.rects[index] if ptr.type == lib.SUBTITLE_NONE: @@ -60,30 +60,30 @@ cdef Subtitle build_subtitle(SubtitleSet subtitle, int index): elif ptr.type == lib.SUBTITLE_ASS: return AssSubtitle(subtitle, index) else: - raise ValueError('unknown subtitle type %r' % ptr.type) + raise ValueError("unknown subtitle type %r" % ptr.type) cdef class Subtitle: def __cinit__(self, SubtitleSet subtitle, int index): if index < 0 or index >= subtitle.proxy.struct.num_rects: - raise ValueError('subtitle rect index out of range') + raise ValueError("subtitle rect index out of range") self.proxy = subtitle.proxy self.ptr = self.proxy.struct.rects[index] if self.ptr.type == lib.SUBTITLE_NONE: - self.type = b'none' + self.type = b"none" elif self.ptr.type == lib.SUBTITLE_BITMAP: - self.type = b'bitmap' + self.type = b"bitmap" elif self.ptr.type == lib.SUBTITLE_TEXT: - self.type = b'text' + self.type = b"text" elif self.ptr.type == lib.SUBTITLE_ASS: - self.type = b'ass' + self.type = b"ass" else: - raise ValueError('unknown subtitle type %r' % self.ptr.type) + raise ValueError("unknown subtitle type %r" % self.ptr.type) def __repr__(self): - return '<%s.%s at 0x%x>' % ( + return "<%s.%s at 0x%x>" % ( self.__class__.__module__, self.__class__.__name__, id(self), @@ -100,7 +100,7 @@ cdef class BitmapSubtitle(Subtitle): ) def __repr__(self): - return '<%s.%s %dx%d at %d,%d; at 0x%x>' % ( + return "<%s.%s %dx%d at %d,%d; at 0x%x>" % ( self.__class__.__module__, self.__class__.__name__, self.width, @@ -136,9 +136,9 @@ cdef class BitmapSubtitlePlane: def __cinit__(self, BitmapSubtitle subtitle, int index): if index >= 4: - raise ValueError('BitmapSubtitles have only 4 planes') + raise ValueError("BitmapSubtitles have only 4 planes") if not subtitle.ptr.linesize[index]: - raise ValueError('plane does not exist') + raise ValueError("plane does not exist") self.subtitle = subtitle self.index = index @@ -154,7 +154,7 @@ cdef class BitmapSubtitlePlane: cdef class TextSubtitle(Subtitle): def __repr__(self): - return '<%s.%s %r at 0x%x>' % ( + return "<%s.%s %r at 0x%x>" % ( self.__class__.__module__, self.__class__.__name__, self.text, @@ -168,7 +168,7 @@ cdef class TextSubtitle(Subtitle): cdef class AssSubtitle(Subtitle): def __repr__(self): - return '<%s.%s %r at 0x%x>' % ( + return "<%s.%s %r at 0x%x>" % ( self.__class__.__module__, self.__class__.__name__, self.ass, diff --git a/av/video/format.pyx b/av/video/format.pyx index 602ec5275..853472cf7 100644 --- a/av/video/format.pyx +++ b/av/video/format.pyx @@ -14,7 +14,7 @@ cdef lib.AVPixelFormat get_pix_fmt(const char *name) except lib.AV_PIX_FMT_NONE: cdef lib.AVPixelFormat pix_fmt = lib.av_get_pix_fmt(name) if pix_fmt == lib.AV_PIX_FMT_NONE: - raise ValueError('not a pixel format: %r' % name) + raise ValueError("not a pixel format: %r" % name) return pix_fmt @@ -29,7 +29,6 @@ cdef class VideoFormat: """ def __cinit__(self, name, width=0, height=0): - if name is _cinit_bypass_sentinel: return @@ -54,9 +53,9 @@ cdef class VideoFormat: def __repr__(self): if self.width or self.height: - return '' % (self.__class__.__name__, self.name, self.width, self.height) + return "" % (self.__class__.__name__, self.name, self.width, self.height) else: - return '' % (self.__class__.__name__, self.name) + return "" % (self.__class__.__name__, self.name) def __int__(self): return int(self.pix_fmt) diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 9498938e6..732e5aac8 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -21,20 +21,20 @@ cdef VideoFrame alloc_video_frame(): return VideoFrame.__new__(VideoFrame, _cinit_bypass_sentinel) -PictureType = define_enum('PictureType', __name__, ( - ('NONE', lib.AV_PICTURE_TYPE_NONE, "Undefined"), - ('I', lib.AV_PICTURE_TYPE_I, "Intra"), - ('P', lib.AV_PICTURE_TYPE_P, "Predicted"), - ('B', lib.AV_PICTURE_TYPE_B, "Bi-directional predicted"), - ('S', lib.AV_PICTURE_TYPE_S, "S(GMC)-VOP MPEG-4"), - ('SI', lib.AV_PICTURE_TYPE_SI, "Switching intra"), - ('SP', lib.AV_PICTURE_TYPE_SP, "Switching predicted"), - ('BI', lib.AV_PICTURE_TYPE_BI, "BI type"), +PictureType = define_enum("PictureType", __name__, ( + ("NONE", lib.AV_PICTURE_TYPE_NONE, "Undefined"), + ("I", lib.AV_PICTURE_TYPE_I, "Intra"), + ("P", lib.AV_PICTURE_TYPE_P, "Predicted"), + ("B", lib.AV_PICTURE_TYPE_B, "Bi-directional predicted"), + ("S", lib.AV_PICTURE_TYPE_S, "S(GMC)-VOP MPEG-4"), + ("SI", lib.AV_PICTURE_TYPE_SI, "Switching intra"), + ("SP", lib.AV_PICTURE_TYPE_SP, "Switching predicted"), + ("BI", lib.AV_PICTURE_TYPE_BI, "BI type"), )) cdef byteswap_array(array, bint big_endian): - if (sys.byteorder == 'big') != big_endian: + if (sys.byteorder == "big") != big_endian: return array.byteswap() else: return array @@ -57,7 +57,7 @@ cdef copy_array_to_plane(array, VideoPlane plane, unsigned int bytes_per_pixel): o_pos += o_stride -cdef useful_array(VideoPlane plane, unsigned int bytes_per_pixel=1, str dtype='uint8'): +cdef useful_array(VideoPlane plane, unsigned int bytes_per_pixel=1, str dtype="uint8"): """ Return the useful part of the VideoPlane as a single dimensional array. @@ -74,7 +74,7 @@ cdef useful_array(VideoPlane plane, unsigned int bytes_per_pixel=1, str dtype='u cdef class VideoFrame(Frame): - def __cinit__(self, width=0, height=0, format='yuv420p'): + def __cinit__(self, width=0, height=0, format="yuv420p"): if width is _cinit_bypass_sentinel: return @@ -122,7 +122,7 @@ cdef class VideoFrame(Frame): self._np_buffer = None def __repr__(self): - return '' % ( + return "" % ( self.__class__.__name__, self.index, self.pts, @@ -146,7 +146,7 @@ cdef class VideoFrame(Frame): count = self.format.ptr.comp[i].plane + 1 if max_plane_count < count: max_plane_count = count - if self.format.name == 'pal8': + if self.format.name == "pal8": max_plane_count = 2 cdef int plane_count = 0 @@ -288,7 +288,7 @@ cdef class VideoFrame(Frame): import numpy as np - if frame.format.name in ('yuv420p', 'yuvj420p'): + if frame.format.name in ("yuv420p", "yuvj420p"): assert frame.width % 2 == 0 assert frame.height % 2 == 0 # Fast path for the case that the entire YUV data is contiguous @@ -306,76 +306,76 @@ cdef class VideoFrame(Frame): useful_array(frame.planes[1]), useful_array(frame.planes[2]) )).reshape(-1, frame.width) - elif frame.format.name in ('yuv444p', 'yuvj444p'): + elif frame.format.name in ("yuv444p", "yuvj444p"): return np.hstack(( useful_array(frame.planes[0]), useful_array(frame.planes[1]), useful_array(frame.planes[2]) )).reshape(-1, frame.height, frame.width) - elif frame.format.name == 'yuyv422': + elif frame.format.name == "yuyv422": assert frame.width % 2 == 0 assert frame.height % 2 == 0 return useful_array(frame.planes[0], 2).reshape(frame.height, frame.width, -1) - elif frame.format.name == 'gbrp': + elif frame.format.name == "gbrp": array = np.empty((frame.height, frame.width, 3), dtype="uint8") array[:, :, 0] = useful_array(frame.planes[2], 1).reshape(-1, frame.width) array[:, :, 1] = useful_array(frame.planes[0], 1).reshape(-1, frame.width) array[:, :, 2] = useful_array(frame.planes[1], 1).reshape(-1, frame.width) return array - elif frame.format.name in ('gbrp10be', 'gbrp12be', 'gbrp14be', 'gbrp16be', 'gbrp10le', 'gbrp12le', 'gbrp14le', 'gbrp16le'): + elif frame.format.name in ("gbrp10be", "gbrp12be", "gbrp14be", "gbrp16be", "gbrp10le", "gbrp12le", "gbrp14le", "gbrp16le"): array = np.empty((frame.height, frame.width, 3), dtype="uint16") array[:, :, 0] = useful_array(frame.planes[2], 2, "uint16").reshape(-1, frame.width) array[:, :, 1] = useful_array(frame.planes[0], 2, "uint16").reshape(-1, frame.width) array[:, :, 2] = useful_array(frame.planes[1], 2, "uint16").reshape(-1, frame.width) - return byteswap_array(array, frame.format.name.endswith('be')) - elif frame.format.name in ('gbrpf32be', 'gbrpf32le'): + return byteswap_array(array, frame.format.name.endswith("be")) + elif frame.format.name in ("gbrpf32be", "gbrpf32le"): array = np.empty((frame.height, frame.width, 3), dtype="float32") array[:, :, 0] = useful_array(frame.planes[2], 4, "float32").reshape(-1, frame.width) array[:, :, 1] = useful_array(frame.planes[0], 4, "float32").reshape(-1, frame.width) array[:, :, 2] = useful_array(frame.planes[1], 4, "float32").reshape(-1, frame.width) - return byteswap_array(array, frame.format.name.endswith('be')) - elif frame.format.name in ('rgb24', 'bgr24'): + return byteswap_array(array, frame.format.name.endswith("be")) + elif frame.format.name in ("rgb24", "bgr24"): return useful_array(frame.planes[0], 3).reshape(frame.height, frame.width, -1) - elif frame.format.name in ('argb', 'rgba', 'abgr', 'bgra'): + elif frame.format.name in ("argb", "rgba", "abgr", "bgra"): return useful_array(frame.planes[0], 4).reshape(frame.height, frame.width, -1) - elif frame.format.name in ('gray', 'gray8', 'rgb8', 'bgr8'): + elif frame.format.name in ("gray", "gray8", "rgb8", "bgr8"): return useful_array(frame.planes[0]).reshape(frame.height, frame.width) - elif frame.format.name in ('gray16be', 'gray16le'): + elif frame.format.name in ("gray16be", "gray16le"): return byteswap_array( - useful_array(frame.planes[0], 2, 'uint16').reshape(frame.height, frame.width), - frame.format.name == 'gray16be', + useful_array(frame.planes[0], 2, "uint16").reshape(frame.height, frame.width), + frame.format.name == "gray16be", ) - elif frame.format.name in ('rgb48be', 'rgb48le'): + elif frame.format.name in ("rgb48be", "rgb48le"): return byteswap_array( - useful_array(frame.planes[0], 6, 'uint16').reshape(frame.height, frame.width, -1), - frame.format.name == 'rgb48be', + useful_array(frame.planes[0], 6, "uint16").reshape(frame.height, frame.width, -1), + frame.format.name == "rgb48be", ) - elif frame.format.name in ('rgba64be', 'rgba64le'): + elif frame.format.name in ("rgba64be", "rgba64le"): return byteswap_array( - useful_array(frame.planes[0], 8, 'uint16').reshape(frame.height, frame.width, -1), - frame.format.name == 'rgba64be', + useful_array(frame.planes[0], 8, "uint16").reshape(frame.height, frame.width, -1), + frame.format.name == "rgba64be", ) - elif frame.format.name == 'pal8': + elif frame.format.name == "pal8": image = useful_array(frame.planes[0]).reshape(frame.height, frame.width) - palette = np.frombuffer(frame.planes[1], 'i4').astype('>i4').reshape(-1, 1).view(np.uint8) + palette = np.frombuffer(frame.planes[1], "i4").astype(">i4").reshape(-1, 1).view(np.uint8) return image, palette - elif frame.format.name == 'nv12': + elif frame.format.name == "nv12": return np.hstack(( useful_array(frame.planes[0]), useful_array(frame.planes[1], 2) )).reshape(-1, frame.width) else: - raise ValueError('Conversion to numpy array with format `%s` is not yet supported' % frame.format.name) + raise ValueError("Conversion to numpy array with format `%s` is not yet supported" % frame.format.name) @staticmethod def from_image(img): """ Construct a frame from a ``PIL.Image``. """ - if img.mode != 'RGB': - img = img.convert('RGB') + if img.mode != "RGB": + img = img.convert("RGB") - cdef VideoFrame frame = VideoFrame(img.size[0], img.size[1], 'rgb24') + cdef VideoFrame frame = VideoFrame(img.size[0], img.size[1], "rgb24") copy_array_to_plane(img, frame.planes[0], 3) return frame @@ -383,7 +383,7 @@ cdef class VideoFrame(Frame): @staticmethod def from_numpy_buffer(array, format="rgb24"): if format in ("rgb24", "bgr24"): - check_ndarray(array, 'uint8', 3) + check_ndarray(array, "uint8", 3) check_ndarray_shape(array, array.shape[2] == 3) height, width = array.shape[:2] elif format in ("gray", "gray8", "rgb8", "bgr8"): @@ -461,7 +461,7 @@ cdef class VideoFrame(Frame): self._init_user_attributes() @staticmethod - def from_ndarray(array, format='rgb24'): + def from_ndarray(array, format="rgb24"): """ Construct a frame from a numpy array. @@ -472,18 +472,18 @@ cdef class VideoFrame(Frame): `palette` must have shape (256, 4) and is given in ARGB format (PyAV will swap bytes if needed). """ - if format == 'pal8': + if format == "pal8": array, palette = array - check_ndarray(array, 'uint8', 2) - check_ndarray(palette, 'uint8', 2) + check_ndarray(array, "uint8", 2) + check_ndarray(palette, "uint8", 2) check_ndarray_shape(palette, palette.shape == (256, 4)) frame = VideoFrame(array.shape[1], array.shape[0], format) copy_array_to_plane(array, frame.planes[0], 1) - frame.planes[1].update(palette.view('>i4').astype('i4').tobytes()) + frame.planes[1].update(palette.view(">i4").astype("i4").tobytes()) return frame - elif format in ('yuv420p', 'yuvj420p'): - check_ndarray(array, 'uint8', 2) + elif format in ("yuv420p", "yuvj420p"): + check_ndarray(array, "uint8", 2) check_ndarray_shape(array, array.shape[0] % 3 == 0) check_ndarray_shape(array, array.shape[1] % 2 == 0) @@ -495,8 +495,8 @@ cdef class VideoFrame(Frame): copy_array_to_plane(flat[u_start:v_start], frame.planes[1], 1) copy_array_to_plane(flat[v_start:], frame.planes[2], 1) return frame - elif format in ('yuv444p', 'yuvj444p'): - check_ndarray(array, 'uint8', 3) + elif format in ("yuv444p", "yuvj444p"): + check_ndarray(array, "uint8", 3) check_ndarray_shape(array, array.shape[0] == 3) frame = VideoFrame(array.shape[2], array.shape[1], format) @@ -505,13 +505,13 @@ cdef class VideoFrame(Frame): copy_array_to_plane(array[1], frame.planes[1], 1) copy_array_to_plane(array[2], frame.planes[2], 1) return frame - elif format == 'yuyv422': - check_ndarray(array, 'uint8', 3) + elif format == "yuyv422": + check_ndarray(array, "uint8", 3) check_ndarray_shape(array, array.shape[0] % 2 == 0) check_ndarray_shape(array, array.shape[1] % 2 == 0) check_ndarray_shape(array, array.shape[2] == 2) - elif format == 'gbrp': - check_ndarray(array, 'uint8', 3) + elif format == "gbrp": + check_ndarray(array, "uint8", 3) check_ndarray_shape(array, array.shape[2] == 3) frame = VideoFrame(array.shape[1], array.shape[0], format) @@ -519,51 +519,51 @@ cdef class VideoFrame(Frame): copy_array_to_plane(array[:, :, 2], frame.planes[1], 1) copy_array_to_plane(array[:, :, 0], frame.planes[2], 1) return frame - elif format in ('gbrp10be', 'gbrp12be', 'gbrp14be', 'gbrp16be', 'gbrp10le', 'gbrp12le', 'gbrp14le', 'gbrp16le'): - check_ndarray(array, 'uint16', 3) + elif format in ("gbrp10be", "gbrp12be", "gbrp14be", "gbrp16be", "gbrp10le", "gbrp12le", "gbrp14le", "gbrp16le"): + check_ndarray(array, "uint16", 3) check_ndarray_shape(array, array.shape[2] == 3) frame = VideoFrame(array.shape[1], array.shape[0], format) - copy_array_to_plane(byteswap_array(array[:, :, 1], format.endswith('be')), frame.planes[0], 2) - copy_array_to_plane(byteswap_array(array[:, :, 2], format.endswith('be')), frame.planes[1], 2) - copy_array_to_plane(byteswap_array(array[:, :, 0], format.endswith('be')), frame.planes[2], 2) + copy_array_to_plane(byteswap_array(array[:, :, 1], format.endswith("be")), frame.planes[0], 2) + copy_array_to_plane(byteswap_array(array[:, :, 2], format.endswith("be")), frame.planes[1], 2) + copy_array_to_plane(byteswap_array(array[:, :, 0], format.endswith("be")), frame.planes[2], 2) return frame - elif format in ('gbrpf32be', 'gbrpf32le'): - check_ndarray(array, 'float32', 3) + elif format in ("gbrpf32be", "gbrpf32le"): + check_ndarray(array, "float32", 3) check_ndarray_shape(array, array.shape[2] == 3) frame = VideoFrame(array.shape[1], array.shape[0], format) - copy_array_to_plane(byteswap_array(array[:, :, 1], format.endswith('be')), frame.planes[0], 4) - copy_array_to_plane(byteswap_array(array[:, :, 2], format.endswith('be')), frame.planes[1], 4) - copy_array_to_plane(byteswap_array(array[:, :, 0], format.endswith('be')), frame.planes[2], 4) + copy_array_to_plane(byteswap_array(array[:, :, 1], format.endswith("be")), frame.planes[0], 4) + copy_array_to_plane(byteswap_array(array[:, :, 2], format.endswith("be")), frame.planes[1], 4) + copy_array_to_plane(byteswap_array(array[:, :, 0], format.endswith("be")), frame.planes[2], 4) return frame - elif format in ('rgb24', 'bgr24'): - check_ndarray(array, 'uint8', 3) + elif format in ("rgb24", "bgr24"): + check_ndarray(array, "uint8", 3) check_ndarray_shape(array, array.shape[2] == 3) - elif format in ('argb', 'rgba', 'abgr', 'bgra'): - check_ndarray(array, 'uint8', 3) + elif format in ("argb", "rgba", "abgr", "bgra"): + check_ndarray(array, "uint8", 3) check_ndarray_shape(array, array.shape[2] == 4) - elif format in ('gray', 'gray8', 'rgb8', 'bgr8'): - check_ndarray(array, 'uint8', 2) - elif format in ('gray16be', 'gray16le'): - check_ndarray(array, 'uint16', 2) + elif format in ("gray", "gray8", "rgb8", "bgr8"): + check_ndarray(array, "uint8", 2) + elif format in ("gray16be", "gray16le"): + check_ndarray(array, "uint16", 2) frame = VideoFrame(array.shape[1], array.shape[0], format) - copy_array_to_plane(byteswap_array(array, format == 'gray16be'), frame.planes[0], 2) + copy_array_to_plane(byteswap_array(array, format == "gray16be"), frame.planes[0], 2) return frame - elif format in ('rgb48be', 'rgb48le'): - check_ndarray(array, 'uint16', 3) + elif format in ("rgb48be", "rgb48le"): + check_ndarray(array, "uint16", 3) check_ndarray_shape(array, array.shape[2] == 3) frame = VideoFrame(array.shape[1], array.shape[0], format) - copy_array_to_plane(byteswap_array(array, format == 'rgb48be'), frame.planes[0], 6) + copy_array_to_plane(byteswap_array(array, format == "rgb48be"), frame.planes[0], 6) return frame - elif format in ('rgba64be', 'rgba64le'): - check_ndarray(array, 'uint16', 3) + elif format in ("rgba64be", "rgba64le"): + check_ndarray(array, "uint16", 3) check_ndarray_shape(array, array.shape[2] == 4) frame = VideoFrame(array.shape[1], array.shape[0], format) - copy_array_to_plane(byteswap_array(array, format == 'rgba64be'), frame.planes[0], 8) + copy_array_to_plane(byteswap_array(array, format == "rgba64be"), frame.planes[0], 8) return frame - elif format == 'nv12': - check_ndarray(array, 'uint8', 2) + elif format == "nv12": + check_ndarray(array, "uint8", 2) check_ndarray_shape(array, array.shape[0] % 3 == 0) check_ndarray_shape(array, array.shape[1] % 2 == 0) @@ -574,7 +574,7 @@ cdef class VideoFrame(Frame): copy_array_to_plane(flat[uv_start:], frame.planes[1], 2) return frame else: - raise ValueError('Conversion from numpy array with format `%s` is not yet supported' % format) + raise ValueError("Conversion from numpy array with format `%s` is not yet supported" % format) frame = VideoFrame(array.shape[1], array.shape[0], format) copy_array_to_plane(array, frame.planes[0], 1 if array.ndim == 2 else array.shape[2]) diff --git a/av/video/reformatter.pyx b/av/video/reformatter.pyx index 68208c220..ee599b817 100644 --- a/av/video/reformatter.pyx +++ b/av/video/reformatter.pyx @@ -7,44 +7,44 @@ from av.video.format cimport VideoFormat from av.video.frame cimport alloc_video_frame -Interpolation = define_enum('Interpolation', __name__, ( - ('FAST_BILINEAR', lib.SWS_FAST_BILINEAR, "Fast bilinear"), - ('BILINEAR', lib.SWS_BILINEAR, "Bilinear"), - ('BICUBIC', lib.SWS_BICUBIC, "Bicubic"), - ('X', lib.SWS_X, "Experimental"), - ('POINT', lib.SWS_POINT, "Nearest neighbor / point"), - ('AREA', lib.SWS_AREA, "Area averaging"), - ('BICUBLIN', lib.SWS_BICUBLIN, "Luma bicubic / chroma bilinear"), - ('GAUSS', lib.SWS_GAUSS, "Gaussian"), - ('SINC', lib.SWS_SINC, "Sinc"), - ('LANCZOS', lib.SWS_LANCZOS, "Lanczos"), - ('SPLINE', lib.SWS_SPLINE, "Bicubic spline"), +Interpolation = define_enum("Interpolation", __name__, ( + ("FAST_BILINEAR", lib.SWS_FAST_BILINEAR, "Fast bilinear"), + ("BILINEAR", lib.SWS_BILINEAR, "Bilinear"), + ("BICUBIC", lib.SWS_BICUBIC, "Bicubic"), + ("X", lib.SWS_X, "Experimental"), + ("POINT", lib.SWS_POINT, "Nearest neighbor / point"), + ("AREA", lib.SWS_AREA, "Area averaging"), + ("BICUBLIN", lib.SWS_BICUBLIN, "Luma bicubic / chroma bilinear"), + ("GAUSS", lib.SWS_GAUSS, "Gaussian"), + ("SINC", lib.SWS_SINC, "Sinc"), + ("LANCZOS", lib.SWS_LANCZOS, "Lanczos"), + ("SPLINE", lib.SWS_SPLINE, "Bicubic spline"), )) -Colorspace = define_enum('Colorspace', __name__, ( - ('ITU709', lib.SWS_CS_ITU709), - ('FCC', lib.SWS_CS_FCC), - ('ITU601', lib.SWS_CS_ITU601), - ('ITU624', lib.SWS_CS_ITU624), - ('SMPTE170M', lib.SWS_CS_SMPTE170M), - ('SMPTE240M', lib.SWS_CS_SMPTE240M), - ('DEFAULT', lib.SWS_CS_DEFAULT), +Colorspace = define_enum("Colorspace", __name__, ( + ("ITU709", lib.SWS_CS_ITU709), + ("FCC", lib.SWS_CS_FCC), + ("ITU601", lib.SWS_CS_ITU601), + ("ITU624", lib.SWS_CS_ITU624), + ("SMPTE170M", lib.SWS_CS_SMPTE170M), + ("SMPTE240M", lib.SWS_CS_SMPTE240M), + ("DEFAULT", lib.SWS_CS_DEFAULT), # Lowercase for b/c. - ('itu709', lib.SWS_CS_ITU709), - ('fcc', lib.SWS_CS_FCC), - ('itu601', lib.SWS_CS_ITU601), - ('itu624', lib.SWS_CS_SMPTE170M), - ('smpte240', lib.SWS_CS_SMPTE240M), - ('default', lib.SWS_CS_DEFAULT), + ("itu709", lib.SWS_CS_ITU709), + ("fcc", lib.SWS_CS_FCC), + ("itu601", lib.SWS_CS_ITU601), + ("itu624", lib.SWS_CS_SMPTE170M), + ("smpte240", lib.SWS_CS_SMPTE240M), + ("default", lib.SWS_CS_DEFAULT), )) -ColorRange = define_enum('ColorRange', __name__, ( - ('UNSPECIFIED', lib.AVCOL_RANGE_UNSPECIFIED, "Unspecified"), - ('MPEG', lib.AVCOL_RANGE_MPEG, "MPEG (limited) YUV range, 219*2^(n-8)"), - ('JPEG', lib.AVCOL_RANGE_JPEG, "JPEG (full) YUV range, 2^n-1"), - ('NB', lib.AVCOL_RANGE_NB, "Not part of ABI"), +ColorRange = define_enum("ColorRange", __name__, ( + ("UNSPECIFIED", lib.AVCOL_RANGE_UNSPECIFIED, "Unspecified"), + ("MPEG", lib.AVCOL_RANGE_MPEG, "MPEG (limited) YUV range, 219*2^(n-8)"), + ("JPEG", lib.AVCOL_RANGE_JPEG, "JPEG (full) YUV range, 2^n-1"), + ("NB", lib.AVCOL_RANGE_NB, "Not part of ABI"), )) cdef class VideoReformatter: diff --git a/av/video/stream.pyx b/av/video/stream.pyx index 8694b63ba..9f4bacc87 100644 --- a/av/video/stream.pyx +++ b/av/video/stream.pyx @@ -1,7 +1,7 @@ cdef class VideoStream(Stream): def __repr__(self): - return '' % ( + return "" % ( self.__class__.__name__, self.index, self.name, From 1962443425d9f63c80d220546ac5f3dfb0799f6f Mon Sep 17 00:00:00 2001 From: JoeUgly <41972063+JoeUgly@users.noreply.github.com> Date: Tue, 9 Jan 2024 18:01:09 -0500 Subject: [PATCH 166/846] Deprecate VideoCodecContext.gop_size for decoders (fixes: #1254) --- av/video/codeccontext.pyx | 15 +++++++++++++++ tests/test_codec_context.py | 10 ++++++++++ tests/test_file_probing.py | 2 -- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.pyx index 9d4ec13a8..4328c13c6 100644 --- a/av/video/codeccontext.pyx +++ b/av/video/codeccontext.pyx @@ -1,3 +1,5 @@ +import warnings + from libc.stdint cimport int64_t cimport libav as lib @@ -9,6 +11,8 @@ from av.video.format cimport VideoFormat, get_pix_fmt, get_video_format from av.video.frame cimport VideoFrame, alloc_video_frame from av.video.reformatter cimport VideoReformatter +from av.deprecation import AVDeprecationWarning + cdef class VideoCodecContext(CodecContext): @@ -126,10 +130,21 @@ cdef class VideoCodecContext(CodecContext): self.framerate = value property gop_size: + """This only makes sense for encoders.""" def __get__(self): + if self.is_decoder: + warnings.warn( + "Using VideoCodecContext.gop_size for decoders is deprecated.", + AVDeprecationWarning + ) return self.ptr.gop_size def __set__(self, int value): + if self.is_decoder: + warnings.warn( + "Using VideoCodecContext.gop_size for decoders is deprecated.", + AVDeprecationWarning + ) self.ptr.gop_size = value property sample_aspect_ratio: diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index d4ba2fdd2..5f115445e 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -80,6 +80,16 @@ def test_decoder_extradata(self): self.assertEqual(ctx.extradata, None) self.assertEqual(ctx.extradata_size, 0) + def test_decoder_gop_size(self): + ctx = av.codec.Codec("h264", "r").create() + + with warnings.catch_warnings(record=True) as captured: + self.assertIsInstance(ctx.gop_size, int) + self.assertEqual( + captured[0].message.args[0], + "Using VideoCodecContext.gop_size for decoders is deprecated.", + ) + def test_decoder_timebase(self): ctx = av.codec.Codec("h264", "r").create() diff --git a/tests/test_file_probing.py b/tests/test_file_probing.py index c67f7fb8e..c3f8966de 100644 --- a/tests/test_file_probing.py +++ b/tests/test_file_probing.py @@ -308,7 +308,6 @@ def test_stream_probing(self): self.assertEqual(stream.display_aspect_ratio, Fraction(4, 3)) self.assertEqual(stream.format.name, "yuv420p") self.assertFalse(stream.has_b_frames) - self.assertEqual(stream.gop_size, 12) self.assertEqual(stream.height, 576) self.assertEqual(stream.max_bit_rate, None) self.assertEqual(stream.sample_aspect_ratio, Fraction(16, 15)) @@ -383,7 +382,6 @@ def test_stream_probing(self): self.assertEqual(stream.display_aspect_ratio, None) self.assertEqual(stream.format, None) self.assertFalse(stream.has_b_frames) - self.assertEqual(stream.gop_size, 12) self.assertEqual(stream.height, 0) self.assertEqual(stream.max_bit_rate, None) self.assertEqual(stream.sample_aspect_ratio, None) From 5f730da7fe2ebe9d81010f7d86aec9fec0e411be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sviatoslav=20Sydorenko=20=28=D0=A1=D0=B2=D1=8F=D1=82=D0=BE?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=20=D0=A1=D0=B8=D0=B4=D0=BE=D1=80=D0=B5?= =?UTF-8?q?=D0=BD=D0=BA=D0=BE=29?= Date: Sun, 21 Jan 2024 20:29:14 +0100 Subject: [PATCH 167/846] Stop using `wheel` as an unconditional build dep It used to be an incorrect recommendation in some of the upstream docs. `setuptools` auto-injects it when building wheels and it's unnecessary when building sdist. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2d748e35f..d4da9718e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools", "wheel", "cython"] +requires = ["setuptools", "cython"] [tool.isort] default_section = "THIRDPARTY" From 3079210a8dbd82967ab3dfc65b9b93ed2554ed5c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 1 Feb 2024 21:59:38 -0500 Subject: [PATCH 168/846] Style with latest version of black --- av/deprecation.py | 1 - examples/numpy/generate_video_with_pts.py | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/av/deprecation.py b/av/deprecation.py index f36d2fe6f..ab1bbc559 100644 --- a/av/deprecation.py +++ b/av/deprecation.py @@ -22,7 +22,6 @@ class MethodDeprecationWarning(AVDeprecationWarning): class renamed_attr: - """Proxy for renamed attributes (or methods) on classes. Getting and setting values will be redirected to the provided name, and warnings will be issues every time. diff --git a/examples/numpy/generate_video_with_pts.py b/examples/numpy/generate_video_with_pts.py index c570a07d2..95288fc72 100644 --- a/examples/numpy/generate_video_with_pts.py +++ b/examples/numpy/generate_video_with_pts.py @@ -50,9 +50,9 @@ # draw blocks of a progress bar cx = int(width / total_frames * (frame_i + 0.5)) cy = int(height / 2) - the_canvas[ - cy - block_h2 : cy + block_h2, cx - block_w2 : cx + block_w2 - ] = nice_color + the_canvas[cy - block_h2 : cy + block_h2, cx - block_w2 : cx + block_w2] = ( + nice_color + ) frame = av.VideoFrame.from_ndarray(the_canvas, format="rgb24") From 3f75cc76c9558242bd86fba02997ca52ec3c3638 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 1 Feb 2024 21:49:10 -0500 Subject: [PATCH 169/846] Remove stalebot --- .github/workflows/issues.yml | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 .github/workflows/issues.yml diff --git a/.github/workflows/issues.yml b/.github/workflows/issues.yml deleted file mode 100644 index c88cccec4..000000000 --- a/.github/workflows/issues.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: issues -on: - schedule: - - cron: '30 1 * * *' - -jobs: - stale: - runs-on: ubuntu-latest - steps: - - uses: actions/stale@v8 - with: - stale-issue-label: stale - stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.' - days-before-stale: 120 - days-before-close: 14 - days-before-pr-stale: -1 - days-before-pr-close: -1 - operations-per-run: 60 From 365ddebb5630d298c7d9615f31df0aa3399c3c23 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 3 Feb 2024 13:50:56 -0500 Subject: [PATCH 170/846] Upgrade to ffmpeg 6.1 --- .github/workflows/tests.yml | 14 ++++++++------ scripts/ffmpeg-6.1.json | 3 +++ tests/test_file_probing.py | 5 ++--- 3 files changed, 13 insertions(+), 9 deletions(-) create mode 100644 scripts/ffmpeg-6.1.json diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c40b09460..d93fde792 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -38,11 +38,12 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-latest, python: 3.8, ffmpeg: "6.0", extras: true} + - {os: ubuntu-latest, python: 3.8, ffmpeg: "6.1", extras: true} + - {os: ubuntu-latest, python: 3.8, ffmpeg: "6.0"} - {os: ubuntu-latest, python: 3.8, ffmpeg: "5.1"} - {os: ubuntu-latest, python: 3.8, ffmpeg: "5.0"} - - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "5.0"} - - {os: macos-latest, python: 3.8, ffmpeg: "5.0"} + - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1"} + - {os: macos-latest, python: 3.8, ffmpeg: "6.1"} env: PYAV_PYTHON: python${{ matrix.config.python }} @@ -119,6 +120,7 @@ jobs: fail-fast: false matrix: config: + - {os: windows-latest, python: 3.8, ffmpeg: "6.1"} - {os: windows-latest, python: 3.8, ffmpeg: "6.0"} - {os: windows-latest, python: 3.8, ffmpeg: "5.1"} - {os: windows-latest, python: 3.8, ffmpeg: "5.0"} @@ -165,7 +167,7 @@ jobs: - name: Build source package run: | pip install cython - python scripts/fetch-vendor.py --config-file scripts/ffmpeg-6.0.json /tmp/vendor + python scripts/fetch-vendor.py --config-file scripts/ffmpeg-6.1.json /tmp/vendor PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig python setup.py sdist - name: Upload source package uses: actions/upload-artifact@v3 @@ -208,8 +210,8 @@ jobs: env: CIBW_ARCHS: ${{ matrix.arch }} CIBW_BEFORE_ALL_LINUX: yum install -y alsa-lib libxcb - CIBW_BEFORE_BUILD: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-6.0.json /tmp/vendor - CIBW_BEFORE_BUILD_WINDOWS: python scripts\fetch-vendor.py --config-file scripts\ffmpeg-6.0.json C:\cibw\vendor + CIBW_BEFORE_BUILD: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-6.1.json /tmp/vendor + CIBW_BEFORE_BUILD_WINDOWS: python scripts\fetch-vendor.py --config-file scripts\ffmpeg-6.1.json C:\cibw\vendor CIBW_ENVIRONMENT_LINUX: LD_LIBRARY_PATH=/tmp/vendor/lib:$LD_LIBRARY_PATH PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig CIBW_ENVIRONMENT_MACOS: PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig LDFLAGS=-headerpad_max_install_names CIBW_ENVIRONMENT_WINDOWS: INCLUDE=C:\\cibw\\vendor\\include LIB=C:\\cibw\\vendor\\lib PYAV_SKIP_TESTS=unicode_filename diff --git a/scripts/ffmpeg-6.1.json b/scripts/ffmpeg-6.1.json new file mode 100644 index 000000000..f1585521e --- /dev/null +++ b/scripts/ffmpeg-6.1.json @@ -0,0 +1,3 @@ +{ + "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/6.1.0-1/ffmpeg-{platform}.tar.gz"] +} diff --git a/tests/test_file_probing.py b/tests/test_file_probing.py index c3f8966de..62e842f25 100644 --- a/tests/test_file_probing.py +++ b/tests/test_file_probing.py @@ -321,13 +321,13 @@ def test_stream_probing(self): # Deprecated properties. with warnings.catch_warnings(record=True) as captured: - self.assertIsNone(stream.framerate) + stream.framerate self.assertEqual( captured[0].message.args[0], "VideoStream.framerate is deprecated as it is not always set; please use VideoStream.average_rate.", ) with warnings.catch_warnings(record=True) as captured: - self.assertIsNone(stream.rate) + stream.rate self.assertEqual( captured[0].message.args[0], "VideoStream.rate is deprecated as it is not always set; please use VideoStream.average_rate.", @@ -361,7 +361,6 @@ def test_stream_probing(self): self.assertTrue(str(stream).startswith(" Date: Mon, 15 Jan 2024 22:14:11 -0500 Subject: [PATCH 171/846] Test for VideoCodecContext.gop_size setter --- av/video/codeccontext.pyx | 6 +++++- tests/test_codec_context.py | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.pyx index 4328c13c6..0c2ac097a 100644 --- a/av/video/codeccontext.pyx +++ b/av/video/codeccontext.pyx @@ -130,7 +130,11 @@ cdef class VideoCodecContext(CodecContext): self.framerate = value property gop_size: - """This only makes sense for encoders.""" + """ + Sets the number of frames between keyframes. Used only for encoding. + + :type: int + """ def __get__(self): if self.is_decoder: warnings.warn( diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 5f115445e..d0ba508fb 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -267,6 +267,7 @@ def video_encoding(self, codec_name, options={}, codec_tag=None): height = options.pop("height", 480) max_frames = options.pop("max_frames", 50) time_base = options.pop("time_base", video_stream.time_base) + gop_size = options.pop("gop_size", 20) ctx = codec.create() ctx.width = width @@ -274,6 +275,7 @@ def video_encoding(self, codec_name, options={}, codec_tag=None): ctx.time_base = time_base ctx.framerate = 1 / ctx.time_base ctx.pix_fmt = pix_fmt + ctx.gop_size = gop_size ctx.options = options # TODO if codec_tag: ctx.codec_tag = codec_tag @@ -309,15 +311,34 @@ def video_encoding(self, codec_name, options={}, codec_tag=None): ctx = av.Codec(dec_codec_name, "r").create() ctx.open() + keyframe_indices = [] decoded_frame_count = 0 for frame in iter_raw_frames(path, packet_sizes, ctx): decoded_frame_count += 1 self.assertEqual(frame.width, width) self.assertEqual(frame.height, height) self.assertEqual(frame.format.name, pix_fmt) + if frame.key_frame: + keyframe_indices.append(frame.index) self.assertEqual(frame_count, decoded_frame_count) + self.assertIsInstance( + all(keyframe_index for keyframe_index in keyframe_indices), int + ) + decoded_gop_sizes = [ + j - i for i, j in zip(keyframe_indices[:-1], keyframe_indices[1:]) + ] + if codec_name in ("dvvideo", "dnxhd") and all( + i == 1 for i in decoded_gop_sizes + ): + raise SkipTest() + for i in decoded_gop_sizes: + self.assertEqual(i, gop_size) + + final_gop_size = decoded_frame_count - max(keyframe_indices) + self.assertLessEqual(final_gop_size, gop_size) + def test_encoding_pcm_s24le(self): self.audio_encoding("pcm_s24le") From 9c1589efdc79f6db33e077c939e5a7dafd68cfcb Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 15 Feb 2024 00:51:42 -0500 Subject: [PATCH 172/846] Remove commented-out code in graph We also remove some unnecessary newlines. --- av/filter/graph.pyx | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/av/filter/graph.pyx b/av/filter/graph.pyx index 7253c25b4..c727c0931 100644 --- a/av/filter/graph.pyx +++ b/av/filter/graph.pyx @@ -12,9 +12,7 @@ from av.video.frame cimport VideoFrame cdef class Graph: - def __cinit__(self): - self.ptr = lib.avfilter_graph_alloc() self.configured = False self._name_counts = {} @@ -41,41 +39,14 @@ cdef class Graph: if self.configured and not force: return - # if auto_buffer: - # for ctx in self._context_by_ptr.itervalues(): - # for in_ in ctx.inputs: - # if not in_.link: - # if in_.type == "video": - # pass - err_check(lib.avfilter_graph_config(self.ptr, NULL)) self.configured = True # We get auto-inserted stuff here. self._auto_register() - # def parse_string(self, str filter_str): - # err_check(lib.avfilter_graph_parse2(self.ptr, filter_str, &self.inputs, &self.outputs)) - # - # cdef lib.AVFilterInOut *input_ - # while input_ != NULL: - # print 'in ', input_.pad_idx, (input_.name if input_.name != NULL else ''), input_.filter_ctx.name, input_.filter_ctx.filter.name - # input_ = input_.next - # - # cdef lib.AVFilterInOut *output - # while output != NULL: - # print 'out', output.pad_idx, (output.name if output.name != NULL else ''), output.filter_ctx.name, output.filter_ctx.filter.name - # output = output.next - - # NOTE: Only FFmpeg supports this. - # def dump(self): - # cdef char *buf = lib.avfilter_graph_dump(self.ptr, "") - # cdef str ret = buf - # lib.av_free(buf) - # return ret def add(self, filter, args=None, **kwargs): - cdef Filter cy_filter if isinstance(filter, str): cy_filter = Filter(filter) @@ -124,7 +95,6 @@ cdef class Graph: self._nb_filters_seen = self.ptr.nb_filters def add_buffer(self, template=None, width=None, height=None, format=None, name=None, time_base=None): - if template is not None: if width is None: width = template.width @@ -195,7 +165,6 @@ cdef class Graph: return self.add("abuffer", name=name, **kwargs) def push(self, frame): - if frame is None: contexts = self._context_by_type.get("buffer", []) + self._context_by_type.get("abuffer", []) elif isinstance(frame, VideoFrame): @@ -211,7 +180,6 @@ cdef class Graph: contexts[0].push(frame) def pull(self): - vsinks = self._context_by_type.get("buffersink", []) asinks = self._context_by_type.get("abuffersink", []) From c34aae2ba6558d3641e46bc02156fab9679c3e3e Mon Sep 17 00:00:00 2001 From: JoeUgly <41972063+JoeUgly@users.noreply.github.com> Date: Thu, 15 Feb 2024 18:56:54 -0500 Subject: [PATCH 173/846] Create codec_context.delay. Create tests for codec_context.delay and codec.delay. --- av/codec/context.pyx | 10 ++++++++++ tests/test_codec.py | 4 ++++ tests/test_codec_context.py | 6 ++++++ 3 files changed, 20 insertions(+) diff --git a/av/codec/context.pyx b/av/codec/context.pyx index bf87b4391..5a28cd69e 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -640,3 +640,13 @@ cdef class CodecContext: def __set__(self, value): self.ptr.skip_frame = SkipType[value].value + + property delay: + """Codec delay. + + Wraps :ffmpeg:`AVCodecContext.delay`. + + """ + + def __get__(self): + return self.ptr.delay diff --git a/tests/test_codec.py b/tests/test_codec.py index b7af5eedf..511a5b434 100644 --- a/tests/test_codec.py +++ b/tests/test_codec.py @@ -30,6 +30,7 @@ def test_codec_mpeg4_decoder(self): self.assertIn(c.id, (12, 13)) self.assertTrue(c.is_decoder) self.assertFalse(c.is_encoder) + self.assertTrue(c.delay) # audio self.assertIsNone(c.audio_formats) @@ -51,6 +52,7 @@ def test_codec_mpeg4_encoder(self): self.assertIn(c.id, (12, 13)) self.assertTrue(c.is_encoder) self.assertFalse(c.is_decoder) + self.assertTrue(c.delay) # audio self.assertIsNone(c.audio_formats) @@ -72,6 +74,7 @@ def test_codec_opus_decoder(self): self.assertEqual(c.type, "audio") self.assertTrue(c.is_decoder) self.assertFalse(c.is_encoder) + self.assertTrue(c.delay) # audio self.assertIsNone(c.audio_formats) @@ -89,6 +92,7 @@ def test_codec_opus_encoder(self): self.assertEqual(c.type, "audio") self.assertTrue(c.is_encoder) self.assertFalse(c.is_decoder) + self.assertTrue(c.delay) # audio formats = c.audio_formats diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index d0ba508fb..c8664972a 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -44,6 +44,12 @@ def test_skip_frame_default(self): ctx = Codec("png", "w").create() self.assertEqual(ctx.skip_frame.name, "DEFAULT") + def test_codec_delay(self): + with av.open(fate_suite("mkv/codec_delay_opus.mkv")) as container: + self.assertEqual(container.streams.audio[0].codec_context.delay, 312) + with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: + self.assertEqual(container.streams.video[0].codec_context.delay, 0) + def test_codec_tag(self): ctx = Codec("mpeg4", "w").create() self.assertEqual(ctx.codec_tag, "\x00\x00\x00\x00") From 1eb416bfc2de3b9a06bb7f228a7b37f6e72debf9 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 15 Feb 2024 00:20:21 -0500 Subject: [PATCH 174/846] Don't use frame.index in docs --- docs/index.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 386e819bd..a1a6d3dff 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -54,8 +54,8 @@ Basic Demo container = av.open(path_to_video) - for frame in container.decode(video=0): - frame.to_image().save('frame-%04d.jpg' % frame.index) + for index, frame in enumerate(container.decode(video=0)): + frame.to_image().save(f"frame-{index:04d}.jpg") Overview From 17e86d152ff3b13427f29a457824b6227d210f6d Mon Sep 17 00:00:00 2001 From: JoeUgly <41972063+JoeUgly@users.noreply.github.com> Date: Fri, 16 Feb 2024 21:30:07 -0500 Subject: [PATCH 175/846] Deprecate frame.index --- av/audio/frame.pyx | 13 +++++++++++++ av/video/frame.pyx | 13 +++++++++++++ tests/test_codec_context.py | 13 ++++++++++++- 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/av/audio/frame.pyx b/av/audio/frame.pyx index aa6562259..5b3aa1812 100644 --- a/av/audio/frame.pyx +++ b/av/audio/frame.pyx @@ -4,6 +4,10 @@ from av.audio.plane cimport AudioPlane from av.error cimport err_check from av.utils cimport check_ndarray, check_ndarray_shape +import warnings + +from av.deprecation import AVDeprecationWarning + cdef object _cinit_bypass_sentinel @@ -193,3 +197,12 @@ cdef class AudioFrame(Frame): # convert and return data return np.vstack([np.frombuffer(x, dtype=dtype, count=count) for x in self.planes]) + + def __getattribute__(self, attribute): + "This method should be deleted when `frame.index` is removed." + if attribute == 'index': + warnings.warn( + "Using `frame.index` is deprecated.", + AVDeprecationWarning + ) + return Frame.__getattribute__(self, attribute) diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 732e5aac8..886d28277 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -8,6 +8,10 @@ from av.utils cimport check_ndarray, check_ndarray_shape from av.video.format cimport get_pix_fmt, get_video_format from av.video.plane cimport VideoPlane, YUVPlanes +import warnings + +from av.deprecation import AVDeprecationWarning + cdef object _cinit_bypass_sentinel @@ -580,3 +584,12 @@ cdef class VideoFrame(Frame): copy_array_to_plane(array, frame.planes[0], 1 if array.ndim == 2 else array.shape[2]) return frame + + def __getattribute__(self, attribute): + "This method should be deleted when `frame.index` is removed." + if attribute == 'index': + warnings.warn( + "Using `frame.index` is deprecated.", + AVDeprecationWarning + ) + return Frame.__getattribute__(self, attribute) diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index c8664972a..43eaa748f 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -96,6 +96,17 @@ def test_decoder_gop_size(self): "Using VideoCodecContext.gop_size for decoders is deprecated.", ) + def test_frame_index(self): + container = av.open(fate_suite("h264/interlaced_crop.mp4")) + stream = container.streams[0] + for frame in container.decode(stream): + with warnings.catch_warnings(record=True) as captured: + self.assertIsInstance(frame.index, int) + self.assertEqual( + captured[0].message.args[0], + "Using `frame.index` is deprecated.", + ) + def test_decoder_timebase(self): ctx = av.codec.Codec("h264", "r").create() @@ -325,7 +336,7 @@ def video_encoding(self, codec_name, options={}, codec_tag=None): self.assertEqual(frame.height, height) self.assertEqual(frame.format.name, pix_fmt) if frame.key_frame: - keyframe_indices.append(frame.index) + keyframe_indices.append(decoded_frame_count) self.assertEqual(frame_count, decoded_frame_count) From cb20e760b78a5a855e5aa46f2bb41e7bfe8c58bc Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 17 Feb 2024 03:11:51 -0500 Subject: [PATCH 176/846] Upgrade Sphinx to 5.1.0 --- tests/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/requirements.txt b/tests/requirements.txt index 36d21ecf9..8c1e633a7 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -6,4 +6,4 @@ flake8 isort numpy pillow -sphinx < 4.4 +sphinx==5.1.0 From ff5650b2a747100b5641cf5bf0b1ffc55b11c408 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 17 Feb 2024 03:12:48 -0500 Subject: [PATCH 177/846] Remove Hacking doc page --- HACKING.rst | 59 ------------------------------------ docs/development/hacking.rst | 6 ---- 2 files changed, 65 deletions(-) delete mode 100644 HACKING.rst delete mode 100644 docs/development/hacking.rst diff --git a/HACKING.rst b/HACKING.rst deleted file mode 100644 index cc8be9008..000000000 --- a/HACKING.rst +++ /dev/null @@ -1,59 +0,0 @@ -Hacking on PyAV -=============== - -The Goal --------- - -The goal of PyAV is to not only wrap FFmpeg in Python and provide complete access to the library for power users, but to make FFmpeg approachable without the need to understand all of the underlying mechanics. - - -Names and Structure -------------------- - -As much as reasonable, PyAV mirrors FFmpeg's structure and naming. Ideally, searching for documentation for ``CodecContext.bit_rate`` leads to ``AVCodecContext.bit_rate`` as well. - -We allow ourselves to depart from FFmpeg to make everything feel more consistent, e.g.: - -- we change a few names to make them more readable, by adding underscores, etc.; -- all of the audio classes are prefixed with ``Audio``, while some of the FFmpeg structs are prefixed with ``Sample`` (e.g. ``AudioFormat`` vs ``AVSampleFormat``). - -We will also sometimes duplicate APIs in order to provide both a low-level and high-level experience, e.g.: - -- Object flags are usually exposed as a :class:`av.enum.EnumFlag` (with FFmpeg names) under a ``flags`` attribute, **and** each flag is also a boolean attribute (with more Pythonic names). - - -Version Compatibility ---------------------- - -We currently support FFmpeg 4.0 through 4.2, on Python 3.5 through 3.8, on Linux, macOS, and Windows. We `continually test `_ these configurations. - -Differences are handled at compile time, in C, by checking against ``LIBAV*_VERSION_INT`` macros. We have not been able to perform this sort of checking in Cython as we have not been able to have it fully remove the code-paths, and so there are missing functions in newer FFmpeg's, and deprecated ones that emit compiler warnings in older FFmpeg's. - -Unfortunately, this means that PyAV is built for the existing FFmpeg, and must be rebuilt when FFmpeg is updated. - -We used to do this detection in small ``*.pyav.h`` headers in the ``include`` directory (and there are still some there as of writing), but the preferred method is to create ``*-shims.c`` files that are cimport-ed by the one module that uses them. - -You can use the same build system as continuous integration for local development:: - - # Prep the environment. - source scripts/activate.sh - - # Build FFmpeg. - ./scripts/build-deps - - # Build PyAV. - make - - # Run the tests. - make test - - -Code Formatting and Linting ---------------------------- - -``isort`` and ``flake8`` are integrated into the continuous integration, and are required to pass for code to be merged into develop. You can run these via ``scripts/test``:: - - ./scripts/test isort - ./scripts/test flake8 - - diff --git a/docs/development/hacking.rst b/docs/development/hacking.rst deleted file mode 100644 index 72e61e963..000000000 --- a/docs/development/hacking.rst +++ /dev/null @@ -1,6 +0,0 @@ - -.. It is all in the other file (that we want at the top-level of the repo). - -.. _hacking: - -.. include:: ../../HACKING.rst From ae069e5b124ca5de307d7694829f0789be2b9eba Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 17 Feb 2024 05:08:25 -0500 Subject: [PATCH 178/846] black, ruff, isort, pyupgrade, main docs generator I also manually removed commented-out variable defaults, which are certainly wrong in newer versions of sphinx. --- docs/conf.py | 314 +++++++++++++++------------------------------------ 1 file changed, 88 insertions(+), 226 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 11cc7cd2e..3124bba59 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,206 +1,90 @@ -# -*- coding: utf-8 -*- -# -# PyAV documentation build configuration file, created by -# sphinx-quickstart on Fri Dec 7 22:13:16 2012. -# -# This file is execfile()d with the current directory set to its containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -from docutils import nodes import logging -import math import os import re import sys -import sys import xml.etree.ElementTree as etree -import sphinx +from docutils import nodes from sphinx import addnodes from sphinx.util.docutils import SphinxDirective +import sphinx logging.basicConfig() - -if sphinx.version_info < (1, 8): - print("Sphinx {} is too old; we require >= 1.8.".format(sphinx.__version__), file=sys.stderr) - exit(1) - - # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('..')) +sys.path.insert(0, os.path.abspath("..")) # -- General configuration ----------------------------------------------------- -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.intersphinx', - 'sphinx.ext.todo', - 'sphinx.ext.coverage', - 'sphinx.ext.viewcode', - 'sphinx.ext.extlinks', - 'sphinx.ext.doctest', - - # We used to use doxylink, but we found its caching behaviour annoying, and - # so made a minimally viable version of our own. + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.todo", + "sphinx.ext.coverage", + "sphinx.ext.viewcode", + "sphinx.ext.extlinks", + "sphinx.ext.doctest", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'PyAV' -copyright = '2023, The PyAV Team' +project = "PyAV" +copyright = "2023, The PyAV Team" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # about = {} -with open('../av/about.py') as fp: +with open("../av/about.py") as fp: exec(fp.read(), about) # The full version, including alpha/beta/rc tags. -release = about['__version__'] +release = about["__version__"] # The short X.Y version. -version = release.split('-')[0] - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None +version = release.split("-")[0] -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = ['_build'] - -# The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False +exclude_patterns = ["_build"] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - +pygments_style = "sphinx" # -- Options for HTML output --------------------------------------------------- -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = 'pyav' -html_theme_path = [os.path.abspath(os.path.join(__file__, '..', '_themes'))] -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +html_theme = "pyav" +html_theme_path = [os.path.abspath(os.path.join(__file__, "..", "_themes"))] # The name of an image file (relative to this directory) to place at the top # of the sidebar. -html_logo = '_static/logo-250.png' +html_logo = "_static/logo-250.png" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -html_favicon = '_static/favicon.png' +html_favicon = "_static/favicon.png" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True +html_static_path = ["_static"] -# If true, the index is split into individual pages for each letter. -#html_split_index = False -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -doctest_global_setup = ''' +doctest_global_setup = """ import errno import os @@ -226,33 +110,33 @@ def sandboxed(*args, **kwargs): video_path = curated('pexels/time-lapse-video-of-night-sky-857195.mp4') -''' +""" -doctest_global_cleanup = ''' +doctest_global_cleanup = """ os.chdir(_cwd) -''' +""" -doctest_test_doctest_blocks = '' +doctest_test_doctest_blocks = "" extlinks = { - 'ffstruct': ('http://ffmpeg.org/doxygen/trunk/struct%s.html', 'struct '), - 'issue': ('https://github.com/PyAV-Org/PyAV/issues/%s', '#'), - 'pr': ('https://github.com/PyAV-Org/PyAV/pull/%s', '#'), - 'gh-user': ('https://github.com/%s', '@'), + "ffstruct": ("http://ffmpeg.org/doxygen/trunk/struct%s.html", "struct "), + "issue": ("https://github.com/PyAV-Org/PyAV/issues/%s", "#"), + "pr": ("https://github.com/PyAV-Org/PyAV/pull/%s", "#"), + "gh-user": ("https://github.com/%s", "@"), } intersphinx_mapping = { - 'https://docs.python.org/3': None, + "https://docs.python.org/3": None, } -autodoc_member_order = 'bysource' +autodoc_member_order = "bysource" autodoc_default_options = { - 'undoc-members': True, - 'show-inheritance': True, + "undoc-members": True, + "show-inheritance": True, } @@ -260,57 +144,54 @@ def sandboxed(*args, **kwargs): class PyInclude(SphinxDirective): - has_content = True def run(self): - - - source = '\n'.join(self.content) + source = "\n".join(self.content) output = [] - def write(*content, sep=' ', end='\n'): + + def write(*content, sep=" ", end="\n"): output.append(sep.join(map(str, content)) + end) namespace = dict(write=write) - exec(compile(source, '', 'exec'), namespace, namespace) + exec(compile(source, "", "exec"), namespace, namespace) - output = ''.join(output).splitlines() - self.state_machine.insert_input(output, 'blah') + output = "".join(output).splitlines() + self.state_machine.insert_input(output, "blah") - return [] #[nodes.literal('hello', repr(content))] + return [] # [nodes.literal('hello', repr(content))] def load_entrypoint(name): - - parts = name.split(':') + parts = name.split(":") if len(parts) == 1: - parts = name.rsplit('.', 1) + parts = name.rsplit(".", 1) mod_name, attrs = parts - attrs = attrs.split('.') + attrs = attrs.split(".") try: - obj = __import__(mod_name, fromlist=['.']) + obj = __import__(mod_name, fromlist=["."]) except ImportError as e: - print('Error while importing.', (name, mod_name, attrs, e)) + print("Error while importing.", (name, mod_name, attrs, e)) raise + for attr in attrs: obj = getattr(obj, attr) + return obj -class EnumTable(SphinxDirective): +class EnumTable(SphinxDirective): required_arguments = 1 option_spec = { - 'class': lambda x: x, + "class": lambda x: x, } def run(self): - - cls_ep = self.options.get('class') + cls_ep = self.options.get("class") cls = load_entrypoint(cls_ep) if cls_ep else None enum = load_entrypoint(self.arguments[0]) - properties = {} if cls is not None: @@ -346,20 +227,19 @@ def makerow(*texts): for text in texts: if text is None: continue - row += nodes.entry('', nodes.paragraph('', str(text))) + row += nodes.entry("", nodes.paragraph("", str(text))) return row thead += makerow( - '{} Attribute'.format(cls.__name__) if cls else None, - '{} Name'.format(enum.__name__), - 'Flag Value', - 'Meaning in FFmpeg', + f"{cls.__name__} Attribute" if cls else None, + f"{enum.__name__} Name", + "Flag Value", + "Meaning in FFmpeg", ) seen = set() for name, item in enum._by_name.items(): - if name.lower() in seen: continue seen.add(name.lower()) @@ -371,32 +251,24 @@ def makerow(*texts): continue attr = None - value = '0x{:X}'.format(item.value) - - doc = item.__doc__ or '-' - - tbody += makerow( - attr, - name, - value, - doc, - ) + value = f"0x{item.value:X}" + doc = item.__doc__ or "-" + tbody += makerow(attr, name, value, doc) return [table] - - doxylink = {} -ffmpeg_tagfile = os.path.abspath(os.path.join(__file__, '..', '_build', 'doxygen', 'tagfile.xml')) +ffmpeg_tagfile = os.path.abspath( + os.path.join(__file__, "..", "_build", "doxygen", "tagfile.xml") +) if not os.path.exists(ffmpeg_tagfile): print("ERROR: Missing FFmpeg tagfile.") exit(1) -doxylink['ffmpeg'] = (ffmpeg_tagfile, 'https://ffmpeg.org/doxygen/trunk/') +doxylink["ffmpeg"] = (ffmpeg_tagfile, "https://ffmpeg.org/doxygen/trunk/") def doxylink_create_handler(app, file_name, url_base): - print("Finding all names in Doxygen tagfile", file_name) doc = etree.parse(file_name) @@ -405,30 +277,29 @@ def doxylink_create_handler(app, file_name, url_base): parent_map = {} # ElementTree doesn't five us access to parents. urls = {} - for node in root.findall('.//name/..'): - + for node in root.findall(".//name/.."): for child in node: parent_map[child] = node - kind = node.attrib['kind'] - if kind not in ('function', 'struct', 'variable'): + kind = node.attrib["kind"] + if kind not in ("function", "struct", "variable"): continue - name = node.find('name').text + name = node.find("name").text - if kind not in ('function', ): + if kind not in ("function",): parent = parent_map.get(node) - parent_name = parent.find('name') if parent else None + parent_name = parent.find("name") if parent else None if parent_name is not None: - name = '{}.{}'.format(parent_name.text, name) + name = f"{parent_name.text}.{name}" - filenode = node.find('filename') + filenode = node.find("filename") if filenode is not None: url = filenode.text else: - url = '{}#{}'.format( - node.find('anchorfile').text, - node.find('anchor').text, + url = "{}#{}".format( + node.find("anchorfile").text, + node.find("anchor").text, ) urls.setdefault(kind, {})[name] = url @@ -436,9 +307,9 @@ def doxylink_create_handler(app, file_name, url_base): def get_url(name): # These are all the kinds that seem to exist. for kind in ( - 'function', - 'struct', - 'variable', # These are struct members. + "function", + "struct", + "variable", # These are struct members. # 'class', # 'define', # 'enumeration', @@ -454,10 +325,8 @@ def get_url(name): except KeyError: pass - def _doxylink_handler(name, rawtext, text, lineno, inliner, options={}, content=[]): - - m = re.match(r'^(.+?)(?:<(.+?)>)?$', text) + m = re.match(r"^(.+?)(?:<(.+?)>)?$", text) title, name = m.groups() name = name or title @@ -469,30 +338,23 @@ def _doxylink_handler(name, rawtext, text, lineno, inliner, options={}, content= node = addnodes.literal_strong(title, title) if url: url = url_base + url - node = nodes.reference( - '', '', node, refuri=url - ) + node = nodes.reference("", "", node, refuri=url) return [node], [] return _doxylink_handler - - def setup(app): + app.add_css_file("custom.css") - app.add_css_file('custom.css') - - app.add_directive('flagtable', EnumTable) - app.add_directive('enumtable', EnumTable) - app.add_directive('pyinclude', PyInclude) + app.add_directive("flagtable", EnumTable) + app.add_directive("enumtable", EnumTable) + app.add_directive("pyinclude", PyInclude) - skip = os.environ.get('PYAV_SKIP_DOXYLINK') + skip = os.environ.get("PYAV_SKIP_DOXYLINK") for role, (filename, url_base) in doxylink.items(): if skip: app.add_role(role, lambda *args: ([], [])) else: app.add_role(role, doxylink_create_handler(app, filename, url_base)) - - From 285cf407afaa6510f47ca03ce41c509618615bad Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 17 Feb 2024 05:17:45 -0500 Subject: [PATCH 179/846] Bump copyright year in docs --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 3124bba59..8f17daa83 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -43,7 +43,7 @@ # General information about the project. project = "PyAV" -copyright = "2023, The PyAV Team" +copyright = "2024, The PyAV Team" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the From 25c07584e18d53aeb71116660acfdd8e5340c7fa Mon Sep 17 00:00:00 2001 From: Philip de Nier Date: Fri, 16 Feb 2024 15:05:23 +0000 Subject: [PATCH 180/846] Check None packet when setting time_base after decode --- av/codec/context.pyx | 3 ++- tests/test_decode.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 5a28cd69e..09cf93b0c 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -527,7 +527,8 @@ cdef class CodecContext: # is carrying around. # TODO: Somehow get this from the stream so we can not pass the # packet here (because flushing packets are bogus). - frame._time_base = packet._time_base + if packet is not None: + frame._time_base = packet._time_base frame.index = self.ptr.frame_number - 1 diff --git a/tests/test_decode.py b/tests/test_decode.py index bc9c96e58..87a84ba12 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -124,3 +124,35 @@ def test_decode_close_then_use(self): getattr(container, attr) except AssertionError: pass + + def test_flush_decoded_video_frame_count(self): + container = av.open(fate_suite("h264/interlaced_crop.mp4")) + video_stream = next(s for s in container.streams if s.type == "video") + + self.assertIs(video_stream, container.streams.video[0]) + + # Decode the first GOP, which requires a flush to get all frames + have_keyframe = False + input_count = 0 + output_count = 0 + + for packet in container.demux(video_stream): + if packet.is_keyframe: + if have_keyframe: + break + have_keyframe = True + + input_count += 1 + + for frame in video_stream.decode(packet): + output_count += 1 + + # Check the test works as expected and requires a flush + self.assertLess(output_count, input_count) + + for frame in video_stream.decode(None): + # The Frame._time_base is not set by PyAV + self.assertIsNone(frame.time_base) + output_count += 1 + + self.assertEqual(output_count, input_count) From b0e0b938df09380896f57b1735f7a259d2228ef3 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 20 Feb 2024 22:10:51 -0500 Subject: [PATCH 181/846] Always use f-strings always use f-strings in pure python blocks. Spacing is also changed to better match black. --- av/__main__.py | 9 +++---- av/_core.pyx | 2 ++ av/audio/codeccontext.pyx | 1 - av/audio/fifo.pyx | 22 ++++++++-------- av/audio/format.pyx | 7 ++---- av/audio/frame.pyx | 32 ++++++++---------------- av/audio/layout.pyx | 12 ++++----- av/audio/stream.pyx | 13 +++------- av/buffer.pyx | 7 ++++-- av/codec/codec.pyx | 28 ++++++++++++--------- av/codec/context.pyx | 19 +++----------- av/container/core.pyx | 2 +- av/container/input.pyx | 6 +---- av/container/output.pyx | 11 +++----- av/container/pyio.pyx | 2 -- av/container/streams.pyx | 1 - av/data/stream.pyx | 13 ++++------ av/datasets.py | 6 ++--- av/deprecation.py | 2 +- av/descriptor.pyx | 6 ++--- av/dictionary.pyx | 3 +-- av/enum.pyx | 47 ++++++++++------------------------- av/error.pyx | 6 ----- av/filter/context.pyx | 24 ++++++++++-------- av/filter/filter.pyx | 3 +-- av/filter/graph.pyx | 6 ++--- av/filter/pad.pyx | 26 ++++++------------- av/format.pyx | 5 ++-- av/frame.pyx | 16 +++--------- av/logging.pyx | 7 ------ av/option.pyx | 12 ++++----- av/packet.pyx | 13 +++------- av/plane.pyx | 8 +++--- av/sidedata/motionvectors.pyx | 2 -- av/sidedata/sidedata.pyx | 5 ---- av/stream.pyx | 13 ++++------ av/utils.pyx | 11 +++++--- av/video/frame.pyx | 12 ++++----- 38 files changed, 156 insertions(+), 264 deletions(-) diff --git a/av/__main__.py b/av/__main__.py index b5718ba8b..4e5d62692 100644 --- a/av/__main__.py +++ b/av/__main__.py @@ -7,13 +7,11 @@ def main(): parser.add_argument("--version", action="store_true") args = parser.parse_args() - # --- - if args.version: import av import av._core - print("PyAV v" + av.__version__) + print(f"PyAV v{av.__version__}") by_config = {} for libname, config in sorted(av._core.library_meta.items()): @@ -22,14 +20,13 @@ def main(): by_config.setdefault( (config["configuration"], config["license"]), [] ).append((libname, config)) + for (config, license), libs in sorted(by_config.items()): print("library configuration:", config) print("library license:", license) for libname, config in libs: version = config["version"] - print( - "%-13s %3d.%3d.%3d" % (libname, version[0], version[1], version[2]) - ) + print(f"{libname:<13} {version[0]:3d}.{version[1]:3d}.{version[2]:3d}") if args.codecs: from av.codec.codec import dump_codecs diff --git a/av/_core.pyx b/av/_core.pyx index 1c3e4816a..15da5dcc6 100644 --- a/av/_core.pyx +++ b/av/_core.pyx @@ -12,9 +12,11 @@ time_base = lib.AV_TIME_BASE cdef decode_version(v): if v < 0: return (-1, -1, -1) + cdef int major = (v >> 16) & 0xff cdef int minor = (v >> 8) & 0xff cdef int micro = (v) & 0xff + return (major, minor, micro) library_meta = { diff --git a/av/audio/codeccontext.pyx b/av/audio/codeccontext.pyx index 8446fbcd0..e8461156a 100644 --- a/av/audio/codeccontext.pyx +++ b/av/audio/codeccontext.pyx @@ -8,7 +8,6 @@ from av.packet cimport Packet cdef class AudioCodecContext(CodecContext): - cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec): CodecContext._init(self, ptr, codec) diff --git a/av/audio/fifo.pyx b/av/audio/fifo.pyx index 605f7644d..7bc4b0d48 100644 --- a/av/audio/fifo.pyx +++ b/av/audio/fifo.pyx @@ -3,23 +3,18 @@ from av.error cimport err_check cdef class AudioFifo: - """A simple audio sample FIFO (First In First Out) buffer.""" def __repr__(self): try: - result = "" % ( - self.__class__.__name__, - self.samples, - self.sample_rate, - self.layout, - self.format, - id(self), + result = ( + f"" ) except AttributeError: - result = "" % ( - self.__class__.__name__, - id(self), + result = ( + f"" ) return result @@ -90,7 +85,9 @@ cdef class AudioFifo: if self.pts_per_sample and frame.ptr.pts != lib.AV_NOPTS_VALUE: expected_pts = (self.pts_per_sample * self.samples_written) if frame.ptr.pts != expected_pts: - raise ValueError("Frame.pts (%d) != expected (%d); fix or set to None." % (frame.ptr.pts, expected_pts)) + raise ValueError( + "Frame.pts (%d) != expected (%d); fix or set to None." % (frame.ptr.pts, expected_pts) + ) err_check(lib.av_audio_fifo_write( self.ptr, @@ -173,6 +170,7 @@ cdef class AudioFifo: frames.append(frame) else: break + return frames property format: diff --git a/av/audio/format.pyx b/av/audio/format.pyx index 4cecc7cf1..09b939221 100644 --- a/av/audio/format.pyx +++ b/av/audio/format.pyx @@ -18,11 +18,9 @@ cdef AudioFormat get_audio_format(lib.AVSampleFormat c_format): cdef class AudioFormat: - """Descriptor of audio formats.""" def __cinit__(self, name): - if name is _cinit_bypass_sentinel: return @@ -33,7 +31,7 @@ cdef class AudioFormat: sample_fmt = lib.av_get_sample_fmt(name) if sample_fmt < 0: - raise ValueError("Not a sample format: %r" % name) + raise ValueError(f"Not a sample format: {name!r}") self._init(sample_fmt) @@ -41,7 +39,7 @@ cdef class AudioFormat: self.sample_fmt = sample_fmt def __repr__(self): - return "" % (self.name) + return f"" property name: """Canonical name of the sample format. @@ -128,7 +126,6 @@ cdef class AudioFormat: """ def __get__(self): - if self.is_planar: raise ValueError("no planar container formats") diff --git a/av/audio/frame.pyx b/av/audio/frame.pyx index 5b3aa1812..a78b65fea 100644 --- a/av/audio/frame.pyx +++ b/av/audio/frame.pyx @@ -37,7 +37,6 @@ cdef AudioFrame alloc_audio_frame(): cdef class AudioFrame(Frame): - """A frame of audio.""" def __cinit__(self, format="s16", layout="stereo", samples=0, align=1): @@ -61,7 +60,6 @@ cdef class AudioFrame(Frame): self.ptr.channels = self.layout.nb_channels if self.layout.channels and nb_samples: - # Cleanup the old buffer. lib.av_freep(&self._buffer) @@ -95,15 +93,9 @@ cdef class AudioFrame(Frame): self.format = get_audio_format(self.ptr.format) def __repr__(self): - return "" % ( - self.__class__.__name__, - self.index, - self.pts, - self.samples, - self.rate, - self.layout.name, - self.format.name, - id(self), + return ( + f" 8: - raise ValueError("no layout with %d channels" % layout) + raise ValueError(f"no layout with {layout} channels") + c_layout = default_layouts[layout] elif isinstance(layout, str): c_layout = lib.av_get_channel_layout(layout) @@ -82,7 +81,7 @@ cdef class AudioLayout: raise TypeError("layout must be str or int") if not c_layout: - raise ValueError("invalid channel layout %r" % layout) + raise ValueError(f"invalid channel layout: {layout}") self._init(c_layout) @@ -92,7 +91,7 @@ cdef class AudioLayout: self.channels = tuple(AudioChannel(self, i) for i in range(self.nb_channels)) def __repr__(self): - return "" % (self.__class__.__name__, self.name) + return f"" property name: """The canonical name of the audio layout.""" @@ -104,12 +103,11 @@ cdef class AudioLayout: cdef class AudioChannel: - def __cinit__(self, AudioLayout layout, int index): self.channel = lib.av_channel_layout_extract_channel(layout.layout, index) def __repr__(self): - return "" % (self.__class__.__name__, self.name, self.description) + return f"" property name: """The canonical name of the audio channel.""" diff --git a/av/audio/stream.pyx b/av/audio/stream.pyx index e40659212..3c6b76fcf 100644 --- a/av/audio/stream.pyx +++ b/av/audio/stream.pyx @@ -1,12 +1,7 @@ cdef class AudioStream(Stream): - def __repr__(self): - return "" % ( - self.__class__.__name__, - self.index, - self.name, - self.rate, - self.layout.name, - self.format.name if self.format else None, - id(self), + form = self.format.name if self.format else None + return ( + f"" ) diff --git a/av/buffer.pyx b/av/buffer.pyx index d5e49fe96..8c8418beb 100644 --- a/av/buffer.pyx +++ b/av/buffer.pyx @@ -6,7 +6,6 @@ from av.bytesource cimport ByteSource, bytesource cdef class Buffer: - """A base class for PyAV objects which support the buffer protocol, such as :class:`.Packet` and :class:`.Plane`. @@ -24,6 +23,7 @@ cdef class Buffer: def __getbuffer__(self, Py_buffer *view, int flags): if flags & PyBUF_WRITABLE and not self._buffer_writable(): raise ValueError("buffer is not writable") + PyBuffer_FillInfo(view, self, self._buffer_ptr(), self._buffer_size(), 0, flags) @property @@ -59,8 +59,11 @@ cdef class Buffer: """ if not self._buffer_writable(): raise ValueError("buffer is not writable") + cdef ByteSource source = bytesource(input) cdef size_t size = self._buffer_size() + if source.length != size: - raise ValueError("got %d bytes; need %d bytes" % (source.length, size)) + raise ValueError(f"got {source.length} bytes; need {size} bytes") + memcpy(self._buffer_ptr(), source.ptr, size) diff --git a/av/codec/codec.pyx b/av/codec/codec.pyx index 069df5059..8d8e2d9e1 100644 --- a/av/codec/codec.pyx +++ b/av/codec/codec.pyx @@ -352,7 +352,8 @@ codec_descriptor = wrap_avclass(lib.avcodec_get_class()) def dump_codecs(): """Print information about available codecs.""" - print """Codecs: + print( + """Codecs: D..... = Decoding supported .E.... = Encoding supported ..V... = Video codec @@ -362,9 +363,9 @@ def dump_codecs(): ....L. = Lossy compression .....S = Lossless compression ------""" + ) for name in sorted(codecs_available): - try: e_codec = Codec(name, "w") except ValueError: @@ -379,15 +380,18 @@ def dump_codecs(): codec = e_codec or d_codec try: - print " %s%s%s%s%s%s %-18s %s" % ( - ".D"[bool(d_codec)], - ".E"[bool(e_codec)], - codec.type[0].upper(), - ".I"[codec.intra_only], - ".L"[codec.lossy], - ".S"[codec.lossless], - codec.name, - codec.long_name + print( + " %s%s%s%s%s%s %-18s %s" + % ( + ".D"[bool(d_codec)], + ".E"[bool(e_codec)], + codec.type[0].upper(), + ".I"[codec.intra_only], + ".L"[codec.lossy], + ".S"[codec.lossless], + codec.name, + codec.long_name, + ) ) except Exception as e: - print "...... %-18s ERROR: %s" % (codec.name, e) + print(f"...... {codec.name:<18} ERROR: {e}") diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 09cf93b0c..8930c1c56 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -151,7 +151,6 @@ cdef class CodecContext: self.stream_index = -1 # This is set by the container immediately. cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec): - self.ptr = ptr if self.ptr.codec and codec and self.ptr.codec != codec: raise RuntimeError("Wrapping CodecContext with mismatched codec.") @@ -256,7 +255,6 @@ cdef class CodecContext: return lib.av_codec_is_decoder(self.ptr.codec) cpdef open(self, bint strict=True): - if lib.avcodec_is_open(self.ptr): if strict: raise ValueError("CodecContext is already open.") @@ -302,12 +300,9 @@ cdef class CodecContext: lib.av_parser_close(self.parser) def __repr__(self): - return "" % ( - self.__class__.__name__, - self.type or "", - self.name or "", - id(self), - ) + _type = self.type or "" + name = self.name or "" + return f"" def parse(self, raw_input=None): """Split up a byte stream into list of :class:`.Packet`. @@ -329,7 +324,7 @@ cdef class CodecContext: if not self.parser: self.parser = lib.av_parser_init(self.codec.ptr.id) if not self.parser: - raise ValueError("No parser for %s" % self.codec.name) + raise ValueError(f"No parser for {self.codec.name}") cdef ByteSource source = bytesource(raw_input, allow_none=True) @@ -344,7 +339,6 @@ cdef class CodecContext: packets = [] while True: - with nogil: consumed = lib.av_parser_parse2( self.parser, @@ -357,7 +351,6 @@ cdef class CodecContext: err_check(consumed) if out_size: - # We copy the data immediately, as we have yet to figure out # the expected lifetime of the buffer we get back. All of the # examples decode it immediately. @@ -389,7 +382,6 @@ cdef class CodecContext: return packets def _send_frame_and_recv(self, Frame frame): - cdef Packet packet cdef int res @@ -403,7 +395,6 @@ cdef class CodecContext: packet = self._recv_packet() cdef _send_packet_and_recv(self, Packet packet): - cdef Frame frame cdef int res @@ -427,7 +418,6 @@ cdef class CodecContext: raise NotImplementedError("Base CodecContext cannot decode.") cdef _recv_frame(self): - if not self._next_frame: self._next_frame = self._alloc_next_frame() cdef Frame frame = self._next_frame @@ -445,7 +435,6 @@ cdef class CodecContext: return frame cdef _recv_packet(self): - cdef Packet packet = Packet() cdef int res diff --git a/av/container/core.pyx b/av/container/core.pyx index e7bb8f4b9..8134d8bfc 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -280,7 +280,7 @@ cdef class Container: self.close() def __repr__(self): - return "" % (self.__class__.__name__, self.file or self.name) + return f"" cdef int err_check(self, int value) except -1: return err_check(value, filename=self.name) diff --git a/av/container/input.pyx b/av/container/input.pyx index eed457a2c..56477afba 100644 --- a/av/container/input.pyx +++ b/av/container/input.pyx @@ -21,9 +21,7 @@ cdef close_input(InputContainer self): cdef class InputContainer(Container): - def __cinit__(self, *args, **kwargs): - cdef CodecContext py_codec_context cdef unsigned int i cdef lib.AVStream *stream @@ -151,17 +149,15 @@ cdef class InputContainer(Container): self.set_timeout(self.read_timeout) try: - for i in range(self.ptr.nb_streams): include_stream[i] = False for stream in streams: i = stream.index if i >= self.ptr.nb_streams: - raise ValueError("stream index %d out of range" % i) + raise ValueError(f"stream index {i} out of range") include_stream[i] = True while True: - packet = Packet() try: self.start_timeout() diff --git a/av/container/output.pyx b/av/container/output.pyx index 8f71b446e..695479597 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -30,7 +30,6 @@ cdef close_output(OutputContainer self): cdef class OutputContainer(Container): - def __cinit__(self, *args, **kwargs): self.streams = StreamContainer() self.metadata = {} @@ -74,12 +73,10 @@ cdef class OutputContainer(Container): codec = codec_obj.ptr # Assert that this format supports the requested codec. - if not lib.avformat_query_codec( - self.ptr.oformat, - codec.id, - lib.FF_COMPLIANCE_NORMAL, - ): - raise ValueError("%r format does not support %r codec" % (self.format.name, codec_name)) + if not lib.avformat_query_codec(self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL): + raise ValueError( + f"{self.format.name!r} format does not support {codec_name!r} codec" + ) # Create new stream in the AVFormatContext, set AVCodecContext values. lib.avformat_new_stream(self.ptr, codec) diff --git a/av/container/pyio.pyx b/av/container/pyio.pyx index d68a69944..47110d495 100644 --- a/av/container/pyio.pyx +++ b/av/container/pyio.pyx @@ -8,9 +8,7 @@ ctypedef int64_t (*seek_func_t)(void *opaque, int64_t offset, int whence) noexce cdef class PyIOFile: - def __cinit__(self, file, buffer_size, writeable=None): - self.file = file cdef seek_func_t seek_func = NULL diff --git a/av/container/streams.pyx b/av/container/streams.pyx index 6875d6715..921bd40b2 100644 --- a/av/container/streams.pyx +++ b/av/container/streams.pyx @@ -95,7 +95,6 @@ cdef class StreamContainer: selection = [] for x in _flatten((args, kwargs)): - if x is None: pass diff --git a/av/data/stream.pyx b/av/data/stream.pyx index 097777999..094a25704 100644 --- a/av/data/stream.pyx +++ b/av/data/stream.pyx @@ -2,15 +2,12 @@ cimport libav as lib cdef class DataStream(Stream): - def __repr__(self): - return "" % ( - self.__class__.__name__, - self.index, - self.type or "", - self.name or "", - id(self), - ) + cls_name = self.__class__.__name__ + _type = self.type or "" + name = self.name or "" + + return f"" def encode(self, frame=None): return [] diff --git a/av/datasets.py b/av/datasets.py index b3a5eed85..5c189365a 100644 --- a/av/datasets.py +++ b/av/datasets.py @@ -60,7 +60,7 @@ def cached_download(url, name): clean_name = os.path.normpath(name) if clean_name != name: - raise ValueError("{} is not normalized.".format(name)) + raise ValueError(f"{name} is not normalized.") for dir_ in iter_data_dirs(): path = os.path.join(dir_, name) @@ -70,11 +70,11 @@ def cached_download(url, name): dir_ = next(iter_data_dirs(True)) path = os.path.join(dir_, name) - log.info("Downloading {} to {}".format(url, path)) + log.info(f"Downloading {url} to {path}") response = urlopen(url) if response.getcode() != 200: - raise ValueError("HTTP {}".format(response.getcode())) + raise ValueError(f"HTTP {response.getcode()}") dir_ = os.path.dirname(path) try: diff --git a/av/deprecation.py b/av/deprecation.py index ab1bbc559..b9a65f135 100644 --- a/av/deprecation.py +++ b/av/deprecation.py @@ -74,7 +74,7 @@ def __init__(self, func): def __get__(self, instance, cls): warning = MethodDeprecationWarning( - "{}.{} is deprecated.".format(cls.__name__, self.func.__name__) + f"{cls.__name__}.{self.func.__name__} is deprecated." ) warnings.warn(warning, stacklevel=2) return self.func.__get__(instance, cls) diff --git a/av/descriptor.pyx b/av/descriptor.pyx index 5840d2d80..a17072470 100644 --- a/av/descriptor.pyx +++ b/av/descriptor.pyx @@ -14,13 +14,13 @@ cdef Descriptor wrap_avclass(const lib.AVClass *ptr): cdef class Descriptor: - def __cinit__(self, sentinel): if sentinel is not _cinit_sentinel: raise RuntimeError("Cannot construct av.Descriptor") property name: - def __get__(self): return self.ptr.class_name if self.ptr.class_name else None + def __get__(self): + return self.ptr.class_name if self.ptr.class_name else None property options: def __get__(self): @@ -56,4 +56,4 @@ cdef class Descriptor: return self._options def __repr__(self): - return "<%s %s at 0x%x>" % (self.__class__.__name__, self.name, id(self)) + return f"<{self.__class__.__name__} {self.name} at 0x{id(self):x}>" diff --git a/av/dictionary.pyx b/av/dictionary.pyx index 27a265f77..15de38381 100644 --- a/av/dictionary.pyx +++ b/av/dictionary.pyx @@ -4,7 +4,6 @@ from av.error cimport err_check cdef class _Dictionary: - def __cinit__(self, *args, **kwargs): for arg in args: self.update(arg) @@ -40,7 +39,7 @@ cdef class _Dictionary: yield element.key def __repr__(self): - return "av.Dictionary(%r)" % dict(self) + return f"av.Dictionary({dict(self)!r})" cpdef _Dictionary copy(self): cdef _Dictionary other = Dictionary() diff --git a/av/enum.pyx b/av/enum.pyx index bc704a11d..a21d66e81 100644 --- a/av/enum.pyx +++ b/av/enum.pyx @@ -16,13 +16,11 @@ cdef sentinel = object() class EnumType(type): - def __new__(mcl, name, bases, attrs, *args): # Just adapting the method signature. return super().__new__(mcl, name, bases, attrs) def __init__(self, name, bases, attrs, items): - self._by_name = {} self._by_value = {} self._all = [] @@ -31,7 +29,6 @@ class EnumType(type): self._create(*spec) def _create(self, name, value, doc=None, by_value_only=False): - # We only have one instance per value. try: item = self._by_value[value] @@ -53,7 +50,6 @@ class EnumType(type): return iter(self._all) def __getitem__(self, key): - if isinstance(key, str): return self._by_name[key] @@ -71,12 +67,9 @@ class EnumType(type): if isinstance(key, self): return key - raise TypeError("{0} indices must be str, int, or {0}".format( - self.__name__, - )) + raise TypeError(f"{self.__name__} indices must be str, int, or itself") def _get(self, long value, bint create=False): - try: return self._by_value[value] except KeyError: @@ -85,10 +78,9 @@ class EnumType(type): if not create: return - return self._create("{}_{}".format(self.__name__.upper(), value), value, by_value_only=True) + return self._create(f"{self.__name__.upper()}_{value}", value, by_value_only=True) def _get_multi_flags(self, long value): - try: return self._by_value[value] except KeyError: @@ -134,7 +126,6 @@ copyreg.constructor(_unpickle) cdef class EnumItem: - """ Enumerations are when an attribute may only take on a single value at once, and they are represented as integers in the FFmpeg API. We associate names with each @@ -171,9 +162,8 @@ cdef class EnumItem: cdef Py_hash_t _hash def __cinit__(self, sentinel_, str name, int value, doc=None): - if sentinel_ is not sentinel: - raise RuntimeError("Cannot instantiate {}.".format(self.__class__.__name__)) + raise RuntimeError(f"Cannot instantiate {self.__class__.__name__}.") self.name = name self.value = value @@ -190,12 +180,7 @@ cdef class EnumItem: self._hash = hash_ def __repr__(self): - return "<{}.{}:{}(0x{:x})>".format( - self.__class__.__module__, - self.__class__.__name__, - self.name, - self.value, - ) + return f"<{self.__class__.__module__}.{self.__class__.__name__}:{self.name}(0x{self.value:x})>" def __str__(self): return self.name @@ -210,19 +195,16 @@ cdef class EnumItem: return (_unpickle, (self.__class__.__module__, self.__class__.__name__, self.name)) def __eq__(self, other): - if isinstance(other, str): - if self.name == other: # The quick method. return True try: other_inst = self.__class__._by_name[other] except KeyError: - raise ValueError("{} does not have item named {!r}".format( - self.__class__.__name__, - other, - )) + raise ValueError( + f"{self.__class__.__name__} does not have item named {other!r}" + ) else: return self is other_inst @@ -231,18 +213,16 @@ cdef class EnumItem: return True if other in self.__class__._by_value: return False - raise ValueError("{} does not have item valued {}".format( - self.__class__.__name__, - other, - )) + raise ValueError( + f"{self.__class__.__name__} does not have item valued {other}" + ) if isinstance(other, self.__class__): return self is other - raise TypeError("'==' not supported between {} and {}".format( - self.__class__.__name__, - type(other).__name__, - )) + raise TypeError( + f"'==' not supported between {self.__class__.__name__} and {type(other).__name__}" + ) def __ne__(self, other): return not (self == other) @@ -321,7 +301,6 @@ cdef class EnumFlag(EnumItem): cdef class EnumProperty: - cdef object enum cdef object fget cdef object fset diff --git a/av/error.pyx b/av/error.pyx index 249be7d53..7cb1476c7 100644 --- a/av/error.pyx +++ b/av/error.pyx @@ -50,7 +50,6 @@ cpdef tag_to_code(bytes tag): class FFmpegError(Exception): - """Exception class for errors from within FFmpeg. .. attribute:: errno @@ -187,7 +186,6 @@ ErrorType.tag = property(lambda self: code_to_tag(self.value)) for enum in ErrorType: - # Mimick the errno module. globals()[enum.name] = enum if enum.value == c_PYAV_STASHED_ERROR: @@ -285,7 +283,6 @@ cdef object _local = local() cdef int _err_count = 0 cdef int stash_exception(exc_info=None): - global _err_count existing = getattr(_local, "exc_info", None) @@ -338,16 +335,13 @@ class UndefinedError(FFmpegError): cpdef make_error(int res, filename=None, log=None): - cdef int code = -res cdef bytes py_buffer cdef char *c_buffer if code == c_PYAV_STASHED_ERROR: message = PYAV_STASHED_ERROR_message - else: - # Jump through some hoops due to Python 2 in same codebase. py_buffer = b"\0" * lib.AV_ERROR_MAX_STRING_SIZE c_buffer = py_buffer diff --git a/av/filter/context.pyx b/av/filter/context.pyx index 1a1522d40..f36ce5290 100644 --- a/av/filter/context.pyx +++ b/av/filter/context.pyx @@ -20,17 +20,18 @@ cdef FilterContext wrap_filter_context(Graph graph, Filter filter, lib.AVFilterC cdef class FilterContext: - def __cinit__(self, sentinel): if sentinel is not _cinit_sentinel: raise RuntimeError("cannot construct FilterContext") def __repr__(self): - return "" % ( - (repr(self.ptr.name) if self.ptr.name != NULL else "") if self.ptr != NULL else "None", - self.filter.ptr.name if self.filter and self.filter.ptr != NULL else None, - id(self), - ) + if self.ptr != NULL: + name = repr(self.ptr.name) if self.ptr.name != NULL else "" + else: + name = "None" + + parent = self.filter.ptr.name if self.filter and self.filter.ptr != NULL else None + return f"" property name: def __get__(self): @@ -50,7 +51,6 @@ cdef class FilterContext: return self._outputs def init(self, args=None, **kwargs): - if self.inited: raise ValueError("already inited") if args and kwargs: @@ -68,7 +68,7 @@ cdef class FilterContext: self.inited = True if dict_: - raise ValueError("unused config: %s" % ", ".join(sorted(dict_))) + raise ValueError(f"unused config: {', '.join(sorted(dict_))}") def link_to(self, FilterContext input_, int output_idx=0, int input_idx=0): err_check(lib.avfilter_link(self.ptr, output_idx, input_.ptr, input_idx)) @@ -89,7 +89,9 @@ cdef class FilterContext: # Delegate to the input. if len(self.inputs) != 1: - raise ValueError("cannot delegate push without single input; found %d" % len(self.inputs)) + raise ValueError( + f"cannot delegate push without single input; found {len(self.inputs)}" + ) if not self.inputs[0].link: raise ValueError("cannot delegate push without linked input") self.inputs[0].linked.context.push(frame) @@ -105,7 +107,9 @@ cdef class FilterContext: else: # Delegate to the output. if len(self.outputs) != 1: - raise ValueError("cannot delegate pull without single output; found %d" % len(self.outputs)) + raise ValueError( + f"cannot delegate pull without single output; found {len(self.outputs)}" + ) if not self.outputs[0].link: raise ValueError("cannot delegate pull without linked output") return self.outputs[0].linked.context.pull() diff --git a/av/filter/filter.pyx b/av/filter/filter.pyx index 611dfd81e..22163bc55 100644 --- a/av/filter/filter.pyx +++ b/av/filter/filter.pyx @@ -22,7 +22,6 @@ cpdef enum FilterFlags: cdef class Filter: - def __cinit__(self, name): if name is _cinit_sentinel: return @@ -30,7 +29,7 @@ cdef class Filter: raise TypeError("takes a filter name as a string") self.ptr = lib.avfilter_get_by_name(name) if not self.ptr: - raise ValueError("no filter %s" % name) + raise ValueError(f"no filter {name}") property descriptor: def __get__(self): diff --git a/av/filter/graph.pyx b/av/filter/graph.pyx index c727c0931..391677063 100644 --- a/av/filter/graph.pyx +++ b/av/filter/graph.pyx @@ -172,10 +172,10 @@ cdef class Graph: elif isinstance(frame, AudioFrame): contexts = self._context_by_type.get("abuffer", []) else: - raise ValueError("can only AudioFrame, VideoFrame or None; got %s" % type(frame)) + raise ValueError(f"can only AudioFrame, VideoFrame or None; got {type(frame)}") if len(contexts) != 1: - raise ValueError("can only auto-push with single buffer; found %s" % len(contexts)) + raise ValueError(f"can only auto-push with single buffer; found {len(contexts)}") contexts[0].push(frame) @@ -185,6 +185,6 @@ cdef class Graph: nsinks = len(vsinks) + len(asinks) if nsinks != 1: - raise ValueError("can only auto-pull with single sink; found %s" % nsinks) + raise ValueError(f"can only auto-pull with single sink; found {nsinks}") return (vsinks or asinks)[0].pull() diff --git a/av/filter/pad.pyx b/av/filter/pad.pyx index 66915dcf4..d12238cc5 100644 --- a/av/filter/pad.pyx +++ b/av/filter/pad.pyx @@ -5,19 +5,15 @@ cdef object _cinit_sentinel = object() cdef class FilterPad: - def __cinit__(self, sentinel): if sentinel is not _cinit_sentinel: raise RuntimeError("cannot construct FilterPad") def __repr__(self): - return "" % ( - self.filter.name, - "inputs" if self.is_input else "outputs", - self.index, - self.name, - self.type, - ) + _filter = self.filter.name + _io = "inputs" if self.is_input else "outputs" + + return f"" property is_output: def __get__(self): @@ -40,17 +36,12 @@ cdef class FilterPad: cdef class FilterContextPad(FilterPad): - def __repr__(self): + _filter = self.filter.name + _io = "inputs" if self.is_input else "outputs" + context = self.context.name - return "" % ( - self.filter.name, - "inputs" if self.is_input else "outputs", - self.index, - self.context.name, - self.name, - self.type, - ) + return f"" property link: def __get__(self): @@ -71,7 +62,6 @@ cdef class FilterContextPad(FilterPad): cdef tuple alloc_filter_pads(Filter filter, const lib.AVFilterPad *ptr, bint is_input, FilterContext context=None): - if not ptr: return () diff --git a/av/format.pyx b/av/format.pyx index 07f883cdc..aeeb81349 100644 --- a/av/format.pyx +++ b/av/format.pyx @@ -60,7 +60,6 @@ cdef class ContainerFormat: """ def __cinit__(self, name, mode=None): - if name is _cinit_bypass_sentinel: return @@ -77,10 +76,10 @@ cdef class ContainerFormat: self.optr = lib.av_guess_format(name, NULL, NULL) if not self.iptr and not self.optr: - raise ValueError("no container format %r" % name) + raise ValueError(f"no container format {name!r}") def __repr__(self): - return "" % (self.__class__.__name__, self.name) + return f"" property descriptor: def __get__(self): diff --git a/av/frame.pyx b/av/frame.pyx index d81fd6acb..076236bff 100644 --- a/av/frame.pyx +++ b/av/frame.pyx @@ -21,12 +21,7 @@ cdef class Frame: lib.av_frame_free(&self.ptr) def __repr__(self): - return "av.%s #%d pts=%s at 0x%x>" % ( - self.__class__.__name__, - self.index, - self.pts, - id(self), - ) + return f"av.{self.__class__.__name__} #{self.index} pts={self.pts} at 0x{id(self):x}>" cdef _copy_internal_attributes(self, Frame source, bint data_layout=True): """Mimic another frame.""" @@ -45,7 +40,6 @@ cdef class Frame: pass # Dummy to match the API of the others. cdef _rebase_time(self, lib.AVRational dst): - if not dst.num: raise ValueError("Cannot rebase to zero time.") @@ -57,10 +51,7 @@ cdef class Frame: return if self.ptr.pts != lib.AV_NOPTS_VALUE: - self.ptr.pts = lib.av_rescale_q( - self.ptr.pts, - self._time_base, dst - ) + self.ptr.pts = lib.av_rescale_q(self.ptr.pts, self._time_base, dst) self._time_base = dst @@ -135,7 +126,8 @@ cdef class Frame: :type: bool """ - def __get__(self): return self.ptr.decode_error_flags != 0 or bool(self.ptr.flags & lib.AV_FRAME_FLAG_CORRUPT) + def __get__(self): + return self.ptr.decode_error_flags != 0 or bool(self.ptr.flags & lib.AV_FRAME_FLAG_CORRUPT) @property def side_data(self): diff --git a/av/logging.pyx b/av/logging.pyx index fa2ced823..49d4a80da 100644 --- a/av/logging.pyx +++ b/av/logging.pyx @@ -169,7 +169,6 @@ cdef global_captures = [] cdef thread_captures = {} cdef class Capture: - """A context manager for capturing logs. :param bool local: Should logs from all threads be captured, or just one @@ -188,7 +187,6 @@ cdef class Capture: cdef list captures def __init__(self, bint local=True): - self.logs = [] if local: @@ -259,7 +257,6 @@ cdef void log_callback(void *ptr, int level, const char *format, lib.va_list arg return with gil: - try: log_callback_gil(level, name, message) @@ -272,7 +269,6 @@ cdef void log_callback(void *ptr, int level, const char *format, lib.va_list arg cdef log_callback_gil(int level, const char *c_name, const char *c_message): - global error_count global skip_count global last_log @@ -294,9 +290,7 @@ cdef log_callback_gil(int level, const char *c_name, const char *c_message): cdef object repeat_log = None with skip_lock: - if is_interesting: - is_repeated = skip_repeated and last_log == log if is_repeated: @@ -329,7 +323,6 @@ cdef log_callback_gil(int level, const char *c_name, const char *c_message): cdef log_callback_emit(log): - lib_level, name, message = log captures = thread_captures.get(get_ident()) or global_captures diff --git a/av/option.pyx b/av/option.pyx index 02bc8e83a..5de7fa2a5 100644 --- a/av/option.pyx +++ b/av/option.pyx @@ -142,18 +142,16 @@ cdef class Option(BaseOption): return self._norm_range(self.ptr.max) def __repr__(self): - return "" % ( - self.__class__.__name__, - self.name, - self.type, - self.offset, - id(self), + return ( + f"" ) cdef OptionChoice wrap_option_choice(const lib.AVOption *ptr, bint is_default): if ptr == NULL: return None + cdef OptionChoice obj = OptionChoice(_cinit_sentinel) obj.ptr = ptr obj.is_default = is_default @@ -171,4 +169,4 @@ cdef class OptionChoice(BaseOption): return self.ptr.default_val.i64 def __repr__(self): - return "" % (self.__class__.__name__, self.name, id(self)) + return f"" diff --git a/av/packet.pyx b/av/packet.pyx index b5736facd..f07343b56 100644 --- a/av/packet.pyx +++ b/av/packet.pyx @@ -21,7 +21,6 @@ cdef class Packet(Buffer): self.ptr = lib.av_packet_alloc() def __init__(self, input=None): - cdef size_t size = 0 cdef ByteSource source = None @@ -48,13 +47,10 @@ cdef class Packet(Buffer): lib.av_packet_free(&self.ptr) def __repr__(self): - return "" % ( - self.__class__.__name__, - self._stream.index if self._stream else 0, - self.dts, - self.pts, - self.ptr.size, - id(self), + stream = self._stream.index if self._stream else 0 + return ( + f"" ) # Buffer protocol. @@ -64,7 +60,6 @@ cdef class Packet(Buffer): return self.ptr.data cdef _rebase_time(self, lib.AVRational dst): - if not dst.num: raise ValueError("Cannot rebase to zero time.") diff --git a/av/plane.pyx b/av/plane.pyx index 5d1320b1a..c733b20a7 100644 --- a/av/plane.pyx +++ b/av/plane.pyx @@ -11,11 +11,9 @@ cdef class Plane(Buffer): self.index = index def __repr__(self): - return "" % ( - self.__class__.__name__, - self.buffer_size, - self.buffer_ptr, - id(self), + return ( + f"" ) cdef void* _buffer_ptr(self): diff --git a/av/sidedata/motionvectors.pyx b/av/sidedata/motionvectors.pyx index af83132c8..b0b0b705f 100644 --- a/av/sidedata/motionvectors.pyx +++ b/av/sidedata/motionvectors.pyx @@ -7,7 +7,6 @@ cdef object _cinit_bypass_sentinel = object() # Cython doesn't let us inherit from the abstract Sequence, so we will subclass # it later. cdef class _MotionVectors(SideData): - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._vectors = {} @@ -54,7 +53,6 @@ class MotionVectors(_MotionVectors, Sequence): cdef class MotionVector: - def __init__(self, sentinel, _MotionVectors parent, int index): if sentinel is not _cinit_bypass_sentinel: raise RuntimeError("cannot manually instatiate MotionVector") diff --git a/av/sidedata/sidedata.pyx b/av/sidedata/sidedata.pyx index 201e14cfe..05ed00219 100644 --- a/av/sidedata/sidedata.pyx +++ b/av/sidedata/sidedata.pyx @@ -30,7 +30,6 @@ Type = define_enum("Type", __name__, ( cdef SideData wrap_side_data(Frame frame, int index): - cdef lib.AVFrameSideDataType type_ = frame.ptr.side_data[index].type if type_ == lib.AV_FRAME_DATA_MOTION_VECTORS: return MotionVectors(_cinit_bypass_sentinel, frame, index) @@ -39,7 +38,6 @@ cdef SideData wrap_side_data(Frame frame, int index): cdef class SideData(Buffer): - def __init__(self, sentinel, Frame frame, int index): if sentinel is not _cinit_bypass_sentinel: raise RuntimeError("cannot manually instatiate SideData") @@ -65,9 +63,7 @@ cdef class SideData(Buffer): cdef class _SideDataContainer: - def __init__(self, Frame frame): - self.frame = frame self._by_index = [] self._by_type = {} @@ -86,7 +82,6 @@ cdef class _SideDataContainer: return iter(self._by_index) def __getitem__(self, key): - if isinstance(key, int): return self._by_index[key] diff --git a/av/stream.pyx b/av/stream.pyx index f65cee609..3f5bfcf01 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -77,7 +77,7 @@ cdef class Stream: def __cinit__(self, name): if name is _cinit_bypass_sentinel: return - raise RuntimeError('cannot manually instantiate Stream') + raise RuntimeError("cannot manually instantiate Stream") cdef _init(self, Container container, lib.AVStream *stream, CodecContext codec_context): self.container = container @@ -96,12 +96,9 @@ cdef class Stream: ) def __repr__(self): - return '' % ( - self.__class__.__name__, - self.index, - self.type or '', - self.name or '', - id(self), + return ( + f"'}/" + f"{self.name or ''} at 0x{id(self):x}>" ) def __getattr__(self, name): @@ -109,7 +106,7 @@ cdef class Stream: # See: https://github.com/PyAV-Org/PyAV/issues/1005 if self.ptr.codecpar.codec_type == lib.AVMEDIA_TYPE_VIDEO and name in ("framerate", "rate"): warnings.warn( - "VideoStream.%s is deprecated as it is not always set; please use VideoStream.average_rate." % name, + f"VideoStream.{name} is deprecated as it is not always set; please use VideoStream.average_rate.", AVDeprecationWarning ) diff --git a/av/utils.pyx b/av/utils.pyx index 1b25a38fe..6d40dd889 100644 --- a/av/utils.pyx +++ b/av/utils.pyx @@ -30,8 +30,14 @@ cdef dict avdict_to_dict(lib.AVDictionary *input, str encoding, str errors): cdef dict_to_avdict(lib.AVDictionary **dst, dict src, str encoding, str errors): lib.av_dict_free(dst) for key, value in src.items(): - err_check(lib.av_dict_set(dst, _encode(key, encoding, errors), - _encode(value, encoding, errors), 0)) + err_check( + lib.av_dict_set( + dst, + _encode(key, encoding, errors), + _encode(value, encoding, errors), + 0 + ) + ) # === FRACTIONS === @@ -43,7 +49,6 @@ cdef object avrational_to_fraction(const lib.AVRational *input): cdef object to_avrational(object value, lib.AVRational *input): - if value is None: input.num = 0 input.den = 1 diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 886d28277..a4aba9ff5 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -578,7 +578,7 @@ cdef class VideoFrame(Frame): copy_array_to_plane(flat[uv_start:], frame.planes[1], 2) return frame else: - raise ValueError("Conversion from numpy array with format `%s` is not yet supported" % format) + raise ValueError(f"Conversion from numpy array with format `{format}` is not yet supported") frame = VideoFrame(array.shape[1], array.shape[0], format) copy_array_to_plane(array, frame.planes[0], 1 if array.ndim == 2 else array.shape[2]) @@ -586,10 +586,8 @@ cdef class VideoFrame(Frame): return frame def __getattribute__(self, attribute): - "This method should be deleted when `frame.index` is removed." - if attribute == 'index': - warnings.warn( - "Using `frame.index` is deprecated.", - AVDeprecationWarning - ) + # This method should be deleted when `frame.index` is removed + if attribute == "index": + warnings.warn("Using `frame.index` is deprecated.", AVDeprecationWarning) + return Frame.__getattribute__(self, attribute) From cde99507555fc388c8dc17a7bbf66a9bfdc1bd17 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 21 Feb 2024 00:25:22 -0500 Subject: [PATCH 182/846] Remove deprecated `Buffer.to_bytes` --- av/buffer.pyx | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/av/buffer.pyx b/av/buffer.pyx index 8c8418beb..20c7c4877 100644 --- a/av/buffer.pyx +++ b/av/buffer.pyx @@ -1,7 +1,6 @@ from cpython cimport PyBUF_WRITABLE, PyBuffer_FillInfo from libc.string cimport memcpy -from av import deprecation from av.bytesource cimport ByteSource, bytesource @@ -36,20 +35,6 @@ cdef class Buffer: """The memory address of the buffer.""" return self._buffer_ptr() - @deprecation.method - def to_bytes(self): - """Return the contents of this buffer as ``bytes``. - - This copies the entire contents; consider using something that uses - the `buffer protocol `_ - as that will be more efficient. - - This is largely for Python2, as Python 3 can do the same via - ``bytes(the_buffer)``. - - """ - return (self._buffer_ptr())[:self._buffer_size()] - def update(self, input): """Replace the data in this object with the given buffer. From 6bc962116c397497bab2f9bfcc9a21b72a8d527a Mon Sep 17 00:00:00 2001 From: JoeUgly <41972063+JoeUgly@users.noreply.github.com> Date: Wed, 21 Feb 2024 18:43:42 -0500 Subject: [PATCH 183/846] Convert deprecated cython extension class properties to new syntax part 1 --- av/descriptor.pyx | 70 +++++++++++++-------------- av/format.pyx | 102 +++++++++++++++++++------------------- av/frame.pyx | 81 ++++++++++++++++--------------- av/option.pyx | 120 ++++++++++++++++++++++----------------------- av/packet.pyx | 121 ++++++++++++++++++++++++---------------------- av/stream.pyx | 80 +++++++++++++++--------------- 6 files changed, 293 insertions(+), 281 deletions(-) diff --git a/av/descriptor.pyx b/av/descriptor.pyx index a17072470..720db1625 100644 --- a/av/descriptor.pyx +++ b/av/descriptor.pyx @@ -18,42 +18,42 @@ cdef class Descriptor: if sentinel is not _cinit_sentinel: raise RuntimeError("Cannot construct av.Descriptor") - property name: - def __get__(self): - return self.ptr.class_name if self.ptr.class_name else None - - property options: - def __get__(self): - cdef const lib.AVOption *ptr = self.ptr.option - cdef const lib.AVOption *choice_ptr - cdef Option option - cdef OptionChoice option_choice - cdef bint choice_is_default - if self._options is None: - options = [] - ptr = self.ptr.option - while ptr != NULL and ptr.name != NULL: - if ptr.type == lib.AV_OPT_TYPE_CONST: - ptr += 1 - continue - choices = [] - if ptr.unit != NULL: # option has choices (matching const options) - choice_ptr = self.ptr.option - while choice_ptr != NULL and choice_ptr.name != NULL: - if choice_ptr.type != lib.AV_OPT_TYPE_CONST or choice_ptr.unit != ptr.unit: - choice_ptr += 1 - continue - choice_is_default = (choice_ptr.default_val.i64 == ptr.default_val.i64 or - ptr.type == lib.AV_OPT_TYPE_FLAGS and - choice_ptr.default_val.i64 & ptr.default_val.i64) - option_choice = wrap_option_choice(choice_ptr, choice_is_default) - choices.append(option_choice) - choice_ptr += 1 - option = wrap_option(tuple(choices), ptr) - options.append(option) + @property + def name(self): + return self.ptr.class_name if self.ptr.class_name else None + + @property + def options(self): + cdef const lib.AVOption *ptr = self.ptr.option + cdef const lib.AVOption *choice_ptr + cdef Option option + cdef OptionChoice option_choice + cdef bint choice_is_default + if self._options is None: + options = [] + ptr = self.ptr.option + while ptr != NULL and ptr.name != NULL: + if ptr.type == lib.AV_OPT_TYPE_CONST: ptr += 1 - self._options = tuple(options) - return self._options + continue + choices = [] + if ptr.unit != NULL: # option has choices (matching const options) + choice_ptr = self.ptr.option + while choice_ptr != NULL and choice_ptr.name != NULL: + if choice_ptr.type != lib.AV_OPT_TYPE_CONST or choice_ptr.unit != ptr.unit: + choice_ptr += 1 + continue + choice_is_default = (choice_ptr.default_val.i64 == ptr.default_val.i64 or + ptr.type == lib.AV_OPT_TYPE_FLAGS and + choice_ptr.default_val.i64 & ptr.default_val.i64) + option_choice = wrap_option_choice(choice_ptr, choice_is_default) + choices.append(option_choice) + choice_ptr += 1 + option = wrap_option(tuple(choices), ptr) + options.append(option) + ptr += 1 + self._options = tuple(options) + return self._options def __repr__(self): return f"<{self.__class__.__name__} {self.name} at 0x{id(self):x}>" diff --git a/av/format.pyx b/av/format.pyx index aeeb81349..06b533cf4 100644 --- a/av/format.pyx +++ b/av/format.pyx @@ -81,59 +81,59 @@ cdef class ContainerFormat: def __repr__(self): return f"" - property descriptor: - def __get__(self): - if self.iptr: - return wrap_avclass(self.iptr.priv_class) - else: - return wrap_avclass(self.optr.priv_class) - - property options: - def __get__(self): - return self.descriptor.options - - property input: + @property + def descriptor(self): + if self.iptr: + return wrap_avclass(self.iptr.priv_class) + else: + return wrap_avclass(self.optr.priv_class) + + @property + def options(self): + return self.descriptor.options + + @property + def input(self): """An input-only view of this format.""" - def __get__(self): - if self.iptr == NULL: - return None - elif self.optr == NULL: - return self - else: - return build_container_format(self.iptr, NULL) - - property output: + if self.iptr == NULL: + return None + elif self.optr == NULL: + return self + else: + return build_container_format(self.iptr, NULL) + + @property + def output(self): """An output-only view of this format.""" - def __get__(self): - if self.optr == NULL: - return None - elif self.iptr == NULL: - return self - else: - return build_container_format(NULL, self.optr) - - property is_input: - def __get__(self): - return self.iptr != NULL - - property is_output: - def __get__(self): - return self.optr != NULL - - property long_name: - def __get__(self): - # We prefer the output names since the inputs may represent - # multiple formats. - return self.optr.long_name if self.optr else self.iptr.long_name - - property extensions: - def __get__(self): - cdef set exts = set() - if self.iptr and self.iptr.extensions: - exts.update(self.iptr.extensions.split(",")) - if self.optr and self.optr.extensions: - exts.update(self.optr.extensions.split(",")) - return exts + if self.optr == NULL: + return None + elif self.iptr == NULL: + return self + else: + return build_container_format(NULL, self.optr) + + @property + def is_input(self): + return self.iptr != NULL + + @property + def is_output(self): + return self.optr != NULL + + @property + def long_name(self): + # We prefer the output names since the inputs may represent + # multiple formats. + return self.optr.long_name if self.optr else self.iptr.long_name + + @property + def extensions(self): + cdef set exts = set() + if self.iptr and self.iptr.extensions: + exts.update(self.iptr.extensions.split(",")) + if self.optr and self.optr.extensions: + exts.update(self.optr.extensions.split(",")) + return exts @Flags.property def flags(self): diff --git a/av/frame.pyx b/av/frame.pyx index 076236bff..1c8fb357a 100644 --- a/av/frame.pyx +++ b/av/frame.pyx @@ -55,7 +55,8 @@ cdef class Frame: self._time_base = dst - property dts: + @property + def dts(self): """ The decoding timestamp copied from the :class:`~av.packet.Packet` that triggered returning this frame in :attr:`time_base` units. @@ -63,18 +64,19 @@ cdef class Frame: :type: int """ - def __get__(self): - if self.ptr.pkt_dts == lib.AV_NOPTS_VALUE: - return None - return self.ptr.pkt_dts - - def __set__(self, value): - if value is None: - self.ptr.pkt_dts = lib.AV_NOPTS_VALUE - else: - self.ptr.pkt_dts = value - - property pts: + if self.ptr.pkt_dts == lib.AV_NOPTS_VALUE: + return None + return self.ptr.pkt_dts + + @dts.setter + def dts(self, value): + if value is None: + self.ptr.pkt_dts = lib.AV_NOPTS_VALUE + else: + self.ptr.pkt_dts = value + + @property + def pts(self): """ The presentation timestamp in :attr:`time_base` units for this frame. @@ -82,18 +84,19 @@ cdef class Frame: :type: int """ - def __get__(self): - if self.ptr.pts == lib.AV_NOPTS_VALUE: - return None - return self.ptr.pts - - def __set__(self, value): - if value is None: - self.ptr.pts = lib.AV_NOPTS_VALUE - else: - self.ptr.pts = value - - property time: + if self.ptr.pts == lib.AV_NOPTS_VALUE: + return None + return self.ptr.pts + + @pts.setter + def pts(self, value): + if value is None: + self.ptr.pts = lib.AV_NOPTS_VALUE + else: + self.ptr.pts = value + + @property + def time(self): """ The presentation time in seconds for this frame. @@ -101,33 +104,33 @@ cdef class Frame: :type: float """ - def __get__(self): - if self.ptr.pts == lib.AV_NOPTS_VALUE: - return None - else: - return float(self.ptr.pts) * self._time_base.num / self._time_base.den + if self.ptr.pts == lib.AV_NOPTS_VALUE: + return None + else: + return float(self.ptr.pts) * self._time_base.num / self._time_base.den - property time_base: + @property + def time_base(self): """ The unit of time (in fractional seconds) in which timestamps are expressed. :type: fractions.Fraction """ - def __get__(self): - if self._time_base.num: - return avrational_to_fraction(&self._time_base) + if self._time_base.num: + return avrational_to_fraction(&self._time_base) - def __set__(self, value): - to_avrational(value, &self._time_base) + @time_base.setter + def time_base(self, value): + to_avrational(value, &self._time_base) - property is_corrupt: + @property + def is_corrupt(self): """ Is this frame corrupt? :type: bool """ - def __get__(self): - return self.ptr.decode_error_flags != 0 or bool(self.ptr.flags & lib.AV_FRAME_FLAG_CORRUPT) + return self.ptr.decode_error_flags != 0 or bool(self.ptr.flags & lib.AV_FRAME_FLAG_CORRUPT) @property def side_data(self): diff --git a/av/option.pyx b/av/option.pyx index 5de7fa2a5..6ffba50fa 100644 --- a/av/option.pyx +++ b/av/option.pyx @@ -64,82 +64,82 @@ cdef class BaseOption: if sentinel is not _cinit_sentinel: raise RuntimeError(f"Cannot construct av.{self.__class__.__name__}") - property name: - def __get__(self): - return self.ptr.name + @property + def name(self): + return self.ptr.name - property help: - def __get__(self): - return self.ptr.help if self.ptr.help != NULL else "" + @property + def help(self): + return self.ptr.help if self.ptr.help != NULL else "" - property flags: - def __get__(self): - return self.ptr.flags + @property + def flags(self): + return self.ptr.flags # Option flags - property is_encoding_param: - def __get__(self): - return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_ENCODING_PARAM) - property is_decoding_param: - def __get__(self): - return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_DECODING_PARAM) - property is_audio_param: - def __get__(self): - return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_AUDIO_PARAM) - property is_video_param: - def __get__(self): - return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_VIDEO_PARAM) - property is_subtitle_param: - def __get__(self): - return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_SUBTITLE_PARAM) - property is_export: - def __get__(self): - return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_EXPORT) - property is_readonly: - def __get__(self): - return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_READONLY) - property is_filtering_param: - def __get__(self): - return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_FILTERING_PARAM) + @property + def is_encoding_param(self): + return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_ENCODING_PARAM) + @property + def is_decoding_param(self): + return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_DECODING_PARAM) + @property + def is_audio_param(self): + return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_AUDIO_PARAM) + @property + def is_video_param(self): + return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_VIDEO_PARAM) + @property + def is_subtitle_param(self): + return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_SUBTITLE_PARAM) + @property + def is_export(self): + return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_EXPORT) + @property + def is_readonly(self): + return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_READONLY) + @property + def is_filtering_param(self): + return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_FILTERING_PARAM) cdef class Option(BaseOption): - property type: - def __get__(self): - return OptionType._get(self.ptr.type, create=True) + @property + def type(self): + return OptionType._get(self.ptr.type, create=True) - property offset: + @property + def offset(self): """ This can be used to find aliases of an option. Options in a particular descriptor with the same offset are aliases. """ - def __get__(self): - return self.ptr.offset - - property default: - def __get__(self): - if self.ptr.type in _INT_TYPES: - return self.ptr.default_val.i64 - if self.ptr.type in (lib.AV_OPT_TYPE_DOUBLE, lib.AV_OPT_TYPE_FLOAT, - lib.AV_OPT_TYPE_RATIONAL): - return self.ptr.default_val.dbl - if self.ptr.type in (lib.AV_OPT_TYPE_STRING, lib.AV_OPT_TYPE_BINARY, - lib.AV_OPT_TYPE_IMAGE_SIZE, lib.AV_OPT_TYPE_VIDEO_RATE, - lib.AV_OPT_TYPE_COLOR): - return self.ptr.default_val.str if self.ptr.default_val.str != NULL else "" + return self.ptr.offset + + @property + def default(self): + if self.ptr.type in _INT_TYPES: + return self.ptr.default_val.i64 + if self.ptr.type in (lib.AV_OPT_TYPE_DOUBLE, lib.AV_OPT_TYPE_FLOAT, + lib.AV_OPT_TYPE_RATIONAL): + return self.ptr.default_val.dbl + if self.ptr.type in (lib.AV_OPT_TYPE_STRING, lib.AV_OPT_TYPE_BINARY, + lib.AV_OPT_TYPE_IMAGE_SIZE, lib.AV_OPT_TYPE_VIDEO_RATE, + lib.AV_OPT_TYPE_COLOR): + return self.ptr.default_val.str if self.ptr.default_val.str != NULL else "" def _norm_range(self, value): if self.ptr.type in _INT_TYPES: return int(value) return value - property min: - def __get__(self): - return self._norm_range(self.ptr.min) + @property + def min(self): + return self._norm_range(self.ptr.min) - property max: - def __get__(self): - return self._norm_range(self.ptr.max) + @property + def max(self): + return self._norm_range(self.ptr.max) def __repr__(self): return ( @@ -164,9 +164,9 @@ cdef class OptionChoice(BaseOption): choices of non-const option with same unit. """ - property value: - def __get__(self): - return self.ptr.default_val.i64 + @property + def value(self): + return self.ptr.default_val.i64 def __repr__(self): return f"" diff --git a/av/packet.pyx b/av/packet.pyx index f07343b56..fee1b05d2 100644 --- a/av/packet.pyx +++ b/av/packet.pyx @@ -94,34 +94,37 @@ cdef class Packet(Buffer): res = self._stream.decode(self) return res[0] if res else None - property stream_index: - def __get__(self): - return self.ptr.stream_index + @property + def stream_index(self): + return self.ptr.stream_index - property stream: + @property + def stream(self): """ The :class:`Stream` this packet was demuxed from. """ - def __get__(self): - return self._stream + return self._stream - def __set__(self, Stream stream): - self._stream = stream - self.ptr.stream_index = stream.ptr.index + @stream.setter + def stream(self, Stream stream): + self._stream = stream + self.ptr.stream_index = stream.ptr.index - property time_base: + @property + def time_base(self): """ The unit of time (in fractional seconds) in which timestamps are expressed. :type: fractions.Fraction """ - def __get__(self): - return avrational_to_fraction(&self._time_base) + return avrational_to_fraction(&self._time_base) - def __set__(self, value): - to_avrational(value, &self._time_base) + @time_base.setter + def time_base(self, value): + to_avrational(value, &self._time_base) - property pts: + @property + def pts(self): """ The presentation timestamp in :attr:`time_base` units for this packet. @@ -129,33 +132,35 @@ cdef class Packet(Buffer): :type: int """ - def __get__(self): - if self.ptr.pts != lib.AV_NOPTS_VALUE: - return self.ptr.pts + if self.ptr.pts != lib.AV_NOPTS_VALUE: + return self.ptr.pts - def __set__(self, v): - if v is None: - self.ptr.pts = lib.AV_NOPTS_VALUE - else: - self.ptr.pts = v + @pts.setter + def pts(self, v): + if v is None: + self.ptr.pts = lib.AV_NOPTS_VALUE + else: + self.ptr.pts = v - property dts: + @property + def dts(self): """ The decoding timestamp in :attr:`time_base` units for this packet. :type: int """ - def __get__(self): - if self.ptr.dts != lib.AV_NOPTS_VALUE: - return self.ptr.dts + if self.ptr.dts != lib.AV_NOPTS_VALUE: + return self.ptr.dts - def __set__(self, v): - if v is None: - self.ptr.dts = lib.AV_NOPTS_VALUE - else: - self.ptr.dts = v + @dts.setter + def dts(self, v): + if v is None: + self.ptr.dts = lib.AV_NOPTS_VALUE + else: + self.ptr.dts = v - property pos: + @property + def pos(self): """ The byte position of this packet within the :class:`.Stream`. @@ -163,20 +168,20 @@ cdef class Packet(Buffer): :type: int """ - def __get__(self): - if self.ptr.pos != -1: - return self.ptr.pos + if self.ptr.pos != -1: + return self.ptr.pos - property size: + @property + def size(self): """ The size in bytes of this packet's data. :type: int """ - def __get__(self): - return self.ptr.size + return self.ptr.size - property duration: + @property + def duration(self): """ The duration in :attr:`time_base` units for this packet. @@ -184,24 +189,26 @@ cdef class Packet(Buffer): :type: int """ - def __get__(self): - if self.ptr.duration != lib.AV_NOPTS_VALUE: - return self.ptr.duration + if self.ptr.duration != lib.AV_NOPTS_VALUE: + return self.ptr.duration - property is_keyframe: - def __get__(self): return bool(self.ptr.flags & lib.AV_PKT_FLAG_KEY) + @property + def is_keyframe(self): + return bool(self.ptr.flags & lib.AV_PKT_FLAG_KEY) - def __set__(self, v): - if v: - self.ptr.flags |= lib.AV_PKT_FLAG_KEY - else: - self.ptr.flags &= ~(lib.AV_PKT_FLAG_KEY) + @is_keyframe.setter + def is_keyframe(self, v): + if v: + self.ptr.flags |= lib.AV_PKT_FLAG_KEY + else: + self.ptr.flags &= ~(lib.AV_PKT_FLAG_KEY) - property is_corrupt: - def __get__(self): return bool(self.ptr.flags & lib.AV_PKT_FLAG_CORRUPT) + @property + def is_corrupt(self): return bool(self.ptr.flags & lib.AV_PKT_FLAG_CORRUPT) - def __set__(self, v): - if v: - self.ptr.flags |= lib.AV_PKT_FLAG_CORRUPT - else: - self.ptr.flags &= ~(lib.AV_PKT_FLAG_CORRUPT) + @is_corrupt.setter + def is_corrupt(self, v): + if v: + self.ptr.flags |= lib.AV_PKT_FLAG_CORRUPT + else: + self.ptr.flags &= ~(lib.AV_PKT_FLAG_CORRUPT) diff --git a/av/stream.pyx b/av/stream.pyx index 3f5bfcf01..6980f5d9a 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -193,15 +193,15 @@ cdef class Stream: return nb_side_data, side_data - property id: + @property + def id(self): """ The format-specific ID of this stream. :type: int """ - def __get__(self): - return self.ptr.id + return self.ptr.id cdef _set_id(self, value): """ @@ -212,35 +212,37 @@ cdef class Stream: else: self.ptr.id = value - property profile: + @property + def profile(self): """ The profile of this stream. :type: str """ - def __get__(self): - if self.codec_context: - return self.codec_context.profile - else: - return None + if self.codec_context: + return self.codec_context.profile + else: + return None - property index: + @property + def index(self): """ The index of this stream in its :class:`.Container`. :type: int """ - def __get__(self): return self.ptr.index + return self.ptr.index + - property time_base: + @property + def time_base(self): """ The unit of time (in fractional seconds) in which timestamps are expressed. :type: :class:`~fractions.Fraction` or ``None`` """ - def __get__(self): - return avrational_to_fraction(&self.ptr.time_base) + return avrational_to_fraction(&self.ptr.time_base) cdef _set_time_base(self, value): """ @@ -248,7 +250,8 @@ cdef class Stream: """ to_avrational(value, &self.ptr.time_base) - property average_rate: + @property + def average_rate(self): """ The average frame rate of this video stream. @@ -259,10 +262,10 @@ cdef class Stream: """ - def __get__(self): - return avrational_to_fraction(&self.ptr.avg_frame_rate) + return avrational_to_fraction(&self.ptr.avg_frame_rate) - property base_rate: + @property + def base_rate(self): """ The base frame rate of this stream. @@ -273,10 +276,10 @@ cdef class Stream: :type: :class:`~fractions.Fraction` or ``None`` """ - def __get__(self): - return avrational_to_fraction(&self.ptr.r_frame_rate) + return avrational_to_fraction(&self.ptr.r_frame_rate) - property guessed_rate: + @property + def guessed_rate(self): """The guessed frame rate of this stream. This is a wrapper around :ffmpeg:`av_guess_frame_rate`, and uses multiple @@ -285,34 +288,34 @@ cdef class Stream: :type: :class:`~fractions.Fraction` or ``None`` """ - def __get__(self): - # The two NULL arguments aren't used in FFmpeg >= 4.0 - cdef lib.AVRational val = lib.av_guess_frame_rate(NULL, self.ptr, NULL) - return avrational_to_fraction(&val) + # The two NULL arguments aren't used in FFmpeg >= 4.0 + cdef lib.AVRational val = lib.av_guess_frame_rate(NULL, self.ptr, NULL) + return avrational_to_fraction(&val) - property start_time: + @property + def start_time(self): """ The presentation timestamp in :attr:`time_base` units of the first frame in this stream. :type: :class:`int` or ``None`` """ - def __get__(self): - if self.ptr.start_time != lib.AV_NOPTS_VALUE: - return self.ptr.start_time + if self.ptr.start_time != lib.AV_NOPTS_VALUE: + return self.ptr.start_time - property duration: + @property + def duration(self): """ The duration of this stream in :attr:`time_base` units. :type: :class:`int` or ``None`` """ - def __get__(self): - if self.ptr.duration != lib.AV_NOPTS_VALUE: - return self.ptr.duration + if self.ptr.duration != lib.AV_NOPTS_VALUE: + return self.ptr.duration - property frames: + @property + def frames(self): """ The number of frames this stream contains. @@ -320,17 +323,16 @@ cdef class Stream: :type: :class:`int` """ - def __get__(self): - return self.ptr.nb_frames + return self.ptr.nb_frames - property language: + @property + def language(self): """ The language of the stream. :type: :class:`str` or ``None`` """ - def __get__(self): - return self.metadata.get('language') + return self.metadata.get('language') @property def type(self): From 92a47509ec5fe2fe1da2390325c548dbd642b794 Mon Sep 17 00:00:00 2001 From: JoeUgly <41972063+JoeUgly@users.noreply.github.com> Date: Thu, 22 Feb 2024 20:34:54 -0500 Subject: [PATCH 184/846] Convert deprecated cython extension class properties to new syntax part 2 --- av/audio/codeccontext.pyx | 79 +++++++++++++++++++----------------- av/audio/fifo.pyx | 24 +++++------ av/audio/format.pyx | 85 ++++++++++++++++++++------------------- av/audio/frame.pyx | 28 +++++++------ av/audio/layout.pyx | 24 +++++------ 5 files changed, 125 insertions(+), 115 deletions(-) diff --git a/av/audio/codeccontext.pyx b/av/audio/codeccontext.pyx index e8461156a..1978759ae 100644 --- a/av/audio/codeccontext.pyx +++ b/av/audio/codeccontext.pyx @@ -53,69 +53,76 @@ cdef class AudioCodecContext(CodecContext): cdef AudioFrame aframe = frame aframe._init_user_attributes() - property frame_size: + @property + def frame_size(self): """ Number of samples per channel in an audio frame. :type: int """ - def __get__(self): return self.ptr.frame_size + return self.ptr.frame_size - property sample_rate: + + @property + def sample_rate(self): """ Sample rate of the audio data, in samples per second. :type: int """ - def __get__(self): - return self.ptr.sample_rate + return self.ptr.sample_rate - def __set__(self, int value): - self.ptr.sample_rate = value + @sample_rate.setter + def sample_rate(self, int value): + self.ptr.sample_rate = value - property rate: + @property + def rate(self): """Another name for :attr:`sample_rate`.""" - def __get__(self): - return self.sample_rate + return self.sample_rate - def __set__(self, value): - self.sample_rate = value + @rate.setter + def rate(self, value): + self.sample_rate = value # TODO: Integrate into AudioLayout. - property channels: - def __get__(self): - return self.ptr.channels - - def __set__(self, value): - self.ptr.channels = value - self.ptr.channel_layout = lib.av_get_default_channel_layout(value) - property channel_layout: - def __get__(self): - return self.ptr.channel_layout - - property layout: + @property + def channels(self): + return self.ptr.channels + + @channels.setter + def channels(self, value): + self.ptr.channels = value + self.ptr.channel_layout = lib.av_get_default_channel_layout(value) + @property + def channel_layout(self): + return self.ptr.channel_layout + + @property + def layout(self): """ The audio channel layout. :type: AudioLayout """ - def __get__(self): - return get_audio_layout(self.ptr.channels, self.ptr.channel_layout) + return get_audio_layout(self.ptr.channels, self.ptr.channel_layout) - def __set__(self, value): - cdef AudioLayout layout = AudioLayout(value) - self.ptr.channel_layout = layout.layout - self.ptr.channels = layout.nb_channels + @layout.setter + def layout(self, value): + cdef AudioLayout layout = AudioLayout(value) + self.ptr.channel_layout = layout.layout + self.ptr.channels = layout.nb_channels - property format: + @property + def format(self): """ The audio sample format. :type: AudioFormat """ - def __get__(self): - return get_audio_format(self.ptr.sample_fmt) + return get_audio_format(self.ptr.sample_fmt) - def __set__(self, value): - cdef AudioFormat format = AudioFormat(value) - self.ptr.sample_fmt = format.sample_fmt + @format.setter + def format(self, value): + cdef AudioFormat format = AudioFormat(value) + self.ptr.sample_fmt = format.sample_fmt diff --git a/av/audio/fifo.pyx b/av/audio/fifo.pyx index 7bc4b0d48..83d9cc71d 100644 --- a/av/audio/fifo.pyx +++ b/av/audio/fifo.pyx @@ -173,19 +173,19 @@ cdef class AudioFifo: return frames - property format: + @property + def format(self): """The :class:`.AudioFormat` of this FIFO.""" - def __get__(self): - return self.template.format - property layout: + return self.template.format + @property + def layout(self): """The :class:`.AudioLayout` of this FIFO.""" - def __get__(self): - return self.template.layout - property sample_rate: - def __get__(self): - return self.template.sample_rate + return self.template.layout + @property + def sample_rate(self): + return self.template.sample_rate - property samples: + @property + def samples(self): """Number of audio samples (per channel) in the buffer.""" - def __get__(self): - return lib.av_audio_fifo_size(self.ptr) if self.ptr else 0 + return lib.av_audio_fifo_size(self.ptr) if self.ptr else 0 diff --git a/av/audio/format.pyx b/av/audio/format.pyx index 09b939221..608610781 100644 --- a/av/audio/format.pyx +++ b/av/audio/format.pyx @@ -41,70 +41,72 @@ cdef class AudioFormat: def __repr__(self): return f"" - property name: + @property + def name(self): """Canonical name of the sample format. >>> SampleFormat('s16p').name 's16p' """ - def __get__(self): - return lib.av_get_sample_fmt_name(self.sample_fmt) + return lib.av_get_sample_fmt_name(self.sample_fmt) - property bytes: + @property + def bytes(self): """Number of bytes per sample. >>> SampleFormat('s16p').bytes 2 """ - def __get__(self): - return lib.av_get_bytes_per_sample(self.sample_fmt) + return lib.av_get_bytes_per_sample(self.sample_fmt) - property bits: + @property + def bits(self): """Number of bits per sample. >>> SampleFormat('s16p').bits 16 """ - def __get__(self): - return lib.av_get_bytes_per_sample(self.sample_fmt) << 3 + return lib.av_get_bytes_per_sample(self.sample_fmt) << 3 - property is_planar: + @property + def is_planar(self): """Is this a planar format? Strictly opposite of :attr:`is_packed`. """ - def __get__(self): - return bool(lib.av_sample_fmt_is_planar(self.sample_fmt)) + return bool(lib.av_sample_fmt_is_planar(self.sample_fmt)) - property is_packed: + @property + def is_packed(self): """Is this a planar format? Strictly opposite of :attr:`is_planar`. """ - def __get__(self): - return not lib.av_sample_fmt_is_planar(self.sample_fmt) + return not lib.av_sample_fmt_is_planar(self.sample_fmt) - property planar: + @property + def planar(self): """The planar variant of this format. Is itself when planar: + >>> from av import AudioFormat as Format >>> fmt = Format('s16p') >>> fmt.planar is fmt True """ - def __get__(self): - if self.is_planar: - return self - return get_audio_format(lib.av_get_planar_sample_fmt(self.sample_fmt)) + if self.is_planar: + return self + return get_audio_format(lib.av_get_planar_sample_fmt(self.sample_fmt)) - property packed: + @property + def packed(self): """The packed variant of this format. Is itself when packed: @@ -114,30 +116,29 @@ cdef class AudioFormat: True """ - def __get__(self): - if self.is_packed: - return self - return get_audio_format(lib.av_get_packed_sample_fmt(self.sample_fmt)) + if self.is_packed: + return self + return get_audio_format(lib.av_get_packed_sample_fmt(self.sample_fmt)) - property container_name: + @property + def container_name(self): """The name of a :class:`ContainerFormat` which directly accepts this data. :raises ValueError: when planar, since there are no such containers. """ - def __get__(self): - if self.is_planar: - raise ValueError("no planar container formats") - - if self.sample_fmt == lib.AV_SAMPLE_FMT_U8: - return "u8" - elif self.sample_fmt == lib.AV_SAMPLE_FMT_S16: - return "s16" + container_format_postfix - elif self.sample_fmt == lib.AV_SAMPLE_FMT_S32: - return "s32" + container_format_postfix - elif self.sample_fmt == lib.AV_SAMPLE_FMT_FLT: - return "f32" + container_format_postfix - elif self.sample_fmt == lib.AV_SAMPLE_FMT_DBL: - return "f64" + container_format_postfix - - raise ValueError("unknown layout") + if self.is_planar: + raise ValueError("no planar container formats") + + if self.sample_fmt == lib.AV_SAMPLE_FMT_U8: + return "u8" + elif self.sample_fmt == lib.AV_SAMPLE_FMT_S16: + return "s16" + container_format_postfix + elif self.sample_fmt == lib.AV_SAMPLE_FMT_S32: + return "s32" + container_format_postfix + elif self.sample_fmt == lib.AV_SAMPLE_FMT_FLT: + return "f32" + container_format_postfix + elif self.sample_fmt == lib.AV_SAMPLE_FMT_DBL: + return "f64" + container_format_postfix + + raise ValueError("unknown layout") diff --git a/av/audio/frame.pyx b/av/audio/frame.pyx index a78b65fea..ac3638230 100644 --- a/av/audio/frame.pyx +++ b/av/audio/frame.pyx @@ -141,34 +141,36 @@ cdef class AudioFrame(Frame): return tuple([AudioPlane(self, i) for i in range(plane_count)]) - property samples: + @property + def samples(self): """ Number of audio samples (per channel). :type: int """ - def __get__(self): - return self.ptr.nb_samples + return self.ptr.nb_samples - property sample_rate: + @property + def sample_rate(self): """ Sample rate of the audio data, in samples per second. :type: int """ - def __get__(self): - return self.ptr.sample_rate + return self.ptr.sample_rate - def __set__(self, value): - self.ptr.sample_rate = value + @sample_rate.setter + def sample_rate(self, value): + self.ptr.sample_rate = value - property rate: + @property + def rate(self): """Another name for :attr:`sample_rate`.""" - def __get__(self): - return self.ptr.sample_rate + return self.ptr.sample_rate - def __set__(self, value): - self.ptr.sample_rate = value + @rate.setter + def rate(self, value): + self.ptr.sample_rate = value def to_ndarray(self, **kwargs): """Get a numpy array of this frame. diff --git a/av/audio/layout.pyx b/av/audio/layout.pyx index 6178482fd..79b4f3388 100644 --- a/av/audio/layout.pyx +++ b/av/audio/layout.pyx @@ -93,13 +93,13 @@ cdef class AudioLayout: def __repr__(self): return f"" - property name: + @property + def name(self): """The canonical name of the audio layout.""" - def __get__(self): - cdef char out[32] - # Passing 0 as number of channels... fix this later? - lib.av_get_channel_layout_string(out, 32, 0, self.layout) - return out + cdef char out[32] + # Passing 0 as number of channels... fix this later? + lib.av_get_channel_layout_string(out, 32, 0, self.layout) + return out cdef class AudioChannel: @@ -109,12 +109,12 @@ cdef class AudioChannel: def __repr__(self): return f"" - property name: + @property + def name(self): """The canonical name of the audio channel.""" - def __get__(self): - return lib.av_get_channel_name(self.channel) + return lib.av_get_channel_name(self.channel) - property description: + @property + def description(self): """A human description of the audio channel.""" - def __get__(self): - return channel_descriptions.get(self.name) + return channel_descriptions.get(self.name) From 44070568ab954c5d7d82ed9af9748dd64a06e694 Mon Sep 17 00:00:00 2001 From: JoeUgly <41972063+JoeUgly@users.noreply.github.com> Date: Fri, 23 Feb 2024 20:49:40 -0500 Subject: [PATCH 185/846] Convert deprecated cython extension class properties to new syntax part 3 --- av/data/stream.pyx | 12 ++--- av/filter/context.pyx | 32 +++++------ av/filter/filter.pyx | 110 +++++++++++++++++++------------------- av/filter/link.pyx | 60 ++++++++++----------- av/filter/pad.pyx | 40 +++++++------- av/subtitles/subtitle.pyx | 44 +++++++-------- 6 files changed, 149 insertions(+), 149 deletions(-) diff --git a/av/data/stream.pyx b/av/data/stream.pyx index 094a25704..e8d6624e2 100644 --- a/av/data/stream.pyx +++ b/av/data/stream.pyx @@ -15,9 +15,9 @@ cdef class DataStream(Stream): def decode(self, packet=None, count=0): return [] - property name: - def __get__(self): - cdef const lib.AVCodecDescriptor *desc = lib.avcodec_descriptor_get(self.ptr.codecpar.codec_id) - if desc == NULL: - return None - return desc.name + @property + def name(self): + cdef const lib.AVCodecDescriptor *desc = lib.avcodec_descriptor_get(self.ptr.codecpar.codec_id) + if desc == NULL: + return None + return desc.name diff --git a/av/filter/context.pyx b/av/filter/context.pyx index f36ce5290..54ed710ab 100644 --- a/av/filter/context.pyx +++ b/av/filter/context.pyx @@ -33,22 +33,22 @@ cdef class FilterContext: parent = self.filter.ptr.name if self.filter and self.filter.ptr != NULL else None return f"" - property name: - def __get__(self): - if self.ptr.name != NULL: - return self.ptr.name - - property inputs: - def __get__(self): - if self._inputs is None: - self._inputs = alloc_filter_pads(self.filter, self.ptr.input_pads, True, self) - return self._inputs - - property outputs: - def __get__(self): - if self._outputs is None: - self._outputs = alloc_filter_pads(self.filter, self.ptr.output_pads, False, self) - return self._outputs + @property + def name(self): + if self.ptr.name != NULL: + return self.ptr.name + + @property + def inputs(self): + if self._inputs is None: + self._inputs = alloc_filter_pads(self.filter, self.ptr.input_pads, True, self) + return self._inputs + + @property + def outputs(self): + if self._outputs is None: + self._outputs = alloc_filter_pads(self.filter, self.ptr.output_pads, False, self) + return self._outputs def init(self, args=None, **kwargs): if self.inited: diff --git a/av/filter/filter.pyx b/av/filter/filter.pyx index 22163bc55..d4880dc15 100644 --- a/av/filter/filter.pyx +++ b/av/filter/filter.pyx @@ -31,61 +31,61 @@ cdef class Filter: if not self.ptr: raise ValueError(f"no filter {name}") - property descriptor: - def __get__(self): - if self._descriptor is None: - self._descriptor = wrap_avclass(self.ptr.priv_class) - return self._descriptor - - property options: - def __get__(self): - if self.descriptor is None: - return - return self.descriptor.options - - property name: - def __get__(self): - return self.ptr.name - - property description: - def __get__(self): - return self.ptr.description - - property flags: - def __get__(self): - return self.ptr.flags - - property dynamic_inputs: - def __get__(self): - return bool(self.ptr.flags & lib.AVFILTER_FLAG_DYNAMIC_INPUTS) - - property dynamic_outputs: - def __get__(self): - return bool(self.ptr.flags & lib.AVFILTER_FLAG_DYNAMIC_OUTPUTS) - - property timeline_support: - def __get__(self): - return bool(self.ptr.flags & lib.AVFILTER_FLAG_SUPPORT_TIMELINE_GENERIC) - - property slice_threads: - def __get__(self): - return bool(self.ptr.flags & lib.AVFILTER_FLAG_SLICE_THREADS) - - property command_support: - def __get__(self): - return self.ptr.process_command != NULL - - property inputs: - def __get__(self): - if self._inputs is None: - self._inputs = alloc_filter_pads(self, self.ptr.inputs, True) - return self._inputs - - property outputs: - def __get__(self): - if self._outputs is None: - self._outputs = alloc_filter_pads(self, self.ptr.outputs, False) - return self._outputs + @property + def descriptor(self): + if self._descriptor is None: + self._descriptor = wrap_avclass(self.ptr.priv_class) + return self._descriptor + + @property + def options(self): + if self.descriptor is None: + return + return self.descriptor.options + + @property + def name(self): + return self.ptr.name + + @property + def description(self): + return self.ptr.description + + @property + def flags(self): + return self.ptr.flags + + @property + def dynamic_inputs(self): + return bool(self.ptr.flags & lib.AVFILTER_FLAG_DYNAMIC_INPUTS) + + @property + def dynamic_outputs(self): + return bool(self.ptr.flags & lib.AVFILTER_FLAG_DYNAMIC_OUTPUTS) + + @property + def timeline_support(self): + return bool(self.ptr.flags & lib.AVFILTER_FLAG_SUPPORT_TIMELINE_GENERIC) + + @property + def slice_threads(self): + return bool(self.ptr.flags & lib.AVFILTER_FLAG_SLICE_THREADS) + + @property + def command_support(self): + return self.ptr.process_command != NULL + + @property + def inputs(self): + if self._inputs is None: + self._inputs = alloc_filter_pads(self, self.ptr.inputs, True) + return self._inputs + + @property + def outputs(self): + if self._outputs is None: + self._outputs = alloc_filter_pads(self, self.ptr.outputs, False) + return self._outputs cdef get_filter_names(): diff --git a/av/filter/link.pyx b/av/filter/link.pyx index b9e28e082..78b7da30f 100644 --- a/av/filter/link.pyx +++ b/av/filter/link.pyx @@ -12,38 +12,38 @@ cdef class FilterLink: if sentinel is not _cinit_sentinel: raise RuntimeError("cannot instantiate FilterLink") - property input: - def __get__(self): - if self._input: - return self._input - cdef lib.AVFilterContext *cctx = self.ptr.src - cdef unsigned int i - for i in range(cctx.nb_outputs): - if self.ptr == cctx.outputs[i]: - break - else: - raise RuntimeError("could not find link in context") - ctx = self.graph._context_by_ptr[cctx] - self._input = ctx.outputs[i] + @property + def input(self): + if self._input: return self._input - - property output: - def __get__(self): - if self._output: - return self._output - cdef lib.AVFilterContext *cctx = self.ptr.dst - cdef unsigned int i - for i in range(cctx.nb_inputs): - if self.ptr == cctx.inputs[i]: - break - else: - raise RuntimeError("could not find link in context") - try: - ctx = self.graph._context_by_ptr[cctx] - except KeyError: - raise RuntimeError("could not find context in graph", (cctx.name, cctx.filter.name)) - self._output = ctx.inputs[i] + cdef lib.AVFilterContext *cctx = self.ptr.src + cdef unsigned int i + for i in range(cctx.nb_outputs): + if self.ptr == cctx.outputs[i]: + break + else: + raise RuntimeError("could not find link in context") + ctx = self.graph._context_by_ptr[cctx] + self._input = ctx.outputs[i] + return self._input + + @property + def output(self): + if self._output: return self._output + cdef lib.AVFilterContext *cctx = self.ptr.dst + cdef unsigned int i + for i in range(cctx.nb_inputs): + if self.ptr == cctx.inputs[i]: + break + else: + raise RuntimeError("could not find link in context") + try: + ctx = self.graph._context_by_ptr[cctx] + except KeyError: + raise RuntimeError("could not find context in graph", (cctx.name, cctx.filter.name)) + self._output = ctx.inputs[i] + return self._output cdef FilterLink wrap_filter_link(Graph graph, lib.AVFilterLink *ptr): diff --git a/av/filter/pad.pyx b/av/filter/pad.pyx index d12238cc5..873b31b04 100644 --- a/av/filter/pad.pyx +++ b/av/filter/pad.pyx @@ -15,13 +15,13 @@ cdef class FilterPad: return f"" - property is_output: - def __get__(self): - return not self.is_input + @property + def is_output(self): + return not self.is_input - property name: - def __get__(self): - return lib.avfilter_pad_get_name(self.base_ptr, self.index) + @property + def name(self): + return lib.avfilter_pad_get_name(self.base_ptr, self.index) @property def type(self): @@ -43,22 +43,22 @@ cdef class FilterContextPad(FilterPad): return f"" - property link: - def __get__(self): - if self._link: - return self._link - cdef lib.AVFilterLink **links = self.context.ptr.inputs if self.is_input else self.context.ptr.outputs - cdef lib.AVFilterLink *link = links[self.index] - if not link: - return - self._link = wrap_filter_link(self.context.graph, link) + @property + def link(self): + if self._link: return self._link + cdef lib.AVFilterLink **links = self.context.ptr.inputs if self.is_input else self.context.ptr.outputs + cdef lib.AVFilterLink *link = links[self.index] + if not link: + return + self._link = wrap_filter_link(self.context.graph, link) + return self._link - property linked: - def __get__(self): - cdef FilterLink link = self.link - if link: - return link.input if self.is_input else link.output + @property + def linked(self): + cdef FilterLink link = self.link + if link: + return link.input if self.is_input else link.output cdef tuple alloc_filter_pads(Filter filter, const lib.AVFilterPad *ptr, bint is_input, FilterContext context=None): diff --git a/av/subtitles/subtitle.pyx b/av/subtitles/subtitle.pyx index 5d6905cd8..9e98f01aa 100644 --- a/av/subtitles/subtitle.pyx +++ b/av/subtitles/subtitle.pyx @@ -20,14 +20,14 @@ cdef class SubtitleSet: id(self), ) - property format: - def __get__(self): return self.proxy.struct.format - property start_display_time: - def __get__(self): return self.proxy.struct.start_display_time - property end_display_time: - def __get__(self): return self.proxy.struct.end_display_time - property pts: - def __get__(self): return self.proxy.struct.pts + @property + def format(self): return self.proxy.struct.format + @property + def start_display_time(self): return self.proxy.struct.start_display_time + @property + def end_display_time(self): return self.proxy.struct.end_display_time + @property + def pts(self): return self.proxy.struct.pts def __len__(self): return len(self.rects) @@ -110,16 +110,16 @@ cdef class BitmapSubtitle(Subtitle): id(self), ) - property x: - def __get__(self): return self.ptr.x - property y: - def __get__(self): return self.ptr.y - property width: - def __get__(self): return self.ptr.w - property height: - def __get__(self): return self.ptr.h - property nb_colors: - def __get__(self): return self.ptr.nb_colors + @property + def x(self): return self.ptr.x + @property + def y(self): return self.ptr.y + @property + def width(self): return self.ptr.w + @property + def height(self): return self.ptr.h + @property + def nb_colors(self): return self.ptr.nb_colors def __len__(self): return len(self.planes) @@ -161,8 +161,8 @@ cdef class TextSubtitle(Subtitle): id(self), ) - property text: - def __get__(self): return self.ptr.text + @property + def text(self): return self.ptr.text cdef class AssSubtitle(Subtitle): @@ -175,5 +175,5 @@ cdef class AssSubtitle(Subtitle): id(self), ) - property ass: - def __get__(self): return self.ptr.ass + @property + def ass(self): return self.ptr.ass From 1e263c07c20aa0fe8b534ee7bcc4bde6752c5b2c Mon Sep 17 00:00:00 2001 From: JoeUgly <41972063+JoeUgly@users.noreply.github.com> Date: Sat, 24 Feb 2024 20:52:41 -0500 Subject: [PATCH 186/846] Convert deprecated cython extension class properties to new syntax part 4 --- av/codec/codec.pyx | 22 ++-- av/codec/context.pyx | 266 +++++++++++++++++++------------------- av/container/input.pyx | 42 +++--- av/video/codeccontext.pyx | 193 ++++++++++++++------------- av/video/format.pyx | 99 +++++++------- av/video/frame.pyx | 24 ++-- av/video/plane.pyx | 6 +- tests/test_colorspace.py | 2 +- 8 files changed, 343 insertions(+), 311 deletions(-) diff --git a/av/codec/codec.pyx b/av/codec/codec.pyx index 8d8e2d9e1..b350a8b4f 100644 --- a/av/codec/codec.pyx +++ b/av/codec/codec.pyx @@ -209,17 +209,17 @@ cdef class Codec: from .context import CodecContext return CodecContext.create(self) - property is_decoder: - def __get__(self): - return not self.is_encoder + @property + def is_decoder(self): + return not self.is_encoder - property descriptor: - def __get__(self): return wrap_avclass(self.ptr.priv_class) + @property + def descriptor(self): return wrap_avclass(self.ptr.priv_class) - property name: - def __get__(self): return self.ptr.name or "" - property long_name: - def __get__(self): return self.ptr.long_name or "" + @property + def name(self): return self.ptr.name or "" + @property + def long_name(self): return self.ptr.long_name or "" @property def type(self): @@ -231,8 +231,8 @@ cdef class Codec: """ return lib.av_get_media_type_string(self.ptr.type) - property id: - def __get__(self): return self.ptr.id + @property + def id(self): return self.ptr.id @property def frame_rates(self): diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 8930c1c56..638ff65a8 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -215,44 +215,45 @@ cdef class CodecContext: skip_manual = flags2.flag_property("SKIP_MANUAL") ro_flush_noop = flags2.flag_property("RO_FLUSH_NOOP") - property extradata: - def __get__(self): - if self.ptr.extradata_size > 0: - return (self.ptr.extradata)[:self.ptr.extradata_size] - else: - return None - - def __set__(self, data): - if not self.is_decoder: - raise ValueError("Can only set extradata for decoders.") - - if data is None: - lib.av_freep(&self.ptr.extradata) - self.ptr.extradata_size = 0 - else: - source = bytesource(data) - self.ptr.extradata = lib.av_realloc(self.ptr.extradata, source.length + lib.AV_INPUT_BUFFER_PADDING_SIZE) - if not self.ptr.extradata: - raise MemoryError("Cannot allocate extradata") - memcpy(self.ptr.extradata, source.ptr, source.length) - self.ptr.extradata_size = source.length - self.extradata_set = True - - property extradata_size: - def __get__(self): - return self.ptr.extradata_size - - property is_open: - def __get__(self): - return lib.avcodec_is_open(self.ptr) - - property is_encoder: - def __get__(self): - return lib.av_codec_is_encoder(self.ptr.codec) - - property is_decoder: - def __get__(self): - return lib.av_codec_is_decoder(self.ptr.codec) + @property + def extradata(self): + if self.ptr.extradata_size > 0: + return (self.ptr.extradata)[:self.ptr.extradata_size] + else: + return None + + @extradata.setter + def extradata(self, data): + if not self.is_decoder: + raise ValueError("Can only set extradata for decoders.") + + if data is None: + lib.av_freep(&self.ptr.extradata) + self.ptr.extradata_size = 0 + else: + source = bytesource(data) + self.ptr.extradata = lib.av_realloc(self.ptr.extradata, source.length + lib.AV_INPUT_BUFFER_PADDING_SIZE) + if not self.ptr.extradata: + raise MemoryError("Cannot allocate extradata") + memcpy(self.ptr.extradata, source.ptr, source.length) + self.ptr.extradata_size = source.length + self.extradata_set = True + + @property + def extradata_size(self): + return self.ptr.extradata_size + + @property + def is_open(self): + return lib.avcodec_is_open(self.ptr) + + @property + def is_encoder(self): + return lib.av_codec_is_encoder(self.ptr.codec) + + @property + def is_decoder(self): + return lib.av_codec_is_decoder(self.ptr.codec) cpdef open(self, bint strict=True): if lib.avcodec_is_open(self.ptr): @@ -521,122 +522,125 @@ cdef class CodecContext: frame.index = self.ptr.frame_number - 1 - property name: - def __get__(self): - return self.codec.name - - property type: - def __get__(self): - return self.codec.type - - property profile: - def __get__(self): - if self.ptr.codec and lib.av_get_profile_name(self.ptr.codec, self.ptr.profile): - return lib.av_get_profile_name(self.ptr.codec, self.ptr.profile) - - property time_base: - def __get__(self): - if self.is_decoder: - warnings.warn( - "Using CodecContext.time_base for decoders is deprecated.", - AVDeprecationWarning - ) - return avrational_to_fraction(&self.ptr.time_base) - - def __set__(self, value): - if self.is_decoder: - warnings.warn( - "Using CodecContext.time_base for decoders is deprecated.", - AVDeprecationWarning - ) - to_avrational(value, &self.ptr.time_base) - - property codec_tag: - def __get__(self): - return self.ptr.codec_tag.to_bytes(4, byteorder="little", signed=False).decode( - encoding="ascii") - - def __set__(self, value): - if isinstance(value, str) and len(value) == 4: - self.ptr.codec_tag = int.from_bytes(value.encode(encoding="ascii"), - byteorder="little", signed=False) - else: - raise ValueError("Codec tag should be a 4 character string.") - - property ticks_per_frame: - def __get__(self): - return self.ptr.ticks_per_frame - - property bit_rate: - def __get__(self): - return self.ptr.bit_rate if self.ptr.bit_rate > 0 else None - - def __set__(self, int value): - self.ptr.bit_rate = value - - property max_bit_rate: - def __get__(self): - if self.ptr.rc_max_rate > 0: - return self.ptr.rc_max_rate - else: - return None - - property bit_rate_tolerance: - def __get__(self): - self.ptr.bit_rate_tolerance - - def __set__(self, int value): - self.ptr.bit_rate_tolerance = value - - property thread_count: + @property + def name(self): + return self.codec.name + + @property + def type(self): + return self.codec.type + + @property + def profile(self): + if self.ptr.codec and lib.av_get_profile_name(self.ptr.codec, self.ptr.profile): + return lib.av_get_profile_name(self.ptr.codec, self.ptr.profile) + + @property + def time_base(self): + if self.is_decoder: + warnings.warn( + "Using CodecContext.time_base for decoders is deprecated.", + AVDeprecationWarning + ) + return avrational_to_fraction(&self.ptr.time_base) + + @time_base.setter + def time_base(self, value): + if self.is_decoder: + warnings.warn( + "Using CodecContext.time_base for decoders is deprecated.", + AVDeprecationWarning + ) + to_avrational(value, &self.ptr.time_base) + + @property + def codec_tag(self): + return self.ptr.codec_tag.to_bytes(4, byteorder="little", signed=False).decode( + encoding="ascii") + + @codec_tag.setter + def codec_tag(self, value): + if isinstance(value, str) and len(value) == 4: + self.ptr.codec_tag = int.from_bytes(value.encode(encoding="ascii"), + byteorder="little", signed=False) + else: + raise ValueError("Codec tag should be a 4 character string.") + + @property + def ticks_per_frame(self): + return self.ptr.ticks_per_frame + + @property + def bit_rate(self): + return self.ptr.bit_rate if self.ptr.bit_rate > 0 else None + + @bit_rate.setter + def bit_rate(self, int value): + self.ptr.bit_rate = value + + @property + def max_bit_rate(self): + if self.ptr.rc_max_rate > 0: + return self.ptr.rc_max_rate + else: + return None + + @property + def bit_rate_tolerance(self): + self.ptr.bit_rate_tolerance + + @bit_rate_tolerance.setter + def bit_rate_tolerance(self, int value): + self.ptr.bit_rate_tolerance = value + + @property + def thread_count(self): """How many threads to use; 0 means auto. Wraps :ffmpeg:`AVCodecContext.thread_count`. """ + return self.ptr.thread_count - def __get__(self): - return self.ptr.thread_count - - def __set__(self, int value): - if lib.avcodec_is_open(self.ptr): - raise RuntimeError("Cannot change thread_count after codec is open.") - self.ptr.thread_count = value + @thread_count.setter + def thread_count(self, int value): + if lib.avcodec_is_open(self.ptr): + raise RuntimeError("Cannot change thread_count after codec is open.") + self.ptr.thread_count = value - property thread_type: + @property + def thread_type(self): """One of :class:`.ThreadType`. Wraps :ffmpeg:`AVCodecContext.thread_type`. """ + return ThreadType.get(self.ptr.thread_type, create=True) - def __get__(self): - return ThreadType.get(self.ptr.thread_type, create=True) - - def __set__(self, value): - if lib.avcodec_is_open(self.ptr): - raise RuntimeError("Cannot change thread_type after codec is open.") - self.ptr.thread_type = ThreadType[value].value + @thread_type.setter + def thread_type(self, value): + if lib.avcodec_is_open(self.ptr): + raise RuntimeError("Cannot change thread_type after codec is open.") + self.ptr.thread_type = ThreadType[value].value - property skip_frame: + @property + def skip_frame(self): """One of :class:`.SkipType`. Wraps ffmpeg:`AVCodecContext.skip_frame`. """ + return SkipType._get(self.ptr.skip_frame, create=True) - def __get__(self): - return SkipType._get(self.ptr.skip_frame, create=True) - - def __set__(self, value): - self.ptr.skip_frame = SkipType[value].value + @skip_frame.setter + def skip_frame(self, value): + self.ptr.skip_frame = SkipType[value].value - property delay: + @property + def delay(self): """Codec delay. Wraps :ffmpeg:`AVCodecContext.delay`. """ - - def __get__(self): - return self.ptr.delay + return self.ptr.delay diff --git a/av/container/input.pyx b/av/container/input.pyx index 56477afba..28867cf9b 100644 --- a/av/container/input.pyx +++ b/av/container/input.pyx @@ -87,27 +87,27 @@ cdef class InputContainer(Container): def __dealloc__(self): close_input(self) - property start_time: - def __get__(self): - self._assert_open() - if self.ptr.start_time != lib.AV_NOPTS_VALUE: - return self.ptr.start_time - - property duration: - def __get__(self): - self._assert_open() - if self.ptr.duration != lib.AV_NOPTS_VALUE: - return self.ptr.duration - - property bit_rate: - def __get__(self): - self._assert_open() - return self.ptr.bit_rate - - property size: - def __get__(self): - self._assert_open() - return lib.avio_size(self.ptr.pb) + @property + def start_time(self): + self._assert_open() + if self.ptr.start_time != lib.AV_NOPTS_VALUE: + return self.ptr.start_time + + @property + def duration(self): + self._assert_open() + if self.ptr.duration != lib.AV_NOPTS_VALUE: + return self.ptr.duration + + @property + def bit_rate(self): + self._assert_open() + return self.ptr.bit_rate + + @property + def size(self): + self._assert_open() + return lib.avio_size(self.ptr.pb) def close(self): close_input(self) diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.pyx index 0c2ac097a..70bc36d3f 100644 --- a/av/video/codeccontext.pyx +++ b/av/video/codeccontext.pyx @@ -70,116 +70,124 @@ cdef class VideoCodecContext(CodecContext): cdef _build_format(self): self._format = get_video_format(self.ptr.pix_fmt, self.ptr.width, self.ptr.height) - property format: - def __get__(self): - return self._format + @property + def format(self): + return self._format - def __set__(self, VideoFormat format): - self.ptr.pix_fmt = format.pix_fmt - self.ptr.width = format.width - self.ptr.height = format.height - self._build_format() # Kinda wasteful. + @format.setter + def format(self, VideoFormat format): + self.ptr.pix_fmt = format.pix_fmt + self.ptr.width = format.width + self.ptr.height = format.height + self._build_format() # Kinda wasteful. - property width: - def __get__(self): - return self.ptr.width + @property + def width(self): + return self.ptr.width - def __set__(self, unsigned int value): - self.ptr.width = value - self._build_format() + @width.setter + def width(self, unsigned int value): + self.ptr.width = value + self._build_format() - property height: - def __get__(self): - return self.ptr.height + @property + def height(self): + return self.ptr.height - def __set__(self, unsigned int value): - self.ptr.height = value - self._build_format() + @height.setter + def height(self, unsigned int value): + self.ptr.height = value + self._build_format() - property pix_fmt: + @property + def pix_fmt(self): """ The pixel format's name. :type: str """ - def __get__(self): - return self._format.name + return self._format.name - def __set__(self, value): - self.ptr.pix_fmt = get_pix_fmt(value) - self._build_format() + @pix_fmt.setter + def pix_fmt(self, value): + self.ptr.pix_fmt = get_pix_fmt(value) + self._build_format() - property framerate: + @property + def framerate(self): """ The frame rate, in frames per second. :type: fractions.Fraction """ - def __get__(self): - return avrational_to_fraction(&self.ptr.framerate) + return avrational_to_fraction(&self.ptr.framerate) - def __set__(self, value): - to_avrational(value, &self.ptr.framerate) + @framerate.setter + def framerate(self, value): + to_avrational(value, &self.ptr.framerate) - property rate: + @property + def rate(self): """Another name for :attr:`framerate`.""" - def __get__(self): - return self.framerate + return self.framerate - def __set__(self, value): - self.framerate = value + @rate.setter + def rate(self, value): + self.framerate = value - property gop_size: + @property + def gop_size(self): """ Sets the number of frames between keyframes. Used only for encoding. :type: int """ - def __get__(self): - if self.is_decoder: - warnings.warn( - "Using VideoCodecContext.gop_size for decoders is deprecated.", - AVDeprecationWarning - ) - return self.ptr.gop_size - - def __set__(self, int value): - if self.is_decoder: - warnings.warn( - "Using VideoCodecContext.gop_size for decoders is deprecated.", - AVDeprecationWarning - ) - self.ptr.gop_size = value - - property sample_aspect_ratio: - def __get__(self): - return avrational_to_fraction(&self.ptr.sample_aspect_ratio) - - def __set__(self, value): - to_avrational(value, &self.ptr.sample_aspect_ratio) - - property display_aspect_ratio: - def __get__(self): - cdef lib.AVRational dar - - lib.av_reduce( - &dar.num, &dar.den, - self.ptr.width * self.ptr.sample_aspect_ratio.num, - self.ptr.height * self.ptr.sample_aspect_ratio.den, 1024*1024) - - return avrational_to_fraction(&dar) - - property has_b_frames: - def __get__(self): - return bool(self.ptr.has_b_frames) - - property coded_width: - def __get__(self): - return self.ptr.coded_width - - property coded_height: - def __get__(self): - return self.ptr.coded_height + if self.is_decoder: + warnings.warn( + "Using VideoCodecContext.gop_size for decoders is deprecated.", + AVDeprecationWarning + ) + return self.ptr.gop_size + + @gop_size.setter + def gop_size(self, int value): + if self.is_decoder: + warnings.warn( + "Using VideoCodecContext.gop_size for decoders is deprecated.", + AVDeprecationWarning + ) + self.ptr.gop_size = value + + @property + def sample_aspect_ratio(self): + return avrational_to_fraction(&self.ptr.sample_aspect_ratio) + + @sample_aspect_ratio.setter + def sample_aspect_ratio(self, value): + to_avrational(value, &self.ptr.sample_aspect_ratio) + + @property + def display_aspect_ratio(self): + cdef lib.AVRational dar + + lib.av_reduce( + &dar.num, &dar.den, + self.ptr.width * self.ptr.sample_aspect_ratio.num, + self.ptr.height * self.ptr.sample_aspect_ratio.den, 1024*1024) + + return avrational_to_fraction(&dar) + + @property + def has_b_frames(self): + return bool(self.ptr.has_b_frames) + + @property + def coded_width(self): + return self.ptr.coded_width + + @property + def coded_height(self): + return self.ptr.coded_height @property def color_range(self): @@ -188,20 +196,21 @@ cdef class VideoCodecContext(CodecContext): Wraps :ffmpeg:`AVFrame.color_range`. """ - def __get__(self): - return self.ptr.color_range + return self.ptr.color_range - def __set__(self, value): - self.ptr.color_range = value + @color_range.setter + def color_range(self, value): + self.ptr.color_range = value - property max_b_frames: + @property + def max_b_frames(self): """ - The maximum run of consecutive B frames when encoding a video. + The maximum run of consecutive B frames when encoding a video. :type: int """ - def __get__(self): - return self.ptr.max_b_frames + return self.ptr.max_b_frames - def __set__(self, value): - self.ptr.max_b_frames = value + @max_b_frames.setter + def max_b_frames(self, value): + self.ptr.max_b_frames = value diff --git a/av/video/format.pyx b/av/video/format.pyx index 853472cf7..c9fac8e90 100644 --- a/av/video/format.pyx +++ b/av/video/format.pyx @@ -60,39 +60,50 @@ cdef class VideoFormat: def __int__(self): return int(self.pix_fmt) - property name: + @property + def name(self): """Canonical name of the pixel format.""" - def __get__(self): - return self.ptr.name + return self.ptr.name - property bits_per_pixel: - def __get__(self): return lib.av_get_bits_per_pixel(self.ptr) + @property + def bits_per_pixel(self): + return lib.av_get_bits_per_pixel(self.ptr) - property padded_bits_per_pixel: - def __get__(self): return lib.av_get_padded_bits_per_pixel(self.ptr) + @property + def padded_bits_per_pixel(self): return lib.av_get_padded_bits_per_pixel(self.ptr) - property is_big_endian: + @property + def is_big_endian(self): """Pixel format is big-endian.""" - def __get__(self): return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_BE) + return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_BE) - property has_palette: + + @property + def has_palette(self): """Pixel format has a palette in data[1], values are indexes in this palette.""" - def __get__(self): return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_PAL) + return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_PAL) + - property is_bit_stream: + @property + def is_bit_stream(self): """All values of a component are bit-wise packed end to end.""" - def __get__(self): return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_BITSTREAM) + return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_BITSTREAM) + # Skipping PIX_FMT_HWACCEL # """Pixel format is an HW accelerated format.""" - property is_planar: + @property + def is_planar(self): """At least one pixel component is not in the first data plane.""" - def __get__(self): return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_PLANAR) + return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_PLANAR) + - property is_rgb: + @property + def is_rgb(self): """The pixel format contains RGB-like data (as opposed to YUV/grayscale).""" - def __get__(self): return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_RGB) + return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_RGB) + cpdef chroma_width(self, int luma_width=0): """chroma_width(luma_width=0) @@ -124,53 +135,53 @@ cdef class VideoFormatComponent: self.index = index self.ptr = &format.ptr.comp[index] - property plane: + @property + def plane(self): """The index of the plane which contains this component.""" - def __get__(self): - return self.ptr.plane + return self.ptr.plane - property bits: + @property + def bits(self): """Number of bits in the component.""" - def __get__(self): - return self.ptr.depth + return self.ptr.depth - property is_alpha: + @property + def is_alpha(self): """Is this component an alpha channel?""" - def __get__(self): - return ((self.index == 1 and self.format.ptr.nb_components == 2) or - (self.index == 3 and self.format.ptr.nb_components == 4)) + return ((self.index == 1 and self.format.ptr.nb_components == 2) or + (self.index == 3 and self.format.ptr.nb_components == 4)) - property is_luma: + @property + def is_luma(self): """Is this compoment a luma channel?""" - def __get__(self): - return self.index == 0 and ( - self.format.ptr.nb_components == 1 or - self.format.ptr.nb_components == 2 or - not self.format.is_rgb - ) - - property is_chroma: + return self.index == 0 and ( + self.format.ptr.nb_components == 1 or + self.format.ptr.nb_components == 2 or + not self.format.is_rgb + ) + + @property + def is_chroma(self): """Is this component a chroma channel?""" - def __get__(self): - return (self.index == 1 or self.index == 2) and (self.format.ptr.log2_chroma_w or self.format.ptr.log2_chroma_h) + return (self.index == 1 or self.index == 2) and (self.format.ptr.log2_chroma_w or self.format.ptr.log2_chroma_h) - property width: + @property + def width(self): """The width of this component's plane. Requires the parent :class:`VideoFormat` to have a width. """ - def __get__(self): - return self.format.chroma_width() if self.is_chroma else self.format.width + return self.format.chroma_width() if self.is_chroma else self.format.width - property height: + @property + def height(self): """The height of this component's plane. Requires the parent :class:`VideoFormat` to have a height. """ - def __get__(self): - return self.format.chroma_height() if self.is_chroma else self.format.height + return self.format.chroma_height() if self.is_chroma else self.format.height names = set() diff --git a/av/video/frame.pyx b/av/video/frame.pyx index a4aba9ff5..174fb814b 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -158,29 +158,37 @@ cdef class VideoFrame(Frame): plane_count += 1 return tuple([VideoPlane(self, i) for i in range(plane_count)]) - property width: + @property + def width(self): """Width of the image, in pixels.""" - def __get__(self): return self.ptr.width + return self.ptr.width + - property height: + @property + def height(self): """Height of the image, in pixels.""" - def __get__(self): return self.ptr.height + return self.ptr.height + - property key_frame: + @property + def key_frame(self): """Is this frame a key frame? Wraps :ffmpeg:`AVFrame.key_frame`. """ - def __get__(self): return self.ptr.key_frame + return self.ptr.key_frame + - property interlaced_frame: + @property + def interlaced_frame(self): """Is this frame an interlaced or progressive? Wraps :ffmpeg:`AVFrame.interlaced_frame`. """ - def __get__(self): return self.ptr.interlaced_frame + return self.ptr.interlaced_frame + @property def pict_type(self): diff --git a/av/video/plane.pyx b/av/video/plane.pyx index de8959e67..5b5760b2b 100644 --- a/av/video/plane.pyx +++ b/av/video/plane.pyx @@ -27,14 +27,14 @@ cdef class VideoPlane(Plane): cdef size_t _buffer_size(self): return self.buffer_size - property line_size: + @property + def line_size(self): """ Bytes per horizontal line in this plane. :type: int """ - def __get__(self): - return self.frame.ptr.linesize[self.index] + return self.frame.ptr.linesize[self.index] cdef class YUVPlanes(VideoPlane): diff --git a/tests/test_colorspace.py b/tests/test_colorspace.py index 13784fec3..fc060eb74 100644 --- a/tests/test_colorspace.py +++ b/tests/test_colorspace.py @@ -11,7 +11,7 @@ def test_color_range(self): ) stream = container.streams.video[0] - self.assertEqual(stream.color_range, None) + self.assertEqual(stream.codec_context.color_range, 2) for packet in container.demux(stream): for frame in packet.decode(): From 75a19da0ba2b4d9a7e7ffde08d594d6d54306831 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 25 Feb 2024 00:33:21 -0500 Subject: [PATCH 187/846] Don't use editorconfig --- tests/requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/requirements.txt b/tests/requirements.txt index 8c1e633a7..71a7d58fb 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,7 +1,6 @@ autopep8 black cython -editorconfig flake8 isort numpy From 01a0cd8acf23f1d01410048232c38303a24ff454 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 25 Feb 2024 00:41:18 -0500 Subject: [PATCH 188/846] Add disclaimer for source installs --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b4dedceed..2559d9b41 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ If you want to use your existing FFmpeg, the source version of PyAV is on [PyPI] pip install av --no-binary av ``` +Installing from source is not supported on Windows. + Alternative installation methods -------------------------------- @@ -40,10 +42,10 @@ conda install av -c conda-forge See the [Conda install][conda-install] docs to get started with (mini)Conda. -And if you want to build from the absolute source (for development or testing): +And if you want to build from the absolute source (POSIX only): ```bash -git clone git@github.com:PyAV-Org/PyAV +git clone https://github.com/PyAV-Org/PyAV.git cd PyAV source scripts/activate.sh @@ -54,6 +56,7 @@ pip install --upgrade -r tests/requirements.txt # Build PyAV. make +pip install . ``` --- From d432bb24a49e92cc7025da9b0a6627c928655187 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 25 Feb 2024 01:38:53 -0500 Subject: [PATCH 189/846] Partially revert #1061 The YUV fastpath did not preserve the underlying byte-order, which causes issues for applications that rely on it. --- av/video/frame.pyx | 22 ++++++---------------- av/video/plane.pxd | 4 ---- av/video/plane.pyx | 17 ----------------- 3 files changed, 6 insertions(+), 37 deletions(-) diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 174fb814b..60a2addf2 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -6,7 +6,7 @@ from av.enum cimport define_enum from av.error cimport err_check from av.utils cimport check_ndarray, check_ndarray_shape from av.video.format cimport get_pix_fmt, get_video_format -from av.video.plane cimport VideoPlane, YUVPlanes +from av.video.plane cimport VideoPlane import warnings @@ -303,21 +303,11 @@ cdef class VideoFrame(Frame): if frame.format.name in ("yuv420p", "yuvj420p"): assert frame.width % 2 == 0 assert frame.height % 2 == 0 - # Fast path for the case that the entire YUV data is contiguous - if ( - frame.planes[0].line_size == frame.planes[0].width and - frame.planes[1].line_size == frame.planes[1].width and - frame.planes[2].line_size == frame.planes[2].width - ): - yuv_planes = YUVPlanes(frame, 0) - return useful_array(yuv_planes).reshape(frame.height * 3 // 2, frame.width) - else: - # Otherwise, we need to copy the data through the use of np.hstack - return np.hstack(( - useful_array(frame.planes[0]), - useful_array(frame.planes[1]), - useful_array(frame.planes[2]) - )).reshape(-1, frame.width) + return np.hstack(( + useful_array(frame.planes[0]), + useful_array(frame.planes[1]), + useful_array(frame.planes[2]) + )).reshape(-1, frame.width) elif frame.format.name in ("yuv444p", "yuvj444p"): return np.hstack(( useful_array(frame.planes[0]), diff --git a/av/video/plane.pxd b/av/video/plane.pxd index a74eea206..f9abf22b6 100644 --- a/av/video/plane.pxd +++ b/av/video/plane.pxd @@ -6,7 +6,3 @@ cdef class VideoPlane(Plane): cdef readonly size_t buffer_size cdef readonly unsigned int width, height - - -cdef class YUVPlanes(VideoPlane): - pass diff --git a/av/video/plane.pyx b/av/video/plane.pyx index 5b5760b2b..6bf03b1bc 100644 --- a/av/video/plane.pyx +++ b/av/video/plane.pyx @@ -35,20 +35,3 @@ cdef class VideoPlane(Plane): :type: int """ return self.frame.ptr.linesize[self.index] - - -cdef class YUVPlanes(VideoPlane): - def __cinit__(self, VideoFrame frame, int index): - if index != 0: - raise RuntimeError("YUVPlanes only supports index 0") - if frame.format.name not in ("yuvj420p", "yuv420p"): - raise RuntimeError("YUVPlane only supports yuv420p and yuvj420p") - if frame.ptr.linesize[0] < 0: - raise RuntimeError("YUVPlane only supports positive linesize") - self.width = frame.width - self.height = frame.height * 3 // 2 - self.buffer_size = self.height * abs(self.frame.ptr.linesize[0]) - self.frame = frame - - cdef void* _buffer_ptr(self): - return self.frame.ptr.extended_data[self.index] From 72621f2d52513eab10835e7c50ba772ed8d7ad26 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 27 Feb 2024 13:29:14 -0500 Subject: [PATCH 190/846] Use f-strings in video and subtitle dir --- .gitignore | 1 + av/data/stream.pyx | 9 +++--- av/subtitles/codeccontext.pyx | 1 - av/subtitles/stream.pyx | 1 - av/subtitles/subtitle.pyx | 55 ++++++++++------------------------- av/video/codeccontext.pyx | 2 -- av/video/format.pyx | 6 ++-- av/video/frame.pyx | 18 ++++-------- av/video/plane.pyx | 2 +- av/video/reformatter.pyx | 7 +---- av/video/stream.pyx | 13 +++------ 11 files changed, 36 insertions(+), 79 deletions(-) diff --git a/.gitignore b/.gitignore index 6e5a2b48e..2e71c06bc 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ /ipch /msvc-projects /src +/docs/_ffmpeg # Testing. *.spyderproject diff --git a/av/data/stream.pyx b/av/data/stream.pyx index e8d6624e2..8136c6395 100644 --- a/av/data/stream.pyx +++ b/av/data/stream.pyx @@ -3,11 +3,10 @@ cimport libav as lib cdef class DataStream(Stream): def __repr__(self): - cls_name = self.__class__.__name__ - _type = self.type or "" - name = self.name or "" - - return f"" + return ( + f"'}/" + f"{self.name or ''} at 0x{id(self):x}>" + ) def encode(self, frame=None): return [] diff --git a/av/subtitles/codeccontext.pyx b/av/subtitles/codeccontext.pyx index c3f433abe..227add919 100644 --- a/av/subtitles/codeccontext.pyx +++ b/av/subtitles/codeccontext.pyx @@ -6,7 +6,6 @@ from av.subtitles.subtitle cimport SubtitleProxy, SubtitleSet cdef class SubtitleCodecContext(CodecContext): - cdef _send_packet_and_recv(self, Packet packet): cdef SubtitleProxy proxy = SubtitleProxy() diff --git a/av/subtitles/stream.pyx b/av/subtitles/stream.pyx index aea5c57e2..8da6043f0 100644 --- a/av/subtitles/stream.pyx +++ b/av/subtitles/stream.pyx @@ -1,3 +1,2 @@ - cdef class SubtitleStream(Stream): pass diff --git a/av/subtitles/subtitle.pyx b/av/subtitles/subtitle.pyx index 9e98f01aa..5b2fc8a4a 100644 --- a/av/subtitles/subtitle.pyx +++ b/av/subtitles/subtitle.pyx @@ -7,18 +7,13 @@ cdef class SubtitleProxy: cdef class SubtitleSet: - def __cinit__(self, SubtitleProxy proxy): self.proxy = proxy cdef int i self.rects = tuple(build_subtitle(self, i) for i in range(self.proxy.struct.num_rects)) def __repr__(self): - return "<%s.%s at 0x%x>" % ( - self.__class__.__module__, - self.__class__.__name__, - id(self), - ) + return f"<{self.__class__.__module__}.{self.__class__.__name__} at 0x{id(self):x}>" @property def format(self): return self.proxy.struct.format @@ -64,7 +59,6 @@ cdef Subtitle build_subtitle(SubtitleSet subtitle, int index): cdef class Subtitle: - def __cinit__(self, SubtitleSet subtitle, int index): if index < 0 or index >= subtitle.proxy.struct.num_rects: raise ValueError("subtitle rect index out of range") @@ -80,18 +74,13 @@ cdef class Subtitle: elif self.ptr.type == lib.SUBTITLE_ASS: self.type = b"ass" else: - raise ValueError("unknown subtitle type %r" % self.ptr.type) + raise ValueError(f"unknown subtitle type {self.ptr.type!r}") def __repr__(self): - return "<%s.%s at 0x%x>" % ( - self.__class__.__module__, - self.__class__.__name__, - id(self), - ) + return f"<{self.__class__.__module__}.{self.__class__.__name__} at 0x{id(self):x}>" cdef class BitmapSubtitle(Subtitle): - def __cinit__(self, SubtitleSet subtitle, int index): self.planes = tuple( BitmapSubtitlePlane(self, i) @@ -100,14 +89,9 @@ cdef class BitmapSubtitle(Subtitle): ) def __repr__(self): - return "<%s.%s %dx%d at %d,%d; at 0x%x>" % ( - self.__class__.__module__, - self.__class__.__name__, - self.width, - self.height, - self.x, - self.y, - id(self), + return ( + f"<{self.__class__.__module__}.{self.__class__.__name__} " + f"{self.width}x{self.height} at {self.x},{self.y}; at 0x{id(self):x}>" ) @property @@ -132,9 +116,7 @@ cdef class BitmapSubtitle(Subtitle): cdef class BitmapSubtitlePlane: - def __cinit__(self, BitmapSubtitle subtitle, int index): - if index >= 4: raise ValueError("BitmapSubtitles have only 4 planes") if not subtitle.ptr.linesize[index]: @@ -146,34 +128,29 @@ cdef class BitmapSubtitlePlane: self._buffer = subtitle.ptr.data[index] # New-style buffer support. - def __getbuffer__(self, Py_buffer *view, int flags): PyBuffer_FillInfo(view, self, self._buffer, self.buffer_size, 0, flags) cdef class TextSubtitle(Subtitle): - def __repr__(self): - return "<%s.%s %r at 0x%x>" % ( - self.__class__.__module__, - self.__class__.__name__, - self.text, - id(self), + return ( + f"<{self.__class__.__module__}.{self.__class__.__name__} " + f"{self.text!r} at 0x{id(self):x}>" ) @property - def text(self): return self.ptr.text + def text(self): + return self.ptr.text cdef class AssSubtitle(Subtitle): - def __repr__(self): - return "<%s.%s %r at 0x%x>" % ( - self.__class__.__module__, - self.__class__.__name__, - self.ass, - id(self), + return ( + f"<{self.__class__.__module__}.{self.__class__.__name__} " + f"{self.ass!r} at 0x{id(self):x}>" ) @property - def ass(self): return self.ptr.ass + def ass(self): + return self.ptr.ass diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.pyx index 70bc36d3f..e896b44f0 100644 --- a/av/video/codeccontext.pyx +++ b/av/video/codeccontext.pyx @@ -15,7 +15,6 @@ from av.deprecation import AVDeprecationWarning cdef class VideoCodecContext(CodecContext): - def __cinit__(self, *args, **kwargs): self.last_w = 0 self.last_h = 0 @@ -30,7 +29,6 @@ cdef class VideoCodecContext(CodecContext): self.ptr.time_base.den = self.ptr.framerate.num or lib.AV_TIME_BASE cdef _prepare_frames_for_encode(self, Frame input): - if not input: return [None] diff --git a/av/video/format.pyx b/av/video/format.pyx index c9fac8e90..6ae66c3a2 100644 --- a/av/video/format.pyx +++ b/av/video/format.pyx @@ -4,6 +4,7 @@ cdef object _cinit_bypass_sentinel = object() cdef VideoFormat get_video_format(lib.AVPixelFormat c_format, unsigned int width, unsigned int height): if c_format == lib.AV_PIX_FMT_NONE: return None + cdef VideoFormat format = VideoFormat.__new__(VideoFormat, _cinit_bypass_sentinel) format._init(c_format, width, height) return format @@ -53,9 +54,9 @@ cdef class VideoFormat: def __repr__(self): if self.width or self.height: - return "" % (self.__class__.__name__, self.name, self.width, self.height) + return f"" else: - return "" % (self.__class__.__name__, self.name) + return f"" def __int__(self): return int(self.pix_fmt) @@ -129,7 +130,6 @@ cdef class VideoFormat: cdef class VideoFormatComponent: - def __cinit__(self, VideoFormat format, size_t index): self.format = format self.index = index diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 60a2addf2..4425e18a9 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -77,9 +77,7 @@ cdef useful_array(VideoPlane plane, unsigned int bytes_per_pixel=1, str dtype="u cdef class VideoFrame(Frame): - def __cinit__(self, width=0, height=0, format="yuv420p"): - if width is _cinit_bypass_sentinel: return @@ -88,7 +86,6 @@ cdef class VideoFrame(Frame): self._init(c_format, width, height) cdef _init(self, lib.AVPixelFormat format, unsigned int width, unsigned int height): - cdef int res = 0 with nogil: @@ -126,14 +123,9 @@ cdef class VideoFrame(Frame): self._np_buffer = None def __repr__(self): - return "" % ( - self.__class__.__name__, - self.index, - self.pts, - self.format.name, - self.width, - self.height, - id(self), + return ( + f"" ) @property @@ -367,7 +359,9 @@ cdef class VideoFrame(Frame): useful_array(frame.planes[1], 2) )).reshape(-1, frame.width) else: - raise ValueError("Conversion to numpy array with format `%s` is not yet supported" % frame.format.name) + raise ValueError( + f"Conversion to numpy array with format `{frame.format.name}` is not yet supported" + ) @staticmethod def from_image(img): diff --git a/av/video/plane.pyx b/av/video/plane.pyx index 6bf03b1bc..908b48716 100644 --- a/av/video/plane.pyx +++ b/av/video/plane.pyx @@ -17,7 +17,7 @@ cdef class VideoPlane(Plane): self.height = component.height break else: - raise RuntimeError("could not find plane %d of %r" % (index, frame.format)) + raise RuntimeError(f"could not find plane {index} of {frame.format!r}") # Sometimes, linesize is negative (and that is meaningful). We are only # insisting that the buffer size be based on the extent of linesize, and diff --git a/av/video/reformatter.pyx b/av/video/reformatter.pyx index ee599b817..b36b646c5 100644 --- a/av/video/reformatter.pyx +++ b/av/video/reformatter.pyx @@ -150,12 +150,8 @@ cdef class VideoReformatter: cdef int brightness, contrast, saturation cdef int ret - if ( - src_colorspace != dst_colorspace or - src_color_range != dst_color_range - ): + if src_colorspace != dst_colorspace or src_color_range != dst_color_range: with nogil: - # Casts for const-ness, because Cython isn't expressive enough. ret = lib.sws_getColorspaceDetails( self.ptr, @@ -171,7 +167,6 @@ cdef class VideoReformatter: err_check(ret) with nogil: - # Grab the coefficients for the requested transforms. # The inv_table brings us to linear, and `tbl` to the new space. if src_colorspace != lib.SWS_CS_DEFAULT: diff --git a/av/video/stream.pyx b/av/video/stream.pyx index 9f4bacc87..08949be2e 100644 --- a/av/video/stream.pyx +++ b/av/video/stream.pyx @@ -1,12 +1,7 @@ cdef class VideoStream(Stream): - def __repr__(self): - return "" % ( - self.__class__.__name__, - self.index, - self.name, - self.format.name if self.format else None, - self.codec_context.width, - self.codec_context.height, - id(self), + return ( + f"" ) From 632ee67f19fccbd310459702589282742e50ddbf Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 27 Feb 2024 17:41:31 -0500 Subject: [PATCH 191/846] Avoid using setup.cfg if possible --- setup.cfg | 10 ---------- setup.py | 18 ++++++++++++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/setup.cfg b/setup.cfg index c96fb624e..2b4cedb6e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,13 +3,3 @@ filename = *.py,*.pyx,*.pxd ignore = E203,W503 max-line-length = 142 per-file-ignores = __init__.py: E402,F401 *.pyx,*.pxd: E211,E225,E227,E402,E999 - -[metadata] -license = BSD -long_description = file: README.md -long_description_content_type = text/markdown -project_urls = - Bug Reports = https://github.com/PyAV-Org/PyAV/issues - Documentation = https://pyav.org/docs - Feedstock = https://github.com/conda-forge/av-feedstock - Download = https://pypi.org/project/av diff --git a/setup.py b/setup.py index b9e9ba2d4..59239c10a 100644 --- a/setup.py +++ b/setup.py @@ -179,13 +179,25 @@ def parse_cflags(raw_flags): exec(fp.read(), about) package_folders = pathlib.Path("av").glob("**/") -package_data = {".".join(pckg.parts): ["*.pxd"] for pckg in package_folders} +package_data = {".".join(pckg.parts): ["*.pxd", "*.pyi", "*.typed"] for pckg in package_folders} +with open("README.md") as f: + long_description = f.read() + setup( name="av", version=about["__version__"], description="Pythonic bindings for FFmpeg's libraries.", + long_description=long_description, + long_description_content_type="text/markdown", + license="BSD", + project_urls={ + "Bug Reports": "https://github.com/PyAV-Org/PyAV/issues", + "Documentation": "https://pyav.org/docs", + "Feedstock": "https://github.com/conda-forge/av-feedstock", + "Download": "https://pypi.org/project/av", + }, author="Mike Boers", author_email="pyav@mikeboers.com", url="https://github.com/PyAV-Org/PyAV", @@ -196,9 +208,7 @@ def parse_cflags(raw_flags): ext_modules=ext_modules, test_suite="tests", entry_points={ - "console_scripts": [ - "pyav = av.__main__:main", - ], + "console_scripts": ["pyav = av.__main__:main"], }, classifiers=[ "Development Status :: 5 - Production/Stable", From b0e453786a794d200d4c4502b30e0e4dd9e4a8b6 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 27 Feb 2024 18:35:32 -0500 Subject: [PATCH 192/846] Add type hints --- av/__init__.pyi | 18 ++++++++ av/_core.pyi | 9 ++++ av/audio/codeccontext.pyi | 19 +++++++++ av/audio/format.pyi | 11 +++++ av/audio/frame.pyi | 24 +++++++++++ av/audio/layout.pyi | 10 +++++ av/audio/plane.pyi | 2 + av/audio/resampler.pyi | 17 ++++++++ av/audio/stream.pyi | 3 ++ av/codec/codec.pyi | 60 ++++++++++++++++++++++++++ av/codec/context.pyi | 75 ++++++++++++++++++++++++++++++++ av/container/__init__.pyi | 3 ++ av/container/core.pyi | 90 +++++++++++++++++++++++++++++++++++++++ av/container/input.pyi | 32 ++++++++++++++ av/container/output.pyi | 4 ++ av/container/streams.pyi | 83 ++++++++++++++++++++++++++++++++++++ av/data/stream.pyi | 7 +++ av/dictionary.pyi | 10 +++++ av/enum.pyi | 54 +++++++++++++++++++++++ av/error.pyi | 16 +++++++ av/filter/__init__.pyi | 3 ++ av/filter/context.pyi | 6 +++ av/filter/filter.pyi | 13 ++++++ av/filter/graph.pyi | 35 +++++++++++++++ av/format.pyi | 46 ++++++++++++++++++++ av/frame.pyi | 9 ++++ av/logging.pyi | 43 +++++++++++++++++++ av/option.pyi | 31 ++++++++++++++ av/packet.pyi | 17 ++++++++ av/py.typed | 0 av/stream.pyi | 26 +++++++++++ av/video/__init__.pyi | 2 + av/video/codeccontext.pyi | 26 +++++++++++ av/video/format.pyi | 24 +++++++++++ av/video/frame.pyi | 52 ++++++++++++++++++++++ av/video/plane.pyi | 9 ++++ av/video/reformatter.pyi | 52 ++++++++++++++++++++++ av/video/stream.pyi | 26 +++++++++++ 38 files changed, 967 insertions(+) create mode 100644 av/__init__.pyi create mode 100644 av/_core.pyi create mode 100644 av/audio/codeccontext.pyi create mode 100644 av/audio/format.pyi create mode 100644 av/audio/frame.pyi create mode 100644 av/audio/layout.pyi create mode 100644 av/audio/plane.pyi create mode 100644 av/audio/resampler.pyi create mode 100644 av/audio/stream.pyi create mode 100644 av/codec/codec.pyi create mode 100644 av/codec/context.pyi create mode 100644 av/container/__init__.pyi create mode 100644 av/container/core.pyi create mode 100644 av/container/input.pyi create mode 100644 av/container/output.pyi create mode 100644 av/container/streams.pyi create mode 100644 av/data/stream.pyi create mode 100644 av/dictionary.pyi create mode 100644 av/enum.pyi create mode 100644 av/error.pyi create mode 100644 av/filter/__init__.pyi create mode 100644 av/filter/context.pyi create mode 100644 av/filter/filter.pyi create mode 100644 av/filter/graph.pyi create mode 100644 av/format.pyi create mode 100644 av/frame.pyi create mode 100644 av/logging.pyi create mode 100644 av/option.pyi create mode 100644 av/packet.pyi create mode 100644 av/py.typed create mode 100644 av/stream.pyi create mode 100644 av/video/__init__.pyi create mode 100644 av/video/codeccontext.pyi create mode 100644 av/video/format.pyi create mode 100644 av/video/frame.pyi create mode 100644 av/video/plane.pyi create mode 100644 av/video/reformatter.pyi create mode 100644 av/video/stream.pyi diff --git a/av/__init__.pyi b/av/__init__.pyi new file mode 100644 index 000000000..493c62012 --- /dev/null +++ b/av/__init__.pyi @@ -0,0 +1,18 @@ +from av import error, logging + +from .audio.format import * +from .audio.frame import * +from .audio.layout import * +from .audio.resampler import * +from .codec.codec import * +from .container.core import * +from .container.input import * +from .container.output import * +from .error import * +from .filter import * +from .format import * +from .packet import * +from .video.format import * +from .video.frame import * + +__version__: str diff --git a/av/_core.pyi b/av/_core.pyi new file mode 100644 index 000000000..4fbfe8525 --- /dev/null +++ b/av/_core.pyi @@ -0,0 +1,9 @@ +from typing import TypedDict + +class _Meta(TypedDict): + version: tuple[int, int, int] + configuration: str + license: str + +library_meta: dict[str, _Meta] +library_versions: dict[str, tuple[int, int, int]] diff --git a/av/audio/codeccontext.pyi b/av/audio/codeccontext.pyi new file mode 100644 index 000000000..6dcf039fa --- /dev/null +++ b/av/audio/codeccontext.pyi @@ -0,0 +1,19 @@ +from typing import Literal + +from av.codec.context import CodecContext +from av.frame import Frame +from av.packet import Packet + +from .format import AudioFormat +from .frame import AudioFrame +from .layout import AudioLayout + +class AudioCodecContext(CodecContext): + frame_size: int + sample_rate: int + rate: int + channels: int + channel_layout: int + layout: AudioLayout + format: AudioFormat + type: Literal["audio"] diff --git a/av/audio/format.pyi b/av/audio/format.pyi new file mode 100644 index 000000000..2a2519008 --- /dev/null +++ b/av/audio/format.pyi @@ -0,0 +1,11 @@ +from typing import Any + +class AudioFormat: + name: str + bytes: int + bits: int + is_planar: bool + is_packed: bool + planar: Any + packed: Any + container_name: str diff --git a/av/audio/frame.pyi b/av/audio/frame.pyi new file mode 100644 index 000000000..7f78535bf --- /dev/null +++ b/av/audio/frame.pyi @@ -0,0 +1,24 @@ +from av.frame import Frame + +from .plane import AudioPlane + +format_dtypes: dict[str, str] + +class AudioFrame(Frame): + planes: tuple[AudioPlane, ...] + samples: int + sample_rate: int + rate: int + + def __init__( + self, + format: str = "s16", + layout: str = "stereo", + samples: int = 0, + align: int = 1, + ): ... + def to_ndarray(self): ... + @staticmethod + def from_ndarray( + array, format: str = "s16", layout: str = "stereo" + ) -> AudioFrame: ... diff --git a/av/audio/layout.pyi b/av/audio/layout.pyi new file mode 100644 index 000000000..600ffeef5 --- /dev/null +++ b/av/audio/layout.pyi @@ -0,0 +1,10 @@ +channel_descriptions: dict[str, str] + +class AudioLayout: + layout: int + nb_channels: int + channels: tuple[AudioChannels] + +class AudioChannels: + name: str + description: str diff --git a/av/audio/plane.pyi b/av/audio/plane.pyi new file mode 100644 index 000000000..c9a190bf1 --- /dev/null +++ b/av/audio/plane.pyi @@ -0,0 +1,2 @@ +class AudioPlane: + buffer_size: int diff --git a/av/audio/resampler.pyi b/av/audio/resampler.pyi new file mode 100644 index 000000000..7c2033e83 --- /dev/null +++ b/av/audio/resampler.pyi @@ -0,0 +1,17 @@ +from .format import AudioFormat +from .frame import AudioFrame + +class AudioResampler: + rate: int + frame_size: int + format: AudioFormat + graph: None + + def __init__( + self, + format=None, + layout=None, + rate: int | None = None, + frame_size: int | None = None, + ): ... + def resample(self, frame: AudioFrame | None) -> list: ... diff --git a/av/audio/stream.pyi b/av/audio/stream.pyi new file mode 100644 index 000000000..59c259613 --- /dev/null +++ b/av/audio/stream.pyi @@ -0,0 +1,3 @@ +from av.stream import Stream + +class AudioStream(Stream): ... diff --git a/av/codec/codec.pyi b/av/codec/codec.pyi new file mode 100644 index 000000000..a992b6128 --- /dev/null +++ b/av/codec/codec.pyi @@ -0,0 +1,60 @@ +from fractions import Fraction +from typing import Literal + +from av.audio.format import AudioFormat +from av.enum import EnumFlag +from av.video.format import VideoFormat + +class Properties(EnumFlag): + NONE: int + INTRA_ONLY: int + LOSSY: int + LOSSLESS: int + REORDER: int + BITMAP_SUB: int + TEXT_SUB: int + +class Capabilities(EnumFlag): + NONE: int + DARW_HORIZ_BAND: int + DR1: int + HWACCEL: int + DELAY: int + SMALL_LAST_FRAME: int + HWACCEL_VDPAU: int + SUBFRAMES: int + EXPERIMENTAL: int + CHANNEL_CONF: int + NEG_LINESIZES: int + FRAME_THREADS: int + SLICE_THREADS: int + PARAM_CHANGE: int + AUTO_THREADS: int + VARIABLE_FRAME_SIZE: int + AVOID_PROBING: int + HARDWARE: int + HYBRID: int + ENCODER_REORDERED_OPAQUE: int + ENCODER_FLUSH: int + +class UnknownCodecError(ValueError): ... + +class Codec: + is_decoder: bool + name: str + long_name: str + type: Literal["video", "audio", "data", "subtitle", "attachment"] + frame_rates: list[Fraction] | None + audio_rates: list[int] | None + video_formats: list[VideoFormat] | None + audio_formats: list[AudioFormat] | None + + def __init__(self, name: str, mode: Literal["r", "w"]): ... + +class codec_descriptor: + name: str + options: tuple[int, ...] + +codecs_available: set[str] + +def dump_codecs() -> None: ... diff --git a/av/codec/context.pyi b/av/codec/context.pyi new file mode 100644 index 000000000..6ef407e6d --- /dev/null +++ b/av/codec/context.pyi @@ -0,0 +1,75 @@ +from typing import Any, Iterator, Literal + +from av.enum import EnumFlag, EnumItem +from av.frame import Frame +from av.packet import Packet + +class ThreadType(EnumFlag): + NONE: int + FRAME: int + SLICE: int + AUTO: int + +class SkipType(EnumItem): + NONE: int + DEFAULT: int + NONREF: int + BIDIR: int + NONINTRA: int + NONKEY: int + ALL: int + +class Flags(EnumFlag): + NONE: int + UNALIGNED: int + QSCALE: int + # 4MV + OUTPUT_CORRUPT: int + QPEL: int + DROPCHANGED: int + PASS1: int + PASS2: int + LOOP_FILTER: int + GRAY: int + PSNR: int + INTERLACED_DCT: int + LOW_DELAY: int + GLOBAL_HEADER: int + BITEXACT: int + AC_PRED: int + INTERLACED_ME: int + CLOSED_GOP: int + +class Flags2(EnumFlag): + NONE: int + FAST: int + NO_OUTPUT: int + LOCAL_HEADER: int + CHUNKS: int + IGNORE_CROP: int + SHOW_ALL: int + EXPORT_MVS: int + SKIP_MANUAL: int + RO_FLUSH_NOOP: int + +class CodecContext: + extradata: bytes | None + is_open: bool + is_encoder: bool + is_decoder: bool + name: str + type: Literal["video", "audio", "data", "subtitle", "attachment"] + profile: str | None + codec_tag: str + bit_rate: int | None + max_bit_rate: int | None + bit_rate_tolerance: int + thread_count: int + thread_type: Any + skip_frame: Any + + def open(self, strict: bool = True) -> None: ... + def close(self, strict: bool = True) -> None: ... + @staticmethod + def create(codec, mode=None) -> CodecContext: ... + def parse(self, raw_input=None) -> list: ... diff --git a/av/container/__init__.pyi b/av/container/__init__.pyi new file mode 100644 index 000000000..7160777cc --- /dev/null +++ b/av/container/__init__.pyi @@ -0,0 +1,3 @@ +from .core import * +from .input import * +from .output import * diff --git a/av/container/core.pyi b/av/container/core.pyi new file mode 100644 index 000000000..81d8c3d2e --- /dev/null +++ b/av/container/core.pyi @@ -0,0 +1,90 @@ +from numbers import Real +from pathlib import Path +from typing import Any, Iterator, Literal, overload + +from .input import InputContainer +from .output import OutputContainer +from .streams import StreamContainer + +class Container: + writeable: bool + name: str + metadata_encoding: str + metadata_errors: str + file: Any + buffer_size: int + input_was_opened: bool + io_open: Any + open_files: Any + format: str | None + options: dict[str, str] + container_options: dict[str, str] + stream_options: list[str] + streams: StreamContainer + duration: int | None + metadata: dict[str, str] + open_timeout: Real | None + read_timeout: Real | None + + def __enter__(self) -> Container: ... + def __exit__(self, exc_type, exc_val, exc_tb): ... + def err_check(self, value: int) -> int: ... + def set_timeout(self, timeout: Real | None) -> None: ... + def start_timeout(self) -> None: ... + +@overload +def open( + file: Any, + mode: Literal["r"], + format: str | None = None, + options: dict[str, str] | None = None, + container_options: dict[str, str] | None = None, + stream_options: list[str] | None = None, + metadata_encoding: str = "utf-8", + metadata_errors: str = "strict", + buffer_size: int = 32768, + timeout=Real | None | tuple[Real | None, Real | None], + io_open=None, +) -> InputContainer: ... +@overload +def open( + file: str | Path, + mode: Literal["r"] | None = None, + format: str | None = None, + options: dict[str, str] | None = None, + container_options: dict[str, str] | None = None, + stream_options: list[str] | None = None, + metadata_encoding: str = "utf-8", + metadata_errors: str = "strict", + buffer_size: int = 32768, + timeout=Real | None | tuple[Real | None, Real | None], + io_open=None, +) -> InputContainer: ... +@overload +def open( + file: Any, + mode: Literal["w"], + format: str | None = None, + options: dict[str, str] | None = None, + container_options: dict[str, str] | None = None, + stream_options: list[str] | None = None, + metadata_encoding: str = "utf-8", + metadata_errors: str = "strict", + buffer_size: int = 32768, + timeout=Real | None | tuple[Real | None, Real | None], + io_open=None, +) -> OutputContainer: ... +@overload +def open( + file: Any, + mode: Literal["r", "w"] | None = None, + format: str | None = None, + options: dict[str, str] | None = None, + container_options: dict[str, str] | None = None, + stream_options: list[str] | None = None, + metadata_encoding: str = "utf-8", + metadata_errors: str = "strict", + buffer_size: int = 32768, + timeout=Real | None | tuple[Real | None, Real | None], + io_open=None, +) -> InputContainer | OutputContainer: ... diff --git a/av/container/input.pyi b/av/container/input.pyi new file mode 100644 index 000000000..af2e4265f --- /dev/null +++ b/av/container/input.pyi @@ -0,0 +1,32 @@ +from typing import Iterator, Literal, overload + +from av.audio.stream import AudioStream +from av.frame import Frame +from av.packet import Packet +from av.video.frame import VideoFrame +from av.video.stream import VideoStream + +from .core import Container +from .streams import Stream + +class InputContainer(Container): + bit_rate: int + size: int + + def close(self) -> None: ... + def demux(self, *args, **kwargs) -> Iterator[Packet]: ... + @overload + def decode(self, *args: VideoStream, **kwargs) -> Iterator[VideoFrame]: ... + @overload + def decode(self, *args, **kwargs) -> Iterator[Frame]: ... + def seek( + self, + offset: int, + *, + whence: Literal["time"] = "time", + backward: bool = True, + any_frame: bool = False, + stream: Stream | VideoStream | AudioStream | None = None, + unsupported_frame_offset: bool = False, + unsupported_byte_offset: bool = False, + ) -> None: ... diff --git a/av/container/output.pyi b/av/container/output.pyi new file mode 100644 index 000000000..854b9f12c --- /dev/null +++ b/av/container/output.pyi @@ -0,0 +1,4 @@ +from .core import Container + +class OutputContainer(Container): + def start_encoding(self) -> None: ... diff --git a/av/container/streams.pyi b/av/container/streams.pyi new file mode 100644 index 000000000..be01100b8 --- /dev/null +++ b/av/container/streams.pyi @@ -0,0 +1,83 @@ +from fractions import Fraction +from typing import Iterator, Literal, overload + +from av.video.stream import VideoStream + +class Codec: + name: str + mode: Literal["r", "w"] + + frame_rates: list[Fraction] | None + audio_rates: list[int] | None + +class CodecContext: + name: str + bit_rate: int | None + width: int + height: int + pix_fmt: str | None + sample_aspect_ratio: Fraction | None + sample_rate: int | None + channels: int + extradata_size: int + is_open: Literal[0, 1] + is_encoder: Literal[0, 1] + is_decoder: Literal[0, 1] + +class Stream: + thread_type: Literal["NONE", "FRAME", "SLICE", "AUTO"] + + id: int + profile: str | None + codec_context: CodecContext + + index: int + time_base: Fraction | None + average_rate: Fraction | None + base_rate: Fraction | None + guessed_rate: Fraction | None + + start_time: int | None + duration: int | None + frames: int + language: str | None + + # Defined by `av_get_media_type_string` at + # https://ffmpeg.org/doxygen/6.0/libavutil_2utils_8c_source.html + type: Literal["video", "audio", "data", "subtitle", "attachment"] + + # From `codec_context` + name: str + bit_rate: int | None + sample_rate: int | None + channels: int + extradata_size: int + is_open: Literal[0, 1] + is_encoder: Literal[0, 1] + is_decoder: Literal[0, 1] + + def decode(self, packet=None): ... + def encode(self, frame=None): ... + +class StreamContainer: + video: tuple[VideoStream, ...] + audio: tuple[Stream, ...] + subtitles: tuple[Stream, ...] + data: tuple[Stream, ...] + other: tuple[Stream, ...] + + def __init__(self) -> None: ... + def add_stream(self, stream: Stream) -> None: ... + def __len__(self) -> int: ... + def __iter__(self) -> Iterator[Stream]: ... + @overload + def __getitem__(self, index: int) -> Stream: ... + @overload + def __getitem__(self, index: slice) -> list[Stream]: ... + @overload + def __getitem__(self, index: int | slice) -> Stream | list[Stream]: ... + def get( + self, + *args: int | Stream | dict[str, int | tuple[int, ...]], + **kwargs: int | tuple[int, ...], + ) -> list[Stream]: ... diff --git a/av/data/stream.pyi b/av/data/stream.pyi new file mode 100644 index 000000000..f84e5f9c2 --- /dev/null +++ b/av/data/stream.pyi @@ -0,0 +1,7 @@ +from av.frame import Frame +from av.packet import Packet +from av.stream import Stream + +class DataStream(Stream): + def encode(self, frame: Frame | None = None) -> list[Packet]: ... + def decode(self, packet: Packet | None = None, count: int = 0) -> list[Frame]: ... diff --git a/av/dictionary.pyi b/av/dictionary.pyi new file mode 100644 index 000000000..a6868bea2 --- /dev/null +++ b/av/dictionary.pyi @@ -0,0 +1,10 @@ +from collections.abc import MutableMapping +from typing import Iterator + +class Dictionary(MutableMapping[str, str]): + def __getitem__(self, key: str) -> str: ... + def __setitem__(self, key: str, value: str) -> None: ... + def __delitem__(self, key: str) -> None: ... + def __len__(self) -> int: ... + def __iter__(self) -> Iterator[str]: ... + def __repr__(self) -> str: ... diff --git a/av/enum.pyi b/av/enum.pyi new file mode 100644 index 000000000..12d523ac2 --- /dev/null +++ b/av/enum.pyi @@ -0,0 +1,54 @@ +from typing import Literal, Sequence, overload + +class EnumType(type): + def __init__(self, name, bases, attrs, items): ... + def _create(self, name: str, value: int, doc=None, by_value_only=False): ... + def __len__(self): ... + def __iter__(self): ... + def __getitem__(self, key): ... + def _get(self, value: int, create: bool = False): ... + def _get_multi_flags(self, value: int): ... + def get( + self, key, default: int | None = None, create: bool = False + ) -> int | None: ... + +class EnumItem: + name: str + value: int + + def __int__(self) -> int: ... + def __hash__(self) -> int: ... + def __reduce__(self): ... + def __eq__(self, other) -> bool: ... + def __ne__(self, other) -> bool: ... + +class EnumFlag(EnumItem): + flags: tuple[EnumFlag] + + def __and__(self, other): ... + def __or__(self, other): ... + def __xor__(self, other): ... + def __invert__(self): ... + def __nonzero__(self) -> bool: ... + +@overload +def define_enum( + name: str, + module: str, + items: Sequence[tuple[str, int] | None], + is_flags: Literal[True], +) -> EnumFlag: ... +@overload +def define_enum( + name: str, + module: str, + items: Sequence[tuple[str, int] | None], + is_flags: Literal[False], +) -> EnumItem: ... +@overload +def define_enum( + name: str, + module: str, + items: Sequence[tuple[str, int] | None], + is_flags: bool = False, +) -> EnumItem | EnumFlag: ... diff --git a/av/error.pyi b/av/error.pyi new file mode 100644 index 000000000..89f12c22d --- /dev/null +++ b/av/error.pyi @@ -0,0 +1,16 @@ +from av.logging import get_last_error + +classes: dict[int, Exception] + +def code_to_tag(code: int) -> bytes: ... +def tag_to_code(tag: bytes) -> int: ... +def make_error(res: int, filename=None, log=None): ... + +class FFmpegError(Exception): + def __init__(self, code: int, message, filename=None, log=None): ... + +class LookupError(FFmpegError): ... +class HTTPError(FFmpegError): ... +class HTTPClientError(FFmpegError): ... +class UndefinedError(FFmpegError): ... +class InvalidDataError(ValueError): ... diff --git a/av/filter/__init__.pyi b/av/filter/__init__.pyi new file mode 100644 index 000000000..8a6b5a59b --- /dev/null +++ b/av/filter/__init__.pyi @@ -0,0 +1,3 @@ +from .context import * +from .filter import * +from .graph import * diff --git a/av/filter/context.pyi b/av/filter/context.pyi new file mode 100644 index 000000000..b5433f870 --- /dev/null +++ b/av/filter/context.pyi @@ -0,0 +1,6 @@ +class FilterContext: + name: str | None + + def init(self, args=None, **kwargs) -> None: ... + def link_to(self, input_, output_idx: int = 0, input_idx: int = 0) -> None: ... + def push(self, frame) -> None: ... diff --git a/av/filter/filter.pyi b/av/filter/filter.pyi new file mode 100644 index 000000000..f5b1aac60 --- /dev/null +++ b/av/filter/filter.pyi @@ -0,0 +1,13 @@ +class Filter: + name: str + description: str + options: tuple | None + dynamic_inputs: bool + dynamic_outputs: bool + timeline_support: bool + slice_threads: bool + command_support: bool + inputs: tuple + outputs: tuple + +filters_available: set[str] diff --git a/av/filter/graph.pyi b/av/filter/graph.pyi new file mode 100644 index 000000000..b08d314ec --- /dev/null +++ b/av/filter/graph.pyi @@ -0,0 +1,35 @@ +from fractions import Fraction + +from av.audio.frame import AudioFrame +from av.audio.stream import AudioStream +from av.video.frame import VideoFrame +from av.video.stream import VideoStream + +from .context import FilterContext +from .filter import Filter + +class Graph: + def __init__(self) -> None: ... + def configure(self, auto_buffer: bool = True, force: bool = False) -> None: ... + def add(self, filter: str | Filter, args=None, **kwargs: str) -> FilterContext: ... + def add_buffer( + self, + template: VideoStream | None = None, + width: int | None = None, + height: int | None = None, + format=None, + name: str | None = None, + time_base: Fraction | None = None, + ) -> FilterContext: ... + def add_abuffer( + self, + template: AudioStream | None = None, + sample_rate: int | None = None, + format=None, + layout=None, + channels: int | None = None, + name: str | None = None, + time_base: Fraction | None = None, + ) -> FilterContext: ... + def push(self, frame: None | AudioFrame | VideoFrame) -> None: ... + def pull(self) -> VideoFrame | AudioFrame: ... diff --git a/av/format.pyi b/av/format.pyi new file mode 100644 index 000000000..7eb3d1ce5 --- /dev/null +++ b/av/format.pyi @@ -0,0 +1,46 @@ +from .enum import EnumFlag + +class Flags(EnumFlag): + NOFILE: int + NEEDNUMBER: int + SHOW_IDS: int + GLOBALHEADER: int + NOTIMESTAMPS: int + GENERIC_INDEX: int + TS_DISCONT: int + VARIABLE_FPS: int + NODIMENSIONS: int + NOSTREAMS: int + NOBINSEARCH: int + NOGENSEARCH: int + NO_BYTE_SEEK: int + ALLOW_FLUSH: int + TS_NONSTRICT: int + TS_NEGATIVE: int + SEEK_TO_PTS: int + +class ContainerFormat: + is_input: bool + is_output: bool + long_name: str + + # flags + no_file: int + need_number: int + show_ids: int + global_header: int + no_timestamps: int + generic_index: int + ts_discont: int + variable_fps: int + no_dimensions: int + no_streams: int + no_bin_search: int + no_gen_search: int + no_byte_seek: int + allow_flush: int + ts_nonstrict: int + ts_negative: int + seek_to_pts: int + +formats_available: set[str] diff --git a/av/frame.pyi b/av/frame.pyi new file mode 100644 index 000000000..8e81a8198 --- /dev/null +++ b/av/frame.pyi @@ -0,0 +1,9 @@ +from fractions import Fraction + +class Frame: + dts: int | None + pts: int | None + time: float | None + time_base: Fraction + is_corrupt: bool + side_data: dict[str, str] diff --git a/av/logging.pyi b/av/logging.pyi new file mode 100644 index 000000000..996b7528a --- /dev/null +++ b/av/logging.pyi @@ -0,0 +1,43 @@ +import logging +from threading import Lock +from typing import Callable + +PANIC: int +FATAL: int +ERROR: int +WARNING: int +INFO: int +VERBOSE: int +DEBUG: int +TRACE: int +CRITICAL: int + +def adapt_level(level: int) -> int: ... +def get_level() -> int: ... +def set_level(level: int) -> None: ... +def restore_default_callback() -> None: ... +def get_print_after_shutdown() -> bool: ... +def set_print_after_shutdown(v: bool) -> None: ... +def get_skip_repeated() -> bool: ... +def set_skip_repeated(v: bool) -> None: ... +def get_last_error() -> tuple[int, tuple[int, str, str] | None]: ... +def log(level: int, name: str, message: str) -> None: ... + +class Capture: + logs: list[tuple[int, str, str]] + + def __init__(self, local: bool = True) -> None: ... + def __enter__(self) -> list[tuple[int, str, str]]: ... + def __exit__( + self, type_: type | None, value: Exception | None, traceback: Callable | None + ) -> None: ... + +level_threshold: int +print_after_shutdown: bool +skip_repeated: bool +skip_lock: Lock +last_log: tuple[int, str, str] | None +skip_count: int +last_error: tuple[int, str, str] | None +global_captures: list[list[tuple[int, str, str]]] +thread_captures: dict diff --git a/av/option.pyi b/av/option.pyi new file mode 100644 index 000000000..14ceed99b --- /dev/null +++ b/av/option.pyi @@ -0,0 +1,31 @@ +from av.enum import EnumFlag, EnumItem + +class OptionType(EnumItem): + FLAGS: int + INT: int + INT64: int + DOUBLE: int + FLOAT: int + STRING: int + RATIONAL: int + BINARY: int + DICT: int + CONST: int + IMAGE_SIZE: int + PIXEL_FMT: int + SAMPLE_FMT: int + VIDEO_RATE: int + DURATION: int + COLOR: int + CHANNEL_LAYOUT: int + BOOL: int + +class OptionFlags(EnumFlag): + ENCODING_PARAM: int + DECODING_PARAM: int + AUDIO_PARAM: int + VIDEO_PARAM: int + SUBTITLE_PARAM: int + EXPORT: int + READONLY: int + FILTERING_PARAM: int diff --git a/av/packet.pyi b/av/packet.pyi new file mode 100644 index 000000000..ee3d2f376 --- /dev/null +++ b/av/packet.pyi @@ -0,0 +1,17 @@ +from fractions import Fraction + +from .stream import Stream + +class Packet: + stream: Stream + stream_index: int + time_base: Fraction + pts: int | None + dts: int + pos: int | None + size: int + duration: int | None + is_keyframe: bool + is_corrupt: bool + + def __init__(self, input: int | None = None): ... diff --git a/av/py.typed b/av/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/av/stream.pyi b/av/stream.pyi new file mode 100644 index 000000000..7c220e2c4 --- /dev/null +++ b/av/stream.pyi @@ -0,0 +1,26 @@ +from fractions import Fraction +from typing import Literal + +from .enum import EnumItem + +class SideData(EnumItem): + DISPLAYMATRIX: int + +class Stream: + name: str | None + id: int + profile: str + side_data: dict[str, str] + index: int + time_base: Fraction | None + average_rate: Fraction | None + base_rate: Fraction | None + guessed_rate: Fraction | None + start_time: int | None + duration: int | None + frames: int + language: str | None + type: Literal["video", "audio", "data", "subtitle", "attachment"] + + def encode(self, frame=None): ... + def decode(self, packet=None): ... diff --git a/av/video/__init__.pyi b/av/video/__init__.pyi new file mode 100644 index 000000000..4a25d8837 --- /dev/null +++ b/av/video/__init__.pyi @@ -0,0 +1,2 @@ +from .frame import VideoFrame +from .stream import VideoStream diff --git a/av/video/codeccontext.pyi b/av/video/codeccontext.pyi new file mode 100644 index 000000000..e6ecbcbd4 --- /dev/null +++ b/av/video/codeccontext.pyi @@ -0,0 +1,26 @@ +from fractions import Fraction +from typing import Iterator, Literal + +from av.codec.context import CodecContext +from av.packet import Packet + +from .frame import VideoFrame + +class VideoCodecContext(CodecContext): + width: int + height: int + bits_per_codec_sample: int + pix_fmt: str + framerate: Fraction + rate: Fraction + gop_size: int + sample_aspect_ratio: Fraction + display_aspect_ratio: Fraction + has_b_frames: bool + coded_width: int + coded_height: int + color_range: int + type: Literal["video"] + def encode(self, frame: VideoFrame | None = None) -> list[Packet]: ... + def encode_lazy(self, frame: VideoFrame | None = None) -> Iterator[Packet]: ... + def decode(self, packet: Packet | None = None) -> list[VideoFrame]: ... diff --git a/av/video/format.pyi b/av/video/format.pyi new file mode 100644 index 000000000..33aa8d3c2 --- /dev/null +++ b/av/video/format.pyi @@ -0,0 +1,24 @@ +class VideoFormat: + name: str + bits_per_pixel: int + padded_bits_per_pixel: int + is_big_endian: bool + has_palette: bool + is_bit_stream: bool + is_planar: bool + is_rgb: bool + + def __init__(self, name: str, width: int = 0, height: int = 0): ... + def chroma_width(self, luma_width: int = 0) -> int: ... + def chroma_height(self, luma_height: int = 0) -> int: ... + +class VideoFormatComponent: + plane: int + bits: int + is_alpha: bool + is_luma: bool + is_chroma: bool + width: int + height: int + + def __init__(self, format: VideoFormat, index: int): ... diff --git a/av/video/frame.pyi b/av/video/frame.pyi new file mode 100644 index 000000000..a387ad37f --- /dev/null +++ b/av/video/frame.pyi @@ -0,0 +1,52 @@ +import numpy as np +from PIL import Image + +from av.enum import EnumItem +from av.frame import Frame + +from .format import VideoFormat +from .plane import VideoPlane + +class PictureType(EnumItem): + NONE: int + I: int + P: int + B: int + S: int + SI: int + SP: int + BI: int + +class VideoFrame(Frame): + format: VideoFormat + pts: int + time: float + planes: tuple[VideoPlane, ...] + width: int + height: int + key_frame: bool + interlaced_frame: bool + pict_type: int + + @staticmethod + def from_image(img: Image.Image) -> VideoFrame: ... + @staticmethod + def from_ndarray(array: np.ndarray, format: str = "rgb24") -> VideoFrame: ... + @staticmethod + def from_numpy_buffer(array: np.ndarray, format: str = "rgb24"): ... + def __init__( + self, name: str, width: int = 0, height: int = 0, format: str = "yuv420p" + ): ... + def to_image(self, **kwargs) -> Image.Image: ... + def to_ndarray(self, **kwargs) -> np.ndarray: ... + def reformat( + self, + width: int | None = None, + height: int | None = None, + format: str | None = None, + src_colorspace=None, + dst_colorspace=None, + interpolation: int | str | None = None, + src_color_range: int | str | None = None, + dst_color_range: int | str | None = None, + ) -> VideoFrame: ... diff --git a/av/video/plane.pyi b/av/video/plane.pyi new file mode 100644 index 000000000..592d3eb15 --- /dev/null +++ b/av/video/plane.pyi @@ -0,0 +1,9 @@ +from .frame import VideoFrame + +class VideoPlane: + line_size: int + width: int + height: int + buffer_size: int + + def __init__(self, frame: VideoFrame, index: int): ... diff --git a/av/video/reformatter.pyi b/av/video/reformatter.pyi new file mode 100644 index 000000000..f17055a4f --- /dev/null +++ b/av/video/reformatter.pyi @@ -0,0 +1,52 @@ +from av.enum import EnumItem + +from .format import VideoFormat +from .frame import VideoFrame + +class Interpolation(EnumItem): + FAST_BILINEAER: int + BILINEAR: int + BICUBIC: int + X: int + POINT: int + AREA: int + BICUBLIN: int + GAUSS: int + SINC: int + LANCZOS: int + SPLINE: int + +class Colorspace(EnumItem): + ITU709: int + FCC: int + ITU601: int + ITU624: int + SMPTE170M: int + SMPTE240M: int + DEFAULT: int + itu709: int + fcc: int + itu601: int + itu624: int + smpte240: int + default: int + +class ColorRange(EnumItem): + UNSPECIFIED: int + MPEG: int + JPEG: int + NB: int + +class VideoReformatter: + def reformat( + self, + frame: VideoFrame, + width: int | None = None, + height: int | None = None, + format: str | None = None, + src_colorspace=None, + dst_colorspace=None, + interpolation: int | str | None = None, + src_color_range: int | str | None = None, + dst_color_range: int | str | None = None, + ) -> VideoFrame: ... diff --git a/av/video/stream.pyi b/av/video/stream.pyi new file mode 100644 index 000000000..f6be52460 --- /dev/null +++ b/av/video/stream.pyi @@ -0,0 +1,26 @@ +from fractions import Fraction +from typing import Any + +from av.packet import Packet +from av.stream import Stream + +from .codeccontext import VideoCodecContext +from .frame import VideoFrame + +class VideoStream(Stream): + name: str + width: int + height: int + pix_fmt: str | None + sample_aspect_ratio: Fraction | None + codec_context: VideoCodecContext + + # from codec context + bit_rate: int | None + max_bit_rate: int | None + bit_rate_tolerance: int + thread_count: int + thread_type: Any + + def encode(self, frame: VideoFrame | None = None) -> list[Packet]: ... + def decode(self, packet: Packet | None = None) -> list[VideoFrame]: ... From 6b50a00ee8668fb518be69612469e85f51ec6170 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 27 Feb 2024 18:52:04 -0500 Subject: [PATCH 193/846] Use isort with standard config --- av/_core.pyx | 1 - av/audio/fifo.pxd | 2 +- av/audio/frame.pxd | 2 +- av/bytesource.pyx | 2 +- av/codec/context.pxd | 2 +- av/codec/context.pyx | 2 +- av/container/core.pxd | 1 - av/container/core.pyx | 2 +- av/container/output.pyx | 1 - av/container/pyio.pxd | 2 +- av/container/pyio.pyx | 3 +-- av/datasets.py | 3 +-- av/error.pyx | 3 +-- av/filter/graph.pyx | 2 +- av/logging.pyx | 5 ++--- av/stream.pyx | 4 ++-- av/utils.pxd | 2 +- av/utils.pyx | 1 - av/video/codeccontext.pyx | 2 +- av/video/frame.pxd | 2 +- av/video/reformatter.pyx | 3 +-- examples/basics/parse.py | 1 - examples/basics/remux.py | 1 - examples/basics/save_keyframes.py | 1 - examples/basics/thread_type.py | 1 - examples/numpy/barcode.py | 3 +-- examples/numpy/generate_video.py | 1 - examples/numpy/generate_video_with_pts.py | 3 +-- pyproject.toml | 7 +------ tests/common.py | 3 +-- tests/test_audioformat.py | 1 - tests/test_codec.py | 1 - tests/test_codec_context.py | 6 +++--- tests/test_colorspace.py | 2 +- tests/test_doctests.py | 2 +- tests/test_encode.py | 7 +++---- tests/test_enums.py | 1 - tests/test_file_probing.py | 2 +- tests/test_filters.py | 4 ++-- tests/test_subtitles.py | 2 +- tests/test_timeout.py | 5 ++--- 41 files changed, 37 insertions(+), 64 deletions(-) diff --git a/av/_core.pyx b/av/_core.pyx index 15da5dcc6..4100c5857 100644 --- a/av/_core.pyx +++ b/av/_core.pyx @@ -1,6 +1,5 @@ cimport libav as lib - # Initialise libraries. lib.avformat_network_init() lib.avdevice_register_all() diff --git a/av/audio/fifo.pxd b/av/audio/fifo.pxd index cf3a9dbec..0ace5e4b1 100644 --- a/av/audio/fifo.pxd +++ b/av/audio/fifo.pxd @@ -1,5 +1,5 @@ -from libc.stdint cimport int64_t, uint64_t cimport libav as lib +from libc.stdint cimport int64_t, uint64_t from av.audio.frame cimport AudioFrame diff --git a/av/audio/frame.pxd b/av/audio/frame.pxd index a438fe627..e7ee88591 100644 --- a/av/audio/frame.pxd +++ b/av/audio/frame.pxd @@ -1,5 +1,5 @@ -from libc.stdint cimport uint8_t, uint64_t cimport libav as lib +from libc.stdint cimport uint8_t, uint64_t from av.audio.format cimport AudioFormat from av.audio.layout cimport AudioLayout diff --git a/av/bytesource.pyx b/av/bytesource.pyx index 447df55d3..9192c6d1a 100644 --- a/av/bytesource.pyx +++ b/av/bytesource.pyx @@ -2,7 +2,7 @@ from cpython.buffer cimport ( PyBUF_SIMPLE, PyBuffer_Release, PyObject_CheckBuffer, - PyObject_GetBuffer + PyObject_GetBuffer, ) diff --git a/av/codec/context.pxd b/av/codec/context.pxd index 4a59b106e..f247655ff 100644 --- a/av/codec/context.pxd +++ b/av/codec/context.pxd @@ -1,5 +1,5 @@ -from libc.stdint cimport int64_t cimport libav as lib +from libc.stdint cimport int64_t from av.bytesource cimport ByteSource from av.codec.codec cimport Codec diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 638ff65a8..e2557e702 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -1,9 +1,9 @@ import warnings +cimport libav as lib from libc.errno cimport EAGAIN from libc.stdint cimport uint8_t from libc.string cimport memcpy -cimport libav as lib from av.bytesource cimport ByteSource, bytesource from av.codec.codec cimport Codec, wrap_codec diff --git a/av/container/core.pxd b/av/container/core.pxd index 8ca5ded16..1aed54b90 100644 --- a/av/container/core.pxd +++ b/av/container/core.pxd @@ -6,7 +6,6 @@ from av.dictionary cimport _Dictionary from av.format cimport ContainerFormat from av.stream cimport Stream - # Interrupt callback information, times are in seconds. ctypedef struct timeout_info: double start_time diff --git a/av/container/core.pyx b/av/container/core.pyx index 8134d8bfc..240db340e 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -1,9 +1,9 @@ from cython.operator cimport dereference from libc.stdint cimport int64_t -from pathlib import Path import os import time +from pathlib import Path cimport libav as lib diff --git a/av/container/output.pyx b/av/container/output.pyx index 695479597..55e8b5006 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -12,7 +12,6 @@ from av.utils cimport dict_to_avdict, to_avrational from av.dictionary import Dictionary - log = logging.getLogger(__name__) diff --git a/av/container/pyio.pxd b/av/container/pyio.pxd index e93a11dc8..b3ec04087 100644 --- a/av/container/pyio.pxd +++ b/av/container/pyio.pxd @@ -1,5 +1,5 @@ -from libc.stdint cimport int64_t, uint8_t cimport libav as lib +from libc.stdint cimport int64_t, uint8_t cdef int pyio_read(void *opaque, uint8_t *buf, int buf_size) noexcept nogil diff --git a/av/container/pyio.pyx b/av/container/pyio.pyx index 47110d495..ab29cee11 100644 --- a/av/container/pyio.pyx +++ b/av/container/pyio.pyx @@ -1,9 +1,8 @@ -from libc.string cimport memcpy cimport libav as lib +from libc.string cimport memcpy from av.error cimport stash_exception - ctypedef int64_t (*seek_func_t)(void *opaque, int64_t offset, int whence) noexcept nogil diff --git a/av/datasets.py b/av/datasets.py index 5c189365a..15ffe9643 100644 --- a/av/datasets.py +++ b/av/datasets.py @@ -1,9 +1,8 @@ -from urllib.request import urlopen import errno import logging import os import sys - +from urllib.request import urlopen log = logging.getLogger(__name__) diff --git a/av/error.pyx b/av/error.pyx index 7cb1476c7..64cd70594 100644 --- a/av/error.pyx +++ b/av/error.pyx @@ -2,15 +2,14 @@ cimport libav as lib from av.logging cimport get_last_error -from threading import local import errno import os import sys import traceback +from threading import local from av.enum import define_enum - # Will get extended with all of the exceptions. __all__ = [ "ErrorType", "FFmpegError", "LookupError", "HTTPError", "HTTPClientError", diff --git a/av/filter/graph.pyx b/av/filter/graph.pyx index 391677063..f6376b3a3 100644 --- a/av/filter/graph.pyx +++ b/av/filter/graph.pyx @@ -1,5 +1,5 @@ -from fractions import Fraction import warnings +from fractions import Fraction from av.audio.format cimport AudioFormat from av.audio.frame cimport AudioFrame diff --git a/av/logging.pyx b/av/logging.pyx index 49d4a80da..8940c3139 100644 --- a/av/logging.pyx +++ b/av/logging.pyx @@ -32,15 +32,14 @@ API Reference from __future__ import absolute_import +cimport libav as lib from libc.stdio cimport fprintf, stderr from libc.stdlib cimport free, malloc -cimport libav as lib -from threading import Lock, get_ident import logging import os import sys - +from threading import Lock, get_ident # Library levels. # QUIET = lib.AV_LOG_QUIET # -8; not really a level. diff --git a/av/stream.pyx b/av/stream.pyx index 6980f5d9a..225255d7f 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -1,7 +1,7 @@ import warnings -from libc.stdint cimport int32_t cimport libav as lib +from libc.stdint cimport int32_t from av.enum cimport define_enum from av.error cimport err_check @@ -10,7 +10,7 @@ from av.utils cimport ( avdict_to_dict, avrational_to_fraction, dict_to_avdict, - to_avrational + to_avrational, ) from av.deprecation import AVDeprecationWarning diff --git a/av/utils.pxd b/av/utils.pxd index bc5d56927..125272c5c 100644 --- a/av/utils.pxd +++ b/av/utils.pxd @@ -1,5 +1,5 @@ -from libc.stdint cimport uint64_t cimport libav as lib +from libc.stdint cimport uint64_t cdef dict avdict_to_dict(lib.AVDictionary *input, str encoding, str errors) diff --git a/av/utils.pyx b/av/utils.pyx index 6d40dd889..cc01925de 100644 --- a/av/utils.pyx +++ b/av/utils.pyx @@ -6,7 +6,6 @@ cimport libav as lib from av.error cimport err_check - # === DICTIONARIES === # ==================== diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.pyx index e896b44f0..8fe5c276e 100644 --- a/av/video/codeccontext.pyx +++ b/av/video/codeccontext.pyx @@ -1,7 +1,7 @@ import warnings -from libc.stdint cimport int64_t cimport libav as lib +from libc.stdint cimport int64_t from av.codec.context cimport CodecContext from av.frame cimport Frame diff --git a/av/video/frame.pxd b/av/video/frame.pxd index 9e97d946f..53a154a9c 100644 --- a/av/video/frame.pxd +++ b/av/video/frame.pxd @@ -1,5 +1,5 @@ -from libc.stdint cimport uint8_t cimport libav as lib +from libc.stdint cimport uint8_t from av.frame cimport Frame from av.video.format cimport VideoFormat diff --git a/av/video/reformatter.pyx b/av/video/reformatter.pyx index b36b646c5..f41094eda 100644 --- a/av/video/reformatter.pyx +++ b/av/video/reformatter.pyx @@ -1,12 +1,11 @@ -from libc.stdint cimport uint8_t cimport libav as lib +from libc.stdint cimport uint8_t from av.enum cimport define_enum from av.error cimport err_check from av.video.format cimport VideoFormat from av.video.frame cimport alloc_video_frame - Interpolation = define_enum("Interpolation", __name__, ( ("FAST_BILINEAR", lib.SWS_FAST_BILINEAR, "Fast bilinear"), ("BILINEAR", lib.SWS_BILINEAR, "Bilinear"), diff --git a/examples/basics/parse.py b/examples/basics/parse.py index 42a8af160..f4baaecb7 100644 --- a/examples/basics/parse.py +++ b/examples/basics/parse.py @@ -4,7 +4,6 @@ import av import av.datasets - # We want an H.264 stream in the Annex B byte-stream format. # We haven't exposed bitstream filters yet, so we're gonna use the `ffmpeg` CLI. h264_path = "night-sky.h264" diff --git a/examples/basics/remux.py b/examples/basics/remux.py index feb25cbcd..befc24f8a 100644 --- a/examples/basics/remux.py +++ b/examples/basics/remux.py @@ -1,7 +1,6 @@ import av import av.datasets - input_ = av.open(av.datasets.curated("pexels/time-lapse-video-of-night-sky-857195.mp4")) output = av.open("remuxed.mkv", "w") diff --git a/examples/basics/save_keyframes.py b/examples/basics/save_keyframes.py index 8d31aa621..bc47376cd 100644 --- a/examples/basics/save_keyframes.py +++ b/examples/basics/save_keyframes.py @@ -1,7 +1,6 @@ import av import av.datasets - content = av.datasets.curated("pexels/time-lapse-video-of-night-sky-857195.mp4") with av.open(content) as container: # Signal that we only want to look at keyframes. diff --git a/examples/basics/thread_type.py b/examples/basics/thread_type.py index 2fa7562c9..966a8c8c0 100644 --- a/examples/basics/thread_type.py +++ b/examples/basics/thread_type.py @@ -3,7 +3,6 @@ import av import av.datasets - print("Decoding with default (slice) threading...") container = av.open( diff --git a/examples/numpy/barcode.py b/examples/numpy/barcode.py index 2a3dfff0d..bd40f17a3 100644 --- a/examples/numpy/barcode.py +++ b/examples/numpy/barcode.py @@ -1,10 +1,9 @@ -from PIL import Image import numpy as np +from PIL import Image import av import av.datasets - container = av.open( av.datasets.curated("pexels/time-lapse-video-of-sunset-by-the-sea-854400.mp4") ) diff --git a/examples/numpy/generate_video.py b/examples/numpy/generate_video.py index 250a0bcc1..8b490ea1f 100644 --- a/examples/numpy/generate_video.py +++ b/examples/numpy/generate_video.py @@ -2,7 +2,6 @@ import av - duration = 4 fps = 24 total_frames = duration * fps diff --git a/examples/numpy/generate_video_with_pts.py b/examples/numpy/generate_video_with_pts.py index 95288fc72..d84b34817 100644 --- a/examples/numpy/generate_video_with_pts.py +++ b/examples/numpy/generate_video_with_pts.py @@ -1,13 +1,12 @@ #!/usr/bin/env python3 -from fractions import Fraction import colorsys +from fractions import Fraction import numpy as np import av - (width, height) = (640, 360) total_frames = 20 fps = 30 diff --git a/pyproject.toml b/pyproject.toml index d4da9718e..267002b31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,11 +2,6 @@ requires = ["setuptools", "cython"] [tool.isort] -default_section = "THIRDPARTY" -from_first = true +profile = "black" known_first_party = ["av"] -line_length = 88 -lines_after_imports = 2 -multi_line_output = 3 -sections = ["FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] skip = ["av/__init__.py"] diff --git a/tests/common.py b/tests/common.py index 5b12ca0d9..e2b5f4e16 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1,13 +1,12 @@ -from unittest import TestCase as _Base import datetime import errno import functools import os import types +from unittest import TestCase as _Base from av.datasets import fate as fate_suite - try: import PIL.Image as Image import PIL.ImageFilter as ImageFilter diff --git a/tests/test_audioformat.py b/tests/test_audioformat.py index 5d4eb7871..5334b37d6 100644 --- a/tests/test_audioformat.py +++ b/tests/test_audioformat.py @@ -4,7 +4,6 @@ from .common import TestCase - postfix = "le" if sys.byteorder == "little" else "be" diff --git a/tests/test_codec.py b/tests/test_codec.py index 511a5b434..98b059fbf 100644 --- a/tests/test_codec.py +++ b/tests/test_codec.py @@ -5,7 +5,6 @@ from .common import TestCase - # some older ffmpeg versions have no native opus encoder try: opus_c = Codec("opus", "w") diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 43eaa748f..0be4ed621 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -1,12 +1,12 @@ -from fractions import Fraction -from unittest import SkipTest import os import warnings +from fractions import Fraction +from unittest import SkipTest +import av from av import AudioResampler, Codec, Packet from av.codec.codec import UnknownCodecError from av.video.frame import PictureType -import av from .common import TestCase, fate_suite diff --git a/tests/test_colorspace.py b/tests/test_colorspace.py index fc060eb74..e1c4ee8b1 100644 --- a/tests/test_colorspace.py +++ b/tests/test_colorspace.py @@ -1,5 +1,5 @@ -from av.video.reformatter import ColorRange, Colorspace import av +from av.video.reformatter import ColorRange, Colorspace from .common import TestCase, fate_suite diff --git a/tests/test_doctests.py b/tests/test_doctests.py index 1449355bd..63220889c 100644 --- a/tests/test_doctests.py +++ b/tests/test_doctests.py @@ -1,7 +1,7 @@ -from unittest import TestCase import doctest import pkgutil import re +from unittest import TestCase import av import av.datasets diff --git a/tests/test_encode.py b/tests/test_encode.py index e0a90db18..3ca031794 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -1,18 +1,17 @@ -from fractions import Fraction -from unittest import SkipTest import io import math +from fractions import Fraction +from unittest import SkipTest import numpy as np +import av from av import AudioFrame, VideoFrame from av.audio.stream import AudioStream from av.video.stream import VideoStream -import av from .common import Image, TestCase, fate_suite - WIDTH = 320 HEIGHT = 240 DURATION = 48 diff --git a/tests/test_enums.py b/tests/test_enums.py index bc8385f5e..39bf856ad 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -4,7 +4,6 @@ from .common import TestCase - # This must be at the top-level. PickleableFooBar = define_enum("PickleableFooBar", __name__, [("FOO", 1)]) diff --git a/tests/test_file_probing.py b/tests/test_file_probing.py index 62e842f25..c3d239c50 100644 --- a/tests/test_file_probing.py +++ b/tests/test_file_probing.py @@ -1,5 +1,5 @@ -from fractions import Fraction import warnings +from fractions import Fraction import av diff --git a/tests/test_filters.py b/tests/test_filters.py index 31cbb46f6..dd090fe1d 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -1,13 +1,13 @@ +import errno from fractions import Fraction from unittest import SkipTest -import errno import numpy as np +import av from av import AudioFrame, VideoFrame from av.audio.frame import format_dtypes from av.filter import Filter, Graph -import av from .common import Image, TestCase, fate_suite diff --git a/tests/test_subtitles.py b/tests/test_subtitles.py index 04e613203..fc0bf9d8e 100644 --- a/tests/test_subtitles.py +++ b/tests/test_subtitles.py @@ -1,5 +1,5 @@ -from av.subtitles.subtitle import AssSubtitle, BitmapSubtitle import av +from av.subtitles.subtitle import AssSubtitle, BitmapSubtitle from .common import TestCase, fate_suite diff --git a/tests/test_timeout.py b/tests/test_timeout.py index 7463c0b18..cf6e7f0f8 100644 --- a/tests/test_timeout.py +++ b/tests/test_timeout.py @@ -1,13 +1,12 @@ -from http.server import BaseHTTPRequestHandler -from socketserver import TCPServer import threading import time +from http.server import BaseHTTPRequestHandler +from socketserver import TCPServer import av from .common import TestCase, fate_suite - PORT = 8002 CONTENT = open(fate_suite("mpeg2/mpeg2_field_encoding.ts"), "rb").read() # Needs to be long enough for all host OSes to deal. From 3898919569d3ad68a5bea02fea53fbb03d6471b4 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 28 Feb 2024 00:23:58 -0500 Subject: [PATCH 194/846] Add type stubs for subtitles dir --- av/codec/context.pyi | 4 +++- av/packet.pyi | 6 +++++- av/subtitles/stream.pyi | 3 +++ av/subtitles/subtitle.pyi | 38 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 av/subtitles/stream.pyi create mode 100644 av/subtitles/subtitle.pyi diff --git a/av/codec/context.pyi b/av/codec/context.pyi index 6ef407e6d..e163dd7f4 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -72,4 +72,6 @@ class CodecContext: def close(self, strict: bool = True) -> None: ... @staticmethod def create(codec, mode=None) -> CodecContext: ... - def parse(self, raw_input=None) -> list: ... + def parse( + self, raw_input: bytes | bytearray | memoryview | None = None + ) -> list[Packet]: ... diff --git a/av/packet.pyi b/av/packet.pyi index ee3d2f376..690dc8374 100644 --- a/av/packet.pyi +++ b/av/packet.pyi @@ -1,4 +1,7 @@ from fractions import Fraction +from typing import Iterator + +from av.subtitles.subtitle import SubtitleSet from .stream import Stream @@ -14,4 +17,5 @@ class Packet: is_keyframe: bool is_corrupt: bool - def __init__(self, input: int | None = None): ... + def __init__(self, input: int | None = None) -> None: ... + def decode(self) -> Iterator[SubtitleSet]: ... diff --git a/av/subtitles/stream.pyi b/av/subtitles/stream.pyi new file mode 100644 index 000000000..38f4ae6cb --- /dev/null +++ b/av/subtitles/stream.pyi @@ -0,0 +1,3 @@ +from av.stream import Stream + +class SubtitleStream(Stream): ... diff --git a/av/subtitles/subtitle.pyi b/av/subtitles/subtitle.pyi new file mode 100644 index 000000000..4b68aeb66 --- /dev/null +++ b/av/subtitles/subtitle.pyi @@ -0,0 +1,38 @@ +from typing import Any, Iterator, Literal + +from .stream import SubtitleStream + +class SubtitleSet: + format: int + start_display_time: int + end_display_time: int + pts: int + + def __len__(self) -> int: ... + def __iter__(self) -> Iterator[Subtitle]: ... + def __getitem__(self, i: int) -> Subtitle: ... + +class Subtitle: + type: Literal["none", "bitmap", "text", "ass"] + +class BitmapSubtitle(Subtitle): + type: Literal["bitmap"] + x: int + y: int + width: int + height: int + nb_colors: Any + planes: tuple[BitmapSubtitlePlane, ...] + +class BitmapSubtitlePlane: + subtitle: BitmapSubtitle + index: int + buffer_size: int + +class TextSubtitle(Subtitle): + type: Literal["text"] + text: str + +class AssSubtitle(Subtitle): + type: Literal["text"] + ass: str From f24e08f883c41264536c9e4cc4ca5012f51150e1 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 2 Mar 2024 03:57:47 -0500 Subject: [PATCH 195/846] Remove long, doesn't exist in Python 3 --- av/container/input.pyx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/av/container/input.pyx b/av/container/input.pyx index 28867cf9b..47cd98c4d 100644 --- a/av/container/input.pyx +++ b/av/container/input.pyx @@ -244,8 +244,9 @@ cdef class InputContainer(Container): # We used to take floats here and assume they were in seconds. This # was super confusing, so lets go in the complete opposite direction # and reject non-ints. - if not isinstance(offset, (int, long)): + if not isinstance(offset, int): raise TypeError("Container.seek only accepts integer offset.", type(offset)) + cdef int64_t c_offset = offset cdef int flags = 0 From bc4b7a8fc85d5364d6049a3f2085f26ad1e7f92c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 2 Mar 2024 05:10:09 -0500 Subject: [PATCH 196/846] Improve InputContainer.decode type stubs --- av/container/input.pyi | 15 ++++++++++++--- av/container/output.pyi | 1 + 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/av/container/input.pyi b/av/container/input.pyi index af2e4265f..d20dce5e2 100644 --- a/av/container/input.pyi +++ b/av/container/input.pyi @@ -1,8 +1,10 @@ from typing import Iterator, Literal, overload +from av.audio.frame import AudioFrame from av.audio.stream import AudioStream -from av.frame import Frame from av.packet import Packet +from av.subtitles.stream import SubtitleStream +from av.subtitles.subtitle import SubtitleSet from av.video.frame import VideoFrame from av.video.stream import VideoStream @@ -13,12 +15,19 @@ class InputContainer(Container): bit_rate: int size: int + def __enter__(self) -> InputContainer: ... def close(self) -> None: ... def demux(self, *args, **kwargs) -> Iterator[Packet]: ... @overload - def decode(self, *args: VideoStream, **kwargs) -> Iterator[VideoFrame]: ... + def decode(self, *args: VideoStream) -> Iterator[VideoFrame]: ... @overload - def decode(self, *args, **kwargs) -> Iterator[Frame]: ... + def decode(self, *args: AudioStream) -> Iterator[AudioFrame]: ... + @overload + def decode(self, *args: SubtitleStream) -> Iterator[SubtitleSet]: ... + @overload + def decode( + self, *args, **kwargs + ) -> Iterator[VideoFrame | AudioFrame | SubtitleSet]: ... def seek( self, offset: int, diff --git a/av/container/output.pyi b/av/container/output.pyi index 854b9f12c..18ec7a8c6 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -1,4 +1,5 @@ from .core import Container class OutputContainer(Container): + def __enter__(self) -> OutputContainer: ... def start_encoding(self) -> None: ... From 288db200082c19c4e396d6b3f2d31c44fa7e9640 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 2 Mar 2024 19:47:36 -0500 Subject: [PATCH 197/846] Bump to next release candidate --- av/about.py | 2 +- docs/Makefile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/av/about.py b/av/about.py index 5b461163e..282214405 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "11.0.0" +__version__ = "12.0.0rc1" diff --git a/docs/Makefile b/docs/Makefile index b1791159f..9ebbf3c5d 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -38,5 +38,5 @@ open: open _build/html/index.html upload: - rsync -avxP --delete _build/html/ root@basswood-io.com:/var/www/pyav + rsync -avxP --delete _build/html/ root@basswood-io.com:/var/www/pyav/docs/develop From c47d01be1737087ce4a52fc9f5dc6c8c910be451 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 2 Mar 2024 20:02:22 -0500 Subject: [PATCH 198/846] Remove deprecated Packet.decode_one --- av/packet.pyx | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/av/packet.pyx b/av/packet.pyx index fee1b05d2..116fab6b7 100644 --- a/av/packet.pyx +++ b/av/packet.pyx @@ -4,8 +4,6 @@ from av.bytesource cimport bytesource from av.error cimport err_check from av.utils cimport avrational_to_fraction, to_avrational -from av import deprecation - cdef class Packet(Buffer): @@ -81,19 +79,6 @@ cdef class Packet(Buffer): """ return self._stream.decode(self) - @deprecation.method - def decode_one(self): - """ - Send the packet's data to the decoder and return the first decoded frame. - - Returns ``None`` if there is no frame. - - .. warning:: This method is deprecated, as it silently discards any - other frames which were decoded. - """ - res = self._stream.decode(self) - return res[0] if res else None - @property def stream_index(self): return self.ptr.stream_index From 18bc34b5fc36542919967853992235e1a1322537 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 3 Mar 2024 16:42:08 -0500 Subject: [PATCH 199/846] Add color_primaries, color_trc, colorspace --- av/video/codeccontext.pyi | 4 +++ av/video/codeccontext.pyx | 58 +++++++++++++++++++++++++++++++++- docs/conf.py | 9 ++++-- include/libavcodec/avcodec.pxd | 5 +++ include/libavutil/avutil.pxd | 43 +++++++++++++++++++++++++ tests/test_colorspace.py | 21 +++++++++++- 6 files changed, 136 insertions(+), 4 deletions(-) diff --git a/av/video/codeccontext.pyi b/av/video/codeccontext.pyi index e6ecbcbd4..a749ad3f9 100644 --- a/av/video/codeccontext.pyi +++ b/av/video/codeccontext.pyi @@ -20,6 +20,10 @@ class VideoCodecContext(CodecContext): coded_width: int coded_height: int color_range: int + color_primaries: int + color_trc: int + colorspace: int + type: Literal["video"] def encode(self, frame: VideoFrame | None = None) -> list[Packet]: ... def encode_lazy(self, frame: VideoFrame | None = None) -> Iterator[Packet]: ... diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.pyx index 8fe5c276e..2e101d935 100644 --- a/av/video/codeccontext.pyx +++ b/av/video/codeccontext.pyx @@ -177,22 +177,33 @@ cdef class VideoCodecContext(CodecContext): @property def has_b_frames(self): + """ + :type: bool + """ return bool(self.ptr.has_b_frames) @property def coded_width(self): + """ + :type: int + """ return self.ptr.coded_width @property def coded_height(self): + """ + :type: int + """ return self.ptr.coded_height @property def color_range(self): """ - Color range of context. + Describes the signal range of the colorspace. Wraps :ffmpeg:`AVFrame.color_range`. + + :type: int """ return self.ptr.color_range @@ -200,6 +211,51 @@ cdef class VideoCodecContext(CodecContext): def color_range(self, value): self.ptr.color_range = value + @property + def color_primaries(self): + """ + Describes the RGB/XYZ matrix of the colorspace. + + Wraps :ffmpeg:`AVFrame.color_primaries`. + + :type: int + """ + return self.ptr.color_primaries + + @color_primaries.setter + def color_primaries(self, value): + self.ptr.color_primaries = value + + @property + def color_trc(self): + """ + Describes the linearization function (a.k.a. transformation characteristics) of the colorspace. + + Wraps :ffmpeg:`AVFrame.color_trc`. + + :type: int + """ + return self.ptr.color_trc + + @color_trc.setter + def color_trc(self, value): + self.ptr.color_trc = value + + @property + def colorspace(self): + """ + Describes the YUV/RGB transformation matrix of the colorspace. + + Wraps :ffmpeg:`AVFrame.colorspace`. + + :type: int + """ + return self.ptr.colorspace + + @colorspace.setter + def colorspace(self, value): + self.ptr.colorspace = value + @property def max_b_frames(self): """ diff --git a/docs/conf.py b/docs/conf.py index 8f17daa83..a893a631c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -332,8 +332,13 @@ def _doxylink_handler(name, rawtext, text, lineno, inliner, options={}, content= url = get_url(name) if not url: - print("ERROR: Could not find", name) - exit(1) + if name == "AVFrame.color_primaries": + url = "structAVFrame.html#a59a3f830494f2ed1133103a1bc9481e7" + elif name == "AVFrame.color_trc": + url = "structAVFrame.html#ab09abb126e3922bc1d010cf044087939" + else: + print("ERROR: Could not find", name) + exit(1) node = addnodes.literal_strong(title, title) if url: diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 1daeac15b..7d841559d 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -205,6 +205,9 @@ cdef extern from "libavcodec/avcodec.h" nogil: int max_b_frames int has_b_frames AVColorRange color_range + AVColorPrimaries color_primaries + AVColorTransferCharacteristic color_trc + AVColorSpace colorspace # Audio. AVSampleFormat sample_fmt @@ -359,6 +362,8 @@ cdef extern from "libavcodec/avcodec.h" nogil: int flags int decode_error_flags AVColorRange color_range + AVColorPrimaries color_primaries + AVColorTransferCharacteristic color_trc AVColorSpace colorspace cdef AVFrame* avcodec_alloc_frame() diff --git a/include/libavutil/avutil.pxd b/include/libavutil/avutil.pxd index 47941b2cf..f9af7a7b0 100644 --- a/include/libavutil/avutil.pxd +++ b/include/libavutil/avutil.pxd @@ -62,6 +62,49 @@ cdef extern from "libavutil/avutil.h" nogil: AVCOL_RANGE_JPEG AVCOL_RANGE_NB + cdef enum AVColorPrimaries: + AVCOL_PRI_RESERVED0 + AVCOL_PRI_BT709 + AVCOL_PRI_UNSPECIFIED + AVCOL_PRI_RESERVED + AVCOL_PRI_BT470M + AVCOL_PRI_BT470BG + AVCOL_PRI_SMPTE170M + AVCOL_PRI_SMPTE240M + AVCOL_PRI_FILM + AVCOL_PRI_BT2020 + AVCOL_PRI_SMPTE428 + AVCOL_PRI_SMPTEST428_1 + AVCOL_PRI_SMPTE431 + AVCOL_PRI_SMPTE432 + AVCOL_PRI_EBU3213 + AVCOL_PRI_JEDEC_P22 + AVCOL_PRI_NB + + cdef enum AVColorTransferCharacteristic: + AVCOL_TRC_RESERVED0 + AVCOL_TRC_BT709 + AVCOL_TRC_UNSPECIFIED + AVCOL_TRC_RESERVED + AVCOL_TRC_GAMMA22 + AVCOL_TRC_GAMMA28 + AVCOL_TRC_SMPTE170M + AVCOL_TRC_SMPTE240M + AVCOL_TRC_LINEAR + AVCOL_TRC_LOG + AVCOL_TRC_LOG_SQRT + AVCOL_TRC_IEC61966_2_4 + AVCOL_TRC_BT1361_ECG + AVCOL_TRC_IEC61966_2_1 + AVCOL_TRC_BT2020_10 + AVCOL_TRC_BT2020_12 + AVCOL_TRC_SMPTE2084 + AVCOL_TRC_SMPTEST2084 + AVCOL_TRC_SMPTE428 + AVCOL_TRC_SMPTEST428_1 + AVCOL_TRC_ARIB_STD_B67 + AVCOL_TRC_NB + cdef double M_PI cdef void* av_malloc(size_t size) diff --git a/tests/test_colorspace.py b/tests/test_colorspace.py index e1c4ee8b1..99c9f2fa2 100644 --- a/tests/test_colorspace.py +++ b/tests/test_colorspace.py @@ -5,16 +5,35 @@ class TestColorSpace(TestCase): - def test_color_range(self): + def test_penguin_joke(self): container = av.open( fate_suite("amv/MTV_high_res_320x240_sample_Penguin_Joke_MTV_from_WMV.amv") ) stream = container.streams.video[0] self.assertEqual(stream.codec_context.color_range, 2) + self.assertEqual(stream.codec_context.color_range, ColorRange.JPEG) + + self.assertEqual(stream.codec_context.color_primaries, 2) + self.assertEqual(stream.codec_context.color_trc, 2) + + self.assertEqual(stream.codec_context.colorspace, 5) + self.assertEqual(stream.codec_context.colorspace, Colorspace.ITU601) for packet in container.demux(stream): for frame in packet.decode(): self.assertEqual(frame.color_range, ColorRange.JPEG) # a.k.a "pc" self.assertEqual(frame.colorspace, Colorspace.ITU601) return + + def test_sky_timelapse(self): + container = av.open( + av.datasets.curated("pexels/time-lapse-video-of-night-sky-857195.mp4") + ) + stream = container.streams.video[0] + + self.assertEqual(stream.codec_context.color_range, 1) + self.assertEqual(stream.codec_context.color_range, ColorRange.MPEG) + self.assertEqual(stream.codec_context.color_primaries, 1) + self.assertEqual(stream.codec_context.color_trc, 1) + self.assertEqual(stream.codec_context.colorspace, 1) From fc8d60aacad8e30226f1bc23d509e0bc1a87dffa Mon Sep 17 00:00:00 2001 From: laggykiller Date: Mon, 4 Mar 2024 06:56:10 +0800 Subject: [PATCH 200/846] Fix typo in stream.pyx --- av/stream.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/stream.pyx b/av/stream.pyx index 225255d7f..3f40f21d8 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -180,7 +180,7 @@ cdef class Stream: return self.codec_context.decode(packet) cdef _get_side_data(self, lib.AVStream *stream): - # Get DISPLAYMATRIX SideDate from a lib.AVStream object. + # Get DISPLAYMATRIX SideData from a lib.AVStream object. # Returns: tuple[int, dict[str, Any]] nb_side_data = stream.nb_side_data From 1a1a6e21bb61f301ab0e5a207316473cf0e62a28 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Mon, 4 Mar 2024 06:54:21 +0800 Subject: [PATCH 201/846] Add missing stub in av.filter --- av/filter/link.pyi | 5 +++++ av/filter/pad.pyi | 10 ++++++++++ 2 files changed, 15 insertions(+) create mode 100644 av/filter/link.pyi create mode 100644 av/filter/pad.pyi diff --git a/av/filter/link.pyi b/av/filter/link.pyi new file mode 100644 index 000000000..dd420ad91 --- /dev/null +++ b/av/filter/link.pyi @@ -0,0 +1,5 @@ +from .pad import FilterContextPad + +class FilterLink: + input: FilterContextPad + output: FilterContextPad diff --git a/av/filter/pad.pyi b/av/filter/pad.pyi new file mode 100644 index 000000000..1a6c9bda6 --- /dev/null +++ b/av/filter/pad.pyi @@ -0,0 +1,10 @@ +from .link import FilterLink + +class FilterPad: + is_output: bool + name: str + type: str + +class FilterContextPad(FilterPad): + link: FilterLink | None + linked: FilterContextPad | None From ef97595d3f3be2ef60a72fc247a59255835ffa1d Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 3 Mar 2024 20:33:45 -0500 Subject: [PATCH 202/846] Fix type stubs in subtitle.pyi --- av/subtitles/subtitle.pyi | 14 ++++++-------- tests/test_subtitles.py | 3 +++ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/av/subtitles/subtitle.pyi b/av/subtitles/subtitle.pyi index 4b68aeb66..0363c2399 100644 --- a/av/subtitles/subtitle.pyi +++ b/av/subtitles/subtitle.pyi @@ -1,6 +1,4 @@ -from typing import Any, Iterator, Literal - -from .stream import SubtitleStream +from typing import Iterator, Literal class SubtitleSet: format: int @@ -13,15 +11,15 @@ class SubtitleSet: def __getitem__(self, i: int) -> Subtitle: ... class Subtitle: - type: Literal["none", "bitmap", "text", "ass"] + type: Literal[b"none", b"bitmap", b"text", b"ass"] class BitmapSubtitle(Subtitle): - type: Literal["bitmap"] + type: Literal[b"bitmap"] x: int y: int width: int height: int - nb_colors: Any + nb_colors: int planes: tuple[BitmapSubtitlePlane, ...] class BitmapSubtitlePlane: @@ -30,9 +28,9 @@ class BitmapSubtitlePlane: buffer_size: int class TextSubtitle(Subtitle): - type: Literal["text"] + type: Literal[b"text"] text: str class AssSubtitle(Subtitle): - type: Literal["text"] + type: Literal[b"ass"] ass: str diff --git a/tests/test_subtitles.py b/tests/test_subtitles.py index fc0bf9d8e..c457124f2 100644 --- a/tests/test_subtitles.py +++ b/tests/test_subtitles.py @@ -23,6 +23,7 @@ def test_movtext(self): sub = subset[0] self.assertIsInstance(sub, AssSubtitle) + self.assertEqual(sub.type, b"ass") self.assertEqual(sub.ass, "0,0,Default,,0,0,0,,- Test 1.\\N- Test 2.") def test_vobsub(self): @@ -43,10 +44,12 @@ def test_vobsub(self): sub = subset[0] self.assertIsInstance(sub, BitmapSubtitle) + self.assertEqual(sub.type, b"bitmap") self.assertEqual(sub.x, 259) self.assertEqual(sub.y, 379) self.assertEqual(sub.width, 200) self.assertEqual(sub.height, 24) + self.assertEqual(sub.nb_colors, 4) bms = sub.planes self.assertEqual(len(bms), 1) From 956acfb27f40cb63da7f1df51b7642172cf13826 Mon Sep 17 00:00:00 2001 From: laggykiller <61652821+laggykiller@users.noreply.github.com> Date: Mon, 4 Mar 2024 22:54:58 +0800 Subject: [PATCH 203/846] Add codec context subtitle stub --- av/subtitles/codeccontext.pyi | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 av/subtitles/codeccontext.pyi diff --git a/av/subtitles/codeccontext.pyi b/av/subtitles/codeccontext.pyi new file mode 100644 index 000000000..0762c19f0 --- /dev/null +++ b/av/subtitles/codeccontext.pyi @@ -0,0 +1,6 @@ +from typing import Literal + +from av.codec.context import CodecContext + +class SubtitleCodecContext(CodecContext): + type: Literal["subtitle"] From 34359e23ead403af302282d8f069455de7a6eced Mon Sep 17 00:00:00 2001 From: laggykiller <61652821+laggykiller@users.noreply.github.com> Date: Mon, 4 Mar 2024 23:20:32 +0800 Subject: [PATCH 204/846] Add stub for buffer, descriptor, plane --- av/buffer.pyi | 4 ++++ av/descriptor.pyi | 7 +++++++ av/option.pyi | 25 ++++++++++++++++++++++++- av/plane.pyi | 8 ++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 av/buffer.pyi create mode 100644 av/descriptor.pyi create mode 100644 av/plane.pyi diff --git a/av/buffer.pyi b/av/buffer.pyi new file mode 100644 index 000000000..41db73e03 --- /dev/null +++ b/av/buffer.pyi @@ -0,0 +1,4 @@ +class Buffer: + buffer_size: int + buffer_ptr: int + def update(self, input: bytes) -> None: ... diff --git a/av/descriptor.pyi b/av/descriptor.pyi new file mode 100644 index 000000000..ae1998391 --- /dev/null +++ b/av/descriptor.pyi @@ -0,0 +1,7 @@ +from typing import NoReturn + +from .option import Option + +class Descriptor: + name: str + options: tuple[Option, ...] diff --git a/av/option.pyi b/av/option.pyi index 14ceed99b..b7ba670f2 100644 --- a/av/option.pyi +++ b/av/option.pyi @@ -1,4 +1,4 @@ -from av.enum import EnumFlag, EnumItem +from .enum import EnumFlag, EnumItem class OptionType(EnumItem): FLAGS: int @@ -29,3 +29,26 @@ class OptionFlags(EnumFlag): EXPORT: int READONLY: int FILTERING_PARAM: int + +class BaseOption: + name: str + help: str + flags: int + is_encoding_param: bool + is_decoding_param: bool + is_audio_param: bool + is_video_param: bool + is_subtitle_param: bool + is_export: bool + is_readonly: bool + is_filtering_param: bool + +class Option(BaseOption): + type: OptionType + offset: int + default: int + min: int + max: int + +class OptionChoice(BaseOption): + value: int diff --git a/av/plane.pyi b/av/plane.pyi new file mode 100644 index 000000000..99594d11a --- /dev/null +++ b/av/plane.pyi @@ -0,0 +1,8 @@ +from .buffer import Buffer +from .frame import Frame + +class Plane(Buffer): + frame: Frame + index: int + + def __init__(self, frame: Frame, index: int) -> None: ... From 4962f0d5a9e1325ee53380f945b66d0e257d2156 Mon Sep 17 00:00:00 2001 From: laggykiller <61652821+laggykiller@users.noreply.github.com> Date: Tue, 5 Mar 2024 04:04:25 +0800 Subject: [PATCH 205/846] Improve subs in av/codec/codec and av/codec/context --- av/codec/codec.pyi | 10 +++++++++- av/codec/context.pyi | 9 ++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/av/codec/codec.pyi b/av/codec/codec.pyi index a992b6128..572fa3149 100644 --- a/av/codec/codec.pyi +++ b/av/codec/codec.pyi @@ -2,9 +2,12 @@ from fractions import Fraction from typing import Literal from av.audio.format import AudioFormat +from av.descriptor import Descriptor from av.enum import EnumFlag from av.video.format import VideoFormat +from .context import CodecContext + class Properties(EnumFlag): NONE: int INTRA_ONLY: int @@ -41,15 +44,20 @@ class UnknownCodecError(ValueError): ... class Codec: is_decoder: bool + descriptor: Descriptor name: str long_name: str type: Literal["video", "audio", "data", "subtitle", "attachment"] + id: int frame_rates: list[Fraction] | None audio_rates: list[int] | None video_formats: list[VideoFormat] | None audio_formats: list[AudioFormat] | None + properties: Properties + capabilities: Capabilities - def __init__(self, name: str, mode: Literal["r", "w"]): ... + def __init__(self, name: str, mode: Literal["r", "w"]) -> None: ... + def create(self) -> CodecContext: ... class codec_descriptor: name: str diff --git a/av/codec/context.pyi b/av/codec/context.pyi index e163dd7f4..ee81daae5 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -1,9 +1,10 @@ -from typing import Any, Iterator, Literal +from typing import Any, Literal from av.enum import EnumFlag, EnumItem -from av.frame import Frame from av.packet import Packet +from .codec import Codec + class ThreadType(EnumFlag): NONE: int FRAME: int @@ -71,7 +72,9 @@ class CodecContext: def open(self, strict: bool = True) -> None: ... def close(self, strict: bool = True) -> None: ... @staticmethod - def create(codec, mode=None) -> CodecContext: ... + def create( + codec: str | Codec, mode: Literal["r", "w"] | None = None + ) -> CodecContext: ... def parse( self, raw_input: bytes | bytearray | memoryview | None = None ) -> list[Packet]: ... From 584e5fe9e7a34b8fd037bb06af4e63b0f738361f Mon Sep 17 00:00:00 2001 From: laggykiller <61652821+laggykiller@users.noreply.github.com> Date: Tue, 5 Mar 2024 12:01:16 +0800 Subject: [PATCH 206/846] Improve stub in av.audio --- av/audio/codeccontext.pyi | 3 --- av/audio/format.pyi | 6 ++---- av/audio/frame.pyi | 17 ++++++++++++++--- av/audio/layout.pyi | 5 +++-- av/audio/plane.pyi | 4 +++- av/audio/resampler.pyi | 13 ++++++++----- av/audio/stream.pyi | 13 ++++++++++++- 7 files changed, 42 insertions(+), 19 deletions(-) diff --git a/av/audio/codeccontext.pyi b/av/audio/codeccontext.pyi index 6dcf039fa..ed478a906 100644 --- a/av/audio/codeccontext.pyi +++ b/av/audio/codeccontext.pyi @@ -1,11 +1,8 @@ from typing import Literal from av.codec.context import CodecContext -from av.frame import Frame -from av.packet import Packet from .format import AudioFormat -from .frame import AudioFrame from .layout import AudioLayout class AudioCodecContext(CodecContext): diff --git a/av/audio/format.pyi b/av/audio/format.pyi index 2a2519008..bb4a1e603 100644 --- a/av/audio/format.pyi +++ b/av/audio/format.pyi @@ -1,11 +1,9 @@ -from typing import Any - class AudioFormat: name: str bytes: int bits: int is_planar: bool is_packed: bool - planar: Any - packed: Any + planar: AudioFormat + packed: AudioFormat container_name: str diff --git a/av/audio/frame.pyi b/av/audio/frame.pyi index 7f78535bf..777c3dd70 100644 --- a/av/audio/frame.pyi +++ b/av/audio/frame.pyi @@ -1,8 +1,19 @@ +from typing import Any, Union + +import numpy as np + from av.frame import Frame from .plane import AudioPlane format_dtypes: dict[str, str] +_SupportedNDarray = Union[ + np.ndarray[Any, np.dtype[np.float64]], # f8 + np.ndarray[Any, np.dtype[np.float32]], # f4 + np.ndarray[Any, np.dtype[np.int32]], # i4 + np.ndarray[Any, np.dtype[np.int16]], # i2 + np.ndarray[Any, np.dtype[np.uint8]], # u1 +] class AudioFrame(Frame): planes: tuple[AudioPlane, ...] @@ -16,9 +27,9 @@ class AudioFrame(Frame): layout: str = "stereo", samples: int = 0, align: int = 1, - ): ... - def to_ndarray(self): ... + ) -> None: ... @staticmethod def from_ndarray( - array, format: str = "s16", layout: str = "stereo" + array: _SupportedNDarray, format: str = "s16", layout: str = "stereo" ) -> AudioFrame: ... + def to_ndarray(self) -> _SupportedNDarray: ... diff --git a/av/audio/layout.pyi b/av/audio/layout.pyi index 600ffeef5..338532e1a 100644 --- a/av/audio/layout.pyi +++ b/av/audio/layout.pyi @@ -1,10 +1,11 @@ channel_descriptions: dict[str, str] class AudioLayout: + name: str layout: int nb_channels: int - channels: tuple[AudioChannels] + channels: tuple[AudioChannel, ...] -class AudioChannels: +class AudioChannel: name: str description: str diff --git a/av/audio/plane.pyi b/av/audio/plane.pyi index c9a190bf1..64524dcdb 100644 --- a/av/audio/plane.pyi +++ b/av/audio/plane.pyi @@ -1,2 +1,4 @@ -class AudioPlane: +from av.plane import Plane + +class AudioPlane(Plane): buffer_size: int diff --git a/av/audio/resampler.pyi b/av/audio/resampler.pyi index 7c2033e83..32861258e 100644 --- a/av/audio/resampler.pyi +++ b/av/audio/resampler.pyi @@ -1,17 +1,20 @@ +from av.filter.graph import Graph + from .format import AudioFormat from .frame import AudioFrame +from .layout import AudioLayout class AudioResampler: rate: int frame_size: int format: AudioFormat - graph: None + graph: Graph | None def __init__( self, - format=None, - layout=None, + format: AudioFormat | None = None, + layout: AudioLayout | None = None, rate: int | None = None, frame_size: int | None = None, - ): ... - def resample(self, frame: AudioFrame | None) -> list: ... + ) -> None: ... + def resample(self, frame: AudioFrame | None) -> list[AudioFrame]: ... diff --git a/av/audio/stream.pyi b/av/audio/stream.pyi index 59c259613..c85f15787 100644 --- a/av/audio/stream.pyi +++ b/av/audio/stream.pyi @@ -1,3 +1,14 @@ +from typing import Literal + +from av.packet import Packet from av.stream import Stream -class AudioStream(Stream): ... +from .format import AudioFormat +from .frame import AudioFrame + +class AudioStream(Stream): + format: AudioFormat + type: Literal["audio"] + + def encode(self, frame: AudioFrame | None = None) -> list[Packet]: ... + def decode(self, packet: Packet | None = None) -> list[AudioFrame]: ... From 57b8b611817ea437660c9530dd4face430a0da1e Mon Sep 17 00:00:00 2001 From: laggykiller Date: Tue, 5 Mar 2024 12:32:06 +0800 Subject: [PATCH 207/846] Add codec_context typing for AudioStream --- av/audio/stream.pyi | 2 ++ 1 file changed, 2 insertions(+) diff --git a/av/audio/stream.pyi b/av/audio/stream.pyi index c85f15787..00f4fd04f 100644 --- a/av/audio/stream.pyi +++ b/av/audio/stream.pyi @@ -3,11 +3,13 @@ from typing import Literal from av.packet import Packet from av.stream import Stream +from .codeccontext import AudioCodecContext from .format import AudioFormat from .frame import AudioFrame class AudioStream(Stream): format: AudioFormat + codec_context: AudioCodecContext type: Literal["audio"] def encode(self, frame: AudioFrame | None = None) -> list[Packet]: ... From a7202a7e56de16553ef688995f3e4514ca6f0291 Mon Sep 17 00:00:00 2001 From: laggykiller <61652821+laggykiller@users.noreply.github.com> Date: Tue, 5 Mar 2024 13:04:29 +0800 Subject: [PATCH 208/846] Improve stubs in av.container --- av/container/core.pyi | 46 ++++++++++++++++++++------- av/container/input.pyi | 11 ++++--- av/container/output.pyi | 16 ++++++++++ av/container/streams.pyi | 69 +++++----------------------------------- 4 files changed, 66 insertions(+), 76 deletions(-) diff --git a/av/container/core.pyi b/av/container/core.pyi index 81d8c3d2e..c33f3a4dc 100644 --- a/av/container/core.pyi +++ b/av/container/core.pyi @@ -1,11 +1,31 @@ from numbers import Real from pathlib import Path -from typing import Any, Iterator, Literal, overload +from types import TracebackType +from typing import Any, Callable, Literal, Type, overload + +from av.enum import EnumFlag from .input import InputContainer from .output import OutputContainer from .streams import StreamContainer +class Flags(EnumFlag): + GENPTS: int + IGNIDX: int + NONBLOCK: int + IGNDTS: int + NOFILLIN: int + NOPARSE: int + NOBUFFER: int + CUSTOM_IO: int + DISCARD_CORRUPT: int + FLUSH_PACKETS: int + BITEXACT: int + SORT_DTS: int + FAST_SEEK: int + SHORTEST: int + AUTO_BSF: int + class Container: writeable: bool name: str @@ -21,13 +41,17 @@ class Container: container_options: dict[str, str] stream_options: list[str] streams: StreamContainer - duration: int | None metadata: dict[str, str] open_timeout: Real | None read_timeout: Real | None def __enter__(self) -> Container: ... - def __exit__(self, exc_type, exc_val, exc_tb): ... + def __exit__( + self, + exc_type: Type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool: ... def err_check(self, value: int) -> int: ... def set_timeout(self, timeout: Real | None) -> None: ... def start_timeout(self) -> None: ... @@ -43,8 +67,8 @@ def open( metadata_encoding: str = "utf-8", metadata_errors: str = "strict", buffer_size: int = 32768, - timeout=Real | None | tuple[Real | None, Real | None], - io_open=None, + timeout: Real | None | tuple[Real | None, Real | None] = None, + io_open: Callable[..., Any] | None = None, ) -> InputContainer: ... @overload def open( @@ -57,8 +81,8 @@ def open( metadata_encoding: str = "utf-8", metadata_errors: str = "strict", buffer_size: int = 32768, - timeout=Real | None | tuple[Real | None, Real | None], - io_open=None, + timeout: Real | None | tuple[Real | None, Real | None] = None, + io_open: Callable[..., Any] | None = None, ) -> InputContainer: ... @overload def open( @@ -71,8 +95,8 @@ def open( metadata_encoding: str = "utf-8", metadata_errors: str = "strict", buffer_size: int = 32768, - timeout=Real | None | tuple[Real | None, Real | None], - io_open=None, + timeout: Real | None | tuple[Real | None, Real | None] = None, + io_open: Callable[..., Any] | None = None, ) -> OutputContainer: ... @overload def open( @@ -85,6 +109,6 @@ def open( metadata_encoding: str = "utf-8", metadata_errors: str = "strict", buffer_size: int = 32768, - timeout=Real | None | tuple[Real | None, Real | None], - io_open=None, + timeout: Real | None | tuple[Real | None, Real | None] = None, + io_open: Callable[..., Any] | None = None, ) -> InputContainer | OutputContainer: ... diff --git a/av/container/input.pyi b/av/container/input.pyi index d20dce5e2..2c1328943 100644 --- a/av/container/input.pyi +++ b/av/container/input.pyi @@ -1,23 +1,25 @@ -from typing import Iterator, Literal, overload +from typing import Any, Iterator, Literal, overload from av.audio.frame import AudioFrame from av.audio.stream import AudioStream from av.packet import Packet +from av.stream import Stream from av.subtitles.stream import SubtitleStream from av.subtitles.subtitle import SubtitleSet from av.video.frame import VideoFrame from av.video.stream import VideoStream from .core import Container -from .streams import Stream class InputContainer(Container): + start_time: int + duration: int | None bit_rate: int size: int def __enter__(self) -> InputContainer: ... def close(self) -> None: ... - def demux(self, *args, **kwargs) -> Iterator[Packet]: ... + def demux(self, *args: Any, **kwargs: Any) -> Iterator[Packet]: ... @overload def decode(self, *args: VideoStream) -> Iterator[VideoFrame]: ... @overload @@ -26,7 +28,7 @@ class InputContainer(Container): def decode(self, *args: SubtitleStream) -> Iterator[SubtitleSet]: ... @overload def decode( - self, *args, **kwargs + self, *args: Any, **kwargs: Any ) -> Iterator[VideoFrame | AudioFrame | SubtitleSet]: ... def seek( self, @@ -39,3 +41,4 @@ class InputContainer(Container): unsupported_frame_offset: bool = False, unsupported_byte_offset: bool = False, ) -> None: ... + def flush_buffers(self) -> None: ... diff --git a/av/container/output.pyi b/av/container/output.pyi index 18ec7a8c6..0400dadbb 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -1,5 +1,21 @@ +from fractions import Fraction +from typing import Sequence + +from av.packet import Packet +from av.stream import Stream + from .core import Container class OutputContainer(Container): def __enter__(self) -> OutputContainer: ... + def add_stream( + self, + codec_name: str | None = None, + rate: Fraction | int | float | None = None, + template: Stream | None = None, + options: dict[str, str] | None = None, + ) -> Stream: ... def start_encoding(self) -> None: ... + def close(self) -> None: ... + def mux(self, packets: Sequence[Packet]) -> None: ... + def mux_one(self, packet: Packet) -> None: ... diff --git a/av/container/streams.pyi b/av/container/streams.pyi index be01100b8..cb658d35f 100644 --- a/av/container/streams.pyi +++ b/av/container/streams.pyi @@ -1,69 +1,16 @@ -from fractions import Fraction -from typing import Iterator, Literal, overload +from typing import Iterator, overload +from av.audio.stream import AudioStream +from av.data.stream import DataStream +from av.stream import Stream +from av.subtitles.stream import SubtitleStream from av.video.stream import VideoStream -class Codec: - name: str - mode: Literal["r", "w"] - - frame_rates: list[Fraction] | None - audio_rates: list[int] | None - -class CodecContext: - name: str - bit_rate: int | None - width: int - height: int - pix_fmt: str | None - sample_aspect_ratio: Fraction | None - sample_rate: int | None - channels: int - extradata_size: int - is_open: Literal[0, 1] - is_encoder: Literal[0, 1] - is_decoder: Literal[0, 1] - -class Stream: - thread_type: Literal["NONE", "FRAME", "SLICE", "AUTO"] - - id: int - profile: str | None - codec_context: CodecContext - - index: int - time_base: Fraction | None - average_rate: Fraction | None - base_rate: Fraction | None - guessed_rate: Fraction | None - - start_time: int | None - duration: int | None - frames: int - language: str | None - - # Defined by `av_get_media_type_string` at - # https://ffmpeg.org/doxygen/6.0/libavutil_2utils_8c_source.html - type: Literal["video", "audio", "data", "subtitle", "attachment"] - - # From `codec_context` - name: str - bit_rate: int | None - sample_rate: int | None - channels: int - extradata_size: int - is_open: Literal[0, 1] - is_encoder: Literal[0, 1] - is_decoder: Literal[0, 1] - - def decode(self, packet=None): ... - def encode(self, frame=None): ... - class StreamContainer: video: tuple[VideoStream, ...] - audio: tuple[Stream, ...] - subtitles: tuple[Stream, ...] - data: tuple[Stream, ...] + audio: tuple[AudioStream, ...] + subtitles: tuple[SubtitleStream, ...] + data: tuple[DataStream, ...] other: tuple[Stream, ...] def __init__(self) -> None: ... From c50ecfa485fa505373d0fee2d4794b94b1f3e383 Mon Sep 17 00:00:00 2001 From: laggykiller <61652821+laggykiller@users.noreply.github.com> Date: Tue, 5 Mar 2024 13:33:50 +0800 Subject: [PATCH 209/846] Improve stubs in av.filter --- av/filter/context.pyi | 15 ++++++++++++--- av/filter/filter.pyi | 14 +++++++++++--- av/filter/graph.pyi | 16 ++++++++++++---- 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/av/filter/context.pyi b/av/filter/context.pyi index b5433f870..0d70c0095 100644 --- a/av/filter/context.pyi +++ b/av/filter/context.pyi @@ -1,6 +1,15 @@ +from av.frame import Frame + +from .pad import FilterContextPad + class FilterContext: name: str | None + inputs: tuple[FilterContextPad, ...] + outputs: tuple[FilterContextPad, ...] - def init(self, args=None, **kwargs) -> None: ... - def link_to(self, input_, output_idx: int = 0, input_idx: int = 0) -> None: ... - def push(self, frame) -> None: ... + def init(self, args: str | None = None, **kwargs: str | None) -> None: ... + def link_to( + self, input_: FilterContext, output_idx: int = 0, input_idx: int = 0 + ) -> None: ... + def push(self, frame: Frame) -> None: ... + def pull(self) -> Frame: ... diff --git a/av/filter/filter.pyi b/av/filter/filter.pyi index f5b1aac60..f94c79e1c 100644 --- a/av/filter/filter.pyi +++ b/av/filter/filter.pyi @@ -1,13 +1,21 @@ +from av.descriptor import Descriptor +from av.option import Option + +from .pad import FilterPad + class Filter: name: str description: str - options: tuple | None + + descriptor: Descriptor + options: tuple[Option, ...] | None + flags: int dynamic_inputs: bool dynamic_outputs: bool timeline_support: bool slice_threads: bool command_support: bool - inputs: tuple - outputs: tuple + inputs: tuple[FilterPad, ...] + outputs: tuple[FilterPad, ...] filters_available: set[str] diff --git a/av/filter/graph.pyi b/av/filter/graph.pyi index b08d314ec..75930d08a 100644 --- a/av/filter/graph.pyi +++ b/av/filter/graph.pyi @@ -1,7 +1,11 @@ from fractions import Fraction +from typing import Any +from av.audio.format import AudioFormat from av.audio.frame import AudioFrame +from av.audio.layout import AudioLayout from av.audio.stream import AudioStream +from av.video.format import VideoFormat from av.video.frame import VideoFrame from av.video.stream import VideoStream @@ -9,15 +13,19 @@ from .context import FilterContext from .filter import Filter class Graph: + configured: bool + def __init__(self) -> None: ... def configure(self, auto_buffer: bool = True, force: bool = False) -> None: ... - def add(self, filter: str | Filter, args=None, **kwargs: str) -> FilterContext: ... + def add( + self, filter: str | Filter, args: Any = None, **kwargs: str + ) -> FilterContext: ... def add_buffer( self, template: VideoStream | None = None, width: int | None = None, height: int | None = None, - format=None, + format: VideoFormat | None = None, name: str | None = None, time_base: Fraction | None = None, ) -> FilterContext: ... @@ -25,8 +33,8 @@ class Graph: self, template: AudioStream | None = None, sample_rate: int | None = None, - format=None, - layout=None, + format: AudioFormat | None = None, + layout: AudioLayout | None = None, channels: int | None = None, name: str | None = None, time_base: Fraction | None = None, From 1bddc7e8a563f4c11ad885d91679255a39d371e2 Mon Sep 17 00:00:00 2001 From: laggykiller <61652821+laggykiller@users.noreply.github.com> Date: Tue, 5 Mar 2024 13:59:18 +0800 Subject: [PATCH 210/846] Improve stubs in av.video --- av/audio/stream.pyi | 2 +- av/video/format.pyi | 4 ++-- av/video/frame.pyi | 38 ++++++++++++++++++++++++++------------ av/video/plane.pyi | 2 +- av/video/reformatter.pyi | 5 ++--- av/video/stream.pyi | 6 ++++-- 6 files changed, 36 insertions(+), 21 deletions(-) diff --git a/av/audio/stream.pyi b/av/audio/stream.pyi index 00f4fd04f..e14323f2a 100644 --- a/av/audio/stream.pyi +++ b/av/audio/stream.pyi @@ -8,9 +8,9 @@ from .format import AudioFormat from .frame import AudioFrame class AudioStream(Stream): + type: Literal["audio"] format: AudioFormat codec_context: AudioCodecContext - type: Literal["audio"] def encode(self, frame: AudioFrame | None = None) -> list[Packet]: ... def decode(self, packet: Packet | None = None) -> list[AudioFrame]: ... diff --git a/av/video/format.pyi b/av/video/format.pyi index 33aa8d3c2..16739a0e3 100644 --- a/av/video/format.pyi +++ b/av/video/format.pyi @@ -8,7 +8,7 @@ class VideoFormat: is_planar: bool is_rgb: bool - def __init__(self, name: str, width: int = 0, height: int = 0): ... + def __init__(self, name: str, width: int = 0, height: int = 0) -> None: ... def chroma_width(self, luma_width: int = 0) -> int: ... def chroma_height(self, luma_height: int = 0) -> int: ... @@ -21,4 +21,4 @@ class VideoFormatComponent: width: int height: int - def __init__(self, format: VideoFormat, index: int): ... + def __init__(self, format: VideoFormat, index: int) -> None: ... diff --git a/av/video/frame.pyi b/av/video/frame.pyi index a387ad37f..da6575328 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -1,3 +1,5 @@ +from typing import Any, Union + import numpy as np from PIL import Image @@ -6,6 +8,13 @@ from av.frame import Frame from .format import VideoFormat from .plane import VideoPlane +from .reformatter import ColorRange, Colorspace + +_SupportedNDarray = Union[ + np.ndarray[Any, np.dtype[np.uint8]], + np.ndarray[Any, np.dtype[np.uint16]], + np.ndarray[Any, np.dtype[np.float32]], +] class PictureType(EnumItem): NONE: int @@ -27,26 +36,31 @@ class VideoFrame(Frame): key_frame: bool interlaced_frame: bool pict_type: int + colorspace: int + color_range: int - @staticmethod - def from_image(img: Image.Image) -> VideoFrame: ... - @staticmethod - def from_ndarray(array: np.ndarray, format: str = "rgb24") -> VideoFrame: ... - @staticmethod - def from_numpy_buffer(array: np.ndarray, format: str = "rgb24"): ... def __init__( - self, name: str, width: int = 0, height: int = 0, format: str = "yuv420p" - ): ... - def to_image(self, **kwargs) -> Image.Image: ... - def to_ndarray(self, **kwargs) -> np.ndarray: ... + self, width: int = 0, height: int = 0, format: str = "yuv420p" + ) -> None: ... def reformat( self, width: int | None = None, height: int | None = None, format: str | None = None, - src_colorspace=None, - dst_colorspace=None, + src_colorspace: Colorspace | None = None, + dst_colorspace: Colorspace | None = None, interpolation: int | str | None = None, src_color_range: int | str | None = None, dst_color_range: int | str | None = None, ) -> VideoFrame: ... + def to_rgb(self, **kwargs: Any) -> VideoFrame: ... + def to_image(self, **kwargs: Any) -> Image.Image: ... + def to_ndarray(self, **kwargs: Any) -> _SupportedNDarray: ... + @staticmethod + def from_image(img: Image.Image) -> VideoFrame: ... + @staticmethod + def from_numpy_buffer( + array: _SupportedNDarray, format: str = "rgb24" + ) -> VideoFrame: ... + @staticmethod + def from_ndarray(array: _SupportedNDarray, format: str = "rgb24") -> VideoFrame: ... diff --git a/av/video/plane.pyi b/av/video/plane.pyi index 592d3eb15..9bc1f0d77 100644 --- a/av/video/plane.pyi +++ b/av/video/plane.pyi @@ -6,4 +6,4 @@ class VideoPlane: height: int buffer_size: int - def __init__(self, frame: VideoFrame, index: int): ... + def __init__(self, frame: VideoFrame, index: int) -> None: ... diff --git a/av/video/reformatter.pyi b/av/video/reformatter.pyi index f17055a4f..a68dc9718 100644 --- a/av/video/reformatter.pyi +++ b/av/video/reformatter.pyi @@ -1,6 +1,5 @@ from av.enum import EnumItem -from .format import VideoFormat from .frame import VideoFrame class Interpolation(EnumItem): @@ -44,8 +43,8 @@ class VideoReformatter: width: int | None = None, height: int | None = None, format: str | None = None, - src_colorspace=None, - dst_colorspace=None, + src_colorspace: Colorspace | None = None, + dst_colorspace: Colorspace | None = None, interpolation: int | str | None = None, src_color_range: int | str | None = None, dst_color_range: int | str | None = None, diff --git a/av/video/stream.pyi b/av/video/stream.pyi index f6be52460..2ef4adf89 100644 --- a/av/video/stream.pyi +++ b/av/video/stream.pyi @@ -1,19 +1,21 @@ from fractions import Fraction -from typing import Any +from typing import Any, Literal from av.packet import Packet from av.stream import Stream from .codeccontext import VideoCodecContext +from .format import VideoFormat from .frame import VideoFrame class VideoStream(Stream): - name: str width: int height: int + format: VideoFormat pix_fmt: str | None sample_aspect_ratio: Fraction | None codec_context: VideoCodecContext + type: Literal["video"] # from codec context bit_rate: int | None From e4aa4c62df2142a18983171e6ab6ff665d9b6bb0 Mon Sep 17 00:00:00 2001 From: laggykiller <61652821+laggykiller@users.noreply.github.com> Date: Tue, 5 Mar 2024 14:28:30 +0800 Subject: [PATCH 211/846] Improve stub for enum, error, logging, stream --- av/enum.pyi | 45 ++++++++++++++++++++++++++++---------------- av/error.pyi | 51 +++++++++++++++++++++++++++++++++++++++++++++++--- av/logging.pyi | 9 ++++++--- av/stream.pyi | 14 ++++++++++++-- 4 files changed, 95 insertions(+), 24 deletions(-) diff --git a/av/enum.pyi b/av/enum.pyi index 12d523ac2..a4fe92e51 100644 --- a/av/enum.pyi +++ b/av/enum.pyi @@ -1,15 +1,26 @@ -from typing import Literal, Sequence, overload +from typing import Any, Callable, Iterable, Literal, Sequence, overload class EnumType(type): - def __init__(self, name, bases, attrs, items): ... - def _create(self, name: str, value: int, doc=None, by_value_only=False): ... - def __len__(self): ... - def __iter__(self): ... - def __getitem__(self, key): ... - def _get(self, value: int, create: bool = False): ... - def _get_multi_flags(self, value: int): ... + def __init__( + self, + name: str, + bases: tuple[type, ...], + attrs: dict[str, Any], + items: Iterable[tuple[str, Any, str | None, bool]], + ) -> None: ... + def _create( + self, name: str, value: int, doc: str | None = None, by_value_only: bool = False + ) -> None: ... + def __len__(self) -> None: ... + def __iter__(self) -> None: ... + def __getitem__(self, key: str | int | EnumType) -> None: ... + def _get(self, value: int, create: bool = False) -> None: ... + def _get_multi_flags(self, value: int) -> None: ... def get( - self, key, default: int | None = None, create: bool = False + self, + key: str | int | EnumType, + default: int | None = None, + create: bool = False, ) -> int | None: ... class EnumItem: @@ -18,17 +29,19 @@ class EnumItem: def __int__(self) -> int: ... def __hash__(self) -> int: ... - def __reduce__(self): ... - def __eq__(self, other) -> bool: ... - def __ne__(self, other) -> bool: ... + def __reduce__( + self, + ) -> tuple[Callable[[str, str, str], EnumItem], tuple[str, str, str]]: ... + def __eq__(self, other: object) -> bool: ... + def __ne__(self, other: object) -> bool: ... class EnumFlag(EnumItem): flags: tuple[EnumFlag] - def __and__(self, other): ... - def __or__(self, other): ... - def __xor__(self, other): ... - def __invert__(self): ... + def __and__(self, other: object) -> EnumFlag: ... + def __or__(self, other: object) -> EnumFlag: ... + def __xor__(self, other: object) -> EnumFlag: ... + def __invert__(self) -> bool: ... def __nonzero__(self) -> bool: ... @overload diff --git a/av/error.pyi b/av/error.pyi index 89f12c22d..db6581238 100644 --- a/av/error.pyi +++ b/av/error.pyi @@ -1,13 +1,58 @@ -from av.logging import get_last_error +from .enum import EnumItem classes: dict[int, Exception] def code_to_tag(code: int) -> bytes: ... def tag_to_code(tag: bytes) -> int: ... -def make_error(res: int, filename=None, log=None): ... +def make_error( + res: int, + filename: str | None = None, + log: tuple[int, tuple[int, str, str] | None] | None = None, +) -> None: ... + +class ErrorType(EnumItem): + BSF_NOT_FOUND: int + BUG: int + BUFFER_TOO_SMALL: int + DECODER_NOT_FOUND: int + DEMUXER_NOT_FOUND: int + ENCODER_NOT_FOUND: int + EOF: int + EXIT: int + EXTERNAL: int + FILTER_NOT_FOUND: int + INVALIDDATA: int + MUXER_NOT_FOUND: int + OPTION_NOT_FOUND: int + PATCHWELCOME: int + PROTOCOL_NOT_FOUND: int + UNKNOWN: int + EXPERIMENTAL: int + INPUT_CHANGED: int + OUTPUT_CHANGED: int + HTTP_BAD_REQUEST: int + HTTP_UNAUTHORIZED: int + HTTP_FORBIDDEN: int + HTTP_NOT_FOUND: int + HTTP_OTHER_4XX: int + HTTP_SERVER_ERROR: int + PYAV_CALLBACK: int + + tag: bytes class FFmpegError(Exception): - def __init__(self, code: int, message, filename=None, log=None): ... + errno: int + strerror: str + filename: str + log: tuple[int, tuple[int, str, str] | None] + + def __init__( + self, + code: int, + message: str, + filename: str | None = None, + log: tuple[int, tuple[int, str, str] | None] | None = None, + ) -> None: ... class LookupError(FFmpegError): ... class HTTPError(FFmpegError): ... diff --git a/av/logging.pyi b/av/logging.pyi index 996b7528a..a85eba2df 100644 --- a/av/logging.pyi +++ b/av/logging.pyi @@ -1,6 +1,6 @@ import logging from threading import Lock -from typing import Callable +from typing import Any, Callable PANIC: int FATAL: int @@ -29,7 +29,10 @@ class Capture: def __init__(self, local: bool = True) -> None: ... def __enter__(self) -> list[tuple[int, str, str]]: ... def __exit__( - self, type_: type | None, value: Exception | None, traceback: Callable | None + self, + type_: type | None, + value: Exception | None, + traceback: Callable[..., Any] | None, ) -> None: ... level_threshold: int @@ -40,4 +43,4 @@ last_log: tuple[int, str, str] | None skip_count: int last_error: tuple[int, str, str] | None global_captures: list[list[tuple[int, str, str]]] -thread_captures: dict +thread_captures: dict[int, list[tuple[int, str, str]]] diff --git a/av/stream.pyi b/av/stream.pyi index 7c220e2c4..79555e0a0 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -1,15 +1,25 @@ from fractions import Fraction from typing import Literal +from .codec import CodecContext +from .container import Container from .enum import EnumItem +from .frame import Frame +from .packet import Packet class SideData(EnumItem): DISPLAYMATRIX: int class Stream: name: str | None + thread_type: Literal["NONE", "FRAME", "SLICE", "AUTO"] + + container: Container + codec_context: CodecContext + metadata: dict[str, str] id: int profile: str + nb_side_data: int side_data: dict[str, str] index: int time_base: Fraction | None @@ -22,5 +32,5 @@ class Stream: language: str | None type: Literal["video", "audio", "data", "subtitle", "attachment"] - def encode(self, frame=None): ... - def decode(self, packet=None): ... + def encode(self, frame: Frame | None = None) -> list[Packet]: ... + def decode(self, packet: Packet | None = None) -> list[Frame]: ... From ec3c6d87943d2db167c4774fb00a8541c15da287 Mon Sep 17 00:00:00 2001 From: laggykiller <61652821+laggykiller@users.noreply.github.com> Date: Tue, 5 Mar 2024 14:29:08 +0800 Subject: [PATCH 212/846] add missing stub in av.sidedata --- av/sidedata/motionvectors.pyi | 22 ++++++++++++++++++++++ av/sidedata/sidedata.pyi | 31 +++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 av/sidedata/motionvectors.pyi create mode 100644 av/sidedata/sidedata.pyi diff --git a/av/sidedata/motionvectors.pyi b/av/sidedata/motionvectors.pyi new file mode 100644 index 000000000..5fe4e733a --- /dev/null +++ b/av/sidedata/motionvectors.pyi @@ -0,0 +1,22 @@ +from typing import Any, Sequence + +import numpy as np + +from .sidedata import SideData + +class MotionVectors(SideData, Sequence[Any]): + def __getitem__(self, index: int) -> MotionVector: ... + def __len__(self) -> int: ... + def to_ndarray(self) -> np.ndarray[Any, Any]: ... + +class MotionVector: + source: int + w: int + h: int + src_x: int + src_y: int + dst_x: int + dst_y: int + motion_x: int + motion_y: int + motion_scale: int diff --git a/av/sidedata/sidedata.pyi b/av/sidedata/sidedata.pyi new file mode 100644 index 000000000..c0d4ba314 --- /dev/null +++ b/av/sidedata/sidedata.pyi @@ -0,0 +1,31 @@ +from collections.abc import Mapping + +from av.buffer import Buffer +from av.enum import EnumItem +from av.frame import Frame + +class Type(EnumItem): + PANSCAN: int + A53_CC: int + STEREO3D: int + MATRIXENCODING: int + DOWNMIX_INFO: int + REPLAYGAIN: int + DISPLAYMATRIX: int + AFD: int + MOTION_VECTORS: int + SKIP_SAMPLES: int + AUDIO_SERVICE_TYPE: int + MASTERING_DISPLAY_METADATA: int + GOP_TIMECODE: int + SPHERICAL: int + CONTENT_LIGHT_LEVEL: int + ICC_PROFILE: int + SEI_UNREGISTERED: int + +class SideData(Buffer): + type: Type + DISPLAYMATRIX: int + +class SideDataContainer(Mapping[str, int]): + frame: Frame From d68fd1cb6c05ea01e71c17e9b595d1e2175055c1 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 5 Mar 2024 01:50:41 -0500 Subject: [PATCH 213/846] Yeah, we can just delete decode --- av/stream.pyi | 1 - av/stream.pyx | 14 -------------- 2 files changed, 15 deletions(-) diff --git a/av/stream.pyi b/av/stream.pyi index 79555e0a0..ffe4f8722 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -33,4 +33,3 @@ class Stream: type: Literal["video", "audio", "data", "subtitle", "attachment"] def encode(self, frame: Frame | None = None) -> list[Packet]: ... - def decode(self, packet: Packet | None = None) -> list[Frame]: ... diff --git a/av/stream.pyx b/av/stream.pyx index 3f40f21d8..0ed3e50df 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -165,20 +165,6 @@ cdef class Stream: packet.ptr.stream_index = self.ptr.index return packets - def decode(self, packet=None): - """ - Decode a :class:`.Packet` and return a list of :class:`.AudioFrame` - or :class:`.VideoFrame`. - - :return: :class:`list` of :class:`.Frame` subclasses. - - .. seealso:: This is mostly a passthrough to :meth:`.CodecContext.decode`. - """ - if self.codec_context is None: - raise RuntimeError("Stream.decode requires a valid CodecContext") - - return self.codec_context.decode(packet) - cdef _get_side_data(self, lib.AVStream *stream): # Get DISPLAYMATRIX SideData from a lib.AVStream object. # Returns: tuple[int, dict[str, Any]] From ac0353dc7ff1597a2062b816d914477b3c0c5411 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 5 Mar 2024 13:42:29 -0500 Subject: [PATCH 214/846] Fix all the type errors mypy complains about --- av/audio/stream.pyi | 2 +- av/format.pyi | 2 ++ av/sidedata/motionvectors.pyi | 11 ++++++++--- av/sidedata/sidedata.pyi | 11 ++++++++++- av/video/stream.pyi | 2 +- 5 files changed, 22 insertions(+), 6 deletions(-) diff --git a/av/audio/stream.pyi b/av/audio/stream.pyi index e14323f2a..cf4173759 100644 --- a/av/audio/stream.pyi +++ b/av/audio/stream.pyi @@ -12,5 +12,5 @@ class AudioStream(Stream): format: AudioFormat codec_context: AudioCodecContext - def encode(self, frame: AudioFrame | None = None) -> list[Packet]: ... + def encode(self, frame: AudioFrame | None = None) -> list[Packet]: ... # type: ignore[override] def decode(self, packet: Packet | None = None) -> list[AudioFrame]: ... diff --git a/av/format.pyi b/av/format.pyi index 7eb3d1ce5..874920928 100644 --- a/av/format.pyi +++ b/av/format.pyi @@ -1,3 +1,5 @@ +__all__ = ("ContainerFormat", "formats_available") + from .enum import EnumFlag class Flags(EnumFlag): diff --git a/av/sidedata/motionvectors.pyi b/av/sidedata/motionvectors.pyi index 5fe4e733a..eb514eb70 100644 --- a/av/sidedata/motionvectors.pyi +++ b/av/sidedata/motionvectors.pyi @@ -1,11 +1,16 @@ -from typing import Any, Sequence +from typing import Any, Sequence, overload import numpy as np from .sidedata import SideData -class MotionVectors(SideData, Sequence[Any]): - def __getitem__(self, index: int) -> MotionVector: ... +class MotionVectors(SideData, Sequence[MotionVector]): + @overload + def __getitem__(self, index: int): ... + @overload + def __getitem__(self, index: slice): ... + @overload + def __getitem__(self, index: int | slice): ... def __len__(self) -> int: ... def to_ndarray(self) -> np.ndarray[Any, Any]: ... diff --git a/av/sidedata/sidedata.pyi b/av/sidedata/sidedata.pyi index c0d4ba314..ac28f0dee 100644 --- a/av/sidedata/sidedata.pyi +++ b/av/sidedata/sidedata.pyi @@ -1,4 +1,5 @@ from collections.abc import Mapping +from typing import Iterator, Sequence, overload from av.buffer import Buffer from av.enum import EnumItem @@ -27,5 +28,13 @@ class SideData(Buffer): type: Type DISPLAYMATRIX: int -class SideDataContainer(Mapping[str, int]): +class SideDataContainer(Mapping): frame: Frame + def __len__(self) -> int: ... + def __iter__(self) -> Iterator[SideData]: ... + @overload + def __getitem__(self, key: int) -> SideData: ... + @overload + def __getitem__(self, key: slice) -> Sequence[SideData]: ... + @overload + def __getitem__(self, key: int | slice) -> SideData | Sequence[SideData]: ... diff --git a/av/video/stream.pyi b/av/video/stream.pyi index 2ef4adf89..b0266328a 100644 --- a/av/video/stream.pyi +++ b/av/video/stream.pyi @@ -24,5 +24,5 @@ class VideoStream(Stream): thread_count: int thread_type: Any - def encode(self, frame: VideoFrame | None = None) -> list[Packet]: ... + def encode(self, frame: VideoFrame | None = None) -> list[Packet]: ... # type: ignore[override] def decode(self, packet: Packet | None = None) -> list[VideoFrame]: ... From 9ad1d9198ef2b823fa751f26868395d3f68d5f16 Mon Sep 17 00:00:00 2001 From: Joe Schiff <41972063+JoeSchiff@users.noreply.github.com> Date: Fri, 8 Mar 2024 01:27:03 -0500 Subject: [PATCH 215/846] expose discard, trusted and disposable packet flags --------- Co-authored-by: Scott Kirklin Co-authored-by: WyattBlue Co-authored-by: JoeUgly <41972063+JoeUgly@users.noreply.github.com> --- av/packet.pyi | 3 +++ av/packet.pyx | 16 ++++++++++++- include/libavcodec/avcodec.pxd | 3 +++ tests/test_packet.py | 41 ++++++++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 tests/test_packet.py diff --git a/av/packet.pyi b/av/packet.pyi index 690dc8374..cca33009c 100644 --- a/av/packet.pyi +++ b/av/packet.pyi @@ -16,6 +16,9 @@ class Packet: duration: int | None is_keyframe: bool is_corrupt: bool + is_discard: bool + is_trusted: bool + is_disposable: bool def __init__(self, input: int | None = None) -> None: ... def decode(self) -> Iterator[SubtitleSet]: ... diff --git a/av/packet.pyx b/av/packet.pyx index 116fab6b7..24fd5581b 100644 --- a/av/packet.pyx +++ b/av/packet.pyx @@ -189,7 +189,8 @@ cdef class Packet(Buffer): self.ptr.flags &= ~(lib.AV_PKT_FLAG_KEY) @property - def is_corrupt(self): return bool(self.ptr.flags & lib.AV_PKT_FLAG_CORRUPT) + def is_corrupt(self): + return bool(self.ptr.flags & lib.AV_PKT_FLAG_CORRUPT) @is_corrupt.setter def is_corrupt(self, v): @@ -197,3 +198,16 @@ cdef class Packet(Buffer): self.ptr.flags |= lib.AV_PKT_FLAG_CORRUPT else: self.ptr.flags &= ~(lib.AV_PKT_FLAG_CORRUPT) + + @property + def is_discard(self): + return bool(self.ptr.flags & lib.AV_PKT_FLAG_DISCARD) + + @property + def is_trusted(self): + return bool(self.ptr.flags & lib.AV_PKT_FLAG_TRUSTED) + + @property + def is_disposable(self): + return bool(self.ptr.flags & lib.AV_PKT_FLAG_DISPOSABLE) + diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 7d841559d..b58047014 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -90,6 +90,9 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef enum: AV_PKT_FLAG_KEY AV_PKT_FLAG_CORRUPT + AV_PKT_FLAG_DISCARD + AV_PKT_FLAG_TRUSTED + AV_PKT_FLAG_DISPOSABLE cdef enum: AV_FRAME_FLAG_CORRUPT diff --git a/tests/test_packet.py b/tests/test_packet.py new file mode 100644 index 000000000..8a2b6266c --- /dev/null +++ b/tests/test_packet.py @@ -0,0 +1,41 @@ +import av + +from .common import TestCase, fate_suite + + +class TestProperties(TestCase): + def test_is_keyframe(self): + with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: + stream = container.streams.video[0] + for i, packet in enumerate(container.demux(stream)): + if i in (0, 21, 45, 69, 93, 117): + self.assertTrue(packet.is_keyframe) + else: + self.assertFalse(packet.is_keyframe) + + def test_is_corrupt(self): + with av.open(fate_suite("mov/white_zombie_scrunch-part.mov")) as container: + stream = container.streams.video[0] + for i, packet in enumerate(container.demux(stream)): + if i == 65: + self.assertTrue(packet.is_corrupt) + else: + self.assertFalse(packet.is_corrupt) + + def test_is_discard(self): + with av.open(fate_suite("mov/mov-1elist-ends-last-bframe.mov")) as container: + stream = container.streams.video[0] + for i, packet in enumerate(container.demux(stream)): + if i == 46: + self.assertTrue(packet.is_discard) + else: + self.assertFalse(packet.is_discard) + + def test_is_disposable(self): + with av.open(fate_suite("hap/HAPQA_NoSnappy_127x1.mov")) as container: + stream = container.streams.video[0] + for i, packet in enumerate(container.demux(stream)): + if i == 0: + self.assertTrue(packet.is_disposable) + else: + self.assertFalse(packet.is_disposable) From bd6104afdf640e28882f7d9add244e7daf435ecd Mon Sep 17 00:00:00 2001 From: laggykiller Date: Sat, 9 Mar 2024 19:59:03 +0800 Subject: [PATCH 216/846] Fix colorspace signature --- av/video/frame.pyi | 5 ++--- av/video/reformatter.pyi | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/av/video/frame.pyi b/av/video/frame.pyi index da6575328..17cacf05a 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -8,7 +8,6 @@ from av.frame import Frame from .format import VideoFormat from .plane import VideoPlane -from .reformatter import ColorRange, Colorspace _SupportedNDarray = Union[ np.ndarray[Any, np.dtype[np.uint8]], @@ -47,8 +46,8 @@ class VideoFrame(Frame): width: int | None = None, height: int | None = None, format: str | None = None, - src_colorspace: Colorspace | None = None, - dst_colorspace: Colorspace | None = None, + src_colorspace: int | None = None, + dst_colorspace: int | None = None, interpolation: int | str | None = None, src_color_range: int | str | None = None, dst_color_range: int | str | None = None, diff --git a/av/video/reformatter.pyi b/av/video/reformatter.pyi index a68dc9718..abd545332 100644 --- a/av/video/reformatter.pyi +++ b/av/video/reformatter.pyi @@ -43,8 +43,8 @@ class VideoReformatter: width: int | None = None, height: int | None = None, format: str | None = None, - src_colorspace: Colorspace | None = None, - dst_colorspace: Colorspace | None = None, + src_colorspace: int | None = None, + dst_colorspace: int | None = None, interpolation: int | str | None = None, src_color_range: int | str | None = None, dst_color_range: int | str | None = None, From 2560dc3e3ab7169e6472d33ebc1aab52b8bfba09 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 9 Mar 2024 07:05:22 -0500 Subject: [PATCH 217/846] Move encode and decode to concrete classes --- av/audio/stream.pyi | 14 +++++++++++--- av/audio/stream.pyx | 31 ++++++++++++++++++++++++++++++- av/stream.pyi | 2 -- av/stream.pyx | 19 ------------------- av/video/codeccontext.pyi | 10 ++++++---- av/video/stream.pyi | 31 +++++++++++++++++++++---------- av/video/stream.pyx | 32 +++++++++++++++++++++++++++++++- 7 files changed, 99 insertions(+), 40 deletions(-) diff --git a/av/audio/stream.pyi b/av/audio/stream.pyi index cf4173759..9e972c56d 100644 --- a/av/audio/stream.pyi +++ b/av/audio/stream.pyi @@ -6,11 +6,19 @@ from av.stream import Stream from .codeccontext import AudioCodecContext from .format import AudioFormat from .frame import AudioFrame +from .layout import AudioLayout class AudioStream(Stream): - type: Literal["audio"] - format: AudioFormat codec_context: AudioCodecContext + # From codec context + frame_size: int + sample_rate: int + rate: int + channels: int + channel_layout: int + layout: AudioLayout + format: AudioFormat + type: Literal["audio"] - def encode(self, frame: AudioFrame | None = None) -> list[Packet]: ... # type: ignore[override] + def encode(self, frame: AudioFrame | None = None) -> list[Packet]: ... def decode(self, packet: Packet | None = None) -> list[AudioFrame]: ... diff --git a/av/audio/stream.pyx b/av/audio/stream.pyx index 3c6b76fcf..f6dc3b065 100644 --- a/av/audio/stream.pyx +++ b/av/audio/stream.pyx @@ -1,7 +1,36 @@ +from av.packet cimport Packet + + cdef class AudioStream(Stream): def __repr__(self): form = self.format.name if self.format else None return ( - f"" ) + + def encode(self, frame=None): + """ + Encode an :class:`.AudioFrame` and return a list of :class:`.Packet`. + + :return: :class:`list` of :class:`.Packet`. + + .. seealso:: This is mostly a passthrough to :meth:`.CodecContext.encode`. + """ + + packets = self.codec_context.encode(frame) + cdef Packet packet + for packet in packets: + packet._stream = self + packet.ptr.stream_index = self.ptr.index + + return packets + + def decode(self, packet=None): + """ + Decode a :class:`.Packet` and return a list of :class:`.AudioFrame`. + :return: :class:`list` of :class:`.AudioFrame` + .. seealso:: This is a passthrough to :meth:`.CodecContext.decode`. + """ + + return self.codec_context.decode(packet) diff --git a/av/stream.pyi b/av/stream.pyi index ffe4f8722..4c4c05030 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -31,5 +31,3 @@ class Stream: frames: int language: str | None type: Literal["video", "audio", "data", "subtitle", "attachment"] - - def encode(self, frame: Frame | None = None) -> list[Packet]: ... diff --git a/av/stream.pyx b/av/stream.pyx index 0ed3e50df..08744771e 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -146,25 +146,6 @@ cdef class Stream: # Lets just copy what we want. err_check(lib.avcodec_parameters_from_context(self.ptr.codecpar, self.codec_context.ptr)) - def encode(self, frame=None): - """ - Encode an :class:`.AudioFrame` or :class:`.VideoFrame` and return a list - of :class:`.Packet`. - - :return: :class:`list` of :class:`.Packet`. - - .. seealso:: This is mostly a passthrough to :meth:`.CodecContext.encode`. - """ - if self.codec_context is None: - raise RuntimeError("Stream.encode requires a valid CodecContext") - - packets = self.codec_context.encode(frame) - cdef Packet packet - for packet in packets: - packet._stream = self - packet.ptr.stream_index = self.ptr.index - return packets - cdef _get_side_data(self, lib.AVStream *stream): # Get DISPLAYMATRIX SideData from a lib.AVStream object. # Returns: tuple[int, dict[str, Any]] diff --git a/av/video/codeccontext.pyi b/av/video/codeccontext.pyi index a749ad3f9..4576f5155 100644 --- a/av/video/codeccontext.pyi +++ b/av/video/codeccontext.pyi @@ -4,18 +4,20 @@ from typing import Iterator, Literal from av.codec.context import CodecContext from av.packet import Packet +from .format import VideoFormat from .frame import VideoFrame class VideoCodecContext(CodecContext): + format: VideoFormat width: int height: int bits_per_codec_sample: int - pix_fmt: str + pix_fmt: str | None framerate: Fraction rate: Fraction gop_size: int - sample_aspect_ratio: Fraction - display_aspect_ratio: Fraction + sample_aspect_ratio: Fraction | None + display_aspect_ratio: Fraction | None has_b_frames: bool coded_width: int coded_height: int @@ -23,8 +25,8 @@ class VideoCodecContext(CodecContext): color_primaries: int color_trc: int colorspace: int - type: Literal["video"] + def encode(self, frame: VideoFrame | None = None) -> list[Packet]: ... def encode_lazy(self, frame: VideoFrame | None = None) -> Iterator[Packet]: ... def decode(self, packet: Packet | None = None) -> list[VideoFrame]: ... diff --git a/av/video/stream.pyi b/av/video/stream.pyi index b0266328a..7ff1a4034 100644 --- a/av/video/stream.pyi +++ b/av/video/stream.pyi @@ -9,20 +9,31 @@ from .format import VideoFormat from .frame import VideoFrame class VideoStream(Stream): - width: int - height: int - format: VideoFormat - pix_fmt: str | None - sample_aspect_ratio: Fraction | None - codec_context: VideoCodecContext - type: Literal["video"] - - # from codec context bit_rate: int | None max_bit_rate: int | None bit_rate_tolerance: int thread_count: int thread_type: Any + codec_context: VideoCodecContext + # from codec context + format: VideoFormat + width: int + height: int + bits_per_codec_sample: int + pix_fmt: str | None + framerate: Fraction + rate: Fraction + gop_size: int + sample_aspect_ratio: Fraction | None + display_aspect_ratio: Fraction | None + has_b_frames: bool + coded_width: int + coded_height: int + color_range: int + color_primaries: int + color_trc: int + colorspace: int + type: Literal["video"] - def encode(self, frame: VideoFrame | None = None) -> list[Packet]: ... # type: ignore[override] + def encode(self, frame: VideoFrame | None = None) -> list[Packet]: ... def decode(self, packet: Packet | None = None) -> list[VideoFrame]: ... diff --git a/av/video/stream.pyx b/av/video/stream.pyx index 08949be2e..b88b3919a 100644 --- a/av/video/stream.pyx +++ b/av/video/stream.pyx @@ -1,7 +1,37 @@ +from av.packet cimport Packet + + cdef class VideoStream(Stream): def __repr__(self): return ( - f"" ) + + def encode(self, frame=None): + """ + Encode an :class:`.VideoFrame` and return a list of :class:`.Packet`. + + :return: :class:`list` of :class:`.Packet`. + + .. seealso:: This is mostly a passthrough to :meth:`.CodecContext.encode`. + """ + + packets = self.codec_context.encode(frame) + cdef Packet packet + for packet in packets: + packet._stream = self + packet.ptr.stream_index = self.ptr.index + + return packets + + + def decode(self, packet=None): + """ + Decode a :class:`.Packet` and return a list of :class:`.VideoFrame`. + :return: :class:`list` of :class:`.Frame` subclasses. + .. seealso:: This is a passthrough to :meth:`.CodecContext.decode`. + """ + + return self.codec_context.decode(packet) From 91d56d3918e456da0efb3791372849678e16240c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 9 Mar 2024 07:46:30 -0500 Subject: [PATCH 218/846] Use cpdef for encode/decode --- av/audio/stream.pxd | 5 ++++- av/audio/stream.pyx | 6 ++++-- av/video/stream.pxd | 5 ++++- av/video/stream.pyx | 6 +++--- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/av/audio/stream.pxd b/av/audio/stream.pxd index f8f68c263..a366ad556 100644 --- a/av/audio/stream.pxd +++ b/av/audio/stream.pxd @@ -1,5 +1,8 @@ from av.stream cimport Stream +from av.packet cimport Packet +from .frame cimport AudioFrame cdef class AudioStream(Stream): - pass + cpdef encode(self, AudioFrame frame=?) + cpdef decode(self, Packet packet=?) diff --git a/av/audio/stream.pyx b/av/audio/stream.pyx index f6dc3b065..2dce95083 100644 --- a/av/audio/stream.pyx +++ b/av/audio/stream.pyx @@ -1,5 +1,7 @@ from av.packet cimport Packet +from .frame cimport AudioFrame + cdef class AudioStream(Stream): def __repr__(self): @@ -9,7 +11,7 @@ cdef class AudioStream(Stream): f" {self.layout.name}, {form} at 0x{id(self):x}>" ) - def encode(self, frame=None): + cpdef encode(self, AudioFrame frame=None): """ Encode an :class:`.AudioFrame` and return a list of :class:`.Packet`. @@ -26,7 +28,7 @@ cdef class AudioStream(Stream): return packets - def decode(self, packet=None): + cpdef decode(self, Packet packet=None): """ Decode a :class:`.Packet` and return a list of :class:`.AudioFrame`. :return: :class:`list` of :class:`.AudioFrame` diff --git a/av/video/stream.pxd b/av/video/stream.pxd index 01b8d9d41..252243dd5 100644 --- a/av/video/stream.pxd +++ b/av/video/stream.pxd @@ -1,5 +1,8 @@ from av.stream cimport Stream +from .frame cimport VideoFrame +from av.packet cimport Packet cdef class VideoStream(Stream): - pass + cpdef encode(self, VideoFrame frame=?) + cpdef decode(self, Packet packet=?) diff --git a/av/video/stream.pyx b/av/video/stream.pyx index b88b3919a..bff1f77f4 100644 --- a/av/video/stream.pyx +++ b/av/video/stream.pyx @@ -1,5 +1,5 @@ from av.packet cimport Packet - +from .frame cimport VideoFrame cdef class VideoStream(Stream): def __repr__(self): @@ -9,7 +9,7 @@ cdef class VideoStream(Stream): f"{self.codec_context.height} at 0x{id(self):x}>" ) - def encode(self, frame=None): + cpdef encode(self, VideoFrame frame=None): """ Encode an :class:`.VideoFrame` and return a list of :class:`.Packet`. @@ -27,7 +27,7 @@ cdef class VideoStream(Stream): return packets - def decode(self, packet=None): + cpdef decode(self, Packet packet=None): """ Decode a :class:`.Packet` and return a list of :class:`.VideoFrame`. :return: :class:`list` of :class:`.Frame` subclasses. From d6a66c5b5a2fad30e13db8d8e18af556b578c370 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 9 Mar 2024 07:59:46 -0500 Subject: [PATCH 219/846] Only VideoStream now has avg_rate, base, guessed --- av/audio/stream.pxd | 3 ++- av/stream.pyx | 42 ------------------------------------- av/video/stream.pxd | 3 ++- av/video/stream.pyx | 43 ++++++++++++++++++++++++++++++++++++++ tests/test_file_probing.py | 14 +------------ 5 files changed, 48 insertions(+), 57 deletions(-) diff --git a/av/audio/stream.pxd b/av/audio/stream.pxd index a366ad556..8462061f8 100644 --- a/av/audio/stream.pxd +++ b/av/audio/stream.pxd @@ -1,8 +1,9 @@ -from av.stream cimport Stream from av.packet cimport Packet +from av.stream cimport Stream from .frame cimport AudioFrame + cdef class AudioStream(Stream): cpdef encode(self, AudioFrame frame=?) cpdef decode(self, Packet packet=?) diff --git a/av/stream.pyx b/av/stream.pyx index 08744771e..1b5e967dd 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -217,48 +217,6 @@ cdef class Stream: """ to_avrational(value, &self.ptr.time_base) - @property - def average_rate(self): - """ - The average frame rate of this video stream. - - This is calculated when the file is opened by looking at the first - few frames and averaging their rate. - - :type: :class:`~fractions.Fraction` or ``None`` - - - """ - return avrational_to_fraction(&self.ptr.avg_frame_rate) - - @property - def base_rate(self): - """ - The base frame rate of this stream. - - This is calculated as the lowest framerate at which the timestamps of - frames can be represented accurately. See :ffmpeg:`AVStream.r_frame_rate` - for more. - - :type: :class:`~fractions.Fraction` or ``None`` - - """ - return avrational_to_fraction(&self.ptr.r_frame_rate) - - @property - def guessed_rate(self): - """The guessed frame rate of this stream. - - This is a wrapper around :ffmpeg:`av_guess_frame_rate`, and uses multiple - heuristics to decide what is "the" frame rate. - - :type: :class:`~fractions.Fraction` or ``None`` - - """ - # The two NULL arguments aren't used in FFmpeg >= 4.0 - cdef lib.AVRational val = lib.av_guess_frame_rate(NULL, self.ptr, NULL) - return avrational_to_fraction(&val) - @property def start_time(self): """ diff --git a/av/video/stream.pxd b/av/video/stream.pxd index 252243dd5..f0dcfb9b2 100644 --- a/av/video/stream.pxd +++ b/av/video/stream.pxd @@ -1,6 +1,7 @@ +from av.packet cimport Packet from av.stream cimport Stream + from .frame cimport VideoFrame -from av.packet cimport Packet cdef class VideoStream(Stream): diff --git a/av/video/stream.pyx b/av/video/stream.pyx index bff1f77f4..abd4eac52 100644 --- a/av/video/stream.pyx +++ b/av/video/stream.pyx @@ -1,6 +1,11 @@ +cimport libav as lib + from av.packet cimport Packet +from av.utils cimport avrational_to_fraction, to_avrational + from .frame cimport VideoFrame + cdef class VideoStream(Stream): def __repr__(self): return ( @@ -35,3 +40,41 @@ cdef class VideoStream(Stream): """ return self.codec_context.decode(packet) + + @property + def average_rate(self): + """ + The average frame rate of this video stream. + + This is calculated when the file is opened by looking at the first + few frames and averaging their rate. + + :type: :class:`~fractions.Fraction` or ``None`` + """ + return avrational_to_fraction(&self.ptr.avg_frame_rate) + + @property + def base_rate(self): + """ + The base frame rate of this stream. + + This is calculated as the lowest framerate at which the timestamps of + frames can be represented accurately. See :ffmpeg:`AVStream.r_frame_rate` + for more. + + :type: :class:`~fractions.Fraction` or ``None`` + """ + return avrational_to_fraction(&self.ptr.r_frame_rate) + + @property + def guessed_rate(self): + """The guessed frame rate of this stream. + + This is a wrapper around :ffmpeg:`av_guess_frame_rate`, and uses multiple + heuristics to decide what is "the" frame rate. + + :type: :class:`~fractions.Fraction` or ``None`` + """ + # The two NULL arguments aren't used in FFmpeg >= 4.0 + cdef lib.AVRational val = lib.av_guess_frame_rate(NULL, self.ptr, NULL) + return avrational_to_fraction(&val) diff --git a/tests/test_file_probing.py b/tests/test_file_probing.py index c3d239c50..928a5bd1d 100644 --- a/tests/test_file_probing.py +++ b/tests/test_file_probing.py @@ -34,20 +34,12 @@ def test_stream_probing(self): ) # actual stream properties - self.assertEqual(stream.average_rate, None) - self.assertEqual(stream.base_rate, None) - self.assertEqual(stream.guessed_rate, None) self.assertEqual(stream.duration, 554880) self.assertEqual(stream.frames, 0) self.assertEqual(stream.id, 256) self.assertEqual(stream.index, 0) self.assertEqual(stream.language, "eng") - self.assertEqual( - stream.metadata, - { - "language": "eng", - }, - ) + self.assertEqual(stream.metadata, {"language": "eng"}) self.assertEqual(stream.profile, "LC") self.assertEqual(stream.start_time, 126000) self.assertEqual(stream.time_base, Fraction(1, 90000)) @@ -98,9 +90,6 @@ def test_stream_probing(self): ) # actual stream properties - self.assertEqual(stream.average_rate, None) - self.assertEqual(stream.base_rate, None) - self.assertEqual(stream.guessed_rate, None) self.assertEqual(stream.duration, None) self.assertEqual(stream.frames, 0) self.assertEqual(stream.id, 0) @@ -239,7 +228,6 @@ def test_stream_probing(self): ) # actual stream properties - self.assertEqual(stream.average_rate, None) self.assertEqual(stream.duration, 8140) self.assertEqual(stream.frames, 6) self.assertEqual(stream.id, 1) From ba35f556a82faae678225f962d02c44ed62d9e46 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 9 Mar 2024 16:37:22 -0500 Subject: [PATCH 220/846] Update Stream docs --- av/audio/stream.pyx | 6 ++++-- av/container/core.pyx | 1 - av/video/frame.pyx | 4 +--- av/video/stream.pyx | 6 ++++-- docs/api/stream.rst | 29 ----------------------------- 5 files changed, 9 insertions(+), 37 deletions(-) diff --git a/av/audio/stream.pyx b/av/audio/stream.pyx index 2dce95083..7185f389d 100644 --- a/av/audio/stream.pyx +++ b/av/audio/stream.pyx @@ -15,7 +15,7 @@ cdef class AudioStream(Stream): """ Encode an :class:`.AudioFrame` and return a list of :class:`.Packet`. - :return: :class:`list` of :class:`.Packet`. + :rtype: list[Packet] .. seealso:: This is mostly a passthrough to :meth:`.CodecContext.encode`. """ @@ -31,7 +31,9 @@ cdef class AudioStream(Stream): cpdef decode(self, Packet packet=None): """ Decode a :class:`.Packet` and return a list of :class:`.AudioFrame`. - :return: :class:`list` of :class:`.AudioFrame` + + :rtype: list[AudioFrame] + .. seealso:: This is a passthrough to :meth:`.CodecContext.decode`. """ diff --git a/av/container/core.pyx b/av/container/core.pyx index 240db340e..548fe4fc2 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -366,7 +366,6 @@ def open( Honored only when ``file`` is a file-like object. Defaults to 32768 (32k). :param timeout: How many seconds to wait for data before giving up, as a float, or a :ref:`(open timeout, read timeout) ` tuple. - :type timeout: float or tuple :param callable io_open: Custom I/O callable for opening files/streams. This option is intended for formats that need to open additional file-like objects to ``file`` using custom I/O. diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 4425e18a9..6ff982491 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -464,9 +464,7 @@ cdef class VideoFrame(Frame): .. note:: For formats which expect an array of ``uint16``, the samples must be in the system's native byte order. - .. note:: for ``pal8``, an ``(image, palette)`` pair must be passed. - `palette` must have shape (256, 4) and is given in ARGB format - (PyAV will swap bytes if needed). + .. note:: for ``pal8``, an ``(image, palette)`` pair must be passed. `palette` must have shape (256, 4) and is given in ARGB format (PyAV will swap bytes if needed). """ if format == "pal8": array, palette = array diff --git a/av/video/stream.pyx b/av/video/stream.pyx index abd4eac52..af80f27a1 100644 --- a/av/video/stream.pyx +++ b/av/video/stream.pyx @@ -18,7 +18,7 @@ cdef class VideoStream(Stream): """ Encode an :class:`.VideoFrame` and return a list of :class:`.Packet`. - :return: :class:`list` of :class:`.Packet`. + :rtype: list[Packet] .. seealso:: This is mostly a passthrough to :meth:`.CodecContext.encode`. """ @@ -35,7 +35,9 @@ cdef class VideoStream(Stream): cpdef decode(self, Packet packet=None): """ Decode a :class:`.Packet` and return a list of :class:`.VideoFrame`. - :return: :class:`list` of :class:`.Frame` subclasses. + + :rtype: list[VideoFrame] + .. seealso:: This is a passthrough to :meth:`.CodecContext.decode`. """ diff --git a/docs/api/stream.rst b/docs/api/stream.rst index 49ff05ac8..a6bc1fc8b 100644 --- a/docs/api/stream.rst +++ b/docs/api/stream.rst @@ -65,14 +65,6 @@ Basics .. autoattribute:: Stream.index -Transcoding -~~~~~~~~~~~ - -.. automethod:: Stream.encode - -.. automethod:: Stream.decode - - Timing ~~~~~~ @@ -87,27 +79,6 @@ Timing .. autoattribute:: Stream.frames -.. _frame_rates: - -Frame Rates -........... - - -These attributes are different ways of calculating frame rates. - -Since containers don't need to explicitly identify a frame rate, nor -even have a static frame rate, these attributes are not guaranteed to be accurate. -You must experiment with them with your media to see which ones work for you for your purposes. - -Whenever possible, we advise that you use raw timing instead of frame rates. - -.. autoattribute:: Stream.average_rate - -.. autoattribute:: Stream.base_rate - -.. autoattribute:: Stream.guessed_rate - - Others ~~~~~~ From 7a5c2d1aabfb75ead1dd3d73dc990a6a84185dd6 Mon Sep 17 00:00:00 2001 From: Roland van Laar Date: Tue, 12 Mar 2024 08:29:27 +0100 Subject: [PATCH 221/846] Expose bits_per_coded_sample on VideoCodecContext --------- Co-authored-by: JoeUgly <41972063+JoeUgly@users.noreply.github.com> Co-authored-by: WyattBlue --- av/video/codeccontext.pyx | 20 ++++++++++++++++++++ include/libavcodec/avcodec.pxd | 2 ++ tests/test_codec_context.py | 25 +++++++++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.pyx index 2e101d935..8f6762fcf 100644 --- a/av/video/codeccontext.pyx +++ b/av/video/codeccontext.pyx @@ -97,6 +97,26 @@ cdef class VideoCodecContext(CodecContext): self.ptr.height = value self._build_format() + @property + def bits_per_coded_sample(self): + """ + The number of bits per sample in the codedwords, basically the bitrate per sample. + It is mandatory for this to be set for some formats to decode them. + + Wraps :ffmpeg:`AVCodecContext::bits_per_coded_sample` + + :type: int + """ + return self.ptr.bits_per_coded_sample + + @bits_per_coded_sample.setter + def bits_per_coded_sample(self, int value): + if self.is_encoder: + raise ValueError("Not supported for encoders") + + self.ptr.bits_per_coded_sample = value + self._build_format() + @property def pix_fmt(self): """ diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index b58047014..49758be4c 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -171,6 +171,8 @@ cdef extern from "libavcodec/avcodec.h" nogil: int bit_rate_tolerance int mb_decision + int bits_per_coded_sample + int global_quality int compression_level diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 0be4ed621..2f9a34aa7 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -146,6 +146,31 @@ def test_encoder_pix_fmt(self): self.assertEqual(str(cm.exception), "not a pixel format: '__unknown_pix_fmt'") self.assertEqual(ctx.pix_fmt, "yuv420p") + def test_bits_per_coded_sample(self): + with av.open(fate_suite("qtrle/aletrek-rle.mov")) as container: + stream = container.streams.video[0] + stream.bits_per_coded_sample = 32 + + for packet in container.demux(stream): + for frame in packet.decode(): + pass + self.assertEqual(packet.stream.bits_per_coded_sample, 32) + + with av.open(fate_suite("qtrle/aletrek-rle.mov")) as container: + stream = container.streams.video[0] + stream.bits_per_coded_sample = 31 + + with self.assertRaises(av.error.InvalidDataError): + for packet in container.demux(stream): + for frame in packet.decode(): + pass + + with av.open(self.sandboxed("output.mov"), "w") as output: + stream = output.add_stream("qtrle") + + with self.assertRaises(ValueError): + stream.codec_context.bits_per_coded_sample = 32 + def test_parse(self): # This one parses into a single packet. self._assert_parse("mpeg4", fate_suite("h264/interlaced_crop.mp4")) From 9e4bb694b2e2e2a0c133d05fbc645d173f03834e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 12 Mar 2024 03:54:14 -0400 Subject: [PATCH 222/846] Fix docs, fix mac --- .github/workflows/tests.yml | 1 - av/video/codeccontext.pyx | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d93fde792..2282950b6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -72,7 +72,6 @@ jobs: fi ;; macos-latest) - brew update brew install automake libtool nasm pkg-config shtool texi2html wget brew install libass libjpeg libpng libvorbis libvpx opus theora x264 ;; diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.pyx index 8f6762fcf..5ef49ee4c 100644 --- a/av/video/codeccontext.pyx +++ b/av/video/codeccontext.pyx @@ -100,10 +100,9 @@ cdef class VideoCodecContext(CodecContext): @property def bits_per_coded_sample(self): """ - The number of bits per sample in the codedwords, basically the bitrate per sample. - It is mandatory for this to be set for some formats to decode them. + The number of bits per sample in the codedwords. It's mandatory for this to be set for some formats to decode properly. - Wraps :ffmpeg:`AVCodecContext::bits_per_coded_sample` + Wraps :ffmpeg:`AVCodecContext.bits_per_coded_sample`. :type: int """ From c6f80a32bfeda3286e0749ba3f523b3f3c53b6c4 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 14 Mar 2024 02:28:14 -0400 Subject: [PATCH 223/846] sub.ass now returns as bytes I was getting some UnicodeDecoding errors while I was processing some input. Since ffmpeg docs do not promise char* ass contains UTF-8 strings, I thought it was better to have @property ass return bytes instead of str. --- av/subtitles/subtitle.pyi | 2 +- av/subtitles/subtitle.pyx | 8 +++++++- tests/test_subtitles.py | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/av/subtitles/subtitle.pyi b/av/subtitles/subtitle.pyi index 0363c2399..6dcd16e8d 100644 --- a/av/subtitles/subtitle.pyi +++ b/av/subtitles/subtitle.pyi @@ -33,4 +33,4 @@ class TextSubtitle(Subtitle): class AssSubtitle(Subtitle): type: Literal[b"ass"] - ass: str + ass: bytes diff --git a/av/subtitles/subtitle.pyx b/av/subtitles/subtitle.pyx index 5b2fc8a4a..d8d45e3fc 100644 --- a/av/subtitles/subtitle.pyx +++ b/av/subtitles/subtitle.pyx @@ -1,6 +1,10 @@ from cpython cimport PyBuffer_FillInfo +cdef extern from "Python.h": + bytes PyBytes_FromString(char*) + + cdef class SubtitleProxy: def __dealloc__(self): lib.avsubtitle_free(&self.struct) @@ -153,4 +157,6 @@ cdef class AssSubtitle(Subtitle): @property def ass(self): - return self.ptr.ass + if self.ptr.ass is not NULL: + return PyBytes_FromString(self.ptr.ass) + return b"" diff --git a/tests/test_subtitles.py b/tests/test_subtitles.py index c457124f2..fe4f2ee3d 100644 --- a/tests/test_subtitles.py +++ b/tests/test_subtitles.py @@ -24,7 +24,7 @@ def test_movtext(self): sub = subset[0] self.assertIsInstance(sub, AssSubtitle) self.assertEqual(sub.type, b"ass") - self.assertEqual(sub.ass, "0,0,Default,,0,0,0,,- Test 1.\\N- Test 2.") + self.assertEqual(sub.ass, b"0,0,Default,,0,0,0,,- Test 1.\\N- Test 2.") def test_vobsub(self): path = fate_suite("sub/vobsub.sub") From e80ee6395c2c2bc82dedfa5e08e1807eed9918dd Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 14 Mar 2024 02:41:43 -0400 Subject: [PATCH 224/846] Don't require libx264 for tests --- tests/test_codec_context.py | 2 +- tests/test_encode.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 2f9a34aa7..1a2b189cc 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -269,7 +269,7 @@ def image_sequence_encode(self, codec_name): self.assertEqual(frame.format.name, pix_fmt) def test_encoding_h264(self): - self.video_encoding("libx264", {"crf": "19"}) + self.video_encoding("h264", {"crf": "19"}) def test_encoding_mpeg4(self): self.video_encoding("mpeg4") diff --git a/tests/test_encode.py b/tests/test_encode.py index 3ca031794..d0939470a 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -151,7 +151,7 @@ def test_encoding_with_pts(self): path = self.sandboxed("video_with_pts.mov") with av.open(path, "w") as output: - stream = output.add_stream("libx264", 24) + stream = output.add_stream("h264", 24) self.assertIn(stream, output.streams.video) stream.width = WIDTH stream.height = HEIGHT From 6daab70cf9dfab9111f917f19215ba437aedeada Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 14 Mar 2024 17:11:58 -0400 Subject: [PATCH 225/846] Remove scratchpad (#1335) These code examples are outdated, contain naive (sometimes wrong) implementations, and in general, would be a pain to maintain if it were kept up to date. --- scratchpad/README | 4 - scratchpad/__init__.py | 3 - scratchpad/audio.py | 100 ------ scratchpad/audio_player.py | 81 ----- scratchpad/average.py | 59 ---- scratchpad/cctx_decode.py | 32 -- scratchpad/cctx_encode.py | 47 --- scratchpad/container-gc.py | 44 --- scratchpad/decode.py | 164 ---------- scratchpad/dump_format.py | 10 - scratchpad/encode.py | 91 ------ scratchpad/encode_frames.py | 40 --- scratchpad/filter_audio.py | 122 -------- scratchpad/frame_seek_example.py | 418 ------------------------- scratchpad/glproxy.py | 97 ------ scratchpad/graph.py | 18 -- scratchpad/player.py | 100 ------ scratchpad/qtproxy.py | 22 -- scratchpad/remux.py | 65 ---- scratchpad/resource_use.py | 61 ---- scratchpad/save_subtitles.py | 61 ---- scratchpad/second_seek_example.py | 499 ------------------------------ scratchpad/seekmany.py | 49 --- scratchpad/show_frames_opencv.py | 21 -- scratchpad/sidedata.py | 25 -- 25 files changed, 2233 deletions(-) delete mode 100644 scratchpad/README delete mode 100644 scratchpad/__init__.py delete mode 100644 scratchpad/audio.py delete mode 100644 scratchpad/audio_player.py delete mode 100644 scratchpad/average.py delete mode 100644 scratchpad/cctx_decode.py delete mode 100644 scratchpad/cctx_encode.py delete mode 100644 scratchpad/container-gc.py delete mode 100644 scratchpad/decode.py delete mode 100644 scratchpad/dump_format.py delete mode 100644 scratchpad/encode.py delete mode 100644 scratchpad/encode_frames.py delete mode 100644 scratchpad/filter_audio.py delete mode 100644 scratchpad/frame_seek_example.py delete mode 100644 scratchpad/glproxy.py delete mode 100644 scratchpad/graph.py delete mode 100644 scratchpad/player.py delete mode 100644 scratchpad/qtproxy.py delete mode 100644 scratchpad/remux.py delete mode 100644 scratchpad/resource_use.py delete mode 100644 scratchpad/save_subtitles.py delete mode 100644 scratchpad/second_seek_example.py delete mode 100644 scratchpad/seekmany.py delete mode 100644 scratchpad/show_frames_opencv.py delete mode 100644 scratchpad/sidedata.py diff --git a/scratchpad/README b/scratchpad/README deleted file mode 100644 index 83d571e01..000000000 --- a/scratchpad/README +++ /dev/null @@ -1,4 +0,0 @@ -This directory is for the PyAV developers to dump partial or exprimental tests. -The contents of this directory are not guaranteed to work, or make sense in any way. - -Have fun! diff --git a/scratchpad/__init__.py b/scratchpad/__init__.py deleted file mode 100644 index ab4bc5141..000000000 --- a/scratchpad/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -import logging - -logging.basicConfig(level=logging.WARNING) diff --git a/scratchpad/audio.py b/scratchpad/audio.py deleted file mode 100644 index addae0915..000000000 --- a/scratchpad/audio.py +++ /dev/null @@ -1,100 +0,0 @@ -import array -import argparse -import sys -import pprint -import subprocess - -from PIL import Image -import av - - -def print_data(frame): - for i, plane in enumerate(frame.planes or ()): - data = bytes(plane) - print('\tPLANE %d, %d bytes' % (i, len(data))) - data = data.encode('hex') - for i in range(0, len(data), 128): - print('\t\t\t%s' % data[i:i + 128]) - - -arg_parser = argparse.ArgumentParser() -arg_parser.add_argument('path') -arg_parser.add_argument('-p', '--play', action='store_true') -arg_parser.add_argument('-d', '--data', action='store_true') -arg_parser.add_argument('-f', '--format') -arg_parser.add_argument('-l', '--layout') -arg_parser.add_argument('-r', '--rate', type=int) -arg_parser.add_argument('-s', '--size', type=int, default=1024) -arg_parser.add_argument('-c', '--count', type=int, default=5) -args = arg_parser.parse_args() - -ffplay = None - -container = av.open(args.path) -stream = next(s for s in container.streams if s.type == 'audio') - -fifo = av.AudioFifo() if args.size else None -resampler = av.AudioResampler( - format=av.AudioFormat(args.format or stream.format.name).packed if args.format else None, - layout=int(args.layout) if args.layout and args.layout.isdigit() else args.layout, - rate=args.rate, -) if (args.format or args.layout or args.rate) else None - -read_count = 0 -fifo_count = 0 -sample_count = 0 - -for i, packet in enumerate(container.demux(stream)): - - for frame in packet.decode(): - - read_count += 1 - print('>>>> %04d' % read_count, frame) - if args.data: - print_data(frame) - - frames = [frame] - - if resampler: - for i, frame in enumerate(frames): - frame = resampler.resample(frame) - print('RESAMPLED', frame) - if args.data: - print_data(frame) - frames[i] = frame - - if fifo: - - to_process = frames - frames = [] - - for frame in to_process: - fifo.write(frame) - while frame: - frame = fifo.read(args.size) - if frame: - fifo_count += 1 - print('|||| %04d' % fifo_count, frame) - if args.data: - print_data(frame) - frames.append(frame) - - if frames and args.play: - if not ffplay: - cmd = ['ffplay', - '-f', frames[0].format.packed.container_name, - '-ar', str(args.rate or stream.rate), - '-ac', str(len(resampler.layout.channels if resampler else stream.layout.channels)), - '-vn', '-', - ] - print('PLAY', ' '.join(cmd)) - ffplay = subprocess.Popen(cmd, stdin=subprocess.PIPE) - try: - for frame in frames: - ffplay.stdin.write(bytes(frame.planes[0])) - except IOError as e: - print(e) - exit() - - if args.count and read_count >= args.count: - exit() diff --git a/scratchpad/audio_player.py b/scratchpad/audio_player.py deleted file mode 100644 index 8322a3206..000000000 --- a/scratchpad/audio_player.py +++ /dev/null @@ -1,81 +0,0 @@ -import array -import argparse -import sys -import pprint -import subprocess -import time - -from qtproxy import Q - -import av - - -parser = argparse.ArgumentParser() -parser.add_argument('path') -args = parser.parse_args() - -container = av.open(args.path) -stream = next(s for s in container.streams if s.type == 'audio') - -fifo = av.AudioFifo() -resampler = av.AudioResampler( - format=av.AudioFormat('s16').packed, - layout='stereo', - rate=48000, -) - - - -qformat = Q.AudioFormat() -qformat.setByteOrder(Q.AudioFormat.LittleEndian) -qformat.setChannelCount(2) -qformat.setCodec('audio/pcm') -qformat.setSampleRate(48000) -qformat.setSampleSize(16) -qformat.setSampleType(Q.AudioFormat.SignedInt) - -output = Q.AudioOutput(qformat) -output.setBufferSize(2 * 2 * 48000) - -device = output.start() - -print(qformat, output, device) - -def decode_iter(): - try: - for pi, packet in enumerate(container.demux(stream)): - for fi, frame in enumerate(packet.decode()): - yield pi, fi, frame - except: - return - -for pi, fi, frame in decode_iter(): - - frame = resampler.resample(frame) - print(pi, fi, frame, output.state()) - - bytes_buffered = output.bufferSize() - output.bytesFree() - us_processed = output.processedUSecs() - us_buffered = 1000000 * bytes_buffered / (2 * 16 / 8) / 48000 - print('pts: %.3f, played: %.3f, buffered: %.3f' % (frame.time or 0, us_processed / 1000000.0, us_buffered / 1000000.0)) - - - data = bytes(frame.planes[0]) - while data: - written = device.write(data) - if written: - # print 'wrote', written - data = data[written:] - else: - # print 'did not accept data; sleeping' - time.sleep(0.033) - - if False and pi % 100 == 0: - output.reset() - print(output.state(), output.error()) - device = output.start() - - # time.sleep(0.05) - -while output.state() == Q.Audio.ActiveState: - time.sleep(0.1) diff --git a/scratchpad/average.py b/scratchpad/average.py deleted file mode 100644 index f72297f08..000000000 --- a/scratchpad/average.py +++ /dev/null @@ -1,59 +0,0 @@ -import argparse -import os -import sys -import pprint -import itertools - -import cv2 - -from av import open - - -parser = argparse.ArgumentParser() -parser.add_argument('-f', '--format') -parser.add_argument('-n', '--frames', type=int, default=0) -parser.add_argument('path', nargs='+') -args = parser.parse_args() - -max_size = 24 * 60 # One minute's worth. - - -def frame_iter(video): - count = 0 - streams = [s for s in video.streams if s.type == b'video'] - streams = [streams[0]] - for packet in video.demux(streams): - for frame in packet.decode(): - yield frame - count += 1 - if args.frames and count > args.frames: - return - - -for src_path in args.path: - - print('reading', src_path) - - basename = os.path.splitext(os.path.basename(src_path))[0] - dir_name = os.path.join('sandbox', basename) - if not os.path.exists(dir_name): - os.makedirs(dir_name) - - video = open(src_path, format=args.format) - frames = frame_iter(video) - - sum_ = None - - for fi, frame in enumerate(frame_iter(video)): - - if sum_ is None: - sum_ = frame.to_ndarray().astype(float) - else: - sum_ += frame.to_ndarray().astype(float) - - sum_ /= (fi + 1) - - dst_path = os.path.join('sandbox', os.path.basename(src_path) + '-avg.jpeg') - print('writing', (fi + 1), 'frames to', dst_path) - - cv2.imwrite(dst_path, sum_) diff --git a/scratchpad/cctx_decode.py b/scratchpad/cctx_decode.py deleted file mode 100644 index bdb6724f4..000000000 --- a/scratchpad/cctx_decode.py +++ /dev/null @@ -1,32 +0,0 @@ -import logging - -logging.basicConfig() - - -import av -from av.codec import CodecContext, CodecParser -from av.video import VideoFrame -from av.packet import Packet - - -cc = CodecContext.create('mpeg4', 'r') -print(cc) - - -fh = open('test.mp4', 'r') - -frame_count = 0 - -while True: - - chunk = fh.read(819200) - for packet in cc.parse(chunk or None, allow_stream=True): - print(packet) - for frame in cc.decode(packet) or (): - print(frame) - img = frame.to_image() - img.save('sandbox/test.%04d.jpg' % frame_count) - frame_count += 1 - - if not chunk: - break # EOF! diff --git a/scratchpad/cctx_encode.py b/scratchpad/cctx_encode.py deleted file mode 100644 index 165203c1c..000000000 --- a/scratchpad/cctx_encode.py +++ /dev/null @@ -1,47 +0,0 @@ -import logging - -from PIL import Image, ImageDraw, ImageFont - -from av.codec import CodecContext -from av.video import VideoFrame -from tests.common import fate_suite - - -logging.basicConfig() - - -cc = CodecContext.create('flv', 'w') -print(cc) - -base_img = Image.open(fate_suite('png1/lena-rgb24.png')) -font = ImageFont.truetype("/System/Library/Fonts/Menlo.ttc", 15) - - -fh = open('test.flv', 'wb') - -for i in range(30): - - print(i) - img = base_img.copy() - draw = ImageDraw.Draw(img) - draw.text((10, 10), "FRAME %02d" % i, font=font) - - frame = VideoFrame.from_image(img) - frame = frame.reformat(format='yuv420p') - print(' ', frame) - - packet = cc.encode(frame) - print(' ', packet) - - fh.write(bytes(packet)) - -print('Flushing...') - -while True: - packet = cc.encode() - if not packet: - break - print(' ', packet) - fh.write(bytes(packet)) - -print('Done!') diff --git a/scratchpad/container-gc.py b/scratchpad/container-gc.py deleted file mode 100644 index a5bd2130d..000000000 --- a/scratchpad/container-gc.py +++ /dev/null @@ -1,44 +0,0 @@ -import resource -import gc - - -import av -import av.datasets - -path = av.datasets.curated('pexels/time-lapse-video-of-night-sky-857195.mp4') - - -def format_bytes(n): - order = 0 - while n > 1024: - order += 1 - n //= 1024 - return '%d%sB' % (n, ('', 'k', 'M', 'G', 'T', 'P')[order]) - -after = resource.getrusage(resource.RUSAGE_SELF) - -count = 0 - -streams = [] - -while True: - - container = av.open(path) - # streams.append(container.streams.video[0]) - - del container - gc.collect() - - count += 1 - if not count % 100: - pass - # streams.clear() - # gc.collect() - - before = after - after = resource.getrusage(resource.RUSAGE_SELF) - print('{:6d} {} ({})'.format( - count, - format_bytes(after.ru_maxrss), - format_bytes(after.ru_maxrss - before.ru_maxrss), - )) diff --git a/scratchpad/decode.py b/scratchpad/decode.py deleted file mode 100644 index edfb413b7..000000000 --- a/scratchpad/decode.py +++ /dev/null @@ -1,164 +0,0 @@ -import array -import argparse -import logging -import sys -import pprint -import subprocess - -from PIL import Image - -from av import open, time_base - - -logging.basicConfig(level=logging.DEBUG) - - -def format_time(time, time_base): - if time is None: - return 'None' - return '%.3fs (%s or %s/%s)' % (time_base * time, time_base * time, time_base.numerator * time, time_base.denominator) - - -arg_parser = argparse.ArgumentParser() -arg_parser.add_argument('path') -arg_parser.add_argument('-f', '--format') -arg_parser.add_argument('-a', '--audio', action='store_true') -arg_parser.add_argument('-v', '--video', action='store_true') -arg_parser.add_argument('-s', '--subs', action='store_true') -arg_parser.add_argument('-d', '--data', action='store_true') -arg_parser.add_argument('--dump-packets', action='store_true') -arg_parser.add_argument('--dump-planes', action='store_true') -arg_parser.add_argument('-p', '--play', action='store_true') -arg_parser.add_argument('-t', '--thread-type') -arg_parser.add_argument('-o', '--option', action='append', default=[]) -arg_parser.add_argument('-c', '--count', type=int, default=5) -args = arg_parser.parse_args() - - -proc = None - -options = dict(x.split('=') for x in args.option) -container = open(args.path, format=args.format, options=options) - -print('container:', container) -print('\tformat:', container.format) -print('\tduration:', float(container.duration) / time_base) -print('\tmetadata:') -for k, v in sorted(container.metadata.items()): - print('\t\t%s: %r' % (k, v)) -print() - -print(len(container.streams), 'stream(s):') -for i, stream in enumerate(container.streams): - - if args.thread_type: - stream.codec_context.thread_type = args.thread_type - - print('\t%r' % stream) - print('\t\ttime_base: %r' % stream.time_base) - print('\t\trate: %r' % stream.rate) - print('\t\tstart_time: %r' % stream.start_time) - print('\t\tduration: %s' % format_time(stream.duration, stream.time_base)) - print('\t\tbit_rate: %r' % stream.bit_rate) - print('\t\tbit_rate_tolerance: %r' % stream.bit_rate_tolerance) - - codec_context = stream.codec_context - if codec_context: - print('\t\tcodec_context:', codec_context) - print('\t\t\ttime_base:', codec_context.time_base) - - if stream.type == b'audio': - print('\t\taudio:') - print('\t\t\tformat:', stream.format) - print('\t\t\tchannels: %s' % stream.channels) - - elif stream.type == 'video': - print('\t\tvideo:') - print('\t\t\tformat:', stream.format) - print('\t\t\taverage_rate: %r' % stream.average_rate) - - print('\t\tmetadata:') - for k, v in sorted(stream.metadata.items()): - print('\t\t\t%s: %r' % (k, v)) - - print() - - -streams = [s for s in container.streams if - (s.type == 'audio' and args.audio) or - (s.type == 'video' and args.video) or - (s.type == 'subtitle' and args.subs) or - (s.type == 'data' and args.data) -] - - -frame_count = 0 - -for i, packet in enumerate(container.demux(streams)): - - print('%02d %r' % (i, packet)) - print('\ttime_base: %s' % packet.time_base) - print('\tduration: %s' % format_time(packet.duration, packet.stream.time_base)) - print('\tpts: %s' % format_time(packet.pts, packet.stream.time_base)) - print('\tdts: %s' % format_time(packet.dts, packet.stream.time_base)) - print('\tkey: %s' % packet.is_keyframe) - - if args.dump_packets: - print(bytes(packet)) - - for frame in packet.decode(): - - frame_count += 1 - - print('\tdecoded:', frame) - print('\t\ttime_base: %s' % frame.time_base) - print('\t\tpts:', format_time(frame.pts, packet.stream.time_base)) - - if packet.stream.type == 'video': - pass - - elif packet.stream.type == 'audio': - print('\t\tsamples:', frame.samples) - print('\t\tformat:', frame.format.name) - print('\t\tlayout:', frame.layout.name) - - elif packet.stream.type == 'subtitle': - - sub = frame - - print('\t\tformat:', sub.format) - print('\t\tstart_display_time:', format_time(sub.start_display_time, packet.stream.time_base)) - print('\t\tend_display_time:', format_time(sub.end_display_time, packet.stream.time_base)) - print('\t\trects: %d' % len(sub.rects)) - for rect in sub.rects: - print('\t\t\t%r' % rect) - if rect.type == 'ass': - print('\t\t\t\tass: %r' % rect.ass) - - if args.play and packet.stream.type == 'audio': - if not proc: - cmd = ['ffplay', - '-f', 's16le', - '-ar', str(packet.stream.time_base), - '-vn', '-', - ] - proc = subprocess.Popen(cmd, stdin=subprocess.PIPE) - try: - proc.stdin.write(bytes(frame.planes[0])) - except IOError as e: - print(e) - exit() - - if args.dump_planes: - print('\t\tplanes') - for i, plane in enumerate(frame.planes or ()): - data = bytes(plane) - print('\t\t\tPLANE %d, %d bytes' % (i, len(data))) - data = data.encode('hex') - for i in range(0, len(data), 128): - print('\t\t\t%s' % data[i:i + 128]) - - if args.count and frame_count >= args.count: - exit() - - print() diff --git a/scratchpad/dump_format.py b/scratchpad/dump_format.py deleted file mode 100644 index 80682fb47..000000000 --- a/scratchpad/dump_format.py +++ /dev/null @@ -1,10 +0,0 @@ -import sys -import logging - -logging.basicConfig(level=logging.DEBUG) -logging.getLogger('libav').setLevel(logging.DEBUG) - -import av - -fh = av.open(sys.argv[1]) -print(fh.dumps_format()) diff --git a/scratchpad/encode.py b/scratchpad/encode.py deleted file mode 100644 index 099ac4a14..000000000 --- a/scratchpad/encode.py +++ /dev/null @@ -1,91 +0,0 @@ -import argparse -import logging -import os -import sys - -import av -from tests.common import asset, sandboxed - - -arg_parser = argparse.ArgumentParser() -arg_parser.add_argument('-v', '--verbose', action='store_true') -arg_parser.add_argument('input', nargs=1) -args = arg_parser.parse_args() - -input_file = av.open(args.input[0]) -input_video_stream = None # next((s for s in input_file.streams if s.type == 'video'), None) -input_audio_stream = next((s for s in input_file.streams if s.type == 'audio'), None) - -# open output file -output_file_path = sandboxed('encoded-' + os.path.basename(args.input[0])) -output_file = av.open(output_file_path, 'w') -output_video_stream = output_file.add_stream("mpeg4", 24) if input_video_stream else None -output_audio_stream = output_file.add_stream("mp3") if input_audio_stream else None - - -frame_count = 0 - - -for packet in input_file.demux([s for s in (input_video_stream, input_audio_stream) if s]): - - - if args.verbose: - print('in ', packet) - - for frame in packet.decode(): - - if args.verbose: - print('\t%s' % frame) - - if packet.stream.type == b'video': - if frame_count % 10 == 0: - if frame_count: - print() - print(('%03d:' % frame_count), end=' ') - sys.stdout.write('.') - sys.stdout.flush() - - frame_count += 1 - - # Signal to generate it's own timestamps. - frame.pts = None - - stream = output_audio_stream if packet.stream.type == b'audio' else output_video_stream - output_packets = [output_audio_stream.encode(frame)] - while output_packets[-1]: - output_packets.append(output_audio_stream.encode(None)) - - for p in output_packets: - if p: - if args.verbose: - print('OUT', p) - output_file.mux(p) - - if frame_count >= 100: - break - -print('-' * 78) - -# Finally we need to flush out the frames that are buffered in the encoder. -# To do that we simply call encode with no args until we get a None returned -if output_audio_stream: - while True: - output_packet = output_audio_stream.encode(None) - if output_packet: - if args.verbose: - print('<<<', output_packet) - output_file.mux(output_packet) - else: - break - -if output_video_stream: - while True: - output_packet = output_video_stream.encode(None) - if output_packet: - if args.verbose: - print('<<<', output_packet) - output_file.mux(output_packet) - else: - break - -output_file.close() diff --git a/scratchpad/encode_frames.py b/scratchpad/encode_frames.py deleted file mode 100644 index 642a1c2c6..000000000 --- a/scratchpad/encode_frames.py +++ /dev/null @@ -1,40 +0,0 @@ -import argparse -import os -import sys - -import av -import cv2 - - -arg_parser = argparse.ArgumentParser() -arg_parser.add_argument('-r', '--rate', default='23.976') -arg_parser.add_argument('-f', '--format', default='yuv420p') -arg_parser.add_argument('-w', '--width', type=int) -arg_parser.add_argument('--height', type=int) -arg_parser.add_argument('-b', '--bitrate', type=int, default=8000000) -arg_parser.add_argument('-c', '--codec', default='mpeg4') -arg_parser.add_argument('inputs', nargs='+') -arg_parser.add_argument('output', nargs=1) -args = arg_parser.parse_args() - - -output = av.open(args.output[0], 'w') -stream = output.add_stream(args.codec, args.rate) -stream.bit_rate = args.bitrate -stream.pix_fmt = args.format - -for i, path in enumerate(args.inputs): - - print(os.path.basename(path)) - - img = cv2.imread(path) - - if not i: - stream.height = args.height or (args.width * img.shape[0] / img.shape[1]) or img.shape[0] - stream.width = args.width or img.shape[1] - - frame = av.VideoFrame.from_ndarray(img, format='bgr24') - packet = stream.encode(frame) - output.mux(packet) - -output.close() diff --git a/scratchpad/filter_audio.py b/scratchpad/filter_audio.py deleted file mode 100644 index 092aba3ae..000000000 --- a/scratchpad/filter_audio.py +++ /dev/null @@ -1,122 +0,0 @@ -""" -Simple audio filtering example ported from C code: - https://github.com/FFmpeg/FFmpeg/blob/master/doc/examples/filter_audio.c -""" -from fractions import Fraction -import hashlib -import sys - -import numpy as np - -import av -import av.audio.frame as af -import av.filter - - -FRAME_SIZE = 1024 - -INPUT_SAMPLE_RATE = 48000 -INPUT_FORMAT = 'fltp' -INPUT_CHANNEL_LAYOUT = '5.0(side)' # -> AV_CH_LAYOUT_5POINT0 - -OUTPUT_SAMPLE_RATE = 44100 -OUTPUT_FORMAT = 's16' # notice, packed audio format, expect only one plane in output -OUTPUT_CHANNEL_LAYOUT = 'stereo' # -> AV_CH_LAYOUT_STEREO - -VOLUME_VAL = 0.90 - - -def init_filter_graph(): - graph = av.filter.Graph() - - output_format = 'sample_fmts={}:sample_rates={}:channel_layouts={}'.format( - OUTPUT_FORMAT, - OUTPUT_SAMPLE_RATE, - OUTPUT_CHANNEL_LAYOUT - ) - print('Output format: {}'.format(output_format)) - - # initialize filters - filter_chain = [ - graph.add_abuffer(format=INPUT_FORMAT, - sample_rate=INPUT_SAMPLE_RATE, - layout=INPUT_CHANNEL_LAYOUT, - time_base=Fraction(1, INPUT_SAMPLE_RATE)), - # initialize filter with keyword parameters - graph.add('volume', volume=str(VOLUME_VAL)), - # or compound string configuration - graph.add('aformat', output_format), - graph.add('abuffersink') - ] - - # link up the filters into a chain - print('Filter graph:') - for c, n in zip(filter_chain, filter_chain[1:]): - print('\t{} -> {}'.format(c, n)) - c.link_to(n) - - # initialize the filter graph - graph.configure() - - return graph - - -def get_input(frame_num): - """ - Manually construct and update AudioFrame. - Consider using AudioFrame.from_ndarry for most real life numpy->AudioFrame conversions. - - :param frame_num: - :return: - """ - frame = av.AudioFrame(format=INPUT_FORMAT, layout=INPUT_CHANNEL_LAYOUT, samples=FRAME_SIZE) - frame.sample_rate = INPUT_SAMPLE_RATE - frame.pts = frame_num * FRAME_SIZE - - for i in range(len(frame.layout.channels)): - data = np.zeros(FRAME_SIZE, dtype=af.format_dtypes[INPUT_FORMAT]) - for j in range(FRAME_SIZE): - data[j] = np.sin(2 * np.pi * (frame_num + j) * (i + 1) / float(FRAME_SIZE)) - frame.planes[i].update(data) - - return frame - - -def process_output(frame): - data = frame.to_ndarray() - for i in range(data.shape[0]): - m = hashlib.md5(data[i, :].tobytes()) - print('Plane: {:0d} checksum: {}'.format(i, m.hexdigest())) - - -def main(duration): - frames_count = int(duration * INPUT_SAMPLE_RATE / FRAME_SIZE) - - graph = init_filter_graph() - - for f in range(frames_count): - frame = get_input(f) - - # submit the frame for processing - graph.push(frame) - - # pull frames from graph until graph has done processing or is waiting for a new input - while True: - try: - out_frame = graph.pull() - process_output(out_frame) - except (BlockingIOError, av.EOFError): - break - - # process any remaining buffered frames - while True: - try: - out_frame = graph.pull() - process_output(out_frame) - except (BlockingIOError, av.EOFError): - break - - -if __name__ == '__main__': - duration = 1.0 if len(sys.argv) < 2 else float(sys.argv[1]) - main(duration) diff --git a/scratchpad/frame_seek_example.py b/scratchpad/frame_seek_example.py deleted file mode 100644 index 25b2d27d0..000000000 --- a/scratchpad/frame_seek_example.py +++ /dev/null @@ -1,418 +0,0 @@ -""" -Note this example only really works accurately on constant frame rate media. -""" -from PyQt4 import QtGui -from PyQt4 import QtCore -from PyQt4.QtCore import Qt - -import sys -import av - - -AV_TIME_BASE = 1000000 - -def pts_to_frame(pts, time_base, frame_rate, start_time): - return int(pts * time_base * frame_rate) - int(start_time * time_base * frame_rate) - -def get_frame_rate(stream): - - if stream.average_rate.denominator and stream.average_rate.numerator: - return float(stream.average_rate) - if stream.time_base.denominator and stream.time_base.numerator: - return 1.0 / float(stream.time_base) - else: - raise ValueError("Unable to determine FPS") - -def get_frame_count(f, stream): - - if stream.frames: - return stream.frames - elif stream.duration: - return pts_to_frame(stream.duration, float(stream.time_base), get_frame_rate(stream), 0) - elif f.duration: - return pts_to_frame(f.duration, 1 / float(AV_TIME_BASE), get_frame_rate(stream), 0) - - else: - raise ValueError("Unable to determine number for frames") - -class FrameGrabber(QtCore.QObject): - - frame_ready = QtCore.pyqtSignal(object, object) - update_frame_range = QtCore.pyqtSignal(object) - - def __init__(self, parent=None): - super(FrameGrabber, self).__init__(parent) - self.file = None - self.stream = None - self.frame = None - self.active_frame = None - self.start_time = 0 - self.pts_seen = False - self.nb_frames = None - - self.rate = None - self.time_base = None - - def next_frame(self): - - frame_index = None - - rate = self.rate - time_base = self.time_base - - self.pts_seen = False - - for packet in self.file.demux(self.stream): - #print " pkt", packet.pts, packet.dts, packet - if packet.pts: - self.pts_seen = True - - for frame in packet.decode(): - - if frame_index is None: - - if self.pts_seen: - pts = frame.pts - else: - pts = frame.dts - - if not pts is None: - frame_index = pts_to_frame(pts, time_base, rate, self.start_time) - - elif not frame_index is None: - frame_index += 1 - - - yield frame_index, frame - - - @QtCore.pyqtSlot(object) - def request_frame(self, target_frame): - - frame = self.get_frame(target_frame) - if not frame: - return - - rgba = frame.reformat(frame.width, frame.height, "rgb24", 'itu709') - #print rgba.to_image().save("test.png") - # could use the buffer interface here instead, some versions of PyQt don't support it for some reason - # need to track down which version they added support for it - self.frame = bytearray(rgba.planes[0]) - bytesPerPixel = 3 - img = QtGui.QImage(self.frame, rgba.width, rgba.height, rgba.width * bytesPerPixel, QtGui.QImage.Format_RGB888) - - #img = QtGui.QImage(rgba.planes[0], rgba.width, rgba.height, QtGui.QImage.Format_RGB888) - - #pixmap = QtGui.QPixmap.fromImage(img) - self.frame_ready.emit(img, target_frame) - - def get_frame(self, target_frame): - - if target_frame != self.active_frame: - return - print('seeking to', target_frame) - - seek_frame = target_frame - - rate = self.rate - time_base = self.time_base - - frame = None - reseek = 250 - - original_target_frame_pts = None - - while reseek >= 0: - - # convert seek_frame to pts - target_sec = seek_frame * 1 / rate - target_pts = int(target_sec / time_base) + self.start_time - - if original_target_frame_pts is None: - original_target_frame_pts = target_pts - - self.stream.seek(int(target_pts)) - - frame_index = None - - frame_cache = [] - - for i, (frame_index, frame) in enumerate(self.next_frame()): - - # optimization if the time slider has changed, the requested frame no longer valid - if target_frame != self.active_frame: - return - - print(" ", i, "at frame", frame_index, "at ts:", frame.pts, frame.dts, "target:", target_pts, 'orig', original_target_frame_pts) - - if frame_index is None: - pass - - elif frame_index >= target_frame: - break - - frame_cache.append(frame) - - # Check if we over seeked, if we over seekd we need to seek to a earlier time - # but still looking for the target frame - if frame_index != target_frame: - - if frame_index is None: - over_seek = '?' - else: - over_seek = frame_index - target_frame - if frame_index > target_frame: - - print(over_seek, frame_cache) - if over_seek <= len(frame_cache): - print("over seeked by %i, using cache" % over_seek) - frame = frame_cache[-over_seek] - break - - - seek_frame -= 1 - reseek -= 1 - print("over seeked by %s, backtracking.. seeking: %i target: %i retry: %i" % (str(over_seek), seek_frame, target_frame, reseek)) - - else: - break - - if reseek < 0: - raise ValueError("seeking failed %i" % frame_index) - - # frame at this point should be the correct frame - - if frame: - - return frame - - else: - raise ValueError("seeking failed %i" % target_frame) - - def get_frame_count(self): - - frame_count = None - - if self.stream.frames: - frame_count = self.stream.frames - elif self.stream.duration: - frame_count = pts_to_frame(self.stream.duration, float(self.stream.time_base), get_frame_rate(self.stream), 0) - elif self.file.duration: - frame_count = pts_to_frame(self.file.duration, 1 / float(AV_TIME_BASE), get_frame_rate(self.stream), 0) - else: - raise ValueError("Unable to determine number for frames") - - seek_frame = frame_count - - retry = 100 - - while retry: - target_sec = seek_frame * 1 / self.rate - target_pts = int(target_sec / self.time_base) + self.start_time - - self.stream.seek(int(target_pts)) - - frame_index = None - - for frame_index, frame in self.next_frame(): - print(frame_index, frame) - continue - - if not frame_index is None: - break - else: - seek_frame -= 1 - retry -= 1 - - - print("frame count seeked", frame_index, "container frame count", frame_count) - - return frame_index or frame_count - - @QtCore.pyqtSlot(object) - def set_file(self, path): - self.file = av.open(path) - self.stream = next(s for s in self.file.streams if s.type == b'video') - self.rate = get_frame_rate(self.stream) - self.time_base = float(self.stream.time_base) - - - index, first_frame = next(self.next_frame()) - self.stream.seek(self.stream.start_time) - - # find the pts of the first frame - index, first_frame = next(self.next_frame()) - - if self.pts_seen: - pts = first_frame.pts - else: - pts = first_frame.dts - - self.start_time = pts or first_frame.dts - - print("First pts", pts, self.stream.start_time, first_frame) - - #self.nb_frames = get_frame_count(self.file, self.stream) - self.nb_frames = self.get_frame_count() - - self.update_frame_range.emit(self.nb_frames) - - - - - -class DisplayWidget(QtGui.QLabel): - def __init__(self, parent=None): - super(DisplayWidget, self).__init__(parent) - #self.setScaledContents(True) - self.setMinimumSize(1920 / 10, 1080 / 10) - - size_policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) - size_policy.setHeightForWidth(True) - - self.setSizePolicy(size_policy) - - self.setAlignment(Qt.AlignHCenter | Qt.AlignBottom) - - self.pixmap = None - self.setMargin(10) - - def heightForWidth(self, width): - return width * 9 / 16.0 - - @QtCore.pyqtSlot(object, object) - def setPixmap(self, img, index): - #if index == self.current_index: - self.pixmap = QtGui.QPixmap.fromImage(img) - - #super(DisplayWidget, self).setPixmap(self.pixmap) - super(DisplayWidget, self).setPixmap(self.pixmap.scaled(self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) - - def sizeHint(self): - width = self.width() - return QtCore.QSize(width, self.heightForWidth(width)) - - def resizeEvent(self, event): - if self.pixmap: - super(DisplayWidget, self).setPixmap(self.pixmap.scaled(self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) - - def sizeHint(self): - return QtCore.QSize(1920 / 2.5, 1080 / 2.5) - - -class VideoPlayerWidget(QtGui.QWidget): - - request_frame = QtCore.pyqtSignal(object) - - load_file = QtCore.pyqtSignal(object) - - def __init__(self, parent=None): - super(VideoPlayerWidget, self).__init__(parent) - self.display = DisplayWidget() - self.timeline = QtGui.QScrollBar(Qt.Horizontal) - self.frame_grabber = FrameGrabber() - - self.frame_control = QtGui.QSpinBox() - self.frame_control.setFixedWidth(100) - - self.timeline.valueChanged.connect(self.frame_changed) - self.frame_control.valueChanged.connect(self.frame_changed) - - self.request_frame.connect(self.frame_grabber.request_frame) - self.load_file.connect(self.frame_grabber.set_file) - - self.frame_grabber.frame_ready.connect(self.display.setPixmap) - self.frame_grabber.update_frame_range.connect(self.set_frame_range) - - self.frame_grabber_thread = QtCore.QThread() - - self.frame_grabber.moveToThread(self.frame_grabber_thread) - self.frame_grabber_thread.start() - - control_layout = QtGui.QHBoxLayout() - control_layout.addWidget(self.frame_control) - control_layout.addWidget(self.timeline) - - layout = QtGui.QVBoxLayout() - layout.addWidget(self.display) - layout.addLayout(control_layout) - self.setLayout(layout) - self.setAcceptDrops(True) - - def set_file(self, path): - #self.frame_grabber.set_file(path) - self.load_file.emit(path) - self.frame_changed(0) - - @QtCore.pyqtSlot(object) - def set_frame_range(self, maximum): - print("frame range =", maximum) - self.timeline.setMaximum(maximum) - self.frame_control.setMaximum(maximum) - - def frame_changed(self, value): - self.timeline.blockSignals(True) - self.frame_control.blockSignals(True) - - self.timeline.setValue(value) - self.frame_control.setValue(value) - - self.timeline.blockSignals(False) - self.frame_control.blockSignals(False) - - #self.display.current_index = value - self.frame_grabber.active_frame = value - - self.request_frame.emit(value) - - def keyPressEvent(self, event): - if event.key() in (Qt.Key_Right, Qt.Key_Left): - direction = 1 - if event.key() == Qt.Key_Left: - direction = -1 - - if event.modifiers() == Qt.ShiftModifier: - print('shift') - direction *= 10 - - self.timeline.setValue(self.timeline.value() + direction) - - else: - super(VideoPlayerWidget, self).keyPressEvent(event) - - def mousePressEvent(self, event): - # clear focus of spinbox - focused_widget = QtGui.QApplication.focusWidget() - if focused_widget: - focused_widget.clearFocus() - - super(VideoPlayerWidget, self).mousePressEvent(event) - - def dragEnterEvent(self, event): - event.accept() - - def dropEvent(self, event): - - mime = event.mimeData() - event.accept() - - - if mime.hasUrls(): - path = str(mime.urls()[0].path()) - self.set_file(path) - def closeEvent(self, event): - - self.frame_grabber.active_frame = -1 - self.frame_grabber_thread.quit() - self.frame_grabber_thread.wait() - - event.accept() - - -if __name__ == "__main__": - app = QtGui.QApplication(sys.argv) - window = VideoPlayerWidget() - test_file = sys.argv[1] - window.set_file(test_file) - window.show() - sys.exit(app.exec_()) diff --git a/scratchpad/glproxy.py b/scratchpad/glproxy.py deleted file mode 100644 index b6054e648..000000000 --- a/scratchpad/glproxy.py +++ /dev/null @@ -1,97 +0,0 @@ -'''Mikes wrapper for the visualizer???''' -from contextlib import contextmanager - -from OpenGL.GLUT import * -from OpenGL.GLU import * -from OpenGL.GL import * -import OpenGL - - -__all__ = ''' - gl - glu - glut -'''.strip().split() - - -class ModuleProxy(object): - - def __init__(self, name, module): - self.name = name - self.module = module - - def __getattr__(self, name): - if name.isupper(): - return getattr(self.module, self.name.upper() + '_' + name) - else: - # convert to camel case - name = name.split('_') - name = [x[0].upper() + x[1:] for x in name] - name = ''.join(name) - return getattr(self.module, self.name + name) - - -class GLProxy(ModuleProxy): - - @contextmanager - def matrix(self): - self.module.glPushMatrix() - try: - yield - finally: - self.module.glPopMatrix() - - @contextmanager - def attrib(self, *args): - mask = 0 - for arg in args: - if isinstance(arg, str): - arg = getattr(self.module, 'GL_%s_BIT' % arg.upper()) - mask |= arg - self.module.glPushAttrib(mask) - try: - yield - finally: - self.module.glPopAttrib() - - def enable(self, *args, **kwargs): - self._enable(True, args, kwargs) - return self._apply_on_exit(self._enable, False, args, kwargs) - - def disable(self, *args, **kwargs): - self._enable(False, args, kwargs) - return self._apply_on_exit(self._enable, True, args, kwargs) - - def _enable(self, enable, args, kwargs): - todo = [] - for arg in args: - if isinstance(arg, str): - arg = getattr(self.module, 'GL_%s' % arg.upper()) - todo.append((arg, enable)) - for key, value in kwargs.iteritems(): - flag = getattr(self.module, 'GL_%s' % key.upper()) - value = value if enable else not value - todo.append((flag, value)) - for flag, value in todo: - if value: - self.module.glEnable(flag) - else: - self.module.glDisable(flag) - - def begin(self, arg): - if isinstance(arg, str): - arg = getattr(self.module, 'GL_%s' % arg.upper()) - self.module.glBegin(arg) - return self._apply_on_exit(self.module.glEnd) - - @contextmanager - def _apply_on_exit(self, func, *args, **kwargs): - try: - yield - finally: - func(*args, **kwargs) - - -gl = GLProxy('gl', OpenGL.GL) -glu = ModuleProxy('glu', OpenGL.GLU) -glut = ModuleProxy('glut', OpenGL.GLUT) diff --git a/scratchpad/graph.py b/scratchpad/graph.py deleted file mode 100644 index d1a209c37..000000000 --- a/scratchpad/graph.py +++ /dev/null @@ -1,18 +0,0 @@ -from av.filter.graph import Graph - -g = Graph() -print(g.dump()) - -f = g.pull() - -print(f) - -f = f.reformat(format='rgb24') - -print(f) - -img = f.to_image() - -print(img) - -img.save('graph.png') diff --git a/scratchpad/player.py b/scratchpad/player.py deleted file mode 100644 index e3a7898dc..000000000 --- a/scratchpad/player.py +++ /dev/null @@ -1,100 +0,0 @@ -import argparse -import ctypes -import os -import sys -import pprint -import time - -from qtproxy import Q -from glproxy import gl - -import av - -WIDTH = 960 -HEIGHT = 540 - - -class PlayerGLWidget(Q.GLWidget): - - def initializeGL(self): - print('initialize GL') - gl.clearColor(0, 0, 0, 0) - - gl.enable(gl.TEXTURE_2D) - - # gl.texEnv(gl.TEXTURE_ENV, gl.TEXTURE_ENV_MODE, gl.DECAL) - self.tex_id = gl.genTextures(1) - gl.bindTexture(gl.TEXTURE_2D, self.tex_id) - gl.texParameter(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) - gl.texParameter(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST) - - print('texture id', self.tex_id) - - def setImage(self, w, h, img): - gl.texImage2D(gl.TEXTURE_2D, 0, 3, w, h, 0, gl.RGB, gl.UNSIGNED_BYTE, img) - - def resizeGL(self, w, h): - print('resize to', w, h) - gl.viewport(0, 0, w, h) - # gl.matrixMode(gl.PROJECTION) - # gl.loadIdentity() - # gl.ortho(0, w, 0, h, -10, 10) - # gl.matrixMode(gl.MODELVIEW) - - def paintGL(self): - # print 'paint!' - gl.clear(gl.COLOR_BUFFER_BIT) - with gl.begin('polygon'): - gl.texCoord(0, 0); gl.vertex(-1, 1) - gl.texCoord(1, 0); gl.vertex(1, 1) - gl.texCoord(1, 1); gl.vertex(1, -1) - gl.texCoord(0, 1); gl.vertex(-1, -1) - - - -parser = argparse.ArgumentParser() -parser.add_argument('-f', '--format') -parser.add_argument('path') -args = parser.parse_args() - - -def _iter_images(): - video = av.open(args.path, format=args.format) - stream = next(s for s in video.streams if s.type == b'video') - for packet in video.demux(stream): - for frame in packet.decode(): - yield frame.reformat(frame.width, frame.height, 'rgb24') - -image_iter = _iter_images() - -app = Q.Application([]) - -glwidget = PlayerGLWidget() -glwidget.setFixedWidth(WIDTH) -glwidget.setFixedHeight(HEIGHT) -glwidget.show() -glwidget.raise_() - -start_time = 0 -count = 0 - -timer = Q.Timer() -timer.setInterval(1000 / 30) -@timer.timeout.connect -def on_timeout(*args): - - global start_time, count - start_time = start_time or time.time() - - frame = next(image_iter) - ptr = ctypes.c_void_p(frame.planes[0].ptr) - glwidget.setImage(frame.width, frame.height, ptr) - glwidget.updateGL() - - count += 1 - elapsed = time.time() - start_time - print(frame.pts, frame.dts, '%.2ffps' % (count / elapsed)) - -timer.start() - -app.exec_() diff --git a/scratchpad/qtproxy.py b/scratchpad/qtproxy.py deleted file mode 100644 index a39b76fa2..000000000 --- a/scratchpad/qtproxy.py +++ /dev/null @@ -1,22 +0,0 @@ -import sys - -sys.path.append('/usr/local/lib/python2.7/site-packages') -from PyQt4 import QtCore, QtGui, QtOpenGL, QtMultimedia - - -class QtProxy(object): - - def __init__(self, *modules): - self._modules = modules - - def __getattr__(self, base_name): - for mod in self._modules: - for prefix in ('Q', '', 'Qt'): - name = prefix + base_name - obj = getattr(mod, name, None) - if obj is not None: - setattr(self, base_name, obj) - return obj - raise AttributeError(base_name) - -Q = QtProxy(QtGui, QtCore, QtCore.Qt, QtOpenGL, QtMultimedia) diff --git a/scratchpad/remux.py b/scratchpad/remux.py deleted file mode 100644 index 99de07c2c..000000000 --- a/scratchpad/remux.py +++ /dev/null @@ -1,65 +0,0 @@ -import argparse -import logging - -import av - - -logging.basicConfig(level=logging.DEBUG) - - -arg_parser = argparse.ArgumentParser() -arg_parser.add_argument("input") -arg_parser.add_argument("output") -arg_parser.add_argument("-F", "--iformat") -arg_parser.add_argument("-O", "--ioption", action="append", default=[]) -arg_parser.add_argument("-f", "--oformat") -arg_parser.add_argument("-o", "--ooption", action="append", default=[]) -arg_parser.add_argument("-a", "--noaudio", action="store_true") -arg_parser.add_argument("-v", "--novideo", action="store_true") -arg_parser.add_argument("-s", "--nosubs", action="store_true") -arg_parser.add_argument("-d", "--nodata", action="store_true") -arg_parser.add_argument("-c", "--count", type=int, default=0) -args = arg_parser.parse_args() - - -input_ = av.open( - args.input, - format=args.iformat, - options=dict(x.split("=") for x in args.ioption), -) -output = av.open( - args.output, - "w", - format=args.oformat, - options=dict(x.split("=") for x in args.ooption), -) - -in_to_out = {} - -for i, stream in enumerate(input_.streams): - if ( - (stream.type == "audio" and not args.noaudio) - or (stream.type == "video" and not args.novideo) - or (stream.type == "subtitle" and not args.nosubtitle) - or (stream.type == "data" and not args.nodata) - ): - in_to_out[stream] = output.add_stream(template=stream) - -for i, packet in enumerate(input_.demux(list(in_to_out.keys()))): - - if args.count and i >= args.count: - break - print("%02d %r" % (i, packet)) - print("\tin: ", packet.stream) - - if packet.dts is None: - continue - - packet.stream = in_to_out[packet.stream] - - print("\tout:", packet.stream) - - output.mux(packet) - - -output.close() diff --git a/scratchpad/resource_use.py b/scratchpad/resource_use.py deleted file mode 100644 index 3387b0d70..000000000 --- a/scratchpad/resource_use.py +++ /dev/null @@ -1,61 +0,0 @@ -import argparse -import resource -import gc - -import av - - -parser = argparse.ArgumentParser() -parser.add_argument('-c', '--count', type=int, default=5) -parser.add_argument('-f', '--frames', type=int, default=100) -parser.add_argument('--print', dest='print_', action='store_true') -parser.add_argument('--to-rgb', action='store_true') -parser.add_argument('--to-image', action='store_true') -parser.add_argument('--gc', '-g', action='store_true') -parser.add_argument('input') -args = parser.parse_args() - -def format_bytes(n): - order = 0 - while n > 1024: - order += 1 - n //= 1024 - return '%d%sB' % (n, ('', 'k', 'M', 'G', 'T', 'P')[order]) - -usage = [] - -for round_ in range(args.count): - - print('Round %d/%d:' % (round_ + 1, args.count)) - - if args.gc: - gc.collect() - - usage.append(resource.getrusage(resource.RUSAGE_SELF)) - - fh = av.open(args.input) - vs = next(s for s in fh.streams if s.type == 'video') - - fi = 0 - for packet in fh.demux([vs]): - for frame in packet.decode(): - if args.print_: - print(frame) - if args.to_rgb: - print(frame.to_rgb()) - if args.to_image: - print(frame.to_image()) - fi += 1 - if fi > args.frames: - break - - frame = packet = fh = vs = None - - - -usage.append(resource.getrusage(resource.RUSAGE_SELF)) - -for i in range(len(usage) - 1): - before = usage[i] - after = usage[i + 1] - print('%s (%s)' % (format_bytes(after.ru_maxrss), format_bytes(after.ru_maxrss - before.ru_maxrss))) diff --git a/scratchpad/save_subtitles.py b/scratchpad/save_subtitles.py deleted file mode 100644 index 8666501d8..000000000 --- a/scratchpad/save_subtitles.py +++ /dev/null @@ -1,61 +0,0 @@ -""" - -As you can see, the subtitle API needs some work. - -""" - -import os -import sys -import pprint - -from PIL import Image - -from av import open - - -if not os.path.exists('subtitles'): - os.makedirs('subtitles') - - -video = open(sys.argv[1]) - -streams = [s for s in video.streams if s.type == b'subtitle'] -if not streams: - print('no subtitles') - exit(1) - -print(streams) - -count = 0 -for pi, packet in enumerate(video.demux([streams[0]])): - - print('packet', pi) - for si, subtitle in enumerate(packet.decode()): - - print('\tsubtitle', si, subtitle) - - for ri, rect in enumerate(subtitle.rects): - if rect.type == 'ass': - print('\t\tass: ', rect, rect.ass.rstrip('\n')) - if rect.type == 'text': - print('\t\ttext: ', rect, rect.text.rstrip('\n')) - if rect.type == 'bitmap': - print('\t\tbitmap: ', rect, rect.width, rect.height, rect.pict_buffers) - buffers = [b for b in rect.pict_buffers if b is not None] - if buffers: - imgs = [ - Image.frombuffer('L', (rect.width, rect.height), buffer, "raw", "L", 0, 1) - for buffer in buffers - ] - if len(imgs) == 1: - img = imgs[0] - elif len(imgs) == 2: - img = Image.merge('LA', imgs) - else: - img = Image.merge('RGBA', imgs) - img.save('subtitles/%04d.png' % count) - - count += 1 - if count > 10: - pass - # exit() diff --git a/scratchpad/second_seek_example.py b/scratchpad/second_seek_example.py deleted file mode 100644 index 58b3c3811..000000000 --- a/scratchpad/second_seek_example.py +++ /dev/null @@ -1,499 +0,0 @@ -""" -Note this example only really works accurately on constant frame rate media. -""" -from PyQt4 import QtGui -from PyQt4 import QtCore -from PyQt4.QtCore import Qt - -import sys -import av - - -AV_TIME_BASE = 1000000 - -def pts_to_frame(pts, time_base, frame_rate, start_time): - return int(pts * time_base * frame_rate) - int(start_time * time_base * frame_rate) - -def get_frame_rate(stream): - - if stream.average_rate.denominator and stream.average_rate.numerator: - return float(stream.average_rate) - if stream.time_base.denominator and stream.time_base.numerator: - return 1.0 / float(stream.time_base) - else: - raise ValueError("Unable to determine FPS") - -def get_frame_count(f, stream): - - if stream.frames: - return stream.frames - elif stream.duration: - return pts_to_frame(stream.duration, float(stream.time_base), get_frame_rate(stream), 0) - elif f.duration: - return pts_to_frame(f.duration, 1 / float(AV_TIME_BASE), get_frame_rate(stream), 0) - - else: - raise ValueError("Unable to determine number for frames") - -class FrameGrabber(QtCore.QObject): - - frame_ready = QtCore.pyqtSignal(object, object) - update_frame_range = QtCore.pyqtSignal(object, object) - - def __init__(self, parent=None): - super(FrameGrabber, self).__init__(parent) - self.file = None - self.stream = None - self.frame = None - self.active_time = None - self.start_time = 0 - self.pts_seen = False - self.nb_frames = None - - self.rate = None - self.time_base = None - - self.pts_map = {} - - def next_frame(self): - - frame_index = None - - rate = self.rate - time_base = self.time_base - - self.pts_seen = False - - for packet in self.file.demux(self.stream): - #print " pkt", packet.pts, packet.dts, packet - if packet.pts: - self.pts_seen = True - - for frame in packet.decode(): - - if frame_index is None: - - if self.pts_seen: - pts = frame.pts - else: - pts = frame.dts - - if not pts is None: - frame_index = pts_to_frame(pts, time_base, rate, self.start_time) - - elif not frame_index is None: - frame_index += 1 - - if not frame.dts in self.pts_map: - secs = None - - if not pts is None: - secs = pts * time_base - - self.pts_map[frame.dts] = secs - - - #if frame.pts == None: - - - - yield frame_index, frame - - - - @QtCore.pyqtSlot(object) - def request_time(self, second): - - frame = self.get_frame(second) - if not frame: - return - - rgba = frame.reformat(frame.width, frame.height, "rgb24", 'itu709') - #print rgba.to_image().save("test.png") - # could use the buffer interface here instead, some versions of PyQt don't support it for some reason - # need to track down which version they added support for it - self.frame = bytearray(rgba.planes[0]) - bytesPerPixel = 3 - img = QtGui.QImage(self.frame, rgba.width, rgba.height, rgba.width * bytesPerPixel, QtGui.QImage.Format_RGB888) - - #img = QtGui.QImage(rgba.planes[0], rgba.width, rgba.height, QtGui.QImage.Format_RGB888) - - #pixmap = QtGui.QPixmap.fromImage(img) - self.frame_ready.emit(img, second) - - def get_frame(self, target_sec): - - if target_sec != self.active_time: - return - print('seeking to', target_sec) - - rate = self.rate - time_base = self.time_base - - target_pts = int(target_sec / time_base) + self.start_time - seek_pts = target_pts - - - self.stream.seek(seek_pts) - - #frame_cache = [] - - last_frame = None - - for i, (frame_index, frame) in enumerate(self.next_frame()): - - - if target_sec != self.active_time: - return - - pts = frame.dts - if self.pts_seen: - pts = frame.pts - - if pts > target_pts: - break - - print(frame.pts, seek_pts) - last_frame = frame - - if last_frame: - - return last_frame - - - def get_frame_old(self, target_frame): - - if target_frame != self.active_frame: - return - print('seeking to', target_frame) - - seek_frame = target_frame - - rate = self.rate - time_base = self.time_base - - frame = None - reseek = 250 - - original_target_frame_pts = None - - while reseek >= 0: - - # convert seek_frame to pts - target_sec = seek_frame * 1 / rate - target_pts = int(target_sec / time_base) + self.start_time - - if original_target_frame_pts is None: - original_target_frame_pts = target_pts - - self.stream.seek(int(target_pts)) - - frame_index = None - - frame_cache = [] - - for i, (frame_index, frame) in enumerate(self.next_frame()): - - # optimization if the time slider has changed, the requested frame no longer valid - if target_frame != self.active_frame: - return - - print(" ", i, "at frame", frame_index, "at ts:", frame.pts, frame.dts, "target:", target_pts, 'orig', original_target_frame_pts) - - if frame_index is None: - pass - - elif frame_index >= target_frame: - break - - frame_cache.append(frame) - - # Check if we over seeked, if we over seekd we need to seek to a earlier time - # but still looking for the target frame - if frame_index != target_frame: - - if frame_index is None: - over_seek = '?' - else: - over_seek = frame_index - target_frame - if frame_index > target_frame: - - print(over_seek, frame_cache) - if over_seek <= len(frame_cache): - print("over seeked by %i, using cache" % over_seek) - frame = frame_cache[-over_seek] - break - - - seek_frame -= 1 - reseek -= 1 - print("over seeked by %s, backtracking.. seeking: %i target: %i retry: %i" % (str(over_seek), seek_frame, target_frame, reseek)) - - else: - break - - if reseek < 0: - raise ValueError("seeking failed %i" % frame_index) - - # frame at this point should be the correct frame - - if frame: - - return frame - - else: - raise ValueError("seeking failed %i" % target_frame) - - def get_frame_count(self): - - frame_count = None - - if self.stream.frames: - frame_count = self.stream.frames - elif self.stream.duration: - frame_count = pts_to_frame(self.stream.duration, float(self.stream.time_base), get_frame_rate(self.stream), 0) - elif self.file.duration: - frame_count = pts_to_frame(self.file.duration, 1 / float(AV_TIME_BASE), get_frame_rate(self.stream), 0) - else: - raise ValueError("Unable to determine number for frames") - - seek_frame = frame_count - - retry = 100 - - while retry: - target_sec = seek_frame * 1 / self.rate - target_pts = int(target_sec / self.time_base) + self.start_time - - self.stream.seek(int(target_pts)) - - frame_index = None - - for frame_index, frame in self.next_frame(): - print(frame_index, frame) - continue - - if not frame_index is None: - break - else: - seek_frame -= 1 - retry -= 1 - - - print("frame count seeked", frame_index, "container frame count", frame_count) - - return frame_index or frame_count - - @QtCore.pyqtSlot(object) - def set_file(self, path): - self.file = av.open(path) - self.stream = next(s for s in self.file.streams if s.type == b'video') - self.rate = get_frame_rate(self.stream) - self.time_base = float(self.stream.time_base) - - - index, first_frame = next(self.next_frame()) - self.stream.seek(self.stream.start_time) - - # find the pts of the first frame - index, first_frame = next(self.next_frame()) - - if self.pts_seen: - pts = first_frame.pts - else: - pts = first_frame.dts - - self.start_time = pts or first_frame.dts - - print("First pts", pts, self.stream.start_time, first_frame) - - #self.nb_frames = get_frame_count(self.file, self.stream) - self.nb_frames = self.get_frame_count() - - dur = None - - if self.stream.duration: - dur = self.stream.duration * self.time_base - else: - dur = self.file.duration * 1.0 / float(AV_TIME_BASE) - - self.update_frame_range.emit(dur, self.rate) - - - - - -class DisplayWidget(QtGui.QLabel): - def __init__(self, parent=None): - super(DisplayWidget, self).__init__(parent) - #self.setScaledContents(True) - self.setMinimumSize(1920 / 10, 1080 / 10) - - size_policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) - size_policy.setHeightForWidth(True) - - self.setSizePolicy(size_policy) - - self.setAlignment(Qt.AlignHCenter | Qt.AlignBottom) - - self.pixmap = None - self.setMargin(10) - - def heightForWidth(self, width): - return width * 9 / 16.0 - - @QtCore.pyqtSlot(object, object) - def setPixmap(self, img, index): - #if index == self.current_index: - self.pixmap = QtGui.QPixmap.fromImage(img) - - #super(DisplayWidget, self).setPixmap(self.pixmap) - super(DisplayWidget, self).setPixmap(self.pixmap.scaled(self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) - - def sizeHint(self): - width = self.width() - return QtCore.QSize(width, self.heightForWidth(width)) - - def resizeEvent(self, event): - if self.pixmap: - super(DisplayWidget, self).setPixmap(self.pixmap.scaled(self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) - - def sizeHint(self): - return QtCore.QSize(1920 / 2.5, 1080 / 2.5) - - -class VideoPlayerWidget(QtGui.QWidget): - - request_time = QtCore.pyqtSignal(object) - - load_file = QtCore.pyqtSignal(object) - - def __init__(self, parent=None): - super(VideoPlayerWidget, self).__init__(parent) - - self.rate = None - - self.display = DisplayWidget() - self.timeline = QtGui.QScrollBar(Qt.Horizontal) - self.timeline_base = 100000 - - self.frame_grabber = FrameGrabber() - - self.frame_control = QtGui.QDoubleSpinBox() - self.frame_control.setFixedWidth(100) - - self.timeline.valueChanged.connect(self.slider_changed) - self.frame_control.valueChanged.connect(self.frame_changed) - - self.request_time.connect(self.frame_grabber.request_time) - self.load_file.connect(self.frame_grabber.set_file) - - self.frame_grabber.frame_ready.connect(self.display.setPixmap) - self.frame_grabber.update_frame_range.connect(self.set_frame_range) - - self.frame_grabber_thread = QtCore.QThread() - - self.frame_grabber.moveToThread(self.frame_grabber_thread) - self.frame_grabber_thread.start() - - control_layout = QtGui.QHBoxLayout() - control_layout.addWidget(self.frame_control) - control_layout.addWidget(self.timeline) - - layout = QtGui.QVBoxLayout() - layout.addWidget(self.display) - layout.addLayout(control_layout) - self.setLayout(layout) - self.setAcceptDrops(True) - - def set_file(self, path): - #self.frame_grabber.set_file(path) - self.load_file.emit(path) - self.frame_changed(0) - - @QtCore.pyqtSlot(object, object) - def set_frame_range(self, maximum, rate): - print("frame range =", maximum, rate, int(maximum * self.timeline_base)) - - self.timeline.setMaximum(int(maximum * self.timeline_base)) - - self.frame_control.setMaximum(maximum) - self.frame_control.setSingleStep(1 / rate) - #self.timeline.setSingleStep( int(AV_TIME_BASE * 1/rate)) - self.rate = rate - - def slider_changed(self, value): - print('..', value) - self.frame_changed(value * 1.0 / float(self.timeline_base)) - - def frame_changed(self, value): - self.timeline.blockSignals(True) - self.frame_control.blockSignals(True) - - self.timeline.setValue(int(value * self.timeline_base)) - self.frame_control.setValue(value) - - self.timeline.blockSignals(False) - self.frame_control.blockSignals(False) - - #self.display.current_index = value - self.frame_grabber.active_time = value - - self.request_time.emit(value) - - def keyPressEvent(self, event): - if event.key() in (Qt.Key_Right, Qt.Key_Left): - direction = 1 - if event.key() == Qt.Key_Left: - direction = -1 - - if event.modifiers() == Qt.ShiftModifier: - print('shift') - direction *= 10 - - direction = direction * 1 / self.rate - - self.frame_changed(self.frame_control.value() + direction) - - else: - super(VideoPlayerWidget, self).keyPressEvent(event) - - def mousePressEvent(self, event): - # clear focus of spinbox - focused_widget = QtGui.QApplication.focusWidget() - if focused_widget: - focused_widget.clearFocus() - - super(VideoPlayerWidget, self).mousePressEvent(event) - - def dragEnterEvent(self, event): - event.accept() - - def dropEvent(self, event): - - mime = event.mimeData() - event.accept() - - - if mime.hasUrls(): - path = str(mime.urls()[0].path()) - self.set_file(path) - def closeEvent(self, event): - - self.frame_grabber.active_time = -1 - self.frame_grabber_thread.quit() - self.frame_grabber_thread.wait() - - for key, value in sorted(self.frame_grabber.pts_map.items()): - print(key, '=', value) - - event.accept() - - -if __name__ == "__main__": - app = QtGui.QApplication(sys.argv) - window = VideoPlayerWidget() - test_file = sys.argv[1] - window.set_file(test_file) - window.show() - sys.exit(app.exec_()) diff --git a/scratchpad/seekmany.py b/scratchpad/seekmany.py deleted file mode 100644 index 0b37e5118..000000000 --- a/scratchpad/seekmany.py +++ /dev/null @@ -1,49 +0,0 @@ -import sys - -import av - -container = av.open(sys.argv[1]) -duration = container.duration -stream = container.streams.video[0] - -print('container.duration', duration, float(duration) / av.time_base) -print('container.time_base', av.time_base) -print('stream.duration', stream.duration) -print('stream.time_base', stream.time_base) -print('codec.time_base', stream.codec_context.time_base) -print('scale', float(stream.codec_context.time_base / stream.time_base)) -print() - -exit() - -real_duration = float(duration) / av.time_base -steps = 120 -tolerance = real_duration / (steps * 4) -print('real_duration', real_duration) -print() - -def iter_frames(): - for packet in container.demux(stream): - for frame in packet.decode(): - yield frame - -for i in range(steps): - - time = real_duration * i / steps - min_time = time - tolerance - - pts = time / stream.time_base - - print('seeking', time, pts) - stream.seek(int(pts)) - - skipped = 0 - for frame in iter_frames(): - ftime = float(frame.pts * stream.time_base) - if ftime >= min_time: - break - skipped += 1 - else: - print(' WARNING: iterated to the end') - - print(' ', skipped, frame.pts, float(frame.pts * stream.time_base)) # WTF is this stream.time_base? diff --git a/scratchpad/show_frames_opencv.py b/scratchpad/show_frames_opencv.py deleted file mode 100644 index c618afaac..000000000 --- a/scratchpad/show_frames_opencv.py +++ /dev/null @@ -1,21 +0,0 @@ -import os -import sys - -import cv2 -from av import open - - -video = open(sys.argv[1]) - -stream = next(s for s in video.streams if s.type == 'video') - -for packet in video.demux(stream): - for frame in packet.decode(): - # some other formats gray16be, bgr24, rgb24 - img = frame.to_ndarray(format='bgr24') - cv2.imshow("Test", img) - - if cv2.waitKey(1) == 27: - break - -cv2.destroyAllWindows() diff --git a/scratchpad/sidedata.py b/scratchpad/sidedata.py deleted file mode 100644 index a1157d7dd..000000000 --- a/scratchpad/sidedata.py +++ /dev/null @@ -1,25 +0,0 @@ -import sys - -import av - - -fh = av.open(sys.argv[1]) -fh.streams.video[0].export_mvs = True -# fh.streams.video[0].flags2 |= 'EXPORT_MVS' - -for pi, packet in enumerate(fh.demux()): - for fi, frame in enumerate(packet.decode()): - - for di, data in enumerate(frame.side_data): - - print(pi, fi, di, data) - - print(data.to_ndarray()) - - for mi, vec in enumerate(data): - - print(mi, vec) - - if mi > 10: - exit() - From 4984b8dc35d7d3048f00076362fb7815716842d1 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 14 Mar 2024 21:12:56 -0400 Subject: [PATCH 226/846] Remove whence parm in `InputContainer.seek()` --- av/container/input.pyi | 3 +-- av/container/input.pyx | 14 ++++---------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/av/container/input.pyi b/av/container/input.pyi index 2c1328943..f85eaec46 100644 --- a/av/container/input.pyi +++ b/av/container/input.pyi @@ -1,4 +1,4 @@ -from typing import Any, Iterator, Literal, overload +from typing import Any, Iterator, overload from av.audio.frame import AudioFrame from av.audio.stream import AudioStream @@ -34,7 +34,6 @@ class InputContainer(Container): self, offset: int, *, - whence: Literal["time"] = "time", backward: bool = True, any_frame: bool = False, stream: Stream | VideoStream | AudioStream | None = None, diff --git a/av/container/input.pyx b/av/container/input.pyx index 47cd98c4d..acf02fbab 100644 --- a/av/container/input.pyx +++ b/av/container/input.pyx @@ -208,10 +208,10 @@ cdef class InputContainer(Container): for frame in packet.decode(): yield frame - def seek(self, offset, *, str whence="time", bint backward=True, - bint any_frame=False, Stream stream=None, - bint unsupported_frame_offset=False, - bint unsupported_byte_offset=False): + def seek( + self, offset, *, bint backward=True, bint any_frame=False, Stream stream=None, + bint unsupported_frame_offset=False, bint unsupported_byte_offset=False + ): """seek(offset, *, backward=True, any_frame=False, stream=None) Seek to a (key)frame nearsest to the given timestamp. @@ -252,12 +252,6 @@ cdef class InputContainer(Container): cdef int flags = 0 cdef int ret - # We used to support whence in 'time', 'frame', and 'byte', but later - # realized that FFmpeg doens't implement the frame or byte ones. - # We don't even document this anymore, but do allow 'time' to pass through. - if whence != "time": - raise ValueError("whence != 'time' is no longer supported") - if backward: flags |= lib.AVSEEK_FLAG_BACKWARD if any_frame: From 94e484267eba195233cefb6c4486d3017bd5b506 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 16 Mar 2024 00:45:34 -0400 Subject: [PATCH 227/846] Use ffmpeg 6.1.1 --- scripts/ffmpeg-6.1.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ffmpeg-6.1.json b/scripts/ffmpeg-6.1.json index f1585521e..d5b84be19 100644 --- a/scripts/ffmpeg-6.1.json +++ b/scripts/ffmpeg-6.1.json @@ -1,3 +1,3 @@ { - "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/6.1.0-1/ffmpeg-{platform}.tar.gz"] + "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/6.1.1-1/ffmpeg-{platform}.tar.gz"] } From 941463e4cdbef822343052d40ba6ffab6375fddd Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 20 Mar 2024 00:45:58 -0400 Subject: [PATCH 228/846] Update authors --- AUTHORS.rst | 148 +++++++++++++++++++++++++++------------------------- 1 file changed, 78 insertions(+), 70 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index ea72e7cf6..87f2492ad 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -9,76 +9,84 @@ All contributors (by number of commits): - Mark Reid ; `@markreidvfx `_ -* Vidar Tonaas Fauske ; `@vidartf `_ -* Billy Shambrook ; `@billyshambrook `_ -* Casper van der Wel -* Philip de Nier -* Tadas Dailyda * WyattBlue -* Justin Wong <46082645+uvjustin@users.noreply.github.com> -- Alba Mendez -- Xinran Xu ; `@xxr3376 `_ -- Dan Allan ; `@danielballan `_ -- Dave Johansen -- Mark Harfouche -- Christoph Rackwitz -- Alireza Davoudi ; `@adavoudi `_ -- Jonathan Drolet -- Moritz Kassner ; `@mkassner `_ -- Thomas A Caswell ; `@tacaswell `_ -- Ulrik Mikaelsson ; `@rawler `_ -- Wel C. van der -- Will Patera ; `@willpatera `_ +- Vidar Tonaas Fauske ; `@vidartf `_ +- Billy Shambrook ; `@billyshambrook `_ +- Casper van der Wel +- Philip de Nier +- Tadas Dailyda +- laggykiller <61652821+laggykiller@users.noreply.github.com> +- JoeUgly <41972063+JoeUgly@users.noreply.github.com> +- Justin Wong <46082645+uvjustin@users.noreply.github.com> -* rutsh -* Felix Vollmer -* Santiago Castro -* Christian Clauss -* Ihor Liubymov -* Johannes Erdfelt -* Karl Litterfeldt ; `@litterfeldt `_ -* Martin Larralde -* Simon-Martin Schröder -* mephi42 -* Miles Kaufmann -* Pablo Prietz -* Radek Senfeld ; `@radek-senfeld `_ -* Benjamin Chrétien <2742231+bchretien@users.noreply.github.com> -* Marc Mueller <30130371+cdce8p@users.noreply.github.com> -* zzjjbb <31069326+zzjjbb@users.noreply.github.com> -* Hanz <40712686+HanzCEO@users.noreply.github.com> -* Ian Lee -* Ryan Huang -* Arthur Barros -* Carlos Ruiz -* David Plowman -* Maxime Desroches -* egao1980 -* Eric Kalosa-Kenyon -* Gemfield -* Jonathan Martin -* Johan Jeppsson Karlin -* Philipp Klaus -* Mattias Wadman -* Manuel Goacolou -* Julian Schweizer -* Ömer Sezgin Uğurlu -* Orivej Desh -* Philipp Krähenbühl -* ramoncaldeira -* Santiago Castro -* Kengo Sawatsu -* FirefoxMetzger -* Brendan Long ; `@brendanlong `_ -* Семён Марьясин -* Stephen.Y -* Tom Flanagan -* Tim O'Shea -* Tim Ahpee -* Jonas Tingeborn -* Pino Toscano -* Ulrik Mikaelsson -* Vasiliy Kotov -* Koichi Akabe -* David Joy +* Alba Mendez +* Mark Harfouche +* Xinran Xu ; `@xxr3376 `_ +* laggykiller +* Dan Allan ; `@danielballan `_ +* Dave Johansen +* Christoph Rackwitz +* Alireza Davoudi ; `@adavoudi `_ +* Jonathan Drolet +* Moritz Kassner ; `@mkassner `_ +* Thomas A Caswell ; `@tacaswell `_ +* Ulrik Mikaelsson ; `@rawler `_ +* Wel C. van der +* Will Patera ; `@willpatera `_ + +- rutsh +- Felix Vollmer +- Santiago Castro +- Christian Clauss +- Ihor Liubymov +- Johannes Erdfelt +- Karl Litterfeldt ; `@litterfeldt `_ +- Martin Larralde +- Simon-Martin Schröder +- mephi42 +- Miles Kaufmann +- Pablo Prietz +- Radek Senfeld ; `@radek-senfeld `_ +- Benjamin Chrétien <2742231+bchretien@users.noreply.github.com> +- Marc Mueller <30130371+cdce8p@users.noreply.github.com> +- zzjjbb <31069326+zzjjbb@users.noreply.github.com> +- Hanz <40712686+HanzCEO@users.noreply.github.com> +- Joe Schiff <41972063+JoeSchiff@users.noreply.github.com> +- Ian Lee +- Ryan Huang +- Arthur Barros +- Carlos Ruiz +- David Plowman +- Maxime Desroches +- egao1980 +- Eric Kalosa-Kenyon +- Gemfield +- Jonathan Martin +- Johan Jeppsson Karlin +- Philipp Klaus +- Mattias Wadman +- Manuel Goacolou +- Julian Schweizer +- Ömer Sezgin Uğurlu +- Orivej Desh +- Philipp Krähenbühl +- ramoncaldeira +- Roland van Laar +- Santiago Castro +- Kengo Sawatsu +- FirefoxMetzger +- hyenal +- Brendan Long ; `@brendanlong `_ +- Семён Марьясин +- Stephen.Y +- Tom Flanagan +- Tim O'Shea +- Tim Ahpee +- Jonas Tingeborn +- Pino Toscano +- Ulrik Mikaelsson +- Vasiliy Kotov +- Koichi Akabe +- David Joy +- Sviatoslav Sydorenko (Святослав Сидоренко) From 8b0a3443cd9630f4efdd4bf23591bf23f5e6ca72 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 14 Mar 2024 21:07:23 -0400 Subject: [PATCH 229/846] Release v12.0.0 --- CHANGELOG.rst | 31 +++++++++++++++++++++++++++++++ av/about.py | 2 +- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a6d4950c5..72aa0897b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,37 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. +v12.0.0 +------- + +Major: + +- Add type hints. +- Update FFmpeg to 6.1.1 for the binary wheels. +- Update libraries for the binary wheels (notably dav1d to 1.4.1). +- Deprecate VideoCodecContext.gop_size for decoders by @JoeSchiff in (:issue:`1256`). +- Deprecate frame.index by @JoeSchiff in (:issue:`1218`). + +Features: + +- Allow using pathlib.Path for av.open by @WyattBlue in (:issue:`1231`). +- Add `max_b_frames` property to CodecContext by @davidplowman in (:issue:`1119`). +- Add `encode_lazy` method to CodecContext by @rawler in (:issue:`1092`). +- Add `color_range` to CodecContext/Frame by @johanjeppsson in (:issue:`686`). +- Set `time_base` for AudioResampler by @daveisfera in (:issue:`1209`). +- Add support for ffmpeg's AVCodecContext::delay by @JoeSchiff in (:issue:`1279`). +- Add `color_primaries`, `color_trc`, `colorspace` to VideoStream by @WyattBlue in (:issue:`1304`). +- Add `bits_per_coded_sample` to VideoCodecContext by @rvanlaar in (:issue:`1203`). +- AssSubtitle.ass now returns as bytes by @WyattBlue in (:issue:`1333`). +- Expose DISPLAYMATRIX side data by @hyenal in (:issue:`1249`). + +Fixes: + +- Convert deprecated Cython extension class properties to decorator syntax by @JoeSchiff +- Check None packet when setting time_base after decode by @philipnbbc in (:issue:`1281`). +- Remove deprecated `Buffer.to_bytes` by @WyattBlue in (:issue:`1286`). +- Remove deprecated `Packet.decode_one` by @WyattBlue in (:issue:`1301`). + v11.0.0 ------- diff --git a/av/about.py b/av/about.py index 282214405..0e21ac941 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "12.0.0rc1" +__version__ = "12.0.0" From 28779f25b07c48d1a4808eee93ac8ee974fa73e0 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 20 Mar 2024 11:42:10 -0400 Subject: [PATCH 230/846] Run on release --- .github/workflows/tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2282950b6..c716293dc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,5 +1,7 @@ name: tests on: + release: + types: [created] push: branches: main paths-ignore: From 00a3a42acb9bb3eed3fbfb11b1854929c114ec9f Mon Sep 17 00:00:00 2001 From: laggykiller Date: Thu, 21 Mar 2024 10:28:33 +0800 Subject: [PATCH 231/846] Fix wheels not published to pypi on release --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c716293dc..4a86aacc2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -242,7 +242,7 @@ jobs: name: dist path: dist/ - name: Publish to PyPI - if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/') + if: github.event_name == 'release' && github.event.action == 'published' uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ From de13457476fd76bc1a9d80435780dee2fafa7c04 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Thu, 21 Mar 2024 15:46:30 +0800 Subject: [PATCH 232/846] Trigger workflow on release publish --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4a86aacc2..58be2cd20 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,7 +1,7 @@ name: tests on: release: - types: [created] + types: [published] push: branches: main paths-ignore: From 55b28fc8654ae17e39c97010b11a847d5f210bc9 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 22 Mar 2024 03:19:47 -0400 Subject: [PATCH 233/846] Bump to next dev version --- README.md | 2 +- av/about.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2559d9b41..c396d0dd1 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ Have fun, [read the docs][docs], [come chat with us][gitter], and good luck! [conda-badge]: https://img.shields.io/conda/vn/conda-forge/av.svg?colorB=CCB39A [conda]: https://anaconda.org/conda-forge/av -[docs-badge]: https://img.shields.io/badge/docs-on%20pyav.org-blue.svg +[docs-badge]: https://img.shields.io/badge/docs-on%20pyav.basswood-io.com-blue.svg [docs]: https://pyav.basswood-io.com [gitter-badge]: https://img.shields.io/gitter/room/nwjs/nw.js.svg?logo=gitter&colorB=cc2b5e [gitter]: https://app.gitter.im/#/room/#PyAV-Org_User-Help:gitter.im diff --git a/av/about.py b/av/about.py index 0e21ac941..79a759ae1 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "12.0.0" +__version__ = "13.0.0rc1" From ec95e82574599d0e7c9db8d9eaf0cdd2939a63da Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 22 Mar 2024 03:43:09 -0400 Subject: [PATCH 234/846] Fix badge --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c396d0dd1..70023dbf4 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ PyAV ==== -[![GitHub Test Status][github-tests-badge]][github-tests] \ +[![GitHub Test Status][github-tests-badge]][github-tests] [![Gitter Chat][gitter-badge]][gitter] [![Documentation][docs-badge]][docs] \ [![Python Package Index][pypi-badge]][pypi] [![Conda Forge][conda-badge]][conda] @@ -67,7 +67,7 @@ Have fun, [read the docs][docs], [come chat with us][gitter], and good luck! [conda-badge]: https://img.shields.io/conda/vn/conda-forge/av.svg?colorB=CCB39A [conda]: https://anaconda.org/conda-forge/av -[docs-badge]: https://img.shields.io/badge/docs-on%20pyav.basswood-io.com-blue.svg +[docs-badge]: https://img.shields.io/badge/docs-on%20pyav.basswood--io.com-blue.svg [docs]: https://pyav.basswood-io.com [gitter-badge]: https://img.shields.io/gitter/room/nwjs/nw.js.svg?logo=gitter&colorB=cc2b5e [gitter]: https://app.gitter.im/#/room/#PyAV-Org_User-Help:gitter.im From 5707fb605cbd132e03c60e1a9e9463fd747ea93c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 22 Mar 2024 03:27:15 -0400 Subject: [PATCH 235/846] Drop support for Python 3.8 --- .github/workflows/tests.yml | 25 ++++++++++++------------- setup.py | 3 +-- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 58be2cd20..fea7d9d29 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,7 +17,6 @@ on: jobs: style: runs-on: ubuntu-latest - steps: - name: Checkout uses: actions/checkout@v4 @@ -25,7 +24,7 @@ jobs: - name: Python uses: actions/setup-python@v4 with: - python-version: 3.8 + python-version: 3.9 - name: Packages run: pip install -r tests/requirements.txt @@ -40,12 +39,12 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-latest, python: 3.8, ffmpeg: "6.1", extras: true} - - {os: ubuntu-latest, python: 3.8, ffmpeg: "6.0"} - - {os: ubuntu-latest, python: 3.8, ffmpeg: "5.1"} - - {os: ubuntu-latest, python: 3.8, ffmpeg: "5.0"} + - {os: ubuntu-latest, python: 3.9, ffmpeg: "6.1", extras: true} + - {os: ubuntu-latest, python: 3.9, ffmpeg: "6.0"} + - {os: ubuntu-latest, python: 3.9, ffmpeg: "5.1"} + - {os: ubuntu-latest, python: 3.9, ffmpeg: "5.0"} - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1"} - - {os: macos-latest, python: 3.8, ffmpeg: "6.1"} + - {os: macos-latest, python: 3.9, ffmpeg: "6.1"} env: PYAV_PYTHON: python${{ matrix.config.python }} @@ -121,10 +120,10 @@ jobs: fail-fast: false matrix: config: - - {os: windows-latest, python: 3.8, ffmpeg: "6.1"} - - {os: windows-latest, python: 3.8, ffmpeg: "6.0"} - - {os: windows-latest, python: 3.8, ffmpeg: "5.1"} - - {os: windows-latest, python: 3.8, ffmpeg: "5.0"} + - {os: windows-latest, python: 3.9, ffmpeg: "6.1"} + - {os: windows-latest, python: 3.9, ffmpeg: "6.0"} + - {os: windows-latest, python: 3.9, ffmpeg: "5.1"} + - {os: windows-latest, python: 3.9, ffmpeg: "5.0"} steps: - name: Checkout @@ -164,7 +163,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: - python-version: 3.8 + python-version: 3.9 - name: Build source package run: | pip install cython @@ -198,7 +197,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: - python-version: 3.8 + python-version: 3.9 - name: Set up QEMU if: matrix.os == 'ubuntu-latest' uses: docker/setup-qemu-action@v3 diff --git a/setup.py b/setup.py index 59239c10a..cefe7ca6a 100644 --- a/setup.py +++ b/setup.py @@ -203,7 +203,7 @@ def parse_cflags(raw_flags): url="https://github.com/PyAV-Org/PyAV", packages=find_packages(exclude=["build*", "examples*", "scratchpad*", "tests*"]), package_data=package_data, - python_requires=">=3.8", + python_requires=">=3.9", zip_safe=False, ext_modules=ext_modules, test_suite="tests", @@ -220,7 +220,6 @@ def parse_cflags(raw_flags): "Operating System :: Unix", "Operating System :: Microsoft :: Windows", "Programming Language :: Cython", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", From 6e15755f8da1e0d2f69d3538b252d48050f471d0 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 23 Mar 2024 02:15:54 -0400 Subject: [PATCH 236/846] Remove section on hwaccel --- docs/overview/caveats.rst | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/overview/caveats.rst b/docs/overview/caveats.rst index 093d9bb1a..43b764e48 100644 --- a/docs/overview/caveats.rst +++ b/docs/overview/caveats.rst @@ -18,12 +18,6 @@ Unsupported Features Our goal is to provide all of the features that make sense for the contexts that PyAV would be used in. If there is something missing, please reach out on Gitter_ or open a feature request on GitHub_ (or even better a pull request). Your request will be more likely to be addressed if you can point to the relevant `FFmpeg API documentation `__. -There are some features we may elect to not implement because we don't believe they fit the PyAV ethos. The only one that we've encountered so far is hardware decoding. The `FFmpeg man page `__ discusses the drawback of ``-hwaccel``: - - Note that most acceleration methods are intended for playback and will not be faster than software decoding on modern CPUs. Additionally, ``ffmpeg`` will usually need to copy the decoded frames from the GPU memory into the system memory, resulting in further performance loss. - -Since PyAV is not expected to be used in a high performance playback loop, we do not find the added code complexity worth the benefits of supporting this feature. - Sub-Interpeters --------------- From 7e775cac832e2b84de2e98cfa3feaaba619d862c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 23 Mar 2024 02:31:39 -0400 Subject: [PATCH 237/846] Drop support for ffmpeg 5.0 --- .github/workflows/tests.yml | 2 -- docs/overview/installation.rst | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fea7d9d29..c24212b84 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -42,7 +42,6 @@ jobs: - {os: ubuntu-latest, python: 3.9, ffmpeg: "6.1", extras: true} - {os: ubuntu-latest, python: 3.9, ffmpeg: "6.0"} - {os: ubuntu-latest, python: 3.9, ffmpeg: "5.1"} - - {os: ubuntu-latest, python: 3.9, ffmpeg: "5.0"} - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1"} - {os: macos-latest, python: 3.9, ffmpeg: "6.1"} @@ -123,7 +122,6 @@ jobs: - {os: windows-latest, python: 3.9, ffmpeg: "6.1"} - {os: windows-latest, python: 3.9, ffmpeg: "6.0"} - {os: windows-latest, python: 3.9, ffmpeg: "5.1"} - - {os: windows-latest, python: 3.9, ffmpeg: "5.0"} steps: - name: Checkout diff --git a/docs/overview/installation.rst b/docs/overview/installation.rst index 67b2655d6..57746b724 100644 --- a/docs/overview/installation.rst +++ b/docs/overview/installation.rst @@ -55,7 +55,7 @@ See the `Conda quick install `_ docs t Bring your own FFmpeg --------------------- -PyAV can also be compiled against your own build of FFmpeg ((version ``5.0`` or higher). You can force installing PyAV from source by running: +PyAV can also be compiled against your own build of FFmpeg ((version ``5.1`` or higher). You can force installing PyAV from source by running: .. code-block:: bash From 3acc9185a3b0714cf298c882c4617bf017ddfe2f Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 23 Mar 2024 03:01:30 -0400 Subject: [PATCH 238/846] Unneeded, Cython inserts this code for us --- av/stream.pyx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/av/stream.pyx b/av/stream.pyx index 1b5e967dd..9ce255e30 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -110,11 +110,6 @@ cdef class Stream: AVDeprecationWarning ) - if name == "side_data": - return self.side_data - elif name == "nb_side_data": - return self.nb_side_data - # Convenience getter for codec context properties. if self.codec_context is not None: return getattr(self.codec_context, name) From fde8f2cdb243a5ce150624c935732a2162d6f980 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 23 Mar 2024 03:46:31 -0400 Subject: [PATCH 239/846] Remove deprecated VideoStream.frame_rate --- av/audio/stream.pyx | 3 +++ av/data/stream.pyx | 3 +++ av/stream.pyx | 13 ------------- av/subtitles/stream.pyx | 3 ++- av/video/stream.pyx | 6 ++++++ tests/test_file_probing.py | 23 +++-------------------- 6 files changed, 17 insertions(+), 34 deletions(-) diff --git a/av/audio/stream.pyx b/av/audio/stream.pyx index 7185f389d..4d633edce 100644 --- a/av/audio/stream.pyx +++ b/av/audio/stream.pyx @@ -11,6 +11,9 @@ cdef class AudioStream(Stream): f" {self.layout.name}, {form} at 0x{id(self):x}>" ) + def __getattr__(self, name): + return getattr(self.codec_context, name) + cpdef encode(self, AudioFrame frame=None): """ Encode an :class:`.AudioFrame` and return a list of :class:`.Packet`. diff --git a/av/data/stream.pyx b/av/data/stream.pyx index 8136c6395..3ba451d0e 100644 --- a/av/data/stream.pyx +++ b/av/data/stream.pyx @@ -8,6 +8,9 @@ cdef class DataStream(Stream): f"{self.name or ''} at 0x{id(self):x}>" ) + def __getattr__(self, name): + return getattr(self.codec_context, name) + def encode(self, frame=None): return [] diff --git a/av/stream.pyx b/av/stream.pyx index 9ce255e30..0cf3a7011 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -101,19 +101,6 @@ cdef class Stream: f"{self.name or ''} at 0x{id(self):x}>" ) - def __getattr__(self, name): - # Deprecate framerate pass-through as it is not always set. - # See: https://github.com/PyAV-Org/PyAV/issues/1005 - if self.ptr.codecpar.codec_type == lib.AVMEDIA_TYPE_VIDEO and name in ("framerate", "rate"): - warnings.warn( - f"VideoStream.{name} is deprecated as it is not always set; please use VideoStream.average_rate.", - AVDeprecationWarning - ) - - # Convenience getter for codec context properties. - if self.codec_context is not None: - return getattr(self.codec_context, name) - def __setattr__(self, name, value): if name == "id": self._set_id(value) diff --git a/av/subtitles/stream.pyx b/av/subtitles/stream.pyx index 8da6043f0..6cf0c2d88 100644 --- a/av/subtitles/stream.pyx +++ b/av/subtitles/stream.pyx @@ -1,2 +1,3 @@ cdef class SubtitleStream(Stream): - pass + def __getattr__(self, name): + return getattr(self.codec_context, name) diff --git a/av/video/stream.pyx b/av/video/stream.pyx index af80f27a1..ae57ed136 100644 --- a/av/video/stream.pyx +++ b/av/video/stream.pyx @@ -14,6 +14,12 @@ cdef class VideoStream(Stream): f"{self.codec_context.height} at 0x{id(self):x}>" ) + def __getattr__(self, name): + if name in ("framerate", "rate"): + raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'") + + return getattr(self.codec_context, name) + cpdef encode(self, VideoFrame frame=None): """ Encode an :class:`.VideoFrame` and return a list of :class:`.Packet`. diff --git a/tests/test_file_probing.py b/tests/test_file_probing.py index 928a5bd1d..a4e35a1e1 100644 --- a/tests/test_file_probing.py +++ b/tests/test_file_probing.py @@ -1,4 +1,3 @@ -import warnings from fractions import Fraction import av @@ -169,9 +168,6 @@ def test_stream_probing(self): self.assertTrue(str(stream).startswith(" at ")) # actual stream properties - self.assertEqual(stream.average_rate, None) - self.assertEqual(stream.base_rate, None) - self.assertEqual(stream.guessed_rate, None) self.assertEqual(stream.duration, 37537) self.assertEqual(stream.frames, 0) self.assertEqual(stream.id, 1) @@ -189,9 +185,7 @@ def test_stream_probing(self): self.assertEqual(stream.start_time, 0) self.assertEqual(stream.time_base, Fraction(1, 90000)) self.assertEqual(stream.type, "data") - - # codec context properties - self.assertEqual(stream.codec, None) + self.assertEqual(hasattr(stream, "codec"), False) class TestSubtitleProbe(TestCase): @@ -307,19 +301,8 @@ def test_stream_probing(self): self.assertIn(stream.coded_width, (720, 0)) self.assertIn(stream.coded_height, (576, 0)) - # Deprecated properties. - with warnings.catch_warnings(record=True) as captured: - stream.framerate - self.assertEqual( - captured[0].message.args[0], - "VideoStream.framerate is deprecated as it is not always set; please use VideoStream.average_rate.", - ) - with warnings.catch_warnings(record=True) as captured: - stream.rate - self.assertEqual( - captured[0].message.args[0], - "VideoStream.rate is deprecated as it is not always set; please use VideoStream.average_rate.", - ) + self.assertEqual(hasattr(stream, "framerate"), False) + self.assertEqual(hasattr(stream, "rate"), False) class TestVideoProbeCorrupt(TestCase): From 2f0223e9b37f4c39e11bfc9dde7881cf1f17100e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 25 Mar 2024 02:39:46 -0400 Subject: [PATCH 240/846] Use 6.1.1 in activate.sh --- scripts/activate.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/activate.sh b/scripts/activate.sh index 167266549..21eb76488 100755 --- a/scripts/activate.sh +++ b/scripts/activate.sh @@ -14,7 +14,7 @@ if [[ ! "$PYAV_LIBRARY" ]]; then if [[ "$1" ]]; then PYAV_LIBRARY="$1" else - PYAV_LIBRARY=ffmpeg-6.0 + PYAV_LIBRARY=ffmpeg-6.1.1 echo "No \$PYAV_LIBRARY set; defaulting to $PYAV_LIBRARY" fi fi From 614f6ab7fcd09adc6777b60335f65236051b175a Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 25 Mar 2024 03:26:06 -0400 Subject: [PATCH 241/846] Fix mypy errors in tests --- av/container/core.pyi | 3 ++- av/container/output.pyi | 2 +- av/format.pyi | 3 ++- av/video/plane.pyi | 4 +++- av/video/stream.pyi | 3 ++- tests/common.py | 29 ++++++++++++++------------- tests/test_colorspace.py | 4 ++-- tests/test_encode.py | 13 +++++++----- tests/test_enums.py | 10 +--------- tests/test_filters.py | 43 ++++------------------------------------ tests/test_python_io.py | 21 ++++++++++++++------ tests/test_videoframe.py | 6 ++++-- 12 files changed, 59 insertions(+), 82 deletions(-) diff --git a/av/container/core.pyi b/av/container/core.pyi index c33f3a4dc..862f0d326 100644 --- a/av/container/core.pyi +++ b/av/container/core.pyi @@ -4,6 +4,7 @@ from types import TracebackType from typing import Any, Callable, Literal, Type, overload from av.enum import EnumFlag +from av.format import ContainerFormat from .input import InputContainer from .output import OutputContainer @@ -36,7 +37,7 @@ class Container: input_was_opened: bool io_open: Any open_files: Any - format: str | None + format: ContainerFormat options: dict[str, str] container_options: dict[str, str] stream_options: list[str] diff --git a/av/container/output.pyi b/av/container/output.pyi index 0400dadbb..6df32c897 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -17,5 +17,5 @@ class OutputContainer(Container): ) -> Stream: ... def start_encoding(self) -> None: ... def close(self) -> None: ... - def mux(self, packets: Sequence[Packet]) -> None: ... + def mux(self, packets: Packet | Sequence[Packet]) -> None: ... def mux_one(self, packet: Packet) -> None: ... diff --git a/av/format.pyi b/av/format.pyi index 874920928..d2aef4764 100644 --- a/av/format.pyi +++ b/av/format.pyi @@ -22,9 +22,10 @@ class Flags(EnumFlag): SEEK_TO_PTS: int class ContainerFormat: + name: str + long_name: str is_input: bool is_output: bool - long_name: str # flags no_file: int diff --git a/av/video/plane.pyi b/av/video/plane.pyi index 9bc1f0d77..e4a0a206c 100644 --- a/av/video/plane.pyi +++ b/av/video/plane.pyi @@ -1,6 +1,8 @@ +from av.plane import Plane + from .frame import VideoFrame -class VideoPlane: +class VideoPlane(Plane): line_size: int width: int height: int diff --git a/av/video/stream.pyi b/av/video/stream.pyi index 7ff1a4034..4b78efd06 100644 --- a/av/video/stream.pyi +++ b/av/video/stream.pyi @@ -1,5 +1,5 @@ from fractions import Fraction -from typing import Any, Literal +from typing import Any, Iterator, Literal from av.packet import Packet from av.stream import Stream @@ -36,4 +36,5 @@ class VideoStream(Stream): type: Literal["video"] def encode(self, frame: VideoFrame | None = None) -> list[Packet]: ... + def encode_lazy(self, frame: VideoFrame | None = None) -> Iterator[Packet]: ... def decode(self, packet: Packet | None = None) -> list[VideoFrame]: ... diff --git a/tests/common.py b/tests/common.py index e2b5f4e16..6c2b2d2e1 100644 --- a/tests/common.py +++ b/tests/common.py @@ -8,17 +8,18 @@ from av.datasets import fate as fate_suite try: - import PIL.Image as Image - import PIL.ImageFilter as ImageFilter + import PIL # noqa + + has_pillow = True except ImportError: - Image = ImageFilter = None + has_pillow = False is_windows = os.name == "nt" skip_tests = frozenset(os.environ.get("PYAV_SKIP_TESTS", "").split(",")) -def makedirs(path): +def makedirs(path: str) -> None: try: os.makedirs(path) except OSError as e: @@ -29,23 +30,21 @@ def makedirs(path): _start_time = datetime.datetime.now() -def _sandbox(timed=False): +def _sandbox(timed: bool = False) -> str: root = os.path.abspath(os.path.join(__file__, "..", "..", "sandbox")) - sandbox = ( - os.path.join( - root, - _start_time.strftime("%Y%m%d-%H%M%S"), - ) - if timed - else root - ) + if timed: + sandbox = os.path.join(root, _start_time.strftime("%Y%m%d-%H%M%S")) + else: + sandbox = root + if not os.path.exists(sandbox): os.makedirs(sandbox) + return sandbox -def asset(*args): +def asset(*args: str) -> str: adir = os.path.dirname(__file__) return os.path.abspath(os.path.join(adir, "assets", *args)) @@ -148,6 +147,8 @@ def assertNdarraysEqual(self, a, b): self.fail("ndarrays contents differ\n%s" % msg) def assertImagesAlmostEqual(self, a, b, epsilon=0.1, *args): + import PIL.ImageFilter as ImageFilter + self.assertEqual(a.size, b.size, "sizes dont match") a = a.filter(ImageFilter.BLUR).getdata() b = b.filter(ImageFilter.BLUR).getdata() diff --git a/tests/test_colorspace.py b/tests/test_colorspace.py index 99c9f2fa2..571e7bd2b 100644 --- a/tests/test_colorspace.py +++ b/tests/test_colorspace.py @@ -5,7 +5,7 @@ class TestColorSpace(TestCase): - def test_penguin_joke(self): + def test_penguin_joke(self) -> None: container = av.open( fate_suite("amv/MTV_high_res_320x240_sample_Penguin_Joke_MTV_from_WMV.amv") ) @@ -26,7 +26,7 @@ def test_penguin_joke(self): self.assertEqual(frame.colorspace, Colorspace.ITU601) return - def test_sky_timelapse(self): + def test_sky_timelapse(self) -> None: container = av.open( av.datasets.curated("pexels/time-lapse-video-of-night-sky-857195.mp4") ) diff --git a/tests/test_encode.py b/tests/test_encode.py index d0939470a..e4336c735 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -1,6 +1,7 @@ import io import math from fractions import Fraction +from typing import cast from unittest import SkipTest import numpy as np @@ -10,21 +11,23 @@ from av.audio.stream import AudioStream from av.video.stream import VideoStream -from .common import Image, TestCase, fate_suite +from .common import TestCase, fate_suite, has_pillow WIDTH = 320 HEIGHT = 240 DURATION = 48 -def write_rgb_rotate(output): - if not Image: - raise SkipTest() +def write_rgb_rotate(output: av.container.OutputContainer) -> None: + if not has_pillow: + raise SkipTest("Don't have Pillow") + + import PIL.Image as Image output.metadata["title"] = "container" output.metadata["key"] = "value" - stream = output.add_stream("mpeg4", 24) + stream = cast(VideoStream, output.add_stream("mpeg4", 24)) stream.width = WIDTH stream.height = HEIGHT stream.pix_fmt = "yuv420p" diff --git a/tests/test_enums.py b/tests/test_enums.py index 39bf856ad..8e4839a77 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -10,15 +10,7 @@ class TestEnums(TestCase): def define_foobar(self, **kwargs): - return define_enum( - "Foobar", - __name__, - ( - ("FOO", 1), - ("BAR", 2), - ), - **kwargs - ) + return define_enum("Foobar", __name__, (("FOO", 1), ("BAR", 2)), **kwargs) def test_basics(self): cls = self.define_foobar() diff --git a/tests/test_filters.py b/tests/test_filters.py index dd090fe1d..326ef7fb9 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -1,6 +1,5 @@ import errno from fractions import Fraction -from unittest import SkipTest import numpy as np @@ -9,7 +8,7 @@ from av.audio.frame import format_dtypes from av.filter import Filter, Graph -from .common import Image, TestCase, fate_suite +from .common import TestCase, has_pillow def generate_audio_frame( @@ -86,7 +85,7 @@ def test_generator_graph(self): frame = sink.pull() self.assertIsInstance(frame, VideoFrame) - if Image: + if has_pillow: frame.to_image().save(self.sandboxed("mandelbrot2.png")) def test_auto_find_sink(self): @@ -97,7 +96,7 @@ def test_auto_find_sink(self): frame = graph.pull() - if Image: + if has_pillow: frame.to_image().save(self.sandboxed("mandelbrot3.png")) def test_delegate_sink(self): @@ -108,43 +107,9 @@ def test_delegate_sink(self): frame = src.pull() - if Image: + if has_pillow: frame.to_image().save(self.sandboxed("mandelbrot4.png")) - def test_haldclut_graph(self): - raise SkipTest() - - graph = Graph() - - img = Image.open(fate_suite("png1/lena-rgb24.png")) - frame = VideoFrame.from_image(img) - img_source = graph.add_buffer(frame) - - hald_img = Image.open("hald_7.png") - hald_frame = VideoFrame.from_image(hald_img) - hald_source = graph.add_buffer(hald_frame) - - hald_filter = graph.add("haldclut") - - sink = graph.add("buffersink") - - img_source.link(0, hald_filter, 0) - hald_source.link(0, hald_filter, 1) - hald_filter.link(0, sink, 0) - graph.config() - - self.assertIs(img_source.outputs[0].linked_to, hald_filter.inputs[0]) - self.assertIs(hald_source.outputs[0].linked_to, hald_filter.inputs[1]) - self.assertIs(hald_filter.outputs[0].linked_to, sink.inputs[0]) - - hald_source.push(hald_frame) - - img_source.push(frame) - - frame = sink.pull() - self.assertIsInstance(frame, VideoFrame) - frame.to_image().save(self.sandboxed("filtered.png")) - def test_audio_buffer_sink(self): graph = Graph() audio_buffer = graph.add_abuffer( diff --git a/tests/test_python_io.py b/tests/test_python_io.py index 84e94d7a3..42b8dd132 100644 --- a/tests/test_python_io.py +++ b/tests/test_python_io.py @@ -3,7 +3,14 @@ import av -from .common import Image, MethodLogger, TestCase, fate_png, fate_suite, run_in_sandbox +from .common import ( + MethodLogger, + TestCase, + fate_png, + fate_suite, + has_pillow, + run_in_sandbox, +) from .test_encode import assert_rgb_rotate, write_rgb_rotate @@ -206,9 +213,11 @@ def test_writing_to_custom_io_dash(self): assert_rgb_rotate(self, container, is_dash=True) def test_writing_to_custom_io_image2(self): - if not Image: + if not has_pillow: raise SkipTest() + import PIL.Image as Image + # Custom I/O that opens file and logs calls wrapped_custom_io = CustomIOLogger() @@ -263,7 +272,7 @@ def test_writing_to_custom_io_image2(self): self.assertEqual(stream.format.width, width) self.assertEqual(stream.format.height, height) - def test_writing_to_file(self): + def test_writing_to_file(self) -> None: path = self.sandboxed("writing.mp4") with open(path, "wb") as fh: @@ -273,7 +282,7 @@ def test_writing_to_file(self): with av.open(path) as container: assert_rgb_rotate(self, container) - def test_writing_to_pipe_readonly(self): + def test_writing_to_pipe_readonly(self) -> None: buf = ReadOnlyPipe() with self.assertRaises(ValueError) as cm: self.write(buf) @@ -291,10 +300,10 @@ def test_writing_to_pipe_writeonly(self): str(cm.exception), ) - def read(self, fh, seekable=True): + def read(self, fh, seekable: bool = True) -> None: wrapped = MethodLogger(fh) - with av.open(wrapped) as container: + with av.open(wrapped, "r") as container: self.assertEqual(container.format.name, "mpegts") self.assertEqual( container.format.long_name, "MPEG-TS (MPEG-2 Transport Stream)" diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 32f6c6cd3..28540ccc3 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -4,7 +4,7 @@ from av import VideoFrame -from .common import Image, TestCase, fate_png +from .common import TestCase, fate_png, has_pillow class TestVideoFrameConstructors(TestCase): @@ -90,10 +90,12 @@ def test_memoryview_read(self): class TestVideoFrameImage(TestCase): def setUp(self): - if not Image: + if not has_pillow: raise SkipTest() def test_roundtrip(self): + import PIL.Image as Image + image = Image.open(fate_png()) frame = VideoFrame.from_image(image) img = frame.to_image() From 1dfe7763023102a98cf94416c456eaafe67fdb93 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 25 Mar 2024 04:12:36 -0400 Subject: [PATCH 242/846] Use pyproject.toml for flake8 --- pyproject.toml | 9 +++++++++ setup.cfg | 5 ----- tests/requirements.txt | 8 ++++---- 3 files changed, 13 insertions(+), 9 deletions(-) delete mode 100644 setup.cfg diff --git a/pyproject.toml b/pyproject.toml index 267002b31..73e3cf334 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,3 +5,12 @@ requires = ["setuptools", "cython"] profile = "black" known_first_party = ["av"] skip = ["av/__init__.py"] + +[tool.flake8] +filename = ["*.py", "*.pyx", "*.pxd"] +ignore = ["E203", "W503"] +max-line-length = 142 +per-file-ignores = [ + "__init__.py:E402,F401", + "*.pyx,*.pxd:E211,E225,E227,E402,E999", +] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 2b4cedb6e..000000000 --- a/setup.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[flake8] -filename = *.py,*.pyx,*.pxd -ignore = E203,W503 -max-line-length = 142 -per-file-ignores = __init__.py: E402,F401 *.pyx,*.pxd: E211,E225,E227,E402,E999 diff --git a/tests/requirements.txt b/tests/requirements.txt index 71a7d58fb..fd44ed1b8 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,8 +1,8 @@ -autopep8 -black cython -flake8 -isort numpy pillow +black +isort +flake8 +flake8-pyproject sphinx==5.1.0 From 554f2c3f4fceaa7b194d98e37caa655cfa72e5a4 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 25 Mar 2024 17:21:28 -0400 Subject: [PATCH 243/846] Add missing stubs for AudioFrame --- av/audio/frame.pyi | 4 ++++ tests/test_audioframe.py | 18 +++++++++--------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/av/audio/frame.pyi b/av/audio/frame.pyi index 777c3dd70..c733a039d 100644 --- a/av/audio/frame.pyi +++ b/av/audio/frame.pyi @@ -4,6 +4,8 @@ import numpy as np from av.frame import Frame +from .format import AudioFormat +from .layout import AudioLayout from .plane import AudioPlane format_dtypes: dict[str, str] @@ -20,6 +22,8 @@ class AudioFrame(Frame): samples: int sample_rate: int rate: int + format: AudioFormat + layout: AudioLayout def __init__( self, diff --git a/tests/test_audioframe.py b/tests/test_audioframe.py index f4ffeb661..378665b78 100644 --- a/tests/test_audioframe.py +++ b/tests/test_audioframe.py @@ -6,14 +6,14 @@ class TestAudioFrameConstructors(TestCase): - def test_null_constructor(self): + def test_null_constructor(self) -> None: frame = AudioFrame() self.assertEqual(frame.format.name, "s16") self.assertEqual(frame.layout.name, "stereo") self.assertEqual(len(frame.planes), 0) self.assertEqual(frame.samples, 0) - def test_manual_flt_mono_constructor(self): + def test_manual_flt_mono_constructor(self) -> None: frame = AudioFrame(format="flt", layout="mono", samples=160) self.assertEqual(frame.format.name, "flt") self.assertEqual(frame.layout.name, "mono") @@ -21,7 +21,7 @@ def test_manual_flt_mono_constructor(self): self.assertEqual(frame.planes[0].buffer_size, 640) self.assertEqual(frame.samples, 160) - def test_manual_flt_stereo_constructor(self): + def test_manual_flt_stereo_constructor(self) -> None: frame = AudioFrame(format="flt", layout="stereo", samples=160) self.assertEqual(frame.format.name, "flt") self.assertEqual(frame.layout.name, "stereo") @@ -29,7 +29,7 @@ def test_manual_flt_stereo_constructor(self): self.assertEqual(frame.planes[0].buffer_size, 1280) self.assertEqual(frame.samples, 160) - def test_manual_fltp_stereo_constructor(self): + def test_manual_fltp_stereo_constructor(self) -> None: frame = AudioFrame(format="fltp", layout="stereo", samples=160) self.assertEqual(frame.format.name, "fltp") self.assertEqual(frame.layout.name, "stereo") @@ -38,7 +38,7 @@ def test_manual_fltp_stereo_constructor(self): self.assertEqual(frame.planes[1].buffer_size, 640) self.assertEqual(frame.samples, 160) - def test_manual_s16_mono_constructor(self): + def test_manual_s16_mono_constructor(self) -> None: frame = AudioFrame(format="s16", layout="mono", samples=160) self.assertEqual(frame.format.name, "s16") self.assertEqual(frame.layout.name, "mono") @@ -46,7 +46,7 @@ def test_manual_s16_mono_constructor(self): self.assertEqual(frame.planes[0].buffer_size, 320) self.assertEqual(frame.samples, 160) - def test_manual_s16_mono_constructor_align_8(self): + def test_manual_s16_mono_constructor_align_8(self) -> None: frame = AudioFrame(format="s16", layout="mono", samples=159, align=8) self.assertEqual(frame.format.name, "s16") self.assertEqual(frame.layout.name, "mono") @@ -54,7 +54,7 @@ def test_manual_s16_mono_constructor_align_8(self): self.assertEqual(frame.planes[0].buffer_size, 320) self.assertEqual(frame.samples, 159) - def test_manual_s16_stereo_constructor(self): + def test_manual_s16_stereo_constructor(self) -> None: frame = AudioFrame(format="s16", layout="stereo", samples=160) self.assertEqual(frame.format.name, "s16") self.assertEqual(frame.layout.name, "stereo") @@ -62,7 +62,7 @@ def test_manual_s16_stereo_constructor(self): self.assertEqual(frame.planes[0].buffer_size, 640) self.assertEqual(frame.samples, 160) - def test_manual_s16p_stereo_constructor(self): + def test_manual_s16p_stereo_constructor(self) -> None: frame = AudioFrame(format="s16p", layout="stereo", samples=160) self.assertEqual(frame.format.name, "s16p") self.assertEqual(frame.layout.name, "stereo") @@ -73,7 +73,7 @@ def test_manual_s16p_stereo_constructor(self): class TestAudioFrameConveniences(TestCase): - def test_basic_to_ndarray(self): + def test_basic_to_ndarray(self) -> None: frame = AudioFrame(format="s16p", layout="stereo", samples=160) array = frame.to_ndarray() self.assertEqual(array.dtype, "i2") From 1b27fc96ff3d1dc9eb7774f065a5e8e9c43475d8 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 25 Mar 2024 17:31:21 -0400 Subject: [PATCH 244/846] Remove encode and decode for DataStream --- av/data/stream.pyi | 3 +-- av/data/stream.pyx | 11 +---------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/av/data/stream.pyi b/av/data/stream.pyi index f84e5f9c2..45a669d4f 100644 --- a/av/data/stream.pyi +++ b/av/data/stream.pyi @@ -3,5 +3,4 @@ from av.packet import Packet from av.stream import Stream class DataStream(Stream): - def encode(self, frame: Frame | None = None) -> list[Packet]: ... - def decode(self, packet: Packet | None = None, count: int = 0) -> list[Frame]: ... + name: str | None diff --git a/av/data/stream.pyx b/av/data/stream.pyx index 3ba451d0e..4cc957762 100644 --- a/av/data/stream.pyx +++ b/av/data/stream.pyx @@ -4,19 +4,10 @@ cimport libav as lib cdef class DataStream(Stream): def __repr__(self): return ( - f"'}/" + f"'} at 0x{id(self):x}>" ) - def __getattr__(self, name): - return getattr(self.codec_context, name) - - def encode(self, frame=None): - return [] - - def decode(self, packet=None, count=0): - return [] - @property def name(self): cdef const lib.AVCodecDescriptor *desc = lib.avcodec_descriptor_get(self.ptr.codecpar.codec_id) From 376702a0a940b07a42d18286003a229e697123bb Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 3 Apr 2024 22:06:20 -0400 Subject: [PATCH 245/846] Remove user help issue template --- .github/ISSUE_TEMPLATE/user-help.md | 52 ----------------------------- 1 file changed, 52 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/user-help.md diff --git a/.github/ISSUE_TEMPLATE/user-help.md b/.github/ISSUE_TEMPLATE/user-help.md deleted file mode 100644 index 46d6776f6..000000000 --- a/.github/ISSUE_TEMPLATE/user-help.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -name: User help -about: Request help with using PyAV. -title: "How do I FOO?" -labels: 'user help' -assignees: '' - ---- - -**IMPORTANT:** Be sure to replace all template sections {{ like this }} or your issue may be discarded. - - -## Overview - -{{ A clear and concise description of your problem. }} - - -## Expected behavior - -{{ A clear and concise description of what you expected to happen. }} - - -## Actual behavior - -{{ A clear and concise description of what actually happened. }} - -Traceback: -``` -{{ Include complete tracebacks if there are any exceptions. }} -``` - - -## Investigation - -{{ What you tried so far to fix your problem. }} - - -## Research - -I have done the following: - -- [ ] Checked the [PyAV documentation](https://pyav.org/docs) -- [ ] Searched on [Google](https://www.google.com/search?q=pyav+how+do+I+foo) -- [ ] Searched on [Stack Overflow](https://stackoverflow.com/search?q=pyav) -- [ ] Looked through [old GitHub issues](https://github.com/PyAV-Org/PyAV/issues?&q=is%3Aissue) -- [ ] Asked on [PyAV Gitter](https://app.gitter.im/#/room/#PyAV-Org_User-Help:gitter.im) -- [ ] ... and waited 72 hours for a response. - - -## Additional context - -{{ Add any other context about the problem here. }} From f70a35d57f504e7e3c1128371ca8bb045b9b96b0 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 6 Apr 2024 21:57:40 -0400 Subject: [PATCH 246/846] Use python setup 5 --- .github/workflows/tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c24212b84..c530cfbc3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,7 +22,7 @@ jobs: uses: actions/checkout@v4 - name: Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.9 @@ -54,7 +54,7 @@ jobs: name: Checkout - name: Python ${{ matrix.config.python }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.config.python }} @@ -159,7 +159,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: 3.9 - name: Build source package @@ -193,7 +193,7 @@ jobs: arch: AMD64 steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: 3.9 - name: Set up QEMU From e884e6cd1dac2646fd002b441ca4fdfb2ef818c5 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 6 Apr 2024 23:36:25 -0400 Subject: [PATCH 247/846] Support only ffmpeg 6.0 and above --- .github/workflows/tests.yml | 4 +--- docs/overview/installation.rst | 6 +++--- scripts/fetch-vendor.py | 10 ++++------ scripts/ffmpeg-5.0.json | 3 --- scripts/ffmpeg-5.1.json | 3 --- 5 files changed, 8 insertions(+), 18 deletions(-) delete mode 100644 scripts/ffmpeg-5.0.json delete mode 100644 scripts/ffmpeg-5.1.json diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c530cfbc3..0655951b1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -41,7 +41,6 @@ jobs: config: - {os: ubuntu-latest, python: 3.9, ffmpeg: "6.1", extras: true} - {os: ubuntu-latest, python: 3.9, ffmpeg: "6.0"} - - {os: ubuntu-latest, python: 3.9, ffmpeg: "5.1"} - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1"} - {os: macos-latest, python: 3.9, ffmpeg: "6.1"} @@ -121,7 +120,6 @@ jobs: config: - {os: windows-latest, python: 3.9, ffmpeg: "6.1"} - {os: windows-latest, python: 3.9, ffmpeg: "6.0"} - - {os: windows-latest, python: 3.9, ffmpeg: "5.1"} steps: - name: Checkout @@ -214,7 +212,7 @@ jobs: CIBW_ENVIRONMENT_MACOS: PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig LDFLAGS=-headerpad_max_install_names CIBW_ENVIRONMENT_WINDOWS: INCLUDE=C:\\cibw\\vendor\\include LIB=C:\\cibw\\vendor\\lib PYAV_SKIP_TESTS=unicode_filename CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: delvewheel repair --add-path C:\cibw\vendor\bin -w {dest_dir} {wheel} - CIBW_SKIP: cp36-* pp36-* pp38-win* *-musllinux* + CIBW_SKIP: pp38-win* *-musllinux* CIBW_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m unittest discover -t {project} -s tests && mv {project}/av.disabled {project}/av CIBW_TEST_REQUIRES: numpy # skip tests when there are no binary wheels of numpy diff --git a/docs/overview/installation.rst b/docs/overview/installation.rst index 57746b724..4fdeb38a5 100644 --- a/docs/overview/installation.rst +++ b/docs/overview/installation.rst @@ -4,14 +4,14 @@ Installation Binary wheels ------------- -Since release 8.0.0 binary wheels are provided on PyPI for Linux, Mac and Windows linked against FFmpeg. The most straight-forward way to install PyAV is to run: +Binary wheels are provided on PyPI for Linux, MacOS, and Windows linked against FFmpeg. The most straight-forward way to install PyAV is to run: .. code-block:: bash pip install av -Currently FFmpeg 6.0 is used with the following features enabled for all platforms: +Currently FFmpeg 6.1.1 is used with the following features enabled for all platforms: - fontconfig - gmp @@ -55,7 +55,7 @@ See the `Conda quick install `_ docs t Bring your own FFmpeg --------------------- -PyAV can also be compiled against your own build of FFmpeg ((version ``5.1`` or higher). You can force installing PyAV from source by running: +PyAV can also be compiled against your own build of FFmpeg ((version ``6.0`` or higher). You can force installing PyAV from source by running: .. code-block:: bash diff --git a/scripts/fetch-vendor.py b/scripts/fetch-vendor.py index 9ea329eb3..c08bcc1f7 100644 --- a/scripts/fetch-vendor.py +++ b/scripts/fetch-vendor.py @@ -3,7 +3,6 @@ import json import os import platform -import shutil import struct import subprocess @@ -35,12 +34,11 @@ def get_platform(): args = parser.parse_args() logging.basicConfig(level=logging.INFO) -# read config file -with open(args.config_file, "r") as fp: +with open(args.config_file) as fp: config = json.load(fp) # ensure destination directory exists -logging.info("Creating directory %s" % args.destination_dir) +logging.info(f"Creating directory {args.destination_dir}") if not os.path.exists(args.destination_dir): os.makedirs(args.destination_dir) @@ -51,7 +49,7 @@ def get_platform(): tarball_name = tarball_url.split("/")[-1] tarball_file = os.path.join(args.cache_dir, tarball_name) if not os.path.exists(tarball_file): - logging.info("Downloading %s" % tarball_url) + logging.info(f"Downloading {tarball_url}") if not os.path.exists(args.cache_dir): os.mkdir(args.cache_dir) subprocess.check_call( @@ -59,5 +57,5 @@ def get_platform(): ) # extract tarball - logging.info("Extracting %s" % tarball_name) + logging.info(f"Extracting {tarball_name}") subprocess.check_call(["tar", "-C", args.destination_dir, "-xf", tarball_file]) diff --git a/scripts/ffmpeg-5.0.json b/scripts/ffmpeg-5.0.json deleted file mode 100644 index 41969df2c..000000000 --- a/scripts/ffmpeg-5.0.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.0.1-1/ffmpeg-{platform}.tar.gz"] -} diff --git a/scripts/ffmpeg-5.1.json b/scripts/ffmpeg-5.1.json deleted file mode 100644 index 75e4b8eca..000000000 --- a/scripts/ffmpeg-5.1.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.1.3-1/ffmpeg-{platform}.tar.gz"] -} From c4ca5eb50b53d0919f1100b3d1e4e1dd2fb63acc Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 7 Apr 2024 13:05:18 -0400 Subject: [PATCH 248/846] Don't require looking at Gitter --- .github/ISSUE_TEMPLATE/build-bug-report.md | 9 +-------- .github/ISSUE_TEMPLATE/runtime-bug-report.md | 10 +--------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/build-bug-report.md b/.github/ISSUE_TEMPLATE/build-bug-report.md index 298424330..da0c17add 100644 --- a/.github/ISSUE_TEMPLATE/build-bug-report.md +++ b/.github/ISSUE_TEMPLATE/build-bug-report.md @@ -51,22 +51,15 @@ Build report: ``` {{ Complete output of `python setup.py config --verbose`. }} ``` -- FFmpeg: -``` -{{ Complete output of `ffmpeg -version` }} -``` - ## Research I have done the following: -- [ ] Checked the [PyAV documentation](https://pyav.org/docs) +- [ ] Checked the [PyAV documentation](https://pyav.basswood-io.com) - [ ] Searched on [Google](https://www.google.com/search?q=pyav+how+do+I+foo) - [ ] Searched on [Stack Overflow](https://stackoverflow.com/search?q=pyav) - [ ] Looked through [old GitHub issues](https://github.com/PyAV-Org/PyAV/issues?&q=is%3Aissue) -- [ ] Asked on [PyAV Gitter](https://app.gitter.im/#/room/#PyAV-Org_User-Help:gitter.im) -- [ ] ... and waited 72 hours for a response. ## Additional context diff --git a/.github/ISSUE_TEMPLATE/runtime-bug-report.md b/.github/ISSUE_TEMPLATE/runtime-bug-report.md index 49fec3088..267598841 100644 --- a/.github/ISSUE_TEMPLATE/runtime-bug-report.md +++ b/.github/ISSUE_TEMPLATE/runtime-bug-report.md @@ -51,23 +51,15 @@ Traceback: ``` {{ Complete output of `python setup.py config --verbose`. }} ``` -- FFmpeg: -``` -{{ Complete output of `ffmpeg -version` }} -``` - ## Research I have done the following: -- [ ] Checked the [PyAV documentation](https://pyav.org/docs) +- [ ] Checked the [PyAV documentation](https://pyav.basswood-io.com) - [ ] Searched on [Google](https://www.google.com/search?q=pyav+how+do+I+foo) - [ ] Searched on [Stack Overflow](https://stackoverflow.com/search?q=pyav) - [ ] Looked through [old GitHub issues](https://github.com/PyAV-Org/PyAV/issues?&q=is%3Aissue) -- [ ] Asked on [PyAV Gitter](https://app.gitter.im/#/room/#PyAV-Org_User-Help:gitter.im) -- [ ] ... and waited 72 hours for a response. - ## Additional context From 19fab0179ed8dbdb8b483cf42a5c9caed6353761 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 7 Apr 2024 13:47:10 -0400 Subject: [PATCH 249/846] Add type stubs for datasets.py --- av/audio/layout.pyi | 1 + av/datasets.py | 9 +++++---- tests/test_audiolayout.py | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/av/audio/layout.pyi b/av/audio/layout.pyi index 338532e1a..a59398fcc 100644 --- a/av/audio/layout.pyi +++ b/av/audio/layout.pyi @@ -5,6 +5,7 @@ class AudioLayout: layout: int nb_channels: int channels: tuple[AudioChannel, ...] + def __init__(self, layout: int | str | AudioLayout): ... class AudioChannel: name: str diff --git a/av/datasets.py b/av/datasets.py index 15ffe9643..5954a9c98 100644 --- a/av/datasets.py +++ b/av/datasets.py @@ -2,12 +2,13 @@ import logging import os import sys +from typing import Iterator from urllib.request import urlopen log = logging.getLogger(__name__) -def iter_data_dirs(check_writable=False): +def iter_data_dirs(check_writable: bool = False) -> Iterator[str]: try: yield os.environ["PYAV_TESTDATA_DIR"] except KeyError: @@ -42,7 +43,7 @@ def iter_data_dirs(check_writable=False): yield os.path.join(os.path.expanduser("~"), ".pyav", "datasets") -def cached_download(url, name): +def cached_download(url: str, name: str) -> str: """Download the data at a URL, and cache it under the given name. The file is stored under `pyav/test` with the given name in the directory @@ -96,7 +97,7 @@ def cached_download(url, name): return path -def fate(name): +def fate(name: str) -> str: """Download and return a path to a sample from the FFmpeg test suite. Data is handled by :func:`cached_download`. @@ -110,7 +111,7 @@ def fate(name): ) -def curated(name): +def curated(name: str) -> str: """Download and return a path to a sample that is curated by the PyAV developers. Data is handled by :func:`cached_download`. diff --git a/tests/test_audiolayout.py b/tests/test_audiolayout.py index 0d13a2a18..9cecac19a 100644 --- a/tests/test_audiolayout.py +++ b/tests/test_audiolayout.py @@ -8,7 +8,7 @@ def test_stereo_properties(self): layout = AudioLayout("stereo") self._test_stereo(layout) - def test_2channel_properties(self): + def test_2channel_properties(self) -> None: layout = AudioLayout(2) self._test_stereo(layout) From 82e3397e0a42b559118821368c8f31af95829db8 Mon Sep 17 00:00:00 2001 From: Santtu Keskinen Date: Wed, 10 Apr 2024 01:44:49 +0300 Subject: [PATCH 250/846] Add bitstream filters --------- Co-authored-by: WyattBlue --- av/__init__.py | 1 + av/bitstream.pxd | 10 +++++ av/bitstream.pyi | 10 +++++ av/bitstream.pyx | 82 ++++++++++++++++++++++++++++++++++++++ av/packet.pyi | 6 +-- docs/api/bitstream.rst | 9 +++++ docs/index.rst | 1 + include/libav.pxd | 1 + include/libavcodec/bsf.pxd | 36 +++++++++++++++++ tests/test_bitstream.py | 62 ++++++++++++++++++++++++++++ 10 files changed, 215 insertions(+), 3 deletions(-) create mode 100644 av/bitstream.pxd create mode 100644 av/bitstream.pyi create mode 100644 av/bitstream.pyx create mode 100644 docs/api/bitstream.rst create mode 100644 include/libavcodec/bsf.pxd create mode 100644 tests/test_bitstream.py diff --git a/av/__init__.py b/av/__init__.py index 8c87e17cc..b4705dc69 100644 --- a/av/__init__.py +++ b/av/__init__.py @@ -29,6 +29,7 @@ from av.audio.frame import AudioFrame from av.audio.layout import AudioLayout from av.audio.resampler import AudioResampler +from av.bitstream import BitStreamFilterContext, bitstream_filters_available from av.codec.codec import Codec, codecs_available from av.codec.context import CodecContext from av.container import open diff --git a/av/bitstream.pxd b/av/bitstream.pxd new file mode 100644 index 000000000..2ebc1da75 --- /dev/null +++ b/av/bitstream.pxd @@ -0,0 +1,10 @@ +cimport libav as lib + +from av.packet cimport Packet + + +cdef class BitStreamFilterContext: + + cdef const lib.AVBSFContext *ptr + + cpdef filter(self, Packet packet=?) diff --git a/av/bitstream.pyi b/av/bitstream.pyi new file mode 100644 index 000000000..c3d4232cd --- /dev/null +++ b/av/bitstream.pyi @@ -0,0 +1,10 @@ +from .packet import Packet +from .stream import Stream + +class BitStreamFilterContext: + def __init__( + self, filter_description: str | bytes, stream: Stream | None = None + ): ... + def filter(self, packet: Packet | None) -> list[Packet]: ... + +bitstream_filters_available: set[str] diff --git a/av/bitstream.pyx b/av/bitstream.pyx new file mode 100644 index 000000000..b1fedb761 --- /dev/null +++ b/av/bitstream.pyx @@ -0,0 +1,82 @@ +cimport libav as lib +from libc.errno cimport EAGAIN + +from av.error cimport err_check +from av.packet cimport Packet +from av.stream cimport Stream + + +cdef class BitStreamFilterContext: + """ + Initializes a bitstream filter: a way to directly modify packet data. + + Wraps :ffmpeg:`AVBSFContext` + """ + def __cinit__(self, filter_description, Stream stream=None): + cdef int res + cdef char *filter_str = filter_description + + with nogil: + res = lib.av_bsf_list_parse_str(filter_str, &self.ptr) + err_check(res) + + if stream is not None: + with nogil: + res = lib.avcodec_parameters_copy(self.ptr.par_in, stream.ptr.codecpar) + err_check(res) + with nogil: + res = lib.avcodec_parameters_copy(self.ptr.par_out, stream.ptr.codecpar) + err_check(res) + + with nogil: + res = lib.av_bsf_init(self.ptr) + err_check(res) + + def __dealloc__(self): + if self.ptr: + lib.av_bsf_free(&self.ptr) + + cpdef filter(self, Packet packet=None): + """ + Processes a packet based on the filter_description set during initialization. + Multiple packets may be created. + + :type: list[Packet] + """ + cdef int res + cdef Packet new_packet + + with nogil: + res = lib.av_bsf_send_packet(self.ptr, packet.ptr if packet is not None else NULL) + err_check(res) + + output = [] + while True: + new_packet = Packet() + with nogil: + res = lib.av_bsf_receive_packet(self.ptr, new_packet.ptr) + + if res == -EAGAIN or res == lib.AVERROR_EOF: + return output + + err_check(res) + if res: + return output + + output.append(new_packet) + + +cdef get_filter_names(): + names = set() + cdef const lib.AVBitStreamFilter *ptr + cdef void *opaque = NULL + while True: + ptr = lib.av_bsf_iterate(&opaque) + if ptr: + names.add(ptr.name) + else: + break + + return names + +bitstream_filters_available = get_filter_names() diff --git a/av/packet.pyi b/av/packet.pyi index cca33009c..91775eff7 100644 --- a/av/packet.pyi +++ b/av/packet.pyi @@ -1,11 +1,11 @@ from fractions import Fraction -from typing import Iterator +from typing import Buffer, Iterator from av.subtitles.subtitle import SubtitleSet from .stream import Stream -class Packet: +class Packet(Buffer): stream: Stream stream_index: int time_base: Fraction @@ -20,5 +20,5 @@ class Packet: is_trusted: bool is_disposable: bool - def __init__(self, input: int | None = None) -> None: ... + def __init__(self, input: int | bytes | None = None) -> None: ... def decode(self) -> Iterator[SubtitleSet]: ... diff --git a/docs/api/bitstream.rst b/docs/api/bitstream.rst new file mode 100644 index 000000000..4dab1bde5 --- /dev/null +++ b/docs/api/bitstream.rst @@ -0,0 +1,9 @@ + +Bitstream Filters +================= + +.. automodule:: av.bitstream + + .. autoclass:: BitStreamFilterContext + :members: + diff --git a/docs/index.rst b/docs/index.rst index a1a6d3dff..66c8ffbba 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -21,6 +21,7 @@ Currently we provide: - ``libavcodec``: :class:`.Codec`, :class:`.CodecContext`, + :class:`.BitStreamFilterContext`, audio/video :class:`frames <.Frame>`, :class:`data planes <.Plane>`, :class:`subtitles <.Subtitle>`; diff --git a/include/libav.pxd b/include/libav.pxd index b9bfe3943..4312be2e8 100644 --- a/include/libav.pxd +++ b/include/libav.pxd @@ -7,6 +7,7 @@ include "libavutil/samplefmt.pxd" include "libavutil/motion_vector.pxd" include "libavcodec/avcodec.pxd" +include "libavcodec/bsf.pxd" include "libavdevice/avdevice.pxd" include "libavformat/avformat.pxd" include "libswresample/swresample.pxd" diff --git a/include/libavcodec/bsf.pxd b/include/libavcodec/bsf.pxd new file mode 100644 index 000000000..27619442a --- /dev/null +++ b/include/libavcodec/bsf.pxd @@ -0,0 +1,36 @@ + +cdef extern from "libavcodec/bsf.h" nogil: + + cdef struct AVBitStreamFilter: + const char *name + AVCodecID *codec_ids + + cdef struct AVCodecParameters: + pass + + cdef struct AVBSFContext: + const AVBitStreamFilter *filter + const AVCodecParameters *par_in + const AVCodecParameters *par_out + + cdef const AVBitStreamFilter* av_bsf_get_by_name(const char *name) + + cdef int av_bsf_list_parse_str( + const char *str, + AVBSFContext **bsf + ) + + cdef int av_bsf_init(AVBSFContext *ctx) + cdef void av_bsf_free(AVBSFContext **ctx) + + cdef AVBitStreamFilter* av_bsf_iterate(void **opaque) + + cdef int av_bsf_send_packet( + AVBSFContext *ctx, + AVPacket *pkt + ) + + cdef int av_bsf_receive_packet( + AVBSFContext *ctx, + AVPacket *pkt + ) diff --git a/tests/test_bitstream.py b/tests/test_bitstream.py new file mode 100644 index 000000000..ddc1bfde2 --- /dev/null +++ b/tests/test_bitstream.py @@ -0,0 +1,62 @@ +import av +from av import Packet +from av.bitstream import BitStreamFilterContext, bitstream_filters_available + +from .common import TestCase, fate_suite + + +class TestBitStreamFilters(TestCase): + def test_filters_availible(self) -> None: + self.assertIn("h264_mp4toannexb", bitstream_filters_available) + + def test_filter_chomp(self) -> None: + ctx = BitStreamFilterContext("chomp") + + src_packets: tuple[Packet, None] = (Packet(b"\x0012345\0\0\0"), None) + self.assertEqual(bytes(src_packets[0]), b"\x0012345\0\0\0") + + result_packets = [] + for p in src_packets: + result_packets.extend(ctx.filter(p)) + + self.assertEqual(len(result_packets), 1) + self.assertEqual(bytes(result_packets[0]), b"\x0012345") + + def test_filter_setts(self) -> None: + ctx = BitStreamFilterContext("setts=pts=N") + + ctx2 = BitStreamFilterContext(b"setts=pts=N") + del ctx2 + + p1 = Packet(b"\0") + p1.pts = 42 + p2 = Packet(b"\0") + p2.pts = 50 + src_packets = [p1, p2, None] + + result_packets: list[Packet] = [] + for p in src_packets: + result_packets.extend(ctx.filter(p)) + + self.assertEqual(len(result_packets), 2) + self.assertEqual(result_packets[0].pts, 0) + self.assertEqual(result_packets[1].pts, 1) + + def test_filter_h264_mp4toannexb(self) -> None: + def is_annexb(packet: Packet) -> bool: + data = bytes(packet) + return data[:3] == b"\0\0\x01" or data[:4] == b"\0\0\0\x01" + + with av.open(fate_suite("h264/interlaced_crop.mp4"), "r") as container: + stream = container.streams.video[0] + ctx = BitStreamFilterContext("h264_mp4toannexb", stream) + + res_packets = [] + for p in container.demux(stream): + self.assertFalse(is_annexb(p)) + res_packets.extend(ctx.filter(p)) + + self.assertEqual(len(res_packets), stream.frames) + + for p in res_packets: + self.assertTrue(is_annexb(p)) From 21ddc604fd71d6c2c85c23af8f9ef8780c493f57 Mon Sep 17 00:00:00 2001 From: Santtu Keskinen Date: Wed, 17 Apr 2024 19:53:56 +0300 Subject: [PATCH 251/846] Bitstream filter API fixes --- av/bitstream.pxd | 1 + av/bitstream.pyi | 6 +++++- av/bitstream.pyx | 25 ++++++++++++++++------ include/libavcodec/bsf.pxd | 4 ++++ tests/test_bitstream.py | 44 ++++++++++++++++++++++++++++++++++---- 5 files changed, 69 insertions(+), 11 deletions(-) diff --git a/av/bitstream.pxd b/av/bitstream.pxd index 2ebc1da75..620fa0366 100644 --- a/av/bitstream.pxd +++ b/av/bitstream.pxd @@ -8,3 +8,4 @@ cdef class BitStreamFilterContext: cdef const lib.AVBSFContext *ptr cpdef filter(self, Packet packet=?) + cpdef flush(self) diff --git a/av/bitstream.pyi b/av/bitstream.pyi index c3d4232cd..477c65f2d 100644 --- a/av/bitstream.pyi +++ b/av/bitstream.pyi @@ -3,8 +3,12 @@ from .stream import Stream class BitStreamFilterContext: def __init__( - self, filter_description: str | bytes, stream: Stream | None = None + self, + filter_description: str | bytes, + in_stream: Stream | None = None, + out_stream: Stream | None = None, ): ... def filter(self, packet: Packet | None) -> list[Packet]: ... + def flush(self) -> None: ... bitstream_filters_available: set[str] diff --git a/av/bitstream.pyx b/av/bitstream.pyx index b1fedb761..b5361f8c2 100644 --- a/av/bitstream.pyx +++ b/av/bitstream.pyx @@ -11,8 +11,11 @@ cdef class BitStreamFilterContext: Initializes a bitstream filter: a way to directly modify packet data. Wraps :ffmpeg:`AVBSFContext` + + :param Stream in_stream: A stream that defines the input codec for the bitfilter. + :param Stream out_stream: A stream whose codec is overwritten using the output parameters from the bitfilter. """ - def __cinit__(self, filter_description, Stream stream=None): + def __cinit__(self, filter_description, Stream in_stream=None, Stream out_stream=None): cdef int res cdef char *filter_str = filter_description @@ -20,18 +23,21 @@ cdef class BitStreamFilterContext: res = lib.av_bsf_list_parse_str(filter_str, &self.ptr) err_check(res) - if stream is not None: - with nogil: - res = lib.avcodec_parameters_copy(self.ptr.par_in, stream.ptr.codecpar) - err_check(res) + if in_stream is not None: with nogil: - res = lib.avcodec_parameters_copy(self.ptr.par_out, stream.ptr.codecpar) + res = lib.avcodec_parameters_copy(self.ptr.par_in, in_stream.ptr.codecpar) err_check(res) with nogil: res = lib.av_bsf_init(self.ptr) err_check(res) + if out_stream is not None: + with nogil: + res = lib.avcodec_parameters_copy(out_stream.ptr.codecpar, self.ptr.par_out) + err_check(res) + lib.avcodec_parameters_to_context(out_stream.codec_context.ptr, out_stream.ptr.codecpar) + def __dealloc__(self): if self.ptr: lib.av_bsf_free(&self.ptr) @@ -65,6 +71,13 @@ cdef class BitStreamFilterContext: output.append(new_packet) + cpdef flush(self): + """ + Reset the internal state of the filter. + Should be called e.g. when seeking. + Can be used to make the filter usable again after draining it with EOF marker packet. + """ + lib.av_bsf_flush(self.ptr) cdef get_filter_names(): names = set() diff --git a/include/libavcodec/bsf.pxd b/include/libavcodec/bsf.pxd index 27619442a..4a558b478 100644 --- a/include/libavcodec/bsf.pxd +++ b/include/libavcodec/bsf.pxd @@ -34,3 +34,7 @@ cdef extern from "libavcodec/bsf.h" nogil: AVBSFContext *ctx, AVPacket *pkt ) + + cdef void av_bsf_flush( + AVBSFContext *ctx + ) diff --git a/tests/test_bitstream.py b/tests/test_bitstream.py index ddc1bfde2..937448fe5 100644 --- a/tests/test_bitstream.py +++ b/tests/test_bitstream.py @@ -5,6 +5,11 @@ from .common import TestCase, fate_suite +def is_annexb(packet: Packet) -> bool: + data = bytes(packet) + return data[:3] == b"\0\0\x01" or data[:4] == b"\0\0\0\x01" + + class TestBitStreamFilters(TestCase): def test_filters_availible(self) -> None: self.assertIn("h264_mp4toannexb", bitstream_filters_available) @@ -43,10 +48,6 @@ def test_filter_setts(self) -> None: self.assertEqual(result_packets[1].pts, 1) def test_filter_h264_mp4toannexb(self) -> None: - def is_annexb(packet: Packet) -> bool: - data = bytes(packet) - return data[:3] == b"\0\0\x01" or data[:4] == b"\0\0\0\x01" - with av.open(fate_suite("h264/interlaced_crop.mp4"), "r") as container: stream = container.streams.video[0] ctx = BitStreamFilterContext("h264_mp4toannexb", stream) @@ -60,3 +61,38 @@ def is_annexb(packet: Packet) -> bool: for p in res_packets: self.assertTrue(is_annexb(p)) + + def test_filter_output_parameters(self) -> None: + with av.open(fate_suite("h264/interlaced_crop.mp4"), "r") as container: + stream = container.streams.video[0] + + self.assertFalse(is_annexb(stream.codec_context.extradata)) + ctx = BitStreamFilterContext("h264_mp4toannexb", stream) + self.assertFalse(is_annexb(stream.codec_context.extradata)) + del ctx + + _ = BitStreamFilterContext("h264_mp4toannexb", stream, out_stream=stream) + self.assertTrue(is_annexb(stream.codec_context.extradata)) + + def test_filter_flush(self) -> None: + with av.open(fate_suite("h264/interlaced_crop.mp4"), "r") as container: + stream = container.streams.video[0] + ctx = BitStreamFilterContext("h264_mp4toannexb", stream) + + res_packets = [] + for p in container.demux(stream): + res_packets.extend(ctx.filter(p)) + self.assertEqual(len(res_packets), stream.frames) + + container.seek(0) + # Without flushing, we expect to get an error: "A non-NULL packet sent after an EOF." + with self.assertRaises(ValueError): + for p in container.demux(stream): + ctx.filter(p) + + ctx.flush() + container.seek(0) + for p in container.demux(stream): + res_packets.extend(ctx.filter(p)) + + self.assertEqual(len(res_packets), stream.frames * 2) From 8dd74b8c23eb3126c1a5940b34771ea3ddfed64e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 17 Apr 2024 15:50:14 -0400 Subject: [PATCH 252/846] Fix type hint for packet, is_annexb --- av/packet.pyi | 4 +++- tests/test_bitstream.py | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/av/packet.pyi b/av/packet.pyi index 91775eff7..14b4b0bce 100644 --- a/av/packet.pyi +++ b/av/packet.pyi @@ -1,5 +1,6 @@ +from collections.abc import Buffer from fractions import Fraction -from typing import Buffer, Iterator +from typing import Iterator from av.subtitles.subtitle import SubtitleSet @@ -22,3 +23,4 @@ class Packet(Buffer): def __init__(self, input: int | bytes | None = None) -> None: ... def decode(self) -> Iterator[SubtitleSet]: ... + def __buffer__(self, arg1) -> memoryview: ... diff --git a/tests/test_bitstream.py b/tests/test_bitstream.py index 937448fe5..8538594a7 100644 --- a/tests/test_bitstream.py +++ b/tests/test_bitstream.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import av from av import Packet from av.bitstream import BitStreamFilterContext, bitstream_filters_available @@ -5,7 +7,10 @@ from .common import TestCase, fate_suite -def is_annexb(packet: Packet) -> bool: +def is_annexb(packet: Packet | bytes | None) -> bool: + if packet is None: + return False + data = bytes(packet) return data[:3] == b"\0\0\x01" or data[:4] == b"\0\0\0\x01" From defce2380c3962bd3b61d853f63cd381e37b1947 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 18 Apr 2024 04:34:44 -0400 Subject: [PATCH 253/846] Add S12M_TIMECODE --- av/sidedata/sidedata.pxd | 2 -- av/sidedata/sidedata.pyi | 1 + av/sidedata/sidedata.pyx | 3 ++- include/libavcodec/avcodec.pxd | 3 +-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/av/sidedata/sidedata.pxd b/av/sidedata/sidedata.pxd index ac58f1477..8a2f6d07c 100644 --- a/av/sidedata/sidedata.pxd +++ b/av/sidedata/sidedata.pxd @@ -7,7 +7,6 @@ from av.frame cimport Frame cdef class SideData(Buffer): - cdef Frame frame cdef lib.AVFrameSideData *ptr cdef _Dictionary metadata @@ -16,7 +15,6 @@ cdef class SideData(Buffer): cdef SideData wrap_side_data(Frame frame, int index) cdef class _SideDataContainer: - cdef Frame frame cdef list _by_index diff --git a/av/sidedata/sidedata.pyi b/av/sidedata/sidedata.pyi index ac28f0dee..e814bb222 100644 --- a/av/sidedata/sidedata.pyi +++ b/av/sidedata/sidedata.pyi @@ -23,6 +23,7 @@ class Type(EnumItem): CONTENT_LIGHT_LEVEL: int ICC_PROFILE: int SEI_UNREGISTERED: int + S12M_TIMECODE: int class SideData(Buffer): type: Type diff --git a/av/sidedata/sidedata.pyx b/av/sidedata/sidedata.pyx index 05ed00219..9b423a30b 100644 --- a/av/sidedata/sidedata.pyx +++ b/av/sidedata/sidedata.pyx @@ -25,7 +25,8 @@ Type = define_enum("Type", __name__, ( ("SPHERICAL", lib.AV_FRAME_DATA_SPHERICAL), ("CONTENT_LIGHT_LEVEL", lib.AV_FRAME_DATA_CONTENT_LIGHT_LEVEL), ("ICC_PROFILE", lib.AV_FRAME_DATA_ICC_PROFILE), - ("SEI_UNREGISTERED", lib.AV_FRAME_DATA_SEI_UNREGISTERED) if lib.AV_FRAME_DATA_SEI_UNREGISTERED != -1 else None, + ("SEI_UNREGISTERED", lib.AV_FRAME_DATA_SEI_UNREGISTERED), + ("S12M_TIMECODE", lib.AV_FRAME_DATA_S12M_TIMECODE), )) diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 49758be4c..16d88a473 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -10,8 +10,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: #define AV_FRAME_DATA_SEI_UNREGISTERED -1 #endif """ - - # custom cdef set pyav_get_available_codecs() cdef int avcodec_version() @@ -326,6 +324,7 @@ cdef extern from "libavcodec/avcodec.h" nogil: AV_FRAME_DATA_QP_TABLE_PROPERTIES AV_FRAME_DATA_QP_TABLE_DATA AV_FRAME_DATA_SEI_UNREGISTERED + AV_FRAME_DATA_S12M_TIMECODE cdef struct AVFrameSideData: AVFrameSideDataType type From f77e3d4a9271402947ca9d9d67c45dddd594c60f Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 24 Apr 2024 17:14:42 -0400 Subject: [PATCH 254/846] Disable logs by default --- av/logging.pyi | 7 +- av/logging.pyx | 168 +++++++++++++++++---------------------- docs/index.rst | 1 + tests/test_colorspace.py | 1 + tests/test_logging.py | 14 ++++ tests/test_python_io.py | 4 + 6 files changed, 93 insertions(+), 102 deletions(-) diff --git a/av/logging.pyi b/av/logging.pyi index a85eba2df..64ca9df3c 100644 --- a/av/logging.pyi +++ b/av/logging.pyi @@ -13,11 +13,9 @@ TRACE: int CRITICAL: int def adapt_level(level: int) -> int: ... -def get_level() -> int: ... -def set_level(level: int) -> None: ... +def get_level() -> int | None: ... +def set_level(level: int | None) -> None: ... def restore_default_callback() -> None: ... -def get_print_after_shutdown() -> bool: ... -def set_print_after_shutdown(v: bool) -> None: ... def get_skip_repeated() -> bool: ... def set_skip_repeated(v: bool) -> None: ... def get_last_error() -> tuple[int, tuple[int, str, str] | None]: ... @@ -36,7 +34,6 @@ class Capture: ) -> None: ... level_threshold: int -print_after_shutdown: bool skip_repeated: bool skip_lock: Lock last_log: tuple[int, str, str] | None diff --git a/av/logging.pyx b/av/logging.pyx index 8940c3139..d3c3bfce5 100644 --- a/av/logging.pyx +++ b/av/logging.pyx @@ -1,6 +1,21 @@ """ -FFmpeg has a logging system that it uses extensively. PyAV hooks into that system -to translate FFmpeg logs into Python's +FFmpeg has a logging system that it uses extensively. It's very noisy so PyAV turns it +off by default. This, unfortunately has the effect of making raised errors having less +detailed messages. It's therefore recommended to use VERBOSE when developing. + +.. _enable_logging: + +Enabling Logging +~~~~~~~~~~~~~~~~~ + +You can hook the logging system with Python by setting the log level:: + + import av + + av.logging.set_level(av.logging.VERBOSE) + + +PyAV hooks into that system to translate FFmpeg logs into Python's `logging system `_. If you are not already using Python's logging system, you can initialize it @@ -10,19 +25,12 @@ quickly with:: logging.basicConfig() -.. _disable_logging: - -Disabling Logging -~~~~~~~~~~~~~~~~~ - -You can disable hooking the logging system with an environment variable:: - - export PYAV_LOGGING=off - -or at runtime with :func:`restore_default_callback`. +Note that handling logs with Python sometimes doesn't play nice multi-threads workflows. +An alternative is :func:`restore_default_callback`. -This will leave (or restore) the FFmpeg logging system, which prints to the terminal. -This may also result in raised errors having less detailed messages. +This will restores FFmpeg's logging default system, which prints to the terminal. +Like with setting the log level to ``None``, this may also result in raised errors +having less detailed messages. API Reference @@ -37,7 +45,6 @@ from libc.stdio cimport fprintf, stderr from libc.stdlib cimport free, malloc import logging -import os import sys from threading import Lock, get_ident @@ -72,13 +79,10 @@ cpdef adapt_level(int level): elif level <= lib.AV_LOG_DEBUG: return 5 # Lower than any logging constant. else: # lib.AV_LOG_TRACE - return 1 # ... yeah. + return 1 -# While we start with the level quite low, Python defaults to INFO, and so -# they will not show. The logging system can add significant overhead, so -# be wary of dropping this lower. -cdef int level_threshold = lib.AV_LOG_VERBOSE +cdef object level_threshold = None # ... but lets limit ourselves to WARNING (assuming nobody already did this). if "libav" not in logging.Logger.manager.loggerDict: @@ -86,33 +90,30 @@ if "libav" not in logging.Logger.manager.loggerDict: def get_level(): - """Return current FFmpeg logging threshold. See :func:`set_level`.""" + """Returns the current log level. See :func:`set_level`.""" return level_threshold -def set_level(int level): +def set_level(level): """set_level(level) - Sets logging threshold when converting from FFmpeg's logging system - to Python's. It is recommended to use the constants available in this - module to set the level: ``PANIC``, ``FATAL``, ``ERROR``, - ``WARNING``, ``INFO``, ``VERBOSE``, and ``DEBUG``. - - While less efficient, it is generally preferable to modify logging - with Python's :mod:`logging`, e.g.:: + Sets PyAV's log level. It can be set to constants available in this + module: ``PANIC``, ``FATAL``, ``ERROR``, ``WARNING``, ``INFO``, + ``VERBOSE``, ``DEBUG``, or ``None`` (the default). - logging.getLogger('libav').setLevel(logging.ERROR) + PyAV defaults to totally ignoring all ffmpeg logs. This has the side effect of + making certain Exceptions have no messages. It's therefore recommended to use: - PyAV defaults to translating everything except ``AV_LOG_DEBUG``, so this - function is only nessesary to use if you want to see those messages as well. - ``AV_LOG_DEBUG`` will be translated to a level 5 message, which is lower - than any builtin Python logging level, so you must lower that as well:: - - logging.getLogger().setLevel(5) + av.logging.set_level(av.logging.VERBOSE) + When developing your application. """ global level_threshold - level_threshold = level + + if level is None or type(level) is int: + level_threshold = level + else: + raise ValueError("level must be: int | None") def restore_default_callback(): @@ -120,20 +121,6 @@ def restore_default_callback(): lib.av_log_set_callback(lib.av_log_default_callback) -cdef bint print_after_shutdown = False - - -def get_print_after_shutdown(): - """Will logging continue to ``stderr`` after Python shutdown?""" - return print_after_shutdown - - -def set_print_after_shutdown(v): - """Set if logging should continue to ``stderr`` after Python shutdown.""" - global print_after_shutdown - print_after_shutdown = bool(v) - - cdef bint skip_repeated = True cdef skip_lock = Lock() cdef object last_log = None @@ -226,47 +213,6 @@ cpdef log(int level, str name, str message): free(obj) -cdef void log_callback(void *ptr, int level, const char *format, lib.va_list args) noexcept nogil: - - cdef bint inited = lib.Py_IsInitialized() - if not inited and not print_after_shutdown: - return - - # Fast path: avoid logging overhead. This should match the - # log_callback_gil() checks that result in ignoring the message. - with gil: - if level > level_threshold and level != lib.AV_LOG_ERROR: - return - - # Format the message. - cdef char message[1024] - lib.vsnprintf(message, 1023, format, args) - - # Get the name. - cdef const char *name = NULL - cdef lib.AVClass *cls = (ptr)[0] if ptr else NULL - if cls and cls.item_name: - # I'm not 100% on this, but this should be static, and so - # it doesn't matter if the AVClass that returned it vanishes or not. - name = cls.item_name(ptr) - - if not inited: - fprintf(stderr, "av.logging (after shutdown): %s[%d]: %s\n", - name, level, message) - return - - with gil: - try: - log_callback_gil(level, name, message) - - except Exception as e: - fprintf(stderr, "av.logging: exception while handling %s[%d]: %s\n", - name, level, message) - # For some reason lib.PyErr_PrintEx(0) won't work. - exc, type_, tb = sys.exc_info() - lib.PyErr_Display(exc, type_, tb) - - cdef log_callback_gil(int level, const char *c_name, const char *c_message): global error_count global skip_count @@ -336,8 +282,36 @@ cdef log_callback_emit(log): logger.log(py_level, message.strip()) -# Start the magic! -# We allow the user to fully disable the logging system as it will not play -# nicely with subinterpreters due to FFmpeg-created threads. -if os.environ.get("PYAV_LOGGING") != "off": - lib.av_log_set_callback(log_callback) +cdef void log_callback(void *ptr, int level, const char *format, lib.va_list args) noexcept nogil: + cdef bint inited = lib.Py_IsInitialized() + if not inited: + return + + with gil: + if level_threshold is None: + return + if level > level_threshold and level != lib.AV_LOG_ERROR: + return + + # Format the message. + cdef char message[1024] + lib.vsnprintf(message, 1023, format, args) + + # Get the name. + cdef const char *name = NULL + cdef lib.AVClass *cls = (ptr)[0] if ptr else NULL + if cls and cls.item_name: + name = cls.item_name(ptr) + + with gil: + try: + log_callback_gil(level, name, message) + except Exception as e: + fprintf(stderr, "av.logging: exception while handling %s[%d]: %s\n", + name, level, message) + # For some reason lib.PyErr_PrintEx(0) won't work. + exc, type_, tb = sys.exc_info() + lib.PyErr_Display(exc, type_, tb) + + +lib.av_log_set_callback(log_callback) diff --git a/docs/index.rst b/docs/index.rst index 66c8ffbba..afeaa60d6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -53,6 +53,7 @@ Basic Demo import av + av.logging.set_level(av.logging.VERBOSE) container = av.open(path_to_video) for index, frame in enumerate(container.decode(video=0)): diff --git a/tests/test_colorspace.py b/tests/test_colorspace.py index 571e7bd2b..afc16846f 100644 --- a/tests/test_colorspace.py +++ b/tests/test_colorspace.py @@ -22,6 +22,7 @@ def test_penguin_joke(self) -> None: for packet in container.demux(stream): for frame in packet.decode(): + assert isinstance(frame, av.VideoFrame) self.assertEqual(frame.color_range, ColorRange.JPEG) # a.k.a "pc" self.assertEqual(frame.colorspace, Colorspace.ITU601) return diff --git a/tests/test_logging.py b/tests/test_logging.py index 2e35879e1..2a09a9c1a 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -22,6 +22,8 @@ def test_adapt_level(self): ) def test_threaded_captures(self): + av.logging.set_level(av.logging.VERBOSE) + with av.logging.Capture(local=True) as logs: do_log("main") thread = threading.Thread(target=do_log, args=("thread",)) @@ -29,8 +31,11 @@ def test_threaded_captures(self): thread.join() self.assertIn((av.logging.INFO, "test", "main"), logs) + av.logging.set_level(None) def test_global_captures(self): + av.logging.set_level(av.logging.VERBOSE) + with av.logging.Capture(local=False) as logs: do_log("main") thread = threading.Thread(target=do_log, args=("thread",)) @@ -39,8 +44,11 @@ def test_global_captures(self): self.assertIn((av.logging.INFO, "test", "main"), logs) self.assertIn((av.logging.INFO, "test", "thread"), logs) + av.logging.set_level(None) def test_repeats(self): + av.logging.set_level(av.logging.VERBOSE) + with av.logging.Capture() as logs: do_log("foo") do_log("foo") @@ -62,7 +70,11 @@ def test_repeats(self): ], ) + av.logging.set_level(None) + def test_error(self): + av.logging.set_level(av.logging.VERBOSE) + log = (av.logging.ERROR, "test", "This is a test.") av.logging.log(*log) try: @@ -71,3 +83,5 @@ def test_error(self): self.assertEqual(e.log, log) else: self.fail() + + av.logging.set_level(None) diff --git a/tests/test_python_io.py b/tests/test_python_io.py index 42b8dd132..58ae4fd0d 100644 --- a/tests/test_python_io.py +++ b/tests/test_python_io.py @@ -292,6 +292,8 @@ def test_writing_to_pipe_readonly(self) -> None: ) def test_writing_to_pipe_writeonly(self): + av.logging.set_level(av.logging.VERBOSE) + buf = WriteOnlyPipe() with self.assertRaises(ValueError) as cm: self.write(buf) @@ -300,6 +302,8 @@ def test_writing_to_pipe_writeonly(self): str(cm.exception), ) + av.logging.set_level(None) + def read(self, fh, seekable: bool = True) -> None: wrapped = MethodLogger(fh) From fb880d21e833da6be4bde390675a28e549f32b4f Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 24 Apr 2024 18:43:10 -0400 Subject: [PATCH 255/846] Default callback no longer needs gil --- av/logging.pyi | 11 ----------- av/logging.pyx | 14 +++++++++----- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/av/logging.pyi b/av/logging.pyi index 64ca9df3c..1db5e4a3b 100644 --- a/av/logging.pyi +++ b/av/logging.pyi @@ -1,5 +1,3 @@ -import logging -from threading import Lock from typing import Any, Callable PANIC: int @@ -32,12 +30,3 @@ class Capture: value: Exception | None, traceback: Callable[..., Any] | None, ) -> None: ... - -level_threshold: int -skip_repeated: bool -skip_lock: Lock -last_log: tuple[int, str, str] | None -skip_count: int -last_error: tuple[int, str, str] | None -global_captures: list[list[tuple[int, str, str]]] -thread_captures: dict[int, list[tuple[int, str, str]]] diff --git a/av/logging.pyx b/av/logging.pyx index d3c3bfce5..1006d094c 100644 --- a/av/logging.pyx +++ b/av/logging.pyx @@ -49,7 +49,6 @@ import sys from threading import Lock, get_ident # Library levels. -# QUIET = lib.AV_LOG_QUIET # -8; not really a level. PANIC = lib.AV_LOG_PANIC # 0 FATAL = lib.AV_LOG_FATAL # 8 ERROR = lib.AV_LOG_ERROR @@ -110,8 +109,12 @@ def set_level(level): """ global level_threshold - if level is None or type(level) is int: + if level is None: level_threshold = level + lib.av_log_set_callback(nolog_callback) + elif type(level) is int: + level_threshold = level + lib.av_log_set_callback(log_callback) else: raise ValueError("level must be: int | None") @@ -288,8 +291,6 @@ cdef void log_callback(void *ptr, int level, const char *format, lib.va_list arg return with gil: - if level_threshold is None: - return if level > level_threshold and level != lib.AV_LOG_ERROR: return @@ -314,4 +315,7 @@ cdef void log_callback(void *ptr, int level, const char *format, lib.va_list arg lib.PyErr_Display(exc, type_, tb) -lib.av_log_set_callback(log_callback) +cdef void nolog_callback(void *ptr, int level, const char *format, lib.va_list args) noexcept nogil: + pass + +lib.av_log_set_callback(nolog_callback) From c983f75852df22f76c7f74290fe65861109331f9 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 24 Apr 2024 23:47:00 -0400 Subject: [PATCH 256/846] Workaround bug in Actions --- .github/workflows/tests.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0655951b1..8912dd02e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -42,7 +42,7 @@ jobs: - {os: ubuntu-latest, python: 3.9, ffmpeg: "6.1", extras: true} - {os: ubuntu-latest, python: 3.9, ffmpeg: "6.0"} - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1"} - - {os: macos-latest, python: 3.9, ffmpeg: "6.1"} + # - {os: macos-latest, python: 3.9, ffmpeg: "6.1"} env: PYAV_PYTHON: python${{ matrix.config.python }} @@ -70,7 +70,7 @@ jobs: sudo apt-get install doxygen fi ;; - macos-latest) + macos-13) brew install automake libtool nasm pkg-config shtool texi2html wget brew install libass libjpeg libpng libvorbis libvpx opus theora x264 ;; @@ -177,9 +177,9 @@ jobs: fail-fast: false matrix: include: - - os: macos-latest + - os: macos-13 arch: arm64 - - os: macos-latest + - os: macos-13 arch: x86_64 - os: ubuntu-latest arch: aarch64 @@ -198,7 +198,7 @@ jobs: if: matrix.os == 'ubuntu-latest' uses: docker/setup-qemu-action@v3 - name: Install packages - if: matrix.os == 'macos-latest' + if: matrix.os == 'macos-13' run: | brew update brew install pkg-config From 19afbf277150b99e0770d55e29ae0611becb04a8 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 25 Apr 2024 01:15:58 -0400 Subject: [PATCH 257/846] Update doc site --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index cefe7ca6a..0b3ce00f7 100644 --- a/setup.py +++ b/setup.py @@ -194,8 +194,7 @@ def parse_cflags(raw_flags): license="BSD", project_urls={ "Bug Reports": "https://github.com/PyAV-Org/PyAV/issues", - "Documentation": "https://pyav.org/docs", - "Feedstock": "https://github.com/conda-forge/av-feedstock", + "Documentation": "https://pyav.basswood-io.com", "Download": "https://pypi.org/project/av", }, author="Mike Boers", From 7f357af753f18840d7d802c91a6e85d239e8c78a Mon Sep 17 00:00:00 2001 From: Santtu Keskinen Date: Wed, 24 Apr 2024 22:56:06 +0300 Subject: [PATCH 258/846] Expose CodecContext flush_buffers --- av/codec/context.pxd | 2 ++ av/codec/context.pyi | 4 ++++ av/codec/context.pyx | 11 +++++++++++ av/container/input.pyx | 5 ++--- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/av/codec/context.pxd b/av/codec/context.pxd index f247655ff..58bc6a07d 100644 --- a/av/codec/context.pxd +++ b/av/codec/context.pxd @@ -37,6 +37,8 @@ cdef class CodecContext: cpdef encode(self, Frame frame=?) cpdef decode(self, Packet packet=?) + cpdef flush_buffers(self) + # Used by both transcode APIs to setup user-land objects. # TODO: Remove the `Packet` from `_setup_decoded_frame` (because flushing # packets are bogus). It should take all info it needs from the context and/or stream. diff --git a/av/codec/context.pyi b/av/codec/context.pyi index ee81daae5..0477aeb93 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -1,6 +1,7 @@ from typing import Any, Literal from av.enum import EnumFlag, EnumItem +from av.frame import Frame from av.packet import Packet from .codec import Codec @@ -78,3 +79,6 @@ class CodecContext: def parse( self, raw_input: bytes | bytearray | memoryview | None = None ) -> list[Packet]: ... + def encode(self, frame: Frame | None) -> list[Packet]: ... + def decode(self, packet: Packet | None) -> list[Frame]: ... + def flush_buffers(self) -> None: ... diff --git a/av/codec/context.pyx b/av/codec/context.pyx index e2557e702..d94a08015 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -510,6 +510,17 @@ cdef class CodecContext: res.append(frame) return res + cpdef flush_buffers(self): + """Reset the internal codec state and discard all internal buffers. + + Should be called before you start decoding from a new position e.g. + when seeking or when switching to a different stream. + + """ + if self.is_open: + with nogil: + lib.avcodec_flush_buffers(self.ptr) + cdef _setup_decoded_frame(self, Frame frame, Packet packet): # Propagate our manual times. diff --git a/av/container/input.pyx b/av/container/input.pyx index acf02fbab..88cc95ee0 100644 --- a/av/container/input.pyx +++ b/av/container/input.pyx @@ -278,6 +278,5 @@ cdef class InputContainer(Container): for stream in self.streams: codec_context = stream.codec_context - if codec_context and codec_context.is_open: - with nogil: - lib.avcodec_flush_buffers(codec_context.ptr) + if codec_context: + codec_context.flush_buffers() From ba7a2c9f716af506838d399e6ed27ed6d64d2435 Mon Sep 17 00:00:00 2001 From: Artturin Date: Sat, 27 Apr 2024 01:56:57 +0300 Subject: [PATCH 259/846] setup.py: use PKG_CONFIG env var to get the pkg-config to use pkg-config may be prefixed when cross-compiling --- setup.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 0b3ce00f7..9f43f5f27 100644 --- a/setup.py +++ b/setup.py @@ -72,16 +72,17 @@ def get_config_from_pkg_config(): """ Get distutils-compatible extension arguments using pkg-config. """ + pkg_config = os.environ.get('PKG_CONFIG', 'pkg-config') try: raw_cflags = subprocess.check_output( - ["pkg-config", "--cflags", "--libs"] + [pkg_config, "--cflags", "--libs"] + ["lib" + name for name in FFMPEG_LIBRARIES] ) except FileNotFoundError: - print("pkg-config is required for building PyAV") + print(f"{pkg_config} is required for building PyAV") exit(1) except subprocess.CalledProcessError: - print("pkg-config could not find libraries {}".format(FFMPEG_LIBRARIES)) + print(f"{pkg_config} could not find libraries {FFMPEG_LIBRARIES}") exit(1) known, unknown = parse_cflags(raw_cflags.decode("utf-8")) From 73c59eaf4e9e9fa8e319fe383d66e1d390165b19 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 27 Apr 2024 03:43:57 -0400 Subject: [PATCH 260/846] Use mypy in lint --- .github/workflows/tests.yml | 2 +- Makefile | 3 ++- av/codec/context.pyi | 2 -- setup.py | 6 ++++-- tests/requirements.txt | 1 + 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8912dd02e..4dd70a1ad 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -24,7 +24,7 @@ jobs: - name: Python uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: 3.12 - name: Packages run: pip install -r tests/requirements.txt diff --git a/Makefile b/Makefile index 9a0b0a95f..968494e2e 100644 --- a/Makefile +++ b/Makefile @@ -25,9 +25,10 @@ fate-suite: rsync -vrltLW rsync://fate-suite.ffmpeg.org/fate-suite/ tests/assets/fate-suite/ lint: - black --check av examples tests + black --check av examples tests setup.py flake8 av examples tests isort --check-only --diff av examples tests + mypy av tests test: $(PYTHON) setup.py test diff --git a/av/codec/context.pyi b/av/codec/context.pyi index 0477aeb93..4f7f3d2dc 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -79,6 +79,4 @@ class CodecContext: def parse( self, raw_input: bytes | bytearray | memoryview | None = None ) -> list[Packet]: ... - def encode(self, frame: Frame | None) -> list[Packet]: ... - def decode(self, packet: Packet | None) -> list[Frame]: ... def flush_buffers(self) -> None: ... diff --git a/setup.py b/setup.py index 9f43f5f27..8cfcbaa58 100644 --- a/setup.py +++ b/setup.py @@ -72,7 +72,7 @@ def get_config_from_pkg_config(): """ Get distutils-compatible extension arguments using pkg-config. """ - pkg_config = os.environ.get('PKG_CONFIG', 'pkg-config') + pkg_config = os.environ.get("PKG_CONFIG", "pkg-config") try: raw_cflags = subprocess.check_output( [pkg_config, "--cflags", "--libs"] @@ -180,7 +180,9 @@ def parse_cflags(raw_flags): exec(fp.read(), about) package_folders = pathlib.Path("av").glob("**/") -package_data = {".".join(pckg.parts): ["*.pxd", "*.pyi", "*.typed"] for pckg in package_folders} +package_data = { + ".".join(pckg.parts): ["*.pxd", "*.pyi", "*.typed"] for pckg in package_folders +} with open("README.md") as f: diff --git a/tests/requirements.txt b/tests/requirements.txt index fd44ed1b8..f9f354439 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -6,3 +6,4 @@ isort flake8 flake8-pyproject sphinx==5.1.0 +mypy==1.10.0 From fa49068b6bfa20b6e9a0ff22cb3612a66ffff8e2 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 27 Apr 2024 04:14:06 -0400 Subject: [PATCH 261/846] Separate smoke tests from building --- .github/workflows/smoke.yml | 152 ++++++++++++++++++++++++++++++++++++ .github/workflows/tests.yml | 151 +---------------------------------- 2 files changed, 153 insertions(+), 150 deletions(-) create mode 100644 .github/workflows/smoke.yml diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml new file mode 100644 index 000000000..69d5db44e --- /dev/null +++ b/.github/workflows/smoke.yml @@ -0,0 +1,152 @@ +name: smoke +on: + push: + branches: main + paths-ignore: + - '**.md' + - '**.rst' + - '**.txt' + pull_request: + branches: main + paths-ignore: + - '**.md' + - '**.rst' + - '**.txt' +jobs: + style: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Python + uses: actions/setup-python@v5 + with: + python-version: 3.12 + + - name: Packages + run: pip install -r tests/requirements.txt + + - name: Linters + run: make lint + + nix: + name: "py-${{ matrix.config.python }} lib-${{ matrix.config.ffmpeg }} ${{matrix.config.os}}" + runs-on: ${{ matrix.config.os }} + strategy: + fail-fast: false + matrix: + config: + - {os: ubuntu-latest, python: 3.9, ffmpeg: "6.1", extras: true} + - {os: ubuntu-latest, python: 3.9, ffmpeg: "6.0"} + - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1"} + #- {os: macos-13, python: 3.12, ffmpeg: "6.1"} + + env: + PYAV_PYTHON: python${{ matrix.config.python }} + PYAV_LIBRARY: ffmpeg-${{ matrix.config.ffmpeg }} + + steps: + - uses: actions/checkout@v4 + name: Checkout + + - name: Python ${{ matrix.config.python }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.config.python }} + + - name: OS Packages + run: | + case ${{ matrix.config.os }} in + ubuntu-latest) + sudo apt-get update + sudo apt-get install autoconf automake build-essential cmake \ + libtool mercurial pkg-config texinfo wget yasm zlib1g-dev + sudo apt-get install libass-dev libfreetype6-dev libjpeg-dev \ + libtheora-dev libvorbis-dev libx264-dev + if [[ "${{ matrix.config.extras }}" ]]; then + sudo apt-get install doxygen + fi + ;; + macos-13) + brew install automake libtool nasm pkg-config shtool texi2html wget + brew install libass libjpeg libpng libvorbis libvpx opus theora x264 + ;; + esac + + - name: Pip and FFmpeg + run: | + . scripts/activate.sh ffmpeg-${{ matrix.config.ffmpeg }} + scripts/build-deps + + - name: Build + run: | + . scripts/activate.sh ffmpeg-${{ matrix.config.ffmpeg }} + scripts/build + + - name: Test + run: | + . scripts/activate.sh ffmpeg-${{ matrix.config.ffmpeg }} + python -m av --version # Assert it can import. + scripts/test + + - name: Docs + if: matrix.config.extras + run: | + . scripts/activate.sh ffmpeg-${{ matrix.config.ffmpeg }} + make -C docs html + + - name: Doctest + if: matrix.config.extras + run: | + . scripts/activate.sh ffmpeg-${{ matrix.config.ffmpeg }} + make -C docs test + + - name: Examples + if: matrix.config.extras + run: | + . scripts/activate.sh ffmpeg-${{ matrix.config.ffmpeg }} + scripts/test examples + + windows: + name: "py-${{ matrix.config.python }} lib-${{ matrix.config.ffmpeg }} ${{matrix.config.os}}" + runs-on: ${{ matrix.config.os }} + + strategy: + fail-fast: false + matrix: + config: + - {os: windows-latest, python: 3.9, ffmpeg: "6.1"} + - {os: windows-latest, python: 3.9, ffmpeg: "6.0"} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Conda + shell: bash + run: | + . $CONDA/etc/profile.d/conda.sh + conda config --set always_yes true + conda config --add channels conda-forge + conda create -q -n pyav \ + cython \ + numpy \ + pillow \ + python=${{ matrix.config.python }} \ + setuptools + + - name: Build + shell: bash + run: | + . $CONDA/etc/profile.d/conda.sh + conda activate pyav + python scripts\\fetch-vendor.py --config-file scripts\\ffmpeg-${{ matrix.config.ffmpeg }}.json $CONDA_PREFIX\\Library + python setup.py build_ext --inplace --ffmpeg-dir=$CONDA_PREFIX\\Library + + - name: Test + shell: bash + run: | + . $CONDA/etc/profile.d/conda.sh + conda activate pyav + python setup.py test diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4dd70a1ad..d1c57372b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,157 +2,8 @@ name: tests on: release: types: [published] - push: - branches: main - paths-ignore: - - '**.md' - - '**.rst' - - '**.txt' - pull_request: - branches: main - paths-ignore: - - '**.md' - - '**.rst' - - '**.txt' + workflow_dispatch: jobs: - style: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Python - uses: actions/setup-python@v5 - with: - python-version: 3.12 - - - name: Packages - run: pip install -r tests/requirements.txt - - - name: Linters - run: make lint - - nix: - name: "py-${{ matrix.config.python }} lib-${{ matrix.config.ffmpeg }} ${{matrix.config.os}}" - runs-on: ${{ matrix.config.os }} - strategy: - fail-fast: false - matrix: - config: - - {os: ubuntu-latest, python: 3.9, ffmpeg: "6.1", extras: true} - - {os: ubuntu-latest, python: 3.9, ffmpeg: "6.0"} - - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1"} - # - {os: macos-latest, python: 3.9, ffmpeg: "6.1"} - - env: - PYAV_PYTHON: python${{ matrix.config.python }} - PYAV_LIBRARY: ffmpeg-${{ matrix.config.ffmpeg }} - - steps: - - uses: actions/checkout@v4 - name: Checkout - - - name: Python ${{ matrix.config.python }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.config.python }} - - - name: OS Packages - run: | - case ${{ matrix.config.os }} in - ubuntu-latest) - sudo apt-get update - sudo apt-get install autoconf automake build-essential cmake \ - libtool mercurial pkg-config texinfo wget yasm zlib1g-dev - sudo apt-get install libass-dev libfreetype6-dev libjpeg-dev \ - libtheora-dev libvorbis-dev libx264-dev - if [[ "${{ matrix.config.extras }}" ]]; then - sudo apt-get install doxygen - fi - ;; - macos-13) - brew install automake libtool nasm pkg-config shtool texi2html wget - brew install libass libjpeg libpng libvorbis libvpx opus theora x264 - ;; - esac - - - name: Pip and FFmpeg - run: | - . scripts/activate.sh ffmpeg-${{ matrix.config.ffmpeg }} - scripts/build-deps - - - name: Build - run: | - . scripts/activate.sh ffmpeg-${{ matrix.config.ffmpeg }} - scripts/build - - - name: Test - run: | - . scripts/activate.sh ffmpeg-${{ matrix.config.ffmpeg }} - python -m av --version # Assert it can import. - scripts/test - - - name: Docs - if: matrix.config.extras - run: | - . scripts/activate.sh ffmpeg-${{ matrix.config.ffmpeg }} - make -C docs html - - - name: Doctest - if: matrix.config.extras - run: | - . scripts/activate.sh ffmpeg-${{ matrix.config.ffmpeg }} - make -C docs test - - - name: Examples - if: matrix.config.extras - run: | - . scripts/activate.sh ffmpeg-${{ matrix.config.ffmpeg }} - scripts/test examples - - windows: - name: "py-${{ matrix.config.python }} lib-${{ matrix.config.ffmpeg }} ${{matrix.config.os}}" - runs-on: ${{ matrix.config.os }} - - strategy: - fail-fast: false - matrix: - config: - - {os: windows-latest, python: 3.9, ffmpeg: "6.1"} - - {os: windows-latest, python: 3.9, ffmpeg: "6.0"} - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Conda - shell: bash - run: | - . $CONDA/etc/profile.d/conda.sh - conda config --set always_yes true - conda config --add channels conda-forge - conda create -q -n pyav \ - cython \ - numpy \ - pillow \ - python=${{ matrix.config.python }} \ - setuptools - - - name: Build - shell: bash - run: | - . $CONDA/etc/profile.d/conda.sh - conda activate pyav - python scripts\\fetch-vendor.py --config-file scripts\\ffmpeg-${{ matrix.config.ffmpeg }}.json $CONDA_PREFIX\\Library - python setup.py build_ext --inplace --ffmpeg-dir=$CONDA_PREFIX\\Library - - - name: Test - shell: bash - run: | - . $CONDA/etc/profile.d/conda.sh - conda activate pyav - python setup.py test - package-source: runs-on: ubuntu-latest steps: From ea6c6d84a65925666b40874f4278e69535f28dac Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 27 Apr 2024 04:40:25 -0400 Subject: [PATCH 262/846] Add macos to smoke tests --- .github/workflows/smoke.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 69d5db44e..a0fa94fe5 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -40,7 +40,7 @@ jobs: - {os: ubuntu-latest, python: 3.9, ffmpeg: "6.1", extras: true} - {os: ubuntu-latest, python: 3.9, ffmpeg: "6.0"} - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1"} - #- {os: macos-13, python: 3.12, ffmpeg: "6.1"} + - {os: macos-12, python: 3.9, ffmpeg: "6.1"} env: PYAV_PYTHON: python${{ matrix.config.python }} @@ -68,7 +68,7 @@ jobs: sudo apt-get install doxygen fi ;; - macos-13) + macos-12) brew install automake libtool nasm pkg-config shtool texi2html wget brew install libass libjpeg libpng libvorbis libvpx opus theora x264 ;; From 683ce79a9245535b3c42339032f12171b361a975 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 5 May 2024 03:43:54 -0400 Subject: [PATCH 263/846] Build with Arm for PyPy now --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d1c57372b..17c9a687b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -28,7 +28,7 @@ jobs: fail-fast: false matrix: include: - - os: macos-13 + - os: macos-latest arch: arm64 - os: macos-13 arch: x86_64 @@ -63,7 +63,7 @@ jobs: CIBW_ENVIRONMENT_MACOS: PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig LDFLAGS=-headerpad_max_install_names CIBW_ENVIRONMENT_WINDOWS: INCLUDE=C:\\cibw\\vendor\\include LIB=C:\\cibw\\vendor\\lib PYAV_SKIP_TESTS=unicode_filename CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: delvewheel repair --add-path C:\cibw\vendor\bin -w {dest_dir} {wheel} - CIBW_SKIP: pp38-win* *-musllinux* + CIBW_SKIP: "*-musllinux*" CIBW_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m unittest discover -t {project} -s tests && mv {project}/av.disabled {project}/av CIBW_TEST_REQUIRES: numpy # skip tests when there are no binary wheels of numpy From dcee25a3146b50839dabe7c249633a718db5a6d5 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 7 May 2024 18:37:58 -0400 Subject: [PATCH 264/846] Set version to 12.1.0 --- av/about.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/about.py b/av/about.py index 79a759ae1..5170f5963 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "13.0.0rc1" +__version__ = "12.1.0rc1" From 7ee9aa0710f087b2d72a7b18a5ad7a8212c1dfb0 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 7 May 2024 20:32:53 -0400 Subject: [PATCH 265/846] Prevent being YAML'd in the future --- .github/workflows/smoke.yml | 8 ++++---- .github/workflows/tests.yml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index a0fa94fe5..2c360ce55 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -22,7 +22,7 @@ jobs: - name: Python uses: actions/setup-python@v5 with: - python-version: 3.12 + python-version: "3.12" - name: Packages run: pip install -r tests/requirements.txt @@ -37,10 +37,10 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-latest, python: 3.9, ffmpeg: "6.1", extras: true} - - {os: ubuntu-latest, python: 3.9, ffmpeg: "6.0"} + - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.1", extras: true} + - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.0"} - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1"} - - {os: macos-12, python: 3.9, ffmpeg: "6.1"} + - {os: macos-12, python: "3.9", ffmpeg: "6.1"} env: PYAV_PYTHON: python${{ matrix.config.python }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 17c9a687b..564a1c922 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: "3.9" - name: Build source package run: | pip install cython @@ -44,7 +44,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: "3.9" - name: Set up QEMU if: matrix.os == 'ubuntu-latest' uses: docker/setup-qemu-action@v3 From 471fc13dc5ed8c6a42ed8aa074144e53749282ca Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 9 May 2024 01:47:23 -0400 Subject: [PATCH 266/846] Subtitles: make text bytes, type hints --- av/packet.pyi | 3 +-- av/subtitles/codeccontext.pyx | 13 +++++++------ av/subtitles/subtitle.pxd | 5 ----- av/subtitles/subtitle.pyi | 3 ++- av/subtitles/subtitle.pyx | 4 +++- 5 files changed, 13 insertions(+), 15 deletions(-) diff --git a/av/packet.pyi b/av/packet.pyi index 14b4b0bce..24cb9f5a0 100644 --- a/av/packet.pyi +++ b/av/packet.pyi @@ -1,6 +1,5 @@ from collections.abc import Buffer from fractions import Fraction -from typing import Iterator from av.subtitles.subtitle import SubtitleSet @@ -22,5 +21,5 @@ class Packet(Buffer): is_disposable: bool def __init__(self, input: int | bytes | None = None) -> None: ... - def decode(self) -> Iterator[SubtitleSet]: ... + def decode(self) -> list[SubtitleSet]: ... def __buffer__(self, arg1) -> memoryview: ... diff --git a/av/subtitles/codeccontext.pyx b/av/subtitles/codeccontext.pyx index 227add919..10e8a6009 100644 --- a/av/subtitles/codeccontext.pyx +++ b/av/subtitles/codeccontext.pyx @@ -8,13 +8,14 @@ from av.subtitles.subtitle cimport SubtitleProxy, SubtitleSet cdef class SubtitleCodecContext(CodecContext): cdef _send_packet_and_recv(self, Packet packet): cdef SubtitleProxy proxy = SubtitleProxy() - cdef int got_frame = 0 - err_check(lib.avcodec_decode_subtitle2( - self.ptr, - &proxy.struct, - &got_frame, - packet.ptr if packet else NULL)) + + err_check( + lib.avcodec_decode_subtitle2( + self.ptr, &proxy.struct, &got_frame, packet.ptr if packet else NULL + ) + ) + if got_frame: return [SubtitleSet(proxy)] else: diff --git a/av/subtitles/subtitle.pxd b/av/subtitles/subtitle.pxd index e9003ab9b..29ddc4220 100644 --- a/av/subtitles/subtitle.pxd +++ b/av/subtitles/subtitle.pxd @@ -4,19 +4,16 @@ from av.packet cimport Packet cdef class SubtitleProxy: - cdef lib.AVSubtitle struct cdef class SubtitleSet: - cdef readonly Packet packet cdef SubtitleProxy proxy cdef readonly tuple rects cdef class Subtitle: - cdef SubtitleProxy proxy cdef lib.AVSubtitleRect *ptr cdef readonly bytes type @@ -28,11 +25,9 @@ cdef class ASSSubtitle(Subtitle): pass cdef class BitmapSubtitle(Subtitle): - cdef readonly planes cdef class BitmapSubtitlePlane: - cdef readonly BitmapSubtitle subtitle cdef readonly int index cdef readonly long buffer_size diff --git a/av/subtitles/subtitle.pyi b/av/subtitles/subtitle.pyi index 6dcd16e8d..cdbe7fb31 100644 --- a/av/subtitles/subtitle.pyi +++ b/av/subtitles/subtitle.pyi @@ -5,6 +5,7 @@ class SubtitleSet: start_display_time: int end_display_time: int pts: int + rects: tuple[Subtitle] def __len__(self) -> int: ... def __iter__(self) -> Iterator[Subtitle]: ... @@ -29,7 +30,7 @@ class BitmapSubtitlePlane: class TextSubtitle(Subtitle): type: Literal[b"text"] - text: str + text: bytes class AssSubtitle(Subtitle): type: Literal[b"ass"] diff --git a/av/subtitles/subtitle.pyx b/av/subtitles/subtitle.pyx index d8d45e3fc..e1deca7b7 100644 --- a/av/subtitles/subtitle.pyx +++ b/av/subtitles/subtitle.pyx @@ -145,7 +145,9 @@ cdef class TextSubtitle(Subtitle): @property def text(self): - return self.ptr.text + if self.ptr.text is not NULL: + return PyBytes_FromString(self.ptr.text) + return b"" cdef class AssSubtitle(Subtitle): From f42cc8852a67cbdbae3f7775f94fb55a3687cca9 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 9 May 2024 04:22:40 -0400 Subject: [PATCH 267/846] Allow packet.duration to be writable --- av/packet.pyx | 7 +++++++ tests/test_packet.py | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/av/packet.pyx b/av/packet.pyx index 24fd5581b..112421090 100644 --- a/av/packet.pyx +++ b/av/packet.pyx @@ -177,6 +177,13 @@ cdef class Packet(Buffer): if self.ptr.duration != lib.AV_NOPTS_VALUE: return self.ptr.duration + @duration.setter + def duration(self, v): + if v is None: + self.ptr.duration = lib.AV_NOPTS_VALUE + else: + self.ptr.duration = v + @property def is_keyframe(self): return bool(self.ptr.flags & lib.AV_PKT_FLAG_KEY) diff --git a/tests/test_packet.py b/tests/test_packet.py index 8a2b6266c..cb0909116 100644 --- a/tests/test_packet.py +++ b/tests/test_packet.py @@ -39,3 +39,11 @@ def test_is_disposable(self): self.assertTrue(packet.is_disposable) else: self.assertFalse(packet.is_disposable) + + def test_set_duration(self): + with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: + for packet in container.demux(): + old_duration = packet.duration + packet.duration += 10 + + self.assertEqual(packet.duration, old_duration + 10) From b8dc8b98912f3a29a00fa4c00ab23a6a0cdaf624 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 14 May 2024 02:34:21 -0400 Subject: [PATCH 268/846] Fix #1378 Use 0 (unknown color range) if the given a None value instead of copying the input frame. --- av/video/reformatter.pyx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/av/video/reformatter.pyx b/av/video/reformatter.pyx index f41094eda..11ee1bf74 100644 --- a/av/video/reformatter.pyx +++ b/av/video/reformatter.pyx @@ -87,8 +87,8 @@ cdef class VideoReformatter: cdef int c_src_colorspace = (Colorspace[src_colorspace].value if src_colorspace is not None else frame.colorspace) cdef int c_dst_colorspace = (Colorspace[dst_colorspace].value if dst_colorspace is not None else frame.colorspace) cdef int c_interpolation = (Interpolation[interpolation] if interpolation is not None else Interpolation.BILINEAR).value - cdef int c_src_color_range = (ColorRange[src_color_range].value if src_color_range is not None else frame.color_range) - cdef int c_dst_color_range = (ColorRange[dst_color_range].value if dst_color_range is not None else frame.color_range) + cdef int c_src_color_range = (ColorRange[src_color_range].value if src_color_range is not None else 0) + cdef int c_dst_color_range = (ColorRange[dst_color_range].value if dst_color_range is not None else 0) return self._reformat( frame, From ace1bfa7e513d1cefc5f1d4ca5ac69a22675ca2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Tue, 28 May 2024 07:48:33 +0200 Subject: [PATCH 269/846] Update binary FFmpeg build This adds support for `webp` and removes X11 libraries which were dragged into the macOs builds. --- scripts/ffmpeg-6.1.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ffmpeg-6.1.json b/scripts/ffmpeg-6.1.json index d5b84be19..49ce58d17 100644 --- a/scripts/ffmpeg-6.1.json +++ b/scripts/ffmpeg-6.1.json @@ -1,3 +1,3 @@ { - "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/6.1.1-1/ffmpeg-{platform}.tar.gz"] + "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/6.1.1-3/ffmpeg-{platform}.tar.gz"] } From 3d0299f7022a032ae094322db431fb4bcc94244d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Tue, 28 May 2024 07:52:13 +0200 Subject: [PATCH 270/846] Restore support for Python 3.8 This reverts 5707fb605cbd132e03c60e1a9e9463fd747ea93c. --- .github/workflows/smoke.yml | 10 +++++----- .github/workflows/tests.yml | 4 ++-- setup.py | 3 ++- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 2c360ce55..39cf096cd 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -37,10 +37,10 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.1", extras: true} - - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.0"} + - {os: ubuntu-latest, python: "3.8", ffmpeg: "6.1", extras: true} + - {os: ubuntu-latest, python: "3.8", ffmpeg: "6.0"} - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1"} - - {os: macos-12, python: "3.9", ffmpeg: "6.1"} + - {os: macos-12, python: "3.8", ffmpeg: "6.1"} env: PYAV_PYTHON: python${{ matrix.config.python }} @@ -116,8 +116,8 @@ jobs: fail-fast: false matrix: config: - - {os: windows-latest, python: 3.9, ffmpeg: "6.1"} - - {os: windows-latest, python: 3.9, ffmpeg: "6.0"} + - {os: windows-latest, python: "3.8", ffmpeg: "6.1"} + - {os: windows-latest, python: "3.8", ffmpeg: "6.0"} steps: - name: Checkout diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 564a1c922..fcafb8d82 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.9" + python-version: "3.8" - name: Build source package run: | pip install cython @@ -44,7 +44,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.9" + python-version: "3.8" - name: Set up QEMU if: matrix.os == 'ubuntu-latest' uses: docker/setup-qemu-action@v3 diff --git a/setup.py b/setup.py index 8cfcbaa58..cdedbcfb6 100644 --- a/setup.py +++ b/setup.py @@ -205,7 +205,7 @@ def parse_cflags(raw_flags): url="https://github.com/PyAV-Org/PyAV", packages=find_packages(exclude=["build*", "examples*", "scratchpad*", "tests*"]), package_data=package_data, - python_requires=">=3.9", + python_requires=">=3.8", zip_safe=False, ext_modules=ext_modules, test_suite="tests", @@ -222,6 +222,7 @@ def parse_cflags(raw_flags): "Operating System :: Unix", "Operating System :: Microsoft :: Windows", "Programming Language :: Cython", + "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", From 5e23c2adc445b2b22c8c2137bcb1a55cabf5bf56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Mon, 27 May 2024 23:48:15 +0200 Subject: [PATCH 271/846] Update GitHub actions to their latest version The behaviour of `actions/upload-artifact` have changed, we now need distinct names for each artifact. This also means adjusting our use of `actions/download-artifact` to collect all artifacts. --- .github/workflows/tests.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fcafb8d82..14bc87525 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,9 +17,9 @@ jobs: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-6.1.json /tmp/vendor PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig python setup.py sdist - name: Upload source package - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: dist + name: dist-source path: dist/ package-wheel: @@ -73,9 +73,9 @@ jobs: cibuildwheel --output-dir dist shell: bash - name: Upload wheels - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: dist + name: dist-${{ matrix.os }}-${{ matrix.arch }} path: dist/ publish: @@ -83,9 +83,9 @@ jobs: needs: [package-source, package-wheel] steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: - name: dist + merge-multiple: true path: dist/ - name: Publish to PyPI if: github.event_name == 'release' && github.event.action == 'published' From e4a25fdac8c5cc155a3ffe3df75585c9deb70dd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Tue, 28 May 2024 07:57:52 +0200 Subject: [PATCH 272/846] Pin macOs to version 14 for arm64 Using `macos-latest` has bitten us in the past, so pin the version of macOs used to build wheels. --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 14bc87525..b66ef500b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -28,7 +28,7 @@ jobs: fail-fast: false matrix: include: - - os: macos-latest + - os: macos-14 arch: arm64 - os: macos-13 arch: x86_64 From f2d1f37991067aedd90bc979ebcca83f136cb3a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Wed, 29 May 2024 23:58:58 +0200 Subject: [PATCH 273/846] Restore tests against FFmpeg 5.x This reverts e884e6cd1dac2646fd002b441ca4fdfb2ef818c5. So far we have not made any changes which justify dropping FFmpeg 5.x, and we do not change the supported platforms in a minor release. We will drop compatibility for FFmpeg 5.x once we switch to the new audio channel API. --- .github/workflows/smoke.yml | 4 ++++ docs/overview/installation.rst | 2 +- scripts/ffmpeg-5.0.json | 3 +++ scripts/ffmpeg-5.1.json | 3 +++ 4 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 scripts/ffmpeg-5.0.json create mode 100644 scripts/ffmpeg-5.1.json diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 39cf096cd..7694fbfdb 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -39,6 +39,8 @@ jobs: config: - {os: ubuntu-latest, python: "3.8", ffmpeg: "6.1", extras: true} - {os: ubuntu-latest, python: "3.8", ffmpeg: "6.0"} + - {os: ubuntu-latest, python: "3.8", ffmpeg: "5.1"} + - {os: ubuntu-latest, python: "3.8", ffmpeg: "5.0"} - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1"} - {os: macos-12, python: "3.8", ffmpeg: "6.1"} @@ -118,6 +120,8 @@ jobs: config: - {os: windows-latest, python: "3.8", ffmpeg: "6.1"} - {os: windows-latest, python: "3.8", ffmpeg: "6.0"} + - {os: windows-latest, python: "3.8", ffmpeg: "5.1"} + - {os: windows-latest, python: "3.8", ffmpeg: "5.0"} steps: - name: Checkout diff --git a/docs/overview/installation.rst b/docs/overview/installation.rst index 4fdeb38a5..c43504d7a 100644 --- a/docs/overview/installation.rst +++ b/docs/overview/installation.rst @@ -55,7 +55,7 @@ See the `Conda quick install `_ docs t Bring your own FFmpeg --------------------- -PyAV can also be compiled against your own build of FFmpeg ((version ``6.0`` or higher). You can force installing PyAV from source by running: +PyAV can also be compiled against your own build of FFmpeg ((version ``5.0`` or higher). You can force installing PyAV from source by running: .. code-block:: bash diff --git a/scripts/ffmpeg-5.0.json b/scripts/ffmpeg-5.0.json new file mode 100644 index 000000000..41969df2c --- /dev/null +++ b/scripts/ffmpeg-5.0.json @@ -0,0 +1,3 @@ +{ + "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.0.1-1/ffmpeg-{platform}.tar.gz"] +} diff --git a/scripts/ffmpeg-5.1.json b/scripts/ffmpeg-5.1.json new file mode 100644 index 000000000..75e4b8eca --- /dev/null +++ b/scripts/ffmpeg-5.1.json @@ -0,0 +1,3 @@ +{ + "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.1.3-1/ffmpeg-{platform}.tar.gz"] +} From 873f17975834aef62c682c955f150650f0237071 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 29 May 2024 20:39:51 -0400 Subject: [PATCH 274/846] Release v12.1.0 --- AUTHORS.py | 3 + AUTHORS.rst | 158 +++++++++++++++++++++++++------------------------- CHANGELOG.rst | 22 +++++++ av/about.py | 2 +- 4 files changed, 105 insertions(+), 80 deletions(-) diff --git a/AUTHORS.py b/AUTHORS.py index eb7ac721c..fd47a713d 100644 --- a/AUTHORS.py +++ b/AUTHORS.py @@ -26,6 +26,7 @@ 'tcaswell@bnl.gov': 'tcaswell@gmail.com', 'xxr3376@gmail.com': 'xxr@megvii.com', 'dallan@pha.jhu.edu': 'daniel.b.allan@gmail.com', + '61652821+laggykiller@users.noreply.github.com': 'chaudominic2@gmail.com', } @@ -55,6 +56,8 @@ 'vidartf@gmail.com': 'vidartf', 'willpatera@gmail.com': 'willpatera', 'xxr@megvii.com': 'xxr3376', + 'chaudominic2@gmail.com': 'laggykiller', + 'wyattblue@auto-editor.com': 'WyattBlue', } diff --git a/AUTHORS.rst b/AUTHORS.rst index 87f2492ad..68595a7da 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -7,86 +7,86 @@ All contributors (by number of commits): * Jeremy Lainé ; `@jlaine `_ +- WyattBlue ; `@WyattBlue `_ - Mark Reid ; `@markreidvfx `_ -* WyattBlue +* Vidar Tonaas Fauske ; `@vidartf `_ +* laggykiller ; `@laggykiller `_ +* Billy Shambrook ; `@billyshambrook `_ +* Casper van der Wel +* Philip de Nier +* Tadas Dailyda +* JoeUgly <41972063+JoeUgly@users.noreply.github.com> +* Justin Wong <46082645+uvjustin@users.noreply.github.com> -- Vidar Tonaas Fauske ; `@vidartf `_ -- Billy Shambrook ; `@billyshambrook `_ -- Casper van der Wel -- Philip de Nier -- Tadas Dailyda -- laggykiller <61652821+laggykiller@users.noreply.github.com> -- JoeUgly <41972063+JoeUgly@users.noreply.github.com> -- Justin Wong <46082645+uvjustin@users.noreply.github.com> +- Alba Mendez +- Mark Harfouche +- Xinran Xu ; `@xxr3376 `_ +- Dan Allan ; `@danielballan `_ +- Dave Johansen +- Christoph Rackwitz +- Alireza Davoudi ; `@adavoudi `_ +- Jonathan Drolet +- Moritz Kassner ; `@mkassner `_ +- Santtu Keskinen +- Thomas A Caswell ; `@tacaswell `_ +- Ulrik Mikaelsson ; `@rawler `_ +- Wel C. van der +- Will Patera ; `@willpatera `_ -* Alba Mendez -* Mark Harfouche -* Xinran Xu ; `@xxr3376 `_ -* laggykiller -* Dan Allan ; `@danielballan `_ -* Dave Johansen -* Christoph Rackwitz -* Alireza Davoudi ; `@adavoudi `_ -* Jonathan Drolet -* Moritz Kassner ; `@mkassner `_ -* Thomas A Caswell ; `@tacaswell `_ -* Ulrik Mikaelsson ; `@rawler `_ -* Wel C. van der -* Will Patera ; `@willpatera `_ - -- rutsh -- Felix Vollmer -- Santiago Castro -- Christian Clauss -- Ihor Liubymov -- Johannes Erdfelt -- Karl Litterfeldt ; `@litterfeldt `_ -- Martin Larralde -- Simon-Martin Schröder -- mephi42 -- Miles Kaufmann -- Pablo Prietz -- Radek Senfeld ; `@radek-senfeld `_ -- Benjamin Chrétien <2742231+bchretien@users.noreply.github.com> -- Marc Mueller <30130371+cdce8p@users.noreply.github.com> -- zzjjbb <31069326+zzjjbb@users.noreply.github.com> -- Hanz <40712686+HanzCEO@users.noreply.github.com> -- Joe Schiff <41972063+JoeSchiff@users.noreply.github.com> -- Ian Lee -- Ryan Huang -- Arthur Barros -- Carlos Ruiz -- David Plowman -- Maxime Desroches -- egao1980 -- Eric Kalosa-Kenyon -- Gemfield -- Jonathan Martin -- Johan Jeppsson Karlin -- Philipp Klaus -- Mattias Wadman -- Manuel Goacolou -- Julian Schweizer -- Ömer Sezgin Uğurlu -- Orivej Desh -- Philipp Krähenbühl -- ramoncaldeira -- Roland van Laar -- Santiago Castro -- Kengo Sawatsu -- FirefoxMetzger -- hyenal -- Brendan Long ; `@brendanlong `_ -- Семён Марьясин -- Stephen.Y -- Tom Flanagan -- Tim O'Shea -- Tim Ahpee -- Jonas Tingeborn -- Pino Toscano -- Ulrik Mikaelsson -- Vasiliy Kotov -- Koichi Akabe -- David Joy -- Sviatoslav Sydorenko (Святослав Сидоренко) +* rutsh +* Felix Vollmer +* Santiago Castro +* Christian Clauss +* Ihor Liubymov +* Johannes Erdfelt +* Karl Litterfeldt ; `@litterfeldt `_ +* Martin Larralde +* Simon-Martin Schröder +* mephi42 +* Miles Kaufmann +* Pablo Prietz +* Radek Senfeld ; `@radek-senfeld `_ +* Benjamin Chrétien <2742231+bchretien@users.noreply.github.com> +* Marc Mueller <30130371+cdce8p@users.noreply.github.com> +* zzjjbb <31069326+zzjjbb@users.noreply.github.com> +* Hanz <40712686+HanzCEO@users.noreply.github.com> +* Joe Schiff <41972063+JoeSchiff@users.noreply.github.com> +* Artturin +* Ian Lee +* Ryan Huang +* Arthur Barros +* Carlos Ruiz +* David Plowman +* Maxime Desroches +* egao1980 +* Eric Kalosa-Kenyon +* Gemfield +* Jonathan Martin +* Johan Jeppsson Karlin +* Philipp Klaus +* Mattias Wadman +* Manuel Goacolou +* Julian Schweizer +* Ömer Sezgin Uğurlu +* Orivej Desh +* Philipp Krähenbühl +* ramoncaldeira +* Roland van Laar +* Santiago Castro +* Kengo Sawatsu +* FirefoxMetzger +* hyenal +* Brendan Long ; `@brendanlong `_ +* Семён Марьясин +* Stephen.Y +* Tom Flanagan +* Tim O'Shea +* Tim Ahpee +* Jonas Tingeborn +* Pino Toscano +* Ulrik Mikaelsson +* Vasiliy Kotov +* Koichi Akabe +* David Joy +* Sviatoslav Sydorenko (Святослав Сидоренко) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 72aa0897b..526ce98a6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,28 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. + +v12.1.0 +------- + +Features: + +- Build binary wheels with webp support. +- Allow disabling logs, disable logs by default. +- Add bitstream filters by @skeskinen in (:issue:`1375`) (:issue:`1379`). +- Expose CodecContext flush_buffers by @skeskinen in (:issue:`1382`). + +Fixes: + +- Fix type stubs, add missing type stubs. +- Add S12M_TIMECODE by @WyattBlue in (:issue:`1381`). +- Subtitle.text now returns bytes by @WyattBlue in (:issue:`13981). +- Allow packet.duration to be writable by @WyattBlue in (:issue:`1399`). +- Remove deprecated `VideoStream.frame_rate` by @WyattBlue in (:issue:`1351`). +- Build with Arm for PyPy now by @WyattBlue in (:issue:`1395`). +- Fix #1378 by @WyattBlue in (:issue:`1400`). +- setup.py: use PKG_CONFIG env var to get the pkg-config to use by @Artturin in (:issue:`1387`). + v12.0.0 ------- diff --git a/av/about.py b/av/about.py index 5170f5963..13b4a72f4 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "12.1.0rc1" +__version__ = "12.1.0" From c9e4f3eda5b25bb620e526cfb30139994fac6904 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 30 May 2024 01:56:04 -0400 Subject: [PATCH 275/846] Bump to next dev version --- av/about.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/about.py b/av/about.py index 13b4a72f4..79a759ae1 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "12.1.0" +__version__ = "13.0.0rc1" From 80f1dd7103eaf5afa00b6abff1d003b845779b5e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 30 May 2024 02:09:41 -0400 Subject: [PATCH 276/846] Drop support for python3.8 --- .github/workflows/smoke.yml | 18 +++++++++--------- .github/workflows/tests.yml | 4 ++-- setup.py | 3 +-- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 7694fbfdb..5a23ce156 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -37,12 +37,12 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-latest, python: "3.8", ffmpeg: "6.1", extras: true} - - {os: ubuntu-latest, python: "3.8", ffmpeg: "6.0"} - - {os: ubuntu-latest, python: "3.8", ffmpeg: "5.1"} - - {os: ubuntu-latest, python: "3.8", ffmpeg: "5.0"} + - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.1", extras: true} + - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.0"} + - {os: ubuntu-latest, python: "3.9", ffmpeg: "5.1"} + - {os: ubuntu-latest, python: "3.9", ffmpeg: "5.0"} - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1"} - - {os: macos-12, python: "3.8", ffmpeg: "6.1"} + - {os: macos-12, python: "3.9", ffmpeg: "6.1"} env: PYAV_PYTHON: python${{ matrix.config.python }} @@ -118,10 +118,10 @@ jobs: fail-fast: false matrix: config: - - {os: windows-latest, python: "3.8", ffmpeg: "6.1"} - - {os: windows-latest, python: "3.8", ffmpeg: "6.0"} - - {os: windows-latest, python: "3.8", ffmpeg: "5.1"} - - {os: windows-latest, python: "3.8", ffmpeg: "5.0"} + - {os: windows-latest, python: "3.9", ffmpeg: "6.1"} + - {os: windows-latest, python: "3.9", ffmpeg: "6.0"} + - {os: windows-latest, python: "3.9", ffmpeg: "5.1"} + - {os: windows-latest, python: "3.9", ffmpeg: "5.0"} steps: - name: Checkout diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b66ef500b..550065a84 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.8" + python-version: "3.9" - name: Build source package run: | pip install cython @@ -44,7 +44,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.8" + python-version: "3.9" - name: Set up QEMU if: matrix.os == 'ubuntu-latest' uses: docker/setup-qemu-action@v3 diff --git a/setup.py b/setup.py index cdedbcfb6..8cfcbaa58 100644 --- a/setup.py +++ b/setup.py @@ -205,7 +205,7 @@ def parse_cflags(raw_flags): url="https://github.com/PyAV-Org/PyAV", packages=find_packages(exclude=["build*", "examples*", "scratchpad*", "tests*"]), package_data=package_data, - python_requires=">=3.8", + python_requires=">=3.9", zip_safe=False, ext_modules=ext_modules, test_suite="tests", @@ -222,7 +222,6 @@ def parse_cflags(raw_flags): "Operating System :: Unix", "Operating System :: Microsoft :: Windows", "Programming Language :: Cython", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", From 77e86c2c666b61b70165ce306077d9c36c9821e3 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 31 May 2024 05:32:58 -0400 Subject: [PATCH 277/846] Fix for arch/gcc14 --- av/bitstream.pxd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/bitstream.pxd b/av/bitstream.pxd index 620fa0366..dbb89c984 100644 --- a/av/bitstream.pxd +++ b/av/bitstream.pxd @@ -5,7 +5,7 @@ from av.packet cimport Packet cdef class BitStreamFilterContext: - cdef const lib.AVBSFContext *ptr + cdef lib.AVBSFContext *ptr cpdef filter(self, Packet packet=?) cpdef flush(self) From deb1cfc31f86ef1cd62f514b067d1889a458f7df Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 3 Jun 2024 02:55:12 -0400 Subject: [PATCH 278/846] Drop ffmpeg 5 --- .github/workflows/smoke.yml | 4 ---- docs/overview/installation.rst | 2 +- scripts/ffmpeg-5.0.json | 3 --- scripts/ffmpeg-5.1.json | 3 --- 4 files changed, 1 insertion(+), 11 deletions(-) delete mode 100644 scripts/ffmpeg-5.0.json delete mode 100644 scripts/ffmpeg-5.1.json diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 5a23ce156..1eda45934 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -39,8 +39,6 @@ jobs: config: - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.1", extras: true} - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.0"} - - {os: ubuntu-latest, python: "3.9", ffmpeg: "5.1"} - - {os: ubuntu-latest, python: "3.9", ffmpeg: "5.0"} - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1"} - {os: macos-12, python: "3.9", ffmpeg: "6.1"} @@ -120,8 +118,6 @@ jobs: config: - {os: windows-latest, python: "3.9", ffmpeg: "6.1"} - {os: windows-latest, python: "3.9", ffmpeg: "6.0"} - - {os: windows-latest, python: "3.9", ffmpeg: "5.1"} - - {os: windows-latest, python: "3.9", ffmpeg: "5.0"} steps: - name: Checkout diff --git a/docs/overview/installation.rst b/docs/overview/installation.rst index c43504d7a..1e57c8bef 100644 --- a/docs/overview/installation.rst +++ b/docs/overview/installation.rst @@ -55,7 +55,7 @@ See the `Conda quick install `_ docs t Bring your own FFmpeg --------------------- -PyAV can also be compiled against your own build of FFmpeg ((version ``5.0`` or higher). You can force installing PyAV from source by running: +PyAV can also be compiled against your own build of FFmpeg (version ``6.0`` or higher). You can force installing PyAV from source by running: .. code-block:: bash diff --git a/scripts/ffmpeg-5.0.json b/scripts/ffmpeg-5.0.json deleted file mode 100644 index 41969df2c..000000000 --- a/scripts/ffmpeg-5.0.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.0.1-1/ffmpeg-{platform}.tar.gz"] -} diff --git a/scripts/ffmpeg-5.1.json b/scripts/ffmpeg-5.1.json deleted file mode 100644 index 75e4b8eca..000000000 --- a/scripts/ffmpeg-5.1.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.1.3-1/ffmpeg-{platform}.tar.gz"] -} From 723dd775c98d26a957d43e225aae09214b73a719 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 4 Jun 2024 03:00:10 -0400 Subject: [PATCH 279/846] long doesn't exist anymore --- av/packet.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/packet.pyx b/av/packet.pyx index 112421090..63b0b50ab 100644 --- a/av/packet.pyx +++ b/av/packet.pyx @@ -25,7 +25,7 @@ cdef class Packet(Buffer): if input is None: return - if isinstance(input, (int, long)): + if isinstance(input, int): size = input else: source = bytesource(input) From e09067bb4f92f3e71a7d58ef2a1ec32535b4ee76 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 4 Jun 2024 03:02:34 -0400 Subject: [PATCH 280/846] Clean up time docs --- docs/api/time.rst | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/api/time.rst b/docs/api/time.rst index fd65de1a2..a063e876e 100644 --- a/docs/api/time.rst +++ b/docs/api/time.rst @@ -57,7 +57,7 @@ In many cases a stream has a time base of ``1 / frame_rate``, and then its frame 1 -For convenince, :attr:`.Frame.time` is a ``float`` in seconds: +For convenience, :attr:`.Frame.time` is a ``float`` in seconds: .. doctest:: @@ -65,10 +65,10 @@ For convenince, :attr:`.Frame.time` is a ``float`` in seconds: 0.04 -FFMpeg Internals +FFmpeg Internals ---------------- -.. note:: Time in FFmpeg is not 100% clear to us (see :ref:`authority_of_docs`). At times the FFmpeg documentation and canonical seeming posts in the forums appear contradictory. We've experiemented with it, and what follows is the picture that we are operating under. +.. note:: Time in FFmpeg is not 100% clear to us (see :ref:`authority_of_docs`). At times the FFmpeg documentation and canonical seeming posts in the forums appear contradictory. We've experimented with it, and what follows is the picture that we are operating under. Both :ffmpeg:`AVStream` and :ffmpeg:`AVCodecContext` have a ``time_base`` member. However, they are used for different purposes, and (this author finds) it is too easy to abstract the concept too far. @@ -82,9 +82,6 @@ For encoding, you (the PyAV developer / FFmpeg "user") must set :ffmpeg:`AVCodec You then prepare :ffmpeg:`AVFrame.pts` in :ffmpeg:`AVCodecContext.time_base`. The encoded :ffmpeg:`AVPacket.pts` is simply copied from the frame by the library, and so is still in the codec's time base. You must rescale it to :ffmpeg:`AVStream.time_base` before muxing (as all stream operations assume the packet time is in stream time base). -For fixed-fps content your frames' ``pts`` would be the frame or sample index (for video and audio, respectively). PyAV should attempt to do this. - - Decoding ........ From b474fcacbaaf3c4e8bf28e8aea56893f78e13eb6 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 4 Jun 2024 03:15:34 -0400 Subject: [PATCH 281/846] Change notice about logging --- docs/overview/caveats.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/overview/caveats.rst b/docs/overview/caveats.rst index 43b764e48..9e1f69810 100644 --- a/docs/overview/caveats.rst +++ b/docs/overview/caveats.rst @@ -30,7 +30,7 @@ The two main features that are able to cause lockups are: 1. Python IO (passing a file-like object to ``av.open``). While this is in theory possible, so far it seems like the callbacks are made in the calling thread, and so are safe. -2. Logging. As soon as you en/decode with threads you are highly likely to get log messages issues from threads started by FFmpeg, and you will get lockups. See :ref:`disable_logging`. +2. Logging. If you have logging enabled (disabled by default), those log messages could cause lockups when using threads. .. _garbage_collection: From 21d09212c9a2481d64663f865fb7e0a7c2f05ae9 Mon Sep 17 00:00:00 2001 From: Santtu Keskinen Date: Thu, 6 Jun 2024 23:56:41 +0300 Subject: [PATCH 282/846] Add flush_buffers to CodecContext docs --- docs/api/codec.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/api/codec.rst b/docs/api/codec.rst index ebc147c30..a3cc90e86 100644 --- a/docs/api/codec.rst +++ b/docs/api/codec.rst @@ -97,6 +97,7 @@ Transcoding .. automethod:: CodecContext.parse .. automethod:: CodecContext.encode .. automethod:: CodecContext.decode +.. automethod:: CodecContext.flush_buffers Flags From 5ab63db8c557cdd0bb48fd9a70e3ba6475954b70 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 12 Jun 2024 15:29:50 -0400 Subject: [PATCH 283/846] Format docs script --- docs/api/error_table.py | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/docs/api/error_table.py b/docs/api/error_table.py index e67b9f40b..3a1b5f219 100644 --- a/docs/api/error_table.py +++ b/docs/api/error_table.py @@ -1,37 +1,25 @@ - import av -rows = [( - #'Tag (Code)', - 'Exception Class', - 'Code/Enum Name', - 'FFmpeg Error Message', -)] +rows = [("Exception Class", "Code/Enum Name", "FFmpeg Error Message")] for code, cls in av.error.classes.items(): - enum = av.error.ErrorType.get(code) - + if not enum: continue - if enum.tag == b'PyAV': + if enum.tag == b"PyAV": continue - rows.append(( - #'{} ({})'.format(enum.tag, code), - '``av.{}``'.format(cls.__name__), - '``av.error.{}``'.format(enum.name), - enum.strerror, - )) + rows.append((f"``av.{cls.__name__}``", f"``av.error.{enum.name}``", enum.strerror)) lens = [max(len(row[i]) for row in rows) for i in range(len(rows[0]))] -header = tuple('=' * x for x in lens) +header = tuple("=" * x for x in lens) rows.insert(0, header) rows.insert(2, header) rows.append(header) for row in rows: - print(' '.join('{:{}s}'.format(cell, len_) for cell, len_ in zip(row, lens))) + print(" ".join("{:{}s}".format(cell, len_) for cell, len_ in zip(row, lens))) From 3646f8d39206cc8b72ca54cc841077b66bde8c5a Mon Sep 17 00:00:00 2001 From: Moonsik Park Date: Mon, 17 Jun 2024 18:03:22 +0900 Subject: [PATCH 284/846] Use `av_guess_sample_aspect_ratio` when reporting stream SAR, DAR --- av/video/stream.pyi | 4 ++-- av/video/stream.pyx | 29 +++++++++++++++++++++++++++++ include/libavformat/avformat.pxd | 6 ++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/av/video/stream.pyi b/av/video/stream.pyi index 4b78efd06..929847ed4 100644 --- a/av/video/stream.pyi +++ b/av/video/stream.pyi @@ -14,6 +14,8 @@ class VideoStream(Stream): bit_rate_tolerance: int thread_count: int thread_type: Any + sample_aspect_ratio: Fraction | None + display_aspect_ratio: Fraction | None codec_context: VideoCodecContext # from codec context format: VideoFormat @@ -24,8 +26,6 @@ class VideoStream(Stream): framerate: Fraction rate: Fraction gop_size: int - sample_aspect_ratio: Fraction | None - display_aspect_ratio: Fraction | None has_b_frames: bool coded_width: int coded_height: int diff --git a/av/video/stream.pyx b/av/video/stream.pyx index ae57ed136..6ab685167 100644 --- a/av/video/stream.pyx +++ b/av/video/stream.pyx @@ -86,3 +86,32 @@ cdef class VideoStream(Stream): # The two NULL arguments aren't used in FFmpeg >= 4.0 cdef lib.AVRational val = lib.av_guess_frame_rate(NULL, self.ptr, NULL) return avrational_to_fraction(&val) + + @property + def sample_aspect_ratio(self): + """The guessed sample aspect ratio (SAR) of this stream. + + This is a wrapper around :ffmpeg:`av_guess_sample_aspect_ratio`, and uses multiple + heuristics to decide what is "the" sample aspect ratio. + + :type: :class:`~fractions.Fraction` or ``None`` + """ + cdef lib.AVRational sar = lib.av_guess_sample_aspect_ratio(self.container.ptr, self.ptr, NULL) + return avrational_to_fraction(&sar) + + @property + def display_aspect_ratio(self): + """The guessed display aspect ratio (DAR) of this stream. + + This is calculated from :meth:`.VideoStream.guessed_sample_aspect_ratio`. + + :type: :class:`~fractions.Fraction` or ``None`` + """ + cdef lib.AVRational dar + + lib.av_reduce( + &dar.num, &dar.den, + self.format.width * self.sample_aspect_ratio.num, + self.format.height * self.sample_aspect_ratio.den, 1024*1024) + + return avrational_to_fraction(&dar) diff --git a/include/libavformat/avformat.pxd b/include/libavformat/avformat.pxd index 224e76b4e..9d9061cc2 100644 --- a/include/libavformat/avformat.pxd +++ b/include/libavformat/avformat.pxd @@ -325,6 +325,12 @@ cdef extern from "libavformat/avformat.h" nogil: AVFrame *frame ) + cdef AVRational av_guess_sample_aspect_ratio( + AVFormatContext *ctx, + AVStream *stream, + AVFrame *frame + ) + cdef const AVInputFormat* av_demuxer_iterate(void **opaque) cdef const AVOutputFormat* av_muxer_iterate(void **opaque) From 66d94a4ab86f0467cc94c4bb750e9211c67bb37b Mon Sep 17 00:00:00 2001 From: Moonsik Park Date: Tue, 18 Jun 2024 12:40:34 +0900 Subject: [PATCH 285/846] Expose `av_frame_make_writable()` for `av.Frame` --- av/frame.pyi | 2 ++ av/frame.pyx | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/av/frame.pyi b/av/frame.pyi index 8e81a8198..47ed9f0cc 100644 --- a/av/frame.pyi +++ b/av/frame.pyi @@ -7,3 +7,5 @@ class Frame: time_base: Fraction is_corrupt: bool side_data: dict[str, str] + + def make_writable(self) -> None: ... diff --git a/av/frame.pyx b/av/frame.pyx index 1c8fb357a..489eef540 100644 --- a/av/frame.pyx +++ b/av/frame.pyx @@ -1,3 +1,4 @@ +from av.error cimport err_check from av.utils cimport avrational_to_fraction, to_avrational from av.sidedata.sidedata import SideDataContainer @@ -137,3 +138,13 @@ cdef class Frame: if self._side_data is None: self._side_data = SideDataContainer(self) return self._side_data + + def make_writable(self): + """ + Ensures that the frame data is writable. Copy the data to new buffer if it is not. + This is a wrapper around :ffmpeg:`av_frame_make_writable`. + """ + cdef int ret + + ret = lib.av_frame_make_writable(self.ptr) + err_check(ret) From 080d675dad4cd41de91bdb11aadce6823a44f368 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 18 Jun 2024 15:32:33 -0400 Subject: [PATCH 286/846] Type stub all error exceptions --- av/_core.pyi | 2 ++ av/error.pyi | 48 +++++++++++++++++++++++++++++++++++++++++++++++- av/error.pyx | 2 -- 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/av/_core.pyi b/av/_core.pyi index 4fbfe8525..26b3bc0c8 100644 --- a/av/_core.pyi +++ b/av/_core.pyi @@ -7,3 +7,5 @@ class _Meta(TypedDict): library_meta: dict[str, _Meta] library_versions: dict[str, tuple[int, int, int]] + +time_base: int diff --git a/av/error.pyi b/av/error.pyi index db6581238..527d8c00d 100644 --- a/av/error.pyi +++ b/av/error.pyi @@ -1,3 +1,5 @@ +import builtins + from .enum import EnumItem classes: dict[int, Exception] @@ -58,4 +60,48 @@ class LookupError(FFmpegError): ... class HTTPError(FFmpegError): ... class HTTPClientError(FFmpegError): ... class UndefinedError(FFmpegError): ... -class InvalidDataError(ValueError): ... +class InvalidDataError(FFmpegError, builtins.ValueError): ... +class BugError(FFmpegError, builtins.RuntimeError): ... +class BufferTooSmallError(FFmpegError, builtins.ValueError): ... +class BSFNotFoundError(LookupError): ... +class DecoderNotFoundError(LookupError): ... +class DemuxerNotFoundError(LookupError): ... +class EncoderNotFoundError(LookupError): ... +class ExitError(FFmpegError): ... +class ExternalError(FFmpegError): ... +class FilterNotFoundError(LookupError): ... +class MuxerNotFoundError(LookupError): ... +class OptionNotFoundError(LookupError): ... +class PatchWelcomeError(FFmpegError): ... +class ProtocolNotFoundError(LookupError): ... +class UnknownError(FFmpegError): ... +class ExperimentalError(FFmpegError): ... +class InputChangedError(FFmpegError): ... +class OutputChangedError(FFmpegError): ... +class HTTPBadRequestError(HTTPClientError): ... +class HTTPUnauthorizedError(HTTPClientError): ... +class HTTPForbiddenError(HTTPClientError): ... +class HTTPNotFoundError(HTTPClientError): ... +class HTTPOtherClientError(HTTPClientError): ... +class HTTPServerError(HTTPError): ... +class PyAVCallbackError(FFmpegError, builtins.RuntimeError): ... +class BrokenPipeError(FFmpegError, builtins.BrokenPipeError): ... +class ChildProcessError(FFmpegError, builtins.ChildProcessError): ... +class ConnectionAbortedError(FFmpegError, builtins.ConnectionAbortedError): ... +class ConnectionRefusedError(FFmpegError, builtins.ConnectionRefusedError): ... +class ConnectionResetError(FFmpegError, builtins.ConnectionResetError): ... +class BlockingIOError(FFmpegError, builtins.BlockingIOError): ... +class EOFError(FFmpegError, builtins.EOFError): ... +class FileExistsError(FFmpegError, builtins.FileExistsError): ... +class FileNotFoundError(FFmpegError, builtins.FileNotFoundError): ... +class InterruptedError(FFmpegError, builtins.InterruptedError): ... +class IsADirectoryError(FFmpegError, builtins.IsADirectoryError): ... +class MemoryError(FFmpegError, builtins.MemoryError): ... +class NotADirectoryError(FFmpegError, builtins.NotADirectoryError): ... +class NotImplementedError(FFmpegError, builtins.NotImplementedError): ... +class OverflowError(FFmpegError, builtins.OverflowError): ... +class OSError(FFmpegError, builtins.OSError): ... +class PermissionError(FFmpegError, builtins.PermissionError): ... +class ProcessLookupError(FFmpegError, builtins.ProcessLookupError): ... +class TimeoutError(FFmpegError, builtins.TimeoutError): ... +class ValueError(FFmpegError, builtins.ValueError): ... diff --git a/av/error.pyx b/av/error.pyx index 64cd70594..97fab0e41 100644 --- a/av/error.pyx +++ b/av/error.pyx @@ -250,8 +250,6 @@ _extend_builtin("ValueError", (errno.EINVAL, )) _extend_builtin("MemoryError", (errno.ENOMEM, )) _extend_builtin("NotImplementedError", (errno.ENOSYS, )) _extend_builtin("OverflowError", (errno.ERANGE, )) -if IOError is not OSError: - _extend_builtin("IOError", (errno.EIO, )) # The rest of them (for now) _extend_builtin("OSError", [code for code in errno.errorcode if code not in classes]) From 84d758dcbfa3a19c11463eb6528bac9f6458dbdb Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 21 Jun 2024 02:56:19 -0400 Subject: [PATCH 287/846] Add fifo stub, remove `__init__.pyi` --- av/__init__.pyi | 18 ------------------ av/audio/fifo.pyi | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 18 deletions(-) delete mode 100644 av/__init__.pyi create mode 100644 av/audio/fifo.pyi diff --git a/av/__init__.pyi b/av/__init__.pyi deleted file mode 100644 index 493c62012..000000000 --- a/av/__init__.pyi +++ /dev/null @@ -1,18 +0,0 @@ -from av import error, logging - -from .audio.format import * -from .audio.frame import * -from .audio.layout import * -from .audio.resampler import * -from .codec.codec import * -from .container.core import * -from .container.input import * -from .container.output import * -from .error import * -from .filter import * -from .format import * -from .packet import * -from .video.format import * -from .video.frame import * - -__version__: str diff --git a/av/audio/fifo.pyi b/av/audio/fifo.pyi new file mode 100644 index 000000000..aece9d381 --- /dev/null +++ b/av/audio/fifo.pyi @@ -0,0 +1,16 @@ +from .format import AudioFormat +from .frame import AudioFrame +from .layout import AudioLayout + +class AudioFifo: + def write(self, frame: AudioFrame) -> None: ... + def read(self, samples: int = 0, partial: bool = False) -> AudioFrame | None: ... + def read_many(self, samples: int, partial: bool = False) -> list[AudioFrame]: ... + @property + def format(self) -> AudioFormat: ... + @property + def layout(self) -> AudioLayout: ... + @property + def sample_rate(self) -> int: ... + @property + def samples(self) -> int: ... From 2c8135535c57154c3227c0bba783bf7bdec86a06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Fri, 21 Jun 2024 10:33:29 +0200 Subject: [PATCH 288/846] Fix `AudioLayout`'s "copy" constructor (fixes: #1434) There are several places where `AudioLayout` instances can be created from another instance, for example the `AudioCodecContext.layout` setter, but this variant of the constructor is broken. --- av/audio/layout.pyx | 2 +- tests/test_audiolayout.py | 9 +++++++-- tests/test_codec_context.py | 11 +++++++---- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/av/audio/layout.pyx b/av/audio/layout.pyx index 79b4f3388..76edd177d 100644 --- a/av/audio/layout.pyx +++ b/av/audio/layout.pyx @@ -76,7 +76,7 @@ cdef class AudioLayout: elif isinstance(layout, str): c_layout = lib.av_get_channel_layout(layout) elif isinstance(layout, AudioLayout): - c_layout = layout.layout + c_layout = (layout).layout else: raise TypeError("layout must be str or int") diff --git a/tests/test_audiolayout.py b/tests/test_audiolayout.py index 9cecac19a..2a163186b 100644 --- a/tests/test_audiolayout.py +++ b/tests/test_audiolayout.py @@ -4,14 +4,19 @@ class TestAudioLayout(TestCase): - def test_stereo_properties(self): + def test_stereo_from_str(self): layout = AudioLayout("stereo") self._test_stereo(layout) - def test_2channel_properties(self) -> None: + def test_stereo_from_int(self): layout = AudioLayout(2) self._test_stereo(layout) + def test_stereo_from_layout(self): + layout = AudioLayout("stereo") + layout2 = AudioLayout(layout) + self._test_stereo(layout2) + def test_channel_counts(self): self.assertRaises(ValueError, AudioLayout, -1) self.assertRaises(ValueError, AudioLayout, 9) diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 1a2b189cc..d30401f4e 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -4,7 +4,7 @@ from unittest import SkipTest import av -from av import AudioResampler, Codec, Packet +from av import AudioLayout, AudioResampler, Codec, Packet from av.codec.codec import UnknownCodecError from av.video.frame import PictureType @@ -393,6 +393,12 @@ def test_encoding_mp2(self): maxDiff = None def audio_encoding(self, codec_name): + self._audio_encoding(codec_name=codec_name, channel_layout="stereo") + self._audio_encoding( + codec_name=codec_name, channel_layout=AudioLayout("stereo") + ) + + def _audio_encoding(self, *, codec_name, channel_layout): try: codec = Codec(codec_name, "w") except UnknownCodecError: @@ -404,14 +410,12 @@ def audio_encoding(self, codec_name): sample_fmt = ctx.codec.audio_formats[-1].name sample_rate = 48000 - channel_layout = "stereo" channels = 2 ctx.time_base = Fraction(1) / sample_rate ctx.sample_rate = sample_rate ctx.format = sample_fmt ctx.layout = channel_layout - ctx.channels = channels ctx.open() @@ -549,7 +553,6 @@ def audio_encoding(self, codec_name): ctx.sample_rate = sample_rate ctx.format = sample_fmt ctx.layout = channel_layout - ctx.channels = channels ctx.open() result_samples = 0 From a1400ad2b09f72ee2a5286da77a97a2667686335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Fri, 21 Jun 2024 09:05:52 +0200 Subject: [PATCH 289/846] Add type definitions for `AudioFormat` constructor, fix docstrings The docstrings contained examples referring to erroneous class names instead of `AudioFormat`. --- av/audio/format.pyi | 2 ++ av/audio/format.pyx | 11 +++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/av/audio/format.pyi b/av/audio/format.pyi index bb4a1e603..5f7e322ed 100644 --- a/av/audio/format.pyi +++ b/av/audio/format.pyi @@ -7,3 +7,5 @@ class AudioFormat: planar: AudioFormat packed: AudioFormat container_name: str + + def __init__(self, name: str | AudioFormat) -> None: ... diff --git a/av/audio/format.pyx b/av/audio/format.pyx index 608610781..f86e7b604 100644 --- a/av/audio/format.pyx +++ b/av/audio/format.pyx @@ -45,7 +45,7 @@ cdef class AudioFormat: def name(self): """Canonical name of the sample format. - >>> SampleFormat('s16p').name + >>> AudioFormat('s16p').name 's16p' """ @@ -55,7 +55,7 @@ cdef class AudioFormat: def bytes(self): """Number of bytes per sample. - >>> SampleFormat('s16p').bytes + >>> AudioFormat('s16p').bytes 2 """ @@ -65,7 +65,7 @@ cdef class AudioFormat: def bits(self): """Number of bits per sample. - >>> SampleFormat('s16p').bits + >>> AudioFormat('s16p').bits 16 """ @@ -95,8 +95,7 @@ cdef class AudioFormat: Is itself when planar: - >>> from av import AudioFormat as Format - >>> fmt = Format('s16p') + >>> fmt = AudioFormat('s16p') >>> fmt.planar is fmt True @@ -111,7 +110,7 @@ cdef class AudioFormat: Is itself when packed: - >>> fmt = Format('s16') + >>> fmt = AudioFormat('s16') >>> fmt.packed is fmt True From 74fe3fd8feb17e3f4e8147b575547d07fdead7b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Fri, 21 Jun 2024 09:45:24 +0200 Subject: [PATCH 290/846] Add type definitions for `AudioCodecContext` encoding / decoding The `encode`, `encode_lazy` and `decode` methods were missing type definitions. --- av/audio/codeccontext.pyi | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/av/audio/codeccontext.pyi b/av/audio/codeccontext.pyi index ed478a906..ada12efcb 100644 --- a/av/audio/codeccontext.pyi +++ b/av/audio/codeccontext.pyi @@ -1,8 +1,10 @@ -from typing import Literal +from typing import Iterator, Literal from av.codec.context import CodecContext +from av.packet import Packet from .format import AudioFormat +from .frame import AudioFrame from .layout import AudioLayout class AudioCodecContext(CodecContext): @@ -14,3 +16,7 @@ class AudioCodecContext(CodecContext): layout: AudioLayout format: AudioFormat type: Literal["audio"] + + def encode(self, frame: AudioFrame | None = None) -> list[Packet]: ... + def encode_lazy(self, frame: AudioFrame | None = None) -> Iterator[Packet]: ... + def decode(self, packet: Packet | None = None) -> list[AudioFrame]: ... From 5c5e31bfbe408b4040524e339530ae43c59cde79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Fri, 21 Jun 2024 10:52:15 +0200 Subject: [PATCH 291/846] Fix `Buffer` subclass type definitions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It does not make sense to special-case the `Packet` class concerning buffer protocol support, we have multiple classes inhering our `Buffer` class: - `Packet` - `Plane` and its subclasses `AudioPlane` and `VideoPlane` - `SideData` We cannot use `collection.abc.Buffer`, yet as this is only available in Python 3.12 and up. We do however want to declare that `bytes(foo)` is available, so declare a `__bytes__` method in `Buffer`. --- av/buffer.pyi | 5 +++++ av/packet.pyi | 3 +-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/av/buffer.pyi b/av/buffer.pyi index 41db73e03..bc1090d1d 100644 --- a/av/buffer.pyi +++ b/av/buffer.pyi @@ -1,4 +1,9 @@ +# When Python 3.12 becomes our lowest supported version, we could make this +# class inherit `collections.abc.Buffer`. + class Buffer: buffer_size: int buffer_ptr: int def update(self, input: bytes) -> None: ... + def __buffer__(self, flags: int) -> memoryview: ... + def __bytes__(self) -> bytes: ... diff --git a/av/packet.pyi b/av/packet.pyi index 24cb9f5a0..f122c2886 100644 --- a/av/packet.pyi +++ b/av/packet.pyi @@ -1,8 +1,8 @@ -from collections.abc import Buffer from fractions import Fraction from av.subtitles.subtitle import SubtitleSet +from .buffer import Buffer from .stream import Stream class Packet(Buffer): @@ -22,4 +22,3 @@ class Packet(Buffer): def __init__(self, input: int | bytes | None = None) -> None: ... def decode(self) -> list[SubtitleSet]: ... - def __buffer__(self, arg1) -> memoryview: ... From 6369e37a1aab4e9e6671b06e909ed4f14d4a42ba Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 21 Jun 2024 18:00:36 -0400 Subject: [PATCH 292/846] Support Python 3.8 Do not remove until EOL date has passed --- .github/workflows/smoke.yml | 10 +++++----- .github/workflows/tests.yml | 4 ++-- setup.py | 3 ++- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 1eda45934..39cf096cd 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -37,10 +37,10 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.1", extras: true} - - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.0"} + - {os: ubuntu-latest, python: "3.8", ffmpeg: "6.1", extras: true} + - {os: ubuntu-latest, python: "3.8", ffmpeg: "6.0"} - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1"} - - {os: macos-12, python: "3.9", ffmpeg: "6.1"} + - {os: macos-12, python: "3.8", ffmpeg: "6.1"} env: PYAV_PYTHON: python${{ matrix.config.python }} @@ -116,8 +116,8 @@ jobs: fail-fast: false matrix: config: - - {os: windows-latest, python: "3.9", ffmpeg: "6.1"} - - {os: windows-latest, python: "3.9", ffmpeg: "6.0"} + - {os: windows-latest, python: "3.8", ffmpeg: "6.1"} + - {os: windows-latest, python: "3.8", ffmpeg: "6.0"} steps: - name: Checkout diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 550065a84..b66ef500b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.9" + python-version: "3.8" - name: Build source package run: | pip install cython @@ -44,7 +44,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.9" + python-version: "3.8" - name: Set up QEMU if: matrix.os == 'ubuntu-latest' uses: docker/setup-qemu-action@v3 diff --git a/setup.py b/setup.py index 8cfcbaa58..cdedbcfb6 100644 --- a/setup.py +++ b/setup.py @@ -205,7 +205,7 @@ def parse_cflags(raw_flags): url="https://github.com/PyAV-Org/PyAV", packages=find_packages(exclude=["build*", "examples*", "scratchpad*", "tests*"]), package_data=package_data, - python_requires=">=3.9", + python_requires=">=3.8", zip_safe=False, ext_modules=ext_modules, test_suite="tests", @@ -222,6 +222,7 @@ def parse_cflags(raw_flags): "Operating System :: Unix", "Operating System :: Microsoft :: Windows", "Programming Language :: Cython", + "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", From 96a05270b474f515531567f70d731a48e0437e2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Sat, 22 Jun 2024 00:20:48 +0200 Subject: [PATCH 293/846] Add type definitions for `CodecContext` properties Add type definitions for `options` and `time_base`. --- av/codec/context.pyi | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/av/codec/context.pyi b/av/codec/context.pyi index 4f7f3d2dc..d84b41de1 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -1,7 +1,7 @@ +from fractions import Fraction from typing import Any, Literal from av.enum import EnumFlag, EnumItem -from av.frame import Frame from av.packet import Packet from .codec import Codec @@ -60,8 +60,10 @@ class CodecContext: is_encoder: bool is_decoder: bool name: str + options: dict[str, str] type: Literal["video", "audio", "data", "subtitle", "attachment"] profile: str | None + time_base: Fraction codec_tag: str bit_rate: int | None max_bit_rate: int | None From 777726112046d5a7b024c005f2fbf4b4e7109806 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 25 Jun 2024 14:18:14 -0400 Subject: [PATCH 294/846] Remove SubtitleSet.packet Always returned `None`. Appeared to be not implemented correctly. Undocumented in type stubs. --- av/subtitles/subtitle.pxd | 3 --- 1 file changed, 3 deletions(-) diff --git a/av/subtitles/subtitle.pxd b/av/subtitles/subtitle.pxd index 29ddc4220..508eb9034 100644 --- a/av/subtitles/subtitle.pxd +++ b/av/subtitles/subtitle.pxd @@ -1,14 +1,11 @@ cimport libav as lib -from av.packet cimport Packet - cdef class SubtitleProxy: cdef lib.AVSubtitle struct cdef class SubtitleSet: - cdef readonly Packet packet cdef SubtitleProxy proxy cdef readonly tuple rects From 69cab4956413985050538614947c29630b089261 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 25 Jun 2024 15:36:06 -0400 Subject: [PATCH 295/846] Make Subtitle an abstract base class --- av/codec/context.pyx | 2 +- av/subtitles/stream.pyx | 3 +++ av/subtitles/subtitle.pyi | 3 +-- av/subtitles/subtitle.pyx | 11 ++++++++--- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/av/codec/context.pyx b/av/codec/context.pyx index d94a08015..f8c1263c6 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -638,7 +638,7 @@ cdef class CodecContext: def skip_frame(self): """One of :class:`.SkipType`. - Wraps ffmpeg:`AVCodecContext.skip_frame`. + Wraps :ffmpeg:`AVCodecContext.skip_frame`. """ return SkipType._get(self.ptr.skip_frame, create=True) diff --git a/av/subtitles/stream.pyx b/av/subtitles/stream.pyx index 6cf0c2d88..1deed5867 100644 --- a/av/subtitles/stream.pyx +++ b/av/subtitles/stream.pyx @@ -1,3 +1,6 @@ cdef class SubtitleStream(Stream): + """ + A :class:`SubtitleStream` can contain many :class:`SubtitleSet` objects accessible via decoding. + """ def __getattr__(self, name): return getattr(self.codec_context, name) diff --git a/av/subtitles/subtitle.pyi b/av/subtitles/subtitle.pyi index cdbe7fb31..2ac9195ec 100644 --- a/av/subtitles/subtitle.pyi +++ b/av/subtitles/subtitle.pyi @@ -11,8 +11,7 @@ class SubtitleSet: def __iter__(self) -> Iterator[Subtitle]: ... def __getitem__(self, i: int) -> Subtitle: ... -class Subtitle: - type: Literal[b"none", b"bitmap", b"text", b"ass"] +class Subtitle: ... class BitmapSubtitle(Subtitle): type: Literal[b"bitmap"] diff --git a/av/subtitles/subtitle.pyx b/av/subtitles/subtitle.pyx index e1deca7b7..7ced655f6 100644 --- a/av/subtitles/subtitle.pyx +++ b/av/subtitles/subtitle.pyx @@ -11,6 +11,9 @@ cdef class SubtitleProxy: cdef class SubtitleSet: + """ + A :class:`SubtitleSet` can contain many :class:`Subtitle` objects. + """ def __cinit__(self, SubtitleProxy proxy): self.proxy = proxy cdef int i @@ -50,9 +53,7 @@ cdef Subtitle build_subtitle(SubtitleSet subtitle, int index): raise ValueError("subtitle rect index out of range") cdef lib.AVSubtitleRect *ptr = subtitle.proxy.struct.rects[index] - if ptr.type == lib.SUBTITLE_NONE: - return Subtitle(subtitle, index) - elif ptr.type == lib.SUBTITLE_BITMAP: + if ptr.type == lib.SUBTITLE_BITMAP: return BitmapSubtitle(subtitle, index) elif ptr.type == lib.SUBTITLE_TEXT: return TextSubtitle(subtitle, index) @@ -63,6 +64,10 @@ cdef Subtitle build_subtitle(SubtitleSet subtitle, int index): cdef class Subtitle: + """ + An abstract base class for each concrete type of subtitle. + Wraps :ffmpeg:`AVSubtitleRect` + """ def __cinit__(self, SubtitleSet subtitle, int index): if index < 0 or index >= subtitle.proxy.struct.num_rects: raise ValueError("subtitle rect index out of range") From 268f9daafad86f584158fbadcfdde1324c48f321 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 25 Jun 2024 17:56:56 -0400 Subject: [PATCH 296/846] Add Graph.vpush() and Graph.vpull() --- av/container/input.pyi | 6 ++++++ av/filter/graph.pxd | 1 - av/filter/graph.pyi | 2 ++ av/filter/graph.pyx | 18 +++++++++++++++--- tests/test_filters.py | 10 +++++----- 5 files changed, 28 insertions(+), 9 deletions(-) diff --git a/av/container/input.pyi b/av/container/input.pyi index f85eaec46..90154c331 100644 --- a/av/container/input.pyi +++ b/av/container/input.pyi @@ -21,6 +21,12 @@ class InputContainer(Container): def close(self) -> None: ... def demux(self, *args: Any, **kwargs: Any) -> Iterator[Packet]: ... @overload + def decode(self, video: int) -> Iterator[VideoFrame]: ... + @overload + def decode(self, audio: int) -> Iterator[AudioFrame]: ... + @overload + def decode(self, subtitles: int) -> Iterator[SubtitleSet]: ... + @overload def decode(self, *args: VideoStream) -> Iterator[VideoFrame]: ... @overload def decode(self, *args: AudioStream) -> Iterator[AudioFrame]: ... diff --git a/av/filter/graph.pxd b/av/filter/graph.pxd index c9226749c..b3bf352a3 100644 --- a/av/filter/graph.pxd +++ b/av/filter/graph.pxd @@ -4,7 +4,6 @@ from av.filter.context cimport FilterContext cdef class Graph: - cdef lib.AVFilterGraph *ptr cdef readonly bint configured diff --git a/av/filter/graph.pyi b/av/filter/graph.pyi index 75930d08a..625364f35 100644 --- a/av/filter/graph.pyi +++ b/av/filter/graph.pyi @@ -41,3 +41,5 @@ class Graph: ) -> FilterContext: ... def push(self, frame: None | AudioFrame | VideoFrame) -> None: ... def pull(self) -> VideoFrame | AudioFrame: ... + def vpush(self, frame: VideoFrame | None) -> None: ... + def vpull(self) -> VideoFrame: ... diff --git a/av/filter/graph.pyx b/av/filter/graph.pyx index f6376b3a3..53689c432 100644 --- a/av/filter/graph.pyx +++ b/av/filter/graph.pyx @@ -174,11 +174,15 @@ cdef class Graph: else: raise ValueError(f"can only AudioFrame, VideoFrame or None; got {type(frame)}") - if len(contexts) != 1: - raise ValueError(f"can only auto-push with single buffer; found {len(contexts)}") + for ctx in contexts: + ctx.push(frame) - contexts[0].push(frame) + def vpush(self, VideoFrame frame): + for ctx in self._context_by_type.get("buffer", []): + ctx.push(frame) + + # TODO: Test complex filter graphs, add `at: int = 0` arg to pull() and vpull(). def pull(self): vsinks = self._context_by_type.get("buffersink", []) asinks = self._context_by_type.get("abuffersink", []) @@ -188,3 +192,11 @@ cdef class Graph: raise ValueError(f"can only auto-pull with single sink; found {nsinks}") return (vsinks or asinks)[0].pull() + + def vpull(self): + vsinks = self._context_by_type.get("buffersink", []) + nsinks = len(vsinks) + if nsinks != 1: + raise ValueError(f"can only auto-pull with single sink; found {nsinks}") + + return vsinks[0].pull() diff --git a/tests/test_filters.py b/tests/test_filters.py index 326ef7fb9..3d9e6e9d0 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -204,7 +204,7 @@ def test_video_buffer(self): for frame in input_container.decode(): self.assertEqual(frame.time_base, Fraction(1, 30)) - graph.push(frame) + graph.vpush(frame) filtered_frames = pull_until_blocked(graph) if frame.pts == 0: @@ -220,7 +220,7 @@ def test_video_buffer(self): self.assertEqual(filtered_frames[1].pts, (frame.pts - 1) * 2 + 1) self.assertEqual(filtered_frames[1].time_base, Fraction(1, 60)) - def test_EOF(self): + def test_EOF(self) -> None: input_container = av.open(format="lavfi", file="color=c=pink:duration=1:r=30") video_stream = input_container.streams.video[0] @@ -233,12 +233,12 @@ def test_EOF(self): graph.configure() for frame in input_container.decode(video=0): - graph.push(frame) + graph.vpush(frame) - graph.push(None) + graph.vpush(None) # if we do not push None, we get a BlockingIOError - palette_frame = graph.pull() + palette_frame = graph.vpull() self.assertIsInstance(palette_frame, av.VideoFrame) self.assertEqual(palette_frame.width, 16) From ff65d9ece969fafc596db0498bec706b20caebee Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 25 Jun 2024 19:35:58 -0400 Subject: [PATCH 297/846] Set version to 12.2.0 --- av/about.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/about.py b/av/about.py index 79a759ae1..239627e93 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "13.0.0rc1" +__version__ = "12.2.0rc1" From c8906362b4a64fa59a5a31d170970d9ad9a81a6d Mon Sep 17 00:00:00 2001 From: Moonsik Park Date: Wed, 26 Jun 2024 11:45:11 +0900 Subject: [PATCH 298/846] Remove circular reference between `Graph` and `FilterContext` --- av/filter/context.pxd | 2 +- av/filter/context.pyi | 3 +++ av/filter/context.pyx | 11 ++++++++++- av/filter/graph.pxd | 2 ++ 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/av/filter/context.pxd b/av/filter/context.pxd index 18954fbdd..ae9f27c99 100644 --- a/av/filter/context.pxd +++ b/av/filter/context.pxd @@ -7,7 +7,7 @@ from av.filter.graph cimport Graph cdef class FilterContext: cdef lib.AVFilterContext *ptr - cdef readonly Graph graph + cdef readonly object _graph cdef readonly Filter filter cdef object _inputs diff --git a/av/filter/context.pyi b/av/filter/context.pyi index 0d70c0095..7c00087a9 100644 --- a/av/filter/context.pyi +++ b/av/filter/context.pyi @@ -1,3 +1,4 @@ +from av.filter import Graph from av.frame import Frame from .pad import FilterContextPad @@ -11,5 +12,7 @@ class FilterContext: def link_to( self, input_: FilterContext, output_idx: int = 0, input_idx: int = 0 ) -> None: ... + @property + def graph(self) -> Graph: ... def push(self, frame: Frame) -> None: ... def pull(self) -> Frame: ... diff --git a/av/filter/context.pyx b/av/filter/context.pyx index 54ed710ab..b820d3d18 100644 --- a/av/filter/context.pyx +++ b/av/filter/context.pyx @@ -1,3 +1,5 @@ +import weakref + from av.audio.frame cimport alloc_audio_frame from av.dictionary cimport _Dictionary from av.dictionary import Dictionary @@ -13,7 +15,7 @@ cdef object _cinit_sentinel = object() cdef FilterContext wrap_filter_context(Graph graph, Filter filter, lib.AVFilterContext *ptr): cdef FilterContext self = FilterContext(_cinit_sentinel) - self.graph = graph + self._graph = weakref.ref(graph) self.filter = filter self.ptr = ptr return self @@ -72,6 +74,13 @@ cdef class FilterContext: def link_to(self, FilterContext input_, int output_idx=0, int input_idx=0): err_check(lib.avfilter_link(self.ptr, output_idx, input_.ptr, input_idx)) + + @property + def graph(self): + if (graph := self._graph()): + return graph + else: + raise RuntimeError("graph is unallocated") def push(self, Frame frame): cdef int res diff --git a/av/filter/graph.pxd b/av/filter/graph.pxd index b3bf352a3..2e52bd6ec 100644 --- a/av/filter/graph.pxd +++ b/av/filter/graph.pxd @@ -4,6 +4,8 @@ from av.filter.context cimport FilterContext cdef class Graph: + cdef object __weakref__ + cdef lib.AVFilterGraph *ptr cdef readonly bint configured From 29fb3fd06b6ce216b10f3d4e20d04cb5b9b33611 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 27 Jun 2024 12:27:07 -0400 Subject: [PATCH 299/846] Add docstrings for vpush and vpull --- av/filter/graph.pyx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/av/filter/graph.pyx b/av/filter/graph.pyx index 53689c432..e2d95e4cd 100644 --- a/av/filter/graph.pyx +++ b/av/filter/graph.pyx @@ -178,6 +178,7 @@ cdef class Graph: ctx.push(frame) def vpush(self, VideoFrame frame): + """Like `push`, but only for VideoFrames.""" for ctx in self._context_by_type.get("buffer", []): ctx.push(frame) @@ -194,6 +195,7 @@ cdef class Graph: return (vsinks or asinks)[0].pull() def vpull(self): + """Like `pull`, but only for VideoFrames.""" vsinks = self._context_by_type.get("buffersink", []) nsinks = len(vsinks) if nsinks != 1: From fb6c064e80fe63f414a621eb8f2b6a44a22c788b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Fri, 28 Jun 2024 08:41:55 +0200 Subject: [PATCH 300/846] Release v12.2.0 --- CHANGELOG.rst | 13 +++++++++++++ av/about.py | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 526ce98a6..3f29ef131 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,19 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. +v12.2.0 +------- + +Features: + +- Add a `make_writable` method to `Frame` instances (:issue:`1414`). +- Use `av_guess_sample_aspect_ratio` to report sample and display aspect ratios. + +Fixes: + +- Fix a crash when assigning an `AudioLayout` to `AudioCodecContext.layout` (:issue:`1434`). +- Remove a circular reference which caused `AudioSampler` to occupy memory until garbage collected (:issue:`1429`). +- Fix more type stubs, remove incorrect `__init__.pyi`. v12.1.0 ------- diff --git a/av/about.py b/av/about.py index 239627e93..59c3246f6 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "12.2.0rc1" +__version__ = "12.2.0" From cd3f4c263ac3d84c7633954ad6d3228be1e0b421 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 1 Jul 2024 04:11:53 -0400 Subject: [PATCH 301/846] Resampler: format, layout accepts `str` `int` too --- av/audio/resampler.pyi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/av/audio/resampler.pyi b/av/audio/resampler.pyi index 32861258e..cbf2134aa 100644 --- a/av/audio/resampler.pyi +++ b/av/audio/resampler.pyi @@ -12,8 +12,8 @@ class AudioResampler: def __init__( self, - format: AudioFormat | None = None, - layout: AudioLayout | None = None, + format: str | int | AudioFormat | None = None, + layout: str | int | AudioLayout | None = None, rate: int | None = None, frame_size: int | None = None, ) -> None: ... From b98a12edb2918f22c8e747f0714edf48e19b9f59 Mon Sep 17 00:00:00 2001 From: Matteo Destro Date: Tue, 2 Jul 2024 07:57:35 +0200 Subject: [PATCH 302/846] Support libav's `av_log_set_level` --- av/logging.pyi | 1 + av/logging.pyx | 11 +++++++++++ include/libavutil/avutil.pxd | 1 + 3 files changed, 13 insertions(+) diff --git a/av/logging.pyi b/av/logging.pyi index 1db5e4a3b..8c32de77d 100644 --- a/av/logging.pyi +++ b/av/logging.pyi @@ -13,6 +13,7 @@ CRITICAL: int def adapt_level(level: int) -> int: ... def get_level() -> int | None: ... def set_level(level: int | None) -> None: ... +def set_libav_level(level: int) -> None: ... def restore_default_callback() -> None: ... def get_skip_repeated() -> bool: ... def set_skip_repeated(v: bool) -> None: ... diff --git a/av/logging.pyx b/av/logging.pyx index 1006d094c..6b6858db6 100644 --- a/av/logging.pyx +++ b/av/logging.pyx @@ -119,6 +119,17 @@ def set_level(level): raise ValueError("level must be: int | None") +def set_libav_level(level): + """Set libav's log level. It can be set to constants available in this + module: ``PANIC``, ``FATAL``, ``ERROR``, ``WARNING``, ``INFO``, + ``VERBOSE``, ``DEBUG``. + + When PyAV logging is disabled, setting this will change the level of + the logs printed to the terminal. + """ + lib.av_log_set_level(level) + + def restore_default_callback(): """Revert back to FFmpeg's log callback, which prints to the terminal.""" lib.av_log_set_callback(lib.av_log_default_callback) diff --git a/include/libavutil/avutil.pxd b/include/libavutil/avutil.pxd index f9af7a7b0..b4184d0de 100644 --- a/include/libavutil/avutil.pxd +++ b/include/libavutil/avutil.pxd @@ -399,3 +399,4 @@ cdef extern from "libavutil/log.h" nogil: ctypedef void(*av_log_callback)(void *, int, const char *, va_list) void av_log_default_callback(void *, int, const char *, va_list) void av_log_set_callback (av_log_callback callback) + void av_log_set_level(int level) From 42f3e1e6ab0af53c9bee5f9000fa10ad730b2d94 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 9 Jul 2024 16:31:05 -0400 Subject: [PATCH 303/846] Add Graph.link_nodes --- av/filter/graph.pyi | 1 + av/filter/graph.pyx | 9 ++++++++- tests/test_filters.py | 15 ++++----------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/av/filter/graph.pyi b/av/filter/graph.pyi index 625364f35..337be2bee 100644 --- a/av/filter/graph.pyi +++ b/av/filter/graph.pyi @@ -17,6 +17,7 @@ class Graph: def __init__(self) -> None: ... def configure(self, auto_buffer: bool = True, force: bool = False) -> None: ... + def link_nodes(self, *nodes: FilterContext) -> Graph: ... def add( self, filter: str | Filter, args: Any = None, **kwargs: str ) -> FilterContext: ... diff --git a/av/filter/graph.pyx b/av/filter/graph.pyx index e2d95e4cd..e0effd12f 100644 --- a/av/filter/graph.pyx +++ b/av/filter/graph.pyx @@ -45,6 +45,13 @@ cdef class Graph: # We get auto-inserted stuff here. self._auto_register() + def link_nodes(self, *nodes): + """ + Links nodes together for simple filter graphs. + """ + for c, n in zip(nodes, nodes[1:]): + c.link_to(n) + return self def add(self, filter, args=None, **kwargs): cdef Filter cy_filter @@ -68,7 +75,7 @@ cdef class Graph: # There might have been automatic contexts added (e.g. resamplers, # fifos, and scalers). It is more likely to see them after the graph - # is configured, but we wan't to be safe. + # is configured, but we want to be safe. self._auto_register() return ctx diff --git a/tests/test_filters.py b/tests/test_filters.py index 3d9e6e9d0..1f3b8ae99 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -128,14 +128,9 @@ def test_audio_buffer_sink(self): if e.errno != errno.EAGAIN: raise - @staticmethod - def link_nodes(*nodes): - for c, n in zip(nodes, nodes[1:]): - c.link_to(n) - def test_audio_buffer_resample(self): graph = Graph() - self.link_nodes( + graph.link_nodes( graph.add_abuffer( format="fltp", sample_rate=48000, @@ -146,8 +141,7 @@ def test_audio_buffer_resample(self): "aformat", "sample_fmts=s16:sample_rates=44100:channel_layouts=stereo" ), graph.add("abuffersink"), - ) - graph.configure() + ).configure() graph.push( generate_audio_frame( @@ -161,7 +155,7 @@ def test_audio_buffer_resample(self): def test_audio_buffer_volume_filter(self): graph = Graph() - self.link_nodes( + graph.link_nodes( graph.add_abuffer( format="fltp", sample_rate=48000, @@ -170,8 +164,7 @@ def test_audio_buffer_volume_filter(self): ), graph.add("volume", volume="0.5"), graph.add("abuffersink"), - ) - graph.configure() + ).configure() input_frame = generate_audio_frame( 0, input_format="fltp", layout="stereo", sample_rate=48000 From a493150c98b1241ec828524059fd124bf7cfb33f Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 12 Jul 2024 02:41:22 -0400 Subject: [PATCH 304/846] Add default codec properties --- av/container/output.pyi | 6 ++++++ av/container/output.pyx | 23 +++++++++++++++++++++++ av/format.pyi | 4 ++++ tests/test_containerformat.py | 26 +++++++++++++++++++++----- 4 files changed, 54 insertions(+), 5 deletions(-) diff --git a/av/container/output.pyi b/av/container/output.pyi index 6df32c897..882592667 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -19,3 +19,9 @@ class OutputContainer(Container): def close(self) -> None: ... def mux(self, packets: Packet | Sequence[Packet]) -> None: ... def mux_one(self, packet: Packet) -> None: ... + @property + def default_video_codec(self) -> str: ... + @property + def default_audio_codec(self) -> str: ... + @property + def default_subtitle_codec(self) -> str: ... diff --git a/av/container/output.pyx b/av/container/output.pyx index 55e8b5006..184a30b2d 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -1,6 +1,8 @@ import logging import os +cimport libav as lib + from av.codec.codec cimport Codec from av.codec.context cimport CodecContext, wrap_codec_context from av.container.streams cimport StreamContainer @@ -192,6 +194,27 @@ cdef class OutputContainer(Container): self._started = True + @property + def default_video_codec(self): + """ + Returns the default video codec this container recommends. + """ + return lib.avcodec_get_name(self.format.optr.video_codec) + + @property + def default_audio_codec(self): + """ + Returns the default audio codec this container recommends. + """ + return lib.avcodec_get_name(self.format.optr.audio_codec) + + @property + def default_subtitle_codec(self): + """ + Returns the default subtitle codec this container recommends. + """ + return lib.avcodec_get_name(self.format.optr.subtitle_codec) + def close(self): for stream in self.streams: if stream.codec_context: diff --git a/av/format.pyi b/av/format.pyi index d2aef4764..c3506ed51 100644 --- a/av/format.pyi +++ b/av/format.pyi @@ -1,5 +1,7 @@ __all__ = ("ContainerFormat", "formats_available") +from typing import Literal + from .enum import EnumFlag class Flags(EnumFlag): @@ -22,10 +24,12 @@ class Flags(EnumFlag): SEEK_TO_PTS: int class ContainerFormat: + def __init__(self, name: str, mode: Literal["r", "w"] | None = None) -> None: ... name: str long_name: str is_input: bool is_output: bool + extensions: set[str] # flags no_file: int diff --git a/tests/test_containerformat.py b/tests/test_containerformat.py index dea3d29dc..f0460b131 100644 --- a/tests/test_containerformat.py +++ b/tests/test_containerformat.py @@ -1,10 +1,15 @@ -from av import ContainerFormat, formats_available +from av import ContainerFormat, formats_available, open from .common import TestCase class TestContainerFormats(TestCase): - def test_matroska(self): + def test_matroska(self) -> None: + with open("test.mkv", "w") as container: + self.assertNotEqual(container.default_video_codec, "none") + self.assertNotEqual(container.default_audio_codec, "none") + self.assertEqual(container.default_subtitle_codec, "ass") + fmt = ContainerFormat("matroska") self.assertTrue(fmt.is_input) self.assertTrue(fmt.is_output) @@ -13,7 +18,12 @@ def test_matroska(self): self.assertIn("mkv", fmt.extensions) self.assertFalse(fmt.no_file) - def test_mov(self): + def test_mov(self) -> None: + with open("test.mov", "w") as container: + self.assertNotEqual(container.default_video_codec, "none") + self.assertNotEqual(container.default_audio_codec, "none") + self.assertEqual(container.default_subtitle_codec, "none") + fmt = ContainerFormat("mov") self.assertTrue(fmt.is_input) self.assertTrue(fmt.is_output) @@ -22,7 +32,13 @@ def test_mov(self): self.assertIn("mov", fmt.extensions) self.assertFalse(fmt.no_file) - def test_stream_segment(self): + def test_gif(self) -> None: + with open("test.gif", "w") as container: + self.assertEqual(container.default_video_codec, "gif") + self.assertEqual(container.default_audio_codec, "none") + self.assertEqual(container.default_subtitle_codec, "none") + + def test_stream_segment(self) -> None: # This format goes by two names, check both. fmt = ContainerFormat("stream_segment") self.assertFalse(fmt.is_input) @@ -40,5 +56,5 @@ def test_stream_segment(self): self.assertEqual(fmt.extensions, set()) self.assertTrue(fmt.no_file) - def test_formats_available(self): + def test_formats_available(self) -> None: self.assertTrue(formats_available) From 996715a0bb33a7725ee131d28ea73cdabc5dc566 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 12 Jul 2024 03:24:48 -0400 Subject: [PATCH 305/846] Bump to next dev version --- av/about.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/about.py b/av/about.py index 59c3246f6..b0de45783 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "12.2.0" +__version__ = "12.3.0rc1" From 5e758b4539455f6482ab9d87535b752546add917 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 13 Jul 2024 16:01:21 -0400 Subject: [PATCH 306/846] Use ffmpeg 6.1.1-4 --- .github/workflows/tests.yml | 3 +++ scripts/ffmpeg-6.1.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b66ef500b..2e4c221d9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -53,6 +53,9 @@ jobs: run: | brew update brew install pkg-config + - name: Set deployment target + if: matrix.os == 'macos-13' || matrix.os == 'macos-14' + run: echo "MACOSX_DEPLOYMENT_TARGET=10.13" >> $GITHUB_ENV - name: Build wheels env: CIBW_ARCHS: ${{ matrix.arch }} diff --git a/scripts/ffmpeg-6.1.json b/scripts/ffmpeg-6.1.json index 49ce58d17..d2b41f4c1 100644 --- a/scripts/ffmpeg-6.1.json +++ b/scripts/ffmpeg-6.1.json @@ -1,3 +1,3 @@ { - "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/6.1.1-3/ffmpeg-{platform}.tar.gz"] + "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/6.1.1-4/ffmpeg-{platform}.tar.gz"] } From 6488603a5edece1fb96b3643c465dcc8fe177c6a Mon Sep 17 00:00:00 2001 From: Matteo Destro Date: Sun, 14 Jul 2024 09:54:15 +0200 Subject: [PATCH 307/846] Include libav headers in final distribution --- av/__init__.py | 12 ++++++++++++ setup.py | 13 ++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/av/__init__.py b/av/__init__.py index b4705dc69..68b9fed09 100644 --- a/av/__init__.py +++ b/av/__init__.py @@ -41,3 +41,15 @@ # Backwards compatibility AVError = FFmpegError # noqa: F405 + + +def get_include() -> str: + """ + Returns the path to the `include` folder to be used when building extensions to av. + """ + # Installed package + include_path = os.path.join(os.path.dirname(__file__), "include") + if os.path.exists(include_path): + return include_path + # Running from source directory + return os.path.join(os.path.dirname(__file__), os.pardir, "include") diff --git a/setup.py b/setup.py index cdedbcfb6..2a88c8967 100644 --- a/setup.py +++ b/setup.py @@ -184,6 +184,13 @@ def parse_cflags(raw_flags): ".".join(pckg.parts): ["*.pxd", "*.pyi", "*.typed"] for pckg in package_folders } +# Add include/ headers to av.include +package_dir = { + ".".join(["av", *pckg.parts]): str(pckg) + for pckg in pathlib.Path("include").glob("**/") +} +package_data.update({pckg: ["*.pxd"] for pckg in package_dir}) + with open("README.md") as f: long_description = f.read() @@ -203,7 +210,11 @@ def parse_cflags(raw_flags): author="Mike Boers", author_email="pyav@mikeboers.com", url="https://github.com/PyAV-Org/PyAV", - packages=find_packages(exclude=["build*", "examples*", "scratchpad*", "tests*"]), + packages=find_packages( + exclude=["build*", "examples*", "scratchpad*", "tests*", "include*"] + ) + + list(package_dir.keys()), + package_dir=package_dir, package_data=package_data, python_requires=">=3.8", zip_safe=False, From 5730ef0430cdc9803df04d042f76f13467fee2d7 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 15 Jul 2024 14:09:51 -0400 Subject: [PATCH 308/846] Remove TextSubtitle, add dialogue property AssSubtitle is used even for non ASS/SSA formats. These changes make that more obvious to users. FFmpeg does not export ass_split.h, so the dialogue getter is implemented directly in Cython. --- av/subtitles/subtitle.pyi | 13 ++++---- av/subtitles/subtitle.pyx | 69 +++++++++++++++++++++++++++++---------- tests/test_subtitles.py | 6 +++- 3 files changed, 64 insertions(+), 24 deletions(-) diff --git a/av/subtitles/subtitle.pyi b/av/subtitles/subtitle.pyi index 2ac9195ec..2a35d0a55 100644 --- a/av/subtitles/subtitle.pyi +++ b/av/subtitles/subtitle.pyi @@ -27,10 +27,11 @@ class BitmapSubtitlePlane: index: int buffer_size: int -class TextSubtitle(Subtitle): - type: Literal[b"text"] - text: bytes - class AssSubtitle(Subtitle): - type: Literal[b"ass"] - ass: bytes + type: Literal[b"ass", b"text"] + @property + def ass(self) -> bytes: ... + @property + def dialogue(self) -> bytes: ... + @property + def text(self) -> bytes: ... diff --git a/av/subtitles/subtitle.pyx b/av/subtitles/subtitle.pyx index 7ced655f6..373bb529b 100644 --- a/av/subtitles/subtitle.pyx +++ b/av/subtitles/subtitle.pyx @@ -55,9 +55,7 @@ cdef Subtitle build_subtitle(SubtitleSet subtitle, int index): if ptr.type == lib.SUBTITLE_BITMAP: return BitmapSubtitle(subtitle, index) - elif ptr.type == lib.SUBTITLE_TEXT: - return TextSubtitle(subtitle, index) - elif ptr.type == lib.SUBTITLE_ASS: + elif ptr.type == lib.SUBTITLE_ASS or ptr.type == lib.SUBTITLE_TEXT: return AssSubtitle(subtitle, index) else: raise ValueError("unknown subtitle type %r" % ptr.type) @@ -141,7 +139,10 @@ cdef class BitmapSubtitlePlane: PyBuffer_FillInfo(view, self, self._buffer, self.buffer_size, 0, flags) -cdef class TextSubtitle(Subtitle): +cdef class AssSubtitle(Subtitle): + """ + Represents an ASS/Text subtitle format, as opposed to a bitmap Subtitle format. + """ def __repr__(self): return ( f"<{self.__class__.__module__}.{self.__class__.__name__} " @@ -149,21 +150,55 @@ cdef class TextSubtitle(Subtitle): ) @property - def text(self): - if self.ptr.text is not NULL: - return PyBytes_FromString(self.ptr.text) + def ass(self): + """ + Returns the subtitle in the ASS/SSA format. Used by the vast majority of subtitle formats. + """ + if self.ptr.ass is not NULL: + return PyBytes_FromString(self.ptr.ass) return b"" - -cdef class AssSubtitle(Subtitle): - def __repr__(self): - return ( - f"<{self.__class__.__module__}.{self.__class__.__name__} " - f"{self.ass!r} at 0x{id(self):x}>" - ) + @property + def dialogue(self): + """ + Extract the dialogue from the ass format. Strip comments. + """ + comma_count = 0 + i = 0 + cdef bytes ass_text = self.ass + cdef bytes result = b"" + + while comma_count < 8 and i < len(ass_text): + if bytes([ass_text[i]]) == b",": + comma_count += 1 + i += 1 + + state = False + while i < len(ass_text): + char = bytes([ass_text[i]]) + next_char = b"" if i + 1 >= len(ass_text) else bytes([ass_text[i + 1]]) + + if char == b"\\" and next_char == b"N": + result += b"\n" + i += 2 + continue + + if not state: + if char == b"{" and next_char != b"\\": + state = True + else: + result += char + elif char == b"}": + state = False + i += 1 + + return result @property - def ass(self): - if self.ptr.ass is not NULL: - return PyBytes_FromString(self.ptr.ass) + def text(self): + """ + Rarely used attribute. You're probably looking for dialogue. + """ + if self.ptr.text is not NULL: + return PyBytes_FromString(self.ptr.text) return b"" diff --git a/tests/test_subtitles.py b/tests/test_subtitles.py index fe4f2ee3d..cc1409f2b 100644 --- a/tests/test_subtitles.py +++ b/tests/test_subtitles.py @@ -5,7 +5,7 @@ class TestSubtitle(TestCase): - def test_movtext(self): + def test_movtext(self) -> None: path = fate_suite("sub/MovText_capability_tester.mp4") subs = [] @@ -23,8 +23,12 @@ def test_movtext(self): sub = subset[0] self.assertIsInstance(sub, AssSubtitle) + assert isinstance(sub, AssSubtitle) + self.assertEqual(sub.type, b"ass") + self.assertEqual(sub.text, b"") self.assertEqual(sub.ass, b"0,0,Default,,0,0,0,,- Test 1.\\N- Test 2.") + self.assertEqual(sub.dialogue, b"- Test 1.\n- Test 2.") def test_vobsub(self): path = fate_suite("sub/vobsub.sub") From 4bd4984daaf1c81731185e3bcada1e017ca1720c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 16 Jul 2024 02:01:21 -0400 Subject: [PATCH 309/846] Remove TextSubtitle in docs --- av/container/core.pyx | 2 +- docs/api/subtitles.rst | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/av/container/core.pyx b/av/container/core.pyx index 548fe4fc2..df1b25939 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -365,7 +365,7 @@ def open( :param int buffer_size: Size of buffer for Python input/output operations in bytes. Honored only when ``file`` is a file-like object. Defaults to 32768 (32k). :param timeout: How many seconds to wait for data before giving up, as a float, or a - :ref:`(open timeout, read timeout) ` tuple. + :ref:`(open timeout, read timeout)` tuple. :param callable io_open: Custom I/O callable for opening files/streams. This option is intended for formats that need to open additional file-like objects to ``file`` using custom I/O. diff --git a/docs/api/subtitles.rst b/docs/api/subtitles.rst index 19d75621c..949896d6d 100644 --- a/docs/api/subtitles.rst +++ b/docs/api/subtitles.rst @@ -15,14 +15,11 @@ Subtitles .. autoclass:: Subtitle :members: - .. autoclass:: BitmapSubtitle - :members: - - .. autoclass:: BitmapSubtitlePlane + .. autoclass:: AssSubtitle :members: - .. autoclass:: TextSubtitle + .. autoclass:: BitmapSubtitle :members: - .. autoclass:: AssSubtitle + .. autoclass:: BitmapSubtitlePlane :members: From 4edb00f56bb540a43120f278b0646cd9c34a56ec Mon Sep 17 00:00:00 2001 From: elxy Date: Wed, 17 Jul 2024 10:40:32 +0800 Subject: [PATCH 310/846] Fix values of dstRange/srcRange when calling sws_setColorspaceDetails() In sws_setColorspaceDetails(), dstRange/srcRange is 1 for JPEG range and 0 for MPEG range. --- av/video/reformatter.pyx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/av/video/reformatter.pyx b/av/video/reformatter.pyx index 11ee1bf74..624454f0c 100644 --- a/av/video/reformatter.pyx +++ b/av/video/reformatter.pyx @@ -76,9 +76,9 @@ cdef class VideoReformatter: :type dst_colorspace: :class:`Colorspace` or ``str`` :param interpolation: The interpolation method to use, or ``None`` for ``BILINEAR``. :type interpolation: :class:`Interpolation` or ``str`` - :param src_color_range: Current color range, or ``None`` for the frame color range. + :param src_color_range: Current color range, or ``None`` for the ``UNSPECIFIED``. :type src_color_range: :class:`color range` or ``str`` - :param dst_color_range: Desired color range, or ``None`` for the frame color range. + :param dst_color_range: Desired color range, or ``None`` for the ``UNSPECIFIED``. :type dst_color_range: :class:`color range` or ``str`` """ @@ -110,6 +110,10 @@ cdef class VideoReformatter: if frame.ptr.format < 0: raise ValueError("Frame does not have format set.") + # The definition of color range in pixfmt.h and swscale.h is different. + src_color_range = 1 if src_color_range == ColorRange.JPEG.value else 0 + dst_color_range = 1 if dst_color_range == ColorRange.JPEG.value else 0 + cdef lib.AVPixelFormat src_format = frame.ptr.format # Shortcut! From be562241e31668bd92bd710878155ab5539d035f Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 16 Jul 2024 22:23:10 -0400 Subject: [PATCH 311/846] Cleanup codec/context.pyx --- av/codec/context.pyx | 106 +++++++++++++------------------------------ 1 file changed, 32 insertions(+), 74 deletions(-) diff --git a/av/codec/context.pyx b/av/codec/context.pyx index f8c1263c6..1d96899e2 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -25,7 +25,6 @@ cdef CodecContext wrap_codec_context(lib.AVCodecContext *c_ctx, const lib.AVCode cdef CodecContext py_ctx - # TODO: This. if c_ctx.codec_type == lib.AVMEDIA_TYPE_VIDEO: from av.video.codeccontext import VideoCodecContext py_ctx = VideoCodecContext(_cinit_sentinel) @@ -45,69 +44,46 @@ cdef CodecContext wrap_codec_context(lib.AVCodecContext *c_ctx, const lib.AVCode ThreadType = define_enum("ThreadType", __name__, ( ("NONE", 0), - ("FRAME", lib.FF_THREAD_FRAME, - """Decode more than one frame at once"""), - ("SLICE", lib.FF_THREAD_SLICE, - """Decode more than one part of a single frame at once"""), - ("AUTO", lib.FF_THREAD_SLICE | lib.FF_THREAD_FRAME, - """Decode using both FRAME and SLICE methods."""), + ("FRAME", lib.FF_THREAD_FRAME, "Decode more than one frame at once"), + ("SLICE", lib.FF_THREAD_SLICE, "Decode more than one part of a single frame at once"), + ("AUTO", lib.FF_THREAD_SLICE | lib.FF_THREAD_FRAME, "Decode using both FRAME and SLICE methods."), ), is_flags=True) SkipType = define_enum("SkipType", __name__, ( - ("NONE", lib.AVDISCARD_NONE, - """Discard nothing"""), - ("DEFAULT", lib.AVDISCARD_DEFAULT, - """Discard useless packets like 0 size packets in AVI"""), - ("NONREF", lib.AVDISCARD_NONREF, - """Discard all non reference"""), - ("BIDIR", lib.AVDISCARD_BIDIR, - """Discard all bidirectional frames"""), - ("NONINTRA", lib.AVDISCARD_NONINTRA, - """Discard all non intra frames"""), - ("NONKEY", lib.AVDISCARD_NONKEY, - """Discard all frames except keyframes"""), - ("ALL", lib.AVDISCARD_ALL, - """Discard all"""), + ("NONE", lib.AVDISCARD_NONE, "Discard nothing"), + ("DEFAULT", lib.AVDISCARD_DEFAULT, "Discard useless packets like 0 size packets in AVI"), + ("NONREF", lib.AVDISCARD_NONREF, "Discard all non reference"), + ("BIDIR", lib.AVDISCARD_BIDIR, "Discard all bidirectional frames"), + ("NONINTRA", lib.AVDISCARD_NONINTRA, "Discard all non intra frames"), + ("NONKEY", lib.AVDISCARD_NONKEY, "Discard all frames except keyframes"), + ("ALL", lib.AVDISCARD_ALL, "Discard all"), )) Flags = define_enum("Flags", __name__, ( ("NONE", 0), ("UNALIGNED", lib.AV_CODEC_FLAG_UNALIGNED, - """Allow decoders to produce frames with data planes that are not aligned - to CPU requirements (e.g. due to cropping)."""), - ("QSCALE", lib.AV_CODEC_FLAG_QSCALE, - """Use fixed qscale."""), - ("4MV", lib.AV_CODEC_FLAG_4MV, - """4 MV per MB allowed / advanced prediction for H.263."""), - ("OUTPUT_CORRUPT", lib.AV_CODEC_FLAG_OUTPUT_CORRUPT, - """Output even those frames that might be corrupted."""), - ("QPEL", lib.AV_CODEC_FLAG_QPEL, - """Use qpel MC."""), + "Allow decoders to produce frames with data planes that are not aligned to CPU requirements (e.g. due to cropping)." + ), + ("QSCALE", lib.AV_CODEC_FLAG_QSCALE, "Use fixed qscale."), + ("4MV", lib.AV_CODEC_FLAG_4MV, "4 MV per MB allowed / advanced prediction for H.263."), + ("OUTPUT_CORRUPT", lib.AV_CODEC_FLAG_OUTPUT_CORRUPT, "Output even those frames that might be corrupted."), + ("QPEL", lib.AV_CODEC_FLAG_QPEL, "Use qpel MC."), ("DROPCHANGED", 1 << 5, - """Don't output frames whose parameters differ from first - decoded frame in stream."""), - ("PASS1", lib.AV_CODEC_FLAG_PASS1, - """Use internal 2pass ratecontrol in first pass mode."""), - ("PASS2", lib.AV_CODEC_FLAG_PASS2, - """Use internal 2pass ratecontrol in second pass mode."""), - ("LOOP_FILTER", lib.AV_CODEC_FLAG_LOOP_FILTER, - """loop filter."""), - ("GRAY", lib.AV_CODEC_FLAG_GRAY, - """Only decode/encode grayscale."""), - ("PSNR", lib.AV_CODEC_FLAG_PSNR, - """error[?] variables will be set during encoding."""), - ("INTERLACED_DCT", lib.AV_CODEC_FLAG_INTERLACED_DCT, - """Use interlaced DCT."""), - ("LOW_DELAY", lib.AV_CODEC_FLAG_LOW_DELAY, - """Force low delay."""), + "Don't output frames whose parameters differ from first decoded frame in stream." + ), + ("PASS1", lib.AV_CODEC_FLAG_PASS1, "Use internal 2pass ratecontrol in first pass mode."), + ("PASS2", lib.AV_CODEC_FLAG_PASS2, "Use internal 2pass ratecontrol in second pass mode."), + ("LOOP_FILTER", lib.AV_CODEC_FLAG_LOOP_FILTER, "loop filter."), + ("GRAY", lib.AV_CODEC_FLAG_GRAY, "Only decode/encode grayscale."), + ("PSNR", lib.AV_CODEC_FLAG_PSNR, "error[?] variables will be set during encoding."), + ("INTERLACED_DCT", lib.AV_CODEC_FLAG_INTERLACED_DCT, "Use interlaced DCT."), + ("LOW_DELAY", lib.AV_CODEC_FLAG_LOW_DELAY, "Force low delay."), ("GLOBAL_HEADER", lib.AV_CODEC_FLAG_GLOBAL_HEADER, - """Place global headers in extradata instead of every keyframe."""), - ("BITEXACT", lib.AV_CODEC_FLAG_BITEXACT, - """Use only bitexact stuff (except (I)DCT)."""), - ("AC_PRED", lib.AV_CODEC_FLAG_AC_PRED, - """H.263 advanced intra coding / MPEG-4 AC prediction"""), - ("INTERLACED_ME", lib.AV_CODEC_FLAG_INTERLACED_ME, - """Interlaced motion estimation"""), + "Place global headers in extradata instead of every keyframe." + ), + ("BITEXACT", lib.AV_CODEC_FLAG_BITEXACT, "Use only bitexact stuff (except (I)DCT)."), + ("AC_PRED", lib.AV_CODEC_FLAG_AC_PRED, "H.263 advanced intra coding / MPEG-4 AC prediction"), + ("INTERLACED_ME", lib.AV_CODEC_FLAG_INTERLACED_ME, "Interlaced motion estimation"), ("CLOSED_GOP", lib.AV_CODEC_FLAG_CLOSED_GOP), ), is_flags=True) @@ -168,11 +144,7 @@ cdef class CodecContext: def _set_flags(self, value): self.ptr.flags = value - flags = Flags.property( - _get_flags, - _set_flags, - """Flag property of :class:`.Flags`.""" - ) + flags = Flags.property(_get_flags, _set_flags, "Flag property of :class:`.Flags`.") unaligned = flags.flag_property("UNALIGNED") qscale = flags.flag_property("QSCALE") @@ -199,12 +171,7 @@ cdef class CodecContext: def _set_flags2(self, value): self.ptr.flags2 = value - flags2 = Flags2.property( - _get_flags2, - _set_flags2, - """Flag property of :class:`.Flags2`.""" - ) - + flags2 = Flags2.property(_get_flags2, _set_flags2, "Flag property of :class:`.Flags2`.") fast = flags2.flag_property("FAST") no_output = flags2.flag_property("NO_OUTPUT") local_header = flags2.flag_property("LOCAL_HEADER") @@ -261,14 +228,6 @@ cdef class CodecContext: raise ValueError("CodecContext is already open.") return - # We might pass partial frames. - # TODO: What is this for?! This is causing problems with raw decoding - # as the internal parser doesn't seem to see a frame until it sees - # the next one. - # if self.codec.ptr.capabilities & lib.CODEC_CAP_TRUNCATED: - # self.ptr.flags |= lib.CODEC_FLAG_TRUNCATED - - # TODO: Do this better. cdef _Dictionary options = Dictionary() options.update(self.options or {}) @@ -377,7 +336,6 @@ cdef class CodecContext: in_size -= consumed if not in_size: - # Aaaand now we're done. break return packets From a6627f8ac24ac447801a383805186b61944518b3 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 16 Jul 2024 22:56:17 -0400 Subject: [PATCH 312/846] Remove frame.index --- av/audio/frame.pyx | 9 +-------- av/codec/context.pyx | 3 --- av/frame.pxd | 7 ------- av/frame.pyx | 3 +-- av/video/frame.pyx | 4 ++-- include/libavcodec/avcodec.pxd | 2 -- tests/test_codec_context.py | 11 ----------- 7 files changed, 4 insertions(+), 35 deletions(-) diff --git a/av/audio/frame.pyx b/av/audio/frame.pyx index ac3638230..1225f8de2 100644 --- a/av/audio/frame.pyx +++ b/av/audio/frame.pyx @@ -94,7 +94,7 @@ cdef class AudioFrame(Frame): def __repr__(self): return ( - f"" + return f"av.{self.__class__.__name__} pts={self.pts} at 0x{id(self):x}>" cdef _copy_internal_attributes(self, Frame source, bint data_layout=True): """Mimic another frame.""" - self.index = source.index self._time_base = source._time_base lib.av_frame_copy_props(self.ptr, source.ptr) if data_layout: diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 6ff982491..e64eb8573 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -124,8 +124,8 @@ cdef class VideoFrame(Frame): def __repr__(self): return ( - f"" + f"" ) @property diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 16d88a473..cc11a5e9b 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -174,8 +174,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: int global_quality int compression_level - int frame_number - int qmin int qmax int rc_max_rate diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index d30401f4e..59a10a297 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -96,17 +96,6 @@ def test_decoder_gop_size(self): "Using VideoCodecContext.gop_size for decoders is deprecated.", ) - def test_frame_index(self): - container = av.open(fate_suite("h264/interlaced_crop.mp4")) - stream = container.streams[0] - for frame in container.decode(stream): - with warnings.catch_warnings(record=True) as captured: - self.assertIsInstance(frame.index, int) - self.assertEqual( - captured[0].message.args[0], - "Using `frame.index` is deprecated.", - ) - def test_decoder_timebase(self): ctx = av.codec.Codec("h264", "r").create() From 53bb5ca8a4c2d3ea83ea5e463bd46771718b94e4 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 17 Jul 2024 13:11:43 -0400 Subject: [PATCH 313/846] Fix segfault when calling subtitle_stream.decode() --- av/subtitles/codeccontext.pyx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/av/subtitles/codeccontext.pyx b/av/subtitles/codeccontext.pyx index 10e8a6009..c0712c92c 100644 --- a/av/subtitles/codeccontext.pyx +++ b/av/subtitles/codeccontext.pyx @@ -7,13 +7,14 @@ from av.subtitles.subtitle cimport SubtitleProxy, SubtitleSet cdef class SubtitleCodecContext(CodecContext): cdef _send_packet_and_recv(self, Packet packet): + if packet is None: + raise RuntimeError("packet cannot be None") + cdef SubtitleProxy proxy = SubtitleProxy() cdef int got_frame = 0 err_check( - lib.avcodec_decode_subtitle2( - self.ptr, &proxy.struct, &got_frame, packet.ptr if packet else NULL - ) + lib.avcodec_decode_subtitle2(self.ptr, &proxy.struct, &got_frame, packet.ptr) ) if got_frame: From d21b8a2a3340e94b8afdd1b433679ae8e19d6434 Mon Sep 17 00:00:00 2001 From: Moonsik Park Date: Thu, 18 Jul 2024 15:07:24 +0900 Subject: [PATCH 314/846] Flushing subtitle decoder requires a new uninitialized packet --- av/subtitles/stream.pxd | 3 ++- av/subtitles/stream.pyi | 5 ++++- av/subtitles/stream.pyx | 17 +++++++++++++++++ tests/test_subtitles.py | 12 ++++++++++++ 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/av/subtitles/stream.pxd b/av/subtitles/stream.pxd index e21dceb23..745032af9 100644 --- a/av/subtitles/stream.pxd +++ b/av/subtitles/stream.pxd @@ -1,5 +1,6 @@ +from av.packet cimport Packet from av.stream cimport Stream cdef class SubtitleStream(Stream): - pass + cpdef decode(self, Packet packet=?) diff --git a/av/subtitles/stream.pyi b/av/subtitles/stream.pyi index 38f4ae6cb..cb1ac34a2 100644 --- a/av/subtitles/stream.pyi +++ b/av/subtitles/stream.pyi @@ -1,3 +1,6 @@ +from av.packet import Packet from av.stream import Stream +from av.subtitles.subtitle import SubtitleSet -class SubtitleStream(Stream): ... +class SubtitleStream(Stream): + def decode(self, packet: Packet | None = None) -> list[SubtitleSet]: ... diff --git a/av/subtitles/stream.pyx b/av/subtitles/stream.pyx index 1deed5867..9f90b9871 100644 --- a/av/subtitles/stream.pyx +++ b/av/subtitles/stream.pyx @@ -1,6 +1,23 @@ +from av.packet cimport Packet +from av.stream cimport Stream + + cdef class SubtitleStream(Stream): """ A :class:`SubtitleStream` can contain many :class:`SubtitleSet` objects accessible via decoding. """ def __getattr__(self, name): return getattr(self.codec_context, name) + + cpdef decode(self, Packet packet=None): + """ + Decode a :class:`.Packet` and return a list of :class:`.SubtitleSet`. + + :rtype: list[SubtitleSet] + + .. seealso:: This is a passthrough to :meth:`.CodecContext.decode`. + """ + if not packet: + packet = Packet() + + return self.codec_context.decode(packet) diff --git a/tests/test_subtitles.py b/tests/test_subtitles.py index cc1409f2b..9a5e9ceb1 100644 --- a/tests/test_subtitles.py +++ b/tests/test_subtitles.py @@ -58,3 +58,15 @@ def test_vobsub(self): bms = sub.planes self.assertEqual(len(bms), 1) self.assertEqual(len(memoryview(bms[0])), 4800) + + def test_subtitle_flush(self) -> None: + path = fate_suite("sub/MovText_capability_tester.mp4") + + subs = [] + with av.open(path) as container: + stream = container.streams.subtitles[0] + for packet in container.demux(stream): + subs.extend(stream.decode(packet)) + subs.extend(stream.decode()) + + self.assertEqual(len(subs), 3) From 8720cd5b60e2aca962121d59427a691d832a6351 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 18 Jul 2024 23:03:46 -0400 Subject: [PATCH 315/846] Use ffmpeg 6.1.1-5 --- scripts/ffmpeg-6.1.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ffmpeg-6.1.json b/scripts/ffmpeg-6.1.json index d2b41f4c1..9bae82dd3 100644 --- a/scripts/ffmpeg-6.1.json +++ b/scripts/ffmpeg-6.1.json @@ -1,3 +1,3 @@ { - "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/6.1.1-4/ffmpeg-{platform}.tar.gz"] + "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/6.1.1-5/ffmpeg-{platform}.tar.gz"] } From 68ac522517f00696928221f13ce7c830af47ada6 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 20 Jul 2024 12:35:58 -0400 Subject: [PATCH 316/846] Update authors and changelog --- AUTHORS.rst | 5 ++++- CHANGELOG.rst | 22 +++++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 68595a7da..af7ea08cc 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -24,11 +24,12 @@ All contributors (by number of commits): - Xinran Xu ; `@xxr3376 `_ - Dan Allan ; `@danielballan `_ - Dave Johansen +- Moonsik Park +- Santtu Keskinen - Christoph Rackwitz - Alireza Davoudi ; `@adavoudi `_ - Jonathan Drolet - Moritz Kassner ; `@mkassner `_ -- Santtu Keskinen - Thomas A Caswell ; `@tacaswell `_ - Ulrik Mikaelsson ; `@rawler `_ - Wel C. van der @@ -43,6 +44,7 @@ All contributors (by number of commits): * Karl Litterfeldt ; `@litterfeldt `_ * Martin Larralde * Simon-Martin Schröder +* Matteo Destro * mephi42 * Miles Kaufmann * Pablo Prietz @@ -61,6 +63,7 @@ All contributors (by number of commits): * Maxime Desroches * egao1980 * Eric Kalosa-Kenyon +* elxy * Gemfield * Jonathan Martin * Johan Jeppsson Karlin diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3f29ef131..dad978552 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,26 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. + +v12.3.0 +------- + +Features: + +- Support libav's `av_log_set_level` by @materight in (:issue:`1448`). +- Add Graph.link_nodes by @WyattBlue in (:issue:`1449`). +- Add default codec properties by @WyattBlue in (:issue:`1450`). +- Use ffmpeg 6.1.1-5 by @WyattBlue in (:issue:`1462`). +- Add text and dialogue property to AssSubtitle, remove TextSubtitle by @WyattBlue in (:issue:`1456`). + +Fixes: + +- Include libav headers in final distribution by @materight in (:issue:`1455`). +- Fix segfault when calling subtitle_stream.decode() by @WyattBlue in (:issue:`1460`). +- flushing subtitle decoder requires a new uninitialized packet by @moonsikpark in (:issue:`1461`). +- Set default color range for VideoReformatter.format() by @elxy in (:issue:`1458`). +- Resampler: format, layout accepts `str` `int` too by @WyattBlue in (:issue:`1446`). + v12.2.0 ------- @@ -44,7 +64,7 @@ Fixes: - Fix type stubs, add missing type stubs. - Add S12M_TIMECODE by @WyattBlue in (:issue:`1381`). -- Subtitle.text now returns bytes by @WyattBlue in (:issue:`13981). +- Subtitle.text now returns bytes by @WyattBlue in (:issue:`1398`). - Allow packet.duration to be writable by @WyattBlue in (:issue:`1399`). - Remove deprecated `VideoStream.frame_rate` by @WyattBlue in (:issue:`1351`). - Build with Arm for PyPy now by @WyattBlue in (:issue:`1395`). From b1ba1e7d13620dc3c7c8e861ab1b332fdc1f3747 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 20 Jul 2024 19:54:10 -0400 Subject: [PATCH 317/846] Add container.supported_codecs --- av/container/output.pyi | 2 ++ av/container/output.pyx | 20 ++++++++++++++++++++ include/libavcodec/avcodec.pxd | 15 +++++++-------- include/libavformat/avformat.pxd | 2 ++ tests/test_containerformat.py | 3 +++ 5 files changed, 34 insertions(+), 8 deletions(-) diff --git a/av/container/output.pyi b/av/container/output.pyi index 882592667..9e46b413a 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -25,3 +25,5 @@ class OutputContainer(Container): def default_audio_codec(self) -> str: ... @property def default_subtitle_codec(self) -> str: ... + @property + def supported_codecs(self) -> set[str]: ... diff --git a/av/container/output.pyx b/av/container/output.pyx index 184a30b2d..c4e39263d 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -194,6 +194,26 @@ cdef class OutputContainer(Container): self._started = True + @property + def supported_codecs(self): + """ + Returns a set of all codecs this format supports. + """ + result = set() + cdef const lib.AVCodec *codec = NULL + cdef void *opaque = NULL + + while True: + codec = lib.av_codec_iterate(&opaque) + if codec == NULL: + break + + if lib.avformat_query_codec(self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL) == 1: + result.add(codec.name) + + return result + + @property def default_video_codec(self): """ diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index cc11a5e9b..d4335b2a9 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -1,15 +1,14 @@ from libc.stdint cimport int8_t, int64_t, uint16_t, uint32_t +cdef extern from "libavcodec/codec.h": + struct AVCodecTag: + pass + +cdef extern from "libavcodec/codec_id.h": + AVCodecID av_codec_get_id(const AVCodecTag *const *tags, uint32_t tag) + cdef extern from "libavcodec/avcodec.h" nogil: - """ - // AV_FRAME_DATA_SEI_UNREGISTERED available since version 56.54.100 of libavutil (FFmpeg >= 4.4) - #define HAS_AV_FRAME_DATA_SEI_UNREGISTERED (LIBAVUTIL_VERSION_INT >= 3683940) - - #if !HAS_AV_FRAME_DATA_SEI_UNREGISTERED - #define AV_FRAME_DATA_SEI_UNREGISTERED -1 - #endif - """ cdef set pyav_get_available_codecs() cdef int avcodec_version() diff --git a/include/libavformat/avformat.pxd b/include/libavformat/avformat.pxd index 9d9061cc2..f51ba269b 100644 --- a/include/libavformat/avformat.pxd +++ b/include/libavformat/avformat.pxd @@ -115,6 +115,8 @@ cdef extern from "libavformat/avformat.h" nogil: # const AVCodecTag* const *codec_tag const AVClass *priv_class + int avformat_query_codec(const AVOutputFormat *oformat, AVCodecID codec_id, int std_compliance) + # AVInputFormat.flags and AVOutputFormat.flags cdef enum: AVFMT_NOFILE diff --git a/tests/test_containerformat.py b/tests/test_containerformat.py index f0460b131..5b6d31c35 100644 --- a/tests/test_containerformat.py +++ b/tests/test_containerformat.py @@ -9,6 +9,7 @@ def test_matroska(self) -> None: self.assertNotEqual(container.default_video_codec, "none") self.assertNotEqual(container.default_audio_codec, "none") self.assertEqual(container.default_subtitle_codec, "ass") + self.assertIn("ass", container.supported_codecs) fmt = ContainerFormat("matroska") self.assertTrue(fmt.is_input) @@ -23,6 +24,7 @@ def test_mov(self) -> None: self.assertNotEqual(container.default_video_codec, "none") self.assertNotEqual(container.default_audio_codec, "none") self.assertEqual(container.default_subtitle_codec, "none") + self.assertIn("h264", container.supported_codecs) fmt = ContainerFormat("mov") self.assertTrue(fmt.is_input) @@ -37,6 +39,7 @@ def test_gif(self) -> None: self.assertEqual(container.default_video_codec, "gif") self.assertEqual(container.default_audio_codec, "none") self.assertEqual(container.default_subtitle_codec, "none") + self.assertIn("gif", container.supported_codecs) def test_stream_segment(self) -> None: # This format goes by two names, check both. From 4baf011d9d76cb99e1d7deb96c819632cc4a8fff Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 20 Jul 2024 20:27:56 -0400 Subject: [PATCH 318/846] Update changelog --- CHANGELOG.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index dad978552..2b7de678b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -25,7 +25,8 @@ Features: - Support libav's `av_log_set_level` by @materight in (:issue:`1448`). - Add Graph.link_nodes by @WyattBlue in (:issue:`1449`). - Add default codec properties by @WyattBlue in (:issue:`1450`). -- Use ffmpeg 6.1.1-5 by @WyattBlue in (:issue:`1462`). +- Remove the xvid and ass packages in ffmpeg binaries because they were unused by @WyattBlue in (:issue:`1462`). +- Add supported_codecs property to OutputContainer by @WyattBlue in (:issue:`1465`). - Add text and dialogue property to AssSubtitle, remove TextSubtitle by @WyattBlue in (:issue:`1456`). Fixes: From 0b5e4dfeef351042ed94aff5b7b7c4510ba1aae9 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 20 Jul 2024 20:28:33 -0400 Subject: [PATCH 319/846] Release 12.3.0 --- av/about.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/about.py b/av/about.py index b0de45783..e4400ae60 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "12.3.0rc1" +__version__ = "12.3.0" From 55953089f078d5691d5009772dc2f7710aca74aa Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 20 Jul 2024 22:28:50 -0400 Subject: [PATCH 320/846] Bump to next dev version --- av/about.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/about.py b/av/about.py index e4400ae60..79a759ae1 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "12.3.0" +__version__ = "13.0.0rc1" From b2193fd120bdc1f8b6b5b836e72aa895808729a4 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 25 Jul 2024 21:08:05 -0400 Subject: [PATCH 321/846] Build with ffmpeg 7 --- .github/workflows/smoke.yml | 3 + av/audio/codeccontext.pxd | 2 - av/audio/codeccontext.pyi | 4 +- av/audio/codeccontext.pyx | 24 +------ av/audio/fifo.pyx | 6 +- av/audio/frame.pxd | 3 +- av/audio/frame.pyx | 24 +++---- av/audio/layout.pxd | 26 ++------ av/audio/layout.pyi | 10 +-- av/audio/layout.pyx | 108 ++++++------------------------ av/audio/resampler.pyx | 31 +++++---- av/codec/codec.pyx | 7 +- av/codec/context.pyx | 4 -- av/container/core.pyx | 18 ++--- av/container/output.pyx | 4 +- av/container/pyio.pxd | 12 +--- av/container/pyio.pyx | 17 +++-- av/frame.pyx | 3 +- av/option.pyx | 6 +- include/libavcodec/avcodec.pxd | 48 +++++++++++--- include/libavformat/avformat.pxd | 2 +- include/libavutil/avutil.pxd | 14 +--- scripts/activate.sh | 2 +- scripts/build-deps | 1 + scripts/ffmpeg-7.0.json | 3 + tests/test_audiolayout.py | 47 ++++--------- tests/test_audioresampler.py | 91 +++++++++++++------------- tests/test_codec.py | 18 ++--- tests/test_codec_context.py | 109 +------------------------------ tests/test_encode.py | 8 +-- tests/test_filters.py | 2 +- 31 files changed, 207 insertions(+), 450 deletions(-) create mode 100644 scripts/ffmpeg-7.0.json diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 39cf096cd..22d9235b7 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -38,8 +38,10 @@ jobs: matrix: config: - {os: ubuntu-latest, python: "3.8", ffmpeg: "6.1", extras: true} + # TODO: - {os: ubuntu-latest, python: "3.8", ffmpeg: "7.0"} - {os: ubuntu-latest, python: "3.8", ffmpeg: "6.0"} - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1"} + - {os: macos-12, python: "3.8", ffmpeg: "7.0"} - {os: macos-12, python: "3.8", ffmpeg: "6.1"} env: @@ -116,6 +118,7 @@ jobs: fail-fast: false matrix: config: + - {os: windows-latest, python: "3.8", ffmpeg: "7.0"} - {os: windows-latest, python: "3.8", ffmpeg: "6.1"} - {os: windows-latest, python: "3.8", ffmpeg: "6.0"} diff --git a/av/audio/codeccontext.pxd b/av/audio/codeccontext.pxd index 277d47780..55ad15e9f 100644 --- a/av/audio/codeccontext.pxd +++ b/av/audio/codeccontext.pxd @@ -5,9 +5,7 @@ from av.codec.context cimport CodecContext cdef class AudioCodecContext(CodecContext): - # Hold onto the frames that we will decode until we have a full one. cdef AudioFrame next_frame - # For encoding. cdef AudioResampler resampler diff --git a/av/audio/codeccontext.pyi b/av/audio/codeccontext.pyi index ada12efcb..e11a42cdc 100644 --- a/av/audio/codeccontext.pyi +++ b/av/audio/codeccontext.pyi @@ -11,12 +11,12 @@ class AudioCodecContext(CodecContext): frame_size: int sample_rate: int rate: int - channels: int - channel_layout: int layout: AudioLayout format: AudioFormat type: Literal["audio"] + @property + def channels(self) -> int: ... def encode(self, frame: AudioFrame | None = None) -> list[Packet]: ... def encode_lazy(self, frame: AudioFrame | None = None) -> Iterator[Packet]: ... def decode(self, packet: Packet | None = None) -> list[AudioFrame]: ... diff --git a/av/audio/codeccontext.pyx b/av/audio/codeccontext.pyx index 1978759ae..1723cbac7 100644 --- a/av/audio/codeccontext.pyx +++ b/av/audio/codeccontext.pyx @@ -11,13 +11,6 @@ cdef class AudioCodecContext(CodecContext): cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec): CodecContext._init(self, ptr, codec) - # Sometimes there isn't a layout set, but there are a number of - # channels. Assume it is the default layout. - # TODO: Put this behind `not bare_metal`. - # TODO: Do this more efficiently. - if self.ptr.channels and not self.ptr.channel_layout: - self.ptr.channel_layout = get_audio_layout(self.ptr.channels, 0).layout - cdef _set_default_time_base(self): self.ptr.time_base.num = 1 self.ptr.time_base.den = self.ptr.sample_rate @@ -62,7 +55,6 @@ cdef class AudioCodecContext(CodecContext): """ return self.ptr.frame_size - @property def sample_rate(self): """ @@ -85,18 +77,9 @@ cdef class AudioCodecContext(CodecContext): def rate(self, value): self.sample_rate = value - # TODO: Integrate into AudioLayout. @property def channels(self): - return self.ptr.channels - - @channels.setter - def channels(self, value): - self.ptr.channels = value - self.ptr.channel_layout = lib.av_get_default_channel_layout(value) - @property - def channel_layout(self): - return self.ptr.channel_layout + return self.layout.nb_channels @property def layout(self): @@ -105,13 +88,12 @@ cdef class AudioCodecContext(CodecContext): :type: AudioLayout """ - return get_audio_layout(self.ptr.channels, self.ptr.channel_layout) + return get_audio_layout(self.ptr.ch_layout) @layout.setter def layout(self, value): cdef AudioLayout layout = AudioLayout(value) - self.ptr.channel_layout = layout.layout - self.ptr.channels = layout.nb_channels + self.ptr.ch_layout = layout.layout @property def format(self): diff --git a/av/audio/fifo.pyx b/av/audio/fifo.pyx index 83d9cc71d..9b1380270 100644 --- a/av/audio/fifo.pyx +++ b/av/audio/fifo.pyx @@ -61,7 +61,7 @@ cdef class AudioFifo: self.ptr = lib.av_audio_fifo_alloc( frame.ptr.format, - len(frame.layout.channels), # TODO: Can we safely use frame.ptr.nb_channels? + frame.layout.nb_channels, frame.ptr.nb_samples * 2, # Just a default number of samples; it will adjust. ) @@ -71,7 +71,7 @@ cdef class AudioFifo: # Make sure nothing changed. elif ( frame.ptr.format != self.template.ptr.format or - frame.ptr.channel_layout != self.template.ptr.channel_layout or + # TODO: frame.ptr.ch_layout != self.template.ptr.ch_layout or frame.ptr.sample_rate != self.template.ptr.sample_rate or (frame._time_base.num and self.template._time_base.num and ( frame._time_base.num != self.template._time_base.num or @@ -131,7 +131,7 @@ cdef class AudioFifo: frame._copy_internal_attributes(self.template) frame._init( self.template.ptr.format, - self.template.ptr.channel_layout, + self.template.ptr.ch_layout, samples, 1, # Align? ) diff --git a/av/audio/frame.pxd b/av/audio/frame.pxd index e7ee88591..398d76d33 100644 --- a/av/audio/frame.pxd +++ b/av/audio/frame.pxd @@ -7,7 +7,6 @@ from av.frame cimport Frame cdef class AudioFrame(Frame): - # For raw storage of the frame's data; don't ever touch this. cdef uint8_t *_buffer cdef size_t _buffer_size @@ -26,7 +25,7 @@ cdef class AudioFrame(Frame): :type: AudioFormat """ - cdef _init(self, lib.AVSampleFormat format, uint64_t layout, unsigned int nb_samples, unsigned int align) + cdef _init(self, lib.AVSampleFormat format, lib.AVChannelLayout layout, unsigned int nb_samples, unsigned int align) cdef _init_user_attributes(self) cdef AudioFrame alloc_audio_frame() diff --git a/av/audio/frame.pyx b/av/audio/frame.pyx index 1225f8de2..1e45842ea 100644 --- a/av/audio/frame.pyx +++ b/av/audio/frame.pyx @@ -4,10 +4,6 @@ from av.audio.plane cimport AudioPlane from av.error cimport err_check from av.utils cimport check_ndarray, check_ndarray_shape -import warnings - -from av.deprecation import AVDeprecationWarning - cdef object _cinit_bypass_sentinel @@ -47,26 +43,22 @@ cdef class AudioFrame(Frame): cdef AudioLayout cy_layout = AudioLayout(layout) self._init(cy_format.sample_fmt, cy_layout.layout, samples, align) - cdef _init(self, lib.AVSampleFormat format, uint64_t layout, unsigned int nb_samples, unsigned int align): - + cdef _init(self, lib.AVSampleFormat format, lib.AVChannelLayout layout, unsigned int nb_samples, unsigned int align): self.ptr.nb_samples = nb_samples self.ptr.format = format - self.ptr.channel_layout = layout + self.ptr.ch_layout = layout # Sometimes this is called twice. Oh well. self._init_user_attributes() - # Audio filters need AVFrame.channels to match number of channels from layout. - self.ptr.channels = self.layout.nb_channels - - if self.layout.channels and nb_samples: + if self.layout.nb_channels != 0 and nb_samples: # Cleanup the old buffer. lib.av_freep(&self._buffer) # Get a new one. self._buffer_size = err_check(lib.av_samples_get_buffer_size( NULL, - len(self.layout.channels), + self.layout.nb_channels, nb_samples, format, align @@ -78,7 +70,7 @@ cdef class AudioFrame(Frame): # Connect the data pointers to the buffer. err_check(lib.avcodec_fill_audio_frame( self.ptr, - len(self.layout.channels), + self.layout.nb_channels, self.ptr.format, self._buffer, self._buffer_size, @@ -89,7 +81,7 @@ cdef class AudioFrame(Frame): lib.av_freep(&self._buffer) cdef _init_user_attributes(self): - self.layout = get_audio_layout(0, self.ptr.channel_layout) + self.layout = get_audio_layout(self.ptr.ch_layout) self.format = get_audio_format(self.ptr.format) def __repr__(self): @@ -114,7 +106,7 @@ cdef class AudioFrame(Frame): ) # check input format - nb_channels = len(AudioLayout(layout).channels) + nb_channels = AudioLayout(layout).nb_channels check_ndarray(array, dtype, 2) if AudioFormat(format).is_planar: check_ndarray_shape(array, array.shape[0] == nb_channels) @@ -188,6 +180,6 @@ cdef class AudioFrame(Frame): if self.format.is_planar: count = self.samples else: - count = self.samples * len(self.layout.channels) + count = self.samples * self.layout.nb_channels return np.vstack([np.frombuffer(x, dtype=dtype, count=count) for x in self.planes]) diff --git a/av/audio/layout.pxd b/av/audio/layout.pxd index c46b9c741..c7a2368f1 100644 --- a/av/audio/layout.pxd +++ b/av/audio/layout.pxd @@ -1,26 +1,8 @@ -from libc.stdint cimport uint64_t +cimport libav as lib cdef class AudioLayout: + cdef lib.AVChannelLayout layout + cdef _init(self, lib.AVChannelLayout layout) - # The layout for FFMpeg; this is essentially a bitmask of channels. - cdef uint64_t layout - cdef int nb_channels - - cdef readonly tuple channels - """ - A tuple of :class:`AudioChannel` objects. - - :type: tuple - """ - - cdef _init(self, uint64_t layout) - - -cdef class AudioChannel: - - # The channel for FFmpeg. - cdef uint64_t channel - - -cdef AudioLayout get_audio_layout(int channels, uint64_t c_layout) +cdef AudioLayout get_audio_layout(lib.AVChannelLayout c_layout) diff --git a/av/audio/layout.pyi b/av/audio/layout.pyi index a59398fcc..073cd1723 100644 --- a/av/audio/layout.pyi +++ b/av/audio/layout.pyi @@ -1,12 +1,4 @@ -channel_descriptions: dict[str, str] - class AudioLayout: name: str - layout: int nb_channels: int - channels: tuple[AudioChannel, ...] - def __init__(self, layout: int | str | AudioLayout): ... - -class AudioChannel: - name: str - description: str + def __init__(self, layout: str | AudioLayout): ... diff --git a/av/audio/layout.pyx b/av/audio/layout.pyx index 76edd177d..59753138e 100644 --- a/av/audio/layout.pyx +++ b/av/audio/layout.pyx @@ -3,118 +3,50 @@ cimport libav as lib cdef object _cinit_bypass_sentinel -cdef AudioLayout get_audio_layout(int channels, uint64_t c_layout): +cdef AudioLayout get_audio_layout(lib.AVChannelLayout c_layout): """Get an AudioLayout from Cython land.""" cdef AudioLayout layout = AudioLayout.__new__(AudioLayout, _cinit_bypass_sentinel) - if channels and not c_layout: - c_layout = default_layouts[channels] layout._init(c_layout) return layout -# TODO: What about av_get_default_channel_layout(...)? -cdef uint64_t default_layouts[17] -default_layouts[0] = 0 -default_layouts[1] = lib.AV_CH_LAYOUT_MONO -default_layouts[2] = lib.AV_CH_LAYOUT_STEREO -default_layouts[3] = lib.AV_CH_LAYOUT_2POINT1 -default_layouts[4] = lib.AV_CH_LAYOUT_4POINT0 -default_layouts[5] = lib.AV_CH_LAYOUT_5POINT0_BACK -default_layouts[6] = lib.AV_CH_LAYOUT_5POINT1_BACK -default_layouts[7] = lib.AV_CH_LAYOUT_6POINT1 -default_layouts[8] = lib.AV_CH_LAYOUT_7POINT1 -default_layouts[9] = 0x01FF -default_layouts[10] = 0x03FF -default_layouts[11] = 0x07FF -default_layouts[12] = 0x0FFF -default_layouts[13] = 0x1FFF -default_layouts[14] = 0x3FFF -default_layouts[15] = 0x7FFF -default_layouts[16] = 0xFFFF - - -cdef dict channel_descriptions = { - "FL": "front left", - "FR": "front right", - "FC": "front center", - "LFE": "low frequency", - "BL": "back left", - "BR": "back right", - "FLC": "front left-of-center", - "FRC": "front right-of-center", - "BC": "back center", - "SL": "side left", - "SR": "side right", - "TC": "top center", - "TFL": "top front left", - "TFC": "top front center", - "TFR": "top front right", - "TBL": "top back left", - "TBC": "top back center", - "TBR": "top back right", - "DL": "downmix left", - "DR": "downmix right", - "WL": "wide left", - "WR": "wide right", - "SDL": "surround direct left", - "SDR": "surround direct right", - "LFE2": "low frequency 2", -} - - cdef class AudioLayout: def __init__(self, layout): if layout is _cinit_bypass_sentinel: return - cdef uint64_t c_layout - if isinstance(layout, int): - if layout < 0 or layout > 8: - raise ValueError(f"no layout with {layout} channels") - - c_layout = default_layouts[layout] - elif isinstance(layout, str): - c_layout = lib.av_get_channel_layout(layout) + if type(layout) is str: + ret = lib.av_channel_layout_from_string(&c_layout, layout) + if ret != 0: + raise ValueError(f"Invalid layout: {layout}") elif isinstance(layout, AudioLayout): c_layout = (layout).layout else: - raise TypeError("layout must be str or int") - - if not c_layout: - raise ValueError(f"invalid channel layout: {layout}") + raise TypeError(f"layout must be of type: string | av.AudioLayout, got {type(layout)}") self._init(c_layout) - cdef _init(self, uint64_t layout): + cdef _init(self, lib.AVChannelLayout layout): self.layout = layout - self.nb_channels = lib.av_get_channel_layout_nb_channels(layout) # This just counts bits. - self.channels = tuple(AudioChannel(self, i) for i in range(self.nb_channels)) def __repr__(self): return f"" - @property - def name(self): - """The canonical name of the audio layout.""" - cdef char out[32] - # Passing 0 as number of channels... fix this later? - lib.av_get_channel_layout_string(out, 32, 0, self.layout) - return out - + def __eq__(self, other): + return isinstance(other, AudioLayout) and self.name == other.name and self.nb_channels == other.nb_channels -cdef class AudioChannel: - def __cinit__(self, AudioLayout layout, int index): - self.channel = lib.av_channel_layout_extract_channel(layout.layout, index) - - def __repr__(self): - return f"" + @property + def nb_channels(self): + return self.layout.nb_channels @property def name(self): - """The canonical name of the audio channel.""" - return lib.av_get_channel_name(self.channel) + """The canonical name of the audio layout.""" + cdef char layout_name[128] # Adjust buffer size as needed + cdef int ret - @property - def description(self): - """A human description of the audio channel.""" - return channel_descriptions.get(self.name) + ret = lib.av_channel_layout_describe(&self.layout, layout_name, sizeof(layout_name)) + if ret < 0: + raise RuntimeError(f"Failed to get layout name: {ret}") + + return layout_name diff --git a/av/audio/resampler.pyx b/av/audio/resampler.pyx index 89a5428e1..59cddec8c 100644 --- a/av/audio/resampler.pyx +++ b/av/audio/resampler.pyx @@ -23,8 +23,9 @@ cdef class AudioResampler: def __cinit__(self, format=None, layout=None, rate=None, frame_size=None): if format is not None: self.format = format if isinstance(format, AudioFormat) else AudioFormat(format) + if layout is not None: - self.layout = layout if isinstance(layout, AudioLayout) else AudioLayout(layout) + self.layout = AudioLayout(layout) self.rate = int(rate) if rate else 0 self.frame_size = int(frame_size) if frame_size else 0 @@ -62,7 +63,7 @@ cdef class AudioResampler: # Check if we can passthrough or if there is actually work to do. if ( frame.format.sample_fmt == self.format.sample_fmt and - frame.layout.layout == self.layout.layout and + frame.layout == self.layout and frame.sample_rate == self.rate and self.frame_size == 0 ): @@ -75,15 +76,19 @@ cdef class AudioResampler: extra_args = {} if frame.time_base is not None: extra_args["time_base"] = str(frame.time_base) - abuffer = self.graph.add("abuffer", - sample_rate=str(frame.sample_rate), - sample_fmt=AudioFormat(frame.format).name, - channel_layout=frame.layout.name, - **extra_args) - aformat = self.graph.add("aformat", - sample_rates=str(self.rate), - sample_fmts=self.format.name, - channel_layouts=str(self.layout.layout)) + abuffer = self.graph.add( + "abuffer", + sample_rate=str(frame.sample_rate), + sample_fmt=AudioFormat(frame.format).name, + channel_layout=frame.layout.name, + **extra_args, + ) + aformat = self.graph.add( + "aformat", + sample_rates=str(self.rate), + sample_fmts=self.format.name, + channel_layouts=self.layout.name, + ) abuffersink = self.graph.add("abuffersink") abuffer.link_to(aformat) aformat.link_to(abuffersink) @@ -93,11 +98,9 @@ cdef class AudioResampler: lib.av_buffersink_set_frame_size((abuffersink).ptr, self.frame_size) elif frame is not None: - - # Assert the settings are the same on consecutive frames. if ( frame.format.sample_fmt != self.template.format.sample_fmt or - frame.layout.layout != self.template.layout.layout or + frame.layout != self.template.layout or frame.sample_rate != self.template.rate ): raise ValueError("Frame does not match AudioResampler setup.") diff --git a/av/codec/codec.pyx b/av/codec/codec.pyx index b350a8b4f..9c4b7ed94 100644 --- a/av/codec/codec.pyx +++ b/av/codec/codec.pyx @@ -139,7 +139,6 @@ class UnknownCodecError(ValueError): cdef class Codec: - """Codec(name, mode='r') :param str name: The codec name. @@ -161,7 +160,6 @@ cdef class Codec: """ def __cinit__(self, name, mode="r"): - if name is _cinit_sentinel: return @@ -189,7 +187,6 @@ cdef class Codec: raise RuntimeError("Found codec does not match mode.", name, mode) cdef _init(self, name=None): - if not self.ptr: raise UnknownCodecError(name) @@ -218,6 +215,7 @@ cdef class Codec: @property def name(self): return self.ptr.name or "" + @property def long_name(self): return self.ptr.long_name or "" @@ -343,9 +341,8 @@ cdef get_codec_names(): break return names -codecs_available = get_codec_names() - +codecs_available = get_codec_names() codec_descriptor = wrap_avclass(lib.avcodec_get_class()) diff --git a/av/codec/context.pyx b/av/codec/context.pyx index dc71bb2a5..70100e65e 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -532,10 +532,6 @@ cdef class CodecContext: else: raise ValueError("Codec tag should be a 4 character string.") - @property - def ticks_per_frame(self): - return self.ptr.ticks_per_frame - @property def bit_rate(self): return self.ptr.bit_rate if self.ptr.bit_rate > 0 else None diff --git a/av/container/core.pyx b/av/container/core.pyx index df1b25939..683a3f65d 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -100,29 +100,29 @@ cdef int pyav_io_open_gil(lib.AVFormatContext *s, return stash_exception() -cdef void pyav_io_close(lib.AVFormatContext *s, - lib.AVIOContext *pb) noexcept nogil: +cdef int pyav_io_close(lib.AVFormatContext *s, lib.AVIOContext *pb) noexcept nogil: with gil: - pyav_io_close_gil(s, pb) + return pyav_io_close_gil(s, pb) - -cdef void pyav_io_close_gil(lib.AVFormatContext *s, - lib.AVIOContext *pb) noexcept: +cdef int pyav_io_close_gil(lib.AVFormatContext *s, lib.AVIOContext *pb) noexcept: cdef Container container + cdef int result = 0 try: container = dereference(s).opaque if container.open_files is not None and pb.opaque in container.open_files: - pyio_close_custom_gil(pb) + result = pyio_close_custom_gil(pb) # Remove it from the container so that it can be deallocated del container.open_files[pb.opaque] else: - pyio_close_gil(pb) + result = pyio_close_gil(pb) except Exception as e: stash_exception() + result = lib.AVERROR_UNKNOWN # Or another appropriate error code + return result Flags = define_enum("Flags", __name__, ( ("GENPTS", lib.AVFMT_FLAG_GENPTS, @@ -242,7 +242,7 @@ cdef class Container: if io_open is not None: self.ptr.io_open = pyav_io_open - self.ptr.io_close = pyav_io_close + self.ptr.io_close2 = pyav_io_close self.ptr.flags |= lib.AVFMT_FLAG_CUSTOM_IO cdef lib.AVInputFormat *ifmt diff --git a/av/container/output.pyx b/av/container/output.pyx index c4e39263d..00f5d55b3 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -97,7 +97,6 @@ cdef class OutputContainer(Container): codec_context.height = 480 codec_context.bit_rate = 1024000 codec_context.bit_rate_tolerance = 128000 - codec_context.ticks_per_frame = 1 to_avrational(rate or 24, &codec_context.framerate) stream.avg_frame_rate = codec_context.framerate @@ -109,8 +108,7 @@ cdef class OutputContainer(Container): codec_context.bit_rate = 128000 codec_context.bit_rate_tolerance = 32000 codec_context.sample_rate = rate or 48000 - codec_context.channels = 2 - codec_context.channel_layout = lib.AV_CH_LAYOUT_STEREO + lib.av_channel_layout_default(&codec_context.ch_layout, 2) # Some formats want stream headers to be separate if self.ptr.oformat.flags & lib.AVFMT_GLOBALHEADER: diff --git a/av/container/pyio.pxd b/av/container/pyio.pxd index b3ec04087..80edc8a6b 100644 --- a/av/container/pyio.pxd +++ b/av/container/pyio.pxd @@ -3,18 +3,12 @@ from libc.stdint cimport int64_t, uint8_t cdef int pyio_read(void *opaque, uint8_t *buf, int buf_size) noexcept nogil - -cdef int pyio_write(void *opaque, uint8_t *buf, int buf_size) noexcept nogil - +cdef int pyio_write(void *opaque, const uint8_t *buf, int buf_size) noexcept nogil cdef int64_t pyio_seek(void *opaque, int64_t offset, int whence) noexcept nogil - -cdef void pyio_close_gil(lib.AVIOContext *pb) - -cdef void pyio_close_custom_gil(lib.AVIOContext *pb) - +cdef int pyio_close_gil(lib.AVIOContext *pb) +cdef int pyio_close_custom_gil(lib.AVIOContext *pb) cdef class PyIOFile: - # File-like source. cdef readonly object file cdef object fread diff --git a/av/container/pyio.pyx b/av/container/pyio.pyx index ab29cee11..821ae9c0c 100644 --- a/av/container/pyio.pyx +++ b/av/container/pyio.pyx @@ -47,7 +47,8 @@ cdef class PyIOFile: self.buffer = lib.av_malloc(buffer_size) self.iocontext = lib.avio_alloc_context( - self.buffer, buffer_size, + self.buffer, + buffer_size, writeable, self, # User data. pyio_read, @@ -92,11 +93,11 @@ cdef int pyio_read_gil(void *opaque, uint8_t *buf, int buf_size) noexcept: return stash_exception() -cdef int pyio_write(void *opaque, uint8_t *buf, int buf_size) noexcept nogil: +cdef int pyio_write(void *opaque, const uint8_t *buf, int buf_size) noexcept nogil: with gil: return pyio_write_gil(opaque, buf, buf_size) -cdef int pyio_write_gil(void *opaque, uint8_t *buf, int buf_size) noexcept: +cdef int pyio_write_gil(void *opaque, const uint8_t *buf, int buf_size) noexcept: cdef PyIOFile self cdef bytes bytes_to_write cdef int bytes_written @@ -143,24 +144,26 @@ cdef int64_t pyio_seek_gil(void *opaque, int64_t offset, int whence): return stash_exception() -cdef void pyio_close_gil(lib.AVIOContext *pb): +cdef int pyio_close_gil(lib.AVIOContext *pb): try: - lib.avio_close(pb) + return lib.avio_close(pb) except Exception as e: stash_exception() -cdef void pyio_close_custom_gil(lib.AVIOContext *pb): +cdef int pyio_close_custom_gil(lib.AVIOContext *pb): cdef PyIOFile self try: self = pb.opaque # Flush bytes in the AVIOContext buffers to the custom I/O - lib.avio_flush(pb) + result = lib.avio_flush(pb) if self.fclose is not None: self.fclose() + return 0 + except Exception as e: stash_exception() diff --git a/av/frame.pyx b/av/frame.pyx index 7fa8a701e..b3ffff99b 100644 --- a/av/frame.pyx +++ b/av/frame.pyx @@ -33,8 +33,7 @@ cdef class Frame: self.ptr.format = source.ptr.format self.ptr.width = source.ptr.width self.ptr.height = source.ptr.height - self.ptr.channel_layout = source.ptr.channel_layout - self.ptr.channels = source.ptr.channels + self.ptr.ch_layout = source.ptr.ch_layout cdef _init_user_attributes(self): pass # Dummy to match the API of the others. diff --git a/av/option.pyx b/av/option.pyx index 6ffba50fa..0c7b5fb28 100644 --- a/av/option.pyx +++ b/av/option.pyx @@ -25,7 +25,7 @@ OptionType = define_enum("OptionType", __name__, ( ("RATIONAL", lib.AV_OPT_TYPE_RATIONAL), ("BINARY", lib.AV_OPT_TYPE_BINARY), ("DICT", lib.AV_OPT_TYPE_DICT), - # ("UINT64", lib.AV_OPT_TYPE_UINT64), # Added recently, and not yet used AFAICT. + ("UINT64", lib.AV_OPT_TYPE_UINT64), ("CONST", lib.AV_OPT_TYPE_CONST), ("IMAGE_SIZE", lib.AV_OPT_TYPE_IMAGE_SIZE), ("PIXEL_FMT", lib.AV_OPT_TYPE_PIXEL_FMT), @@ -33,7 +33,7 @@ OptionType = define_enum("OptionType", __name__, ( ("VIDEO_RATE", lib.AV_OPT_TYPE_VIDEO_RATE), ("DURATION", lib.AV_OPT_TYPE_DURATION), ("COLOR", lib.AV_OPT_TYPE_COLOR), - ("CHANNEL_LAYOUT", lib.AV_OPT_TYPE_CHANNEL_LAYOUT), + ("CHANNEL_LAYOUT", lib.AV_OPT_TYPE_CHLAYOUT), ("BOOL", lib.AV_OPT_TYPE_BOOL), )) @@ -44,7 +44,7 @@ cdef tuple _INT_TYPES = ( lib.AV_OPT_TYPE_PIXEL_FMT, lib.AV_OPT_TYPE_SAMPLE_FMT, lib.AV_OPT_TYPE_DURATION, - lib.AV_OPT_TYPE_CHANNEL_LAYOUT, + lib.AV_OPT_TYPE_CHLAYOUT, lib.AV_OPT_TYPE_BOOL, ) diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index d4335b2a9..1b509eafc 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -8,6 +8,42 @@ cdef extern from "libavcodec/codec_id.h": AVCodecID av_codec_get_id(const AVCodecTag *const *tags, uint32_t tag) +cdef extern from "libavutil/channel_layout.h": + ctypedef enum AVChannelOrder: + AV_CHANNEL_ORDER_UNSPEC + AV_CHANNEL_ORDER_NATIVE + AV_CHANNEL_ORDER_CUSTOM + AV_CHANNEL_ORDER_AMBISONIC + + ctypedef enum AVChannel: + AV_CHAN_NONE = -1 + AV_CHAN_FRONT_LEFT + AV_CHAN_FRONT_RIGHT + AV_CHAN_FRONT_CENTER + # ... other channel enum values ... + + ctypedef struct AVChannelCustom: + AVChannel id + char name[16] + void *opaque + + ctypedef struct AVChannelLayout: + AVChannelOrder order + int nb_channels + uint64_t mask + # union: + # uint64_t mask + # AVChannelCustom *map + void *opaque + + int av_channel_layout_default(AVChannelLayout *ch_layout, int nb_channels) + int av_channel_layout_from_mask(AVChannelLayout *channel_layout, uint64_t mask) + int av_channel_layout_from_string(AVChannelLayout *channel_layout, const char *str) + void av_channel_layout_uninit(AVChannelLayout *channel_layout) + int av_channel_layout_copy(AVChannelLayout *dst, const AVChannelLayout *src) + int av_channel_layout_describe(const AVChannelLayout *channel_layout, char *buf, size_t buf_size) + + cdef extern from "libavcodec/avcodec.h" nogil: cdef set pyav_get_available_codecs() @@ -116,7 +152,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVDISCARD_ALL cdef struct AVCodec: - char *name char *long_name AVMediaType type @@ -144,7 +179,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVCodecDescriptor* avcodec_descriptor_get(AVCodecID) cdef struct AVCodecContext: - AVClass *av_class AVMediaType codec_type @@ -154,7 +188,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: int flags int flags2 - int thread_count int thread_type @@ -164,12 +197,10 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVFrame* coded_frame int bit_rate - int bit_rate_tolerance int mb_decision int bits_per_coded_sample - int global_quality int compression_level @@ -184,7 +215,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVRational framerate AVRational pkt_timebase AVRational time_base - int ticks_per_frame int extradata_size uint8_t *extradata @@ -212,9 +242,8 @@ cdef extern from "libavcodec/avcodec.h" nogil: # Audio. AVSampleFormat sample_fmt int sample_rate - int channels + AVChannelLayout ch_layout int frame_size - int channel_layout #: .. todo:: ``get_buffer`` is deprecated for get_buffer2 in newer versions of FFmpeg. int get_buffer(AVCodecContext *ctx, AVFrame *frame) @@ -349,8 +378,7 @@ cdef extern from "libavcodec/avcodec.h" nogil: int nb_samples # Audio samples int sample_rate # Audio Sample rate - int channels # Number of audio channels - int channel_layout # Audio channel_layout + AVChannelLayout ch_layout int64_t pts int64_t pkt_dts diff --git a/include/libavformat/avformat.pxd b/include/libavformat/avformat.pxd index f51ba269b..29a892176 100644 --- a/include/libavformat/avformat.pxd +++ b/include/libavformat/avformat.pxd @@ -198,7 +198,7 @@ cdef extern from "libavformat/avformat.h" nogil: int flags, AVDictionary **options ) - void (*io_close)( + int (*io_close2)( AVFormatContext *s, AVIOContext *pb ) diff --git a/include/libavutil/avutil.pxd b/include/libavutil/avutil.pxd index b4184d0de..f874761ea 100644 --- a/include/libavutil/avutil.pxd +++ b/include/libavutil/avutil.pxd @@ -217,12 +217,6 @@ cdef extern from "libavutil/channel_layout.h" nogil: cdef uint64_t av_get_channel_layout(char* name) cdef int av_get_channel_layout_nb_channels(uint64_t channel_layout) cdef int64_t av_get_default_channel_layout(int nb_channels) - cdef void av_get_channel_layout_string( - char* buff, - int buf_size, - int nb_channels, - uint64_t channel_layout - ) # Channels. cdef uint64_t av_channel_layout_extract_channel(uint64_t layout, int index) @@ -260,14 +254,12 @@ cdef extern from "libavutil/audio_fifo.h" nogil: cdef extern from "stdarg.h" nogil: - # For logging. Should really be in another PXD. ctypedef struct va_list: pass cdef extern from "Python.h" nogil: - # For logging. See av/logging.pyx for an explanation. cdef int Py_AddPendingCall(void *, void *) void PyErr_PrintEx(int set_sys_last_vars) @@ -276,9 +268,7 @@ cdef extern from "Python.h" nogil: cdef extern from "libavutil/opt.h" nogil: - cdef enum AVOptionType: - AV_OPT_TYPE_FLAGS AV_OPT_TYPE_INT AV_OPT_TYPE_INT64 @@ -288,7 +278,7 @@ cdef extern from "libavutil/opt.h" nogil: AV_OPT_TYPE_RATIONAL AV_OPT_TYPE_BINARY AV_OPT_TYPE_DICT - #AV_OPT_TYPE_UINT64 # since FFmpeg 3.3 + AV_OPT_TYPE_UINT64 AV_OPT_TYPE_CONST AV_OPT_TYPE_IMAGE_SIZE AV_OPT_TYPE_PIXEL_FMT @@ -296,7 +286,7 @@ cdef extern from "libavutil/opt.h" nogil: AV_OPT_TYPE_VIDEO_RATE AV_OPT_TYPE_DURATION AV_OPT_TYPE_COLOR - AV_OPT_TYPE_CHANNEL_LAYOUT + AV_OPT_TYPE_CHLAYOUT AV_OPT_TYPE_BOOL cdef struct AVOption_default_val: diff --git a/scripts/activate.sh b/scripts/activate.sh index 21eb76488..67cc5462e 100755 --- a/scripts/activate.sh +++ b/scripts/activate.sh @@ -14,7 +14,7 @@ if [[ ! "$PYAV_LIBRARY" ]]; then if [[ "$1" ]]; then PYAV_LIBRARY="$1" else - PYAV_LIBRARY=ffmpeg-6.1.1 + PYAV_LIBRARY=ffmpeg-7.0.1 echo "No \$PYAV_LIBRARY set; defaulting to $PYAV_LIBRARY" fi fi diff --git a/scripts/build-deps b/scripts/build-deps index 33c64727e..f151fc88d 100755 --- a/scripts/build-deps +++ b/scripts/build-deps @@ -43,6 +43,7 @@ echo ./configure --disable-stripping \ --enable-debug=3 \ --enable-gpl \ + --enable-version3 \ --enable-libx264 \ --enable-libxml2 \ --enable-shared \ diff --git a/scripts/ffmpeg-7.0.json b/scripts/ffmpeg-7.0.json new file mode 100644 index 000000000..4e69666eb --- /dev/null +++ b/scripts/ffmpeg-7.0.json @@ -0,0 +1,3 @@ +{ + "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.0.1-1/ffmpeg-{platform}.tar.gz"] +} diff --git a/tests/test_audiolayout.py b/tests/test_audiolayout.py index 2a163186b..113402607 100644 --- a/tests/test_audiolayout.py +++ b/tests/test_audiolayout.py @@ -8,47 +8,22 @@ def test_stereo_from_str(self): layout = AudioLayout("stereo") self._test_stereo(layout) - def test_stereo_from_int(self): - layout = AudioLayout(2) - self._test_stereo(layout) - def test_stereo_from_layout(self): layout = AudioLayout("stereo") layout2 = AudioLayout(layout) self._test_stereo(layout2) - def test_channel_counts(self): - self.assertRaises(ValueError, AudioLayout, -1) - self.assertRaises(ValueError, AudioLayout, 9) - def _test_stereo(self, layout): self.assertEqual(layout.name, "stereo") - self.assertEqual(len(layout.channels), 2) + self.assertEqual(layout.nb_channels, 2) self.assertEqual(repr(layout), "") - self.assertEqual(layout.channels[0].name, "FL") - self.assertEqual(layout.channels[0].description, "front left") - self.assertEqual( - repr(layout.channels[0]), "" - ) - self.assertEqual(layout.channels[1].name, "FR") - self.assertEqual(layout.channels[1].description, "front right") - self.assertEqual( - repr(layout.channels[1]), "" - ) - - def test_defaults(self): - for i, name in enumerate( - """ - mono - stereo - 2.1 - 4.0 - 5.0 - 5.1 - 6.1 - 7.1 - """.strip().split() - ): - layout = AudioLayout(i + 1) - self.assertEqual(layout.name, name) - self.assertEqual(len(layout.channels), i + 1) + # self.assertEqual(layout.channels[0].name, "FL") + # self.assertEqual(layout.channels[0].description, "front left") + # self.assertEqual( + # repr(layout.channels[0]), "" + # ) + # self.assertEqual(layout.channels[1].name, "FR") + # self.assertEqual(layout.channels[1].description, "front right") + # self.assertEqual( + # repr(layout.channels[1]), "" + # ) diff --git a/tests/test_audioresampler.py b/tests/test_audioresampler.py index 2a9ae25c0..dc084fbba 100644 --- a/tests/test_audioresampler.py +++ b/tests/test_audioresampler.py @@ -68,50 +68,53 @@ def test_matching_passthrough(self): oframes = resampler.resample(None) self.assertEqual(len(oframes), 0) - def test_pts_assertion_same_rate(self): - resampler = AudioResampler("s16", "mono") - - # resample one frame - iframe = AudioFrame("s16", "stereo", 1024) - iframe.sample_rate = 48000 - iframe.time_base = "1/48000" - iframe.pts = 0 - - oframes = resampler.resample(iframe) - self.assertEqual(len(oframes), 1) - - oframe = oframes[0] - self.assertEqual(oframe.pts, 0) - self.assertEqual(oframe.time_base, iframe.time_base) - self.assertEqual(oframe.sample_rate, iframe.sample_rate) - self.assertEqual(oframe.samples, iframe.samples) - - # resample another frame - iframe.pts = 1024 - - oframes = resampler.resample(iframe) - self.assertEqual(len(oframes), 1) - - oframe = oframes[0] - self.assertEqual(oframe.pts, 1024) - self.assertEqual(oframe.time_base, iframe.time_base) - self.assertEqual(oframe.sample_rate, iframe.sample_rate) - self.assertEqual(oframe.samples, iframe.samples) - - # resample another frame with a pts gap, do not raise exception - iframe.pts = 9999 - oframes = resampler.resample(iframe) - self.assertEqual(len(oframes), 1) - - oframe = oframes[0] - self.assertEqual(oframe.pts, 9999) - self.assertEqual(oframe.time_base, iframe.time_base) - self.assertEqual(oframe.sample_rate, iframe.sample_rate) - self.assertEqual(oframe.samples, iframe.samples) - - # flush - oframes = resampler.resample(None) - self.assertEqual(len(oframes), 0) + # TODO: Fails on ffmpeg 6 + # def test_pts_assertion_same_rate(self): + # av.logging.set_level(av.logging.VERBOSE) + # resampler = AudioResampler("s16", "mono") + + # # resample one frame + # iframe = AudioFrame("s16", "stereo", 1024) + # iframe.sample_rate = 48000 + # iframe.time_base = "1/48000" + # iframe.pts = 0 + + # oframes = resampler.resample(iframe) + # self.assertEqual(len(oframes), 1) + + # oframe = oframes[0] + # self.assertEqual(oframe.pts, 0) + # self.assertEqual(oframe.time_base, iframe.time_base) + # self.assertEqual(oframe.sample_rate, iframe.sample_rate) + # self.assertEqual(oframe.samples, iframe.samples) + + # # resample another frame + # iframe.pts = 1024 + + # oframes = resampler.resample(iframe) + # self.assertEqual(len(oframes), 1) + + # oframe = oframes[0] + # self.assertEqual(oframe.pts, 1024) + # self.assertEqual(oframe.time_base, iframe.time_base) + # self.assertEqual(oframe.sample_rate, iframe.sample_rate) + # self.assertEqual(oframe.samples, iframe.samples) + + # # resample another frame with a pts gap, do not raise exception + # iframe.pts = 9999 + # oframes = resampler.resample(iframe) + # self.assertEqual(len(oframes), 1) + + # oframe = oframes[0] + # self.assertEqual(oframe.pts, 9999) + # self.assertEqual(oframe.time_base, iframe.time_base) + # self.assertEqual(oframe.sample_rate, iframe.sample_rate) + # self.assertEqual(oframe.samples, iframe.samples) + + # # flush + # oframes = resampler.resample(None) + # self.assertEqual(len(oframes), 0) + # av.logging.set_level(None) def test_pts_assertion_new_rate_up(self): resampler = AudioResampler("s16", "mono", 44100) diff --git a/tests/test_codec.py b/tests/test_codec.py index 98b059fbf..5c0665e8d 100644 --- a/tests/test_codec.py +++ b/tests/test_codec.py @@ -1,6 +1,6 @@ import unittest -from av import AudioFormat, Codec, VideoFormat, codecs_available +from av import AudioFormat, Codec, codecs_available from av.codec.codec import UnknownCodecError from .common import TestCase @@ -36,10 +36,10 @@ def test_codec_mpeg4_decoder(self): self.assertIsNone(c.audio_rates) # video - formats = c.video_formats - self.assertTrue(formats) - self.assertIsInstance(formats[0], VideoFormat) - self.assertTrue(any(f.name == "yuv420p" for f in formats)) + # formats = c.video_formats + # self.assertTrue(formats) + # self.assertIsInstance(formats[0], VideoFormat) + # self.assertTrue(any(f.name == "yuv420p" for f in formats)) self.assertIsNone(c.frame_rates) @@ -58,10 +58,10 @@ def test_codec_mpeg4_encoder(self): self.assertIsNone(c.audio_rates) # video - formats = c.video_formats - self.assertTrue(formats) - self.assertIsInstance(formats[0], VideoFormat) - self.assertTrue(any(f.name == "yuv420p" for f in formats)) + # formats = c.video_formats + # self.assertTrue(formats) + # self.assertIsInstance(formats[0], VideoFormat) + # self.assertTrue(any(f.name == "yuv420p" for f in formats)) self.assertIsNone(c.frame_rates) diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 59a10a297..3ead81fc2 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -399,7 +399,6 @@ def _audio_encoding(self, *, codec_name, channel_layout): sample_fmt = ctx.codec.audio_formats[-1].name sample_rate = 48000 - channels = 2 ctx.time_base = Fraction(1) / sample_rate ctx.sample_rate = sample_rate @@ -418,122 +417,19 @@ def _audio_encoding(self, *, codec_name, channel_layout): samples = 0 packet_sizes = [] - pts_expected = [ - 0, - 1098, - 2212, - 3327, - 4441, - 5556, - 6670, - 7785, - 8900, - 10014, - 11129, - 12243, - 13358, - 14472, - 15587, - 16701, - 17816, - 18931, - 20045, - 21160, - 22274, - 23389, - 24503, - 25618, - 26732, - 27847, - 28962, - 30076, - 31191, - 32305, - 33420, - 34534, - 35649, - 36763, - 37878, - 38993, - 40107, - 41222, - 42336, - 43451, - 44565, - 45680, - 46795, - 47909, - 49024, - 50138, - 51253, - 52367, - 53482, - 54596, - 55711, - 56826, - 57940, - 59055, - 60169, - 61284, - 62398, - 63513, - 64627, - 65742, - 66857, - 67971, - 69086, - 70200, - 71315, - 72429, - 73544, - 74658, - 75773, - 76888, - 78002, - 79117, - 80231, - 81346, - 82460, - 83575, - 84689, - 85804, - 86919, - 88033, - 89148, - 90262, - 91377, - 92491, - 93606, - 94720, - 95835, - 96950, - 98064, - 99179, - 100293, - 101408, - ] - if codec_name == "aac": - pts_expected_encoded = list((-1024 + n * 1024 for n in range(101))) - elif codec_name == "mp2": - pts_expected_encoded = list((-481 + n * 1152 for n in range(89))) - else: - pts_expected_encoded = pts_expected.copy() with open(path, "wb") as f: for frame in iter_frames(container, audio_stream): resampled_frames = resampler.resample(frame) for resampled_frame in resampled_frames: - self.assertEqual(resampled_frame.pts, pts_expected.pop(0)) self.assertEqual(resampled_frame.time_base, Fraction(1, 48000)) samples += resampled_frame.samples for packet in ctx.encode(resampled_frame): - self.assertEqual(packet.pts, pts_expected_encoded.pop(0)) self.assertEqual(packet.time_base, Fraction(1, 48000)) packet_sizes.append(packet.size) f.write(packet) for packet in ctx.encode(None): - self.assertEqual(packet.pts, pts_expected_encoded.pop(0)) self.assertEqual(packet.time_base, Fraction(1, 48000)) packet_sizes.append(packet.size) f.write(packet) @@ -546,10 +442,7 @@ def _audio_encoding(self, *, codec_name, channel_layout): result_samples = 0 - # should have more asserts but not sure what to check - # libav and ffmpeg give different results - # so can really use checksums for frame in iter_raw_frames(path, packet_sizes, ctx): result_samples += frame.samples self.assertEqual(frame.sample_rate, sample_rate) - self.assertEqual(len(frame.layout.channels), channels) + self.assertEqual(frame.layout.nb_channels, 2) diff --git a/tests/test_encode.py b/tests/test_encode.py index e4336c735..58d3b1515 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -121,7 +121,6 @@ def assert_rgb_rotate(self, input_, is_dash=False): self.assertEqual(stream.format.name, "yuv420p") self.assertEqual(stream.format.width, WIDTH) self.assertEqual(stream.format.height, HEIGHT) - self.assertEqual(stream.ticks_per_frame, 1) class TestBasicVideoEncoding(TestCase): @@ -139,7 +138,6 @@ def test_default_options(self): self.assertEqual(stream.format.width, 640) self.assertEqual(stream.height, 480) self.assertEqual(stream.pix_fmt, "yuv420p") - self.assertEqual(stream.ticks_per_frame, 1) self.assertEqual(stream.width, 640) def test_encoding(self): @@ -193,7 +191,6 @@ def test_default_options(self): self.assertEqual(stream.bit_rate, 128000) self.assertEqual(stream.format.name, "s16") self.assertEqual(stream.sample_rate, 48000) - self.assertEqual(stream.ticks_per_frame, 1) def test_transcode(self): path = self.sandboxed("audio_transcode.mov") @@ -204,7 +201,6 @@ def test_transcode(self): sample_rate = 48000 channel_layout = "stereo" - channels = 2 sample_fmt = "s16" stream = output.add_stream("mp2", sample_rate) @@ -215,7 +211,6 @@ def test_transcode(self): ctx.sample_rate = sample_rate ctx.format = sample_fmt ctx.layout = channel_layout - ctx.channels = channels with av.open( fate_suite("audio-reference/chorusnoise_2ch_44kHz_s16.wav") @@ -238,7 +233,6 @@ def test_transcode(self): self.assertIsInstance(stream, AudioStream) # codec context properties - self.assertEqual(stream.channels, channels) self.assertEqual(stream.format.name, "s16p") self.assertEqual(stream.sample_rate, sample_rate) @@ -254,7 +248,7 @@ def test_stream_index(self): astream = output.add_stream("mp2", 48000) self.assertIn(astream, output.streams.audio) - astream.channels = 2 + astream.layout = "stereo" astream.format = "s16" self.assertEqual(vstream.index, 0) diff --git a/tests/test_filters.py b/tests/test_filters.py index 1f3b8ae99..27663081e 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -27,7 +27,7 @@ def generate_audio_frame( frame.sample_rate = sample_rate frame.pts = frame_num * frame_size - for i in range(len(frame.layout.channels)): + for i in range(frame.layout.nb_channels): data = np.zeros(frame_size, dtype=format_dtypes[input_format]) for j in range(frame_size): data[j] = np.sin(2 * np.pi * (frame_num + j) * (i + 1) / float(frame_size)) From 582b23044fdfb41342678ffae990ba1b70bea5df Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 31 Jul 2024 17:12:13 -0400 Subject: [PATCH 322/846] Use pytest instead of setup.py test --- .github/workflows/smoke.yml | 3 --- Makefile | 8 +++++++- scripts/activate.sh | 9 ++++++--- scripts/build-deps | 5 ++--- scripts/test | 2 +- tests/requirements.txt | 1 + 6 files changed, 17 insertions(+), 11 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 22d9235b7..6e85e1d77 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -24,9 +24,6 @@ jobs: with: python-version: "3.12" - - name: Packages - run: pip install -r tests/requirements.txt - - name: Linters run: make lint diff --git a/Makefile b/Makefile index 968494e2e..4830ef6a1 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,9 @@ LDFLAGS ?= "" CFLAGS ?= "-O0" PYAV_PYTHON ?= python +PYAV_PIP ?= pip PYTHON := $(PYAV_PYTHON) +PIP := $(PYAV_PIP) .PHONY: default build clean fate-suite lint test @@ -11,6 +13,8 @@ default: build build: + # Always try to install the Python dependencies they are cheap. + $(PIP) install --upgrade -r tests/requirements.txt CFLAGS=$(CFLAGS) LDFLAGS=$(LDFLAGS) $(PYTHON) setup.py build_ext --inplace --debug clean: @@ -25,10 +29,12 @@ fate-suite: rsync -vrltLW rsync://fate-suite.ffmpeg.org/fate-suite/ tests/assets/fate-suite/ lint: + $(PIP) install --upgrade -r tests/requirements.txt black --check av examples tests setup.py flake8 av examples tests isort --check-only --diff av examples tests mypy av tests test: - $(PYTHON) setup.py test + $(PIP) install --upgrade -r tests/requirements.txt + $(PYTHON) -m pytest diff --git a/scripts/activate.sh b/scripts/activate.sh index 67cc5462e..99be26dcc 100755 --- a/scripts/activate.sh +++ b/scripts/activate.sh @@ -9,10 +9,13 @@ fi export PYAV_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.."; pwd)" if [[ ! "$PYAV_LIBRARY" ]]; then - - # Pull from command line argument. if [[ "$1" ]]; then - PYAV_LIBRARY="$1" + if [[ "$1" == ffmpeg-* ]]; then + PYAV_LIBRARY="$1" + else + echo "Error: PYAV_LIBRARY must start with 'ffmpeg-'" >&2 + return 1 + fi else PYAV_LIBRARY=ffmpeg-7.0.1 echo "No \$PYAV_LIBRARY set; defaulting to $PYAV_LIBRARY" diff --git a/scripts/build-deps b/scripts/build-deps index f151fc88d..ef7ebff22 100755 --- a/scripts/build-deps +++ b/scripts/build-deps @@ -8,12 +8,11 @@ fi cd "$PYAV_ROOT" # Always try to install the Python dependencies they are cheap. -$PYAV_PIP install --upgrade -r tests/requirements.txt - +$PYAV_PIP install --upgrade -r tests/requirements.txt 2> /dev/null # Skip the rest of the build if it already exists. if [[ -e "$PYAV_LIBRARY_PREFIX/bin/ffmpeg" ]]; then - echo "We have a cached build of $PYAV_LIBRARY; skipping re-build." + echo "We have a cached build of ffmpeg-$PYAV_LIBRARY; skipping re-build." exit 0 fi diff --git a/scripts/test b/scripts/test index 01991f5a6..270a4ec69 100755 --- a/scripts/test +++ b/scripts/test @@ -19,7 +19,7 @@ istest() { } if istest main; then - $PYAV_PYTHON setup.py test + $PYAV_PYTHON -m pytest fi if istest examples; then diff --git a/tests/requirements.txt b/tests/requirements.txt index f9f354439..2907b9e22 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -5,5 +5,6 @@ black isort flake8 flake8-pyproject +pytest sphinx==5.1.0 mypy==1.10.0 From b68ec33c27db641ecef9643d50b837ffa91f4090 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 31 Jul 2024 19:25:46 -0400 Subject: [PATCH 323/846] Fix resampler for ffmpeg 6 To get ffmpeg 6 to work, we need to set AVFrame.channels. This does not exist in FFmpeg 7, and worse yet, Cython's compile time is terrible. To get the builds working for both 6 and 7, we implement our own comptime in scripts/comptime. --- .github/workflows/smoke.yml | 1 + Makefile | 2 +- av/audio/frame.pyx | 1 + include/libavcodec/avcodec.pxd | 2 + scripts/build | 1 + scripts/comptime.py | 30 +++++++++++ tests/test_audioresampler.py | 95 +++++++++++++++++----------------- 7 files changed, 84 insertions(+), 48 deletions(-) create mode 100644 scripts/comptime.py diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 6e85e1d77..485227334 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -142,6 +142,7 @@ jobs: . $CONDA/etc/profile.d/conda.sh conda activate pyav python scripts\\fetch-vendor.py --config-file scripts\\ffmpeg-${{ matrix.config.ffmpeg }}.json $CONDA_PREFIX\\Library + python scripts\\comptime.py ${{ matrix.config.ffmpeg }} python setup.py build_ext --inplace --ffmpeg-dir=$CONDA_PREFIX\\Library - name: Test diff --git a/Makefile b/Makefile index 4830ef6a1..a31c7077b 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ LDFLAGS ?= "" -CFLAGS ?= "-O0" +CFLAGS ?= "-O0 -Wno-incompatible-function-pointer-types" PYAV_PYTHON ?= python PYAV_PIP ?= pip diff --git a/av/audio/frame.pyx b/av/audio/frame.pyx index 1e45842ea..8260ccd2a 100644 --- a/av/audio/frame.pyx +++ b/av/audio/frame.pyx @@ -47,6 +47,7 @@ cdef class AudioFrame(Frame): self.ptr.nb_samples = nb_samples self.ptr.format = format self.ptr.ch_layout = layout + # [FFMPEG6] self.ptr.channels = layout.nb_channels # ffmpeg 6 only # Sometimes this is called twice. Oh well. self._init_user_attributes() diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 1b509eafc..9e9cc46a7 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -378,6 +378,8 @@ cdef extern from "libavcodec/avcodec.h" nogil: int nb_samples # Audio samples int sample_rate # Audio Sample rate + # [FFMPEG6] int channels + AVChannelLayout ch_layout int64_t pts diff --git a/scripts/build b/scripts/build index 8d7e3b06e..c1e711d69 100755 --- a/scripts/build +++ b/scripts/build @@ -21,4 +21,5 @@ which ffmpeg || exit 2 ffmpeg -version || exit 3 echo +"$PYAV_PYTHON" scripts/comptime.py "$PYAV_PYTHON" setup.py config build_ext --inplace || exit 1 diff --git a/scripts/comptime.py b/scripts/comptime.py new file mode 100644 index 000000000..797d125e0 --- /dev/null +++ b/scripts/comptime.py @@ -0,0 +1,30 @@ +import os +import sys + +def replace_in_file(file_path): + try: + with open(file_path, "r", encoding="utf-8") as file: + content = file.read() + + modified_content = content.replace("# [FFMPEG6] ", "") + + with open(file_path, "w") as file: + file.write(modified_content) + except UnicodeDecodeError: + pass + + +def process_directory(directory): + for root, dirs, files in os.walk(directory): + for file in files: + file_path = os.path.join(root, file) + replace_in_file(file_path) + +if sys.platform == "win32": + is_6 = sys.argv[1].startswith("6") +else: + is_6 = os.environ.get("PYAV_LIBRARY").startswith("ffmpeg-6") + +if is_6: + process_directory("av") + process_directory("include") diff --git a/tests/test_audioresampler.py b/tests/test_audioresampler.py index dc084fbba..58185f9d1 100644 --- a/tests/test_audioresampler.py +++ b/tests/test_audioresampler.py @@ -68,53 +68,54 @@ def test_matching_passthrough(self): oframes = resampler.resample(None) self.assertEqual(len(oframes), 0) - # TODO: Fails on ffmpeg 6 - # def test_pts_assertion_same_rate(self): - # av.logging.set_level(av.logging.VERBOSE) - # resampler = AudioResampler("s16", "mono") - - # # resample one frame - # iframe = AudioFrame("s16", "stereo", 1024) - # iframe.sample_rate = 48000 - # iframe.time_base = "1/48000" - # iframe.pts = 0 - - # oframes = resampler.resample(iframe) - # self.assertEqual(len(oframes), 1) - - # oframe = oframes[0] - # self.assertEqual(oframe.pts, 0) - # self.assertEqual(oframe.time_base, iframe.time_base) - # self.assertEqual(oframe.sample_rate, iframe.sample_rate) - # self.assertEqual(oframe.samples, iframe.samples) - - # # resample another frame - # iframe.pts = 1024 - - # oframes = resampler.resample(iframe) - # self.assertEqual(len(oframes), 1) - - # oframe = oframes[0] - # self.assertEqual(oframe.pts, 1024) - # self.assertEqual(oframe.time_base, iframe.time_base) - # self.assertEqual(oframe.sample_rate, iframe.sample_rate) - # self.assertEqual(oframe.samples, iframe.samples) - - # # resample another frame with a pts gap, do not raise exception - # iframe.pts = 9999 - # oframes = resampler.resample(iframe) - # self.assertEqual(len(oframes), 1) - - # oframe = oframes[0] - # self.assertEqual(oframe.pts, 9999) - # self.assertEqual(oframe.time_base, iframe.time_base) - # self.assertEqual(oframe.sample_rate, iframe.sample_rate) - # self.assertEqual(oframe.samples, iframe.samples) - - # # flush - # oframes = resampler.resample(None) - # self.assertEqual(len(oframes), 0) - # av.logging.set_level(None) + def test_pts_assertion_same_rate(self): + import av + + av.logging.set_level(av.logging.VERBOSE) + resampler = AudioResampler("s16", "mono") + + # resample one frame + iframe = AudioFrame("s16", "stereo", 1024) + iframe.sample_rate = 48000 + iframe.time_base = Fraction(1, 48000) + iframe.pts = 0 + + oframes = resampler.resample(iframe) + self.assertEqual(len(oframes), 1) + + oframe = oframes[0] + self.assertEqual(oframe.pts, 0) + self.assertEqual(oframe.time_base, iframe.time_base) + self.assertEqual(oframe.sample_rate, iframe.sample_rate) + self.assertEqual(oframe.samples, iframe.samples) + + # resample another frame + iframe.pts = 1024 + + oframes = resampler.resample(iframe) + self.assertEqual(len(oframes), 1) + + oframe = oframes[0] + self.assertEqual(oframe.pts, 1024) + self.assertEqual(oframe.time_base, iframe.time_base) + self.assertEqual(oframe.sample_rate, iframe.sample_rate) + self.assertEqual(oframe.samples, iframe.samples) + + # resample another frame with a pts gap, do not raise exception + iframe.pts = 9999 + oframes = resampler.resample(iframe) + self.assertEqual(len(oframes), 1) + + oframe = oframes[0] + self.assertEqual(oframe.pts, 9999) + self.assertEqual(oframe.time_base, iframe.time_base) + self.assertEqual(oframe.sample_rate, iframe.sample_rate) + self.assertEqual(oframe.samples, iframe.samples) + + # flush + oframes = resampler.resample(None) + self.assertEqual(len(oframes), 0) + av.logging.set_level(None) def test_pts_assertion_new_rate_up(self): resampler = AudioResampler("s16", "mono", 44100) From a51589435592e5245c483bd3c8f881b9a437eec3 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 1 Aug 2024 00:14:13 -0400 Subject: [PATCH 324/846] Put pip installs in Makefile --- .github/workflows/smoke.yml | 2 +- Makefile | 7 +++---- docs/Makefile | 4 ++++ scripts/build | 1 + scripts/build-deps | 3 --- tests/requirements.txt | 10 ---------- 6 files changed, 9 insertions(+), 18 deletions(-) delete mode 100644 tests/requirements.txt diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 485227334..18949e6e9 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -87,7 +87,7 @@ jobs: run: | . scripts/activate.sh ffmpeg-${{ matrix.config.ffmpeg }} python -m av --version # Assert it can import. - scripts/test + make test - name: Docs if: matrix.config.extras diff --git a/Makefile b/Makefile index a31c7077b..4adb3b47f 100644 --- a/Makefile +++ b/Makefile @@ -13,8 +13,7 @@ default: build build: - # Always try to install the Python dependencies they are cheap. - $(PIP) install --upgrade -r tests/requirements.txt + $(PIP) install --upgrade cython CFLAGS=$(CFLAGS) LDFLAGS=$(LDFLAGS) $(PYTHON) setup.py build_ext --inplace --debug clean: @@ -29,12 +28,12 @@ fate-suite: rsync -vrltLW rsync://fate-suite.ffmpeg.org/fate-suite/ tests/assets/fate-suite/ lint: - $(PIP) install --upgrade -r tests/requirements.txt + $(PIP) install -U black isort flake8 flake8-pyproject pillow numpy mypy==1.10.0 black --check av examples tests setup.py flake8 av examples tests isort --check-only --diff av examples tests mypy av tests test: - $(PIP) install --upgrade -r tests/requirements.txt + $(PIP) install --upgrade cython numpy pillow pytest $(PYTHON) -m pytest diff --git a/docs/Makefile b/docs/Makefile index 9ebbf3c5d..e0662e90c 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -4,6 +4,9 @@ SPHINXBUILD = sphinx-build BUILDDIR = _build FFMPEGDIR = _ffmpeg +PYAV_PIP ?= pip +PIP := $(PYAV_PIP) + ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(SPHINXOPTS) . .PHONY: clean html open upload default @@ -29,6 +32,7 @@ clean: rm -rf $(BUILDDIR) $(FFMPEGDIR) html: $(RENDERED) $(TAGFILE) + $(PIP) install -U sphinx==5.1.0 $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html test: diff --git a/scripts/build b/scripts/build index c1e711d69..67b3836f2 100755 --- a/scripts/build +++ b/scripts/build @@ -21,5 +21,6 @@ which ffmpeg || exit 2 ffmpeg -version || exit 3 echo +$PYAV_PIP install -U cython 2> /dev/null "$PYAV_PYTHON" scripts/comptime.py "$PYAV_PYTHON" setup.py config build_ext --inplace || exit 1 diff --git a/scripts/build-deps b/scripts/build-deps index ef7ebff22..ab3b4b256 100755 --- a/scripts/build-deps +++ b/scripts/build-deps @@ -7,9 +7,6 @@ fi cd "$PYAV_ROOT" -# Always try to install the Python dependencies they are cheap. -$PYAV_PIP install --upgrade -r tests/requirements.txt 2> /dev/null - # Skip the rest of the build if it already exists. if [[ -e "$PYAV_LIBRARY_PREFIX/bin/ffmpeg" ]]; then echo "We have a cached build of ffmpeg-$PYAV_LIBRARY; skipping re-build." diff --git a/tests/requirements.txt b/tests/requirements.txt deleted file mode 100644 index 2907b9e22..000000000 --- a/tests/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -cython -numpy -pillow -black -isort -flake8 -flake8-pyproject -pytest -sphinx==5.1.0 -mypy==1.10.0 From 5001c193bb29ae925b0e1954c8fd03de8dd90bc3 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 1 Aug 2024 00:51:59 -0400 Subject: [PATCH 325/846] Apply comptime in tests --- .github/workflows/tests.yml | 12 ++++++++++-- scripts/comptime.py | 6 ++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2e4c221d9..dba278f7f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,6 +11,8 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.8" + - name: Compile Time + run: python scripts/comptime.py 6.1 - name: Build source package run: | pip install cython @@ -53,14 +55,20 @@ jobs: run: | brew update brew install pkg-config - - name: Set deployment target + - name: Anon1 if: matrix.os == 'macos-13' || matrix.os == 'macos-14' - run: echo "MACOSX_DEPLOYMENT_TARGET=10.13" >> $GITHUB_ENV + run: | + echo "MACOSX_DEPLOYMENT_TARGET=10.13" >> $GITHUB_ENV + python scripts/comptime.py 7 + - name: Anon2 + if: matrix.os != 'macos-13' && matrix.os != 'macos-14' + run: python scripts/comptime.py 6.1 - name: Build wheels env: CIBW_ARCHS: ${{ matrix.arch }} CIBW_BEFORE_ALL_LINUX: yum install -y alsa-lib libxcb CIBW_BEFORE_BUILD: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-6.1.json /tmp/vendor + CIBW_BEFORE_BUILD_MACOS: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.0.json /tmp/vendor CIBW_BEFORE_BUILD_WINDOWS: python scripts\fetch-vendor.py --config-file scripts\ffmpeg-6.1.json C:\cibw\vendor CIBW_ENVIRONMENT_LINUX: LD_LIBRARY_PATH=/tmp/vendor/lib:$LD_LIBRARY_PATH PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig CIBW_ENVIRONMENT_MACOS: PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig LDFLAGS=-headerpad_max_install_names diff --git a/scripts/comptime.py b/scripts/comptime.py index 797d125e0..a03a465f9 100644 --- a/scripts/comptime.py +++ b/scripts/comptime.py @@ -20,10 +20,12 @@ def process_directory(directory): file_path = os.path.join(root, file) replace_in_file(file_path) -if sys.platform == "win32": + +version = os.environ.get("PYAV_LIBRARY") +if version is None: is_6 = sys.argv[1].startswith("6") else: - is_6 = os.environ.get("PYAV_LIBRARY").startswith("ffmpeg-6") + is_6 = version.startswith("ffmpeg-6") if is_6: process_directory("av") From f817c95fe1905c47afc7f13af5832c184fcf948e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 3 Aug 2024 23:17:59 -0400 Subject: [PATCH 326/846] Fix Windows CI --- .github/workflows/smoke.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 18949e6e9..8dd04d0cd 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -133,6 +133,7 @@ jobs: cython \ numpy \ pillow \ + pytest \ python=${{ matrix.config.python }} \ setuptools @@ -150,4 +151,4 @@ jobs: run: | . $CONDA/etc/profile.d/conda.sh conda activate pyav - python setup.py test + python -m pytest From a351a972e063220751e39955bd2ec1c972e0c62f Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 3 Aug 2024 23:25:30 -0400 Subject: [PATCH 327/846] Drop Python 3.8, indicate support for 3.13 --- .github/workflows/smoke.yml | 16 ++++++++-------- .github/workflows/tests.yml | 4 ++-- setup.py | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 8dd04d0cd..3acc6fb61 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -34,12 +34,12 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-latest, python: "3.8", ffmpeg: "6.1", extras: true} - # TODO: - {os: ubuntu-latest, python: "3.8", ffmpeg: "7.0"} - - {os: ubuntu-latest, python: "3.8", ffmpeg: "6.0"} + - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.1", extras: true} + # TODO: - {os: ubuntu-latest, python: "3.9", ffmpeg: "7.0"} + - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.0"} - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1"} - - {os: macos-12, python: "3.8", ffmpeg: "7.0"} - - {os: macos-12, python: "3.8", ffmpeg: "6.1"} + - {os: macos-12, python: "3.9", ffmpeg: "7.0"} + - {os: macos-12, python: "3.9", ffmpeg: "6.1"} env: PYAV_PYTHON: python${{ matrix.config.python }} @@ -115,9 +115,9 @@ jobs: fail-fast: false matrix: config: - - {os: windows-latest, python: "3.8", ffmpeg: "7.0"} - - {os: windows-latest, python: "3.8", ffmpeg: "6.1"} - - {os: windows-latest, python: "3.8", ffmpeg: "6.0"} + - {os: windows-latest, python: "3.9", ffmpeg: "7.0"} + - {os: windows-latest, python: "3.9", ffmpeg: "6.1"} + - {os: windows-latest, python: "3.9", ffmpeg: "6.0"} steps: - name: Checkout diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index dba278f7f..5c7951153 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.8" + python-version: "3.9" - name: Compile Time run: python scripts/comptime.py 6.1 - name: Build source package @@ -46,7 +46,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.8" + python-version: "3.9" - name: Set up QEMU if: matrix.os == 'ubuntu-latest' uses: docker/setup-qemu-action@v3 diff --git a/setup.py b/setup.py index 2a88c8967..7df0a9c2f 100644 --- a/setup.py +++ b/setup.py @@ -216,7 +216,7 @@ def parse_cflags(raw_flags): + list(package_dir.keys()), package_dir=package_dir, package_data=package_data, - python_requires=">=3.8", + python_requires=">=3.9", zip_safe=False, ext_modules=ext_modules, test_suite="tests", @@ -233,11 +233,11 @@ def parse_cflags(raw_flags): "Operating System :: Unix", "Operating System :: Microsoft :: Windows", "Programming Language :: Cython", - "Programming Language :: Python :: 3.8", "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", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Multimedia :: Sound/Audio", "Topic :: Multimedia :: Sound/Audio :: Conversion", From f5ec8ca837c41fb8905f2c1cd76e64c2c93c3624 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 4 Aug 2024 00:26:21 -0400 Subject: [PATCH 328/846] Build wheels with ffmpeg 7 --- .github/workflows/smoke.yml | 10 +++++----- .github/workflows/tests.yml | 14 ++++---------- scripts/activate.sh | 2 +- scripts/build-deps | 5 +++-- 4 files changed, 13 insertions(+), 18 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 3acc6fb61..4dbcc096b 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -34,12 +34,12 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.1", extras: true} - # TODO: - {os: ubuntu-latest, python: "3.9", ffmpeg: "7.0"} + - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.1.1", extras: true} + - {os: ubuntu-latest, python: "3.9", ffmpeg: "7.0.2"} - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.0"} - - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1"} - - {os: macos-12, python: "3.9", ffmpeg: "7.0"} - - {os: macos-12, python: "3.9", ffmpeg: "6.1"} + - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1.1"} + - {os: macos-12, python: "3.9", ffmpeg: "7.0.2"} + - {os: macos-12, python: "3.9", ffmpeg: "6.1.1"} env: PYAV_PYTHON: python${{ matrix.config.python }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5c7951153..27e394948 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,12 +11,10 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.9" - - name: Compile Time - run: python scripts/comptime.py 6.1 - name: Build source package run: | pip install cython - python scripts/fetch-vendor.py --config-file scripts/ffmpeg-6.1.json /tmp/vendor + python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.0.json /tmp/vendor PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig python setup.py sdist - name: Upload source package uses: actions/upload-artifact@v4 @@ -55,21 +53,17 @@ jobs: run: | brew update brew install pkg-config - - name: Anon1 + - name: Set Minimum MacOS Target if: matrix.os == 'macos-13' || matrix.os == 'macos-14' run: | echo "MACOSX_DEPLOYMENT_TARGET=10.13" >> $GITHUB_ENV - python scripts/comptime.py 7 - - name: Anon2 - if: matrix.os != 'macos-13' && matrix.os != 'macos-14' - run: python scripts/comptime.py 6.1 - name: Build wheels env: CIBW_ARCHS: ${{ matrix.arch }} CIBW_BEFORE_ALL_LINUX: yum install -y alsa-lib libxcb - CIBW_BEFORE_BUILD: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-6.1.json /tmp/vendor + CIBW_BEFORE_BUILD: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.0.json /tmp/vendor CIBW_BEFORE_BUILD_MACOS: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.0.json /tmp/vendor - CIBW_BEFORE_BUILD_WINDOWS: python scripts\fetch-vendor.py --config-file scripts\ffmpeg-6.1.json C:\cibw\vendor + CIBW_BEFORE_BUILD_WINDOWS: python scripts\fetch-vendor.py --config-file scripts\ffmpeg-7.0.json C:\cibw\vendor CIBW_ENVIRONMENT_LINUX: LD_LIBRARY_PATH=/tmp/vendor/lib:$LD_LIBRARY_PATH PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig CIBW_ENVIRONMENT_MACOS: PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig LDFLAGS=-headerpad_max_install_names CIBW_ENVIRONMENT_WINDOWS: INCLUDE=C:\\cibw\\vendor\\include LIB=C:\\cibw\\vendor\\lib PYAV_SKIP_TESTS=unicode_filename diff --git a/scripts/activate.sh b/scripts/activate.sh index 99be26dcc..b7bdf7a6f 100755 --- a/scripts/activate.sh +++ b/scripts/activate.sh @@ -17,7 +17,7 @@ if [[ ! "$PYAV_LIBRARY" ]]; then return 1 fi else - PYAV_LIBRARY=ffmpeg-7.0.1 + PYAV_LIBRARY=ffmpeg-7.0.2 echo "No \$PYAV_LIBRARY set; defaulting to $PYAV_LIBRARY" fi fi diff --git a/scripts/build-deps b/scripts/build-deps index ab3b4b256..4cb90f074 100755 --- a/scripts/build-deps +++ b/scripts/build-deps @@ -33,8 +33,6 @@ cd $PYAV_LIBRARY echo ./configure ./configure \ --disable-doc \ - --disable-mmx \ - --disable-optimizations \ --disable-static \ --disable-stripping \ --enable-debug=3 \ @@ -43,6 +41,9 @@ echo ./configure --enable-libx264 \ --enable-libxml2 \ --enable-shared \ + --enable-sse \ + --enable-avx \ + --enable-avx2 \ --prefix="$PYAV_LIBRARY_PREFIX" \ || exit 2 echo From 3499288628e2dbb8c7703dbbf9f2ff8f43a5a43d Mon Sep 17 00:00:00 2001 From: Andrew Wason Date: Thu, 8 Aug 2024 11:33:23 -0400 Subject: [PATCH 329/846] Add support for av_buffersink_set_frame_size Fixes https://github.com/PyAV-Org/PyAV/issues/1482 --- av/audio/resampler.pyx | 4 +--- av/filter/graph.pyi | 1 + av/filter/graph.pyx | 9 +++++++++ tests/test_filters.py | 25 +++++++++++++++++++++++++ 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/av/audio/resampler.pyx b/av/audio/resampler.pyx index 59cddec8c..5c6020762 100644 --- a/av/audio/resampler.pyx +++ b/av/audio/resampler.pyx @@ -1,5 +1,3 @@ -cimport libav as lib - from av.filter.context cimport FilterContext import errno @@ -95,7 +93,7 @@ cdef class AudioResampler: self.graph.configure() if self.frame_size > 0: - lib.av_buffersink_set_frame_size((abuffersink).ptr, self.frame_size) + self.graph.set_audio_frame_size(self.frame_size) elif frame is not None: if ( diff --git a/av/filter/graph.pyi b/av/filter/graph.pyi index 337be2bee..9d8480b17 100644 --- a/av/filter/graph.pyi +++ b/av/filter/graph.pyi @@ -40,6 +40,7 @@ class Graph: name: str | None = None, time_base: Fraction | None = None, ) -> FilterContext: ... + def set_audio_frame_size(self, frame_size: int) -> None: ... def push(self, frame: None | AudioFrame | VideoFrame) -> None: ... def pull(self) -> VideoFrame | AudioFrame: ... def vpush(self, frame: VideoFrame | None) -> None: ... diff --git a/av/filter/graph.pyx b/av/filter/graph.pyx index e0effd12f..e0ec6bf60 100644 --- a/av/filter/graph.pyx +++ b/av/filter/graph.pyx @@ -171,6 +171,15 @@ cdef class Graph: return self.add("abuffer", name=name, **kwargs) + def set_audio_frame_size(self, frame_size): + if not self.configured: + raise ValueError("graph not configured") + sinks = self._context_by_type.get("abuffersink", []) + if not sinks: + raise ValueError("missing abuffersink filter") + for sink in sinks: + lib.av_buffersink_set_frame_size((sink).ptr, frame_size) + def push(self, frame): if frame is None: contexts = self._context_by_type.get("buffer", []) + self._context_by_type.get("abuffer", []) diff --git a/tests/test_filters.py b/tests/test_filters.py index 27663081e..399820c93 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -153,6 +153,31 @@ def test_audio_buffer_resample(self): self.assertEqual(out_frame.layout.name, "stereo") self.assertEqual(out_frame.sample_rate, 44100) + def test_audio_buffer_frame_size(self): + graph = Graph() + graph.link_nodes( + graph.add_abuffer( + format="fltp", + sample_rate=48000, + layout="stereo", + time_base=Fraction(1, 48000), + ), + graph.add("abuffersink"), + ).configure() + graph.set_audio_frame_size(256) + graph.push( + generate_audio_frame( + 0, + input_format="fltp", + layout="stereo", + sample_rate=48000, + frame_size=1024, + ) + ) + out_frame = graph.pull() + self.assertEqual(out_frame.sample_rate, 48000) + self.assertEqual(out_frame.samples, 256) + def test_audio_buffer_volume_filter(self): graph = Graph() graph.link_nodes( From 9cbb41b783c560abb112b4ae448e5eb75656ce61 Mon Sep 17 00:00:00 2001 From: Andrew Wason Date: Thu, 8 Aug 2024 13:47:38 -0400 Subject: [PATCH 330/846] Add doc comment --- av/filter/graph.pyx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/av/filter/graph.pyx b/av/filter/graph.pyx index e0ec6bf60..c1a2d7a06 100644 --- a/av/filter/graph.pyx +++ b/av/filter/graph.pyx @@ -172,6 +172,10 @@ cdef class Graph: return self.add("abuffer", name=name, **kwargs) def set_audio_frame_size(self, frame_size): + """ + Set the audio frame size for the graphs `abuffersink`. + See `av_buffersink_set_frame_size `_. + """ if not self.configured: raise ValueError("graph not configured") sinks = self._context_by_type.get("abuffersink", []) From f9d375cabd423b3ad3358a60e5d1db590d8db53b Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 8 Aug 2024 17:39:53 -0400 Subject: [PATCH 331/846] Use ffmpeg 7.0.2, flatten json --- scripts/fetch-vendor.py | 33 ++++++++++++++++----------------- scripts/ffmpeg-6.0.json | 4 ++-- scripts/ffmpeg-6.1.json | 4 ++-- scripts/ffmpeg-7.0.json | 4 ++-- 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/scripts/fetch-vendor.py b/scripts/fetch-vendor.py index c08bcc1f7..5041f4265 100644 --- a/scripts/fetch-vendor.py +++ b/scripts/fetch-vendor.py @@ -42,20 +42,19 @@ def get_platform(): if not os.path.exists(args.destination_dir): os.makedirs(args.destination_dir) -for url_template in config["urls"]: - tarball_url = url_template.replace("{platform}", get_platform()) - - # download tarball - tarball_name = tarball_url.split("/")[-1] - tarball_file = os.path.join(args.cache_dir, tarball_name) - if not os.path.exists(tarball_file): - logging.info(f"Downloading {tarball_url}") - if not os.path.exists(args.cache_dir): - os.mkdir(args.cache_dir) - subprocess.check_call( - ["curl", "--location", "--output", tarball_file, "--silent", tarball_url] - ) - - # extract tarball - logging.info(f"Extracting {tarball_name}") - subprocess.check_call(["tar", "-C", args.destination_dir, "-xf", tarball_file]) +tarball_url = config["url"].replace("{platform}", get_platform()) + +# download tarball +tarball_name = tarball_url.split("/")[-1] +tarball_file = os.path.join(args.cache_dir, tarball_name) +if not os.path.exists(tarball_file): + logging.info(f"Downloading {tarball_url}") + if not os.path.exists(args.cache_dir): + os.mkdir(args.cache_dir) + subprocess.check_call( + ["curl", "--location", "--output", tarball_file, "--silent", tarball_url] + ) + +# extract tarball +logging.info(f"Extracting {tarball_name}") +subprocess.check_call(["tar", "-C", args.destination_dir, "-xf", tarball_file]) diff --git a/scripts/ffmpeg-6.0.json b/scripts/ffmpeg-6.0.json index 0eb2034ee..af6b3332d 100644 --- a/scripts/ffmpeg-6.0.json +++ b/scripts/ffmpeg-6.0.json @@ -1,3 +1,3 @@ { - "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/6.0.0-1/ffmpeg-{platform}.tar.gz"] -} + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/6.0.0-1/ffmpeg-{platform}.tar.gz" +} \ No newline at end of file diff --git a/scripts/ffmpeg-6.1.json b/scripts/ffmpeg-6.1.json index 9bae82dd3..bf0604812 100644 --- a/scripts/ffmpeg-6.1.json +++ b/scripts/ffmpeg-6.1.json @@ -1,3 +1,3 @@ { - "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/6.1.1-5/ffmpeg-{platform}.tar.gz"] -} + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/6.1.1-5/ffmpeg-{platform}.tar.gz" +} \ No newline at end of file diff --git a/scripts/ffmpeg-7.0.json b/scripts/ffmpeg-7.0.json index 4e69666eb..6cbe76b2c 100644 --- a/scripts/ffmpeg-7.0.json +++ b/scripts/ffmpeg-7.0.json @@ -1,3 +1,3 @@ { - "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.0.1-1/ffmpeg-{platform}.tar.gz"] -} + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.0.2-1/ffmpeg-{platform}.tar.gz" +} \ No newline at end of file From af55a9637ad2f3ef9783f5fb37d0e3933c515e8e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 9 Aug 2024 14:27:06 -0400 Subject: [PATCH 332/846] Add attachment stream class --- av/attachments/__init__.py | 0 av/attachments/codeccontext.pxd | 5 +++++ av/attachments/codeccontext.pyi | 6 ++++++ av/attachments/codeccontext.pyx | 8 ++++++++ av/attachments/stream.pxd | 5 +++++ av/attachments/stream.pyi | 4 ++++ av/attachments/stream.pyx | 10 ++++++++++ av/codec/context.pyx | 3 +++ av/stream.pyx | 12 ++++++------ 9 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 av/attachments/__init__.py create mode 100644 av/attachments/codeccontext.pxd create mode 100644 av/attachments/codeccontext.pyi create mode 100644 av/attachments/codeccontext.pyx create mode 100644 av/attachments/stream.pxd create mode 100644 av/attachments/stream.pyi create mode 100644 av/attachments/stream.pyx diff --git a/av/attachments/__init__.py b/av/attachments/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/av/attachments/codeccontext.pxd b/av/attachments/codeccontext.pxd new file mode 100644 index 000000000..9a2a71efc --- /dev/null +++ b/av/attachments/codeccontext.pxd @@ -0,0 +1,5 @@ +from av.codec.context cimport CodecContext + + +cdef class AttachmentCodecContext(CodecContext): + pass diff --git a/av/attachments/codeccontext.pyi b/av/attachments/codeccontext.pyi new file mode 100644 index 000000000..2bc21cc59 --- /dev/null +++ b/av/attachments/codeccontext.pyi @@ -0,0 +1,6 @@ +from typing import Literal + +from av.codec.context import CodecContext + +class AttachmentCodecContext(CodecContext): + type: Literal["attachment"] diff --git a/av/attachments/codeccontext.pyx b/av/attachments/codeccontext.pyx new file mode 100644 index 000000000..f83972cc2 --- /dev/null +++ b/av/attachments/codeccontext.pyx @@ -0,0 +1,8 @@ +cimport libav as lib + +from av.error cimport err_check +from av.packet cimport Packet + + +cdef class AttachmentCodecContext(CodecContext): + pass \ No newline at end of file diff --git a/av/attachments/stream.pxd b/av/attachments/stream.pxd new file mode 100644 index 000000000..81f788b77 --- /dev/null +++ b/av/attachments/stream.pxd @@ -0,0 +1,5 @@ +from av.stream cimport Stream + + +cdef class AttachmentStream(Stream): + pass diff --git a/av/attachments/stream.pyi b/av/attachments/stream.pyi new file mode 100644 index 000000000..b237be771 --- /dev/null +++ b/av/attachments/stream.pyi @@ -0,0 +1,4 @@ +from av.stream import Stream + +class AttachmentStream(Stream): + pass diff --git a/av/attachments/stream.pyx b/av/attachments/stream.pyx new file mode 100644 index 000000000..97b8b8da3 --- /dev/null +++ b/av/attachments/stream.pyx @@ -0,0 +1,10 @@ +from av.packet cimport Packet +from av.stream cimport Stream + + +cdef class AttachmentStream(Stream): + """ + An :class:`AttachmentStream`. + """ + def __getattr__(self, name): + return getattr(self.codec_context, name) diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 70100e65e..643f56160 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -34,6 +34,9 @@ cdef CodecContext wrap_codec_context(lib.AVCodecContext *c_ctx, const lib.AVCode elif c_ctx.codec_type == lib.AVMEDIA_TYPE_SUBTITLE: from av.subtitles.codeccontext import SubtitleCodecContext py_ctx = SubtitleCodecContext(_cinit_sentinel) + elif c_ctx.codec_type == lib.AVMEDIA_TYPE_ATTACHMENT: + from av.attachments.codeccontext import AttachmentCodecContext + py_ctx = AttachmentCodecContext(_cinit_sentinel) else: py_ctx = CodecContext(_cinit_sentinel) diff --git a/av/stream.pyx b/av/stream.pyx index 0cf3a7011..44354b651 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -1,5 +1,3 @@ -import warnings - cimport libav as lib from libc.stdint cimport int32_t @@ -13,8 +11,6 @@ from av.utils cimport ( to_avrational, ) -from av.deprecation import AVDeprecationWarning - cdef object _cinit_bypass_sentinel = object() @@ -47,6 +43,9 @@ cdef Stream wrap_stream(Container container, lib.AVStream *c_stream, CodecContex elif c_stream.codecpar.codec_type == lib.AVMEDIA_TYPE_SUBTITLE: from av.subtitles.stream import SubtitleStream py_stream = SubtitleStream.__new__(SubtitleStream, _cinit_bypass_sentinel) + elif c_stream.codecpar.codec_type == lib.AVMEDIA_TYPE_ATTACHMENT: + from av.attachments.stream import AttachmentStream + py_stream = AttachmentStream.__new__(AttachmentStream, _cinit_bypass_sentinel) elif c_stream.codecpar.codec_type == lib.AVMEDIA_TYPE_DATA: from av.data.stream import DataStream py_stream = DataStream.__new__(DataStream, _cinit_bypass_sentinel) @@ -96,9 +95,10 @@ cdef class Stream: ) def __repr__(self): + name = getattr(self, "name", None) return ( f"'}/" - f"{self.name or ''} at 0x{id(self):x}>" + f"{name or ''} at 0x{id(self):x}>" ) def __setattr__(self, name, value): @@ -239,7 +239,7 @@ cdef class Stream: :type: :class:`str` or ``None`` """ - return self.metadata.get('language') + return self.metadata.get("language") @property def type(self): From b7b1b156e7877071f3168522cc6ab93ffd832fc2 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 10 Aug 2024 03:48:21 -0400 Subject: [PATCH 333/846] Add attachments to StreamContainer --- av/attachments/codeccontext.pxd | 5 ----- av/attachments/codeccontext.pyi | 6 ------ av/attachments/codeccontext.pyx | 8 -------- av/attachments/stream.pyi | 6 +++++- av/attachments/stream.pyx | 11 ++++++++--- av/codec/context.pyx | 3 --- av/container/streams.pxd | 1 + av/container/streams.pyi | 2 ++ av/container/streams.pyx | 4 +++- 9 files changed, 19 insertions(+), 27 deletions(-) delete mode 100644 av/attachments/codeccontext.pxd delete mode 100644 av/attachments/codeccontext.pyi delete mode 100644 av/attachments/codeccontext.pyx diff --git a/av/attachments/codeccontext.pxd b/av/attachments/codeccontext.pxd deleted file mode 100644 index 9a2a71efc..000000000 --- a/av/attachments/codeccontext.pxd +++ /dev/null @@ -1,5 +0,0 @@ -from av.codec.context cimport CodecContext - - -cdef class AttachmentCodecContext(CodecContext): - pass diff --git a/av/attachments/codeccontext.pyi b/av/attachments/codeccontext.pyi deleted file mode 100644 index 2bc21cc59..000000000 --- a/av/attachments/codeccontext.pyi +++ /dev/null @@ -1,6 +0,0 @@ -from typing import Literal - -from av.codec.context import CodecContext - -class AttachmentCodecContext(CodecContext): - type: Literal["attachment"] diff --git a/av/attachments/codeccontext.pyx b/av/attachments/codeccontext.pyx deleted file mode 100644 index f83972cc2..000000000 --- a/av/attachments/codeccontext.pyx +++ /dev/null @@ -1,8 +0,0 @@ -cimport libav as lib - -from av.error cimport err_check -from av.packet cimport Packet - - -cdef class AttachmentCodecContext(CodecContext): - pass \ No newline at end of file diff --git a/av/attachments/stream.pyi b/av/attachments/stream.pyi index b237be771..3d660e4a0 100644 --- a/av/attachments/stream.pyi +++ b/av/attachments/stream.pyi @@ -1,4 +1,8 @@ +from typing import Literal + from av.stream import Stream class AttachmentStream(Stream): - pass + type: Literal["attachment"] + @property + def mimetype(self) -> str | None: ... diff --git a/av/attachments/stream.pyx b/av/attachments/stream.pyx index 97b8b8da3..b5b55ac1a 100644 --- a/av/attachments/stream.pyx +++ b/av/attachments/stream.pyx @@ -1,4 +1,3 @@ -from av.packet cimport Packet from av.stream cimport Stream @@ -6,5 +5,11 @@ cdef class AttachmentStream(Stream): """ An :class:`AttachmentStream`. """ - def __getattr__(self, name): - return getattr(self.codec_context, name) + + @property + def name(self): + return self.metadata.get("filename") + + @property + def mimetype(self): + return self.metadata.get("mimetype") diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 643f56160..70100e65e 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -34,9 +34,6 @@ cdef CodecContext wrap_codec_context(lib.AVCodecContext *c_ctx, const lib.AVCode elif c_ctx.codec_type == lib.AVMEDIA_TYPE_SUBTITLE: from av.subtitles.codeccontext import SubtitleCodecContext py_ctx = SubtitleCodecContext(_cinit_sentinel) - elif c_ctx.codec_type == lib.AVMEDIA_TYPE_ATTACHMENT: - from av.attachments.codeccontext import AttachmentCodecContext - py_ctx = AttachmentCodecContext(_cinit_sentinel) else: py_ctx = CodecContext(_cinit_sentinel) diff --git a/av/container/streams.pxd b/av/container/streams.pxd index bf217d7c6..43f7f3e4f 100644 --- a/av/container/streams.pxd +++ b/av/container/streams.pxd @@ -9,6 +9,7 @@ cdef class StreamContainer: cdef readonly tuple video cdef readonly tuple audio cdef readonly tuple subtitles + cdef readonly tuple attachments cdef readonly tuple data cdef readonly tuple other diff --git a/av/container/streams.pyi b/av/container/streams.pyi index cb658d35f..9f67e7981 100644 --- a/av/container/streams.pyi +++ b/av/container/streams.pyi @@ -1,5 +1,6 @@ from typing import Iterator, overload +from av.attachments.stream import AttachmentStream from av.audio.stream import AudioStream from av.data.stream import DataStream from av.stream import Stream @@ -10,6 +11,7 @@ class StreamContainer: video: tuple[VideoStream, ...] audio: tuple[AudioStream, ...] subtitles: tuple[SubtitleStream, ...] + attachments: tuple[AttachmentStream, ...] data: tuple[DataStream, ...] other: tuple[Stream, ...] diff --git a/av/container/streams.pyx b/av/container/streams.pyx index 921bd40b2..6071bc811 100644 --- a/av/container/streams.pyx +++ b/av/container/streams.pyx @@ -12,7 +12,6 @@ def _flatten(input_): cdef class StreamContainer: - """ A tuple-like container of :class:`Stream`. @@ -33,6 +32,7 @@ cdef class StreamContainer: self.audio = () self.subtitles = () self.data = () + self.attachments = () self.other = () cdef add_stream(self, Stream stream): @@ -46,6 +46,8 @@ cdef class StreamContainer: self.audio = self.audio + (stream, ) elif stream.ptr.codecpar.codec_type == lib.AVMEDIA_TYPE_SUBTITLE: self.subtitles = self.subtitles + (stream, ) + elif stream.ptr.codecpar.codec_type == lib.AVMEDIA_TYPE_ATTACHMENT: + self.attachments = self.attachments + (stream, ) elif stream.ptr.codecpar.codec_type == lib.AVMEDIA_TYPE_DATA: self.data = self.data + (stream, ) else: From 622456948f83890ca8512cd107dcd4dcbc07636a Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 11 Aug 2024 02:46:04 -0400 Subject: [PATCH 334/846] Fix Video CC (`self._format` can be None) This fixes wyattblue/auto-editor #489. --- av/video/codeccontext.pyi | 2 +- av/video/codeccontext.pyx | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/av/video/codeccontext.pyi b/av/video/codeccontext.pyi index 4576f5155..7bdda89e0 100644 --- a/av/video/codeccontext.pyi +++ b/av/video/codeccontext.pyi @@ -8,7 +8,7 @@ from .format import VideoFormat from .frame import VideoFrame class VideoCodecContext(CodecContext): - format: VideoFormat + format: VideoFormat | None width: int height: int bits_per_codec_sample: int diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.pyx index 5ef49ee4c..32c74f867 100644 --- a/av/video/codeccontext.pyx +++ b/av/video/codeccontext.pyx @@ -34,6 +34,9 @@ cdef class VideoCodecContext(CodecContext): cdef VideoFrame vframe = input + if self._format is None: + raise ValueError("self._format is None, cannot encode") + # Reformat if it doesn't match. if ( vframe.format.pix_fmt != self._format.pix_fmt or @@ -42,11 +45,9 @@ cdef class VideoCodecContext(CodecContext): ): if not self.reformatter: self.reformatter = VideoReformatter() + vframe = self.reformatter.reformat( - vframe, - self.ptr.width, - self.ptr.height, - self._format, + vframe, self.ptr.width, self.ptr.height, self._format ) # There is no pts, so create one. @@ -121,9 +122,9 @@ cdef class VideoCodecContext(CodecContext): """ The pixel format's name. - :type: str + :type: str | None """ - return self._format.name + return getattr(self._format, "name") @pix_fmt.setter def pix_fmt(self, value): From ba1ace7d0f5025ad10ed92e3f6dc85dca6374fa5 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 11 Aug 2024 03:15:22 -0400 Subject: [PATCH 335/846] Disallow accessing gop_size, timebase as a decoder --- av/codec/context.pyx | 12 ++---------- av/video/codeccontext.pyx | 14 ++------------ av/video/frame.pyx | 11 ----------- tests/test_codec_context.py | 25 ++++++------------------- 4 files changed, 10 insertions(+), 52 deletions(-) diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 70100e65e..66b21f9a3 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -1,5 +1,3 @@ -import warnings - cimport libav as lib from libc.errno cimport EAGAIN from libc.stdint cimport uint8_t @@ -504,19 +502,13 @@ cdef class CodecContext: @property def time_base(self): if self.is_decoder: - warnings.warn( - "Using CodecContext.time_base for decoders is deprecated.", - AVDeprecationWarning - ) + raise RuntimeError("Cannot access 'time_base' as a decoder") return avrational_to_fraction(&self.ptr.time_base) @time_base.setter def time_base(self, value): if self.is_decoder: - warnings.warn( - "Using CodecContext.time_base for decoders is deprecated.", - AVDeprecationWarning - ) + raise RuntimeError("Cannot access 'time_base' as a decoder") to_avrational(value, &self.ptr.time_base) @property diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.pyx index 32c74f867..c37223784 100644 --- a/av/video/codeccontext.pyx +++ b/av/video/codeccontext.pyx @@ -1,5 +1,3 @@ -import warnings - cimport libav as lib from libc.stdint cimport int64_t @@ -11,8 +9,6 @@ from av.video.format cimport VideoFormat, get_pix_fmt, get_video_format from av.video.frame cimport VideoFrame, alloc_video_frame from av.video.reformatter cimport VideoReformatter -from av.deprecation import AVDeprecationWarning - cdef class VideoCodecContext(CodecContext): def __cinit__(self, *args, **kwargs): @@ -161,19 +157,13 @@ cdef class VideoCodecContext(CodecContext): :type: int """ if self.is_decoder: - warnings.warn( - "Using VideoCodecContext.gop_size for decoders is deprecated.", - AVDeprecationWarning - ) + raise RuntimeError("Cannnot access 'gop_size' as a decoder") return self.ptr.gop_size @gop_size.setter def gop_size(self, int value): if self.is_decoder: - warnings.warn( - "Using VideoCodecContext.gop_size for decoders is deprecated.", - AVDeprecationWarning - ) + raise RuntimeError("Cannnot access 'gop_size' as a decoder") self.ptr.gop_size = value @property diff --git a/av/video/frame.pyx b/av/video/frame.pyx index e64eb8573..c27ead7b4 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -8,10 +8,6 @@ from av.utils cimport check_ndarray, check_ndarray_shape from av.video.format cimport get_pix_fmt, get_video_format from av.video.plane cimport VideoPlane -import warnings - -from av.deprecation import AVDeprecationWarning - cdef object _cinit_bypass_sentinel @@ -574,10 +570,3 @@ cdef class VideoFrame(Frame): copy_array_to_plane(array, frame.planes[0], 1 if array.ndim == 2 else array.shape[2]) return frame - - def __getattribute__(self, attribute): - # This method should be deleted when `frame.index` is removed - if attribute == "index": - warnings.warn("Using `frame.index` is deprecated.", AVDeprecationWarning) - - return Frame.__getattribute__(self, attribute) diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 3ead81fc2..7ada318d9 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -1,5 +1,4 @@ import os -import warnings from fractions import Fraction from unittest import SkipTest @@ -89,29 +88,17 @@ def test_decoder_extradata(self): def test_decoder_gop_size(self): ctx = av.codec.Codec("h264", "r").create() - with warnings.catch_warnings(record=True) as captured: - self.assertIsInstance(ctx.gop_size, int) - self.assertEqual( - captured[0].message.args[0], - "Using VideoCodecContext.gop_size for decoders is deprecated.", - ) + with self.assertRaises(RuntimeError): + ctx.gop_size - def test_decoder_timebase(self): + def test_decoder_timebase(self) -> None: ctx = av.codec.Codec("h264", "r").create() - with warnings.catch_warnings(record=True) as captured: - self.assertIsNone(ctx.time_base) - self.assertEqual( - captured[0].message.args[0], - "Using CodecContext.time_base for decoders is deprecated.", - ) + with self.assertRaises(RuntimeError): + ctx.time_base - with warnings.catch_warnings(record=True) as captured: + with self.assertRaises(RuntimeError): ctx.time_base = Fraction(1, 25) - self.assertEqual( - captured[0].message.args[0], - "Using CodecContext.time_base for decoders is deprecated.", - ) def test_encoder_extradata(self): ctx = av.codec.Codec("h264", "w").create() From 2941cc33635b91a99afc73ffd43fc2b01660d2a8 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 11 Aug 2024 04:31:17 -0400 Subject: [PATCH 336/846] Add `best()` to StreamContainer Closes #1481 --- av/container/streams.pxd | 7 ++++- av/container/streams.pyi | 8 +++++- av/container/streams.pyx | 47 +++++++++++++++++++++++++++++++- docs/api/attachments.rst | 8 ++++++ docs/api/stream.rst | 6 ++++ include/libavformat/avformat.pxd | 10 ++++++- tests/test_streams.py | 20 ++++++++++---- 7 files changed, 97 insertions(+), 9 deletions(-) create mode 100644 docs/api/attachments.rst diff --git a/av/container/streams.pxd b/av/container/streams.pxd index 43f7f3e4f..097176e10 100644 --- a/av/container/streams.pxd +++ b/av/container/streams.pxd @@ -1,8 +1,11 @@ +cimport libav as lib + from av.stream cimport Stream +from .core cimport Container -cdef class StreamContainer: +cdef class StreamContainer: cdef list _streams # For the different types. @@ -14,3 +17,5 @@ cdef class StreamContainer: cdef readonly tuple other cdef add_stream(self, Stream stream) + cdef int _get_best_stream_index(self, Container container, lib.AVMediaType type_enum, Stream related) noexcept + diff --git a/av/container/streams.pyi b/av/container/streams.pyi index 9f67e7981..5d8647afe 100644 --- a/av/container/streams.pyi +++ b/av/container/streams.pyi @@ -1,4 +1,4 @@ -from typing import Iterator, overload +from typing import Iterator, Literal, overload from av.attachments.stream import AttachmentStream from av.audio.stream import AudioStream @@ -30,3 +30,9 @@ class StreamContainer: *args: int | Stream | dict[str, int | tuple[int, ...]], **kwargs: int | tuple[int, ...], ) -> list[Stream]: ... + def best( + self, + type: Literal["video", "audio", "subtitle", "data", "attachment"], + /, + related: Stream | None = None, + ) -> Stream | None: ... diff --git a/av/container/streams.pyx b/av/container/streams.pyx index 6071bc811..48830278c 100644 --- a/av/container/streams.pyx +++ b/av/container/streams.pyx @@ -1,4 +1,3 @@ - cimport libav as lib @@ -10,6 +9,19 @@ def _flatten(input_): else: yield x +cdef lib.AVMediaType _get_media_type_enum(str type): + if type == "video": + return lib.AVMEDIA_TYPE_VIDEO + elif type == "audio": + return lib.AVMEDIA_TYPE_AUDIO + elif type == "subtitle": + return lib.AVMEDIA_TYPE_SUBTITLE + elif type == "attachment": + return lib.AVMEDIA_TYPE_ATTACHMENT + elif type == "data": + return lib.AVMEDIA_TYPE_DATA + else: + raise ValueError(f"Invalid stream type: {type}") cdef class StreamContainer: """ @@ -121,3 +133,36 @@ cdef class StreamContainer: raise TypeError("Argument must be Stream or int.", type(x)) return selection or self._streams[:] + + cdef int _get_best_stream_index(self, Container container, lib.AVMediaType type_enum, Stream related) noexcept: + cdef int stream_index + + if related is None: + stream_index = lib.av_find_best_stream(container.ptr, type_enum, -1, -1, NULL, 0) + else: + stream_index = lib.av_find_best_stream(container.ptr, type_enum, -1, related.ptr.index, NULL, 0) + + return stream_index + + def best(self, str type, /, Stream related = None): + """best(type: Literal["video", "audio", "subtitle", "attachment", "data"], /, related: Stream | None) + Finds the "best" stream in the file. Wraps :ffmpeg:`av_find_best_stream` + + :param type: The type of stream to find + :param related: A related stream to use as a reference (optional) + :return: The best stream of the specified type + :rtype: Stream | None + """ + cdef type_enum = _get_media_type_enum(type) + + if len(self._streams) == 0: + return None + + cdef container = self._streams[0].container + + cdef int stream_index = self._get_best_stream_index(container, type_enum, related) + + if stream_index < 0: + return None + + return self._streams[stream_index] diff --git a/docs/api/attachments.rst b/docs/api/attachments.rst new file mode 100644 index 000000000..6e59a202b --- /dev/null +++ b/docs/api/attachments.rst @@ -0,0 +1,8 @@ + +Attachments +=========== + +.. automodule:: av.attachments.stream + + .. autoclass:: AttachmentStream + :members: diff --git a/docs/api/stream.rst b/docs/api/stream.rst index a6bc1fc8b..99a30b136 100644 --- a/docs/api/stream.rst +++ b/docs/api/stream.rst @@ -16,6 +16,8 @@ Dynamic Slicing .. automethod:: StreamContainer.get +.. automethod:: StreamContainer.best + Typed Collections ~~~~~~~~~~~~~~~~~ @@ -35,6 +37,10 @@ dynamic capabilities of :meth:`.get`: A tuple of :class:`SubtitleStream`. +.. attribute:: StreamContainer.attachments + + A tuple of :class:`AttachmentStream`. + .. attribute:: StreamContainer.data A tuple of :class:`DataStream`. diff --git a/include/libavformat/avformat.pxd b/include/libavformat/avformat.pxd index 29a892176..195134970 100644 --- a/include/libavformat/avformat.pxd +++ b/include/libavformat/avformat.pxd @@ -28,7 +28,6 @@ cdef extern from "libavformat/avformat.h" nogil: AVMEDIA_TYPE_NB cdef struct AVStream: - int index int id @@ -164,6 +163,15 @@ cdef extern from "libavformat/avformat.h" nogil: unsigned int max_probe_size ) + cdef int av_find_best_stream( + AVFormatContext *ic, + AVMediaType type, + int wanted_stream_nb, + int related_stream, + AVCodec **decoder_ret, + int flags + ) + cdef AVInputFormat* av_find_input_format(const char *name) # http://ffmpeg.org/doxygen/trunk/structAVFormatContext.html diff --git a/tests/test_streams.py b/tests/test_streams.py index a4e3eae5e..c146abfb1 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -14,16 +14,26 @@ def test_stream_tuples(self): audio_streams = tuple([s for s in container.streams if s.type == "audio"]) self.assertEqual(audio_streams, container.streams.audio) - def test_selection(self): - container = av.open(fate_suite("h264/interlaced_crop.mp4")) + def test_selection(self) -> None: + container = av.open( + fate_suite("amv/MTV_high_res_320x240_sample_Penguin_Joke_MTV_from_WMV.amv") + ) video = container.streams.video[0] - # audio_stream = container.streams.audio[0] - # audio_streams = list(container.streams.audio[0:2]) + audio = container.streams.audio[0] self.assertEqual([video], container.streams.get(video=0)) self.assertEqual([video], container.streams.get(video=(0,))) - # TODO: Find something in the fate suite with video, audio, and subtitles. + self.assertEqual(video, container.streams.best("video")) + self.assertEqual(audio, container.streams.best("audio")) + + container = av.open(fate_suite("sub/MovText_capability_tester.mp4")) + subtitle = container.streams.subtitles[0] + self.assertEqual(subtitle, container.streams.best("subtitle")) + + container = av.open(fate_suite("mxf/track_01_v02.mxf")) + data = container.streams.data[0] + self.assertEqual(data, container.streams.best("data")) def test_noside_data(self): container = av.open(fate_suite("h264/interlaced_crop.mp4")) From 5753e259fbcd4794aacb2c439d0dee72a173ea1a Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 11 Aug 2024 05:17:18 -0400 Subject: [PATCH 337/846] Add docstrings for best, AttachmentStream --- av/attachments/stream.pyx | 13 ++++++++++++- av/container/streams.pyx | 4 +++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/av/attachments/stream.pyx b/av/attachments/stream.pyx index b5b55ac1a..de7d10119 100644 --- a/av/attachments/stream.pyx +++ b/av/attachments/stream.pyx @@ -3,13 +3,24 @@ from av.stream cimport Stream cdef class AttachmentStream(Stream): """ - An :class:`AttachmentStream`. + An :class:`AttachmentStream` represents a stream of attachment data within a media container. + Typically used to attach font files that are referenced in ASS/SSA Subtitle Streams. """ @property def name(self): + """ + Returns the file name of the attachment. + + :rtype: str | None + """ return self.metadata.get("filename") @property def mimetype(self): + """ + Returns the MIME type of the attachment. + + :rtype: str | None + """ return self.metadata.get("mimetype") diff --git a/av/container/streams.pyx b/av/container/streams.pyx index 48830278c..17e4992d8 100644 --- a/av/container/streams.pyx +++ b/av/container/streams.pyx @@ -146,7 +146,9 @@ cdef class StreamContainer: def best(self, str type, /, Stream related = None): """best(type: Literal["video", "audio", "subtitle", "attachment", "data"], /, related: Stream | None) - Finds the "best" stream in the file. Wraps :ffmpeg:`av_find_best_stream` + Finds the "best" stream in the file. Wraps :ffmpeg:`av_find_best_stream`. Example:: + + stream = container.streams.best("video") :param type: The type of stream to find :param related: A related stream to use as a reference (optional) From cd56a75eac5b81f21166f29f649cadd91a17e5e9 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 11 Aug 2024 12:43:32 -0400 Subject: [PATCH 338/846] Return None instead of erroring --- av/video/codeccontext.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.pyx index c37223784..5764c400b 100644 --- a/av/video/codeccontext.pyx +++ b/av/video/codeccontext.pyx @@ -120,7 +120,7 @@ cdef class VideoCodecContext(CodecContext): :type: str | None """ - return getattr(self._format, "name") + return getattr(self._format, "name", None) @pix_fmt.setter def pix_fmt(self, value): From 8d76aac36788b5ce3811e151053f11a2690513bb Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 15 Aug 2024 17:16:44 -0400 Subject: [PATCH 339/846] Edit readme to reflect changes in build step --- README.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 70023dbf4..27e5182d6 100644 --- a/README.md +++ b/README.md @@ -49,13 +49,18 @@ git clone https://github.com/PyAV-Org/PyAV.git cd PyAV source scripts/activate.sh -# Either install the testing dependencies: -pip install --upgrade -r tests/requirements.txt -# or have it all, including FFmpeg, built/installed for you: +# Build ffmpeg from source. You can skip this step +# if ffmpeg is already installed. ./scripts/build-deps -# Build PyAV. +# Build PyAV make + +# Testing +make test + +# Install globally +deactivate pip install . ``` From 4f14a7cece991c3764416f6fdc8ff34092582eea Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 16 Aug 2024 20:17:58 -0400 Subject: [PATCH 340/846] Skip 3.13 Windows (broken numpy) --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 27e394948..91496725c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -72,7 +72,7 @@ jobs: CIBW_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m unittest discover -t {project} -s tests && mv {project}/av.disabled {project}/av CIBW_TEST_REQUIRES: numpy # skip tests when there are no binary wheels of numpy - CIBW_TEST_SKIP: pp* *_i686 + CIBW_TEST_SKIP: pp* *_i686 cp313-win_amd64 run: | pip install cibuildwheel delvewheel cibuildwheel --output-dir dist From 9b2576293aaa5fbb518a85e93400864a0c0d1b16 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 16 Aug 2024 23:07:55 -0400 Subject: [PATCH 341/846] We don't use setup.py to do tests anymore --- setup.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 7df0a9c2f..f3a3f6447 100644 --- a/setup.py +++ b/setup.py @@ -210,16 +210,13 @@ def parse_cflags(raw_flags): author="Mike Boers", author_email="pyav@mikeboers.com", url="https://github.com/PyAV-Org/PyAV", - packages=find_packages( - exclude=["build*", "examples*", "scratchpad*", "tests*", "include*"] - ) + packages=find_packages(exclude=["build*", "examples*", "tests*", "include*"]) + list(package_dir.keys()), package_dir=package_dir, package_data=package_data, python_requires=">=3.9", zip_safe=False, ext_modules=ext_modules, - test_suite="tests", entry_points={ "console_scripts": ["pyav = av.__main__:main"], }, From 8042cf8240d2bbd0a06d02d8218324aff1c4956a Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 18 Aug 2024 18:52:16 -0400 Subject: [PATCH 342/846] Try 3.13 Numpy --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 91496725c..27e394948 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -72,7 +72,7 @@ jobs: CIBW_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m unittest discover -t {project} -s tests && mv {project}/av.disabled {project}/av CIBW_TEST_REQUIRES: numpy # skip tests when there are no binary wheels of numpy - CIBW_TEST_SKIP: pp* *_i686 cp313-win_amd64 + CIBW_TEST_SKIP: pp* *_i686 run: | pip install cibuildwheel delvewheel cibuildwheel --output-dir dist From 64612012be2e0d6005d4f2d398d3ba1384fc7d4c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 19 Aug 2024 04:58:48 -0400 Subject: [PATCH 343/846] Add back channels property to AudioLayout --- av/audio/layout.pyi | 8 +++++++ av/audio/layout.pyx | 38 +++++++++++++++++++++++++++++++--- include/libavcodec/avcodec.pxd | 3 +++ setup.py | 18 ++++++++++++++++ tests/test_audiolayout.py | 5 ++++- 5 files changed, 68 insertions(+), 4 deletions(-) diff --git a/av/audio/layout.pyi b/av/audio/layout.pyi index 073cd1723..9fdf0ac15 100644 --- a/av/audio/layout.pyi +++ b/av/audio/layout.pyi @@ -1,4 +1,12 @@ +from dataclasses import dataclass + class AudioLayout: name: str nb_channels: int + channels: tuple[AudioChannel, ...] def __init__(self, layout: str | AudioLayout): ... + +@dataclass +class AudioChannel: + name: str + description: str diff --git a/av/audio/layout.pyx b/av/audio/layout.pyx index 59753138e..ea259d0fd 100644 --- a/av/audio/layout.pyx +++ b/av/audio/layout.pyx @@ -1,5 +1,16 @@ cimport libav as lib +from cpython.bytes cimport PyBytes_FromStringAndSize +from dataclasses import dataclass + + +@dataclass +class AudioChannel: + name: str + description: str + + def __repr__(self): + return f"" cdef object _cinit_bypass_sentinel @@ -40,13 +51,34 @@ cdef class AudioLayout: return self.layout.nb_channels @property - def name(self): + def channels(self): + cdef lib.AVChannel channel + cdef char buf[16] + cdef char buf2[128] + + results = [] + + for index in range(self.layout.nb_channels): + channel = lib.av_channel_layout_channel_from_index(&self.layout, index); + size = lib.av_channel_name(buf, sizeof(buf), channel) - 1 + size2 = lib.av_channel_description(buf2, sizeof(buf2), channel) - 1 + results.append( + AudioChannel( + PyBytes_FromStringAndSize(buf, size).decode("utf-8"), + PyBytes_FromStringAndSize(buf2, size2).decode("utf-8"), + ) + ) + + return tuple(results) + + @property + def name(self) -> str: """The canonical name of the audio layout.""" - cdef char layout_name[128] # Adjust buffer size as needed + cdef char layout_name[128] cdef int ret ret = lib.av_channel_layout_describe(&self.layout, layout_name, sizeof(layout_name)) if ret < 0: raise RuntimeError(f"Failed to get layout name: {ret}") - return layout_name + return layout_name \ No newline at end of file diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 9e9cc46a7..9add5ae2d 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -42,6 +42,9 @@ cdef extern from "libavutil/channel_layout.h": void av_channel_layout_uninit(AVChannelLayout *channel_layout) int av_channel_layout_copy(AVChannelLayout *dst, const AVChannelLayout *src) int av_channel_layout_describe(const AVChannelLayout *channel_layout, char *buf, size_t buf_size) + int av_channel_name(char *buf, size_t buf_size, AVChannel channel_id) + int av_channel_description(char *buf, size_t buf_size, AVChannel channel_id) + AVChannel av_channel_layout_channel_from_index(AVChannelLayout *channel_layout, unsigned int idx) cdef extern from "libavcodec/avcodec.h" nogil: diff --git a/setup.py b/setup.py index f3a3f6447..5329223b3 100644 --- a/setup.py +++ b/setup.py @@ -27,6 +27,21 @@ old_embed_signature = EmbedSignature._embed_signature +def insert_enum_in_generated_files(source): + # Work around Cython failing to add `enum` to `AVChannel` type. + # TODO: Make Cython bug report + if source.endswith(".c"): + with open(source, "r") as file: + content = file.read() + + # Replace "AVChannel __pyx_v_channel;" with "enum AVChannel __pyx_v_channel;" + modified_content = re.sub( + r"\b(? None: self.assertEqual(layout.name, "stereo") self.assertEqual(layout.nb_channels, 2) self.assertEqual(repr(layout), "") + + # Re-enable when FFmpeg 6.0 is dropped. + # self.assertEqual(layout.channels[0].name, "FL") # self.assertEqual(layout.channels[0].description, "front left") # self.assertEqual( From 1411bf77eec86bb47a2dc79c5a6aaeff7d7ca706 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 1 Sep 2024 03:14:04 -0400 Subject: [PATCH 344/846] Write release notes --- CHANGELOG.rst | 23 +++++++++++++++++++++++ av/about.py | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2b7de678b..45cbf280b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,29 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. +v13.0.0 +------- + +Major: + +- Drop FFmpeg 5, Support FFmpeg 7. +- Drop Python 3.8, Support Python 3.13. +- Update FFmpeg to 7.0.2 for the binary wheels. +- Disallow initializing an AudioLayout object with an int. +- Disallow accessing gop_size, timebase as a decoder (Raised deprecation warning before). +- Remove `ticks_per_frame` property because it was deprecated in FFmpeg. + +Features: + +- Add AttachmentStream class. +- Add `best()` method to StreamContainer. +- Add `set_audio_frame_size()` method to Graph object. +- Add `nb_channels` property to AudioLayout object. + +Fixes: + +- Fix VideoCC's repl breaking when `self._format` is None. +- Fix getting `pix_fmt` property when VideoCC's `self._format` is None. v12.3.0 ------- diff --git a/av/about.py b/av/about.py index 79a759ae1..1311252a5 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "13.0.0rc1" +__version__ = "13.0.0" From 5dc17e8a49d929bda845096816390479e5b37bf9 Mon Sep 17 00:00:00 2001 From: Dexer <73297572+DexerBR@users.noreply.github.com> Date: Mon, 2 Sep 2024 13:10:43 -0300 Subject: [PATCH 345/846] add from_bytes --- av/video/frame.pyi | 9 +++++++++ av/video/frame.pyx | 40 ++++++++++++++++++++++++++++++++++------ 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/av/video/frame.pyi b/av/video/frame.pyi index 17cacf05a..9c04c9bac 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -63,3 +63,12 @@ class VideoFrame(Frame): ) -> VideoFrame: ... @staticmethod def from_ndarray(array: _SupportedNDarray, format: str = "rgb24") -> VideoFrame: ... + @staticmethod + def from_bytes( + data: bytes, + width: int, + height: int, + format="rgba", + flip_horizontal=False, + flip_vertical=False, + ) -> VideoFrame: ... diff --git a/av/video/frame.pyx b/av/video/frame.pyx index c27ead7b4..ee060e16a 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -40,9 +40,8 @@ cdef byteswap_array(array, bint big_endian): return array -cdef copy_array_to_plane(array, VideoPlane plane, unsigned int bytes_per_pixel): - cdef bytes imgbytes = array.tobytes() - cdef const uint8_t[:] i_buf = imgbytes +cdef copy_bytes_to_plane(img_bytes, VideoPlane plane, unsigned int bytes_per_pixel, bint flip_horizontal, bint flip_vertical): + cdef const uint8_t[:] i_buf = img_bytes cdef size_t i_pos = 0 cdef size_t i_stride = plane.width * bytes_per_pixel cdef size_t i_size = plane.height * i_stride @@ -51,12 +50,33 @@ cdef copy_array_to_plane(array, VideoPlane plane, unsigned int bytes_per_pixel): cdef size_t o_pos = 0 cdef size_t o_stride = abs(plane.line_size) - while i_pos < i_size: - o_buf[o_pos:o_pos + i_stride] = i_buf[i_pos:i_pos + i_stride] - i_pos += i_stride + cdef int start_row, end_row, step + if flip_vertical: + start_row = plane.height - 1 + end_row = -1 + step = -1 + else: + start_row = 0 + end_row = plane.height + step = 1 + + cdef int i, j + for row in range(start_row, end_row, step): + i_pos = row * i_stride + if flip_horizontal: + for i in range(0, i_stride, bytes_per_pixel): + for j in range(bytes_per_pixel): + o_buf[o_pos + i + j] = i_buf[i_pos + i_stride - i - bytes_per_pixel + j] + else: + o_buf[o_pos:o_pos + i_stride] = i_buf[i_pos:i_pos + i_stride] o_pos += o_stride +cdef copy_array_to_plane(array, VideoPlane plane, unsigned int bytes_per_pixel): + cdef bytes imgbytes = array.tobytes() + copy_bytes_to_plane(imgbytes, plane, bytes_per_pixel, False, False) + + cdef useful_array(VideoPlane plane, unsigned int bytes_per_pixel=1, str dtype="uint8"): """ Return the useful part of the VideoPlane as a single dimensional array. @@ -570,3 +590,11 @@ cdef class VideoFrame(Frame): copy_array_to_plane(array, frame.planes[0], 1 if array.ndim == 2 else array.shape[2]) return frame + + def from_bytes(img_bytes: bytes, width: int, height: int, format="rgba", flip_horizontal=False, flip_vertical=False): + frame = VideoFrame(width, height, format) + if format == "rgba": + copy_bytes_to_plane(img_bytes, frame.planes[0], 4, flip_horizontal, flip_vertical) + else: + raise NotImplementedError(f"Format '{format}' is not supported.") + return frame From 0e4b711ab7f5b67f6490c7a0f23b555e87d89d6c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 3 Sep 2024 02:22:21 -0400 Subject: [PATCH 346/846] Update type stubs for `from_bytes()` --- AUTHORS.rst | 2 ++ CHANGELOG.rst | 1 + av/video/frame.pyi | 6 +++--- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index af7ea08cc..bf3a6f49d 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -48,12 +48,14 @@ All contributors (by number of commits): * mephi42 * Miles Kaufmann * Pablo Prietz +* Andrew Wason * Radek Senfeld ; `@radek-senfeld `_ * Benjamin Chrétien <2742231+bchretien@users.noreply.github.com> * Marc Mueller <30130371+cdce8p@users.noreply.github.com> * zzjjbb <31069326+zzjjbb@users.noreply.github.com> * Hanz <40712686+HanzCEO@users.noreply.github.com> * Joe Schiff <41972063+JoeSchiff@users.noreply.github.com> +* Dexer <73297572+DexerBR@users.noreply.github.com> * Artturin * Ian Lee * Ryan Huang diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 45cbf280b..75bd7adff 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -34,6 +34,7 @@ Features: - Add `best()` method to StreamContainer. - Add `set_audio_frame_size()` method to Graph object. - Add `nb_channels` property to AudioLayout object. +- Add `from_bytes()` method to VideoFrame object. Fixes: diff --git a/av/video/frame.pyi b/av/video/frame.pyi index 9c04c9bac..de84faaa0 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -68,7 +68,7 @@ class VideoFrame(Frame): data: bytes, width: int, height: int, - format="rgba", - flip_horizontal=False, - flip_vertical=False, + format: str = "rgba", + flip_horizontal: bool = False, + flip_vertical: bool = False, ) -> VideoFrame: ... From 75c0e0bb1cee28c20564e75ed4b42d08499ea974 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 3 Sep 2024 17:42:52 -0400 Subject: [PATCH 347/846] Remove unneeded libraries --- .github/workflows/smoke.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 4dbcc096b..f5fabe342 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -60,16 +60,13 @@ jobs: ubuntu-latest) sudo apt-get update sudo apt-get install autoconf automake build-essential cmake \ - libtool mercurial pkg-config texinfo wget yasm zlib1g-dev - sudo apt-get install libass-dev libfreetype6-dev libjpeg-dev \ - libtheora-dev libvorbis-dev libx264-dev + libtool pkg-config yasm zlib1g-dev libvorbis-dev libx264-dev if [[ "${{ matrix.config.extras }}" ]]; then - sudo apt-get install doxygen + sudo apt-get install doxygen wget fi ;; macos-12) - brew install automake libtool nasm pkg-config shtool texi2html wget - brew install libass libjpeg libpng libvorbis libvpx opus theora x264 + brew install automake libtool nasm pkg-config libpng libvorbis libvpx opus x264 ;; esac From 6b20f97fec0735034fe243fddaec7bd5a8cb945d Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 4 Sep 2024 00:03:44 -0400 Subject: [PATCH 348/846] Use GitHub Discussions over Gitter GH Discussions support upvoting threads and upvoting. Features lacking in Gitter. Gitter is also less activate. --- README.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 27e5182d6..10cd7709d 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ PyAV ==== -[![GitHub Test Status][github-tests-badge]][github-tests] -[![Gitter Chat][gitter-badge]][gitter] [![Documentation][docs-badge]][docs] \ -[![Python Package Index][pypi-badge]][pypi] [![Conda Forge][conda-badge]][conda] - PyAV is a Pythonic binding for the [FFmpeg][ffmpeg] libraries. We aim to provide all of the power and control of the underlying library, but manage the gritty details as much as possible. +--- + +[![GitHub Test Status][github-tests-badge]][github-tests] [![Documentation][docs-badge]][docs] [![Python Package Index][pypi-badge]][pypi] [![Conda Forge][conda-badge]][conda] + PyAV is for direct and precise access to your media via containers, streams, packets, codecs, and frames. It exposes a few transformations of that data, and helps you get your data to/from other packages (e.g. Numpy and Pillow). This power does come with some responsibility as working with media is horrendously complicated and PyAV can't abstract it away or make all the best decisions for you. If the `ffmpeg` command does the job without you bending over backwards, PyAV is likely going to be more of a hindrance than a help. @@ -66,7 +66,7 @@ pip install . --- -Have fun, [read the docs][docs], [come chat with us][gitter], and good luck! +Have fun, [read the docs][docs], [come chat with us][discuss], and good luck! @@ -74,10 +74,9 @@ Have fun, [read the docs][docs], [come chat with us][gitter], and good luck! [conda]: https://anaconda.org/conda-forge/av [docs-badge]: https://img.shields.io/badge/docs-on%20pyav.basswood--io.com-blue.svg [docs]: https://pyav.basswood-io.com -[gitter-badge]: https://img.shields.io/gitter/room/nwjs/nw.js.svg?logo=gitter&colorB=cc2b5e -[gitter]: https://app.gitter.im/#/room/#PyAV-Org_User-Help:gitter.im [pypi-badge]: https://img.shields.io/pypi/v/av.svg?colorB=CCB39A [pypi]: https://pypi.org/project/av +[discuss]: https://github.com/PyAV-Org/PyAV/discussions [github-tests-badge]: https://github.com/PyAV-Org/PyAV/workflows/tests/badge.svg [github-tests]: https://github.com/PyAV-Org/PyAV/actions?workflow=tests From 71ad5c3ecda2500c5b48bf9beeb854e10927fbee Mon Sep 17 00:00:00 2001 From: Mark Harfouche Date: Wed, 4 Sep 2024 19:15:30 -0400 Subject: [PATCH 349/846] Declare `write_packet` C function as const Otherwise llvm 17 complains --- include/libavformat/avformat.pxd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/libavformat/avformat.pxd b/include/libavformat/avformat.pxd index 195134970..1e16378bf 100644 --- a/include/libavformat/avformat.pxd +++ b/include/libavformat/avformat.pxd @@ -79,7 +79,7 @@ cdef extern from "libavformat/avformat.h" nogil: int write_flag, void *opaque, int(*read_packet)(void *opaque, uint8_t *buf, int buf_size), - int(*write_packet)(void *opaque, uint8_t *buf, int buf_size), + int(*write_packet)(void *opaque, const uint8_t *buf, int buf_size), int64_t(*seek)(void *opaque, int64_t offset, int whence) ) From 880903280d54309d800aefeabc4468cf7cbdbcb3 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 11 Sep 2024 17:01:48 -0400 Subject: [PATCH 350/846] Update issue templates --- .github/ISSUE_TEMPLATE/ffmpeg-feature-request.md | 6 ------ .github/ISSUE_TEMPLATE/pyav-feature-request.md | 6 ------ .github/ISSUE_TEMPLATE/runtime-bug-report.md | 7 ++----- 3 files changed, 2 insertions(+), 17 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/ffmpeg-feature-request.md b/.github/ISSUE_TEMPLATE/ffmpeg-feature-request.md index 115c15b13..d47b011e1 100644 --- a/.github/ISSUE_TEMPLATE/ffmpeg-feature-request.md +++ b/.github/ISSUE_TEMPLATE/ffmpeg-feature-request.md @@ -2,7 +2,6 @@ name: FFmpeg feature request about: Request a feature of FFmpeg be exposed or supported by PyAV. title: "Allow FOO to BAR" -labels: enhancement assignees: '' --- @@ -51,11 +50,6 @@ Example: ``` {{ Complete output of `python setup.py config --verbose`. }} ``` -- FFmpeg: -``` -{{ Complete output of `ffmpeg -version` }} -``` - ## Additional context diff --git a/.github/ISSUE_TEMPLATE/pyav-feature-request.md b/.github/ISSUE_TEMPLATE/pyav-feature-request.md index 7433cbfbe..cd024d61d 100644 --- a/.github/ISSUE_TEMPLATE/pyav-feature-request.md +++ b/.github/ISSUE_TEMPLATE/pyav-feature-request.md @@ -2,7 +2,6 @@ name: PyAV feature request about: Request a feature of PyAV that is not provided by FFmpeg. title: "Allow FOO to BAR" -labels: enhancement assignees: '' --- @@ -10,11 +9,6 @@ assignees: '' **IMPORTANT:** Be sure to replace all template sections {{ like this }} or your issue may be discarded. -## Overview - -{{ A clear and concise description of what the feature is. }} - - ## Desired Behavior {{ A description of how you think PyAV should behave. }} diff --git a/.github/ISSUE_TEMPLATE/runtime-bug-report.md b/.github/ISSUE_TEMPLATE/runtime-bug-report.md index 267598841..9c8632a7d 100644 --- a/.github/ISSUE_TEMPLATE/runtime-bug-report.md +++ b/.github/ISSUE_TEMPLATE/runtime-bug-report.md @@ -2,7 +2,6 @@ name: Runtime bug report about: Report on an issue while running PyAV. title: "The FOO does not BAR." -labels: bug assignees: '' --- @@ -47,10 +46,8 @@ Traceback: ``` {{ Complete output of `python -m av --version`. If this command won't run, you are likely dealing with the build issue and should use the appropriate template. }} ``` -- PyAV build: -``` -{{ Complete output of `python setup.py config --verbose`. }} -``` +- [ ] I am/tried using the binary wheels +- [ ] I compiled from source ## Research From ee5c40e41ca8badf4a14c38794cb19647084f656 Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Wed, 11 Sep 2024 18:58:55 -0600 Subject: [PATCH 351/846] Allow `extradata` to be set for encoders --- av/codec/context.pyx | 3 --- av/container/output.pyx | 21 +++++++++++++++------ tests/test_codec_context.py | 6 +++--- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 66b21f9a3..e2e3632ee 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -189,9 +189,6 @@ cdef class CodecContext: @extradata.setter def extradata(self, data): - if not self.is_decoder: - raise ValueError("Can only set extradata for decoders.") - if data is None: lib.av_freep(&self.ptr.extradata) self.ptr.extradata_size = 0 diff --git a/av/container/output.pyx b/av/container/output.pyx index 00f5d55b3..717df8fb3 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -93,10 +93,14 @@ cdef class OutputContainer(Container): # Now lets set some more sane video defaults elif codec.type == lib.AVMEDIA_TYPE_VIDEO: codec_context.pix_fmt = lib.AV_PIX_FMT_YUV420P - codec_context.width = 640 - codec_context.height = 480 - codec_context.bit_rate = 1024000 - codec_context.bit_rate_tolerance = 128000 + codec_context.width = kwargs.pop("width", 640) + codec_context.height = kwargs.pop("height", 480) + codec_context.bit_rate = kwargs.pop("bit_rate", 1024000) + codec_context.bit_rate_tolerance = kwargs.pop("bit_rate_tolerance", 128000) + try: + to_avrational(kwargs.pop("time_base"), &codec_context.time_base) + except KeyError: + pass to_avrational(rate or 24, &codec_context.framerate) stream.avg_frame_rate = codec_context.framerate @@ -105,9 +109,14 @@ cdef class OutputContainer(Container): # Some sane audio defaults elif codec.type == lib.AVMEDIA_TYPE_AUDIO: codec_context.sample_fmt = codec.sample_fmts[0] - codec_context.bit_rate = 128000 - codec_context.bit_rate_tolerance = 32000 + codec_context.bit_rate = kwargs.pop("bit_rate", 128000) + codec_context.bit_rate_tolerance = kwargs.pop("bit_rate_tolerance", 32000) + try: + to_avrational(kwargs.pop("time_base"), &codec_context.time_base) + except KeyError: + pass codec_context.sample_rate = rate or 48000 + stream.time_base = codec_context.time_base lib.av_channel_layout_default(&codec_context.ch_layout, 2) # Some formats want stream headers to be separate diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 7ada318d9..634fe0280 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -105,9 +105,9 @@ def test_encoder_extradata(self): self.assertEqual(ctx.extradata, None) self.assertEqual(ctx.extradata_size, 0) - with self.assertRaises(ValueError) as cm: - ctx.extradata = b"123" - self.assertEqual(str(cm.exception), "Can only set extradata for decoders.") + ctx.extradata = b"123" + self.assertEqual(ctx.extradata, b"123") + self.assertEqual(ctx.extradata_size, 3) def test_encoder_pix_fmt(self): ctx = av.codec.Codec("h264", "w").create() From 00b914e5d38a5a8846696dd0959248ac77129d62 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 18 Sep 2024 00:27:42 -0400 Subject: [PATCH 352/846] Add audio atempo example --- examples/audio_atempo.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 examples/audio_atempo.py diff --git a/examples/audio_atempo.py b/examples/audio_atempo.py new file mode 100644 index 000000000..62a126705 --- /dev/null +++ b/examples/audio_atempo.py @@ -0,0 +1,36 @@ +import av + +av.logging.set_level(av.logging.VERBOSE) + +input_file = av.open("input.wav") +output_file = av.open("output.wav", mode="w") + +input_stream = input_file.streams.audio[0] + +output_stream = output_file.add_stream("pcm_s16le", rate=input_stream.rate) +assert isinstance(output_stream, av.audio.AudioStream) + +graph = av.filter.Graph() + +graph.link_nodes( + graph.add_abuffer(template=input_stream), + graph.add("atempo", "2.0"), + graph.add("abuffersink"), +).configure() + +for frame in input_file.decode(input_stream): + graph.push(frame) + while True: + try: + for packet in output_stream.encode(graph.pull()): + output_file.mux(packet) + except (av.BlockingIOError, av.EOFError): + break + +# Flush the stream +for packet in output_stream.encode(None): + output_file.mux(packet) + +# Close the files +input_file.close() +output_file.close() From d8819800d14255199915315fb4ce0f01ee8b94eb Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 18 Sep 2024 12:28:25 -0400 Subject: [PATCH 353/846] Add smart overloading for `add_stream()` --- av/container/output.pyi | 21 ++++++++++++++++++++- av/stream.pyi | 3 ++- av/video/format.pyi | 2 ++ tests/test_encode.py | 26 +++++++++----------------- 4 files changed, 33 insertions(+), 19 deletions(-) diff --git a/av/container/output.pyi b/av/container/output.pyi index 9e46b413a..8cbd5543b 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -1,13 +1,32 @@ from fractions import Fraction -from typing import Sequence +from typing import Literal, Sequence, overload +from av.audio.stream import AudioStream from av.packet import Packet from av.stream import Stream +from av.video.stream import VideoStream from .core import Container class OutputContainer(Container): def __enter__(self) -> OutputContainer: ... + @overload + def add_stream( + self, + codec_name: Literal["pcm_s16le", "aac", "mp3", "mp2"], + rate: Fraction | int | float | None = None, + template: None = None, + options: dict[str, str] | None = None, + ) -> AudioStream: ... + @overload + def add_stream( + self, + codec_name: Literal["h264", "mpeg4", "png"], + rate: Fraction | int | float | None = None, + template: None = None, + options: dict[str, str] | None = None, + ) -> VideoStream: ... + @overload def add_stream( self, codec_name: str | None = None, diff --git a/av/stream.pyi b/av/stream.pyi index 4c4c05030..212c721a9 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -1,7 +1,7 @@ from fractions import Fraction from typing import Literal -from .codec import CodecContext +from .codec import Codec, CodecContext from .container import Container from .enum import EnumItem from .frame import Frame @@ -15,6 +15,7 @@ class Stream: thread_type: Literal["NONE", "FRAME", "SLICE", "AUTO"] container: Container + codec: Codec codec_context: CodecContext metadata: dict[str, str] id: int diff --git a/av/video/format.pyi b/av/video/format.pyi index 16739a0e3..17c66de82 100644 --- a/av/video/format.pyi +++ b/av/video/format.pyi @@ -7,6 +7,8 @@ class VideoFormat: is_bit_stream: bool is_planar: bool is_rgb: bool + width: int + height: int def __init__(self, name: str, width: int = 0, height: int = 0) -> None: ... def chroma_width(self, luma_width: int = 0) -> int: ... diff --git a/tests/test_encode.py b/tests/test_encode.py index 58d3b1515..e7c3c02f3 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -1,7 +1,6 @@ import io import math from fractions import Fraction -from typing import cast from unittest import SkipTest import numpy as np @@ -27,7 +26,7 @@ def write_rgb_rotate(output: av.container.OutputContainer) -> None: output.metadata["title"] = "container" output.metadata["key"] = "value" - stream = cast(VideoStream, output.add_stream("mpeg4", 24)) + stream = output.add_stream("mpeg4", 24) stream.width = WIDTH stream.height = HEIGHT stream.pix_fmt = "yuv420p" @@ -66,15 +65,13 @@ def write_rgb_rotate(output: av.container.OutputContainer) -> None: output.mux(packet) -def assert_rgb_rotate(self, input_, is_dash=False): +def assert_rgb_rotate( + self, input_: av.container.InputContainer, is_dash: bool = False +) -> None: # Now inspect it a little. self.assertEqual(len(input_.streams), 1) if is_dash: - # FFmpeg 4.2 added parsing of the programme information and it is named "Title" - if av.library_versions["libavformat"] >= (58, 28): - self.assertTrue( - input_.metadata.get("Title") == "container", input_.metadata - ) + self.assertTrue(input_.metadata.get("Title") == "container", input_.metadata) else: self.assertEqual(input_.metadata.get("title"), "container", input_.metadata) self.assertEqual(input_.metadata.get("key"), None) @@ -90,19 +87,14 @@ def assert_rgb_rotate(self, input_, is_dash=False): expected_frames = 0 expected_id = 0 else: - if av.library_versions["libavformat"] < (58, 76): - # FFmpeg < 4.4 - expected_average_rate = Fraction(1152, 47) - expected_duration = 24064 - else: - # FFmpeg >= 4.4 - expected_average_rate = 24 - expected_duration = 24576 + expected_average_rate = 24 + expected_duration = 24576 expected_frames = 48 expected_id = 1 # actual stream properties self.assertIsInstance(stream, VideoStream) + assert isinstance(stream, VideoStream) self.assertEqual(stream.average_rate, expected_average_rate) self.assertEqual(stream.base_rate, 24) self.assertEqual(stream.duration, expected_duration) @@ -124,7 +116,7 @@ def assert_rgb_rotate(self, input_, is_dash=False): class TestBasicVideoEncoding(TestCase): - def test_default_options(self): + def test_default_options(self) -> None: with av.open(self.sandboxed("output.mov"), "w") as output: stream = output.add_stream("mpeg4") self.assertIn(stream, output.streams.video) From 2fd2210b157aebf248a2192b14cfeb74ee5c6be5 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 19 Sep 2024 21:54:10 -0400 Subject: [PATCH 354/846] Add type stubs for test_encode.py --- Makefile | 2 +- av/audio/codeccontext.pyi | 10 +++++++-- av/audio/stream.pyi | 12 +++++++--- av/container/output.pyi | 4 ++++ av/video/codeccontext.pyi | 1 + av/video/stream.pyi | 1 + tests/test_encode.py | 46 +++++++++++++++++++-------------------- 7 files changed, 46 insertions(+), 30 deletions(-) diff --git a/Makefile b/Makefile index 4adb3b47f..2dc617edf 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ fate-suite: rsync -vrltLW rsync://fate-suite.ffmpeg.org/fate-suite/ tests/assets/fate-suite/ lint: - $(PIP) install -U black isort flake8 flake8-pyproject pillow numpy mypy==1.10.0 + $(PIP) install -U black isort flake8 flake8-pyproject pillow numpy mypy==1.11.2 black --check av examples tests setup.py flake8 av examples tests isort --check-only --diff av examples tests diff --git a/av/audio/codeccontext.pyi b/av/audio/codeccontext.pyi index e11a42cdc..afd28ec34 100644 --- a/av/audio/codeccontext.pyi +++ b/av/audio/codeccontext.pyi @@ -11,10 +11,16 @@ class AudioCodecContext(CodecContext): frame_size: int sample_rate: int rate: int - layout: AudioLayout - format: AudioFormat type: Literal["audio"] + @property + def format(self) -> AudioFormat: ... + @format.setter + def format(self, value: AudioFormat | str) -> None: ... + @property + def layout(self) -> AudioLayout: ... + @layout.setter + def layout(self, value: AudioLayout | str) -> None: ... @property def channels(self) -> int: ... def encode(self, frame: AudioFrame | None = None) -> list[Packet]: ... diff --git a/av/audio/stream.pyi b/av/audio/stream.pyi index 9e972c56d..48a9d65c6 100644 --- a/av/audio/stream.pyi +++ b/av/audio/stream.pyi @@ -13,12 +13,18 @@ class AudioStream(Stream): # From codec context frame_size: int sample_rate: int + bit_rate: int rate: int channels: int - channel_layout: int - layout: AudioLayout - format: AudioFormat type: Literal["audio"] + @property + def format(self) -> AudioFormat: ... + @format.setter + def format(self, value: AudioFormat | str) -> None: ... + @property + def layout(self) -> AudioLayout: ... + @layout.setter + def layout(self, value: AudioLayout | str) -> None: ... def encode(self, frame: AudioFrame | None = None) -> list[Packet]: ... def decode(self, packet: Packet | None = None) -> list[AudioFrame]: ... diff --git a/av/container/output.pyi b/av/container/output.pyi index 8cbd5543b..a3218ae34 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -1,6 +1,7 @@ from fractions import Fraction from typing import Literal, Sequence, overload +from av.audio.layout import AudioLayout from av.audio.stream import AudioStream from av.packet import Packet from av.stream import Stream @@ -17,6 +18,7 @@ class OutputContainer(Container): rate: Fraction | int | float | None = None, template: None = None, options: dict[str, str] | None = None, + **kwargs, ) -> AudioStream: ... @overload def add_stream( @@ -25,6 +27,7 @@ class OutputContainer(Container): rate: Fraction | int | float | None = None, template: None = None, options: dict[str, str] | None = None, + **kwargs, ) -> VideoStream: ... @overload def add_stream( @@ -33,6 +36,7 @@ class OutputContainer(Container): rate: Fraction | int | float | None = None, template: Stream | None = None, options: dict[str, str] | None = None, + **kwargs, ) -> Stream: ... def start_encoding(self) -> None: ... def close(self) -> None: ... diff --git a/av/video/codeccontext.pyi b/av/video/codeccontext.pyi index 7bdda89e0..ebef8b275 100644 --- a/av/video/codeccontext.pyi +++ b/av/video/codeccontext.pyi @@ -19,6 +19,7 @@ class VideoCodecContext(CodecContext): sample_aspect_ratio: Fraction | None display_aspect_ratio: Fraction | None has_b_frames: bool + max_b_frames: int coded_width: int coded_height: int color_range: int diff --git a/av/video/stream.pyi b/av/video/stream.pyi index 929847ed4..94ffccd64 100644 --- a/av/video/stream.pyi +++ b/av/video/stream.pyi @@ -27,6 +27,7 @@ class VideoStream(Stream): rate: Fraction gop_size: int has_b_frames: bool + max_b_frames: int coded_width: int coded_height: int color_range: int diff --git a/tests/test_encode.py b/tests/test_encode.py index e7c3c02f3..f19ad709d 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -132,7 +132,7 @@ def test_default_options(self) -> None: self.assertEqual(stream.pix_fmt, "yuv420p") self.assertEqual(stream.width, 640) - def test_encoding(self): + def test_encoding(self) -> None: path = self.sandboxed("rgb_rotate.mov") with av.open(path, "w") as output: @@ -140,7 +140,7 @@ def test_encoding(self): with av.open(path) as input: assert_rgb_rotate(self, input) - def test_encoding_with_pts(self): + def test_encoding_with_pts(self) -> None: path = self.sandboxed("video_with_pts.mov") with av.open(path, "w") as output: @@ -163,7 +163,7 @@ def test_encoding_with_pts(self): self.assertEqual(packet.time_base, Fraction(1, 24)) output.mux(packet) - def test_encoding_with_unicode_filename(self): + def test_encoding_with_unicode_filename(self) -> None: path = self.sandboxed("¢∞§¶•ªº.mov") with av.open(path, "w") as output: @@ -173,7 +173,7 @@ def test_encoding_with_unicode_filename(self): class TestBasicAudioEncoding(TestCase): - def test_default_options(self): + def test_default_options(self) -> None: with av.open(self.sandboxed("output.mov"), "w") as output: stream = output.add_stream("mp2") self.assertIn(stream, output.streams.audio) @@ -184,7 +184,7 @@ def test_default_options(self): self.assertEqual(stream.format.name, "s16") self.assertEqual(stream.sample_rate, 48000) - def test_transcode(self): + def test_transcode(self) -> None: path = self.sandboxed("audio_transcode.mov") with av.open(path, "w") as output: @@ -199,10 +199,9 @@ def test_transcode(self): self.assertIn(stream, output.streams.audio) ctx = stream.codec_context - ctx.time_base = sample_rate ctx.sample_rate = sample_rate - ctx.format = sample_fmt - ctx.layout = channel_layout + stream.format = sample_fmt # type: ignore + ctx.layout = channel_layout # type: ignore with av.open( fate_suite("audio-reference/chorusnoise_2ch_44kHz_s16.wav") @@ -230,7 +229,7 @@ def test_transcode(self): class TestEncodeStreamSemantics(TestCase): - def test_stream_index(self): + def test_stream_index(self) -> None: with av.open(self.sandboxed("output.mov"), "w") as output: vstream = output.add_stream("mpeg4", 24) self.assertIn(vstream, output.streams.video) @@ -240,8 +239,8 @@ def test_stream_index(self): astream = output.add_stream("mp2", 48000) self.assertIn(astream, output.streams.audio) - astream.layout = "stereo" - astream.format = "s16" + astream.layout = "stereo" # type: ignore + astream.format = "s16" # type: ignore self.assertEqual(vstream.index, 0) self.assertEqual(astream.index, 1) @@ -268,7 +267,7 @@ def test_stream_index(self): self.assertIs(apacket.stream, astream) self.assertEqual(apacket.stream_index, 1) - def test_stream_audio_resample(self): + def test_stream_audio_resample(self) -> None: with av.open(self.sandboxed("output.mov"), "w") as output: vstream = output.add_stream("mpeg4", 24) vstream.pix_fmt = "yuv420p" @@ -299,7 +298,7 @@ def test_stream_audio_resample(self): self.assertEqual(apacket.pts, pts_expected.pop(0)) self.assertEqual(apacket.time_base, Fraction(1, 8000)) - def test_set_id_and_time_base(self): + def test_set_id_and_time_base(self) -> None: with av.open(self.sandboxed("output.mov"), "w") as output: stream = output.add_stream("mp2") self.assertIn(stream, output.streams.audio) @@ -315,7 +314,7 @@ def test_set_id_and_time_base(self): self.assertEqual(stream.time_base, Fraction(1, 48000)) -def encode_file_with_max_b_frames(max_b_frames): +def encode_file_with_max_b_frames(max_b_frames: int) -> io.BytesIO: """ Create an encoded video file (or file-like object) with the given maximum run of B frames. @@ -354,7 +353,7 @@ def encode_file_with_max_b_frames(max_b_frames): return file -def max_b_frame_run_in_file(file): +def max_b_frame_run_in_file(file: io.BytesIO) -> int: """ Count the maximum run of B frames in a file (or file-like object). @@ -363,18 +362,17 @@ def max_b_frame_run_in_file(file): Returns: non-negative integer which is the maximum B frame run length. """ - container = av.open(file) + container = av.open(file, "r") stream = container.streams.video[0] max_b_frame_run = 0 b_frame_run = 0 - for packet in container.demux(stream): - for frame in packet.decode(): - if frame.pict_type == av.video.frame.PictureType.B: - b_frame_run += 1 - else: - max_b_frame_run = max(max_b_frame_run, b_frame_run) - b_frame_run = 0 + for frame in container.decode(stream): + if frame.pict_type == av.video.frame.PictureType.B: + b_frame_run += 1 + else: + max_b_frame_run = max(max_b_frame_run, b_frame_run) + b_frame_run = 0 # Outside chance that the longest run was at the end of the file. max_b_frame_run = max(max_b_frame_run, b_frame_run) @@ -385,7 +383,7 @@ def max_b_frame_run_in_file(file): class TestMaxBFrameEncoding(TestCase): - def test_max_b_frames(self): + def test_max_b_frames(self) -> None: """ Test that we never get longer runs of B frames than we asked for with the max_b_frames property. From b074fe6faac5ecc56ea79fc8902511b6c320536b Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 19 Sep 2024 23:04:29 -0400 Subject: [PATCH 355/846] Add type stubs for test_audiofifo.py --- av/audio/fifo.pyi | 6 +++++ tests/common.py | 8 +++---- tests/test_audiofifo.py | 52 +++++++++++++++++++++-------------------- tests/test_encode.py | 2 +- 4 files changed, 38 insertions(+), 30 deletions(-) diff --git a/av/audio/fifo.pyi b/av/audio/fifo.pyi index aece9d381..085ed4bba 100644 --- a/av/audio/fifo.pyi +++ b/av/audio/fifo.pyi @@ -14,3 +14,9 @@ class AudioFifo: def sample_rate(self) -> int: ... @property def samples(self) -> int: ... + @property + def samples_written(self) -> int: ... + @property + def samples_read(self) -> int: ... + @property + def pts_per_sample(self) -> float: ... diff --git a/tests/common.py b/tests/common.py index 6c2b2d2e1..8c0db1129 100644 --- a/tests/common.py +++ b/tests/common.py @@ -57,7 +57,7 @@ def fate_png(): return fate_suite("png1/55c99e750a5fd6_50314226.png") -def sandboxed(*args, **kwargs): +def sandboxed(*args, **kwargs) -> str: do_makedirs = kwargs.pop("makedirs", True) base = kwargs.pop("sandbox", None) timed = kwargs.pop("timed", False) @@ -114,16 +114,16 @@ def _filter(self, type_): class TestCase(_Base): @classmethod - def _sandbox(cls, timed=True): + def _sandbox(cls, timed: bool = True) -> str: path = os.path.join(_sandbox(timed=timed), cls.__name__) makedirs(path) return path @property - def sandbox(self): + def sandbox(self) -> str: return self._sandbox(timed=True) - def sandboxed(self, *args, **kwargs): + def sandboxed(self, *args, **kwargs) -> str: kwargs.setdefault("sandbox", self.sandbox) kwargs.setdefault("timed", True) return sandboxed(*args, **kwargs) diff --git a/tests/test_audiofifo.py b/tests/test_audiofifo.py index 0cbb4acc4..2874c7145 100644 --- a/tests/test_audiofifo.py +++ b/tests/test_audiofifo.py @@ -1,10 +1,12 @@ +from fractions import Fraction + import av from .common import TestCase, fate_suite class TestAudioFifo(TestCase): - def test_data(self): + def test_data(self) -> None: container = av.open(fate_suite("audio-reference/chorusnoise_2ch_44kHz_s16.wav")) stream = container.streams.audio[0] @@ -13,23 +15,22 @@ def test_data(self): input_ = [] output = [] - for i, packet in enumerate(container.demux(stream)): - for frame in packet.decode(): - input_.append(bytes(frame.planes[0])) - fifo.write(frame) - for frame in fifo.read_many(512, partial=i == 10): - output.append(bytes(frame.planes[0])) + for i, frame in enumerate(container.decode(stream)): + input_.append(bytes(frame.planes[0])) + fifo.write(frame) + for frame in fifo.read_many(512, partial=i == 10): + output.append(bytes(frame.planes[0])) if i == 10: break - input_ = b"".join(input_) - output = b"".join(output) - min_len = min(len(input_), len(output)) + input_bytes = b"".join(input_) + output_bytes = b"".join(output) + min_len = min(len(input_bytes), len(output_bytes)) - self.assertTrue(min_len > 10 * 512 * 2 * 2) - self.assertTrue(input_[:min_len] == output[:min_len]) + assert min_len > 10 * 512 * 2 * 2 + assert input_bytes[:min_len] == output_bytes[:min_len] - def test_pts_simple(self): + def test_pts_simple(self) -> None: fifo = av.AudioFifo() # ensure __repr__ does not crash @@ -42,7 +43,7 @@ def test_pts_simple(self): iframe = av.AudioFrame(samples=1024) iframe.pts = 0 iframe.sample_rate = 48000 - iframe.time_base = "1/48000" + iframe.time_base = Fraction("1/48000") fifo.write(iframe) @@ -54,9 +55,9 @@ def test_pts_simple(self): ) oframe = fifo.read(512) - self.assertTrue(oframe is not None) - self.assertEqual(oframe.pts, 0) - self.assertEqual(oframe.time_base, iframe.time_base) + assert oframe is not None + assert oframe.pts == 0 + assert oframe.time_base == iframe.time_base self.assertEqual(fifo.samples_written, 1024) self.assertEqual(fifo.samples_read, 512) @@ -65,20 +66,21 @@ def test_pts_simple(self): iframe.pts = 1024 fifo.write(iframe) oframe = fifo.read(512) - self.assertTrue(oframe is not None) + assert oframe is not None + self.assertEqual(oframe.pts, 512) self.assertEqual(oframe.time_base, iframe.time_base) iframe.pts = 9999 # Wrong! self.assertRaises(ValueError, fifo.write, iframe) - def test_pts_complex(self): + def test_pts_complex(self) -> None: fifo = av.AudioFifo() iframe = av.AudioFrame(samples=1024) iframe.pts = 0 iframe.sample_rate = 48000 - iframe.time_base = "1/96000" + iframe.time_base = Fraction("1/96000") fifo.write(iframe) iframe.pts = 2048 @@ -89,23 +91,23 @@ def test_pts_complex(self): self.assertEqual(oframe.pts, 2048) self.assertEqual(fifo.pts_per_sample, 2.0) - def test_missing_sample_rate(self): + def test_missing_sample_rate(self) -> None: fifo = av.AudioFifo() iframe = av.AudioFrame(samples=1024) iframe.pts = 0 - iframe.time_base = "1/48000" + iframe.time_base = Fraction("1/48000") fifo.write(iframe) oframe = fifo.read(512) - self.assertTrue(oframe is not None) + assert oframe is not None self.assertIsNone(oframe.pts) self.assertEqual(oframe.sample_rate, 0) self.assertEqual(oframe.time_base, iframe.time_base) - def test_missing_time_base(self): + def test_missing_time_base(self) -> None: fifo = av.AudioFifo() iframe = av.AudioFrame(samples=1024) @@ -116,7 +118,7 @@ def test_missing_time_base(self): oframe = fifo.read(512) - self.assertTrue(oframe is not None) + assert oframe is not None self.assertIsNone(oframe.pts) self.assertIsNone(oframe.time_base) self.assertEqual(oframe.sample_rate, iframe.sample_rate) diff --git a/tests/test_encode.py b/tests/test_encode.py index f19ad709d..c77593c8a 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -220,8 +220,8 @@ def test_transcode(self) -> None: ) self.assertEqual(container.metadata.get("key"), None) + assert isinstance(container.streams[0], AudioStream) stream = container.streams[0] - self.assertIsInstance(stream, AudioStream) # codec context properties self.assertEqual(stream.format.name, "s16p") From 113ba78bb06b98ce8c5b16207069c29788d012dd Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 19 Sep 2024 23:47:17 -0400 Subject: [PATCH 356/846] Add type stubs for test_audioformat, audioframe --- tests/test_audioformat.py | 26 ++++---- tests/test_audioframe.py | 136 +++++++++++++++++++------------------- 2 files changed, 80 insertions(+), 82 deletions(-) diff --git a/tests/test_audioformat.py b/tests/test_audioformat.py index 5334b37d6..733761f57 100644 --- a/tests/test_audioformat.py +++ b/tests/test_audioformat.py @@ -8,20 +8,20 @@ class TestAudioFormats(TestCase): - def test_s16_inspection(self): + def test_s16_inspection(self) -> None: fmt = AudioFormat("s16") - self.assertEqual(fmt.name, "s16") - self.assertFalse(fmt.is_planar) - self.assertEqual(fmt.bits, 16) - self.assertEqual(fmt.bytes, 2) - self.assertEqual(fmt.container_name, "s16" + postfix) - self.assertEqual(fmt.planar.name, "s16p") - self.assertIs(fmt.packed, fmt) + assert fmt.name == "s16" + assert not fmt.is_planar + assert fmt.bits == 16 + assert fmt.bytes == 2 + assert fmt.container_name == "s16" + postfix + assert fmt.planar.name == "s16p" + assert fmt.packed is fmt - def test_s32p_inspection(self): + def test_s32p_inspection(self) -> None: fmt = AudioFormat("s32p") - self.assertEqual(fmt.name, "s32p") - self.assertTrue(fmt.is_planar) - self.assertEqual(fmt.bits, 32) - self.assertEqual(fmt.bytes, 4) + assert fmt.name == "s32p" + assert fmt.is_planar + assert fmt.bits == 32 + assert fmt.bytes == 4 self.assertRaises(ValueError, lambda: fmt.container_name) diff --git a/tests/test_audioframe.py b/tests/test_audioframe.py index 378665b78..4a382d18c 100644 --- a/tests/test_audioframe.py +++ b/tests/test_audioframe.py @@ -1,4 +1,4 @@ -import numpy +import numpy as np from av import AudioFrame @@ -76,114 +76,112 @@ class TestAudioFrameConveniences(TestCase): def test_basic_to_ndarray(self) -> None: frame = AudioFrame(format="s16p", layout="stereo", samples=160) array = frame.to_ndarray() - self.assertEqual(array.dtype, "i2") - self.assertEqual(array.shape, (2, 160)) + assert array.dtype == "i2" + assert array.shape == (2, 160) - def test_ndarray_dbl(self): + def test_ndarray_dbl(self) -> None: layouts = [ - ("dbl", "mono", "f8", (1, 160)), - ("dbl", "stereo", "f8", (1, 320)), - ("dblp", "mono", "f8", (1, 160)), - ("dblp", "stereo", "f8", (2, 160)), + ("dbl", "mono", (1, 160)), + ("dbl", "stereo", (1, 320)), + ("dblp", "mono", (1, 160)), + ("dblp", "stereo", (2, 160)), ] - for format, layout, dtype, size in layouts: - array = numpy.ndarray(shape=size, dtype=dtype) + for format, layout, size in layouts: + array = np.zeros(shape=size, dtype="f8") for i in range(size[0]): - array[i][:] = numpy.random.rand(size[1]) + array[i][:] = np.random.rand(size[1]) frame = AudioFrame.from_ndarray(array, format=format, layout=layout) - self.assertEqual(frame.format.name, format) - self.assertEqual(frame.layout.name, layout) - self.assertEqual(frame.samples, 160) + assert frame.format.name == format + assert frame.layout.name == layout + assert frame.samples == 160 self.assertNdarraysEqual(frame.to_ndarray(), array) - def test_from_ndarray_value_error(self): + def test_from_ndarray_value_error(self) -> None: # incorrect dtype - array = numpy.ndarray(shape=(1, 160), dtype="f2") + array = np.zeros(shape=(1, 160), dtype="f2") with self.assertRaises(ValueError) as cm: AudioFrame.from_ndarray(array, format="flt", layout="mono") - self.assertEqual( - str(cm.exception), - "Expected numpy array with dtype `float32` but got `float16`", + assert ( + str(cm.exception) + == "Expected numpy array with dtype `float32` but got `float16`" ) # incorrect number of dimensions - array = numpy.ndarray(shape=(1, 160, 2), dtype="f4") + array = np.zeros(shape=(1, 160, 2), dtype="f4") with self.assertRaises(ValueError) as cm: AudioFrame.from_ndarray(array, format="flt", layout="mono") - self.assertEqual( - str(cm.exception), "Expected numpy array with ndim `2` but got `3`" - ) + assert str(cm.exception) == "Expected numpy array with ndim `2` but got `3`" # incorrect shape - array = numpy.ndarray(shape=(2, 160), dtype="f4") + array = np.zeros(shape=(2, 160), dtype="f4") with self.assertRaises(ValueError) as cm: AudioFrame.from_ndarray(array, format="flt", layout="mono") - self.assertEqual(str(cm.exception), "Unexpected numpy array shape `(2, 160)`") + assert str(cm.exception) == "Unexpected numpy array shape `(2, 160)`" - def test_ndarray_flt(self): + def test_ndarray_flt(self) -> None: layouts = [ - ("flt", "mono", "f4", (1, 160)), - ("flt", "stereo", "f4", (1, 320)), - ("fltp", "mono", "f4", (1, 160)), - ("fltp", "stereo", "f4", (2, 160)), + ("flt", "mono", (1, 160)), + ("flt", "stereo", (1, 320)), + ("fltp", "mono", (1, 160)), + ("fltp", "stereo", (2, 160)), ] - for format, layout, dtype, size in layouts: - array = numpy.ndarray(shape=size, dtype=dtype) + for format, layout, size in layouts: + array: np.ndarray = np.zeros(shape=size, dtype="f4") for i in range(size[0]): - array[i][:] = numpy.random.rand(size[1]) + array[i][:] = np.random.rand(size[1]) frame = AudioFrame.from_ndarray(array, format=format, layout=layout) - self.assertEqual(frame.format.name, format) - self.assertEqual(frame.layout.name, layout) - self.assertEqual(frame.samples, 160) + assert frame.format.name == format + assert frame.layout.name == layout + assert frame.samples == 160 self.assertNdarraysEqual(frame.to_ndarray(), array) - def test_ndarray_s16(self): + def test_ndarray_s16(self) -> None: layouts = [ - ("s16", "mono", "i2", (1, 160)), - ("s16", "stereo", "i2", (1, 320)), - ("s16p", "mono", "i2", (1, 160)), - ("s16p", "stereo", "i2", (2, 160)), + ("s16", "mono", (1, 160)), + ("s16", "stereo", (1, 320)), + ("s16p", "mono", (1, 160)), + ("s16p", "stereo", (2, 160)), ] - for format, layout, dtype, size in layouts: - array = numpy.random.randint(0, 256, size=size, dtype=dtype) + for format, layout, size in layouts: + array = np.random.randint(0, 256, size=size, dtype="i2") frame = AudioFrame.from_ndarray(array, format=format, layout=layout) - self.assertEqual(frame.format.name, format) - self.assertEqual(frame.layout.name, layout) - self.assertEqual(frame.samples, 160) + assert frame.format.name == format + assert frame.layout.name == layout + assert frame.samples == 160 self.assertNdarraysEqual(frame.to_ndarray(), array) - def test_ndarray_s16p_align_8(self): + def test_ndarray_s16p_align_8(self) -> None: frame = AudioFrame(format="s16p", layout="stereo", samples=159, align=8) array = frame.to_ndarray() - self.assertEqual(array.dtype, "i2") - self.assertEqual(array.shape, (2, 159)) + assert array.dtype == "i2" + assert array.shape == (2, 159) - def test_ndarray_s32(self): + def test_ndarray_s32(self) -> None: layouts = [ - ("s32", "mono", "i4", (1, 160)), - ("s32", "stereo", "i4", (1, 320)), - ("s32p", "mono", "i4", (1, 160)), - ("s32p", "stereo", "i4", (2, 160)), + ("s32", "mono", (1, 160)), + ("s32", "stereo", (1, 320)), + ("s32p", "mono", (1, 160)), + ("s32p", "stereo", (2, 160)), ] - for format, layout, dtype, size in layouts: - array = numpy.random.randint(0, 256, size=size, dtype=dtype) + for format, layout, size in layouts: + array = np.random.randint(0, 256, size=size, dtype="i4") frame = AudioFrame.from_ndarray(array, format=format, layout=layout) - self.assertEqual(frame.format.name, format) - self.assertEqual(frame.layout.name, layout) - self.assertEqual(frame.samples, 160) + assert frame.format.name == format + assert frame.layout.name == layout + assert frame.samples == 160 self.assertNdarraysEqual(frame.to_ndarray(), array) - def test_ndarray_u8(self): + def test_ndarray_u8(self) -> None: layouts = [ - ("u8", "mono", "u1", (1, 160)), - ("u8", "stereo", "u1", (1, 320)), - ("u8p", "mono", "u1", (1, 160)), - ("u8p", "stereo", "u1", (2, 160)), + ("u8", "mono", (1, 160)), + ("u8", "stereo", (1, 320)), + ("u8p", "mono", (1, 160)), + ("u8p", "stereo", (2, 160)), ] - for format, layout, dtype, size in layouts: - array = numpy.random.randint(0, 256, size=size, dtype=dtype) + for format, layout, size in layouts: + array = np.random.randint(0, 256, size=size, dtype="u1") frame = AudioFrame.from_ndarray(array, format=format, layout=layout) - self.assertEqual(frame.format.name, format) - self.assertEqual(frame.layout.name, layout) - self.assertEqual(frame.samples, 160) + assert frame.format.name == format + assert frame.layout.name == layout + assert frame.samples == 160 self.assertNdarraysEqual(frame.to_ndarray(), array) From 07715a1b2e487ece4dbb7e93fc28fc35e9639c22 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 20 Sep 2024 00:19:41 -0400 Subject: [PATCH 357/846] Add type stubs, fixes for test_codec --- av/codec/codec.pyi | 4 +- tests/test_codec.py | 123 ++++++++++++++++++++------------------------ 2 files changed, 59 insertions(+), 68 deletions(-) diff --git a/av/codec/codec.pyi b/av/codec/codec.pyi index 572fa3149..f6f5c2e65 100644 --- a/av/codec/codec.pyi +++ b/av/codec/codec.pyi @@ -43,6 +43,7 @@ class Capabilities(EnumFlag): class UnknownCodecError(ValueError): ... class Codec: + is_encoder: bool is_decoder: bool descriptor: Descriptor name: str @@ -55,8 +56,9 @@ class Codec: audio_formats: list[AudioFormat] | None properties: Properties capabilities: Capabilities + delay: bool - def __init__(self, name: str, mode: Literal["r", "w"]) -> None: ... + def __init__(self, name: str, mode: Literal["r", "w"] = "r") -> None: ... def create(self) -> CodecContext: ... class codec_descriptor: diff --git a/tests/test_codec.py b/tests/test_codec.py index 5c0665e8d..357509b2d 100644 --- a/tests/test_codec.py +++ b/tests/test_codec.py @@ -1,110 +1,99 @@ -import unittest - -from av import AudioFormat, Codec, codecs_available +from av import AudioFormat, Codec, VideoFormat, codecs_available from av.codec.codec import UnknownCodecError from .common import TestCase -# some older ffmpeg versions have no native opus encoder -try: - opus_c = Codec("opus", "w") - opus_encoder_missing = False -except UnknownCodecError: - opus_encoder_missing = True - class TestCodecs(TestCase): - def test_codec_bogus(self): + def test_codec_bogus(self) -> None: with self.assertRaises(UnknownCodecError): Codec("bogus123") with self.assertRaises(UnknownCodecError): Codec("bogus123", "w") - def test_codec_mpeg4_decoder(self): + def test_codec_mpeg4_decoder(self) -> None: c = Codec("mpeg4") - self.assertEqual(c.name, "mpeg4") - self.assertEqual(c.long_name, "MPEG-4 part 2") - self.assertEqual(c.type, "video") - self.assertIn(c.id, (12, 13)) - self.assertTrue(c.is_decoder) - self.assertFalse(c.is_encoder) - self.assertTrue(c.delay) + assert c.name == "mpeg4" + assert c.long_name == "MPEG-4 part 2" + assert c.type == "video" + assert c.id in (12, 13) + assert c.is_decoder + assert not c.is_encoder + assert c.delay # audio - self.assertIsNone(c.audio_formats) - self.assertIsNone(c.audio_rates) + assert c.audio_formats is None + assert c.audio_rates is None # video # formats = c.video_formats - # self.assertTrue(formats) - # self.assertIsInstance(formats[0], VideoFormat) - # self.assertTrue(any(f.name == "yuv420p" for f in formats)) + # assert formats + # assert isinstance(formats[0], VideoFormat) + # assert any(f.name == "yuv420p" for f in formats) - self.assertIsNone(c.frame_rates) + assert c.frame_rates is None - def test_codec_mpeg4_encoder(self): + def test_codec_mpeg4_encoder(self) -> None: c = Codec("mpeg4", "w") - self.assertEqual(c.name, "mpeg4") - self.assertEqual(c.long_name, "MPEG-4 part 2") - self.assertEqual(c.type, "video") - self.assertIn(c.id, (12, 13)) - self.assertTrue(c.is_encoder) - self.assertFalse(c.is_decoder) - self.assertTrue(c.delay) + assert c.name == "mpeg4" + assert c.long_name == "MPEG-4 part 2" + assert c.type == "video" + assert c.id in (12, 13) + assert c.is_encoder + assert not c.is_decoder + assert c.delay # audio - self.assertIsNone(c.audio_formats) - self.assertIsNone(c.audio_rates) + assert c.audio_formats is None + assert c.audio_rates is None # video - # formats = c.video_formats - # self.assertTrue(formats) - # self.assertIsInstance(formats[0], VideoFormat) - # self.assertTrue(any(f.name == "yuv420p" for f in formats)) - - self.assertIsNone(c.frame_rates) + formats = c.video_formats + assert formats + assert isinstance(formats[0], VideoFormat) + assert any(f.name == "yuv420p" for f in formats) + assert c.frame_rates is None - def test_codec_opus_decoder(self): + def test_codec_opus_decoder(self) -> None: c = Codec("opus") self.assertEqual(c.name, "opus") self.assertEqual(c.long_name, "Opus") - self.assertEqual(c.type, "audio") - self.assertTrue(c.is_decoder) - self.assertFalse(c.is_encoder) - self.assertTrue(c.delay) + assert c.type == "audio" + assert c.is_decoder + assert not c.is_encoder + assert c.delay # audio - self.assertIsNone(c.audio_formats) - self.assertIsNone(c.audio_rates) + assert c.audio_formats is None + assert c.audio_rates is None # video - self.assertIsNone(c.video_formats) - self.assertIsNone(c.frame_rates) + assert c.video_formats is None + assert c.frame_rates is None - @unittest.skipIf(opus_encoder_missing, "Opus encoder is not available") - def test_codec_opus_encoder(self): + def test_codec_opus_encoder(self) -> None: c = Codec("opus", "w") - self.assertIn(c.name, ("opus", "libopus")) - self.assertIn(c.long_name, ("Opus", "libopus Opus")) - self.assertEqual(c.type, "audio") - self.assertTrue(c.is_encoder) - self.assertFalse(c.is_decoder) - self.assertTrue(c.delay) + assert c.name in ("opus", "libopus") + assert c.long_name in ("Opus", "libopus Opus") + assert c.type == "audio" + assert c.is_encoder + assert not c.is_decoder + assert c.delay # audio formats = c.audio_formats - self.assertTrue(formats) - self.assertIsInstance(formats[0], AudioFormat) - self.assertTrue(any(f.name in ["flt", "fltp"] for f in formats)) + assert formats + assert isinstance(formats[0], AudioFormat) + assert any(f.name in ("flt", "fltp") for f in formats) - self.assertIsNotNone(c.audio_rates) - self.assertIn(48000, c.audio_rates) + assert c.audio_rates is not None + assert 48000 in c.audio_rates # video - self.assertIsNone(c.video_formats) - self.assertIsNone(c.frame_rates) + assert c.video_formats is None + assert c.frame_rates is None - def test_codecs_available(self): - self.assertTrue(codecs_available) + def test_codecs_available(self) -> None: + assert codecs_available From 030632e2e24902eb4e61eb869999f4beaff9ae5e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 20 Sep 2024 01:58:33 -0400 Subject: [PATCH 358/846] Add boolean flags to Codec type stub --- av/codec/codec.pyi | 45 +++++++++++++++++++++- av/codec/codec.pyx | 7 +++- av/codec/context.pyi | 1 + tests/test_codec_context.py | 76 +++++++++++++++++++------------------ 4 files changed, 88 insertions(+), 41 deletions(-) diff --git a/av/codec/codec.pyi b/av/codec/codec.pyi index f6f5c2e65..6cb421430 100644 --- a/av/codec/codec.pyi +++ b/av/codec/codec.pyi @@ -1,9 +1,12 @@ from fractions import Fraction -from typing import Literal +from typing import Literal, overload +from av.audio.codeccontext import AudioCodecContext from av.audio.format import AudioFormat from av.descriptor import Descriptor from av.enum import EnumFlag +from av.subtitles.codeccontext import SubtitleCodecContext +from av.video.codeccontext import VideoCodecContext from av.video.format import VideoFormat from .context import CodecContext @@ -56,10 +59,48 @@ class Codec: audio_formats: list[AudioFormat] | None properties: Properties capabilities: Capabilities + intra_only: bool + lossy: bool + lossless: bool + reorder: bool + bitmap_sub: bool + text_sub: bool + draw_horiz_band: bool + dr1: bool + hwaccel: bool delay: bool + small_last_frame: bool + hwaccel_vdpau: bool + subframes: bool + experimental: bool + channel_conf: bool + neg_linesizes: bool + frame_threads: bool + slice_threads: bool + param_change: bool + auto_threads: bool + variable_frame_size: bool + avoid_probing: bool + hardware: bool + hybrid: bool + encoder_reordered_opaque: bool + encoder_flush: bool def __init__(self, name: str, mode: Literal["r", "w"] = "r") -> None: ... - def create(self) -> CodecContext: ... + @overload + def create(self, kind: Literal["video"]) -> VideoCodecContext: ... + @overload + def create(self, kind: Literal["audio"]) -> AudioCodecContext: ... + @overload + def create(self, kind: Literal["subtitle"]) -> SubtitleCodecContext: ... + @overload + def create(self, kind: None = None) -> CodecContext: ... + @overload + def create( + self, kind: Literal["video", "audio", "subtitle"] | None = None + ) -> ( + VideoCodecContext | AudioCodecContext | SubtitleCodecContext | CodecContext + ): ... class codec_descriptor: name: str diff --git a/av/codec/codec.pyx b/av/codec/codec.pyx index 9c4b7ed94..da66d7184 100644 --- a/av/codec/codec.pyx +++ b/av/codec/codec.pyx @@ -201,8 +201,11 @@ cdef class Codec: if self.is_encoder and lib.av_codec_is_decoder(self.ptr): raise RuntimeError("%s is both encoder and decoder.") - def create(self): - """Create a :class:`.CodecContext` for this codec.""" + def create(self, str kind = None): + """Create a :class:`.CodecContext` for this codec. + + :param str kind: Gives a hint to static type checkers for what exact CodecContext is used. + """ from .context import CodecContext return CodecContext.create(self) diff --git a/av/codec/context.pyi b/av/codec/context.pyi index d84b41de1..f193487a9 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -60,6 +60,7 @@ class CodecContext: is_encoder: bool is_decoder: bool name: str + codec: Codec options: dict[str, str] type: Literal["video", "audio", "data", "subtitle", "attachment"] profile: str | None diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 634fe0280..ad6247f2c 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os from fractions import Fraction from unittest import SkipTest @@ -10,12 +12,6 @@ from .common import TestCase, fate_suite -def iter_frames(container, stream): - for packet in container.demux(stream): - for frame in packet.decode(): - yield frame - - def iter_raw_frames(path, packet_sizes, ctx): with open(path, "rb") as f: for i, size in enumerate(packet_sizes): @@ -27,6 +23,7 @@ def iter_raw_frames(path, packet_sizes, ctx): break for frame in ctx.decode(packet): yield frame + while True: try: frames = ctx.decode(None) @@ -177,20 +174,20 @@ def _assert_parse(self, codec_name, path): class TestEncoding(TestCase): - def test_encoding_png(self): + def test_encoding_png(self) -> None: self.image_sequence_encode("png") - def test_encoding_mjpeg(self): + def test_encoding_mjpeg(self) -> None: self.image_sequence_encode("mjpeg") - def test_encoding_tiff(self): + def test_encoding_tiff(self) -> None: self.image_sequence_encode("tiff") - def image_sequence_encode(self, codec_name): + def image_sequence_encode(self, codec_name: str) -> None: try: codec = Codec(codec_name, "w") except UnknownCodecError: - raise SkipTest() + raise SkipTest(f"Unknown codec: {codec_name}") container = av.open(fate_suite("h264/interlaced_crop.mp4")) video_stream = container.streams.video[0] @@ -198,19 +195,22 @@ def image_sequence_encode(self, codec_name): width = 640 height = 480 - ctx = codec.create() + ctx = codec.create("video") + assert ctx.codec.video_formats pix_fmt = ctx.codec.video_formats[0].name ctx.width = width ctx.height = height + + assert video_stream.time_base is not None ctx.time_base = video_stream.time_base ctx.pix_fmt = pix_fmt ctx.open() frame_count = 1 path_list = [] - for frame in iter_frames(container, video_stream): + for frame in container.decode(video_stream): new_frame = frame.reformat(width, height, pix_fmt) new_packets = ctx.encode(new_frame) @@ -232,7 +232,7 @@ def image_sequence_encode(self, codec_name): if frame_count > 5: break - ctx = av.Codec(codec_name, "r").create() + ctx = av.Codec(codec_name, "r").create("video") for path in path_list: with open(path, "rb") as f: @@ -275,7 +275,7 @@ def video_encoding(self, codec_name, options={}, codec_tag=None): try: codec = Codec(codec_name, "w") except UnknownCodecError: - raise SkipTest() + raise SkipTest(f"Unknown codec: {codec_name}") container = av.open(fate_suite("h264/interlaced_crop.mp4")) video_stream = container.streams.video[0] @@ -299,12 +299,12 @@ def video_encoding(self, codec_name, options={}, codec_tag=None): ctx.codec_tag = codec_tag ctx.open() - path = self.sandboxed("encoder.%s" % codec_name) + path = self.sandboxed(f"encoder.{codec_name}") packet_sizes = [] frame_count = 0 with open(path, "wb") as f: - for frame in iter_frames(container, video_stream): + for frame in container.decode(video_stream): new_frame = frame.reformat(width, height, pix_fmt) # reset the picture type @@ -357,40 +357,42 @@ def video_encoding(self, codec_name, options={}, codec_tag=None): final_gop_size = decoded_frame_count - max(keyframe_indices) self.assertLessEqual(final_gop_size, gop_size) - def test_encoding_pcm_s24le(self): + def test_encoding_pcm_s24le(self) -> None: self.audio_encoding("pcm_s24le") - def test_encoding_aac(self): + def test_encoding_aac(self) -> None: self.audio_encoding("aac") - def test_encoding_mp2(self): + def test_encoding_mp2(self) -> None: self.audio_encoding("mp2") - maxDiff = None - - def audio_encoding(self, codec_name): + def audio_encoding(self, codec_name: str) -> None: self._audio_encoding(codec_name=codec_name, channel_layout="stereo") self._audio_encoding( codec_name=codec_name, channel_layout=AudioLayout("stereo") ) - def _audio_encoding(self, *, codec_name, channel_layout): + def _audio_encoding( + self, *, codec_name: str, channel_layout: str | AudioLayout + ) -> None: try: codec = Codec(codec_name, "w") except UnknownCodecError: - raise SkipTest() + raise SkipTest(f"Unknown codec: {codec_name}") + + ctx = codec.create(kind="audio") - ctx = codec.create() if ctx.codec.experimental: - raise SkipTest() + raise SkipTest(f"Experimental codec: {codec_name}") + assert ctx.codec.audio_formats sample_fmt = ctx.codec.audio_formats[-1].name sample_rate = 48000 ctx.time_base = Fraction(1) / sample_rate ctx.sample_rate = sample_rate - ctx.format = sample_fmt - ctx.layout = channel_layout + ctx.format = sample_fmt # type: ignore + ctx.layout = channel_layout # type: ignore ctx.open() @@ -399,32 +401,32 @@ def _audio_encoding(self, *, codec_name, channel_layout): container = av.open(fate_suite("audio-reference/chorusnoise_2ch_44kHz_s16.wav")) audio_stream = container.streams.audio[0] - path = self.sandboxed("encoder.%s" % codec_name) + path = self.sandboxed(f"encoder.{codec_name}") samples = 0 packet_sizes = [] with open(path, "wb") as f: - for frame in iter_frames(container, audio_stream): + for frame in container.decode(audio_stream): resampled_frames = resampler.resample(frame) for resampled_frame in resampled_frames: - self.assertEqual(resampled_frame.time_base, Fraction(1, 48000)) + assert resampled_frame.time_base == Fraction(1, 48000) samples += resampled_frame.samples for packet in ctx.encode(resampled_frame): - self.assertEqual(packet.time_base, Fraction(1, 48000)) + assert packet.time_base == Fraction(1, 48000) packet_sizes.append(packet.size) f.write(packet) for packet in ctx.encode(None): - self.assertEqual(packet.time_base, Fraction(1, 48000)) + assert packet.time_base == Fraction(1, 48000) packet_sizes.append(packet.size) f.write(packet) - ctx = Codec(codec_name, "r").create() + ctx = Codec(codec_name, "r").create("audio") ctx.sample_rate = sample_rate - ctx.format = sample_fmt - ctx.layout = channel_layout + ctx.format = sample_fmt # type: ignore + ctx.layout = channel_layout # type: ignore ctx.open() result_samples = 0 From d4745bd08e01461c9f9b705e7b30af69661969cf Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 23 Sep 2024 05:45:58 -0400 Subject: [PATCH 359/846] Disallow setting frame.time_base with string --- av/utils.pxd | 2 +- av/utils.pyx | 12 +---------- tests/test_audioresampler.py | 40 +++++++++++++++++------------------- tests/test_codec_context.py | 2 +- tests/test_videoframe.py | 3 ++- 5 files changed, 24 insertions(+), 35 deletions(-) diff --git a/av/utils.pxd b/av/utils.pxd index 125272c5c..7f342c05b 100644 --- a/av/utils.pxd +++ b/av/utils.pxd @@ -7,7 +7,7 @@ cdef dict_to_avdict(lib.AVDictionary **dst, dict src, str encoding, str errors) cdef object avrational_to_fraction(const lib.AVRational *input) -cdef object to_avrational(object value, lib.AVRational *input) +cdef void to_avrational(object frac, lib.AVRational *input) cdef check_ndarray(object array, object dtype, int ndim) diff --git a/av/utils.pyx b/av/utils.pyx index cc01925de..afc82d24d 100644 --- a/av/utils.pyx +++ b/av/utils.pyx @@ -47,17 +47,7 @@ cdef object avrational_to_fraction(const lib.AVRational *input): return Fraction(input.num, input.den) -cdef object to_avrational(object value, lib.AVRational *input): - if value is None: - input.num = 0 - input.den = 1 - return - - if isinstance(value, Fraction): - frac = value - else: - frac = Fraction(value) - +cdef void to_avrational(object frac, lib.AVRational *input): input.num = frac.numerator input.den = frac.denominator diff --git a/tests/test_audioresampler.py b/tests/test_audioresampler.py index 58185f9d1..47dd1ddc3 100644 --- a/tests/test_audioresampler.py +++ b/tests/test_audioresampler.py @@ -1,12 +1,13 @@ from fractions import Fraction +import av from av import AudioFrame, AudioResampler from .common import TestCase class TestAudioResampler(TestCase): - def test_flush_immediately(self): + def test_flush_immediately(self) -> None: """ If we flush the resampler before passing any input, it returns a `None` frame without setting up the graph. @@ -18,7 +19,7 @@ def test_flush_immediately(self): oframes = resampler.resample(None) self.assertEqual(len(oframes), 0) - def test_identity_passthrough(self): + def test_identity_passthrough(self) -> None: """ If we don't ask it to do anything, it won't. """ @@ -37,13 +38,13 @@ def test_identity_passthrough(self): oframes = resampler.resample(iframe) self.assertEqual(len(oframes), 1) - self.assertIs(iframe, oframes[0]) + assert iframe is oframes[0] # flush oframes = resampler.resample(None) self.assertEqual(len(oframes), 0) - def test_matching_passthrough(self): + def test_matching_passthrough(self) -> None: """ If the frames match, it won't do anything. """ @@ -68,9 +69,7 @@ def test_matching_passthrough(self): oframes = resampler.resample(None) self.assertEqual(len(oframes), 0) - def test_pts_assertion_same_rate(self): - import av - + def test_pts_assertion_same_rate(self) -> None: av.logging.set_level(av.logging.VERBOSE) resampler = AudioResampler("s16", "mono") @@ -117,13 +116,13 @@ def test_pts_assertion_same_rate(self): self.assertEqual(len(oframes), 0) av.logging.set_level(None) - def test_pts_assertion_new_rate_up(self): + def test_pts_assertion_new_rate_up(self) -> None: resampler = AudioResampler("s16", "mono", 44100) # resample one frame iframe = AudioFrame("s16", "stereo", 1024) iframe.sample_rate = 48000 - iframe.time_base = "1/48000" + iframe.time_base = Fraction(1, 48000) iframe.pts = 0 oframes = resampler.resample(iframe) @@ -137,7 +136,7 @@ def test_pts_assertion_new_rate_up(self): iframe = AudioFrame("s16", "stereo", 1024) iframe.sample_rate = 48000 - iframe.time_base = "1/48000" + iframe.time_base = Fraction(1, 48000) iframe.pts = 1024 oframes = resampler.resample(iframe) @@ -159,13 +158,13 @@ def test_pts_assertion_new_rate_up(self): self.assertEqual(oframe.sample_rate, 44100) self.assertEqual(oframe.samples, 15) - def test_pts_assertion_new_rate_down(self): + def test_pts_assertion_new_rate_down(self) -> None: resampler = AudioResampler("s16", "mono", 48000) # resample one frame iframe = AudioFrame("s16", "stereo", 1024) iframe.sample_rate = 44100 - iframe.time_base = "1/44100" + iframe.time_base = Fraction(1, 44100) iframe.pts = 0 oframes = resampler.resample(iframe) @@ -179,7 +178,7 @@ def test_pts_assertion_new_rate_down(self): iframe = AudioFrame("s16", "stereo", 1024) iframe.sample_rate = 44100 - iframe.time_base = "1/44100" + iframe.time_base = Fraction(1, 44100) iframe.pts = 1024 oframes = resampler.resample(iframe) @@ -201,13 +200,13 @@ def test_pts_assertion_new_rate_down(self): self.assertEqual(oframe.sample_rate, 48000) self.assertEqual(oframe.samples, 18) - def test_pts_assertion_new_rate_fltp(self): + def test_pts_assertion_new_rate_fltp(self) -> None: resampler = AudioResampler("fltp", "mono", 8000, 1024) # resample one frame iframe = AudioFrame("s16", "mono", 1024) iframe.sample_rate = 8000 - iframe.time_base = "1/1000" + iframe.time_base = Fraction(1, 1000) iframe.pts = 0 oframes = resampler.resample(iframe) @@ -221,7 +220,7 @@ def test_pts_assertion_new_rate_fltp(self): iframe = AudioFrame("s16", "mono", 1024) iframe.sample_rate = 8000 - iframe.time_base = "1/1000" + iframe.time_base = Fraction(1, 1000) iframe.pts = 8192 oframes = resampler.resample(iframe) @@ -237,7 +236,7 @@ def test_pts_assertion_new_rate_fltp(self): oframes = resampler.resample(None) self.assertEqual(len(oframes), 0) - def test_pts_missing_time_base(self): + def test_pts_missing_time_base(self) -> None: resampler = AudioResampler("s16", "mono", 44100) # resample one frame @@ -263,7 +262,7 @@ def test_pts_missing_time_base(self): self.assertEqual(oframe.sample_rate, 44100) self.assertEqual(oframe.samples, 16) - def test_mismatched_input(self): + def test_mismatched_input(self) -> None: """ Consecutive frames must have the same layout, sample format and sample rate. """ @@ -279,6 +278,5 @@ def test_mismatched_input(self): iframe.sample_rate = 48000 with self.assertRaises(ValueError) as cm: resampler.resample(iframe) - self.assertEqual( - str(cm.exception), "Frame does not match AudioResampler setup." - ) + + assert str(cm.exception) == "Frame does not match AudioResampler setup." diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index ad6247f2c..12997d76a 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -266,7 +266,7 @@ def test_encoding_dnxhd(self): "pix_fmt": "yuv422p", "width": 1920, "height": 1080, - "time_base": "1001/30000", + "time_base": Fraction(1001, 30_000), "max_frames": 5, } self.video_encoding("dnxhd", options) diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 28540ccc3..9dff66d7f 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -1,3 +1,4 @@ +from fractions import Fraction from unittest import SkipTest import numpy @@ -595,7 +596,7 @@ class TestVideoFrameTiming(TestCase): def test_reformat_pts(self): frame = VideoFrame(640, 480, "rgb24") frame.pts = 123 - frame.time_base = "456/1" # Just to be different. + frame.time_base = Fraction("456/1") frame = frame.reformat(320, 240) self.assertEqual(frame.pts, 123) self.assertEqual(frame.time_base, 456) From 68d6a992b8446881ade7f3be9c47a1b1f4542a90 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 23 Sep 2024 06:16:22 -0400 Subject: [PATCH 360/846] Prefer asserts because mypy understands them --- tests/test_encode.py | 98 +++++------ tests/test_file_probing.py | 320 ++++++++++++++++++------------------ tests/test_filters.py | 62 +++---- tests/test_logging.py | 6 +- tests/test_options.py | 14 +- tests/test_packet.py | 33 ++-- tests/test_python_io.py | 43 +++-- tests/test_seek.py | 12 +- tests/test_subtitles.py | 56 ++++--- tests/test_videoframe.py | 324 ++++++++++++++++++------------------- 10 files changed, 478 insertions(+), 490 deletions(-) diff --git a/tests/test_encode.py b/tests/test_encode.py index c77593c8a..79c8423bf 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -69,7 +69,7 @@ def assert_rgb_rotate( self, input_: av.container.InputContainer, is_dash: bool = False ) -> None: # Now inspect it a little. - self.assertEqual(len(input_.streams), 1) + assert len(input_.streams) == 1 if is_dash: self.assertTrue(input_.metadata.get("Title") == "container", input_.metadata) else: @@ -81,7 +81,7 @@ def assert_rgb_rotate( if is_dash: # The DASH format doesn't provide a duration for the stream # and so the container duration (micro seconds) is checked instead - self.assertEqual(input_.duration, 2000000) + assert input_.duration == 2000000 expected_average_rate = 24 expected_duration = None expected_frames = 0 @@ -95,24 +95,24 @@ def assert_rgb_rotate( # actual stream properties self.assertIsInstance(stream, VideoStream) assert isinstance(stream, VideoStream) - self.assertEqual(stream.average_rate, expected_average_rate) - self.assertEqual(stream.base_rate, 24) - self.assertEqual(stream.duration, expected_duration) - self.assertEqual(stream.guessed_rate, 24) - self.assertEqual(stream.frames, expected_frames) - self.assertEqual(stream.id, expected_id) - self.assertEqual(stream.index, 0) - self.assertEqual(stream.profile, "Simple Profile") - self.assertEqual(stream.start_time, 0) - self.assertEqual(stream.time_base, Fraction(1, 12288)) - self.assertEqual(stream.type, "video") + assert stream.average_rate == expected_average_rate + assert stream.base_rate == 24 + assert stream.duration == expected_duration + assert stream.guessed_rate == 24 + assert stream.frames == expected_frames + assert stream.id == expected_id + assert stream.index == 0 + assert stream.profile == "Simple Profile" + assert stream.start_time == 0 + assert stream.time_base == Fraction(1, 12288) + assert stream.type == "video" # codec context properties - self.assertEqual(stream.codec.name, "mpeg4") - self.assertEqual(stream.codec.long_name, "MPEG-4 part 2") - self.assertEqual(stream.format.name, "yuv420p") - self.assertEqual(stream.format.width, WIDTH) - self.assertEqual(stream.format.height, HEIGHT) + assert stream.codec.name == "mpeg4" + assert stream.codec.long_name == "MPEG-4 part 2" + assert stream.format.name == "yuv420p" + assert stream.format.width == WIDTH + assert stream.format.height == HEIGHT class TestBasicVideoEncoding(TestCase): @@ -120,17 +120,17 @@ def test_default_options(self) -> None: with av.open(self.sandboxed("output.mov"), "w") as output: stream = output.add_stream("mpeg4") self.assertIn(stream, output.streams.video) - self.assertEqual(stream.average_rate, Fraction(24, 1)) - self.assertEqual(stream.time_base, None) + assert stream.average_rate == Fraction(24, 1) + assert stream.time_base is None # codec context properties - self.assertEqual(stream.bit_rate, 1024000) - self.assertEqual(stream.format.height, 480) - self.assertEqual(stream.format.name, "yuv420p") - self.assertEqual(stream.format.width, 640) - self.assertEqual(stream.height, 480) - self.assertEqual(stream.pix_fmt, "yuv420p") - self.assertEqual(stream.width, 640) + assert stream.bit_rate == 1024000 + assert stream.format.height == 480 + assert stream.format.name == "yuv420p" + assert stream.format.width == 640 + assert stream.height == 480 + assert stream.pix_fmt == "yuv420p" + assert stream.width == 640 def test_encoding(self) -> None: path = self.sandboxed("rgb_rotate.mov") @@ -156,11 +156,11 @@ def test_encoding_with_pts(self) -> None: frame.time_base = Fraction(1, 48000) for packet in stream.encode(frame): - self.assertEqual(packet.time_base, Fraction(1, 24)) + assert packet.time_base == Fraction(1, 24) output.mux(packet) for packet in stream.encode(None): - self.assertEqual(packet.time_base, Fraction(1, 24)) + assert packet.time_base == Fraction(1, 24) output.mux(packet) def test_encoding_with_unicode_filename(self) -> None: @@ -177,12 +177,12 @@ def test_default_options(self) -> None: with av.open(self.sandboxed("output.mov"), "w") as output: stream = output.add_stream("mp2") self.assertIn(stream, output.streams.audio) - self.assertEqual(stream.time_base, None) + assert stream.time_base is None # codec context properties - self.assertEqual(stream.bit_rate, 128000) - self.assertEqual(stream.format.name, "s16") - self.assertEqual(stream.sample_rate, 48000) + assert stream.bit_rate == 128000 + assert stream.format.name == "s16" + assert stream.sample_rate == 48000 def test_transcode(self) -> None: path = self.sandboxed("audio_transcode.mov") @@ -214,18 +214,18 @@ def test_transcode(self) -> None: output.mux(packet) with av.open(path) as container: - self.assertEqual(len(container.streams), 1) + assert len(container.streams) == 1 self.assertEqual( container.metadata.get("title"), "container", container.metadata ) - self.assertEqual(container.metadata.get("key"), None) + assert container.metadata.get("key") is None assert isinstance(container.streams[0], AudioStream) stream = container.streams[0] # codec context properties - self.assertEqual(stream.format.name, "s16p") - self.assertEqual(stream.sample_rate, sample_rate) + assert stream.format.name == "s16p" + assert stream.sample_rate == sample_rate class TestEncodeStreamSemantics(TestCase): @@ -242,14 +242,14 @@ def test_stream_index(self) -> None: astream.layout = "stereo" # type: ignore astream.format = "s16" # type: ignore - self.assertEqual(vstream.index, 0) - self.assertEqual(astream.index, 1) + assert vstream.index == 0 + assert astream.index == 1 vframe = VideoFrame(320, 240, "yuv420p") vpacket = vstream.encode(vframe)[0] self.assertIs(vpacket.stream, vstream) - self.assertEqual(vpacket.stream_index, 0) + assert vpacket.stream_index == 0 for i in range(10): if astream.frame_size != 0: @@ -265,7 +265,7 @@ def test_stream_index(self) -> None: break self.assertIs(apacket.stream, astream) - self.assertEqual(apacket.stream_index, 1) + assert apacket.stream_index == 1 def test_stream_audio_resample(self) -> None: with av.open(self.sandboxed("output.mov"), "w") as output: @@ -289,14 +289,14 @@ def test_stream_audio_resample(self) -> None: apackets = astream.encode(aframe) if apackets: apacket = apackets[0] - self.assertEqual(apacket.pts, pts_expected.pop(0)) - self.assertEqual(apacket.time_base, Fraction(1, 8000)) + assert apacket.pts == pts_expected.pop(0) + assert apacket.time_base == Fraction(1, 8000) apackets = astream.encode(None) if apackets: apacket = apackets[0] - self.assertEqual(apacket.pts, pts_expected.pop(0)) - self.assertEqual(apacket.time_base, Fraction(1, 8000)) + assert apacket.pts == pts_expected.pop(0) + assert apacket.time_base == Fraction(1, 8000) def test_set_id_and_time_base(self) -> None: with av.open(self.sandboxed("output.mov"), "w") as output: @@ -304,14 +304,14 @@ def test_set_id_and_time_base(self) -> None: self.assertIn(stream, output.streams.audio) # set id - self.assertEqual(stream.id, 0) + assert stream.id == 0 stream.id = 1 - self.assertEqual(stream.id, 1) + assert stream.id == 1 # set time_base - self.assertEqual(stream.time_base, None) + assert stream.time_base is None stream.time_base = Fraction(1, 48000) - self.assertEqual(stream.time_base, Fraction(1, 48000)) + assert stream.time_base == Fraction(1, 48000) def encode_file_with_max_b_frames(max_b_frames: int) -> io.BytesIO: diff --git a/tests/test_file_probing.py b/tests/test_file_probing.py index a4e35a1e1..aab232804 100644 --- a/tests/test_file_probing.py +++ b/tests/test_file_probing.py @@ -10,17 +10,17 @@ def setUp(self): self.file = av.open(fate_suite("aac/latm_stereo_to_51.ts")) def test_container_probing(self): - self.assertEqual(self.file.bit_rate, 269558) - self.assertEqual(self.file.duration, 6165333) - self.assertEqual(str(self.file.format), "") - self.assertEqual(self.file.format.name, "mpegts") + assert self.file.bit_rate == 269558 + assert self.file.duration == 6165333 + assert str(self.file.format) == "" + assert self.file.format.name == "mpegts" self.assertEqual( self.file.format.long_name, "MPEG-TS (MPEG-2 Transport Stream)" ) - self.assertEqual(self.file.metadata, {}) - self.assertEqual(self.file.size, 207740) - self.assertEqual(self.file.start_time, 1400000) - self.assertEqual(len(self.file.streams), 1) + assert self.file.metadata == {} + assert self.file.size == 207740 + assert self.file.start_time == 1400000 + assert len(self.file.streams) == 1 def test_stream_probing(self): stream = self.file.streams[0] @@ -33,29 +33,29 @@ def test_stream_probing(self): ) # actual stream properties - self.assertEqual(stream.duration, 554880) - self.assertEqual(stream.frames, 0) - self.assertEqual(stream.id, 256) - self.assertEqual(stream.index, 0) - self.assertEqual(stream.language, "eng") - self.assertEqual(stream.metadata, {"language": "eng"}) - self.assertEqual(stream.profile, "LC") - self.assertEqual(stream.start_time, 126000) - self.assertEqual(stream.time_base, Fraction(1, 90000)) - self.assertEqual(stream.type, "audio") + assert stream.duration == 554880 + assert stream.frames == 0 + assert stream.id == 256 + assert stream.index == 0 + assert stream.language == "eng" + assert stream.metadata == {"language": "eng"} + assert stream.profile == "LC" + assert stream.start_time == 126000 + assert stream.time_base == Fraction(1, 90000) + assert stream.type == "audio" # codec context properties - self.assertEqual(stream.bit_rate, None) - self.assertEqual(stream.channels, 2) - self.assertEqual(stream.codec.name, "aac_latm") + assert stream.bit_rate is None + assert stream.channels == 2 + assert stream.codec.name == "aac_latm" self.assertEqual( stream.codec.long_name, "AAC LATM (Advanced Audio Coding LATM syntax)" ) - self.assertEqual(stream.format.bits, 32) - self.assertEqual(stream.format.name, "fltp") - self.assertEqual(stream.layout.name, "stereo") - self.assertEqual(stream.max_bit_rate, None) - self.assertEqual(stream.sample_rate, 48000) + assert stream.format.bits == 32 + assert stream.format.name == "fltp" + assert stream.layout.name == "stereo" + assert stream.max_bit_rate is None + assert stream.sample_rate == 48000 class TestAudioProbeCorrupt(TestCase): @@ -68,15 +68,15 @@ def setUp(self): self.file = av.open(path) def test_container_probing(self): - self.assertEqual(self.file.bit_rate, 0) - self.assertEqual(self.file.duration, None) - self.assertEqual(str(self.file.format), "") - self.assertEqual(self.file.format.name, "flac") - self.assertEqual(self.file.format.long_name, "raw FLAC") - self.assertEqual(self.file.metadata, {}) - self.assertEqual(self.file.size, 0) - self.assertEqual(self.file.start_time, None) - self.assertEqual(len(self.file.streams), 1) + assert self.file.bit_rate == 0 + assert self.file.duration is None + assert str(self.file.format) == "" + assert self.file.format.name == "flac" + assert self.file.format.long_name == "raw FLAC" + assert self.file.metadata == {} + assert self.file.size == 0 + assert self.file.start_time is None + assert len(self.file.streams) == 1 def test_stream_probing(self): stream = self.file.streams[0] @@ -89,26 +89,26 @@ def test_stream_probing(self): ) # actual stream properties - self.assertEqual(stream.duration, None) - self.assertEqual(stream.frames, 0) - self.assertEqual(stream.id, 0) - self.assertEqual(stream.index, 0) - self.assertEqual(stream.language, None) - self.assertEqual(stream.metadata, {}) - self.assertEqual(stream.profile, None) - self.assertEqual(stream.start_time, None) - self.assertEqual(stream.time_base, Fraction(1, 90000)) - self.assertEqual(stream.type, "audio") + assert stream.duration is None + assert stream.frames == 0 + assert stream.id == 0 + assert stream.index == 0 + assert stream.language is None + assert stream.metadata == {} + assert stream.profile is None + assert stream.start_time is None + assert stream.time_base == Fraction(1, 90000) + assert stream.type == "audio" # codec context properties - self.assertEqual(stream.bit_rate, None) - self.assertEqual(stream.codec.name, "flac") - self.assertEqual(stream.codec.long_name, "FLAC (Free Lossless Audio Codec)") - self.assertEqual(stream.channels, 0) - self.assertEqual(stream.format, None) - self.assertEqual(stream.layout.name, "0 channels") - self.assertEqual(stream.max_bit_rate, None) - self.assertEqual(stream.sample_rate, 0) + assert stream.bit_rate is None + assert stream.codec.name == "flac" + assert stream.codec.long_name == "FLAC (Free Lossless Audio Codec)" + assert stream.channels == 0 + assert stream.format is None + assert stream.layout.name == "0 channels" + assert stream.max_bit_rate is None + assert stream.sample_rate == 0 class TestDataProbe(TestCase): @@ -116,14 +116,14 @@ def setUp(self): self.file = av.open(fate_suite("mxf/track_01_v02.mxf")) def test_container_probing(self): - self.assertEqual(self.file.bit_rate, 27872687) - self.assertEqual(self.file.duration, 417083) - self.assertEqual(str(self.file.format), "") - self.assertEqual(self.file.format.name, "mxf") - self.assertEqual(self.file.format.long_name, "MXF (Material eXchange Format)") - self.assertEqual(self.file.size, 1453153) - self.assertEqual(self.file.start_time, 0) - self.assertEqual(len(self.file.streams), 4) + assert self.file.bit_rate == 27872687 + assert self.file.duration == 417083 + assert str(self.file.format) == "" + assert self.file.format.name == "mxf" + assert self.file.format.long_name == "MXF (Material eXchange Format)" + assert self.file.size == 1453153 + assert self.file.start_time == 0 + assert len(self.file.streams) == 4 for key, value, min_version in ( ("application_platform", "AAFSDK (MacOS X)", None), @@ -159,20 +159,18 @@ def test_container_probing(self): for name, version in min_version.items() ): continue - self.assertEqual(self.file.metadata.get(key), value) + assert self.file.metadata.get(key) == value def test_stream_probing(self): stream = self.file.streams[0] - # check __repr__ - self.assertTrue(str(stream).startswith(" at ")) + assert str(stream).startswith(" at ") - # actual stream properties - self.assertEqual(stream.duration, 37537) - self.assertEqual(stream.frames, 0) - self.assertEqual(stream.id, 1) - self.assertEqual(stream.index, 0) - self.assertEqual(stream.language, None) + assert stream.duration == 37537 + assert stream.frames == 0 + assert stream.id == 1 + assert stream.index == 0 + assert stream.language is None self.assertEqual( stream.metadata, { @@ -181,10 +179,10 @@ def test_stream_probing(self): "track_name": "Base", }, ) - self.assertEqual(stream.profile, None) - self.assertEqual(stream.start_time, 0) - self.assertEqual(stream.time_base, Fraction(1, 90000)) - self.assertEqual(stream.type, "data") + assert stream.profile is None + assert stream.start_time == 0 + assert stream.time_base == Fraction(1, 90000) + assert stream.type == "data" self.assertEqual(hasattr(stream, "codec"), False) @@ -193,13 +191,13 @@ def setUp(self): self.file = av.open(fate_suite("sub/MovText_capability_tester.mp4")) def test_container_probing(self): - self.assertEqual(self.file.bit_rate, 810) - self.assertEqual(self.file.duration, 8140000) + assert self.file.bit_rate == 810 + assert self.file.duration == 8140000 self.assertEqual( str(self.file.format), "" ) - self.assertEqual(self.file.format.name, "mov,mp4,m4a,3gp,3g2,mj2") - self.assertEqual(self.file.format.long_name, "QuickTime / MOV") + assert self.file.format.name == "mov,mp4,m4a,3gp,3g2,mj2" + assert self.file.format.long_name == "QuickTime / MOV" self.assertEqual( self.file.metadata, { @@ -209,9 +207,9 @@ def test_container_probing(self): "minor_version": "1", }, ) - self.assertEqual(self.file.size, 825) - self.assertEqual(self.file.start_time, None) - self.assertEqual(len(self.file.streams), 1) + assert self.file.size == 825 + assert self.file.start_time is None + assert len(self.file.streams) == 1 def test_stream_probing(self): stream = self.file.streams[0] @@ -222,11 +220,11 @@ def test_stream_probing(self): ) # actual stream properties - self.assertEqual(stream.duration, 8140) - self.assertEqual(stream.frames, 6) - self.assertEqual(stream.id, 1) - self.assertEqual(stream.index, 0) - self.assertEqual(stream.language, "und") + assert stream.duration == 8140 + assert stream.frames == 6 + assert stream.id == 1 + assert stream.index == 0 + assert stream.language == "und" self.assertEqual( stream.metadata, { @@ -235,14 +233,14 @@ def test_stream_probing(self): "language": "und", }, ) - self.assertEqual(stream.profile, None) - self.assertEqual(stream.start_time, None) - self.assertEqual(stream.time_base, Fraction(1, 1000)) - self.assertEqual(stream.type, "subtitle") + assert stream.profile is None + assert stream.start_time is None + assert stream.time_base == Fraction(1, 1000) + assert stream.type == "subtitle" # codec context properties - self.assertEqual(stream.codec.name, "mov_text") - self.assertEqual(stream.codec.long_name, "3GPP Timed Text subtitle") + assert stream.codec.name == "mov_text" + assert stream.codec.long_name == "3GPP Timed Text subtitle" class TestVideoProbe(TestCase): @@ -250,17 +248,15 @@ def setUp(self): self.file = av.open(fate_suite("mpeg2/mpeg2_field_encoding.ts")) def test_container_probing(self): - self.assertEqual(self.file.bit_rate, 3950617) - self.assertEqual(self.file.duration, 1620000) - self.assertEqual(str(self.file.format), "") - self.assertEqual(self.file.format.name, "mpegts") - self.assertEqual( - self.file.format.long_name, "MPEG-TS (MPEG-2 Transport Stream)" - ) - self.assertEqual(self.file.metadata, {}) - self.assertEqual(self.file.size, 800000) - self.assertEqual(self.file.start_time, 22953408322) - self.assertEqual(len(self.file.streams), 1) + assert self.file.bit_rate == 3950617 + assert self.file.duration == 1620000 + assert str(self.file.format) == "" + assert self.file.format.name == "mpegts" + assert self.file.format.long_name == "MPEG-TS (MPEG-2 Transport Stream)" + assert self.file.metadata == {} + assert self.file.size == 800000 + assert self.file.start_time == 22953408322 + assert len(self.file.streams) == 1 def test_stream_probing(self): stream = self.file.streams[0] @@ -271,38 +267,38 @@ def test_stream_probing(self): ) # actual stream properties - self.assertEqual(stream.average_rate, Fraction(25, 1)) - self.assertEqual(stream.duration, 145800) - self.assertEqual(stream.frames, 0) - self.assertEqual(stream.id, 4131) - self.assertEqual(stream.index, 0) - self.assertEqual(stream.language, None) - self.assertEqual(stream.metadata, {}) - self.assertEqual(stream.profile, "Simple") - self.assertEqual(stream.start_time, 2065806749) - self.assertEqual(stream.time_base, Fraction(1, 90000)) - self.assertEqual(stream.type, "video") + assert stream.average_rate == Fraction(25, 1) + assert stream.duration == 145800 + assert stream.frames == 0 + assert stream.id == 4131 + assert stream.index == 0 + assert stream.language is None + assert stream.metadata == {} + assert stream.profile == "Simple" + assert stream.start_time == 2065806749 + assert stream.time_base == Fraction(1, 90000) + assert stream.type == "video" # codec context properties - self.assertEqual(stream.bit_rate, 3364800) - self.assertEqual(stream.codec.long_name, "MPEG-2 video") - self.assertEqual(stream.codec.name, "mpeg2video") - self.assertEqual(stream.display_aspect_ratio, Fraction(4, 3)) - self.assertEqual(stream.format.name, "yuv420p") - self.assertFalse(stream.has_b_frames) - self.assertEqual(stream.height, 576) - self.assertEqual(stream.max_bit_rate, None) - self.assertEqual(stream.sample_aspect_ratio, Fraction(16, 15)) - self.assertEqual(stream.width, 720) + assert stream.bit_rate == 3364800 + assert stream.codec.long_name == "MPEG-2 video" + assert stream.codec.name == "mpeg2video" + assert stream.display_aspect_ratio == Fraction(4, 3) + assert stream.format.name == "yuv420p" + assert not stream.has_b_frames + assert stream.height == 576 + assert stream.max_bit_rate is None + assert stream.sample_aspect_ratio == Fraction(16, 15) + assert stream.width == 720 # For some reason, these behave differently on OS X (@mikeboers) and # Ubuntu (Travis). We think it is FFmpeg, but haven't been able to # confirm. - self.assertIn(stream.coded_width, (720, 0)) - self.assertIn(stream.coded_height, (576, 0)) + assert stream.coded_width in (720, 0) + assert stream.coded_height in (576, 0) - self.assertEqual(hasattr(stream, "framerate"), False) - self.assertEqual(hasattr(stream, "rate"), False) + assert not hasattr(stream, "framerate") + assert not hasattr(stream, "rate") class TestVideoProbeCorrupt(TestCase): @@ -314,48 +310,46 @@ def setUp(self): self.file = av.open(path) def test_container_probing(self): - self.assertEqual(str(self.file.format), "") - self.assertEqual(self.file.format.name, "h264") - self.assertEqual(self.file.format.long_name, "raw H.264 video") - self.assertEqual(self.file.size, 0) - self.assertEqual(self.file.bit_rate, 0) - self.assertEqual(self.file.duration, None) + assert str(self.file.format) == "" + assert self.file.format.name == "h264" + assert self.file.format.long_name == "raw H.264 video" + assert self.file.size == 0 + assert self.file.bit_rate == 0 + assert self.file.duration is None - self.assertEqual(len(self.file.streams), 1) - self.assertEqual(self.file.start_time, None) - self.assertEqual(self.file.metadata, {}) + assert len(self.file.streams) == 1 + assert self.file.start_time is None + assert self.file.metadata == {} def test_stream_probing(self): stream = self.file.streams[0] # ensure __repr__ does not crash - self.assertTrue(str(stream).startswith(" None: input_container = av.open(format="lavfi", file="color=c=pink:duration=1:r=30") @@ -258,6 +258,6 @@ def test_EOF(self) -> None: # if we do not push None, we get a BlockingIOError palette_frame = graph.vpull() - self.assertIsInstance(palette_frame, av.VideoFrame) - self.assertEqual(palette_frame.width, 16) - self.assertEqual(palette_frame.height, 16) + assert isinstance(palette_frame, av.VideoFrame) + assert palette_frame.width == 16 + assert palette_frame.height == 16 diff --git a/tests/test_logging.py b/tests/test_logging.py index 2a09a9c1a..5602ce751 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -14,8 +14,8 @@ def do_log(message): class TestLogging(TestCase): def test_adapt_level(self): - self.assertEqual(av.logging.adapt_level(av.logging.ERROR), logging.ERROR) - self.assertEqual(av.logging.adapt_level(av.logging.WARNING), logging.WARNING) + assert av.logging.adapt_level(av.logging.ERROR) == logging.ERROR + assert av.logging.adapt_level(av.logging.WARNING) == logging.WARNING self.assertEqual( av.logging.adapt_level((av.logging.WARNING + av.logging.ERROR) // 2), logging.WARNING, @@ -80,7 +80,7 @@ def test_error(self): try: av.error.err_check(-errno.EPERM) except OSError as e: - self.assertEqual(e.log, log) + assert e.log == log else: self.fail() diff --git a/tests/test_options.py b/tests/test_options.py index 790780b20..f5593a77d 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -1,19 +1,17 @@ from av import ContainerFormat from av.option import Option, OptionType -from .common import TestCase - -class TestOptions(TestCase): - def test_mov_options(self): +class TestOptions: + def test_mov_options(self) -> None: mov = ContainerFormat("mov") - options = mov.descriptor.options + options = mov.descriptor.options # type: ignore by_name = {opt.name: opt for opt in options} opt = by_name.get("use_absolute_path") - self.assertIsInstance(opt, Option) - self.assertEqual(opt.name, "use_absolute_path") + assert isinstance(opt, Option) + assert opt.name == "use_absolute_path" # This was not a good option to actually test. - self.assertIn(opt.type, (OptionType.BOOL, OptionType.INT)) + assert opt.type in (OptionType.BOOL, OptionType.INT) diff --git a/tests/test_packet.py b/tests/test_packet.py index cb0909116..423396f71 100644 --- a/tests/test_packet.py +++ b/tests/test_packet.py @@ -1,49 +1,50 @@ import av -from .common import TestCase, fate_suite +from .common import fate_suite -class TestProperties(TestCase): - def test_is_keyframe(self): +class TestProperties: + def test_is_keyframe(self) -> None: with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: stream = container.streams.video[0] for i, packet in enumerate(container.demux(stream)): if i in (0, 21, 45, 69, 93, 117): - self.assertTrue(packet.is_keyframe) + assert packet.is_keyframe else: - self.assertFalse(packet.is_keyframe) + assert not packet.is_keyframe - def test_is_corrupt(self): + def test_is_corrupt(self) -> None: with av.open(fate_suite("mov/white_zombie_scrunch-part.mov")) as container: stream = container.streams.video[0] for i, packet in enumerate(container.demux(stream)): if i == 65: - self.assertTrue(packet.is_corrupt) + assert packet.is_corrupt else: - self.assertFalse(packet.is_corrupt) + assert not packet.is_corrupt - def test_is_discard(self): + def test_is_discard(self) -> None: with av.open(fate_suite("mov/mov-1elist-ends-last-bframe.mov")) as container: stream = container.streams.video[0] for i, packet in enumerate(container.demux(stream)): if i == 46: - self.assertTrue(packet.is_discard) + assert packet.is_discard else: - self.assertFalse(packet.is_discard) + assert not packet.is_discard - def test_is_disposable(self): + def test_is_disposable(self) -> None: with av.open(fate_suite("hap/HAPQA_NoSnappy_127x1.mov")) as container: stream = container.streams.video[0] for i, packet in enumerate(container.demux(stream)): if i == 0: - self.assertTrue(packet.is_disposable) + assert packet.is_disposable else: - self.assertFalse(packet.is_disposable) + assert not packet.is_disposable - def test_set_duration(self): + def test_set_duration(self) -> None: with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: for packet in container.demux(): + assert packet.duration is not None old_duration = packet.duration packet.duration += 10 - self.assertEqual(packet.duration, old_duration + 10) + assert packet.duration == old_duration + 10 diff --git a/tests/test_python_io.py b/tests/test_python_io.py index 58ae4fd0d..9f5e55999 100644 --- a/tests/test_python_io.py +++ b/tests/test_python_io.py @@ -243,34 +243,34 @@ def test_writing_to_custom_io_image2(self): output.mux(packet) # Check that "frame_count" files were opened using the custom IO - self.assertEqual(len(wrapped_custom_io._log), frame_count) - self.assertEqual(len(wrapped_custom_io._method_log), frame_count) + assert len(wrapped_custom_io._log) == frame_count + assert len(wrapped_custom_io._method_log) == frame_count # Check that all files were written to all_write = all( method_log._filter("write") for method_log in wrapped_custom_io._method_log ) - self.assertTrue(all_write) + assert all_write # Check that all files were closed all_closed = all( method_log._filter("close") for method_log in wrapped_custom_io._method_log ) - self.assertTrue(all_closed) + assert all_closed # Check contents. with av.open(sequence_filename, "r", "image2") as container: - self.assertEqual(len(container.streams), 1) + assert len(container.streams) == 1 stream = container.streams[0] - self.assertIsInstance(stream, av.video.stream.VideoStream) - self.assertEqual(stream.duration, frame_count) - self.assertEqual(stream.type, "video") + assert isinstance(stream, av.video.stream.VideoStream) + assert stream.duration == frame_count + assert stream.type == "video" # codec context properties - self.assertEqual(stream.codec.name, "png") - self.assertEqual(stream.format.name, "rgb24") - self.assertEqual(stream.format.width, width) - self.assertEqual(stream.format.height, height) + assert stream.codec.name == "png" + assert stream.format.name == "rgb24" + assert stream.format.width == width + assert stream.format.height == height def test_writing_to_file(self) -> None: path = self.sandboxed("writing.mp4") @@ -286,9 +286,9 @@ def test_writing_to_pipe_readonly(self) -> None: buf = ReadOnlyPipe() with self.assertRaises(ValueError) as cm: self.write(buf) - self.assertEqual( - str(cm.exception), - "File object has no write() method, or writable() returned False.", + assert ( + str(cm.exception) + == "File object has no write() method, or writable() returned False." ) def test_writing_to_pipe_writeonly(self): @@ -297,10 +297,7 @@ def test_writing_to_pipe_writeonly(self): buf = WriteOnlyPipe() with self.assertRaises(ValueError) as cm: self.write(buf) - self.assertIn( - "[mp4] muxer does not support non seekable output", - str(cm.exception), - ) + assert "[mp4] muxer does not support non seekable output" in str(cm.exception) av.logging.set_level(None) @@ -308,14 +305,14 @@ def read(self, fh, seekable: bool = True) -> None: wrapped = MethodLogger(fh) with av.open(wrapped, "r") as container: - self.assertEqual(container.format.name, "mpegts") + assert container.format.name == "mpegts" self.assertEqual( container.format.long_name, "MPEG-TS (MPEG-2 Transport Stream)" ) - self.assertEqual(len(container.streams), 1) + assert len(container.streams) == 1 if seekable: - self.assertEqual(container.size, 800000) - self.assertEqual(container.metadata, {}) + assert container.size == 800000 + assert container.metadata == {} # Check method calls. self.assertTrue(wrapped._filter("read")) diff --git a/tests/test_seek.py b/tests/test_seek.py index c29b3c9d6..ee6f84536 100644 --- a/tests/test_seek.py +++ b/tests/test_seek.py @@ -47,7 +47,7 @@ def test_seek_start(self): for packet in container.demux(): seek_packet_count += 1 - self.assertEqual(total_packet_count, seek_packet_count) + assert total_packet_count == seek_packet_count def test_seek_middle(self): container = av.open(fate_suite("h264/interlaced_crop.mp4")) @@ -84,8 +84,8 @@ def test_seek_end(self): seek_packet_count += 1 # there should be some packet because we're seeking to the last keyframe - self.assertTrue(seek_packet_count > 0) - self.assertTrue(seek_packet_count < middle_packet_count) + assert seek_packet_count > 0 + assert seek_packet_count < middle_packet_count def test_decode_half(self): container = av.open(fate_suite("h264/interlaced_crop.mp4")) @@ -98,7 +98,7 @@ def test_decode_half(self): for frame in packet.decode(): total_frame_count += 1 - self.assertEqual(video_stream.frames, total_frame_count) + assert video_stream.frames == total_frame_count # set target frame to middle frame target_frame = int(total_frame_count / 2.0) @@ -123,7 +123,7 @@ def test_decode_half(self): if current_frame is not None and current_frame >= target_frame: frame_count += 1 - self.assertEqual(frame_count, total_frame_count - target_frame) + assert frame_count == total_frame_count - target_frame def test_stream_seek(self): container = av.open(fate_suite("h264/interlaced_crop.mp4")) @@ -160,7 +160,7 @@ def test_stream_seek(self): if current_frame is not None and current_frame >= target_frame: frame_count += 1 - self.assertEqual(frame_count, total_frame_count - target_frame) + assert frame_count == total_frame_count - target_frame if __name__ == "__main__": diff --git a/tests/test_subtitles.py b/tests/test_subtitles.py index 9a5e9ceb1..e2b5ab512 100644 --- a/tests/test_subtitles.py +++ b/tests/test_subtitles.py @@ -1,10 +1,10 @@ import av from av.subtitles.subtitle import AssSubtitle, BitmapSubtitle -from .common import TestCase, fate_suite +from .common import fate_suite -class TestSubtitle(TestCase): +class TestSubtitle: def test_movtext(self) -> None: path = fate_suite("sub/MovText_capability_tester.mp4") @@ -13,24 +13,22 @@ def test_movtext(self) -> None: for packet in container.demux(): subs.extend(packet.decode()) - self.assertEqual(len(subs), 3) + assert len(subs) == 3 subset = subs[0] - self.assertEqual(subset.format, 1) - self.assertEqual(subset.pts, 970000) - self.assertEqual(subset.start_display_time, 0) - self.assertEqual(subset.end_display_time, 1570) + assert subset.format == 1 + assert subset.pts == 970000 + assert subset.start_display_time == 0 + assert subset.end_display_time == 1570 sub = subset[0] - self.assertIsInstance(sub, AssSubtitle) assert isinstance(sub, AssSubtitle) + assert sub.type == b"ass" + assert sub.text == b"" + assert sub.ass == b"0,0,Default,,0,0,0,,- Test 1.\\N- Test 2." + assert sub.dialogue == b"- Test 1.\n- Test 2." - self.assertEqual(sub.type, b"ass") - self.assertEqual(sub.text, b"") - self.assertEqual(sub.ass, b"0,0,Default,,0,0,0,,- Test 1.\\N- Test 2.") - self.assertEqual(sub.dialogue, b"- Test 1.\n- Test 2.") - - def test_vobsub(self): + def test_vobsub(self) -> None: path = fate_suite("sub/vobsub.sub") subs = [] @@ -38,26 +36,26 @@ def test_vobsub(self): for packet in container.demux(): subs.extend(packet.decode()) - self.assertEqual(len(subs), 43) + assert len(subs) == 43 subset = subs[0] - self.assertEqual(subset.format, 0) - self.assertEqual(subset.pts, 132499044) - self.assertEqual(subset.start_display_time, 0) - self.assertEqual(subset.end_display_time, 4960) + assert subset.format == 0 + assert subset.pts == 132499044 + assert subset.start_display_time == 0 + assert subset.end_display_time == 4960 sub = subset[0] - self.assertIsInstance(sub, BitmapSubtitle) - self.assertEqual(sub.type, b"bitmap") - self.assertEqual(sub.x, 259) - self.assertEqual(sub.y, 379) - self.assertEqual(sub.width, 200) - self.assertEqual(sub.height, 24) - self.assertEqual(sub.nb_colors, 4) + assert isinstance(sub, BitmapSubtitle) + assert sub.type == b"bitmap" + assert sub.x == 259 + assert sub.y == 379 + assert sub.width == 200 + assert sub.height == 24 + assert sub.nb_colors == 4 bms = sub.planes - self.assertEqual(len(bms), 1) - self.assertEqual(len(memoryview(bms[0])), 4800) + assert len(bms) == 1 + assert len(memoryview(bms[0])) == 4800 # type: ignore def test_subtitle_flush(self) -> None: path = fate_suite("sub/MovText_capability_tester.mp4") @@ -69,4 +67,4 @@ def test_subtitle_flush(self) -> None: subs.extend(stream.decode(packet)) subs.extend(stream.decode()) - self.assertEqual(len(subs), 3) + assert len(subs) == 3 diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 9dff66d7f..a7b753de7 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -12,67 +12,67 @@ class TestVideoFrameConstructors(TestCase): def test_invalid_pixel_format(self): with self.assertRaises(ValueError) as cm: VideoFrame(640, 480, "__unknown_pix_fmt") - self.assertEqual(str(cm.exception), "not a pixel format: '__unknown_pix_fmt'") + assert str(cm.exception) == "not a pixel format: '__unknown_pix_fmt'" def test_null_constructor(self): frame = VideoFrame() - self.assertEqual(frame.width, 0) - self.assertEqual(frame.height, 0) - self.assertEqual(frame.format.name, "yuv420p") + assert frame.width == 0 + assert frame.height == 0 + assert frame.format.name == "yuv420p" def test_manual_yuv_constructor(self): frame = VideoFrame(640, 480, "yuv420p") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "yuv420p") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "yuv420p" def test_manual_rgb_constructor(self): frame = VideoFrame(640, 480, "rgb24") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "rgb24") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "rgb24" class TestVideoFramePlanes(TestCase): def test_null_planes(self): frame = VideoFrame() # yuv420p - self.assertEqual(len(frame.planes), 0) + assert len(frame.planes) == 0 def test_yuv420p_planes(self): frame = VideoFrame(640, 480, "yuv420p") - self.assertEqual(len(frame.planes), 3) - self.assertEqual(frame.planes[0].width, 640) - self.assertEqual(frame.planes[0].height, 480) - self.assertEqual(frame.planes[0].line_size, 640) - self.assertEqual(frame.planes[0].buffer_size, 640 * 480) + assert len(frame.planes) == 3 + assert frame.planes[0].width == 640 + assert frame.planes[0].height == 480 + assert frame.planes[0].line_size == 640 + assert frame.planes[0].buffer_size == 640 * 480 for i in range(1, 3): - self.assertEqual(frame.planes[i].width, 320) - self.assertEqual(frame.planes[i].height, 240) - self.assertEqual(frame.planes[i].line_size, 320) - self.assertEqual(frame.planes[i].buffer_size, 320 * 240) + assert frame.planes[i].width == 320 + assert frame.planes[i].height == 240 + assert frame.planes[i].line_size == 320 + assert frame.planes[i].buffer_size == 320 * 240 def test_yuv420p_planes_align(self): # If we request 8-byte alignment for a width which is not a multiple of 8, # the line sizes are larger than the plane width. frame = VideoFrame(318, 238, "yuv420p") - self.assertEqual(len(frame.planes), 3) - self.assertEqual(frame.planes[0].width, 318) - self.assertEqual(frame.planes[0].height, 238) - self.assertEqual(frame.planes[0].line_size, 320) - self.assertEqual(frame.planes[0].buffer_size, 320 * 238) + assert len(frame.planes) == 3 + assert frame.planes[0].width == 318 + assert frame.planes[0].height == 238 + assert frame.planes[0].line_size == 320 + assert frame.planes[0].buffer_size == 320 * 238 for i in range(1, 3): - self.assertEqual(frame.planes[i].width, 159) - self.assertEqual(frame.planes[i].height, 119) - self.assertEqual(frame.planes[i].line_size, 160) - self.assertEqual(frame.planes[i].buffer_size, 160 * 119) + assert frame.planes[i].width == 159 + assert frame.planes[i].height == 119 + assert frame.planes[i].line_size == 160 + assert frame.planes[i].buffer_size == 160 * 119 def test_rgb24_planes(self): frame = VideoFrame(640, 480, "rgb24") - self.assertEqual(len(frame.planes), 1) - self.assertEqual(frame.planes[0].width, 640) - self.assertEqual(frame.planes[0].height, 480) - self.assertEqual(frame.planes[0].line_size, 640 * 3) - self.assertEqual(frame.planes[0].buffer_size, 640 * 480 * 3) + assert len(frame.planes) == 1 + assert frame.planes[0].width == 640 + assert frame.planes[0].height == 480 + assert frame.planes[0].line_size == 640 * 3 + assert frame.planes[0].buffer_size == 640 * 480 * 3 class TestVideoFrameBuffers(TestCase): @@ -80,13 +80,13 @@ def test_memoryview_read(self): frame = VideoFrame(640, 480, "rgb24") frame.planes[0].update(b"01234" + (b"x" * (640 * 480 * 3 - 5))) mem = memoryview(frame.planes[0]) - self.assertEqual(mem.ndim, 1) - self.assertEqual(mem.shape, (640 * 480 * 3,)) + assert mem.ndim == 1 + assert mem.shape == (640 * 480 * 3,) self.assertFalse(mem.readonly) - self.assertEqual(mem[1], 49) - self.assertEqual(mem[:7], b"01234xx") + assert mem[1] == 49 + assert mem[:7] == b"01234xx" mem[1] = 46 - self.assertEqual(mem[:7], b"0.234xx") + assert mem[:7] == b"0.234xx" class TestVideoFrameImage(TestCase): @@ -131,253 +131,253 @@ def test_to_image_rgb24(self): pos += 1 img = frame.to_image() - self.assertEqual(img.size, (width, height)) - self.assertEqual(img.tobytes(), expected) + assert img.size == (width, height) + assert img.tobytes() == expected def test_to_image_with_dimensions(self): frame = VideoFrame(640, 480, format="rgb24") img = frame.to_image(width=320, height=240) - self.assertEqual(img.size, (320, 240)) + assert img.size == (320, 240) class TestVideoFrameNdarray(TestCase): def assertPixelValue16(self, plane, expected, byteorder: str): view = memoryview(plane) if byteorder == "big": - self.assertEqual(view[0], (expected >> 8) & 0xFF) - self.assertEqual(view[1], expected & 0xFF) + assert view[0] == (expected >> 8 & 0xFF) + assert view[1] == expected & 0xFF else: - self.assertEqual(view[0], expected & 0xFF) - self.assertEqual(view[1], (expected >> 8) & 0xFF) + assert view[0] == expected & 0xFF + assert view[1] == (expected >> 8 & 0xFF) def test_basic_to_ndarray(self): frame = VideoFrame(640, 480, "rgb24") array = frame.to_ndarray() - self.assertEqual(array.shape, (480, 640, 3)) + assert array.shape == (480, 640, 3) def test_ndarray_gray(self): array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) for format in ["gray", "gray8"]: frame = VideoFrame.from_ndarray(array, format=format) - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "gray") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "gray" self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_gray_align(self): array = numpy.random.randint(0, 256, size=(238, 318), dtype=numpy.uint8) for format in ["gray", "gray8"]: frame = VideoFrame.from_ndarray(array, format=format) - self.assertEqual(frame.width, 318) - self.assertEqual(frame.height, 238) - self.assertEqual(frame.format.name, "gray") + assert frame.width == 318 + assert frame.height == 238 + assert frame.format.name == "gray" self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_rgb(self): array = numpy.random.randint(0, 256, size=(480, 640, 3), dtype=numpy.uint8) for format in ["rgb24", "bgr24"]: frame = VideoFrame.from_ndarray(array, format=format) - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, format) + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == format self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_rgb_align(self): array = numpy.random.randint(0, 256, size=(238, 318, 3), dtype=numpy.uint8) for format in ["rgb24", "bgr24"]: frame = VideoFrame.from_ndarray(array, format=format) - self.assertEqual(frame.width, 318) - self.assertEqual(frame.height, 238) - self.assertEqual(frame.format.name, format) + assert frame.width == 318 + assert frame.height == 238 + assert frame.format.name == format self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_rgba(self): array = numpy.random.randint(0, 256, size=(480, 640, 4), dtype=numpy.uint8) for format in ["argb", "rgba", "abgr", "bgra"]: frame = VideoFrame.from_ndarray(array, format=format) - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, format) + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == format self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_rgba_align(self): array = numpy.random.randint(0, 256, size=(238, 318, 4), dtype=numpy.uint8) for format in ["argb", "rgba", "abgr", "bgra"]: frame = VideoFrame.from_ndarray(array, format=format) - self.assertEqual(frame.width, 318) - self.assertEqual(frame.height, 238) - self.assertEqual(frame.format.name, format) + assert frame.width == 318 + assert frame.height == 238 + assert frame.format.name == format self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_gbrp(self): array = numpy.random.randint(0, 256, size=(480, 640, 3), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="gbrp") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "gbrp") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "gbrp" self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_gbrp_align(self): array = numpy.random.randint(0, 256, size=(238, 318, 3), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="gbrp") - self.assertEqual(frame.width, 318) - self.assertEqual(frame.height, 238) - self.assertEqual(frame.format.name, "gbrp") + assert frame.width == 318 + assert frame.height == 238 + assert frame.format.name == "gbrp" self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_gbrp10(self): array = numpy.random.randint(0, 1024, size=(480, 640, 3), dtype=numpy.uint16) for format in ["gbrp10be", "gbrp10le"]: frame = VideoFrame.from_ndarray(array, format=format) - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, format) + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == format self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_gbrp10_align(self): array = numpy.random.randint(0, 1024, size=(238, 318, 3), dtype=numpy.uint16) for format in ["gbrp10be", "gbrp10le"]: frame = VideoFrame.from_ndarray(array, format=format) - self.assertEqual(frame.width, 318) - self.assertEqual(frame.height, 238) - self.assertEqual(frame.format.name, format) + assert frame.width == 318 + assert frame.height == 238 + assert frame.format.name == format self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_gbrp12(self): array = numpy.random.randint(0, 4096, size=(480, 640, 3), dtype=numpy.uint16) for format in ["gbrp12be", "gbrp12le"]: frame = VideoFrame.from_ndarray(array, format=format) - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, format) + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == format self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_gbrp12_align(self): array = numpy.random.randint(0, 4096, size=(238, 318, 3), dtype=numpy.uint16) for format in ["gbrp12be", "gbrp12le"]: frame = VideoFrame.from_ndarray(array, format=format) - self.assertEqual(frame.width, 318) - self.assertEqual(frame.height, 238) - self.assertEqual(frame.format.name, format) + assert frame.width == 318 + assert frame.height == 238 + assert frame.format.name == format self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_gbrp14(self): array = numpy.random.randint(0, 16384, size=(480, 640, 3), dtype=numpy.uint16) for format in ["gbrp14be", "gbrp14le"]: frame = VideoFrame.from_ndarray(array, format=format) - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, format) + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == format self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_gbrp14_align(self): array = numpy.random.randint(0, 16384, size=(238, 318, 3), dtype=numpy.uint16) for format in ["gbrp14be", "gbrp14le"]: frame = VideoFrame.from_ndarray(array, format=format) - self.assertEqual(frame.width, 318) - self.assertEqual(frame.height, 238) - self.assertEqual(frame.format.name, format) + assert frame.width == 318 + assert frame.height == 238 + assert frame.format.name == format self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_gbrp16(self): array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) for format in ["gbrp16be", "gbrp16le"]: frame = VideoFrame.from_ndarray(array, format=format) - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, format) + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == format self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_gbrp16_align(self): array = numpy.random.randint(0, 65536, size=(238, 318, 3), dtype=numpy.uint16) for format in ["gbrp16be", "gbrp16le"]: frame = VideoFrame.from_ndarray(array, format=format) - self.assertEqual(frame.width, 318) - self.assertEqual(frame.height, 238) - self.assertEqual(frame.format.name, format) + assert frame.width == 318 + assert frame.height == 238 + assert frame.format.name == format self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_gbrpf32(self): array = numpy.random.random_sample(size=(480, 640, 3)).astype(numpy.float32) for format in ["gbrpf32be", "gbrpf32le"]: frame = VideoFrame.from_ndarray(array, format=format) - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, format) + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == format self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_gbrpf32_align(self): array = numpy.random.random_sample(size=(238, 318, 3)).astype(numpy.float32) for format in ["gbrpf32be", "gbrpf32le"]: frame = VideoFrame.from_ndarray(array, format=format) - self.assertEqual(frame.width, 318) - self.assertEqual(frame.height, 238) - self.assertEqual(frame.format.name, format) + assert frame.width == 318 + assert frame.height == 238 + assert frame.format.name == format self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_yuv420p(self): array = numpy.random.randint(0, 256, size=(720, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuv420p") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "yuv420p") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "yuv420p" self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_yuv420p_align(self): array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuv420p") - self.assertEqual(frame.width, 318) - self.assertEqual(frame.height, 238) - self.assertEqual(frame.format.name, "yuv420p") + assert frame.width == 318 + assert frame.height == 238 + assert frame.format.name == "yuv420p" self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_yuvj420p(self): array = numpy.random.randint(0, 256, size=(720, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuvj420p") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "yuvj420p") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "yuvj420p" self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_yuyv422(self): array = numpy.random.randint(0, 256, size=(480, 640, 2), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuyv422") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "yuyv422") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "yuyv422" self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_yuv444p(self): array = numpy.random.randint(0, 256, size=(3, 480, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuv444p") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "yuv444p") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "yuv444p" self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_yuvj444p(self): array = numpy.random.randint(0, 256, size=(3, 480, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuvj444p") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "yuvj444p") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "yuvj444p" self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_yuyv422_align(self): array = numpy.random.randint(0, 256, size=(238, 318, 2), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuyv422") - self.assertEqual(frame.width, 318) - self.assertEqual(frame.height, 238) - self.assertEqual(frame.format.name, "yuyv422") + assert frame.width == 318 + assert frame.height == 238 + assert frame.format.name == "yuyv422" self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_gray16be(self): array = numpy.random.randint(0, 65536, size=(480, 640), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="gray16be") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "gray16be") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "gray16be" self.assertNdarraysEqual(frame.to_ndarray(), array) # check endianness by examining value of first pixel @@ -386,9 +386,9 @@ def test_ndarray_gray16be(self): def test_ndarray_gray16le(self): array = numpy.random.randint(0, 65536, size=(480, 640), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="gray16le") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "gray16le") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "gray16le" self.assertNdarraysEqual(frame.to_ndarray(), array) # check endianness by examining value of first pixel @@ -397,9 +397,9 @@ def test_ndarray_gray16le(self): def test_ndarray_rgb48be(self): array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="rgb48be") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "rgb48be") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "rgb48be" self.assertNdarraysEqual(frame.to_ndarray(), array) # check endianness by examining red value of first pixel @@ -408,9 +408,9 @@ def test_ndarray_rgb48be(self): def test_ndarray_rgb48le(self): array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="rgb48le") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "rgb48le") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "rgb48le" self.assertNdarraysEqual(frame.to_ndarray(), array) # check endianness by examining red value of first pixel @@ -419,9 +419,9 @@ def test_ndarray_rgb48le(self): def test_ndarray_rgb48le_align(self): array = numpy.random.randint(0, 65536, size=(238, 318, 3), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="rgb48le") - self.assertEqual(frame.width, 318) - self.assertEqual(frame.height, 238) - self.assertEqual(frame.format.name, "rgb48le") + assert frame.width == 318 + assert frame.height == 238 + assert frame.format.name == "rgb48le" self.assertNdarraysEqual(frame.to_ndarray(), array) # check endianness by examining red value of first pixel @@ -430,9 +430,9 @@ def test_ndarray_rgb48le_align(self): def test_ndarray_rgba64be(self): array = numpy.random.randint(0, 65536, size=(480, 640, 4), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="rgba64be") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "rgba64be") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "rgba64be" self.assertNdarraysEqual(frame.to_ndarray(), array) # check endianness by examining red value of first pixel @@ -441,9 +441,9 @@ def test_ndarray_rgba64be(self): def test_ndarray_rgba64le(self): array = numpy.random.randint(0, 65536, size=(480, 640, 4), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="rgba64le") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "rgba64le") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "rgba64le" self.assertNdarraysEqual(frame.to_ndarray(), array) # check endianness by examining red value of first pixel @@ -452,26 +452,26 @@ def test_ndarray_rgba64le(self): def test_ndarray_rgb8(self): array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="rgb8") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "rgb8") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "rgb8" self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_bgr8(self): array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="bgr8") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "bgr8") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "bgr8" self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_pal8(self): array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) palette = numpy.random.randint(0, 256, size=(256, 4), dtype=numpy.uint8) frame = VideoFrame.from_ndarray((array, palette), format="pal8") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "pal8") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "pal8" returned = frame.to_ndarray() self.assertTrue((type(returned) is tuple) and len(returned) == 2) self.assertNdarraysEqual(returned[0], array) @@ -480,17 +480,17 @@ def test_ndarray_pal8(self): def test_ndarray_nv12(self): array = numpy.random.randint(0, 256, size=(720, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="nv12") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "nv12") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "nv12" self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_nv12_align(self): array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="nv12") - self.assertEqual(frame.width, 318) - self.assertEqual(frame.height, 238) - self.assertEqual(frame.format.name, "nv12") + assert frame.width == 318 + assert frame.height == 238 + assert frame.format.name == "nv12" self.assertNdarraysEqual(frame.to_ndarray(), array) @@ -593,20 +593,20 @@ def test_shares_memory_bgr24(self): class TestVideoFrameTiming(TestCase): - def test_reformat_pts(self): + def test_reformat_pts(self) -> None: frame = VideoFrame(640, 480, "rgb24") frame.pts = 123 frame.time_base = Fraction("456/1") frame = frame.reformat(320, 240) - self.assertEqual(frame.pts, 123) - self.assertEqual(frame.time_base, 456) + assert frame.pts == 123 + assert frame.time_base == 456 class TestVideoFrameReformat(TestCase): def test_reformat_identity(self): frame1 = VideoFrame(640, 480, "rgb24") frame2 = frame1.reformat(640, 480, "rgb24") - self.assertIs(frame1, frame2) + assert frame1 is frame2 def test_reformat_colourspace(self): # This is allowed. From 985f9bf9d9232d5e9d77b603a24bfed65cfb9a1e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 23 Sep 2024 18:39:40 -0400 Subject: [PATCH 361/846] Replace self.assertEqual with `assert` statement Works better with mypy. `assert`s work equally well since we're using pytest now. --- tests/test_audiofifo.py | 25 ++++++------ tests/test_bitstream.py | 26 ++++++------ tests/test_codec_context.py | 62 ++++++++++++++--------------- tests/test_colorspace.py | 36 ++++++++--------- tests/test_containerformat.py | 74 ++++++++++++++++------------------- tests/test_decode.py | 44 ++++++++++----------- tests/test_encode.py | 8 ++-- tests/test_enums.py | 42 ++++++++++---------- tests/test_seek.py | 7 ---- tests/test_streams.py | 32 +++++++-------- tests/test_videoformat.py | 70 ++++++++++++++++----------------- 11 files changed, 205 insertions(+), 221 deletions(-) diff --git a/tests/test_audiofifo.py b/tests/test_audiofifo.py index 2874c7145..9eee420a8 100644 --- a/tests/test_audiofifo.py +++ b/tests/test_audiofifo.py @@ -59,17 +59,17 @@ def test_pts_simple(self) -> None: assert oframe.pts == 0 assert oframe.time_base == iframe.time_base - self.assertEqual(fifo.samples_written, 1024) - self.assertEqual(fifo.samples_read, 512) - self.assertEqual(fifo.pts_per_sample, 1.0) + assert fifo.samples_written == 1024 + assert fifo.samples_read == 512 + assert fifo.pts_per_sample == 1.0 iframe.pts = 1024 fifo.write(iframe) oframe = fifo.read(512) assert oframe is not None - self.assertEqual(oframe.pts, 512) - self.assertEqual(oframe.time_base, iframe.time_base) + assert oframe.pts == 512 + assert oframe.time_base == iframe.time_base iframe.pts = 9999 # Wrong! self.assertRaises(ValueError, fifo.write, iframe) @@ -88,8 +88,8 @@ def test_pts_complex(self) -> None: oframe = fifo.read_many(1024)[-1] - self.assertEqual(oframe.pts, 2048) - self.assertEqual(fifo.pts_per_sample, 2.0) + assert oframe.pts == 2048 + assert fifo.pts_per_sample == 2.0 def test_missing_sample_rate(self) -> None: fifo = av.AudioFifo() @@ -103,9 +103,9 @@ def test_missing_sample_rate(self) -> None: oframe = fifo.read(512) assert oframe is not None - self.assertIsNone(oframe.pts) - self.assertEqual(oframe.sample_rate, 0) - self.assertEqual(oframe.time_base, iframe.time_base) + assert oframe.pts is None + assert oframe.sample_rate == 0 + assert oframe.time_base == iframe.time_base def test_missing_time_base(self) -> None: fifo = av.AudioFifo() @@ -119,6 +119,5 @@ def test_missing_time_base(self) -> None: oframe = fifo.read(512) assert oframe is not None - self.assertIsNone(oframe.pts) - self.assertIsNone(oframe.time_base) - self.assertEqual(oframe.sample_rate, iframe.sample_rate) + assert oframe.pts is None and oframe.time_base is None + assert oframe.sample_rate == iframe.sample_rate diff --git a/tests/test_bitstream.py b/tests/test_bitstream.py index 8538594a7..335abac1c 100644 --- a/tests/test_bitstream.py +++ b/tests/test_bitstream.py @@ -23,14 +23,14 @@ def test_filter_chomp(self) -> None: ctx = BitStreamFilterContext("chomp") src_packets: tuple[Packet, None] = (Packet(b"\x0012345\0\0\0"), None) - self.assertEqual(bytes(src_packets[0]), b"\x0012345\0\0\0") + assert bytes(src_packets[0]) == b"\x0012345\0\0\0" result_packets = [] for p in src_packets: result_packets.extend(ctx.filter(p)) - self.assertEqual(len(result_packets), 1) - self.assertEqual(bytes(result_packets[0]), b"\x0012345") + assert len(result_packets) == 1 + assert bytes(result_packets[0]) == b"\x0012345" def test_filter_setts(self) -> None: ctx = BitStreamFilterContext("setts=pts=N") @@ -48,9 +48,9 @@ def test_filter_setts(self) -> None: for p in src_packets: result_packets.extend(ctx.filter(p)) - self.assertEqual(len(result_packets), 2) - self.assertEqual(result_packets[0].pts, 0) - self.assertEqual(result_packets[1].pts, 1) + assert len(result_packets) == 2 + assert result_packets[0].pts == 0 + assert result_packets[1].pts == 1 def test_filter_h264_mp4toannexb(self) -> None: with av.open(fate_suite("h264/interlaced_crop.mp4"), "r") as container: @@ -62,22 +62,22 @@ def test_filter_h264_mp4toannexb(self) -> None: self.assertFalse(is_annexb(p)) res_packets.extend(ctx.filter(p)) - self.assertEqual(len(res_packets), stream.frames) + assert len(res_packets) == stream.frames for p in res_packets: - self.assertTrue(is_annexb(p)) + assert is_annexb(p) def test_filter_output_parameters(self) -> None: with av.open(fate_suite("h264/interlaced_crop.mp4"), "r") as container: stream = container.streams.video[0] - self.assertFalse(is_annexb(stream.codec_context.extradata)) + assert not is_annexb(stream.codec_context.extradata) ctx = BitStreamFilterContext("h264_mp4toannexb", stream) - self.assertFalse(is_annexb(stream.codec_context.extradata)) + assert not is_annexb(stream.codec_context.extradata) del ctx _ = BitStreamFilterContext("h264_mp4toannexb", stream, out_stream=stream) - self.assertTrue(is_annexb(stream.codec_context.extradata)) + assert is_annexb(stream.codec_context.extradata) def test_filter_flush(self) -> None: with av.open(fate_suite("h264/interlaced_crop.mp4"), "r") as container: @@ -87,7 +87,7 @@ def test_filter_flush(self) -> None: res_packets = [] for p in container.demux(stream): res_packets.extend(ctx.filter(p)) - self.assertEqual(len(res_packets), stream.frames) + assert len(res_packets) == stream.frames container.seek(0) # Without flushing, we expect to get an error: "A non-NULL packet sent after an EOF." @@ -100,4 +100,4 @@ def test_filter_flush(self) -> None: for p in container.demux(stream): res_packets.extend(ctx.filter(p)) - self.assertEqual(len(res_packets), stream.frames * 2) + assert len(res_packets) == stream.frames * 2 diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 12997d76a..cb89b74d0 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -38,52 +38,52 @@ def iter_raw_frames(path, packet_sizes, ctx): class TestCodecContext(TestCase): def test_skip_frame_default(self): ctx = Codec("png", "w").create() - self.assertEqual(ctx.skip_frame.name, "DEFAULT") + assert ctx.skip_frame.name == "DEFAULT" def test_codec_delay(self): with av.open(fate_suite("mkv/codec_delay_opus.mkv")) as container: - self.assertEqual(container.streams.audio[0].codec_context.delay, 312) + assert container.streams.audio[0].codec_context.delay == 312 with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: - self.assertEqual(container.streams.video[0].codec_context.delay, 0) + assert container.streams.video[0].codec_context.delay == 0 def test_codec_tag(self): ctx = Codec("mpeg4", "w").create() - self.assertEqual(ctx.codec_tag, "\x00\x00\x00\x00") + assert ctx.codec_tag == "\x00\x00\x00\x00" ctx.codec_tag = "xvid" - self.assertEqual(ctx.codec_tag, "xvid") + assert ctx.codec_tag == "xvid" # wrong length with self.assertRaises(ValueError) as cm: ctx.codec_tag = "bob" - self.assertEqual(str(cm.exception), "Codec tag should be a 4 character string.") + assert str(cm.exception) == "Codec tag should be a 4 character string." # wrong type with self.assertRaises(ValueError) as cm: ctx.codec_tag = 123 - self.assertEqual(str(cm.exception), "Codec tag should be a 4 character string.") + assert str(cm.exception) == "Codec tag should be a 4 character string." with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: - self.assertEqual(container.streams[0].codec_tag, "avc1") + assert container.streams[0].codec_tag == "avc1" def test_decoder_extradata(self): ctx = av.codec.Codec("h264", "r").create() - self.assertEqual(ctx.extradata, None) - self.assertEqual(ctx.extradata_size, 0) + assert ctx.extradata is None + assert ctx.extradata_size == 0 ctx.extradata = b"123" - self.assertEqual(ctx.extradata, b"123") - self.assertEqual(ctx.extradata_size, 3) + assert ctx.extradata == b"123" + assert ctx.extradata_size == 3 ctx.extradata = b"54321" - self.assertEqual(ctx.extradata, b"54321") - self.assertEqual(ctx.extradata_size, 5) + assert ctx.extradata == b"54321" + assert ctx.extradata_size == 5 ctx.extradata = None - self.assertEqual(ctx.extradata, None) - self.assertEqual(ctx.extradata_size, 0) + assert ctx.extradata is None + assert ctx.extradata_size == 0 - def test_decoder_gop_size(self): - ctx = av.codec.Codec("h264", "r").create() + def test_decoder_gop_size(self) -> None: + ctx = av.codec.Codec("h264", "r").create("video") with self.assertRaises(RuntimeError): ctx.gop_size @@ -99,7 +99,7 @@ def test_decoder_timebase(self) -> None: def test_encoder_extradata(self): ctx = av.codec.Codec("h264", "w").create() - self.assertEqual(ctx.extradata, None) + assert ctx.extradata is None self.assertEqual(ctx.extradata_size, 0) ctx.extradata = b"123" @@ -170,7 +170,7 @@ def _assert_parse(self, codec_name, path): parsed_source = b"".join(bytes(p) for p in packets) self.assertEqual(len(parsed_source), len(full_source)) - self.assertEqual(full_source, parsed_source) + assert full_source == parsed_source class TestEncoding(TestCase): @@ -214,7 +214,7 @@ def image_sequence_encode(self, codec_name: str) -> None: new_frame = frame.reformat(width, height, pix_fmt) new_packets = ctx.encode(new_frame) - self.assertEqual(len(new_packets), 1) + assert len(new_packets) == 1 new_packet = new_packets[0] path = self.sandboxed( @@ -240,9 +240,9 @@ def image_sequence_encode(self, codec_name: str) -> None: packet = Packet(size) size = f.readinto(packet) frame = ctx.decode(packet)[0] - self.assertEqual(frame.width, width) - self.assertEqual(frame.height, height) - self.assertEqual(frame.format.name, pix_fmt) + assert frame.width == width + assert frame.height == height + assert frame.format.name == pix_fmt def test_encoding_h264(self): self.video_encoding("h264", {"crf": "19"}) @@ -333,13 +333,13 @@ def video_encoding(self, codec_name, options={}, codec_tag=None): decoded_frame_count = 0 for frame in iter_raw_frames(path, packet_sizes, ctx): decoded_frame_count += 1 - self.assertEqual(frame.width, width) - self.assertEqual(frame.height, height) - self.assertEqual(frame.format.name, pix_fmt) + assert frame.width == width + assert frame.height == height + assert frame.format.name == pix_fmt if frame.key_frame: keyframe_indices.append(decoded_frame_count) - self.assertEqual(frame_count, decoded_frame_count) + assert frame_count == decoded_frame_count self.assertIsInstance( all(keyframe_index for keyframe_index in keyframe_indices), int @@ -352,7 +352,7 @@ def video_encoding(self, codec_name, options={}, codec_tag=None): ): raise SkipTest() for i in decoded_gop_sizes: - self.assertEqual(i, gop_size) + assert i == gop_size final_gop_size = decoded_frame_count - max(keyframe_indices) self.assertLessEqual(final_gop_size, gop_size) @@ -433,5 +433,5 @@ def _audio_encoding( for frame in iter_raw_frames(path, packet_sizes, ctx): result_samples += frame.samples - self.assertEqual(frame.sample_rate, sample_rate) - self.assertEqual(frame.layout.nb_channels, 2) + assert frame.sample_rate == sample_rate + assert frame.layout.nb_channels == 2 diff --git a/tests/test_colorspace.py b/tests/test_colorspace.py index afc16846f..b54229643 100644 --- a/tests/test_colorspace.py +++ b/tests/test_colorspace.py @@ -1,31 +1,29 @@ import av from av.video.reformatter import ColorRange, Colorspace -from .common import TestCase, fate_suite +from .common import fate_suite -class TestColorSpace(TestCase): +class TestColorSpace: def test_penguin_joke(self) -> None: container = av.open( fate_suite("amv/MTV_high_res_320x240_sample_Penguin_Joke_MTV_from_WMV.amv") ) stream = container.streams.video[0] - self.assertEqual(stream.codec_context.color_range, 2) - self.assertEqual(stream.codec_context.color_range, ColorRange.JPEG) + assert stream.codec_context.color_range == 2 + assert stream.codec_context.color_range == ColorRange.JPEG - self.assertEqual(stream.codec_context.color_primaries, 2) - self.assertEqual(stream.codec_context.color_trc, 2) + assert stream.codec_context.color_primaries == 2 + assert stream.codec_context.color_trc == 2 - self.assertEqual(stream.codec_context.colorspace, 5) - self.assertEqual(stream.codec_context.colorspace, Colorspace.ITU601) + assert stream.codec_context.colorspace == 5 + assert stream.codec_context.colorspace == Colorspace.ITU601 - for packet in container.demux(stream): - for frame in packet.decode(): - assert isinstance(frame, av.VideoFrame) - self.assertEqual(frame.color_range, ColorRange.JPEG) # a.k.a "pc" - self.assertEqual(frame.colorspace, Colorspace.ITU601) - return + for frame in container.decode(stream): + assert frame.color_range == ColorRange.JPEG # a.k.a "pc" + assert frame.colorspace == Colorspace.ITU601 + return def test_sky_timelapse(self) -> None: container = av.open( @@ -33,8 +31,8 @@ def test_sky_timelapse(self) -> None: ) stream = container.streams.video[0] - self.assertEqual(stream.codec_context.color_range, 1) - self.assertEqual(stream.codec_context.color_range, ColorRange.MPEG) - self.assertEqual(stream.codec_context.color_primaries, 1) - self.assertEqual(stream.codec_context.color_trc, 1) - self.assertEqual(stream.codec_context.colorspace, 1) + assert stream.codec_context.color_range == 1 + assert stream.codec_context.color_range == ColorRange.MPEG + assert stream.codec_context.color_primaries == 1 + assert stream.codec_context.color_trc == 1 + assert stream.codec_context.colorspace == 1 diff --git a/tests/test_containerformat.py b/tests/test_containerformat.py index 5b6d31c35..6475cf9cd 100644 --- a/tests/test_containerformat.py +++ b/tests/test_containerformat.py @@ -1,63 +1,57 @@ from av import ContainerFormat, formats_available, open -from .common import TestCase - -class TestContainerFormats(TestCase): +class TestContainerFormats: def test_matroska(self) -> None: with open("test.mkv", "w") as container: - self.assertNotEqual(container.default_video_codec, "none") - self.assertNotEqual(container.default_audio_codec, "none") - self.assertEqual(container.default_subtitle_codec, "ass") - self.assertIn("ass", container.supported_codecs) + assert container.default_video_codec != "none" + assert container.default_audio_codec != "none" + assert container.default_subtitle_codec == "ass" + assert "ass" in container.supported_codecs fmt = ContainerFormat("matroska") - self.assertTrue(fmt.is_input) - self.assertTrue(fmt.is_output) - self.assertEqual(fmt.name, "matroska") - self.assertEqual(fmt.long_name, "Matroska") - self.assertIn("mkv", fmt.extensions) - self.assertFalse(fmt.no_file) + assert fmt.is_input and fmt.is_output + assert fmt.name == "matroska" + assert fmt.long_name == "Matroska" + assert "mkv" in fmt.extensions + assert not fmt.no_file def test_mov(self) -> None: with open("test.mov", "w") as container: - self.assertNotEqual(container.default_video_codec, "none") - self.assertNotEqual(container.default_audio_codec, "none") - self.assertEqual(container.default_subtitle_codec, "none") - self.assertIn("h264", container.supported_codecs) + assert container.default_video_codec != "none" + assert container.default_audio_codec != "none" + assert container.default_subtitle_codec == "none" + assert "h264" in container.supported_codecs fmt = ContainerFormat("mov") - self.assertTrue(fmt.is_input) - self.assertTrue(fmt.is_output) - self.assertEqual(fmt.name, "mov") - self.assertEqual(fmt.long_name, "QuickTime / MOV") - self.assertIn("mov", fmt.extensions) - self.assertFalse(fmt.no_file) + assert fmt.is_input and fmt.is_output + assert fmt.name == "mov" + assert fmt.long_name == "QuickTime / MOV" + assert "mov" in fmt.extensions + assert not fmt.no_file def test_gif(self) -> None: with open("test.gif", "w") as container: - self.assertEqual(container.default_video_codec, "gif") - self.assertEqual(container.default_audio_codec, "none") - self.assertEqual(container.default_subtitle_codec, "none") - self.assertIn("gif", container.supported_codecs) + assert container.default_video_codec == "gif" + assert container.default_audio_codec == "none" + assert container.default_subtitle_codec == "none" + assert "gif" in container.supported_codecs def test_stream_segment(self) -> None: # This format goes by two names, check both. fmt = ContainerFormat("stream_segment") - self.assertFalse(fmt.is_input) - self.assertTrue(fmt.is_output) - self.assertEqual(fmt.name, "stream_segment") - self.assertEqual(fmt.long_name, "streaming segment muxer") - self.assertEqual(fmt.extensions, set()) - self.assertTrue(fmt.no_file) + assert not fmt.is_input and fmt.is_output + assert fmt.name == "stream_segment" + assert fmt.long_name == "streaming segment muxer" + assert fmt.extensions == set() + assert fmt.no_file fmt = ContainerFormat("ssegment") - self.assertFalse(fmt.is_input) - self.assertTrue(fmt.is_output) - self.assertEqual(fmt.name, "ssegment") - self.assertEqual(fmt.long_name, "streaming segment muxer") - self.assertEqual(fmt.extensions, set()) - self.assertTrue(fmt.no_file) + assert not fmt.is_input and fmt.is_output + assert fmt.name == "ssegment" + assert fmt.long_name == "streaming segment muxer" + assert fmt.extensions == set() + assert fmt.no_file def test_formats_available(self) -> None: - self.assertTrue(formats_available) + assert formats_available diff --git a/tests/test_decode.py b/tests/test_decode.py index 87a84ba12..82e8461e2 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -6,21 +6,19 @@ class TestDecode(TestCase): - def test_decoded_video_frame_count(self): + def test_decoded_video_frame_count(self) -> None: container = av.open(fate_suite("h264/interlaced_crop.mp4")) video_stream = next(s for s in container.streams if s.type == "video") - self.assertIs(video_stream, container.streams.video[0]) + assert video_stream is container.streams.video[0] frame_count = 0 + for frame in container.decode(video_stream): + frame_count += 1 - for packet in container.demux(video_stream): - for frame in packet.decode(): - frame_count += 1 + assert frame_count == video_stream.frames - self.assertEqual(frame_count, video_stream.frames) - - def test_decode_audio_corrupt(self): + def test_decode_audio_corrupt(self) -> None: # write an empty file path = self.sandboxed("empty.flac") with open(path, "wb"): @@ -35,36 +33,38 @@ def test_decode_audio_corrupt(self): frame_count += 1 packet_count += 1 - self.assertEqual(packet_count, 1) - self.assertEqual(frame_count, 0) + assert packet_count == 1 + assert frame_count == 0 - def test_decode_audio_sample_count(self): + def test_decode_audio_sample_count(self) -> None: container = av.open(fate_suite("audio-reference/chorusnoise_2ch_44kHz_s16.wav")) audio_stream = next(s for s in container.streams if s.type == "audio") - self.assertIs(audio_stream, container.streams.audio[0]) + assert audio_stream is container.streams.audio[0] + assert isinstance(audio_stream, av.audio.AudioStream) sample_count = 0 - for packet in container.demux(audio_stream): - for frame in packet.decode(): - sample_count += frame.samples + for frame in container.decode(audio_stream): + sample_count += frame.samples + assert audio_stream.duration is not None + assert audio_stream.time_base is not None total_samples = ( audio_stream.duration * audio_stream.sample_rate.numerator ) / audio_stream.time_base.denominator - self.assertEqual(sample_count, total_samples) + assert sample_count == total_samples def test_decoded_time_base(self): container = av.open(fate_suite("h264/interlaced_crop.mp4")) stream = container.streams.video[0] - self.assertEqual(stream.time_base, Fraction(1, 25)) + assert stream.time_base == Fraction(1, 25) for packet in container.demux(stream): for frame in packet.decode(): - self.assertEqual(packet.time_base, frame.time_base) - self.assertEqual(stream.time_base, frame.time_base) + assert packet.time_base == frame.time_base + assert stream.time_base == frame.time_base return def test_decoded_motion_vectors(self): @@ -109,8 +109,8 @@ def test_decode_video_corrupt(self): frame_count += 1 packet_count += 1 - self.assertEqual(packet_count, 1) - self.assertEqual(frame_count, 0) + assert packet_count == 1 + assert frame_count == 0 def test_decode_close_then_use(self): container = av.open(fate_suite("h264/interlaced_crop.mp4")) @@ -155,4 +155,4 @@ def test_flush_decoded_video_frame_count(self): self.assertIsNone(frame.time_base) output_count += 1 - self.assertEqual(output_count, input_count) + assert output_count == input_count diff --git a/tests/test_encode.py b/tests/test_encode.py index 79c8423bf..40b17cd73 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -232,13 +232,13 @@ class TestEncodeStreamSemantics(TestCase): def test_stream_index(self) -> None: with av.open(self.sandboxed("output.mov"), "w") as output: vstream = output.add_stream("mpeg4", 24) - self.assertIn(vstream, output.streams.video) + assert vstream in output.streams.video vstream.pix_fmt = "yuv420p" vstream.width = 320 vstream.height = 240 astream = output.add_stream("mp2", 48000) - self.assertIn(astream, output.streams.audio) + assert astream in output.streams.audio astream.layout = "stereo" # type: ignore astream.format = "s16" # type: ignore @@ -248,7 +248,7 @@ def test_stream_index(self) -> None: vframe = VideoFrame(320, 240, "yuv420p") vpacket = vstream.encode(vframe)[0] - self.assertIs(vpacket.stream, vstream) + assert vpacket.stream is vstream assert vpacket.stream_index == 0 for i in range(10): @@ -264,7 +264,7 @@ def test_stream_index(self) -> None: apacket = apackets[0] break - self.assertIs(apacket.stream, astream) + assert apacket.stream is astream assert apacket.stream_index == 1 def test_stream_audio_resample(self) -> None: diff --git a/tests/test_enums.py b/tests/test_enums.py index 8e4839a77..30eb125f1 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -31,9 +31,9 @@ def test_access(self): foo2 = cls["FOO"] foo3 = cls[1] foo4 = cls[foo1] - self.assertIs(foo1, foo2) - self.assertIs(foo1, foo3) - self.assertIs(foo1, foo4) + assert foo1 is foo2 + assert foo1 is foo3 + assert foo1 is foo4 self.assertIn(foo1, cls) self.assertIn("FOO", cls) @@ -44,7 +44,7 @@ def test_access(self): self.assertRaises(TypeError, lambda: cls[()]) self.assertEqual(cls.get("FOO"), foo1) - self.assertIs(cls.get("not a foo"), None) + assert cls.get("not a foo") is None def test_casting(self): cls = self.define_foobar() @@ -86,8 +86,8 @@ def test_as_key(self): d = {foo: "value"} self.assertEqual(d[foo], "value") - self.assertIs(d.get("FOO"), None) - self.assertIs(d.get(1), None) + assert d.get("FOO") is None + assert d.get(1) is None def test_pickleable(self): cls = PickleableFooBar @@ -97,7 +97,7 @@ def test_pickleable(self): foo2 = pickle.loads(enc) - self.assertIs(foo, foo2) + assert foo is foo2 def test_create_unknown(self): cls = self.define_foobar() @@ -118,7 +118,7 @@ def test_multiple_names(self): ), ) - self.assertIs(cls.F, cls.FOO) + assert cls.F is cls.FOO self.assertEqual(cls.F.name, "FOO") self.assertNotEqual(cls.F.name, "F") # This is actually the string. @@ -140,24 +140,24 @@ def test_flag_basics(self): bar = cls.BAR foobar = foo | bar - self.assertIs(foobar, cls.FOOBAR) + assert foobar is cls.FOOBAR foo2 = foobar & foo - self.assertIs(foo2, foo) + assert foo2 is foo bar2 = foobar ^ foo - self.assertIs(bar2, bar) + assert bar2 is bar bar3 = foobar & ~foo - self.assertIs(bar3, bar) + assert bar3 is bar x = cls.FOO x |= cls.BAR - self.assertIs(x, cls.FOOBAR) + assert x is cls.FOOBAR x = cls.FOOBAR x &= cls.FOO - self.assertIs(x, cls.FOO) + assert x is cls.FOO def test_multi_flags_basics(self): cls = self.define_foobar(is_flags=True) @@ -173,9 +173,9 @@ def test_multi_flags_basics(self): foobar3 = cls[3] foobar4 = cls[foobar] - self.assertIs(foobar, foobar2) - self.assertIs(foobar, foobar3) - self.assertIs(foobar, foobar4) + assert foobar is foobar2 + assert foobar is foobar3 + assert foobar is foobar4 self.assertRaises(KeyError, lambda: cls["FOO|BAR"]) @@ -186,7 +186,7 @@ def test_multi_flags_create_missing(self): cls = self.define_foobar(is_flags=True) foobar = cls[3] - self.assertIs(foobar, cls.FOO | cls.BAR) + assert foobar is cls.FOO | cls.BAR self.assertRaises(KeyError, lambda: cls[4]) # Not FOO or BAR self.assertRaises(KeyError, lambda: cls[7]) # FOO and BAR and missing flag. @@ -212,16 +212,16 @@ def flags(self, value): obj = Class("FOO") - self.assertIs(obj.flags, Flags.FOO) + assert obj.flags is Flags.FOO self.assertTrue(obj.foo) self.assertFalse(obj.bar) obj.bar = True - self.assertIs(obj.flags, foobar) + assert obj.flags is foobar self.assertTrue(obj.foo) self.assertTrue(obj.bar) obj.foo = False - self.assertIs(obj.flags, Flags.BAR) + assert obj.flags is Flags.BAR self.assertFalse(obj.foo) self.assertTrue(obj.bar) diff --git a/tests/test_seek.py b/tests/test_seek.py index ee6f84536..1ba71cc34 100644 --- a/tests/test_seek.py +++ b/tests/test_seek.py @@ -13,13 +13,6 @@ def timestamp_to_frame(timestamp, stream): return frame -def step_forward(container, stream): - for packet in container.demux(stream): - for frame in packet.decode(): - if frame: - return frame - - class TestSeek(TestCase): def test_seek_float(self): container = av.open(fate_suite("h264/interlaced_crop.mp4")) diff --git a/tests/test_streams.py b/tests/test_streams.py index c146abfb1..64bb63c36 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -1,18 +1,18 @@ import av -from .common import TestCase, fate_suite +from .common import fate_suite -class TestStreams(TestCase): - def test_stream_tuples(self): +class TestStreams: + def test_stream_tuples(self) -> None: for fate_name in ("h264/interlaced_crop.mp4",): container = av.open(fate_suite(fate_name)) video_streams = tuple([s for s in container.streams if s.type == "video"]) - self.assertEqual(video_streams, container.streams.video) + assert video_streams == container.streams.video audio_streams = tuple([s for s in container.streams if s.type == "audio"]) - self.assertEqual(audio_streams, container.streams.audio) + assert audio_streams == container.streams.audio def test_selection(self) -> None: container = av.open( @@ -21,29 +21,29 @@ def test_selection(self) -> None: video = container.streams.video[0] audio = container.streams.audio[0] - self.assertEqual([video], container.streams.get(video=0)) - self.assertEqual([video], container.streams.get(video=(0,))) + assert [video] == container.streams.get(video=0) + assert [video] == container.streams.get(video=(0,)) - self.assertEqual(video, container.streams.best("video")) - self.assertEqual(audio, container.streams.best("audio")) + assert video == container.streams.best("video") + assert audio == container.streams.best("audio") container = av.open(fate_suite("sub/MovText_capability_tester.mp4")) subtitle = container.streams.subtitles[0] - self.assertEqual(subtitle, container.streams.best("subtitle")) + assert subtitle == container.streams.best("subtitle") container = av.open(fate_suite("mxf/track_01_v02.mxf")) data = container.streams.data[0] - self.assertEqual(data, container.streams.best("data")) + assert data == container.streams.best("data") - def test_noside_data(self): + def test_noside_data(self) -> None: container = av.open(fate_suite("h264/interlaced_crop.mp4")) video = container.streams.video[0] - self.assertEqual(video.nb_side_data, 0) + assert video.nb_side_data == 0 - def test_side_data(self): + def test_side_data(self) -> None: container = av.open(fate_suite("mov/displaymatrix.mov")) video = container.streams.video[0] - self.assertEqual(video.nb_side_data, 1) - self.assertEqual(video.side_data["DISPLAYMATRIX"], -90.0) + assert video.nb_side_data == 1 + assert video.side_data["DISPLAYMATRIX"] == -90.0 diff --git a/tests/test_videoformat.py b/tests/test_videoformat.py index 61b9ca0fc..4670dc217 100644 --- a/tests/test_videoformat.py +++ b/tests/test_videoformat.py @@ -7,47 +7,47 @@ class TestVideoFormats(TestCase): def test_invalid_pixel_format(self): with self.assertRaises(ValueError) as cm: VideoFormat("__unknown_pix_fmt", 640, 480) - self.assertEqual(str(cm.exception), "not a pixel format: '__unknown_pix_fmt'") + assert str(cm.exception) == "not a pixel format: '__unknown_pix_fmt'" def test_rgb24_inspection(self): fmt = VideoFormat("rgb24", 640, 480) - self.assertEqual(fmt.name, "rgb24") - self.assertEqual(len(fmt.components), 3) + assert fmt.name == "rgb24" + assert len(fmt.components) == 3 self.assertFalse(fmt.is_planar) self.assertFalse(fmt.has_palette) self.assertTrue(fmt.is_rgb) - self.assertEqual(fmt.chroma_width(), 640) - self.assertEqual(fmt.chroma_height(), 480) - self.assertEqual(fmt.chroma_width(1024), 1024) - self.assertEqual(fmt.chroma_height(1024), 1024) + assert fmt.chroma_width() == 640 + assert fmt.chroma_height() == 480 + assert fmt.chroma_width(1024) == 1024 + assert fmt.chroma_height(1024) == 1024 for i in range(3): comp = fmt.components[i] - self.assertEqual(comp.plane, 0) - self.assertEqual(comp.bits, 8) + assert comp.plane == 0 + assert comp.bits == 8 self.assertFalse(comp.is_luma) self.assertFalse(comp.is_chroma) self.assertFalse(comp.is_alpha) - self.assertEqual(comp.width, 640) - self.assertEqual(comp.height, 480) + assert comp.width == 640 + assert comp.height == 480 def test_yuv420p_inspection(self): fmt = VideoFormat("yuv420p", 640, 480) - self.assertEqual(fmt.name, "yuv420p") - self.assertEqual(len(fmt.components), 3) + assert fmt.name == "yuv420p" + assert len(fmt.components) == 3 self._test_yuv420(fmt) def _test_yuv420(self, fmt): self.assertTrue(fmt.is_planar) self.assertFalse(fmt.has_palette) self.assertFalse(fmt.is_rgb) - self.assertEqual(fmt.chroma_width(), 320) - self.assertEqual(fmt.chroma_height(), 240) - self.assertEqual(fmt.chroma_width(1024), 512) - self.assertEqual(fmt.chroma_height(1024), 512) + assert fmt.chroma_width() == 320 + assert fmt.chroma_height() == 240 + assert fmt.chroma_width(1024) == 512 + assert fmt.chroma_height(1024) == 512 for i, comp in enumerate(fmt.components): comp = fmt.components[i] - self.assertEqual(comp.plane, i) - self.assertEqual(comp.bits, 8) + assert comp.plane == i + assert comp.bits == 8 self.assertFalse(fmt.components[0].is_chroma) self.assertTrue(fmt.components[1].is_chroma) self.assertTrue(fmt.components[2].is_chroma) @@ -57,38 +57,38 @@ def _test_yuv420(self, fmt): self.assertFalse(fmt.components[0].is_alpha) self.assertFalse(fmt.components[1].is_alpha) self.assertFalse(fmt.components[2].is_alpha) - self.assertEqual(fmt.components[0].width, 640) - self.assertEqual(fmt.components[1].width, 320) - self.assertEqual(fmt.components[2].width, 320) + assert fmt.components[0].width == 640 + assert fmt.components[1].width == 320 + assert fmt.components[2].width == 320 def test_yuva420p_inspection(self): fmt = VideoFormat("yuva420p", 640, 480) - self.assertEqual(len(fmt.components), 4) + assert len(fmt.components) == 4 self._test_yuv420(fmt) self.assertFalse(fmt.components[3].is_chroma) - self.assertEqual(fmt.components[3].width, 640) + assert fmt.components[3].width == 640 def test_gray16be_inspection(self): fmt = VideoFormat("gray16be", 640, 480) - self.assertEqual(fmt.name, "gray16be") - self.assertEqual(len(fmt.components), 1) + assert fmt.name == "gray16be" + assert len(fmt.components) == 1 self.assertFalse(fmt.is_planar) self.assertFalse(fmt.has_palette) self.assertFalse(fmt.is_rgb) - self.assertEqual(fmt.chroma_width(), 640) - self.assertEqual(fmt.chroma_height(), 480) - self.assertEqual(fmt.chroma_width(1024), 1024) - self.assertEqual(fmt.chroma_height(1024), 1024) + assert fmt.chroma_width() == 640 + assert fmt.chroma_height() == 480 + assert fmt.chroma_width(1024) == 1024 + assert fmt.chroma_height(1024) == 1024 comp = fmt.components[0] - self.assertEqual(comp.plane, 0) - self.assertEqual(comp.bits, 16) + assert comp.plane == 0 + assert comp.bits == 16 self.assertTrue(comp.is_luma) self.assertFalse(comp.is_chroma) - self.assertEqual(comp.width, 640) - self.assertEqual(comp.height, 480) + assert comp.width == 640 + assert comp.height == 480 self.assertFalse(comp.is_alpha) def test_pal8_inspection(self): fmt = VideoFormat("pal8", 640, 480) - self.assertEqual(len(fmt.components), 1) + assert len(fmt.components) == 1 self.assertTrue(fmt.has_palette) From b824d26857de58529b7f3308565ad2b01bb1c979 Mon Sep 17 00:00:00 2001 From: Carlos Ruiz Date: Thu, 19 Sep 2024 15:43:24 +0200 Subject: [PATCH 362/846] feat: expose opaque and opaque_ref Allows tagging AVPackets and AVFrames with any Python object and, when AV_CODEC_FLAG_COPY_OPAQUE is enabled, ffmpeg will take care of propagating the references to those objects from packets -> frames (decoders) and frames -> packets (encoders). --- av/codec/context.pyx | 11 +++++++++++ av/frame.pyx | 14 ++++++++++++++ av/opaque.pxd | 12 ++++++++++++ av/opaque.pyx | 32 ++++++++++++++++++++++++++++++++ av/packet.pyx | 16 +++++++++++++++- include/libav.pxd | 1 + include/libavcodec/avcodec.pxd | 8 ++++++++ include/libavutil/buffer.pxd | 9 +++++++++ 8 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 av/opaque.pxd create mode 100644 av/opaque.pyx create mode 100644 include/libavutil/buffer.pxd diff --git a/av/codec/context.pyx b/av/codec/context.pyx index e2e3632ee..bfaa9544d 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -69,6 +69,14 @@ Flags = define_enum("Flags", __name__, ( ("DROPCHANGED", 1 << 5, "Don't output frames whose parameters differ from first decoded frame in stream." ), + ("RECON_FRAME", lib.AV_CODEC_FLAG_RECON_FRAME, "Request the encoder to output reconstructed frames, i.e. frames that would be produced by decoding the encoded bistream."), + ("COPY_OPAQUE", lib.AV_CODEC_FLAG_COPY_OPAQUE, + """Request the decoder to propagate each packet's AVPacket.opaque and AVPacket.opaque_ref + to its corresponding output AVFrame. Request the encoder to propagate each frame's + AVFrame.opaque and AVFrame.opaque_ref values to its corresponding output AVPacket."""), + ("FRAME_DURATION", lib.AV_CODEC_FLAG_FRAME_DURATION, + """Signal to the encoder that the values of AVFrame.duration are valid and should be + used (typically for transferring them to output packets)."""), ("PASS1", lib.AV_CODEC_FLAG_PASS1, "Use internal 2pass ratecontrol in first pass mode."), ("PASS2", lib.AV_CODEC_FLAG_PASS2, "Use internal 2pass ratecontrol in second pass mode."), ("LOOP_FILTER", lib.AV_CODEC_FLAG_LOOP_FILTER, "loop filter."), @@ -150,6 +158,9 @@ cdef class CodecContext: output_corrupt = flags.flag_property("OUTPUT_CORRUPT") qpel = flags.flag_property("QPEL") drop_changed = flags.flag_property("DROPCHANGED") + recon_frame = flags.flag_property("RECON_FRAME") + copy_opaque = flags.flag_property("COPY_OPAQUE") + frame_duration = flags.flag_property("FRAME_DURATION") pass1 = flags.flag_property("PASS1") pass2 = flags.flag_property("PASS2") loop_filter = flags.flag_property("LOOP_FILTER") diff --git a/av/frame.pyx b/av/frame.pyx index b3ffff99b..a8871fd8f 100644 --- a/av/frame.pyx +++ b/av/frame.pyx @@ -1,4 +1,5 @@ from av.error cimport err_check +from av.opaque cimport opaque_container from av.utils cimport avrational_to_fraction, to_avrational from av.sidedata.sidedata import SideDataContainer @@ -146,3 +147,16 @@ cdef class Frame: ret = lib.av_frame_make_writable(self.ptr) err_check(ret) + + @property + def opaque(self): + if self.ptr.opaque_ref is not NULL: + return opaque_container.get( self.ptr.opaque_ref.data) + + @opaque.setter + def opaque(self, v): + lib.av_buffer_unref(&self.ptr.opaque_ref) + + if v is None: + return + self.ptr.opaque_ref = opaque_container.add(v) diff --git a/av/opaque.pxd b/av/opaque.pxd new file mode 100644 index 000000000..f5c38d7fa --- /dev/null +++ b/av/opaque.pxd @@ -0,0 +1,12 @@ +cimport libav as lib + + +cdef class OpaqueContainer: + cdef dict _by_name + + cdef lib.AVBufferRef *add(self, object v) + cdef object get(self, bytes name) + cdef object pop(self, bytes name) + + +cdef OpaqueContainer opaque_container diff --git a/av/opaque.pyx b/av/opaque.pyx new file mode 100644 index 000000000..1e6769898 --- /dev/null +++ b/av/opaque.pyx @@ -0,0 +1,32 @@ +cimport libav as lib +from libc.stdint cimport uint8_t + +from uuid import uuid4 + + +cdef void key_free(void *opaque, uint8_t *data) noexcept nogil: + cdef char *name = data + with gil: + opaque_container.pop(name) + + +cdef class OpaqueContainer: + """A container that holds references to Python objects, indexed by uuid""" + + def __cinit__(self): + self._by_name = {} + + cdef lib.AVBufferRef *add(self, v): + cdef bytes uuid = str(uuid4()).encode("utf-8") + cdef lib.AVBufferRef *ref = lib.av_buffer_create(uuid, len(uuid), &key_free, NULL, 0) + self._by_name[uuid] = v + return ref + + cdef object get(self, bytes name): + return self._by_name.get(name) + + cdef object pop(self, bytes name): + return self._by_name.pop(name) + + +cdef opaque_container = OpaqueContainer() diff --git a/av/packet.pyx b/av/packet.pyx index 63b0b50ab..b5c9251eb 100644 --- a/av/packet.pyx +++ b/av/packet.pyx @@ -2,6 +2,7 @@ cimport libav as lib from av.bytesource cimport bytesource from av.error cimport err_check +from av.opaque cimport opaque_container from av.utils cimport avrational_to_fraction, to_avrational @@ -207,7 +208,7 @@ cdef class Packet(Buffer): self.ptr.flags &= ~(lib.AV_PKT_FLAG_CORRUPT) @property - def is_discard(self): + def is_discard(self): return bool(self.ptr.flags & lib.AV_PKT_FLAG_DISCARD) @property @@ -218,3 +219,16 @@ cdef class Packet(Buffer): def is_disposable(self): return bool(self.ptr.flags & lib.AV_PKT_FLAG_DISPOSABLE) + @property + def opaque(self): + if self.ptr.opaque_ref is not NULL: + return opaque_container.get( self.ptr.opaque_ref.data) + + @opaque.setter + def opaque(self, v): + lib.av_buffer_unref(&self.ptr.opaque_ref) + + if v is None: + return + self.ptr.opaque_ref = opaque_container.add(v) + diff --git a/include/libav.pxd b/include/libav.pxd index 4312be2e8..c793b9988 100644 --- a/include/libav.pxd +++ b/include/libav.pxd @@ -1,4 +1,5 @@ include "libavutil/avutil.pxd" +include "libavutil/buffer.pxd" include "libavutil/channel_layout.pxd" include "libavutil/dict.pxd" include "libavutil/error.pxd" diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 9add5ae2d..805bf50cb 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -99,6 +99,9 @@ cdef extern from "libavcodec/avcodec.h" nogil: AV_CODEC_FLAG_OUTPUT_CORRUPT AV_CODEC_FLAG_QPEL AV_CODEC_FLAG_DROPCHANGED + AV_CODEC_FLAG_RECON_FRAME + AV_CODEC_FLAG_COPY_OPAQUE + AV_CODEC_FLAG_FRAME_DURATION AV_CODEC_FLAG_PASS1 AV_CODEC_FLAG_PASS2 AV_CODEC_FLAG_LOOP_FILTER @@ -392,6 +395,7 @@ cdef extern from "libavcodec/avcodec.h" nogil: uint8_t **base void *opaque + AVBufferRef *opaque_ref AVDictionary *metadata int flags int decode_error_flags @@ -416,6 +420,10 @@ cdef extern from "libavcodec/avcodec.h" nogil: int64_t pos + void *opaque + AVBufferRef *opaque_ref + + cdef int avcodec_fill_audio_frame( AVFrame *frame, int nb_channels, diff --git a/include/libavutil/buffer.pxd b/include/libavutil/buffer.pxd new file mode 100644 index 000000000..daf86105b --- /dev/null +++ b/include/libavutil/buffer.pxd @@ -0,0 +1,9 @@ +from libc.stdint cimport uint8_t + +cdef extern from "libavutil/buffer.h" nogil: + + AVBufferRef *av_buffer_create(uint8_t *data, size_t size, void (*free)(void *opaque, uint8_t *data), void *opaque, int flags) + void av_buffer_unref(AVBufferRef **buf) + + cdef struct AVBufferRef: + uint8_t *data From 170577208a5f73ef2ee5987b697ff9776b71e4a3 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 24 Sep 2024 01:21:19 -0400 Subject: [PATCH 363/846] Test opaque --- av/codec/context.pyi | 23 +++++++++++++++++++++++ av/frame.pyi | 1 + av/packet.pyi | 1 + tests/test_videoframe.py | 18 ++++++++++++++++-- 4 files changed, 41 insertions(+), 2 deletions(-) diff --git a/av/codec/context.pyi b/av/codec/context.pyi index f193487a9..805b06faf 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -73,6 +73,29 @@ class CodecContext: thread_type: Any skip_frame: Any + # flgas + unaligned: bool + qscale: bool + four_mv: bool + output_corrupt: bool + qpel: bool + drop_changed: bool + recon_frame: bool + copy_opaque: bool + frame_duration: bool + pass1: bool + pass2: bool + loop_filter: bool + gray: bool + psnr: bool + interlaced_dct: bool + low_delay: bool + global_header: bool + bitexact: bool + ac_pred: bool + interlaced_me: bool + closed_gop: bool + def open(self, strict: bool = True) -> None: ... def close(self, strict: bool = True) -> None: ... @staticmethod diff --git a/av/frame.pyi b/av/frame.pyi index 47ed9f0cc..6e5348119 100644 --- a/av/frame.pyi +++ b/av/frame.pyi @@ -7,5 +7,6 @@ class Frame: time_base: Fraction is_corrupt: bool side_data: dict[str, str] + opaque: object def make_writable(self) -> None: ... diff --git a/av/packet.pyi b/av/packet.pyi index f122c2886..9bdbb8c62 100644 --- a/av/packet.pyi +++ b/av/packet.pyi @@ -14,6 +14,7 @@ class Packet(Buffer): pos: int | None size: int duration: int | None + opaque: object is_keyframe: bool is_corrupt: bool is_discard: bool diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index a7b753de7..b0ea0bcc5 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -1,11 +1,25 @@ +import time from fractions import Fraction from unittest import SkipTest import numpy +import av from av import VideoFrame -from .common import TestCase, fate_png, has_pillow +from .common import TestCase, fate_png, fate_suite, has_pillow + + +class TestOpaque: + def test_opaque(self) -> None: + with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: + video_stream = container.streams.video[0] + video_stream.codec_context.copy_opaque = True + for packet_idx, packet in enumerate(container.demux()): + packet.opaque = (time.time(), packet_idx) + for frame in packet.decode(): + assert isinstance(frame, av.frame.Frame) + assert type(frame.opaque) is tuple and len(frame.opaque) == 2 class TestVideoFrameConstructors(TestCase): @@ -33,7 +47,7 @@ def test_manual_rgb_constructor(self): assert frame.format.name == "rgb24" -class TestVideoFramePlanes(TestCase): +class TestVideoFramePlanes: def test_null_planes(self): frame = VideoFrame() # yuv420p assert len(frame.planes) == 0 From 3c0f4966f719c2b5fe9d52b9605b3b133560df22 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 24 Sep 2024 01:38:14 -0400 Subject: [PATCH 364/846] Continue test refactor --- Makefile | 2 +- av/__init__.py | 33 ++++++++- av/container/core.pyi | 4 +- tests/common.py | 2 + tests/test_codec_context.py | 70 ++++++++++++++----- tests/test_decode.py | 16 ++--- tests/test_enums.py | 24 +++---- tests/test_errors.py | 48 ++++++------- tests/test_file_probing.py | 135 ++++++++++++++---------------------- tests/test_seek.py | 97 ++++++++++++-------------- tests/test_timeout.py | 32 ++++----- tests/test_videoframe.py | 14 ++-- 12 files changed, 251 insertions(+), 226 deletions(-) diff --git a/Makefile b/Makefile index 2dc617edf..43e589717 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,7 @@ fate-suite: lint: $(PIP) install -U black isort flake8 flake8-pyproject pillow numpy mypy==1.11.2 black --check av examples tests setup.py - flake8 av examples tests + flake8 av isort --check-only --diff av examples tests mypy av tests diff --git a/av/__init__.py b/av/__init__.py index 68b9fed09..266aec3e1 100644 --- a/av/__init__.py +++ b/av/__init__.py @@ -15,20 +15,22 @@ + os.environ["PATH"] ) -# MUST import the core before anything else in order to initalize the underlying +# MUST import the core before anything else in order to initialize the underlying # library that is being wrapped. from av._core import time_base, library_versions # Capture logging (by importing it). from av import logging -# For convenience, IMPORT ALL OF THE THINGS (that are constructable by the user). +# For convenience, import all common attributes. from av.about import __version__ +from av.audio.codeccontext import AudioCodecContext from av.audio.fifo import AudioFifo from av.audio.format import AudioFormat from av.audio.frame import AudioFrame from av.audio.layout import AudioLayout from av.audio.resampler import AudioResampler +from av.audio.stream import AudioStream from av.bitstream import BitStreamFilterContext, bitstream_filters_available from av.codec.codec import Codec, codecs_available from av.codec.context import CodecContext @@ -36,12 +38,39 @@ from av.format import ContainerFormat, formats_available from av.packet import Packet from av.error import * # noqa: F403; This is limited to exception types. +from av.video.codeccontext import VideoCodecContext from av.video.format import VideoFormat from av.video.frame import VideoFrame +from av.video.stream import VideoStream # Backwards compatibility AVError = FFmpegError # noqa: F405 +__all__ = ( + "time_base", + "library_versions", + "AudioCodecContext", + "AudioFifo", + "AudioFormat", + "AudioFrame", + "AudioLayout", + "AudioResampler", + "AudioStream", + "BitStreamFilterContext", + "bitstream_filters_available", + "Codec", + "codecs_available", + "CodecContext", + "open", + "ContainerFormat", + "formats_available", + "Packet", + "VideoCodecContext", + "VideoFormat", + "VideoFrame", + "VideoStream", +) + def get_include() -> str: """ diff --git a/av/container/core.pyi b/av/container/core.pyi index 862f0d326..c1d43ca4c 100644 --- a/av/container/core.pyi +++ b/av/container/core.pyi @@ -1,4 +1,4 @@ -from numbers import Real +from fractions import Fraction from pathlib import Path from types import TracebackType from typing import Any, Callable, Literal, Type, overload @@ -10,6 +10,8 @@ from .input import InputContainer from .output import OutputContainer from .streams import StreamContainer +Real = int | float | Fraction + class Flags(EnumFlag): GENPTS: int IGNIDX: int diff --git a/tests/common.py b/tests/common.py index 8c0db1129..ece6ac5e1 100644 --- a/tests/common.py +++ b/tests/common.py @@ -14,6 +14,8 @@ except ImportError: has_pillow = False +__all__ = ("fate_suite",) + is_windows = os.name == "nt" skip_tests = frozenset(os.environ.get("PYAV_SKIP_TESTS", "").split(",")) diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index cb89b74d0..2b28730a4 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -2,17 +2,48 @@ import os from fractions import Fraction +from typing import Iterator, TypedDict, overload from unittest import SkipTest import av -from av import AudioLayout, AudioResampler, Codec, Packet +from av import ( + AudioCodecContext, + AudioFrame, + AudioLayout, + AudioResampler, + Codec, + Packet, + VideoCodecContext, + VideoFrame, +) from av.codec.codec import UnknownCodecError from av.video.frame import PictureType from .common import TestCase, fate_suite -def iter_raw_frames(path, packet_sizes, ctx): +class Options(TypedDict, total=False): + b: str + crf: str + pix_fmt: str + width: int + height: int + max_frames: int + time_base: Fraction + gop_size: int + + +@overload +def iter_raw_frames( + path: str, packet_sizes: list, ctx: VideoCodecContext +) -> Iterator[VideoFrame]: ... +@overload +def iter_raw_frames( + path: str, packet_sizes: list, ctx: AudioCodecContext +) -> Iterator[AudioFrame]: ... +def iter_raw_frames( + path: str, packet_sizes: list, ctx: VideoCodecContext | AudioCodecContext +) -> Iterator[VideoFrame | AudioFrame]: with open(path, "rb") as f: for i, size in enumerate(packet_sizes): packet = Packet(size) @@ -244,24 +275,24 @@ def image_sequence_encode(self, codec_name: str) -> None: assert frame.height == height assert frame.format.name == pix_fmt - def test_encoding_h264(self): + def test_encoding_h264(self) -> None: self.video_encoding("h264", {"crf": "19"}) - def test_encoding_mpeg4(self): + def test_encoding_mpeg4(self) -> None: self.video_encoding("mpeg4") - def test_encoding_xvid(self): + def test_encoding_xvid(self) -> None: self.video_encoding("mpeg4", codec_tag="xvid") - def test_encoding_mpeg1video(self): + def test_encoding_mpeg1video(self) -> None: self.video_encoding("mpeg1video") - def test_encoding_dvvideo(self): - options = {"pix_fmt": "yuv411p", "width": 720, "height": 480} + def test_encoding_dvvideo(self) -> None: + options: Options = {"pix_fmt": "yuv411p", "width": 720, "height": 480} self.video_encoding("dvvideo", options) - def test_encoding_dnxhd(self): - options = { + def test_encoding_dnxhd(self) -> None: + options: Options = { "b": "90M", # bitrate "pix_fmt": "yuv422p", "width": 1920, @@ -271,7 +302,12 @@ def test_encoding_dnxhd(self): } self.video_encoding("dnxhd", options) - def video_encoding(self, codec_name, options={}, codec_tag=None): + def video_encoding( + self, + codec_name: str, + options: Options = {}, + codec_tag: str | None = None, + ) -> None: try: codec = Codec(codec_name, "w") except UnknownCodecError: @@ -280,6 +316,8 @@ def video_encoding(self, codec_name, options={}, codec_tag=None): container = av.open(fate_suite("h264/interlaced_crop.mp4")) video_stream = container.streams.video[0] + assert video_stream.time_base is not None + pix_fmt = options.pop("pix_fmt", "yuv420p") width = options.pop("width", 640) height = options.pop("height", 480) @@ -287,14 +325,14 @@ def video_encoding(self, codec_name, options={}, codec_tag=None): time_base = options.pop("time_base", video_stream.time_base) gop_size = options.pop("gop_size", 20) - ctx = codec.create() + ctx = codec.create("video") ctx.width = width ctx.height = height ctx.time_base = time_base ctx.framerate = 1 / ctx.time_base ctx.pix_fmt = pix_fmt ctx.gop_size = gop_size - ctx.options = options # TODO + ctx.options = options # type: ignore if codec_tag: ctx.codec_tag = codec_tag ctx.open() @@ -326,7 +364,7 @@ def video_encoding(self, codec_name, options={}, codec_tag=None): if codec_name == "libx264": dec_codec_name = "h264" - ctx = av.Codec(dec_codec_name, "r").create() + ctx = av.Codec(dec_codec_name, "r").create("video") ctx.open() keyframe_indices = [] @@ -341,7 +379,7 @@ def video_encoding(self, codec_name, options={}, codec_tag=None): assert frame_count == decoded_frame_count - self.assertIsInstance( + assert isinstance( all(keyframe_index for keyframe_index in keyframe_indices), int ) decoded_gop_sizes = [ @@ -350,7 +388,7 @@ def video_encoding(self, codec_name, options={}, codec_tag=None): if codec_name in ("dvvideo", "dnxhd") and all( i == 1 for i in decoded_gop_sizes ): - raise SkipTest() + raise SkipTest("I'm not sure why we skip this actually.") for i in decoded_gop_sizes: assert i == gop_size diff --git a/tests/test_decode.py b/tests/test_decode.py index 82e8461e2..aafea3b22 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -51,8 +51,10 @@ def test_decode_audio_sample_count(self) -> None: assert audio_stream.duration is not None assert audio_stream.time_base is not None total_samples = ( - audio_stream.duration * audio_stream.sample_rate.numerator - ) / audio_stream.time_base.denominator + audio_stream.duration + * audio_stream.sample_rate.numerator + / audio_stream.time_base.denominator + ) assert sample_count == total_samples def test_decoded_time_base(self): @@ -125,11 +127,9 @@ def test_decode_close_then_use(self): except AssertionError: pass - def test_flush_decoded_video_frame_count(self): + def test_flush_decoded_video_frame_count(self) -> None: container = av.open(fate_suite("h264/interlaced_crop.mp4")) - video_stream = next(s for s in container.streams if s.type == "video") - - self.assertIs(video_stream, container.streams.video[0]) + video_stream = container.streams.video[0] # Decode the first GOP, which requires a flush to get all frames have_keyframe = False @@ -148,11 +148,11 @@ def test_flush_decoded_video_frame_count(self): output_count += 1 # Check the test works as expected and requires a flush - self.assertLess(output_count, input_count) + assert output_count < input_count for frame in video_stream.decode(None): # The Frame._time_base is not set by PyAV - self.assertIsNone(frame.time_base) + assert frame.time_base is None output_count += 1 assert output_count == input_count diff --git a/tests/test_enums.py b/tests/test_enums.py index 30eb125f1..e96956eca 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -15,15 +15,13 @@ def define_foobar(self, **kwargs): def test_basics(self): cls = self.define_foobar() - self.assertIsInstance(cls, EnumType) + assert isinstance(cls, EnumType) foo = cls.FOO - self.assertIsInstance(foo, cls) - self.assertEqual(foo.name, "FOO") - self.assertEqual(foo.value, 1) - - self.assertNotIsInstance(foo, PickleableFooBar) + assert isinstance(foo, cls) + assert foo.name == "FOO" and foo.value == 1 + assert not isinstance(foo, PickleableFooBar) def test_access(self): cls = self.define_foobar() @@ -35,9 +33,9 @@ def test_access(self): assert foo1 is foo3 assert foo1 is foo4 - self.assertIn(foo1, cls) - self.assertIn("FOO", cls) - self.assertIn(1, cls) + assert foo1 in cls + assert "FOO" in cls + assert 1 in cls self.assertRaises(KeyError, lambda: cls["not a foo"]) self.assertRaises(KeyError, lambda: cls[10]) @@ -53,12 +51,12 @@ def test_casting(self): self.assertEqual(repr(foo), "") str_foo = str(foo) - self.assertIsInstance(str_foo, str) - self.assertEqual(str_foo, "FOO") + assert isinstance(str_foo, str) + assert str_foo == "FOO" int_foo = int(foo) - self.assertIsInstance(int_foo, int) - self.assertEqual(int_foo, 1) + assert isinstance(int_foo, int) + assert int_foo == 1 def test_iteration(self): cls = self.define_foobar() diff --git a/tests/test_errors.py b/tests/test_errors.py index c7dc1729e..fcd08845e 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -7,53 +7,47 @@ class TestErrorBasics(TestCase): - def test_stringify(self): + def test_stringify(self) -> None: for cls in (av.ValueError, av.FileNotFoundError, av.DecoderNotFoundError): e = cls(1, "foo") - self.assertEqual(str(e), "[Errno 1] foo") - self.assertEqual(repr(e), "{}(1, 'foo')".format(cls.__name__)) - self.assertEqual( - traceback.format_exception_only(cls, e)[-1], - "{}{}: [Errno 1] foo\n".format( - "av.error.", - cls.__name__, - ), + assert f"{e}" == "[Errno 1] foo" + assert f"{e!r}" == f"{cls.__name__}(1, 'foo')" + assert ( + traceback.format_exception_only(cls, e)[-1] + == f"av.error.{cls.__name__}: [Errno 1] foo\n" ) for cls in (av.ValueError, av.FileNotFoundError, av.DecoderNotFoundError): e = cls(1, "foo", "bar.txt") - self.assertEqual(str(e), "[Errno 1] foo: 'bar.txt'") - self.assertEqual(repr(e), "{}(1, 'foo', 'bar.txt')".format(cls.__name__)) - self.assertEqual( - traceback.format_exception_only(cls, e)[-1], - "{}{}: [Errno 1] foo: 'bar.txt'\n".format( - "av.error.", - cls.__name__, - ), + assert f"{e}" == "[Errno 1] foo: 'bar.txt'" + assert f"{e!r}" == f"{cls.__name__}(1, 'foo', 'bar.txt')" + assert ( + traceback.format_exception_only(cls, e)[-1] + == f"av.error.{cls.__name__}: [Errno 1] foo: 'bar.txt'\n" ) - def test_bases(self): - self.assertTrue(issubclass(av.ValueError, ValueError)) - self.assertTrue(issubclass(av.ValueError, av.FFmpegError)) + def test_bases(self) -> None: + assert issubclass(av.ValueError, ValueError) + assert issubclass(av.ValueError, av.FFmpegError) - self.assertTrue(issubclass(av.FileNotFoundError, FileNotFoundError)) - self.assertTrue(issubclass(av.FileNotFoundError, OSError)) - self.assertTrue(issubclass(av.FileNotFoundError, av.FFmpegError)) + assert issubclass(av.FileNotFoundError, FileNotFoundError) + assert issubclass(av.FileNotFoundError, OSError) + assert issubclass(av.FileNotFoundError, av.FFmpegError) def test_filenotfound(self): """Catch using builtin class on Python 3.3""" try: av.open("does not exist") except FileNotFoundError as e: - self.assertEqual(e.errno, errno.ENOENT) + assert e.errno == errno.ENOENT if is_windows: self.assertTrue( e.strerror in ["Error number -2 occurred", "No such file or directory"] ) else: - self.assertEqual(e.strerror, "No such file or directory") - self.assertEqual(e.filename, "does not exist") + assert e.strerror == "No such file or directory" + assert e.filename == "does not exist" else: self.fail("no exception raised") @@ -62,6 +56,6 @@ def test_buffertoosmall(self): try: av.error.err_check(-av.error.BUFFER_TOO_SMALL.value) except av.BufferTooSmallError as e: - self.assertEqual(e.errno, av.error.BUFFER_TOO_SMALL.value) + assert e.errno == av.error.BUFFER_TOO_SMALL.value else: self.fail("no exception raised") diff --git a/tests/test_file_probing.py b/tests/test_file_probing.py index aab232804..d67a69195 100644 --- a/tests/test_file_probing.py +++ b/tests/test_file_probing.py @@ -112,10 +112,10 @@ def test_stream_probing(self): class TestDataProbe(TestCase): - def setUp(self): + def setUp(self) -> None: self.file = av.open(fate_suite("mxf/track_01_v02.mxf")) - def test_container_probing(self): + def test_container_probing(self) -> None: assert self.file.bit_rate == 27872687 assert self.file.duration == 417083 assert str(self.file.format) == "" @@ -125,43 +125,28 @@ def test_container_probing(self): assert self.file.start_time == 0 assert len(self.file.streams) == 4 - for key, value, min_version in ( - ("application_platform", "AAFSDK (MacOS X)", None), - ("comment_Comments", "example comment", None), - ( - "comment_UNC Path", - "/Users/mark/Desktop/dnxhr_tracknames_export.aaf", - None, - ), - ("company_name", "Avid Technology, Inc.", None), - ("generation_uid", "b6bcfcab-70ff-7331-c592-233869de11d2", None), - ("material_package_name", "Example.new.04", None), + for key, value in ( + ("application_platform", "AAFSDK (MacOS X)"), + ("comment_Comments", "example comment"), + ("comment_UNC Path", "/Users/mark/Desktop/dnxhr_tracknames_export.aaf"), + ("company_name", "Avid Technology, Inc."), + ("generation_uid", "b6bcfcab-70ff-7331-c592-233869de11d2"), + ("material_package_name", "Example.new.04"), ( "material_package_umid", "0x060A2B340101010101010F001300000057E19D16BA8202DB060E2B347F7F2A80", - None, ), - ("modification_date", "2016-09-20T20:33:26.000000Z", None), - # Next one is FFmpeg >= 4.2. - ( - "operational_pattern_ul", - "060e2b34.04010102.0d010201.10030000", - {"libavformat": (58, 29)}, - ), - ("product_name", "Avid Media Composer 8.6.3.43955", None), - ("product_uid", "acfbf03a-4f42-a231-d0b7-c06ecd3d4ad7", None), - ("product_version", "Unknown version", None), - ("project_name", "UHD", None), - ("uid", "4482d537-4203-ea40-9e4e-08a22900dd39", None), + ("modification_date", "2016-09-20T20:33:26.000000Z"), + ("operational_pattern_ul", "060e2b34.04010102.0d010201.10030000"), + ("product_name", "Avid Media Composer 8.6.3.43955"), + ("product_uid", "acfbf03a-4f42-a231-d0b7-c06ecd3d4ad7"), + ("product_version", "Unknown version"), + ("project_name", "UHD"), + ("uid", "4482d537-4203-ea40-9e4e-08a22900dd39"), ): - if min_version and any( - av.library_versions[name] < version - for name, version in min_version.items() - ): - continue assert self.file.metadata.get(key) == value - def test_stream_probing(self): + def test_stream_probing(self) -> None: stream = self.file.streams[0] assert str(stream).startswith(" at ") @@ -171,53 +156,43 @@ def test_stream_probing(self): assert stream.id == 1 assert stream.index == 0 assert stream.language is None - self.assertEqual( - stream.metadata, - { - "data_type": "video", - "file_package_umid": "0x060A2B340101010101010F001300000057E19D16BA8302DB060E2B347F7F2A80", - "track_name": "Base", - }, - ) + assert stream.metadata == { + "data_type": "video", + "file_package_umid": "0x060A2B340101010101010F001300000057E19D16BA8302DB060E2B347F7F2A80", + "track_name": "Base", + } assert stream.profile is None assert stream.start_time == 0 assert stream.time_base == Fraction(1, 90000) assert stream.type == "data" - self.assertEqual(hasattr(stream, "codec"), False) + assert not hasattr(stream, "codec") class TestSubtitleProbe(TestCase): - def setUp(self): + def setUp(self) -> None: self.file = av.open(fate_suite("sub/MovText_capability_tester.mp4")) - def test_container_probing(self): + def test_container_probing(self) -> None: assert self.file.bit_rate == 810 assert self.file.duration == 8140000 - self.assertEqual( - str(self.file.format), "" - ) + assert str(self.file.format) == "" assert self.file.format.name == "mov,mp4,m4a,3gp,3g2,mj2" assert self.file.format.long_name == "QuickTime / MOV" - self.assertEqual( - self.file.metadata, - { - "compatible_brands": "isom", - "creation_time": "2012-07-04T05:10:41.000000Z", - "major_brand": "isom", - "minor_version": "1", - }, - ) + assert self.file.metadata == { + "compatible_brands": "isom", + "creation_time": "2012-07-04T05:10:41.000000Z", + "major_brand": "isom", + "minor_version": "1", + } assert self.file.size == 825 assert self.file.start_time is None assert len(self.file.streams) == 1 - def test_stream_probing(self): + def test_stream_probing(self) -> None: stream = self.file.streams[0] # check __repr__ - self.assertTrue( - str(stream).startswith(" None: self.file = av.open(fate_suite("mpeg2/mpeg2_field_encoding.ts")) - def test_container_probing(self): + def test_container_probing(self) -> None: assert self.file.bit_rate == 3950617 assert self.file.duration == 1620000 assert str(self.file.format) == "" @@ -258,12 +230,12 @@ def test_container_probing(self): assert self.file.start_time == 22953408322 assert len(self.file.streams) == 1 - def test_stream_probing(self): + def test_stream_probing(self) -> None: stream = self.file.streams[0] - # check __repr__ - self.assertTrue( - str(stream).startswith(" None: path = self.sandboxed("empty.h264") with open(path, "wb"): pass self.file = av.open(path) - def test_container_probing(self): + def test_container_probing(self) -> None: assert str(self.file.format) == "" assert self.file.format.name == "h264" assert self.file.format.long_name == "raw H.264 video" @@ -321,13 +289,13 @@ def test_container_probing(self): assert self.file.start_time is None assert self.file.metadata == {} - def test_stream_probing(self): + def test_stream_probing(self) -> None: stream = self.file.streams[0] - # ensure __repr__ does not crash assert str(stream).startswith(" float: fps = stream.average_rate time_base = stream.time_base start_time = stream.start_time - frame = (timestamp - start_time) * float(time_base) * float(fps) - return frame + assert time_base is not None and start_time is not None and fps is not None + return (timestamp - start_time) * float(time_base) * float(fps) class TestSeek(TestCase): - def test_seek_float(self): + def test_seek_float(self) -> None: container = av.open(fate_suite("h264/interlaced_crop.mp4")) self.assertRaises(TypeError, container.seek, 1.0) - def test_seek_int64(self): + def test_seek_int64(self) -> None: # Assert that it accepts large values. # Issue 251 pointed this out. container = av.open(fate_suite("h264/interlaced_crop.mp4")) container.seek(2**32) - def test_seek_start(self): + def test_seek_start(self) -> None: container = av.open(fate_suite("h264/interlaced_crop.mp4")) # count all the packets @@ -42,8 +40,9 @@ def test_seek_start(self): assert total_packet_count == seek_packet_count - def test_seek_middle(self): + def test_seek_middle(self) -> None: container = av.open(fate_suite("h264/interlaced_crop.mp4")) + assert container.duration is not None # count all the packets total_packet_count = 0 @@ -57,10 +56,11 @@ def test_seek_middle(self): for packet in container.demux(): seek_packet_count += 1 - self.assertTrue(seek_packet_count < total_packet_count) + assert seek_packet_count < total_packet_count - def test_seek_end(self): + def test_seek_end(self) -> None: container = av.open(fate_suite("h264/interlaced_crop.mp4")) + assert container.duration is not None # seek to middle container.seek(container.duration // 2) @@ -80,21 +80,19 @@ def test_seek_end(self): assert seek_packet_count > 0 assert seek_packet_count < middle_packet_count - def test_decode_half(self): + def test_decode_half(self) -> None: container = av.open(fate_suite("h264/interlaced_crop.mp4")) + video_stream = container.streams.video[0] - video_stream = next(s for s in container.streams if s.type == "video") total_frame_count = 0 - - # Count number of frames in video - for packet in container.demux(video_stream): - for frame in packet.decode(): - total_frame_count += 1 + for frame in container.decode(video_stream): + total_frame_count += 1 assert video_stream.frames == total_frame_count + assert video_stream.average_rate is not None # set target frame to middle frame - target_frame = int(total_frame_count / 2.0) + target_frame = total_frame_count // 2 target_timestamp = int( (target_frame * av.time_base) / video_stream.average_rate ) @@ -105,35 +103,33 @@ def test_decode_half(self): current_frame = None frame_count = 0 - for packet in container.demux(video_stream): - for frame in packet.decode(): - if current_frame is None: - current_frame = timestamp_to_frame(frame.pts, video_stream) - else: - current_frame += 1 + for frame in container.decode(video_stream): + if current_frame is None: + current_frame = timestamp_to_frame(frame.pts, video_stream) + else: + current_frame += 1 - # start counting once we reach the target frame - if current_frame is not None and current_frame >= target_frame: - frame_count += 1 + # start counting once we reach the target frame + if current_frame is not None and current_frame >= target_frame: + frame_count += 1 assert frame_count == total_frame_count - target_frame - def test_stream_seek(self): + def test_stream_seek(self) -> None: container = av.open(fate_suite("h264/interlaced_crop.mp4")) + video_stream = container.streams.video[0] - video_stream = next(s for s in container.streams if s.type == "video") - total_frame_count = 0 + assert video_stream.time_base is not None + assert video_stream.start_time is not None + assert video_stream.average_rate is not None - # Count number of frames in video - for packet in container.demux(video_stream): - for frame in packet.decode(): - total_frame_count += 1 + total_frame_count = 0 + for frame in container.decode(video_stream): + total_frame_count += 1 - target_frame = int(total_frame_count / 2.0) + target_frame = total_frame_count // 2 time_base = float(video_stream.time_base) - - rate = float(video_stream.average_rate) - target_sec = target_frame * 1 / rate + target_sec = target_frame * 1 / float(video_stream.average_rate) target_timestamp = int(target_sec / time_base) + video_stream.start_time container.seek(target_timestamp, stream=video_stream) @@ -141,20 +137,15 @@ def test_stream_seek(self): current_frame = None frame_count = 0 - for packet in container.demux(video_stream): - for frame in packet.decode(): - if current_frame is None: - current_frame = timestamp_to_frame(frame.pts, video_stream) - - else: - current_frame += 1 + for frame in container.decode(video_stream): + if current_frame is None: + assert frame.pts is not None + current_frame = timestamp_to_frame(frame.pts, video_stream) + else: + current_frame += 1 - # start counting once we reach the target frame - if current_frame is not None and current_frame >= target_frame: - frame_count += 1 + # start counting once we reach the target frame + if current_frame is not None and current_frame >= target_frame: + frame_count += 1 assert frame_count == total_frame_count - target_frame - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_timeout.py b/tests/test_timeout.py index cf6e7f0f8..0640237f9 100644 --- a/tests/test_timeout.py +++ b/tests/test_timeout.py @@ -17,54 +17,54 @@ class HttpServer(TCPServer): allow_reuse_address = True - def handle_error(self, request, client_address): + def handle_error(self, request: object, client_address: object) -> None: pass class SlowRequestHandler(BaseHTTPRequestHandler): - def do_GET(self): + def do_GET(self) -> None: time.sleep(DELAY) self.send_response(200) self.send_header("Content-Length", str(len(CONTENT))) self.end_headers() self.wfile.write(CONTENT) - def log_message(self, format, *args): + def log_message(self, format: object, *args: object) -> None: pass class TestTimeout(TestCase): - def setUp(cls): + def setUp(cls) -> None: cls._server = HttpServer(("", PORT), SlowRequestHandler) cls._thread = threading.Thread(target=cls._server.handle_request) cls._thread.daemon = True # Make sure the tests will exit. cls._thread.start() - def tearDown(cls): + def tearDown(cls) -> None: cls._thread.join(1) # Can't wait forever or the tests will never exit. cls._server.server_close() - def test_no_timeout(self): + def test_no_timeout(self) -> None: start = time.time() - av.open("http://localhost:%d/mpeg2_field_encoding.ts" % PORT) + av.open(f"http://localhost:{PORT}/mpeg2_field_encoding.ts") duration = time.time() - start - self.assertGreater(duration, DELAY) + assert duration > DELAY - def test_open_timeout(self): + def test_open_timeout(self) -> None: with self.assertRaises(av.ExitError): start = time.time() - av.open( - "http://localhost:%d/mpeg2_field_encoding.ts" % PORT, timeout=TIMEOUT - ) + av.open(f"http://localhost:{PORT}/mpeg2_field_encoding.ts", timeout=TIMEOUT) + duration = time.time() - start - self.assertLess(duration, DELAY) + assert duration < DELAY - def test_open_timeout_2(self): + def test_open_timeout_2(self) -> None: with self.assertRaises(av.ExitError): start = time.time() av.open( - "http://localhost:%d/mpeg2_field_encoding.ts" % PORT, + f"http://localhost:{PORT}/mpeg2_field_encoding.ts", timeout=(TIMEOUT, None), ) + duration = time.time() - start - self.assertLess(duration, DELAY) + assert duration < DELAY diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index b0ea0bcc5..60cf78035 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -19,6 +19,10 @@ def test_opaque(self) -> None: packet.opaque = (time.time(), packet_idx) for frame in packet.decode(): assert isinstance(frame, av.frame.Frame) + + if frame.opaque is None: + continue + assert type(frame.opaque) is tuple and len(frame.opaque) == 2 @@ -28,19 +32,19 @@ def test_invalid_pixel_format(self): VideoFrame(640, 480, "__unknown_pix_fmt") assert str(cm.exception) == "not a pixel format: '__unknown_pix_fmt'" - def test_null_constructor(self): + def test_null_constructor(self) -> None: frame = VideoFrame() assert frame.width == 0 assert frame.height == 0 assert frame.format.name == "yuv420p" - def test_manual_yuv_constructor(self): + def test_manual_yuv_constructor(self) -> None: frame = VideoFrame(640, 480, "yuv420p") assert frame.width == 640 assert frame.height == 480 assert frame.format.name == "yuv420p" - def test_manual_rgb_constructor(self): + def test_manual_rgb_constructor(self) -> None: frame = VideoFrame(640, 480, "rgb24") assert frame.width == 640 assert frame.height == 480 @@ -48,11 +52,11 @@ def test_manual_rgb_constructor(self): class TestVideoFramePlanes: - def test_null_planes(self): + def test_null_planes(self) -> None: frame = VideoFrame() # yuv420p assert len(frame.planes) == 0 - def test_yuv420p_planes(self): + def test_yuv420p_planes(self) -> None: frame = VideoFrame(640, 480, "yuv420p") assert len(frame.planes) == 3 assert frame.planes[0].width == 640 From d1a21b52d07754efa104af6ee60e470d7ecbd44d Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 24 Sep 2024 16:21:39 -0400 Subject: [PATCH 365/846] Make tests more idiomatic for pytest We don't need to put tests in a class anymore. I also sprinkled some type hints here and there. Left a few because the commit would be too big otherwise. --- Makefile | 2 +- tests/common.py | 82 +-- tests/test_audioformat.py | 47 +- tests/test_audioframe.py | 381 ++++++----- tests/test_audiolayout.py | 54 +- tests/test_audioresampler.py | 517 +++++++------- tests/test_bitstream.py | 134 ++-- tests/test_codec.py | 183 +++-- tests/test_codec_context.py | 44 +- tests/test_colorspace.py | 64 +- tests/test_containerformat.py | 111 +-- tests/test_deprecation.py | 60 +- tests/test_dictionary.py | 25 +- tests/test_encode.py | 22 +- tests/test_enums.py | 342 +++++----- tests/test_errors.py | 112 +-- tests/test_python_io.py | 53 +- tests/test_videoframe.py | 1209 +++++++++++++++++---------------- 18 files changed, 1723 insertions(+), 1719 deletions(-) diff --git a/Makefile b/Makefile index 43e589717..77178b9d1 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ fate-suite: rsync -vrltLW rsync://fate-suite.ffmpeg.org/fate-suite/ tests/assets/fate-suite/ lint: - $(PIP) install -U black isort flake8 flake8-pyproject pillow numpy mypy==1.11.2 + $(PIP) install -U black isort flake8 flake8-pyproject pillow numpy mypy==1.11.2 pytest black --check av examples tests setup.py flake8 av isort --check-only --diff av examples tests diff --git a/tests/common.py b/tests/common.py index ece6ac5e1..33b307c80 100644 --- a/tests/common.py +++ b/tests/common.py @@ -5,6 +5,8 @@ import types from unittest import TestCase as _Base +import numpy as np + from av.datasets import fate as fate_suite try: @@ -55,7 +57,7 @@ def asset(*args: str) -> str: os.environ["PYAV_TESTDATA_DIR"] = asset() -def fate_png(): +def fate_png() -> str: return fate_suite("png1/55c99e750a5fd6_50314226.png") @@ -85,33 +87,32 @@ def _inner(self, *args, **kwargs): return _inner -class MethodLogger: - def __init__(self, obj): - self._obj = obj - self._log = [] +def assertNdarraysEqual(a: np.ndarray, b: np.ndarray) -> None: + assert a.shape == b.shape + + comparison = a == b + if not comparison.all(): + it = np.nditer(comparison, flags=["multi_index"]) + msg = "" + for equal in it: + if not equal: + msg += "- arrays differ at index {}; {} {}\n".format( + it.multi_index, + a[it.multi_index], + b[it.multi_index], + ) + assert False, f"ndarrays contents differ\n{msg}" - def __getattr__(self, name): - value = getattr(self._obj, name) - if isinstance( - value, - ( - types.MethodType, - types.FunctionType, - types.BuiltinFunctionType, - types.BuiltinMethodType, - ), - ): - return functools.partial(self._method, name, value) - else: - self._log.append(("__getattr__", (name,), {})) - return value - def _method(self, name, meth, *args, **kwargs): - self._log.append((name, args, kwargs)) - return meth(*args, **kwargs) +def assertImagesAlmostEqual(a, b, epsilon=0.1): + import PIL.ImageFilter as ImageFilter - def _filter(self, type_): - return [log for log in self._log if log[0] == type_] + assert a.size == b.size + a = a.filter(ImageFilter.BLUR).getdata() + b = b.filter(ImageFilter.BLUR).getdata() + for i, ax, bx in zip(range(len(a)), a, b): + diff = sum(abs(ac / 256 - bc / 256) for ac, bc in zip(ax, bx)) / 3 + assert diff < epsilon, f"images differed by {diff} at index {i}; {ax} {bx}" class TestCase(_Base): @@ -129,34 +130,3 @@ def sandboxed(self, *args, **kwargs) -> str: kwargs.setdefault("sandbox", self.sandbox) kwargs.setdefault("timed", True) return sandboxed(*args, **kwargs) - - def assertNdarraysEqual(self, a, b): - import numpy - - self.assertEqual(a.shape, b.shape) - - comparison = a == b - if not comparison.all(): - it = numpy.nditer(comparison, flags=["multi_index"]) - msg = "" - for equal in it: - if not equal: - msg += "- arrays differ at index %s; %s %s\n" % ( - it.multi_index, - a[it.multi_index], - b[it.multi_index], - ) - self.fail("ndarrays contents differ\n%s" % msg) - - def assertImagesAlmostEqual(self, a, b, epsilon=0.1, *args): - import PIL.ImageFilter as ImageFilter - - self.assertEqual(a.size, b.size, "sizes dont match") - a = a.filter(ImageFilter.BLUR).getdata() - b = b.filter(ImageFilter.BLUR).getdata() - for i, ax, bx in zip(range(len(a)), a, b): - diff = sum(abs(ac / 256 - bc / 256) for ac, bc in zip(ax, bx)) / 3 - if diff > epsilon: - self.fail( - "images differed by %s at index %d; %s %s" % (diff, i, ax, bx) - ) diff --git a/tests/test_audioformat.py b/tests/test_audioformat.py index 733761f57..244d3ad7d 100644 --- a/tests/test_audioformat.py +++ b/tests/test_audioformat.py @@ -1,27 +1,28 @@ import sys +import pytest + from av import AudioFormat -from .common import TestCase - -postfix = "le" if sys.byteorder == "little" else "be" - - -class TestAudioFormats(TestCase): - def test_s16_inspection(self) -> None: - fmt = AudioFormat("s16") - assert fmt.name == "s16" - assert not fmt.is_planar - assert fmt.bits == 16 - assert fmt.bytes == 2 - assert fmt.container_name == "s16" + postfix - assert fmt.planar.name == "s16p" - assert fmt.packed is fmt - - def test_s32p_inspection(self) -> None: - fmt = AudioFormat("s32p") - assert fmt.name == "s32p" - assert fmt.is_planar - assert fmt.bits == 32 - assert fmt.bytes == 4 - self.assertRaises(ValueError, lambda: fmt.container_name) + +def test_s16_inspection() -> None: + fmt = AudioFormat("s16") + postfix = "le" if sys.byteorder == "little" else "be" + + assert fmt.name == "s16" + assert not fmt.is_planar + assert fmt.bits == 16 + assert fmt.bytes == 2 + assert fmt.container_name == "s16" + postfix + assert fmt.planar.name == "s16p" + assert fmt.packed is fmt + + +def test_s32p_inspection() -> None: + fmt = AudioFormat("s32p") + assert fmt.name == "s32p" + assert fmt.is_planar + assert fmt.bits == 32 + assert fmt.bytes == 4 + + pytest.raises(ValueError, lambda: fmt.container_name) diff --git a/tests/test_audioframe.py b/tests/test_audioframe.py index 4a382d18c..7211ad023 100644 --- a/tests/test_audioframe.py +++ b/tests/test_audioframe.py @@ -1,187 +1,202 @@ +from re import escape + import numpy as np +import pytest from av import AudioFrame -from .common import TestCase - - -class TestAudioFrameConstructors(TestCase): - def test_null_constructor(self) -> None: - frame = AudioFrame() - self.assertEqual(frame.format.name, "s16") - self.assertEqual(frame.layout.name, "stereo") - self.assertEqual(len(frame.planes), 0) - self.assertEqual(frame.samples, 0) - - def test_manual_flt_mono_constructor(self) -> None: - frame = AudioFrame(format="flt", layout="mono", samples=160) - self.assertEqual(frame.format.name, "flt") - self.assertEqual(frame.layout.name, "mono") - self.assertEqual(len(frame.planes), 1) - self.assertEqual(frame.planes[0].buffer_size, 640) - self.assertEqual(frame.samples, 160) - - def test_manual_flt_stereo_constructor(self) -> None: - frame = AudioFrame(format="flt", layout="stereo", samples=160) - self.assertEqual(frame.format.name, "flt") - self.assertEqual(frame.layout.name, "stereo") - self.assertEqual(len(frame.planes), 1) - self.assertEqual(frame.planes[0].buffer_size, 1280) - self.assertEqual(frame.samples, 160) - - def test_manual_fltp_stereo_constructor(self) -> None: - frame = AudioFrame(format="fltp", layout="stereo", samples=160) - self.assertEqual(frame.format.name, "fltp") - self.assertEqual(frame.layout.name, "stereo") - self.assertEqual(len(frame.planes), 2) - self.assertEqual(frame.planes[0].buffer_size, 640) - self.assertEqual(frame.planes[1].buffer_size, 640) - self.assertEqual(frame.samples, 160) - - def test_manual_s16_mono_constructor(self) -> None: - frame = AudioFrame(format="s16", layout="mono", samples=160) - self.assertEqual(frame.format.name, "s16") - self.assertEqual(frame.layout.name, "mono") - self.assertEqual(len(frame.planes), 1) - self.assertEqual(frame.planes[0].buffer_size, 320) - self.assertEqual(frame.samples, 160) - - def test_manual_s16_mono_constructor_align_8(self) -> None: - frame = AudioFrame(format="s16", layout="mono", samples=159, align=8) - self.assertEqual(frame.format.name, "s16") - self.assertEqual(frame.layout.name, "mono") - self.assertEqual(len(frame.planes), 1) - self.assertEqual(frame.planes[0].buffer_size, 320) - self.assertEqual(frame.samples, 159) - - def test_manual_s16_stereo_constructor(self) -> None: - frame = AudioFrame(format="s16", layout="stereo", samples=160) - self.assertEqual(frame.format.name, "s16") - self.assertEqual(frame.layout.name, "stereo") - self.assertEqual(len(frame.planes), 1) - self.assertEqual(frame.planes[0].buffer_size, 640) - self.assertEqual(frame.samples, 160) - - def test_manual_s16p_stereo_constructor(self) -> None: - frame = AudioFrame(format="s16p", layout="stereo", samples=160) - self.assertEqual(frame.format.name, "s16p") - self.assertEqual(frame.layout.name, "stereo") - self.assertEqual(len(frame.planes), 2) - self.assertEqual(frame.planes[0].buffer_size, 320) - self.assertEqual(frame.planes[1].buffer_size, 320) - self.assertEqual(frame.samples, 160) - - -class TestAudioFrameConveniences(TestCase): - def test_basic_to_ndarray(self) -> None: - frame = AudioFrame(format="s16p", layout="stereo", samples=160) - array = frame.to_ndarray() - assert array.dtype == "i2" - assert array.shape == (2, 160) - - def test_ndarray_dbl(self) -> None: - layouts = [ - ("dbl", "mono", (1, 160)), - ("dbl", "stereo", (1, 320)), - ("dblp", "mono", (1, 160)), - ("dblp", "stereo", (2, 160)), - ] - for format, layout, size in layouts: - array = np.zeros(shape=size, dtype="f8") - for i in range(size[0]): - array[i][:] = np.random.rand(size[1]) - frame = AudioFrame.from_ndarray(array, format=format, layout=layout) - assert frame.format.name == format - assert frame.layout.name == layout - assert frame.samples == 160 - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_from_ndarray_value_error(self) -> None: - # incorrect dtype - array = np.zeros(shape=(1, 160), dtype="f2") - with self.assertRaises(ValueError) as cm: - AudioFrame.from_ndarray(array, format="flt", layout="mono") - assert ( - str(cm.exception) - == "Expected numpy array with dtype `float32` but got `float16`" - ) - - # incorrect number of dimensions - array = np.zeros(shape=(1, 160, 2), dtype="f4") - with self.assertRaises(ValueError) as cm: - AudioFrame.from_ndarray(array, format="flt", layout="mono") - assert str(cm.exception) == "Expected numpy array with ndim `2` but got `3`" - - # incorrect shape - array = np.zeros(shape=(2, 160), dtype="f4") - with self.assertRaises(ValueError) as cm: - AudioFrame.from_ndarray(array, format="flt", layout="mono") - assert str(cm.exception) == "Unexpected numpy array shape `(2, 160)`" - - def test_ndarray_flt(self) -> None: - layouts = [ - ("flt", "mono", (1, 160)), - ("flt", "stereo", (1, 320)), - ("fltp", "mono", (1, 160)), - ("fltp", "stereo", (2, 160)), - ] - for format, layout, size in layouts: - array: np.ndarray = np.zeros(shape=size, dtype="f4") - for i in range(size[0]): - array[i][:] = np.random.rand(size[1]) - frame = AudioFrame.from_ndarray(array, format=format, layout=layout) - assert frame.format.name == format - assert frame.layout.name == layout - assert frame.samples == 160 - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_s16(self) -> None: - layouts = [ - ("s16", "mono", (1, 160)), - ("s16", "stereo", (1, 320)), - ("s16p", "mono", (1, 160)), - ("s16p", "stereo", (2, 160)), - ] - for format, layout, size in layouts: - array = np.random.randint(0, 256, size=size, dtype="i2") - frame = AudioFrame.from_ndarray(array, format=format, layout=layout) - assert frame.format.name == format - assert frame.layout.name == layout - assert frame.samples == 160 - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_s16p_align_8(self) -> None: - frame = AudioFrame(format="s16p", layout="stereo", samples=159, align=8) - array = frame.to_ndarray() - assert array.dtype == "i2" - assert array.shape == (2, 159) - - def test_ndarray_s32(self) -> None: - layouts = [ - ("s32", "mono", (1, 160)), - ("s32", "stereo", (1, 320)), - ("s32p", "mono", (1, 160)), - ("s32p", "stereo", (2, 160)), - ] - for format, layout, size in layouts: - array = np.random.randint(0, 256, size=size, dtype="i4") - frame = AudioFrame.from_ndarray(array, format=format, layout=layout) - assert frame.format.name == format - assert frame.layout.name == layout - assert frame.samples == 160 - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_u8(self) -> None: - layouts = [ - ("u8", "mono", (1, 160)), - ("u8", "stereo", (1, 320)), - ("u8p", "mono", (1, 160)), - ("u8p", "stereo", (2, 160)), - ] - for format, layout, size in layouts: - array = np.random.randint(0, 256, size=size, dtype="u1") - frame = AudioFrame.from_ndarray(array, format=format, layout=layout) - assert frame.format.name == format - assert frame.layout.name == layout - assert frame.samples == 160 - self.assertNdarraysEqual(frame.to_ndarray(), array) +from .common import assertNdarraysEqual + + +def test_null_constructor() -> None: + frame = AudioFrame() + assert frame.format.name == "s16" + assert frame.layout.name == "stereo" + assert len(frame.planes) == 0 + assert frame.samples == 0 + + +def test_manual_flt_mono_constructor() -> None: + frame = AudioFrame(format="flt", layout="mono", samples=160) + assert frame.format.name == "flt" + assert frame.layout.name == "mono" + assert len(frame.planes) == 1 + assert frame.planes[0].buffer_size == 640 + assert frame.samples == 160 + + +def test_manual_flt_stereo_constructor() -> None: + frame = AudioFrame(format="flt", layout="stereo", samples=160) + assert frame.format.name == "flt" + assert frame.layout.name == "stereo" + assert len(frame.planes) == 1 + assert frame.planes[0].buffer_size == 1280 + assert frame.samples == 160 + + +def test_manual_fltp_stereo_constructor() -> None: + frame = AudioFrame(format="fltp", layout="stereo", samples=160) + assert frame.format.name == "fltp" + assert frame.layout.name == "stereo" + assert len(frame.planes) == 2 + assert frame.planes[0].buffer_size == 640 + assert frame.planes[1].buffer_size == 640 + assert frame.samples == 160 + + +def test_manual_s16_mono_constructor() -> None: + frame = AudioFrame(format="s16", layout="mono", samples=160) + assert frame.format.name == "s16" + assert frame.layout.name == "mono" + assert len(frame.planes) == 1 + assert frame.planes[0].buffer_size == 320 + assert frame.samples == 160 + + +def test_manual_s16_mono_constructor_align_8() -> None: + frame = AudioFrame(format="s16", layout="mono", samples=159, align=8) + assert frame.format.name == "s16" + assert frame.layout.name == "mono" + assert len(frame.planes) == 1 + assert frame.planes[0].buffer_size == 320 + assert frame.samples == 159 + + +def test_manual_s16_stereo_constructor() -> None: + frame = AudioFrame(format="s16", layout="stereo", samples=160) + assert frame.format.name == "s16" + assert frame.layout.name == "stereo" + assert len(frame.planes) == 1 + assert frame.planes[0].buffer_size == 640 + assert frame.samples == 160 + + +def test_manual_s16p_stereo_constructor() -> None: + frame = AudioFrame(format="s16p", layout="stereo", samples=160) + assert frame.format.name == "s16p" + assert frame.layout.name == "stereo" + assert len(frame.planes) == 2 + assert frame.planes[0].buffer_size == 320 + assert frame.planes[1].buffer_size == 320 + assert frame.samples == 160 + + +def test_basic_to_ndarray() -> None: + frame = AudioFrame(format="s16p", layout="stereo", samples=160) + array = frame.to_ndarray() + assert array.dtype == "i2" + assert array.shape == (2, 160) + + +def test_ndarray_dbl() -> None: + layouts = [ + ("dbl", "mono", (1, 160)), + ("dbl", "stereo", (1, 320)), + ("dblp", "mono", (1, 160)), + ("dblp", "stereo", (2, 160)), + ] + for format, layout, size in layouts: + array = np.zeros(shape=size, dtype="f8") + for i in range(size[0]): + array[i][:] = np.random.rand(size[1]) + frame = AudioFrame.from_ndarray(array, format=format, layout=layout) + assert frame.format.name == format + assert frame.layout.name == layout + assert frame.samples == 160 + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_from_ndarray_value_error() -> None: + # incorrect dtype + array = np.zeros(shape=(1, 160), dtype="f2") + with pytest.raises( + ValueError, match="Expected numpy array with dtype `float32` but got `float16`" + ) as cm: + AudioFrame.from_ndarray(array, format="flt", layout="mono") + + # incorrect number of dimensions + array = np.zeros(shape=(1, 160, 2), dtype="f4") + with pytest.raises( + ValueError, match="Expected numpy array with ndim `2` but got `3`" + ) as cm: + AudioFrame.from_ndarray(array, format="flt", layout="mono") + + # incorrect shape + array = np.zeros(shape=(2, 160), dtype="f4") + with pytest.raises( + ValueError, match=escape("Unexpected numpy array shape `(2, 160)`") + ) as cm: + AudioFrame.from_ndarray(array, format="flt", layout="mono") + + +def test_ndarray_flt() -> None: + layouts = [ + ("flt", "mono", (1, 160)), + ("flt", "stereo", (1, 320)), + ("fltp", "mono", (1, 160)), + ("fltp", "stereo", (2, 160)), + ] + for format, layout, size in layouts: + array: np.ndarray = np.zeros(shape=size, dtype="f4") + for i in range(size[0]): + array[i][:] = np.random.rand(size[1]) + frame = AudioFrame.from_ndarray(array, format=format, layout=layout) + assert frame.format.name == format + assert frame.layout.name == layout + assert frame.samples == 160 + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_s16() -> None: + layouts = [ + ("s16", "mono", (1, 160)), + ("s16", "stereo", (1, 320)), + ("s16p", "mono", (1, 160)), + ("s16p", "stereo", (2, 160)), + ] + for format, layout, size in layouts: + array = np.random.randint(0, 256, size=size, dtype="i2") + frame = AudioFrame.from_ndarray(array, format=format, layout=layout) + assert frame.format.name == format + assert frame.layout.name == layout + assert frame.samples == 160 + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_s16p_align_8() -> None: + frame = AudioFrame(format="s16p", layout="stereo", samples=159, align=8) + array = frame.to_ndarray() + assert array.dtype == "i2" + assert array.shape == (2, 159) + + +def test_ndarray_s32() -> None: + layouts = [ + ("s32", "mono", (1, 160)), + ("s32", "stereo", (1, 320)), + ("s32p", "mono", (1, 160)), + ("s32p", "stereo", (2, 160)), + ] + for format, layout, size in layouts: + array = np.random.randint(0, 256, size=size, dtype="i4") + frame = AudioFrame.from_ndarray(array, format=format, layout=layout) + assert frame.format.name == format + assert frame.layout.name == layout + assert frame.samples == 160 + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_u8() -> None: + layouts = [ + ("u8", "mono", (1, 160)), + ("u8", "stereo", (1, 320)), + ("u8p", "mono", (1, 160)), + ("u8p", "stereo", (2, 160)), + ] + for format, layout, size in layouts: + array = np.random.randint(0, 256, size=size, dtype="u1") + frame = AudioFrame.from_ndarray(array, format=format, layout=layout) + assert frame.format.name == format + assert frame.layout.name == layout + assert frame.samples == 160 + assertNdarraysEqual(frame.to_ndarray(), array) diff --git a/tests/test_audiolayout.py b/tests/test_audiolayout.py index 9cf257f68..35ba4c160 100644 --- a/tests/test_audiolayout.py +++ b/tests/test_audiolayout.py @@ -1,32 +1,26 @@ from av import AudioLayout -from .common import TestCase - - -class TestAudioLayout(TestCase): - def test_stereo_from_str(self): - layout = AudioLayout("stereo") - self._test_stereo(layout) - - def test_stereo_from_layout(self): - layout = AudioLayout("stereo") - layout2 = AudioLayout(layout) - self._test_stereo(layout2) - - def _test_stereo(self, layout: AudioLayout) -> None: - self.assertEqual(layout.name, "stereo") - self.assertEqual(layout.nb_channels, 2) - self.assertEqual(repr(layout), "") - - # Re-enable when FFmpeg 6.0 is dropped. - - # self.assertEqual(layout.channels[0].name, "FL") - # self.assertEqual(layout.channels[0].description, "front left") - # self.assertEqual( - # repr(layout.channels[0]), "" - # ) - # self.assertEqual(layout.channels[1].name, "FR") - # self.assertEqual(layout.channels[1].description, "front right") - # self.assertEqual( - # repr(layout.channels[1]), "" - # ) + +def _test_stereo(layout: AudioLayout) -> None: + assert layout.name == "stereo" + assert layout.nb_channels == 2 + assert repr(layout) == "" + + # Re-enable when FFmpeg 6.0 is dropped. + + # assert layout.channels[0].name == "FL" + # assert layout.channels[0].description == "front left" + # assert repr(layout.channels[0]) == "" + # assert layout.channels[1].name == "FR" + # assert layout.channels[1].description == "front right" + # assert repr(layout.channels[1]) == "" + + +def test_stereo_from_str() -> None: + layout = AudioLayout("stereo") + _test_stereo(layout) + + +def test_stereo_from_layout() -> None: + layout2 = AudioLayout(AudioLayout("stereo")) + _test_stereo(layout2) diff --git a/tests/test_audioresampler.py b/tests/test_audioresampler.py index 47dd1ddc3..cad8ebe34 100644 --- a/tests/test_audioresampler.py +++ b/tests/test_audioresampler.py @@ -1,282 +1,289 @@ from fractions import Fraction +import pytest + import av from av import AudioFrame, AudioResampler -from .common import TestCase +def test_flush_immediately() -> None: + """ + If we flush the resampler before passing any input, it returns + a `None` frame without setting up the graph. + """ -class TestAudioResampler(TestCase): - def test_flush_immediately(self) -> None: - """ - If we flush the resampler before passing any input, it returns - a `None` frame without setting up the graph. - """ + resampler = AudioResampler() - resampler = AudioResampler() + # flush + oframes = resampler.resample(None) + assert len(oframes) == 0 - # flush - oframes = resampler.resample(None) - self.assertEqual(len(oframes), 0) - def test_identity_passthrough(self) -> None: - """ - If we don't ask it to do anything, it won't. - """ +def test_identity_passthrough() -> None: + """ + If we don't ask it to do anything, it won't. + """ - resampler = AudioResampler() + resampler = AudioResampler() - # resample one frame - iframe = AudioFrame("s16", "stereo", 1024) + # resample one frame + iframe = AudioFrame("s16", "stereo", 1024) - oframes = resampler.resample(iframe) - self.assertEqual(len(oframes), 1) - self.assertIs(iframe, oframes[0]) + oframes = resampler.resample(iframe) + assert len(oframes) == 1 + assert iframe is oframes[0] - # resample another frame - iframe.pts = 1024 + # resample another frame + iframe.pts = 1024 - oframes = resampler.resample(iframe) - self.assertEqual(len(oframes), 1) - assert iframe is oframes[0] + oframes = resampler.resample(iframe) + assert len(oframes) == 1 + assert iframe is oframes[0] - # flush - oframes = resampler.resample(None) - self.assertEqual(len(oframes), 0) + # flush + oframes = resampler.resample(None) + assert len(oframes) == 0 - def test_matching_passthrough(self) -> None: - """ - If the frames match, it won't do anything. - """ - resampler = AudioResampler("s16", "stereo") +def test_matching_passthrough() -> None: + """ + If the frames match, it won't do anything. + """ - # resample one frame - iframe = AudioFrame("s16", "stereo", 1024) + resampler = AudioResampler("s16", "stereo") - oframes = resampler.resample(iframe) - self.assertEqual(len(oframes), 1) - self.assertIs(iframe, oframes[0]) + # resample one frame + iframe = AudioFrame("s16", "stereo", 1024) - # resample another frame - iframe.pts = 1024 + oframes = resampler.resample(iframe) + assert len(oframes) == 1 + assert iframe is oframes[0] - oframes = resampler.resample(iframe) - self.assertEqual(len(oframes), 1) - self.assertIs(iframe, oframes[0]) + # resample another frame + iframe.pts = 1024 - # flush - oframes = resampler.resample(None) - self.assertEqual(len(oframes), 0) + oframes = resampler.resample(iframe) + assert len(oframes) == 1 + assert iframe is oframes[0] - def test_pts_assertion_same_rate(self) -> None: - av.logging.set_level(av.logging.VERBOSE) - resampler = AudioResampler("s16", "mono") + # flush + oframes = resampler.resample(None) + assert len(oframes) == 0 - # resample one frame - iframe = AudioFrame("s16", "stereo", 1024) - iframe.sample_rate = 48000 - iframe.time_base = Fraction(1, 48000) - iframe.pts = 0 - - oframes = resampler.resample(iframe) - self.assertEqual(len(oframes), 1) - - oframe = oframes[0] - self.assertEqual(oframe.pts, 0) - self.assertEqual(oframe.time_base, iframe.time_base) - self.assertEqual(oframe.sample_rate, iframe.sample_rate) - self.assertEqual(oframe.samples, iframe.samples) - - # resample another frame - iframe.pts = 1024 - - oframes = resampler.resample(iframe) - self.assertEqual(len(oframes), 1) - - oframe = oframes[0] - self.assertEqual(oframe.pts, 1024) - self.assertEqual(oframe.time_base, iframe.time_base) - self.assertEqual(oframe.sample_rate, iframe.sample_rate) - self.assertEqual(oframe.samples, iframe.samples) - - # resample another frame with a pts gap, do not raise exception - iframe.pts = 9999 - oframes = resampler.resample(iframe) - self.assertEqual(len(oframes), 1) - - oframe = oframes[0] - self.assertEqual(oframe.pts, 9999) - self.assertEqual(oframe.time_base, iframe.time_base) - self.assertEqual(oframe.sample_rate, iframe.sample_rate) - self.assertEqual(oframe.samples, iframe.samples) - - # flush - oframes = resampler.resample(None) - self.assertEqual(len(oframes), 0) - av.logging.set_level(None) - - def test_pts_assertion_new_rate_up(self) -> None: - resampler = AudioResampler("s16", "mono", 44100) - - # resample one frame - iframe = AudioFrame("s16", "stereo", 1024) - iframe.sample_rate = 48000 - iframe.time_base = Fraction(1, 48000) - iframe.pts = 0 - - oframes = resampler.resample(iframe) - self.assertEqual(len(oframes), 1) - - oframe = oframes[0] - self.assertEqual(oframe.pts, 0) - self.assertEqual(oframe.time_base, Fraction(1, 44100)) - self.assertEqual(oframe.sample_rate, 44100) - self.assertEqual(oframe.samples, 925) - - iframe = AudioFrame("s16", "stereo", 1024) - iframe.sample_rate = 48000 - iframe.time_base = Fraction(1, 48000) - iframe.pts = 1024 - - oframes = resampler.resample(iframe) - self.assertEqual(len(oframes), 1) - - oframe = oframes[0] - self.assertEqual(oframe.pts, 925) - self.assertEqual(oframe.time_base, Fraction(1, 44100)) - self.assertEqual(oframe.sample_rate, 44100) - self.assertEqual(oframe.samples, 941) - - # flush - oframes = resampler.resample(None) - self.assertEqual(len(oframes), 1) - - oframe = oframes[0] - self.assertEqual(oframe.pts, 941 + 925) - self.assertEqual(oframe.time_base, Fraction(1, 44100)) - self.assertEqual(oframe.sample_rate, 44100) - self.assertEqual(oframe.samples, 15) - - def test_pts_assertion_new_rate_down(self) -> None: - resampler = AudioResampler("s16", "mono", 48000) - - # resample one frame - iframe = AudioFrame("s16", "stereo", 1024) - iframe.sample_rate = 44100 - iframe.time_base = Fraction(1, 44100) - iframe.pts = 0 - - oframes = resampler.resample(iframe) - self.assertEqual(len(oframes), 1) - - oframe = oframes[0] - self.assertEqual(oframe.pts, 0) - self.assertEqual(oframe.time_base, Fraction(1, 48000)) - self.assertEqual(oframe.sample_rate, 48000) - self.assertEqual(oframe.samples, 1098) - - iframe = AudioFrame("s16", "stereo", 1024) - iframe.sample_rate = 44100 - iframe.time_base = Fraction(1, 44100) - iframe.pts = 1024 - - oframes = resampler.resample(iframe) - self.assertEqual(len(oframes), 1) - - oframe = oframes[0] - self.assertEqual(oframe.pts, 1098) - self.assertEqual(oframe.time_base, Fraction(1, 48000)) - self.assertEqual(oframe.sample_rate, 48000) - self.assertEqual(oframe.samples, 1114) - - # flush - oframes = resampler.resample(None) - self.assertEqual(len(oframes), 1) - - oframe = oframes[0] - self.assertEqual(oframe.pts, 1114 + 1098) - self.assertEqual(oframe.time_base, Fraction(1, 48000)) - self.assertEqual(oframe.sample_rate, 48000) - self.assertEqual(oframe.samples, 18) - - def test_pts_assertion_new_rate_fltp(self) -> None: - resampler = AudioResampler("fltp", "mono", 8000, 1024) - - # resample one frame - iframe = AudioFrame("s16", "mono", 1024) - iframe.sample_rate = 8000 - iframe.time_base = Fraction(1, 1000) - iframe.pts = 0 - - oframes = resampler.resample(iframe) - self.assertEqual(len(oframes), 1) - - oframe = oframes[0] - self.assertEqual(oframe.pts, 0) - self.assertEqual(oframe.time_base, Fraction(1, 8000)) - self.assertEqual(oframe.sample_rate, 8000) - self.assertEqual(oframe.samples, 1024) - - iframe = AudioFrame("s16", "mono", 1024) - iframe.sample_rate = 8000 - iframe.time_base = Fraction(1, 1000) - iframe.pts = 8192 - - oframes = resampler.resample(iframe) - self.assertEqual(len(oframes), 1) - - oframe = oframes[0] - self.assertEqual(oframe.pts, 65536) - self.assertEqual(oframe.time_base, Fraction(1, 8000)) - self.assertEqual(oframe.sample_rate, 8000) - self.assertEqual(oframe.samples, 1024) - - # flush - oframes = resampler.resample(None) - self.assertEqual(len(oframes), 0) - - def test_pts_missing_time_base(self) -> None: - resampler = AudioResampler("s16", "mono", 44100) - - # resample one frame - iframe = AudioFrame("s16", "stereo", 1024) - iframe.sample_rate = 48000 - iframe.pts = 0 - - oframes = resampler.resample(iframe) - self.assertEqual(len(oframes), 1) - - oframe = oframes[0] - self.assertEqual(oframe.pts, 0) - self.assertEqual(oframe.time_base, Fraction(1, 44100)) - self.assertEqual(oframe.sample_rate, 44100) - - # flush - oframes = resampler.resample(None) - self.assertEqual(len(oframes), 1) - - oframe = oframes[0] - self.assertEqual(oframe.pts, 925) - self.assertEqual(oframe.time_base, Fraction(1, 44100)) - self.assertEqual(oframe.sample_rate, 44100) - self.assertEqual(oframe.samples, 16) - - def test_mismatched_input(self) -> None: - """ - Consecutive frames must have the same layout, sample format and sample rate. - """ - resampler = AudioResampler("s16", "mono", 44100) - - # resample one frame - iframe = AudioFrame("s16", "stereo", 1024) - iframe.sample_rate = 48000 - resampler.resample(iframe) - # resample another frame with a sample format - iframe = AudioFrame("s16", "mono", 1024) - iframe.sample_rate = 48000 - with self.assertRaises(ValueError) as cm: - resampler.resample(iframe) +def test_pts_assertion_same_rate() -> None: + av.logging.set_level(av.logging.VERBOSE) + resampler = AudioResampler("s16", "mono") + + # resample one frame + iframe = AudioFrame("s16", "stereo", 1024) + iframe.sample_rate = 48000 + iframe.time_base = Fraction(1, 48000) + iframe.pts = 0 + + oframes = resampler.resample(iframe) + assert len(oframes) == 1 + + oframe = oframes[0] + assert oframe.pts == 0 + assert oframe.time_base == iframe.time_base + assert oframe.sample_rate == iframe.sample_rate + assert oframe.samples == iframe.samples + + # resample another frame + iframe.pts = 1024 + + oframes = resampler.resample(iframe) + assert len(oframes) == 1 + + oframe = oframes[0] + assert oframe.pts == 1024 + assert oframe.time_base == iframe.time_base + assert oframe.sample_rate == iframe.sample_rate + assert oframe.samples == iframe.samples + + # resample another frame with a pts gap, do not raise exception + iframe.pts = 9999 + oframes = resampler.resample(iframe) + assert len(oframes) == 1 - assert str(cm.exception) == "Frame does not match AudioResampler setup." + oframe = oframes[0] + assert oframe.pts == 9999 + assert oframe.time_base == iframe.time_base + assert oframe.sample_rate == iframe.sample_rate + assert oframe.samples == iframe.samples + + # flush + oframes = resampler.resample(None) + assert len(oframes) == 0 + av.logging.set_level(None) + + +def test_pts_assertion_new_rate_up() -> None: + resampler = AudioResampler("s16", "mono", 44100) + + # resample one frame + iframe = AudioFrame("s16", "stereo", 1024) + iframe.sample_rate = 48000 + iframe.time_base = Fraction(1, 48000) + iframe.pts = 0 + + oframes = resampler.resample(iframe) + assert len(oframes) == 1 + + oframe = oframes[0] + assert oframe.pts == 0 + assert oframe.time_base == Fraction(1, 44100) + assert oframe.sample_rate == 44100 + assert oframe.samples == 925 + + iframe = AudioFrame("s16", "stereo", 1024) + iframe.sample_rate = 48000 + iframe.time_base = Fraction(1, 48000) + iframe.pts = 1024 + + oframes = resampler.resample(iframe) + assert len(oframes) == 1 + + oframe = oframes[0] + assert oframe.pts == 925 + assert oframe.time_base == Fraction(1, 44100) + assert oframe.sample_rate == 44100 + assert oframe.samples == 941 + + # flush + oframes = resampler.resample(None) + assert len(oframes) == 1 + + oframe = oframes[0] + assert oframe.pts == 941 + 925 + assert oframe.time_base == Fraction(1, 44100) + assert oframe.sample_rate == 44100 + assert oframe.samples == 15 + + +def test_pts_assertion_new_rate_down() -> None: + resampler = AudioResampler("s16", "mono", 48000) + + # resample one frame + iframe = AudioFrame("s16", "stereo", 1024) + iframe.sample_rate = 44100 + iframe.time_base = Fraction(1, 44100) + iframe.pts = 0 + + oframes = resampler.resample(iframe) + assert len(oframes) == 1 + + oframe = oframes[0] + assert oframe.pts == 0 + assert oframe.time_base == Fraction(1, 48000) + assert oframe.sample_rate == 48000 + assert oframe.samples == 1098 + + iframe = AudioFrame("s16", "stereo", 1024) + iframe.sample_rate = 44100 + iframe.time_base = Fraction(1, 44100) + iframe.pts = 1024 + + oframes = resampler.resample(iframe) + assert len(oframes) == 1 + + oframe = oframes[0] + assert oframe.pts == 1098 + assert oframe.time_base == Fraction(1, 48000) + assert oframe.sample_rate == 48000 + assert oframe.samples == 1114 + + # flush + oframes = resampler.resample(None) + assert len(oframes) == 1 + + oframe = oframes[0] + assert oframe.pts == 1114 + 1098 + assert oframe.time_base == Fraction(1, 48000) + assert oframe.sample_rate == 48000 + assert oframe.samples == 18 + + +def test_pts_assertion_new_rate_fltp() -> None: + resampler = AudioResampler("fltp", "mono", 8000, 1024) + + # resample one frame + iframe = AudioFrame("s16", "mono", 1024) + iframe.sample_rate = 8000 + iframe.time_base = Fraction(1, 1000) + iframe.pts = 0 + + oframes = resampler.resample(iframe) + assert len(oframes) == 1 + + oframe = oframes[0] + assert oframe.pts == 0 + assert oframe.time_base == Fraction(1, 8000) + assert oframe.sample_rate == 8000 + assert oframe.samples == 1024 + + iframe = AudioFrame("s16", "mono", 1024) + iframe.sample_rate = 8000 + iframe.time_base = Fraction(1, 1000) + iframe.pts = 8192 + + oframes = resampler.resample(iframe) + assert len(oframes) == 1 + + oframe = oframes[0] + assert oframe.pts == 65536 + assert oframe.time_base == Fraction(1, 8000) + assert oframe.sample_rate == 8000 + assert oframe.samples == 1024 + + # flush + oframes = resampler.resample(None) + assert len(oframes) == 0 + + +def test_pts_missing_time_base() -> None: + resampler = AudioResampler("s16", "mono", 44100) + + # resample one frame + iframe = AudioFrame("s16", "stereo", 1024) + iframe.sample_rate = 48000 + iframe.pts = 0 + + oframes = resampler.resample(iframe) + assert len(oframes) == 1 + + oframe = oframes[0] + assert oframe.pts == 0 + assert oframe.time_base == Fraction(1, 44100) + assert oframe.sample_rate == 44100 + + # flush + oframes = resampler.resample(None) + assert len(oframes) == 1 + + oframe = oframes[0] + assert oframe.pts == 925 + assert oframe.time_base == Fraction(1, 44100) + assert oframe.sample_rate == 44100 + assert oframe.samples == 16 + + +def test_mismatched_input() -> None: + """ + Consecutive frames must have the same layout, sample format and sample rate. + """ + resampler = AudioResampler("s16", "mono", 44100) + + # resample one frame + iframe = AudioFrame("s16", "stereo", 1024) + iframe.sample_rate = 48000 + resampler.resample(iframe) + + # resample another frame with a sample format + iframe = AudioFrame("s16", "mono", 1024) + iframe.sample_rate = 48000 + with pytest.raises( + ValueError, match="Frame does not match AudioResampler setup." + ) as cm: + resampler.resample(iframe) diff --git a/tests/test_bitstream.py b/tests/test_bitstream.py index 335abac1c..26e54599b 100644 --- a/tests/test_bitstream.py +++ b/tests/test_bitstream.py @@ -1,5 +1,7 @@ from __future__ import annotations +import pytest + import av from av import Packet from av.bitstream import BitStreamFilterContext, bitstream_filters_available @@ -15,89 +17,93 @@ def is_annexb(packet: Packet | bytes | None) -> bool: return data[:3] == b"\0\0\x01" or data[:4] == b"\0\0\0\x01" -class TestBitStreamFilters(TestCase): - def test_filters_availible(self) -> None: - self.assertIn("h264_mp4toannexb", bitstream_filters_available) +def test_filters_availible() -> None: + assert "h264_mp4toannexb" in bitstream_filters_available - def test_filter_chomp(self) -> None: - ctx = BitStreamFilterContext("chomp") - src_packets: tuple[Packet, None] = (Packet(b"\x0012345\0\0\0"), None) - assert bytes(src_packets[0]) == b"\x0012345\0\0\0" +def test_filter_chomp() -> None: + ctx = BitStreamFilterContext("chomp") - result_packets = [] - for p in src_packets: - result_packets.extend(ctx.filter(p)) + src_packets: tuple[Packet, None] = (Packet(b"\x0012345\0\0\0"), None) + assert bytes(src_packets[0]) == b"\x0012345\0\0\0" - assert len(result_packets) == 1 - assert bytes(result_packets[0]) == b"\x0012345" + result_packets = [] + for p in src_packets: + result_packets.extend(ctx.filter(p)) - def test_filter_setts(self) -> None: - ctx = BitStreamFilterContext("setts=pts=N") + assert len(result_packets) == 1 + assert bytes(result_packets[0]) == b"\x0012345" - ctx2 = BitStreamFilterContext(b"setts=pts=N") - del ctx2 - p1 = Packet(b"\0") - p1.pts = 42 - p2 = Packet(b"\0") - p2.pts = 50 - src_packets = [p1, p2, None] +def test_filter_setts() -> None: + ctx = BitStreamFilterContext("setts=pts=N") - result_packets: list[Packet] = [] - for p in src_packets: - result_packets.extend(ctx.filter(p)) + ctx2 = BitStreamFilterContext(b"setts=pts=N") + del ctx2 - assert len(result_packets) == 2 - assert result_packets[0].pts == 0 - assert result_packets[1].pts == 1 + p1 = Packet(b"\0") + p1.pts = 42 + p2 = Packet(b"\0") + p2.pts = 50 + src_packets = [p1, p2, None] - def test_filter_h264_mp4toannexb(self) -> None: - with av.open(fate_suite("h264/interlaced_crop.mp4"), "r") as container: - stream = container.streams.video[0] - ctx = BitStreamFilterContext("h264_mp4toannexb", stream) + result_packets: list[Packet] = [] + for p in src_packets: + result_packets.extend(ctx.filter(p)) - res_packets = [] - for p in container.demux(stream): - self.assertFalse(is_annexb(p)) - res_packets.extend(ctx.filter(p)) + assert len(result_packets) == 2 + assert result_packets[0].pts == 0 + assert result_packets[1].pts == 1 - assert len(res_packets) == stream.frames - for p in res_packets: - assert is_annexb(p) +def test_filter_h264_mp4toannexb() -> None: + with av.open(fate_suite("h264/interlaced_crop.mp4"), "r") as container: + stream = container.streams.video[0] + ctx = BitStreamFilterContext("h264_mp4toannexb", stream) - def test_filter_output_parameters(self) -> None: - with av.open(fate_suite("h264/interlaced_crop.mp4"), "r") as container: - stream = container.streams.video[0] + res_packets = [] + for p in container.demux(stream): + assert not is_annexb(p) + res_packets.extend(ctx.filter(p)) - assert not is_annexb(stream.codec_context.extradata) - ctx = BitStreamFilterContext("h264_mp4toannexb", stream) - assert not is_annexb(stream.codec_context.extradata) - del ctx + assert len(res_packets) == stream.frames - _ = BitStreamFilterContext("h264_mp4toannexb", stream, out_stream=stream) - assert is_annexb(stream.codec_context.extradata) + for p in res_packets: + assert is_annexb(p) - def test_filter_flush(self) -> None: - with av.open(fate_suite("h264/interlaced_crop.mp4"), "r") as container: - stream = container.streams.video[0] - ctx = BitStreamFilterContext("h264_mp4toannexb", stream) - res_packets = [] - for p in container.demux(stream): - res_packets.extend(ctx.filter(p)) - assert len(res_packets) == stream.frames +def test_filter_output_parameters() -> None: + with av.open(fate_suite("h264/interlaced_crop.mp4"), "r") as container: + stream = container.streams.video[0] + + assert not is_annexb(stream.codec_context.extradata) + ctx = BitStreamFilterContext("h264_mp4toannexb", stream) + assert not is_annexb(stream.codec_context.extradata) + del ctx - container.seek(0) - # Without flushing, we expect to get an error: "A non-NULL packet sent after an EOF." - with self.assertRaises(ValueError): - for p in container.demux(stream): - ctx.filter(p) + _ = BitStreamFilterContext("h264_mp4toannexb", stream, out_stream=stream) + assert is_annexb(stream.codec_context.extradata) - ctx.flush() - container.seek(0) + +def test_filter_flush() -> None: + with av.open(fate_suite("h264/interlaced_crop.mp4"), "r") as container: + stream = container.streams.video[0] + ctx = BitStreamFilterContext("h264_mp4toannexb", stream) + + res_packets = [] + for p in container.demux(stream): + res_packets.extend(ctx.filter(p)) + assert len(res_packets) == stream.frames + + container.seek(0) + # Without flushing, we expect to get an error: "A non-NULL packet sent after an EOF." + with pytest.raises(ValueError): for p in container.demux(stream): - res_packets.extend(ctx.filter(p)) + ctx.filter(p) + + ctx.flush() + container.seek(0) + for p in container.demux(stream): + res_packets.extend(ctx.filter(p)) - assert len(res_packets) == stream.frames * 2 + assert len(res_packets) == stream.frames * 2 diff --git a/tests/test_codec.py b/tests/test_codec.py index 357509b2d..70e688435 100644 --- a/tests/test_codec.py +++ b/tests/test_codec.py @@ -1,99 +1,90 @@ +import pytest + from av import AudioFormat, Codec, VideoFormat, codecs_available from av.codec.codec import UnknownCodecError -from .common import TestCase - - -class TestCodecs(TestCase): - def test_codec_bogus(self) -> None: - with self.assertRaises(UnknownCodecError): - Codec("bogus123") - with self.assertRaises(UnknownCodecError): - Codec("bogus123", "w") - - def test_codec_mpeg4_decoder(self) -> None: - c = Codec("mpeg4") - - assert c.name == "mpeg4" - assert c.long_name == "MPEG-4 part 2" - assert c.type == "video" - assert c.id in (12, 13) - assert c.is_decoder - assert not c.is_encoder - assert c.delay - - # audio - assert c.audio_formats is None - assert c.audio_rates is None - - # video - # formats = c.video_formats - # assert formats - # assert isinstance(formats[0], VideoFormat) - # assert any(f.name == "yuv420p" for f in formats) - - assert c.frame_rates is None - - def test_codec_mpeg4_encoder(self) -> None: - c = Codec("mpeg4", "w") - assert c.name == "mpeg4" - assert c.long_name == "MPEG-4 part 2" - assert c.type == "video" - assert c.id in (12, 13) - assert c.is_encoder - assert not c.is_decoder - assert c.delay - - # audio - assert c.audio_formats is None - assert c.audio_rates is None - - # video - formats = c.video_formats - assert formats - assert isinstance(formats[0], VideoFormat) - assert any(f.name == "yuv420p" for f in formats) - assert c.frame_rates is None - - def test_codec_opus_decoder(self) -> None: - c = Codec("opus") - - self.assertEqual(c.name, "opus") - self.assertEqual(c.long_name, "Opus") - assert c.type == "audio" - assert c.is_decoder - assert not c.is_encoder - assert c.delay - - # audio - assert c.audio_formats is None - assert c.audio_rates is None - - # video - assert c.video_formats is None - assert c.frame_rates is None - - def test_codec_opus_encoder(self) -> None: - c = Codec("opus", "w") - assert c.name in ("opus", "libopus") - assert c.long_name in ("Opus", "libopus Opus") - assert c.type == "audio" - assert c.is_encoder - assert not c.is_decoder - assert c.delay - - # audio - formats = c.audio_formats - assert formats - assert isinstance(formats[0], AudioFormat) - assert any(f.name in ("flt", "fltp") for f in formats) - - assert c.audio_rates is not None - assert 48000 in c.audio_rates - - # video - assert c.video_formats is None - assert c.frame_rates is None - - def test_codecs_available(self) -> None: - assert codecs_available + +def test_codec_bogus() -> None: + with pytest.raises(UnknownCodecError): + Codec("bogus123") + with pytest.raises(UnknownCodecError): + Codec("bogus123", "w") + + +def test_codec_mpeg4_decoder() -> None: + c = Codec("mpeg4") + + assert c.name == "mpeg4" + assert c.long_name == "MPEG-4 part 2" + assert c.type == "video" + assert c.id in (12, 13) + assert c.is_decoder + assert not c.is_encoder + assert c.delay + + assert c.audio_formats is None and c.audio_rates is None + + # formats = c.video_formats + # assert formats + # assert isinstance(formats[0], VideoFormat) + # assert any(f.name == "yuv420p" for f in formats) + + assert c.frame_rates is None + + +def test_codec_mpeg4_encoder() -> None: + c = Codec("mpeg4", "w") + assert c.name == "mpeg4" + assert c.long_name == "MPEG-4 part 2" + assert c.type == "video" + assert c.id in (12, 13) + assert c.is_encoder + assert not c.is_decoder + assert c.delay + + assert c.audio_formats is None and c.audio_rates is None + + formats = c.video_formats + assert formats + assert isinstance(formats[0], VideoFormat) + assert any(f.name == "yuv420p" for f in formats) + assert c.frame_rates is None + + +def test_codec_opus_decoder() -> None: + c = Codec("opus") + + assert c.name == "opus" + assert c.long_name == "Opus" + assert c.type == "audio" + assert c.is_decoder + assert not c.is_encoder + assert c.delay + + assert c.audio_formats is None and c.audio_rates is None + assert c.video_formats is None and c.frame_rates is None + + +def test_codec_opus_encoder() -> None: + c = Codec("opus", "w") + assert c.name in ("opus", "libopus") + assert c.long_name in ("Opus", "libopus Opus") + assert c.type == "audio" + assert c.is_encoder + assert not c.is_decoder + assert c.delay + + # audio + formats = c.audio_formats + assert formats + assert isinstance(formats[0], AudioFormat) + assert any(f.name in ("flt", "fltp") for f in formats) + + assert c.audio_rates is not None + assert 48000 in c.audio_rates + + assert c.video_formats is None and c.frame_rates is None + + +def test_codecs_available() -> None: + assert codecs_available diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 2b28730a4..24b49f09a 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -3,7 +3,8 @@ import os from fractions import Fraction from typing import Iterator, TypedDict, overload -from unittest import SkipTest + +import pytest import av from av import ( @@ -116,39 +117,39 @@ def test_decoder_extradata(self): def test_decoder_gop_size(self) -> None: ctx = av.codec.Codec("h264", "r").create("video") - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): ctx.gop_size def test_decoder_timebase(self) -> None: ctx = av.codec.Codec("h264", "r").create() - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): ctx.time_base - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): ctx.time_base = Fraction(1, 25) def test_encoder_extradata(self): ctx = av.codec.Codec("h264", "w").create() assert ctx.extradata is None - self.assertEqual(ctx.extradata_size, 0) + assert ctx.extradata_size == 0 ctx.extradata = b"123" - self.assertEqual(ctx.extradata, b"123") - self.assertEqual(ctx.extradata_size, 3) + assert ctx.extradata == b"123" + assert ctx.extradata_size == 3 def test_encoder_pix_fmt(self): ctx = av.codec.Codec("h264", "w").create() # valid format ctx.pix_fmt = "yuv420p" - self.assertEqual(ctx.pix_fmt, "yuv420p") + assert ctx.pix_fmt == "yuv420p" # invalid format with self.assertRaises(ValueError) as cm: ctx.pix_fmt = "__unknown_pix_fmt" - self.assertEqual(str(cm.exception), "not a pixel format: '__unknown_pix_fmt'") - self.assertEqual(ctx.pix_fmt, "yuv420p") + assert str(cm.exception) == "not a pixel format: '__unknown_pix_fmt'" + assert ctx.pix_fmt == "yuv420p" def test_bits_per_coded_sample(self): with av.open(fate_suite("qtrle/aletrek-rle.mov")) as container: @@ -158,21 +159,20 @@ def test_bits_per_coded_sample(self): for packet in container.demux(stream): for frame in packet.decode(): pass - self.assertEqual(packet.stream.bits_per_coded_sample, 32) + assert packet.stream.bits_per_coded_sample == 32 with av.open(fate_suite("qtrle/aletrek-rle.mov")) as container: stream = container.streams.video[0] stream.bits_per_coded_sample = 31 - with self.assertRaises(av.error.InvalidDataError): - for packet in container.demux(stream): - for frame in packet.decode(): - pass + with pytest.raises(av.error.InvalidDataError): + for frame in container.decode(stream): + pass with av.open(self.sandboxed("output.mov"), "w") as output: stream = output.add_stream("qtrle") - with self.assertRaises(ValueError): + with pytest.raises(ValueError): stream.codec_context.bits_per_coded_sample = 32 def test_parse(self): @@ -200,7 +200,7 @@ def _assert_parse(self, codec_name, path): packets.extend(ctx.parse()) parsed_source = b"".join(bytes(p) for p in packets) - self.assertEqual(len(parsed_source), len(full_source)) + assert len(parsed_source) == len(full_source) assert full_source == parsed_source @@ -218,7 +218,7 @@ def image_sequence_encode(self, codec_name: str) -> None: try: codec = Codec(codec_name, "w") except UnknownCodecError: - raise SkipTest(f"Unknown codec: {codec_name}") + pytest.skip(f"Unknown codec: {codec_name}") container = av.open(fate_suite("h264/interlaced_crop.mp4")) video_stream = container.streams.video[0] @@ -311,7 +311,7 @@ def video_encoding( try: codec = Codec(codec_name, "w") except UnknownCodecError: - raise SkipTest(f"Unknown codec: {codec_name}") + pytest.skip(f"Unknown codec: {codec_name}") container = av.open(fate_suite("h264/interlaced_crop.mp4")) video_stream = container.streams.video[0] @@ -388,7 +388,7 @@ def video_encoding( if codec_name in ("dvvideo", "dnxhd") and all( i == 1 for i in decoded_gop_sizes ): - raise SkipTest("I'm not sure why we skip this actually.") + pytest.skip() for i in decoded_gop_sizes: assert i == gop_size @@ -416,12 +416,12 @@ def _audio_encoding( try: codec = Codec(codec_name, "w") except UnknownCodecError: - raise SkipTest(f"Unknown codec: {codec_name}") + pytest.skip(f"Unknown codec: {codec_name}") ctx = codec.create(kind="audio") if ctx.codec.experimental: - raise SkipTest(f"Experimental codec: {codec_name}") + pytest.skip(f"Experimental codec: {codec_name}") assert ctx.codec.audio_formats sample_fmt = ctx.codec.audio_formats[-1].name diff --git a/tests/test_colorspace.py b/tests/test_colorspace.py index b54229643..7574c147d 100644 --- a/tests/test_colorspace.py +++ b/tests/test_colorspace.py @@ -4,35 +4,35 @@ from .common import fate_suite -class TestColorSpace: - def test_penguin_joke(self) -> None: - container = av.open( - fate_suite("amv/MTV_high_res_320x240_sample_Penguin_Joke_MTV_from_WMV.amv") - ) - stream = container.streams.video[0] - - assert stream.codec_context.color_range == 2 - assert stream.codec_context.color_range == ColorRange.JPEG - - assert stream.codec_context.color_primaries == 2 - assert stream.codec_context.color_trc == 2 - - assert stream.codec_context.colorspace == 5 - assert stream.codec_context.colorspace == Colorspace.ITU601 - - for frame in container.decode(stream): - assert frame.color_range == ColorRange.JPEG # a.k.a "pc" - assert frame.colorspace == Colorspace.ITU601 - return - - def test_sky_timelapse(self) -> None: - container = av.open( - av.datasets.curated("pexels/time-lapse-video-of-night-sky-857195.mp4") - ) - stream = container.streams.video[0] - - assert stream.codec_context.color_range == 1 - assert stream.codec_context.color_range == ColorRange.MPEG - assert stream.codec_context.color_primaries == 1 - assert stream.codec_context.color_trc == 1 - assert stream.codec_context.colorspace == 1 +def test_penguin_joke() -> None: + container = av.open( + fate_suite("amv/MTV_high_res_320x240_sample_Penguin_Joke_MTV_from_WMV.amv") + ) + stream = container.streams.video[0] + + assert stream.codec_context.color_range == 2 + assert stream.codec_context.color_range == ColorRange.JPEG + + assert stream.codec_context.color_primaries == 2 + assert stream.codec_context.color_trc == 2 + + assert stream.codec_context.colorspace == 5 + assert stream.codec_context.colorspace == Colorspace.ITU601 + + for frame in container.decode(stream): + assert frame.color_range == ColorRange.JPEG # a.k.a "pc" + assert frame.colorspace == Colorspace.ITU601 + return + + +def test_sky_timelapse() -> None: + container = av.open( + av.datasets.curated("pexels/time-lapse-video-of-night-sky-857195.mp4") + ) + stream = container.streams.video[0] + + assert stream.codec_context.color_range == 1 + assert stream.codec_context.color_range == ColorRange.MPEG + assert stream.codec_context.color_primaries == 1 + assert stream.codec_context.color_trc == 1 + assert stream.codec_context.colorspace == 1 diff --git a/tests/test_containerformat.py b/tests/test_containerformat.py index 6475cf9cd..43f1eca9f 100644 --- a/tests/test_containerformat.py +++ b/tests/test_containerformat.py @@ -1,57 +1,60 @@ from av import ContainerFormat, formats_available, open -class TestContainerFormats: - def test_matroska(self) -> None: - with open("test.mkv", "w") as container: - assert container.default_video_codec != "none" - assert container.default_audio_codec != "none" - assert container.default_subtitle_codec == "ass" - assert "ass" in container.supported_codecs - - fmt = ContainerFormat("matroska") - assert fmt.is_input and fmt.is_output - assert fmt.name == "matroska" - assert fmt.long_name == "Matroska" - assert "mkv" in fmt.extensions - assert not fmt.no_file - - def test_mov(self) -> None: - with open("test.mov", "w") as container: - assert container.default_video_codec != "none" - assert container.default_audio_codec != "none" - assert container.default_subtitle_codec == "none" - assert "h264" in container.supported_codecs - - fmt = ContainerFormat("mov") - assert fmt.is_input and fmt.is_output - assert fmt.name == "mov" - assert fmt.long_name == "QuickTime / MOV" - assert "mov" in fmt.extensions - assert not fmt.no_file - - def test_gif(self) -> None: - with open("test.gif", "w") as container: - assert container.default_video_codec == "gif" - assert container.default_audio_codec == "none" - assert container.default_subtitle_codec == "none" - assert "gif" in container.supported_codecs - - def test_stream_segment(self) -> None: - # This format goes by two names, check both. - fmt = ContainerFormat("stream_segment") - assert not fmt.is_input and fmt.is_output - assert fmt.name == "stream_segment" - assert fmt.long_name == "streaming segment muxer" - assert fmt.extensions == set() - assert fmt.no_file - - fmt = ContainerFormat("ssegment") - assert not fmt.is_input and fmt.is_output - assert fmt.name == "ssegment" - assert fmt.long_name == "streaming segment muxer" - assert fmt.extensions == set() - assert fmt.no_file - - def test_formats_available(self) -> None: - assert formats_available +def test_matroska() -> None: + with open("test.mkv", "w") as container: + assert container.default_video_codec != "none" + assert container.default_audio_codec != "none" + assert container.default_subtitle_codec == "ass" + assert "ass" in container.supported_codecs + + fmt = ContainerFormat("matroska") + assert fmt.is_input and fmt.is_output + assert fmt.name == "matroska" + assert fmt.long_name == "Matroska" + assert "mkv" in fmt.extensions + assert not fmt.no_file + + +def test_mov() -> None: + with open("test.mov", "w") as container: + assert container.default_video_codec != "none" + assert container.default_audio_codec != "none" + assert container.default_subtitle_codec == "none" + assert "h264" in container.supported_codecs + + fmt = ContainerFormat("mov") + assert fmt.is_input and fmt.is_output + assert fmt.name == "mov" + assert fmt.long_name == "QuickTime / MOV" + assert "mov" in fmt.extensions + assert not fmt.no_file + + +def test_gif() -> None: + with open("test.gif", "w") as container: + assert container.default_video_codec == "gif" + assert container.default_audio_codec == "none" + assert container.default_subtitle_codec == "none" + assert "gif" in container.supported_codecs + + +def test_stream_segment() -> None: + # This format goes by two names, check both. + fmt = ContainerFormat("stream_segment") + assert not fmt.is_input and fmt.is_output + assert fmt.name == "stream_segment" + assert fmt.long_name == "streaming segment muxer" + assert fmt.extensions == set() + assert fmt.no_file + + fmt = ContainerFormat("ssegment") + assert not fmt.is_input and fmt.is_output + assert fmt.name == "ssegment" + assert fmt.long_name == "streaming segment muxer" + assert fmt.extensions == set() + assert fmt.no_file + + +def test_formats_available() -> None: + assert formats_available diff --git a/tests/test_deprecation.py b/tests/test_deprecation.py index f8857ab73..f1b0b5d4b 100644 --- a/tests/test_deprecation.py +++ b/tests/test_deprecation.py @@ -2,48 +2,42 @@ from av import deprecation -from .common import TestCase +def test_method() -> None: + class Example: + def __init__(self, x: int = 100) -> None: + self.x = x -class TestDeprecations(TestCase): - def test_method(self): - class Example: - def __init__(self, x=100): - self.x = x + @deprecation.method + def foo(self, a: int, b: int) -> int: + return self.x + a + b - @deprecation.method - def foo(self, a, b): - return self.x + a + b + obj = Example() - obj = Example() + with warnings.catch_warnings(record=True) as captured: + assert obj.foo(20, b=3) == 123 + assert "Example.foo is deprecated" in str(captured[0].message) - with warnings.catch_warnings(record=True) as captured: - self.assertEqual(obj.foo(20, b=3), 123) - self.assertIn("Example.foo is deprecated", captured[0].message.args[0]) - def test_renamed_attr(self): - class Example: - new_value = "foo" - old_value = deprecation.renamed_attr("new_value") +def test_renamed_attr() -> None: + class Example: + new_value = "foo" + old_value = deprecation.renamed_attr("new_value") - def new_func(self, a, b): - return a + b + def new_func(self, a: int, b: int) -> int: + return a + b - old_func = deprecation.renamed_attr("new_func") + old_func = deprecation.renamed_attr("new_func") - obj = Example() + obj = Example() - with warnings.catch_warnings(record=True) as captured: - self.assertEqual(obj.old_value, "foo") - self.assertIn( - "Example.old_value is deprecated", captured[0].message.args[0] - ) + with warnings.catch_warnings(record=True) as captured: + assert obj.old_value == "foo" + assert "Example.old_value is deprecated" in str(captured[0].message) - obj.old_value = "bar" - self.assertIn( - "Example.old_value is deprecated", captured[1].message.args[0] - ) + obj.old_value = "bar" + assert "Example.old_value is deprecated" in str(captured[1].message) - with warnings.catch_warnings(record=True) as captured: - self.assertEqual(obj.old_func(1, 2), 3) - self.assertIn("Example.old_func is deprecated", captured[0].message.args[0]) + with warnings.catch_warnings(record=True) as captured: + assert obj.old_func(1, 2) == 3 + assert "Example.old_func is deprecated" in str(captured[0].message) diff --git a/tests/test_dictionary.py b/tests/test_dictionary.py index a1c2f80d8..e577c7a07 100644 --- a/tests/test_dictionary.py +++ b/tests/test_dictionary.py @@ -1,18 +1,17 @@ -from av.dictionary import Dictionary +import pytest -from .common import TestCase +from av.dictionary import Dictionary -class TestDictionary(TestCase): - def test_basics(self): - d = Dictionary() - d["key"] = "value" +def test_dictionary() -> None: + d = Dictionary() + d["key"] = "value" - self.assertEqual(d["key"], "value") - self.assertIn("key", d) - self.assertEqual(len(d), 1) - self.assertEqual(list(d), ["key"]) + assert d["key"] == "value" + assert "key" in d + assert len(d) == 1 + assert list(d) == ["key"] - self.assertEqual(d.pop("key"), "value") - self.assertRaises(KeyError, d.pop, "key") - self.assertEqual(len(d), 0) + assert d.pop("key") == "value" + pytest.raises(KeyError, d.pop, "key") + assert len(d) == 0 diff --git a/tests/test_encode.py b/tests/test_encode.py index 40b17cd73..e2ab1ba2f 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -70,11 +70,8 @@ def assert_rgb_rotate( ) -> None: # Now inspect it a little. assert len(input_.streams) == 1 - if is_dash: - self.assertTrue(input_.metadata.get("Title") == "container", input_.metadata) - else: - self.assertEqual(input_.metadata.get("title"), "container", input_.metadata) - self.assertEqual(input_.metadata.get("key"), None) + assert input_.metadata.get("Title" if is_dash else "title") == "container" + assert input_.metadata.get("key") is None stream = input_.streams[0] @@ -93,7 +90,6 @@ def assert_rgb_rotate( expected_id = 1 # actual stream properties - self.assertIsInstance(stream, VideoStream) assert isinstance(stream, VideoStream) assert stream.average_rate == expected_average_rate assert stream.base_rate == 24 @@ -119,7 +115,7 @@ class TestBasicVideoEncoding(TestCase): def test_default_options(self) -> None: with av.open(self.sandboxed("output.mov"), "w") as output: stream = output.add_stream("mpeg4") - self.assertIn(stream, output.streams.video) + assert stream in output.streams.video assert stream.average_rate == Fraction(24, 1) assert stream.time_base is None @@ -145,7 +141,7 @@ def test_encoding_with_pts(self) -> None: with av.open(path, "w") as output: stream = output.add_stream("h264", 24) - self.assertIn(stream, output.streams.video) + assert stream in output.streams.video stream.width = WIDTH stream.height = HEIGHT stream.pix_fmt = "yuv420p" @@ -176,7 +172,7 @@ class TestBasicAudioEncoding(TestCase): def test_default_options(self) -> None: with av.open(self.sandboxed("output.mov"), "w") as output: stream = output.add_stream("mp2") - self.assertIn(stream, output.streams.audio) + assert stream in output.streams.audio assert stream.time_base is None # codec context properties @@ -196,7 +192,7 @@ def test_transcode(self) -> None: sample_fmt = "s16" stream = output.add_stream("mp2", sample_rate) - self.assertIn(stream, output.streams.audio) + assert stream in output.streams.audio ctx = stream.codec_context ctx.sample_rate = sample_rate @@ -215,9 +211,7 @@ def test_transcode(self) -> None: with av.open(path) as container: assert len(container.streams) == 1 - self.assertEqual( - container.metadata.get("title"), "container", container.metadata - ) + assert container.metadata.get("title") == "container" assert container.metadata.get("key") is None assert isinstance(container.streams[0], AudioStream) @@ -301,7 +295,7 @@ def test_stream_audio_resample(self) -> None: def test_set_id_and_time_base(self) -> None: with av.open(self.sandboxed("output.mov"), "w") as output: stream = output.add_stream("mp2") - self.assertIn(stream, output.streams.audio) + assert stream in output.streams.audio # set id assert stream.id == 0 diff --git a/tests/test_enums.py b/tests/test_enums.py index e96956eca..64b76d9eb 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -1,225 +1,223 @@ import pickle -from av.enum import EnumType, define_enum +import pytest -from .common import TestCase +from av.enum import EnumType, define_enum -# This must be at the top-level. PickleableFooBar = define_enum("PickleableFooBar", __name__, [("FOO", 1)]) -class TestEnums(TestCase): - def define_foobar(self, **kwargs): - return define_enum("Foobar", __name__, (("FOO", 1), ("BAR", 2)), **kwargs) +def define_foobar(is_flags: bool = False): + return define_enum("Foobar", __name__, (("FOO", 1), ("BAR", 2)), is_flags=is_flags) + + +def test_basics(): + cls = define_foobar() + + assert isinstance(cls, EnumType) + + foo = cls.FOO + + assert isinstance(foo, cls) + assert foo.name == "FOO" and foo.value == 1 + assert not isinstance(foo, PickleableFooBar) + + +def test_access(): + cls = define_foobar() + foo1 = cls.FOO + foo2 = cls["FOO"] + foo3 = cls[1] + foo4 = cls[foo1] + assert foo1 is foo2 + assert foo1 is foo3 + assert foo1 is foo4 + + assert foo1 in cls and "FOO" in cls and 1 in cls + + pytest.raises(KeyError, lambda: cls["not a foo"]) + pytest.raises(KeyError, lambda: cls[10]) + pytest.raises(TypeError, lambda: cls[()]) + + assert cls.get("FOO") == foo1 + assert cls.get("not a foo") is None + + +def test_casting(): + cls = define_foobar() + foo = cls.FOO + + assert repr(foo) == "" + + str_foo = str(foo) + assert isinstance(str_foo, str) and str_foo == "FOO" + + int_foo = int(foo) + assert isinstance(int_foo, int) and int_foo == 1 + + +def test_iteration(): + cls = define_foobar() + assert list(cls) == [cls.FOO, cls.BAR] + + +def test_equality(): + cls = define_foobar() + foo = cls.FOO + bar = cls.BAR + + assert foo == "FOO" and foo == 1 and foo == foo + assert foo != "BAR" and foo != 2 and foo != bar - def test_basics(self): - cls = self.define_foobar() + pytest.raises(ValueError, lambda: foo == "not a foo") + pytest.raises(ValueError, lambda: foo == 10) + pytest.raises(TypeError, lambda: foo == ()) - assert isinstance(cls, EnumType) - foo = cls.FOO +def test_as_key(): + cls = define_foobar() + foo = cls.FOO - assert isinstance(foo, cls) - assert foo.name == "FOO" and foo.value == 1 - assert not isinstance(foo, PickleableFooBar) + d = {foo: "value"} + assert d[foo] == "value" + assert d.get("FOO") is None + assert d.get(1) is None - def test_access(self): - cls = self.define_foobar() - foo1 = cls.FOO - foo2 = cls["FOO"] - foo3 = cls[1] - foo4 = cls[foo1] - assert foo1 is foo2 - assert foo1 is foo3 - assert foo1 is foo4 - assert foo1 in cls - assert "FOO" in cls - assert 1 in cls +def test_pickleable(): + cls = PickleableFooBar + foo = cls.FOO - self.assertRaises(KeyError, lambda: cls["not a foo"]) - self.assertRaises(KeyError, lambda: cls[10]) - self.assertRaises(TypeError, lambda: cls[()]) + enc = pickle.dumps(foo) - self.assertEqual(cls.get("FOO"), foo1) - assert cls.get("not a foo") is None + foo2 = pickle.loads(enc) - def test_casting(self): - cls = self.define_foobar() - foo = cls.FOO + assert foo is foo2 - self.assertEqual(repr(foo), "") - str_foo = str(foo) - assert isinstance(str_foo, str) - assert str_foo == "FOO" +def test_create_unknown(): + cls = define_foobar() + baz = cls.get(3, create=True) - int_foo = int(foo) - assert isinstance(int_foo, int) - assert int_foo == 1 + assert baz.name == "FOOBAR_3" + assert baz.value == 3 - def test_iteration(self): - cls = self.define_foobar() - self.assertEqual(list(cls), [cls.FOO, cls.BAR]) - def test_equality(self): - cls = self.define_foobar() - foo = cls.FOO - bar = cls.BAR +def test_multiple_names(): + cls = define_enum( + "FFooBBar", + __name__, + (("FOO", 1), ("F", 1), ("BAR", 2), ("B", 2)), + ) - self.assertEqual(foo, "FOO") - self.assertEqual(foo, 1) - self.assertEqual(foo, foo) - self.assertNotEqual(foo, "BAR") - self.assertNotEqual(foo, 2) - self.assertNotEqual(foo, bar) - - self.assertRaises(ValueError, lambda: foo == "not a foo") - self.assertRaises(ValueError, lambda: foo == 10) - self.assertRaises(TypeError, lambda: foo == ()) + assert cls.F is cls.FOO - def test_as_key(self): - cls = self.define_foobar() - foo = cls.FOO + assert cls.F.name == "FOO" + assert cls.F.name != "F" # This is actually the string. - d = {foo: "value"} - self.assertEqual(d[foo], "value") - assert d.get("FOO") is None - assert d.get(1) is None + assert cls.F == "FOO" + assert cls.F == "F" + assert cls.F != "BAR" + assert cls.F != "B" + pytest.raises(ValueError, lambda: cls.F == "x") - def test_pickleable(self): - cls = PickleableFooBar - foo = cls.FOO - enc = pickle.dumps(foo) +def test_flag_basics(): + cls = define_enum( + "FoobarAllFlags", + __name__, + {"FOO": 1, "BAR": 2, "FOOBAR": 3}.items(), + is_flags=True, + ) + foo = cls.FOO + bar = cls.BAR - foo2 = pickle.loads(enc) + foobar = foo | bar + assert foobar is cls.FOOBAR - assert foo is foo2 + foo2 = foobar & foo + assert foo2 is foo - def test_create_unknown(self): - cls = self.define_foobar() - baz = cls.get(3, create=True) + bar2 = foobar ^ foo + assert bar2 is bar - self.assertEqual(baz.name, "FOOBAR_3") - self.assertEqual(baz.value, 3) + bar3 = foobar & ~foo + assert bar3 is bar - def test_multiple_names(self): - cls = define_enum( - "FFooBBar", - __name__, - ( - ("FOO", 1), - ("F", 1), - ("BAR", 2), - ("B", 2), - ), - ) + x = cls.FOO + x |= cls.BAR + assert x is cls.FOOBAR - assert cls.F is cls.FOO - - self.assertEqual(cls.F.name, "FOO") - self.assertNotEqual(cls.F.name, "F") # This is actually the string. - - self.assertEqual(cls.F, "FOO") - self.assertEqual(cls.F, "F") - self.assertNotEqual(cls.F, "BAR") - self.assertNotEqual(cls.F, "B") - self.assertRaises(ValueError, lambda: cls.F == "x") - - def test_flag_basics(self): - cls = define_enum( - "FoobarAllFlags", - __name__, - dict(FOO=1, BAR=2, FOOBAR=3).items(), - is_flags=True, - ) - foo = cls.FOO - bar = cls.BAR + x = cls.FOOBAR + x &= cls.FOO + assert x is cls.FOO - foobar = foo | bar - assert foobar is cls.FOOBAR - foo2 = foobar & foo - assert foo2 is foo +def test_multi_flags_basics(): + cls = define_foobar(is_flags=True) - bar2 = foobar ^ foo - assert bar2 is bar + foo = cls.FOO + bar = cls.BAR + foobar = foo | bar + assert foobar.name == "FOO|BAR" + assert foobar.value == 3 + assert foobar.flags == (foo, bar) - bar3 = foobar & ~foo - assert bar3 is bar + foobar2 = foo | bar + foobar3 = cls[3] + foobar4 = cls[foobar] - x = cls.FOO - x |= cls.BAR - assert x is cls.FOOBAR + assert foobar is foobar2 + assert foobar is foobar3 + assert foobar is foobar4 - x = cls.FOOBAR - x &= cls.FOO - assert x is cls.FOO + pytest.raises(KeyError, lambda: cls["FOO|BAR"]) - def test_multi_flags_basics(self): - cls = self.define_foobar(is_flags=True) - - foo = cls.FOO - bar = cls.BAR - foobar = foo | bar - self.assertEqual(foobar.name, "FOO|BAR") - self.assertEqual(foobar.value, 3) - self.assertEqual(foobar.flags, (foo, bar)) + assert len(cls) == 2 # It didn't get bigger + assert list(cls) == [foo, bar] - foobar2 = foo | bar - foobar3 = cls[3] - foobar4 = cls[foobar] - assert foobar is foobar2 - assert foobar is foobar3 - assert foobar is foobar4 +def test_multi_flags_create_missing(): + cls = define_foobar(is_flags=True) - self.assertRaises(KeyError, lambda: cls["FOO|BAR"]) + foobar = cls[3] + assert foobar is cls.FOO | cls.BAR - self.assertEqual(len(cls), 2) # It didn't get bigger - self.assertEqual(list(cls), [foo, bar]) + pytest.raises(KeyError, lambda: cls[4]) # Not FOO or BAR + pytest.raises(KeyError, lambda: cls[7]) # FOO and BAR and missing flag. - def test_multi_flags_create_missing(self): - cls = self.define_foobar(is_flags=True) - foobar = cls[3] - assert foobar is cls.FOO | cls.BAR +def test_properties(): + Flags = define_foobar(is_flags=True) + foobar = Flags.FOO | Flags.BAR - self.assertRaises(KeyError, lambda: cls[4]) # Not FOO or BAR - self.assertRaises(KeyError, lambda: cls[7]) # FOO and BAR and missing flag. + class Class: + def __init__(self, value): + self.value = Flags[value].value - def test_properties(self): - Flags = self.define_foobar(is_flags=True) - foobar = Flags.FOO | Flags.BAR + @Flags.property + def flags(self): + return self.value - class Class: - def __init__(self, value): - self.value = Flags[value].value + @flags.setter + def flags(self, value): + self.value = value - @Flags.property - def flags(self): - return self.value + foo = flags.flag_property("FOO") + bar = flags.flag_property("BAR") - @flags.setter - def flags(self, value): - self.value = value - - foo = flags.flag_property("FOO") - bar = flags.flag_property("BAR") - - obj = Class("FOO") + obj = Class("FOO") - assert obj.flags is Flags.FOO - self.assertTrue(obj.foo) - self.assertFalse(obj.bar) + assert obj.flags is Flags.FOO + assert obj.foo + assert not obj.bar - obj.bar = True - assert obj.flags is foobar - self.assertTrue(obj.foo) - self.assertTrue(obj.bar) + obj.bar = True + assert obj.flags is foobar + assert obj.foo + assert obj.bar - obj.foo = False - assert obj.flags is Flags.BAR - self.assertFalse(obj.foo) - self.assertTrue(obj.bar) + obj.foo = False + assert obj.flags is Flags.BAR + assert not obj.foo + assert obj.bar diff --git a/tests/test_errors.py b/tests/test_errors.py index fcd08845e..5f7440402 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -1,61 +1,63 @@ import errno -import traceback +from traceback import format_exception_only import av -from .common import TestCase, is_windows - - -class TestErrorBasics(TestCase): - def test_stringify(self) -> None: - for cls in (av.ValueError, av.FileNotFoundError, av.DecoderNotFoundError): - e = cls(1, "foo") - assert f"{e}" == "[Errno 1] foo" - assert f"{e!r}" == f"{cls.__name__}(1, 'foo')" - assert ( - traceback.format_exception_only(cls, e)[-1] - == f"av.error.{cls.__name__}: [Errno 1] foo\n" - ) - - for cls in (av.ValueError, av.FileNotFoundError, av.DecoderNotFoundError): - e = cls(1, "foo", "bar.txt") - assert f"{e}" == "[Errno 1] foo: 'bar.txt'" - assert f"{e!r}" == f"{cls.__name__}(1, 'foo', 'bar.txt')" - assert ( - traceback.format_exception_only(cls, e)[-1] - == f"av.error.{cls.__name__}: [Errno 1] foo: 'bar.txt'\n" +from .common import is_windows + + +def test_stringify() -> None: + for cls in (av.ValueError, av.FileNotFoundError, av.DecoderNotFoundError): + e = cls(1, "foo") + assert f"{e}" == "[Errno 1] foo" + assert f"{e!r}" == f"{cls.__name__}(1, 'foo')" + assert ( + format_exception_only(cls, e)[-1] + == f"av.error.{cls.__name__}: [Errno 1] foo\n" + ) + + for cls in (av.ValueError, av.FileNotFoundError, av.DecoderNotFoundError): + e = cls(1, "foo", "bar.txt") + assert f"{e}" == "[Errno 1] foo: 'bar.txt'" + assert f"{e!r}" == f"{cls.__name__}(1, 'foo', 'bar.txt')" + assert ( + format_exception_only(cls, e)[-1] + == f"av.error.{cls.__name__}: [Errno 1] foo: 'bar.txt'\n" + ) + + +def test_bases() -> None: + assert issubclass(av.ValueError, ValueError) + assert issubclass(av.ValueError, av.FFmpegError) + + assert issubclass(av.FileNotFoundError, FileNotFoundError) + assert issubclass(av.FileNotFoundError, OSError) + assert issubclass(av.FileNotFoundError, av.FFmpegError) + + +def test_filenotfound(): + """Catch using builtin class on Python 3.3""" + try: + av.open("does not exist") + except FileNotFoundError as e: + assert e.errno == errno.ENOENT + if is_windows: + assert e.strerror in ( + "Error number -2 occurred", + "No such file or directory", ) - - def test_bases(self) -> None: - assert issubclass(av.ValueError, ValueError) - assert issubclass(av.ValueError, av.FFmpegError) - - assert issubclass(av.FileNotFoundError, FileNotFoundError) - assert issubclass(av.FileNotFoundError, OSError) - assert issubclass(av.FileNotFoundError, av.FFmpegError) - - def test_filenotfound(self): - """Catch using builtin class on Python 3.3""" - try: - av.open("does not exist") - except FileNotFoundError as e: - assert e.errno == errno.ENOENT - if is_windows: - self.assertTrue( - e.strerror - in ["Error number -2 occurred", "No such file or directory"] - ) - else: - assert e.strerror == "No such file or directory" - assert e.filename == "does not exist" - else: - self.fail("no exception raised") - - def test_buffertoosmall(self): - """Throw an exception from an enum.""" - try: - av.error.err_check(-av.error.BUFFER_TOO_SMALL.value) - except av.BufferTooSmallError as e: - assert e.errno == av.error.BUFFER_TOO_SMALL.value else: - self.fail("no exception raised") + assert e.strerror == "No such file or directory" + assert e.filename == "does not exist" + else: + assert False, "No exception raised!" + + +def test_buffertoosmall(): + """Throw an exception from an enum.""" + try: + av.error.err_check(-av.error.BUFFER_TOO_SMALL.value) + except av.BufferTooSmallError as e: + assert e.errno == av.error.BUFFER_TOO_SMALL.value + else: + assert False, "No exception raised!" diff --git a/tests/test_python_io.py b/tests/test_python_io.py index 9f5e55999..8324b870b 100644 --- a/tests/test_python_io.py +++ b/tests/test_python_io.py @@ -1,19 +1,43 @@ +import functools +import types from io import BytesIO from unittest import SkipTest import av -from .common import ( - MethodLogger, - TestCase, - fate_png, - fate_suite, - has_pillow, - run_in_sandbox, -) +from .common import TestCase, fate_png, fate_suite, has_pillow, run_in_sandbox from .test_encode import assert_rgb_rotate, write_rgb_rotate +class MethodLogger: + def __init__(self, obj): + self._obj = obj + self._log = [] + + def __getattr__(self, name): + value = getattr(self._obj, name) + if isinstance( + value, + ( + types.MethodType, + types.FunctionType, + types.BuiltinFunctionType, + types.BuiltinMethodType, + ), + ): + return functools.partial(self._method, name, value) + else: + self._log.append(("__getattr__", (name,), {})) + return value + + def _method(self, name, meth, *args, **kwargs): + self._log.append((name, args, kwargs)) + return meth(*args, **kwargs) + + def _filter(self, type_): + return [log for log in self._log if log[0] == type_] + + class BrokenBuffer(BytesIO): """ Buffer which can be "broken" to simulate an I/O error. @@ -103,7 +127,7 @@ def io_open(self, url, flags, options): elif (flags & 2) == 2: mode = "wb" else: - raise RuntimeError("Unsupported io open mode {}".format(flags)) + raise RuntimeError(f"Unsupported io open mode {flags}") return MethodLogger(open(url, mode)) @@ -137,9 +161,10 @@ def test_reading_from_write_readonly(self): buf = WriteOnlyPipe(fh.read()) with self.assertRaises(ValueError) as cm: self.read(buf, seekable=False) - self.assertEqual( - str(cm.exception), - "File object has no read() method, or readable() returned False.", + + assert ( + str(cm.exception) + == "File object has no read() method, or readable() returned False." ) def test_writing_to_buffer(self): @@ -199,13 +224,13 @@ def test_writing_to_custom_io_dash(self): all_write = all( method_log._filter("write") for method_log in wrapped_custom_io._method_log ) - self.assertTrue(all_write) + assert all_write # Check that all files were closed all_closed = all( method_log._filter("close") for method_log in wrapped_custom_io._method_log ) - self.assertTrue(all_closed) + assert all_closed # Check contents. # Note that the dash demuxer doesn't support custom I/O. diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 60cf78035..7d7180ebf 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -3,114 +3,135 @@ from unittest import SkipTest import numpy +import pytest import av from av import VideoFrame -from .common import TestCase, fate_png, fate_suite, has_pillow - - -class TestOpaque: - def test_opaque(self) -> None: - with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: - video_stream = container.streams.video[0] - video_stream.codec_context.copy_opaque = True - for packet_idx, packet in enumerate(container.demux()): - packet.opaque = (time.time(), packet_idx) - for frame in packet.decode(): - assert isinstance(frame, av.frame.Frame) - - if frame.opaque is None: - continue - - assert type(frame.opaque) is tuple and len(frame.opaque) == 2 - - -class TestVideoFrameConstructors(TestCase): - def test_invalid_pixel_format(self): - with self.assertRaises(ValueError) as cm: - VideoFrame(640, 480, "__unknown_pix_fmt") - assert str(cm.exception) == "not a pixel format: '__unknown_pix_fmt'" - - def test_null_constructor(self) -> None: - frame = VideoFrame() - assert frame.width == 0 - assert frame.height == 0 - assert frame.format.name == "yuv420p" - - def test_manual_yuv_constructor(self) -> None: - frame = VideoFrame(640, 480, "yuv420p") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "yuv420p" - - def test_manual_rgb_constructor(self) -> None: - frame = VideoFrame(640, 480, "rgb24") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "rgb24" - - -class TestVideoFramePlanes: - def test_null_planes(self) -> None: - frame = VideoFrame() # yuv420p - assert len(frame.planes) == 0 - - def test_yuv420p_planes(self) -> None: - frame = VideoFrame(640, 480, "yuv420p") - assert len(frame.planes) == 3 - assert frame.planes[0].width == 640 - assert frame.planes[0].height == 480 - assert frame.planes[0].line_size == 640 - assert frame.planes[0].buffer_size == 640 * 480 - for i in range(1, 3): - assert frame.planes[i].width == 320 - assert frame.planes[i].height == 240 - assert frame.planes[i].line_size == 320 - assert frame.planes[i].buffer_size == 320 * 240 - - def test_yuv420p_planes_align(self): - # If we request 8-byte alignment for a width which is not a multiple of 8, - # the line sizes are larger than the plane width. - frame = VideoFrame(318, 238, "yuv420p") - assert len(frame.planes) == 3 - assert frame.planes[0].width == 318 - assert frame.planes[0].height == 238 - assert frame.planes[0].line_size == 320 - assert frame.planes[0].buffer_size == 320 * 238 - for i in range(1, 3): - assert frame.planes[i].width == 159 - assert frame.planes[i].height == 119 - assert frame.planes[i].line_size == 160 - assert frame.planes[i].buffer_size == 160 * 119 - - def test_rgb24_planes(self): - frame = VideoFrame(640, 480, "rgb24") - assert len(frame.planes) == 1 - assert frame.planes[0].width == 640 - assert frame.planes[0].height == 480 - assert frame.planes[0].line_size == 640 * 3 - assert frame.planes[0].buffer_size == 640 * 480 * 3 - - -class TestVideoFrameBuffers(TestCase): - def test_memoryview_read(self): - frame = VideoFrame(640, 480, "rgb24") - frame.planes[0].update(b"01234" + (b"x" * (640 * 480 * 3 - 5))) - mem = memoryview(frame.planes[0]) - assert mem.ndim == 1 - assert mem.shape == (640 * 480 * 3,) - self.assertFalse(mem.readonly) - assert mem[1] == 49 - assert mem[:7] == b"01234xx" - mem[1] = 46 - assert mem[:7] == b"0.234xx" +from .common import ( + TestCase, + assertImagesAlmostEqual, + assertNdarraysEqual, + fate_png, + fate_suite, + has_pillow, +) + + +def assertPixelValue16(plane, expected, byteorder: str) -> None: + view = memoryview(plane) + if byteorder == "big": + assert view[0] == (expected >> 8 & 0xFF) + assert view[1] == expected & 0xFF + else: + assert view[0] == expected & 0xFF + assert view[1] == (expected >> 8 & 0xFF) + + +def test_opaque() -> None: + with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: + video_stream = container.streams.video[0] + video_stream.codec_context.copy_opaque = True + for packet_idx, packet in enumerate(container.demux()): + packet.opaque = (time.time(), packet_idx) + for frame in packet.decode(): + assert isinstance(frame, av.frame.Frame) + + if frame.opaque is None: + continue + + assert type(frame.opaque) is tuple and len(frame.opaque) == 2 + + +def test_invalid_pixel_format(): + with pytest.raises( + ValueError, match="not a pixel format: '__unknown_pix_fmt'" + ) as cm: + VideoFrame(640, 480, "__unknown_pix_fmt") + + +def test_null_constructor() -> None: + frame = VideoFrame() + assert frame.width == 0 + assert frame.height == 0 + assert frame.format.name == "yuv420p" + + +def test_manual_yuv_constructor() -> None: + frame = VideoFrame(640, 480, "yuv420p") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "yuv420p" + + +def test_manual_rgb_constructor() -> None: + frame = VideoFrame(640, 480, "rgb24") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "rgb24" + + +def test_null_planes() -> None: + frame = VideoFrame() # yuv420p + assert len(frame.planes) == 0 + + +def test_yuv420p_planes() -> None: + frame = VideoFrame(640, 480, "yuv420p") + assert len(frame.planes) == 3 + assert frame.planes[0].width == 640 + assert frame.planes[0].height == 480 + assert frame.planes[0].line_size == 640 + assert frame.planes[0].buffer_size == 640 * 480 + for i in range(1, 3): + assert frame.planes[i].width == 320 + assert frame.planes[i].height == 240 + assert frame.planes[i].line_size == 320 + assert frame.planes[i].buffer_size == 320 * 240 + + +def test_yuv420p_planes_align(): + # If we request 8-byte alignment for a width which is not a multiple of 8, + # the line sizes are larger than the plane width. + frame = VideoFrame(318, 238, "yuv420p") + assert len(frame.planes) == 3 + assert frame.planes[0].width == 318 + assert frame.planes[0].height == 238 + assert frame.planes[0].line_size == 320 + assert frame.planes[0].buffer_size == 320 * 238 + for i in range(1, 3): + assert frame.planes[i].width == 159 + assert frame.planes[i].height == 119 + assert frame.planes[i].line_size == 160 + assert frame.planes[i].buffer_size == 160 * 119 + + +def test_rgb24_planes(): + frame = VideoFrame(640, 480, "rgb24") + assert len(frame.planes) == 1 + assert frame.planes[0].width == 640 + assert frame.planes[0].height == 480 + assert frame.planes[0].line_size == 640 * 3 + assert frame.planes[0].buffer_size == 640 * 480 * 3 + + +def test_memoryview_read(): + frame = VideoFrame(640, 480, "rgb24") + frame.planes[0].update(b"01234" + (b"x" * (640 * 480 * 3 - 5))) + mem = memoryview(frame.planes[0]) + assert mem.ndim == 1 + assert mem.shape == (640 * 480 * 3,) + assert not mem.readonly + assert mem[1] == 49 + assert mem[:7] == b"01234xx" + mem[1] = 46 + assert mem[:7] == b"0.234xx" class TestVideoFrameImage(TestCase): def setUp(self): if not has_pillow: - raise SkipTest() + pytest.skip() def test_roundtrip(self): import PIL.Image as Image @@ -119,14 +140,10 @@ def test_roundtrip(self): frame = VideoFrame.from_image(image) img = frame.to_image() img.save(self.sandboxed("roundtrip-high.jpg")) - self.assertImagesAlmostEqual(image, img) + assertImagesAlmostEqual(image, img) def test_to_image_rgb24(self): - sizes = [ - (318, 238), - (320, 240), - (500, 500), - ] + sizes = [(318, 238), (320, 240), (500, 500)] for width, height in sizes: frame = VideoFrame(width, height, format="rgb24") @@ -152,500 +169,488 @@ def test_to_image_rgb24(self): assert img.size == (width, height) assert img.tobytes() == expected - def test_to_image_with_dimensions(self): - frame = VideoFrame(640, 480, format="rgb24") - - img = frame.to_image(width=320, height=240) - assert img.size == (320, 240) - - -class TestVideoFrameNdarray(TestCase): - def assertPixelValue16(self, plane, expected, byteorder: str): - view = memoryview(plane) - if byteorder == "big": - assert view[0] == (expected >> 8 & 0xFF) - assert view[1] == expected & 0xFF - else: - assert view[0] == expected & 0xFF - assert view[1] == (expected >> 8 & 0xFF) - - def test_basic_to_ndarray(self): - frame = VideoFrame(640, 480, "rgb24") - array = frame.to_ndarray() - assert array.shape == (480, 640, 3) - - def test_ndarray_gray(self): - array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) - for format in ["gray", "gray8"]: - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "gray" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_gray_align(self): - array = numpy.random.randint(0, 256, size=(238, 318), dtype=numpy.uint8) - for format in ["gray", "gray8"]: - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 318 - assert frame.height == 238 - assert frame.format.name == "gray" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_rgb(self): - array = numpy.random.randint(0, 256, size=(480, 640, 3), dtype=numpy.uint8) - for format in ["rgb24", "bgr24"]: - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == format - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_rgb_align(self): - array = numpy.random.randint(0, 256, size=(238, 318, 3), dtype=numpy.uint8) - for format in ["rgb24", "bgr24"]: - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 318 - assert frame.height == 238 - assert frame.format.name == format - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_rgba(self): - array = numpy.random.randint(0, 256, size=(480, 640, 4), dtype=numpy.uint8) - for format in ["argb", "rgba", "abgr", "bgra"]: - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == format - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_rgba_align(self): - array = numpy.random.randint(0, 256, size=(238, 318, 4), dtype=numpy.uint8) - for format in ["argb", "rgba", "abgr", "bgra"]: - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 318 - assert frame.height == 238 - assert frame.format.name == format - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_gbrp(self): - array = numpy.random.randint(0, 256, size=(480, 640, 3), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray(array, format="gbrp") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "gbrp" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_gbrp_align(self): - array = numpy.random.randint(0, 256, size=(238, 318, 3), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray(array, format="gbrp") - assert frame.width == 318 - assert frame.height == 238 - assert frame.format.name == "gbrp" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_gbrp10(self): - array = numpy.random.randint(0, 1024, size=(480, 640, 3), dtype=numpy.uint16) - for format in ["gbrp10be", "gbrp10le"]: - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == format - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_gbrp10_align(self): - array = numpy.random.randint(0, 1024, size=(238, 318, 3), dtype=numpy.uint16) - for format in ["gbrp10be", "gbrp10le"]: - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 318 - assert frame.height == 238 - assert frame.format.name == format - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_gbrp12(self): - array = numpy.random.randint(0, 4096, size=(480, 640, 3), dtype=numpy.uint16) - for format in ["gbrp12be", "gbrp12le"]: - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == format - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_gbrp12_align(self): - array = numpy.random.randint(0, 4096, size=(238, 318, 3), dtype=numpy.uint16) - for format in ["gbrp12be", "gbrp12le"]: - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 318 - assert frame.height == 238 - assert frame.format.name == format - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_gbrp14(self): - array = numpy.random.randint(0, 16384, size=(480, 640, 3), dtype=numpy.uint16) - for format in ["gbrp14be", "gbrp14le"]: - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == format - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_gbrp14_align(self): - array = numpy.random.randint(0, 16384, size=(238, 318, 3), dtype=numpy.uint16) - for format in ["gbrp14be", "gbrp14le"]: - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 318 - assert frame.height == 238 - assert frame.format.name == format - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_gbrp16(self): - array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) - for format in ["gbrp16be", "gbrp16le"]: - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == format - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_gbrp16_align(self): - array = numpy.random.randint(0, 65536, size=(238, 318, 3), dtype=numpy.uint16) - for format in ["gbrp16be", "gbrp16le"]: - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 318 - assert frame.height == 238 - assert frame.format.name == format - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_gbrpf32(self): - array = numpy.random.random_sample(size=(480, 640, 3)).astype(numpy.float32) - for format in ["gbrpf32be", "gbrpf32le"]: - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == format - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_gbrpf32_align(self): - array = numpy.random.random_sample(size=(238, 318, 3)).astype(numpy.float32) - for format in ["gbrpf32be", "gbrpf32le"]: - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 318 - assert frame.height == 238 - assert frame.format.name == format - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_yuv420p(self): - array = numpy.random.randint(0, 256, size=(720, 640), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray(array, format="yuv420p") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "yuv420p" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_yuv420p_align(self): - array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray(array, format="yuv420p") - assert frame.width == 318 - assert frame.height == 238 - assert frame.format.name == "yuv420p" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_yuvj420p(self): - array = numpy.random.randint(0, 256, size=(720, 640), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray(array, format="yuvj420p") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "yuvj420p" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_yuyv422(self): - array = numpy.random.randint(0, 256, size=(480, 640, 2), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray(array, format="yuyv422") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "yuyv422" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_yuv444p(self): - array = numpy.random.randint(0, 256, size=(3, 480, 640), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray(array, format="yuv444p") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "yuv444p" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_yuvj444p(self): - array = numpy.random.randint(0, 256, size=(3, 480, 640), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray(array, format="yuvj444p") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "yuvj444p" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_yuyv422_align(self): - array = numpy.random.randint(0, 256, size=(238, 318, 2), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray(array, format="yuyv422") - assert frame.width == 318 - assert frame.height == 238 - assert frame.format.name == "yuyv422" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_gray16be(self): - array = numpy.random.randint(0, 65536, size=(480, 640), dtype=numpy.uint16) - frame = VideoFrame.from_ndarray(array, format="gray16be") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "gray16be" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - # check endianness by examining value of first pixel - self.assertPixelValue16(frame.planes[0], array[0][0], "big") - - def test_ndarray_gray16le(self): - array = numpy.random.randint(0, 65536, size=(480, 640), dtype=numpy.uint16) - frame = VideoFrame.from_ndarray(array, format="gray16le") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "gray16le" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - # check endianness by examining value of first pixel - self.assertPixelValue16(frame.planes[0], array[0][0], "little") - - def test_ndarray_rgb48be(self): - array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) - frame = VideoFrame.from_ndarray(array, format="rgb48be") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "rgb48be" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - # check endianness by examining red value of first pixel - self.assertPixelValue16(frame.planes[0], array[0][0][0], "big") - - def test_ndarray_rgb48le(self): - array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) - frame = VideoFrame.from_ndarray(array, format="rgb48le") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "rgb48le" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - # check endianness by examining red value of first pixel - self.assertPixelValue16(frame.planes[0], array[0][0][0], "little") - - def test_ndarray_rgb48le_align(self): - array = numpy.random.randint(0, 65536, size=(238, 318, 3), dtype=numpy.uint16) - frame = VideoFrame.from_ndarray(array, format="rgb48le") - assert frame.width == 318 - assert frame.height == 238 - assert frame.format.name == "rgb48le" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - # check endianness by examining red value of first pixel - self.assertPixelValue16(frame.planes[0], array[0][0][0], "little") - - def test_ndarray_rgba64be(self): - array = numpy.random.randint(0, 65536, size=(480, 640, 4), dtype=numpy.uint16) - frame = VideoFrame.from_ndarray(array, format="rgba64be") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "rgba64be" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - # check endianness by examining red value of first pixel - self.assertPixelValue16(frame.planes[0], array[0][0][0], "big") - - def test_ndarray_rgba64le(self): - array = numpy.random.randint(0, 65536, size=(480, 640, 4), dtype=numpy.uint16) - frame = VideoFrame.from_ndarray(array, format="rgba64le") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "rgba64le" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - # check endianness by examining red value of first pixel - self.assertPixelValue16(frame.planes[0], array[0][0][0], "little") - - def test_ndarray_rgb8(self): - array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray(array, format="rgb8") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "rgb8" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_bgr8(self): - array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray(array, format="bgr8") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "bgr8" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_pal8(self): - array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) - palette = numpy.random.randint(0, 256, size=(256, 4), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray((array, palette), format="pal8") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "pal8" - returned = frame.to_ndarray() - self.assertTrue((type(returned) is tuple) and len(returned) == 2) - self.assertNdarraysEqual(returned[0], array) - self.assertNdarraysEqual(returned[1], palette) - - def test_ndarray_nv12(self): - array = numpy.random.randint(0, 256, size=(720, 640), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray(array, format="nv12") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "nv12" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_nv12_align(self): - array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray(array, format="nv12") - assert frame.width == 318 - assert frame.height == 238 - assert frame.format.name == "nv12" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - -class TestVideoFrameNumpyBuffer(TestCase): - def test_shares_memory_gray(self): - array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) - frame = VideoFrame.from_numpy_buffer(array, "gray") - self.assertNdarraysEqual(frame.to_ndarray(), array) - - # overwrite the array, the contents thereof - array[...] = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) - # Make sure the frame reflects that - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_shares_memory_gray8(self): - array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) - frame = VideoFrame.from_numpy_buffer(array, "gray8") - self.assertNdarraysEqual(frame.to_ndarray(), array) - - # overwrite the array, the contents thereof - array[...] = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) - # Make sure the frame reflects that - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_shares_memory_rgb8(self): - array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) - frame = VideoFrame.from_numpy_buffer(array, "rgb8") - self.assertNdarraysEqual(frame.to_ndarray(), array) - - # overwrite the array, the contents thereof - array[...] = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) - # Make sure the frame reflects that - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_shares_memory_bgr8(self): - array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) - frame = VideoFrame.from_numpy_buffer(array, "bgr8") - self.assertNdarraysEqual(frame.to_ndarray(), array) - - # overwrite the array, the contents thereof - array[...] = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) - # Make sure the frame reflects that - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_shares_memory_rgb24(self): - array = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8) - frame = VideoFrame.from_numpy_buffer(array, "rgb24") - self.assertNdarraysEqual(frame.to_ndarray(), array) - - # overwrite the array, the contents thereof - array[...] = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8) - # Make sure the frame reflects that - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_shares_memory_yuv420p(self): - array = numpy.random.randint( - 0, 256, size=(512 * 6 // 4, 256), dtype=numpy.uint8 - ) - frame = VideoFrame.from_numpy_buffer(array, "yuv420p") - self.assertNdarraysEqual(frame.to_ndarray(), array) - - # overwrite the array, the contents thereof - array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) - # Make sure the frame reflects that - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_shares_memory_yuvj420p(self): - array = numpy.random.randint( - 0, 256, size=(512 * 6 // 4, 256), dtype=numpy.uint8 - ) - frame = VideoFrame.from_numpy_buffer(array, "yuvj420p") - self.assertNdarraysEqual(frame.to_ndarray(), array) - - # overwrite the array, the contents thereof - array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) - # Make sure the frame reflects that - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_shares_memory_nv12(self): - array = numpy.random.randint( - 0, 256, size=(512 * 6 // 4, 256), dtype=numpy.uint8 - ) - frame = VideoFrame.from_numpy_buffer(array, "nv12") - self.assertNdarraysEqual(frame.to_ndarray(), array) - - # overwrite the array, the contents thereof - array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) - # Make sure the frame reflects that - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_shares_memory_bgr24(self): - array = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8) - frame = VideoFrame.from_numpy_buffer(array, "bgr24") - self.assertNdarraysEqual(frame.to_ndarray(), array) - - # overwrite the array, the contents thereof - array[...] = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8) - # Make sure the frame reflects that - self.assertNdarraysEqual(frame.to_ndarray(), array) - - -class TestVideoFrameTiming(TestCase): - def test_reformat_pts(self) -> None: - frame = VideoFrame(640, 480, "rgb24") - frame.pts = 123 - frame.time_base = Fraction("456/1") - frame = frame.reformat(320, 240) - assert frame.pts == 123 - assert frame.time_base == 456 - - -class TestVideoFrameReformat(TestCase): - def test_reformat_identity(self): - frame1 = VideoFrame(640, 480, "rgb24") - frame2 = frame1.reformat(640, 480, "rgb24") - assert frame1 is frame2 - - def test_reformat_colourspace(self): - # This is allowed. - frame = VideoFrame(640, 480, "rgb24") - frame.reformat(src_colorspace=None, dst_colorspace="smpte240") - - # I thought this was not allowed, but it seems to be. - frame = VideoFrame(640, 480, "yuv420p") - frame.reformat(src_colorspace=None, dst_colorspace="smpte240") - - def test_reformat_pixel_format_align(self): - height = 480 - for width in range(2, 258, 2): - frame_yuv = VideoFrame(width, height, "yuv420p") - for plane in frame_yuv.planes: - plane.update(b"\xff" * plane.buffer_size) - - expected_rgb = numpy.zeros(shape=(height, width, 3), dtype=numpy.uint8) - expected_rgb[:, :, 0] = 255 - expected_rgb[:, :, 1] = 124 - expected_rgb[:, :, 2] = 255 - - frame_rgb = frame_yuv.reformat(format="rgb24") - self.assertNdarraysEqual(frame_rgb.to_ndarray(), expected_rgb) + +def test_basic_to_ndarray(): + array = VideoFrame(640, 480, "rgb24").to_ndarray() + assert array.shape == (480, 640, 3) + + +def test_to_image_with_dimensions(): + img = VideoFrame(640, 480, format="rgb24").to_image(width=320, height=240) + assert img.size == (320, 240) + + +def test_ndarray_gray(): + array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) + for format in ("gray", "gray8"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "gray" + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gray_align(): + array = numpy.random.randint(0, 256, size=(238, 318), dtype=numpy.uint8) + for format in ("gray", "gray8"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == "gray" + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_rgb(): + array = numpy.random.randint(0, 256, size=(480, 640, 3), dtype=numpy.uint8) + for format in ("rgb24", "bgr24"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_rgb_align(): + array = numpy.random.randint(0, 256, size=(238, 318, 3), dtype=numpy.uint8) + for format in ("rgb24", "bgr24"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_rgba(): + array = numpy.random.randint(0, 256, size=(480, 640, 4), dtype=numpy.uint8) + for format in ("argb", "rgba", "abgr", "bgra"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_rgba_align(): + array = numpy.random.randint(0, 256, size=(238, 318, 4), dtype=numpy.uint8) + for format in ("argb", "rgba", "abgr", "bgra"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrp(): + array = numpy.random.randint(0, 256, size=(480, 640, 3), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="gbrp") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "gbrp" + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrp_align(): + array = numpy.random.randint(0, 256, size=(238, 318, 3), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="gbrp") + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == "gbrp" + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrp10(): + array = numpy.random.randint(0, 1024, size=(480, 640, 3), dtype=numpy.uint16) + for format in ("gbrp10be", "gbrp10le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrp10_align(): + array = numpy.random.randint(0, 1024, size=(238, 318, 3), dtype=numpy.uint16) + for format in ("gbrp10be", "gbrp10le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrp12(): + array = numpy.random.randint(0, 4096, size=(480, 640, 3), dtype=numpy.uint16) + for format in ("gbrp12be", "gbrp12le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrp12_align(): + array = numpy.random.randint(0, 4096, size=(238, 318, 3), dtype=numpy.uint16) + for format in ("gbrp12be", "gbrp12le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrp14(): + array = numpy.random.randint(0, 16384, size=(480, 640, 3), dtype=numpy.uint16) + for format in ("gbrp14be", "gbrp14le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrp14_align(): + array = numpy.random.randint(0, 16384, size=(238, 318, 3), dtype=numpy.uint16) + for format in ("gbrp14be", "gbrp14le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrp16(): + array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) + for format in ("gbrp16be", "gbrp16le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrp16_align(): + array = numpy.random.randint(0, 65536, size=(238, 318, 3), dtype=numpy.uint16) + for format in ("gbrp16be", "gbrp16le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrpf32(): + array = numpy.random.random_sample(size=(480, 640, 3)).astype(numpy.float32) + for format in ("gbrpf32be", "gbrpf32le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrpf32_align(): + array = numpy.random.random_sample(size=(238, 318, 3)).astype(numpy.float32) + for format in ["gbrpf32be", "gbrpf32le"]: + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_yuv420p(): + array = numpy.random.randint(0, 256, size=(720, 640), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="yuv420p") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "yuv420p" + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_yuv420p_align(): + array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="yuv420p") + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == "yuv420p" + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_yuvj420p(): + array = numpy.random.randint(0, 256, size=(720, 640), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="yuvj420p") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "yuvj420p" + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_yuyv422(): + array = numpy.random.randint(0, 256, size=(480, 640, 2), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="yuyv422") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "yuyv422" + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_yuv444p(): + array = numpy.random.randint(0, 256, size=(3, 480, 640), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="yuv444p") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "yuv444p" + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_yuvj444p(): + array = numpy.random.randint(0, 256, size=(3, 480, 640), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="yuvj444p") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "yuvj444p" + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_yuyv422_align(): + array = numpy.random.randint(0, 256, size=(238, 318, 2), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="yuyv422") + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == "yuyv422" + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gray16be(): + array = numpy.random.randint(0, 65536, size=(480, 640), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="gray16be") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "gray16be" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining value of first pixel + assertPixelValue16(frame.planes[0], array[0][0], "big") + + +def test_ndarray_gray16le(): + array = numpy.random.randint(0, 65536, size=(480, 640), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="gray16le") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "gray16le" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining value of first pixel + assertPixelValue16(frame.planes[0], array[0][0], "little") + + +def test_ndarray_rgb48be(): + array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="rgb48be") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "rgb48be" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining red value of first pixel + assertPixelValue16(frame.planes[0], array[0][0][0], "big") + + +def test_ndarray_rgb48le(): + array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="rgb48le") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "rgb48le" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining red value of first pixel + assertPixelValue16(frame.planes[0], array[0][0][0], "little") + + +def test_ndarray_rgb48le_align(): + array = numpy.random.randint(0, 65536, size=(238, 318, 3), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="rgb48le") + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == "rgb48le" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining red value of first pixel + assertPixelValue16(frame.planes[0], array[0][0][0], "little") + + +def test_ndarray_rgba64be(): + array = numpy.random.randint(0, 65536, size=(480, 640, 4), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="rgba64be") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "rgba64be" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining red value of first pixel + assertPixelValue16(frame.planes[0], array[0][0][0], "big") + + +def test_ndarray_rgba64le(): + array = numpy.random.randint(0, 65536, size=(480, 640, 4), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="rgba64le") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "rgba64le" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining red value of first pixel + assertPixelValue16(frame.planes[0], array[0][0][0], "little") + + +def test_ndarray_rgb8(): + array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="rgb8") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "rgb8" + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_bgr8(): + array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="bgr8") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "bgr8" + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_pal8(): + array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) + palette = numpy.random.randint(0, 256, size=(256, 4), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray((array, palette), format="pal8") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "pal8" + returned = frame.to_ndarray() + assert type(returned) is tuple and len(returned) == 2 + assertNdarraysEqual(returned[0], array) + assertNdarraysEqual(returned[1], palette) + + +def test_ndarray_nv12(): + array = numpy.random.randint(0, 256, size=(720, 640), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="nv12") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "nv12" + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_nv12_align(): + array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="nv12") + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == "nv12" + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_shares_memory_gray(): + array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + frame = VideoFrame.from_numpy_buffer(array, "gray") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_shares_memory_gray8(): + array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + frame = VideoFrame.from_numpy_buffer(array, "gray8") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_shares_memory_rgb8(): + array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + frame = VideoFrame.from_numpy_buffer(array, "rgb8") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_shares_memory_bgr8(): + array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + frame = VideoFrame.from_numpy_buffer(array, "bgr8") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_shares_memory_rgb24(): + array = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8) + frame = VideoFrame.from_numpy_buffer(array, "rgb24") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_shares_memory_yuv420p(): + array = numpy.random.randint(0, 256, size=(512 * 6 // 4, 256), dtype=numpy.uint8) + frame = VideoFrame.from_numpy_buffer(array, "yuv420p") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_shares_memory_yuvj420p(): + array = numpy.random.randint(0, 256, size=(512 * 6 // 4, 256), dtype=numpy.uint8) + frame = VideoFrame.from_numpy_buffer(array, "yuvj420p") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_shares_memory_nv12(): + array = numpy.random.randint(0, 256, size=(512 * 6 // 4, 256), dtype=numpy.uint8) + frame = VideoFrame.from_numpy_buffer(array, "nv12") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_shares_memory_bgr24(): + array = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8) + frame = VideoFrame.from_numpy_buffer(array, "bgr24") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_reformat_pts() -> None: + frame = VideoFrame(640, 480, "rgb24") + frame.pts = 123 + frame.time_base = Fraction("456/1") + frame = frame.reformat(320, 240) + assert frame.pts == 123 and frame.time_base == 456 + + +def test_reformat_identity(): + frame1 = VideoFrame(640, 480, "rgb24") + frame2 = frame1.reformat(640, 480, "rgb24") + assert frame1 is frame2 + + +def test_reformat_colorspace(): + # This is allowed. + frame = VideoFrame(640, 480, "rgb24") + frame.reformat(src_colorspace=None, dst_colorspace="smpte240") + + # I thought this was not allowed, but it seems to be. + frame = VideoFrame(640, 480, "yuv420p") + frame.reformat(src_colorspace=None, dst_colorspace="smpte240") + + +def test_reformat_pixel_format_align(): + height = 480 + for width in range(2, 258, 2): + frame_yuv = VideoFrame(width, height, "yuv420p") + for plane in frame_yuv.planes: + plane.update(b"\xff" * plane.buffer_size) + + expected_rgb = numpy.zeros(shape=(height, width, 3), dtype=numpy.uint8) + expected_rgb[:, :, 0] = 255 + expected_rgb[:, :, 1] = 124 + expected_rgb[:, :, 2] = 255 + + frame_rgb = frame_yuv.reformat(format="rgb24") + assertNdarraysEqual(frame_rgb.to_ndarray(), expected_rgb) From 5f850cb11071194f6a4eac4c3ace89286a6b02c4 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 24 Sep 2024 22:04:52 -0400 Subject: [PATCH 366/846] Remove deprecation module Going forward, we'll use type stubs / simple warnings for deprecations. --- av/__main__.py | 6 ++- av/codec/context.pyx | 1 - av/deprecation.py | 80 --------------------------------------- tests/test_deprecation.py | 43 --------------------- 4 files changed, 4 insertions(+), 126 deletions(-) delete mode 100644 av/deprecation.py delete mode 100644 tests/test_deprecation.py diff --git a/av/__main__.py b/av/__main__.py index 4e5d62692..bc353d147 100644 --- a/av/__main__.py +++ b/av/__main__.py @@ -1,7 +1,9 @@ +from __future__ import annotations + import argparse -def main(): +def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--codecs", action="store_true") parser.add_argument("--version", action="store_true") @@ -13,7 +15,7 @@ def main(): print(f"PyAV v{av.__version__}") - by_config = {} + by_config: dict = {} for libname, config in sorted(av._core.library_meta.items()): version = config["version"] if version[0] >= 0: diff --git a/av/codec/context.pyx b/av/codec/context.pyx index bfaa9544d..b995bc9f7 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -11,7 +11,6 @@ from av.error cimport err_check from av.packet cimport Packet from av.utils cimport avrational_to_fraction, to_avrational -from av.deprecation import AVDeprecationWarning from av.dictionary import Dictionary diff --git a/av/deprecation.py b/av/deprecation.py deleted file mode 100644 index b9a65f135..000000000 --- a/av/deprecation.py +++ /dev/null @@ -1,80 +0,0 @@ -import functools -import warnings - - -class AVDeprecationWarning(DeprecationWarning): - pass - - -class AttributeRenamedWarning(AVDeprecationWarning): - pass - - -class MethodDeprecationWarning(AVDeprecationWarning): - pass - - -# DeprecationWarning is not printed by default (unless in __main__). We -# really want these to be seen, but also to use the "correct" base classes. -# So we're putting a filter in place to show our warnings. The users can -# turn them back off if they want. -warnings.filterwarnings("default", "", AVDeprecationWarning) - - -class renamed_attr: - """Proxy for renamed attributes (or methods) on classes. - Getting and setting values will be redirected to the provided name, - and warnings will be issues every time. - - """ - - def __init__(self, new_name): - self.new_name = new_name - self._old_name = None - - def old_name(self, cls): - if self._old_name is None: - for k, v in vars(cls).items(): - if v is self: - self._old_name = k - break - return self._old_name - - def __get__(self, instance, cls): - old_name = self.old_name(cls) - warnings.warn( - "{0}.{1} is deprecated; please use {0}.{2}.".format( - cls.__name__, - old_name, - self.new_name, - ), - AttributeRenamedWarning, - stacklevel=2, - ) - return getattr(instance if instance is not None else cls, self.new_name) - - def __set__(self, instance, value): - old_name = self.old_name(instance.__class__) - warnings.warn( - "{0}.{1} is deprecated; please use {0}.{2}.".format( - instance.__class__.__name__, - old_name, - self.new_name, - ), - AttributeRenamedWarning, - stacklevel=2, - ) - setattr(instance, self.new_name, value) - - -class method: - def __init__(self, func): - functools.update_wrapper(self, func, ("__name__", "__doc__")) - self.func = func - - def __get__(self, instance, cls): - warning = MethodDeprecationWarning( - f"{cls.__name__}.{self.func.__name__} is deprecated." - ) - warnings.warn(warning, stacklevel=2) - return self.func.__get__(instance, cls) diff --git a/tests/test_deprecation.py b/tests/test_deprecation.py deleted file mode 100644 index f1b0b5d4b..000000000 --- a/tests/test_deprecation.py +++ /dev/null @@ -1,43 +0,0 @@ -import warnings - -from av import deprecation - - -def test_method() -> None: - class Example: - def __init__(self, x: int = 100) -> None: - self.x = x - - @deprecation.method - def foo(self, a: int, b: int) -> int: - return self.x + a + b - - obj = Example() - - with warnings.catch_warnings(record=True) as captured: - assert obj.foo(20, b=3) == 123 - assert "Example.foo is deprecated" in str(captured[0].message) - - -def test_renamed_attr() -> None: - class Example: - new_value = "foo" - old_value = deprecation.renamed_attr("new_value") - - def new_func(self, a: int, b: int) -> int: - return a + b - - old_func = deprecation.renamed_attr("new_func") - - obj = Example() - - with warnings.catch_warnings(record=True) as captured: - assert obj.old_value == "foo" - assert "Example.old_value is deprecated" in str(captured[0].message) - - obj.old_value = "bar" - assert "Example.old_value is deprecated" in str(captured[1].message) - - with warnings.catch_warnings(record=True) as captured: - assert obj.old_func(1, 2) == 3 - assert "Example.old_func is deprecated" in str(captured[0].message) From 0a7e549da5fe198b0a8e4df3dc10bcd582823e25 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 24 Sep 2024 22:52:13 -0400 Subject: [PATCH 367/846] Use python 3 semantics --- av/logging.pyx | 6 +++--- setup.py | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/av/logging.pyx b/av/logging.pyx index 6b6858db6..66a5095ad 100644 --- a/av/logging.pyx +++ b/av/logging.pyx @@ -38,8 +38,6 @@ API Reference """ -from __future__ import absolute_import - cimport libav as lib from libc.stdio cimport fprintf, stderr from libc.stdlib cimport free, malloc @@ -223,7 +221,9 @@ cpdef log(int level, str name, str message): cdef log_context *obj = malloc(sizeof(log_context)) obj.class_ = &log_class obj.name = name - lib.av_log(obj, level, "%s", message) + cdef bytes message_bytes = message.encode("utf-8") + + lib.av_log(obj, level, "%s", message_bytes) free(obj) diff --git a/setup.py b/setup.py index 5329223b3..52395dbe8 100644 --- a/setup.py +++ b/setup.py @@ -177,12 +177,12 @@ def parse_cflags(raw_flags): library_dirs=extension_extra["library_dirs"], sources=[pyx_path], ), - compiler_directives=dict( - c_string_type="str", - c_string_encoding="ascii", - embedsignature=True, - language_level=2, - ), + compiler_directives={ + "c_string_type": "str", + "c_string_encoding": "ascii", + "embedsignature": True, + "language_level": 3, + }, build_dir="src", include_path=["include"], ) From 3063f6b58143cdb186df2cce9526838f6ceec1e6 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 25 Sep 2024 01:31:23 -0400 Subject: [PATCH 368/846] Write release notes for 13.1.0rc1 --- CHANGELOG.rst | 28 ++++++++++++++++++++++------ av/about.py | 2 +- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 75bd7adff..d43d3ddaa 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,7 +1,7 @@ Changelog ========= -We are operating with `semantic versioning `_. +We are operating with `semantic versioning `_. .. Please try to update this file in the commits that make the changes. @@ -16,6 +16,22 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. +v13.1.0 +------- + +Features: + +- Allow passing Python objects around using `Frame.opaque` and `Packet.opaque`. +- Allow extradata to be set by encoders by @daveisfera in (:issue:`1526`). + +Fixes: + +- Remove the `deprecation` module in anticipation of `PEP 702 `_ +- Add type stubs to previously unannotated API sections. +- Improve type stubs for both `mypy` and `mypy --strict`. +- Permit only setting `time_base` with a Fraction, as mypy is unable to respect different types in getters vs setters. +- Declare `write_packet` function as const by @hmaarrfk in (:issue:`1517`). + v13.0.0 ------- @@ -415,7 +431,7 @@ Micro: Build: - Assert building against compatible FFmpeg. (:issue:`401`) -- Lock down Cython lanaguage level to avoid build warnings. (:issue:`443`) +- Lock down Cython language level to avoid build warnings. (:issue:`443`) Other: @@ -596,9 +612,9 @@ Major: of streams/containers. - ``Stream.encode`` returns a list of packets, instead of a single packet. - ``AudioFifo`` and ``AudioResampler`` will raise ``ValueError`` if input frames - inconsistant ``pts``. + inconsistent ``pts``. - ``time_base`` use has been revisited across the codebase, and may not be converted - bettween ``Stream.time_base`` and ``CodecContext.time_base`` at the same times + between ``Stream.time_base`` and ``CodecContext.time_base`` at the same times in the transcoding pipeline. - ``CodecContext.rate`` has been removed, but proxied to ``VideoCodecContext.framerate`` and ``AudioCodecContext.sample_rate``. The definition is effectively inverted from @@ -611,7 +627,7 @@ Minor: - Added ``Packet.is_keyframe`` and ``Packet.is_corrupt``. (:issue:`226`) -- Many more ``time_base``, ``pts`` and other attributes are writeable. +- Many more ``time_base``, ``pts`` and other attributes are writable. - ``Option`` exposes much more of the API (but not get/set). (:issue:`243`) - Expose metadata encoding controls. @@ -721,7 +737,7 @@ v0.2.3 Major: - Python IO. -- Agressively releases GIL +- Aggressively releases GIL - Add experimental Windows build. (:issue:`84`) diff --git a/av/about.py b/av/about.py index 1311252a5..f0a0fa039 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "13.0.0" +__version__ = "13.1.0rc1" From 339fc4859f2b75994ed0b66f5e51d4954ca0ca5e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 25 Sep 2024 02:04:13 -0400 Subject: [PATCH 369/846] Add workaround for mypy#3004 --- av/audio/codeccontext.pyi | 19 ++++++++++--------- av/audio/frame.pyi | 12 ++++++++++-- av/audio/stream.pyi | 19 ++++++++++--------- av/filter/graph.pyi | 4 ++-- tests/test_codec_context.py | 10 +++++----- tests/test_encode.py | 4 ++-- 6 files changed, 39 insertions(+), 29 deletions(-) diff --git a/av/audio/codeccontext.pyi b/av/audio/codeccontext.pyi index afd28ec34..b3ec3ce6e 100644 --- a/av/audio/codeccontext.pyi +++ b/av/audio/codeccontext.pyi @@ -7,20 +7,21 @@ from .format import AudioFormat from .frame import AudioFrame from .layout import AudioLayout +class _Format: + def __get__(self, i: object | None, owner: type | None = None) -> AudioFormat: ... + def __set__(self, instance: object, value: AudioFormat | str) -> None: ... + +class _Layout: + def __get__(self, i: object | None, owner: type | None = None) -> AudioLayout: ... + def __set__(self, instance: object, value: AudioLayout | str) -> None: ... + class AudioCodecContext(CodecContext): frame_size: int sample_rate: int rate: int type: Literal["audio"] - - @property - def format(self) -> AudioFormat: ... - @format.setter - def format(self, value: AudioFormat | str) -> None: ... - @property - def layout(self) -> AudioLayout: ... - @layout.setter - def layout(self, value: AudioLayout | str) -> None: ... + format: _Format + layout: _Layout @property def channels(self) -> int: ... def encode(self, frame: AudioFrame | None = None) -> list[Packet]: ... diff --git a/av/audio/frame.pyi b/av/audio/frame.pyi index c733a039d..7f61e4e6d 100644 --- a/av/audio/frame.pyi +++ b/av/audio/frame.pyi @@ -17,13 +17,21 @@ _SupportedNDarray = Union[ np.ndarray[Any, np.dtype[np.uint8]], # u1 ] +class _Format: + def __get__(self, i: object | None, owner: type | None = None) -> AudioFormat: ... + def __set__(self, instance: object, value: AudioFormat | str) -> None: ... + +class _Layout: + def __get__(self, i: object | None, owner: type | None = None) -> AudioLayout: ... + def __set__(self, instance: object, value: AudioLayout | str) -> None: ... + class AudioFrame(Frame): planes: tuple[AudioPlane, ...] samples: int sample_rate: int rate: int - format: AudioFormat - layout: AudioLayout + format: _Format + layout: _Layout def __init__( self, diff --git a/av/audio/stream.pyi b/av/audio/stream.pyi index 48a9d65c6..443c85fa1 100644 --- a/av/audio/stream.pyi +++ b/av/audio/stream.pyi @@ -8,6 +8,14 @@ from .format import AudioFormat from .frame import AudioFrame from .layout import AudioLayout +class _Format: + def __get__(self, i: object | None, owner: type | None = None) -> AudioFormat: ... + def __set__(self, instance: object, value: AudioFormat | str) -> None: ... + +class _Layout: + def __get__(self, i: object | None, owner: type | None = None) -> AudioLayout: ... + def __set__(self, instance: object, value: AudioLayout | str) -> None: ... + class AudioStream(Stream): codec_context: AudioCodecContext # From codec context @@ -17,14 +25,7 @@ class AudioStream(Stream): rate: int channels: int type: Literal["audio"] - - @property - def format(self) -> AudioFormat: ... - @format.setter - def format(self, value: AudioFormat | str) -> None: ... - @property - def layout(self) -> AudioLayout: ... - @layout.setter - def layout(self, value: AudioLayout | str) -> None: ... + format: _Format + layout: _Layout def encode(self, frame: AudioFrame | None = None) -> list[Packet]: ... def decode(self, packet: Packet | None = None) -> list[AudioFrame]: ... diff --git a/av/filter/graph.pyi b/av/filter/graph.pyi index 9d8480b17..e170c2ce7 100644 --- a/av/filter/graph.pyi +++ b/av/filter/graph.pyi @@ -34,8 +34,8 @@ class Graph: self, template: AudioStream | None = None, sample_rate: int | None = None, - format: AudioFormat | None = None, - layout: AudioLayout | None = None, + format: AudioFormat | str | None = None, + layout: AudioLayout | str | None = None, channels: int | None = None, name: str | None = None, time_base: Fraction | None = None, diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 24b49f09a..5e18aebb2 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -139,7 +139,7 @@ def test_encoder_extradata(self): assert ctx.extradata_size == 3 def test_encoder_pix_fmt(self): - ctx = av.codec.Codec("h264", "w").create() + ctx = av.codec.Codec("h264", "w").create("video") # valid format ctx.pix_fmt = "yuv420p" @@ -429,8 +429,8 @@ def _audio_encoding( ctx.time_base = Fraction(1) / sample_rate ctx.sample_rate = sample_rate - ctx.format = sample_fmt # type: ignore - ctx.layout = channel_layout # type: ignore + ctx.format = sample_fmt + ctx.layout = channel_layout ctx.open() @@ -463,8 +463,8 @@ def _audio_encoding( ctx = Codec(codec_name, "r").create("audio") ctx.sample_rate = sample_rate - ctx.format = sample_fmt # type: ignore - ctx.layout = channel_layout # type: ignore + ctx.format = sample_fmt + ctx.layout = channel_layout ctx.open() result_samples = 0 diff --git a/tests/test_encode.py b/tests/test_encode.py index e2ab1ba2f..617231409 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -196,8 +196,8 @@ def test_transcode(self) -> None: ctx = stream.codec_context ctx.sample_rate = sample_rate - stream.format = sample_fmt # type: ignore - ctx.layout = channel_layout # type: ignore + stream.format = sample_fmt + ctx.layout = channel_layout with av.open( fate_suite("audio-reference/chorusnoise_2ch_44kHz_s16.wav") From 6e9698f0fc6c9591df6e3ce4bac2c20a6547ae83 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 25 Sep 2024 03:35:42 -0400 Subject: [PATCH 370/846] Add more type stubs for tests --- tests/test_codec_context.py | 20 ++-- tests/test_decode.py | 41 ++++---- tests/test_file_probing.py | 25 ++--- tests/test_filters.py | 20 ++-- tests/test_python_io.py | 192 +++++++++++++++++++----------------- tests/test_videoframe.py | 52 +++++----- 6 files changed, 176 insertions(+), 174 deletions(-) diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 5e18aebb2..a6bae17a0 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -36,14 +36,14 @@ class Options(TypedDict, total=False): @overload def iter_raw_frames( - path: str, packet_sizes: list, ctx: VideoCodecContext + path: str, packet_sizes: list[int], ctx: VideoCodecContext ) -> Iterator[VideoFrame]: ... @overload def iter_raw_frames( - path: str, packet_sizes: list, ctx: AudioCodecContext + path: str, packet_sizes: list[int], ctx: AudioCodecContext ) -> Iterator[AudioFrame]: ... def iter_raw_frames( - path: str, packet_sizes: list, ctx: VideoCodecContext | AudioCodecContext + path: str, packet_sizes: list[int], ctx: VideoCodecContext | AudioCodecContext ) -> Iterator[VideoFrame | AudioFrame]: with open(path, "rb") as f: for i, size in enumerate(packet_sizes): @@ -85,14 +85,16 @@ def test_codec_tag(self): assert ctx.codec_tag == "xvid" # wrong length - with self.assertRaises(ValueError) as cm: + with pytest.raises( + ValueError, match="Codec tag should be a 4 character string" + ): ctx.codec_tag = "bob" - assert str(cm.exception) == "Codec tag should be a 4 character string." # wrong type - with self.assertRaises(ValueError) as cm: + with pytest.raises( + ValueError, match="Codec tag should be a 4 character string" + ): ctx.codec_tag = 123 - assert str(cm.exception) == "Codec tag should be a 4 character string." with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: assert container.streams[0].codec_tag == "avc1" @@ -175,14 +177,14 @@ def test_bits_per_coded_sample(self): with pytest.raises(ValueError): stream.codec_context.bits_per_coded_sample = 32 - def test_parse(self): + def test_parse(self) -> None: # This one parses into a single packet. self._assert_parse("mpeg4", fate_suite("h264/interlaced_crop.mp4")) # This one parses into many small packets. self._assert_parse("mpeg2video", fate_suite("mpeg2/mpeg2_field_encoding.ts")) - def _assert_parse(self, codec_name, path): + def _assert_parse(self, codec_name: str, path: str) -> None: fh = av.open(path) packets = [] for packet in fh.demux(video=0): diff --git a/tests/test_decode.py b/tests/test_decode.py index aafea3b22..7622693b5 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -57,7 +57,7 @@ def test_decode_audio_sample_count(self) -> None: ) assert sample_count == total_samples - def test_decoded_time_base(self): + def test_decoded_time_base(self) -> None: container = av.open(fate_suite("h264/interlaced_crop.mp4")) stream = container.streams.video[0] @@ -65,38 +65,37 @@ def test_decoded_time_base(self): for packet in container.demux(stream): for frame in packet.decode(): + assert not isinstance(frame, av.subtitles.subtitle.SubtitleSet) assert packet.time_base == frame.time_base assert stream.time_base == frame.time_base return - def test_decoded_motion_vectors(self): + def test_decoded_motion_vectors(self) -> None: container = av.open(fate_suite("h264/interlaced_crop.mp4")) stream = container.streams.video[0] codec_context = stream.codec_context codec_context.options = {"flags2": "+export_mvs"} - for packet in container.demux(stream): - for frame in packet.decode(): - vectors = frame.side_data.get("MOTION_VECTORS") - if frame.key_frame: - # Key frame don't have motion vectors - assert vectors is None - else: - assert len(vectors) > 0 - return - - def test_decoded_motion_vectors_no_flag(self): + for frame in container.decode(stream): + vectors = frame.side_data.get("MOTION_VECTORS") + if frame.key_frame: + # Key frame don't have motion vectors + assert vectors is None + else: + assert vectors is not None and len(vectors) > 0 + return + + def test_decoded_motion_vectors_no_flag(self) -> None: container = av.open(fate_suite("h264/interlaced_crop.mp4")) stream = container.streams.video[0] - for packet in container.demux(stream): - for frame in packet.decode(): - vectors = frame.side_data.get("MOTION_VECTORS") - if not frame.key_frame: - assert vectors is None - return + for frame in container.decode(stream): + vectors = frame.side_data.get("MOTION_VECTORS") + if not frame.key_frame: + assert vectors is None + return - def test_decode_video_corrupt(self): + def test_decode_video_corrupt(self) -> None: # write an empty file path = self.sandboxed("empty.h264") with open(path, "wb"): @@ -114,7 +113,7 @@ def test_decode_video_corrupt(self): assert packet_count == 1 assert frame_count == 0 - def test_decode_close_then_use(self): + def test_decode_close_then_use(self) -> None: container = av.open(fate_suite("h264/interlaced_crop.mp4")) container.close() diff --git a/tests/test_file_probing.py b/tests/test_file_probing.py index d67a69195..d8a7fd755 100644 --- a/tests/test_file_probing.py +++ b/tests/test_file_probing.py @@ -14,9 +14,7 @@ def test_container_probing(self): assert self.file.duration == 6165333 assert str(self.file.format) == "" assert self.file.format.name == "mpegts" - self.assertEqual( - self.file.format.long_name, "MPEG-TS (MPEG-2 Transport Stream)" - ) + assert self.file.format.long_name == "MPEG-TS (MPEG-2 Transport Stream)" assert self.file.metadata == {} assert self.file.size == 207740 assert self.file.start_time == 1400000 @@ -25,11 +23,8 @@ def test_container_probing(self): def test_stream_probing(self): stream = self.file.streams[0] - # check __repr__ - self.assertTrue( - str(stream).startswith( - " None: assert self.file.bit_rate == 0 assert self.file.duration is None assert str(self.file.format) == "" @@ -78,14 +73,11 @@ def test_container_probing(self): assert self.file.start_time is None assert len(self.file.streams) == 1 - def test_stream_probing(self): + def test_stream_probing(self) -> None: stream = self.file.streams[0] - # ensure __repr__ does not crash - self.assertTrue( - str(stream).startswith( - " None: def test_stream_probing(self) -> None: stream = self.file.streams[0] - # check __repr__ assert str(stream).startswith(" AudioFrame: """ Generate audio frame representing part of the sinusoidal wave - :param input_format: default: s16 - :param layout: default: stereo - :param sample_rate: default: 44100 - :param frame_size: default: 1024 - :param frame_num: frame number - :return: audio frame for sinusoidal wave audio signal slice """ frame = AudioFrame(format=input_format, layout=layout, samples=frame_size) frame.sample_rate = sample_rate @@ -31,7 +29,7 @@ def generate_audio_frame( data = np.zeros(frame_size, dtype=format_dtypes[input_format]) for j in range(frame_size): data[j] = np.sin(2 * np.pi * (frame_num + j) * (i + 1) / float(frame_size)) - frame.planes[i].update(data) + frame.planes[i].update(data) # type: ignore return frame @@ -79,8 +77,8 @@ def test_generator_graph(self): lutrgb.link_to(sink) # pads and links - self.assertIs(src.outputs[0].link.output, lutrgb.inputs[0]) - self.assertIs(lutrgb.inputs[0].link.input, src.outputs[0]) + assert src.outputs[0].link.output is lutrgb.inputs[0] + assert lutrgb.inputs[0].link.input is src.outputs[0] frame = sink.pull() assert isinstance(frame, VideoFrame) diff --git a/tests/test_python_io.py b/tests/test_python_io.py index 8324b870b..3fb9cf3dd 100644 --- a/tests/test_python_io.py +++ b/tests/test_python_io.py @@ -1,20 +1,34 @@ +from __future__ import annotations + import functools +import io import types from io import BytesIO -from unittest import SkipTest +from re import escape +from typing import TYPE_CHECKING + +import pytest import av from .common import TestCase, fate_png, fate_suite, has_pillow, run_in_sandbox from .test_encode import assert_rgb_rotate, write_rgb_rotate +if TYPE_CHECKING: + from collections.abc import Callable + class MethodLogger: - def __init__(self, obj): + def __init__(self, obj: object) -> None: self._obj = obj - self._log = [] + self._log: list[tuple[str, object]] = [] + + def __getattr__(self, name: str) -> object: + + def _method(name: str, meth: Callable, *args, **kwargs) -> object: + self._log.append((name, args)) + return meth(*args, **kwargs) - def __getattr__(self, name): value = getattr(self._obj, name) if isinstance( value, @@ -25,16 +39,12 @@ def __getattr__(self, name): types.BuiltinMethodType, ), ): - return functools.partial(self._method, name, value) + return functools.partial(_method, name, value) else: - self._log.append(("__getattr__", (name,), {})) + self._log.append(("__getattr__", (name,))) return value - def _method(self, name, meth, *args, **kwargs): - self._log.append((name, args, kwargs)) - return meth(*args, **kwargs) - - def _filter(self, type_): + def _filter(self, type_: str) -> list[tuple[str, object]]: return [log for log in self._log if log[0] == type_] @@ -72,13 +82,13 @@ class ReadOnlyPipe(BytesIO): """ @property - def name(self): + def name(self) -> int: return 123 - def seekable(self): + def seekable(self) -> bool: return False - def writable(self): + def writable(self) -> bool: return False @@ -88,16 +98,46 @@ class WriteOnlyPipe(BytesIO): """ @property - def name(self): + def name(self) -> int: return 123 - def readable(self): + def readable(self) -> bool: return False - def seekable(self): + def seekable(self) -> bool: return False +def read( + fh: io.BufferedReader | BytesIO | ReadOnlyBuffer, seekable: bool = True +) -> None: + wrapped = MethodLogger(fh) + + with av.open(wrapped, "r") as container: + assert container.format.name == "mpegts" + assert container.format.long_name == "MPEG-TS (MPEG-2 Transport Stream)" + assert len(container.streams) == 1 + if seekable: + assert container.size == 800000 + assert container.metadata == {} + + # Check method calls. + assert wrapped._filter("read") + if seekable: + assert wrapped._filter("seek") + + +def write(fh: io.BufferedWriter | BytesIO) -> None: + wrapped = MethodLogger(fh) + + with av.open(wrapped, "w", "mp4") as container: + write_rgb_rotate(container) + + # Check method calls. + assert wrapped._filter("write") + assert wrapped._filter("seek") + + # Using a custom protocol will avoid the DASH muxer detecting or defaulting to a # file: protocol and enabling the use of temporary files and renaming. CUSTOM_IO_PROTOCOL = "pyavtest://" @@ -106,16 +146,16 @@ def seekable(self): class CustomIOLogger: """Log calls to open a file as well as method calls on the files""" - def __init__(self): - self._log = [] - self._method_log = [] + def __init__(self) -> None: + self._log: list[tuple[object, dict]] = [] + self._method_log: list[MethodLogger] = [] - def __call__(self, *args, **kwargs): + def __call__(self, *args, **kwargs) -> MethodLogger: self._log.append((args, kwargs)) self._method_log.append(self.io_open(*args, **kwargs)) return self._method_log[-1] - def io_open(self, url, flags, options): + def io_open(self, url: str, flags, options: object) -> MethodLogger: # Remove the protocol prefix to reveal the local filename if CUSTOM_IO_PROTOCOL in url: url = url.split(CUSTOM_IO_PROTOCOL, 1)[1] @@ -133,55 +173,52 @@ def io_open(self, url, flags, options): class TestPythonIO(TestCase): - def test_basic_errors(self): + def test_basic_errors(self) -> None: self.assertRaises(Exception, av.open, None) self.assertRaises(Exception, av.open, None, "w") - def test_reading_from_buffer(self): + def test_reading_from_buffer(self) -> None: with open(fate_suite("mpeg2/mpeg2_field_encoding.ts"), "rb") as fh: buf = BytesIO(fh.read()) - self.read(buf, seekable=True) + read(buf, seekable=True) - def test_reading_from_buffer_no_seek(self): + def test_reading_from_buffer_no_seek(self) -> None: with open(fate_suite("mpeg2/mpeg2_field_encoding.ts"), "rb") as fh: buf = ReadOnlyBuffer(fh.read()) - self.read(buf, seekable=False) + read(buf, seekable=False) - def test_reading_from_file(self): + def test_reading_from_file(self) -> None: with open(fate_suite("mpeg2/mpeg2_field_encoding.ts"), "rb") as fh: - self.read(fh, seekable=True) + read(fh, seekable=True) - def test_reading_from_pipe_readonly(self): + def test_reading_from_pipe_readonly(self) -> None: with open(fate_suite("mpeg2/mpeg2_field_encoding.ts"), "rb") as fh: buf = ReadOnlyPipe(fh.read()) - self.read(buf, seekable=False) + read(buf, seekable=False) - def test_reading_from_write_readonly(self): + def test_reading_from_write_readonly(self) -> None: with open(fate_suite("mpeg2/mpeg2_field_encoding.ts"), "rb") as fh: buf = WriteOnlyPipe(fh.read()) - with self.assertRaises(ValueError) as cm: - self.read(buf, seekable=False) - assert ( - str(cm.exception) - == "File object has no read() method, or readable() returned False." - ) + msg = escape("File object has no read() method, or readable() returned False.") + with pytest.raises(ValueError, match=msg): + read(buf, seekable=False) - def test_writing_to_buffer(self): + def test_writing_to_buffer(self) -> None: buf = BytesIO() - self.write(buf) + write(buf) # Check contents. - self.assertTrue(buf.tell()) + assert buf.tell() buf.seek(0) - with av.open(buf) as container: + with av.open(buf, "r") as container: assert_rgb_rotate(self, container) - def test_writing_to_buffer_broken(self): + def test_writing_to_buffer_broken(self) -> None: buf = BrokenBuffer() - with self.assertRaises(OSError): + with pytest.raises(OSError): with av.open(buf, "w", "mp4") as container: write_rgb_rotate(container) @@ -198,11 +235,11 @@ def test_writing_to_buffer_broken_with_close(self): buf.broken = True # try to close file - with self.assertRaises(OSError): + with pytest.raises(OSError): container.close() @run_in_sandbox - def test_writing_to_custom_io_dash(self): + def test_writing_to_custom_io_dash(self) -> None: # Custom I/O that opens file and logs calls wrapped_custom_io = CustomIOLogger() @@ -217,8 +254,8 @@ def test_writing_to_custom_io_dash(self): # Check that at least 3 files were opened using the custom IO: # "output_filename", init-stream0.m4s and chunk-stream-0x.m4s - self.assertGreaterEqual(len(wrapped_custom_io._log), 3) - self.assertGreaterEqual(len(wrapped_custom_io._method_log), 3) + assert len(wrapped_custom_io._log) >= 3 + assert len(wrapped_custom_io._method_log) >= 3 # Check that all files were written to all_write = all( @@ -237,9 +274,9 @@ def test_writing_to_custom_io_dash(self): with av.open(output_filename, "r") as container: assert_rgb_rotate(self, container, is_dash=True) - def test_writing_to_custom_io_image2(self): + def test_writing_to_custom_io_image2(self) -> None: if not has_pillow: - raise SkipTest() + pytest.skip() import PIL.Image as Image @@ -263,7 +300,7 @@ def test_writing_to_custom_io_image2(self): stream.height = height stream.pix_fmt = "rgb24" - for frame_i in range(frame_count): + for _ in range(frame_count): for packet in stream.encode(input_frame): output.mux(packet) @@ -286,8 +323,9 @@ def test_writing_to_custom_io_image2(self): # Check contents. with av.open(sequence_filename, "r", "image2") as container: assert len(container.streams) == 1 + assert isinstance(container.streams[0], av.video.stream.VideoStream) + stream = container.streams[0] - assert isinstance(stream, av.video.stream.VideoStream) assert stream.duration == frame_count assert stream.type == "video" @@ -301,7 +339,7 @@ def test_writing_to_file(self) -> None: path = self.sandboxed("writing.mp4") with open(path, "wb") as fh: - self.write(fh) + write(fh) # Check contents. with av.open(path) as container: @@ -309,47 +347,21 @@ def test_writing_to_file(self) -> None: def test_writing_to_pipe_readonly(self) -> None: buf = ReadOnlyPipe() - with self.assertRaises(ValueError) as cm: - self.write(buf) - assert ( - str(cm.exception) - == "File object has no write() method, or writable() returned False." - ) + with pytest.raises( + ValueError, + match=escape( + "File object has no write() method, or writable() returned False." + ), + ) as cm: + write(buf) def test_writing_to_pipe_writeonly(self): av.logging.set_level(av.logging.VERBOSE) buf = WriteOnlyPipe() - with self.assertRaises(ValueError) as cm: - self.write(buf) - assert "[mp4] muxer does not support non seekable output" in str(cm.exception) + with pytest.raises( + ValueError, match=escape("[mp4] muxer does not support non seekable output") + ) as cm: + write(buf) av.logging.set_level(None) - - def read(self, fh, seekable: bool = True) -> None: - wrapped = MethodLogger(fh) - - with av.open(wrapped, "r") as container: - assert container.format.name == "mpegts" - self.assertEqual( - container.format.long_name, "MPEG-TS (MPEG-2 Transport Stream)" - ) - assert len(container.streams) == 1 - if seekable: - assert container.size == 800000 - assert container.metadata == {} - - # Check method calls. - self.assertTrue(wrapped._filter("read")) - if seekable: - self.assertTrue(wrapped._filter("seek")) - - def write(self, fh): - wrapped = MethodLogger(fh) - - with av.open(wrapped, "w", "mp4") as container: - write_rgb_rotate(container) - - # Check method calls. - self.assertTrue(wrapped._filter("write")) - self.assertTrue(wrapped._filter("seek")) diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 7d7180ebf..6b856be86 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -286,7 +286,7 @@ def test_ndarray_gbrp12_align(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_gbrp14(): +def test_ndarray_gbrp14() -> None: array = numpy.random.randint(0, 16384, size=(480, 640, 3), dtype=numpy.uint16) for format in ("gbrp14be", "gbrp14le"): frame = VideoFrame.from_ndarray(array, format=format) @@ -295,7 +295,7 @@ def test_ndarray_gbrp14(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_gbrp14_align(): +def test_ndarray_gbrp14_align() -> None: array = numpy.random.randint(0, 16384, size=(238, 318, 3), dtype=numpy.uint16) for format in ("gbrp14be", "gbrp14le"): frame = VideoFrame.from_ndarray(array, format=format) @@ -304,7 +304,7 @@ def test_ndarray_gbrp14_align(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_gbrp16(): +def test_ndarray_gbrp16() -> None: array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) for format in ("gbrp16be", "gbrp16le"): frame = VideoFrame.from_ndarray(array, format=format) @@ -313,7 +313,7 @@ def test_ndarray_gbrp16(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_gbrp16_align(): +def test_ndarray_gbrp16_align() -> None: array = numpy.random.randint(0, 65536, size=(238, 318, 3), dtype=numpy.uint16) for format in ("gbrp16be", "gbrp16le"): frame = VideoFrame.from_ndarray(array, format=format) @@ -322,7 +322,7 @@ def test_ndarray_gbrp16_align(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_gbrpf32(): +def test_ndarray_gbrpf32() -> None: array = numpy.random.random_sample(size=(480, 640, 3)).astype(numpy.float32) for format in ("gbrpf32be", "gbrpf32le"): frame = VideoFrame.from_ndarray(array, format=format) @@ -331,7 +331,7 @@ def test_ndarray_gbrpf32(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_gbrpf32_align(): +def test_ndarray_gbrpf32_align() -> None: array = numpy.random.random_sample(size=(238, 318, 3)).astype(numpy.float32) for format in ["gbrpf32be", "gbrpf32le"]: frame = VideoFrame.from_ndarray(array, format=format) @@ -340,7 +340,7 @@ def test_ndarray_gbrpf32_align(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_yuv420p(): +def test_ndarray_yuv420p() -> None: array = numpy.random.randint(0, 256, size=(720, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuv420p") assert frame.width == 640 and frame.height == 480 @@ -348,7 +348,7 @@ def test_ndarray_yuv420p(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_yuv420p_align(): +def test_ndarray_yuv420p_align() -> None: array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuv420p") assert frame.width == 318 and frame.height == 238 @@ -356,7 +356,7 @@ def test_ndarray_yuv420p_align(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_yuvj420p(): +def test_ndarray_yuvj420p() -> None: array = numpy.random.randint(0, 256, size=(720, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuvj420p") assert frame.width == 640 and frame.height == 480 @@ -364,7 +364,7 @@ def test_ndarray_yuvj420p(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_yuyv422(): +def test_ndarray_yuyv422() -> None: array = numpy.random.randint(0, 256, size=(480, 640, 2), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuyv422") assert frame.width == 640 and frame.height == 480 @@ -372,7 +372,7 @@ def test_ndarray_yuyv422(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_yuv444p(): +def test_ndarray_yuv444p() -> None: array = numpy.random.randint(0, 256, size=(3, 480, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuv444p") assert frame.width == 640 and frame.height == 480 @@ -380,7 +380,7 @@ def test_ndarray_yuv444p(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_yuvj444p(): +def test_ndarray_yuvj444p() -> None: array = numpy.random.randint(0, 256, size=(3, 480, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuvj444p") assert frame.width == 640 and frame.height == 480 @@ -388,7 +388,7 @@ def test_ndarray_yuvj444p(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_yuyv422_align(): +def test_ndarray_yuyv422_align() -> None: array = numpy.random.randint(0, 256, size=(238, 318, 2), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuyv422") assert frame.width == 318 and frame.height == 238 @@ -396,7 +396,7 @@ def test_ndarray_yuyv422_align(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_gray16be(): +def test_ndarray_gray16be() -> None: array = numpy.random.randint(0, 65536, size=(480, 640), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="gray16be") assert frame.width == 640 and frame.height == 480 @@ -429,7 +429,7 @@ def test_ndarray_rgb48be(): assertPixelValue16(frame.planes[0], array[0][0][0], "big") -def test_ndarray_rgb48le(): +def test_ndarray_rgb48le() -> None: array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="rgb48le") assert frame.width == 640 and frame.height == 480 @@ -440,7 +440,7 @@ def test_ndarray_rgb48le(): assertPixelValue16(frame.planes[0], array[0][0][0], "little") -def test_ndarray_rgb48le_align(): +def test_ndarray_rgb48le_align() -> None: array = numpy.random.randint(0, 65536, size=(238, 318, 3), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="rgb48le") assert frame.width == 318 and frame.height == 238 @@ -451,7 +451,7 @@ def test_ndarray_rgb48le_align(): assertPixelValue16(frame.planes[0], array[0][0][0], "little") -def test_ndarray_rgba64be(): +def test_ndarray_rgba64be() -> None: array = numpy.random.randint(0, 65536, size=(480, 640, 4), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="rgba64be") assert frame.width == 640 and frame.height == 480 @@ -462,7 +462,7 @@ def test_ndarray_rgba64be(): assertPixelValue16(frame.planes[0], array[0][0][0], "big") -def test_ndarray_rgba64le(): +def test_ndarray_rgba64le() -> None: array = numpy.random.randint(0, 65536, size=(480, 640, 4), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="rgba64le") assert frame.width == 640 and frame.height == 480 @@ -473,7 +473,7 @@ def test_ndarray_rgba64le(): assertPixelValue16(frame.planes[0], array[0][0][0], "little") -def test_ndarray_rgb8(): +def test_ndarray_rgb8() -> None: array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="rgb8") assert frame.width == 640 and frame.height == 480 @@ -481,7 +481,7 @@ def test_ndarray_rgb8(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_bgr8(): +def test_ndarray_bgr8() -> None: array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="bgr8") assert frame.width == 640 and frame.height == 480 @@ -501,7 +501,7 @@ def test_ndarray_pal8(): assertNdarraysEqual(returned[1], palette) -def test_ndarray_nv12(): +def test_ndarray_nv12() -> None: array = numpy.random.randint(0, 256, size=(720, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="nv12") assert frame.width == 640 and frame.height == 480 @@ -509,7 +509,7 @@ def test_ndarray_nv12(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_nv12_align(): +def test_ndarray_nv12_align() -> None: array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="nv12") assert frame.width == 318 and frame.height == 238 @@ -517,7 +517,7 @@ def test_ndarray_nv12_align(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_shares_memory_gray(): +def test_shares_memory_gray() -> None: array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) frame = VideoFrame.from_numpy_buffer(array, "gray") assertNdarraysEqual(frame.to_ndarray(), array) @@ -528,7 +528,7 @@ def test_shares_memory_gray(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_shares_memory_gray8(): +def test_shares_memory_gray8() -> None: array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) frame = VideoFrame.from_numpy_buffer(array, "gray8") assertNdarraysEqual(frame.to_ndarray(), array) @@ -539,7 +539,7 @@ def test_shares_memory_gray8(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_shares_memory_rgb8(): +def test_shares_memory_rgb8() -> None: array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) frame = VideoFrame.from_numpy_buffer(array, "rgb8") assertNdarraysEqual(frame.to_ndarray(), array) @@ -550,7 +550,7 @@ def test_shares_memory_rgb8(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_shares_memory_bgr8(): +def test_shares_memory_bgr8() -> None: array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) frame = VideoFrame.from_numpy_buffer(array, "bgr8") assertNdarraysEqual(frame.to_ndarray(), array) From b558b06a7e3a235de277c658ce29707278bafdf0 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 25 Sep 2024 04:33:20 -0400 Subject: [PATCH 371/846] Fix some incorrect type stubs --- av/__init__.py | 1 + av/codec/__init__.py | 9 ++++++ av/codec/context.pyi | 2 ++ av/container/output.pyi | 2 +- av/video/codeccontext.pyi | 2 +- av/video/format.pyi | 1 + av/video/stream.pyi | 2 +- tests/test_audiofifo.py | 14 +++----- tests/test_codec_context.py | 7 ++-- tests/test_file_probing.py | 4 +-- tests/test_logging.py | 31 ++++++++---------- tests/test_open.py | 46 +++++++++++++------------- tests/test_python_io.py | 4 +-- tests/test_videoformat.py | 64 ++++++++++++++++++------------------- 14 files changed, 97 insertions(+), 92 deletions(-) diff --git a/av/__init__.py b/av/__init__.py index 266aec3e1..b5ea598b1 100644 --- a/av/__init__.py +++ b/av/__init__.py @@ -47,6 +47,7 @@ AVError = FFmpegError # noqa: F405 __all__ = ( + "__version__", "time_base", "library_versions", "AudioCodecContext", diff --git a/av/codec/__init__.py b/av/codec/__init__.py index 7a775ed5a..f35f9b7d4 100644 --- a/av/codec/__init__.py +++ b/av/codec/__init__.py @@ -1,2 +1,11 @@ from .codec import Capabilities, Codec, Properties, codec_descriptor, codecs_available from .context import CodecContext + +__all__ = ( + "Capabilities", + "Codec", + "Properties", + "codec_descriptor", + "codecs_available", + "CodecContext", +) diff --git a/av/codec/context.pyi b/av/codec/context.pyi index 805b06faf..d2d03b7ab 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -56,6 +56,7 @@ class Flags2(EnumFlag): class CodecContext: extradata: bytes | None + extradata_size: int is_open: bool is_encoder: bool is_decoder: bool @@ -95,6 +96,7 @@ class CodecContext: ac_pred: bool interlaced_me: bool closed_gop: bool + delay: bool def open(self, strict: bool = True) -> None: ... def close(self, strict: bool = True) -> None: ... diff --git a/av/container/output.pyi b/av/container/output.pyi index a3218ae34..760532a96 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -23,7 +23,7 @@ class OutputContainer(Container): @overload def add_stream( self, - codec_name: Literal["h264", "mpeg4", "png"], + codec_name: Literal["h264", "mpeg4", "png", "qtrle"], rate: Fraction | int | float | None = None, template: None = None, options: dict[str, str] | None = None, diff --git a/av/video/codeccontext.pyi b/av/video/codeccontext.pyi index ebef8b275..7172b50c2 100644 --- a/av/video/codeccontext.pyi +++ b/av/video/codeccontext.pyi @@ -11,7 +11,7 @@ class VideoCodecContext(CodecContext): format: VideoFormat | None width: int height: int - bits_per_codec_sample: int + bits_per_coded_sample: int pix_fmt: str | None framerate: Fraction rate: Fraction diff --git a/av/video/format.pyi b/av/video/format.pyi index 17c66de82..ee16b85b8 100644 --- a/av/video/format.pyi +++ b/av/video/format.pyi @@ -9,6 +9,7 @@ class VideoFormat: is_rgb: bool width: int height: int + components: tuple[VideoFormatComponent, ...] def __init__(self, name: str, width: int = 0, height: int = 0) -> None: ... def chroma_width(self, luma_width: int = 0) -> int: ... diff --git a/av/video/stream.pyi b/av/video/stream.pyi index 94ffccd64..3028bd31c 100644 --- a/av/video/stream.pyi +++ b/av/video/stream.pyi @@ -21,7 +21,7 @@ class VideoStream(Stream): format: VideoFormat width: int height: int - bits_per_codec_sample: int + bits_per_coded_sample: int pix_fmt: str | None framerate: Fraction rate: Fraction diff --git a/tests/test_audiofifo.py b/tests/test_audiofifo.py index 9eee420a8..85e9481ee 100644 --- a/tests/test_audiofifo.py +++ b/tests/test_audiofifo.py @@ -33,11 +33,8 @@ def test_data(self) -> None: def test_pts_simple(self) -> None: fifo = av.AudioFifo() - # ensure __repr__ does not crash - self.assertTrue( - str(fifo).startswith( - " None: fifo.write(iframe) - # ensure __repr__ was updated - self.assertTrue( - str(fifo).startswith( - " at 0x" - ) + assert str(fifo).startswith( + " at 0x" ) oframe = fifo.read(512) diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index a6bae17a0..fa56a5e4f 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -72,7 +72,7 @@ def test_skip_frame_default(self): ctx = Codec("png", "w").create() assert ctx.skip_frame.name == "DEFAULT" - def test_codec_delay(self): + def test_codec_delay(self) -> None: with av.open(fate_suite("mkv/codec_delay_opus.mkv")) as container: assert container.streams.audio[0].codec_context.delay == 312 with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: @@ -131,7 +131,7 @@ def test_decoder_timebase(self) -> None: with pytest.raises(RuntimeError): ctx.time_base = Fraction(1, 25) - def test_encoder_extradata(self): + def test_encoder_extradata(self) -> None: ctx = av.codec.Codec("h264", "w").create() assert ctx.extradata is None assert ctx.extradata_size == 0 @@ -140,7 +140,7 @@ def test_encoder_extradata(self): assert ctx.extradata == b"123" assert ctx.extradata_size == 3 - def test_encoder_pix_fmt(self): + def test_encoder_pix_fmt(self) -> None: ctx = av.codec.Codec("h264", "w").create("video") # valid format @@ -161,6 +161,7 @@ def test_bits_per_coded_sample(self): for packet in container.demux(stream): for frame in packet.decode(): pass + assert isinstance(packet.stream, av.VideoStream) assert packet.stream.bits_per_coded_sample == 32 with av.open(fate_suite("qtrle/aletrek-rle.mov")) as container: diff --git a/tests/test_file_probing.py b/tests/test_file_probing.py index d8a7fd755..87d462264 100644 --- a/tests/test_file_probing.py +++ b/tests/test_file_probing.py @@ -43,9 +43,7 @@ def test_stream_probing(self): assert stream.bit_rate is None assert stream.channels == 2 assert stream.codec.name == "aac_latm" - self.assertEqual( - stream.codec.long_name, "AAC LATM (Advanced Audio Coding LATM syntax)" - ) + assert stream.codec.long_name == "AAC LATM (Advanced Audio Coding LATM syntax)" assert stream.format.bits == 32 assert stream.format.name == "fltp" assert stream.layout.name == "stereo" diff --git a/tests/test_logging.py b/tests/test_logging.py index 5602ce751..8e863791c 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -8,7 +8,7 @@ from .common import TestCase -def do_log(message): +def do_log(message: str) -> None: av.logging.log(av.logging.INFO, "test", message) @@ -16,9 +16,9 @@ class TestLogging(TestCase): def test_adapt_level(self): assert av.logging.adapt_level(av.logging.ERROR) == logging.ERROR assert av.logging.adapt_level(av.logging.WARNING) == logging.WARNING - self.assertEqual( - av.logging.adapt_level((av.logging.WARNING + av.logging.ERROR) // 2), - logging.WARNING, + assert ( + av.logging.adapt_level((av.logging.WARNING + av.logging.ERROR) // 2) + == logging.WARNING ) def test_threaded_captures(self): @@ -30,7 +30,7 @@ def test_threaded_captures(self): thread.start() thread.join() - self.assertIn((av.logging.INFO, "test", "main"), logs) + assert (av.logging.INFO, "test", "main") in logs av.logging.set_level(None) def test_global_captures(self): @@ -42,8 +42,8 @@ def test_global_captures(self): thread.start() thread.join() - self.assertIn((av.logging.INFO, "test", "main"), logs) - self.assertIn((av.logging.INFO, "test", "thread"), logs) + assert (av.logging.INFO, "test", "main") in logs + assert (av.logging.INFO, "test", "thread") in logs av.logging.set_level(None) def test_repeats(self): @@ -59,16 +59,13 @@ def test_repeats(self): logs = [log for log in logs if log[1] == "test"] - self.assertEqual( - logs, - [ - (av.logging.INFO, "test", "foo"), - (av.logging.INFO, "test", "foo"), - (av.logging.INFO, "test", "bar"), - (av.logging.INFO, "test", "bar (repeated 2 more times)"), - (av.logging.INFO, "test", "baz"), - ], - ) + assert logs == [ + (av.logging.INFO, "test", "foo"), + (av.logging.INFO, "test", "foo"), + (av.logging.INFO, "test", "bar"), + (av.logging.INFO, "test", "bar (repeated 2 more times)"), + (av.logging.INFO, "test", "baz"), + ] av.logging.set_level(None) diff --git a/tests/test_open.py b/tests/test_open.py index b77312203..9341e1f56 100644 --- a/tests/test_open.py +++ b/tests/test_open.py @@ -2,34 +2,36 @@ import av -from .common import TestCase, fate_suite +from .common import fate_suite -class TestOpen(TestCase): - def test_path_input(self): - path = Path(fate_suite("h264/interlaced_crop.mp4")) - self.assertIsInstance(path, Path) +def test_path_input() -> None: + path = Path(fate_suite("h264/interlaced_crop.mp4")) + assert isinstance(path, Path) - container = av.open(path) - self.assertIs(type(container), av.container.InputContainer) + container = av.open(path) + assert type(container) is av.container.InputContainer - def test_str_input(self): - path = fate_suite("h264/interlaced_crop.mp4") - self.assertIs(type(path), str) - container = av.open(path) - self.assertIs(type(container), av.container.InputContainer) +def test_str_input() -> None: + path = fate_suite("h264/interlaced_crop.mp4") + assert type(path) is str - def test_path_output(self): - path = Path(fate_suite("h264/interlaced_crop.mp4")) - self.assertIsInstance(path, Path) + container = av.open(path) + assert type(container) is av.container.InputContainer - container = av.open(path, "w") - self.assertIs(type(container), av.container.OutputContainer) - def test_str_output(self): - path = fate_suite("h264/interlaced_crop.mp4") - self.assertIs(type(path), str) +def test_path_output() -> None: + path = Path(fate_suite("h264/interlaced_crop.mp4")) + assert isinstance(path, Path) - container = av.open(path, "w") - self.assertIs(type(container), av.container.OutputContainer) + container = av.open(path, "w") + assert type(container) is av.container.OutputContainer + + +def test_str_output() -> None: + path = fate_suite("h264/interlaced_crop.mp4") + assert type(path) is str + + container = av.open(path, "w") + assert type(container) is av.container.OutputContainer diff --git a/tests/test_python_io.py b/tests/test_python_io.py index 3fb9cf3dd..3cb4b7720 100644 --- a/tests/test_python_io.py +++ b/tests/test_python_io.py @@ -67,7 +67,7 @@ class ReadOnlyBuffer: Minimal buffer which *only* implements the read() method. """ - def __init__(self, data): + def __init__(self, data) -> None: self.data = data def read(self, n): @@ -225,7 +225,7 @@ def test_writing_to_buffer_broken(self) -> None: # break I/O buf.broken = True - def test_writing_to_buffer_broken_with_close(self): + def test_writing_to_buffer_broken_with_close(self) -> None: buf = BrokenBuffer() with av.open(buf, "w", "mp4") as container: diff --git a/tests/test_videoformat.py b/tests/test_videoformat.py index 4670dc217..1e2e0c0f4 100644 --- a/tests/test_videoformat.py +++ b/tests/test_videoformat.py @@ -9,13 +9,13 @@ def test_invalid_pixel_format(self): VideoFormat("__unknown_pix_fmt", 640, 480) assert str(cm.exception) == "not a pixel format: '__unknown_pix_fmt'" - def test_rgb24_inspection(self): + def test_rgb24_inspection(self) -> None: fmt = VideoFormat("rgb24", 640, 480) assert fmt.name == "rgb24" assert len(fmt.components) == 3 - self.assertFalse(fmt.is_planar) - self.assertFalse(fmt.has_palette) - self.assertTrue(fmt.is_rgb) + assert not fmt.is_planar + assert not fmt.has_palette + assert fmt.is_rgb assert fmt.chroma_width() == 640 assert fmt.chroma_height() == 480 assert fmt.chroma_width(1024) == 1024 @@ -24,22 +24,22 @@ def test_rgb24_inspection(self): comp = fmt.components[i] assert comp.plane == 0 assert comp.bits == 8 - self.assertFalse(comp.is_luma) - self.assertFalse(comp.is_chroma) - self.assertFalse(comp.is_alpha) + assert not comp.is_luma + assert not comp.is_chroma + assert not comp.is_alpha assert comp.width == 640 assert comp.height == 480 - def test_yuv420p_inspection(self): + def test_yuv420p_inspection(self) -> None: fmt = VideoFormat("yuv420p", 640, 480) assert fmt.name == "yuv420p" assert len(fmt.components) == 3 self._test_yuv420(fmt) - def _test_yuv420(self, fmt): - self.assertTrue(fmt.is_planar) - self.assertFalse(fmt.has_palette) - self.assertFalse(fmt.is_rgb) + def _test_yuv420(self, fmt: VideoFormat) -> None: + assert fmt.is_planar + assert not fmt.has_palette + assert not fmt.is_rgb assert fmt.chroma_width() == 320 assert fmt.chroma_height() == 240 assert fmt.chroma_width(1024) == 512 @@ -48,33 +48,33 @@ def _test_yuv420(self, fmt): comp = fmt.components[i] assert comp.plane == i assert comp.bits == 8 - self.assertFalse(fmt.components[0].is_chroma) - self.assertTrue(fmt.components[1].is_chroma) - self.assertTrue(fmt.components[2].is_chroma) - self.assertTrue(fmt.components[0].is_luma) - self.assertFalse(fmt.components[1].is_luma) - self.assertFalse(fmt.components[2].is_luma) - self.assertFalse(fmt.components[0].is_alpha) - self.assertFalse(fmt.components[1].is_alpha) - self.assertFalse(fmt.components[2].is_alpha) + assert not fmt.components[0].is_chroma + assert fmt.components[1].is_chroma + assert fmt.components[2].is_chroma + assert fmt.components[0].is_luma + assert not fmt.components[1].is_luma + assert not fmt.components[2].is_luma + assert not fmt.components[0].is_alpha + assert not fmt.components[1].is_alpha + assert not fmt.components[2].is_alpha assert fmt.components[0].width == 640 assert fmt.components[1].width == 320 assert fmt.components[2].width == 320 - def test_yuva420p_inspection(self): + def test_yuva420p_inspection(self) -> None: fmt = VideoFormat("yuva420p", 640, 480) assert len(fmt.components) == 4 self._test_yuv420(fmt) - self.assertFalse(fmt.components[3].is_chroma) + assert not fmt.components[3].is_chroma assert fmt.components[3].width == 640 - def test_gray16be_inspection(self): + def test_gray16be_inspection(self) -> None: fmt = VideoFormat("gray16be", 640, 480) assert fmt.name == "gray16be" assert len(fmt.components) == 1 - self.assertFalse(fmt.is_planar) - self.assertFalse(fmt.has_palette) - self.assertFalse(fmt.is_rgb) + assert not fmt.is_planar + assert not fmt.has_palette + assert not fmt.is_rgb assert fmt.chroma_width() == 640 assert fmt.chroma_height() == 480 assert fmt.chroma_width(1024) == 1024 @@ -82,13 +82,13 @@ def test_gray16be_inspection(self): comp = fmt.components[0] assert comp.plane == 0 assert comp.bits == 16 - self.assertTrue(comp.is_luma) - self.assertFalse(comp.is_chroma) + assert comp.is_luma + assert not comp.is_chroma assert comp.width == 640 assert comp.height == 480 - self.assertFalse(comp.is_alpha) + assert not comp.is_alpha - def test_pal8_inspection(self): + def test_pal8_inspection(self) -> None: fmt = VideoFormat("pal8", 640, 480) assert len(fmt.components) == 1 - self.assertTrue(fmt.has_palette) + assert fmt.has_palette From e3253e109fcf0d4c835c83fe2efea163805b77a7 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 25 Sep 2024 05:21:04 -0400 Subject: [PATCH 372/846] `mypy --strict` now reports less than 100 errors (#1553) --- av/error.pyi | 3 + av/frame.pyi | 8 +- av/video/frame.pyi | 4 +- tests/test_codec_context.py | 4 +- tests/test_decode.py | 2 +- tests/test_encode.py | 10 +-- tests/test_errors.py | 4 +- tests/test_file_probing.py | 7 +- tests/test_filters.py | 7 +- tests/test_logging.py | 145 ++++++++++++++++++------------------ tests/test_python_io.py | 4 +- tests/test_videoframe.py | 48 ++++++------ 12 files changed, 128 insertions(+), 118 deletions(-) diff --git a/av/error.pyi b/av/error.pyi index 527d8c00d..088f78d45 100644 --- a/av/error.pyi +++ b/av/error.pyi @@ -11,6 +11,9 @@ def make_error( filename: str | None = None, log: tuple[int, tuple[int, str, str] | None] | None = None, ) -> None: ... +def err_check(res: int, filename: str | None = None) -> int: ... + +BUFFER_TOO_SMALL: EnumItem class ErrorType(EnumItem): BSF_NOT_FOUND: int diff --git a/av/frame.pyi b/av/frame.pyi index 6e5348119..7c34e91bf 100644 --- a/av/frame.pyi +++ b/av/frame.pyi @@ -1,4 +1,10 @@ from fractions import Fraction +from typing import TypedDict + +from av.sidedata.motionvectors import MotionVectors + +class SideData(TypedDict, total=False): + MOTION_VECTORS: MotionVectors class Frame: dts: int | None @@ -6,7 +12,7 @@ class Frame: time: float | None time_base: Fraction is_corrupt: bool - side_data: dict[str, str] + side_data: SideData opaque: object def make_writable(self) -> None: ... diff --git a/av/video/frame.pyi b/av/video/frame.pyi index de84faaa0..29f961ba4 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -46,8 +46,8 @@ class VideoFrame(Frame): width: int | None = None, height: int | None = None, format: str | None = None, - src_colorspace: int | None = None, - dst_colorspace: int | None = None, + src_colorspace: str | int | None = None, + dst_colorspace: str | int | None = None, interpolation: int | str | None = None, src_color_range: int | str | None = None, dst_color_range: int | str | None = None, diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index fa56a5e4f..6425c4dd1 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -169,7 +169,7 @@ def test_bits_per_coded_sample(self): stream.bits_per_coded_sample = 31 with pytest.raises(av.error.InvalidDataError): - for frame in container.decode(stream): + for _ in container.decode(stream): pass with av.open(self.sandboxed("output.mov"), "w") as output: @@ -396,7 +396,7 @@ def video_encoding( assert i == gop_size final_gop_size = decoded_frame_count - max(keyframe_indices) - self.assertLessEqual(final_gop_size, gop_size) + assert final_gop_size < gop_size def test_encoding_pcm_s24le(self) -> None: self.audio_encoding("pcm_s24le") diff --git a/tests/test_decode.py b/tests/test_decode.py index 7622693b5..20abdf840 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -41,7 +41,7 @@ def test_decode_audio_sample_count(self) -> None: audio_stream = next(s for s in container.streams if s.type == "audio") assert audio_stream is container.streams.audio[0] - assert isinstance(audio_stream, av.audio.AudioStream) + assert isinstance(audio_stream, av.AudioStream) sample_count = 0 diff --git a/tests/test_encode.py b/tests/test_encode.py index 617231409..89d2e3eb3 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -1,9 +1,9 @@ import io import math from fractions import Fraction -from unittest import SkipTest import numpy as np +import pytest import av from av import AudioFrame, VideoFrame @@ -19,7 +19,7 @@ def write_rgb_rotate(output: av.container.OutputContainer) -> None: if not has_pillow: - raise SkipTest("Don't have Pillow") + pytest.skip() import PIL.Image as Image @@ -233,8 +233,8 @@ def test_stream_index(self) -> None: astream = output.add_stream("mp2", 48000) assert astream in output.streams.audio - astream.layout = "stereo" # type: ignore - astream.format = "s16" # type: ignore + astream.layout = "stereo" + astream.format = "s16" assert vstream.index == 0 assert astream.index == 1 @@ -385,4 +385,4 @@ def test_max_b_frames(self) -> None: for max_b_frames in range(4): file = encode_file_with_max_b_frames(max_b_frames) actual_max_b_frames = max_b_frame_run_in_file(file) - self.assertTrue(actual_max_b_frames <= max_b_frames) + assert actual_max_b_frames <= max_b_frames diff --git a/tests/test_errors.py b/tests/test_errors.py index 5f7440402..1cad5d086 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -53,11 +53,11 @@ def test_filenotfound(): assert False, "No exception raised!" -def test_buffertoosmall(): +def test_buffertoosmall() -> None: """Throw an exception from an enum.""" try: av.error.err_check(-av.error.BUFFER_TOO_SMALL.value) - except av.BufferTooSmallError as e: + except av.error.BufferTooSmallError as e: assert e.errno == av.error.BUFFER_TOO_SMALL.value else: assert False, "No exception raised!" diff --git a/tests/test_file_probing.py b/tests/test_file_probing.py index 87d462264..f71391697 100644 --- a/tests/test_file_probing.py +++ b/tests/test_file_probing.py @@ -9,7 +9,7 @@ class TestAudioProbe(TestCase): def setUp(self): self.file = av.open(fate_suite("aac/latm_stereo_to_51.ts")) - def test_container_probing(self): + def test_container_probing(self) -> None: assert self.file.bit_rate == 269558 assert self.file.duration == 6165333 assert str(self.file.format) == "" @@ -20,9 +20,10 @@ def test_container_probing(self): assert self.file.start_time == 1400000 assert len(self.file.streams) == 1 - def test_stream_probing(self): + def test_stream_probing(self) -> None: stream = self.file.streams[0] + assert isinstance(stream, av.AudioStream) assert str(stream).startswith( " None: # write an empty file path = self.sandboxed("empty.flac") with open(path, "wb"): diff --git a/tests/test_filters.py b/tests/test_filters.py index e7bbeca1a..a87d6b5b0 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -126,7 +126,7 @@ def test_audio_buffer_sink(self): if e.errno != errno.EAGAIN: raise - def test_audio_buffer_resample(self): + def test_audio_buffer_resample(self) -> None: graph = Graph() graph.link_nodes( graph.add_abuffer( @@ -147,6 +147,7 @@ def test_audio_buffer_resample(self): ) ) out_frame = graph.pull() + assert isinstance(out_frame, av.AudioFrame) assert out_frame.format.name == "s16" assert out_frame.layout.name == "stereo" assert out_frame.sample_rate == 44100 @@ -202,9 +203,7 @@ def test_audio_buffer_volume_filter(self): input_data = input_frame.to_ndarray() output_data = out_frame.to_ndarray() - self.assertTrue( - np.allclose(input_data * 0.5, output_data), "Check that volume is reduced" - ) + assert np.allclose(input_data * 0.5, output_data) def test_video_buffer(self): input_container = av.open(format="lavfi", file="color=c=pink:duration=1:r=30") diff --git a/tests/test_logging.py b/tests/test_logging.py index 8e863791c..c8c705b1c 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -5,80 +5,81 @@ import av.error import av.logging -from .common import TestCase - def do_log(message: str) -> None: av.logging.log(av.logging.INFO, "test", message) -class TestLogging(TestCase): - def test_adapt_level(self): - assert av.logging.adapt_level(av.logging.ERROR) == logging.ERROR - assert av.logging.adapt_level(av.logging.WARNING) == logging.WARNING - assert ( - av.logging.adapt_level((av.logging.WARNING + av.logging.ERROR) // 2) - == logging.WARNING - ) - - def test_threaded_captures(self): - av.logging.set_level(av.logging.VERBOSE) - - with av.logging.Capture(local=True) as logs: - do_log("main") - thread = threading.Thread(target=do_log, args=("thread",)) - thread.start() - thread.join() - - assert (av.logging.INFO, "test", "main") in logs - av.logging.set_level(None) - - def test_global_captures(self): - av.logging.set_level(av.logging.VERBOSE) - - with av.logging.Capture(local=False) as logs: - do_log("main") - thread = threading.Thread(target=do_log, args=("thread",)) - thread.start() - thread.join() - - assert (av.logging.INFO, "test", "main") in logs - assert (av.logging.INFO, "test", "thread") in logs - av.logging.set_level(None) - - def test_repeats(self): - av.logging.set_level(av.logging.VERBOSE) - - with av.logging.Capture() as logs: - do_log("foo") - do_log("foo") - do_log("bar") - do_log("bar") - do_log("bar") - do_log("baz") - - logs = [log for log in logs if log[1] == "test"] - - assert logs == [ - (av.logging.INFO, "test", "foo"), - (av.logging.INFO, "test", "foo"), - (av.logging.INFO, "test", "bar"), - (av.logging.INFO, "test", "bar (repeated 2 more times)"), - (av.logging.INFO, "test", "baz"), - ] - - av.logging.set_level(None) - - def test_error(self): - av.logging.set_level(av.logging.VERBOSE) - - log = (av.logging.ERROR, "test", "This is a test.") - av.logging.log(*log) - try: - av.error.err_check(-errno.EPERM) - except OSError as e: - assert e.log == log - else: - self.fail() - - av.logging.set_level(None) +def test_adapt_level() -> None: + assert av.logging.adapt_level(av.logging.ERROR) == logging.ERROR + assert av.logging.adapt_level(av.logging.WARNING) == logging.WARNING + assert ( + av.logging.adapt_level((av.logging.WARNING + av.logging.ERROR) // 2) + == logging.WARNING + ) + + +def test_threaded_captures() -> None: + av.logging.set_level(av.logging.VERBOSE) + + with av.logging.Capture(local=True) as logs: + do_log("main") + thread = threading.Thread(target=do_log, args=("thread",)) + thread.start() + thread.join() + + assert (av.logging.INFO, "test", "main") in logs + av.logging.set_level(None) + + +def test_global_captures() -> None: + av.logging.set_level(av.logging.VERBOSE) + + with av.logging.Capture(local=False) as logs: + do_log("main") + thread = threading.Thread(target=do_log, args=("thread",)) + thread.start() + thread.join() + + assert (av.logging.INFO, "test", "main") in logs + assert (av.logging.INFO, "test", "thread") in logs + av.logging.set_level(None) + + +def test_repeats() -> None: + av.logging.set_level(av.logging.VERBOSE) + + with av.logging.Capture() as logs: + do_log("foo") + do_log("foo") + do_log("bar") + do_log("bar") + do_log("bar") + do_log("baz") + + logs = [log for log in logs if log[1] == "test"] + + assert logs == [ + (av.logging.INFO, "test", "foo"), + (av.logging.INFO, "test", "foo"), + (av.logging.INFO, "test", "bar"), + (av.logging.INFO, "test", "bar (repeated 2 more times)"), + (av.logging.INFO, "test", "baz"), + ] + + av.logging.set_level(None) + + +def test_error() -> None: + av.logging.set_level(av.logging.VERBOSE) + + log = (av.logging.ERROR, "test", "This is a test.") + av.logging.log(*log) + try: + av.error.err_check(-errno.EPERM) + except av.error.PermissionError as e: + assert e.log == log + else: + assert False + + av.logging.set_level(None) diff --git a/tests/test_python_io.py b/tests/test_python_io.py index 3cb4b7720..d657404d9 100644 --- a/tests/test_python_io.py +++ b/tests/test_python_io.py @@ -355,13 +355,13 @@ def test_writing_to_pipe_readonly(self) -> None: ) as cm: write(buf) - def test_writing_to_pipe_writeonly(self): + def test_writing_to_pipe_writeonly(self) -> None: av.logging.set_level(av.logging.VERBOSE) buf = WriteOnlyPipe() with pytest.raises( ValueError, match=escape("[mp4] muxer does not support non seekable output") - ) as cm: + ): write(buf) av.logging.set_level(None) diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 6b856be86..8dcdb838a 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -170,17 +170,17 @@ def test_to_image_rgb24(self): assert img.tobytes() == expected -def test_basic_to_ndarray(): +def test_basic_to_ndarray() -> None: array = VideoFrame(640, 480, "rgb24").to_ndarray() assert array.shape == (480, 640, 3) -def test_to_image_with_dimensions(): +def test_to_image_with_dimensions() -> None: img = VideoFrame(640, 480, format="rgb24").to_image(width=320, height=240) assert img.size == (320, 240) -def test_ndarray_gray(): +def test_ndarray_gray() -> None: array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) for format in ("gray", "gray8"): frame = VideoFrame.from_ndarray(array, format=format) @@ -189,7 +189,7 @@ def test_ndarray_gray(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_gray_align(): +def test_ndarray_gray_align() -> None: array = numpy.random.randint(0, 256, size=(238, 318), dtype=numpy.uint8) for format in ("gray", "gray8"): frame = VideoFrame.from_ndarray(array, format=format) @@ -198,7 +198,7 @@ def test_ndarray_gray_align(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_rgb(): +def test_ndarray_rgb() -> None: array = numpy.random.randint(0, 256, size=(480, 640, 3), dtype=numpy.uint8) for format in ("rgb24", "bgr24"): frame = VideoFrame.from_ndarray(array, format=format) @@ -207,7 +207,7 @@ def test_ndarray_rgb(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_rgb_align(): +def test_ndarray_rgb_align() -> None: array = numpy.random.randint(0, 256, size=(238, 318, 3), dtype=numpy.uint8) for format in ("rgb24", "bgr24"): frame = VideoFrame.from_ndarray(array, format=format) @@ -216,7 +216,7 @@ def test_ndarray_rgb_align(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_rgba(): +def test_ndarray_rgba() -> None: array = numpy.random.randint(0, 256, size=(480, 640, 4), dtype=numpy.uint8) for format in ("argb", "rgba", "abgr", "bgra"): frame = VideoFrame.from_ndarray(array, format=format) @@ -225,7 +225,7 @@ def test_ndarray_rgba(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_rgba_align(): +def test_ndarray_rgba_align() -> None: array = numpy.random.randint(0, 256, size=(238, 318, 4), dtype=numpy.uint8) for format in ("argb", "rgba", "abgr", "bgra"): frame = VideoFrame.from_ndarray(array, format=format) @@ -234,7 +234,7 @@ def test_ndarray_rgba_align(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_gbrp(): +def test_ndarray_gbrp() -> None: array = numpy.random.randint(0, 256, size=(480, 640, 3), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="gbrp") assert frame.width == 640 and frame.height == 480 @@ -242,7 +242,7 @@ def test_ndarray_gbrp(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_gbrp_align(): +def test_ndarray_gbrp_align() -> None: array = numpy.random.randint(0, 256, size=(238, 318, 3), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="gbrp") assert frame.width == 318 and frame.height == 238 @@ -250,7 +250,7 @@ def test_ndarray_gbrp_align(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_gbrp10(): +def test_ndarray_gbrp10() -> None: array = numpy.random.randint(0, 1024, size=(480, 640, 3), dtype=numpy.uint16) for format in ("gbrp10be", "gbrp10le"): frame = VideoFrame.from_ndarray(array, format=format) @@ -259,7 +259,7 @@ def test_ndarray_gbrp10(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_gbrp10_align(): +def test_ndarray_gbrp10_align() -> None: array = numpy.random.randint(0, 1024, size=(238, 318, 3), dtype=numpy.uint16) for format in ("gbrp10be", "gbrp10le"): frame = VideoFrame.from_ndarray(array, format=format) @@ -268,7 +268,7 @@ def test_ndarray_gbrp10_align(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_gbrp12(): +def test_ndarray_gbrp12() -> None: array = numpy.random.randint(0, 4096, size=(480, 640, 3), dtype=numpy.uint16) for format in ("gbrp12be", "gbrp12le"): frame = VideoFrame.from_ndarray(array, format=format) @@ -277,7 +277,7 @@ def test_ndarray_gbrp12(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_gbrp12_align(): +def test_ndarray_gbrp12_align() -> None: array = numpy.random.randint(0, 4096, size=(238, 318, 3), dtype=numpy.uint16) for format in ("gbrp12be", "gbrp12le"): frame = VideoFrame.from_ndarray(array, format=format) @@ -407,7 +407,7 @@ def test_ndarray_gray16be() -> None: assertPixelValue16(frame.planes[0], array[0][0], "big") -def test_ndarray_gray16le(): +def test_ndarray_gray16le() -> None: array = numpy.random.randint(0, 65536, size=(480, 640), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="gray16le") assert frame.width == 640 and frame.height == 480 @@ -418,7 +418,7 @@ def test_ndarray_gray16le(): assertPixelValue16(frame.planes[0], array[0][0], "little") -def test_ndarray_rgb48be(): +def test_ndarray_rgb48be() -> None: array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="rgb48be") assert frame.width == 640 and frame.height == 480 @@ -561,7 +561,7 @@ def test_shares_memory_bgr8() -> None: assertNdarraysEqual(frame.to_ndarray(), array) -def test_shares_memory_rgb24(): +def test_shares_memory_rgb24() -> None: array = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8) frame = VideoFrame.from_numpy_buffer(array, "rgb24") assertNdarraysEqual(frame.to_ndarray(), array) @@ -572,7 +572,7 @@ def test_shares_memory_rgb24(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_shares_memory_yuv420p(): +def test_shares_memory_yuv420p() -> None: array = numpy.random.randint(0, 256, size=(512 * 6 // 4, 256), dtype=numpy.uint8) frame = VideoFrame.from_numpy_buffer(array, "yuv420p") assertNdarraysEqual(frame.to_ndarray(), array) @@ -583,7 +583,7 @@ def test_shares_memory_yuv420p(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_shares_memory_yuvj420p(): +def test_shares_memory_yuvj420p() -> None: array = numpy.random.randint(0, 256, size=(512 * 6 // 4, 256), dtype=numpy.uint8) frame = VideoFrame.from_numpy_buffer(array, "yuvj420p") assertNdarraysEqual(frame.to_ndarray(), array) @@ -594,7 +594,7 @@ def test_shares_memory_yuvj420p(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_shares_memory_nv12(): +def test_shares_memory_nv12() -> None: array = numpy.random.randint(0, 256, size=(512 * 6 // 4, 256), dtype=numpy.uint8) frame = VideoFrame.from_numpy_buffer(array, "nv12") assertNdarraysEqual(frame.to_ndarray(), array) @@ -605,7 +605,7 @@ def test_shares_memory_nv12(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_shares_memory_bgr24(): +def test_shares_memory_bgr24() -> None: array = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8) frame = VideoFrame.from_numpy_buffer(array, "bgr24") assertNdarraysEqual(frame.to_ndarray(), array) @@ -624,13 +624,13 @@ def test_reformat_pts() -> None: assert frame.pts == 123 and frame.time_base == 456 -def test_reformat_identity(): +def test_reformat_identity() -> None: frame1 = VideoFrame(640, 480, "rgb24") frame2 = frame1.reformat(640, 480, "rgb24") assert frame1 is frame2 -def test_reformat_colorspace(): +def test_reformat_colorspace() -> None: # This is allowed. frame = VideoFrame(640, 480, "rgb24") frame.reformat(src_colorspace=None, dst_colorspace="smpte240") @@ -640,7 +640,7 @@ def test_reformat_colorspace(): frame.reformat(src_colorspace=None, dst_colorspace="smpte240") -def test_reformat_pixel_format_align(): +def test_reformat_pixel_format_align() -> None: height = 480 for width in range(2, 258, 2): frame_yuv = VideoFrame(width, height, "yuv420p") From 256f1ba9eea9a078289404d856161cf5d1445205 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 25 Sep 2024 05:26:16 -0400 Subject: [PATCH 373/846] If Conda needs this, they can patch it themselves --- av/__init__.py | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/av/__init__.py b/av/__init__.py index b5ea598b1..8752c26fd 100644 --- a/av/__init__.py +++ b/av/__init__.py @@ -1,20 +1,3 @@ -import os -import sys - -# Some Python versions distributed by Conda have a buggy `os.add_dll_directory` -# which prevents binary wheels from finding the FFmpeg DLLs in the `av.libs` -# directory. We work around this by adding `av.libs` to the PATH. -if ( - os.name == "nt" - and sys.version_info[:2] in ((3, 8), (3, 9)) - and os.path.exists(os.path.join(sys.base_prefix, "conda-meta")) -): - os.environ["PATH"] = ( - os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, "av.libs")) - + os.pathsep - + os.environ["PATH"] - ) - # MUST import the core before anything else in order to initialize the underlying # library that is being wrapped. from av._core import time_base, library_versions @@ -77,6 +60,8 @@ def get_include() -> str: """ Returns the path to the `include` folder to be used when building extensions to av. """ + import os + # Installed package include_path = os.path.join(os.path.dirname(__file__), "include") if os.path.exists(include_path): From f32427406a1f940a7c2096435a37ae23af9dd2ef Mon Sep 17 00:00:00 2001 From: Dexer <73297572+DexerBR@users.noreply.github.com> Date: Wed, 25 Sep 2024 12:19:31 -0300 Subject: [PATCH 374/846] fix missing staticmethod in VideoFrame.from_bytes --- av/video/frame.pyx | 1 + 1 file changed, 1 insertion(+) diff --git a/av/video/frame.pyx b/av/video/frame.pyx index ee060e16a..a7c794a55 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -591,6 +591,7 @@ cdef class VideoFrame(Frame): return frame + @staticmethod def from_bytes(img_bytes: bytes, width: int, height: int, format="rgba", flip_horizontal=False, flip_vertical=False): frame = VideoFrame(width, height, format) if format == "rgba": From 020a0ce693490b891cdd7062876888677dfd29a2 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 25 Sep 2024 16:23:15 -0400 Subject: [PATCH 375/846] Add type stubs for common.py funcs --- av/filter/filter.pyi | 2 ++ tests/common.py | 56 ++++++++++++++++++++++++++-------------- tests/test_filters.py | 15 ++++++----- tests/test_videoframe.py | 18 ++++++------- 4 files changed, 55 insertions(+), 36 deletions(-) diff --git a/av/filter/filter.pyi b/av/filter/filter.pyi index f94c79e1c..2751e973c 100644 --- a/av/filter/filter.pyi +++ b/av/filter/filter.pyi @@ -18,4 +18,6 @@ class Filter: inputs: tuple[FilterPad, ...] outputs: tuple[FilterPad, ...] + def __init__(self, name: str) -> None: ... + filters_available: set[str] diff --git a/tests/common.py b/tests/common.py index 33b307c80..c67db04e5 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1,8 +1,11 @@ +from __future__ import annotations + import datetime import errno import functools import os import types +from typing import TYPE_CHECKING from unittest import TestCase as _Base import numpy as np @@ -16,6 +19,14 @@ except ImportError: has_pillow = False +if TYPE_CHECKING: + from typing import Any, Callable, TypeVar + + from PIL.Image import Image + + T = TypeVar("T") + + __all__ = ("fate_suite",) @@ -23,7 +34,7 @@ skip_tests = frozenset(os.environ.get("PYAV_SKIP_TESTS", "").split(",")) -def makedirs(path: str) -> None: +def safe_makedirs(path: str) -> None: try: os.makedirs(path) except OSError as e: @@ -61,22 +72,20 @@ def fate_png() -> str: return fate_suite("png1/55c99e750a5fd6_50314226.png") -def sandboxed(*args, **kwargs) -> str: - do_makedirs = kwargs.pop("makedirs", True) - base = kwargs.pop("sandbox", None) - timed = kwargs.pop("timed", False) - if kwargs: - raise TypeError("extra kwargs: %s" % ", ".join(sorted(kwargs))) - path = os.path.join(_sandbox(timed=timed) if base is None else base, *args) - if do_makedirs: - makedirs(os.path.dirname(path)) +def sandboxed( + *args: str, makedirs: bool = True, sandbox: str | None = None, timed: bool = False +) -> str: + path = os.path.join(_sandbox(timed) if sandbox is None else sandbox, *args) + if makedirs: + safe_makedirs(os.path.dirname(path)) + return path # Decorator for running a test in the sandbox directory -def run_in_sandbox(func): +def run_in_sandbox(func: Callable[..., T]) -> Callable[..., T]: @functools.wraps(func) - def _inner(self, *args, **kwargs): + def _inner(self: Any, *args: Any, **kwargs: Any) -> T: current_dir = os.getcwd() try: os.chdir(self.sandbox) @@ -104,13 +113,13 @@ def assertNdarraysEqual(a: np.ndarray, b: np.ndarray) -> None: assert False, f"ndarrays contents differ\n{msg}" -def assertImagesAlmostEqual(a, b, epsilon=0.1): +def assertImagesAlmostEqual(a: Image, b: Image, epsilon: float = 0.1) -> None: import PIL.ImageFilter as ImageFilter assert a.size == b.size a = a.filter(ImageFilter.BLUR).getdata() b = b.filter(ImageFilter.BLUR).getdata() - for i, ax, bx in zip(range(len(a)), a, b): + for i, ax, bx in zip(range(len(a)), a, b): # type: ignore diff = sum(abs(ac / 256 - bc / 256) for ac, bc in zip(ax, bx)) / 3 assert diff < epsilon, f"images differed by {diff} at index {i}; {ax} {bx}" @@ -119,14 +128,23 @@ class TestCase(_Base): @classmethod def _sandbox(cls, timed: bool = True) -> str: path = os.path.join(_sandbox(timed=timed), cls.__name__) - makedirs(path) + safe_makedirs(path) return path @property def sandbox(self) -> str: return self._sandbox(timed=True) - def sandboxed(self, *args, **kwargs) -> str: - kwargs.setdefault("sandbox", self.sandbox) - kwargs.setdefault("timed", True) - return sandboxed(*args, **kwargs) + def sandboxed( + self, + *args: str, + makedirs: bool = True, + timed: bool = True, + sandbox: str | None = None, + ) -> str: + if sandbox is None: + return sandboxed( + *args, makedirs=makedirs, timed=timed, sandbox=self.sandbox + ) + else: + return sandboxed(*args, makedirs=makedirs, timed=timed, sandbox=sandbox) diff --git a/tests/test_filters.py b/tests/test_filters.py index a87d6b5b0..202da3714 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -34,11 +34,11 @@ def generate_audio_frame( return frame -def pull_until_blocked(graph): - frames = [] +def pull_until_blocked(graph: Graph) -> list[av.VideoFrame]: + frames: list[av.VideoFrame] = [] while True: try: - frames.append(graph.pull()) + frames.append(graph.vpull()) except av.AVError as e: if e.errno != errno.EAGAIN: raise @@ -46,7 +46,7 @@ def pull_until_blocked(graph): class TestFilters(TestCase): - def test_filter_descriptor(self): + def test_filter_descriptor(self) -> None: f = Filter("testsrc") assert f.name == "testsrc" assert f.description == "Generate test pattern." @@ -86,24 +86,25 @@ def test_generator_graph(self): if has_pillow: frame.to_image().save(self.sandboxed("mandelbrot2.png")) - def test_auto_find_sink(self): + def test_auto_find_sink(self) -> None: graph = Graph() src = graph.add("testsrc") src.link_to(graph.add("buffersink")) graph.configure() - frame = graph.pull() + frame = graph.vpull() if has_pillow: frame.to_image().save(self.sandboxed("mandelbrot3.png")) - def test_delegate_sink(self): + def test_delegate_sink(self) -> None: graph = Graph() src = graph.add("testsrc") src.link_to(graph.add("buffersink")) graph.configure() frame = src.pull() + assert isinstance(frame, av.VideoFrame) if has_pillow: frame.to_image().save(self.sandboxed("mandelbrot4.png")) diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 8dcdb838a..9b77af916 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -43,10 +43,8 @@ def test_opaque() -> None: assert type(frame.opaque) is tuple and len(frame.opaque) == 2 -def test_invalid_pixel_format(): - with pytest.raises( - ValueError, match="not a pixel format: '__unknown_pix_fmt'" - ) as cm: +def test_invalid_pixel_format() -> None: + with pytest.raises(ValueError, match="not a pixel format: '__unknown_pix_fmt'"): VideoFrame(640, 480, "__unknown_pix_fmt") @@ -90,7 +88,7 @@ def test_yuv420p_planes() -> None: assert frame.planes[i].buffer_size == 320 * 240 -def test_yuv420p_planes_align(): +def test_yuv420p_planes_align() -> None: # If we request 8-byte alignment for a width which is not a multiple of 8, # the line sizes are larger than the plane width. frame = VideoFrame(318, 238, "yuv420p") @@ -106,7 +104,7 @@ def test_yuv420p_planes_align(): assert frame.planes[i].buffer_size == 160 * 119 -def test_rgb24_planes(): +def test_rgb24_planes() -> None: frame = VideoFrame(640, 480, "rgb24") assert len(frame.planes) == 1 assert frame.planes[0].width == 640 @@ -115,7 +113,7 @@ def test_rgb24_planes(): assert frame.planes[0].buffer_size == 640 * 480 * 3 -def test_memoryview_read(): +def test_memoryview_read() -> None: frame = VideoFrame(640, 480, "rgb24") frame.planes[0].update(b"01234" + (b"x" * (640 * 480 * 3 - 5))) mem = memoryview(frame.planes[0]) @@ -129,11 +127,11 @@ def test_memoryview_read(): class TestVideoFrameImage(TestCase): - def setUp(self): + def setUp(self) -> None: if not has_pillow: pytest.skip() - def test_roundtrip(self): + def test_roundtrip(self) -> None: import PIL.Image as Image image = Image.open(fate_png()) @@ -142,7 +140,7 @@ def test_roundtrip(self): img.save(self.sandboxed("roundtrip-high.jpg")) assertImagesAlmostEqual(image, img) - def test_to_image_rgb24(self): + def test_to_image_rgb24(self) -> None: sizes = [(318, 238), (320, 240), (500, 500)] for width, height in sizes: frame = VideoFrame(width, height, format="rgb24") From 39fecbd85781e8b3dcdc8fa878bb83376611e690 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 2 Oct 2024 12:34:10 -0400 Subject: [PATCH 376/846] Use ffmpeg 7.1 --- .github/workflows/smoke.yml | 10 ++++++---- scripts/ffmpeg-7.1.json | 3 +++ 2 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 scripts/ffmpeg-7.1.json diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index f5fabe342..0ef3f650f 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -36,10 +36,11 @@ jobs: config: - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.1.1", extras: true} - {os: ubuntu-latest, python: "3.9", ffmpeg: "7.0.2"} - - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.0"} + - {os: ubuntu-latest, python: "3.9", ffmpeg: "7.1"} - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1.1"} - - {os: macos-12, python: "3.9", ffmpeg: "7.0.2"} - - {os: macos-12, python: "3.9", ffmpeg: "6.1.1"} + - {os: macos-13, python: "3.9", ffmpeg: "7.1"} + - {os: macos-13, python: "3.9", ffmpeg: "7.0.2"} + - {os: macos-13, python: "3.9", ffmpeg: "6.1.1"} env: PYAV_PYTHON: python${{ matrix.config.python }} @@ -65,7 +66,7 @@ jobs: sudo apt-get install doxygen wget fi ;; - macos-12) + macos-13) brew install automake libtool nasm pkg-config libpng libvorbis libvpx opus x264 ;; esac @@ -112,6 +113,7 @@ jobs: fail-fast: false matrix: config: + - {os: windows-latest, python: "3.9", ffmpeg: "7.1"} - {os: windows-latest, python: "3.9", ffmpeg: "7.0"} - {os: windows-latest, python: "3.9", ffmpeg: "6.1"} - {os: windows-latest, python: "3.9", ffmpeg: "6.0"} diff --git a/scripts/ffmpeg-7.1.json b/scripts/ffmpeg-7.1.json new file mode 100644 index 000000000..06e80636a --- /dev/null +++ b/scripts/ffmpeg-7.1.json @@ -0,0 +1,3 @@ +{ + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.1-1/ffmpeg-{platform}.tar.gz" +} \ No newline at end of file From e3598c2444c6912c5ff1bb7b0f4f0f3af7dac2ad Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 3 Oct 2024 02:47:30 -0400 Subject: [PATCH 377/846] Use pytest --- .github/workflows/tests.yml | 4 ++-- tests/test_videoframe.py | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 27e394948..924642e0f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -69,8 +69,8 @@ jobs: CIBW_ENVIRONMENT_WINDOWS: INCLUDE=C:\\cibw\\vendor\\include LIB=C:\\cibw\\vendor\\lib PYAV_SKIP_TESTS=unicode_filename CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: delvewheel repair --add-path C:\cibw\vendor\bin -w {dest_dir} {wheel} CIBW_SKIP: "*-musllinux*" - CIBW_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m unittest discover -t {project} -s tests && mv {project}/av.disabled {project}/av - CIBW_TEST_REQUIRES: numpy + CIBW_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m pytest {package}/tests && mv {project}/av.disabled {project}/av + CIBW_TEST_REQUIRES: pytest numpy # skip tests when there are no binary wheels of numpy CIBW_TEST_SKIP: pp* *_i686 run: | diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 9b77af916..8e14dcdf3 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -174,6 +174,9 @@ def test_basic_to_ndarray() -> None: def test_to_image_with_dimensions() -> None: + if not has_pillow: + pytest.skip() + img = VideoFrame(640, 480, format="rgb24").to_image(width=320, height=240) assert img.size == (320, 240) From 340ed1c93c579985c5030f863a7d57ad45b28d9a Mon Sep 17 00:00:00 2001 From: Mark Harfouche Date: Fri, 4 Oct 2024 13:55:01 -0400 Subject: [PATCH 378/846] Add ffmpeg version info --- av/__init__.py | 3 ++- av/_core.pyi | 1 + av/_core.pyx | 6 ++++++ include/libavutil/avutil.pxd | 1 + scripts/test | 2 ++ 5 files changed, 12 insertions(+), 1 deletion(-) diff --git a/av/__init__.py b/av/__init__.py index 8752c26fd..9211d7634 100644 --- a/av/__init__.py +++ b/av/__init__.py @@ -1,6 +1,6 @@ # MUST import the core before anything else in order to initialize the underlying # library that is being wrapped. -from av._core import time_base, library_versions +from av._core import time_base, library_versions, ffmpeg_version_info # Capture logging (by importing it). from av import logging @@ -32,6 +32,7 @@ __all__ = ( "__version__", "time_base", + "ffmpeg_version_info", "library_versions", "AudioCodecContext", "AudioFifo", diff --git a/av/_core.pyi b/av/_core.pyi index 26b3bc0c8..0ee0a5626 100644 --- a/av/_core.pyi +++ b/av/_core.pyi @@ -7,5 +7,6 @@ class _Meta(TypedDict): library_meta: dict[str, _Meta] library_versions: dict[str, tuple[int, int, int]] +ffmpeg_version_info: str time_base: int diff --git a/av/_core.pyx b/av/_core.pyx index 4100c5857..edca772f5 100644 --- a/av/_core.pyx +++ b/av/_core.pyx @@ -18,6 +18,12 @@ cdef decode_version(v): return (major, minor, micro) +# Return an informative version string. +# This usually is the actual release version number or a git commit +# description. This string has no fixed format and can change any time. It +# should never be parsed by code. +ffmpeg_version_info = lib.av_version_info() + library_meta = { "libavutil": dict( version=decode_version(lib.avutil_version()), diff --git a/include/libavutil/avutil.pxd b/include/libavutil/avutil.pxd index f874761ea..58dd43922 100644 --- a/include/libavutil/avutil.pxd +++ b/include/libavutil/avutil.pxd @@ -12,6 +12,7 @@ cdef extern from "libavutil/rational.h" nogil: cdef extern from "libavutil/avutil.h" nogil: + cdef const char* av_version_info() cdef int avutil_version() cdef char* avutil_configuration() cdef char* avutil_license() diff --git a/scripts/test b/scripts/test index 270a4ec69..8244778b3 100755 --- a/scripts/test +++ b/scripts/test @@ -18,6 +18,8 @@ istest() { return $? } +$PYAV_PYTHON -c "import av; print(f'PyAV: {av.__version__}'); print(f'FFMPEG: {av.ffmpeg_version_info}')" + if istest main; then $PYAV_PYTHON -m pytest fi From 516e07fcdc9145f39ef123d6fe9a0284d3a1df42 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 5 Oct 2024 20:15:51 -0400 Subject: [PATCH 379/846] Use ffmpeg 7.1 as build default --- scripts/activate.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/activate.sh b/scripts/activate.sh index b7bdf7a6f..72bd44738 100755 --- a/scripts/activate.sh +++ b/scripts/activate.sh @@ -17,7 +17,7 @@ if [[ ! "$PYAV_LIBRARY" ]]; then return 1 fi else - PYAV_LIBRARY=ffmpeg-7.0.2 + PYAV_LIBRARY=ffmpeg-7.1 echo "No \$PYAV_LIBRARY set; defaulting to $PYAV_LIBRARY" fi fi From ab8eefa947ecacceb769de315436992a775d9752 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 5 Oct 2024 20:48:15 -0400 Subject: [PATCH 380/846] Use proper roles in changelog --- AUTHORS.py | 103 ++++++++++++++++++++++++--------------------- AUTHORS.rst | 9 ++-- CHANGELOG.rst | 81 +++++++++++++++++------------------ docs/api/codec.rst | 1 - 4 files changed, 102 insertions(+), 92 deletions(-) diff --git a/AUTHORS.py b/AUTHORS.py index fd47a713d..f17b31aeb 100644 --- a/AUTHORS.py +++ b/AUTHORS.py @@ -1,69 +1,68 @@ import math import subprocess - -print('''Contributors +print( + """Contributors ============ All contributors (by number of commits): -''') +""" +) email_map = { - # Maintainers. - 'git@mikeboers.com': 'github@mikeboers.com', - 'mboers@keypics.com': 'github@mikeboers.com', - 'mikeb@loftysky.com': 'github@mikeboers.com', - 'mikeb@markmedia.co': 'github@mikeboers.com', - 'westernx@mikeboers.com': 'github@mikeboers.com', - + "git@mikeboers.com": "github@mikeboers.com", + "mboers@keypics.com": "github@mikeboers.com", + "mikeb@loftysky.com": "github@mikeboers.com", + "mikeb@markmedia.co": "github@mikeboers.com", + "westernx@mikeboers.com": "github@mikeboers.com", # Junk. - 'mark@mark-VirtualBox.(none)': None, - + "mark@mark-VirtualBox.(none)": None, # Aliases. - 'a.davoudi@aut.ac.ir': 'davoudialireza@gmail.com', - 'tcaswell@bnl.gov': 'tcaswell@gmail.com', - 'xxr3376@gmail.com': 'xxr@megvii.com', - 'dallan@pha.jhu.edu': 'daniel.b.allan@gmail.com', - '61652821+laggykiller@users.noreply.github.com': 'chaudominic2@gmail.com', - + "a.davoudi@aut.ac.ir": "davoudialireza@gmail.com", + "tcaswell@bnl.gov": "tcaswell@gmail.com", + "xxr3376@gmail.com": "xxr@megvii.com", + "dallan@pha.jhu.edu": "daniel.b.allan@gmail.com", + "61652821+laggykiller@users.noreply.github.com": "chaudominic2@gmail.com", } name_map = { - 'caspervdw@gmail.com': 'Casper van der Wel', - 'daniel.b.allan@gmail.com': 'Dan Allan', - 'mgoacolou@cls.fr': 'Manuel Goacolou', - 'mindmark@gmail.com': 'Mark Reid', - 'moritzkassner@gmail.com': 'Moritz Kassner', - 'vidartf@gmail.com': 'Vidar Tonaas Fauske', - 'xxr@megvii.com': 'Xinran Xu', + "caspervdw@gmail.com": "Casper van der Wel", + "daniel.b.allan@gmail.com": "Dan Allan", + "mgoacolou@cls.fr": "Manuel Goacolou", + "mindmark@gmail.com": "Mark Reid", + "moritzkassner@gmail.com": "Moritz Kassner", + "vidartf@gmail.com": "Vidar Tonaas Fauske", + "xxr@megvii.com": "Xinran Xu", } github_map = { - 'billy.shambrook@gmail.com': 'billyshambrook', - 'daniel.b.allan@gmail.com': 'danielballan', - 'davoudialireza@gmail.com': 'adavoudi', - 'github@mikeboers.com': 'mikeboers', - 'jeremy.laine@m4x.org': 'jlaine', - 'kalle.litterfeldt@gmail.com': 'litterfeldt', - 'mindmark@gmail.com': 'markreidvfx', - 'moritzkassner@gmail.com': 'mkassner', - 'rush@logic.cz': 'radek-senfeld', - 'self@brendanlong.com': 'brendanlong', - 'tcaswell@gmail.com': 'tacaswell', - 'ulrik.mikaelsson@magine.com': 'rawler', - 'vidartf@gmail.com': 'vidartf', - 'willpatera@gmail.com': 'willpatera', - 'xxr@megvii.com': 'xxr3376', - 'chaudominic2@gmail.com': 'laggykiller', - 'wyattblue@auto-editor.com': 'WyattBlue', + "billy.shambrook@gmail.com": "billyshambrook", + "daniel.b.allan@gmail.com": "danielballan", + "davoudialireza@gmail.com": "adavoudi", + "github@mikeboers.com": "mikeboers", + "jeremy.laine@m4x.org": "jlaine", + "kalle.litterfeldt@gmail.com": "litterfeldt", + "mindmark@gmail.com": "markreidvfx", + "moritzkassner@gmail.com": "mkassner", + "rush@logic.cz": "radek-senfeld", + "self@brendanlong.com": "brendanlong", + "tcaswell@gmail.com": "tacaswell", + "ulrik.mikaelsson@magine.com": "rawler", + "vidartf@gmail.com": "vidartf", + "willpatera@gmail.com": "willpatera", + "xxr@megvii.com": "xxr3376", + "chaudominic2@gmail.com": "laggykiller", + "wyattblue@auto-editor.com": "WyattBlue", } email_count = {} -for line in subprocess.check_output(['git', 'log', '--format=%aN,%aE']).decode().splitlines(): - name, email = line.strip().rsplit(',', 1) +for line in ( + subprocess.check_output(["git", "log", "--format=%aN,%aE"]).decode().splitlines() +): + name, email = line.strip().rsplit(",", 1) email = email_map.get(email, email) if not email: @@ -89,7 +88,7 @@ names = name_map[email] if isinstance(names, set): - name = ', '.join(sorted(names)) + name = ", ".join(sorted(names)) else: name = names @@ -98,6 +97,16 @@ # The '-' vs '*' is so that Sphinx treats them as different lists, and # introduces a gap bettween them. if github: - print('%s %s <%s>; `@%s `_' % ('-*'[block_i % 2], name, email, github, github)) + print( + "%s %s <%s>; `@%s `_" + % ("-*"[block_i % 2], name, email, github, github) + ) else: - print('%s %s <%s>' % ('-*'[block_i % 2], name, email, )) + print( + "%s %s <%s>" + % ( + "-*"[block_i % 2], + name, + email, + ) + ) diff --git a/AUTHORS.rst b/AUTHORS.rst index bf3a6f49d..bc4db2a5b 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -6,8 +6,8 @@ All contributors (by number of commits): - Mike Boers ; `@mikeboers `_ * Jeremy Lainé ; `@jlaine `_ +* WyattBlue ; `@WyattBlue `_ -- WyattBlue ; `@WyattBlue `_ - Mark Reid ; `@markreidvfx `_ * Vidar Tonaas Fauske ; `@vidartf `_ @@ -19,11 +19,11 @@ All contributors (by number of commits): * JoeUgly <41972063+JoeUgly@users.noreply.github.com> * Justin Wong <46082645+uvjustin@users.noreply.github.com> -- Alba Mendez - Mark Harfouche +- Alba Mendez +- Dave Johansen - Xinran Xu ; `@xxr3376 `_ - Dan Allan ; `@danielballan `_ -- Dave Johansen - Moonsik Park - Santtu Keskinen - Christoph Rackwitz @@ -35,6 +35,7 @@ All contributors (by number of commits): - Wel C. van der - Will Patera ; `@willpatera `_ +* Dexer <73297572+DexerBR@users.noreply.github.com> * rutsh * Felix Vollmer * Santiago Castro @@ -55,11 +56,11 @@ All contributors (by number of commits): * zzjjbb <31069326+zzjjbb@users.noreply.github.com> * Hanz <40712686+HanzCEO@users.noreply.github.com> * Joe Schiff <41972063+JoeSchiff@users.noreply.github.com> -* Dexer <73297572+DexerBR@users.noreply.github.com> * Artturin * Ian Lee * Ryan Huang * Arthur Barros +* Carlos Ruiz * Carlos Ruiz * David Plowman * Maxime Desroches diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d43d3ddaa..941c4dabc 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -21,16 +21,17 @@ v13.1.0 Features: -- Allow passing Python objects around using `Frame.opaque` and `Packet.opaque`. -- Allow extradata to be set by encoders by @daveisfera in (:issue:`1526`). +- Allow passing Python objects around using `Frame.opaque` and `Packet.opaque` by :gh-user:`CarlosRDomin` and :gh-user:`WyattBlue` in (:pr:`1533`). +- Allow extradata to be set by encoders by :gh-user:`daveisfera` in (:pr:`1526`). +- Add getting ffmpeg version info string by :gh-user:`hmaarrfk` in (:pr:`1564`). Fixes: -- Remove the `deprecation` module in anticipation of `PEP 702 `_ +- Remove the `deprecation` module in anticipation of `PEP 702 `_. - Add type stubs to previously unannotated API sections. - Improve type stubs for both `mypy` and `mypy --strict`. - Permit only setting `time_base` with a Fraction, as mypy is unable to respect different types in getters vs setters. -- Declare `write_packet` function as const by @hmaarrfk in (:issue:`1517`). +- Declare `write_packet` function as const by :gh-user:`hmaarrfk` in (:pr:`1517`). v13.0.0 ------- @@ -62,20 +63,20 @@ v12.3.0 Features: -- Support libav's `av_log_set_level` by @materight in (:issue:`1448`). -- Add Graph.link_nodes by @WyattBlue in (:issue:`1449`). -- Add default codec properties by @WyattBlue in (:issue:`1450`). -- Remove the xvid and ass packages in ffmpeg binaries because they were unused by @WyattBlue in (:issue:`1462`). -- Add supported_codecs property to OutputContainer by @WyattBlue in (:issue:`1465`). -- Add text and dialogue property to AssSubtitle, remove TextSubtitle by @WyattBlue in (:issue:`1456`). +- Support libav's `av_log_set_level` by :gh-user:`materight` in (:pr:`1448`). +- Add Graph.link_nodes by :gh-user:`WyattBlue` in (:pr:`1449`). +- Add default codec properties by :gh-user:`WyattBlue` in (:pr:`1450`). +- Remove the xvid and ass packages in ffmpeg binaries because they were unused by :gh-user:`WyattBlue` in (:pr:`1462`). +- Add supported_codecs property to OutputContainer by :gh-user:`WyattBlue` in (:pr:`1465`). +- Add text and dialogue property to AssSubtitle, remove TextSubtitle by :gh-user:`WyattBlue` in (:pr:`1456`). Fixes: -- Include libav headers in final distribution by @materight in (:issue:`1455`). -- Fix segfault when calling subtitle_stream.decode() by @WyattBlue in (:issue:`1460`). -- flushing subtitle decoder requires a new uninitialized packet by @moonsikpark in (:issue:`1461`). -- Set default color range for VideoReformatter.format() by @elxy in (:issue:`1458`). -- Resampler: format, layout accepts `str` `int` too by @WyattBlue in (:issue:`1446`). +- Include libav headers in final distribution by :gh-user:`materight` in (:pr:`1455`). +- Fix segfault when calling subtitle_stream.decode() by :gh-user:`WyattBlue` in (:pr:`1460`). +- flushing subtitle decoder requires a new uninitialized packet by :gh-user:`moonsikpark` in (:pr:`1461`). +- Set default color range for VideoReformatter.format() by :gh-user:`elxy` in (:pr:`1458`). +- Resampler: format, layout accepts `str` `int` too by :gh-user:`WyattBlue` in (:pr:`1446`). v12.2.0 ------- @@ -98,19 +99,19 @@ Features: - Build binary wheels with webp support. - Allow disabling logs, disable logs by default. -- Add bitstream filters by @skeskinen in (:issue:`1375`) (:issue:`1379`). -- Expose CodecContext flush_buffers by @skeskinen in (:issue:`1382`). +- Add bitstream filters by :gh-user:`skeskinen` in (:pr:`1379` :issue:`1375`). +- Expose CodecContext flush_buffers by :gh-user:`skeskinen` in (:pr:`1382`). Fixes: - Fix type stubs, add missing type stubs. -- Add S12M_TIMECODE by @WyattBlue in (:issue:`1381`). -- Subtitle.text now returns bytes by @WyattBlue in (:issue:`1398`). -- Allow packet.duration to be writable by @WyattBlue in (:issue:`1399`). -- Remove deprecated `VideoStream.frame_rate` by @WyattBlue in (:issue:`1351`). -- Build with Arm for PyPy now by @WyattBlue in (:issue:`1395`). -- Fix #1378 by @WyattBlue in (:issue:`1400`). -- setup.py: use PKG_CONFIG env var to get the pkg-config to use by @Artturin in (:issue:`1387`). +- Add S12M_TIMECODE by :gh-user:`WyattBlue` in (:pr:`1381`). +- Subtitle.text now returns bytes by :gh-user:`WyattBlue` in (:pr:`1398`). +- Allow packet.duration to be writable by :gh-user:`WyattBlue` in (:pr:`1399`). +- Remove deprecated `VideoStream.frame_rate` by :gh-user:`WyattBlue` in (:pr:`1351`). +- Build with Arm for PyPy now by :gh-user:`WyattBlue` in (:pr:`1395`). +- Fix #1378 by :gh-user:`WyattBlue` in (:pr:`1400`). +- setup.py: use PKG_CONFIG env var to get the pkg-config to use by :gh-user:`Artturin` in (:pr:`1387`). v12.0.0 ------- @@ -120,28 +121,28 @@ Major: - Add type hints. - Update FFmpeg to 6.1.1 for the binary wheels. - Update libraries for the binary wheels (notably dav1d to 1.4.1). -- Deprecate VideoCodecContext.gop_size for decoders by @JoeSchiff in (:issue:`1256`). -- Deprecate frame.index by @JoeSchiff in (:issue:`1218`). +- Deprecate VideoCodecContext.gop_size for decoders by :gh-user:`JoeSchiff` in (:pr:`1256`). +- Deprecate frame.index by :gh-user:`JoeSchiff` in (:pr:`1218`). Features: -- Allow using pathlib.Path for av.open by @WyattBlue in (:issue:`1231`). -- Add `max_b_frames` property to CodecContext by @davidplowman in (:issue:`1119`). -- Add `encode_lazy` method to CodecContext by @rawler in (:issue:`1092`). -- Add `color_range` to CodecContext/Frame by @johanjeppsson in (:issue:`686`). -- Set `time_base` for AudioResampler by @daveisfera in (:issue:`1209`). -- Add support for ffmpeg's AVCodecContext::delay by @JoeSchiff in (:issue:`1279`). -- Add `color_primaries`, `color_trc`, `colorspace` to VideoStream by @WyattBlue in (:issue:`1304`). -- Add `bits_per_coded_sample` to VideoCodecContext by @rvanlaar in (:issue:`1203`). -- AssSubtitle.ass now returns as bytes by @WyattBlue in (:issue:`1333`). -- Expose DISPLAYMATRIX side data by @hyenal in (:issue:`1249`). +- Allow using pathlib.Path for av.open by :gh-user:`WyattBlue` in (:pr:`1231`). +- Add `max_b_frames` property to CodecContext by :gh-user:`davidplowman` in (:pr:`1119`). +- Add `encode_lazy` method to CodecContext by :gh-user:`rawler` in (:pr:`1092`). +- Add `color_range` to CodecContext/Frame by :gh-user:`johanjeppsson` in (:pr:`686`). +- Set `time_base` for AudioResampler by :gh-user:`daveisfera` in (:issue:`1209`). +- Add support for ffmpeg's AVCodecContext::delay by :gh-user:`JoeSchiff` in (:issue:`1279`). +- Add `color_primaries`, `color_trc`, `colorspace` to VideoStream by :gh-user:`WyattBlue` in (:pr:`1304`). +- Add `bits_per_coded_sample` to VideoCodecContext by :gh-user:`rvanlaar` in (:pr:`1203`). +- AssSubtitle.ass now returns as bytes by :gh-user:`WyattBlue` in (:pr:`1333`). +- Expose DISPLAYMATRIX side data by :gh-user:`hyenal` in (:pr:`1249`). Fixes: -- Convert deprecated Cython extension class properties to decorator syntax by @JoeSchiff -- Check None packet when setting time_base after decode by @philipnbbc in (:issue:`1281`). -- Remove deprecated `Buffer.to_bytes` by @WyattBlue in (:issue:`1286`). -- Remove deprecated `Packet.decode_one` by @WyattBlue in (:issue:`1301`). +- Convert deprecated Cython extension class properties to decorator syntax by :gh-user:`JoeSchiff`. +- Check None packet when setting time_base after decode by :gh-user:`philipnbbc` in (:pr:`1281`). +- Remove deprecated `Buffer.to_bytes` by :gh-user:`WyattBlue` in (:pr:`1286`). +- Remove deprecated `Packet.decode_one` by :gh-user:`WyattBlue` in (:pr:`1301`). v11.0.0 ------- diff --git a/docs/api/codec.rst b/docs/api/codec.rst index a3cc90e86..bd1a6b5f0 100644 --- a/docs/api/codec.rst +++ b/docs/api/codec.rst @@ -79,7 +79,6 @@ Attributes .. autoattribute:: CodecContext.profile .. autoattribute:: CodecContext.time_base -.. autoattribute:: CodecContext.ticks_per_frame .. autoattribute:: CodecContext.bit_rate .. autoattribute:: CodecContext.bit_rate_tolerance From 257f3846391af1b082bd989fc19395f4c7e7f2a1 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 5 Oct 2024 20:56:43 -0400 Subject: [PATCH 381/846] Release 13.1.0 --- av/about.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/about.py b/av/about.py index f0a0fa039..d876dfbee 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "13.1.0rc1" +__version__ = "13.1.0" From 86f66ac1c6682c5c07aeb8df4e070b0fe3723fb7 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 6 Oct 2024 00:47:26 -0400 Subject: [PATCH 382/846] Build wheels with ffmpeg 7.1 --- .github/workflows/tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 924642e0f..b51477083 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,7 +14,7 @@ jobs: - name: Build source package run: | pip install cython - python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.0.json /tmp/vendor + python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.1.json /tmp/vendor PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig python setup.py sdist - name: Upload source package uses: actions/upload-artifact@v4 @@ -61,9 +61,9 @@ jobs: env: CIBW_ARCHS: ${{ matrix.arch }} CIBW_BEFORE_ALL_LINUX: yum install -y alsa-lib libxcb - CIBW_BEFORE_BUILD: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.0.json /tmp/vendor - CIBW_BEFORE_BUILD_MACOS: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.0.json /tmp/vendor - CIBW_BEFORE_BUILD_WINDOWS: python scripts\fetch-vendor.py --config-file scripts\ffmpeg-7.0.json C:\cibw\vendor + CIBW_BEFORE_BUILD: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.1.json /tmp/vendor + CIBW_BEFORE_BUILD_MACOS: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.1.json /tmp/vendor + CIBW_BEFORE_BUILD_WINDOWS: python scripts\fetch-vendor.py --config-file scripts\ffmpeg-7.1.json C:\cibw\vendor CIBW_ENVIRONMENT_LINUX: LD_LIBRARY_PATH=/tmp/vendor/lib:$LD_LIBRARY_PATH PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig CIBW_ENVIRONMENT_MACOS: PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig LDFLAGS=-headerpad_max_install_names CIBW_ENVIRONMENT_WINDOWS: INCLUDE=C:\\cibw\\vendor\\include LIB=C:\\cibw\\vendor\\lib PYAV_SKIP_TESTS=unicode_filename From 80f3beb680785904c0f8eb9c162db1dda6da63b1 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 10 Oct 2024 16:01:10 -0400 Subject: [PATCH 383/846] Add remux subtitles example --- docs/cookbook/audio.rst | 8 ++++++ docs/cookbook/subtitles.rst | 10 ++++++++ examples/{audio_atempo.py => audio/atempo.py} | 4 --- examples/basics/remux.py | 2 ++ examples/basics/save_keyframes.py | 9 ++----- examples/subtitles/remux.py | 25 +++++++++++++++++++ 6 files changed, 47 insertions(+), 11 deletions(-) create mode 100644 docs/cookbook/audio.rst create mode 100644 docs/cookbook/subtitles.rst rename examples/{audio_atempo.py => audio/atempo.py} (91%) create mode 100644 examples/subtitles/remux.py diff --git a/docs/cookbook/audio.rst b/docs/cookbook/audio.rst new file mode 100644 index 000000000..39ccc62b8 --- /dev/null +++ b/docs/cookbook/audio.rst @@ -0,0 +1,8 @@ +Audio +===== + + +Filters +------- + +.. literalinclude:: ../../examples/audio/atempo.py diff --git a/docs/cookbook/subtitles.rst b/docs/cookbook/subtitles.rst new file mode 100644 index 000000000..e3ca79d5d --- /dev/null +++ b/docs/cookbook/subtitles.rst @@ -0,0 +1,10 @@ +Subtitles +========= + + +Remuxing +-------- + +Remuxing is copying stream(s) from one container to the other without transcoding it. By doing so, the data does not suffer any generational loss, and is the full quality that it was in the source container. + +.. literalinclude:: ../../examples/subtitles/remux.py diff --git a/examples/audio_atempo.py b/examples/audio/atempo.py similarity index 91% rename from examples/audio_atempo.py rename to examples/audio/atempo.py index 62a126705..9123a7d9c 100644 --- a/examples/audio_atempo.py +++ b/examples/audio/atempo.py @@ -6,12 +6,9 @@ output_file = av.open("output.wav", mode="w") input_stream = input_file.streams.audio[0] - output_stream = output_file.add_stream("pcm_s16le", rate=input_stream.rate) -assert isinstance(output_stream, av.audio.AudioStream) graph = av.filter.Graph() - graph.link_nodes( graph.add_abuffer(template=input_stream), graph.add("atempo", "2.0"), @@ -31,6 +28,5 @@ for packet in output_stream.encode(None): output_file.mux(packet) -# Close the files input_file.close() output_file.close() diff --git a/examples/basics/remux.py b/examples/basics/remux.py index befc24f8a..5abb199b8 100644 --- a/examples/basics/remux.py +++ b/examples/basics/remux.py @@ -1,6 +1,8 @@ import av import av.datasets +av.logging.set_level(av.logging.VERBOSE) + input_ = av.open(av.datasets.curated("pexels/time-lapse-video-of-night-sky-857195.mp4")) output = av.open("remuxed.mkv", "w") diff --git a/examples/basics/save_keyframes.py b/examples/basics/save_keyframes.py index bc47376cd..6a68de73b 100644 --- a/examples/basics/save_keyframes.py +++ b/examples/basics/save_keyframes.py @@ -7,11 +7,6 @@ stream = container.streams.video[0] stream.codec_context.skip_frame = "NONKEY" - for frame in container.decode(stream): + for i, frame in enumerate(container.decode(stream)): print(frame) - - # We use `frame.pts` as `frame.index` won't make must sense with the `skip_frame`. - frame.to_image().save( - "night-sky.{:04d}.jpg".format(frame.pts), - quality=80, - ) + frame.to_image().save(f"night-sky.{i:04d}.jpg", quality=80) diff --git a/examples/subtitles/remux.py b/examples/subtitles/remux.py new file mode 100644 index 000000000..5b3031788 --- /dev/null +++ b/examples/subtitles/remux.py @@ -0,0 +1,25 @@ +import av + +av.logging.set_level(av.logging.VERBOSE) + +input_ = av.open("resources/webvtt.mkv") +output = av.open("remuxed.vtt", "w") + +in_stream = input_.streams.subtitles[0] +out_stream = output.add_stream(template=in_stream) + +for packet in input_.demux(in_stream): + if packet.dts is None: + continue + packet.stream = out_stream + output.mux(packet) + +input_.close() +output.close() + +print("Remuxing done") + +with av.open("remuxed.vtt") as f: + for subset in f.decode(subtitles=0): + for sub in subset: + print(sub.ass) From ae3cb97027fff155e1d51caeb9990cae59e0bc67 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 10 Oct 2024 16:41:50 -0400 Subject: [PATCH 384/846] PyAV 14 preliminary changes * Remove `AVError` alias, use `FFmpegError` directly * Drop ffmpeg 6 --- .github/workflows/smoke.yml | 16 ++++++---------- CHANGELOG.rst | 8 ++++++++ av/__init__.py | 3 --- av/about.py | 2 +- av/audio/frame.pyx | 1 - av/audio/resampler.pyx | 2 +- av/utils.pyx | 1 - docs/api/utils.rst | 2 -- include/libavcodec/avcodec.pxd | 1 - scripts/ffmpeg-6.0.json | 3 --- scripts/ffmpeg-6.1.json | 3 --- tests/test_filters.py | 2 +- 12 files changed, 17 insertions(+), 27 deletions(-) delete mode 100644 scripts/ffmpeg-6.0.json delete mode 100644 scripts/ffmpeg-6.1.json diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 0ef3f650f..c40464af5 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -34,13 +34,11 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.1.1", extras: true} + - {os: ubuntu-latest, python: "3.9", ffmpeg: "7.1", extras: true} - {os: ubuntu-latest, python: "3.9", ffmpeg: "7.0.2"} - - {os: ubuntu-latest, python: "3.9", ffmpeg: "7.1"} - - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1.1"} - - {os: macos-13, python: "3.9", ffmpeg: "7.1"} - - {os: macos-13, python: "3.9", ffmpeg: "7.0.2"} - - {os: macos-13, python: "3.9", ffmpeg: "6.1.1"} + - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "7.1"} + - {os: macos-14, python: "3.9", ffmpeg: "7.1"} + - {os: macos-14, python: "3.9", ffmpeg: "7.0.2"} env: PYAV_PYTHON: python${{ matrix.config.python }} @@ -61,12 +59,12 @@ jobs: ubuntu-latest) sudo apt-get update sudo apt-get install autoconf automake build-essential cmake \ - libtool pkg-config yasm zlib1g-dev libvorbis-dev libx264-dev + libtool pkg-config nasm zlib1g-dev libvorbis-dev libx264-dev if [[ "${{ matrix.config.extras }}" ]]; then sudo apt-get install doxygen wget fi ;; - macos-13) + macos-14) brew install automake libtool nasm pkg-config libpng libvorbis libvpx opus x264 ;; esac @@ -115,8 +113,6 @@ jobs: config: - {os: windows-latest, python: "3.9", ffmpeg: "7.1"} - {os: windows-latest, python: "3.9", ffmpeg: "7.0"} - - {os: windows-latest, python: "3.9", ffmpeg: "6.1"} - - {os: windows-latest, python: "3.9", ffmpeg: "6.0"} steps: - name: Checkout diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 941c4dabc..5becaf5f6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,14 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. +v14.0.0 +------- + +Major: + +- Drop FFmpeg 6. +- Remove ``AVError`` alias (use ``FFmpegError`` directly instead). + v13.1.0 ------- diff --git a/av/__init__.py b/av/__init__.py index 9211d7634..cbc3c8a2f 100644 --- a/av/__init__.py +++ b/av/__init__.py @@ -26,9 +26,6 @@ from av.video.frame import VideoFrame from av.video.stream import VideoStream -# Backwards compatibility -AVError = FFmpegError # noqa: F405 - __all__ = ( "__version__", "time_base", diff --git a/av/about.py b/av/about.py index d876dfbee..a3d32212b 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "13.1.0" +__version__ = "14.0.0rc1" diff --git a/av/audio/frame.pyx b/av/audio/frame.pyx index 8260ccd2a..1e45842ea 100644 --- a/av/audio/frame.pyx +++ b/av/audio/frame.pyx @@ -47,7 +47,6 @@ cdef class AudioFrame(Frame): self.ptr.nb_samples = nb_samples self.ptr.format = format self.ptr.ch_layout = layout - # [FFMPEG6] self.ptr.channels = layout.nb_channels # ffmpeg 6 only # Sometimes this is called twice. Oh well. self._init_user_attributes() diff --git a/av/audio/resampler.pyx b/av/audio/resampler.pyx index 5c6020762..c46ed8475 100644 --- a/av/audio/resampler.pyx +++ b/av/audio/resampler.pyx @@ -111,7 +111,7 @@ cdef class AudioResampler: output.append(self.graph.pull()) except EOFError: break - except av.AVError as e: + except av.FFmpegError as e: if e.errno != errno.EAGAIN: raise break diff --git a/av/utils.pyx b/av/utils.pyx index afc82d24d..23f2d10b9 100644 --- a/av/utils.pyx +++ b/av/utils.pyx @@ -83,5 +83,4 @@ cdef flag_in_bitfield(uint64_t bitfield, uint64_t flag): # === BACKWARDS COMPAT === -from .error import FFmpegError as AVError from .error import err_check diff --git a/docs/api/utils.rst b/docs/api/utils.rst index 820f30f0a..ecc1c6faa 100644 --- a/docs/api/utils.rst +++ b/docs/api/utils.rst @@ -14,5 +14,3 @@ Other .. automodule:: av.utils :members: - - .. autoclass:: AVError diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 805bf50cb..f414e59d4 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -384,7 +384,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: int nb_samples # Audio samples int sample_rate # Audio Sample rate - # [FFMPEG6] int channels AVChannelLayout ch_layout diff --git a/scripts/ffmpeg-6.0.json b/scripts/ffmpeg-6.0.json deleted file mode 100644 index af6b3332d..000000000 --- a/scripts/ffmpeg-6.0.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/6.0.0-1/ffmpeg-{platform}.tar.gz" -} \ No newline at end of file diff --git a/scripts/ffmpeg-6.1.json b/scripts/ffmpeg-6.1.json deleted file mode 100644 index bf0604812..000000000 --- a/scripts/ffmpeg-6.1.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/6.1.1-5/ffmpeg-{platform}.tar.gz" -} \ No newline at end of file diff --git a/tests/test_filters.py b/tests/test_filters.py index 202da3714..7722de735 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -39,7 +39,7 @@ def pull_until_blocked(graph: Graph) -> list[av.VideoFrame]: while True: try: frames.append(graph.vpull()) - except av.AVError as e: + except av.FFmpegError as e: if e.errno != errno.EAGAIN: raise return frames From 5d5cc3df49b3cfc179c0c491dac19b7c8031b9c3 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 10 Oct 2024 17:00:39 -0400 Subject: [PATCH 385/846] Stop using AVStream.side_data --- av/stream.pxd | 1 - av/stream.pyi | 2 -- av/stream.pyx | 16 ---------------- include/libavformat/avformat.pxd | 4 ---- include/libavutil/avutil.pxd | 3 --- tests/test_streams.py | 16 +++++----------- 6 files changed, 5 insertions(+), 37 deletions(-) diff --git a/av/stream.pxd b/av/stream.pxd index 8ebda5704..d39585167 100644 --- a/av/stream.pxd +++ b/av/stream.pxd @@ -21,7 +21,6 @@ cdef class Stream: # Private API. cdef _init(self, Container, lib.AVStream*, CodecContext) cdef _finalize_for_output(self) - cdef _get_side_data(self, lib.AVStream *stream) cdef _set_time_base(self, value) cdef _set_id(self, value) diff --git a/av/stream.pyi b/av/stream.pyi index 212c721a9..89f49c12f 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -20,8 +20,6 @@ class Stream: metadata: dict[str, str] id: int profile: str - nb_side_data: int - side_data: dict[str, str] index: int time_base: Fraction | None average_rate: Fraction | None diff --git a/av/stream.pyx b/av/stream.pyx index 44354b651..19ac8e703 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -85,8 +85,6 @@ cdef class Stream: self.codec_context = codec_context if self.codec_context: self.codec_context.stream_index = stream.index - - self.nb_side_data, self.side_data = self._get_side_data(stream) self.metadata = avdict_to_dict( stream.metadata, @@ -128,20 +126,6 @@ cdef class Stream: # Lets just copy what we want. err_check(lib.avcodec_parameters_from_context(self.ptr.codecpar, self.codec_context.ptr)) - cdef _get_side_data(self, lib.AVStream *stream): - # Get DISPLAYMATRIX SideData from a lib.AVStream object. - # Returns: tuple[int, dict[str, Any]] - - nb_side_data = stream.nb_side_data - side_data = {} - - for i in range(nb_side_data): - # Based on: https://www.ffmpeg.org/doxygen/trunk/dump_8c_source.html#l00430 - if stream.side_data[i].type == lib.AV_PKT_DATA_DISPLAYMATRIX: - side_data["DISPLAYMATRIX"] = lib.av_display_rotation_get(stream.side_data[i].data) - - return nb_side_data, side_data - @property def id(self): """ diff --git a/include/libavformat/avformat.pxd b/include/libavformat/avformat.pxd index 1e16378bf..5fa25043a 100644 --- a/include/libavformat/avformat.pxd +++ b/include/libavformat/avformat.pxd @@ -46,10 +46,6 @@ cdef extern from "libavformat/avformat.h" nogil: AVRational r_frame_rate AVRational sample_aspect_ratio - int nb_side_data - AVPacketSideData *side_data - - # http://ffmpeg.org/doxygen/trunk/structAVIOContext.html cdef struct AVIOContext: unsigned char* buffer diff --git a/include/libavutil/avutil.pxd b/include/libavutil/avutil.pxd index 58dd43922..ed281aeaf 100644 --- a/include/libavutil/avutil.pxd +++ b/include/libavutil/avutil.pxd @@ -4,9 +4,6 @@ from libc.stdint cimport int64_t, uint8_t, uint64_t, int32_t cdef extern from "libavutil/mathematics.h" nogil: pass -cdef extern from "libavutil/display.h" nogil: - cdef double av_display_rotation_get(const int32_t matrix[9]) - cdef extern from "libavutil/rational.h" nogil: cdef int av_reduce(int *dst_num, int *dst_den, int64_t num, int64_t den, int64_t max) diff --git a/tests/test_streams.py b/tests/test_streams.py index 64bb63c36..b6097d537 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -35,15 +35,9 @@ def test_selection(self) -> None: data = container.streams.data[0] assert data == container.streams.best("data") - def test_noside_data(self) -> None: - container = av.open(fate_suite("h264/interlaced_crop.mp4")) - video = container.streams.video[0] - - assert video.nb_side_data == 0 - - def test_side_data(self) -> None: - container = av.open(fate_suite("mov/displaymatrix.mov")) - video = container.streams.video[0] + # def test_side_data(self) -> None: + # container = av.open(fate_suite("mov/displaymatrix.mov")) + # video = container.streams.video[0] - assert video.nb_side_data == 1 - assert video.side_data["DISPLAYMATRIX"] == -90.0 + # assert video.nb_side_data == 1 + # assert video.side_data["DISPLAYMATRIX"] == -90.0 From c007052d7e79a7ea8e073e8b981a308ee94f8811 Mon Sep 17 00:00:00 2001 From: henri-gasc Date: Thu, 10 Oct 2024 22:22:14 +0000 Subject: [PATCH 386/846] Use GCC compatible CFLAGS in Makefile A quick search in GCC source code show that -Wno-incompatible-function-pointer-types does not exist as a flag. However, there is the close flag -Wno-incompatible-pointer-types --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 77178b9d1..1cd3c39a4 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ LDFLAGS ?= "" -CFLAGS ?= "-O0 -Wno-incompatible-function-pointer-types" +CFLAGS ?= "-O0 -Wno-incompatible-pointer-types -Wno-unreachable-code" PYAV_PYTHON ?= python PYAV_PIP ?= pip From 300149ba34f3c1dffc1aba7802a6fcc5f81fa396 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 10 Oct 2024 20:53:09 -0400 Subject: [PATCH 387/846] Disallow `rate` arg to be a float Additionally, only allow audio stream `rate` to be `int | None` --- av/container/output.pyi | 6 +++--- av/container/output.pyx | 13 +++++++++---- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/av/container/output.pyi b/av/container/output.pyi index 760532a96..7891e17b5 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -15,7 +15,7 @@ class OutputContainer(Container): def add_stream( self, codec_name: Literal["pcm_s16le", "aac", "mp3", "mp2"], - rate: Fraction | int | float | None = None, + rate: int | None = None, template: None = None, options: dict[str, str] | None = None, **kwargs, @@ -24,7 +24,7 @@ class OutputContainer(Container): def add_stream( self, codec_name: Literal["h264", "mpeg4", "png", "qtrle"], - rate: Fraction | int | float | None = None, + rate: Fraction | int | None = None, template: None = None, options: dict[str, str] | None = None, **kwargs, @@ -33,7 +33,7 @@ class OutputContainer(Container): def add_stream( self, codec_name: str | None = None, - rate: Fraction | int | float | None = None, + rate: Fraction | int | None = None, template: Stream | None = None, options: dict[str, str] | None = None, **kwargs, diff --git a/av/container/output.pyx b/av/container/output.pyx index 717df8fb3..9e344804c 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -49,9 +49,8 @@ cdef class OutputContainer(Container): :param str codec_name: The name of a codec. :param rate: The frame rate for video, and sample rate for audio. - Examples for video include ``24``, ``23.976``, and - ``Fraction(30000,1001)``. Examples for audio include ``48000`` - and ``44100``. + Examples for video include ``24`` and ``Fraction(30000, 1001)``. + Examples for audio include ``48000`` and ``44100``. :param template: Copy codec from another :class:`~av.stream.Stream` instance. :param dict options: Stream options. :param \\**kwargs: Set attributes of the stream. @@ -115,7 +114,13 @@ cdef class OutputContainer(Container): to_avrational(kwargs.pop("time_base"), &codec_context.time_base) except KeyError: pass - codec_context.sample_rate = rate or 48000 + + if rate is None: + codec_context.sample_rate = 48000 + elif type(rate) is int: + codec_context.sample_rate = rate + else: + raise TypeError("audio stream `rate` must be: int | None") stream.time_base = codec_context.time_base lib.av_channel_layout_default(&codec_context.ch_layout, 2) From 329c377440b49c0e659b967294a88ff9861d949d Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 12 Oct 2024 02:12:58 -0400 Subject: [PATCH 388/846] Recreate logo in higher resolution Use webp format for logo. Always underline links in the body docs. --- docs/_static/custom.css | 4 ++++ docs/_static/logo-250.png | Bin 12552 -> 0 bytes docs/_static/logo.webp | Bin 0 -> 4154 bytes docs/conf.py | 2 +- 4 files changed, 5 insertions(+), 1 deletion(-) delete mode 100644 docs/_static/logo-250.png create mode 100644 docs/_static/logo.webp diff --git a/docs/_static/custom.css b/docs/_static/custom.css index 1069c8f1d..b95a5f253 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -12,3 +12,7 @@ .ffmpeg-quicklink:after { content: "]"; } + +.body a, .document a { + text-decoration: underline !important; +} diff --git a/docs/_static/logo-250.png b/docs/_static/logo-250.png deleted file mode 100644 index 32cb6bfa1725152483fc7d83f9afc3d6966088b5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12552 zcmV+jG55}iP)004R= z004l4008;_004mL004C`008P>0026e000+nl3&F}001zfNkl@8E-A`U=M86q-75flf4gQ?&`+@kVRRAh(@K}7{YQ1C$!1QBHK(E;5n zUD?U+kDE%mbbWjY;2U5e zkXOs^K%w%+YB95a#Wb|}>3HA)bwIi@JP0hUaNfTT-`&IZdmHEl?5)PVvA~}|Ooij> zI%?|;w%=Ua^WNnaK9T1MbD3|12YdxdU zgE}SpZC6oy`iP=HW?nJ6vV5O*3ORm=Y zoR;vHEMnl;e0}i88HI`xgVBH>RO^vFYpRzk)xN0xLpZtl|2#%G59$<3pyO54o_eJ^ zBVj?ljJ&mtZ6(i+9XxyOKYD{HG_ig#Mw6xbvzZ`ZFqufnDyDs__OnKJHNM{7do?~P zBN_Te%Qq%zJVlBZ;8cydUesjrT0 z_lmcE-c*h*`w2~SB*h_c(bSC#3sd_v{*@*%^&U5xMPzCQzg25Bw_34UE%=7TqVfsg z;8!n@wDT*^2=w5x5&e%V-UvY?zeq=Jp?2%SX9f@!=o8lI(a--$$geO5(u;t4jVZ}aV$Y|Kl67LIXY_e+S&9BRPiNxdNiapb)HP ztO#<}a})5bERvf|_yk4a7txUOzpr4=XA>#TPV$UA4=%foo(ntxe19c$^0eF{tXAvs zS)+U03NZfdANOC(DQeNICM365$SXF|sA1E2&kqfH-B-789EV z?cOhn0{KN+inRvboOtWV;eA@JE-2RFjOb>ImB?UUa`W=()ah;y$vwELhz?K$yl4AW zg19*rA>WB%~S|N0kr+7*DptJ+yCL} zr6>H=B>4NNv6LgPAb{RzMrSZFuxpbkH{TGqB0aa5(BL3cUfx)$n2(HRXc!Ke+1YgI za+8PT9$ZxrWn9y#rX&{xfx;3!X0yfg=ES~<^&XMMCrkN%=t(S)&j{B}#J+3Uek3bGF?tI>gAi)QfcQ){~SZ zkGSr^e``hWP7MQU6rxCBsh*UqLS~QY*|k@@hRFkGe6{MIGZ}ZaijTl*wN|@?&A*X! zc_pZPBA3q?mN=nv81M=KtLc)sZY%^#5_Fn*M@s3WJx{;)!@&YgM&pK2 zA$D=?q9{K|~P<9fcDcdHx+s~0gU@%|4xtp_R zPI^X_2mdLeccvQAUyclK*_slpR@nC5=#CwmM(=-o{!ibYOvxS6A}-u5aa~%;;aGAu zLvHT!^}Go;kF95Z=XR1-Z$W5ibD^2a8Q!wMTMMn1_Bok3Wc&E}Kb=Xsc(zeObZ8}# zm+B0pW)?F0(ZMq&4{Y?VE_W~LoTUhcePApGiysI+2uPW5_6m9_n2S6k%7gzD(K`cs z0Kwe{3j!4Dj94u2!k8X?J2s8p|JLeVOIK|@GNeVruzv|#eZw0hWfm~7Tbs2XJ$v(g zKEn6i+4s>FjJ}}qppHy&O|hQo_jjEzvTy6}zWn*n8_Rz_^mI%_07`{`n;fUGRF98;{a>aI zYj$5k;EopjJ$5@rJ$ThCM{)tI1~8;T%f}u});_j=)+ZbPr_p5J6dK@Lf#d=vvxW40 zEm4v2ug$v0=lv+v`=iM|`76qXy};~K?)5>yT}UOWpYaTv>k?|Nvic3xX?t12!+@Pg zA+T;iAqtf0jGRf!XVQ?)Bf7VVTQlM9A7A<9?{m`|#e||%R9{>dY!X6JMgbN@$i}xO zbQpMh;}xCw>#e<5qQIwN1?ze-m`+27i9<52bG{q*($Bk6(~`T!Mh4rt&_r9fY+7zH zO&T_u^UCO^ueOU>{uD_EK7bHcO3S-Qm1SPDJ8HI)(7?yhJpsUWpgvGg2Y<*i>!1P9 z02#nJr0TjGxV~d>yCWrR72>$=zX}Kj1|gLZuL_-jpMi@=Vc}IF5h)w=iOcwXtAM+K zl&Z8{AOYAc*fwz333PK?EciEy%)s!QTTYuWu>IQ$zS}$bi_Hh$PmB&h?WL%;gC=N7fBkdU zfaY=bE<8aHFxq0`C*0lZ&AAVDnnB6OBRIEwHD-TXp1RAn8fF+FV`DZ10!T!JKu@Krm{_*4O~1U&ql7xk^0>NI40w| zcSK-#O-L>X0(zsF)a*iD7}Mj~c@z4)y>a*H8DDKX`hH?`2wp1DO*fjrxwL%RHg53e zl9~N`c5Zs8G3Q@NqR{ArR|t02byG1|&qKS{A6a*P#)mzg-SW%c1A7NFj|<0Af#hbh zh4bn84C~YWgSiiOnt^`pL!A70HD*6h_#$&-8ZY1>yCX#=0^|K?+cM~C8|Z<+{YVvY z4%sHC>je=ul}j#$;W&uQXj`paA=JjH%K^FXSu0kvBPx;6Xfl(YTg1q|tzYTeA>qBZ zzxs3R`(N*QJu1W(wMyZZPA&)nso5G@CDh-(Xx6>m0#*Mc^7n%W&;i~N_By$#6v%>h zZ%qE>*oZ~5KU(#*U`)R?Iy}hk-DQTiwA@1O?A`0Fx1Z`W1>L%bIP&@u#0aSv1=WhR z?^5ZAQXnq;s&=up0ZwBpt9>2dZlo=>c3g$bMClI-*>x=YmNeYMc`(4#dVX} zOlnpkJ=(^-{nU^yFD_WO_kov}{kAOF-y44)FFU=sItINwO)+61G5cSiGNMmx(1i&8 z9)1jcz8~sndy-p>U^oXYo}6~7WXgwg7H#@gSDHO6BG~U=1?695#cCzDNKfPVmWy5( zUT?auars>wdu<6~fK-fvYA4imriv|~R7&x`K!DGI7p?=M4|U_7@lMX|0Zz$yMjybh zj=wEYrX;@JrrcKF04E4wG@Ch}uHnT8d%gA5oFUV;A50$l-HsDqG>Z*G?WL4kGP%-o zH6>aEukhV3jB4MpbL`GW92}I6p+t>O!X?8Sf<$=3MQAzhsWj{JZ%=yfmv1t&Q-(x^ z23Ay$Vy5Qem$;FVs@LE6N$`tAY&DPt@>w}c@*n=OCzu8z~!?>yCS z-iklI@lmVr@m9H|lPeV>8F@wIm8uWVxW8@JA?<$h;-6P`qV-bX9aoOzq7=KLI|5Ay z%`=BCSTOaCJxlf=Pz;iO5aU1Yc z9e&?f8Q1It&bmi*LEvI`Ax#>DeLUsfo1S=S>2Lkt|7OpoM)gDR^H#aFk}DJfr&4oh znb`2;(((7Sy|Lc+A*3!yK^NqYI>esjW-XYLq3yKi_1-h*O?YSTH(U4Y9NMHonB-%! zUXl(h77Hg+bGf%~r&SB5^m#xmT``c;t2bZ@0HwD(;(A?Sk4=|4T1(IK2Ob4paiOCs zftO?sn*S1%Zi?+?m;k&*)o!#SNbx4FHa@uzm(K784TScYfiFF-;mp zW{K<@nnpo-EI#$^Np3ZP{sgpm;_ZW)Ig_7xf8D1SQx7~F8x>M1tk!C^l9sFChL)|@ z&3|^lXm4S6FZMmN1CxmTbE6vOOk&n?*coJI74y@t2uHos-Ew9gBZ!d&8U60@~vc-+8{Z!KaTt`Qf4Y+xPC8 zdMOL8oPsMrYE~ijqUvval8Q1&@B-fbe?n0>$IiH?SSa3-GqUj^sO}p~~@PLXui5hRlm_AcC>07j>pB9QO3mZOR*km)I)B)}dwZQs zAZ_&yv(>JgBgNZ0Lfkd*jooFepc`%f>TtcSR!gMqv8%#CTc2J|@x30?Yc&lpvIL@!gmZ`SNHq^Gq*U9TJ&%XIP+&|>_ezqDVB=sWYo z8(;poVQ$mdV3bN_g@`-JYJX#^hQE9^W8iJx;@;LAnsW}V+50*(yt#q}=l=4@@^i9n zPz6-+3!-A6Q0>AGDTu55JN<0>va9z#?c%%%_^#al|3XH4M#z{nTum08YN>DTpo(bB zDzM>-5M3b(WaXFO?WM~5c-n)Btr`^RIDGE`a?Ulvw*mInbt@Q>p!w*pPwVDA^2~x? zo;`W`uj%zd{qa&N?Q_b3w44H(BsAIn(ad4Dhp3OVscpvgghtbyekmm zEu;*r3rMM%WxoO!`hHDUtYoBA+w$)$2EM{^nrYcY)!)VQSApH0c{`9Y(wyNgClkyT zncvm7mip&58P|MA6=Aw{PIOTaC@3n$D*9#5oiri7YZI}Qqjw*p;6S^|hBrWW3L4(A zvM^xL@R^JD%uY!?@=k1IFiKIZRC_(YSW8ey!v9_z*{n~)fGzPHdFBXagJ55b!h>>f z)+rTz`QO|At+={^l;u?yt9pN=@KLIGr>lgL!MILf`CiWIR{`~rVhyUob|g2JoRzqy z2$V6AQ9)w4JVX}+fl|Gm5|elF#6cZf4C$0p#MvRo$osnme$6YBQGXF)x_xb~_kW|O zes*N?o`ZW|i4G5x3~v>lo1-bkMBtw1hP1e)cl`SL9DM2sMy-H4#$$MMK3E*VPz5QH z9b6k-i>p*cqC3J`7xxa4A4n&w46c&W#H~9L%ZBGy2suyveuu#65}WB%;F)r6F7?&%dJAEcSf%9bs)A6 za-s`6voG9;>m-k#koFd=h#fetDpQ8;KsyKLHdTYDbu|f=VJnb+*@&)Gh@|BdqVn>w zd@%XmcCX%BnoZvPKPk|5mC8SvZN@h{aeafi-pV^){VIRT=ijaWAR#&kwaVV`CWrz_ z=>;@x7=2{h{4rf0zbiYGMFy;pSHzT&;L#syKGSiD;{oxys1Jole^2OpjLLJg^1e zqze&Uj`e(K4e##>#JJfvy=92)*$!+FwNG?~D3Gfu#bUuQf6B<_JzII7;`s32NZoS_ zK5-RRT${Kaf9o&V>g7GBe)8ADCr|#iHZC#%wU?J&s;ejnWaSkR9TjuwMxh!m|P^_I8oyp7xxa5 zK_FKKw}VI-Y0l6JsU#QGlzjfBM-5hWIa`%)0ZQwf=%OG{RH~&!FY>}8W82@=CFC?` zM{Oo&doO$vE5BBs0x_L;8lyhHecGaXt+_A3Pib)Cz3*q-PA+XyWSPa*)=>L)nGVF5l@f#iKeKvz!bS<>c zncHZci+hL3c)pw!cb$xDhC7iM{;DJK-4}T&x0_sy_?klyKxZ&ep!G7}e_Q945B0Gg zBX#^XF08u)-?GR`>!p=cp9&$dJ4smE?7XFCXf8W5;S_h4?;@)b$2ou;f7sf0QSL5RiO|U?#CJv zU8xW$(Hc0OT1@Zut-H+{lz*JleSRSMyIb&Y4R#w6DnNT4f`fL`Vo8tp)|>BH{N0At z^~3y8tL%eelnRlof?~{q&&9{@?$Ywfn@>a5+9XUtv3NzeX}16sTElj>bF$KX<>KDEWfE8p(vV`rU5Ve0 zzV~N2bfNlgNjX0F^+0}|5nZVe$;>N4Yk+wZ@4IpSIPbp#3~zr!PR3n$$J?*uC_r}x zqMIM4<+ojzY-`Ye-jc7riwIKTSD}!kLKHcBF^}-zpp?z;jB7A+gb797XJ^PSX^1kw zO%YoJONq2yQ|$B}__g^T$*=uD#usByH@Fs7^FXA^Ba@LT)+8aX!td)cu1Rt-8MMcB zwH0R==-_y+j0s4N%j(-|v_(!mxJeFYvVh4SM1IcU)Cj5cyb+h^WSzUhCvHg9PV5rl7r8grs3key$ImpVA-!|7vU+Q*lg zIsWiDN|F+!VifMU&_rq9a$^=Wes?sX1AZp?)ddvpehlx#3T>?xE>bZ7Lli*imSk`x za>4(H^EAwg_yNg<6H&#vQ8t5Lb#8!$Fb726AcN@j zYy_60)60nu_*sT+0TFqXGVBawB|1HYr4}ZS9(L=PTVi&QI%y>r){K&@>lIdVeKLf_ zpCJC*CNHka@Ab#tt$#MG7lOB1S!s{*q7p4;MQG-8_jO9_)#QYkbB`aT_-uk?cyrTF zZYh;4>=C)iXjw^#CAm5!LLrtWGd`CKaWojGKF`3l|%Hj>vUPyG+d2Xa_}Xdm|O$>pDe`Bx*k z&9>Dz8kk*!=bUn)#}2Hfjo>DvTI@qs@xA z@BU0|H|wL7e~0)B1o_z;-joWFwA><;N|kx}?6Eh_e>5~-RrvW%va>s*3UYJB6~L+o zQyw&!{36j0e@5<)kCXlF%c$d}_SI}9&!%GWL8}$~m8{)#@oFHo3CKbU;8<$YWuuU` z^3AA)=dW?1dsfGF)?0Zc^-39X1nWg3~lvFmSvi~Hnc&7DLFOo^fTe$x`$FB8XAV9PMV0jbwGm;@?L zbkm%W+?7Y+ERsRBT0{pau;Lfnzr6r2TUx03pYz$fr;omU=D2>bJGe0YEs|DG#Jf>t zl53M8qCpNZYhqtsqD%b#=WkEPMh4>NZC@=`6a@;3N)QyjhS#1P)#b*fDh+2J+fMHO z&iFLO{R9;~7;_17^0{W|L5bNUKt#was6ri^96J>)hjs;T?CNd36EjKbnenqP;Oo zP=M|N1czqO{M(2*Ys}5R+5G+4_WO`ShClwz$XC&bxlcLV$&(lR$4=&yvg||pO@b|UX$nl*P?YC ziSG&==akFQZ(Zr;auQg|#&u2lR774dS!1ep^BuJPT)~l1R8COMXEh-@f&hA>rQ~R; z_E>JA?#L_mH_~i=^Hw5^AJ6CDyeB1NmKr2h;2^+}yrxQL2?(5{3K%WkwcfCvfiBF;V?LLxUUxqRS z1n-*aiCeiP|3j5;T8mzdtI?(-H zmEPVD9I8$um6?UYzXUHeh-gbSLrkmmB?&qSult9A5`amm$8l!M<#?ob8Y|RkxAKE(vC=RxnwtMEL3H+`2Wl zc1R$ufdCZ3>G`jcy?q9L@gR7WBe@bv&qH{0G0k4D_v^v#;nNnZPV!Od2@MFa4{Nm* zqYyiT*8;yRB&z;`_rmj+Aqxf4Yi<8+H}<#VFvl3djCJN*LJ1iTbXvbnrj5EYperiI51MZ$zoy zw~!z$EjEPATB%~u+NHS>0fHjDf(uO)1vJH_Xi8z;q;WS4=}~_>Ns~8oVcRhGNiIlj z(4GcW#1F*Hx{IupH*$E;eh@$vUK=DIfb$ScsoB{&=FV?BLyF}Sk~mTZLx)`e_C1)6Yi__^H$r5>w+Ml(;Fx@s2SU@-5q0k6(1z)-7m=8 zatFSR+$TAJ_AG?n{yPb;-GgRV92XY+iozdMA+=G@{dO+x&*dB%BeV@+;+RC9e)-V# zNc2NUIo!865Ycy6F@aWR!YIRoGTO4=Mm8Vh?Peke7m>VBa-!^pPFONgQz;O;&6pkSYG+!t&bGD;3|dpg(~#q+QJbTWYyS=as?HRKMlO+;@+i5bp$jx*9LbZrRP=~ zBH7>Y?=OAvGh{A*39s-9Bu9!k6Lb*&Sg*g6Zb_fJc-@gCiFU(2$E6lzDHj;$HV^(o-remjY8bf;i@1jk42#iA8ZMbrk#3xP*$f7Fvm zE~GQInf5!I9;U1$VB{H~p&!$px>hzLtu{7N^+peEDj44SRzdPIqn<0>Tuz?;k9GKs za>V5z2a<;z@x6m>G7x0+wObIZ2W--UDSsRm{bEEf?6c4W$$D)yL1fU0Jr^EqG49K_ znfjtr^P=$QA|uo2^K3tvO}v!9W+djhI@_Qfcw)k>vAaz(&lnlEdM zNu%QUn2RWUBc#Na%eHBQ#wMoPyMY2!SglqPd;daGYU%a1q654OJX6K-FUk%6 zXf;Xmj8`Q1242{ADwA{R`L{&|`&J^krbJI}p_aEEAJT8!Z4K6tJNFZb96~wt2=_rFDP(@wtomqlx9c^Ejpum98(4WU2NW5NG^+gVj5BPVSRqZzV z(FMu7BQI}$Z5@AK67tedZ=mWz8k=gN4O?-)iVq@I02SX0PV4F*?nY)dV?|NENUEK0hkBI1&DsXWOo(n>D+fyD>~+K&A?1cf|1 zqW9ENvmZsJdK4mP4Mx&63hur2rlB(jtCo>7cM&NopT#H9{j^X)35HY=ep%3P%AiPXVK=IDX89&gqT3;E|t@ zGR^85Kj3^f+A`cnD7DX0mB^Ui|;=2esD_D*$ zdU{~KcJ$4OtC+jf8HuKaaem^hoHwc}C8xnISlOea@{rqFCz zH?*fqNq!mpS`MkDu-4juy(`3G1uv1F=s!~RMh_1o89A#Wjx+Sfb($!+S1!sO zq~JPw52WzlhurvHXV@XrM&J-iu=d7k1!bsha|5j;YAb_mVWj0!FTKJcWA9M(&#iyq zj-GdQ4m0*Sc0QdZu~GEDDFd~6*JASX$Kln$CazoDBe^aKRG~SvTH76ESPo|2NwtJ^$gB5uUlnS} zCR`MFsmsaCCSsUv=N(aoWm0?qG&`~F%%Z%C2DZ0NProEySgqIlhOP^MDY$kM z{f=azc7(Rbi*5h$G8<}sL8{*4YUh-`NI3wfDyS;^sD2H6BaYL8tEHpwlSpm3BodeZ z?}|hRQ2K(-984DD$&Q*&mhuU-#t4+Ywm-4EI(a6TOA3ge(-W_HlHQfQR@w|v@B+z2 zb+zD6Kx;>GLBMFfu9w^L^_t^Y@h^zu-evM!C&yymJ%IOKZs{`u{4E zS;k-$R5nAKMOujkmHVwID<&x@ifO*03jtjueZ-Isg}cEks%+w3e-#bt9risbRe?R9 zq2)TqDi`N7Wc(fFtma9`4OOmPd?NF@hpRZ|PQl7r8S^6WC0A15yhL&>%@%bfpu_2Z z&uPzrsX($7NLYfjY2KBFwvuibsG=aXltV`~ZuP=7MD((a9-IwaXO&BP;X2&3OmDps z8QuLmTq`IA-f(i>mGNv=Ew8L~0lBN}cn6|*b>34erG?9k zTZ@r0em=Ua-DTE-;FU&`54z#gv~s+(RbN#M`xc~h$15eqKU8V3Dg-Dov1xmzGYQ~v zq!8Hhdu}O^40@-oG4b8ee(JyDJjdV5CJ}6~vwzsb7s()YB@1aTvPmr^PNWJS+oY$! z-v7SR_H8;G8xjSiD9oQ~tHI^1*dHTR_k0>DP@zd3c9b62iBjlA) z*cO6RUU(!@0!3Ylzo%^a)dy9Ir2=5FqSHGx02^#7()9vItFo(YkPS#Bs;+jV03W(} zZyNA_m%G6g-Bnq@OxqtS!uGS&qAn!bo|lG{Emjj0AeGd86A0sy%fnjB{gdlth4P>> zZbqtwR+YZF!P7lg29EsFRvYQF{v+R!Yx{X{<>4sM;wPU z(>)%ddvI;wA)6Xi5l6W)K5q{^ME9T$;K)t8%F{I-qI+;{pg&a`;h+{`wLS0<-GeKS zKpe+lETrO|bsl(#?!i@wNTei`n}*4h4hD^l3B z3TZc00FR8Yg^db z77~zvgh)U_92^`h925t|LFypO0TPga1SDjPc9r-2zOZj{|3!pE+qSBN$ChgNx9u1WvX z(AuF_`~~6;#gc}mNV!0=CjI+=sM>MZ>!YeVVfn7AF5&t(!wJtrnS4Wd9NKKxM^nXr zl2$fO#nr#sRe#B6$}8WfK8`tZv0fa`3IZ=Yok#!#+HUo9Y=A6G8l3hSQ)R6ZiQLo1=zCEK5wfL>5ng%ROVl2z?Bvvl1Av;&K1 z;IxZ2*OU{`WxNbc=<3bg3d^Ge6esEXxN-|NA6Iv=SULeUW>uI{s`>K+$HC11xsAuQ zvX6ft$}QU_1oYz* z`Qp4UXy8P&ph|$Gb|AioN!NJfX2DDOG{hZ<@RuLKfmUVbWDlzT5Ty*=Dl-~Uo16U{ zukgbn3keYS<~x3Kz2m7cCnv^W=YVK_6X$`A!j6_*8mBn_JIPL@!knDY3Mw;o%?RC6 z+}!aw@lrVbA6&nbB%lhmqk;3HR-~IgQ?3Xsc~iWM6nq^Wam+gQ$*K_4ss%Yhl1rfd=6HwNEcsKUtGs~R}KTn5=pRewgmWx_0VZTsIKwls$ z$S6lxKZj#8Vd7MOO*{(1+=?x}AiI$7TRX2qYi8gQHYzt%AoRsm#i}l|{vgb9fouixE zo1!Em0TI}}92DVE0*cp={{&rUE(y$S1w7)RTv%BYMo}|jP+y0W1zbb^6PTAyjSJJP z1vi4@C4EueL7Ny9;bC8gYsh~93;Jdj;BegtKdpo>J1tlFRlO7#;rY!X|DEOi6aPdc znVG^N4XT`=2UwX2BA^*Hs`>PF@Ms-eL(YqZd1%(Ca$#oAirRp45t2mz;O8MBFv6ol ze1$b+ZIj@~yMIE-%oIVSjcK%$UerP@LlEJU=;sdGb(w?>XEbFXHFf5kl$B(Zt_X|_ z6ycGp_-(cEP=CyU8}>qlIz!j~@?o2F(ZjiE$bRN|h6fnP&{6V=Y1ufoKB2JkGUBm+~e4f}$Wf%S6$c+|cIAX-BTx7FBF z?0QT9I^1wf8sj+%bEggPYv^^4aIPT-M%E!=vz!sbb3bOlH?rQg%cNs&X~mtHJ`-eR z7hQEh2n-l)}Yp~k;oslx#B$__jkL;N6s9%6{O9{i?^&gx`kQ;f^V@p)o zw%QMB79?8M%QT%i{+a=+l1$@%focSy&jSTM3G5+k{^X?!obn4wl%!)ub|CtIwk$(7 zfa4iQ_@$#w4UHub{rqDFoc*{?9!MQsBw0~GBv#rH9W^n8UPFRM>u6KipbY8||A>_W z9Y-rkNoF2-NjI4nE|UuB1n5Vt#~C@?nG}otLrBI=j@Pn>9z5AD+klKT{=ylohVtW# zaI!y-PVMj~{wbcoZ3p4z){qX}IeQP)Kq~VI(uxLfMH9150y=8p)9Be5hGi&OPXvh; z$K@r{#v5sYjZ)1? zJ_s`7U>pBqL#vA`b63FvMn#k`oF;kfM}{SwC#G>?N?R+bLW78Jt#%0FLT?X>uTn?%|k zCs4>Lr_WW9mf;%m`V;-KD)j_(T1+Q;VWGBz&M2fTZ+4*N2C@P0%c^L5@HR0SOGj=8 zohT#rX9rr((zvrX$F3nWZ^vS0#^Zq;xb2{MWI$K_H3EoVHMJ;&`6>vt!AuF0AUW94 z-B}t}yNuaP*3u{tHS_irF!cmU*@woJSVG;#(`)ZIx-rwD<`0R9XvxlFL6A| z9JP`>jXd16WZP73qb}9uiDuGSZnVu!iXF(YUcd3jX>|EIVB`5Y2!@HKAOY1W4++!A zB40^1FDRLpG%L-@%tNoiRQfms38>aen;=9B&?>d&74qD?aYBk+8F;}|`SYy$6}VkWAR=fo7)sv@l`k8jyE z)A(@CddulfH~YD(gUPYtD%yP7aH7?b5*CZiJm$7tIrWl4 z2+uOy4Z2-0pSZk#By;;{=JwIu?V~T|?on>m_r$SX&;GtG!`JXGt26&+76SlQP&gps z1polBA^@EMDnbE!06vjOpG&2rBB7$RnINzd2~FHC5FVrJ`V!Y z?d}2p|I`D8Yy4jMr_r1UKsi|RSwlCf@7sY%FVN_p0z0wPnzL<1Dwx_0MQp00h>doD z&$_{+oXZ)8sg1Q1s$*?Lhsb)}XjsqA)4i`6%Lu%1Um8>a zoOB=`(96c!iaq#}@_3^|1&@eb71_OA7zffD3w!Qn0VK%`yZaMa@$oGL1Rs4);GdzF zjkOe~#=?EB*ia|^WQJZg)KaOX>}{x}QyXe1@Bse%+>fLq>2pMafQqPdAhpgn<5+q3 z_6!E{K4x#W!Amxi>PJr=mU}_mcc!?6437rKGF)RXNuT&AY_i(@cT=`zub+Eym&m-{ zw9ZqxtR6ZljxqmlEA?yoG~A|iyg?p?LS?iz*Qt&|2gBIEwR8XVXL)5mSGX%MJ+hU| zlziZf7M@s%AYTLW?EfU~cjsc!(bTN{B~cH%TNY3ZzD^gFM<@e^>RVr5e${tfULs-Y zajF5ES!*_}*eaO!UNSD)@z-k{*3@x?i>H`%QB<=U82VWYR*`j&?S-3O8(U1vPHFcR0&4=;yGm}G3QJd~{B8;fUH zY@p&`(`hg2?iCDGVwYD&EKkQt@=nft|DOOXm|8FtsQo=U5=5m_Ck(&cFT^}yhOYiTOQHV#NY!;IkFjccUlg*|BHni@y z;HL9%Tn%>GT(g{OrvPDGQUrAbA~>wceEwp;`Xo4*FcZRe7>D8ID!c2v)b0K#dM(;Z zi`2Pap43m)FllBynUdhSZ}<8(v?eFMvrKycyZ}OJU+>cJI_~ANVGt+)Js{BeF}he! z#;^S4fzH7*r3-7mPsf2M1VF49HDUd}0p>*_4rKe~3~P60ex8w#8kKhu7w^@7CL~r2 zD0QMf%!;Rk9zuiO-ZDvD_PyF6x64BvUt#g;Ukda%cOFv)w0Mvh1u_B9_gpMx3khf7z@8JCYYc34KwMsVuzV{;zpKl#F=HtTCoA9u=F2r;MnvSuEPF_W%n8jde#C6 z{Zu3xwU6T7d51w&)I)ZXQSvxC&lu2I9?F3I>2nkz=V`v?It^#C z$UEaO1OuEo$X7umk_@lup`$F8}`jWt?0Q z)8!QJ@BTn)3_9422mng`^#A|&{VRa{`B1FRrBCW;2=N?;E5yiRN3<<~Diq|-@nGQ-=->B7d@-M65yl(kN^Mx E0MEtn Date: Sat, 12 Oct 2024 15:08:31 -0400 Subject: [PATCH 389/846] Fix stream_options type stub --- av/container/core.pyi | 2 +- av/container/core.pyx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/av/container/core.pyi b/av/container/core.pyi index c1d43ca4c..b031fb4b6 100644 --- a/av/container/core.pyi +++ b/av/container/core.pyi @@ -42,7 +42,7 @@ class Container: format: ContainerFormat options: dict[str, str] container_options: dict[str, str] - stream_options: list[str] + stream_options: list[dict[str, str]] streams: StreamContainer metadata: dict[str, str] open_timeout: Real | None diff --git a/av/container/core.pyx b/av/container/core.pyx index 683a3f65d..dab7c865e 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -365,7 +365,7 @@ def open( :param int buffer_size: Size of buffer for Python input/output operations in bytes. Honored only when ``file`` is a file-like object. Defaults to 32768 (32k). :param timeout: How many seconds to wait for data before giving up, as a float, or a - :ref:`(open timeout, read timeout)` tuple. + ``(open timeout, read timeout)`` tuple. :param callable io_open: Custom I/O callable for opening files/streams. This option is intended for formats that need to open additional file-like objects to ``file`` using custom I/O. @@ -378,8 +378,8 @@ def open( For devices (via ``libavdevice``), pass the name of the device to ``format``, e.g.:: - >>> # Open webcam on OS X. - >>> av.open(format='avfoundation', file='0') # doctest: +SKIP + >>> # Open webcam on MacOS. + >>> av.open('0', format='avfoundation') # doctest: +SKIP For DASH and custom I/O using ``io_open``, add a protocol prefix to the ``file`` to prevent the DASH encoder defaulting to the file protocol and using temporary files. From c055671b26ca67324b0b339a4d4e4bca8b6d3228 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 12 Oct 2024 16:52:31 -0400 Subject: [PATCH 390/846] Add docs for recording --- docs/cookbook/audio.rst | 2 ++ docs/cookbook/basics.rst | 15 ++++++++++- examples/basics/record_facecam.py | 41 +++++++++++++++++++++++++++++++ examples/basics/record_screen.py | 32 ++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 examples/basics/record_facecam.py create mode 100644 examples/basics/record_screen.py diff --git a/docs/cookbook/audio.rst b/docs/cookbook/audio.rst index 39ccc62b8..90d6bf0da 100644 --- a/docs/cookbook/audio.rst +++ b/docs/cookbook/audio.rst @@ -5,4 +5,6 @@ Audio Filters ------- +Increase the audio speed by applying the atempo filter. The speed is increased by 2. + .. literalinclude:: ../../examples/audio/atempo.py diff --git a/docs/cookbook/basics.rst b/docs/cookbook/basics.rst index 2896519de..d7c1d6320 100644 --- a/docs/cookbook/basics.rst +++ b/docs/cookbook/basics.rst @@ -15,7 +15,7 @@ If you just want to look at keyframes, you can set :attr:`.CodecContext.skip_fra Remuxing -------- -Remuxing is copying audio/video data from one container to the other without transcoding it. By doing so, the data does not suffer any generational loss, and is the full quality that it was in the source container. +Remuxing is copying audio/video data from one container to the other without transcoding. By doing so, the data does not suffer any generational loss, and is the full quality that it was in the source container. .. literalinclude:: ../../examples/basics/remux.py @@ -40,3 +40,16 @@ Also enabling :data:`~av.codec.context.ThreadType.FRAME` (or :data:`~av.codec.co .. literalinclude:: ../../examples/basics/thread_type.py On the author's machine, the second pass decodes ~5 times faster. + + +Recording the Screen +-------------------- + +.. literalinclude:: ../../examples/basics/record_screen.py + + +Recording a Facecam +------------------- + +.. literalinclude:: ../../examples/basics/record_facecam.py + diff --git a/examples/basics/record_facecam.py b/examples/basics/record_facecam.py new file mode 100644 index 000000000..2200bc546 --- /dev/null +++ b/examples/basics/record_facecam.py @@ -0,0 +1,41 @@ +import av + +av.logging.set_level(av.logging.VERBOSE) + + +""" +This is written for MacOS. Other platforms will need to init `input_` differently. +You may need to change the file "0". Use this command to list all devices: + + ffmpeg -f avfoundation -list_devices true -i "" + +""" + +input_ = av.open( + "0", + format="avfoundation", + container_options={"framerate": "30", "video_size": "1920x1080"}, +) +output = av.open("out.mkv", "w") + +output_stream = output.add_stream("h264", rate=30) +output_stream.width = input_.streams.video[0].width +output_stream.height = input_.streams.video[0].height +output_stream.pix_fmt = "yuv420p" + +try: + while True: + try: + for frame in input_.decode(video=0): + packet = output_stream.encode(frame) + output.mux(packet) + except av.BlockingIOError: + pass +except KeyboardInterrupt: + print("Recording stopped by user") + +packet = output_stream.encode(None) +output.mux(packet) + +input_.close() +output.close() diff --git a/examples/basics/record_screen.py b/examples/basics/record_screen.py new file mode 100644 index 000000000..92818e931 --- /dev/null +++ b/examples/basics/record_screen.py @@ -0,0 +1,32 @@ +import av + +av.logging.set_level(av.logging.VERBOSE) + +""" +This is written for MacOS. Other platforms will need a different file, format pair. +You may need to change the file "1". Use this command to list all devices: + + ffmpeg -f avfoundation -list_devices true -i "" + +""" + +input_ = av.open("1", format="avfoundation") +output = av.open("out.mkv", "w") + +output_stream = output.add_stream("h264", rate=30) +output_stream.width = input_.streams.video[0].width +output_stream.height = input_.streams.video[0].height +output_stream.pix_fmt = "yuv420p" + +try: + for frame in input_.decode(video=0): + packet = output_stream.encode(frame) + output.mux(packet) +except KeyboardInterrupt: + print("Recording stopped by user") + +packet = output_stream.encode(None) +output.mux(packet) + +input_.close() +output.close() From 77ebda5834a6d22f8e0957a0b8fcc83423dfa529 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 12 Oct 2024 17:28:36 -0400 Subject: [PATCH 391/846] Update caveats page --- docs/overview/caveats.rst | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/docs/overview/caveats.rst b/docs/overview/caveats.rst index 9e1f69810..49c97367e 100644 --- a/docs/overview/caveats.rst +++ b/docs/overview/caveats.rst @@ -6,27 +6,25 @@ Caveats Authority of Documentation -------------------------- -FFmpeg_ is extremely complex, and the PyAV developers have not been successful in making it 100% clear to themselves in all aspects. Our understanding of how it works and how to work with it is via reading the docs, digging through the source, perfoming experiments, and hearing from users where PyAV isn't doing the right thing. +FFmpeg_ is extremely complex, and the PyAV developers have not been successful in making it 100% clear to themselves in all aspects. Our understanding of how it works and how to work with it is via reading the docs, digging through the source, performing experiments, and hearing from users where PyAV isn't doing the right thing. -Only where this documentation is about the mechanics of PyAV can it be considered authoritative. Anywhere that we discuss something that is actually about the underlying FFmpeg libraries comes with the caveat that we can not always be 100% on it. - -It is, unfortunately, often on the user the understand and deal with the edge cases. We encourage you to bring them to our attention via GitHub_ so that we can try to make PyAV deal with it, but we can't always make it work. +Only where this documentation is about the mechanics of PyAV can it be considered authoritative. Anywhere that we discuss something that is about the underlying FFmpeg libraries comes with the caveat that we can not be 100% sure on it. It is, unfortunately, often on the user to understand and deal with edge cases. We encourage you to bring them to our attention via GitHub_ so that we can try to make PyAV deal with it. Unsupported Features -------------------- -Our goal is to provide all of the features that make sense for the contexts that PyAV would be used in. If there is something missing, please reach out on Gitter_ or open a feature request on GitHub_ (or even better a pull request). Your request will be more likely to be addressed if you can point to the relevant `FFmpeg API documentation `__. +Our goal is to provide all of the features that make sense for the contexts that PyAV would be used in. If there is something missing, please reach out on GitHub_ or open a feature request (or even better a pull request). Your request will be more likely to be addressed if you can point to the relevant `FFmpeg API documentation `__. -Sub-Interpeters ---------------- +Sub-Interpreters +---------------- Since we rely upon C callbacks in a few locations, PyAV is not fully compatible with sub-interpreters. Users have experienced lockups in WSGI web applications, for example. This is due to the ``PyGILState_Ensure`` calls made by Cython in a C callback from FFmpeg. If this is called in a thread that was not started by Python, it is very likely to break. There is no current instrumentation to detect such events. -The two main features that are able to cause lockups are: +The two main features that can cause lockups are: 1. Python IO (passing a file-like object to ``av.open``). While this is in theory possible, so far it seems like the callbacks are made in the calling thread, and so are safe. @@ -42,10 +40,9 @@ PyAV currently has a number of reference cycles that make it more difficult for Until we resolve this issue, you should explicitly call :meth:`.Container.close` or use the container as a context manager:: - with av.open(path) as fh: + with av.open(path) as container: # Do stuff with it. .. _FFmpeg: https://ffmpeg.org/ -.. _Gitter: https://app.gitter.im/#/room/#PyAV-Org_User-Help:gitter.im -.. _GitHub: https://github.com/PyAV-Org/pyav +.. _GitHub: https://github.com/PyAV-Org/PyAV From d9624b9d1d9868d83e719283e38f9f58b1c7f13e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 17 Oct 2024 19:10:59 -0400 Subject: [PATCH 392/846] Use GitHub Discussions for bug reports --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 52395dbe8..935d233aa 100644 --- a/setup.py +++ b/setup.py @@ -221,7 +221,7 @@ def parse_cflags(raw_flags): long_description_content_type="text/markdown", license="BSD", project_urls={ - "Bug Reports": "https://github.com/PyAV-Org/PyAV/issues", + "Bug Reports": "https://github.com/PyAV-Org/PyAV/discussions/new?category=4-bugs", "Documentation": "https://pyav.basswood-io.com", "Download": "https://pypi.org/project/av", }, From 26e46306e088d61b1433bf855cfee919af39ee27 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 18 Oct 2024 21:43:35 -0400 Subject: [PATCH 393/846] Remove deprecated `avcodec_close()` --- av/codec/context.pxd | 1 + av/codec/context.pyx | 17 +++++++++-------- include/libavcodec/avcodec.pxd | 3 --- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/av/codec/context.pxd b/av/codec/context.pxd index 58bc6a07d..782a66f8f 100644 --- a/av/codec/context.pxd +++ b/av/codec/context.pxd @@ -13,6 +13,7 @@ cdef class CodecContext: # Whether AVCodecContext.extradata should be de-allocated upon destruction. cdef bint extradata_set + cdef bint _is_open # Used as a signal that this is within a stream, and also for us to access # that stream. This is set "manually" by the stream after constructing diff --git a/av/codec/context.pyx b/av/codec/context.pyx index b995bc9f7..c00dec1dd 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -130,6 +130,7 @@ cdef class CodecContext: self.options = {} self.stream_index = -1 # This is set by the container immediately. + self._is_open = False cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec): self.ptr = ptr @@ -217,7 +218,7 @@ cdef class CodecContext: @property def is_open(self): - return lib.avcodec_is_open(self.ptr) + return self._is_open @property def is_encoder(self): @@ -228,7 +229,7 @@ cdef class CodecContext: return lib.av_codec_is_decoder(self.ptr.codec) cpdef open(self, bint strict=True): - if lib.avcodec_is_open(self.ptr): + if self._is_open: if strict: raise ValueError("CodecContext is already open.") return @@ -241,7 +242,7 @@ cdef class CodecContext: self._set_default_time_base() err_check(lib.avcodec_open2(self.ptr, self.codec.ptr, &options.ptr)) - + self._is_open = True self.options = dict(options) cdef _set_default_time_base(self): @@ -249,17 +250,17 @@ cdef class CodecContext: self.ptr.time_base.den = lib.AV_TIME_BASE cpdef close(self, bint strict=True): - if not lib.avcodec_is_open(self.ptr): + if not self._is_open: if strict: raise ValueError("CodecContext is already closed.") return - err_check(lib.avcodec_close(self.ptr)) + self._is_open = False + lib.avcodec_free_context(&self.ptr) def __dealloc__(self): if self.ptr and self.extradata_set: lib.av_freep(&self.ptr.extradata) if self.ptr: - lib.avcodec_close(self.ptr) lib.avcodec_free_context(&self.ptr) if self.parser: lib.av_parser_close(self.parser) @@ -565,7 +566,7 @@ cdef class CodecContext: @thread_count.setter def thread_count(self, int value): - if lib.avcodec_is_open(self.ptr): + if self._is_open: raise RuntimeError("Cannot change thread_count after codec is open.") self.ptr.thread_count = value @@ -580,7 +581,7 @@ cdef class CodecContext: @thread_type.setter def thread_type(self, value): - if lib.avcodec_is_open(self.ptr): + if self._is_open: raise RuntimeError("Cannot change thread_type after codec is open.") self.ptr.thread_type = ThreadType[value].value diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index f414e59d4..71121e976 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -291,9 +291,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVDictionary **options, ) - cdef int avcodec_is_open(AVCodecContext *ctx ) - cdef int avcodec_close(AVCodecContext *ctx) - cdef int AV_NUM_DATA_POINTERS cdef enum AVPacketSideDataType: From b7818c0f94968b889854fc014a0370d6c8cc9066 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 18 Oct 2024 22:31:30 -0400 Subject: [PATCH 394/846] Ignore pillow typings --- tests/common.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/common.py b/tests/common.py index c67db04e5..ce1f2848b 100644 --- a/tests/common.py +++ b/tests/common.py @@ -5,6 +5,7 @@ import functools import os import types +import typing from typing import TYPE_CHECKING from unittest import TestCase as _Base @@ -113,13 +114,14 @@ def assertNdarraysEqual(a: np.ndarray, b: np.ndarray) -> None: assert False, f"ndarrays contents differ\n{msg}" +@typing.no_type_check def assertImagesAlmostEqual(a: Image, b: Image, epsilon: float = 0.1) -> None: import PIL.ImageFilter as ImageFilter assert a.size == b.size a = a.filter(ImageFilter.BLUR).getdata() b = b.filter(ImageFilter.BLUR).getdata() - for i, ax, bx in zip(range(len(a)), a, b): # type: ignore + for i, ax, bx in zip(range(len(a)), a, b): diff = sum(abs(ac / 256 - bc / 256) for ac, bc in zip(ax, bx)) / 3 assert diff < epsilon, f"images differed by {diff} at index {i}; {ax} {bx}" From 5bbd3b1207414135e551ca10be62880272350a70 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 20 Oct 2024 02:06:23 -0400 Subject: [PATCH 395/846] A bit cleaner, but doesn't fix my segfault --- av/container/output.pyx | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/av/container/output.pyx b/av/container/output.pyx index 9e344804c..a0a59c111 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -79,8 +79,7 @@ cdef class OutputContainer(Container): ) # Create new stream in the AVFormatContext, set AVCodecContext values. - lib.avformat_new_stream(self.ptr, codec) - cdef lib.AVStream *stream = self.ptr.streams[self.ptr.nb_streams - 1] + cdef lib.AVStream *stream = lib.avformat_new_stream(self.ptr, codec) cdef lib.AVCodecContext *codec_context = lib.avcodec_alloc_context3(codec) # Copy from the template. @@ -160,10 +159,11 @@ cdef class OutputContainer(Container): # Finalize and open all streams. cdef Stream stream for stream in self.streams: - ctx = stream.codec_context - if not ctx.is_open: + if ctx is None: + raise ValueError(f"Stream {stream.index} has no codec context") + if not ctx.is_open: for k, v in self.options.items(): ctx.options.setdefault(k, v) ctx.open() @@ -190,10 +190,7 @@ cdef class OutputContainer(Container): cdef _Dictionary all_options = Dictionary(self.options, self.container_options) cdef _Dictionary options = all_options.copy() - self.err_check(lib.avformat_write_header( - self.ptr, - &options.ptr - )) + self.err_check(lib.avformat_write_header(self.ptr, &options.ptr)) # Track option usage... for k in all_options: From 49b7c149edb4849407510ed835e3ed598dbec4e0 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 20 Oct 2024 03:27:56 -0400 Subject: [PATCH 396/846] Fix segfault when printing closed video_stream --- av/audio/stream.pyi | 7 +++++-- av/codec/context.pxd | 4 ++-- av/codec/context.pyx | 9 +++++++-- av/container/streams.pyi | 1 - av/video/codeccontext.pyx | 4 ++++ av/video/stream.pyi | 9 ++++++--- tests/test_streams.py | 35 +++++++++++++++++++++++++++++++++++ 7 files changed, 59 insertions(+), 10 deletions(-) diff --git a/av/audio/stream.pyi b/av/audio/stream.pyi index 443c85fa1..4ba4fe48a 100644 --- a/av/audio/stream.pyi +++ b/av/audio/stream.pyi @@ -18,6 +18,9 @@ class _Layout: class AudioStream(Stream): codec_context: AudioCodecContext + def encode(self, frame: AudioFrame | None = None) -> list[Packet]: ... + def decode(self, packet: Packet | None = None) -> list[AudioFrame]: ... + # From codec context frame_size: int sample_rate: int @@ -27,5 +30,5 @@ class AudioStream(Stream): type: Literal["audio"] format: _Format layout: _Layout - def encode(self, frame: AudioFrame | None = None) -> list[Packet]: ... - def decode(self, packet: Packet | None = None) -> list[AudioFrame]: ... + + def close(self, strict: bool = True) -> None: ... diff --git a/av/codec/context.pxd b/av/codec/context.pxd index 782a66f8f..0b891877a 100644 --- a/av/codec/context.pxd +++ b/av/codec/context.pxd @@ -49,8 +49,8 @@ cdef class CodecContext: cdef _setup_decoded_frame(self, Frame, Packet) # Implemented by base for the generic send/recv API. - # Note that the user cannot send without recieving. This is because - # _prepare_frames_for_encode may expand a frame into multiple (e.g. when + # Note that the user cannot send without receiving. This is because + # `_prepare_frames_for_encode` may expand a frame into multiple (e.g. when # resampling audio to a higher rate but with fixed size frames), and the # send/recv buffer may be limited to a single frame. Ergo, we need to flush # the buffer as often as possible. diff --git a/av/codec/context.pyx b/av/codec/context.pyx index c00dec1dd..64c934f05 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -193,10 +193,11 @@ cdef class CodecContext: @property def extradata(self): + if self.ptr is NULL: + return None if self.ptr.extradata_size > 0: return (self.ptr.extradata)[:self.ptr.extradata_size] - else: - return None + return None @extradata.setter def extradata(self, data): @@ -222,10 +223,14 @@ cdef class CodecContext: @property def is_encoder(self): + if self.ptr is NULL: + return False return lib.av_codec_is_encoder(self.ptr.codec) @property def is_decoder(self): + if self.ptr is NULL: + return False return lib.av_codec_is_decoder(self.ptr.codec) cpdef open(self, bint strict=True): diff --git a/av/container/streams.pyi b/av/container/streams.pyi index 5d8647afe..fbaf1b67f 100644 --- a/av/container/streams.pyi +++ b/av/container/streams.pyi @@ -16,7 +16,6 @@ class StreamContainer: other: tuple[Stream, ...] def __init__(self) -> None: ... - def add_stream(self, stream: Stream) -> None: ... def __len__(self) -> int: ... def __iter__(self) -> Iterator[Stream]: ... @overload diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.pyx index 5764c400b..3e55472f6 100644 --- a/av/video/codeccontext.pyx +++ b/av/video/codeccontext.pyx @@ -78,6 +78,8 @@ cdef class VideoCodecContext(CodecContext): @property def width(self): + if self.ptr is NULL: + return 0 return self.ptr.width @width.setter @@ -87,6 +89,8 @@ cdef class VideoCodecContext(CodecContext): @property def height(self): + if self.ptr is NULL: + return 0 return self.ptr.height @height.setter diff --git a/av/video/stream.pyi b/av/video/stream.pyi index 3028bd31c..ad2bbc056 100644 --- a/av/video/stream.pyi +++ b/av/video/stream.pyi @@ -17,6 +17,11 @@ class VideoStream(Stream): sample_aspect_ratio: Fraction | None display_aspect_ratio: Fraction | None codec_context: VideoCodecContext + + def encode(self, frame: VideoFrame | None = None) -> list[Packet]: ... + def encode_lazy(self, frame: VideoFrame | None = None) -> Iterator[Packet]: ... + def decode(self, packet: Packet | None = None) -> list[VideoFrame]: ... + # from codec context format: VideoFormat width: int @@ -36,6 +41,4 @@ class VideoStream(Stream): colorspace: int type: Literal["video"] - def encode(self, frame: VideoFrame | None = None) -> list[Packet]: ... - def encode_lazy(self, frame: VideoFrame | None = None) -> Iterator[Packet]: ... - def decode(self, packet: Packet | None = None) -> list[VideoFrame]: ... + def close(self, strict: bool = True) -> None: ... diff --git a/tests/test_streams.py b/tests/test_streams.py index b6097d537..9828278f0 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -35,6 +35,41 @@ def test_selection(self) -> None: data = container.streams.data[0] assert data == container.streams.best("data") + def test_printing_closed_video_stream(self) -> None: + input_ = av.open( + fate_suite("amv/MTV_high_res_320x240_sample_Penguin_Joke_MTV_from_WMV.amv") + ) + container = av.open("out.mkv", "w") + + video_stream = container.add_stream("h264", rate=30) + # encoder = video_stream.codec.name + "" + + video_stream.width = input_.streams.video[0].width + video_stream.height = input_.streams.video[0].height + video_stream.pix_fmt = "yuv420p" + + for frame in input_.decode(video=0): + container.mux(video_stream.encode(frame)) + break + + encoder = "libx264" + repr = f"{video_stream}" + assert repr.startswith(f"") + + # repr = f"{video_stream}" + # assert repr.startswith(f"") + + video_stream.close() + + repr = f"{video_stream}" + assert repr.startswith(f"") + + container.close() + input_.close() + # def test_side_data(self) -> None: # container = av.open(fate_suite("mov/displaymatrix.mov")) # video = container.streams.video[0] From 2dfa0c00e6117e4f2bf13fc0ba50e5ec550f1969 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 20 Oct 2024 04:48:40 -0400 Subject: [PATCH 397/846] Remove `CodecContext.close()` Use `del` instead. This fixes many null pointer dereferences. --- av/audio/stream.pyi | 2 -- av/codec/context.pxd | 1 - av/codec/context.pyi | 1 - av/codec/context.pyx | 7 ------- av/container/output.pyx | 3 +-- av/video/stream.pyi | 2 -- tests/test_streams.py | 15 ++------------- 7 files changed, 3 insertions(+), 28 deletions(-) diff --git a/av/audio/stream.pyi b/av/audio/stream.pyi index 4ba4fe48a..f92fb52ba 100644 --- a/av/audio/stream.pyi +++ b/av/audio/stream.pyi @@ -30,5 +30,3 @@ class AudioStream(Stream): type: Literal["audio"] format: _Format layout: _Layout - - def close(self, strict: bool = True) -> None: ... diff --git a/av/codec/context.pxd b/av/codec/context.pxd index 0b891877a..2aa71d2a5 100644 --- a/av/codec/context.pxd +++ b/av/codec/context.pxd @@ -30,7 +30,6 @@ cdef class CodecContext: # Public API. cpdef open(self, bint strict=?) - cpdef close(self, bint strict=?) cdef _set_default_time_base(self) diff --git a/av/codec/context.pyi b/av/codec/context.pyi index d2d03b7ab..43c26883c 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -99,7 +99,6 @@ class CodecContext: delay: bool def open(self, strict: bool = True) -> None: ... - def close(self, strict: bool = True) -> None: ... @staticmethod def create( codec: str | Codec, mode: Literal["r", "w"] | None = None diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 64c934f05..0a618933a 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -254,13 +254,6 @@ cdef class CodecContext: self.ptr.time_base.num = 1 self.ptr.time_base.den = lib.AV_TIME_BASE - cpdef close(self, bint strict=True): - if not self._is_open: - if strict: - raise ValueError("CodecContext is already closed.") - return - self._is_open = False - lib.avcodec_free_context(&self.ptr) def __dealloc__(self): if self.ptr and self.extradata_set: diff --git a/av/container/output.pyx b/av/container/output.pyx index a0a59c111..39755ac32 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -246,8 +246,7 @@ cdef class OutputContainer(Container): def close(self): for stream in self.streams: - if stream.codec_context: - stream.codec_context.close(strict=False) + del stream close_output(self) diff --git a/av/video/stream.pyi b/av/video/stream.pyi index ad2bbc056..f0cdd5eb4 100644 --- a/av/video/stream.pyi +++ b/av/video/stream.pyi @@ -40,5 +40,3 @@ class VideoStream(Stream): color_trc: int colorspace: int type: Literal["video"] - - def close(self, strict: bool = True) -> None: ... diff --git a/tests/test_streams.py b/tests/test_streams.py index 9828278f0..075a333b2 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -35,14 +35,14 @@ def test_selection(self) -> None: data = container.streams.data[0] assert data == container.streams.best("data") - def test_printing_closed_video_stream(self) -> None: + def test_printing_video_stream(self) -> None: input_ = av.open( fate_suite("amv/MTV_high_res_320x240_sample_Penguin_Joke_MTV_from_WMV.amv") ) container = av.open("out.mkv", "w") video_stream = container.add_stream("h264", rate=30) - # encoder = video_stream.codec.name + "" + encoder = video_stream.codec.name video_stream.width = input_.streams.video[0].width video_stream.height = input_.streams.video[0].height @@ -52,21 +52,10 @@ def test_printing_closed_video_stream(self) -> None: container.mux(video_stream.encode(frame)) break - encoder = "libx264" repr = f"{video_stream}" assert repr.startswith(f"") - # repr = f"{video_stream}" - # assert repr.startswith(f"") - - video_stream.close() - - repr = f"{video_stream}" - assert repr.startswith(f"") - container.close() input_.close() From 89d131ce8850b765cf115f6f01395bc6d523beb9 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 22 Oct 2024 00:33:01 -0400 Subject: [PATCH 398/846] Set timebases in open --- av/audio/codeccontext.pyx | 5 ----- av/codec/context.pxd | 5 +---- av/codec/context.pyi | 9 ++++++--- av/codec/context.pyx | 31 ++++++++++++++----------------- av/video/codeccontext.pyx | 4 ---- 5 files changed, 21 insertions(+), 33 deletions(-) diff --git a/av/audio/codeccontext.pyx b/av/audio/codeccontext.pyx index 1723cbac7..54319ddaf 100644 --- a/av/audio/codeccontext.pyx +++ b/av/audio/codeccontext.pyx @@ -11,14 +11,9 @@ cdef class AudioCodecContext(CodecContext): cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec): CodecContext._init(self, ptr, codec) - cdef _set_default_time_base(self): - self.ptr.time_base.num = 1 - self.ptr.time_base.den = self.ptr.sample_rate - cdef _prepare_frames_for_encode(self, Frame input_frame): cdef AudioFrame frame = input_frame - cdef bint allow_var_frame_size = self.ptr.codec.capabilities & lib.AV_CODEC_CAP_VARIABLE_FRAME_SIZE # Note that the resampler will simply return an input frame if there is diff --git a/av/codec/context.pxd b/av/codec/context.pxd index 2aa71d2a5..95d799a10 100644 --- a/av/codec/context.pxd +++ b/av/codec/context.pxd @@ -8,12 +8,10 @@ from av.packet cimport Packet cdef class CodecContext: - cdef lib.AVCodecContext *ptr # Whether AVCodecContext.extradata should be de-allocated upon destruction. cdef bint extradata_set - cdef bint _is_open # Used as a signal that this is within a stream, and also for us to access # that stream. This is set "manually" by the stream after constructing @@ -30,8 +28,7 @@ cdef class CodecContext: # Public API. cpdef open(self, bint strict=?) - - cdef _set_default_time_base(self) + cdef readonly bint is_open # Wraps both versions of the transcode API, returning lists. cpdef encode(self, Frame frame=?) diff --git a/av/codec/context.pyi b/av/codec/context.pyi index 43c26883c..d6573cc80 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -57,9 +57,6 @@ class Flags2(EnumFlag): class CodecContext: extradata: bytes | None extradata_size: int - is_open: bool - is_encoder: bool - is_decoder: bool name: str codec: Codec options: dict[str, str] @@ -98,6 +95,12 @@ class CodecContext: closed_gop: bool delay: bool + @property + def is_open(self) -> bool: ... + @property + def is_encoder(self) -> bool: ... + @property + def is_decoder(self) -> bool: ... def open(self, strict: bool = True) -> None: ... @staticmethod def create( diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 0a618933a..0170aae52 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -117,7 +117,6 @@ Flags2 = define_enum("Flags2", __name__, ( cdef class CodecContext: - @staticmethod def create(codec, mode=None): cdef Codec cy_codec = codec if isinstance(codec, Codec) else Codec(codec, mode) @@ -130,7 +129,7 @@ cdef class CodecContext: self.options = {} self.stream_index = -1 # This is set by the container immediately. - self._is_open = False + self.is_open = False cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec): self.ptr = ptr @@ -217,10 +216,6 @@ cdef class CodecContext: def extradata_size(self): return self.ptr.extradata_size - @property - def is_open(self): - return self._is_open - @property def is_encoder(self): if self.ptr is NULL: @@ -234,7 +229,7 @@ cdef class CodecContext: return lib.av_codec_is_decoder(self.ptr.codec) cpdef open(self, bint strict=True): - if self._is_open: + if self.is_open: if strict: raise ValueError("CodecContext is already open.") return @@ -242,19 +237,21 @@ cdef class CodecContext: cdef _Dictionary options = Dictionary() options.update(self.options or {}) - # Assert we have a time_base for encoders. if not self.ptr.time_base.num and self.is_encoder: - self._set_default_time_base() + if self.type == "video": + self.ptr.time_base.num = self.ptr.framerate.den or 1 + self.ptr.time_base.den = self.ptr.framerate.num or lib.AV_TIME_BASE + elif self.type == "audio": + self.ptr.time_base.num = 1 + self.ptr.time_base.den = self.ptr.sample_rate + else: + self.ptr.time_base.num = 1 + self.ptr.time_base.den = lib.AV_TIME_BASE err_check(lib.avcodec_open2(self.ptr, self.codec.ptr, &options.ptr)) - self._is_open = True + self.is_open = True self.options = dict(options) - cdef _set_default_time_base(self): - self.ptr.time_base.num = 1 - self.ptr.time_base.den = lib.AV_TIME_BASE - - def __dealloc__(self): if self.ptr and self.extradata_set: lib.av_freep(&self.ptr.extradata) @@ -564,7 +561,7 @@ cdef class CodecContext: @thread_count.setter def thread_count(self, int value): - if self._is_open: + if self.is_open: raise RuntimeError("Cannot change thread_count after codec is open.") self.ptr.thread_count = value @@ -579,7 +576,7 @@ cdef class CodecContext: @thread_type.setter def thread_type(self, value): - if self._is_open: + if self.is_open: raise RuntimeError("Cannot change thread_type after codec is open.") self.ptr.thread_type = ThreadType[value].value diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.pyx index 3e55472f6..05e35daa8 100644 --- a/av/video/codeccontext.pyx +++ b/av/video/codeccontext.pyx @@ -20,10 +20,6 @@ cdef class VideoCodecContext(CodecContext): self._build_format() self.encoded_frame_count = 0 - cdef _set_default_time_base(self): - self.ptr.time_base.num = self.ptr.framerate.den or 1 - self.ptr.time_base.den = self.ptr.framerate.num or lib.AV_TIME_BASE - cdef _prepare_frames_for_encode(self, Frame input): if not input: return [None] From 637b21aaec66b9467946c9df5d6f9061bb6e4b57 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 22 Oct 2024 01:08:17 -0400 Subject: [PATCH 399/846] Type stub what is read only --- av/codec/context.pxd | 17 ++++++----------- av/codec/context.pyi | 18 +++++++++++------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/av/codec/context.pxd b/av/codec/context.pxd index 95d799a10..42b2d63e7 100644 --- a/av/codec/context.pxd +++ b/av/codec/context.pxd @@ -13,32 +13,27 @@ cdef class CodecContext: # Whether AVCodecContext.extradata should be de-allocated upon destruction. cdef bint extradata_set - # Used as a signal that this is within a stream, and also for us to access - # that stream. This is set "manually" by the stream after constructing - # this object. + # Used as a signal that this is within a stream, and also for us to access that + # stream. This is set "manually" by the stream after constructing this object. cdef int stream_index cdef lib.AVCodecParserContext *parser - cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec) + # Public API. + cdef readonly bint is_open cdef readonly Codec codec - cdef public dict options - - # Public API. cpdef open(self, bint strict=?) - cdef readonly bint is_open # Wraps both versions of the transcode API, returning lists. cpdef encode(self, Frame frame=?) cpdef decode(self, Packet packet=?) - cpdef flush_buffers(self) # Used by both transcode APIs to setup user-land objects. - # TODO: Remove the `Packet` from `_setup_decoded_frame` (because flushing - # packets are bogus). It should take all info it needs from the context and/or stream. + # TODO: Remove the `Packet` from `_setup_decoded_frame` (because flushing packets + # are bogus). It should take all info it needs from the context and/or stream. cdef _prepare_and_time_rebase_frames_for_encode(self, Frame frame) cdef _prepare_frames_for_encode(self, Frame frame) cdef _setup_encoded_packet(self, Packet) diff --git a/av/codec/context.pyi b/av/codec/context.pyi index d6573cc80..d2336ce1d 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -55,23 +55,20 @@ class Flags2(EnumFlag): RO_FLUSH_NOOP: int class CodecContext: - extradata: bytes | None - extradata_size: int name: str - codec: Codec - options: dict[str, str] type: Literal["video", "audio", "data", "subtitle", "attachment"] + options: dict[str, str] profile: str | None + extradata: bytes | None time_base: Fraction codec_tag: str bit_rate: int | None - max_bit_rate: int | None bit_rate_tolerance: int thread_count: int thread_type: Any skip_frame: Any - # flgas + # flags unaligned: bool qscale: bool four_mv: bool @@ -93,7 +90,6 @@ class CodecContext: ac_pred: bool interlaced_me: bool closed_gop: bool - delay: bool @property def is_open(self) -> bool: ... @@ -101,6 +97,14 @@ class CodecContext: def is_encoder(self) -> bool: ... @property def is_decoder(self) -> bool: ... + @property + def codec(self) -> Codec: ... + @property + def max_bit_rate(self) -> int | None: ... + @property + def delay(self) -> bool: ... + @property + def extradata_size(self) -> int: ... def open(self, strict: bool = True) -> None: ... @staticmethod def create( From a4f218f2d964d5a4af66b34d7ca6b2ce2d7ee111 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 25 Oct 2024 21:17:13 -0400 Subject: [PATCH 400/846] Fix for #1599 This eventually applies the "workaround" automatically --- av/container/input.pyx | 1 + av/container/output.pyx | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/av/container/input.pyx b/av/container/input.pyx index 88cc95ee0..7246f8245 100644 --- a/av/container/input.pyx +++ b/av/container/input.pyx @@ -13,6 +13,7 @@ from av.dictionary import Dictionary cdef close_input(InputContainer self): + self.streams = StreamContainer() if self.input_was_opened: with nogil: # This causes `self.ptr` to be set to NULL. diff --git a/av/container/output.pyx b/av/container/output.pyx index 39755ac32..f1894a617 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -18,6 +18,7 @@ log = logging.getLogger(__name__) cdef close_output(OutputContainer self): + self.streams = StreamContainer() if self._started and not self._done: # We must only ever call av_write_trailer *once*, otherwise we get a # segmentation fault. Therefore no matter whether it succeeds or not @@ -245,9 +246,6 @@ cdef class OutputContainer(Container): return lib.avcodec_get_name(self.format.optr.subtitle_codec) def close(self): - for stream in self.streams: - del stream - close_output(self) def mux(self, packets): From ee30e4f24a49bf101072b751b0c39df14d36b851 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 29 Oct 2024 16:59:34 -0400 Subject: [PATCH 401/846] Remove unused `**kwargs` --- av/audio/frame.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/audio/frame.pyx b/av/audio/frame.pyx index 1e45842ea..dd905d152 100644 --- a/av/audio/frame.pyx +++ b/av/audio/frame.pyx @@ -164,7 +164,7 @@ cdef class AudioFrame(Frame): def rate(self, value): self.ptr.sample_rate = value - def to_ndarray(self, **kwargs): + def to_ndarray(self): """Get a numpy array of this frame. .. note:: Numpy must be installed. From 46396c6bdefa7009b347f103052a05591ecd6dd0 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 31 Oct 2024 00:42:44 -0400 Subject: [PATCH 402/846] Better err messages for audio frames --- av/audio/frame.pyx | 8 +++++--- av/utils.pxd | 3 --- av/utils.pyx | 8 -------- av/video/frame.pxd | 2 -- av/video/frame.pyx | 17 ++++++++--------- tests/test_audioframe.py | 3 ++- tests/test_python_io.py | 1 - 7 files changed, 15 insertions(+), 27 deletions(-) diff --git a/av/audio/frame.pyx b/av/audio/frame.pyx index dd905d152..14356cb4e 100644 --- a/av/audio/frame.pyx +++ b/av/audio/frame.pyx @@ -2,7 +2,7 @@ from av.audio.format cimport get_audio_format from av.audio.layout cimport get_audio_layout from av.audio.plane cimport AudioPlane from av.error cimport err_check -from av.utils cimport check_ndarray, check_ndarray_shape +from av.utils cimport check_ndarray cdef object _cinit_bypass_sentinel @@ -109,10 +109,12 @@ cdef class AudioFrame(Frame): nb_channels = AudioLayout(layout).nb_channels check_ndarray(array, dtype, 2) if AudioFormat(format).is_planar: - check_ndarray_shape(array, array.shape[0] == nb_channels) + if array.shape[0] != nb_channels: + raise ValueError(f"Expected planar `array.shape[0]` to equal `{nb_channels}` but got `{array.shape[0]}`") samples = array.shape[1] else: - check_ndarray_shape(array, array.shape[0] == 1) + if array.shape[0] != 1: + raise ValueError(f"Expected packed `array.shape[0]` to equal `1` but got `{array.shape[0]}`") samples = array.shape[1] // nb_channels frame = AudioFrame(format=format, layout=layout, samples=samples) diff --git a/av/utils.pxd b/av/utils.pxd index 7f342c05b..9aeb4a2fb 100644 --- a/av/utils.pxd +++ b/av/utils.pxd @@ -5,11 +5,8 @@ from libc.stdint cimport uint64_t cdef dict avdict_to_dict(lib.AVDictionary *input, str encoding, str errors) cdef dict_to_avdict(lib.AVDictionary **dst, dict src, str encoding, str errors) - cdef object avrational_to_fraction(const lib.AVRational *input) cdef void to_avrational(object frac, lib.AVRational *input) - cdef check_ndarray(object array, object dtype, int ndim) -cdef check_ndarray_shape(object array, bint ok) cdef flag_in_bitfield(uint64_t bitfield, uint64_t flag) diff --git a/av/utils.pyx b/av/utils.pyx index 23f2d10b9..190bbf4d7 100644 --- a/av/utils.pyx +++ b/av/utils.pyx @@ -66,14 +66,6 @@ cdef check_ndarray(object array, object dtype, int ndim): raise ValueError(f"Expected numpy array with ndim `{ndim}` but got `{array.ndim}`") -cdef check_ndarray_shape(object array, bint ok): - """ - Check a numpy array has the expected shape. - """ - if not ok: - raise ValueError(f"Unexpected numpy array shape `{array.shape}`") - - cdef flag_in_bitfield(uint64_t bitfield, uint64_t flag): # Not every flag exists in every version of FFMpeg, so we define them to 0. if not flag: diff --git a/av/video/frame.pxd b/av/video/frame.pxd index 53a154a9c..779b23977 100644 --- a/av/video/frame.pxd +++ b/av/video/frame.pxd @@ -7,14 +7,12 @@ from av.video.reformatter cimport VideoReformatter cdef class VideoFrame(Frame): - # This is the buffer that is used to back everything in the AVFrame. # We don't ever actually access it directly. cdef uint8_t *_buffer cdef object _np_buffer cdef VideoReformatter reformatter - cdef readonly VideoFormat format cdef _init(self, lib.AVPixelFormat format, unsigned int width, unsigned int height) diff --git a/av/video/frame.pyx b/av/video/frame.pyx index a7c794a55..4fb463c8c 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -4,7 +4,7 @@ from libc.stdint cimport uint8_t from av.enum cimport define_enum from av.error cimport err_check -from av.utils cimport check_ndarray, check_ndarray_shape +from av.utils cimport check_ndarray from av.video.format cimport get_pix_fmt, get_video_format from av.video.plane cimport VideoPlane @@ -92,6 +92,11 @@ cdef useful_array(VideoPlane plane, unsigned int bytes_per_pixel=1, str dtype="u return arr.view(np.dtype(dtype)) +cdef check_ndarray_shape(object array, bint ok): + if not ok: + raise ValueError(f"Unexpected numpy array shape `{array.shape}`") + + cdef class VideoFrame(Frame): def __cinit__(self, width=0, height=0, format="yuv420p"): if width is _cinit_bypass_sentinel: @@ -109,18 +114,12 @@ cdef class VideoFrame(Frame): self.ptr.height = height self.ptr.format = format - # Allocate the buffer for the video frame. - # # We enforce aligned buffers, otherwise `sws_scale` can perform # poorly or even cause out-of-bounds reads and writes. if width and height: res = lib.av_image_alloc( - self.ptr.data, - self.ptr.linesize, - width, - height, - format, - 16) + self.ptr.data, self.ptr.linesize, width, height, format, 16 + ) self._buffer = self.ptr.data[0] if res: diff --git a/tests/test_audioframe.py b/tests/test_audioframe.py index 7211ad023..d67e181ce 100644 --- a/tests/test_audioframe.py +++ b/tests/test_audioframe.py @@ -124,7 +124,8 @@ def test_from_ndarray_value_error() -> None: # incorrect shape array = np.zeros(shape=(2, 160), dtype="f4") with pytest.raises( - ValueError, match=escape("Unexpected numpy array shape `(2, 160)`") + ValueError, + match=escape("Expected packed `array.shape[0]` to equal `1` but got `2`"), ) as cm: AudioFrame.from_ndarray(array, format="flt", layout="mono") diff --git a/tests/test_python_io.py b/tests/test_python_io.py index d657404d9..3169ae047 100644 --- a/tests/test_python_io.py +++ b/tests/test_python_io.py @@ -24,7 +24,6 @@ def __init__(self, obj: object) -> None: self._log: list[tuple[str, object]] = [] def __getattr__(self, name: str) -> object: - def _method(name: str, meth: Callable, *args, **kwargs) -> object: self._log.append((name, args)) return meth(*args, **kwargs) From 2f9a2797406f4b85e69da8fd71e481f0d2ec6cea Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 2 Nov 2024 23:06:29 +0100 Subject: [PATCH 403/846] Add __all__ to audio and video packages --- av/audio/__init__.pyi | 4 ++++ av/video/__init__.pyi | 2 ++ 2 files changed, 6 insertions(+) create mode 100644 av/audio/__init__.pyi diff --git a/av/audio/__init__.pyi b/av/audio/__init__.pyi new file mode 100644 index 000000000..73f2eebdd --- /dev/null +++ b/av/audio/__init__.pyi @@ -0,0 +1,4 @@ +from .frame import AudioFrame +from .stream import AudioStream + +__all__ = ("AudioFrame", "AudioStream") diff --git a/av/video/__init__.pyi b/av/video/__init__.pyi index 4a25d8837..8fa8fe7e5 100644 --- a/av/video/__init__.pyi +++ b/av/video/__init__.pyi @@ -1,2 +1,4 @@ from .frame import VideoFrame from .stream import VideoStream + +__all__ = ("VideoFrame", "VideoStream") From f8a07d0b83e2ec1479572542c168b79d7551d817 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 3 Nov 2024 13:42:54 -0500 Subject: [PATCH 404/846] Add `add_stream_from_template()` method --- av/container/output.pyi | 15 +++++++-- av/container/output.pyx | 72 ++++++++++++++++++++++++++++++++++++----- tests/test_streams.py | 19 +++++++++++ 3 files changed, 96 insertions(+), 10 deletions(-) diff --git a/av/container/output.pyi b/av/container/output.pyi index 7891e17b5..c318d93e5 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -1,7 +1,6 @@ from fractions import Fraction -from typing import Literal, Sequence, overload +from typing import Literal, Sequence, TypeVar, overload -from av.audio.layout import AudioLayout from av.audio.stream import AudioStream from av.packet import Packet from av.stream import Stream @@ -9,6 +8,8 @@ from av.video.stream import VideoStream from .core import Container +_StreamT = TypeVar("_StreamT", bound=Stream, default=Stream) + class OutputContainer(Container): def __enter__(self) -> OutputContainer: ... @overload @@ -30,6 +31,15 @@ class OutputContainer(Container): **kwargs, ) -> VideoStream: ... @overload + def add_stream( + self, + codec_name: None = None, + rate: Fraction | int | None = None, + template: _StreamT | None = None, + options: dict[str, str] | None = None, + **kwargs, + ) -> _StreamT: ... + @overload def add_stream( self, codec_name: str | None = None, @@ -38,6 +48,7 @@ class OutputContainer(Container): options: dict[str, str] | None = None, **kwargs, ) -> Stream: ... + def add_stream_from_template(self, template: _StreamT, **kwargs) -> _StreamT: ... def start_encoding(self) -> None: ... def close(self) -> None: ... def mux(self, packets: Packet | Sequence[Packet]) -> None: ... diff --git a/av/container/output.pyx b/av/container/output.pyx index f1894a617..f80c6e55e 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -46,16 +46,15 @@ cdef class OutputContainer(Container): def add_stream(self, codec_name=None, object rate=None, Stream template=None, options=None, **kwargs): """add_stream(codec_name, rate=None) - Create a new stream, and return it. + Creates a new stream from a codec name or a template, and returns it. - :param str codec_name: The name of a codec. - :param rate: The frame rate for video, and sample rate for audio. - Examples for video include ``24`` and ``Fraction(30000, 1001)``. - Examples for audio include ``48000`` and ``44100``. + :param codec_name: The name of a codec. + :type codec_name: str | Codec | None :param template: Copy codec from another :class:`~av.stream.Stream` instance. + :type template: :class:`~av.stream.Stream` | None :param dict options: Stream options. - :param \\**kwargs: Set attributes of the stream. - :returns: The new :class:`~av.stream.Stream`. + :param \\**kwargs: Set attributes for the stream. + :rtype: The new :class:`~av.stream.Stream`. """ @@ -76,7 +75,7 @@ cdef class OutputContainer(Container): # Assert that this format supports the requested codec. if not lib.avformat_query_codec(self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL): raise ValueError( - f"{self.format.name!r} format does not support {codec_name!r} codec" + f"{self.format.name!r} format does not support {codec_obj.name!r} codec" ) # Create new stream in the AVFormatContext, set AVCodecContext values. @@ -147,6 +146,63 @@ cdef class OutputContainer(Container): return py_stream + def add_stream_from_template(self, Stream template not None, **kwargs): + """ + Creates a new stream from a template. + + :param template: Copy codec from another :class:`~av.stream.Stream` instance. + :param \\**kwargs: Set attributes for the stream. + :rtype: The new :class:`~av.stream.Stream`. + + """ + + if not template.codec_context: + raise ValueError("template has no codec context") + + cdef Codec codec_obj = Codec(template.codec_context.codec.name, "w") + cdef const lib.AVCodec *codec = codec_obj.ptr + + # Assert that this format supports the requested codec. + if not lib.avformat_query_codec(self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL): + raise ValueError( + f"{self.format.name!r} format does not support {codec_obj.name!r} codec" + ) + + # Create new stream in the AVFormatContext, set AVCodecContext values. + cdef lib.AVStream *stream = lib.avformat_new_stream(self.ptr, codec) + cdef lib.AVCodecContext *codec_context = lib.avcodec_alloc_context3(codec) + + err_check(lib.avcodec_parameters_to_context(codec_context, template.ptr.codecpar)) + # Reset the codec tag assuming we are remuxing. + codec_context.codec_tag = 0 + + # Some formats want stream headers to be separate + if self.ptr.oformat.flags & lib.AVFMT_GLOBALHEADER: + codec_context.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER + + # Initialise stream codec parameters to populate the codec type. + # + # Subsequent changes to the codec context will be applied just before + # encoding starts in `start_encoding()`. + err_check(lib.avcodec_parameters_from_context(stream.codecpar, codec_context)) + + # Construct the user-land stream + cdef CodecContext py_codec_context = wrap_codec_context(codec_context, codec) + cdef Stream py_stream = wrap_stream(self, stream, py_codec_context) + self.streams.add_stream(py_stream) + + if template.type == "video": + py_stream.time_base = kwargs.pop("time_base", 1 / template.average_rate) + elif template.type == "audio": + py_stream.time_base = kwargs.pop("time_base", 1 / template.rate) + else: + py_stream.time_base = kwargs.pop("time_base", None) + + for k, v in kwargs.items(): + setattr(py_stream, k, v) + + return py_stream + cpdef start_encoding(self): """Write the file header! Called automatically.""" diff --git a/tests/test_streams.py b/tests/test_streams.py index 075a333b2..045745543 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -59,6 +59,25 @@ def test_printing_video_stream(self) -> None: container.close() input_.close() + def test_printing_video_stream2(self) -> None: + input_ = av.open(fate_suite("h264/interlaced_crop.mp4")) + input_stream = input_.streams.video[0] + container = av.open("out.mkv", "w") + + video_stream = container.add_stream_from_template(input_stream) + encoder = video_stream.codec.name + + for frame in input_.decode(video=0): + container.mux(video_stream.encode(frame)) + break + + repr = f"{video_stream}" + assert repr.startswith(f"") + + container.close() + input_.close() + # def test_side_data(self) -> None: # container = av.open(fate_suite("mov/displaymatrix.mov")) # video = container.streams.video[0] From dd389764b430647a74abbfed6dce4021b483dca9 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 3 Nov 2024 17:58:11 -0500 Subject: [PATCH 405/846] Remove `template` arg Use `add_stream_from_template()` is a replacement. This untested argument makes writing type stubs more complex than it needs to. --- CHANGELOG.rst | 5 +++++ av/container/output.pyi | 16 ++-------------- av/container/output.pyx | 29 +++++++++-------------------- 3 files changed, 16 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5becaf5f6..8e8077b79 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -23,6 +23,11 @@ Major: - Drop FFmpeg 6. - Remove ``AVError`` alias (use ``FFmpegError`` directly instead). +- Remove the `template` arg from ``OutputContainer.add_stream()``. + +Features: + +- Add ``OutputContainer.add_stream_from_template()`` by :gh-user:`WyattBlue` and :gh-user:`cdce8p`. v13.1.0 ------- diff --git a/av/container/output.pyi b/av/container/output.pyi index c318d93e5..97b67ee71 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -17,34 +17,22 @@ class OutputContainer(Container): self, codec_name: Literal["pcm_s16le", "aac", "mp3", "mp2"], rate: int | None = None, - template: None = None, options: dict[str, str] | None = None, **kwargs, ) -> AudioStream: ... @overload def add_stream( self, - codec_name: Literal["h264", "mpeg4", "png", "qtrle"], + codec_name: Literal["h264", "hevc", "mpeg4", "png", "gif", "qtrle"], rate: Fraction | int | None = None, - template: None = None, options: dict[str, str] | None = None, **kwargs, ) -> VideoStream: ... @overload def add_stream( self, - codec_name: None = None, + codec_name: str, rate: Fraction | int | None = None, - template: _StreamT | None = None, - options: dict[str, str] | None = None, - **kwargs, - ) -> _StreamT: ... - @overload - def add_stream( - self, - codec_name: str | None = None, - rate: Fraction | int | None = None, - template: Stream | None = None, options: dict[str, str] | None = None, **kwargs, ) -> Stream: ... diff --git a/av/container/output.pyx b/av/container/output.pyx index f80c6e55e..3e4c9dbb4 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -43,33 +43,28 @@ cdef class OutputContainer(Container): with nogil: lib.av_packet_free(&self.packet_ptr) - def add_stream(self, codec_name=None, object rate=None, Stream template=None, options=None, **kwargs): + def add_stream(self, codec_name, rate=None, dict options=None, **kwargs): """add_stream(codec_name, rate=None) - Creates a new stream from a codec name or a template, and returns it. + Creates a new stream from a codec name and returns it. :param codec_name: The name of a codec. - :type codec_name: str | Codec | None - :param template: Copy codec from another :class:`~av.stream.Stream` instance. - :type template: :class:`~av.stream.Stream` | None + :type codec_name: str | Codec :param dict options: Stream options. :param \\**kwargs: Set attributes for the stream. :rtype: The new :class:`~av.stream.Stream`. """ - if (codec_name is None and template is None) or (codec_name is not None and template is not None): - raise ValueError("needs one of codec_name or template") - cdef const lib.AVCodec *codec cdef Codec codec_obj - if codec_name is not None: - codec_obj = codec_name if isinstance(codec_name, Codec) else Codec(codec_name, "w") + if isinstance(codec_name, Codec): + if codec_name.mode != "w": + raise ValueError("codec_name must be an encoder.") + codec_obj = codec_name else: - if not template.codec_context: - raise ValueError("template has no codec context") - codec_obj = template.codec_context.codec + codec_obj = Codec(codec_name, "w") codec = codec_obj.ptr # Assert that this format supports the requested codec. @@ -82,14 +77,8 @@ cdef class OutputContainer(Container): cdef lib.AVStream *stream = lib.avformat_new_stream(self.ptr, codec) cdef lib.AVCodecContext *codec_context = lib.avcodec_alloc_context3(codec) - # Copy from the template. - if template is not None: - err_check(lib.avcodec_parameters_to_context(codec_context, template.ptr.codecpar)) - # Reset the codec tag assuming we are remuxing. - codec_context.codec_tag = 0 - # Now lets set some more sane video defaults - elif codec.type == lib.AVMEDIA_TYPE_VIDEO: + if codec.type == lib.AVMEDIA_TYPE_VIDEO: codec_context.pix_fmt = lib.AV_PIX_FMT_YUV420P codec_context.width = kwargs.pop("width", 640) codec_context.height = kwargs.pop("height", 480) From e86a39f01691d6dd5b639cde909ac547a9bc8e06 Mon Sep 17 00:00:00 2001 From: David Plowman Date: Thu, 13 Apr 2023 12:00:09 +0100 Subject: [PATCH 406/846] Add qmin and qmax parameters to the codec context Allows applications to control the min and max quantisers used for encoding. Also add a test to ensure the encoder is behaving as we expect. --- av/video/codeccontext.pyi | 2 ++ av/video/codeccontext.pyx | 30 +++++++++++++++++++ tests/test_encode.py | 63 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+) diff --git a/av/video/codeccontext.pyi b/av/video/codeccontext.pyi index 7172b50c2..da72053c4 100644 --- a/av/video/codeccontext.pyi +++ b/av/video/codeccontext.pyi @@ -26,6 +26,8 @@ class VideoCodecContext(CodecContext): color_primaries: int color_trc: int colorspace: int + qmin: int + qmax: int type: Literal["video"] def encode(self, frame: VideoFrame | None = None) -> list[Packet]: ... diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.pyx index 05e35daa8..d2f4c9e14 100644 --- a/av/video/codeccontext.pyx +++ b/av/video/codeccontext.pyx @@ -278,3 +278,33 @@ cdef class VideoCodecContext(CodecContext): @max_b_frames.setter def max_b_frames(self, value): self.ptr.max_b_frames = value + + @property + def qmin(self): + """ + The minimum quantiser value of an encoded stream. + + Wraps :ffmpeg:`AVCodecContext.qmin`. + + :type: int + """ + return self.ptr.qmin + + @qmin.setter + def qmin(self, value): + self.ptr.qmin = value + + @property + def qmax(self): + """ + The maximum quantiser value of an encoded stream. + + Wraps :ffmpeg:`AVCodecContext.qmax`. + + :type: int + """ + return self.ptr.qmax + + @qmax.setter + def qmax(self, value): + self.ptr.qmax = value diff --git a/tests/test_encode.py b/tests/test_encode.py index 89d2e3eb3..11d8a4ec9 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -386,3 +386,66 @@ def test_max_b_frames(self) -> None: file = encode_file_with_max_b_frames(max_b_frames) actual_max_b_frames = max_b_frame_run_in_file(file) assert actual_max_b_frames <= max_b_frames + + +def encode_frames_with_qminmax(frames: list, shape: tuple, qminmax: tuple) -> int: + """ + Encode a video with the given quantiser limits, and return how many enocded + bytes we made in total. + + frames: the frames to encode + shape: the (numpy) shape of the video frames + qminmax: two integers with 1 <= qmin <= 31 giving the min and max quantiser. + + Returns: total length of the encoded bytes. + """ + file = io.BytesIO() + container = av.open(file, mode="w", format="mp4") + stream = container.add_stream("h264", rate=30) + stream.height, stream.width, _ = shape + stream.pix_fmt = "yuv420p" + stream.codec_context.gop_size = 15 + stream.codec_context.qmin, stream.codec_context.qmax = qminmax + + bytes_encoded = 0 + for frame in frames: + for packet in stream.encode(frame): + bytes_encoded += packet.size + + for packet in stream.encode(): + bytes_encoded += packet.size + + container.close() + + return bytes_encoded + + +class TestQminQmaxEncoding(TestCase): + def test_qmin_qmax(self) -> None: + """ + Test that we can set the min and max quantisers, and the encoder is reacting + correctly to them. + + Can't see a way to get hold of the quantisers in a decoded video, so instead + we'll encode the same frames with decreasing quantisers, and check that the + file size increases (by a noticeable factor) each time. + """ + # Make a random - but repeatable - 10 frame video sequence. + np.random.seed(0) + frames = [] + shape = (480, 640, 3) + for _ in range(10): + frames.append( + av.VideoFrame.from_ndarray( + np.random.randint(0, 256, shape, dtype=np.uint8), format="rgb24" + ) + ) + + # Get the size of the encoded output for different quantisers. + quantisers = ((31, 31), (15, 15), (1, 1)) + sizes = [ + encode_frames_with_qminmax(frames, shape, qminmax) for qminmax in quantisers + ] + + factor = 1.3 # insist at least 30% larger each time + assert all(small * factor < large for small, large in zip(sizes, sizes[1:])) From d7a6eb998b65236ca95b53e3aef77824edeb16aa Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 7 Nov 2024 15:51:10 -0500 Subject: [PATCH 407/846] Upgrade mypy --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 1cd3c39a4..818c14c00 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ fate-suite: rsync -vrltLW rsync://fate-suite.ffmpeg.org/fate-suite/ tests/assets/fate-suite/ lint: - $(PIP) install -U black isort flake8 flake8-pyproject pillow numpy mypy==1.11.2 pytest + $(PIP) install -U black isort flake8 flake8-pyproject pillow numpy mypy==1.13.0 pytest black --check av examples tests setup.py flake8 av isort --check-only --diff av examples tests From 056486280fd0281ad22518fdebf30ab775bf15f3 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 2 Nov 2024 20:55:06 +0100 Subject: [PATCH 408/846] Make ErrorType a proper Enum --- av/enum.pyi | 7 ++++--- av/error.pyi | 59 +++++++++++++++++++++++++++------------------------- 2 files changed, 35 insertions(+), 31 deletions(-) diff --git a/av/enum.pyi b/av/enum.pyi index a4fe92e51..026a176b3 100644 --- a/av/enum.pyi +++ b/av/enum.pyi @@ -24,9 +24,10 @@ class EnumType(type): ) -> int | None: ... class EnumItem: - name: str - value: int - + @property + def name(self) -> str: ... + @property + def value(self) -> int: ... def __int__(self) -> int: ... def __hash__(self) -> int: ... def __reduce__( diff --git a/av/error.pyi b/av/error.pyi index 088f78d45..4c41f35e7 100644 --- a/av/error.pyi +++ b/av/error.pyi @@ -1,4 +1,5 @@ import builtins +from enum import Enum from .enum import EnumItem @@ -15,41 +16,43 @@ def err_check(res: int, filename: str | None = None) -> int: ... BUFFER_TOO_SMALL: EnumItem -class ErrorType(EnumItem): - BSF_NOT_FOUND: int - BUG: int - BUFFER_TOO_SMALL: int - DECODER_NOT_FOUND: int - DEMUXER_NOT_FOUND: int - ENCODER_NOT_FOUND: int - EOF: int - EXIT: int - EXTERNAL: int - FILTER_NOT_FOUND: int - INVALIDDATA: int - MUXER_NOT_FOUND: int - OPTION_NOT_FOUND: int - PATCHWELCOME: int - PROTOCOL_NOT_FOUND: int - UNKNOWN: int - EXPERIMENTAL: int - INPUT_CHANGED: int - OUTPUT_CHANGED: int - HTTP_BAD_REQUEST: int - HTTP_UNAUTHORIZED: int - HTTP_FORBIDDEN: int - HTTP_NOT_FOUND: int - HTTP_OTHER_4XX: int - HTTP_SERVER_ERROR: int - PYAV_CALLBACK: int +class ErrorType(EnumItem, Enum): + BSF_NOT_FOUND: EnumItem + BUG: EnumItem + BUFFER_TOO_SMALL: EnumItem + DECODER_NOT_FOUND: EnumItem + DEMUXER_NOT_FOUND: EnumItem + ENCODER_NOT_FOUND: EnumItem + EOF: EnumItem + EXIT: EnumItem + EXTERNAL: EnumItem + FILTER_NOT_FOUND: EnumItem + INVALIDDATA: EnumItem + MUXER_NOT_FOUND: EnumItem + OPTION_NOT_FOUND: EnumItem + PATCHWELCOME: EnumItem + PROTOCOL_NOT_FOUND: EnumItem + UNKNOWN: EnumItem + EXPERIMENTAL: EnumItem + INPUT_CHANGED: EnumItem + OUTPUT_CHANGED: EnumItem + HTTP_BAD_REQUEST: EnumItem + HTTP_UNAUTHORIZED: EnumItem + HTTP_FORBIDDEN: EnumItem + HTTP_NOT_FOUND: EnumItem + HTTP_OTHER_4XX: EnumItem + HTTP_SERVER_ERROR: EnumItem + PYAV_CALLBACK: EnumItem - tag: bytes + @property + def tag(self) -> bytes: ... class FFmpegError(Exception): errno: int strerror: str filename: str log: tuple[int, tuple[int, str, str] | None] + type: ErrorType def __init__( self, From cffe0763d7cee3d7605e98125b1012d1d1dfcd38 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 7 Nov 2024 17:55:55 -0500 Subject: [PATCH 409/846] Use IntEnums --- .github/workflows/smoke.yml | 2 +- Makefile | 2 +- av/video/reformatter.pyx | 77 ++++++++++++++++++------------------- docs/Makefile | 14 +++---- docs/api/codec.rst | 1 - docs/api/video.rst | 6 +++ docs/conf.py | 54 ++++++++++++++++++-------- scripts/build | 2 +- tests/test_videoframe.py | 4 +- 9 files changed, 94 insertions(+), 68 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index c40464af5..ca6a3f15c 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -34,7 +34,7 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-latest, python: "3.9", ffmpeg: "7.1", extras: true} + - {os: ubuntu-latest, python: "3.12", ffmpeg: "7.1", extras: true} - {os: ubuntu-latest, python: "3.9", ffmpeg: "7.0.2"} - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "7.1"} - {os: macos-14, python: "3.9", ffmpeg: "7.1"} diff --git a/Makefile b/Makefile index 818c14c00..2afb2e4c3 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ default: build build: - $(PIP) install --upgrade cython + $(PIP) install -U cython setuptools CFLAGS=$(CFLAGS) LDFLAGS=$(LDFLAGS) $(PYTHON) setup.py build_ext --inplace --debug clean: diff --git a/av/video/reformatter.pyx b/av/video/reformatter.pyx index 624454f0c..7e2b4c022 100644 --- a/av/video/reformatter.pyx +++ b/av/video/reformatter.pyx @@ -1,50 +1,49 @@ cimport libav as lib from libc.stdint cimport uint8_t -from av.enum cimport define_enum from av.error cimport err_check from av.video.format cimport VideoFormat from av.video.frame cimport alloc_video_frame -Interpolation = define_enum("Interpolation", __name__, ( - ("FAST_BILINEAR", lib.SWS_FAST_BILINEAR, "Fast bilinear"), - ("BILINEAR", lib.SWS_BILINEAR, "Bilinear"), - ("BICUBIC", lib.SWS_BICUBIC, "Bicubic"), - ("X", lib.SWS_X, "Experimental"), - ("POINT", lib.SWS_POINT, "Nearest neighbor / point"), - ("AREA", lib.SWS_AREA, "Area averaging"), - ("BICUBLIN", lib.SWS_BICUBLIN, "Luma bicubic / chroma bilinear"), - ("GAUSS", lib.SWS_GAUSS, "Gaussian"), - ("SINC", lib.SWS_SINC, "Sinc"), - ("LANCZOS", lib.SWS_LANCZOS, "Lanczos"), - ("SPLINE", lib.SWS_SPLINE, "Bicubic spline"), -)) - -Colorspace = define_enum("Colorspace", __name__, ( - ("ITU709", lib.SWS_CS_ITU709), - ("FCC", lib.SWS_CS_FCC), - ("ITU601", lib.SWS_CS_ITU601), - ("ITU624", lib.SWS_CS_ITU624), - ("SMPTE170M", lib.SWS_CS_SMPTE170M), - ("SMPTE240M", lib.SWS_CS_SMPTE240M), - ("DEFAULT", lib.SWS_CS_DEFAULT), - +from enum import IntEnum + + +class Interpolation(IntEnum): + FAST_BILINEAR: "Fast bilinear" = lib.SWS_FAST_BILINEAR + BILINEAR: "Bilinear" = lib.SWS_BILINEAR + BICUBIC: "Bicubic" = lib.SWS_BICUBIC + X: "Experimental" = lib.SWS_X + POINT: "Nearest neighbor / point" = lib.SWS_POINT + AREA: "Area averaging" = lib.SWS_AREA + BICUBLIN: "Luma bicubic / chroma bilinear" = lib.SWS_BICUBLIN + GAUSS: "Gaussian" = lib.SWS_GAUSS + SINC: "Sinc" = lib.SWS_SINC + LANCZOS: "Bicubic spline" = lib.SWS_LANCZOS + + +class Colorspace(IntEnum): + ITU709 = lib.SWS_CS_ITU709 + FCC = lib.SWS_CS_FCC + ITU601 = lib.SWS_CS_ITU601 + ITU624 = lib.SWS_CS_ITU624 + SMPTE170M = lib.SWS_CS_SMPTE170M + SMPTE240M = lib.SWS_CS_SMPTE240M + DEFAULT = lib.SWS_CS_DEFAULT # Lowercase for b/c. - ("itu709", lib.SWS_CS_ITU709), - ("fcc", lib.SWS_CS_FCC), - ("itu601", lib.SWS_CS_ITU601), - ("itu624", lib.SWS_CS_SMPTE170M), - ("smpte240", lib.SWS_CS_SMPTE240M), - ("default", lib.SWS_CS_DEFAULT), - -)) - -ColorRange = define_enum("ColorRange", __name__, ( - ("UNSPECIFIED", lib.AVCOL_RANGE_UNSPECIFIED, "Unspecified"), - ("MPEG", lib.AVCOL_RANGE_MPEG, "MPEG (limited) YUV range, 219*2^(n-8)"), - ("JPEG", lib.AVCOL_RANGE_JPEG, "JPEG (full) YUV range, 2^n-1"), - ("NB", lib.AVCOL_RANGE_NB, "Not part of ABI"), -)) + itu709 = lib.SWS_CS_ITU709 + fcc = lib.SWS_CS_FCC + itu601 = lib.SWS_CS_ITU601 + itu624 = lib.SWS_CS_SMPTE170M + smpte170m = lib.SWS_CS_SMPTE170M + smpte240m = lib.SWS_CS_SMPTE240M + default = lib.SWS_CS_DEFAULT + +class ColorRange(IntEnum): + UNSPECIFIED: "Unspecified" = lib.AVCOL_RANGE_UNSPECIFIED + MPEG: "MPEG (limited) YUV range, 219*2^(n-8)" = lib.AVCOL_RANGE_MPEG + JPEG: "JPEG (full) YUV range, 2^n-1" = lib.AVCOL_RANGE_JPEG + NB: "Not part of ABI" = lib.AVCOL_RANGE_NB + cdef class VideoReformatter: """An object for reformatting size and pixel format of :class:`.VideoFrame`. diff --git a/docs/Makefile b/docs/Makefile index e0662e90c..bb84c1dba 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,33 +1,31 @@ - SPHINXOPTS = SPHINXBUILD = sphinx-build BUILDDIR = _build FFMPEGDIR = _ffmpeg - PYAV_PIP ?= pip PIP := $(PYAV_PIP) - ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(SPHINXOPTS) . .PHONY: clean html open upload default default: html - TAGFILE := _build/doxygen/tagfile.xml -$(TAGFILE) : - git clone --depth=1 git://source.ffmpeg.org/ffmpeg.git $(FFMPEGDIR) - ./generate-tagfile --library $(FFMPEGDIR) -o $(TAGFILE) +$(TAGFILE): + @if [ ! -d "$(FFMPEGDIR)" ]; then \ + git clone --depth=1 git://source.ffmpeg.org/ffmpeg.git $(FFMPEGDIR); \ + fi + ./generate-tagfile --library $(FFMPEGDIR) -o $(TAGFILE) TEMPLATES := $(wildcard api/*.py development/*.py) RENDERED := $(TEMPLATES:%.py=_build/rst/%.rst) + _build/rst/%.rst: %.py $(TAGFILE) $(shell find ../include ../av -name '*.pyx' -or -name '*.pxd') @ mkdir -p $(@D) python $< > $@.tmp mv $@.tmp $@ - clean: rm -rf $(BUILDDIR) $(FFMPEGDIR) diff --git a/docs/api/codec.rst b/docs/api/codec.rst index bd1a6b5f0..770a271a4 100644 --- a/docs/api/codec.rst +++ b/docs/api/codec.rst @@ -65,7 +65,6 @@ Contexts .. automethod:: CodecContext.create .. automethod:: CodecContext.open -.. automethod:: CodecContext.close Attributes ~~~~~~~~~~ diff --git a/docs/api/video.rst b/docs/api/video.rst index 5e47b1db8..1c56788a1 100644 --- a/docs/api/video.rst +++ b/docs/api/video.rst @@ -117,3 +117,9 @@ Enums .. enumtable:: av.video.reformatter.Colorspace +.. autoclass:: av.video.reformatter.ColorRange + + Wraps the ``AVCOL*`` flags. + + .. enumtable:: av.video.reformatter.ColorRange + diff --git a/docs/conf.py b/docs/conf.py index 419b24238..59090193c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -238,24 +238,48 @@ def makerow(*texts): ) seen = set() - - for name, item in enum._by_name.items(): - if name.lower() in seen: - continue - seen.add(name.lower()) - - try: - attr = properties[item] - except KeyError: - if cls: + if hasattr(enum, "_by_name"): # Our custom enum class + enum_items = enum._by_name.items() + for name, item in enum_items: + if name.lower() in seen: + continue + seen.add(name.lower()) + + try: + attr = properties[item] + except KeyError: + if cls: + continue + attr = None + + value = f"0x{item.value:X}" + doc = item.__doc__ or "-" + tbody += makerow(attr, name, value, doc) + + return [table] + else: # Standard IntEnum + enum_items = [ + (name, item) + for name, item in vars(enum).items() + if isinstance(item, enum) + ] + for name, item in enum_items: + if name.lower() in seen: continue - attr = None + seen.add(name.lower()) + + try: + attr = properties[item] + except KeyError: + if cls: + continue + attr = None - value = f"0x{item.value:X}" - doc = item.__doc__ or "-" - tbody += makerow(attr, name, value, doc) + value = f"0x{item.value:X}" + doc = enum.__annotations__.get(name, "---")[1:-1] + tbody += makerow(attr, name, value, doc) - return [table] + return [table] doxylink = {} diff --git a/scripts/build b/scripts/build index 67b3836f2..7e27d7f74 100755 --- a/scripts/build +++ b/scripts/build @@ -21,6 +21,6 @@ which ffmpeg || exit 2 ffmpeg -version || exit 3 echo -$PYAV_PIP install -U cython 2> /dev/null +$PYAV_PIP install -U cython setuptools 2> /dev/null "$PYAV_PYTHON" scripts/comptime.py "$PYAV_PYTHON" setup.py config build_ext --inplace || exit 1 diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 8e14dcdf3..6396f1f45 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -634,11 +634,11 @@ def test_reformat_identity() -> None: def test_reformat_colorspace() -> None: # This is allowed. frame = VideoFrame(640, 480, "rgb24") - frame.reformat(src_colorspace=None, dst_colorspace="smpte240") + frame.reformat(src_colorspace=None, dst_colorspace="smpte240m") # I thought this was not allowed, but it seems to be. frame = VideoFrame(640, 480, "yuv420p") - frame.reformat(src_colorspace=None, dst_colorspace="smpte240") + frame.reformat(src_colorspace=None, dst_colorspace="smpte240m") def test_reformat_pixel_format_align() -> None: From 2341842e822b994778535b4d0613ec3c54cfb73c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 7 Nov 2024 21:38:08 -0500 Subject: [PATCH 410/846] Fix wrong colorspace lowercase enum --- CHANGELOG.rst | 7 +++++++ av/video/reformatter.pyx | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8e8077b79..5d034b223 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -24,10 +24,17 @@ Major: - Drop FFmpeg 6. - Remove ``AVError`` alias (use ``FFmpegError`` directly instead). - Remove the `template` arg from ``OutputContainer.add_stream()``. +- Remove ``CodecContext.close()`` because it's deprecated in ffmpeg. Features: - Add ``OutputContainer.add_stream_from_template()`` by :gh-user:`WyattBlue` and :gh-user:`cdce8p`. +- Add ``qmin`` and ``qmax`` parameters to the ``VideoCodecContext`` by :gh-user:`davidplowman` in (:pr:`1618`). + +Fixes: + +- Correct ``Colorspace``'s lowercase enums. +- Ensure streams in StreamContainer are released. Fixes :issue:`1599`. v13.1.0 ------- diff --git a/av/video/reformatter.pyx b/av/video/reformatter.pyx index 7e2b4c022..4511d08af 100644 --- a/av/video/reformatter.pyx +++ b/av/video/reformatter.pyx @@ -33,7 +33,7 @@ class Colorspace(IntEnum): itu709 = lib.SWS_CS_ITU709 fcc = lib.SWS_CS_FCC itu601 = lib.SWS_CS_ITU601 - itu624 = lib.SWS_CS_SMPTE170M + itu624 = lib.SWS_CS_ITU624 smpte170m = lib.SWS_CS_SMPTE170M smpte240m = lib.SWS_CS_SMPTE240M default = lib.SWS_CS_DEFAULT From 9fc1878b4d2cf9b313074f1dd972d146be52c6a7 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 10 Nov 2024 01:22:56 -0500 Subject: [PATCH 411/846] Move `key_frame` attribute to Frame class --- av/frame.pyi | 9 ++++++--- av/frame.pyx | 10 ++++++++++ av/video/frame.pyi | 1 - av/video/frame.pyx | 13 ------------- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/av/frame.pyi b/av/frame.pyi index 7c34e91bf..9af81dcfe 100644 --- a/av/frame.pyi +++ b/av/frame.pyi @@ -9,10 +9,13 @@ class SideData(TypedDict, total=False): class Frame: dts: int | None pts: int | None - time: float | None time_base: Fraction - is_corrupt: bool side_data: SideData opaque: object - + @property + def time(self) -> float | None: ... + @property + def is_corrupt(self) -> bool: ... + @property + def key_frame(self) -> bool: ... def make_writable(self) -> None: ... diff --git a/av/frame.pyx b/av/frame.pyx index a8871fd8f..1084ca45e 100644 --- a/av/frame.pyx +++ b/av/frame.pyx @@ -132,6 +132,16 @@ cdef class Frame: """ return self.ptr.decode_error_flags != 0 or bool(self.ptr.flags & lib.AV_FRAME_FLAG_CORRUPT) + @property + def key_frame(self): + """Is this frame a key frame? + + Wraps :ffmpeg:`AVFrame.key_frame`. + + """ + return bool(self.ptr.key_frame) + + @property def side_data(self): if self._side_data is None: diff --git a/av/video/frame.pyi b/av/video/frame.pyi index 29f961ba4..3300e8607 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -32,7 +32,6 @@ class VideoFrame(Frame): planes: tuple[VideoPlane, ...] width: int height: int - key_frame: bool interlaced_frame: bool pict_type: int colorspace: int diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 4fb463c8c..bfc74c40a 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -170,23 +170,11 @@ cdef class VideoFrame(Frame): """Width of the image, in pixels.""" return self.ptr.width - @property def height(self): """Height of the image, in pixels.""" return self.ptr.height - - @property - def key_frame(self): - """Is this frame a key frame? - - Wraps :ffmpeg:`AVFrame.key_frame`. - - """ - return self.ptr.key_frame - - @property def interlaced_frame(self): """Is this frame an interlaced or progressive? @@ -196,7 +184,6 @@ cdef class VideoFrame(Frame): """ return self.ptr.interlaced_frame - @property def pict_type(self): """One of :class:`.PictureType`. From 20a526231cd5b543a926a6a0a8e36312da86a447 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 10 Nov 2024 02:02:08 -0500 Subject: [PATCH 412/846] Use flags instead of deprecated properties --- av/frame.pyx | 2 +- av/video/frame.pyx | 7 ++----- include/libavcodec/avcodec.pxd | 10 +++++----- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/av/frame.pyx b/av/frame.pyx index 1084ca45e..57681bbcd 100644 --- a/av/frame.pyx +++ b/av/frame.pyx @@ -139,7 +139,7 @@ cdef class Frame: Wraps :ffmpeg:`AVFrame.key_frame`. """ - return bool(self.ptr.key_frame) + return bool(self.ptr.flags & lib.AV_FRAME_FLAG_KEY) @property diff --git a/av/video/frame.pyx b/av/video/frame.pyx index bfc74c40a..e5cd523b5 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -177,12 +177,9 @@ cdef class VideoFrame(Frame): @property def interlaced_frame(self): - """Is this frame an interlaced or progressive? + """Is this frame an interlaced or progressive?""" - Wraps :ffmpeg:`AVFrame.interlaced_frame`. - - """ - return self.ptr.interlaced_frame + return bool(self.ptr.flags & lib.AV_FRAME_FLAG_INTERLACED) @property def pict_type(self): diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 71121e976..d680e8b13 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -135,6 +135,9 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef enum: AV_FRAME_FLAG_CORRUPT + AV_FRAME_FLAG_KEY + AV_FRAME_FLAG_DISCARD + AV_FRAME_FLAG_INTERLACED cdef enum: FF_COMPLIANCE_VERY_STRICT @@ -368,19 +371,16 @@ cdef extern from "libavcodec/avcodec.h" nogil: uint8_t **extended_data int format # Should be AVPixelFormat or AVSampleFormat - int key_frame # 0 or 1. AVPictureType pict_type - int interlaced_frame # 0 or 1. - int width int height int nb_side_data AVFrameSideData **side_data - int nb_samples # Audio samples - int sample_rate # Audio Sample rate + int nb_samples + int sample_rate AVChannelLayout ch_layout From 2ec851344dd7c9f289578814c0685c09aaeb0416 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 12 Nov 2024 00:41:53 -0500 Subject: [PATCH 413/846] Remove package section --- docs/overview/installation.rst | 33 +-------------------------------- 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/docs/overview/installation.rst b/docs/overview/installation.rst index 1e57c8bef..419592530 100644 --- a/docs/overview/installation.rst +++ b/docs/overview/installation.rst @@ -11,37 +11,6 @@ Binary wheels are provided on PyPI for Linux, MacOS, and Windows linked against pip install av -Currently FFmpeg 6.1.1 is used with the following features enabled for all platforms: - -- fontconfig -- gmp -- libaom -- libass -- libbluray -- libdav1d -- libfreetype -- libmp3lame -- libopencore-amrnb -- libopencore-amrwb -- libopenjpeg -- libopus -- libspeex -- libtwolame -- libvorbis -- libvpx -- libx264 -- libx265 -- libxml2 -- libxvid -- lzma -- zlib - -The following additional features are also enabled on Linux: - -- gnutls -- libxcb - - Conda ----- @@ -55,7 +24,7 @@ See the `Conda quick install `_ docs t Bring your own FFmpeg --------------------- -PyAV can also be compiled against your own build of FFmpeg (version ``6.0`` or higher). You can force installing PyAV from source by running: +PyAV can also be compiled against your own build of FFmpeg (version ``7.0`` or higher). You can force installing PyAV from source by running: .. code-block:: bash From 850f1152d7dec2108dc606491c3a8d012678f55e Mon Sep 17 00:00:00 2001 From: davidplowman <38045873+davidplowman@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:20:52 +0000 Subject: [PATCH 414/846] Allow the profile of a codec to be set as well as queried The `profile` property of a stream can now be set. To help applications find appropriate profiles, a `profiles` property has been added which lists the available profile names. --- av/codec/context.pyi | 2 ++ av/codec/context.pyx | 49 ++++++++++++++++++++++++++++++++-- av/stream.pyi | 1 + av/stream.pyx | 12 +++++++++ include/libavcodec/avcodec.pxd | 15 ++++++----- tests/test_encode.py | 26 ++++++++++++++++++ 6 files changed, 96 insertions(+), 9 deletions(-) diff --git a/av/codec/context.pyi b/av/codec/context.pyi index d2336ce1d..8c50ba456 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -59,6 +59,8 @@ class CodecContext: type: Literal["video", "audio", "data", "subtitle", "attachment"] options: dict[str, str] profile: str | None + @property + def profiles(self) -> list[str]: ... extradata: bytes | None time_base: Fraction codec_tag: str diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 0170aae52..a1e95048c 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -497,10 +497,55 @@ cdef class CodecContext: def type(self): return self.codec.type + @property + def profiles(self): + """ + List the available profiles for this stream. + + :type: list[str] + """ + ret = [] + if not self.ptr.codec or not self.codec.desc or not self.codec.desc.profiles: + return ret + + # Profiles are always listed in the codec descriptor, but not necessarily in + # the codec itself. So use the descriptor here. + desc = self.codec.desc + cdef int i = 0 + while desc.profiles[i].profile != lib.FF_PROFILE_UNKNOWN: + ret.append(desc.profiles[i].name) + i += 1 + + return ret + @property def profile(self): - if self.ptr.codec and lib.av_get_profile_name(self.ptr.codec, self.ptr.profile): - return lib.av_get_profile_name(self.ptr.codec, self.ptr.profile) + if not self.ptr.codec or not self.codec.desc or not self.codec.desc.profiles: + return + + # Profiles are always listed in the codec descriptor, but not necessarily in + # the codec itself. So use the descriptor here. + desc = self.codec.desc + cdef int i = 0 + while desc.profiles[i].profile != lib.FF_PROFILE_UNKNOWN: + if desc.profiles[i].profile == self.ptr.profile: + return desc.profiles[i].name + i += 1 + + @profile.setter + def profile(self, value): + if not self.codec or not self.codec.desc or not self.codec.desc.profiles: + return + + # Profiles are always listed in the codec descriptor, but not necessarily in + # the codec itself. So use the descriptor here. + desc = self.codec.desc + cdef int i = 0 + while desc.profiles[i].profile != lib.FF_PROFILE_UNKNOWN: + if desc.profiles[i].name == value: + self.ptr.profile = desc.profiles[i].profile + return + i += 1 @property def time_base(self): diff --git a/av/stream.pyi b/av/stream.pyi index 89f49c12f..8c48f3ac7 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -19,6 +19,7 @@ class Stream: codec_context: CodecContext metadata: dict[str, str] id: int + profiles: list[str] profile: str index: int time_base: Fraction | None diff --git a/av/stream.pyx b/av/stream.pyx index 19ac8e703..4c450d283 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -145,6 +145,18 @@ cdef class Stream: else: self.ptr.id = value + @property + def profiles(self): + """ + List the available profiles for this stream. + + :type: list[str] + """ + if self.codec_context: + return self.codec_context.profiles + else: + return [] + @property def profile(self): """ diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index d680e8b13..54fb2293c 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -146,6 +146,9 @@ cdef extern from "libavcodec/avcodec.h" nogil: FF_COMPLIANCE_UNOFFICIAL FF_COMPLIANCE_EXPERIMENTAL + cdef enum: + FF_PROFILE_UNKNOWN = -99 + cdef enum AVCodecID: AV_CODEC_ID_NONE AV_CODEC_ID_MPEG2VIDEO @@ -178,12 +181,17 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef int av_codec_is_encoder(AVCodec*) cdef int av_codec_is_decoder(AVCodec*) + cdef struct AVProfile: + int profile + char *name + cdef struct AVCodecDescriptor: AVCodecID id char *name char *long_name int props char **mime_types + AVProfile *profiles AVCodecDescriptor* avcodec_descriptor_get(AVCodecID) @@ -266,13 +274,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef AVClass* avcodec_get_class() - cdef struct AVCodecDescriptor: - AVCodecID id - AVMediaType type - char *name - char *long_name - int props - cdef AVCodec* avcodec_find_decoder(AVCodecID id) cdef AVCodec* avcodec_find_encoder(AVCodecID id) diff --git a/tests/test_encode.py b/tests/test_encode.py index 11d8a4ec9..90d30dfce 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -449,3 +449,29 @@ def test_qmin_qmax(self) -> None: factor = 1.3 # insist at least 30% larger each time assert all(small * factor < large for small, large in zip(sizes, sizes[1:])) + + +class TestProfiles(TestCase): + def test_profiles(self) -> None: + """ + Test that we can set different encoder profiles. + """ + # Let's try a video and an audio codec. + file = io.BytesIO() + codecs = ( + ("h264", 30), + ("aac", 48000), + ) + + for codec_name, rate in codecs: + print("Testing:", codec_name) + container = av.open(file, mode="w", format="mp4") + stream = container.add_stream(codec_name, rate=rate) + assert len(stream.profiles) >= 1 # check that we're testing something! + + # It should be enough to test setting and retrieving the code. That means + # libav has recognised the profile and set it correctly. + for profile in stream.profiles: + stream.profile = profile + print("Set", profile, "got", stream.profile) + assert stream.profile == profile From 7a9712484a5fa5f060cfc4ef836487c2a7353aed Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 13 Nov 2024 04:05:05 -0500 Subject: [PATCH 415/846] Rethink container flags design --- av/container/core.pyi | 37 +++++++++-------- av/container/core.pyx | 97 ++++++++++--------------------------------- 2 files changed, 42 insertions(+), 92 deletions(-) diff --git a/av/container/core.pyi b/av/container/core.pyi index b031fb4b6..227a7d32a 100644 --- a/av/container/core.pyi +++ b/av/container/core.pyi @@ -1,9 +1,9 @@ +from enum import Flag from fractions import Fraction from pathlib import Path from types import TracebackType -from typing import Any, Callable, Literal, Type, overload +from typing import Any, Callable, ClassVar, Literal, Type, overload -from av.enum import EnumFlag from av.format import ContainerFormat from .input import InputContainer @@ -12,22 +12,22 @@ from .streams import StreamContainer Real = int | float | Fraction -class Flags(EnumFlag): - GENPTS: int - IGNIDX: int - NONBLOCK: int - IGNDTS: int - NOFILLIN: int - NOPARSE: int - NOBUFFER: int - CUSTOM_IO: int - DISCARD_CORRUPT: int - FLUSH_PACKETS: int - BITEXACT: int - SORT_DTS: int - FAST_SEEK: int - SHORTEST: int - AUTO_BSF: int +class Flags(Flag): + gen_pts: ClassVar[Flags] + ign_idx: ClassVar[Flags] + non_block: ClassVar[Flags] + ign_dts: ClassVar[Flags] + no_fillin: ClassVar[Flags] + no_parse: ClassVar[Flags] + no_buffer: ClassVar[Flags] + custom_io: ClassVar[Flags] + discard_corrupt: ClassVar[Flags] + flush_packets: ClassVar[Flags] + bitexact: ClassVar[Flags] + sort_dts: ClassVar[Flags] + fast_seek: ClassVar[Flags] + shortest: ClassVar[Flags] + auto_bsf: ClassVar[Flags] class Container: writeable: bool @@ -47,6 +47,7 @@ class Container: metadata: dict[str, str] open_timeout: Real | None read_timeout: Real | None + flags: int def __enter__(self) -> Container: ... def __exit__( diff --git a/av/container/core.pyx b/av/container/core.pyx index dab7c865e..563c79d21 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -3,6 +3,7 @@ from libc.stdint cimport int64_t import os import time +from enum import Flag from pathlib import Path cimport libav as lib @@ -11,7 +12,6 @@ from av.container.core cimport timeout_info from av.container.input cimport InputContainer from av.container.output cimport OutputContainer from av.container.pyio cimport pyio_close_custom_gil, pyio_close_gil -from av.enum cimport define_enum from av.error cimport err_check, stash_exception from av.format cimport build_container_format from av.utils cimport avdict_to_dict @@ -27,14 +27,12 @@ cdef object _cinit_sentinel = object() cdef object clock = getattr(time, "monotonic", time.time) cdef int interrupt_cb (void *p) noexcept nogil: - cdef timeout_info info = dereference( p) if info.timeout < 0: # timeout < 0 means no timeout return 0 cdef double current_time with gil: - current_time = clock() # Check if the clock has been changed. @@ -124,47 +122,26 @@ cdef int pyav_io_close_gil(lib.AVFormatContext *s, lib.AVIOContext *pb) noexcept return result -Flags = define_enum("Flags", __name__, ( - ("GENPTS", lib.AVFMT_FLAG_GENPTS, - "Generate missing pts even if it requires parsing future frames."), - ("IGNIDX", lib.AVFMT_FLAG_IGNIDX, - "Ignore index."), - ("NONBLOCK", lib.AVFMT_FLAG_NONBLOCK, - "Do not block when reading packets from input."), - ("IGNDTS", lib.AVFMT_FLAG_IGNDTS, - "Ignore DTS on frames that contain both DTS & PTS."), - ("NOFILLIN", lib.AVFMT_FLAG_NOFILLIN, - "Do not infer any values from other values, just return what is stored in the container."), - ("NOPARSE", lib.AVFMT_FLAG_NOPARSE, - """Do not use AVParsers, you also must set AVFMT_FLAG_NOFILLIN as the fillin code works on frames and no parsing -> no frames. - - Also seeking to frames can not work if parsing to find frame boundaries has been disabled."""), - ("NOBUFFER", lib.AVFMT_FLAG_NOBUFFER, - "Do not buffer frames when possible."), - ("CUSTOM_IO", lib.AVFMT_FLAG_CUSTOM_IO, - "The caller has supplied a custom AVIOContext, don't avio_close() it."), - ("DISCARD_CORRUPT", lib.AVFMT_FLAG_DISCARD_CORRUPT, - "Discard frames marked corrupted."), - ("FLUSH_PACKETS", lib.AVFMT_FLAG_FLUSH_PACKETS, - "Flush the AVIOContext every packet."), - ("BITEXACT", lib.AVFMT_FLAG_BITEXACT, - """When muxing, try to avoid writing any random/volatile data to the output. - - This includes any random IDs, real-time timestamps/dates, muxer version, etc. - This flag is mainly intended for testing."""), - ("SORT_DTS", lib.AVFMT_FLAG_SORT_DTS, - "Try to interleave outputted packets by dts (using this flag can slow demuxing down)."), - ("FAST_SEEK", lib.AVFMT_FLAG_FAST_SEEK, - "Enable fast, but inaccurate seeks for some formats."), - ("SHORTEST", lib.AVFMT_FLAG_SHORTEST, - "Stop muxing when the shortest stream stops."), - ("AUTO_BSF", lib.AVFMT_FLAG_AUTO_BSF, - "Add bitstream filters as requested by the muxer."), -), is_flags=True) +class Flags(Flag): + gen_pts: "Generate missing pts even if it requires parsing future frames." = lib.AVFMT_FLAG_GENPTS + ign_idx: "Ignore index." = lib.AVFMT_FLAG_IGNIDX + non_block: "Do not block when reading packets from input." = lib.AVFMT_FLAG_NONBLOCK + ign_dts: "Ignore DTS on frames that contain both DTS & PTS." = lib.AVFMT_FLAG_IGNDTS + no_fillin: "Do not infer any values from other values, just return what is stored in the container." = lib.AVFMT_FLAG_NOFILLIN + no_parse: "Do not use AVParsers, you also must set AVFMT_FLAG_NOFILLIN as the fillin code works on frames and no parsing -> no frames. Also seeking to frames can not work if parsing to find frame boundaries has been disabled." = lib.AVFMT_FLAG_NOPARSE + no_buffer: "Do not buffer frames when possible." = lib.AVFMT_FLAG_NOBUFFER + custom_io: "The caller has supplied a custom AVIOContext, don't avio_close() it." = lib.AVFMT_FLAG_CUSTOM_IO + discard_corrupt: "Discard frames marked corrupted." = lib.AVFMT_FLAG_DISCARD_CORRUPT + flush_packets: "Flush the AVIOContext every packet." = lib.AVFMT_FLAG_FLUSH_PACKETS + bitexact: "When muxing, try to avoid writing any random/volatile data to the output. This includes any random IDs, real-time timestamps/dates, muxer version, etc. This flag is mainly intended for testing." = lib.AVFMT_FLAG_BITEXACT + sort_dts: "Try to interleave outputted packets by dts (using this flag can slow demuxing down)." = lib.AVFMT_FLAG_SORT_DTS + fast_seek: "Enable fast, but inaccurate seeks for some formats." = lib.AVFMT_FLAG_FAST_SEEK + shortest: "Stop muxing when the shortest stream stops." = lib.AVFMT_FLAG_SHORTEST + auto_bsf: "Add bitstream filters as requested by the muxer." = lib.AVFMT_FLAG_AUTO_BSF -cdef class Container: +cdef class Container: def __cinit__(self, sentinel, file_, format_name, options, container_options, stream_options, metadata_encoding, metadata_errors, @@ -248,20 +225,13 @@ cdef class Container: cdef lib.AVInputFormat *ifmt cdef _Dictionary c_options if not self.writeable: - ifmt = self.format.iptr if self.format else NULL - c_options = Dictionary(self.options, self.container_options) self.set_timeout(self.open_timeout) self.start_timeout() with nogil: - res = lib.avformat_open_input( - &self.ptr, - name, - ifmt, - &c_options.ptr - ) + res = lib.avformat_open_input(&self.ptr, name, ifmt, &c_options.ptr) self.set_timeout(None) self.err_check(res) self.input_was_opened = True @@ -304,37 +274,16 @@ cdef class Container: if self.ptr == NULL: raise AssertionError("Container is not open") - def _get_flags(self): + @property + def flags(self): self._assert_open() return self.ptr.flags - def _set_flags(self, value): + @flags.setter + def flags(self, int value): self._assert_open() self.ptr.flags = value - flags = Flags.property( - _get_flags, - _set_flags, - """Flags property of :class:`.Flags`""" - ) - - gen_pts = flags.flag_property("GENPTS") - ign_idx = flags.flag_property("IGNIDX") - non_block = flags.flag_property("NONBLOCK") - ign_dts = flags.flag_property("IGNDTS") - no_fill_in = flags.flag_property("NOFILLIN") - no_parse = flags.flag_property("NOPARSE") - no_buffer = flags.flag_property("NOBUFFER") - custom_io = flags.flag_property("CUSTOM_IO") - discard_corrupt = flags.flag_property("DISCARD_CORRUPT") - flush_packets = flags.flag_property("FLUSH_PACKETS") - bit_exact = flags.flag_property("BITEXACT") - sort_dts = flags.flag_property("SORT_DTS") - fast_seek = flags.flag_property("FAST_SEEK") - shortest = flags.flag_property("SHORTEST") - auto_bsf = flags.flag_property("AUTO_BSF") - - def open( file, mode=None, From 16c25172d3b97a37f545f73c147de77885de2638 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 13 Nov 2024 05:14:21 -0500 Subject: [PATCH 416/846] Use builtin Enum class more often --- av/codec/context.pyi | 33 +++++++++++----------- av/codec/context.pyx | 38 ++++++++++++------------- av/enum.pyx | 35 ++--------------------- av/option.pyi | 6 ++-- av/option.pyx | 66 ++++++++++++++++++++++---------------------- 5 files changed, 74 insertions(+), 104 deletions(-) diff --git a/av/codec/context.pyi b/av/codec/context.pyi index 8c50ba456..8600deaaa 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -1,25 +1,26 @@ +from enum import Enum, Flag from fractions import Fraction -from typing import Any, Literal +from typing import ClassVar, Literal from av.enum import EnumFlag, EnumItem from av.packet import Packet from .codec import Codec -class ThreadType(EnumFlag): - NONE: int - FRAME: int - SLICE: int - AUTO: int +class ThreadType(Flag): + NONE: ClassVar[ThreadType] + FRAME: ClassVar[ThreadType] + SLICE: ClassVar[ThreadType] + AUTO: ClassVar[ThreadType] -class SkipType(EnumItem): - NONE: int - DEFAULT: int - NONREF: int - BIDIR: int - NONINTRA: int - NONKEY: int - ALL: int +class SkipType(Enum): + NONE: ClassVar[SkipType] + DEFAULT: ClassVar[SkipType] + NONREF: ClassVar[SkipType] + BIDIR: ClassVar[SkipType] + NONINTRA: ClassVar[SkipType] + NONKEY: ClassVar[SkipType] + ALL: ClassVar[SkipType] class Flags(EnumFlag): NONE: int @@ -67,8 +68,8 @@ class CodecContext: bit_rate: int | None bit_rate_tolerance: int thread_count: int - thread_type: Any - skip_frame: Any + thread_type: ThreadType + skip_frame: SkipType # flags unaligned: bool diff --git a/av/codec/context.pyx b/av/codec/context.pyx index a1e95048c..528a73872 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -11,6 +11,8 @@ from av.error cimport err_check from av.packet cimport Packet from av.utils cimport avrational_to_fraction, to_avrational +from enum import Enum, Flag + from av.dictionary import Dictionary @@ -39,22 +41,20 @@ cdef CodecContext wrap_codec_context(lib.AVCodecContext *c_ctx, const lib.AVCode return py_ctx -ThreadType = define_enum("ThreadType", __name__, ( - ("NONE", 0), - ("FRAME", lib.FF_THREAD_FRAME, "Decode more than one frame at once"), - ("SLICE", lib.FF_THREAD_SLICE, "Decode more than one part of a single frame at once"), - ("AUTO", lib.FF_THREAD_SLICE | lib.FF_THREAD_FRAME, "Decode using both FRAME and SLICE methods."), -), is_flags=True) +class ThreadType(Flag): + NONE = 0 + FRAME: "Decode more than one frame at once" = lib.FF_THREAD_FRAME + SLICE: "Decode more than one part of a single frame at once" = lib.FF_THREAD_SLICE + AUTO: "Decode using both FRAME and SLICE methods." = lib.FF_THREAD_SLICE | lib.FF_THREAD_FRAME -SkipType = define_enum("SkipType", __name__, ( - ("NONE", lib.AVDISCARD_NONE, "Discard nothing"), - ("DEFAULT", lib.AVDISCARD_DEFAULT, "Discard useless packets like 0 size packets in AVI"), - ("NONREF", lib.AVDISCARD_NONREF, "Discard all non reference"), - ("BIDIR", lib.AVDISCARD_BIDIR, "Discard all bidirectional frames"), - ("NONINTRA", lib.AVDISCARD_NONINTRA, "Discard all non intra frames"), - ("NONKEY", lib.AVDISCARD_NONKEY, "Discard all frames except keyframes"), - ("ALL", lib.AVDISCARD_ALL, "Discard all"), -)) +class SkipType(Enum): + NONE: "Discard nothing" = lib.AVDISCARD_NONE + DEFAULT: "Discard useless packets like 0 size packets in AVI" = lib.AVDISCARD_DEFAULT + NONREF: "Discard all non reference" = lib.AVDISCARD_NONREF + BIDIR: "Discard all bidirectional frames" = lib.AVDISCARD_BIDIR + NONINTRA: "Discard all non intra frames" = lib.AVDISCARD_NONINTRA + NONKEY: "Discard all frames except keyframes" = lib.AVDISCARD_NONKEY + ALL: "Discard all" = lib.AVDISCARD_ALL Flags = define_enum("Flags", __name__, ( ("NONE", 0), @@ -617,13 +617,13 @@ cdef class CodecContext: Wraps :ffmpeg:`AVCodecContext.thread_type`. """ - return ThreadType.get(self.ptr.thread_type, create=True) + return ThreadType(self.ptr.thread_type) @thread_type.setter def thread_type(self, value): if self.is_open: raise RuntimeError("Cannot change thread_type after codec is open.") - self.ptr.thread_type = ThreadType[value].value + self.ptr.thread_type = value.value @property def skip_frame(self): @@ -632,11 +632,11 @@ cdef class CodecContext: Wraps :ffmpeg:`AVCodecContext.skip_frame`. """ - return SkipType._get(self.ptr.skip_frame, create=True) + return SkipType(self.ptr.skip_frame) @skip_frame.setter def skip_frame(self, value): - self.ptr.skip_frame = SkipType[value].value + self.ptr.skip_frame = value.value @property def delay(self): diff --git a/av/enum.pyx b/av/enum.pyx index a21d66e81..9217e67d6 100644 --- a/av/enum.pyx +++ b/av/enum.pyx @@ -126,37 +126,6 @@ copyreg.constructor(_unpickle) cdef class EnumItem: - """ - Enumerations are when an attribute may only take on a single value at once, and - they are represented as integers in the FFmpeg API. We associate names with each - value that are easier to operate with. - - Consider :data:`av.codec.context.SkipType`, which is the type of the :attr:`CodecContext.skip_frame` attribute:: - - >>> fh = av.open(video_path) - >>> cc = fh.streams.video[0].codec_context - - >>> # The skip_frame attribute has a name and value: - >>> cc.skip_frame.name - 'DEFAULT' - >>> cc.skip_frame.value - 0 - - >>> # You can compare it to strings and ints: - >>> cc.skip_frame == 'DEFAULT' - True - >>> cc.skip_frame == 0 - True - - >>> # You can assign strings and ints: - >>> cc.skip_frame = 'NONKEY' - >>> cc.skip_frame == 'NONKEY' - True - >>> cc.skip_frame == 32 - True - - """ - cdef readonly str name cdef readonly int value cdef Py_hash_t _hash @@ -167,7 +136,7 @@ cdef class EnumItem: self.name = name self.value = value - self.__doc__ = doc # This is not cdef because it doesn't work if it is. + self.__doc__ = doc # We need to establish a hash that doesn't collide with anything that # would return true from `__eq__`. This is because these enums (vs @@ -196,7 +165,7 @@ cdef class EnumItem: def __eq__(self, other): if isinstance(other, str): - if self.name == other: # The quick method. + if self.name == other: return True try: diff --git a/av/option.pyi b/av/option.pyi index b7ba670f2..3132f4a02 100644 --- a/av/option.pyi +++ b/av/option.pyi @@ -1,6 +1,6 @@ -from .enum import EnumFlag, EnumItem +from enum import Enum, Flag -class OptionType(EnumItem): +class OptionType(Enum): FLAGS: int INT: int INT64: int @@ -20,7 +20,7 @@ class OptionType(EnumItem): CHANNEL_LAYOUT: int BOOL: int -class OptionFlags(EnumFlag): +class OptionFlags(Flag): ENCODING_PARAM: int DECODING_PARAM: int AUDIO_PARAM: int diff --git a/av/option.pyx b/av/option.pyx index 0c7b5fb28..e58c4c13f 100644 --- a/av/option.pyx +++ b/av/option.pyx @@ -1,8 +1,9 @@ cimport libav as lib -from av.enum cimport define_enum from av.utils cimport flag_in_bitfield +from enum import Enum, Flag + cdef object _cinit_sentinel = object() @@ -15,27 +16,26 @@ cdef Option wrap_option(tuple choices, const lib.AVOption *ptr): return obj -OptionType = define_enum("OptionType", __name__, ( - ("FLAGS", lib.AV_OPT_TYPE_FLAGS), - ("INT", lib.AV_OPT_TYPE_INT), - ("INT64", lib.AV_OPT_TYPE_INT64), - ("DOUBLE", lib.AV_OPT_TYPE_DOUBLE), - ("FLOAT", lib.AV_OPT_TYPE_FLOAT), - ("STRING", lib.AV_OPT_TYPE_STRING), - ("RATIONAL", lib.AV_OPT_TYPE_RATIONAL), - ("BINARY", lib.AV_OPT_TYPE_BINARY), - ("DICT", lib.AV_OPT_TYPE_DICT), - ("UINT64", lib.AV_OPT_TYPE_UINT64), - ("CONST", lib.AV_OPT_TYPE_CONST), - ("IMAGE_SIZE", lib.AV_OPT_TYPE_IMAGE_SIZE), - ("PIXEL_FMT", lib.AV_OPT_TYPE_PIXEL_FMT), - ("SAMPLE_FMT", lib.AV_OPT_TYPE_SAMPLE_FMT), - ("VIDEO_RATE", lib.AV_OPT_TYPE_VIDEO_RATE), - ("DURATION", lib.AV_OPT_TYPE_DURATION), - ("COLOR", lib.AV_OPT_TYPE_COLOR), - ("CHANNEL_LAYOUT", lib.AV_OPT_TYPE_CHLAYOUT), - ("BOOL", lib.AV_OPT_TYPE_BOOL), -)) +class OptionType(Enum): + FLAGS = lib.AV_OPT_TYPE_FLAGS + INT = lib.AV_OPT_TYPE_INT + INT64 = lib.AV_OPT_TYPE_INT64 + DOUBLE = lib.AV_OPT_TYPE_DOUBLE + FLOAT = lib.AV_OPT_TYPE_FLOAT + STRING = lib.AV_OPT_TYPE_STRING + RATIONAL = lib.AV_OPT_TYPE_RATIONAL + BINARY = lib.AV_OPT_TYPE_BINARY + DICT = lib.AV_OPT_TYPE_DICT + UINT64 = lib.AV_OPT_TYPE_UINT64 + CONST = lib.AV_OPT_TYPE_CONST + IMAGE_SIZE = lib.AV_OPT_TYPE_IMAGE_SIZE + PIXEL_FMT = lib.AV_OPT_TYPE_PIXEL_FMT + SAMPLE_FMT = lib.AV_OPT_TYPE_SAMPLE_FMT + VIDEO_RATE = lib.AV_OPT_TYPE_VIDEO_RATE + DURATION = lib.AV_OPT_TYPE_DURATION + COLOR = lib.AV_OPT_TYPE_COLOR + CHANNEL_LAYOUT = lib.AV_OPT_TYPE_CHLAYOUT + BOOL = lib.AV_OPT_TYPE_BOOL cdef tuple _INT_TYPES = ( lib.AV_OPT_TYPE_FLAGS, @@ -48,16 +48,16 @@ cdef tuple _INT_TYPES = ( lib.AV_OPT_TYPE_BOOL, ) -OptionFlags = define_enum("OptionFlags", __name__, ( - ("ENCODING_PARAM", lib.AV_OPT_FLAG_ENCODING_PARAM), - ("DECODING_PARAM", lib.AV_OPT_FLAG_DECODING_PARAM), - ("AUDIO_PARAM", lib.AV_OPT_FLAG_AUDIO_PARAM), - ("VIDEO_PARAM", lib.AV_OPT_FLAG_VIDEO_PARAM), - ("SUBTITLE_PARAM", lib.AV_OPT_FLAG_SUBTITLE_PARAM), - ("EXPORT", lib.AV_OPT_FLAG_EXPORT), - ("READONLY", lib.AV_OPT_FLAG_READONLY), - ("FILTERING_PARAM", lib.AV_OPT_FLAG_FILTERING_PARAM), -), is_flags=True) +class OptionFlags(Flag): + ENCODING_PARAM = lib.AV_OPT_FLAG_ENCODING_PARAM + DECODING_PARAM = lib.AV_OPT_FLAG_DECODING_PARAM + AUDIO_PARAM = lib.AV_OPT_FLAG_AUDIO_PARAM + VIDEO_PARAM = lib.AV_OPT_FLAG_VIDEO_PARAM + SUBTITLE_PARAM = lib.AV_OPT_FLAG_SUBTITLE_PARAM + EXPORT = lib.AV_OPT_FLAG_EXPORT + READONLY = lib.AV_OPT_FLAG_READONLY + FILTERING_PARAM = lib.AV_OPT_FLAG_FILTERING_PARAM + cdef class BaseOption: def __cinit__(self, sentinel): @@ -106,7 +106,7 @@ cdef class BaseOption: cdef class Option(BaseOption): @property def type(self): - return OptionType._get(self.ptr.type, create=True) + return OptionType(self.ptr.type) @property def offset(self): From caaddbf740c118f18f8959cd238afcb67d7d82c3 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 13 Nov 2024 17:46:43 -0500 Subject: [PATCH 417/846] Make PictureType an IntEnum --- av/video/frame.pyi | 18 +++++++++++------- av/video/frame.pyx | 32 +++++++++++++++----------------- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/av/video/frame.pyi b/av/video/frame.pyi index 3300e8607..d837ed606 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -1,9 +1,9 @@ -from typing import Any, Union +from enum import IntEnum +from typing import Any, ClassVar, Union import numpy as np from PIL import Image -from av.enum import EnumItem from av.frame import Frame from .format import VideoFormat @@ -15,7 +15,7 @@ _SupportedNDarray = Union[ np.ndarray[Any, np.dtype[np.float32]], ] -class PictureType(EnumItem): +class PictureType(IntEnum): NONE: int I: int P: int @@ -28,15 +28,19 @@ class PictureType(EnumItem): class VideoFrame(Frame): format: VideoFormat pts: int - time: float planes: tuple[VideoPlane, ...] - width: int - height: int - interlaced_frame: bool pict_type: int colorspace: int color_range: int + @property + def time(self) -> float: ... + @property + def width(self) -> int: ... + @property + def height(self) -> int: ... + @property + def interlaced_frame(self) -> bool: ... def __init__( self, width: int = 0, height: int = 0, format: str = "yuv420p" ) -> None: ... diff --git a/av/video/frame.pyx b/av/video/frame.pyx index e5cd523b5..80cf266f8 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -1,8 +1,8 @@ import sys +from enum import IntEnum from libc.stdint cimport uint8_t -from av.enum cimport define_enum from av.error cimport err_check from av.utils cimport check_ndarray from av.video.format cimport get_pix_fmt, get_video_format @@ -20,18 +20,15 @@ cdef VideoFrame alloc_video_frame(): """ return VideoFrame.__new__(VideoFrame, _cinit_bypass_sentinel) - -PictureType = define_enum("PictureType", __name__, ( - ("NONE", lib.AV_PICTURE_TYPE_NONE, "Undefined"), - ("I", lib.AV_PICTURE_TYPE_I, "Intra"), - ("P", lib.AV_PICTURE_TYPE_P, "Predicted"), - ("B", lib.AV_PICTURE_TYPE_B, "Bi-directional predicted"), - ("S", lib.AV_PICTURE_TYPE_S, "S(GMC)-VOP MPEG-4"), - ("SI", lib.AV_PICTURE_TYPE_SI, "Switching intra"), - ("SP", lib.AV_PICTURE_TYPE_SP, "Switching predicted"), - ("BI", lib.AV_PICTURE_TYPE_BI, "BI type"), -)) - +class PictureType(IntEnum): + NONE = lib.AV_PICTURE_TYPE_NONE # Undefined + I = lib.AV_PICTURE_TYPE_I # Intra + P = lib.AV_PICTURE_TYPE_P # Predicted + B = lib.AV_PICTURE_TYPE_B # Bi-directional predicted + S = lib.AV_PICTURE_TYPE_S # S(GMC)-VOP MPEG-4 + SI = lib.AV_PICTURE_TYPE_SI # Switching intra + SP = lib.AV_PICTURE_TYPE_SP # Switching predicted + BI = lib.AV_PICTURE_TYPE_BI # BI type cdef byteswap_array(array, bint big_endian): if (sys.byteorder == "big") != big_endian: @@ -183,16 +180,17 @@ cdef class VideoFrame(Frame): @property def pict_type(self): - """One of :class:`.PictureType`. + """Returns an integer that corresponds to the PictureType enum. - Wraps :ffmpeg:`AVFrame.pict_type`. + Wraps :ffmpeg:`AVFrame.pict_type` + :type: int """ - return PictureType.get(self.ptr.pict_type, create=True) + return self.ptr.pict_type @pict_type.setter def pict_type(self, value): - self.ptr.pict_type = PictureType[value].value + self.ptr.pict_type = value @property def colorspace(self): From 007368e45fa010b9a178f93984fcfd0d357f7a8d Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 13 Nov 2024 18:42:43 -0500 Subject: [PATCH 418/846] Update Sidedata Type Enums --- av/sidedata/sidedata.pyi | 59 +++++++++++++++++------------- av/sidedata/sidedata.pyx | 66 ++++++++++++++++++++-------------- include/libavcodec/avcodec.pxd | 14 ++++++-- 3 files changed, 86 insertions(+), 53 deletions(-) diff --git a/av/sidedata/sidedata.pyi b/av/sidedata/sidedata.pyi index e814bb222..d165513ab 100644 --- a/av/sidedata/sidedata.pyi +++ b/av/sidedata/sidedata.pyi @@ -1,41 +1,52 @@ from collections.abc import Mapping -from typing import Iterator, Sequence, overload +from enum import Enum +from typing import ClassVar, Iterator, Sequence, overload from av.buffer import Buffer -from av.enum import EnumItem from av.frame import Frame -class Type(EnumItem): - PANSCAN: int - A53_CC: int - STEREO3D: int - MATRIXENCODING: int - DOWNMIX_INFO: int - REPLAYGAIN: int - DISPLAYMATRIX: int - AFD: int - MOTION_VECTORS: int - SKIP_SAMPLES: int - AUDIO_SERVICE_TYPE: int - MASTERING_DISPLAY_METADATA: int - GOP_TIMECODE: int - SPHERICAL: int - CONTENT_LIGHT_LEVEL: int - ICC_PROFILE: int - SEI_UNREGISTERED: int - S12M_TIMECODE: int +class Type(Enum): + PANSCAN: ClassVar[Type] + A53_CC: ClassVar[Type] + STEREO3D: ClassVar[Type] + MATRIXENCODING: ClassVar[Type] + DOWNMIX_INFO: ClassVar[Type] + REPLAYGAIN: ClassVar[Type] + DISPLAYMATRIX: ClassVar[Type] + AFD: ClassVar[Type] + MOTION_VECTORS: ClassVar[Type] + SKIP_SAMPLES: ClassVar[Type] + AUDIO_SERVICE_TYPE: ClassVar[Type] + MASTERING_DISPLAY_METADATA: ClassVar[Type] + GOP_TIMECODE: ClassVar[Type] + SPHERICAL: ClassVar[Type] + CONTENT_LIGHT_LEVEL: ClassVar[Type] + ICC_PROFILE: ClassVar[Type] + S12M_TIMECODE: ClassVar[Type] + DYNAMIC_HDR_PLUS: ClassVar[Type] + REGIONS_OF_INTEREST: ClassVar[Type] + VIDEO_ENC_PARAMS: ClassVar[Type] + SEI_UNREGISTERED: ClassVar[Type] + FILM_GRAIN_PARAMS: ClassVar[Type] + DETECTION_BBOXES: ClassVar[Type] + DOVI_RPU_BUFFER: ClassVar[Type] + DOVI_METADATA: ClassVar[Type] + DYNAMIC_HDR_VIVID: ClassVar[Type] + AMBIENT_VIEWING_ENVIRONMENT: ClassVar[Type] + VIDEO_HINT: ClassVar[Type] class SideData(Buffer): type: Type - DISPLAYMATRIX: int class SideDataContainer(Mapping): frame: Frame def __len__(self) -> int: ... def __iter__(self) -> Iterator[SideData]: ... @overload - def __getitem__(self, key: int) -> SideData: ... + def __getitem__(self, key: str | int | Type) -> SideData: ... @overload def __getitem__(self, key: slice) -> Sequence[SideData]: ... @overload - def __getitem__(self, key: int | slice) -> SideData | Sequence[SideData]: ... + def __getitem__( + self, key: str | int | Type | slice + ) -> SideData | Sequence[SideData]: ... diff --git a/av/sidedata/sidedata.pyx b/av/sidedata/sidedata.pyx index 9b423a30b..753496fea 100644 --- a/av/sidedata/sidedata.pyx +++ b/av/sidedata/sidedata.pyx @@ -1,6 +1,5 @@ -from av.enum cimport define_enum - from collections.abc import Mapping +from enum import Enum from av.sidedata.motionvectors import MotionVectors @@ -8,26 +7,41 @@ from av.sidedata.motionvectors import MotionVectors cdef object _cinit_bypass_sentinel = object() -Type = define_enum("Type", __name__, ( - ("PANSCAN", lib.AV_FRAME_DATA_PANSCAN), - ("A53_CC", lib.AV_FRAME_DATA_A53_CC), - ("STEREO3D", lib.AV_FRAME_DATA_STEREO3D), - ("MATRIXENCODING", lib.AV_FRAME_DATA_MATRIXENCODING), - ("DOWNMIX_INFO", lib.AV_FRAME_DATA_DOWNMIX_INFO), - ("REPLAYGAIN", lib.AV_FRAME_DATA_REPLAYGAIN), - ("DISPLAYMATRIX", lib.AV_FRAME_DATA_DISPLAYMATRIX), - ("AFD", lib.AV_FRAME_DATA_AFD), - ("MOTION_VECTORS", lib.AV_FRAME_DATA_MOTION_VECTORS), - ("SKIP_SAMPLES", lib.AV_FRAME_DATA_SKIP_SAMPLES), - ("AUDIO_SERVICE_TYPE", lib.AV_FRAME_DATA_AUDIO_SERVICE_TYPE), - ("MASTERING_DISPLAY_METADATA", lib.AV_FRAME_DATA_MASTERING_DISPLAY_METADATA), - ("GOP_TIMECODE", lib.AV_FRAME_DATA_GOP_TIMECODE), - ("SPHERICAL", lib.AV_FRAME_DATA_SPHERICAL), - ("CONTENT_LIGHT_LEVEL", lib.AV_FRAME_DATA_CONTENT_LIGHT_LEVEL), - ("ICC_PROFILE", lib.AV_FRAME_DATA_ICC_PROFILE), - ("SEI_UNREGISTERED", lib.AV_FRAME_DATA_SEI_UNREGISTERED), - ("S12M_TIMECODE", lib.AV_FRAME_DATA_S12M_TIMECODE), -)) +class Type(Enum): + """ + Enum class representing different types of frame data in audio/video processing. + Values are mapped to corresponding AV_FRAME_DATA constants from FFmpeg. + + From: https://github.com/FFmpeg/FFmpeg/blob/master/libavutil/frame.h + """ + PANSCAN = lib.AV_FRAME_DATA_PANSCAN + A53_CC = lib.AV_FRAME_DATA_A53_CC + STEREO3D = lib.AV_FRAME_DATA_STEREO3D + MATRIXENCODING = lib.AV_FRAME_DATA_MATRIXENCODING + DOWNMIX_INFO = lib.AV_FRAME_DATA_DOWNMIX_INFO + REPLAYGAIN = lib.AV_FRAME_DATA_REPLAYGAIN + DISPLAYMATRIX = lib.AV_FRAME_DATA_DISPLAYMATRIX + AFD = lib.AV_FRAME_DATA_AFD + MOTION_VECTORS = lib.AV_FRAME_DATA_MOTION_VECTORS + SKIP_SAMPLES = lib.AV_FRAME_DATA_SKIP_SAMPLES + AUDIO_SERVICE_TYPE = lib.AV_FRAME_DATA_AUDIO_SERVICE_TYPE + MASTERING_DISPLAY_METADATA = lib.AV_FRAME_DATA_MASTERING_DISPLAY_METADATA + GOP_TIMECODE = lib.AV_FRAME_DATA_GOP_TIMECODE + SPHERICAL = lib.AV_FRAME_DATA_SPHERICAL + CONTENT_LIGHT_LEVEL = lib.AV_FRAME_DATA_CONTENT_LIGHT_LEVEL + ICC_PROFILE = lib.AV_FRAME_DATA_ICC_PROFILE + S12M_TIMECODE = lib.AV_FRAME_DATA_S12M_TIMECODE + DYNAMIC_HDR_PLUS = lib.AV_FRAME_DATA_DYNAMIC_HDR_PLUS + REGIONS_OF_INTEREST = lib.AV_FRAME_DATA_REGIONS_OF_INTEREST + VIDEO_ENC_PARAMS = lib.AV_FRAME_DATA_VIDEO_ENC_PARAMS + SEI_UNREGISTERED = lib.AV_FRAME_DATA_SEI_UNREGISTERED + FILM_GRAIN_PARAMS = lib.AV_FRAME_DATA_FILM_GRAIN_PARAMS + DETECTION_BBOXES = lib.AV_FRAME_DATA_DETECTION_BBOXES + DOVI_RPU_BUFFER = lib.AV_FRAME_DATA_DOVI_RPU_BUFFER + DOVI_METADATA = lib.AV_FRAME_DATA_DOVI_METADATA + DYNAMIC_HDR_VIVID = lib.AV_FRAME_DATA_DYNAMIC_HDR_VIVID + AMBIENT_VIEWING_ENVIRONMENT = lib.AV_FRAME_DATA_AMBIENT_VIEWING_ENVIRONMENT + VIDEO_HINT = lib.AV_FRAME_DATA_VIDEO_HINT cdef SideData wrap_side_data(Frame frame, int index): @@ -60,7 +74,7 @@ cdef class SideData(Buffer): @property def type(self): - return Type.get(self.ptr.type) or self.ptr.type + return Type(self.ptr.type) cdef class _SideDataContainer: @@ -85,9 +99,9 @@ cdef class _SideDataContainer: def __getitem__(self, key): if isinstance(key, int): return self._by_index[key] - - type_ = Type.get(key) - return self._by_type[type_] + if isinstance(key, str): + return self._by_type[Type[key]] + return self._by_type[key] class SideDataContainer(_SideDataContainer, Mapping): diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 54fb2293c..172c9cc65 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -354,10 +354,18 @@ cdef extern from "libavcodec/avcodec.h" nogil: AV_FRAME_DATA_SPHERICAL AV_FRAME_DATA_CONTENT_LIGHT_LEVEL AV_FRAME_DATA_ICC_PROFILE - AV_FRAME_DATA_QP_TABLE_PROPERTIES - AV_FRAME_DATA_QP_TABLE_DATA - AV_FRAME_DATA_SEI_UNREGISTERED AV_FRAME_DATA_S12M_TIMECODE + AV_FRAME_DATA_DYNAMIC_HDR_PLUS + AV_FRAME_DATA_REGIONS_OF_INTEREST + AV_FRAME_DATA_VIDEO_ENC_PARAMS + AV_FRAME_DATA_SEI_UNREGISTERED + AV_FRAME_DATA_FILM_GRAIN_PARAMS + AV_FRAME_DATA_DETECTION_BBOXES + AV_FRAME_DATA_DOVI_RPU_BUFFER + AV_FRAME_DATA_DOVI_METADATA + AV_FRAME_DATA_DYNAMIC_HDR_VIVID + AV_FRAME_DATA_AMBIENT_VIEWING_ENVIRONMENT + AV_FRAME_DATA_VIDEO_HINT cdef struct AVFrameSideData: AVFrameSideDataType type From e1aaee016e8c10b8b807dc8cd2319427a39896cb Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 13 Nov 2024 18:56:07 -0500 Subject: [PATCH 419/846] Edit stream type stub --- av/stream.pyi | 12 +++++------- av/stream.pyx | 9 ++++----- tests/test_file_probing.py | 2 ++ 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/av/stream.pyi b/av/stream.pyi index 8c48f3ac7..b17587e10 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -1,26 +1,24 @@ +from enum import Enum from fractions import Fraction -from typing import Literal +from typing import ClassVar, Literal from .codec import Codec, CodecContext from .container import Container -from .enum import EnumItem from .frame import Frame from .packet import Packet -class SideData(EnumItem): - DISPLAYMATRIX: int +class SideData(Enum): + DISPLAYMATRIX: ClassVar[SideData] class Stream: name: str | None - thread_type: Literal["NONE", "FRAME", "SLICE", "AUTO"] - container: Container codec: Codec codec_context: CodecContext metadata: dict[str, str] id: int profiles: list[str] - profile: str + profile: str | None index: int time_base: Fraction | None average_rate: Fraction | None diff --git a/av/stream.pyx b/av/stream.pyx index 4c450d283..59e1713c5 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -1,7 +1,8 @@ cimport libav as lib from libc.stdint cimport int32_t -from av.enum cimport define_enum +from enum import Enum + from av.error cimport err_check from av.packet cimport Packet from av.utils cimport ( @@ -14,12 +15,10 @@ from av.utils cimport ( cdef object _cinit_bypass_sentinel = object() - # If necessary more can be added from # https://ffmpeg.org/doxygen/trunk/group__lavc__packet.html#ga9a80bfcacc586b483a973272800edb97 -SideData = define_enum("SideData", __name__, ( - ("DISPLAYMATRIX", lib.AV_PKT_DATA_DISPLAYMATRIX, "Display Matrix"), -)) +class SideData(Enum): + DISPLAYMATRIX: "Display Matrix" = lib.AV_PKT_DATA_DISPLAYMATRIX cdef Stream wrap_stream(Container container, lib.AVStream *c_stream, CodecContext codec_context): """Build an av.Stream for an existing AVStream. diff --git a/tests/test_file_probing.py b/tests/test_file_probing.py index f71391697..ce04189f9 100644 --- a/tests/test_file_probing.py +++ b/tests/test_file_probing.py @@ -75,6 +75,7 @@ def test_container_probing(self) -> None: def test_stream_probing(self) -> None: stream = self.file.streams[0] + assert isinstance(stream, av.AudioStream) assert str(stream).startswith( " None: def test_stream_probing(self) -> None: stream = self.file.streams[0] + assert isinstance(stream, av.VideoStream) assert str(stream).startswith(" Date: Wed, 13 Nov 2024 19:57:56 -0500 Subject: [PATCH 420/846] convert Properties to enum.Flag, simplify codec property access --- av/codec/codec.pyi | 60 +++++++++++++++++++++++------------- av/codec/codec.pyx | 77 ++++++++++++++++++++-------------------------- 2 files changed, 72 insertions(+), 65 deletions(-) diff --git a/av/codec/codec.pyi b/av/codec/codec.pyi index 6cb421430..fe2d2fd41 100644 --- a/av/codec/codec.pyi +++ b/av/codec/codec.pyi @@ -1,5 +1,6 @@ +from enum import Flag from fractions import Fraction -from typing import Literal, overload +from typing import ClassVar, Literal, overload from av.audio.codeccontext import AudioCodecContext from av.audio.format import AudioFormat @@ -11,14 +12,14 @@ from av.video.format import VideoFormat from .context import CodecContext -class Properties(EnumFlag): - NONE: int - INTRA_ONLY: int - LOSSY: int - LOSSLESS: int - REORDER: int - BITMAP_SUB: int - TEXT_SUB: int +class Properties(Flag): + NONE: ClassVar[Properties] + INTRA_ONLY: ClassVar[Properties] + LOSSY: ClassVar[Properties] + LOSSLESS: ClassVar[Properties] + REORDER: ClassVar[Properties] + BITMAP_SUB: ClassVar[Properties] + TEXT_SUB: ClassVar[Properties] class Capabilities(EnumFlag): NONE: int @@ -46,25 +47,40 @@ class Capabilities(EnumFlag): class UnknownCodecError(ValueError): ... class Codec: - is_encoder: bool - is_decoder: bool + @property + def is_encoder(self) -> bool: ... + @property + def is_decoder(self) -> bool: ... descriptor: Descriptor - name: str - long_name: str - type: Literal["video", "audio", "data", "subtitle", "attachment"] - id: int + @property + def name(self) -> str: ... + @property + def long_name(self) -> str: ... + @property + def type(self) -> Literal["video", "audio", "data", "subtitle", "attachment"]: ... + @property + def id(self) -> int: ... frame_rates: list[Fraction] | None audio_rates: list[int] | None video_formats: list[VideoFormat] | None audio_formats: list[AudioFormat] | None - properties: Properties + + @property + def properties(self) -> int: ... + @property + def intra_only(self) -> bool: ... + @property + def lossy(self) -> bool: ... + @property + def lossless(self) -> bool: ... + @property + def reorder(self) -> bool: ... + @property + def bitmap_sub(self) -> bool: ... + @property + def text_sub(self) -> bool: ... + capabilities: Capabilities - intra_only: bool - lossy: bool - lossless: bool - reorder: bool - bitmap_sub: bool - text_sub: bool draw_horiz_band: bool dr1: bool hwaccel: bool diff --git a/av/codec/codec.pyx b/av/codec/codec.pyx index da66d7184..9b29cb178 100644 --- a/av/codec/codec.pyx +++ b/av/codec/codec.pyx @@ -4,9 +4,10 @@ from av.enum cimport define_enum from av.utils cimport avrational_to_fraction from av.video.format cimport get_video_format +from enum import Flag -cdef object _cinit_sentinel = object() +cdef object _cinit_sentinel = object() cdef Codec wrap_codec(const lib.AVCodec *ptr): cdef Codec codec = Codec(_cinit_sentinel) @@ -15,34 +16,14 @@ cdef Codec wrap_codec(const lib.AVCodec *ptr): codec._init() return codec - -Properties = define_enum("Properties", "av.codec", ( - ("NONE", 0), - ("INTRA_ONLY", lib.AV_CODEC_PROP_INTRA_ONLY, - """Codec uses only intra compression. - Video and audio codecs only."""), - ("LOSSY", lib.AV_CODEC_PROP_LOSSY, - """Codec supports lossy compression. Audio and video codecs only. - - Note: A codec may support both lossy and lossless - compression modes."""), - ("LOSSLESS", lib.AV_CODEC_PROP_LOSSLESS, - """Codec supports lossless compression. Audio and video codecs only."""), - ("REORDER", lib.AV_CODEC_PROP_REORDER, - """Codec supports frame reordering. That is, the coded order (the order in which - the encoded packets are output by the encoders / stored / input to the - decoders) may be different from the presentation order of the corresponding - frames. - - For codecs that do not have this property set, PTS and DTS should always be - equal."""), - ("BITMAP_SUB", lib.AV_CODEC_PROP_BITMAP_SUB, - """Subtitle codec is bitmap based - Decoded AVSubtitle data can be read from the AVSubtitleRect->pict field."""), - ("TEXT_SUB", lib.AV_CODEC_PROP_TEXT_SUB, - """Subtitle codec is text based. - Decoded AVSubtitle data can be read from the AVSubtitleRect->ass field."""), -), is_flags=True) +class Properties(Flag): + NONE = 0 + INTRA_ONLY = lib.AV_CODEC_PROP_INTRA_ONLY + LOSSY = lib.AV_CODEC_PROP_LOSSY + LOSSLESS = lib.AV_CODEC_PROP_LOSSLESS + REORDER = lib.AV_CODEC_PROP_REORDER + BITMAP_SUB = lib.AV_CODEC_PROP_BITMAP_SUB + TEXT_SUB = lib.AV_CODEC_PROP_TEXT_SUB Capabilities = define_enum("Capabilities", "av.codec", ( ("NONE", 0), @@ -287,21 +268,33 @@ cdef class Codec: i += 1 return ret - # NOTE: there are some overlaps, which we defer to how `ffmpeg -codecs` - # handles them (by prefering the capablity to the property). - # Also, LOSSLESS and LOSSY don't have to agree. - - @Properties.property + @property def properties(self): - """Flag property of :class:`.Properties`""" return self.desc.props - intra_only = properties.flag_property("INTRA_ONLY") - lossy = properties.flag_property("LOSSY") # Defer to capability. - lossless = properties.flag_property("LOSSLESS") # Defer to capability. - reorder = properties.flag_property("REORDER") - bitmap_sub = properties.flag_property("BITMAP_SUB") - text_sub = properties.flag_property("TEXT_SUB") + @property + def intra_only(self): + return bool(self.desc.props & lib.AV_CODEC_PROP_INTRA_ONLY) + + @property + def lossy(self): + return bool(self.desc.props & lib.AV_CODEC_PROP_LOSSY) + + @property + def lossless(self): + return bool(self.desc.props & lib.AV_CODEC_PROP_LOSSLESS) + + @property + def reorder(self): + return bool(self.desc.props & lib.AV_CODEC_PROP_REORDER) + + @property + def bitmap_sub(self): + return bool(self.desc.props & lib.AV_CODEC_PROP_BITMAP_SUB) + + @property + def text_sub(self): + return bool(self.desc.props & lib.AV_CODEC_PROP_TEXT_SUB) @Capabilities.property def capabilities(self): @@ -324,8 +317,6 @@ cdef class Codec: auto_threads = capabilities.flag_property("AUTO_THREADS") variable_frame_size = capabilities.flag_property("VARIABLE_FRAME_SIZE") avoid_probing = capabilities.flag_property("AVOID_PROBING") - # intra_only = capabilities.flag_property("INTRA_ONLY") # Dupes. - # lossless = capabilities.flag_property("LOSSLESS") # Dupes. hardware = capabilities.flag_property("HARDWARE") hybrid = capabilities.flag_property("HYBRID") encoder_reordered_opaque = capabilities.flag_property("ENCODER_REORDERED_OPAQUE") From 8bf5d03ca7b479ba1ed45d9e0d0600d99532b85d Mon Sep 17 00:00:00 2001 From: David Plowman Date: Wed, 13 Nov 2024 16:16:05 +0000 Subject: [PATCH 421/846] Make VideoFrame.from_numpy_buffer support buffers with padding Some devices have hardware that creates image buffers with padding, so adding support here means less frame buffer copying is required. Specifically, we extend the support to buffers where the pixel rows are contiguous, though the image doesn't comprise all the pixels on the row (and is therefore not strictly contiguous). We also support yuv420p images with padding. These have padding in the middle of the UV rows as well as at the end, so can't be trimmed by the application before being passed in. Instead, the true image width must be passed. Tests are also added to ensure all these cases now avoid copying. --- av/video/frame.pyi | 2 +- av/video/frame.pyx | 50 ++++++---- tests/test_videoframe.py | 201 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 236 insertions(+), 17 deletions(-) diff --git a/av/video/frame.pyi b/av/video/frame.pyi index d837ed606..0739010c1 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -62,7 +62,7 @@ class VideoFrame(Frame): def from_image(img: Image.Image) -> VideoFrame: ... @staticmethod def from_numpy_buffer( - array: _SupportedNDarray, format: str = "rgb24" + array: _SupportedNDarray, format: str = "rgb24", width: int = 0 ) -> VideoFrame: ... @staticmethod def from_ndarray(array: _SupportedNDarray, format: str = "rgb24") -> VideoFrame: ... diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 80cf266f8..862db8513 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -374,31 +374,54 @@ cdef class VideoFrame(Frame): return frame @staticmethod - def from_numpy_buffer(array, format="rgb24"): + def from_numpy_buffer(array, format="rgb24", width=0): + # Usually the width of the array is the same as the width of the image. But sometimes + # this is not possible, for example with yuv420p images that have padding. These are + # awkward because the UV rows at the bottom have padding bytes in the middle of the + # row as well as at the end. To cope with these, callers need to be able to pass the + # actual width to us. + height = array.shape[0] + if not width: + width = array.shape[1] + if format in ("rgb24", "bgr24"): check_ndarray(array, "uint8", 3) check_ndarray_shape(array, array.shape[2] == 3) - height, width = array.shape[:2] + if array.strides[1:] != (3, 1): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = (array.strides[0], ) + elif format in ("rgba", "bgra"): + check_ndarray(array, "uint8", 3) + check_ndarray_shape(array, array.shape[2] == 4) + if array.strides[1:] != (4, 1): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = (array.strides[0], ) elif format in ("gray", "gray8", "rgb8", "bgr8"): check_ndarray(array, "uint8", 2) - height, width = array.shape[:2] + if array.strides[1] != 1: + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = (array.strides[0], ) elif format in ("yuv420p", "yuvj420p", "nv12"): check_ndarray(array, "uint8", 2) check_ndarray_shape(array, array.shape[0] % 3 == 0) check_ndarray_shape(array, array.shape[1] % 2 == 0) - height, width = array.shape[:2] height = height // 6 * 4 + if array.strides[1] != 1: + raise ValueError("provided array does not have C_CONTIGUOUS rows") + if format in ("yuv420p", "yuvj420p"): + # For YUV420 planar formats, the UV plane stride is always half the Y stride. + linesizes = (array.strides[0], array.strides[0] // 2, array.strides[0] // 2) + else: + # Planes where U and V are interleaved have the same stride as Y. + linesizes = (array.strides[0], array.strides[0]) else: raise ValueError(f"Conversion from numpy array with format `{format}` is not yet supported") - if not array.flags["C_CONTIGUOUS"]: - raise ValueError("provided array must be C_CONTIGUOUS") - frame = alloc_video_frame() - frame._image_fill_pointers_numpy(array, width, height, format) + frame._image_fill_pointers_numpy(array, width, height, linesizes, format) return frame - def _image_fill_pointers_numpy(self, buffer, width, height, format): + def _image_fill_pointers_numpy(self, buffer, width, height, linesizes, format): cdef lib.AVPixelFormat c_format cdef uint8_t * c_ptr cdef size_t c_data @@ -433,13 +456,8 @@ cdef class VideoFrame(Frame): self.ptr.format = c_format self.ptr.width = width self.ptr.height = height - res = lib.av_image_fill_linesizes( - self.ptr.linesize, - self.ptr.format, - width, - ) - if res: - err_check(res) + for i, linesize in enumerate(linesizes): + self.ptr.linesize[i] = linesize res = lib.av_image_fill_pointers( self.ptr.data, diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 6396f1f45..32b6e5482 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -528,6 +528,19 @@ def test_shares_memory_gray() -> None: # Make sure the frame reflects that assertNdarraysEqual(frame.to_ndarray(), array) + # repeat the test, but with an array that is not fully contiguous, though the + # pixels in a row are + array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + array = array[:, :300] + assert not array.data.c_contiguous + frame = VideoFrame.from_numpy_buffer(array, "gray") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + def test_shares_memory_gray8() -> None: array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) @@ -539,6 +552,19 @@ def test_shares_memory_gray8() -> None: # Make sure the frame reflects that assertNdarraysEqual(frame.to_ndarray(), array) + # repeat the test, but with an array that is not fully contiguous, though the + # pixels in a row are + array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + array = array[:, :300] + assert not array.data.c_contiguous + frame = VideoFrame.from_numpy_buffer(array, "gray8") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + def test_shares_memory_rgb8() -> None: array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) @@ -550,6 +576,19 @@ def test_shares_memory_rgb8() -> None: # Make sure the frame reflects that assertNdarraysEqual(frame.to_ndarray(), array) + # repeat the test, but with an array that is not fully contiguous, though the + # pixels in a row are + array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + array = array[:, :300] + assert not array.data.c_contiguous + frame = VideoFrame.from_numpy_buffer(array, "rgb8") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + def test_shares_memory_bgr8() -> None: array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) @@ -561,6 +600,19 @@ def test_shares_memory_bgr8() -> None: # Make sure the frame reflects that assertNdarraysEqual(frame.to_ndarray(), array) + # repeat the test, but with an array that is not fully contiguous, though the + # pixels in a row are + array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + array = array[:, :300] + assert not array.data.c_contiguous + frame = VideoFrame.from_numpy_buffer(array, "bgr8") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + def test_shares_memory_rgb24() -> None: array = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8) @@ -572,6 +624,43 @@ def test_shares_memory_rgb24() -> None: # Make sure the frame reflects that assertNdarraysEqual(frame.to_ndarray(), array) + # repeat the test, but with an array that is not fully contiguous, though the + # pixels in a row are + array = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8) + array = array[:, :300, :] + assert not array.data.c_contiguous + frame = VideoFrame.from_numpy_buffer(array, "rgb24") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_shares_memory_rgba() -> None: + array = numpy.random.randint(0, 256, size=(357, 318, 4), dtype=numpy.uint8) + frame = VideoFrame.from_numpy_buffer(array, "rgba") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=(357, 318, 4), dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + + # repeat the test, but with an array that is not fully contiguous, though the + # pixels in a row are + array = numpy.random.randint(0, 256, size=(357, 318, 4), dtype=numpy.uint8) + array = array[:, :300, :] + assert not array.data.c_contiguous + frame = VideoFrame.from_numpy_buffer(array, "rgba") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + def test_shares_memory_yuv420p() -> None: array = numpy.random.randint(0, 256, size=(512 * 6 // 4, 256), dtype=numpy.uint8) @@ -583,6 +672,38 @@ def test_shares_memory_yuv420p() -> None: # Make sure the frame reflects that assertNdarraysEqual(frame.to_ndarray(), array) + # repeat the test, but with an array where there are some padding bytes + # note that the uv rows have half the padding in the middle of a row, and the + # other half at the end + height = 512 + stride = 256 + width = 200 + array = numpy.random.randint( + 0, 256, size=(height * 6 // 4, stride), dtype=numpy.uint8 + ) + uv_width = width // 2 + uv_stride = stride // 2 + + # compare carefully, avoiding all the padding bytes which to_ndarray strips out + frame = VideoFrame.from_numpy_buffer(array, "yuv420p", width=width) + frame_array = frame.to_ndarray() + assertNdarraysEqual(frame_array[:height, :width], array[:height, :width]) + assertNdarraysEqual(frame_array[height:, :uv_width], array[height:, :uv_width]) + assertNdarraysEqual( + frame_array[height:, uv_width:], + array[height:, uv_stride : uv_stride + uv_width], + ) + + # overwrite the array, and check the shared frame buffer changed too! + array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) + frame_array = frame.to_ndarray() + assertNdarraysEqual(frame_array[:height, :width], array[:height, :width]) + assertNdarraysEqual(frame_array[height:, :uv_width], array[height:, :uv_width]) + assertNdarraysEqual( + frame_array[height:, uv_width:], + array[height:, uv_stride : uv_stride + uv_width], + ) + def test_shares_memory_yuvj420p() -> None: array = numpy.random.randint(0, 256, size=(512 * 6 // 4, 256), dtype=numpy.uint8) @@ -594,6 +715,36 @@ def test_shares_memory_yuvj420p() -> None: # Make sure the frame reflects that assertNdarraysEqual(frame.to_ndarray(), array) + # repeat the test with padding, just as we did in the yuv420p case + height = 512 + stride = 256 + width = 200 + array = numpy.random.randint( + 0, 256, size=(height * 6 // 4, stride), dtype=numpy.uint8 + ) + uv_width = width // 2 + uv_stride = stride // 2 + + # compare carefully, avoiding all the padding bytes which to_ndarray strips out + frame = VideoFrame.from_numpy_buffer(array, "yuvj420p", width=width) + frame_array = frame.to_ndarray() + assertNdarraysEqual(frame_array[:height, :width], array[:height, :width]) + assertNdarraysEqual(frame_array[height:, :uv_width], array[height:, :uv_width]) + assertNdarraysEqual( + frame_array[height:, uv_width:], + array[height:, uv_stride : uv_stride + uv_width], + ) + + # overwrite the array, and check the shared frame buffer changed too! + array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) + frame_array = frame.to_ndarray() + assertNdarraysEqual(frame_array[:height, :width], array[:height, :width]) + assertNdarraysEqual(frame_array[height:, :uv_width], array[height:, :uv_width]) + assertNdarraysEqual( + frame_array[height:, uv_width:], + array[height:, uv_stride : uv_stride + uv_width], + ) + def test_shares_memory_nv12() -> None: array = numpy.random.randint(0, 256, size=(512 * 6 // 4, 256), dtype=numpy.uint8) @@ -605,6 +756,19 @@ def test_shares_memory_nv12() -> None: # Make sure the frame reflects that assertNdarraysEqual(frame.to_ndarray(), array) + # repeat the test, but with an array that is not fully contiguous, though the + # pixels in a row are + array = numpy.random.randint(0, 256, size=(512 * 6 // 4, 256), dtype=numpy.uint8) + array = array[:, :200] + assert not array.data.c_contiguous + frame = VideoFrame.from_numpy_buffer(array, "nv12") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + def test_shares_memory_bgr24() -> None: array = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8) @@ -616,6 +780,43 @@ def test_shares_memory_bgr24() -> None: # Make sure the frame reflects that assertNdarraysEqual(frame.to_ndarray(), array) + # repeat the test, but with an array that is not fully contiguous, though the + # pixels in a row are + array = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8) + array = array[:, :300, :] + assert not array.data.c_contiguous + frame = VideoFrame.from_numpy_buffer(array, "bgr24") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_shares_memory_bgra() -> None: + array = numpy.random.randint(0, 256, size=(357, 318, 4), dtype=numpy.uint8) + frame = VideoFrame.from_numpy_buffer(array, "bgra") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=(357, 318, 4), dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + + # repeat the test, but with an array that is not fully contiguous, though the + # pixels in a row are + array = numpy.random.randint(0, 256, size=(357, 318, 4), dtype=numpy.uint8) + array = array[:, :300, :] + assert not array.data.c_contiguous + frame = VideoFrame.from_numpy_buffer(array, "bgra") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + def test_reformat_pts() -> None: frame = VideoFrame(640, 480, "rgb24") From 12a2bdcaafa39c6d33e8b3d2ec8ec9c291be8f5d Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 16 Nov 2024 03:34:00 -0500 Subject: [PATCH 422/846] Add `add_data_stream()` method --- av/container/output.pyi | 11 +++++-- av/container/output.pyx | 67 +++++++++++++++++++++++++++++++++++++++-- tests/test_streams.py | 38 +++++++++++++++++++++++ 3 files changed, 111 insertions(+), 5 deletions(-) diff --git a/av/container/output.pyi b/av/container/output.pyi index 97b67ee71..a7c89452f 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -1,14 +1,16 @@ from fractions import Fraction -from typing import Literal, Sequence, TypeVar, overload +from typing import Literal, Sequence, TypeVar, Union, overload from av.audio.stream import AudioStream +from av.data.stream import DataStream from av.packet import Packet from av.stream import Stream +from av.subtitles.stream import SubtitleStream from av.video.stream import VideoStream from .core import Container -_StreamT = TypeVar("_StreamT", bound=Stream, default=Stream) +_StreamT = TypeVar("_StreamT", bound=Union[VideoStream, AudioStream, SubtitleStream]) class OutputContainer(Container): def __enter__(self) -> OutputContainer: ... @@ -35,8 +37,11 @@ class OutputContainer(Container): rate: Fraction | int | None = None, options: dict[str, str] | None = None, **kwargs, - ) -> Stream: ... + ) -> VideoStream | AudioStream | SubtitleStream: ... def add_stream_from_template(self, template: _StreamT, **kwargs) -> _StreamT: ... + def add_data_stream( + self, codec_name: str | None = None, options: dict[str, str] | None = None + ) -> DataStream: ... def start_encoding(self) -> None: ... def close(self) -> None: ... def mux(self, packets: Packet | Sequence[Packet]) -> None: ... diff --git a/av/container/output.pyx b/av/container/output.pyx index 3e4c9dbb4..a07bc79e8 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -47,6 +47,7 @@ cdef class OutputContainer(Container): """add_stream(codec_name, rate=None) Creates a new stream from a codec name and returns it. + Supports video, audio, and subtitle streams. :param codec_name: The name of a codec. :type codec_name: str | Codec @@ -137,7 +138,7 @@ cdef class OutputContainer(Container): def add_stream_from_template(self, Stream template not None, **kwargs): """ - Creates a new stream from a template. + Creates a new stream from a template. Supports video, audio, and subtitle streams. :param template: Copy codec from another :class:`~av.stream.Stream` instance. :param \\**kwargs: Set attributes for the stream. @@ -192,6 +193,65 @@ cdef class OutputContainer(Container): return py_stream + + def add_data_stream(self, codec_name=None, dict options=None): + """add_data_stream(codec_name=None) + + Creates a new data stream and returns it. + + :param codec_name: Optional name of the data codec (e.g. 'klv') + :type codec_name: str | None + :param dict options: Stream options. + :rtype: The new :class:`~av.data.stream.DataStream`. + """ + cdef const lib.AVCodec *codec = NULL + + if codec_name is not None: + codec = lib.avcodec_find_encoder_by_name(codec_name.encode()) + if codec == NULL: + raise ValueError(f"Unknown data codec: {codec_name}") + + # Assert that this format supports the requested codec + if not lib.avformat_query_codec(self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL): + raise ValueError( + f"{self.format.name!r} format does not support {codec_name!r} codec" + ) + + # Create new stream in the AVFormatContext + cdef lib.AVStream *stream = lib.avformat_new_stream(self.ptr, codec) + if stream == NULL: + raise MemoryError("Could not allocate stream") + + # Set up codec context if we have a codec + cdef lib.AVCodecContext *codec_context = NULL + if codec != NULL: + codec_context = lib.avcodec_alloc_context3(codec) + if codec_context == NULL: + raise MemoryError("Could not allocate codec context") + + # Some formats want stream headers to be separate + if self.ptr.oformat.flags & lib.AVFMT_GLOBALHEADER: + codec_context.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER + + # Initialize stream codec parameters + err_check(lib.avcodec_parameters_from_context(stream.codecpar, codec_context)) + else: + # For raw data streams, just set the codec type + stream.codecpar.codec_type = lib.AVMEDIA_TYPE_DATA + + # Construct the user-land stream + cdef CodecContext py_codec_context = None + if codec_context != NULL: + py_codec_context = wrap_codec_context(codec_context, codec) + + cdef Stream py_stream = wrap_stream(self, stream, py_codec_context) + self.streams.add_stream(py_stream) + + if options: + py_stream.options.update(options) + + return py_stream + cpdef start_encoding(self): """Write the file header! Called automatically.""" @@ -206,8 +266,11 @@ cdef class OutputContainer(Container): cdef Stream stream for stream in self.streams: ctx = stream.codec_context + # Skip codec context handling for data streams without codecs if ctx is None: - raise ValueError(f"Stream {stream.index} has no codec context") + if stream.type != "data": + raise ValueError(f"Stream {stream.index} has no codec context") + continue if not ctx.is_open: for k, v in self.options.items(): diff --git a/tests/test_streams.py b/tests/test_streams.py index 045745543..b1f2bd12f 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -1,3 +1,5 @@ +from fractions import Fraction + import av from .common import fate_suite @@ -78,6 +80,42 @@ def test_printing_video_stream2(self) -> None: container.close() input_.close() + def test_data_stream(self) -> None: + # First test writing and reading a simple data stream + container1 = av.open("data.ts", "w") + data_stream = container1.add_data_stream() + + test_data = [b"test data 1", b"test data 2", b"test data 3"] + for i, data_ in enumerate(test_data): + packet = av.Packet(data_) + packet.pts = i + packet.stream = data_stream + container1.mux(packet) + container1.close() + + # Test reading back the data stream + container = av.open("data.ts") + + # Test best stream selection + data = container.streams.best("data") + assert data == container.streams.data[0] + + # Test get method + assert [data] == container.streams.get(data=0) + assert [data] == container.streams.get(data=(0,)) + + # Verify we can read back all the packets, ignoring empty ones + packets = [p for p in container.demux(data) if bytes(p)] + assert len(packets) == len(test_data) + for packet, original_data in zip(packets, test_data): + assert bytes(packet) == original_data + + # Test string representation + repr = f"{data_stream}" + assert repr.startswith("") + + container.close() + # def test_side_data(self) -> None: # container = av.open(fate_suite("mov/displaymatrix.mov")) # video = container.streams.video[0] From 75ff931b92cfcee77a1f9b86f611de2bbdc4bf80 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 18 Nov 2024 02:50:54 -0500 Subject: [PATCH 423/846] Skip qminmax if not x264 --- tests/test_encode.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_encode.py b/tests/test_encode.py index 90d30dfce..0848e9b1a 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -399,6 +399,10 @@ def encode_frames_with_qminmax(frames: list, shape: tuple, qminmax: tuple) -> in Returns: total length of the encoded bytes. """ + + if av.codec.Codec("h264", "w").name != "libx264": + pytest.skip() + file = io.BytesIO() container = av.open(file, mode="w", format="mp4") stream = container.add_stream("h264", rate=30) From 48e72b69022911d7c04e37af72adefeb2aeff965 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 18 Nov 2024 13:33:47 -0500 Subject: [PATCH 424/846] Fix thread_type setter --- av/about.py | 2 +- av/codec/context.pyi | 2 ++ av/codec/context.pyx | 13 ++++++++----- av/video/stream.pyi | 7 ++++--- data.ts | Bin 0 -> 1128 bytes tests/test_streams.py | 10 ++++++++++ 6 files changed, 25 insertions(+), 9 deletions(-) create mode 100644 data.ts diff --git a/av/about.py b/av/about.py index a3d32212b..73437bbc8 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "14.0.0rc1" +__version__ = "14.0.0rc2" diff --git a/av/codec/context.pyi b/av/codec/context.pyi index 8600deaaa..8b7458597 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -12,6 +12,8 @@ class ThreadType(Flag): FRAME: ClassVar[ThreadType] SLICE: ClassVar[ThreadType] AUTO: ClassVar[ThreadType] + def __get__(self, i: object | None, owner: type | None = None) -> ThreadType: ... + def __set__(self, instance: object, value: int | str | ThreadType) -> None: ... class SkipType(Enum): NONE: ClassVar[SkipType] diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 528a73872..6e039a4e1 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -138,10 +138,8 @@ cdef class CodecContext: self.codec = wrap_codec(codec if codec != NULL else self.ptr.codec) # Set reasonable threading defaults. - # count == 0 -> use as many threads as there are CPUs. - # type == 2 -> thread within a frame. This does not change the API. - self.ptr.thread_count = 0 - self.ptr.thread_type = 2 + self.ptr.thread_count = 0 # use as many threads as there are CPUs. + self.ptr.thread_type = 0x02 # thread within a frame. Does not change the API. def _get_flags(self): return self.ptr.flags @@ -623,7 +621,12 @@ cdef class CodecContext: def thread_type(self, value): if self.is_open: raise RuntimeError("Cannot change thread_type after codec is open.") - self.ptr.thread_type = value.value + if type(value) is int: + self.ptr.thread_type = value + elif type(value) is str: + self.ptr.thread_type = ThreadType[value].value + else: + self.ptr.thread_type = value.value @property def skip_frame(self): diff --git a/av/video/stream.pyi b/av/video/stream.pyi index f0cdd5eb4..dd670d3cf 100644 --- a/av/video/stream.pyi +++ b/av/video/stream.pyi @@ -1,6 +1,7 @@ from fractions import Fraction -from typing import Any, Iterator, Literal +from typing import Iterator, Literal +from av.codec.context import ThreadType from av.packet import Packet from av.stream import Stream @@ -12,8 +13,6 @@ class VideoStream(Stream): bit_rate: int | None max_bit_rate: int | None bit_rate_tolerance: int - thread_count: int - thread_type: Any sample_aspect_ratio: Fraction | None display_aspect_ratio: Fraction | None codec_context: VideoCodecContext @@ -24,6 +23,8 @@ class VideoStream(Stream): # from codec context format: VideoFormat + thread_count: int + thread_type: ThreadType width: int height: int bits_per_coded_sample: int diff --git a/data.ts b/data.ts new file mode 100644 index 0000000000000000000000000000000000000000..a33b8b31531f8b173bfb0b5d0098a72173fe9d19 GIT binary patch literal 1128 zcmZ>F5ENi=`k>0dc#whNKjVJ}#y<@r9zu+4Zf>~+sp*`-sYPX($*Bg0~;pm4-8rxSvCzWC*pLCdjQbwOdEv2Zhy$|fq@M~?5kf8gj36)k#Kip zFqj7nc?O0$hC$;fREIDC(=mf^O9QJS10w@tNosM4LP}yuqJkmN?S>Gy{~Ho+XGXZ) T2 None: fate_suite("amv/MTV_high_res_320x240_sample_Penguin_Joke_MTV_from_WMV.amv") ) video = container.streams.video[0] + + video.thread_type = av.codec.context.ThreadType.AUTO + assert video.thread_type == av.codec.context.ThreadType.AUTO + + video.thread_type = 0x03 + assert video.thread_type == av.codec.context.ThreadType.AUTO + + video.thread_type = "AUTO" + assert video.thread_type == av.codec.context.ThreadType.AUTO + audio = container.streams.audio[0] assert [video] == container.streams.get(video=0) From c29cb8699fd45f33b3aea82e84f686491b5e1b39 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 18 Nov 2024 14:12:46 -0500 Subject: [PATCH 425/846] Clean up data.ts --- data.ts | Bin 1128 -> 0 bytes tests/test_streams.py | 10 ++++++++++ 2 files changed, 10 insertions(+) delete mode 100644 data.ts diff --git a/data.ts b/data.ts deleted file mode 100644 index a33b8b31531f8b173bfb0b5d0098a72173fe9d19..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1128 zcmZ>F5ENi=`k>0dc#whNKjVJ}#y<@r9zu+4Zf>~+sp*`-sYPX($*Bg0~;pm4-8rxSvCzWC*pLCdjQbwOdEv2Zhy$|fq@M~?5kf8gj36)k#Kip zFqj7nc?O0$hC$;fREIDC(=mf^O9QJS10w@tNosM4LP}yuqJkmN?S>Gy{~Ho+XGXZ) T2 None: for fate_name in ("h264/interlaced_crop.mp4",): container = av.open(fate_suite(fate_name)) From ce48bfa4b8afde021a418d79dad450d5b3b4b96e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 19 Nov 2024 03:00:50 -0500 Subject: [PATCH 426/846] time_base cannot be None --- av/container/output.pyx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/av/container/output.pyx b/av/container/output.pyx index a07bc79e8..1227c4340 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -1,5 +1,6 @@ import logging import os +from fractions import Fraction cimport libav as lib @@ -186,7 +187,7 @@ cdef class OutputContainer(Container): elif template.type == "audio": py_stream.time_base = kwargs.pop("time_base", 1 / template.rate) else: - py_stream.time_base = kwargs.pop("time_base", None) + py_stream.time_base = kwargs.pop("time_base", Fraction(0, 1)) for k, v in kwargs.items(): setattr(py_stream, k, v) From 25edc6493ea81599d140053d53d7b9dad06f1fa5 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 19 Nov 2024 03:07:52 -0500 Subject: [PATCH 427/846] Set min MacOS to 11.0 --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b51477083..92bff5f28 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -56,7 +56,7 @@ jobs: - name: Set Minimum MacOS Target if: matrix.os == 'macos-13' || matrix.os == 'macos-14' run: | - echo "MACOSX_DEPLOYMENT_TARGET=10.13" >> $GITHUB_ENV + echo "MACOSX_DEPLOYMENT_TARGET=11.0" >> $GITHUB_ENV - name: Build wheels env: CIBW_ARCHS: ${{ matrix.arch }} From 74f6aec8e9d639785976c7e6108349780673d3d1 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 19 Nov 2024 20:37:10 -0500 Subject: [PATCH 428/846] Test subtitle muxing --- av/container/output.pyx | 20 +++++++------------- tests/test_encode.py | 23 +++++++++++++++++++++++ tests/test_streams.py | 7 ------- 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/av/container/output.pyx b/av/container/output.pyx index 1227c4340..8fad652d4 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -144,14 +144,15 @@ cdef class OutputContainer(Container): :param template: Copy codec from another :class:`~av.stream.Stream` instance. :param \\**kwargs: Set attributes for the stream. :rtype: The new :class:`~av.stream.Stream`. - """ + cdef const lib.AVCodec *codec + cdef Codec codec_obj - if not template.codec_context: - raise ValueError("template has no codec context") - - cdef Codec codec_obj = Codec(template.codec_context.codec.name, "w") - cdef const lib.AVCodec *codec = codec_obj.ptr + if template.type == "subtitle": + codec_obj = template.codec_context.codec + else: + codec_obj = Codec(template.codec_context.codec.name, "w") + codec = codec_obj.ptr # Assert that this format supports the requested codec. if not lib.avformat_query_codec(self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL): @@ -182,13 +183,6 @@ cdef class OutputContainer(Container): cdef Stream py_stream = wrap_stream(self, stream, py_codec_context) self.streams.add_stream(py_stream) - if template.type == "video": - py_stream.time_base = kwargs.pop("time_base", 1 / template.average_rate) - elif template.type == "audio": - py_stream.time_base = kwargs.pop("time_base", 1 / template.rate) - else: - py_stream.time_base = kwargs.pop("time_base", Fraction(0, 1)) - for k, v in kwargs.items(): setattr(py_stream, k, v) diff --git a/tests/test_encode.py b/tests/test_encode.py index 0848e9b1a..c107aa044 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -222,6 +222,29 @@ def test_transcode(self) -> None: assert stream.sample_rate == sample_rate +class TestSubtitleEncoding: + def test_subtitle_muxing(self) -> None: + input_ = av.open(fate_suite("sub/MovText_capability_tester.mp4")) + in_stream = input_.streams.subtitles[0] + + output_bytes = io.BytesIO() + output = av.open(output_bytes, "w", format="mp4") + + out_stream = output.add_stream_from_template(in_stream) + + for packet in input_.demux(in_stream): + if packet.dts is None: + continue + packet.stream = out_stream + output.mux(packet) + + output.close() + output_bytes.seek(0) + assert output_bytes.getvalue().startswith( + b"\x00\x00\x00\x1cftypisom\x00\x00\x02\x00isomiso2mp41\x00\x00\x00\x08free" + ) + + class TestEncodeStreamSemantics(TestCase): def test_stream_index(self) -> None: with av.open(self.sandboxed("output.mov"), "w") as output: diff --git a/tests/test_streams.py b/tests/test_streams.py index ad8be2a62..b7699e622 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -135,10 +135,3 @@ def test_data_stream(self) -> None: assert repr.startswith("") container.close() - - # def test_side_data(self) -> None: - # container = av.open(fate_suite("mov/displaymatrix.mov")) - # video = container.streams.video[0] - - # assert video.nb_side_data == 1 - # assert video.side_data["DISPLAYMATRIX"] == -90.0 From ad99283e977c7bbae803e0abba45f53ef963f233 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 20 Nov 2024 20:28:22 -0500 Subject: [PATCH 429/846] Add function for getting loudnorm stats Getting these stats is otherwise impossible. --- av/filter/loudnorm.pxd | 4 + av/filter/loudnorm.pyi | 3 + av/filter/loudnorm.pyx | 63 ++++++++++++ av/filter/loudnorm_impl.c | 211 ++++++++++++++++++++++++++++++++++++++ av/filter/loudnorm_impl.h | 12 +++ setup.py | 28 ++++- tests/test_streams.py | 11 ++ 7 files changed, 331 insertions(+), 1 deletion(-) create mode 100644 av/filter/loudnorm.pxd create mode 100644 av/filter/loudnorm.pyi create mode 100644 av/filter/loudnorm.pyx create mode 100644 av/filter/loudnorm_impl.c create mode 100644 av/filter/loudnorm_impl.h diff --git a/av/filter/loudnorm.pxd b/av/filter/loudnorm.pxd new file mode 100644 index 000000000..b08d3502f --- /dev/null +++ b/av/filter/loudnorm.pxd @@ -0,0 +1,4 @@ +from av.audio.stream cimport AudioStream + + +cpdef bytes stats(str loudnorm_args, AudioStream stream) diff --git a/av/filter/loudnorm.pyi b/av/filter/loudnorm.pyi new file mode 100644 index 000000000..c680f638d --- /dev/null +++ b/av/filter/loudnorm.pyi @@ -0,0 +1,3 @@ +from av.audio.stream import AudioStream + +def stats(loudnorm_args: str, stream: AudioStream) -> bytes: ... diff --git a/av/filter/loudnorm.pyx b/av/filter/loudnorm.pyx new file mode 100644 index 000000000..662bbd230 --- /dev/null +++ b/av/filter/loudnorm.pyx @@ -0,0 +1,63 @@ +# av/filter/loudnorm.pyx + +cimport libav as lib +from cpython.bytes cimport PyBytes_FromString +from libc.stdlib cimport free + +from av.audio.codeccontext cimport AudioCodecContext +from av.audio.stream cimport AudioStream +from av.container.core cimport Container +from av.stream cimport Stream + + +cdef extern from "libavcodec/avcodec.h": + ctypedef struct AVCodecContext: + pass + +cdef extern from "libavformat/avformat.h": + ctypedef struct AVFormatContext: + pass + +cdef extern from "loudnorm_impl.h": + char* loudnorm_get_stats( + AVFormatContext* fmt_ctx, + int audio_stream_index, + const char* loudnorm_args + ) nogil + + +cpdef bytes stats(str loudnorm_args, AudioStream stream): + """ + Get loudnorm statistics for an audio stream. + + Args: + loudnorm_args (str): Arguments for the loudnorm filter (e.g. "i=-24.0:lra=7.0:tp=-2.0") + stream (AudioStream): Input audio stream to analyze + + Returns: + bytes: JSON string containing the loudnorm statistics + """ + + if "print_format=json" not in loudnorm_args: + loudnorm_args = loudnorm_args + ":print_format=json" + + cdef Container container = stream.container + cdef AVFormatContext* format_ptr = container.ptr + + container.ptr = NULL # Prevent double-free + + cdef int stream_index = stream.index + cdef bytes py_args = loudnorm_args.encode("utf-8") + cdef const char* c_args = py_args + cdef char* result + + with nogil: + result = loudnorm_get_stats(format_ptr, stream_index, c_args) + + if result == NULL: + raise RuntimeError("Failed to get loudnorm stats") + + py_result = result[:] # Make a copy of the string + free(result) # Free the C string + + return py_result diff --git a/av/filter/loudnorm_impl.c b/av/filter/loudnorm_impl.c new file mode 100644 index 000000000..e8d56ddba --- /dev/null +++ b/av/filter/loudnorm_impl.c @@ -0,0 +1,211 @@ +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 + #include +#else + #include +#endif + +#ifdef _WIN32 + static CRITICAL_SECTION json_mutex; + static CONDITION_VARIABLE json_cond; + static int mutex_initialized = 0; +#else + static pthread_mutex_t json_mutex = PTHREAD_MUTEX_INITIALIZER; + static pthread_cond_t json_cond = PTHREAD_COND_INITIALIZER; +#endif + +static char json_buffer[2048] = {0}; +static int json_captured = 0; + +// Custom logging callback +static void logging_callback(void *ptr, int level, const char *fmt, va_list vl) { + char line[2048]; + vsnprintf(line, sizeof(line), fmt, vl); + + const char *json_start = strstr(line, "{"); + if (json_start) { + #ifdef _WIN32 + EnterCriticalSection(&json_mutex); + #else + pthread_mutex_lock(&json_mutex); + #endif + + strncpy(json_buffer, json_start, sizeof(json_buffer) - 1); + json_captured = 1; + + #ifdef _WIN32 + WakeConditionVariable(&json_cond); + LeaveCriticalSection(&json_mutex); + #else + pthread_cond_signal(&json_cond); + pthread_mutex_unlock(&json_mutex); + #endif + } +} + +char* loudnorm_get_stats( + AVFormatContext* fmt_ctx, + int audio_stream_index, + const char* loudnorm_args +) { + char* result = NULL; + json_captured = 0; // Reset the captured flag + memset(json_buffer, 0, sizeof(json_buffer)); // Clear the buffer + + #ifdef _WIN32 + // Initialize synchronization objects if needed + if (!mutex_initialized) { + InitializeCriticalSection(&json_mutex); + InitializeConditionVariable(&json_cond); + mutex_initialized = 1; + } + #endif + + av_log_set_callback(logging_callback); + + AVFilterGraph *filter_graph = NULL; + AVFilterContext *src_ctx = NULL, *sink_ctx = NULL, *loudnorm_ctx = NULL; + + AVCodec *codec = NULL; + AVCodecContext *codec_ctx = NULL; + int ret; + + AVCodecParameters *codecpar = fmt_ctx->streams[audio_stream_index]->codecpar; + codec = (AVCodec *)avcodec_find_decoder(codecpar->codec_id); + codec_ctx = avcodec_alloc_context3(codec); + avcodec_parameters_to_context(codec_ctx, codecpar); + avcodec_open2(codec_ctx, codec, NULL); + + char ch_layout_str[64]; + av_channel_layout_describe(&codecpar->ch_layout, ch_layout_str, sizeof(ch_layout_str)); + + filter_graph = avfilter_graph_alloc(); + + char args[512]; + snprintf(args, sizeof(args), + "time_base=%d/%d:sample_rate=%d:sample_fmt=%s:channel_layout=%s", + fmt_ctx->streams[audio_stream_index]->time_base.num, + fmt_ctx->streams[audio_stream_index]->time_base.den, + codecpar->sample_rate, + av_get_sample_fmt_name(codec_ctx->sample_fmt), + ch_layout_str); + + avfilter_graph_create_filter(&src_ctx, avfilter_get_by_name("abuffer"), + "src", args, NULL, filter_graph); + avfilter_graph_create_filter(&sink_ctx, avfilter_get_by_name("abuffersink"), + "sink", NULL, NULL, filter_graph); + avfilter_graph_create_filter(&loudnorm_ctx, avfilter_get_by_name("loudnorm"), + "loudnorm", loudnorm_args, NULL, filter_graph); + + avfilter_link(src_ctx, 0, loudnorm_ctx, 0); + avfilter_link(loudnorm_ctx, 0, sink_ctx, 0); + avfilter_graph_config(filter_graph, NULL); + + AVPacket *packet = av_packet_alloc(); + AVFrame *frame = av_frame_alloc(); + AVFrame *filt_frame = av_frame_alloc(); + + while ((ret = av_read_frame(fmt_ctx, packet)) >= 0) { + if (packet->stream_index != audio_stream_index) { + av_packet_unref(packet); + continue; + } + + ret = avcodec_send_packet(codec_ctx, packet); + if (ret < 0) { + av_packet_unref(packet); + continue; + } + + while (ret >= 0) { + ret = avcodec_receive_frame(codec_ctx, frame); + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break; + if (ret < 0) goto end; + + ret = av_buffersrc_add_frame_flags(src_ctx, frame, AV_BUFFERSRC_FLAG_KEEP_REF); + if (ret < 0) goto end; + + while (1) { + ret = av_buffersink_get_frame(sink_ctx, filt_frame); + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break; + if (ret < 0) goto end; + av_frame_unref(filt_frame); + } + } + av_packet_unref(packet); + } + + // Flush decoder + avcodec_send_packet(codec_ctx, NULL); + while (avcodec_receive_frame(codec_ctx, frame) >= 0) { + av_buffersrc_add_frame(src_ctx, frame); + } + + // Flush filter + av_buffersrc_add_frame(src_ctx, NULL); + while (av_buffersink_get_frame(sink_ctx, filt_frame) >= 0) { + av_frame_unref(filt_frame); + } + + // Force stats print + if (loudnorm_ctx) { + av_log_set_level(AV_LOG_INFO); + av_opt_set(loudnorm_ctx, "print_format", "json", AV_OPT_SEARCH_CHILDREN); + av_opt_set(loudnorm_ctx, "measured_i", NULL, AV_OPT_SEARCH_CHILDREN); + av_opt_set(loudnorm_ctx, "measured_lra", NULL, AV_OPT_SEARCH_CHILDREN); + av_opt_set(loudnorm_ctx, "measured_tp", NULL, AV_OPT_SEARCH_CHILDREN); + av_opt_set(loudnorm_ctx, "measured_thresh", NULL, AV_OPT_SEARCH_CHILDREN); + avfilter_init_str(loudnorm_ctx, NULL); + } + + avfilter_graph_request_oldest(filter_graph); + +end: + avcodec_free_context(&codec_ctx); + avfilter_graph_free(&filter_graph); + avformat_close_input(&fmt_ctx); + av_frame_free(&filt_frame); + av_frame_free(&frame); + av_packet_free(&packet); + + #ifdef _WIN32 + EnterCriticalSection(&json_mutex); + while (!json_captured) { + if (!SleepConditionVariableCS(&json_cond, &json_mutex, 5000)) { // 5 second timeout + fprintf(stderr, "Timeout waiting for JSON data\n"); + break; + } + } + if (json_captured) { + result = _strdup(json_buffer); // Use _strdup on Windows + } + LeaveCriticalSection(&json_mutex); + #else + struct timespec timeout; + clock_gettime(CLOCK_REALTIME, &timeout); + timeout.tv_sec += 5; // 5 second timeout + + pthread_mutex_lock(&json_mutex); + while (json_captured == 0) { + int ret = pthread_cond_timedwait(&json_cond, &json_mutex, &timeout); + if (ret == ETIMEDOUT) { + fprintf(stderr, "Timeout waiting for JSON data\n"); + break; + } + } + if (json_captured) { + result = strdup(json_buffer); + } + pthread_mutex_unlock(&json_mutex); + #endif + + av_log_set_callback(av_log_default_callback); + return result; +} \ No newline at end of file diff --git a/av/filter/loudnorm_impl.h b/av/filter/loudnorm_impl.h new file mode 100644 index 000000000..7357e4668 --- /dev/null +++ b/av/filter/loudnorm_impl.h @@ -0,0 +1,12 @@ +#ifndef AV_FILTER_LOUDNORM_H +#define AV_FILTER_LOUDNORM_H + +#include + +char* loudnorm_get_stats( + AVFormatContext* fmt_ctx, + int audio_stream_index, + const char* loudnorm_args +); + +#endif // AV_FILTER_LOUDNORM_H \ No newline at end of file diff --git a/setup.py b/setup.py index 935d233aa..685412b99 100644 --- a/setup.py +++ b/setup.py @@ -153,10 +153,36 @@ def parse_cflags(raw_flags): "library_dirs": [], } +loudnorm_extension = Extension( + "av.filter.loudnorm", + sources=[ + "av/filter/loudnorm.pyx", + "av/filter/loudnorm_impl.c", + ], + include_dirs=["av/filter"] + extension_extra["include_dirs"], + libraries=extension_extra["libraries"], + library_dirs=extension_extra["library_dirs"], +) + +# Add the cythonized loudnorm extension to ext_modules +ext_modules = cythonize( + loudnorm_extension, + compiler_directives={ + "c_string_type": "str", + "c_string_encoding": "ascii", + "embedsignature": True, + "language_level": 3, + }, + build_dir="src", + include_path=["include"], +) + # Construct the modules that we find in the "av" directory. -ext_modules = [] for dirname, dirnames, filenames in os.walk("av"): for filename in filenames: + if filename == "loudnorm.pyx": + continue + # We are looking for Cython sources. if filename.startswith(".") or os.path.splitext(filename)[1] != ".pyx": continue diff --git a/tests/test_streams.py b/tests/test_streams.py index b7699e622..c7b234d48 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -26,6 +26,17 @@ def test_stream_tuples(self) -> None: audio_streams = tuple([s for s in container.streams if s.type == "audio"]) assert audio_streams == container.streams.audio + def test_loudnorm(self) -> None: + container = av.open( + fate_suite("amv/MTV_high_res_320x240_sample_Penguin_Joke_MTV_from_WMV.amv") + ) + audio = container.streams.audio[0] + stats = av.filter.loudnorm.stats("i=-24.0:lra=7.0:tp=-2.0", audio) + + assert isinstance(stats, bytes) and len(stats) > 30 + assert b"inf" not in stats + assert b'"input_i"' in stats + def test_selection(self) -> None: container = av.open( fate_suite("amv/MTV_high_res_320x240_sample_Penguin_Joke_MTV_from_WMV.amv") From 23aec498301e50ca391c1fc37f0c6c25f857bd81 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 24 Nov 2024 00:56:09 -0500 Subject: [PATCH 430/846] `skip_frame`: Use str literals instead of Enum --- .github/workflows/smoke.yml | 6 ++--- av/codec/context.pyi | 13 +++------ av/codec/context.pyx | 53 ++++++++++++++++++++++++++++--------- tests/test_codec_context.py | 2 +- 4 files changed, 48 insertions(+), 26 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index ca6a3f15c..352634ff6 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -37,8 +37,8 @@ jobs: - {os: ubuntu-latest, python: "3.12", ffmpeg: "7.1", extras: true} - {os: ubuntu-latest, python: "3.9", ffmpeg: "7.0.2"} - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "7.1"} - - {os: macos-14, python: "3.9", ffmpeg: "7.1"} - - {os: macos-14, python: "3.9", ffmpeg: "7.0.2"} + - {os: macos-14, python: "3.9", ffmpeg: "7.1"} + - {os: macos-14, python: "3.9", ffmpeg: "7.0.2"} env: PYAV_PYTHON: python${{ matrix.config.python }} @@ -65,7 +65,7 @@ jobs: fi ;; macos-14) - brew install automake libtool nasm pkg-config libpng libvorbis libvpx opus x264 + brew install automake libtool nasm libpng libvorbis libvpx opus x264 ;; esac diff --git a/av/codec/context.pyi b/av/codec/context.pyi index 8b7458597..e58b0879d 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -15,15 +15,6 @@ class ThreadType(Flag): def __get__(self, i: object | None, owner: type | None = None) -> ThreadType: ... def __set__(self, instance: object, value: int | str | ThreadType) -> None: ... -class SkipType(Enum): - NONE: ClassVar[SkipType] - DEFAULT: ClassVar[SkipType] - NONREF: ClassVar[SkipType] - BIDIR: ClassVar[SkipType] - NONINTRA: ClassVar[SkipType] - NONKEY: ClassVar[SkipType] - ALL: ClassVar[SkipType] - class Flags(EnumFlag): NONE: int UNALIGNED: int @@ -71,7 +62,9 @@ class CodecContext: bit_rate_tolerance: int thread_count: int thread_type: ThreadType - skip_frame: SkipType + skip_frame: Literal[ + "NONE", "DEFAULT", "NONREF", "BIDIR", "NONINTRA", "NONKEY", "ALL" + ] # flags unaligned: bool diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 6e039a4e1..f420b1157 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -47,14 +47,6 @@ class ThreadType(Flag): SLICE: "Decode more than one part of a single frame at once" = lib.FF_THREAD_SLICE AUTO: "Decode using both FRAME and SLICE methods." = lib.FF_THREAD_SLICE | lib.FF_THREAD_FRAME -class SkipType(Enum): - NONE: "Discard nothing" = lib.AVDISCARD_NONE - DEFAULT: "Discard useless packets like 0 size packets in AVI" = lib.AVDISCARD_DEFAULT - NONREF: "Discard all non reference" = lib.AVDISCARD_NONREF - BIDIR: "Discard all bidirectional frames" = lib.AVDISCARD_BIDIR - NONINTRA: "Discard all non intra frames" = lib.AVDISCARD_NONINTRA - NONKEY: "Discard all frames except keyframes" = lib.AVDISCARD_NONKEY - ALL: "Discard all" = lib.AVDISCARD_ALL Flags = define_enum("Flags", __name__, ( ("NONE", 0), @@ -630,16 +622,53 @@ cdef class CodecContext: @property def skip_frame(self): - """One of :class:`.SkipType`. + """Returns one of the following str literals: - Wraps :ffmpeg:`AVCodecContext.skip_frame`. + "NONE" Discard nothing + "DEFAULT" Discard useless packets like 0 size packets in AVI + "NONREF" Discard all non reference + "BIDIR" Discard all bidirectional frames + "NONINTRA" Discard all non intra frames + "NONKEY Discard all frames except keyframes + "ALL" Discard all + Wraps :ffmpeg:`AVCodecContext.skip_frame`. """ - return SkipType(self.ptr.skip_frame) + value = self.ptr.skip_frame + if value == lib.AVDISCARD_NONE: + return "NONE" + if value == lib.AVDISCARD_DEFAULT: + return "DEFAULT" + if value == lib.AVDISCARD_NONREF: + return "NONREF" + if value == lib.AVDISCARD_BIDIR: + return "BIDIR" + if value == lib.AVDISCARD_NONINTRA: + return "NONINTRA" + if value == lib.AVDISCARD_NONKEY: + return "NONKEY" + if value == lib.AVDISCARD_ALL: + return "ALL" + return f"{value}" @skip_frame.setter def skip_frame(self, value): - self.ptr.skip_frame = value.value + if value == "NONE": + self.ptr.skip_frame = lib.AVDISCARD_NONE + elif value == "DEFAULT": + self.ptr.skip_frame = lib.AVDISCARD_DEFAULT + elif value == "NONREF": + self.ptr.skip_frame = lib.AVDISCARD_NONREF + elif value == "BIDIR": + self.ptr.skip_frame = lib.AVDISCARD_BIDIR + elif value == "NONINTRA": + self.ptr.skip_frame = lib.AVDISCARD_NONINTRA + elif value == "NONKEY": + self.ptr.skip_frame = lib.AVDISCARD_NONKEY + elif value == "ALL": + self.ptr.skip_frame = lib.AVDISCARD_ALL + else: + raise ValueError("Invalid skip_frame type") @property def delay(self): diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 6425c4dd1..1173a5865 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -70,7 +70,7 @@ def iter_raw_frames( class TestCodecContext(TestCase): def test_skip_frame_default(self): ctx = Codec("png", "w").create() - assert ctx.skip_frame.name == "DEFAULT" + assert ctx.skip_frame == "DEFAULT" def test_codec_delay(self) -> None: with av.open(fate_suite("mkv/codec_delay_opus.mkv")) as container: From 95a8c554cca06b3e49ec26d5d2764b1dd65274df Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 28 Nov 2024 17:13:17 -0500 Subject: [PATCH 431/846] Update changelog --- CHANGELOG.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5d034b223..d8687fa2f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -22,18 +22,23 @@ v14.0.0 Major: - Drop FFmpeg 6. -- Remove ``AVError`` alias (use ``FFmpegError`` directly instead). +- Drop support for MacOS <11 in our binary wheels. +- Change many instances ``EnumItem`` s (our custom enum class) with Python's standard Enums. - Remove the `template` arg from ``OutputContainer.add_stream()``. - Remove ``CodecContext.close()`` because it's deprecated in ffmpeg. +- Remove ``AVError`` alias (use ``FFmpegError`` directly instead). Features: - Add ``OutputContainer.add_stream_from_template()`` by :gh-user:`WyattBlue` and :gh-user:`cdce8p`. +- Add ``OutputContainer.add_data_stream()`` by :gh-user:`WyattBlue`. +- Add ``filter.loudnorm.stats()`` function that returns the stats of loudnorm for 2-pass filtering by :gh-user:`WyattBlue`. - Add ``qmin`` and ``qmax`` parameters to the ``VideoCodecContext`` by :gh-user:`davidplowman` in (:pr:`1618`). Fixes: - Correct ``Colorspace``'s lowercase enums. +- Updated ``sidedata.Type`` enum. - Ensure streams in StreamContainer are released. Fixes :issue:`1599`. v13.1.0 From 445d759365ba4e7e7003d7ce2163f621ff42eac9 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 1 Dec 2024 05:23:47 -0500 Subject: [PATCH 432/846] Preserve log level --- av/filter/__init__.py | 1 + av/filter/__init__.pyi | 1 + av/filter/loudnorm.pyx | 6 ++++++ av/filter/loudnorm_impl.c | 16 ++-------------- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/av/filter/__init__.py b/av/filter/__init__.py index 207d6e15e..5dd4430d4 100644 --- a/av/filter/__init__.py +++ b/av/filter/__init__.py @@ -1,2 +1,3 @@ from .filter import Filter, FilterFlags, filter_descriptor, filters_available from .graph import Graph +from .loudnorm import stats diff --git a/av/filter/__init__.pyi b/av/filter/__init__.pyi index 8a6b5a59b..5be1326c9 100644 --- a/av/filter/__init__.pyi +++ b/av/filter/__init__.pyi @@ -1,3 +1,4 @@ from .context import * from .filter import * from .graph import * +from .loudnorm import * diff --git a/av/filter/loudnorm.pyx b/av/filter/loudnorm.pyx index 662bbd230..78f320a9e 100644 --- a/av/filter/loudnorm.pyx +++ b/av/filter/loudnorm.pyx @@ -8,6 +8,7 @@ from av.audio.codeccontext cimport AudioCodecContext from av.audio.stream cimport AudioStream from av.container.core cimport Container from av.stream cimport Stream +from av.logging import get_level, set_level cdef extern from "libavcodec/avcodec.h": @@ -51,6 +52,9 @@ cpdef bytes stats(str loudnorm_args, AudioStream stream): cdef const char* c_args = py_args cdef char* result + # Save log level since C function overwrite it. + level = get_level() + with nogil: result = loudnorm_get_stats(format_ptr, stream_index, c_args) @@ -60,4 +64,6 @@ cpdef bytes stats(str loudnorm_args, AudioStream stream): py_result = result[:] # Make a copy of the string free(result) # Free the C string + set_level(level) + return py_result diff --git a/av/filter/loudnorm_impl.c b/av/filter/loudnorm_impl.c index e8d56ddba..79f960080 100644 --- a/av/filter/loudnorm_impl.c +++ b/av/filter/loudnorm_impl.c @@ -3,7 +3,6 @@ #include #include #include -#include #include #ifdef _WIN32 @@ -154,22 +153,11 @@ char* loudnorm_get_stats( av_frame_unref(filt_frame); } - // Force stats print - if (loudnorm_ctx) { - av_log_set_level(AV_LOG_INFO); - av_opt_set(loudnorm_ctx, "print_format", "json", AV_OPT_SEARCH_CHILDREN); - av_opt_set(loudnorm_ctx, "measured_i", NULL, AV_OPT_SEARCH_CHILDREN); - av_opt_set(loudnorm_ctx, "measured_lra", NULL, AV_OPT_SEARCH_CHILDREN); - av_opt_set(loudnorm_ctx, "measured_tp", NULL, AV_OPT_SEARCH_CHILDREN); - av_opt_set(loudnorm_ctx, "measured_thresh", NULL, AV_OPT_SEARCH_CHILDREN); - avfilter_init_str(loudnorm_ctx, NULL); - } - - avfilter_graph_request_oldest(filter_graph); + // Pushes graph + avfilter_graph_free(&filter_graph); end: avcodec_free_context(&codec_ctx); - avfilter_graph_free(&filter_graph); avformat_close_input(&fmt_ctx); av_frame_free(&filt_frame); av_frame_free(&frame); From 92f547b3e7feea249dc275c6d7df87b7ed67fe3a Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 1 Dec 2024 13:54:11 -0500 Subject: [PATCH 433/846] Use IntEnum for codec capabilities --- av/codec/codec.pyi | 76 ++++++++------------ av/codec/codec.pyx | 175 ++++++++++++++++----------------------------- docs/api/codec.rst | 10 +-- 3 files changed, 94 insertions(+), 167 deletions(-) diff --git a/av/codec/codec.pyi b/av/codec/codec.pyi index fe2d2fd41..3323895ed 100644 --- a/av/codec/codec.pyi +++ b/av/codec/codec.pyi @@ -1,4 +1,4 @@ -from enum import Flag +from enum import Flag, IntEnum from fractions import Fraction from typing import ClassVar, Literal, overload @@ -21,28 +21,29 @@ class Properties(Flag): BITMAP_SUB: ClassVar[Properties] TEXT_SUB: ClassVar[Properties] -class Capabilities(EnumFlag): - NONE: int - DARW_HORIZ_BAND: int - DR1: int - HWACCEL: int - DELAY: int - SMALL_LAST_FRAME: int - HWACCEL_VDPAU: int - SUBFRAMES: int - EXPERIMENTAL: int - CHANNEL_CONF: int - NEG_LINESIZES: int - FRAME_THREADS: int - SLICE_THREADS: int - PARAM_CHANGE: int - AUTO_THREADS: int - VARIABLE_FRAME_SIZE: int - AVOID_PROBING: int - HARDWARE: int - HYBRID: int - ENCODER_REORDERED_OPAQUE: int - ENCODER_FLUSH: int +class Capabilities(IntEnum): + none: int + draw_horiz_band: int + dr1: int + hwaccel: int + delay: int + small_last_frame: int + hwaccel_vdpau: int + subframes: int + experimental: int + channel_conf: int + neg_linesizes: int + frame_threads: int + slice_threads: int + param_change: int + auto_threads: int + variable_frame_size: int + avoid_probing: int + hardware: int + hybrid: int + encoder_reordered_opaque: int + encoder_flush: int + encoder_recon_frame: int class UnknownCodecError(ValueError): ... @@ -79,29 +80,12 @@ class Codec: def bitmap_sub(self) -> bool: ... @property def text_sub(self) -> bool: ... - - capabilities: Capabilities - draw_horiz_band: bool - dr1: bool - hwaccel: bool - delay: bool - small_last_frame: bool - hwaccel_vdpau: bool - subframes: bool - experimental: bool - channel_conf: bool - neg_linesizes: bool - frame_threads: bool - slice_threads: bool - param_change: bool - auto_threads: bool - variable_frame_size: bool - avoid_probing: bool - hardware: bool - hybrid: bool - encoder_reordered_opaque: bool - encoder_flush: bool - + @property + def capabilities(self) -> int: ... + @property + def experimental(self) -> bool: ... + @property + def delay(self) -> bool: ... def __init__(self, name: str, mode: Literal["r", "w"] = "r") -> None: ... @overload def create(self, kind: Literal["video"]) -> VideoCodecContext: ... diff --git a/av/codec/codec.pyx b/av/codec/codec.pyx index 9b29cb178..98a5b50d8 100644 --- a/av/codec/codec.pyx +++ b/av/codec/codec.pyx @@ -1,10 +1,9 @@ from av.audio.format cimport get_audio_format from av.descriptor cimport wrap_avclass -from av.enum cimport define_enum from av.utils cimport avrational_to_fraction from av.video.format cimport get_video_format -from enum import Flag +from enum import Flag, IntEnum cdef object _cinit_sentinel = object() @@ -25,94 +24,30 @@ class Properties(Flag): BITMAP_SUB = lib.AV_CODEC_PROP_BITMAP_SUB TEXT_SUB = lib.AV_CODEC_PROP_TEXT_SUB -Capabilities = define_enum("Capabilities", "av.codec", ( - ("NONE", 0), - ("DRAW_HORIZ_BAND", lib.AV_CODEC_CAP_DRAW_HORIZ_BAND, - """Decoder can use draw_horiz_band callback."""), - ("DR1", lib.AV_CODEC_CAP_DR1, - """Codec uses get_buffer() for allocating buffers and supports custom allocators. - If not set, it might not use get_buffer() at all or use operations that - assume the buffer was allocated by avcodec_default_get_buffer."""), - ("HWACCEL", 1 << 4), - ("DELAY", lib.AV_CODEC_CAP_DELAY, - """Encoder or decoder requires flushing with NULL input at the end in order to - give the complete and correct output. - - NOTE: If this flag is not set, the codec is guaranteed to never be fed with - with NULL data. The user can still send NULL data to the public encode - or decode function, but libavcodec will not pass it along to the codec - unless this flag is set. - - Decoders: - The decoder has a non-zero delay and needs to be fed with avpkt->data=NULL, - avpkt->size=0 at the end to get the delayed data until the decoder no longer - returns frames. - - Encoders: - The encoder needs to be fed with NULL data at the end of encoding until the - encoder no longer returns data. - - NOTE: For encoders implementing the AVCodec.encode2() function, setting this - flag also means that the encoder must set the pts and duration for - each output packet. If this flag is not set, the pts and duration will - be determined by libavcodec from the input frame."""), - ("SMALL_LAST_FRAME", lib.AV_CODEC_CAP_SMALL_LAST_FRAME, - """Codec can be fed a final frame with a smaller size. - This can be used to prevent truncation of the last audio samples."""), - ("HWACCEL_VDPAU", 1 << 7), - ("SUBFRAMES", lib.AV_CODEC_CAP_SUBFRAMES, - """Codec can output multiple frames per AVPacket - Normally demuxers return one frame at a time, demuxers which do not do - are connected to a parser to split what they return into proper frames. - This flag is reserved to the very rare category of codecs which have a - bitstream that cannot be split into frames without timeconsuming - operations like full decoding. Demuxers carrying such bitstreams thus - may return multiple frames in a packet. This has many disadvantages like - prohibiting stream copy in many cases thus it should only be considered - as a last resort."""), - ("EXPERIMENTAL", lib.AV_CODEC_CAP_EXPERIMENTAL, - """Codec is experimental and is thus avoided in favor of non experimental - encoders"""), - ("CHANNEL_CONF", lib.AV_CODEC_CAP_CHANNEL_CONF, - """Codec should fill in channel configuration and samplerate instead of container"""), - ("NEG_LINESIZES", 1 << 11), - ("FRAME_THREADS", lib.AV_CODEC_CAP_FRAME_THREADS, - """Codec supports frame-level multithreading""",), - ("SLICE_THREADS", lib.AV_CODEC_CAP_SLICE_THREADS, - """Codec supports slice-based (or partition-based) multithreading."""), - ("PARAM_CHANGE", lib.AV_CODEC_CAP_PARAM_CHANGE, - """Codec supports changed parameters at any point."""), - ("AUTO_THREADS", lib.AV_CODEC_CAP_OTHER_THREADS, - """Codec supports multithreading through a method other than slice- or - frame-level multithreading. Typically this marks wrappers around - multithreading-capable external libraries."""), - ("VARIABLE_FRAME_SIZE", lib.AV_CODEC_CAP_VARIABLE_FRAME_SIZE, - """Audio encoder supports receiving a different number of samples in each call."""), - ("AVOID_PROBING", lib.AV_CODEC_CAP_AVOID_PROBING, - """Decoder is not a preferred choice for probing. - This indicates that the decoder is not a good choice for probing. - It could for example be an expensive to spin up hardware decoder, - or it could simply not provide a lot of useful information about - the stream. - A decoder marked with this flag should only be used as last resort - choice for probing."""), - ("HARDWARE", lib.AV_CODEC_CAP_HARDWARE, - """Codec is backed by a hardware implementation. Typically used to - identify a non-hwaccel hardware decoder. For information about hwaccels, use - avcodec_get_hw_config() instead."""), - ("HYBRID", lib.AV_CODEC_CAP_HYBRID, - """Codec is potentially backed by a hardware implementation, but not - necessarily. This is used instead of AV_CODEC_CAP_HARDWARE, if the - implementation provides some sort of internal fallback."""), - ("ENCODER_REORDERED_OPAQUE", 1 << 20, # lib.AV_CODEC_CAP_ENCODER_REORDERED_OPAQUE, # FFmpeg 4.2 - """This codec takes the reordered_opaque field from input AVFrames - and returns it in the corresponding field in AVCodecContext after - encoding."""), - ("ENCODER_FLUSH", 1 << 21, # lib.AV_CODEC_CAP_ENCODER_FLUSH # FFmpeg 4.3 - """This encoder can be flushed using avcodec_flush_buffers(). If this - flag is not set, the encoder must be closed and reopened to ensure that - no frames remain pending."""), -), is_flags=True) + +class Capabilities(IntEnum): + none = 0 + draw_horiz_band = lib.AV_CODEC_CAP_DRAW_HORIZ_BAND + dr1 = lib.AV_CODEC_CAP_DR1 + hwaccel = 1 << 4 + delay = lib.AV_CODEC_CAP_DELAY + small_last_frame = lib.AV_CODEC_CAP_SMALL_LAST_FRAME + hwaccel_vdpau = 1 << 7 + subframes = lib.AV_CODEC_CAP_SUBFRAMES + experimental = lib.AV_CODEC_CAP_EXPERIMENTAL + channel_conf = lib.AV_CODEC_CAP_CHANNEL_CONF + neg_linesizes = 1 << 11 + frame_threads = lib.AV_CODEC_CAP_FRAME_THREADS + slice_threads = lib.AV_CODEC_CAP_SLICE_THREADS + param_change = lib.AV_CODEC_CAP_PARAM_CHANGE + auto_threads = lib.AV_CODEC_CAP_OTHER_THREADS + variable_frame_size = lib.AV_CODEC_CAP_VARIABLE_FRAME_SIZE + avoid_probing = lib.AV_CODEC_CAP_AVOID_PROBING + hardware = lib.AV_CODEC_CAP_HARDWARE + hybrid = lib.AV_CODEC_CAP_HYBRID + encoder_reordered_opaque = 1 << 20 + encoder_flush = 1 << 21 + encoder_recon_frame = 1 << 22 class UnknownCodecError(ValueError): @@ -296,32 +231,48 @@ cdef class Codec: def text_sub(self): return bool(self.desc.props & lib.AV_CODEC_PROP_TEXT_SUB) - @Capabilities.property + @property def capabilities(self): - """Flag property of :class:`.Capabilities`""" + """ + Get the capabilities bitmask of the codec. + + This method returns an integer representing the codec capabilities bitmask, + which can be used to check specific codec features by performing bitwise + operations with the Capabilities enum values. + + :example: + + .. code-block:: python + + from av.codec import Codec, Capabilities + + codec = Codec("h264", "w") + + # Check if the codec can be fed a final frame with a smaller size. + # This can be used to prevent truncation of the last audio samples. + small_last_frame = bool(codec.capabilities & Capabilities.small_last_frame) + + :rtype: int + """ return self.ptr.capabilities - draw_horiz_band = capabilities.flag_property("DRAW_HORIZ_BAND") - dr1 = capabilities.flag_property("DR1") - hwaccel = capabilities.flag_property("HWACCEL") - delay = capabilities.flag_property("DELAY") - small_last_frame = capabilities.flag_property("SMALL_LAST_FRAME") - hwaccel_vdpau = capabilities.flag_property("HWACCEL_VDPAU") - subframes = capabilities.flag_property("SUBFRAMES") - experimental = capabilities.flag_property("EXPERIMENTAL") - channel_conf = capabilities.flag_property("CHANNEL_CONF") - neg_linesizes = capabilities.flag_property("NEG_LINESIZES") - frame_threads = capabilities.flag_property("FRAME_THREADS") - slice_threads = capabilities.flag_property("SLICE_THREADS") - param_change = capabilities.flag_property("PARAM_CHANGE") - auto_threads = capabilities.flag_property("AUTO_THREADS") - variable_frame_size = capabilities.flag_property("VARIABLE_FRAME_SIZE") - avoid_probing = capabilities.flag_property("AVOID_PROBING") - hardware = capabilities.flag_property("HARDWARE") - hybrid = capabilities.flag_property("HYBRID") - encoder_reordered_opaque = capabilities.flag_property("ENCODER_REORDERED_OPAQUE") - encoder_flush = capabilities.flag_property("ENCODER_FLUSH") + @property + def experimental(self): + """ + Check if codec is experimental and is thus avoided in favor of non experimental encoders. + + :rtype: bool + """ + return bool(self.ptr.capabilities & lib.AV_CODEC_CAP_EXPERIMENTAL) + @property + def delay(self): + """ + If true, encoder or decoder requires flushing with `None` at the end in order to give the complete and correct output. + + :rtype: bool + """ + return bool(self.ptr.capabilities & lib.AV_CODEC_CAP_DELAY) cdef get_codec_names(): names = set() diff --git a/docs/api/codec.rst b/docs/api/codec.rst index 770a271a4..d4a241425 100644 --- a/docs/api/codec.rst +++ b/docs/api/codec.rst @@ -36,21 +36,13 @@ Flags Wraps :ffmpeg:`AVCodecDescriptor.props` (``AV_CODEC_PROP_*``). - .. enumtable:: av.codec.codec.Properties - :class: av.codec.codec.Codec - .. autoattribute:: Codec.capabilities .. autoclass:: Capabilities Wraps :ffmpeg:`AVCodec.capabilities` (``AV_CODEC_CAP_*``). - Note that ``ffmpeg -codecs`` prefers the properties versions of - ``INTRA_ONLY`` and ``LOSSLESS``. - - .. enumtable:: av.codec.codec.Capabilities - :class: av.codec.codec.Codec - + Note that ``ffmpeg -codecs`` prefers the properties versions of ``INTRA_ONLY`` and ``LOSSLESS``. Contexts -------- From 49c0605d79e5a6f1e654cd45b3bd44d4a1e8e4ba Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 1 Dec 2024 14:59:19 -0500 Subject: [PATCH 434/846] Use IntEnums for codec flags --- av/codec/codec.pyx | 2 +- av/codec/context.pyi | 90 ++++++++------------ av/codec/context.pyx | 179 +++++++++++++++++---------------------- av/enum.pyx | 40 --------- tests/test_videoframe.py | 7 +- 5 files changed, 121 insertions(+), 197 deletions(-) diff --git a/av/codec/codec.pyx b/av/codec/codec.pyx index 98a5b50d8..1493f0f7b 100644 --- a/av/codec/codec.pyx +++ b/av/codec/codec.pyx @@ -117,7 +117,7 @@ cdef class Codec: if self.is_encoder and lib.av_codec_is_decoder(self.ptr): raise RuntimeError("%s is both encoder and decoder.") - def create(self, str kind = None): + def create(self, kind = None): """Create a :class:`.CodecContext` for this codec. :param str kind: Gives a hint to static type checkers for what exact CodecContext is used. diff --git a/av/codec/context.pyi b/av/codec/context.pyi index e58b0879d..a6ca9647e 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -1,8 +1,7 @@ -from enum import Enum, Flag +from enum import Flag, IntEnum from fractions import Fraction from typing import ClassVar, Literal -from av.enum import EnumFlag, EnumItem from av.packet import Packet from .codec import Codec @@ -15,38 +14,39 @@ class ThreadType(Flag): def __get__(self, i: object | None, owner: type | None = None) -> ThreadType: ... def __set__(self, instance: object, value: int | str | ThreadType) -> None: ... -class Flags(EnumFlag): - NONE: int - UNALIGNED: int - QSCALE: int - # 4MV - OUTPUT_CORRUPT: int - QPEL: int - DROPCHANGED: int - PASS1: int - PASS2: int - LOOP_FILTER: int - GRAY: int - PSNR: int - INTERLACED_DCT: int - LOW_DELAY: int - GLOBAL_HEADER: int - BITEXACT: int - AC_PRED: int - INTERLACED_ME: int - CLOSED_GOP: int +class Flags(IntEnum): + unaligned: int + qscale: int + four_mv: int + output_corrupt: int + qpel: int + drop_changed: int + recon_frame: int + copy_opaque: int + frame_duration: int + pass1: int + pass2: int + loop_filter: int + gray: int + psnr: int + interlaced_dct: int + low_delay: int + global_header: int + bitexact: int + ac_pred: int + interlaced_me: int + closed_gop: int -class Flags2(EnumFlag): - NONE: int - FAST: int - NO_OUTPUT: int - LOCAL_HEADER: int - CHUNKS: int - IGNORE_CROP: int - SHOW_ALL: int - EXPORT_MVS: int - SKIP_MANUAL: int - RO_FLUSH_NOOP: int +class Flags2(IntEnum): + fast: int + no_output: int + local_header: int + chunks: int + ignore_crop: int + show_all: int + export_mvs: int + skip_manual: int + ro_flush_noop: int class CodecContext: name: str @@ -65,30 +65,10 @@ class CodecContext: skip_frame: Literal[ "NONE", "DEFAULT", "NONREF", "BIDIR", "NONINTRA", "NONKEY", "ALL" ] - - # flags - unaligned: bool + flags: int qscale: bool - four_mv: bool - output_corrupt: bool - qpel: bool - drop_changed: bool - recon_frame: bool copy_opaque: bool - frame_duration: bool - pass1: bool - pass2: bool - loop_filter: bool - gray: bool - psnr: bool - interlaced_dct: bool - low_delay: bool - global_header: bool - bitexact: bool - ac_pred: bool - interlaced_me: bool - closed_gop: bool - + flags2: int @property def is_open(self) -> bool: ... @property diff --git a/av/codec/context.pyx b/av/codec/context.pyx index f420b1157..29b7b80d1 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -6,12 +6,11 @@ from libc.string cimport memcpy from av.bytesource cimport ByteSource, bytesource from av.codec.codec cimport Codec, wrap_codec from av.dictionary cimport _Dictionary -from av.enum cimport define_enum from av.error cimport err_check from av.packet cimport Packet from av.utils cimport avrational_to_fraction, to_avrational -from enum import Enum, Flag +from enum import Flag, IntEnum from av.dictionary import Dictionary @@ -47,65 +46,39 @@ class ThreadType(Flag): SLICE: "Decode more than one part of a single frame at once" = lib.FF_THREAD_SLICE AUTO: "Decode using both FRAME and SLICE methods." = lib.FF_THREAD_SLICE | lib.FF_THREAD_FRAME - -Flags = define_enum("Flags", __name__, ( - ("NONE", 0), - ("UNALIGNED", lib.AV_CODEC_FLAG_UNALIGNED, - "Allow decoders to produce frames with data planes that are not aligned to CPU requirements (e.g. due to cropping)." - ), - ("QSCALE", lib.AV_CODEC_FLAG_QSCALE, "Use fixed qscale."), - ("4MV", lib.AV_CODEC_FLAG_4MV, "4 MV per MB allowed / advanced prediction for H.263."), - ("OUTPUT_CORRUPT", lib.AV_CODEC_FLAG_OUTPUT_CORRUPT, "Output even those frames that might be corrupted."), - ("QPEL", lib.AV_CODEC_FLAG_QPEL, "Use qpel MC."), - ("DROPCHANGED", 1 << 5, - "Don't output frames whose parameters differ from first decoded frame in stream." - ), - ("RECON_FRAME", lib.AV_CODEC_FLAG_RECON_FRAME, "Request the encoder to output reconstructed frames, i.e. frames that would be produced by decoding the encoded bistream."), - ("COPY_OPAQUE", lib.AV_CODEC_FLAG_COPY_OPAQUE, - """Request the decoder to propagate each packet's AVPacket.opaque and AVPacket.opaque_ref - to its corresponding output AVFrame. Request the encoder to propagate each frame's - AVFrame.opaque and AVFrame.opaque_ref values to its corresponding output AVPacket."""), - ("FRAME_DURATION", lib.AV_CODEC_FLAG_FRAME_DURATION, - """Signal to the encoder that the values of AVFrame.duration are valid and should be - used (typically for transferring them to output packets)."""), - ("PASS1", lib.AV_CODEC_FLAG_PASS1, "Use internal 2pass ratecontrol in first pass mode."), - ("PASS2", lib.AV_CODEC_FLAG_PASS2, "Use internal 2pass ratecontrol in second pass mode."), - ("LOOP_FILTER", lib.AV_CODEC_FLAG_LOOP_FILTER, "loop filter."), - ("GRAY", lib.AV_CODEC_FLAG_GRAY, "Only decode/encode grayscale."), - ("PSNR", lib.AV_CODEC_FLAG_PSNR, "error[?] variables will be set during encoding."), - ("INTERLACED_DCT", lib.AV_CODEC_FLAG_INTERLACED_DCT, "Use interlaced DCT."), - ("LOW_DELAY", lib.AV_CODEC_FLAG_LOW_DELAY, "Force low delay."), - ("GLOBAL_HEADER", lib.AV_CODEC_FLAG_GLOBAL_HEADER, - "Place global headers in extradata instead of every keyframe." - ), - ("BITEXACT", lib.AV_CODEC_FLAG_BITEXACT, "Use only bitexact stuff (except (I)DCT)."), - ("AC_PRED", lib.AV_CODEC_FLAG_AC_PRED, "H.263 advanced intra coding / MPEG-4 AC prediction"), - ("INTERLACED_ME", lib.AV_CODEC_FLAG_INTERLACED_ME, "Interlaced motion estimation"), - ("CLOSED_GOP", lib.AV_CODEC_FLAG_CLOSED_GOP), -), is_flags=True) - -Flags2 = define_enum("Flags2", __name__, ( - ("NONE", 0), - ("FAST", lib.AV_CODEC_FLAG2_FAST, - """Allow non spec compliant speedup tricks."""), - ("NO_OUTPUT", lib.AV_CODEC_FLAG2_NO_OUTPUT, - """Skip bitstream encoding."""), - ("LOCAL_HEADER", lib.AV_CODEC_FLAG2_LOCAL_HEADER, - """Place global headers at every keyframe instead of in extradata."""), - ("CHUNKS", lib.AV_CODEC_FLAG2_CHUNKS, - """Input bitstream might be truncated at a packet boundaries - instead of only at frame boundaries."""), - ("IGNORE_CROP", lib.AV_CODEC_FLAG2_IGNORE_CROP, - """Discard cropping information from SPS."""), - ("SHOW_ALL", lib.AV_CODEC_FLAG2_SHOW_ALL, - """Show all frames before the first keyframe"""), - ("EXPORT_MVS", lib.AV_CODEC_FLAG2_EXPORT_MVS, - """Export motion vectors through frame side data"""), - ("SKIP_MANUAL", lib.AV_CODEC_FLAG2_SKIP_MANUAL, - """Do not skip samples and export skip information as frame side data"""), - ("RO_FLUSH_NOOP", lib.AV_CODEC_FLAG2_RO_FLUSH_NOOP, - """Do not reset ASS ReadOrder field on flush (subtitles decoding)"""), -), is_flags=True) +class Flags(IntEnum): + unaligned = lib.AV_CODEC_FLAG_UNALIGNED + qscale = lib.AV_CODEC_FLAG_QSCALE + four_mv = lib.AV_CODEC_FLAG_4MV + output_corrupt = lib.AV_CODEC_FLAG_OUTPUT_CORRUPT + qpel = lib.AV_CODEC_FLAG_QPEL + drop_changed = 1 << 5 + recon_frame = lib.AV_CODEC_FLAG_RECON_FRAME + copy_opaque = lib.AV_CODEC_FLAG_COPY_OPAQUE + frame_duration = lib.AV_CODEC_FLAG_FRAME_DURATION + pass1 = lib.AV_CODEC_FLAG_PASS1 + pass2 = lib.AV_CODEC_FLAG_PASS2 + loop_filter = lib.AV_CODEC_FLAG_LOOP_FILTER + gray = lib.AV_CODEC_FLAG_GRAY + psnr = lib.AV_CODEC_FLAG_PSNR + interlaced_dct = lib.AV_CODEC_FLAG_INTERLACED_DCT + low_delay = lib.AV_CODEC_FLAG_LOW_DELAY + global_header = lib.AV_CODEC_FLAG_GLOBAL_HEADER + bitexact = lib.AV_CODEC_FLAG_BITEXACT + ac_pred = lib.AV_CODEC_FLAG_AC_PRED + interlaced_me = lib.AV_CODEC_FLAG_INTERLACED_ME + closed_gop = lib.AV_CODEC_FLAG_CLOSED_GOP + +class Flags2(IntEnum): + fast = lib.AV_CODEC_FLAG2_FAST + no_output = lib.AV_CODEC_FLAG2_NO_OUTPUT + local_header = lib.AV_CODEC_FLAG2_LOCAL_HEADER + chunks = lib.AV_CODEC_FLAG2_CHUNKS + ignore_crop = lib.AV_CODEC_FLAG2_IGNORE_CROP + show_all = lib.AV_CODEC_FLAG2_SHOW_ALL + export_mvs = lib.AV_CODEC_FLAG2_EXPORT_MVS + skip_manual = lib.AV_CODEC_FLAG2_SKIP_MANUAL + ro_flush_noop = lib.AV_CODEC_FLAG2_RO_FLUSH_NOOP cdef class CodecContext: @@ -133,53 +106,59 @@ cdef class CodecContext: self.ptr.thread_count = 0 # use as many threads as there are CPUs. self.ptr.thread_type = 0x02 # thread within a frame. Does not change the API. - def _get_flags(self): + @property + def flags(self): + """ + Get and set the flags bitmask of CodecContext. + + :rtype: int + """ return self.ptr.flags - def _set_flags(self, value): + @flags.setter + def flags(self, int value): self.ptr.flags = value - flags = Flags.property(_get_flags, _set_flags, "Flag property of :class:`.Flags`.") - - unaligned = flags.flag_property("UNALIGNED") - qscale = flags.flag_property("QSCALE") - four_mv = flags.flag_property("4MV") - output_corrupt = flags.flag_property("OUTPUT_CORRUPT") - qpel = flags.flag_property("QPEL") - drop_changed = flags.flag_property("DROPCHANGED") - recon_frame = flags.flag_property("RECON_FRAME") - copy_opaque = flags.flag_property("COPY_OPAQUE") - frame_duration = flags.flag_property("FRAME_DURATION") - pass1 = flags.flag_property("PASS1") - pass2 = flags.flag_property("PASS2") - loop_filter = flags.flag_property("LOOP_FILTER") - gray = flags.flag_property("GRAY") - psnr = flags.flag_property("PSNR") - interlaced_dct = flags.flag_property("INTERLACED_DCT") - low_delay = flags.flag_property("LOW_DELAY") - global_header = flags.flag_property("GLOBAL_HEADER") - bitexact = flags.flag_property("BITEXACT") - ac_pred = flags.flag_property("AC_PRED") - interlaced_me = flags.flag_property("INTERLACED_ME") - closed_gop = flags.flag_property("CLOSED_GOP") - - def _get_flags2(self): + @property + def qscale(self): + """ + Use fixed qscale. + + :rtype: bool + """ + return bool(self.ptr.flags & lib.AV_CODEC_FLAG_QSCALE) + + @qscale.setter + def qscale(self, value): + if value: + self.ptr.flags |= lib.AV_CODEC_FLAG_QSCALE + else: + self.ptr.flags &= ~lib.AV_CODEC_FLAG_QSCALE + + @property + def copy_opaque(self): + return bool(self.ptr.flags & lib.AV_CODEC_FLAG_COPY_OPAQUE) + + @copy_opaque.setter + def copy_opaque(self, value): + if value: + self.ptr.flags |= lib.AV_CODEC_FLAG_COPY_OPAQUE + else: + self.ptr.flags &= ~lib.AV_CODEC_FLAG_COPY_OPAQUE + + @property + def flags2(self): + """ + Get and set the flags2 bitmask of CodecContext. + + :rtype: int + """ return self.ptr.flags2 - def _set_flags2(self, value): + @flags2.setter + def flags2(self, int value): self.ptr.flags2 = value - flags2 = Flags2.property(_get_flags2, _set_flags2, "Flag property of :class:`.Flags2`.") - fast = flags2.flag_property("FAST") - no_output = flags2.flag_property("NO_OUTPUT") - local_header = flags2.flag_property("LOCAL_HEADER") - chunks = flags2.flag_property("CHUNKS") - ignore_crop = flags2.flag_property("IGNORE_CROP") - show_all = flags2.flag_property("SHOW_ALL") - export_mvs = flags2.flag_property("EXPORT_MVS") - skip_manual = flags2.flag_property("SKIP_MANUAL") - ro_flush_noop = flags2.flag_property("RO_FLUSH_NOOP") - @property def extradata(self): if self.ptr is NULL: diff --git a/av/enum.pyx b/av/enum.pyx index 9217e67d6..802a731ff 100644 --- a/av/enum.pyx +++ b/av/enum.pyx @@ -198,46 +198,6 @@ cdef class EnumItem: cdef class EnumFlag(EnumItem): - - """ - Flags are sets of boolean attributes, which the FFmpeg API represents as individual - bits in a larger integer which you manipulate with the bitwise operators. - We associate names with each flag that are easier to operate with. - - Consider :data:`CodecContextFlags`, whis is the type of the :attr:`CodecContext.flags` - attribute, and the set of boolean properties:: - - >>> fh = av.open(video_path) - >>> cc = fh.streams.video[0].codec_context - - >>> cc.flags - - - >>> # You can set flags via bitwise operations with the objects, names, or values: - >>> cc.flags |= cc.flags.OUTPUT_CORRUPT - >>> cc.flags |= 'GLOBAL_HEADER' - >>> cc.flags - - - >>> # You can test flags via bitwise operations with objects, names, or values: - >>> bool(cc.flags & cc.flags.OUTPUT_CORRUPT) - True - >>> bool(cc.flags & 'QSCALE') - False - - >>> # There are boolean properties for each flag: - >>> cc.output_corrupt - True - >>> cc.qscale - False - - >>> # You can set them: - >>> cc.qscale = True - >>> cc.flags - - - """ - cdef readonly tuple flags def __cinit__(self, sentinel, name, value, doc=None): diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 32b6e5482..c93a12e32 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -31,7 +31,12 @@ def assertPixelValue16(plane, expected, byteorder: str) -> None: def test_opaque() -> None: with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: video_stream = container.streams.video[0] - video_stream.codec_context.copy_opaque = True + + ctx = video_stream.codec_context + ctx.flags |= av.codec.context.Flags.copy_opaque + + assert video_stream.codec_context.copy_opaque + for packet_idx, packet in enumerate(container.demux()): packet.opaque = (time.time(), packet_idx) for frame in packet.decode(): From eb8428d016344c8c2f6bdcccc21f4d9cd4b0023f Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 1 Dec 2024 15:27:42 -0500 Subject: [PATCH 435/846] Use Flag enum for format flags --- av/format.pyi | 81 ++++++++++++++++++++-------------------------- av/format.pyx | 90 +++++++++++++++++++-------------------------------- 2 files changed, 69 insertions(+), 102 deletions(-) diff --git a/av/format.pyi b/av/format.pyi index c3506ed51..b30a84bf6 100644 --- a/av/format.pyi +++ b/av/format.pyi @@ -1,53 +1,42 @@ -__all__ = ("ContainerFormat", "formats_available") +__all__ = ("Flags", "ContainerFormat", "formats_available") -from typing import Literal +from enum import Flag +from typing import ClassVar, Literal -from .enum import EnumFlag - -class Flags(EnumFlag): - NOFILE: int - NEEDNUMBER: int - SHOW_IDS: int - GLOBALHEADER: int - NOTIMESTAMPS: int - GENERIC_INDEX: int - TS_DISCONT: int - VARIABLE_FPS: int - NODIMENSIONS: int - NOSTREAMS: int - NOBINSEARCH: int - NOGENSEARCH: int - NO_BYTE_SEEK: int - ALLOW_FLUSH: int - TS_NONSTRICT: int - TS_NEGATIVE: int - SEEK_TO_PTS: int +class Flags(Flag): + no_file: ClassVar[Flags] + need_number: ClassVar[Flags] + show_ids: ClassVar[Flags] + global_header: ClassVar[Flags] + no_timestamps: ClassVar[Flags] + generic_index: ClassVar[Flags] + ts_discont: ClassVar[Flags] + variable_fps: ClassVar[Flags] + no_dimensions: ClassVar[Flags] + no_streams: ClassVar[Flags] + no_bin_search: ClassVar[Flags] + no_gen_search: ClassVar[Flags] + no_byte_seek: ClassVar[Flags] + allow_flush: ClassVar[Flags] + ts_nonstrict: ClassVar[Flags] + ts_negative: ClassVar[Flags] + seek_to_pts: ClassVar[Flags] class ContainerFormat: def __init__(self, name: str, mode: Literal["r", "w"] | None = None) -> None: ... - name: str - long_name: str - is_input: bool - is_output: bool - extensions: set[str] - - # flags - no_file: int - need_number: int - show_ids: int - global_header: int - no_timestamps: int - generic_index: int - ts_discont: int - variable_fps: int - no_dimensions: int - no_streams: int - no_bin_search: int - no_gen_search: int - no_byte_seek: int - allow_flush: int - ts_nonstrict: int - ts_negative: int - seek_to_pts: int + @property + def name(self) -> str: ... + @property + def long_name(self) -> str: ... + @property + def is_input(self) -> bool: ... + @property + def is_output(self) -> bool: ... + @property + def extensions(self) -> set[str]: ... + @property + def flags(self) -> int: ... + @property + def no_file(self) -> bool: ... formats_available: set[str] diff --git a/av/format.pyx b/av/format.pyx index 06b533cf4..464e34f49 100644 --- a/av/format.pyx +++ b/av/format.pyx @@ -1,7 +1,8 @@ cimport libav as lib from av.descriptor cimport wrap_avclass -from av.enum cimport define_enum + +from enum import Flag cdef object _cinit_bypass_sentinel = object() @@ -16,41 +17,28 @@ cdef ContainerFormat build_container_format(lib.AVInputFormat* iptr, lib.AVOutpu return format -Flags = define_enum("Flags", __name__, ( - ("NOFILE", lib.AVFMT_NOFILE), - ("NEEDNUMBER", lib.AVFMT_NEEDNUMBER, "Needs '%d' in filename."), - ("SHOW_IDS", lib.AVFMT_SHOW_IDS, "Show format stream IDs numbers."), - ("GLOBALHEADER", lib.AVFMT_GLOBALHEADER, "Format wants global header."), - ("NOTIMESTAMPS", lib.AVFMT_NOTIMESTAMPS, "Format does not need / have any timestamps."), - ("GENERIC_INDEX", lib.AVFMT_GENERIC_INDEX, "Use generic index building code."), - ("TS_DISCONT", lib.AVFMT_TS_DISCONT, - """Format allows timestamp discontinuities. - Note, muxers always require valid (monotone) timestamps"""), - ("VARIABLE_FPS", lib.AVFMT_VARIABLE_FPS, "Format allows variable fps."), - ("NODIMENSIONS", lib.AVFMT_NODIMENSIONS, "Format does not need width/height"), - ("NOSTREAMS", lib.AVFMT_NOSTREAMS, "Format does not require any streams"), - ("NOBINSEARCH", lib.AVFMT_NOBINSEARCH, - "Format does not allow to fall back on binary search via read_timestamp"), - ("NOGENSEARCH", lib.AVFMT_NOGENSEARCH, - "Format does not allow to fall back on generic search"), - ("NO_BYTE_SEEK", lib.AVFMT_NO_BYTE_SEEK, "Format does not allow seeking by bytes"), - ("ALLOW_FLUSH", lib.AVFMT_ALLOW_FLUSH, - """Format allows flushing. If not set, the muxer will not receive a NULL - packet in the write_packet function."""), - ("TS_NONSTRICT", lib.AVFMT_TS_NONSTRICT, - """Format does not require strictly increasing timestamps, but they must - still be monotonic."""), - ("TS_NEGATIVE", lib.AVFMT_TS_NEGATIVE, - """Format allows muxing negative timestamps. If not set the timestamp - will be shifted in av_write_frame and av_interleaved_write_frame so they - start from 0. The user or muxer can override this through - AVFormatContext.avoid_negative_ts"""), - ("SEEK_TO_PTS", lib.AVFMT_SEEK_TO_PTS, "Seeking is based on PTS"), -), is_flags=True) - +class Flags(Flag): + no_file = lib.AVFMT_NOFILE + need_number: "Needs '%d' in filename." = lib.AVFMT_NEEDNUMBER + show_ids: "Show format stream IDs numbers." = lib.AVFMT_SHOW_IDS + global_header: "Format wants global header." = lib.AVFMT_GLOBALHEADER + no_timestamps: "Format does not need / have any timestamps." = lib.AVFMT_NOTIMESTAMPS + generic_index: "Use generic index building code." = lib.AVFMT_GENERIC_INDEX + ts_discont: "Format allows timestamp discontinuities" = lib.AVFMT_TS_DISCONT + variable_fps: "Format allows variable fps." = lib.AVFMT_VARIABLE_FPS + no_dimensions: "Format does not need width/height" = lib.AVFMT_NODIMENSIONS + no_streams: "Format does not require any streams" = lib.AVFMT_NOSTREAMS + no_bin_search: "Format does not allow to fall back on binary search via read_timestamp" = lib.AVFMT_NOBINSEARCH + no_gen_search: "Format does not allow to fall back on generic search" = lib.AVFMT_NOGENSEARCH + no_byte_seek: "Format does not allow seeking by bytes" = lib.AVFMT_NO_BYTE_SEEK + allow_flush: "Format allows flushing. If not set, the muxer will not receive a NULL packet in the write_packet function." = lib.AVFMT_ALLOW_FLUSH + ts_nonstrict: "Format does not require strictly increasing timestamps, but they must still be monotonic." = lib.AVFMT_TS_NONSTRICT + ts_negative: "Format allows muxing negative timestamps." = lib.AVFMT_TS_NEGATIVE + # If not set the timestamp will be shifted in `av_write_frame()` and `av_interleaved_write_frame()` + # so they start from 0. The user or muxer can override this through AVFormatContext.avoid_negative_ts + seek_to_pts: "Seeking is based on PTS" = lib.AVFMT_SEEK_TO_PTS cdef class ContainerFormat: - """Descriptor of a container format. :param str name: The name of the format. @@ -63,12 +51,11 @@ cdef class ContainerFormat: if name is _cinit_bypass_sentinel: return - # We need to hold onto the original name because AVInputFormat.name - # is actually comma-seperated, and so we need to remember which one - # this was. + # We need to hold onto the original name because AVInputFormat.name is + # actually comma-separated, and so we need to remember which one this was. self.name = name - # Searches comma-seperated names. + # Searches comma-separated names. if mode is None or mode == "r": self.iptr = lib.av_find_input_format(name) @@ -135,30 +122,21 @@ cdef class ContainerFormat: exts.update(self.optr.extensions.split(",")) return exts - @Flags.property + @property def flags(self): + """ + Get the flags bitmask for the format. + + :rtype: int + """ return ( (self.iptr.flags if self.iptr else 0) | (self.optr.flags if self.optr else 0) ) - no_file = flags.flag_property("NOFILE") - need_number = flags.flag_property("NEEDNUMBER") - show_ids = flags.flag_property("SHOW_IDS") - global_header = flags.flag_property("GLOBALHEADER") - no_timestamps = flags.flag_property("NOTIMESTAMPS") - generic_index = flags.flag_property("GENERIC_INDEX") - ts_discont = flags.flag_property("TS_DISCONT") - variable_fps = flags.flag_property("VARIABLE_FPS") - no_dimensions = flags.flag_property("NODIMENSIONS") - no_streams = flags.flag_property("NOSTREAMS") - no_bin_search = flags.flag_property("NOBINSEARCH") - no_gen_search = flags.flag_property("NOGENSEARCH") - no_byte_seek = flags.flag_property("NO_BYTE_SEEK") - allow_flush = flags.flag_property("ALLOW_FLUSH") - ts_nonstrict = flags.flag_property("TS_NONSTRICT") - ts_negative = flags.flag_property("TS_NEGATIVE") - seek_to_pts = flags.flag_property("SEEK_TO_PTS") + @property + def no_file(self): + return bool(self.flags & lib.AVFMT_NOFILE) cdef get_output_format_names(): From f00facb88df686f6ad6c076be40e055e8ed9dd5c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 1 Dec 2024 15:45:39 -0500 Subject: [PATCH 436/846] Remove our custom Enum class --- av/codec/codec.pyi | 1 - av/enum.pxd | 4 - av/enum.pyi | 68 --------- av/enum.pyx | 296 --------------------------------------- av/error.pxd | 2 - av/error.pyi | 41 ------ av/error.pyx | 164 ++++++++++++++++------ av/video/reformatter.pyi | 8 +- docs/api/codec.rst | 12 +- docs/api/enum.rst | 24 ---- docs/api/error.rst | 38 +---- docs/api/error_table.py | 25 ---- tests/test_enums.py | 223 ----------------------------- tests/test_errors.py | 6 +- 14 files changed, 136 insertions(+), 776 deletions(-) delete mode 100644 av/enum.pxd delete mode 100644 av/enum.pyi delete mode 100644 av/enum.pyx delete mode 100644 docs/api/enum.rst delete mode 100644 docs/api/error_table.py delete mode 100644 tests/test_enums.py diff --git a/av/codec/codec.pyi b/av/codec/codec.pyi index 3323895ed..32736c080 100644 --- a/av/codec/codec.pyi +++ b/av/codec/codec.pyi @@ -5,7 +5,6 @@ from typing import ClassVar, Literal, overload from av.audio.codeccontext import AudioCodecContext from av.audio.format import AudioFormat from av.descriptor import Descriptor -from av.enum import EnumFlag from av.subtitles.codeccontext import SubtitleCodecContext from av.video.codeccontext import VideoCodecContext from av.video.format import VideoFormat diff --git a/av/enum.pxd b/av/enum.pxd deleted file mode 100644 index 884fdd961..000000000 --- a/av/enum.pxd +++ /dev/null @@ -1,4 +0,0 @@ -cpdef define_enum( - name, module, items, - bint is_flags=* -) diff --git a/av/enum.pyi b/av/enum.pyi deleted file mode 100644 index 026a176b3..000000000 --- a/av/enum.pyi +++ /dev/null @@ -1,68 +0,0 @@ -from typing import Any, Callable, Iterable, Literal, Sequence, overload - -class EnumType(type): - def __init__( - self, - name: str, - bases: tuple[type, ...], - attrs: dict[str, Any], - items: Iterable[tuple[str, Any, str | None, bool]], - ) -> None: ... - def _create( - self, name: str, value: int, doc: str | None = None, by_value_only: bool = False - ) -> None: ... - def __len__(self) -> None: ... - def __iter__(self) -> None: ... - def __getitem__(self, key: str | int | EnumType) -> None: ... - def _get(self, value: int, create: bool = False) -> None: ... - def _get_multi_flags(self, value: int) -> None: ... - def get( - self, - key: str | int | EnumType, - default: int | None = None, - create: bool = False, - ) -> int | None: ... - -class EnumItem: - @property - def name(self) -> str: ... - @property - def value(self) -> int: ... - def __int__(self) -> int: ... - def __hash__(self) -> int: ... - def __reduce__( - self, - ) -> tuple[Callable[[str, str, str], EnumItem], tuple[str, str, str]]: ... - def __eq__(self, other: object) -> bool: ... - def __ne__(self, other: object) -> bool: ... - -class EnumFlag(EnumItem): - flags: tuple[EnumFlag] - - def __and__(self, other: object) -> EnumFlag: ... - def __or__(self, other: object) -> EnumFlag: ... - def __xor__(self, other: object) -> EnumFlag: ... - def __invert__(self) -> bool: ... - def __nonzero__(self) -> bool: ... - -@overload -def define_enum( - name: str, - module: str, - items: Sequence[tuple[str, int] | None], - is_flags: Literal[True], -) -> EnumFlag: ... -@overload -def define_enum( - name: str, - module: str, - items: Sequence[tuple[str, int] | None], - is_flags: Literal[False], -) -> EnumItem: ... -@overload -def define_enum( - name: str, - module: str, - items: Sequence[tuple[str, int] | None], - is_flags: bool = False, -) -> EnumItem | EnumFlag: ... diff --git a/av/enum.pyx b/av/enum.pyx deleted file mode 100644 index 802a731ff..000000000 --- a/av/enum.pyx +++ /dev/null @@ -1,296 +0,0 @@ -""" - -PyAV provides enumeration and flag classes that are similar to the stdlib ``enum`` -module that shipped with Python 3.4. - -PyAV's enums are a little more forgiving to preserve backwards compatibility -with earlier PyAV patterns. e.g., they can be freely compared to strings or -integers for names and values respectively. - -""" - -import copyreg - - -cdef sentinel = object() - - -class EnumType(type): - def __new__(mcl, name, bases, attrs, *args): - # Just adapting the method signature. - return super().__new__(mcl, name, bases, attrs) - - def __init__(self, name, bases, attrs, items): - self._by_name = {} - self._by_value = {} - self._all = [] - - for spec in items: - self._create(*spec) - - def _create(self, name, value, doc=None, by_value_only=False): - # We only have one instance per value. - try: - item = self._by_value[value] - except KeyError: - item = self(sentinel, name, value, doc) - self._by_value[value] = item - - if not by_value_only: - setattr(self, name, item) - self._all.append(item) - self._by_name[name] = item - - return item - - def __len__(self): - return len(self._all) - - def __iter__(self): - return iter(self._all) - - def __getitem__(self, key): - if isinstance(key, str): - return self._by_name[key] - - if isinstance(key, int): - try: - return self._by_value[key] - except KeyError: - pass - - if issubclass(self, EnumFlag): - return self._get_multi_flags(key) - - raise KeyError(key) - - if isinstance(key, self): - return key - - raise TypeError(f"{self.__name__} indices must be str, int, or itself") - - def _get(self, long value, bint create=False): - try: - return self._by_value[value] - except KeyError: - pass - - if not create: - return - - return self._create(f"{self.__name__.upper()}_{value}", value, by_value_only=True) - - def _get_multi_flags(self, long value): - try: - return self._by_value[value] - except KeyError: - pass - - flags = [] - cdef long to_find = value - for item in self: - if item.value & to_find: - flags.append(item) - to_find = to_find ^ item.value - if not to_find: - break - - if to_find: - raise KeyError(value) - - name = "|".join(f.name for f in flags) - cdef EnumFlag combo = self._create(name, value, by_value_only=True) - combo.flags = tuple(flags) - - return combo - - def get(self, key, default=None, create=False): - try: - return self[key] - except KeyError: - if create: - return self._get(key, create=True) - return default - - def property(self, *args, **kwargs): - return EnumProperty(self, *args, **kwargs) - - -def _unpickle(mod_name, cls_name, item_name): - mod = __import__(mod_name, fromlist=["."]) - cls = getattr(mod, cls_name) - return cls[item_name] - - -copyreg.constructor(_unpickle) - - -cdef class EnumItem: - cdef readonly str name - cdef readonly int value - cdef Py_hash_t _hash - - def __cinit__(self, sentinel_, str name, int value, doc=None): - if sentinel_ is not sentinel: - raise RuntimeError(f"Cannot instantiate {self.__class__.__name__}.") - - self.name = name - self.value = value - self.__doc__ = doc - - # We need to establish a hash that doesn't collide with anything that - # would return true from `__eq__`. This is because these enums (vs - # the stdlib ones) are weakly typed (they will compare against string - # names and int values), and if we have the same hash AND are equal, - # then they will be equivalent as keys in a dictionary, which is wierd. - cdef Py_hash_t hash_ = value + 1 - if hash_ == hash(name): - hash_ += 1 - self._hash = hash_ - - def __repr__(self): - return f"<{self.__class__.__module__}.{self.__class__.__name__}:{self.name}(0x{self.value:x})>" - - def __str__(self): - return self.name - - def __int__(self): - return self.value - - def __hash__(self): - return self._hash - - def __reduce__(self): - return (_unpickle, (self.__class__.__module__, self.__class__.__name__, self.name)) - - def __eq__(self, other): - if isinstance(other, str): - if self.name == other: - return True - - try: - other_inst = self.__class__._by_name[other] - except KeyError: - raise ValueError( - f"{self.__class__.__name__} does not have item named {other!r}" - ) - else: - return self is other_inst - - if isinstance(other, int): - if self.value == other: - return True - if other in self.__class__._by_value: - return False - raise ValueError( - f"{self.__class__.__name__} does not have item valued {other}" - ) - - if isinstance(other, self.__class__): - return self is other - - raise TypeError( - f"'==' not supported between {self.__class__.__name__} and {type(other).__name__}" - ) - - def __ne__(self, other): - return not (self == other) - - -cdef class EnumFlag(EnumItem): - cdef readonly tuple flags - - def __cinit__(self, sentinel, name, value, doc=None): - self.flags = (self, ) - - def __and__(self, other): - if not isinstance(other, int): - other = self.__class__[other].value - value = self.value & other - return self.__class__._get_multi_flags(value) - - def __or__(self, other): - if not isinstance(other, int): - other = self.__class__[other].value - value = self.value | other - return self.__class__._get_multi_flags(value) - - def __xor__(self, other): - if not isinstance(other, int): - other = self.__class__[other].value - value = self.value ^ other - return self.__class__._get_multi_flags(value) - - def __invert__(self): - # This can't result in a flag, but is helpful. - return ~self.value - - def __nonzero__(self): - return bool(self.value) - - -cdef class EnumProperty: - cdef object enum - cdef object fget - cdef object fset - cdef public __doc__ - - def __init__(self, enum, fget, fset=None, doc=None): - self.enum = enum - self.fget = fget - self.fset = fset - self.__doc__ = doc or fget.__doc__ - - def setter(self, fset): - self.fset = fset - return self - - def __get__(self, inst, owner): - if inst is not None: - value = self.fget(inst) - return self.enum.get(value, create=True) - else: - return self - - def __set__(self, inst, value): - item = self.enum.get(value) - self.fset(inst, item.value) - - def flag_property(self, name, doc=None): - - item = self.enum[name] - cdef int item_value = item.value - - class Property(property): - pass - - @Property - def _property(inst): - return bool(self.fget(inst) & item_value) - - if self.fset: - @_property.setter - def _property(inst, value): - if value: - flags = self.fget(inst) | item_value - else: - flags = self.fget(inst) & ~item_value - self.fset(inst, flags) - - _property.__doc__ = doc or item.__doc__ - _property._enum_item = item - - return _property - - -cpdef define_enum(name, module, items, bint is_flags=False): - - if is_flags: - base_cls = EnumFlag - else: - base_cls = EnumItem - - # Some items may be None if they correspond to an unsupported FFmpeg feature - cls = EnumType(name, (base_cls, ), {"__module__": module}, [i for i in items if i is not None]) - - return cls diff --git a/av/error.pxd b/av/error.pxd index 836300d22..d9a542a36 100644 --- a/av/error.pxd +++ b/av/error.pxd @@ -1,5 +1,3 @@ cdef int stash_exception(exc_info=*) - cpdef int err_check(int res, filename=*) except -1 -cpdef make_error(int res, filename=*, log=*) diff --git a/av/error.pyi b/av/error.pyi index 4c41f35e7..abbe2188c 100644 --- a/av/error.pyi +++ b/av/error.pyi @@ -1,58 +1,17 @@ import builtins from enum import Enum -from .enum import EnumItem - classes: dict[int, Exception] def code_to_tag(code: int) -> bytes: ... def tag_to_code(tag: bytes) -> int: ... -def make_error( - res: int, - filename: str | None = None, - log: tuple[int, tuple[int, str, str] | None] | None = None, -) -> None: ... def err_check(res: int, filename: str | None = None) -> int: ... -BUFFER_TOO_SMALL: EnumItem - -class ErrorType(EnumItem, Enum): - BSF_NOT_FOUND: EnumItem - BUG: EnumItem - BUFFER_TOO_SMALL: EnumItem - DECODER_NOT_FOUND: EnumItem - DEMUXER_NOT_FOUND: EnumItem - ENCODER_NOT_FOUND: EnumItem - EOF: EnumItem - EXIT: EnumItem - EXTERNAL: EnumItem - FILTER_NOT_FOUND: EnumItem - INVALIDDATA: EnumItem - MUXER_NOT_FOUND: EnumItem - OPTION_NOT_FOUND: EnumItem - PATCHWELCOME: EnumItem - PROTOCOL_NOT_FOUND: EnumItem - UNKNOWN: EnumItem - EXPERIMENTAL: EnumItem - INPUT_CHANGED: EnumItem - OUTPUT_CHANGED: EnumItem - HTTP_BAD_REQUEST: EnumItem - HTTP_UNAUTHORIZED: EnumItem - HTTP_FORBIDDEN: EnumItem - HTTP_NOT_FOUND: EnumItem - HTTP_OTHER_4XX: EnumItem - HTTP_SERVER_ERROR: EnumItem - PYAV_CALLBACK: EnumItem - - @property - def tag(self) -> bytes: ... - class FFmpegError(Exception): errno: int strerror: str filename: str log: tuple[int, tuple[int, str, str] | None] - type: ErrorType def __init__( self, diff --git a/av/error.pyx b/av/error.pyx index 97fab0e41..426d01ed0 100644 --- a/av/error.pyx +++ b/av/error.pyx @@ -1,4 +1,5 @@ cimport libav as lib +from libc.stdlib cimport free, malloc from av.logging cimport get_last_error @@ -8,8 +9,6 @@ import sys import traceback from threading import local -from av.enum import define_enum - # Will get extended with all of the exceptions. __all__ = [ "ErrorType", "FFmpegError", "LookupError", "HTTPError", "HTTPClientError", @@ -63,10 +62,6 @@ class FFmpegError(Exception): The filename that was being operated on (if available). - .. attribute:: type - - The :class:`av.error.ErrorType` enum value for the error type. - .. attribute:: log The tuple from :func:`av.logging.get_last_log`, or ``None``. @@ -74,6 +69,9 @@ class FFmpegError(Exception): """ def __init__(self, code, message, filename=None, log=None): + self.errno = code + self.strerror = message + args = [code, message] if filename or log: args.append(filename) @@ -81,15 +79,6 @@ class FFmpegError(Exception): args.append(log) super(FFmpegError, self).__init__(*args) self.args = tuple(args) # FileNotFoundError/etc. only pulls 2 args. - self.type = ErrorType.get(code, create=True) - - @property - def errno(self): - return self.args[0] - - @property - def strerror(self): - return self.args[1] @property def filename(self): @@ -165,12 +154,77 @@ _ffmpeg_specs = ( ("PYAV_CALLBACK", c_PYAV_STASHED_ERROR, "PyAVCallbackError", RuntimeError), ) +cdef sentinel = object() + + +class EnumType(type): + def __new__(mcl, name, bases, attrs, *args): + # Just adapting the method signature. + return super().__new__(mcl, name, bases, attrs) + + def __init__(self, name, bases, attrs, items): + self._by_name = {} + self._by_value = {} + self._all = [] + + for spec in items: + self._create(*spec) + + def _create(self, name, value, doc=None, by_value_only=False): + # We only have one instance per value. + try: + item = self._by_value[value] + except KeyError: + item = self(sentinel, name, value, doc) + self._by_value[value] = item -# The actual enum. -ErrorType = define_enum("ErrorType", __name__, [x[:2] for x in _ffmpeg_specs]) + return item -# It has to be monkey-patched. -ErrorType.__doc__ = """An enumeration of FFmpeg's error types. + def __len__(self): + return len(self._all) + + def __iter__(self): + return iter(self._all) + + def __getitem__(self, key): + if isinstance(key, str): + return self._by_name[key] + + if isinstance(key, int): + try: + return self._by_value[key] + except KeyError: + pass + + raise KeyError(key) + + if isinstance(key, self): + return key + + raise TypeError(f"{self.__name__} indices must be str, int, or itself") + + def _get(self, long value, bint create=False): + try: + return self._by_value[value] + except KeyError: + pass + + if not create: + return + + return self._create(f"{self.__name__.upper()}_{value}", value, by_value_only=True) + + def get(self, key, default=None, create=False): + try: + return self[key] + except KeyError: + if create: + return self._get(key, create=True) + return default + + +cdef class EnumItem: + """An enumeration of FFmpeg's error types. .. attribute:: tag @@ -181,7 +235,32 @@ ErrorType.__doc__ = """An enumeration of FFmpeg's error types. The error message that would be returned. """ -ErrorType.tag = property(lambda self: code_to_tag(self.value)) + cdef readonly str name + cdef readonly int value + + def __cinit__(self, sentinel_, str name, int value, doc=None): + if sentinel_ is not sentinel: + raise RuntimeError(f"Cannot instantiate {self.__class__.__name__}.") + + self.name = name + self.value = value + self.__doc__ = doc + + def __repr__(self): + return f"<{self.__class__.__module__}.{self.__class__.__name__}:{self.name}(0x{self.value:x})>" + + def __str__(self): + return self.name + + def __int__(self): + return self.value + + @property + def tag(self): + return code_to_tag(self.value) + + +ErrorType = EnumType("ErrorType", (EnumItem, ), {"__module__": __name__}, [x[:2] for x in _ffmpeg_specs]) for enum in ErrorType: @@ -274,6 +353,8 @@ for enum_name, code, name, base in _ffmpeg_specs: globals()[name] = cls __all__.append(name) +del _ffmpeg_specs + # Storage for stashing. cdef object _local = local() @@ -298,6 +379,27 @@ cdef int stash_exception(exc_info=None): cdef int _last_log_count = 0 + +cpdef make_error(int res, filename=None, log=None): + cdef int code = -res + cdef char* error_buffer = malloc(lib.AV_ERROR_MAX_STRING_SIZE * sizeof(char)) + if error_buffer == NULL: + raise MemoryError() + + try: + if code == c_PYAV_STASHED_ERROR: + message = PYAV_STASHED_ERROR_message + else: + lib.av_strerror(res, error_buffer, lib.AV_ERROR_MAX_STRING_SIZE) + # Fallback to OS error string if no message + message = error_buffer or os.strerror(code) + + cls = classes.get(code, UndefinedError) + return cls(code, message, filename, log) + finally: + free(error_buffer) + + cpdef int err_check(int res, filename=None) except -1: """Raise appropriate exceptions from library return code.""" @@ -329,25 +431,3 @@ cpdef int err_check(int res, filename=None) except -1: class UndefinedError(FFmpegError): """Fallback exception type in case FFmpeg returns an error we don't know about.""" pass - - -cpdef make_error(int res, filename=None, log=None): - cdef int code = -res - cdef bytes py_buffer - cdef char *c_buffer - - if code == c_PYAV_STASHED_ERROR: - message = PYAV_STASHED_ERROR_message - else: - # Jump through some hoops due to Python 2 in same codebase. - py_buffer = b"\0" * lib.AV_ERROR_MAX_STRING_SIZE - c_buffer = py_buffer - lib.av_strerror(res, c_buffer, lib.AV_ERROR_MAX_STRING_SIZE) - py_buffer = c_buffer - message = py_buffer.decode("latin1") - - # Default to the OS if we have no message; this should not get called. - message = message or os.strerror(code) - - cls = classes.get(code, UndefinedError) - return cls(code, message, filename, log) diff --git a/av/video/reformatter.pyi b/av/video/reformatter.pyi index abd545332..a601dd335 100644 --- a/av/video/reformatter.pyi +++ b/av/video/reformatter.pyi @@ -1,8 +1,8 @@ -from av.enum import EnumItem +from enum import IntEnum from .frame import VideoFrame -class Interpolation(EnumItem): +class Interpolation(IntEnum): FAST_BILINEAER: int BILINEAR: int BICUBIC: int @@ -15,7 +15,7 @@ class Interpolation(EnumItem): LANCZOS: int SPLINE: int -class Colorspace(EnumItem): +class Colorspace(IntEnum): ITU709: int FCC: int ITU601: int @@ -30,7 +30,7 @@ class Colorspace(EnumItem): smpte240: int default: int -class ColorRange(EnumItem): +class ColorRange(IntEnum): UNSPECIFIED: int MPEG: int JPEG: int diff --git a/docs/api/codec.rst b/docs/api/codec.rst index d4a241425..7e06ff8cc 100644 --- a/docs/api/codec.rst +++ b/docs/api/codec.rst @@ -90,8 +90,8 @@ Transcoding .. automethod:: CodecContext.flush_buffers -Flags -~~~~~ +Enums and Flags +~~~~~~~~~~~~~~~ .. autoattribute:: CodecContext.flags @@ -107,10 +107,6 @@ Flags .. enumtable:: av.codec.context:Flags2 :class: av.codec.context:CodecContext - -Enums -~~~~~ - .. autoclass:: av.codec.context.ThreadType Which multithreading methods to use. @@ -119,8 +115,4 @@ Enums .. enumtable:: av.codec.context.ThreadType -.. autoclass:: av.codec.context.SkipType - - .. enumtable:: av.codec.context.SkipType - diff --git a/docs/api/enum.rst b/docs/api/enum.rst deleted file mode 100644 index 5fdcec4f7..000000000 --- a/docs/api/enum.rst +++ /dev/null @@ -1,24 +0,0 @@ - -Enumerations and Flags -====================== - -.. currentmodule:: av.enum - -.. automodule:: av.enum - - -.. _enums: - -Enumerations ------------- - -.. autoclass:: EnumItem - - -.. _flags: - -Flags ------ - -.. autoclass:: EnumFlag - diff --git a/docs/api/error.rst b/docs/api/error.rst index 3f82f4ec2..557ab188e 100644 --- a/docs/api/error.rst +++ b/docs/api/error.rst @@ -5,39 +5,18 @@ Errors .. _error_behaviour: -General Behaviour +General Behavior ----------------- When PyAV encounters an FFmpeg error, it raises an appropriate exception. FFmpeg has a couple dozen of its own error types which we represent via -:ref:`error_classes` and at a lower level via :ref:`error_types`. +:ref:`error_classes`. FFmpeg will also return more typical errors such as ``ENOENT`` or ``EAGAIN``, which we do our best to translate to extensions of the builtin exceptions as defined by -`PEP 3151 `_ -(and fall back onto ``OSError`` if using Python < 3.3). - - -.. _error_types: - -Error Type Enumerations ------------------------ - -We provide :class:`av.error.ErrorType` as an enumeration of the various FFmpeg errors. -To mimick the stdlib ``errno`` module, all enumeration values are available in -the ``av.error`` module, e.g.:: - - try: - do_something() - except OSError as e: - if e.errno != av.error.FILTER_NOT_FOUND: - raise - handle_error() - - -.. autoclass:: av.error.ErrorType +`PEP 3151 `_. .. _error_classes: @@ -55,7 +34,7 @@ There are two competing ideas that have influenced the final design: 2. We want to use the builtin exceptions whenever possible. -As such, PyAV effectivly shadows as much of the builtin exception heirarchy as +As such, PyAV effectively shadows as much of the builtin exception hierarchy as it requires, extending from both the builtins and from :class:`FFmpegError`. Therefore, an argument error within FFmpeg will raise a ``av.error.ValueError``, which @@ -74,12 +53,3 @@ All of these exceptions are available on the top-level ``av`` package, e.g.:: .. autoclass:: av.FFmpegError - -Mapping Codes and Classes -------------------------- - -Here is how the classes line up with the error codes/enumerations: - -.. include:: ../_build/rst/api/error_table.rst - - diff --git a/docs/api/error_table.py b/docs/api/error_table.py deleted file mode 100644 index 3a1b5f219..000000000 --- a/docs/api/error_table.py +++ /dev/null @@ -1,25 +0,0 @@ -import av - - -rows = [("Exception Class", "Code/Enum Name", "FFmpeg Error Message")] - -for code, cls in av.error.classes.items(): - enum = av.error.ErrorType.get(code) - - if not enum: - continue - - if enum.tag == b"PyAV": - continue - - rows.append((f"``av.{cls.__name__}``", f"``av.error.{enum.name}``", enum.strerror)) - -lens = [max(len(row[i]) for row in rows) for i in range(len(rows[0]))] - -header = tuple("=" * x for x in lens) -rows.insert(0, header) -rows.insert(2, header) -rows.append(header) - -for row in rows: - print(" ".join("{:{}s}".format(cell, len_) for cell, len_ in zip(row, lens))) diff --git a/tests/test_enums.py b/tests/test_enums.py deleted file mode 100644 index 64b76d9eb..000000000 --- a/tests/test_enums.py +++ /dev/null @@ -1,223 +0,0 @@ -import pickle - -import pytest - -from av.enum import EnumType, define_enum - -PickleableFooBar = define_enum("PickleableFooBar", __name__, [("FOO", 1)]) - - -def define_foobar(is_flags: bool = False): - return define_enum("Foobar", __name__, (("FOO", 1), ("BAR", 2)), is_flags=is_flags) - - -def test_basics(): - cls = define_foobar() - - assert isinstance(cls, EnumType) - - foo = cls.FOO - - assert isinstance(foo, cls) - assert foo.name == "FOO" and foo.value == 1 - assert not isinstance(foo, PickleableFooBar) - - -def test_access(): - cls = define_foobar() - foo1 = cls.FOO - foo2 = cls["FOO"] - foo3 = cls[1] - foo4 = cls[foo1] - assert foo1 is foo2 - assert foo1 is foo3 - assert foo1 is foo4 - - assert foo1 in cls and "FOO" in cls and 1 in cls - - pytest.raises(KeyError, lambda: cls["not a foo"]) - pytest.raises(KeyError, lambda: cls[10]) - pytest.raises(TypeError, lambda: cls[()]) - - assert cls.get("FOO") == foo1 - assert cls.get("not a foo") is None - - -def test_casting(): - cls = define_foobar() - foo = cls.FOO - - assert repr(foo) == "" - - str_foo = str(foo) - assert isinstance(str_foo, str) and str_foo == "FOO" - - int_foo = int(foo) - assert isinstance(int_foo, int) and int_foo == 1 - - -def test_iteration(): - cls = define_foobar() - assert list(cls) == [cls.FOO, cls.BAR] - - -def test_equality(): - cls = define_foobar() - foo = cls.FOO - bar = cls.BAR - - assert foo == "FOO" and foo == 1 and foo == foo - assert foo != "BAR" and foo != 2 and foo != bar - - pytest.raises(ValueError, lambda: foo == "not a foo") - pytest.raises(ValueError, lambda: foo == 10) - pytest.raises(TypeError, lambda: foo == ()) - - -def test_as_key(): - cls = define_foobar() - foo = cls.FOO - - d = {foo: "value"} - assert d[foo] == "value" - assert d.get("FOO") is None - assert d.get(1) is None - - -def test_pickleable(): - cls = PickleableFooBar - foo = cls.FOO - - enc = pickle.dumps(foo) - - foo2 = pickle.loads(enc) - - assert foo is foo2 - - -def test_create_unknown(): - cls = define_foobar() - baz = cls.get(3, create=True) - - assert baz.name == "FOOBAR_3" - assert baz.value == 3 - - -def test_multiple_names(): - cls = define_enum( - "FFooBBar", - __name__, - (("FOO", 1), ("F", 1), ("BAR", 2), ("B", 2)), - ) - - assert cls.F is cls.FOO - - assert cls.F.name == "FOO" - assert cls.F.name != "F" # This is actually the string. - - assert cls.F == "FOO" - assert cls.F == "F" - assert cls.F != "BAR" - assert cls.F != "B" - pytest.raises(ValueError, lambda: cls.F == "x") - - -def test_flag_basics(): - cls = define_enum( - "FoobarAllFlags", - __name__, - {"FOO": 1, "BAR": 2, "FOOBAR": 3}.items(), - is_flags=True, - ) - foo = cls.FOO - bar = cls.BAR - - foobar = foo | bar - assert foobar is cls.FOOBAR - - foo2 = foobar & foo - assert foo2 is foo - - bar2 = foobar ^ foo - assert bar2 is bar - - bar3 = foobar & ~foo - assert bar3 is bar - - x = cls.FOO - x |= cls.BAR - assert x is cls.FOOBAR - - x = cls.FOOBAR - x &= cls.FOO - assert x is cls.FOO - - -def test_multi_flags_basics(): - cls = define_foobar(is_flags=True) - - foo = cls.FOO - bar = cls.BAR - foobar = foo | bar - assert foobar.name == "FOO|BAR" - assert foobar.value == 3 - assert foobar.flags == (foo, bar) - - foobar2 = foo | bar - foobar3 = cls[3] - foobar4 = cls[foobar] - - assert foobar is foobar2 - assert foobar is foobar3 - assert foobar is foobar4 - - pytest.raises(KeyError, lambda: cls["FOO|BAR"]) - - assert len(cls) == 2 # It didn't get bigger - assert list(cls) == [foo, bar] - - -def test_multi_flags_create_missing(): - cls = define_foobar(is_flags=True) - - foobar = cls[3] - assert foobar is cls.FOO | cls.BAR - - pytest.raises(KeyError, lambda: cls[4]) # Not FOO or BAR - pytest.raises(KeyError, lambda: cls[7]) # FOO and BAR and missing flag. - - -def test_properties(): - Flags = define_foobar(is_flags=True) - foobar = Flags.FOO | Flags.BAR - - class Class: - def __init__(self, value): - self.value = Flags[value].value - - @Flags.property - def flags(self): - return self.value - - @flags.setter - def flags(self, value): - self.value = value - - foo = flags.flag_property("FOO") - bar = flags.flag_property("BAR") - - obj = Class("FOO") - - assert obj.flags is Flags.FOO - assert obj.foo - assert not obj.bar - - obj.bar = True - assert obj.flags is foobar - assert obj.foo - assert obj.bar - - obj.foo = False - assert obj.flags is Flags.BAR - assert not obj.foo - assert obj.bar diff --git a/tests/test_errors.py b/tests/test_errors.py index 1cad5d086..f654c6b2e 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -55,9 +55,11 @@ def test_filenotfound(): def test_buffertoosmall() -> None: """Throw an exception from an enum.""" + + BUFFER_TOO_SMALL = 1397118274 try: - av.error.err_check(-av.error.BUFFER_TOO_SMALL.value) + av.error.err_check(-BUFFER_TOO_SMALL) except av.error.BufferTooSmallError as e: - assert e.errno == av.error.BUFFER_TOO_SMALL.value + assert e.errno == BUFFER_TOO_SMALL else: assert False, "No exception raised!" From e861bdd0f5e863541272b14c6679af637e3b68d7 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 2 Dec 2024 00:23:39 -0500 Subject: [PATCH 437/846] Update AUTHORS --- AUTHORS.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index bc4db2a5b..9caef903f 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -26,7 +26,9 @@ All contributors (by number of commits): - Dan Allan ; `@danielballan `_ - Moonsik Park - Santtu Keskinen +- Marc Mueller <30130371+cdce8p@users.noreply.github.com> - Christoph Rackwitz +- David Plowman - Alireza Davoudi ; `@adavoudi `_ - Jonathan Drolet - Moritz Kassner ; `@mkassner `_ @@ -52,8 +54,8 @@ All contributors (by number of commits): * Andrew Wason * Radek Senfeld ; `@radek-senfeld `_ * Benjamin Chrétien <2742231+bchretien@users.noreply.github.com> -* Marc Mueller <30130371+cdce8p@users.noreply.github.com> * zzjjbb <31069326+zzjjbb@users.noreply.github.com> +* davidplowman <38045873+davidplowman@users.noreply.github.com> * Hanz <40712686+HanzCEO@users.noreply.github.com> * Joe Schiff <41972063+JoeSchiff@users.noreply.github.com> * Artturin @@ -62,12 +64,12 @@ All contributors (by number of commits): * Arthur Barros * Carlos Ruiz * Carlos Ruiz -* David Plowman * Maxime Desroches * egao1980 * Eric Kalosa-Kenyon * elxy * Gemfield +* henri-gasc * Jonathan Martin * Johan Jeppsson Karlin * Philipp Klaus From e0f0ef415b0ec7dfd48e582cf1735eb3bf04829c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 2 Dec 2024 01:21:35 -0500 Subject: [PATCH 438/846] Release 14.0.0 --- CHANGELOG.rst | 9 +++++---- av/about.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d8687fa2f..8e5a1380f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -23,10 +23,10 @@ Major: - Drop FFmpeg 6. - Drop support for MacOS <11 in our binary wheels. -- Change many instances ``EnumItem`` s (our custom enum class) with Python's standard Enums. -- Remove the `template` arg from ``OutputContainer.add_stream()``. -- Remove ``CodecContext.close()`` because it's deprecated in ffmpeg. +- Deleted PyAV's custom Enum class in favor of Python's standard Enums. +- Remove ``CodecContext.close()`` and ``Stream.side_data`` because it's deprecated in ffmpeg. - Remove ``AVError`` alias (use ``FFmpegError`` directly instead). +- Remove the `template` arg from ``OutputContainer.add_stream()``. Features: @@ -34,9 +34,10 @@ Features: - Add ``OutputContainer.add_data_stream()`` by :gh-user:`WyattBlue`. - Add ``filter.loudnorm.stats()`` function that returns the stats of loudnorm for 2-pass filtering by :gh-user:`WyattBlue`. - Add ``qmin`` and ``qmax`` parameters to the ``VideoCodecContext`` by :gh-user:`davidplowman` in (:pr:`1618`). +- Allow the profile of a codec to be set as well as queried by :gh-user:`davidplowman` in (:pr:`1625`). Fixes: - +- Make ``VideoFrame.from_numpy_buffer()`` support buffers with padding by :gh-user:`davidplowman` in (:pr:`1635`). - Correct ``Colorspace``'s lowercase enums. - Updated ``sidedata.Type`` enum. - Ensure streams in StreamContainer are released. Fixes :issue:`1599`. diff --git a/av/about.py b/av/about.py index 73437bbc8..229469150 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "14.0.0rc2" +__version__ = "14.0.0" From 9560bb8674e3f3e0d164c5c667e0064a0a057d7c Mon Sep 17 00:00:00 2001 From: Mark Harfouche Date: Mon, 2 Dec 2024 10:59:53 -0500 Subject: [PATCH 439/846] Ensure to add the loudnorm_impl.h in the pypi source distribution Thanks! --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 321b65e6d..6539f496d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,5 +3,5 @@ recursive-include av *.pyx *.pxd recursive-include docs *.rst *.py recursive-include examples *.py recursive-include include *.pxd *.h -recursive-include src/av *.c +recursive-include src/av *.c *.h recursive-include tests *.py From bf03325c967e6102e26734dcde813902e32b977a Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 4 Dec 2024 01:01:29 -0500 Subject: [PATCH 440/846] Address #1663 --- av/video/reformatter.pyi | 3 ++- av/video/reformatter.pyx | 24 +++++++++++++++++++----- tests/test_videoframe.py | 29 +++++++++++++++++++++++++++-- 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/av/video/reformatter.pyi b/av/video/reformatter.pyi index a601dd335..fd5dbd053 100644 --- a/av/video/reformatter.pyi +++ b/av/video/reformatter.pyi @@ -27,7 +27,8 @@ class Colorspace(IntEnum): fcc: int itu601: int itu624: int - smpte240: int + smpte170m: int + smpte240m: int default: int class ColorRange(IntEnum): diff --git a/av/video/reformatter.pyx b/av/video/reformatter.pyx index 4511d08af..538cc4953 100644 --- a/av/video/reformatter.pyx +++ b/av/video/reformatter.pyx @@ -45,6 +45,19 @@ class ColorRange(IntEnum): NB: "Not part of ABI" = lib.AVCOL_RANGE_NB +def _resolve_enum_value(value, enum_class, default): + # Helper function to resolve enum values from different input types. + if value is None: + return default + if isinstance(value, enum_class): + return value.value + if isinstance(value, int): + return value + if isinstance(value, str): + return enum_class[value].value + raise ValueError(f"Cannot convert {value} to {enum_class.__name__}") + + cdef class VideoReformatter: """An object for reformatting size and pixel format of :class:`.VideoFrame`. @@ -83,11 +96,12 @@ cdef class VideoReformatter: """ cdef VideoFormat video_format = VideoFormat(format if format is not None else frame.format) - cdef int c_src_colorspace = (Colorspace[src_colorspace].value if src_colorspace is not None else frame.colorspace) - cdef int c_dst_colorspace = (Colorspace[dst_colorspace].value if dst_colorspace is not None else frame.colorspace) - cdef int c_interpolation = (Interpolation[interpolation] if interpolation is not None else Interpolation.BILINEAR).value - cdef int c_src_color_range = (ColorRange[src_color_range].value if src_color_range is not None else 0) - cdef int c_dst_color_range = (ColorRange[dst_color_range].value if dst_color_range is not None else 0) + + cdef int c_src_colorspace = _resolve_enum_value(src_colorspace, Colorspace, frame.colorspace) + cdef int c_dst_colorspace = _resolve_enum_value(dst_colorspace, Colorspace, frame.colorspace) + cdef int c_interpolation = _resolve_enum_value(interpolation, Interpolation, int(Interpolation.BILINEAR)) + cdef int c_src_color_range = _resolve_enum_value(src_color_range, ColorRange, 0) + cdef int c_dst_color_range = _resolve_enum_value(dst_color_range, ColorRange, 0) return self._reformat( frame, diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index c93a12e32..f044be949 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -7,6 +7,7 @@ import av from av import VideoFrame +from av.video.reformatter import ColorRange, Colorspace, Interpolation from .common import ( TestCase, @@ -145,6 +146,24 @@ def test_roundtrip(self) -> None: img.save(self.sandboxed("roundtrip-high.jpg")) assertImagesAlmostEqual(image, img) + def test_interpolation(self) -> None: + import PIL.Image as Image + + image = Image.open(fate_png()) + frame = VideoFrame.from_image(image) + assert frame.width == 330 and frame.height == 330 + + img = frame.to_image(width=200, height=100, interpolation=Interpolation.BICUBIC) + assert img.width == 200 and img.height == 100 + + img = frame.to_image(width=200, height=100, interpolation="BICUBIC") + assert img.width == 200 and img.height == 100 + + img = frame.to_image( + width=200, height=100, interpolation=int(Interpolation.BICUBIC) + ) + assert img.width == 200 and img.height == 100 + def test_to_image_rgb24(self) -> None: sizes = [(318, 238), (320, 240), (500, 500)] for width, height in sizes: @@ -838,14 +857,20 @@ def test_reformat_identity() -> None: def test_reformat_colorspace() -> None: - # This is allowed. frame = VideoFrame(640, 480, "rgb24") frame.reformat(src_colorspace=None, dst_colorspace="smpte240m") - # I thought this was not allowed, but it seems to be. + frame = VideoFrame(640, 480, "rgb24") + frame.reformat(src_colorspace=None, dst_colorspace=Colorspace.smpte240m) + frame = VideoFrame(640, 480, "yuv420p") frame.reformat(src_colorspace=None, dst_colorspace="smpte240m") + frame = VideoFrame(640, 480, "rgb24") + frame.colorspace = Colorspace.smpte240m + assert frame.colorspace == int(Colorspace.smpte240m) + assert frame.colorspace == Colorspace.smpte240m + def test_reformat_pixel_format_align() -> None: height = 480 From 382ea869920e5bca1531d2afaf8834460947dffa Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Thu, 5 Dec 2024 13:35:48 +0000 Subject: [PATCH 441/846] Cleanup `AVStream.side_data` leftovers --- av/stream.pxd | 2 -- av/stream.pyi | 8 +------- av/stream.pyx | 8 +------- 3 files changed, 2 insertions(+), 16 deletions(-) diff --git a/av/stream.pxd b/av/stream.pxd index d39585167..c847f641e 100644 --- a/av/stream.pxd +++ b/av/stream.pxd @@ -12,8 +12,6 @@ cdef class Stream: # Stream attributes. cdef readonly Container container cdef readonly dict metadata - cdef readonly int nb_side_data - cdef readonly dict side_data # CodecContext attributes. cdef readonly CodecContext codec_context diff --git a/av/stream.pyi b/av/stream.pyi index b17587e10..82bb672b2 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -1,14 +1,8 @@ -from enum import Enum from fractions import Fraction -from typing import ClassVar, Literal +from typing import Literal from .codec import Codec, CodecContext from .container import Container -from .frame import Frame -from .packet import Packet - -class SideData(Enum): - DISPLAYMATRIX: ClassVar[SideData] class Stream: name: str | None diff --git a/av/stream.pyx b/av/stream.pyx index 59e1713c5..35b85acdf 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -1,5 +1,4 @@ cimport libav as lib -from libc.stdint cimport int32_t from enum import Enum @@ -15,11 +14,6 @@ from av.utils cimport ( cdef object _cinit_bypass_sentinel = object() -# If necessary more can be added from -# https://ffmpeg.org/doxygen/trunk/group__lavc__packet.html#ga9a80bfcacc586b483a973272800edb97 -class SideData(Enum): - DISPLAYMATRIX: "Display Matrix" = lib.AV_PKT_DATA_DISPLAYMATRIX - cdef Stream wrap_stream(Container container, lib.AVStream *c_stream, CodecContext codec_context): """Build an av.Stream for an existing AVStream. @@ -84,7 +78,7 @@ cdef class Stream: self.codec_context = codec_context if self.codec_context: self.codec_context.stream_index = stream.index - + self.metadata = avdict_to_dict( stream.metadata, encoding=self.container.metadata_encoding, From 5c532dd5f025935da7114a21e34bafcc4ba6bfa6 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 6 Dec 2024 03:06:06 -0500 Subject: [PATCH 442/846] Write changelog --- AUTHORS.rst | 3 ++- CHANGELOG.rst | 10 ++++++++++ av/about.py | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 9caef903f..d5b4cb7ee 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -18,8 +18,8 @@ All contributors (by number of commits): * Tadas Dailyda * JoeUgly <41972063+JoeUgly@users.noreply.github.com> * Justin Wong <46082645+uvjustin@users.noreply.github.com> +* Mark Harfouche -- Mark Harfouche - Alba Mendez - Dave Johansen - Xinran Xu ; `@xxr3376 `_ @@ -73,6 +73,7 @@ All contributors (by number of commits): * Jonathan Martin * Johan Jeppsson Karlin * Philipp Klaus +* Lukas Geiger * Mattias Wadman * Manuel Goacolou * Julian Schweizer diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8e5a1380f..45de1e218 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,15 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. + +v14.0.1 + +Fixes: + +- Include header files in source distribution by :gh-user:`hmaarrfk` in (:pr:`1662`). +- Cleanup ``AVStream.side_data`` leftovers by :gh-user:`lgeiger` in (:pr:`1674`). +- Address :issue:`1663` by :gh-user:`WyattBlue`. + v14.0.0 ------- @@ -37,6 +46,7 @@ Features: - Allow the profile of a codec to be set as well as queried by :gh-user:`davidplowman` in (:pr:`1625`). Fixes: + - Make ``VideoFrame.from_numpy_buffer()`` support buffers with padding by :gh-user:`davidplowman` in (:pr:`1635`). - Correct ``Colorspace``'s lowercase enums. - Updated ``sidedata.Type`` enum. diff --git a/av/about.py b/av/about.py index 229469150..4fcf9b8bb 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "14.0.0" +__version__ = "14.0.1" From 55bcc75ad2ab0424785ba95bd070258f6d40a1e9 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 6 Dec 2024 03:39:49 -0500 Subject: [PATCH 443/846] `mp3` now works with `add_stream_from_template()` --- av/container/output.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/container/output.pyx b/av/container/output.pyx index 8fad652d4..a75e47d43 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -148,7 +148,7 @@ cdef class OutputContainer(Container): cdef const lib.AVCodec *codec cdef Codec codec_obj - if template.type == "subtitle": + if template.type != "video": codec_obj = template.codec_context.codec else: codec_obj = Codec(template.codec_context.codec.name, "w") From ce45f1d07f76907e431a92ebff7d49a0e4c294d9 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 6 Dec 2024 03:43:28 -0500 Subject: [PATCH 444/846] Release 14.0.1 --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 45de1e218..4a30385f9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -24,6 +24,7 @@ Fixes: - Include header files in source distribution by :gh-user:`hmaarrfk` in (:pr:`1662`). - Cleanup ``AVStream.side_data`` leftovers by :gh-user:`lgeiger` in (:pr:`1674`). - Address :issue:`1663` by :gh-user:`WyattBlue`. +- Make ``mp3`` work with ``OutputContainer.add_stream_from_template()``. v14.0.0 ------- From 6839e9f58b162795f6cf02ebe77c847ae6042b9a Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 8 Dec 2024 19:10:23 -0500 Subject: [PATCH 445/846] Fix mypy error --- tests/test_audioframe.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_audioframe.py b/tests/test_audioframe.py index d67e181ce..c6bf87b3f 100644 --- a/tests/test_audioframe.py +++ b/tests/test_audioframe.py @@ -115,11 +115,11 @@ def test_from_ndarray_value_error() -> None: AudioFrame.from_ndarray(array, format="flt", layout="mono") # incorrect number of dimensions - array = np.zeros(shape=(1, 160, 2), dtype="f4") + array2 = np.zeros(shape=(1, 160, 2), dtype="f4") with pytest.raises( ValueError, match="Expected numpy array with ndim `2` but got `3`" ) as cm: - AudioFrame.from_ndarray(array, format="flt", layout="mono") + AudioFrame.from_ndarray(array2, format="flt", layout="mono") # incorrect shape array = np.zeros(shape=(2, 160), dtype="f4") From 6eaf7017cdae839641ed03357fc526abdd50298f Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Mon, 9 Dec 2024 00:55:08 +0000 Subject: [PATCH 446/846] Add `VideoFrame.rotation` --- av/sidedata/sidedata.pxd | 2 ++ av/sidedata/sidedata.pyx | 12 ++++++++++-- av/video/frame.pyi | 2 ++ av/video/frame.pyx | 11 +++++++++++ include/libavutil/avutil.pxd | 3 +++ tests/test_decode.py | 10 ++++++++++ 6 files changed, 38 insertions(+), 2 deletions(-) diff --git a/av/sidedata/sidedata.pxd b/av/sidedata/sidedata.pxd index 8a2f6d07c..5e6e5bf4c 100644 --- a/av/sidedata/sidedata.pxd +++ b/av/sidedata/sidedata.pxd @@ -14,6 +14,8 @@ cdef class SideData(Buffer): cdef SideData wrap_side_data(Frame frame, int index) +cdef int get_display_rotation(Frame frame) + cdef class _SideDataContainer: cdef Frame frame diff --git a/av/sidedata/sidedata.pyx b/av/sidedata/sidedata.pyx index 753496fea..24dbae119 100644 --- a/av/sidedata/sidedata.pyx +++ b/av/sidedata/sidedata.pyx @@ -1,3 +1,5 @@ +from libc.stdint cimport int32_t + from collections.abc import Mapping from enum import Enum @@ -45,13 +47,19 @@ class Type(Enum): cdef SideData wrap_side_data(Frame frame, int index): - cdef lib.AVFrameSideDataType type_ = frame.ptr.side_data[index].type - if type_ == lib.AV_FRAME_DATA_MOTION_VECTORS: + if frame.ptr.side_data[index].type == lib.AV_FRAME_DATA_MOTION_VECTORS: return MotionVectors(_cinit_bypass_sentinel, frame, index) else: return SideData(_cinit_bypass_sentinel, frame, index) +cdef int get_display_rotation(Frame frame): + for i in range(frame.ptr.nb_side_data): + if frame.ptr.side_data[i].type == lib.AV_FRAME_DATA_DISPLAYMATRIX: + return int(lib.av_display_rotation_get(frame.ptr.side_data[i].data)) + return 0 + + cdef class SideData(Buffer): def __init__(self, sentinel, Frame frame, int index): if sentinel is not _cinit_bypass_sentinel: diff --git a/av/video/frame.pyi b/av/video/frame.pyi index 0739010c1..a3eea373d 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -41,6 +41,8 @@ class VideoFrame(Frame): def height(self) -> int: ... @property def interlaced_frame(self) -> bool: ... + @property + def rotation(self) -> int: ... def __init__( self, width: int = 0, height: int = 0, format: str = "yuv420p" ) -> None: ... diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 862db8513..02cde3187 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -4,6 +4,7 @@ from enum import IntEnum from libc.stdint cimport uint8_t from av.error cimport err_check +from av.sidedata.sidedata cimport get_display_rotation from av.utils cimport check_ndarray from av.video.format cimport get_pix_fmt, get_video_format from av.video.plane cimport VideoPlane @@ -172,6 +173,16 @@ cdef class VideoFrame(Frame): """Height of the image, in pixels.""" return self.ptr.height + @property + def rotation(self): + """The rotation component of the `DISPLAYMATRIX` transformation matrix. + + Returns: + int: The angle (in degrees) by which the transformation rotates the frame + counterclockwise. The angle will be in range [-180, 180]. + """ + return get_display_rotation(self) + @property def interlaced_frame(self): """Is this frame an interlaced or progressive?""" diff --git a/include/libavutil/avutil.pxd b/include/libavutil/avutil.pxd index ed281aeaf..58dd43922 100644 --- a/include/libavutil/avutil.pxd +++ b/include/libavutil/avutil.pxd @@ -4,6 +4,9 @@ from libc.stdint cimport int64_t, uint8_t, uint64_t, int32_t cdef extern from "libavutil/mathematics.h" nogil: pass +cdef extern from "libavutil/display.h" nogil: + cdef double av_display_rotation_get(const int32_t matrix[9]) + cdef extern from "libavutil/rational.h" nogil: cdef int av_reduce(int *dst_num, int *dst_den, int64_t num, int64_t den, int64_t max) diff --git a/tests/test_decode.py b/tests/test_decode.py index 20abdf840..05f636977 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -155,3 +155,13 @@ def test_flush_decoded_video_frame_count(self) -> None: output_count += 1 assert output_count == input_count + + def test_no_side_data(self) -> None: + container = av.open(fate_suite("h264/interlaced_crop.mp4")) + frame = next(container.decode(video=0)) + assert frame.rotation == 0 + + def test_side_data(self) -> None: + container = av.open(fate_suite("mov/displaymatrix.mov")) + frame = next(container.decode(video=0)) + assert frame.rotation == -90 From 2c63608a349e9f1f5de702071100bb0c8d268860 Mon Sep 17 00:00:00 2001 From: Matthew Lai Date: Tue, 17 Dec 2024 20:23:53 +0800 Subject: [PATCH 447/846] Implement hardware decoding This implements hardware decoding continuing from the work of @rvillalba-novetta and @mikeboers with cleanup work by @WyattBlue. --- CHANGELOG.rst | 9 ++ av/__init__.py | 1 + av/__main__.py | 14 +++ av/about.py | 2 +- av/audio/codeccontext.pyx | 5 +- av/codec/codec.pxd | 2 + av/codec/codec.pyi | 1 + av/codec/codec.pyx | 38 ++++++++ av/codec/context.pxd | 12 ++- av/codec/context.pyi | 7 +- av/codec/context.pyx | 23 +++-- av/codec/hwaccel.pxd | 20 +++++ av/codec/hwaccel.pyi | 48 ++++++++++ av/codec/hwaccel.pyx | 151 ++++++++++++++++++++++++++++++++ av/container/core.pxd | 3 + av/container/core.pyi | 5 ++ av/container/core.pyx | 11 ++- av/container/input.pyx | 2 +- av/container/output.pyx | 6 +- av/video/codeccontext.pxd | 12 +++ av/video/codeccontext.pyx | 64 +++++++++++++- examples/basics/hw_decode.py | 76 ++++++++++++++++ include/libav.pxd | 3 + include/libavcodec/avcodec.pxd | 8 ++ include/libavcodec/hwaccel.pxd | 19 ++++ include/libavutil/buffer.pxd | 13 ++- include/libavutil/hwcontext.pxd | 24 +++++ scripts/build-deps | 26 ++++++ tests/test_decode.py | 67 ++++++++++++++ 29 files changed, 649 insertions(+), 23 deletions(-) create mode 100644 av/codec/hwaccel.pxd create mode 100644 av/codec/hwaccel.pyi create mode 100644 av/codec/hwaccel.pyx create mode 100644 examples/basics/hw_decode.py create mode 100644 include/libavcodec/hwaccel.pxd create mode 100644 include/libavutil/hwcontext.pxd diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4a30385f9..e65107428 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,7 +17,16 @@ We are operating with `semantic versioning `_. are merged into the "default" branch. +v14.1.0 (Unreleased) +-------------------- + +Features + +- Add hardware decoding by :gh-user:`matthewlai` and :gh-user:`WyattBlue` in (:pr:`1685`). + + v14.0.1 +------- Fixes: diff --git a/av/__init__.py b/av/__init__.py index cbc3c8a2f..e2f9e5a6d 100644 --- a/av/__init__.py +++ b/av/__init__.py @@ -17,6 +17,7 @@ from av.bitstream import BitStreamFilterContext, bitstream_filters_available from av.codec.codec import Codec, codecs_available from av.codec.context import CodecContext +from av.codec.hwaccel import HWConfig from av.container import open from av.format import ContainerFormat, formats_available from av.packet import Packet diff --git a/av/__main__.py b/av/__main__.py index bc353d147..9e2b9d0ac 100644 --- a/av/__main__.py +++ b/av/__main__.py @@ -6,6 +6,8 @@ def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--codecs", action="store_true") + parser.add_argument("--hwdevices", action="store_true") + parser.add_argument("--hwconfigs", action="store_true") parser.add_argument("--version", action="store_true") args = parser.parse_args() @@ -30,6 +32,18 @@ def main() -> None: version = config["version"] print(f"{libname:<13} {version[0]:3d}.{version[1]:3d}.{version[2]:3d}") + if args.hwdevices: + from av.codec.hwaccel import hwdevices_available + + print("Hardware device types:") + for x in hwdevices_available(): + print(" ", x) + + if args.hwconfigs: + from av.codec.codec import dump_hwconfigs + + dump_hwconfigs() + if args.codecs: from av.codec.codec import dump_codecs diff --git a/av/about.py b/av/about.py index 4fcf9b8bb..217fb25cb 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "14.0.1" +__version__ = "14.1.0" diff --git a/av/audio/codeccontext.pyx b/av/audio/codeccontext.pyx index 54319ddaf..856af555c 100644 --- a/av/audio/codeccontext.pyx +++ b/av/audio/codeccontext.pyx @@ -3,13 +3,14 @@ cimport libav as lib from av.audio.format cimport AudioFormat, get_audio_format from av.audio.frame cimport AudioFrame, alloc_audio_frame from av.audio.layout cimport AudioLayout, get_audio_layout +from av.codec.hwaccel cimport HWAccel from av.frame cimport Frame from av.packet cimport Packet cdef class AudioCodecContext(CodecContext): - cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec): - CodecContext._init(self, ptr, codec) + cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec, HWAccel hwaccel): + CodecContext._init(self, ptr, codec, hwaccel) cdef _prepare_frames_for_encode(self, Frame input_frame): diff --git a/av/codec/codec.pxd b/av/codec/codec.pxd index b9925df13..576c659b4 100644 --- a/av/codec/codec.pxd +++ b/av/codec/codec.pxd @@ -7,6 +7,8 @@ cdef class Codec: cdef const lib.AVCodecDescriptor *desc cdef readonly bint is_encoder + cdef tuple _hardware_configs + cdef _init(self, name=?) diff --git a/av/codec/codec.pyi b/av/codec/codec.pyi index 32736c080..73c1f1edb 100644 --- a/av/codec/codec.pyi +++ b/av/codec/codec.pyi @@ -108,3 +108,4 @@ class codec_descriptor: codecs_available: set[str] def dump_codecs() -> None: ... +def dump_hwconfigs() -> None: ... diff --git a/av/codec/codec.pyx b/av/codec/codec.pyx index 1493f0f7b..2dab166ab 100644 --- a/av/codec/codec.pyx +++ b/av/codec/codec.pyx @@ -1,4 +1,5 @@ from av.audio.format cimport get_audio_format +from av.codec.hwaccel cimport wrap_hwconfig from av.descriptor cimport wrap_avclass from av.utils cimport avrational_to_fraction from av.video.format cimport get_video_format @@ -117,6 +118,10 @@ cdef class Codec: if self.is_encoder and lib.av_codec_is_decoder(self.ptr): raise RuntimeError("%s is both encoder and decoder.") + def __repr__(self): + mode = "w" if self.is_encoder else "r" + return f"" + def create(self, kind = None): """Create a :class:`.CodecContext` for this codec. @@ -203,6 +208,23 @@ cdef class Codec: i += 1 return ret + @property + def hardware_configs(self): + if self._hardware_configs: + return self._hardware_configs + ret = [] + cdef int i = 0 + cdef lib.AVCodecHWConfig *ptr + while True: + ptr = lib.avcodec_get_hw_config(self.ptr, i) + if not ptr: + break + ret.append(wrap_hwconfig(ptr)) + i += 1 + ret = tuple(ret) + self._hardware_configs = ret + return ret + @property def properties(self): return self.desc.props @@ -337,3 +359,19 @@ def dump_codecs(): ) except Exception as e: print(f"...... {codec.name:<18} ERROR: {e}") + +def dump_hwconfigs(): + print("Hardware configs:") + for name in sorted(codecs_available): + try: + codec = Codec(name, "r") + except ValueError: + continue + + configs = codec.hardware_configs + if not configs: + continue + + print(" ", codec.name) + for config in configs: + print(" ", config) diff --git a/av/codec/context.pxd b/av/codec/context.pxd index 42b2d63e7..7ba89dab7 100644 --- a/av/codec/context.pxd +++ b/av/codec/context.pxd @@ -3,6 +3,7 @@ from libc.stdint cimport int64_t from av.bytesource cimport ByteSource from av.codec.codec cimport Codec +from av.codec.hwaccel cimport HWAccel from av.frame cimport Frame from av.packet cimport Packet @@ -18,11 +19,12 @@ cdef class CodecContext: cdef int stream_index cdef lib.AVCodecParserContext *parser - cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec) + cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec, HWAccel hwaccel) # Public API. cdef readonly bint is_open cdef readonly Codec codec + cdef readonly HWAccel hwaccel cdef public dict options cpdef open(self, bint strict=?) @@ -31,6 +33,9 @@ cdef class CodecContext: cpdef decode(self, Packet packet=?) cpdef flush_buffers(self) + # Used by hardware-accelerated decode. + cdef HWAccel hwaccel_ctx + # Used by both transcode APIs to setup user-land objects. # TODO: Remove the `Packet` from `_setup_decoded_frame` (because flushing packets # are bogus). It should take all info it needs from the context and/or stream. @@ -49,10 +54,11 @@ cdef class CodecContext: cdef _send_packet_and_recv(self, Packet packet) cdef _recv_frame(self) + cdef _transfer_hwframe(self, Frame frame) + # Implemented by children for the generic send/recv API, so we have the # correct subclass of Frame. cdef Frame _next_frame cdef Frame _alloc_next_frame(self) - -cdef CodecContext wrap_codec_context(lib.AVCodecContext*, const lib.AVCodec*) +cdef CodecContext wrap_codec_context(lib.AVCodecContext*, const lib.AVCodec*, HWAccel hwaccel) diff --git a/av/codec/context.pyi b/av/codec/context.pyi index a6ca9647e..543da208e 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -5,6 +5,7 @@ from typing import ClassVar, Literal from av.packet import Packet from .codec import Codec +from .hwaccel import HWAccel class ThreadType(Flag): NONE: ClassVar[ThreadType] @@ -83,10 +84,14 @@ class CodecContext: def delay(self) -> bool: ... @property def extradata_size(self) -> int: ... + @property + def is_hwaccel(self) -> bool: ... def open(self, strict: bool = True) -> None: ... @staticmethod def create( - codec: str | Codec, mode: Literal["r", "w"] | None = None + codec: str | Codec, + mode: Literal["r", "w"] | None = None, + hwaccel: HWAccel | None = None, ) -> CodecContext: ... def parse( self, raw_input: bytes | bytearray | memoryview | None = None diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 29b7b80d1..e7136b86f 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -18,7 +18,7 @@ from av.dictionary import Dictionary cdef object _cinit_sentinel = object() -cdef CodecContext wrap_codec_context(lib.AVCodecContext *c_ctx, const lib.AVCodec *c_codec): +cdef CodecContext wrap_codec_context(lib.AVCodecContext *c_ctx, const lib.AVCodec *c_codec, HWAccel hwaccel): """Build an av.CodecContext for an existing AVCodecContext.""" cdef CodecContext py_ctx @@ -35,7 +35,7 @@ cdef CodecContext wrap_codec_context(lib.AVCodecContext *c_ctx, const lib.AVCode else: py_ctx = CodecContext(_cinit_sentinel) - py_ctx._init(c_ctx, c_codec) + py_ctx._init(c_ctx, c_codec, hwaccel) return py_ctx @@ -83,10 +83,10 @@ class Flags2(IntEnum): cdef class CodecContext: @staticmethod - def create(codec, mode=None): + def create(codec, mode=None, hwaccel=None): cdef Codec cy_codec = codec if isinstance(codec, Codec) else Codec(codec, mode) cdef lib.AVCodecContext *c_ctx = lib.avcodec_alloc_context3(cy_codec.ptr) - return wrap_codec_context(c_ctx, cy_codec.ptr) + return wrap_codec_context(c_ctx, cy_codec.ptr, hwaccel) def __cinit__(self, sentinel=None, *args, **kwargs): if sentinel is not _cinit_sentinel: @@ -96,11 +96,12 @@ cdef class CodecContext: self.stream_index = -1 # This is set by the container immediately. self.is_open = False - cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec): + cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec, HWAccel hwaccel): self.ptr = ptr if self.ptr.codec and codec and self.ptr.codec != codec: raise RuntimeError("Wrapping CodecContext with mismatched codec.") self.codec = wrap_codec(codec if codec != NULL else self.ptr.codec) + self.hwaccel = hwaccel # Set reasonable threading defaults. self.ptr.thread_count = 0 # use as many threads as there are CPUs. @@ -310,6 +311,13 @@ cdef class CodecContext: return packets + @property + def is_hwaccel(self): + """ + Returns ``True`` if this codec context is hardware accelerated, ``False`` otherwise. + """ + return self.hwaccel_ctx is not None + def _send_frame_and_recv(self, Frame frame): cdef Packet packet @@ -359,10 +367,15 @@ cdef class CodecContext: return err_check(res) + frame = self._transfer_hwframe(frame) + if not res: self._next_frame = None return frame + cdef _transfer_hwframe(self, Frame frame): + return frame + cdef _recv_packet(self): cdef Packet packet = Packet() diff --git a/av/codec/hwaccel.pxd b/av/codec/hwaccel.pxd new file mode 100644 index 000000000..e68f43bb1 --- /dev/null +++ b/av/codec/hwaccel.pxd @@ -0,0 +1,20 @@ +cimport libav as lib + +from av.codec.codec cimport Codec + + +cdef class HWConfig: + cdef object __weakref__ + cdef lib.AVCodecHWConfig *ptr + cdef void _init(self, lib.AVCodecHWConfig *ptr) + +cdef HWConfig wrap_hwconfig(lib.AVCodecHWConfig *ptr) + +cdef class HWAccel: + cdef int _device_type + cdef str _device + cdef readonly Codec codec + cdef readonly HWConfig config + cdef lib.AVBufferRef *ptr + cdef public bint allow_software_fallback + cdef public dict options diff --git a/av/codec/hwaccel.pyi b/av/codec/hwaccel.pyi new file mode 100644 index 000000000..7e4748e23 --- /dev/null +++ b/av/codec/hwaccel.pyi @@ -0,0 +1,48 @@ +from enum import IntEnum + +from av.codec.codec import Codec +from av.video.format import VideoFormat + +class HWDeviceType(IntEnum): + none: int + vdpau: int + cuda: int + vaapi: int + dxva2: int + qsv: int + videotoolbox: int + d3d11va: int + drm: int + opencl: int + mediacodec: int + vulkan: int + d3d12va: int + +class HWConfigMethod(IntEnum): + none: int + hw_device_ctx: int + hw_frame_ctx: int + internal: int + ad_hoc: int + +class HWConfig: + @property + def device_type(self) -> HWDeviceType: ... + @property + def format(self) -> VideoFormat: ... + @property + def methods(self) -> HWConfigMethod: ... + @property + def is_supported(self) -> bool: ... + +class HWAccel: + def __init__( + self, + device_type: str | HWDeviceType, + device: str | None = None, + allow_software_fallback: bool = False, + options: dict[str, object] | None = None, + ) -> None: ... + def create(self, codec: Codec) -> HWAccel: ... + +def hwdevices_available() -> list[str]: ... diff --git a/av/codec/hwaccel.pyx b/av/codec/hwaccel.pyx new file mode 100644 index 000000000..1c96d02e8 --- /dev/null +++ b/av/codec/hwaccel.pyx @@ -0,0 +1,151 @@ +import weakref +from enum import IntEnum + +cimport libav as lib + +from av.codec.codec cimport Codec +from av.dictionary cimport _Dictionary +from av.error cimport err_check +from av.video.format cimport get_video_format +from av.dictionary import Dictionary + + +class HWDeviceType(IntEnum): + none = lib.AV_HWDEVICE_TYPE_NONE + vdpau = lib.AV_HWDEVICE_TYPE_VDPAU + cuda = lib.AV_HWDEVICE_TYPE_CUDA + vaapi = lib.AV_HWDEVICE_TYPE_VAAPI + dxva2 = lib.AV_HWDEVICE_TYPE_DXVA2 + qsv = lib.AV_HWDEVICE_TYPE_QSV + videotoolbox = lib.AV_HWDEVICE_TYPE_VIDEOTOOLBOX + d3d11va = lib.AV_HWDEVICE_TYPE_D3D11VA + drm = lib.AV_HWDEVICE_TYPE_DRM + opencl = lib.AV_HWDEVICE_TYPE_OPENCL + mediacodec = lib.AV_HWDEVICE_TYPE_MEDIACODEC + vulkan = lib.AV_HWDEVICE_TYPE_VULKAN + d3d12va = lib.AV_HWDEVICE_TYPE_D3D12VA + +class HWConfigMethod(IntEnum): + none = 0 + hw_device_ctx = lib.AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX # This is the only one we support. + hw_frame_ctx = lib.AV_CODEC_HW_CONFIG_METHOD_HW_FRAMES_CTX + internal = lib.AV_CODEC_HW_CONFIG_METHOD_INTERNAL + ad_hoc = lib.AV_CODEC_HW_CONFIG_METHOD_AD_HOC + + +cdef object _cinit_sentinel = object() +cdef object _singletons = weakref.WeakValueDictionary() + +cdef HWConfig wrap_hwconfig(lib.AVCodecHWConfig *ptr): + try: + return _singletons[ptr] + except KeyError: + pass + cdef HWConfig config = HWConfig(_cinit_sentinel) + config._init(ptr) + _singletons[ptr] = config + return config + + +cdef class HWConfig: + def __init__(self, sentinel): + if sentinel is not _cinit_sentinel: + raise RuntimeError("Cannot instantiate CodecContext") + + cdef void _init(self, lib.AVCodecHWConfig *ptr): + self.ptr = ptr + + def __repr__(self): + return ( + f"self.ptr:x}>" + ) + + @property + def device_type(self): + return HWDeviceType(self.ptr.device_type) + + @property + def format(self): + return get_video_format(self.ptr.pix_fmt, 0, 0) + + @property + def methods(self): + return HWConfigMethod(self.ptr.methods) + + @property + def is_supported(self): + return bool(self.ptr.methods & lib.AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX) + + +cpdef hwdevices_available(): + result = [] + + cdef lib.AVHWDeviceType x = lib.AV_HWDEVICE_TYPE_NONE + while True: + x = lib.av_hwdevice_iterate_types(x) + if x == lib.AV_HWDEVICE_TYPE_NONE: + break + result.append(lib.av_hwdevice_get_type_name(HWDeviceType(x))) + + return result + + +cdef class HWAccel: + def __init__(self, device_type, device=None, codec=None, allow_software_fallback=True, options=None): + if isinstance(device_type, HWDeviceType): + self._device_type = device_type + elif isinstance(device_type, str): + self._device_type = int(lib.av_hwdevice_find_type_by_name(device_type)) + else: + raise ValueError("Unknown type for device_type") + + self._device = device + self.allow_software_fallback = allow_software_fallback + self.options = {} if not options else dict(options) + self.ptr = NULL + self.codec = codec + self.config = None + + if codec: + self._initialize_hw_context() + + def _initialize_hw_context(self): + cdef HWConfig config + for config in self.codec.hardware_configs: + if not (config.ptr.methods & lib.AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX): + continue + if self._device_type and config.device_type != self._device_type: + continue + break + else: + raise NotImplementedError(f"No supported hardware config for {self.codec}") + + self.config = config + + cdef char *c_device = NULL + if self._device: + device_bytes = self._device.encode() + c_device = device_bytes + cdef _Dictionary c_options = Dictionary(self.options) + + err_check( + lib.av_hwdevice_ctx_create( + &self.ptr, config.ptr.device_type, c_device, c_options.ptr, 0 + ) + ) + + def create(self, Codec codec not None): + """Create a new hardware accelerator context with the given codec""" + if self.ptr: + raise RuntimeError("Hardware context already initialized") + + self.codec = codec + self._initialize_hw_context() + return self + + def __dealloc__(self): + if self.ptr: + lib.av_buffer_unref(&self.ptr) diff --git a/av/container/core.pxd b/av/container/core.pxd index 1aed54b90..87bb792b3 100644 --- a/av/container/core.pxd +++ b/av/container/core.pxd @@ -1,5 +1,6 @@ cimport libav as lib +from av.codec.hwaccel cimport HWAccel from av.container.pyio cimport PyIOFile from av.container.streams cimport StreamContainer from av.dictionary cimport _Dictionary @@ -33,6 +34,8 @@ cdef class Container: cdef readonly dict container_options cdef readonly list stream_options + cdef HWAccel hwaccel + cdef readonly StreamContainer streams cdef readonly dict metadata diff --git a/av/container/core.pyi b/av/container/core.pyi index 227a7d32a..7c681b18b 100644 --- a/av/container/core.pyi +++ b/av/container/core.pyi @@ -4,6 +4,7 @@ from pathlib import Path from types import TracebackType from typing import Any, Callable, ClassVar, Literal, Type, overload +from av.codec.hwaccel import HWAccel from av.format import ContainerFormat from .input import InputContainer @@ -73,6 +74,7 @@ def open( buffer_size: int = 32768, timeout: Real | None | tuple[Real | None, Real | None] = None, io_open: Callable[..., Any] | None = None, + hwaccel: HWAccel | None = None, ) -> InputContainer: ... @overload def open( @@ -87,6 +89,7 @@ def open( buffer_size: int = 32768, timeout: Real | None | tuple[Real | None, Real | None] = None, io_open: Callable[..., Any] | None = None, + hwaccel: HWAccel | None = None, ) -> InputContainer: ... @overload def open( @@ -101,6 +104,7 @@ def open( buffer_size: int = 32768, timeout: Real | None | tuple[Real | None, Real | None] = None, io_open: Callable[..., Any] | None = None, + hwaccel: HWAccel | None = None, ) -> OutputContainer: ... @overload def open( @@ -115,4 +119,5 @@ def open( buffer_size: int = 32768, timeout: Real | None | tuple[Real | None, Real | None] = None, io_open: Callable[..., Any] | None = None, + hwaccel: HWAccel | None = None, ) -> InputContainer | OutputContainer: ... diff --git a/av/container/core.pyx b/av/container/core.pyx index 563c79d21..201570c62 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -8,6 +8,7 @@ from pathlib import Path cimport libav as lib +from av.codec.hwaccel cimport HWAccel from av.container.core cimport timeout_info from av.container.input cimport InputContainer from av.container.output cimport OutputContainer @@ -143,7 +144,7 @@ class Flags(Flag): cdef class Container: def __cinit__(self, sentinel, file_, format_name, options, - container_options, stream_options, + container_options, stream_options, hwaccel, metadata_encoding, metadata_errors, buffer_size, open_timeout, read_timeout, io_open): @@ -164,6 +165,8 @@ cdef class Container: self.container_options = dict(container_options or ()) self.stream_options = [dict(x) for x in stream_options or ()] + self.hwaccel = hwaccel + self.metadata_encoding = metadata_encoding self.metadata_errors = metadata_errors @@ -296,6 +299,7 @@ def open( buffer_size=32768, timeout=None, io_open=None, + hwaccel=None ): """open(file, mode='r', **kwargs) @@ -322,6 +326,7 @@ def open( ``url`` is the url to open, ``flags`` is a combination of AVIO_FLAG_* and ``options`` is a dictionary of additional options. The callable should return a file-like object. + :param HWAccel hwaccel: Optional settings for hardware-accelerated decoding. :rtype: Container For devices (via ``libavdevice``), pass the name of the device to ``format``, @@ -367,7 +372,7 @@ def open( if mode.startswith("r"): return InputContainer(_cinit_sentinel, file, format, options, - container_options, stream_options, metadata_encoding, metadata_errors, + container_options, stream_options, hwaccel, metadata_encoding, metadata_errors, buffer_size, open_timeout, read_timeout, io_open, ) @@ -376,6 +381,6 @@ def open( "Provide stream options via Container.add_stream(..., options={})." ) return OutputContainer(_cinit_sentinel, file, format, options, - container_options, stream_options, metadata_encoding, metadata_errors, + container_options, stream_options, None, metadata_encoding, metadata_errors, buffer_size, open_timeout, read_timeout, io_open, ) diff --git a/av/container/input.pyx b/av/container/input.pyx index 7246f8245..aa9940452 100644 --- a/av/container/input.pyx +++ b/av/container/input.pyx @@ -77,7 +77,7 @@ cdef class InputContainer(Container): codec_context = lib.avcodec_alloc_context3(codec) err_check(lib.avcodec_parameters_to_context(codec_context, stream.codecpar)) codec_context.pkt_timebase = stream.time_base - py_codec_context = wrap_codec_context(codec_context, codec) + py_codec_context = wrap_codec_context(codec_context, codec, self.hwaccel) else: # no decoder is available py_codec_context = None diff --git a/av/container/output.pyx b/av/container/output.pyx index a75e47d43..e61ef2297 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -125,7 +125,7 @@ cdef class OutputContainer(Container): err_check(lib.avcodec_parameters_from_context(stream.codecpar, codec_context)) # Construct the user-land stream - cdef CodecContext py_codec_context = wrap_codec_context(codec_context, codec) + cdef CodecContext py_codec_context = wrap_codec_context(codec_context, codec, None) cdef Stream py_stream = wrap_stream(self, stream, py_codec_context) self.streams.add_stream(py_stream) @@ -179,7 +179,7 @@ cdef class OutputContainer(Container): err_check(lib.avcodec_parameters_from_context(stream.codecpar, codec_context)) # Construct the user-land stream - cdef CodecContext py_codec_context = wrap_codec_context(codec_context, codec) + cdef CodecContext py_codec_context = wrap_codec_context(codec_context, codec, None) cdef Stream py_stream = wrap_stream(self, stream, py_codec_context) self.streams.add_stream(py_stream) @@ -237,7 +237,7 @@ cdef class OutputContainer(Container): # Construct the user-land stream cdef CodecContext py_codec_context = None if codec_context != NULL: - py_codec_context = wrap_codec_context(codec_context, codec) + py_codec_context = wrap_codec_context(codec_context, codec, None) cdef Stream py_stream = wrap_stream(self, stream, py_codec_context) self.streams.add_stream(py_stream) diff --git a/av/video/codeccontext.pxd b/av/video/codeccontext.pxd index 9693caa9b..895ba74b1 100644 --- a/av/video/codeccontext.pxd +++ b/av/video/codeccontext.pxd @@ -1,3 +1,4 @@ +cimport libav as lib from av.codec.context cimport CodecContext from av.video.format cimport VideoFormat @@ -5,8 +6,19 @@ from av.video.frame cimport VideoFrame from av.video.reformatter cimport VideoReformatter +# The get_format callback in AVCodecContext is called by the decoder to pick a format out of a list. +# When we want accelerated decoding, we need to figure out ahead of time what the format should be, +# and find a way to pass that into our callback so we can return it to the decoder. We use the 'opaque' +# user data field in AVCodecContext for that. This is the struct we store a pointer to in that field. +cdef struct AVCodecPrivateData: + lib.AVPixelFormat hardware_pix_fmt + bint allow_software_fallback + + cdef class VideoCodecContext(CodecContext): + cdef AVCodecPrivateData _private_data + cdef VideoFormat _format cdef _build_format(self) diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.pyx index d2f4c9e14..92470c159 100644 --- a/av/video/codeccontext.pyx +++ b/av/video/codeccontext.pyx @@ -2,6 +2,8 @@ cimport libav as lib from libc.stdint cimport int64_t from av.codec.context cimport CodecContext +from av.codec.hwaccel cimport HWAccel, HWConfig +from av.error cimport err_check from av.frame cimport Frame from av.packet cimport Packet from av.utils cimport avrational_to_fraction, to_avrational @@ -10,13 +12,51 @@ from av.video.frame cimport VideoFrame, alloc_video_frame from av.video.reformatter cimport VideoReformatter +cdef lib.AVPixelFormat _get_hw_format(lib.AVCodecContext *ctx, const lib.AVPixelFormat *pix_fmts) noexcept: + # In the case where we requested accelerated decoding, the decoder first calls this function + # with a list that includes both the hardware format and software formats. + # First we try to pick the hardware format if it's in the list. + # However, if the decoder fails to initialize the hardware, it will call this function again, + # with only software formats in pix_fmts. We return ctx->sw_pix_fmt regardless in this case, + # because that should be in the candidate list. If not, we are out of ideas anyways. + cdef AVCodecPrivateData* private_data = ctx.opaque + i = 0 + while pix_fmts[i] != -1: + if pix_fmts[i] == private_data.hardware_pix_fmt: + return pix_fmts[i] + i += 1 + return ctx.sw_pix_fmt if private_data.allow_software_fallback else lib.AV_PIX_FMT_NONE + + cdef class VideoCodecContext(CodecContext): + def __cinit__(self, *args, **kwargs): self.last_w = 0 self.last_h = 0 - cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec): - CodecContext._init(self, ptr, codec) # TODO: Can this be `super`? + cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec, HWAccel hwaccel): + CodecContext._init(self, ptr, codec, hwaccel) # TODO: Can this be `super`? + + if hwaccel is not None: + try: + self.hwaccel_ctx = hwaccel.create(self.codec) + self.ptr.hw_device_ctx = lib.av_buffer_ref(self.hwaccel_ctx.ptr) + self.ptr.pix_fmt = self.hwaccel_ctx.config.ptr.pix_fmt + self.ptr.get_format = _get_hw_format + self._private_data.hardware_pix_fmt = self.hwaccel_ctx.config.ptr.pix_fmt + self._private_data.allow_software_fallback = self.hwaccel.allow_software_fallback + self.ptr.opaque = &self._private_data + except NotImplementedError: + # Some streams may not have a hardware decoder. For example, many action + # cam videos have a low resolution mjpeg stream, which is usually not + # compatible with hardware decoders. + # The user may have passed in a hwaccel because they want to decode the main + # stream with it, so we shouldn't abort even if we find a stream that can't + # be HW decoded. + # If the user wants to make sure hwaccel is actually used, they can check with the + # is_hardware_accelerated() function on each stream's codec context. + self.hwaccel_ctx = None + self._build_format() self.encoded_frame_count = 0 @@ -58,6 +98,26 @@ cdef class VideoCodecContext(CodecContext): cdef VideoFrame vframe = frame vframe._init_user_attributes() + cdef _transfer_hwframe(self, Frame frame): + if self.hwaccel_ctx is None: + return frame + + if frame.ptr.format != self.hwaccel_ctx.config.ptr.pix_fmt: + # If we get a software frame, that means we are in software fallback mode, and don't actually + # need to transfer. + return frame + + cdef Frame frame_sw + + frame_sw = self._alloc_next_frame() + + err_check(lib.av_hwframe_transfer_data(frame_sw.ptr, frame.ptr, 0)) + + # TODO: Is there anything else to transfer?! + frame_sw.pts = frame.pts + + return frame_sw + cdef _build_format(self): self._format = get_video_format(self.ptr.pix_fmt, self.ptr.width, self.ptr.height) diff --git a/examples/basics/hw_decode.py b/examples/basics/hw_decode.py new file mode 100644 index 000000000..1ce7a11af --- /dev/null +++ b/examples/basics/hw_decode.py @@ -0,0 +1,76 @@ +import os +import time + +import av +import av.datasets + +# What accelerator to use. +# Recommendations: +# Windows: +# - d3d11va (Direct3D 11) +# * available with built-in ffmpeg in PyAV binary wheels, and gives access to +# all decoders, but performance may not be as good as vendor native interfaces. +# - cuda (NVIDIA NVDEC), qsv (Intel QuickSync) +# * may be faster than d3d11va, but requires custom ffmpeg built with those libraries. +# Linux (all options require custom FFmpeg): +# - vaapi (Intel, AMD) +# - cuda (NVIDIA) +# Mac: +# - videotoolbox +# * available with built-in ffmpeg in PyAV binary wheels, and gives access to +# all accelerators available on Macs. This is the only option on MacOS. + +HW_DEVICE = os.environ["HW_DEVICE"] if "HW_DEVICE" in os.environ else None + +if "TEST_FILE_PATH" in os.environ: + test_file_path = os.environ["TEST_FILE_PATH"] +else: + test_file_path = av.datasets.curated( + "pexels/time-lapse-video-of-night-sky-857195.mp4" + ) + +if HW_DEVICE is None: + av.codec.hwaccel.dump_hwdevices() + print("Please set HW_DEVICE.") + exit() + +assert HW_DEVICE in av.codec.hwaccel.hwdevices_available, f"{HW_DEVICE} not available." + +print("Decoding in software (auto threading)...") + +container = av.open(test_file_path) + +container.streams.video[0].thread_type = "AUTO" + +start_time = time.time() +frame_count = 0 +for packet in container.demux(video=0): + for _ in packet.decode(): + frame_count += 1 + +sw_time = time.time() - start_time +sw_fps = frame_count / sw_time +assert frame_count == container.streams.video[0].frames +container.close() + +print(f"Decoded with software in {sw_time:.2f}s ({sw_fps:.2f} fps).") + +print(f"Decoding with {HW_DEVICE}") + +hwaccel = av.codec.hwaccel.HWAccel(device_type=HW_DEVICE, allow_software_fallback=False) + +# Note the additional argument here. +container = av.open(test_file_path, hwaccel=hwaccel) + +start_time = time.time() +frame_count = 0 +for packet in container.demux(video=0): + for _ in packet.decode(): + frame_count += 1 + +hw_time = time.time() - start_time +hw_fps = frame_count / hw_time +assert frame_count == container.streams.video[0].frames +container.close() + +print(f"Decoded with {HW_DEVICE} in {hw_time:.2f}s ({hw_fps:.2f} fps).") diff --git a/include/libav.pxd b/include/libav.pxd index c793b9988..e2fe323a4 100644 --- a/include/libav.pxd +++ b/include/libav.pxd @@ -4,11 +4,14 @@ include "libavutil/channel_layout.pxd" include "libavutil/dict.pxd" include "libavutil/error.pxd" include "libavutil/frame.pxd" +include "libavutil/hwcontext.pxd" include "libavutil/samplefmt.pxd" include "libavutil/motion_vector.pxd" include "libavcodec/avcodec.pxd" include "libavcodec/bsf.pxd" +include "libavcodec/hwaccel.pxd" + include "libavdevice/avdevice.pxd" include "libavformat/avformat.pxd" include "libswresample/swresample.pxd" diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 172c9cc65..bcb342373 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -213,6 +213,8 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVFrame* coded_frame + void* opaque + int bit_rate int bit_rate_tolerance int mb_decision @@ -247,6 +249,7 @@ cdef extern from "libavcodec/avcodec.h" nogil: int coded_height AVPixelFormat pix_fmt + AVPixelFormat sw_pix_fmt AVRational sample_aspect_ratio int gop_size # The number of pictures in a group of pictures, or 0 for intra_only. int max_b_frames @@ -266,6 +269,11 @@ cdef extern from "libavcodec/avcodec.h" nogil: int get_buffer(AVCodecContext *ctx, AVFrame *frame) void release_buffer(AVCodecContext *ctx, AVFrame *frame) + # Hardware acceleration + AVHWAccel *hwaccel + AVBufferRef *hw_device_ctx + AVPixelFormat (*get_format)(AVCodecContext *s, const AVPixelFormat *fmt) + # User Data void *opaque diff --git a/include/libavcodec/hwaccel.pxd b/include/libavcodec/hwaccel.pxd new file mode 100644 index 000000000..cb9ac41b6 --- /dev/null +++ b/include/libavcodec/hwaccel.pxd @@ -0,0 +1,19 @@ +cdef extern from "libavcodec/avcodec.h" nogil: + cdef enum: + AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX, + AV_CODEC_HW_CONFIG_METHOD_HW_FRAMES_CTX, + AV_CODEC_HW_CONFIG_METHOD_INTERNAL, + AV_CODEC_HW_CONFIG_METHOD_AD_HOC, + cdef struct AVCodecHWConfig: + AVPixelFormat pix_fmt + int methods + AVHWDeviceType device_type + cdef const AVCodecHWConfig* avcodec_get_hw_config(const AVCodec *codec, int index) + cdef enum: + AV_HWACCEL_CODEC_CAP_EXPERIMENTAL + cdef struct AVHWAccel: + char *name + AVMediaType type + AVCodecID id + AVPixelFormat pix_fmt + int capabilities diff --git a/include/libavutil/buffer.pxd b/include/libavutil/buffer.pxd index daf86105b..d4ff4cd17 100644 --- a/include/libavutil/buffer.pxd +++ b/include/libavutil/buffer.pxd @@ -1,9 +1,18 @@ -from libc.stdint cimport uint8_t +from libc.stdint cimport intptr_t, uint8_t cdef extern from "libavutil/buffer.h" nogil: - AVBufferRef *av_buffer_create(uint8_t *data, size_t size, void (*free)(void *opaque, uint8_t *data), void *opaque, int flags) + AVBufferRef* av_buffer_ref(AVBufferRef *buf) void av_buffer_unref(AVBufferRef **buf) + cdef struct AVBuffer: + uint8_t *data + int size + intptr_t refcount + void (*free)(void *opaque, uint8_t *data) + void *opaque + int flags cdef struct AVBufferRef: + AVBuffer *buffer uint8_t *data + int size diff --git a/include/libavutil/hwcontext.pxd b/include/libavutil/hwcontext.pxd new file mode 100644 index 000000000..beda15a2c --- /dev/null +++ b/include/libavutil/hwcontext.pxd @@ -0,0 +1,24 @@ +cdef extern from "libavutil/hwcontext.h" nogil: + + enum AVHWDeviceType: + AV_HWDEVICE_TYPE_NONE + AV_HWDEVICE_TYPE_VDPAU + AV_HWDEVICE_TYPE_CUDA + AV_HWDEVICE_TYPE_VAAPI + AV_HWDEVICE_TYPE_DXVA2 + AV_HWDEVICE_TYPE_QSV + AV_HWDEVICE_TYPE_VIDEOTOOLBOX + AV_HWDEVICE_TYPE_D3D11VA + AV_HWDEVICE_TYPE_DRM + AV_HWDEVICE_TYPE_OPENCL + AV_HWDEVICE_TYPE_MEDIACODEC + AV_HWDEVICE_TYPE_VULKAN + AV_HWDEVICE_TYPE_D3D12VA + + cdef int av_hwdevice_ctx_create(AVBufferRef **device_ctx, AVHWDeviceType type, const char *device, AVDictionary *opts, int flags) + + cdef AVHWDeviceType av_hwdevice_find_type_by_name(const char *name) + cdef const char *av_hwdevice_get_type_name(AVHWDeviceType type) + cdef AVHWDeviceType av_hwdevice_iterate_types(AVHWDeviceType prev) + + cdef int av_hwframe_transfer_data(AVFrame *dst, const AVFrame *src, int flags) diff --git a/scripts/build-deps b/scripts/build-deps index 4cb90f074..de4a6e547 100755 --- a/scripts/build-deps +++ b/scripts/build-deps @@ -13,6 +13,31 @@ if [[ -e "$PYAV_LIBRARY_PREFIX/bin/ffmpeg" ]]; then exit 0 fi +# Add CUDA support if available +CONFFLAGS_NVIDIA="" +if [[ -e /usr/local/cuda ]]; then + # Get Nvidia headers for ffmpeg + cd $PYAV_LIBRARY_ROOT + if [[ ! -e "$PYAV_LIBRARY_ROOT/nv-codec-headers" ]]; then + git clone https://github.com/FFmpeg/nv-codec-headers.git + cd nv-codec-headers + make -j4 + make PREFIX="$PYAV_LIBRARY_PREFIX" install + fi + + PKG_CONFIG_PATH="/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH" + CONFFLAGS_NVIDIA="--enable-cuda \ + --enable-cuvid \ + --enable-nvenc \ + --enable-nonfree \ + --enable-libnpp \ + --extra-cflags=-I/usr/local/cuda/include \ + --extra-ldflags=-L/usr/local/cuda/lib64" +else + echo "WARNING: Did not find cuda libraries in /usr/local/cuda..." + echo " Building without NVIDIA NVENC/NVDEC support" +fi + mkdir -p "$PYAV_LIBRARY_ROOT" mkdir -p "$PYAV_LIBRARY_PREFIX" @@ -44,6 +69,7 @@ echo ./configure --enable-sse \ --enable-avx \ --enable-avx2 \ + $CONFFLAGS_NVIDIA \ --prefix="$PYAV_LIBRARY_PREFIX" \ || exit 2 echo diff --git a/tests/test_decode.py b/tests/test_decode.py index 05f636977..fc293d201 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -1,10 +1,47 @@ +import functools +import os +import pathlib from fractions import Fraction +import numpy as np +import pytest + import av from .common import TestCase, fate_suite +@functools.cache +def make_h264_test_video(path: str) -> None: + """Generates a black H264 test video for testing hardware decoding.""" + + # We generate a file here that's designed to be as compatible as possible with hardware + # encoders. Hardware encoders are sometimes very picky and the errors we get are often + # opaque, so there is nothing much we (PyAV) can do. The user needs to figure that out + # if they want to use hwaccel. We only want to test the PyAV plumbing here. + # Our video is H264, 1280x720p (note that some decoders have a minimum resolution limit), 24fps, + # 8-bit yuv420p. + pathlib.Path(path).parent.mkdir(parents=True, exist_ok=True) + output_container = av.open(path, "w") + stream = output_container.add_stream("libx264", rate=24) + assert isinstance(stream, av.VideoStream) + stream.width = 1280 + stream.height = 720 + stream.pix_fmt = "yuv420p" + + for _ in range(24): + frame = av.VideoFrame.from_ndarray( + np.zeros((720, 1280, 3), dtype=np.uint8), format="rgb24" + ) + for packet in stream.encode(frame): + output_container.mux(packet) + + for packet in stream.encode(): + output_container.mux(packet) + + output_container.close() + + class TestDecode(TestCase): def test_decoded_video_frame_count(self) -> None: container = av.open(fate_suite("h264/interlaced_crop.mp4")) @@ -165,3 +202,33 @@ def test_side_data(self) -> None: container = av.open(fate_suite("mov/displaymatrix.mov")) frame = next(container.decode(video=0)) assert frame.rotation == -90 + + def test_hardware_decode(self) -> None: + hwdevices_available = av.codec.hwaccel.hwdevices_available() + if "HWACCEL_DEVICE_TYPE" not in os.environ: + pytest.skip( + "Set the HWACCEL_DEVICE_TYPE to run this test. " + f"Options are {' '.join(hwdevices_available)}" + ) + + HWACCEL_DEVICE_TYPE = os.environ["HWACCEL_DEVICE_TYPE"] + assert ( + HWACCEL_DEVICE_TYPE in hwdevices_available + ), f"{HWACCEL_DEVICE_TYPE} not available" + + test_video_path = "tests/assets/black.mp4" + make_h264_test_video(test_video_path) + + hwaccel = av.codec.hwaccel.HWAccel( + device_type=HWACCEL_DEVICE_TYPE, allow_software_fallback=False + ) + + container = av.open(test_video_path, hwaccel=hwaccel) + video_stream = container.streams.video[0] + assert video_stream.codec_context.is_hwaccel + + frame_count = 0 + for frame in container.decode(video_stream): + frame_count += 1 + + assert frame_count == video_stream.frames From a4854a312996e87b6728caa940e2599af3f14d86 Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Tue, 17 Dec 2024 12:27:38 +0000 Subject: [PATCH 448/846] Update CHANGELOG.rst --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e65107428..eed7b2528 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -23,6 +23,7 @@ v14.1.0 (Unreleased) Features - Add hardware decoding by :gh-user:`matthewlai` and :gh-user:`WyattBlue` in (:pr:`1685`). +- Add ``VideoFrame.rotation`` by :gh-user:`lgeiger` in (:pr:`1675`). v14.0.1 From af30b3d02b76f3b56b4eb5f3c1f38746d3838ea5 Mon Sep 17 00:00:00 2001 From: Matthew Lai Date: Tue, 24 Dec 2024 14:17:52 +0800 Subject: [PATCH 449/846] Fixed HWAccel so we don't share contexts between streams --- av/codec/hwaccel.pyx | 26 +++++++++++++++----------- av/container/input.pyx | 7 +++++++ av/video/codeccontext.pyx | 2 +- examples/basics/hw_decode.py | 15 ++++++++------- tests/test_decode.py | 26 ++++++++++++++++---------- 5 files changed, 47 insertions(+), 29 deletions(-) diff --git a/av/codec/hwaccel.pyx b/av/codec/hwaccel.pyx index 1c96d02e8..b80c194af 100644 --- a/av/codec/hwaccel.pyx +++ b/av/codec/hwaccel.pyx @@ -7,6 +7,7 @@ from av.codec.codec cimport Codec from av.dictionary cimport _Dictionary from av.error cimport err_check from av.video.format cimport get_video_format + from av.dictionary import Dictionary @@ -94,11 +95,13 @@ cpdef hwdevices_available(): cdef class HWAccel: - def __init__(self, device_type, device=None, codec=None, allow_software_fallback=True, options=None): + def __init__(self, device_type, device=None, allow_software_fallback=True, options=None): if isinstance(device_type, HWDeviceType): self._device_type = device_type elif isinstance(device_type, str): self._device_type = int(lib.av_hwdevice_find_type_by_name(device_type)) + elif isinstance(device_type, int): + self._device_type = device_type else: raise ValueError("Unknown type for device_type") @@ -106,22 +109,18 @@ cdef class HWAccel: self.allow_software_fallback = allow_software_fallback self.options = {} if not options else dict(options) self.ptr = NULL - self.codec = codec self.config = None - if codec: - self._initialize_hw_context() - - def _initialize_hw_context(self): + def _initialize_hw_context(self, Codec codec not None): cdef HWConfig config - for config in self.codec.hardware_configs: + for config in codec.hardware_configs: if not (config.ptr.methods & lib.AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX): continue if self._device_type and config.device_type != self._device_type: continue break else: - raise NotImplementedError(f"No supported hardware config for {self.codec}") + raise NotImplementedError(f"No supported hardware config for {codec}") self.config = config @@ -142,9 +141,14 @@ cdef class HWAccel: if self.ptr: raise RuntimeError("Hardware context already initialized") - self.codec = codec - self._initialize_hw_context() - return self + ret = HWAccel( + device_type=self._device_type, + device=self._device, + allow_software_fallback=self.allow_software_fallback, + options=self.options + ) + ret._initialize_hw_context(codec) + return ret def __dealloc__(self): if self.ptr: diff --git a/av/container/input.pyx b/av/container/input.pyx index aa9940452..1ba4750d7 100644 --- a/av/container/input.pyx +++ b/av/container/input.pyx @@ -68,6 +68,8 @@ cdef class InputContainer(Container): lib.av_dict_free(&c_options[i]) free(c_options) + at_least_one_accelerated_context = False + self.streams = StreamContainer() for i in range(self.ptr.nb_streams): stream = self.ptr.streams[i] @@ -78,11 +80,16 @@ cdef class InputContainer(Container): err_check(lib.avcodec_parameters_to_context(codec_context, stream.codecpar)) codec_context.pkt_timebase = stream.time_base py_codec_context = wrap_codec_context(codec_context, codec, self.hwaccel) + if py_codec_context.is_hwaccel: + at_least_one_accelerated_context = True else: # no decoder is available py_codec_context = None self.streams.add_stream(wrap_stream(self, stream, py_codec_context)) + if self.hwaccel and not self.hwaccel.allow_software_fallback and not at_least_one_accelerated_context: + raise RuntimeError("Hardware accelerated decode requested but no stream is compatible") + self.metadata = avdict_to_dict(self.ptr.metadata, self.metadata_encoding, self.metadata_errors) def __dealloc__(self): diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.pyx index 92470c159..c9d8eb4c0 100644 --- a/av/video/codeccontext.pyx +++ b/av/video/codeccontext.pyx @@ -54,7 +54,7 @@ cdef class VideoCodecContext(CodecContext): # stream with it, so we shouldn't abort even if we find a stream that can't # be HW decoded. # If the user wants to make sure hwaccel is actually used, they can check with the - # is_hardware_accelerated() function on each stream's codec context. + # is_hwaccel() function on each stream's codec context. self.hwaccel_ctx = None self._build_format() diff --git a/examples/basics/hw_decode.py b/examples/basics/hw_decode.py index 1ce7a11af..605ee1841 100644 --- a/examples/basics/hw_decode.py +++ b/examples/basics/hw_decode.py @@ -3,6 +3,7 @@ import av import av.datasets +from av.codec.hwaccel import HWAccel, hwdevices_available # What accelerator to use. # Recommendations: @@ -30,11 +31,10 @@ ) if HW_DEVICE is None: - av.codec.hwaccel.dump_hwdevices() - print("Please set HW_DEVICE.") + print(f"Please set HW_DEVICE. Options are: {hwdevices_available()}") exit() -assert HW_DEVICE in av.codec.hwaccel.hwdevices_available, f"{HW_DEVICE} not available." +assert HW_DEVICE in hwdevices_available(), f"{HW_DEVICE} not available." print("Decoding in software (auto threading)...") @@ -53,11 +53,12 @@ assert frame_count == container.streams.video[0].frames container.close() -print(f"Decoded with software in {sw_time:.2f}s ({sw_fps:.2f} fps).") +print( + f"Decoded with software in {sw_time:.2f}s ({sw_fps:.2f} fps).\n" + f"Decoding with {HW_DEVICE}" +) -print(f"Decoding with {HW_DEVICE}") - -hwaccel = av.codec.hwaccel.HWAccel(device_type=HW_DEVICE, allow_software_fallback=False) +hwaccel = HWAccel(device_type=HW_DEVICE, allow_software_fallback=False) # Note the additional argument here. container = av.open(test_file_path, hwaccel=hwaccel) diff --git a/tests/test_decode.py b/tests/test_decode.py index fc293d201..c1846af69 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -13,7 +13,7 @@ @functools.cache def make_h264_test_video(path: str) -> None: - """Generates a black H264 test video for testing hardware decoding.""" + """Generates a black H264 test video with two streams for testing hardware decoding.""" # We generate a file here that's designed to be as compatible as possible with hardware # encoders. Hardware encoders are sometimes very picky and the errors we get are often @@ -23,21 +23,27 @@ def make_h264_test_video(path: str) -> None: # 8-bit yuv420p. pathlib.Path(path).parent.mkdir(parents=True, exist_ok=True) output_container = av.open(path, "w") - stream = output_container.add_stream("libx264", rate=24) - assert isinstance(stream, av.VideoStream) - stream.width = 1280 - stream.height = 720 - stream.pix_fmt = "yuv420p" + + streams = [] + for _ in range(2): + stream = output_container.add_stream("libx264", rate=24) + assert isinstance(stream, av.VideoStream) + stream.width = 1280 + stream.height = 720 + stream.pix_fmt = "yuv420p" + streams.append(stream) for _ in range(24): frame = av.VideoFrame.from_ndarray( np.zeros((720, 1280, 3), dtype=np.uint8), format="rgb24" ) - for packet in stream.encode(frame): - output_container.mux(packet) + for stream in streams: + for packet in stream.encode(frame): + output_container.mux(packet) - for packet in stream.encode(): - output_container.mux(packet) + for stream in streams: + for packet in stream.encode(): + output_container.mux(packet) output_container.close() From f3c7c3dd7288bca2926df94a23baf721dd238b34 Mon Sep 17 00:00:00 2001 From: Matthew Lai Date: Wed, 25 Dec 2024 21:16:18 +0800 Subject: [PATCH 450/846] Removed outdated comment --- av/video/reformatter.pyx | 3 --- 1 file changed, 3 deletions(-) diff --git a/av/video/reformatter.pyx b/av/video/reformatter.pyx index 538cc4953..a0c576d12 100644 --- a/av/video/reformatter.pyx +++ b/av/video/reformatter.pyx @@ -139,9 +139,6 @@ cdef class VideoReformatter: ): return frame - # Try and reuse existing SwsContextProxy - # VideoStream.decode will copy its SwsContextProxy to VideoFrame - # So all Video frames from the same VideoStream should have the same one with nogil: self.ptr = lib.sws_getCachedContext( self.ptr, From 8bec947774b6cca4193f0f48f8df93cc131d4895 Mon Sep 17 00:00:00 2001 From: Joe Schiff <41972063+JoeSchiff@users.noreply.github.com> Date: Thu, 26 Dec 2024 18:32:13 -0500 Subject: [PATCH 451/846] Remove tests/requirements.txt mention --- docs/overview/installation.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/overview/installation.rst b/docs/overview/installation.rst index 419592530..cf1364c1a 100644 --- a/docs/overview/installation.rst +++ b/docs/overview/installation.rst @@ -90,9 +90,6 @@ Building from the latest source # Prep a virtualenv. source scripts/activate.sh - # Install basic requirements. - pip install -r tests/requirements.txt - # Optionally build FFmpeg. ./scripts/build-deps From 892c9990673c472e821865ed54ee118ba6723bc3 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 28 Dec 2024 14:59:46 -0500 Subject: [PATCH 452/846] Mark pointer as const --- av/codec/codec.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/codec/codec.pyx b/av/codec/codec.pyx index 2dab166ab..73f085e8d 100644 --- a/av/codec/codec.pyx +++ b/av/codec/codec.pyx @@ -214,7 +214,7 @@ cdef class Codec: return self._hardware_configs ret = [] cdef int i = 0 - cdef lib.AVCodecHWConfig *ptr + cdef const lib.AVCodecHWConfig *ptr while True: ptr = lib.avcodec_get_hw_config(self.ptr, i) if not ptr: From 355f1b47cc78467e1907fb7d1305f081496875df Mon Sep 17 00:00:00 2001 From: Kim Minjong Date: Thu, 9 Jan 2025 14:25:48 +0900 Subject: [PATCH 453/846] Change the raise condition in resampler --- av/audio/resampler.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/audio/resampler.pyx b/av/audio/resampler.pyx index c46ed8475..69d790bad 100644 --- a/av/audio/resampler.pyx +++ b/av/audio/resampler.pyx @@ -95,7 +95,7 @@ cdef class AudioResampler: if self.frame_size > 0: self.graph.set_audio_frame_size(self.frame_size) - elif frame is not None: + if frame is not None: if ( frame.format.sample_fmt != self.template.format.sample_fmt or frame.layout != self.template.layout or From f659cfc0884fda59174759e686910bc45f23cbb8 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 9 Jan 2025 13:36:44 -0500 Subject: [PATCH 454/846] Test with pypy3.10 --- .github/workflows/smoke.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 352634ff6..e4bd3af4b 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -36,7 +36,7 @@ jobs: config: - {os: ubuntu-latest, python: "3.12", ffmpeg: "7.1", extras: true} - {os: ubuntu-latest, python: "3.9", ffmpeg: "7.0.2"} - - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "7.1"} + - {os: ubuntu-latest, python: "pypy3.10", ffmpeg: "7.1"} - {os: macos-14, python: "3.9", ffmpeg: "7.1"} - {os: macos-14, python: "3.9", ffmpeg: "7.0.2"} From d52757109526988cddfa78a39ace15830b92f772 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 11 Jan 2025 15:01:34 -0500 Subject: [PATCH 455/846] Use new template function --- examples/basics/remux.py | 2 +- examples/subtitles/remux.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/basics/remux.py b/examples/basics/remux.py index 5abb199b8..64ac00c99 100644 --- a/examples/basics/remux.py +++ b/examples/basics/remux.py @@ -9,7 +9,7 @@ # Make an output stream using the input as a template. This copies the stream # setup from one to the other. in_stream = input_.streams.video[0] -out_stream = output.add_stream(template=in_stream) +out_stream = output.add_stream_from_template(in_stream) for packet in input_.demux(in_stream): print(packet) diff --git a/examples/subtitles/remux.py b/examples/subtitles/remux.py index 5b3031788..32ee7d6d3 100644 --- a/examples/subtitles/remux.py +++ b/examples/subtitles/remux.py @@ -6,7 +6,7 @@ output = av.open("remuxed.vtt", "w") in_stream = input_.streams.subtitles[0] -out_stream = output.add_stream(template=in_stream) +out_stream = output.add_stream_from_template(in_stream) for packet in input_.demux(in_stream): if packet.dts is None: From 6bee7992d2919f2d6a43e5036c09f4b6cb00383e Mon Sep 17 00:00:00 2001 From: robinechuca Date: Wed, 15 Jan 2025 18:13:43 +0100 Subject: [PATCH 456/846] Support grayf32le and gbrapf32le in numpy convertion --- CHANGELOG.rst | 1 + av/video/frame.pyx | 63 ++++++++++++++++++++++++++++++---------- tests/test_videoframe.py | 36 +++++++++++++++++++++++ 3 files changed, 84 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index eed7b2528..7baadb4a8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -24,6 +24,7 @@ Features - Add hardware decoding by :gh-user:`matthewlai` and :gh-user:`WyattBlue` in (:pr:`1685`). - Add ``VideoFrame.rotation`` by :gh-user:`lgeiger` in (:pr:`1675`). +- Support grayf32le and gbrapf32le in numpy convertion by :gh-user:`robinechuca` in (:pr:`1712`). v14.0.1 diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 02cde3187..6e4a1dbdf 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -295,6 +295,8 @@ cdef class VideoFrame(Frame): .. note:: For ``pal8``, an ``(image, palette)`` tuple will be returned, with the palette being in ARGB (PyAV will swap bytes if needed). + .. note:: For ``gbrp`` formats, channels are flipped to RGB order. + """ cdef VideoFrame frame = self.reformat(**kwargs) @@ -312,7 +314,7 @@ cdef class VideoFrame(Frame): return np.hstack(( useful_array(frame.planes[0]), useful_array(frame.planes[1]), - useful_array(frame.planes[2]) + useful_array(frame.planes[2]), )).reshape(-1, frame.height, frame.width) elif frame.format.name == "yuyv422": assert frame.width % 2 == 0 @@ -320,21 +322,28 @@ cdef class VideoFrame(Frame): return useful_array(frame.planes[0], 2).reshape(frame.height, frame.width, -1) elif frame.format.name == "gbrp": array = np.empty((frame.height, frame.width, 3), dtype="uint8") - array[:, :, 0] = useful_array(frame.planes[2], 1).reshape(-1, frame.width) - array[:, :, 1] = useful_array(frame.planes[0], 1).reshape(-1, frame.width) - array[:, :, 2] = useful_array(frame.planes[1], 1).reshape(-1, frame.width) + array[:, :, 0] = useful_array(frame.planes[2], 1).reshape(frame.height, frame.width) + array[:, :, 1] = useful_array(frame.planes[0], 1).reshape(frame.height, frame.width) + array[:, :, 2] = useful_array(frame.planes[1], 1).reshape(frame.height, frame.width) return array elif frame.format.name in ("gbrp10be", "gbrp12be", "gbrp14be", "gbrp16be", "gbrp10le", "gbrp12le", "gbrp14le", "gbrp16le"): array = np.empty((frame.height, frame.width, 3), dtype="uint16") - array[:, :, 0] = useful_array(frame.planes[2], 2, "uint16").reshape(-1, frame.width) - array[:, :, 1] = useful_array(frame.planes[0], 2, "uint16").reshape(-1, frame.width) - array[:, :, 2] = useful_array(frame.planes[1], 2, "uint16").reshape(-1, frame.width) + array[:, :, 0] = useful_array(frame.planes[2], 2, "uint16").reshape(frame.height, frame.width) + array[:, :, 1] = useful_array(frame.planes[0], 2, "uint16").reshape(frame.height, frame.width) + array[:, :, 2] = useful_array(frame.planes[1], 2, "uint16").reshape(frame.height, frame.width) return byteswap_array(array, frame.format.name.endswith("be")) elif frame.format.name in ("gbrpf32be", "gbrpf32le"): array = np.empty((frame.height, frame.width, 3), dtype="float32") - array[:, :, 0] = useful_array(frame.planes[2], 4, "float32").reshape(-1, frame.width) - array[:, :, 1] = useful_array(frame.planes[0], 4, "float32").reshape(-1, frame.width) - array[:, :, 2] = useful_array(frame.planes[1], 4, "float32").reshape(-1, frame.width) + array[:, :, 0] = useful_array(frame.planes[2], 4, "float32").reshape(frame.height, frame.width) + array[:, :, 1] = useful_array(frame.planes[0], 4, "float32").reshape(frame.height, frame.width) + array[:, :, 2] = useful_array(frame.planes[1], 4, "float32").reshape(frame.height, frame.width) + return byteswap_array(array, frame.format.name.endswith("be")) + elif frame.format.name in ("gbrapf32be", "gbrapf32le"): + array = np.empty((frame.height, frame.width, 4), dtype="float32") + array[:, :, 0] = useful_array(frame.planes[2], 4, "float32").reshape(frame.height, frame.width) + array[:, :, 1] = useful_array(frame.planes[0], 4, "float32").reshape(frame.height, frame.width) + array[:, :, 2] = useful_array(frame.planes[1], 4, "float32").reshape(frame.height, frame.width) + array[:, :, 3] = useful_array(frame.planes[3], 4, "float32").reshape(frame.height, frame.width) return byteswap_array(array, frame.format.name.endswith("be")) elif frame.format.name in ("rgb24", "bgr24"): return useful_array(frame.planes[0], 3).reshape(frame.height, frame.width, -1) @@ -345,17 +354,22 @@ cdef class VideoFrame(Frame): elif frame.format.name in ("gray16be", "gray16le"): return byteswap_array( useful_array(frame.planes[0], 2, "uint16").reshape(frame.height, frame.width), - frame.format.name == "gray16be", + frame.format.name.endswith("be"), + ) + elif frame.format.name in ("grayf32be", "grayf32le"): + return byteswap_array( + useful_array(frame.planes[0], 4, "float32").reshape(frame.height, frame.width), + frame.format.name.endswith("be"), ) elif frame.format.name in ("rgb48be", "rgb48le"): return byteswap_array( useful_array(frame.planes[0], 6, "uint16").reshape(frame.height, frame.width, -1), - frame.format.name == "rgb48be", + frame.format.name.endswith("be"), ) elif frame.format.name in ("rgba64be", "rgba64le"): return byteswap_array( useful_array(frame.planes[0], 8, "uint16").reshape(frame.height, frame.width, -1), - frame.format.name == "rgba64be", + frame.format.name.endswith("be"), ) elif frame.format.name == "pal8": image = useful_array(frame.planes[0]).reshape(frame.height, frame.width) @@ -491,6 +505,8 @@ cdef class VideoFrame(Frame): must be in the system's native byte order. .. note:: for ``pal8``, an ``(image, palette)`` pair must be passed. `palette` must have shape (256, 4) and is given in ARGB format (PyAV will swap bytes if needed). + + .. note:: for ``gbrp`` formats, channels are assumed to be given in RGB order. """ if format == "pal8": array, palette = array @@ -568,19 +584,34 @@ cdef class VideoFrame(Frame): elif format in ("gray16be", "gray16le"): check_ndarray(array, "uint16", 2) frame = VideoFrame(array.shape[1], array.shape[0], format) - copy_array_to_plane(byteswap_array(array, format == "gray16be"), frame.planes[0], 2) + copy_array_to_plane(byteswap_array(array, format.endswith("be")), frame.planes[0], 2) + return frame + elif format in ("grayf32be", "grayf32le"): + check_ndarray(array, "float32", 2) + frame = VideoFrame(array.shape[1], array.shape[0], format) + copy_array_to_plane(byteswap_array(array, format.endswith("be")), frame.planes[0], 4) return frame elif format in ("rgb48be", "rgb48le"): check_ndarray(array, "uint16", 3) check_ndarray_shape(array, array.shape[2] == 3) frame = VideoFrame(array.shape[1], array.shape[0], format) - copy_array_to_plane(byteswap_array(array, format == "rgb48be"), frame.planes[0], 6) + copy_array_to_plane(byteswap_array(array, format.endswith("be")), frame.planes[0], 6) return frame elif format in ("rgba64be", "rgba64le"): check_ndarray(array, "uint16", 3) check_ndarray_shape(array, array.shape[2] == 4) frame = VideoFrame(array.shape[1], array.shape[0], format) - copy_array_to_plane(byteswap_array(array, format == "rgba64be"), frame.planes[0], 8) + copy_array_to_plane(byteswap_array(array, format.endswith("be")), frame.planes[0], 8) + return frame + elif format in ("gbrapf32be", "gbrapf32le"): + check_ndarray(array, "float32", 3) + check_ndarray_shape(array, array.shape[2] == 4) + + frame = VideoFrame(array.shape[1], array.shape[0], format) + copy_array_to_plane(byteswap_array(array[:, :, 1], format.endswith("be")), frame.planes[0], 4) + copy_array_to_plane(byteswap_array(array[:, :, 2], format.endswith("be")), frame.planes[1], 4) + copy_array_to_plane(byteswap_array(array[:, :, 0], format.endswith("be")), frame.planes[2], 4) + copy_array_to_plane(byteswap_array(array[:, :, 3], format.endswith("be")), frame.planes[3], 4) return frame elif format == "nv12": check_ndarray(array, "uint8", 2) diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index f044be949..250641676 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -223,6 +223,24 @@ def test_ndarray_gray_align() -> None: assertNdarraysEqual(frame.to_ndarray(), array) +def test_ndarray_grayf32() -> None: + array = numpy.random.random_sample(size=(480, 640)).astype(numpy.float32) + for format in ("grayf32be", "grayf32le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_grayf32_align() -> None: + array = numpy.random.random_sample(size=(238, 318)).astype(numpy.float32) + for format in ("grayf32be", "grayf32le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + def test_ndarray_rgb() -> None: array = numpy.random.randint(0, 256, size=(480, 640, 3), dtype=numpy.uint8) for format in ("rgb24", "bgr24"): @@ -365,6 +383,24 @@ def test_ndarray_gbrpf32_align() -> None: assertNdarraysEqual(frame.to_ndarray(), array) +def test_ndarray_gbrapf32() -> None: + array = numpy.random.random_sample(size=(480, 640, 4)).astype(numpy.float32) + for format in ("gbrapf32be", "gbrapf32le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrapf32_allign() -> None: + array = numpy.random.random_sample(size=(238, 318, 4)).astype(numpy.float32) + for format in ("gbrapf32be", "gbrapf32le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + def test_ndarray_yuv420p() -> None: array = numpy.random.randint(0, 256, size=(720, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuv420p") From c216de336b132de8ec139b3222513bf11a100198 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 16 Jan 2025 12:44:59 -0500 Subject: [PATCH 457/846] Change copyright year --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 59090193c..789fc0756 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -43,7 +43,7 @@ # General information about the project. project = "PyAV" -copyright = "2024, The PyAV Team" +copyright = "2025, The PyAV Team" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the From 1c7160528a06ce813027e41cc274e2875f4d6bd5 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 18 Jan 2025 23:13:07 -0500 Subject: [PATCH 458/846] Add `stream.disposition` and enum --- CHANGELOG.rst | 1 + av/stream.pyi | 23 +++++++++++++++++++++++ av/stream.pyx | 31 ++++++++++++++++++++++++++++++- include/libav.pxd | 1 - include/libavcodec/avcodec.pxd | 14 ++++++++++++++ include/libavformat/avformat.pxd | 4 +--- include/libavutil/avutil.pxd | 1 + tests/test_colorspace.py | 2 ++ 8 files changed, 72 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7baadb4a8..2f0bd681c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -23,6 +23,7 @@ v14.1.0 (Unreleased) Features - Add hardware decoding by :gh-user:`matthewlai` and :gh-user:`WyattBlue` in (:pr:`1685`). +- Add ``Stream.disposition`` and ``Disposition`` enum by :gh-user:`WyattBlue` in (:pr:`1720`). - Add ``VideoFrame.rotation`` by :gh-user:`lgeiger` in (:pr:`1675`). - Support grayf32le and gbrapf32le in numpy convertion by :gh-user:`robinechuca` in (:pr:`1712`). diff --git a/av/stream.pyi b/av/stream.pyi index 82bb672b2..a2a2e439c 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -1,9 +1,31 @@ +from enum import Flag from fractions import Fraction from typing import Literal from .codec import Codec, CodecContext from .container import Container +class Disposition(Flag): + default: int + dub: int + original: int + comment: int + lyrics: int + karaoke: int + forced: int + hearing_impaired: int + visual_impaired: int + clean_effects: int + attached_pic: int + timed_thumbnails: int + non_diegetic: int + captions: int + descriptions: int + metadata: int + dependent: int + still_image: int + multilayer: int + class Stream: name: str | None container: Container @@ -20,6 +42,7 @@ class Stream: guessed_rate: Fraction | None start_time: int | None duration: int | None + disposition: Disposition frames: int language: str | None type: Literal["video", "audio", "data", "subtitle", "attachment"] diff --git a/av/stream.pyx b/av/stream.pyx index 35b85acdf..d0ecf37ad 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -1,6 +1,6 @@ cimport libav as lib -from enum import Enum +from enum import Flag from av.error cimport err_check from av.packet cimport Packet @@ -12,6 +12,28 @@ from av.utils cimport ( ) +class Disposition(Flag): + default = 1 << 0 + dub = 1 << 1 + original = 1 << 2 + comment = 1 << 3 + lyrics = 1 << 4 + karaoke = 1 << 5 + forced = 1 << 6 + hearing_impaired = 1 << 7 + visual_impaired = 1 << 8 + clean_effects = 1 << 9 + attached_pic = 1 << 10 + timed_thumbnails = 1 << 11 + non_diegetic = 1 << 12 + captions = 1 << 16 + descriptions = 1 << 17 + metadata = 1 << 18 + dependent = 1 << 19 + still_image = 1 << 20 + multilayer = 1 << 21 + + cdef object _cinit_bypass_sentinel = object() cdef Stream wrap_stream(Container container, lib.AVStream *c_stream, CodecContext codec_context): @@ -96,6 +118,9 @@ cdef class Stream: if name == "id": self._set_id(value) return + if name == "disposition": + self.ptr.disposition = value + return # Convenience setter for codec context properties. if self.codec_context is not None: @@ -230,6 +255,10 @@ cdef class Stream: """ return self.metadata.get("language") + @property + def disposition(self): + return Disposition(self.ptr.disposition) + @property def type(self): """ diff --git a/include/libav.pxd b/include/libav.pxd index e2fe323a4..568913208 100644 --- a/include/libav.pxd +++ b/include/libav.pxd @@ -24,6 +24,5 @@ include "libavfilter/buffersrc.pxd" cdef extern from "stdio.h" nogil: - cdef int snprintf(char *output, int n, const char *format, ...) cdef int vsnprintf(char *output, int n, const char *format, va_list args) diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index bcb342373..ee832dd04 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -8,6 +8,16 @@ cdef extern from "libavcodec/codec_id.h": AVCodecID av_codec_get_id(const AVCodecTag *const *tags, uint32_t tag) +cdef extern from "libavcodec/packet.h" nogil: + AVPacketSideData* av_packet_side_data_new( + AVPacketSideData **sides, + int *nb_sides, + AVPacketSideDataType type, + size_t size, + int free_opaque + ) + + cdef extern from "libavutil/channel_layout.h": ctypedef enum AVChannelOrder: AV_CHANNEL_ORDER_UNSPEC @@ -542,6 +552,10 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef struct AVCodecParameters: AVMediaType codec_type AVCodecID codec_id + AVPacketSideData *coded_side_data + int nb_coded_side_data + uint8_t *extradata + int extradata_size cdef int avcodec_parameters_copy( AVCodecParameters *dst, diff --git a/include/libavformat/avformat.pxd b/include/libavformat/avformat.pxd index 5fa25043a..cec89bd38 100644 --- a/include/libavformat/avformat.pxd +++ b/include/libavformat/avformat.pxd @@ -30,18 +30,16 @@ cdef extern from "libavformat/avformat.h" nogil: cdef struct AVStream: int index int id + int disposition AVCodecParameters *codecpar - AVRational time_base int64_t start_time int64_t duration int64_t nb_frames int64_t cur_dts - AVDictionary *metadata - AVRational avg_frame_rate AVRational r_frame_rate AVRational sample_aspect_ratio diff --git a/include/libavutil/avutil.pxd b/include/libavutil/avutil.pxd index 58dd43922..49be65f69 100644 --- a/include/libavutil/avutil.pxd +++ b/include/libavutil/avutil.pxd @@ -30,6 +30,7 @@ cdef extern from "libavutil/avutil.h" nogil: cdef enum AVPixelFormat: AV_PIX_FMT_NONE AV_PIX_FMT_YUV420P + AV_PIX_FMT_RGBA AV_PIX_FMT_RGB24 PIX_FMT_RGB24 PIX_FMT_RGBA diff --git a/tests/test_colorspace.py b/tests/test_colorspace.py index 7574c147d..c76416c80 100644 --- a/tests/test_colorspace.py +++ b/tests/test_colorspace.py @@ -31,6 +31,8 @@ def test_sky_timelapse() -> None: ) stream = container.streams.video[0] + assert stream.disposition == av.stream.Disposition.default + assert stream.codec_context.color_range == 1 assert stream.codec_context.color_range == ColorRange.MPEG assert stream.codec_context.color_primaries == 1 From fdcb032ddbd898e6b6c59b098f9ef69988f5c54a Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 20 Jan 2025 12:22:13 -0500 Subject: [PATCH 459/846] Use new ffmpeg build --- scripts/ffmpeg-7.1.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ffmpeg-7.1.json b/scripts/ffmpeg-7.1.json index 06e80636a..a9fdf419d 100644 --- a/scripts/ffmpeg-7.1.json +++ b/scripts/ffmpeg-7.1.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.1-1/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.1-3/ffmpeg-{platform}.tar.gz" } \ No newline at end of file From c63aedeb3f6bf1afa57d2d67662bbe0fbc1cb08e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 20 Jan 2025 21:07:28 -0500 Subject: [PATCH 460/846] Build aarch64 natively --- .github/workflows/tests.yml | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 92bff5f28..33ffc0b4b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,10 +10,10 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.9" + python-version: "3.13" - name: Build source package run: | - pip install cython + pip install setuptools cython python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.1.json /tmp/vendor PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig python setup.py sdist - name: Upload source package @@ -32,7 +32,7 @@ jobs: arch: arm64 - os: macos-13 arch: x86_64 - - os: ubuntu-latest + - os: ubuntu-24.04-arm arch: aarch64 - os: ubuntu-latest arch: i686 @@ -44,10 +44,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.9" - - name: Set up QEMU - if: matrix.os == 'ubuntu-latest' - uses: docker/setup-qemu-action@v3 + python-version: "3.13" - name: Install packages if: matrix.os == 'macos-13' run: | From 73baa3dcdb3886623b105082fd195775c3f1eb3d Mon Sep 17 00:00:00 2001 From: robinechuca Date: Wed, 22 Jan 2025 14:34:42 +0100 Subject: [PATCH 461/846] Add support for yuv[a]p16 pix_fmt --- av/video/frame.pyi | 8 +- av/video/frame.pyx | 301 ++++++++++++++++++++------------------- tests/test_videoframe.py | 36 +++++ 3 files changed, 197 insertions(+), 148 deletions(-) diff --git a/av/video/frame.pyi b/av/video/frame.pyi index a3eea373d..f58554b8f 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -59,7 +59,9 @@ class VideoFrame(Frame): ) -> VideoFrame: ... def to_rgb(self, **kwargs: Any) -> VideoFrame: ... def to_image(self, **kwargs: Any) -> Image.Image: ... - def to_ndarray(self, **kwargs: Any) -> _SupportedNDarray: ... + def to_ndarray( + self, force_channel_last: bool = False, **kwargs: Any + ) -> _SupportedNDarray: ... @staticmethod def from_image(img: Image.Image) -> VideoFrame: ... @staticmethod @@ -67,7 +69,9 @@ class VideoFrame(Frame): array: _SupportedNDarray, format: str = "rgb24", width: int = 0 ) -> VideoFrame: ... @staticmethod - def from_ndarray(array: _SupportedNDarray, format: str = "rgb24") -> VideoFrame: ... + def from_ndarray( + array: _SupportedNDarray, format: str = "rgb24", channel_last: bool = False + ) -> VideoFrame: ... @staticmethod def from_bytes( data: bytes, diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 6e4a1dbdf..5cdfb233b 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -282,18 +282,26 @@ cdef class VideoFrame(Frame): return Image.frombytes("RGB", (plane.width, plane.height), bytes(o_buf), "raw", "RGB", 0, 1) - def to_ndarray(self, **kwargs): + def to_ndarray(self, force_channel_last=False, **kwargs): """Get a numpy array of this frame. Any ``**kwargs`` are passed to :meth:`.VideoReformatter.reformat`. + The array returned is generally of dimension (height, width, channels). + + :param bool force_channel_last: If False (default), the shape for the yuv444p and yuvj444p + will be (channels, height, width) rather than (height, width, channels) as usual. + This is for backward compatibility and also for keeping that + `bytes(to_ndarray(frame))` should be the same as the ffmpeg cli + when returning the pix_fmt with `-c:v rawvideo`. + .. note:: Numpy must be installed. - .. note:: For formats which return an array of ``uint16`, the samples - will be in the system's native byte order. + .. note:: For formats which return an array of ``uint16`` or ``float32``, + the samples will be in the system's native byte order. .. note:: For ``pal8``, an ``(image, palette)`` tuple will be returned, - with the palette being in ARGB (PyAV will swap bytes if needed). + with the palette being in ARGB (PyAV will swap bytes if needed). .. note:: For ``gbrp`` formats, channels are flipped to RGB order. @@ -302,88 +310,94 @@ cdef class VideoFrame(Frame): import numpy as np - if frame.format.name in ("yuv420p", "yuvj420p"): - assert frame.width % 2 == 0 - assert frame.height % 2 == 0 - return np.hstack(( - useful_array(frame.planes[0]), - useful_array(frame.planes[1]), - useful_array(frame.planes[2]) - )).reshape(-1, frame.width) - elif frame.format.name in ("yuv444p", "yuvj444p"): - return np.hstack(( + # check size + if frame.format.name in {"yuv420p", "yuvj420p", "yuyv422"}: + assert frame.width % 2 == 0, "the width has to be even for this pixel format" + assert frame.height % 2 == 0, "the height has to be even for this pixel format" + + # cases planes are simply concatenated in shape (height, width, channels) + itemsize, dtype = { + "abgr": (4, "uint8"), + "argb": (4, "uint8"), + "bgr24": (3, "uint8"), + "bgr8": (1, "uint8"), + "bgra": (4, "uint8"), + "gbrapf32be": (4, "float32"), + "gbrapf32le": (4, "float32"), + "gbrp": (1, "uint8"), + "gbrp10be": (2, "uint16"), + "gbrp10le": (2, "uint16"), + "gbrp12be": (2, "uint16"), + "gbrp12le": (2, "uint16"), + "gbrp14be": (2, "uint16"), + "gbrp14le": (2, "uint16"), + "gbrp16be": (2, "uint16"), + "gbrp16le": (2, "uint16"), + "gbrpf32be": (4, "float32"), + "gbrpf32le": (4, "float32"), + "gray": (1, "uint8"), + "gray16be": (2, "uint16"), + "gray16le": (2, "uint16"), + "gray8": (1, "uint8"), + "grayf32be": (4, "float32"), + "grayf32le": (4, "float32"), + "rgb24": (3, "uint8"), + "rgb48be": (6, "uint16"), + "rgb48le": (6, "uint16"), + "rgb8": (1, "uint8"), + "rgba": (4, "uint8"), + "rgba64be": (8, "uint16"), + "rgba64le": (8, "uint16"), + "yuv444p": (1, "uint8"), + "yuv444p16be": (2, "uint16"), + "yuv444p16le": (2, "uint16"), + "yuva444p16be": (2, "uint16"), + "yuva444p16le": (2, "uint16"), + "yuvj444p": (1, "uint8"), + "yuyv422": (2, "uint8"), + }.get(frame.format.name, (None, None)) + if itemsize is not None: + layers = [ + useful_array(plan, itemsize, dtype) + .reshape(frame.height, frame.width, -1) + for plan in frame.planes + ] + if len(layers) == 1: # shortcut, avoid memory copy + array = layers[0] + else: # general case + array = np.concatenate(layers, axis=2) + array = byteswap_array(array, frame.format.name.endswith("be")) + if array.shape[2] == 1: # skip last channel for gray images + return array.squeeze(2) + if frame.format.name.startswith("gbr"): # gbr -> rgb + buffer = array[:, :, 0].copy() + array[:, :, 0] = array[:, :, 2] + array[:, :, 2] = array[:, :, 1] + array[:, :, 1] = buffer + if not force_channel_last and frame.format.name in {"yuv444p", "yuvj444p"}: + array = np.moveaxis(array, 2, 0) + return array + + # special cases + if frame.format.name in {"yuv420p", "yuvj420p"}: + return np.hstack([ useful_array(frame.planes[0]), useful_array(frame.planes[1]), useful_array(frame.planes[2]), - )).reshape(-1, frame.height, frame.width) - elif frame.format.name == "yuyv422": - assert frame.width % 2 == 0 - assert frame.height % 2 == 0 - return useful_array(frame.planes[0], 2).reshape(frame.height, frame.width, -1) - elif frame.format.name == "gbrp": - array = np.empty((frame.height, frame.width, 3), dtype="uint8") - array[:, :, 0] = useful_array(frame.planes[2], 1).reshape(frame.height, frame.width) - array[:, :, 1] = useful_array(frame.planes[0], 1).reshape(frame.height, frame.width) - array[:, :, 2] = useful_array(frame.planes[1], 1).reshape(frame.height, frame.width) - return array - elif frame.format.name in ("gbrp10be", "gbrp12be", "gbrp14be", "gbrp16be", "gbrp10le", "gbrp12le", "gbrp14le", "gbrp16le"): - array = np.empty((frame.height, frame.width, 3), dtype="uint16") - array[:, :, 0] = useful_array(frame.planes[2], 2, "uint16").reshape(frame.height, frame.width) - array[:, :, 1] = useful_array(frame.planes[0], 2, "uint16").reshape(frame.height, frame.width) - array[:, :, 2] = useful_array(frame.planes[1], 2, "uint16").reshape(frame.height, frame.width) - return byteswap_array(array, frame.format.name.endswith("be")) - elif frame.format.name in ("gbrpf32be", "gbrpf32le"): - array = np.empty((frame.height, frame.width, 3), dtype="float32") - array[:, :, 0] = useful_array(frame.planes[2], 4, "float32").reshape(frame.height, frame.width) - array[:, :, 1] = useful_array(frame.planes[0], 4, "float32").reshape(frame.height, frame.width) - array[:, :, 2] = useful_array(frame.planes[1], 4, "float32").reshape(frame.height, frame.width) - return byteswap_array(array, frame.format.name.endswith("be")) - elif frame.format.name in ("gbrapf32be", "gbrapf32le"): - array = np.empty((frame.height, frame.width, 4), dtype="float32") - array[:, :, 0] = useful_array(frame.planes[2], 4, "float32").reshape(frame.height, frame.width) - array[:, :, 1] = useful_array(frame.planes[0], 4, "float32").reshape(frame.height, frame.width) - array[:, :, 2] = useful_array(frame.planes[1], 4, "float32").reshape(frame.height, frame.width) - array[:, :, 3] = useful_array(frame.planes[3], 4, "float32").reshape(frame.height, frame.width) - return byteswap_array(array, frame.format.name.endswith("be")) - elif frame.format.name in ("rgb24", "bgr24"): - return useful_array(frame.planes[0], 3).reshape(frame.height, frame.width, -1) - elif frame.format.name in ("argb", "rgba", "abgr", "bgra"): - return useful_array(frame.planes[0], 4).reshape(frame.height, frame.width, -1) - elif frame.format.name in ("gray", "gray8", "rgb8", "bgr8"): - return useful_array(frame.planes[0]).reshape(frame.height, frame.width) - elif frame.format.name in ("gray16be", "gray16le"): - return byteswap_array( - useful_array(frame.planes[0], 2, "uint16").reshape(frame.height, frame.width), - frame.format.name.endswith("be"), - ) - elif frame.format.name in ("grayf32be", "grayf32le"): - return byteswap_array( - useful_array(frame.planes[0], 4, "float32").reshape(frame.height, frame.width), - frame.format.name.endswith("be"), - ) - elif frame.format.name in ("rgb48be", "rgb48le"): - return byteswap_array( - useful_array(frame.planes[0], 6, "uint16").reshape(frame.height, frame.width, -1), - frame.format.name.endswith("be"), - ) - elif frame.format.name in ("rgba64be", "rgba64le"): - return byteswap_array( - useful_array(frame.planes[0], 8, "uint16").reshape(frame.height, frame.width, -1), - frame.format.name.endswith("be"), - ) - elif frame.format.name == "pal8": + ]).reshape(-1, frame.width) + if frame.format.name == "pal8": image = useful_array(frame.planes[0]).reshape(frame.height, frame.width) palette = np.frombuffer(frame.planes[1], "i4").astype(">i4").reshape(-1, 1).view(np.uint8) return image, palette - elif frame.format.name == "nv12": - return np.hstack(( + if frame.format.name == "nv12": + return np.hstack([ useful_array(frame.planes[0]), - useful_array(frame.planes[1], 2) - )).reshape(-1, frame.width) - else: - raise ValueError( - f"Conversion to numpy array with format `{frame.format.name}` is not yet supported" - ) + useful_array(frame.planes[1], 2), + ]).reshape(-1, frame.width) + + raise ValueError( + f"Conversion to numpy array with format `{frame.format.name}` is not yet supported" + ) @staticmethod def from_image(img): @@ -497,17 +511,71 @@ cdef class VideoFrame(Frame): self._init_user_attributes() @staticmethod - def from_ndarray(array, format="rgb24"): + def from_ndarray(array, format="rgb24", channel_last=False): """ Construct a frame from a numpy array. - .. note:: For formats which expect an array of ``uint16``, the samples - must be in the system's native byte order. + :param bool channel_last: If False (default), the shape for the yuv444p and yuvj444p + is given by (channels, height, width) rather than (height, width, channels). + + .. note:: For formats which expect an array of ``uint16``, + the samples must be in the system's native byte order. .. note:: for ``pal8``, an ``(image, palette)`` pair must be passed. `palette` must have shape (256, 4) and is given in ARGB format (PyAV will swap bytes if needed). .. note:: for ``gbrp`` formats, channels are assumed to be given in RGB order. + """ + import numpy as np + + # case layers are concatenated + channels, itemsize, dtype = { + "yuv444p": (3, 1, "uint8"), + "yuvj444p": (3, 1, "uint8"), + "gbrp": (3, 1, "uint8"), + "gbrp10be": (3, 2, "uint16"), + "gbrp12be": (3, 2, "uint16"), + "gbrp14be": (3, 2, "uint16"), + "gbrp16be": (3, 2, "uint16"), + "gbrp10le": (3, 2, "uint16"), + "gbrp12le": (3, 2, "uint16"), + "gbrp14le": (3, 2, "uint16"), + "gbrp16le": (3, 2, "uint16"), + "gbrpf32be": (3, 4, "float32"), + "gbrpf32le": (3, 4, "float32"), + "gray": (1, 1, "uint8"), + "gray8": (1, 1, "uint8"), + "rgb8": (1, 1, "uint8"), + "bgr8": (1, 1, "uint8"), + "gray16be": (1, 2, "uint16"), + "gray16le": (1, 2, "uint16"), + "grayf32be": (1, 4, "float32"), + "grayf32le": (1, 4, "float32"), + "gbrapf32be": (4, 4, "float32"), + "gbrapf32le": (4, 4, "float32"), + "yuv444p16be": (3, 2, "uint16"), + "yuv444p16le": (3, 2, "uint16"), + "yuva444p16be": (4, 2, "uint16"), + "yuva444p16le": (4, 2, "uint16"), + }.get(format, (None, None, None)) + if channels is not None: + if array.ndim == 2: # (height, width) -> (height, width, 1) + array = array[:, :, None] + check_ndarray(array, dtype, 3) + if not channel_last and format in {"yuv444p", "yuvj444p"}: + array = np.moveaxis(array, 0, 2) # (channels, h, w) -> (h, w, channels) + check_ndarray_shape(array, array.shape[2] == channels) + array = byteswap_array(array, format.endswith("be")) + frame = VideoFrame(array.shape[1], array.shape[0], format) + if frame.format.name.startswith("gbr"): # rgb -> gbr + array = np.concatenate([ # not inplace to avoid bad surprises + array[:, :, 1:3], array[:, :, 0:1], array[:, :, 3:], + ], axis=2) + for i in range(channels): + copy_array_to_plane(array[:, :, i], frame.planes[i], itemsize) + return frame + + # other cases if format == "pal8": array, palette = array check_ndarray(array, "uint8", 2) @@ -518,7 +586,7 @@ cdef class VideoFrame(Frame): copy_array_to_plane(array, frame.planes[0], 1) frame.planes[1].update(palette.view(">i4").astype("i4").tobytes()) return frame - elif format in ("yuv420p", "yuvj420p"): + elif format in {"yuv420p", "yuvj420p"}: check_ndarray(array, "uint8", 2) check_ndarray_shape(array, array.shape[0] % 3 == 0) check_ndarray_shape(array, array.shape[1] % 2 == 0) @@ -531,88 +599,29 @@ cdef class VideoFrame(Frame): copy_array_to_plane(flat[u_start:v_start], frame.planes[1], 1) copy_array_to_plane(flat[v_start:], frame.planes[2], 1) return frame - elif format in ("yuv444p", "yuvj444p"): - check_ndarray(array, "uint8", 3) - check_ndarray_shape(array, array.shape[0] == 3) - - frame = VideoFrame(array.shape[2], array.shape[1], format) - array = array.reshape(3, -1) - copy_array_to_plane(array[0], frame.planes[0], 1) - copy_array_to_plane(array[1], frame.planes[1], 1) - copy_array_to_plane(array[2], frame.planes[2], 1) - return frame elif format == "yuyv422": check_ndarray(array, "uint8", 3) check_ndarray_shape(array, array.shape[0] % 2 == 0) check_ndarray_shape(array, array.shape[1] % 2 == 0) check_ndarray_shape(array, array.shape[2] == 2) - elif format == "gbrp": - check_ndarray(array, "uint8", 3) - check_ndarray_shape(array, array.shape[2] == 3) - - frame = VideoFrame(array.shape[1], array.shape[0], format) - copy_array_to_plane(array[:, :, 1], frame.planes[0], 1) - copy_array_to_plane(array[:, :, 2], frame.planes[1], 1) - copy_array_to_plane(array[:, :, 0], frame.planes[2], 1) - return frame - elif format in ("gbrp10be", "gbrp12be", "gbrp14be", "gbrp16be", "gbrp10le", "gbrp12le", "gbrp14le", "gbrp16le"): - check_ndarray(array, "uint16", 3) - check_ndarray_shape(array, array.shape[2] == 3) - - frame = VideoFrame(array.shape[1], array.shape[0], format) - copy_array_to_plane(byteswap_array(array[:, :, 1], format.endswith("be")), frame.planes[0], 2) - copy_array_to_plane(byteswap_array(array[:, :, 2], format.endswith("be")), frame.planes[1], 2) - copy_array_to_plane(byteswap_array(array[:, :, 0], format.endswith("be")), frame.planes[2], 2) - return frame - elif format in ("gbrpf32be", "gbrpf32le"): - check_ndarray(array, "float32", 3) - check_ndarray_shape(array, array.shape[2] == 3) - - frame = VideoFrame(array.shape[1], array.shape[0], format) - copy_array_to_plane(byteswap_array(array[:, :, 1], format.endswith("be")), frame.planes[0], 4) - copy_array_to_plane(byteswap_array(array[:, :, 2], format.endswith("be")), frame.planes[1], 4) - copy_array_to_plane(byteswap_array(array[:, :, 0], format.endswith("be")), frame.planes[2], 4) - return frame - elif format in ("rgb24", "bgr24"): + elif format in {"rgb24", "bgr24"}: check_ndarray(array, "uint8", 3) check_ndarray_shape(array, array.shape[2] == 3) - elif format in ("argb", "rgba", "abgr", "bgra"): + elif format in {"argb", "rgba", "abgr", "bgra"}: check_ndarray(array, "uint8", 3) check_ndarray_shape(array, array.shape[2] == 4) - elif format in ("gray", "gray8", "rgb8", "bgr8"): - check_ndarray(array, "uint8", 2) - elif format in ("gray16be", "gray16le"): - check_ndarray(array, "uint16", 2) - frame = VideoFrame(array.shape[1], array.shape[0], format) - copy_array_to_plane(byteswap_array(array, format.endswith("be")), frame.planes[0], 2) - return frame - elif format in ("grayf32be", "grayf32le"): - check_ndarray(array, "float32", 2) - frame = VideoFrame(array.shape[1], array.shape[0], format) - copy_array_to_plane(byteswap_array(array, format.endswith("be")), frame.planes[0], 4) - return frame - elif format in ("rgb48be", "rgb48le"): + elif format in {"rgb48be", "rgb48le"}: check_ndarray(array, "uint16", 3) check_ndarray_shape(array, array.shape[2] == 3) frame = VideoFrame(array.shape[1], array.shape[0], format) copy_array_to_plane(byteswap_array(array, format.endswith("be")), frame.planes[0], 6) return frame - elif format in ("rgba64be", "rgba64le"): + elif format in {"rgba64be", "rgba64le"}: check_ndarray(array, "uint16", 3) check_ndarray_shape(array, array.shape[2] == 4) frame = VideoFrame(array.shape[1], array.shape[0], format) copy_array_to_plane(byteswap_array(array, format.endswith("be")), frame.planes[0], 8) return frame - elif format in ("gbrapf32be", "gbrapf32le"): - check_ndarray(array, "float32", 3) - check_ndarray_shape(array, array.shape[2] == 4) - - frame = VideoFrame(array.shape[1], array.shape[0], format) - copy_array_to_plane(byteswap_array(array[:, :, 1], format.endswith("be")), frame.planes[0], 4) - copy_array_to_plane(byteswap_array(array[:, :, 2], format.endswith("be")), frame.planes[1], 4) - copy_array_to_plane(byteswap_array(array[:, :, 0], format.endswith("be")), frame.planes[2], 4) - copy_array_to_plane(byteswap_array(array[:, :, 3], format.endswith("be")), frame.planes[3], 4) - return frame elif format == "nv12": check_ndarray(array, "uint8", 2) check_ndarray_shape(array, array.shape[0] % 3 == 0) diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 250641676..8cb520b82 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -449,6 +449,42 @@ def test_ndarray_yuvj444p() -> None: assertNdarraysEqual(frame.to_ndarray(), array) +def test_ndarray_yuv444p16() -> None: + array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) + for format in ("yuv444p16be", "yuv444p16le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_yuv444p16_allign() -> None: + array = numpy.random.randint(0, 65536, size=(238, 318, 3), dtype=numpy.uint16) + for format in ("yuv444p16be", "yuv444p16le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_yuva444p16() -> None: + array = numpy.random.randint(0, 65536, size=(480, 640, 4), dtype=numpy.uint16) + for format in ("yuva444p16be", "yuva444p16le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_yuva444p16_allign() -> None: + array = numpy.random.randint(0, 65536, size=(238, 318, 4), dtype=numpy.uint16) + for format in ("yuva444p16be", "yuva444p16le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + def test_ndarray_yuyv422_align() -> None: array = numpy.random.randint(0, 256, size=(238, 318, 2), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuyv422") From 787917bfba314ddb616efaad5535b40191108110 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 22 Jan 2025 09:03:58 -0500 Subject: [PATCH 462/846] Make parameters same, test channel_last --- av/video/frame.pyi | 2 +- av/video/frame.pyx | 12 +++++------- tests/test_videoframe.py | 15 +++++++++++++-- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/av/video/frame.pyi b/av/video/frame.pyi index f58554b8f..3533c8e3f 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -60,7 +60,7 @@ class VideoFrame(Frame): def to_rgb(self, **kwargs: Any) -> VideoFrame: ... def to_image(self, **kwargs: Any) -> Image.Image: ... def to_ndarray( - self, force_channel_last: bool = False, **kwargs: Any + self, channel_last: bool = False, **kwargs: Any ) -> _SupportedNDarray: ... @staticmethod def from_image(img: Image.Image) -> VideoFrame: ... diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 5cdfb233b..36f4deae1 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -282,18 +282,16 @@ cdef class VideoFrame(Frame): return Image.frombytes("RGB", (plane.width, plane.height), bytes(o_buf), "raw", "RGB", 0, 1) - def to_ndarray(self, force_channel_last=False, **kwargs): + def to_ndarray(self, channel_last=False, **kwargs): """Get a numpy array of this frame. Any ``**kwargs`` are passed to :meth:`.VideoReformatter.reformat`. The array returned is generally of dimension (height, width, channels). - :param bool force_channel_last: If False (default), the shape for the yuv444p and yuvj444p - will be (channels, height, width) rather than (height, width, channels) as usual. - This is for backward compatibility and also for keeping that - `bytes(to_ndarray(frame))` should be the same as the ffmpeg cli - when returning the pix_fmt with `-c:v rawvideo`. + :param bool channel_last: If True, the shape of array will be + (height, width, channels) rather than (channels, height, width) for + the "yuv444p" and "yuvj444p" formats. .. note:: Numpy must be installed. @@ -374,7 +372,7 @@ cdef class VideoFrame(Frame): array[:, :, 0] = array[:, :, 2] array[:, :, 2] = array[:, :, 1] array[:, :, 1] = buffer - if not force_channel_last and frame.format.name in {"yuv444p", "yuvj444p"}: + if not channel_last and frame.format.name in {"yuv444p", "yuvj444p"}: array = np.moveaxis(array, 2, 0) return array diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 8cb520b82..9581a7c41 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -440,6 +440,17 @@ def test_ndarray_yuv444p() -> None: assert frame.format.name == "yuv444p" assertNdarraysEqual(frame.to_ndarray(), array) + array = numpy.random.randint(0, 256, size=(3, 480, 640), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, channel_last=False, format="yuv444p") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "yuv444p" + assertNdarraysEqual(frame.to_ndarray(channel_last=False), array) + assert array.shape != frame.to_ndarray(channel_last=True).shape + assert ( + frame.to_ndarray(channel_last=False).shape + != frame.to_ndarray(channel_last=True).shape + ) + def test_ndarray_yuvj444p() -> None: array = numpy.random.randint(0, 256, size=(3, 480, 640), dtype=numpy.uint8) @@ -458,7 +469,7 @@ def test_ndarray_yuv444p16() -> None: assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_yuv444p16_allign() -> None: +def test_ndarray_yuv444p16_align() -> None: array = numpy.random.randint(0, 65536, size=(238, 318, 3), dtype=numpy.uint16) for format in ("yuv444p16be", "yuv444p16le"): frame = VideoFrame.from_ndarray(array, format=format) @@ -476,7 +487,7 @@ def test_ndarray_yuva444p16() -> None: assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_yuva444p16_allign() -> None: +def test_ndarray_yuva444p16_align() -> None: array = numpy.random.randint(0, 65536, size=(238, 318, 4), dtype=numpy.uint16) for format in ("yuva444p16be", "yuva444p16le"): frame = VideoFrame.from_ndarray(array, format=format) From b073eefa276b2a6787ff1356e85a80f04bc3ffdb Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 25 Jan 2025 14:56:31 -0500 Subject: [PATCH 463/846] Disable building pypy wheels --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 33ffc0b4b..9ef90a4e3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -65,11 +65,11 @@ jobs: CIBW_ENVIRONMENT_MACOS: PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig LDFLAGS=-headerpad_max_install_names CIBW_ENVIRONMENT_WINDOWS: INCLUDE=C:\\cibw\\vendor\\include LIB=C:\\cibw\\vendor\\lib PYAV_SKIP_TESTS=unicode_filename CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: delvewheel repair --add-path C:\cibw\vendor\bin -w {dest_dir} {wheel} - CIBW_SKIP: "*-musllinux*" + CIBW_SKIP: "pp* *-musllinux*" CIBW_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m pytest {package}/tests && mv {project}/av.disabled {project}/av CIBW_TEST_REQUIRES: pytest numpy # skip tests when there are no binary wheels of numpy - CIBW_TEST_SKIP: pp* *_i686 + CIBW_TEST_SKIP: "*_i686" run: | pip install cibuildwheel delvewheel cibuildwheel --output-dir dist From cc06a6c698f68f47d5522059e75e9bd098cd3e23 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 25 Jan 2025 15:12:10 -0500 Subject: [PATCH 464/846] Release 14.1.0 --- CHANGELOG.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2f0bd681c..ead5b6a74 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,16 +17,16 @@ We are operating with `semantic versioning `_. are merged into the "default" branch. -v14.1.0 (Unreleased) --------------------- +v14.1.0 +------- -Features +Features: - Add hardware decoding by :gh-user:`matthewlai` and :gh-user:`WyattBlue` in (:pr:`1685`). - Add ``Stream.disposition`` and ``Disposition`` enum by :gh-user:`WyattBlue` in (:pr:`1720`). - Add ``VideoFrame.rotation`` by :gh-user:`lgeiger` in (:pr:`1675`). - Support grayf32le and gbrapf32le in numpy convertion by :gh-user:`robinechuca` in (:pr:`1712`). - +- Support yuv[a]p16 formats in numpy convertion by :gh-user:`robinechuca` in (:pr:`1722`). v14.0.1 ------- From dd2d71a96715b4e01aa653912f91b47cca1c1d99 Mon Sep 17 00:00:00 2001 From: z-khan Date: Thu, 30 Jan 2025 02:50:24 +1100 Subject: [PATCH 465/846] Allow user to select audio codec when opening an audio device --- av/container/core.pyi | 39 ++++++++++++++++++++++++++- av/container/core.pyx | 46 +++++++++++++++++++++++++++++++- include/libavcodec/avcodec.pxd | 34 +++++++++++++++++++++++ include/libavformat/avformat.pxd | 1 + 4 files changed, 118 insertions(+), 2 deletions(-) diff --git a/av/container/core.pyi b/av/container/core.pyi index 7c681b18b..7ba2511cf 100644 --- a/av/container/core.pyi +++ b/av/container/core.pyi @@ -1,4 +1,4 @@ -from enum import Flag +from enum import Flag, IntEnum from fractions import Fraction from pathlib import Path from types import TracebackType @@ -30,6 +30,43 @@ class Flags(Flag): shortest: ClassVar[Flags] auto_bsf: ClassVar[Flags] +class AudioCodec(IntEnum): + none: int + pcm_alaw: int + pcm_bluray: int + pcm_dvd: int + pcm_f16le: int + pcm_f24le: int + pcm_f32be: int + pcm_f32le: int + pcm_f64be: int + pcm_f64le: int + pcm_lxf: int + pcm_mulaw: int + pcm_s16be: int + pcm_s16be_planar: int + pcm_s16le: int + pcm_s16le_planar: int + pcm_s24be: int + pcm_s24daud: int + pcm_s24le: int + pcm_s24le_planar: int + pcm_s32be: int + pcm_s32le: int + pcm_s32le_planar: int + pcm_s64be: int + pcm_s64le: int + pcm_s8: int + pcm_s8_planar: int + pcm_u16be: int + pcm_u16le: int + pcm_u24be: int + pcm_u24le: int + pcm_u32be: int + pcm_u32le: int + pcm_u8: int + pcm_vidc: int + class Container: writeable: bool name: str diff --git a/av/container/core.pyx b/av/container/core.pyx index 201570c62..3da402f2f 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -3,7 +3,7 @@ from libc.stdint cimport int64_t import os import time -from enum import Flag +from enum import Flag, IntEnum from pathlib import Path cimport libav as lib @@ -141,6 +141,44 @@ class Flags(Flag): shortest: "Stop muxing when the shortest stream stops." = lib.AVFMT_FLAG_SHORTEST auto_bsf: "Add bitstream filters as requested by the muxer." = lib.AVFMT_FLAG_AUTO_BSF +class AudioCodec(IntEnum): + """Enumeration for audio codec IDs.""" + none = lib.AV_CODEC_ID_NONE # No codec. + pcm_alaw = lib.AV_CODEC_ID_PCM_ALAW # PCM A-law. + pcm_bluray = lib.AV_CODEC_ID_PCM_BLURAY # PCM Blu-ray. + pcm_dvd = lib.AV_CODEC_ID_PCM_DVD # PCM DVD. + pcm_f16le = lib.AV_CODEC_ID_PCM_F16LE # PCM F16 little-endian. + pcm_f24le = lib.AV_CODEC_ID_PCM_F24LE # PCM F24 little-endian. + pcm_f32be = lib.AV_CODEC_ID_PCM_F32BE # PCM F32 big-endian. + pcm_f32le = lib.AV_CODEC_ID_PCM_F32LE # PCM F32 little-endian. + pcm_f64be = lib.AV_CODEC_ID_PCM_F64BE # PCM F64 big-endian. + pcm_f64le = lib.AV_CODEC_ID_PCM_F64LE # PCM F64 little-endian. + pcm_lxf = lib.AV_CODEC_ID_PCM_LXF # PCM LXF. + pcm_mulaw = lib.AV_CODEC_ID_PCM_MULAW # PCM μ-law. + pcm_s16be = lib.AV_CODEC_ID_PCM_S16BE # PCM signed 16-bit big-endian. + pcm_s16be_planar = lib.AV_CODEC_ID_PCM_S16BE_PLANAR # PCM signed 16-bit big-endian planar. + pcm_s16le = lib.AV_CODEC_ID_PCM_S16LE # PCM signed 16-bit little-endian. + pcm_s16le_planar = lib.AV_CODEC_ID_PCM_S16LE_PLANAR # PCM signed 16-bit little-endian planar. + pcm_s24be = lib.AV_CODEC_ID_PCM_S24BE # PCM signed 24-bit big-endian. + pcm_s24daud = lib.AV_CODEC_ID_PCM_S24DAUD # PCM signed 24-bit D-Cinema audio. + pcm_s24le = lib.AV_CODEC_ID_PCM_S24LE # PCM signed 24-bit little-endian. + pcm_s24le_planar = lib.AV_CODEC_ID_PCM_S24LE_PLANAR # PCM signed 24-bit little-endian planar. + pcm_s32be = lib.AV_CODEC_ID_PCM_S32BE # PCM signed 32-bit big-endian. + pcm_s32le = lib.AV_CODEC_ID_PCM_S32LE # PCM signed 32-bit little-endian. + pcm_s32le_planar = lib.AV_CODEC_ID_PCM_S32LE_PLANAR # PCM signed 32-bit little-endian planar. + pcm_s64be = lib.AV_CODEC_ID_PCM_S64BE # PCM signed 64-bit big-endian. + pcm_s64le = lib.AV_CODEC_ID_PCM_S64LE # PCM signed 64-bit little-endian. + pcm_s8 = lib.AV_CODEC_ID_PCM_S8 # PCM signed 8-bit. + pcm_s8_planar = lib.AV_CODEC_ID_PCM_S8_PLANAR # PCM signed 8-bit planar. + pcm_u16be = lib.AV_CODEC_ID_PCM_U16BE # PCM unsigned 16-bit big-endian. + pcm_u16le = lib.AV_CODEC_ID_PCM_U16LE # PCM unsigned 16-bit little-endian. + pcm_u24be = lib.AV_CODEC_ID_PCM_U24BE # PCM unsigned 24-bit big-endian. + pcm_u24le = lib.AV_CODEC_ID_PCM_U24LE # PCM unsigned 24-bit little-endian. + pcm_u32be = lib.AV_CODEC_ID_PCM_U32BE # PCM unsigned 32-bit big-endian. + pcm_u32le = lib.AV_CODEC_ID_PCM_U32LE # PCM unsigned 32-bit little-endian. + pcm_u8 = lib.AV_CODEC_ID_PCM_U8 # PCM unsigned 8-bit. + pcm_vidc = lib.AV_CODEC_ID_PCM_VIDC # PCM VIDC. + cdef class Container: def __cinit__(self, sentinel, file_, format_name, options, @@ -176,7 +214,10 @@ cdef class Container: self.buffer_size = buffer_size self.io_open = io_open + acodec = None # no audio codec specified if format_name is not None: + if ":" in format_name: + format_name, acodec = format_name.split(":") self.format = ContainerFormat(format_name) self.input_was_opened = False @@ -211,6 +252,9 @@ cdef class Container: self.ptr.interrupt_callback.callback = interrupt_cb self.ptr.interrupt_callback.opaque = &self.interrupt_callback_info + if acodec is not None: + self.ptr.audio_codec_id = getattr(AudioCodec, acodec) + self.ptr.flags |= lib.AVFMT_FLAG_GENPTS self.ptr.opaque = self diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index ee832dd04..5bed3583d 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -163,6 +163,40 @@ cdef extern from "libavcodec/avcodec.h" nogil: AV_CODEC_ID_NONE AV_CODEC_ID_MPEG2VIDEO AV_CODEC_ID_MPEG1VIDEO + AV_CODEC_ID_PCM_ALAW + AV_CODEC_ID_PCM_BLURAY + AV_CODEC_ID_PCM_DVD + AV_CODEC_ID_PCM_F16LE + AV_CODEC_ID_PCM_F24LE + AV_CODEC_ID_PCM_F32BE + AV_CODEC_ID_PCM_F32LE + AV_CODEC_ID_PCM_F64BE + AV_CODEC_ID_PCM_F64LE + AV_CODEC_ID_PCM_LXF + AV_CODEC_ID_PCM_MULAW + AV_CODEC_ID_PCM_S16BE + AV_CODEC_ID_PCM_S16BE_PLANAR + AV_CODEC_ID_PCM_S16LE + AV_CODEC_ID_PCM_S16LE_PLANAR + AV_CODEC_ID_PCM_S24BE + AV_CODEC_ID_PCM_S24DAUD + AV_CODEC_ID_PCM_S24LE + AV_CODEC_ID_PCM_S24LE_PLANAR + AV_CODEC_ID_PCM_S32BE + AV_CODEC_ID_PCM_S32LE + AV_CODEC_ID_PCM_S32LE_PLANAR + AV_CODEC_ID_PCM_S64BE + AV_CODEC_ID_PCM_S64LE + AV_CODEC_ID_PCM_S8 + AV_CODEC_ID_PCM_S8_PLANAR + AV_CODEC_ID_PCM_U16BE + AV_CODEC_ID_PCM_U16LE + AV_CODEC_ID_PCM_U24BE + AV_CODEC_ID_PCM_U24LE + AV_CODEC_ID_PCM_U32BE + AV_CODEC_ID_PCM_U32LE + AV_CODEC_ID_PCM_U8 + AV_CODEC_ID_PCM_VIDC cdef enum AVDiscard: AVDISCARD_NONE diff --git a/include/libavformat/avformat.pxd b/include/libavformat/avformat.pxd index cec89bd38..6c23e99b4 100644 --- a/include/libavformat/avformat.pxd +++ b/include/libavformat/avformat.pxd @@ -191,6 +191,7 @@ cdef extern from "libavformat/avformat.h" nogil: int flags int64_t max_analyze_duration + AVCodecID audio_codec_id void *opaque int (*io_open)( From b4b13e248365ece781e06d2bf6848f7472de6f47 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 30 Jan 2025 13:46:19 -0500 Subject: [PATCH 466/846] Remove issue templates --- .github/ISSUE_TEMPLATE/build-bug-report.md | 67 ------------------- .../ISSUE_TEMPLATE/ffmpeg-feature-request.md | 56 ---------------- .../ISSUE_TEMPLATE/pyav-feature-request.md | 26 ------- .github/ISSUE_TEMPLATE/runtime-bug-report.md | 63 ----------------- 4 files changed, 212 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/build-bug-report.md delete mode 100644 .github/ISSUE_TEMPLATE/ffmpeg-feature-request.md delete mode 100644 .github/ISSUE_TEMPLATE/pyav-feature-request.md delete mode 100644 .github/ISSUE_TEMPLATE/runtime-bug-report.md diff --git a/.github/ISSUE_TEMPLATE/build-bug-report.md b/.github/ISSUE_TEMPLATE/build-bug-report.md deleted file mode 100644 index da0c17add..000000000 --- a/.github/ISSUE_TEMPLATE/build-bug-report.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -name: Build bug report -about: Report on an issue while building or installing PyAV. -title: "FOO does not build." -labels: build -assignees: '' - ---- - -**IMPORTANT:** Be sure to replace all template sections {{ like this }} or your issue may be discarded. - - -## Overview - -{{ A clear and concise description of what the bug is. }} - - -## Expected behavior - -{{ A clear and concise description of what you expected to happen. }} - - -## Actual behavior - -{{ A clear and concise description of what actually happened. }} - -Build report: -``` -{{ Complete output of `python setup.py build`. Reports that do not show compiler commands will not be accepted (e.g. results from `pip install av`). }} -``` - - -## Investigation - -{{ What you did to isolate the problem. }} - - -## Reproduction - -{{ Steps to reproduce the behavior. }} - - -## Versions - -- OS: {{ e.g. macOS 10.13.6 }} -- PyAV runtime: -``` -{{ Complete output of `python -m av --version` if you can run it. }} -``` -- PyAV build: -``` -{{ Complete output of `python setup.py config --verbose`. }} -``` - -## Research - -I have done the following: - -- [ ] Checked the [PyAV documentation](https://pyav.basswood-io.com) -- [ ] Searched on [Google](https://www.google.com/search?q=pyav+how+do+I+foo) -- [ ] Searched on [Stack Overflow](https://stackoverflow.com/search?q=pyav) -- [ ] Looked through [old GitHub issues](https://github.com/PyAV-Org/PyAV/issues?&q=is%3Aissue) - - -## Additional context - -{{ Add any other context about the problem here. }} diff --git a/.github/ISSUE_TEMPLATE/ffmpeg-feature-request.md b/.github/ISSUE_TEMPLATE/ffmpeg-feature-request.md deleted file mode 100644 index d47b011e1..000000000 --- a/.github/ISSUE_TEMPLATE/ffmpeg-feature-request.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -name: FFmpeg feature request -about: Request a feature of FFmpeg be exposed or supported by PyAV. -title: "Allow FOO to BAR" -assignees: '' - ---- - -**IMPORTANT:** Be sure to replace all template sections {{ like this }} or your issue may be discarded. - - -## Overview - -{{ A clear and concise description of what the feature is. }} - - -## Existing FFmpeg API - -{{ Link to appropriate FFmpeg documentation, ideally the API doxygen files at https://ffmpeg.org/doxygen/trunk/ }} - - -## Expected PyAV API - -{{ A description of how you think PyAV should behave. }} - -Example: -``` -{{ An example of how you think PyAV should behave. }} -``` - - -## Investigation - -{{ What you did to isolate the problem. }} - - -## Reproduction - -{{ Steps to reproduce the behavior. If the problem is media specific, include a link to it. Only send media that you have the rights to. }} - - -## Versions - -- OS: {{ e.g. macOS 10.13.6 }} -- PyAV runtime: -``` -{{ Complete output of `python -m av --version`. If this command won't run, you are likely dealing with the build issue and should use the appropriate template. }} -``` -- PyAV build: -``` -{{ Complete output of `python setup.py config --verbose`. }} -``` - -## Additional context - -{{ Add any other context about the problem here. }} diff --git a/.github/ISSUE_TEMPLATE/pyav-feature-request.md b/.github/ISSUE_TEMPLATE/pyav-feature-request.md deleted file mode 100644 index cd024d61d..000000000 --- a/.github/ISSUE_TEMPLATE/pyav-feature-request.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -name: PyAV feature request -about: Request a feature of PyAV that is not provided by FFmpeg. -title: "Allow FOO to BAR" -assignees: '' - ---- - -**IMPORTANT:** Be sure to replace all template sections {{ like this }} or your issue may be discarded. - - -## Desired Behavior - -{{ A description of how you think PyAV should behave. }} - - -## Example API - -``` -{{ An example of how you think PyAV should behave. }} -``` - - -## Additional context - -{{ Add any other context about the problem here. }} diff --git a/.github/ISSUE_TEMPLATE/runtime-bug-report.md b/.github/ISSUE_TEMPLATE/runtime-bug-report.md deleted file mode 100644 index 9c8632a7d..000000000 --- a/.github/ISSUE_TEMPLATE/runtime-bug-report.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -name: Runtime bug report -about: Report on an issue while running PyAV. -title: "The FOO does not BAR." -assignees: '' - ---- - -**IMPORTANT:** Be sure to replace all template sections {{ like this }} or your issue may be discarded. - - -## Overview - -{{ A clear and concise description of what the bug is. }} - - -## Expected behavior - -{{ A clear and concise description of what you expected to happen. }} - - -## Actual behavior - -{{ A clear and concise description of what actually happened. }} - -Traceback: -``` -{{ Include complete tracebacks if there are any exceptions. }} -``` - - -## Investigation - -{{ What you did to isolate the problem. }} - - -## Reproduction - -{{ Steps to reproduce the behavior. If the problem is media specific, include a link to it. Only send media that you have the rights to. }} - - -## Versions - -- OS: {{ e.g. macOS 10.13.6 }} -- PyAV runtime: -``` -{{ Complete output of `python -m av --version`. If this command won't run, you are likely dealing with the build issue and should use the appropriate template. }} -``` -- [ ] I am/tried using the binary wheels -- [ ] I compiled from source - -## Research - -I have done the following: - -- [ ] Checked the [PyAV documentation](https://pyav.basswood-io.com) -- [ ] Searched on [Google](https://www.google.com/search?q=pyav+how+do+I+foo) -- [ ] Searched on [Stack Overflow](https://stackoverflow.com/search?q=pyav) -- [ ] Looked through [old GitHub issues](https://github.com/PyAV-Org/PyAV/issues?&q=is%3Aissue) - -## Additional context - -{{ Add any other context about the problem here. }} From 970963e3264b0966be71935e35ee57eddca1c637 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 1 Feb 2025 14:21:37 -0500 Subject: [PATCH 467/846] Update authors --- AUTHORS.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index d5b4cb7ee..846cf0d93 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -31,12 +31,15 @@ All contributors (by number of commits): - David Plowman - Alireza Davoudi ; `@adavoudi `_ - Jonathan Drolet +- Lukas Geiger +- Matthew Lai - Moritz Kassner ; `@mkassner `_ - Thomas A Caswell ; `@tacaswell `_ - Ulrik Mikaelsson ; `@rawler `_ - Wel C. van der - Will Patera ; `@willpatera `_ +* Joe Schiff <41972063+JoeSchiff@users.noreply.github.com> * Dexer <73297572+DexerBR@users.noreply.github.com> * rutsh * Felix Vollmer @@ -53,11 +56,11 @@ All contributors (by number of commits): * Pablo Prietz * Andrew Wason * Radek Senfeld ; `@radek-senfeld `_ +* robinechuca * Benjamin Chrétien <2742231+bchretien@users.noreply.github.com> * zzjjbb <31069326+zzjjbb@users.noreply.github.com> * davidplowman <38045873+davidplowman@users.noreply.github.com> * Hanz <40712686+HanzCEO@users.noreply.github.com> -* Joe Schiff <41972063+JoeSchiff@users.noreply.github.com> * Artturin * Ian Lee * Ryan Huang @@ -73,7 +76,7 @@ All contributors (by number of commits): * Jonathan Martin * Johan Jeppsson Karlin * Philipp Klaus -* Lukas Geiger +* Kim Minjong * Mattias Wadman * Manuel Goacolou * Julian Schweizer @@ -99,3 +102,4 @@ All contributors (by number of commits): * Koichi Akabe * David Joy * Sviatoslav Sydorenko (Святослав Сидоренко) +* z-khan From ad6ffe42482b825899b611594a8aeb980dab0f72 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 1 Feb 2025 15:11:24 -0500 Subject: [PATCH 468/846] Inline make_error --- av/container/core.pyi | 1 - av/error.pyx | 39 +++++++++++++++++---------------------- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/av/container/core.pyi b/av/container/core.pyi index 7ba2511cf..fa996d0d2 100644 --- a/av/container/core.pyi +++ b/av/container/core.pyi @@ -94,7 +94,6 @@ class Container: exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> bool: ... - def err_check(self, value: int) -> int: ... def set_timeout(self, timeout: Real | None) -> None: ... def start_timeout(self) -> None: ... diff --git a/av/error.pyx b/av/error.pyx index 426d01ed0..3ef0f44a4 100644 --- a/av/error.pyx +++ b/av/error.pyx @@ -379,27 +379,6 @@ cdef int stash_exception(exc_info=None): cdef int _last_log_count = 0 - -cpdef make_error(int res, filename=None, log=None): - cdef int code = -res - cdef char* error_buffer = malloc(lib.AV_ERROR_MAX_STRING_SIZE * sizeof(char)) - if error_buffer == NULL: - raise MemoryError() - - try: - if code == c_PYAV_STASHED_ERROR: - message = PYAV_STASHED_ERROR_message - else: - lib.av_strerror(res, error_buffer, lib.AV_ERROR_MAX_STRING_SIZE) - # Fallback to OS error string if no message - message = error_buffer or os.strerror(code) - - cls = classes.get(code, UndefinedError) - return cls(code, message, filename, log) - finally: - free(error_buffer) - - cpdef int err_check(int res, filename=None) except -1: """Raise appropriate exceptions from library return code.""" @@ -425,7 +404,23 @@ cpdef int err_check(int res, filename=None) except -1: else: log = None - raise make_error(res, filename, log) + cdef int code = -res + cdef char* error_buffer = malloc(lib.AV_ERROR_MAX_STRING_SIZE * sizeof(char)) + if error_buffer == NULL: + raise MemoryError() + + try: + if code == c_PYAV_STASHED_ERROR: + message = PYAV_STASHED_ERROR_message + else: + lib.av_strerror(res, error_buffer, lib.AV_ERROR_MAX_STRING_SIZE) + # Fallback to OS error string if no message + message = error_buffer or os.strerror(code) + + cls = classes.get(code, UndefinedError) + raise cls(code, message, filename, log) + finally: + free(error_buffer) class UndefinedError(FFmpegError): From 8b3b5c9e254ef85cbef5ebe37e11c2c1aa8c8241 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 1 Feb 2025 15:24:54 -0500 Subject: [PATCH 469/846] Use literal dict --- av/error.pyx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/av/error.pyx b/av/error.pyx index 3ef0f44a4..ea2b9d216 100644 --- a/av/error.pyx +++ b/av/error.pyx @@ -338,13 +338,13 @@ for enum_name, code, name, base in _ffmpeg_specs: name = name or enum_name.title().replace("_", "") + "Error" if base is None: - bases = (FFmpegError, ) + bases = (FFmpegError,) elif issubclass(base, FFmpegError): - bases = (base, ) + bases = (base,) else: bases = (FFmpegError, base) - cls = type(name, bases, dict(__module__=__name__)) + cls = type(name, bases, {"__module__": __name__}) # Register in builder. classes[code] = cls From 824987ba1229c006a00b0f5d19ec149cee65ae27 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 1 Feb 2025 18:33:40 -0500 Subject: [PATCH 470/846] Add mode to Codec, display C func in err --- av/codec/codec.pyi | 2 ++ av/codec/codec.pyx | 6 +++++- av/codec/context.pyx | 8 ++++---- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/av/codec/codec.pyi b/av/codec/codec.pyi index 73c1f1edb..3a6c3a866 100644 --- a/av/codec/codec.pyi +++ b/av/codec/codec.pyi @@ -51,6 +51,8 @@ class Codec: def is_encoder(self) -> bool: ... @property def is_decoder(self) -> bool: ... + @property + def mode(self) -> Literal["r", "w"]: ... descriptor: Descriptor @property def name(self) -> str: ... diff --git a/av/codec/codec.pyx b/av/codec/codec.pyx index 73f085e8d..3e9e028f8 100644 --- a/av/codec/codec.pyx +++ b/av/codec/codec.pyx @@ -119,7 +119,7 @@ cdef class Codec: raise RuntimeError("%s is both encoder and decoder.") def __repr__(self): - mode = "w" if self.is_encoder else "r" + mode = self.mode return f"" def create(self, kind = None): @@ -130,6 +130,10 @@ cdef class Codec: from .context import CodecContext return CodecContext.create(self) + @property + def mode(self): + return "w" if self.is_encoder else "r" + @property def is_decoder(self): return not self.is_encoder diff --git a/av/codec/context.pyx b/av/codec/context.pyx index e7136b86f..a56f14ddd 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -324,7 +324,7 @@ cdef class CodecContext: cdef int res with nogil: res = lib.avcodec_send_frame(self.ptr, frame.ptr if frame is not None else NULL) - err_check(res) + err_check(res, "avcodec_send_frame()") packet = self._recv_packet() while packet: @@ -337,7 +337,7 @@ cdef class CodecContext: cdef int res with nogil: res = lib.avcodec_send_packet(self.ptr, packet.ptr if packet is not None else NULL) - err_check(res) + err_check(res, "avcodec_send_packet()") out = [] while True: @@ -365,7 +365,7 @@ cdef class CodecContext: if res == -EAGAIN or res == lib.AVERROR_EOF: return - err_check(res) + err_check(res, "avcodec_receive_frame()") frame = self._transfer_hwframe(frame) @@ -384,7 +384,7 @@ cdef class CodecContext: res = lib.avcodec_receive_packet(self.ptr, packet.ptr) if res == -EAGAIN or res == lib.AVERROR_EOF: return - err_check(res) + err_check(res, "avcodec_receive_packet()") if not res: return packet From a961fd42ff519d83993854b0356c9cdb4ca09450 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 2 Feb 2025 12:18:59 -0500 Subject: [PATCH 471/846] Remove unused code for passing in a Codec object --- av/container/output.pyx | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/av/container/output.pyx b/av/container/output.pyx index e61ef2297..c2055486e 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -51,23 +51,15 @@ cdef class OutputContainer(Container): Supports video, audio, and subtitle streams. :param codec_name: The name of a codec. - :type codec_name: str | Codec + :type codec_name: str :param dict options: Stream options. :param \\**kwargs: Set attributes for the stream. :rtype: The new :class:`~av.stream.Stream`. """ - cdef const lib.AVCodec *codec - cdef Codec codec_obj - - if isinstance(codec_name, Codec): - if codec_name.mode != "w": - raise ValueError("codec_name must be an encoder.") - codec_obj = codec_name - else: - codec_obj = Codec(codec_name, "w") - codec = codec_obj.ptr + cdef Codec codec_obj = Codec(codec_name, "w") + cdef const lib.AVCodec *codec = codec_obj.ptr # Assert that this format supports the requested codec. if not lib.avformat_query_codec(self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL): From 6294e28a7d19d98eea9e0a8e6f17c94c93e3b30a Mon Sep 17 00:00:00 2001 From: z-khan Date: Mon, 3 Feb 2025 23:59:14 +1100 Subject: [PATCH 472/846] PYAV_LIBRARY_ROOT: make directory before change directory $PYAV_LIBRARY_ROOT should be created before calling cd $PYAV_LIBRARY_ROOT if building with NVIDIA Codec support. --- scripts/build-deps | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/scripts/build-deps b/scripts/build-deps index de4a6e547..9cc795305 100755 --- a/scripts/build-deps +++ b/scripts/build-deps @@ -13,6 +13,9 @@ if [[ -e "$PYAV_LIBRARY_PREFIX/bin/ffmpeg" ]]; then exit 0 fi +mkdir -p "$PYAV_LIBRARY_ROOT" +mkdir -p "$PYAV_LIBRARY_PREFIX" + # Add CUDA support if available CONFFLAGS_NVIDIA="" if [[ -e /usr/local/cuda ]]; then @@ -38,12 +41,8 @@ else echo " Building without NVIDIA NVENC/NVDEC support" fi - -mkdir -p "$PYAV_LIBRARY_ROOT" -mkdir -p "$PYAV_LIBRARY_PREFIX" cd "$PYAV_LIBRARY_ROOT" - # Download and expand the source. if [[ ! -d $PYAV_LIBRARY ]]; then url="https://ffmpeg.org/releases/$PYAV_LIBRARY.tar.gz" From 14ea9c1bab8089c3ad1b4f7f2242c24875435266 Mon Sep 17 00:00:00 2001 From: Matteo Destro Date: Tue, 4 Feb 2025 14:58:19 +0000 Subject: [PATCH 473/846] Add support for flags in hwaccel --- av/codec/hwaccel.pxd | 1 + av/codec/hwaccel.pyi | 1 + av/codec/hwaccel.pyx | 5 +++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/av/codec/hwaccel.pxd b/av/codec/hwaccel.pxd index e68f43bb1..46efdaf3b 100644 --- a/av/codec/hwaccel.pxd +++ b/av/codec/hwaccel.pxd @@ -18,3 +18,4 @@ cdef class HWAccel: cdef lib.AVBufferRef *ptr cdef public bint allow_software_fallback cdef public dict options + cdef public int flags diff --git a/av/codec/hwaccel.pyi b/av/codec/hwaccel.pyi index 7e4748e23..84f1da964 100644 --- a/av/codec/hwaccel.pyi +++ b/av/codec/hwaccel.pyi @@ -42,6 +42,7 @@ class HWAccel: device: str | None = None, allow_software_fallback: bool = False, options: dict[str, object] | None = None, + flags: int | None = None, ) -> None: ... def create(self, codec: Codec) -> HWAccel: ... diff --git a/av/codec/hwaccel.pyx b/av/codec/hwaccel.pyx index b80c194af..257e6e7b2 100644 --- a/av/codec/hwaccel.pyx +++ b/av/codec/hwaccel.pyx @@ -95,7 +95,7 @@ cpdef hwdevices_available(): cdef class HWAccel: - def __init__(self, device_type, device=None, allow_software_fallback=True, options=None): + def __init__(self, device_type, device=None, allow_software_fallback=True, options=None, flags=None): if isinstance(device_type, HWDeviceType): self._device_type = device_type elif isinstance(device_type, str): @@ -108,6 +108,7 @@ cdef class HWAccel: self._device = device self.allow_software_fallback = allow_software_fallback self.options = {} if not options else dict(options) + self.flags = 0 if not flags else flags self.ptr = NULL self.config = None @@ -132,7 +133,7 @@ cdef class HWAccel: err_check( lib.av_hwdevice_ctx_create( - &self.ptr, config.ptr.device_type, c_device, c_options.ptr, 0 + &self.ptr, config.ptr.device_type, c_device, c_options.ptr, self.flags ) ) From bf41ee097637af7ec1eca40500bfec0fbada45ad Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 4 Feb 2025 11:50:14 -0500 Subject: [PATCH 474/846] Put conda over installing from source --- README.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 10cd7709d..7e21198e7 100644 --- a/README.md +++ b/README.md @@ -17,30 +17,33 @@ But where you can't work without it, PyAV is a critical tool. Installation ------------ -Due to the complexity of the dependencies, PyAV is not always the easiest Python package to install from source. Since release 8.0.0 binary wheels are provided on [PyPI][pypi] for Linux, Mac and Windows linked against a modern FFmpeg. You can install these wheels by running: +Binary wheels are provided on [PyPI][pypi] for Linux, MacOS and Windows linked against the latest stable version of ffmpeg. You can install these wheels by running: ```bash pip install av ``` -If you want to use your existing FFmpeg, the source version of PyAV is on [PyPI][pypi] too: +Another way of installing PyAV is via [conda-forge][conda-forge]: ```bash -pip install av --no-binary av +conda install av -c conda-forge ``` -Installing from source is not supported on Windows. +See the [Conda install][conda-install] docs to get started with (mini)Conda. + Alternative installation methods -------------------------------- -Another way of installing PyAV is via [conda-forge][conda-forge]: +Due to the complexity of the dependencies, PyAV is not always the easiest Python package to install from source. If you want to use your existing ffmpeg (must be the correct major version), the source version of PyAV is on [PyPI][pypi]: ```bash -conda install av -c conda-forge +pip install av --no-binary av ``` -See the [Conda install][conda-install] docs to get started with (mini)Conda. +> [!WARNING] +> This installation method won't work for Windows or Debian based systems. + And if you want to build from the absolute source (POSIX only): From 796d8afb657203bc5f1050db76b79e8b0d02de31 Mon Sep 17 00:00:00 2001 From: z-khan Date: Thu, 6 Feb 2025 12:58:20 +1100 Subject: [PATCH 475/846] Add "Bayer" pixel formats --- av/video/format.pyi | 5 ++++- av/video/format.pyx | 5 +++++ av/video/frame.pyx | 35 ++++++++++++++++++++++++++++++++++- 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/av/video/format.pyi b/av/video/format.pyi index ee16b85b8..e102ef4c0 100644 --- a/av/video/format.pyi +++ b/av/video/format.pyi @@ -6,7 +6,10 @@ class VideoFormat: has_palette: bool is_bit_stream: bool is_planar: bool - is_rgb: bool + @property + def is_rgb(self) -> bool: ... + @property + def is_bayer(self) -> bool: ... width: int height: int components: tuple[VideoFormatComponent, ...] diff --git a/av/video/format.pyx b/av/video/format.pyx index 6ae66c3a2..8779f7151 100644 --- a/av/video/format.pyx +++ b/av/video/format.pyx @@ -104,7 +104,12 @@ cdef class VideoFormat: def is_rgb(self): """The pixel format contains RGB-like data (as opposed to YUV/grayscale).""" return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_RGB) + + @property + def is_bayer(self): + """The pixel format contains Bayer data.""" + return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_BAYER) cpdef chroma_width(self, int luma_width=0): """chroma_width(luma_width=0) diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 36f4deae1..b74d58d1c 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -317,6 +317,18 @@ cdef class VideoFrame(Frame): itemsize, dtype = { "abgr": (4, "uint8"), "argb": (4, "uint8"), + "bayer_bggr8": (1, "uint8"), + "bayer_gbrg8": (1, "uint8"), + "bayer_grbg8": (1, "uint8"), + "bayer_rggb8": (1, "uint8"), + "bayer_bggr16le": (2, "uint16"), + "bayer_bggr16be": (2, "uint16"), + "bayer_gbrg16le": (2, "uint16"), + "bayer_gbrg16be": (2, "uint16"), + "bayer_grbg16le": (2, "uint16"), + "bayer_grbg16be": (2, "uint16"), + "bayer_rggb16le": (2, "uint16"), + "bayer_rggb16be": (2, "uint16"), "bgr24": (3, "uint8"), "bgr8": (1, "uint8"), "bgra": (4, "uint8"), @@ -451,6 +463,13 @@ cdef class VideoFrame(Frame): else: # Planes where U and V are interleaved have the same stride as Y. linesizes = (array.strides[0], array.strides[0]) + elif format in {"bayer_bggr8", "bayer_rggb8", "bayer_gbrg8", "bayer_grbg8","bayer_bggr16le", "bayer_rggb16le", "bayer_gbrg16le", "bayer_grbg16le","bayer_bggr16be", "bayer_rggb16be", "bayer_gbrg16be", "bayer_grbg16be"}: + check_ndarray(array, "uint8" if format.endswith("8") else "uint16", 2) + + if array.strides[1] != (1 if format.endswith("8") else 2): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + + linesizes = (array.strides[0],) else: raise ValueError(f"Conversion from numpy array with format `{format}` is not yet supported") @@ -554,7 +573,19 @@ cdef class VideoFrame(Frame): "yuv444p16be": (3, 2, "uint16"), "yuv444p16le": (3, 2, "uint16"), "yuva444p16be": (4, 2, "uint16"), - "yuva444p16le": (4, 2, "uint16"), + "yuva444p16le": (4, 2, "uint16"), + "bayer_bggr8": (1, 1, "uint8"), + "bayer_rggb8": (1, 1, "uint8"), + "bayer_grbg8": (1, 1, "uint8"), + "bayer_gbrg8": (1, 1, "uint8"), + "bayer_bggr16be": (1, 2, "uint16"), + "bayer_bggr16le": (1, 2, "uint16"), + "bayer_rggb16be": (1, 2, "uint16"), + "bayer_rggb16le": (1, 2, "uint16"), + "bayer_grbg16be": (1, 2, "uint16"), + "bayer_grbg16le": (1, 2, "uint16"), + "bayer_gbrg16be": (1, 2, "uint16"), + "bayer_gbrg16le": (1, 2, "uint16"), }.get(format, (None, None, None)) if channels is not None: if array.ndim == 2: # (height, width) -> (height, width, 1) @@ -644,6 +675,8 @@ cdef class VideoFrame(Frame): frame = VideoFrame(width, height, format) if format == "rgba": copy_bytes_to_plane(img_bytes, frame.planes[0], 4, flip_horizontal, flip_vertical) + elif format in ("bayer_bggr8", "bayer_rggb8", "bayer_gbrg8", "bayer_grbg8","bayer_bggr16le", "bayer_rggb16le", "bayer_gbrg16le", "bayer_grbg16le","bayer_bggr16be", "bayer_rggb16be", "bayer_gbrg16be", "bayer_grbg16be"): + copy_bytes_to_plane(img_bytes, frame.planes[0], 1 if format.endswith("8") else 2, flip_horizontal, flip_vertical) else: raise NotImplementedError(f"Format '{format}' is not supported.") return frame From a371f43c165902b59e3fb96d2ca8aa5b4c823ea3 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 5 Feb 2025 21:30:00 -0500 Subject: [PATCH 476/846] Use new Enum syntax --- av/codec/codec.pyi | 60 +++++++++++------------ av/codec/context.pyi | 70 +++++++++++++-------------- av/codec/hwaccel.pyi | 37 +++++++------- av/container/core.pyi | 102 +++++++++++++++++++-------------------- av/format.pyi | 36 +++++++------- av/option.pyi | 53 ++++++++++---------- av/sidedata/sidedata.pyi | 58 +++++++++++----------- av/stream.pyi | 40 +++++++-------- av/video/frame.pyi | 16 +++--- av/video/reformatter.pyi | 59 +++++++++++----------- 10 files changed, 267 insertions(+), 264 deletions(-) diff --git a/av/codec/codec.pyi b/av/codec/codec.pyi index 3a6c3a866..9f80a4909 100644 --- a/av/codec/codec.pyi +++ b/av/codec/codec.pyi @@ -1,6 +1,6 @@ from enum import Flag, IntEnum from fractions import Fraction -from typing import ClassVar, Literal, overload +from typing import ClassVar, Literal, cast, overload from av.audio.codeccontext import AudioCodecContext from av.audio.format import AudioFormat @@ -12,37 +12,37 @@ from av.video.format import VideoFormat from .context import CodecContext class Properties(Flag): - NONE: ClassVar[Properties] - INTRA_ONLY: ClassVar[Properties] - LOSSY: ClassVar[Properties] - LOSSLESS: ClassVar[Properties] - REORDER: ClassVar[Properties] - BITMAP_SUB: ClassVar[Properties] - TEXT_SUB: ClassVar[Properties] + NONE = cast(ClassVar[Properties], ...) + INTRA_ONLY = cast(ClassVar[Properties], ...) + LOSSY = cast(ClassVar[Properties], ...) + LOSSLESS = cast(ClassVar[Properties], ...) + REORDER = cast(ClassVar[Properties], ...) + BITMAP_SUB = cast(ClassVar[Properties], ...) + TEXT_SUB = cast(ClassVar[Properties], ...) class Capabilities(IntEnum): - none: int - draw_horiz_band: int - dr1: int - hwaccel: int - delay: int - small_last_frame: int - hwaccel_vdpau: int - subframes: int - experimental: int - channel_conf: int - neg_linesizes: int - frame_threads: int - slice_threads: int - param_change: int - auto_threads: int - variable_frame_size: int - avoid_probing: int - hardware: int - hybrid: int - encoder_reordered_opaque: int - encoder_flush: int - encoder_recon_frame: int + none = cast(int, ...) + draw_horiz_band = cast(int, ...) + dr1 = cast(int, ...) + hwaccel = cast(int, ...) + delay = cast(int, ...) + small_last_frame = cast(int, ...) + hwaccel_vdpau = cast(int, ...) + subframes = cast(int, ...) + experimental = cast(int, ...) + channel_conf = cast(int, ...) + neg_linesizes = cast(int, ...) + frame_threads = cast(int, ...) + slice_threads = cast(int, ...) + param_change = cast(int, ...) + auto_threads = cast(int, ...) + variable_frame_size = cast(int, ...) + avoid_probing = cast(int, ...) + hardware = cast(int, ...) + hybrid = cast(int, ...) + encoder_reordered_opaque = cast(int, ...) + encoder_flush = cast(int, ...) + encoder_recon_frame = cast(int, ...) class UnknownCodecError(ValueError): ... diff --git a/av/codec/context.pyi b/av/codec/context.pyi index 543da208e..ac57126ef 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -1,6 +1,6 @@ from enum import Flag, IntEnum from fractions import Fraction -from typing import ClassVar, Literal +from typing import ClassVar, Literal, cast from av.packet import Packet @@ -8,46 +8,46 @@ from .codec import Codec from .hwaccel import HWAccel class ThreadType(Flag): - NONE: ClassVar[ThreadType] - FRAME: ClassVar[ThreadType] - SLICE: ClassVar[ThreadType] - AUTO: ClassVar[ThreadType] + NONE = cast(ClassVar[ThreadType], ...) + FRAME = cast(ClassVar[ThreadType], ...) + SLICE = cast(ClassVar[ThreadType], ...) + AUTO = cast(ClassVar[ThreadType], ...) def __get__(self, i: object | None, owner: type | None = None) -> ThreadType: ... def __set__(self, instance: object, value: int | str | ThreadType) -> None: ... class Flags(IntEnum): - unaligned: int - qscale: int - four_mv: int - output_corrupt: int - qpel: int - drop_changed: int - recon_frame: int - copy_opaque: int - frame_duration: int - pass1: int - pass2: int - loop_filter: int - gray: int - psnr: int - interlaced_dct: int - low_delay: int - global_header: int - bitexact: int - ac_pred: int - interlaced_me: int - closed_gop: int + unaligned = cast(int, ...) + qscale = cast(int, ...) + four_mv = cast(int, ...) + output_corrupt = cast(int, ...) + qpel = cast(int, ...) + drop_changed = cast(int, ...) + recon_frame = cast(int, ...) + copy_opaque = cast(int, ...) + frame_duration = cast(int, ...) + pass1 = cast(int, ...) + pass2 = cast(int, ...) + loop_filter = cast(int, ...) + gray = cast(int, ...) + psnr = cast(int, ...) + interlaced_dct = cast(int, ...) + low_delay = cast(int, ...) + global_header = cast(int, ...) + bitexact = cast(int, ...) + ac_pred = cast(int, ...) + interlaced_me = cast(int, ...) + closed_gop = cast(int, ...) class Flags2(IntEnum): - fast: int - no_output: int - local_header: int - chunks: int - ignore_crop: int - show_all: int - export_mvs: int - skip_manual: int - ro_flush_noop: int + fast = cast(int, ...) + no_output = cast(int, ...) + local_header = cast(int, ...) + chunks = cast(int, ...) + ignore_crop = cast(int, ...) + show_all = cast(int, ...) + export_mvs = cast(int, ...) + skip_manual = cast(int, ...) + ro_flush_noop = cast(int, ...) class CodecContext: name: str diff --git a/av/codec/hwaccel.pyi b/av/codec/hwaccel.pyi index 84f1da964..8bdc0a6e0 100644 --- a/av/codec/hwaccel.pyi +++ b/av/codec/hwaccel.pyi @@ -1,29 +1,30 @@ from enum import IntEnum +from typing import cast from av.codec.codec import Codec from av.video.format import VideoFormat class HWDeviceType(IntEnum): - none: int - vdpau: int - cuda: int - vaapi: int - dxva2: int - qsv: int - videotoolbox: int - d3d11va: int - drm: int - opencl: int - mediacodec: int - vulkan: int - d3d12va: int + none = cast(int, ...) + vdpau = cast(int, ...) + cuda = cast(int, ...) + vaapi = cast(int, ...) + dxva2 = cast(int, ...) + qsv = cast(int, ...) + videotoolbox = cast(int, ...) + d3d11va = cast(int, ...) + drm = cast(int, ...) + opencl = cast(int, ...) + mediacodec = cast(int, ...) + vulkan = cast(int, ...) + d3d12va = cast(int, ...) class HWConfigMethod(IntEnum): - none: int - hw_device_ctx: int - hw_frame_ctx: int - internal: int - ad_hoc: int + none = cast(int, ...) + hw_device_ctx = cast(int, ...) + hw_frame_ctx = cast(int, ...) + internal = cast(int, ...) + ad_hoc = cast(int, ...) class HWConfig: @property diff --git a/av/container/core.pyi b/av/container/core.pyi index fa996d0d2..d61d07110 100644 --- a/av/container/core.pyi +++ b/av/container/core.pyi @@ -2,7 +2,7 @@ from enum import Flag, IntEnum from fractions import Fraction from pathlib import Path from types import TracebackType -from typing import Any, Callable, ClassVar, Literal, Type, overload +from typing import Any, Callable, ClassVar, Literal, Type, cast, overload from av.codec.hwaccel import HWAccel from av.format import ContainerFormat @@ -14,58 +14,58 @@ from .streams import StreamContainer Real = int | float | Fraction class Flags(Flag): - gen_pts: ClassVar[Flags] - ign_idx: ClassVar[Flags] - non_block: ClassVar[Flags] - ign_dts: ClassVar[Flags] - no_fillin: ClassVar[Flags] - no_parse: ClassVar[Flags] - no_buffer: ClassVar[Flags] - custom_io: ClassVar[Flags] - discard_corrupt: ClassVar[Flags] - flush_packets: ClassVar[Flags] - bitexact: ClassVar[Flags] - sort_dts: ClassVar[Flags] - fast_seek: ClassVar[Flags] - shortest: ClassVar[Flags] - auto_bsf: ClassVar[Flags] + gen_pts = cast(ClassVar[Flags], ...) + ign_idx = cast(ClassVar[Flags], ...) + non_block = cast(ClassVar[Flags], ...) + ign_dts = cast(ClassVar[Flags], ...) + no_fillin = cast(ClassVar[Flags], ...) + no_parse = cast(ClassVar[Flags], ...) + no_buffer = cast(ClassVar[Flags], ...) + custom_io = cast(ClassVar[Flags], ...) + discard_corrupt = cast(ClassVar[Flags], ...) + flush_packets = cast(ClassVar[Flags], ...) + bitexact = cast(ClassVar[Flags], ...) + sort_dts = cast(ClassVar[Flags], ...) + fast_seek = cast(ClassVar[Flags], ...) + shortest = cast(ClassVar[Flags], ...) + auto_bsf = cast(ClassVar[Flags], ...) class AudioCodec(IntEnum): - none: int - pcm_alaw: int - pcm_bluray: int - pcm_dvd: int - pcm_f16le: int - pcm_f24le: int - pcm_f32be: int - pcm_f32le: int - pcm_f64be: int - pcm_f64le: int - pcm_lxf: int - pcm_mulaw: int - pcm_s16be: int - pcm_s16be_planar: int - pcm_s16le: int - pcm_s16le_planar: int - pcm_s24be: int - pcm_s24daud: int - pcm_s24le: int - pcm_s24le_planar: int - pcm_s32be: int - pcm_s32le: int - pcm_s32le_planar: int - pcm_s64be: int - pcm_s64le: int - pcm_s8: int - pcm_s8_planar: int - pcm_u16be: int - pcm_u16le: int - pcm_u24be: int - pcm_u24le: int - pcm_u32be: int - pcm_u32le: int - pcm_u8: int - pcm_vidc: int + none = cast(int, ...) + pcm_alaw = cast(int, ...) + pcm_bluray = cast(int, ...) + pcm_dvd = cast(int, ...) + pcm_f16le = cast(int, ...) + pcm_f24le = cast(int, ...) + pcm_f32be = cast(int, ...) + pcm_f32le = cast(int, ...) + pcm_f64be = cast(int, ...) + pcm_f64le = cast(int, ...) + pcm_lxf = cast(int, ...) + pcm_mulaw = cast(int, ...) + pcm_s16be = cast(int, ...) + pcm_s16be_planar = cast(int, ...) + pcm_s16le = cast(int, ...) + pcm_s16le_planar = cast(int, ...) + pcm_s24be = cast(int, ...) + pcm_s24daud = cast(int, ...) + pcm_s24le = cast(int, ...) + pcm_s24le_planar = cast(int, ...) + pcm_s32be = cast(int, ...) + pcm_s32le = cast(int, ...) + pcm_s32le_planar = cast(int, ...) + pcm_s64be = cast(int, ...) + pcm_s64le = cast(int, ...) + pcm_s8 = cast(int, ...) + pcm_s8_planar = cast(int, ...) + pcm_u16be = cast(int, ...) + pcm_u16le = cast(int, ...) + pcm_u24be = cast(int, ...) + pcm_u24le = cast(int, ...) + pcm_u32be = cast(int, ...) + pcm_u32le = cast(int, ...) + pcm_u8 = cast(int, ...) + pcm_vidc = cast(int, ...) class Container: writeable: bool diff --git a/av/format.pyi b/av/format.pyi index b30a84bf6..81c5c42ca 100644 --- a/av/format.pyi +++ b/av/format.pyi @@ -1,26 +1,26 @@ __all__ = ("Flags", "ContainerFormat", "formats_available") from enum import Flag -from typing import ClassVar, Literal +from typing import ClassVar, Literal, cast class Flags(Flag): - no_file: ClassVar[Flags] - need_number: ClassVar[Flags] - show_ids: ClassVar[Flags] - global_header: ClassVar[Flags] - no_timestamps: ClassVar[Flags] - generic_index: ClassVar[Flags] - ts_discont: ClassVar[Flags] - variable_fps: ClassVar[Flags] - no_dimensions: ClassVar[Flags] - no_streams: ClassVar[Flags] - no_bin_search: ClassVar[Flags] - no_gen_search: ClassVar[Flags] - no_byte_seek: ClassVar[Flags] - allow_flush: ClassVar[Flags] - ts_nonstrict: ClassVar[Flags] - ts_negative: ClassVar[Flags] - seek_to_pts: ClassVar[Flags] + no_file = cast(ClassVar[Flags], ...) + need_number = cast(ClassVar[Flags], ...) + show_ids = cast(ClassVar[Flags], ...) + global_header = cast(ClassVar[Flags], ...) + no_timestamps = cast(ClassVar[Flags], ...) + generic_index = cast(ClassVar[Flags], ...) + ts_discont = cast(ClassVar[Flags], ...) + variable_fps = cast(ClassVar[Flags], ...) + no_dimensions = cast(ClassVar[Flags], ...) + no_streams = cast(ClassVar[Flags], ...) + no_bin_search = cast(ClassVar[Flags], ...) + no_gen_search = cast(ClassVar[Flags], ...) + no_byte_seek = cast(ClassVar[Flags], ...) + allow_flush = cast(ClassVar[Flags], ...) + ts_nonstrict = cast(ClassVar[Flags], ...) + ts_negative = cast(ClassVar[Flags], ...) + seek_to_pts = cast(ClassVar[Flags], ...) class ContainerFormat: def __init__(self, name: str, mode: Literal["r", "w"] | None = None) -> None: ... diff --git a/av/option.pyi b/av/option.pyi index 3132f4a02..f989a1138 100644 --- a/av/option.pyi +++ b/av/option.pyi @@ -1,34 +1,35 @@ from enum import Enum, Flag +from typing import cast class OptionType(Enum): - FLAGS: int - INT: int - INT64: int - DOUBLE: int - FLOAT: int - STRING: int - RATIONAL: int - BINARY: int - DICT: int - CONST: int - IMAGE_SIZE: int - PIXEL_FMT: int - SAMPLE_FMT: int - VIDEO_RATE: int - DURATION: int - COLOR: int - CHANNEL_LAYOUT: int - BOOL: int + FLAGS = cast(int, ...) + INT = cast(int, ...) + INT64 = cast(int, ...) + DOUBLE = cast(int, ...) + FLOAT = cast(int, ...) + STRING = cast(int, ...) + RATIONAL = cast(int, ...) + BINARY = cast(int, ...) + DICT = cast(int, ...) + CONST = cast(int, ...) + IMAGE_SIZE = cast(int, ...) + PIXEL_FMT = cast(int, ...) + SAMPLE_FMT = cast(int, ...) + VIDEO_RATE = cast(int, ...) + DURATION = cast(int, ...) + COLOR = cast(int, ...) + CHANNEL_LAYOUT = cast(int, ...) + BOOL = cast(int, ...) class OptionFlags(Flag): - ENCODING_PARAM: int - DECODING_PARAM: int - AUDIO_PARAM: int - VIDEO_PARAM: int - SUBTITLE_PARAM: int - EXPORT: int - READONLY: int - FILTERING_PARAM: int + ENCODING_PARAM = cast(int, ...) + DECODING_PARAM = cast(int, ...) + AUDIO_PARAM = cast(int, ...) + VIDEO_PARAM = cast(int, ...) + SUBTITLE_PARAM = cast(int, ...) + EXPORT = cast(int, ...) + READONLY = cast(int, ...) + FILTERING_PARAM = cast(int, ...) class BaseOption: name: str diff --git a/av/sidedata/sidedata.pyi b/av/sidedata/sidedata.pyi index d165513ab..0093fabd0 100644 --- a/av/sidedata/sidedata.pyi +++ b/av/sidedata/sidedata.pyi @@ -1,39 +1,39 @@ from collections.abc import Mapping from enum import Enum -from typing import ClassVar, Iterator, Sequence, overload +from typing import ClassVar, Iterator, Sequence, cast, overload from av.buffer import Buffer from av.frame import Frame class Type(Enum): - PANSCAN: ClassVar[Type] - A53_CC: ClassVar[Type] - STEREO3D: ClassVar[Type] - MATRIXENCODING: ClassVar[Type] - DOWNMIX_INFO: ClassVar[Type] - REPLAYGAIN: ClassVar[Type] - DISPLAYMATRIX: ClassVar[Type] - AFD: ClassVar[Type] - MOTION_VECTORS: ClassVar[Type] - SKIP_SAMPLES: ClassVar[Type] - AUDIO_SERVICE_TYPE: ClassVar[Type] - MASTERING_DISPLAY_METADATA: ClassVar[Type] - GOP_TIMECODE: ClassVar[Type] - SPHERICAL: ClassVar[Type] - CONTENT_LIGHT_LEVEL: ClassVar[Type] - ICC_PROFILE: ClassVar[Type] - S12M_TIMECODE: ClassVar[Type] - DYNAMIC_HDR_PLUS: ClassVar[Type] - REGIONS_OF_INTEREST: ClassVar[Type] - VIDEO_ENC_PARAMS: ClassVar[Type] - SEI_UNREGISTERED: ClassVar[Type] - FILM_GRAIN_PARAMS: ClassVar[Type] - DETECTION_BBOXES: ClassVar[Type] - DOVI_RPU_BUFFER: ClassVar[Type] - DOVI_METADATA: ClassVar[Type] - DYNAMIC_HDR_VIVID: ClassVar[Type] - AMBIENT_VIEWING_ENVIRONMENT: ClassVar[Type] - VIDEO_HINT: ClassVar[Type] + PANSCAN = cast(ClassVar[Type], ...) + A53_CC = cast(ClassVar[Type], ...) + STEREO3D = cast(ClassVar[Type], ...) + MATRIXENCODING = cast(ClassVar[Type], ...) + DOWNMIX_INFO = cast(ClassVar[Type], ...) + REPLAYGAIN = cast(ClassVar[Type], ...) + DISPLAYMATRIX = cast(ClassVar[Type], ...) + AFD = cast(ClassVar[Type], ...) + MOTION_VECTORS = cast(ClassVar[Type], ...) + SKIP_SAMPLES = cast(ClassVar[Type], ...) + AUDIO_SERVICE_TYPE = cast(ClassVar[Type], ...) + MASTERING_DISPLAY_METADATA = cast(ClassVar[Type], ...) + GOP_TIMECODE = cast(ClassVar[Type], ...) + SPHERICAL = cast(ClassVar[Type], ...) + CONTENT_LIGHT_LEVEL = cast(ClassVar[Type], ...) + ICC_PROFILE = cast(ClassVar[Type], ...) + S12M_TIMECODE = cast(ClassVar[Type], ...) + DYNAMIC_HDR_PLUS = cast(ClassVar[Type], ...) + REGIONS_OF_INTEREST = cast(ClassVar[Type], ...) + VIDEO_ENC_PARAMS = cast(ClassVar[Type], ...) + SEI_UNREGISTERED = cast(ClassVar[Type], ...) + FILM_GRAIN_PARAMS = cast(ClassVar[Type], ...) + DETECTION_BBOXES = cast(ClassVar[Type], ...) + DOVI_RPU_BUFFER = cast(ClassVar[Type], ...) + DOVI_METADATA = cast(ClassVar[Type], ...) + DYNAMIC_HDR_VIVID = cast(ClassVar[Type], ...) + AMBIENT_VIEWING_ENVIRONMENT = cast(ClassVar[Type], ...) + VIDEO_HINT = cast(ClassVar[Type], ...) class SideData(Buffer): type: Type diff --git a/av/stream.pyi b/av/stream.pyi index a2a2e439c..88dc7c00b 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -1,30 +1,30 @@ from enum import Flag from fractions import Fraction -from typing import Literal +from typing import Literal, cast from .codec import Codec, CodecContext from .container import Container class Disposition(Flag): - default: int - dub: int - original: int - comment: int - lyrics: int - karaoke: int - forced: int - hearing_impaired: int - visual_impaired: int - clean_effects: int - attached_pic: int - timed_thumbnails: int - non_diegetic: int - captions: int - descriptions: int - metadata: int - dependent: int - still_image: int - multilayer: int + default = cast(int, ...) + dub = cast(int, ...) + original = cast(int, ...) + comment = cast(int, ...) + lyrics = cast(int, ...) + karaoke = cast(int, ...) + forced = cast(int, ...) + hearing_impaired = cast(int, ...) + visual_impaired = cast(int, ...) + clean_effects = cast(int, ...) + attached_pic = cast(int, ...) + timed_thumbnails = cast(int, ...) + non_diegetic = cast(int, ...) + captions = cast(int, ...) + descriptions = cast(int, ...) + metadata = cast(int, ...) + dependent = cast(int, ...) + still_image = cast(int, ...) + multilayer = cast(int, ...) class Stream: name: str | None diff --git a/av/video/frame.pyi b/av/video/frame.pyi index 3533c8e3f..d650cf44c 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -16,14 +16,14 @@ _SupportedNDarray = Union[ ] class PictureType(IntEnum): - NONE: int - I: int - P: int - B: int - S: int - SI: int - SP: int - BI: int + NONE = 0 + I = 1 + P = 2 + B = 3 + S = 4 + SI = 5 + SP = 6 + BI = 7 class VideoFrame(Frame): format: VideoFormat diff --git a/av/video/reformatter.pyi b/av/video/reformatter.pyi index fd5dbd053..5d83fcbe3 100644 --- a/av/video/reformatter.pyi +++ b/av/video/reformatter.pyi @@ -1,41 +1,42 @@ from enum import IntEnum +from typing import cast from .frame import VideoFrame class Interpolation(IntEnum): - FAST_BILINEAER: int - BILINEAR: int - BICUBIC: int - X: int - POINT: int - AREA: int - BICUBLIN: int - GAUSS: int - SINC: int - LANCZOS: int - SPLINE: int + FAST_BILINEAER = cast(int, ...) + BILINEAR = cast(int, ...) + BICUBIC = cast(int, ...) + X = cast(int, ...) + POINT = cast(int, ...) + AREA = cast(int, ...) + BICUBLIN = cast(int, ...) + GAUSS = cast(int, ...) + SINC = cast(int, ...) + LANCZOS = cast(int, ...) + SPLINE = cast(int, ...) class Colorspace(IntEnum): - ITU709: int - FCC: int - ITU601: int - ITU624: int - SMPTE170M: int - SMPTE240M: int - DEFAULT: int - itu709: int - fcc: int - itu601: int - itu624: int - smpte170m: int - smpte240m: int - default: int + ITU709 = cast(int, ...) + FCC = cast(int, ...) + ITU601 = cast(int, ...) + ITU624 = cast(int, ...) + SMPTE170M = cast(int, ...) + SMPTE240M = cast(int, ...) + DEFAULT = cast(int, ...) + itu709 = cast(int, ...) + fcc = cast(int, ...) + itu601 = cast(int, ...) + itu624 = cast(int, ...) + smpte170m = cast(int, ...) + smpte240m = cast(int, ...) + default = cast(int, ...) class ColorRange(IntEnum): - UNSPECIFIED: int - MPEG: int - JPEG: int - NB: int + UNSPECIFIED = 0 + MPEG = 1 + JPEG = 2 + NB = 3 class VideoReformatter: def reformat( From 6b03e4ea3bc4b7e92b872a38d59d87fece8f9cf0 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 5 Feb 2025 21:46:21 -0500 Subject: [PATCH 477/846] Upgrade mypy to 1.15 --- Makefile | 2 +- av/error.pyi | 4 ++-- av/error.pyx | 8 +++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 2afb2e4c3..667ff3116 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ fate-suite: rsync -vrltLW rsync://fate-suite.ffmpeg.org/fate-suite/ tests/assets/fate-suite/ lint: - $(PIP) install -U black isort flake8 flake8-pyproject pillow numpy mypy==1.13.0 pytest + $(PIP) install -U black isort flake8 flake8-pyproject pillow numpy mypy==1.15.0 pytest black --check av examples tests setup.py flake8 av isort --check-only --diff av examples tests diff --git a/av/error.pyi b/av/error.pyi index abbe2188c..e18f7334c 100644 --- a/av/error.pyi +++ b/av/error.pyi @@ -8,8 +8,8 @@ def tag_to_code(tag: bytes) -> int: ... def err_check(res: int, filename: str | None = None) -> int: ... class FFmpegError(Exception): - errno: int - strerror: str + errno: int | None + strerror: str | None filename: str log: tuple[int, tuple[int, str, str] | None] diff --git a/av/error.pyx b/av/error.pyx index ea2b9d216..b8b415207 100644 --- a/av/error.pyx +++ b/av/error.pyx @@ -95,11 +95,13 @@ class FFmpegError(Exception): pass def __str__(self): - msg = f"[Errno {self.errno}] {self.strerror}" - + msg = "" + if self.errno is not None: + msg = f"{msg}[Errno {self.errno}] " + if self.strerror is not None: + msg = f"{msg}{self.strerror}" if self.filename: msg = f"{msg}: {self.filename!r}" - if self.log: msg = f"{msg}; last error log: [{self.log[1].strip()}] {self.log[2].strip()}" From 003c6ed3bd94e16110fc76c8e0a1dbe5dd2ae1ec Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 7 Feb 2025 09:03:38 -0500 Subject: [PATCH 478/846] Check av_buffersrc_add_frame --- av/filter/loudnorm_impl.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/av/filter/loudnorm_impl.c b/av/filter/loudnorm_impl.c index 79f960080..f6e22e4ce 100644 --- a/av/filter/loudnorm_impl.c +++ b/av/filter/loudnorm_impl.c @@ -144,11 +144,13 @@ char* loudnorm_get_stats( // Flush decoder avcodec_send_packet(codec_ctx, NULL); while (avcodec_receive_frame(codec_ctx, frame) >= 0) { - av_buffersrc_add_frame(src_ctx, frame); + ret = av_buffersrc_add_frame(src_ctx, frame); + if (ret < 0) goto end; } // Flush filter - av_buffersrc_add_frame(src_ctx, NULL); + ret = av_buffersrc_add_frame(src_ctx, NULL); + if (ret < 0) goto end; while (av_buffersink_get_frame(sink_ctx, filt_frame) >= 0) { av_frame_unref(filt_frame); } @@ -196,4 +198,4 @@ char* loudnorm_get_stats( av_log_set_callback(av_log_default_callback); return result; -} \ No newline at end of file +} From 3b1b44dcd9047584d2a91c9c525b4949b2b5e502 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 9 Feb 2025 17:21:11 -0500 Subject: [PATCH 479/846] Drop MacOS 11 --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9ef90a4e3..5bf768a28 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -53,7 +53,7 @@ jobs: - name: Set Minimum MacOS Target if: matrix.os == 'macos-13' || matrix.os == 'macos-14' run: | - echo "MACOSX_DEPLOYMENT_TARGET=11.0" >> $GITHUB_ENV + echo "MACOSX_DEPLOYMENT_TARGET=12.0" >> $GITHUB_ENV - name: Build wheels env: CIBW_ARCHS: ${{ matrix.arch }} From e228c9c3f06d309dea19e71701522e3d6d71c4f8 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 11 Feb 2025 14:38:37 -0500 Subject: [PATCH 480/846] Pin ubuntu version --- .github/workflows/smoke.yml | 10 +++++----- .github/workflows/tests.yml | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index e4bd3af4b..416f6fecb 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -14,7 +14,7 @@ on: - '**.txt' jobs: style: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v4 @@ -34,9 +34,9 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-latest, python: "3.12", ffmpeg: "7.1", extras: true} - - {os: ubuntu-latest, python: "3.9", ffmpeg: "7.0.2"} - - {os: ubuntu-latest, python: "pypy3.10", ffmpeg: "7.1"} + - {os: ubuntu-24.04, python: "3.12", ffmpeg: "7.1", extras: true} + - {os: ubuntu-24.04, python: "3.9", ffmpeg: "7.0.2"} + - {os: ubuntu-24.04, python: "pypy3.10", ffmpeg: "7.1"} - {os: macos-14, python: "3.9", ffmpeg: "7.1"} - {os: macos-14, python: "3.9", ffmpeg: "7.0.2"} @@ -56,7 +56,7 @@ jobs: - name: OS Packages run: | case ${{ matrix.config.os }} in - ubuntu-latest) + ubuntu-24.04) sudo apt-get update sudo apt-get install autoconf automake build-essential cmake \ libtool pkg-config nasm zlib1g-dev libvorbis-dev libx264-dev diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5bf768a28..a6c5232b3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,7 +5,7 @@ on: workflow_dispatch: jobs: package-source: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 @@ -34,9 +34,9 @@ jobs: arch: x86_64 - os: ubuntu-24.04-arm arch: aarch64 - - os: ubuntu-latest + - os: ubuntu-24.04 arch: i686 - - os: ubuntu-latest + - os: ubuntu-24.04 arch: x86_64 - os: windows-latest arch: AMD64 From ebb839db6921946770cc767f176dc2cf9e6a6b7a Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 12 Feb 2025 18:21:20 -0500 Subject: [PATCH 481/846] Add supported_np_pix_fmts --- av/video/frame.pyi | 2 ++ av/video/frame.pyx | 13 +++++++++++++ tests/test_videoframe.py | 18 ++++++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/av/video/frame.pyi b/av/video/frame.pyi index d650cf44c..bba60cc5d 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -15,6 +15,8 @@ _SupportedNDarray = Union[ np.ndarray[Any, np.dtype[np.float32]], ] +supported_np_pix_fmts: set[str] + class PictureType(IntEnum): NONE = 0 I = 1 diff --git a/av/video/frame.pyx b/av/video/frame.pyx index b74d58d1c..bb8f83486 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -12,6 +12,19 @@ from av.video.plane cimport VideoPlane cdef object _cinit_bypass_sentinel +# `pix_fmt`s supported by Frame.to_ndarray() and Frame.from_ndarray() +supported_np_pix_fmts = { + "abgr", "argb", "bayer_bggr16be", "bayer_bggr16le", "bayer_bggr8", "bayer_gbrg16be", + "bayer_gbrg16le", "bayer_gbrg8", "bayer_grbg16be", "bayer_grbg16le", "bayer_grbg8", + "bayer_rggb16be", "bayer_rggb16le", "bayer_rggb8", "bgr24", "bgr8", "bgra", + "gbrapf32be", "gbrapf32le", "gbrp", "gbrp10be", "gbrp10le", "gbrp12be", "gbrp12le", + "gbrp14be", "gbrp14le", "gbrp16be", "gbrp16le", "gbrpf32be", "gbrpf32le", "gray", + "gray16be", "gray16le", "gray8", "grayf32be", "grayf32le", "nv12", "pal8", "rgb24", + "rgb48be", "rgb48le", "rgb8", "rgba", "rgba64be", "rgba64le", "yuv420p", "yuv444p", + "yuv444p16be", "yuv444p16le", "yuva444p16be", "yuva444p16le", "yuvj420p", + "yuvj444p", "yuyv422", +} + cdef VideoFrame alloc_video_frame(): """Get a mostly uninitialized VideoFrame. diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 9581a7c41..5ae603f64 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -209,6 +209,7 @@ def test_ndarray_gray() -> None: array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) for format in ("gray", "gray8"): frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == "gray" assertNdarraysEqual(frame.to_ndarray(), array) @@ -218,6 +219,7 @@ def test_ndarray_gray_align() -> None: array = numpy.random.randint(0, 256, size=(238, 318), dtype=numpy.uint8) for format in ("gray", "gray8"): frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts assert frame.width == 318 and frame.height == 238 assert frame.format.name == "gray" assertNdarraysEqual(frame.to_ndarray(), array) @@ -227,6 +229,7 @@ def test_ndarray_grayf32() -> None: array = numpy.random.random_sample(size=(480, 640)).astype(numpy.float32) for format in ("grayf32be", "grayf32le"): frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -236,6 +239,7 @@ def test_ndarray_grayf32_align() -> None: array = numpy.random.random_sample(size=(238, 318)).astype(numpy.float32) for format in ("grayf32be", "grayf32le"): frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts assert frame.width == 318 and frame.height == 238 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -245,6 +249,7 @@ def test_ndarray_rgb() -> None: array = numpy.random.randint(0, 256, size=(480, 640, 3), dtype=numpy.uint8) for format in ("rgb24", "bgr24"): frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -254,6 +259,7 @@ def test_ndarray_rgb_align() -> None: array = numpy.random.randint(0, 256, size=(238, 318, 3), dtype=numpy.uint8) for format in ("rgb24", "bgr24"): frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts assert frame.width == 318 and frame.height == 238 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -263,6 +269,7 @@ def test_ndarray_rgba() -> None: array = numpy.random.randint(0, 256, size=(480, 640, 4), dtype=numpy.uint8) for format in ("argb", "rgba", "abgr", "bgra"): frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -272,6 +279,7 @@ def test_ndarray_rgba_align() -> None: array = numpy.random.randint(0, 256, size=(238, 318, 4), dtype=numpy.uint8) for format in ("argb", "rgba", "abgr", "bgra"): frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts assert frame.width == 318 and frame.height == 238 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -297,6 +305,7 @@ def test_ndarray_gbrp10() -> None: array = numpy.random.randint(0, 1024, size=(480, 640, 3), dtype=numpy.uint16) for format in ("gbrp10be", "gbrp10le"): frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -306,6 +315,7 @@ def test_ndarray_gbrp10_align() -> None: array = numpy.random.randint(0, 1024, size=(238, 318, 3), dtype=numpy.uint16) for format in ("gbrp10be", "gbrp10le"): frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts assert frame.width == 318 and frame.height == 238 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -315,6 +325,7 @@ def test_ndarray_gbrp12() -> None: array = numpy.random.randint(0, 4096, size=(480, 640, 3), dtype=numpy.uint16) for format in ("gbrp12be", "gbrp12le"): frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -324,6 +335,7 @@ def test_ndarray_gbrp12_align() -> None: array = numpy.random.randint(0, 4096, size=(238, 318, 3), dtype=numpy.uint16) for format in ("gbrp12be", "gbrp12le"): frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts assert frame.width == 318 and frame.height == 238 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -333,6 +345,7 @@ def test_ndarray_gbrp14() -> None: array = numpy.random.randint(0, 16384, size=(480, 640, 3), dtype=numpy.uint16) for format in ("gbrp14be", "gbrp14le"): frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -342,6 +355,7 @@ def test_ndarray_gbrp14_align() -> None: array = numpy.random.randint(0, 16384, size=(238, 318, 3), dtype=numpy.uint16) for format in ("gbrp14be", "gbrp14le"): frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts assert frame.width == 318 and frame.height == 238 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -351,6 +365,7 @@ def test_ndarray_gbrp16() -> None: array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) for format in ("gbrp16be", "gbrp16le"): frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -360,6 +375,7 @@ def test_ndarray_gbrp16_align() -> None: array = numpy.random.randint(0, 65536, size=(238, 318, 3), dtype=numpy.uint16) for format in ("gbrp16be", "gbrp16le"): frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts assert frame.width == 318 and frame.height == 238 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -603,6 +619,7 @@ def test_ndarray_pal8(): frame = VideoFrame.from_ndarray((array, palette), format="pal8") assert frame.width == 640 and frame.height == 480 assert frame.format.name == "pal8" + assert frame.format.name in av.video.frame.supported_np_pix_fmts returned = frame.to_ndarray() assert type(returned) is tuple and len(returned) == 2 assertNdarraysEqual(returned[0], array) @@ -614,6 +631,7 @@ def test_ndarray_nv12() -> None: frame = VideoFrame.from_ndarray(array, format="nv12") assert frame.width == 640 and frame.height == 480 assert frame.format.name == "nv12" + assert frame.format.name in av.video.frame.supported_np_pix_fmts assertNdarraysEqual(frame.to_ndarray(), array) From 0eca76eec24574fa965930f9a241754c1b374b83 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 12 Feb 2025 20:38:29 -0500 Subject: [PATCH 482/846] Add yuv422p10le pix_fmt --- av/video/frame.pyx | 42 ++++++++++++++++++++++++++++++++++++---- tests/test_videoframe.py | 9 +++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/av/video/frame.pyx b/av/video/frame.pyx index bb8f83486..4647ab81f 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -20,9 +20,9 @@ supported_np_pix_fmts = { "gbrapf32be", "gbrapf32le", "gbrp", "gbrp10be", "gbrp10le", "gbrp12be", "gbrp12le", "gbrp14be", "gbrp14le", "gbrp16be", "gbrp16le", "gbrpf32be", "gbrpf32le", "gray", "gray16be", "gray16le", "gray8", "grayf32be", "grayf32le", "nv12", "pal8", "rgb24", - "rgb48be", "rgb48le", "rgb8", "rgba", "rgba64be", "rgba64le", "yuv420p", "yuv444p", - "yuv444p16be", "yuv444p16le", "yuva444p16be", "yuva444p16le", "yuvj420p", - "yuvj444p", "yuyv422", + "rgb48be", "rgb48le", "rgb8", "rgba", "rgba64be", "rgba64le", "yuv420p", + "yuv422p10le", "yuv444p", "yuv444p16be", "yuv444p16le", "yuva444p16be", + "yuva444p16le", "yuvj420p", "yuvj444p", "yuyv422", } cdef VideoFrame alloc_video_frame(): @@ -322,7 +322,7 @@ cdef class VideoFrame(Frame): import numpy as np # check size - if frame.format.name in {"yuv420p", "yuvj420p", "yuyv422"}: + if frame.format.name in {"yuv420p", "yuvj420p", "yuyv422", "yuv422p10le"}: assert frame.width % 2 == 0, "the width has to be even for this pixel format" assert frame.height % 2 == 0, "the height has to be even for this pixel format" @@ -408,6 +408,18 @@ cdef class VideoFrame(Frame): useful_array(frame.planes[1]), useful_array(frame.planes[2]), ]).reshape(-1, frame.width) + if frame.format.name == "yuv422p10le": + # Read planes as uint16 at their original width + y = useful_array(frame.planes[0], 2, "uint16").reshape(frame.height, frame.width) + u = useful_array(frame.planes[1], 2, "uint16").reshape(frame.height, frame.width // 2) + v = useful_array(frame.planes[2], 2, "uint16").reshape(frame.height, frame.width // 2) + + # Double the width of U and V by repeating each value + u_full = np.repeat(u, 2, axis=1) + v_full = np.repeat(v, 2, axis=1) + if channel_last: + return np.stack([y, u_full, v_full], axis=2) + return np.stack([y, u_full, v_full], axis=0) if frame.format.name == "pal8": image = useful_array(frame.planes[0]).reshape(frame.height, frame.width) palette = np.frombuffer(frame.planes[1], "i4").astype(">i4").reshape(-1, 1).view(np.uint8) @@ -641,6 +653,28 @@ cdef class VideoFrame(Frame): copy_array_to_plane(flat[u_start:v_start], frame.planes[1], 1) copy_array_to_plane(flat[v_start:], frame.planes[2], 1) return frame + elif format == "yuv422p10le": + if not isinstance(array, np.ndarray) or array.dtype != np.uint16: + raise ValueError("Array must be uint16 type") + + # Convert to channel-first if needed + if channel_last and array.shape[2] == 3: + array = np.moveaxis(array, 2, 0) + elif not (array.shape[0] == 3): + raise ValueError("Array must have shape (3, height, width) or (height, width, 3)") + + height, width = array.shape[1:] + if width % 2 != 0 or height % 2 != 0: + raise ValueError("Width and height must be even") + + frame = VideoFrame(width, height, format) + copy_array_to_plane(array[0], frame.planes[0], 2) + # Subsample U and V by taking every other column + u = array[1, :, ::2].copy() # Need copy to ensure C-contiguous + v = array[2, :, ::2].copy() # Need copy to ensure C-contiguous + copy_array_to_plane(u, frame.planes[1], 2) + copy_array_to_plane(v, frame.planes[2], 2) + return frame elif format == "yuyv422": check_ndarray(array, "uint8", 3) check_ndarray_shape(array, array.shape[0] % 2 == 0) diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 5ae603f64..bef69550b 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -485,6 +485,15 @@ def test_ndarray_yuv444p16() -> None: assertNdarraysEqual(frame.to_ndarray(), array) +def test_ndarray_yuv422p10le() -> None: + array = numpy.random.randint(0, 65536, size=(3, 480, 640), dtype=numpy.uint16) + for format in ("yuv422p10le",): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assert format in av.video.frame.supported_np_pix_fmts + + def test_ndarray_yuv444p16_align() -> None: array = numpy.random.randint(0, 65536, size=(238, 318, 3), dtype=numpy.uint16) for format in ("yuv444p16be", "yuv444p16le"): From ac64a70bf25e6ea699ab0408ae38c19efac082de Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 14 Feb 2025 15:18:05 -0500 Subject: [PATCH 483/846] Add warnings when compiling from source --- setup.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 685412b99..3d4c11fac 100644 --- a/setup.py +++ b/setup.py @@ -6,6 +6,31 @@ import shlex import subprocess import sys +from time import sleep + + +def is_virtualenv(): + return sys.base_prefix != sys.prefix + + +if platform.system() == "Darwin": + major_version = int(platform.mac_ver()[0].split(".")[0]) + if major_version < 12: + print( + "\033[1;91mWarning!\033[0m You are using an EOL, unsupported, and out-of-date OS." + ) + sleep(3) + +# Don't show message when using our project tooling. +if not is_virtualenv() or os.getenv("_PYAV_ACTIVATED", "") != "1": + print( + "\n\033[1;91mWarning!\033[0m You are installing from source.\n" + "It is \033[1;37mEXPECTED\033[0m that it will fail. You are \033[1;37mREQUIRED\033[0m" + " to use ffmpeg 7.\nYou \033[1;37mMUST\033[0m have Cython, pkg-config, and a C compiler.\n" + ) + if os.getenv("GITHUB_ACTIONS") != "true": + sleep(3) + from Cython.Build import cythonize from Cython.Compiler.AutoDocTransforms import EmbedSignature @@ -141,7 +166,7 @@ def parse_cflags(raw_flags): cythonize = lambda ext, **kwargs: [ext] use_pkg_config = False -# Locate FFmpeg libraries and headers. +# Locate ffmpeg libraries and headers. if FFMPEG_DIR is not None: extension_extra = get_config_from_directory(FFMPEG_DIR) elif use_pkg_config: From b2693f5250ad62a2f3147960363a77aebb4cdac8 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 14 Feb 2025 16:41:05 -0500 Subject: [PATCH 484/846] Update authors --- AUTHORS.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 846cf0d93..1792ba863 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -5,8 +5,8 @@ All contributors (by number of commits): - Mike Boers ; `@mikeboers `_ -* Jeremy Lainé ; `@jlaine `_ * WyattBlue ; `@WyattBlue `_ +* Jeremy Lainé ; `@jlaine `_ - Mark Reid ; `@markreidvfx `_ @@ -38,6 +38,7 @@ All contributors (by number of commits): - Ulrik Mikaelsson ; `@rawler `_ - Wel C. van der - Will Patera ; `@willpatera `_ +- z-khan * Joe Schiff <41972063+JoeSchiff@users.noreply.github.com> * Dexer <73297572+DexerBR@users.noreply.github.com> @@ -77,6 +78,7 @@ All contributors (by number of commits): * Johan Jeppsson Karlin * Philipp Klaus * Kim Minjong +* Matteo Destro * Mattias Wadman * Manuel Goacolou * Julian Schweizer @@ -102,4 +104,3 @@ All contributors (by number of commits): * Koichi Akabe * David Joy * Sviatoslav Sydorenko (Святослав Сидоренко) -* z-khan From e2a489857f90833c6ed2ac466f7d89508857eb18 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 14 Feb 2025 16:46:50 -0500 Subject: [PATCH 485/846] Update Changelog --- CHANGELOG.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ead5b6a74..79cca4c55 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,6 +17,21 @@ We are operating with `semantic versioning `_. are merged into the "default" branch. +v14.2.0 (Unreleased) +-------------------- + +Features: + +- Add support for external flags in hwacccel by :gh-user:`materight` in (:pr:`1751`). +- Add Bayer pixel formats by :gh-user:`z-khan` in (:pr:`1755`). +- Add support for yuv422p10le pix_fmt by :gh-user:`WyattBlue` in (:pr:`1767`). +- Add ``supported_np_pix_fmts`` by :gh-user:`WyattBlue` in (:pr:`1766`). + +Misc. +- Drop support for MacOS 11 by :gh-user:`WyattBlue` in (:pr:`1764`). +- Add warnings when compiling from source by :gh-user:`WyattBlue` in (:pr:`1773`). + + v14.1.0 ------- From 7d10213de6d221f878ea73fd2bcc5a35c271b0be Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 16 Feb 2025 11:27:48 -0500 Subject: [PATCH 486/846] Exclude author files --- AUTHORS.py | 1 - setup.py | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/AUTHORS.py b/AUTHORS.py index f17b31aeb..797e423ed 100644 --- a/AUTHORS.py +++ b/AUTHORS.py @@ -78,7 +78,6 @@ last = None block_i = 0 for email, count in sorted(email_count.items(), key=lambda x: (-x[1], x[0])): - # This is the natural log, because of course it should be. ;) order = int(math.log(count)) if last and last != order: diff --git a/setup.py b/setup.py index 3d4c11fac..d0bee300b 100644 --- a/setup.py +++ b/setup.py @@ -279,7 +279,9 @@ def parse_cflags(raw_flags): author="Mike Boers", author_email="pyav@mikeboers.com", url="https://github.com/PyAV-Org/PyAV", - packages=find_packages(exclude=["build*", "examples*", "tests*", "include*"]) + packages=find_packages( + exclude=["build*", "examples*", "tests*", "include*", "AUTHORS*"] + ) + list(package_dir.keys()), package_dir=package_dir, package_data=package_data, From 4f61921242be73eb8e612c5334528757f489ca7e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 16 Feb 2025 16:28:39 -0500 Subject: [PATCH 487/846] Stop publishing sdist sdist takes ~4MB of space, is neglected by PyPA, and is a bad experience for both the developer and the user. PyAV does not have universal support for every arch, and removing source builds in PyPI does not change that. --- .github/workflows/tests.yml | 20 +-------- MANIFEST.in | 7 --- README.md | 16 ++----- pyproject.toml | 51 +++++++++++++++++++++- setup.py | 85 +++++-------------------------------- 5 files changed, 63 insertions(+), 116 deletions(-) delete mode 100644 MANIFEST.in diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a6c5232b3..e9e4003b9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,24 +4,6 @@ on: types: [published] workflow_dispatch: jobs: - package-source: - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - name: Build source package - run: | - pip install setuptools cython - python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.1.json /tmp/vendor - PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig python setup.py sdist - - name: Upload source package - uses: actions/upload-artifact@v4 - with: - name: dist-source - path: dist/ - package-wheel: runs-on: ${{ matrix.os }} strategy: @@ -82,7 +64,7 @@ jobs: publish: runs-on: ubuntu-latest - needs: [package-source, package-wheel] + needs: [package-wheel] steps: - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 6539f496d..000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,7 +0,0 @@ -include *.txt *.md -recursive-include av *.pyx *.pxd -recursive-include docs *.rst *.py -recursive-include examples *.py -recursive-include include *.pxd *.h -recursive-include src/av *.c *.h -recursive-include tests *.py diff --git a/README.md b/README.md index 7e21198e7..232fd6743 100644 --- a/README.md +++ b/README.md @@ -32,20 +32,10 @@ conda install av -c conda-forge See the [Conda install][conda-install] docs to get started with (mini)Conda. -Alternative installation methods --------------------------------- +Installing From Source +---------------------- -Due to the complexity of the dependencies, PyAV is not always the easiest Python package to install from source. If you want to use your existing ffmpeg (must be the correct major version), the source version of PyAV is on [PyPI][pypi]: - -```bash -pip install av --no-binary av -``` - -> [!WARNING] -> This installation method won't work for Windows or Debian based systems. - - -And if you want to build from the absolute source (POSIX only): +Here's how to build PyAV from source source. You must use [MSYS2](https://www.msys2.org/) when using Windows. ```bash git clone https://github.com/PyAV-Org/PyAV.git diff --git a/pyproject.toml b/pyproject.toml index 73e3cf334..15ed77023 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,52 @@ [build-system] -requires = ["setuptools", "cython"] +requires = ["setuptools>61", "cython>=3,<4"] + +[project] +name = "av" +description = "Pythonic bindings for FFmpeg's libraries." +readme = "README.md" +license = {text = "BSD-3-Clause"} +authors = [ + {name = "WyattBlue", email = "wyattblue@auto-editor.com"}, + {name = "Jeremy Lainé", email = "jeremy.laine@m4x.org"}, +] +requires-python = ">=3.9" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Natural Language :: English", + "Operating System :: MacOS :: MacOS X", + "Operating System :: POSIX", + "Operating System :: Unix", + "Operating System :: Microsoft :: Windows", + "Programming Language :: Cython", + "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", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Multimedia :: Sound/Audio", + "Topic :: Multimedia :: Sound/Audio :: Conversion", + "Topic :: Multimedia :: Video", + "Topic :: Multimedia :: Video :: Conversion", +] +dynamic = ["version"] + +[tool.setuptools] +zip-safe = false + +[tool.setuptools.dynamic] +version = {attr = "av.about.__version__"} + +[project.urls] +"Bug Tracker" = "https://github.com/PyAV-Org/PyAV/discussions/new?category=4-bugs" +"Source Code" = "https://github.com/PyAV-Org/PyAV" +homepage = "https://pyav.basswood-io.com" + +[project.scripts] +"pyav" = "av.__main__:main" [tool.isort] profile = "black" @@ -13,4 +60,4 @@ max-line-length = 142 per-file-ignores = [ "__init__.py:E402,F401", "*.pyx,*.pxd:E211,E225,E227,E402,E999", -] +] \ No newline at end of file diff --git a/setup.py b/setup.py index d0bee300b..bef384515 100644 --- a/setup.py +++ b/setup.py @@ -21,22 +21,21 @@ def is_virtualenv(): ) sleep(3) -# Don't show message when using our project tooling. -if not is_virtualenv() or os.getenv("_PYAV_ACTIVATED", "") != "1": - print( - "\n\033[1;91mWarning!\033[0m You are installing from source.\n" - "It is \033[1;37mEXPECTED\033[0m that it will fail. You are \033[1;37mREQUIRED\033[0m" - " to use ffmpeg 7.\nYou \033[1;37mMUST\033[0m have Cython, pkg-config, and a C compiler.\n" - ) - if os.getenv("GITHUB_ACTIONS") != "true": - sleep(3) +print( + "\n\033[1;91mWarning!\033[0m You are installing from source.\n" + "It is \033[1;37mEXPECTED\033[0m that it will fail. You are \033[1;37mREQUIRED\033[0m" + " to use ffmpeg 7.\nYou \033[1;37mMUST\033[0m have Cython, pkg-config, and a C compiler.\n" +) +if os.getenv("GITHUB_ACTIONS") == "true" or is_virtualenv(): + pass +else: + print("\033[1;91mWarning!\033[0m You are not using a virtual environment") from Cython.Build import cythonize from Cython.Compiler.AutoDocTransforms import EmbedSignature from setuptools import Extension, find_packages, setup - FFMPEG_LIBRARIES = [ "avformat", "avcodec", @@ -202,12 +201,8 @@ def parse_cflags(raw_flags): include_path=["include"], ) -# Construct the modules that we find in the "av" directory. for dirname, dirnames, filenames in os.walk("av"): for filename in filenames: - if filename == "loudnorm.pyx": - continue - # We are looking for Cython sources. if filename.startswith(".") or os.path.splitext(filename)[1] != ".pyx": continue @@ -242,74 +237,14 @@ def parse_cflags(raw_flags): for cfile in ext.sources: insert_enum_in_generated_files(cfile) -# Read package metadata -about = {} -about_file = os.path.join(os.path.dirname(__file__), "av", "about.py") -with open(about_file, encoding="utf-8") as fp: - exec(fp.read(), about) package_folders = pathlib.Path("av").glob("**/") package_data = { ".".join(pckg.parts): ["*.pxd", "*.pyi", "*.typed"] for pckg in package_folders } -# Add include/ headers to av.include -package_dir = { - ".".join(["av", *pckg.parts]): str(pckg) - for pckg in pathlib.Path("include").glob("**/") -} -package_data.update({pckg: ["*.pxd"] for pckg in package_dir}) - - -with open("README.md") as f: - long_description = f.read() - setup( - name="av", - version=about["__version__"], - description="Pythonic bindings for FFmpeg's libraries.", - long_description=long_description, - long_description_content_type="text/markdown", - license="BSD", - project_urls={ - "Bug Reports": "https://github.com/PyAV-Org/PyAV/discussions/new?category=4-bugs", - "Documentation": "https://pyav.basswood-io.com", - "Download": "https://pypi.org/project/av", - }, - author="Mike Boers", - author_email="pyav@mikeboers.com", - url="https://github.com/PyAV-Org/PyAV", - packages=find_packages( - exclude=["build*", "examples*", "tests*", "include*", "AUTHORS*"] - ) - + list(package_dir.keys()), - package_dir=package_dir, + packages=find_packages(include=["av*"]), package_data=package_data, - python_requires=">=3.9", - zip_safe=False, ext_modules=ext_modules, - entry_points={ - "console_scripts": ["pyav = av.__main__:main"], - }, - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "License :: OSI Approved :: BSD License", - "Natural Language :: English", - "Operating System :: MacOS :: MacOS X", - "Operating System :: POSIX", - "Operating System :: Unix", - "Operating System :: Microsoft :: Windows", - "Programming Language :: Cython", - "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", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: Multimedia :: Sound/Audio", - "Topic :: Multimedia :: Sound/Audio :: Conversion", - "Topic :: Multimedia :: Video", - "Topic :: Multimedia :: Video :: Conversion", - ], ) From 4a7287a3343f16cf8f7761f88e61149729cb4e43 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 18 Feb 2025 13:57:50 -0500 Subject: [PATCH 488/846] Add opaque param to add_stream_from_template --- av/container/output.pyi | 4 +++- av/container/output.pyx | 16 +++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/av/container/output.pyi b/av/container/output.pyi index a7c89452f..3169f486b 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -38,7 +38,9 @@ class OutputContainer(Container): options: dict[str, str] | None = None, **kwargs, ) -> VideoStream | AudioStream | SubtitleStream: ... - def add_stream_from_template(self, template: _StreamT, **kwargs) -> _StreamT: ... + def add_stream_from_template( + self, template: _StreamT, opaque: bool | None = None, **kwargs + ) -> _StreamT: ... def add_data_stream( self, codec_name: str | None = None, options: dict[str, str] | None = None ) -> DataStream: ... diff --git a/av/container/output.pyx b/av/container/output.pyx index c2055486e..7140c64b5 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -129,20 +129,24 @@ cdef class OutputContainer(Container): return py_stream - def add_stream_from_template(self, Stream template not None, **kwargs): + def add_stream_from_template(self, Stream template not None, opaque=None, **kwargs): """ Creates a new stream from a template. Supports video, audio, and subtitle streams. :param template: Copy codec from another :class:`~av.stream.Stream` instance. + :param opaque: If True, copy opaque data from the template's codec context. :param \\**kwargs: Set attributes for the stream. :rtype: The new :class:`~av.stream.Stream`. """ cdef const lib.AVCodec *codec cdef Codec codec_obj - if template.type != "video": + if opaque is None: + opaque = template.type != "video" + + if opaque: # Copy ctx from template. codec_obj = template.codec_context.codec - else: + else: # Construct new codec object. codec_obj = Codec(template.codec_context.codec.name, "w") codec = codec_obj.ptr @@ -164,10 +168,8 @@ cdef class OutputContainer(Container): if self.ptr.oformat.flags & lib.AVFMT_GLOBALHEADER: codec_context.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER - # Initialise stream codec parameters to populate the codec type. - # - # Subsequent changes to the codec context will be applied just before - # encoding starts in `start_encoding()`. + # Initialize stream codec parameters to populate the codec type. Subsequent changes to + # the codec context will be applied just before encoding starts in `start_encoding()`. err_check(lib.avcodec_parameters_from_context(stream.codecpar, codec_context)) # Construct the user-land stream From 05b38be84f3281ccb7e77455d65e20b33d21194b Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 18 Feb 2025 15:07:48 -0500 Subject: [PATCH 489/846] Use up-to-date Sphinx --- docs/Makefile | 11 +- docs/conf.py | 210 +++---------------- docs/development/includes.py | 365 ---------------------------------- docs/development/includes.rst | 2 - docs/generate-tagfile | 32 --- 5 files changed, 28 insertions(+), 592 deletions(-) delete mode 100644 docs/development/includes.py delete mode 100644 docs/development/includes.rst delete mode 100755 docs/generate-tagfile diff --git a/docs/Makefile b/docs/Makefile index bb84c1dba..7efd36bac 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,7 +1,6 @@ SPHINXOPTS = SPHINXBUILD = sphinx-build BUILDDIR = _build -FFMPEGDIR = _ffmpeg PYAV_PIP ?= pip PIP := $(PYAV_PIP) ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(SPHINXOPTS) . @@ -10,14 +9,6 @@ ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(SPHINXOPTS) . default: html -TAGFILE := _build/doxygen/tagfile.xml - -$(TAGFILE): - @if [ ! -d "$(FFMPEGDIR)" ]; then \ - git clone --depth=1 git://source.ffmpeg.org/ffmpeg.git $(FFMPEGDIR); \ - fi - ./generate-tagfile --library $(FFMPEGDIR) -o $(TAGFILE) - TEMPLATES := $(wildcard api/*.py development/*.py) RENDERED := $(TEMPLATES:%.py=_build/rst/%.rst) @@ -30,7 +21,7 @@ clean: rm -rf $(BUILDDIR) $(FFMPEGDIR) html: $(RENDERED) $(TAGFILE) - $(PIP) install -U sphinx==5.1.0 + $(PIP) install -U sphinx $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html test: diff --git a/docs/conf.py b/docs/conf.py index 789fc0756..1f2541f32 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,23 +1,10 @@ -import logging import os import re import sys -import xml.etree.ElementTree as etree +import sphinx from docutils import nodes -from sphinx import addnodes from sphinx.util.docutils import SphinxDirective -import sphinx - - -logging.basicConfig() - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath("..")) - -# -- General configuration ----------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. @@ -31,37 +18,24 @@ "sphinx.ext.doctest", ] - # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] - -# The suffix of source filenames. source_suffix = ".rst" - -# The master toctree document. master_doc = "index" - -# General information about the project. project = "PyAV" copyright = "2025, The PyAV Team" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. -# + about = {} with open("../av/about.py") as fp: exec(fp.read(), about) -# The full version, including alpha/beta/rc tags. release = about["__version__"] - -# The short X.Y version. version = release.split("-")[0] - exclude_patterns = ["_build"] - -# The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # -- Options for HTML output --------------------------------------------------- @@ -85,7 +59,6 @@ doctest_global_setup = """ - import errno import os @@ -109,29 +82,20 @@ def sandboxed(*args, **kwargs): os.chdir(here) video_path = curated('pexels/time-lapse-video-of-night-sky-857195.mp4') - """ -doctest_global_cleanup = """ - -os.chdir(_cwd) - -""" - - +doctest_global_cleanup = "os.chdir(_cwd)" doctest_test_doctest_blocks = "" - extlinks = { - "ffstruct": ("http://ffmpeg.org/doxygen/trunk/struct%s.html", "struct "), - "issue": ("https://github.com/PyAV-Org/PyAV/issues/%s", "#"), - "pr": ("https://github.com/PyAV-Org/PyAV/pull/%s", "#"), - "gh-user": ("https://github.com/%s", "@"), + "ffmpeg": ("https://ffmpeg.org/doxygen/trunk/%s.html", "%s"), + "ffstruct": ("https://ffmpeg.org/doxygen/trunk/struct%s.html", "struct %s"), + "issue": ("https://github.com/PyAV-Org/PyAV/issues/%s", "#%s"), + "pr": ("https://github.com/PyAV-Org/PyAV/pull/%s", "#%s"), + "gh-user": ("https://github.com/%s", "@%s"), } -intersphinx_mapping = { - "https://docs.python.org/3": None, -} +intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} autodoc_member_order = "bysource" autodoc_default_options = { @@ -238,152 +202,32 @@ def makerow(*texts): ) seen = set() - if hasattr(enum, "_by_name"): # Our custom enum class - enum_items = enum._by_name.items() - for name, item in enum_items: - if name.lower() in seen: - continue - seen.add(name.lower()) - - try: - attr = properties[item] - except KeyError: - if cls: - continue - attr = None - - value = f"0x{item.value:X}" - doc = item.__doc__ or "-" - tbody += makerow(attr, name, value, doc) - - return [table] - else: # Standard IntEnum - enum_items = [ - (name, item) - for name, item in vars(enum).items() - if isinstance(item, enum) - ] - for name, item in enum_items: - if name.lower() in seen: - continue - seen.add(name.lower()) - - try: - attr = properties[item] - except KeyError: - if cls: - continue - attr = None - - value = f"0x{item.value:X}" - doc = enum.__annotations__.get(name, "---")[1:-1] - tbody += makerow(attr, name, value, doc) - - return [table] - - -doxylink = {} -ffmpeg_tagfile = os.path.abspath( - os.path.join(__file__, "..", "_build", "doxygen", "tagfile.xml") -) -if not os.path.exists(ffmpeg_tagfile): - print("ERROR: Missing FFmpeg tagfile.") - exit(1) -doxylink["ffmpeg"] = (ffmpeg_tagfile, "https://ffmpeg.org/doxygen/trunk/") - - -def doxylink_create_handler(app, file_name, url_base): - print("Finding all names in Doxygen tagfile", file_name) - - doc = etree.parse(file_name) - root = doc.getroot() - - parent_map = {} # ElementTree doesn't five us access to parents. - urls = {} - - for node in root.findall(".//name/.."): - for child in node: - parent_map[child] = node - - kind = node.attrib["kind"] - if kind not in ("function", "struct", "variable"): - continue - - name = node.find("name").text - - if kind not in ("function",): - parent = parent_map.get(node) - parent_name = parent.find("name") if parent else None - if parent_name is not None: - name = f"{parent_name.text}.{name}" - - filenode = node.find("filename") - if filenode is not None: - url = filenode.text - else: - url = "{}#{}".format( - node.find("anchorfile").text, - node.find("anchor").text, - ) - - urls.setdefault(kind, {})[name] = url - - def get_url(name): - # These are all the kinds that seem to exist. - for kind in ( - "function", - "struct", - "variable", # These are struct members. - # 'class', - # 'define', - # 'enumeration', - # 'enumvalue', - # 'file', - # 'group', - # 'page', - # 'typedef', - # 'union', - ): + enum_items = [ + (name, item) + for name, item in vars(enum).items() + if isinstance(item, enum) + ] + for name, item in enum_items: + if name.lower() in seen: + continue + seen.add(name.lower()) + try: - return urls[kind][name] + attr = properties[item] except KeyError: - pass - - def _doxylink_handler(name, rawtext, text, lineno, inliner, options={}, content=[]): - m = re.match(r"^(.+?)(?:<(.+?)>)?$", text) - title, name = m.groups() - name = name or title - - url = get_url(name) - if not url: - if name == "AVFrame.color_primaries": - url = "structAVFrame.html#a59a3f830494f2ed1133103a1bc9481e7" - elif name == "AVFrame.color_trc": - url = "structAVFrame.html#ab09abb126e3922bc1d010cf044087939" - else: - print("ERROR: Could not find", name) - exit(1) - - node = addnodes.literal_strong(title, title) - if url: - url = url_base + url - node = nodes.reference("", "", node, refuri=url) + if cls: + continue + attr = None - return [node], [] + value = f"0x{item.value:X}" + doc = enum.__annotations__.get(name, "---")[1:-1] + tbody += makerow(attr, name, value, doc) - return _doxylink_handler + return [table] def setup(app): app.add_css_file("custom.css") - app.add_directive("flagtable", EnumTable) app.add_directive("enumtable", EnumTable) app.add_directive("pyinclude", PyInclude) - - skip = os.environ.get("PYAV_SKIP_DOXYLINK") - for role, (filename, url_base) in doxylink.items(): - if skip: - app.add_role(role, lambda *args: ([], [])) - else: - app.add_role(role, doxylink_create_handler(app, filename, url_base)) diff --git a/docs/development/includes.py b/docs/development/includes.py deleted file mode 100644 index 8f350a81e..000000000 --- a/docs/development/includes.py +++ /dev/null @@ -1,365 +0,0 @@ -import json -import os -import re -import sys - -import xml.etree.ElementTree as etree - -from Cython.Compiler.Main import CompilationOptions, Context -from Cython.Compiler.TreeFragment import parse_from_strings -from Cython.Compiler.Visitor import TreeVisitor -from Cython.Compiler import Nodes - -os.chdir(os.path.abspath(os.path.join(__file__, '..', '..', '..'))) - - -class Visitor(TreeVisitor): - - def __init__(self, state=None): - super(Visitor, self).__init__() - self.state = dict(state or {}) - self.events = [] - - def record_event(self, node, **kw): - state = self.state.copy() - state.update(**kw) - state['node'] = node - state['pos'] = node.pos - state['end_pos'] = node.end_pos() - self.events.append(state) - - def visit_Node(self, node): - self.visitchildren(node) - - def visit_ModuleNode(self, node): - self.state['module'] = node.full_module_name - self.visitchildren(node) - self.state.pop('module') - - def visit_CDefExternNode(self, node): - self.state['extern_from'] = node.include_file - self.visitchildren(node) - self.state.pop('extern_from') - - def visit_CStructOrUnionDefNode(self, node): - self.record_event(node, type='struct', name=node.name) - self.state['struct'] = node.name - self.visitchildren(node) - self.state.pop('struct') - - def visit_CFuncDeclaratorNode(self, node): - if isinstance(node.base, Nodes.CNameDeclaratorNode): - self.record_event(node, type='function', name=node.base.name) - else: - self.visitchildren(node) - - def visit_CVarDefNode(self, node): - - if isinstance(node.declarators[0], Nodes.CNameDeclaratorNode): - - # Grab the type name. - # TODO: Do a better job. - type_ = node.base_type - if hasattr(type_, 'name'): - type_name = type_.name - elif hasattr(type_, 'base_type'): - type_name = type_.base_type.name - else: - type_name = str(type_) - - self.record_event(node, type='variable', name=node.declarators[0].name, - vartype=type_name) - - else: - self.visitchildren(node) - - def visit_CClassDefNode(self, node): - self.state['class'] = node.class_name - self.visitchildren(node) - self.state.pop('class') - - def visit_PropertyNode(self, node): - self.state['property'] = node.name - self.visitchildren(node) - self.state.pop('property') - - def visit_DefNode(self, node): - self.state['function'] = node.name - self.visitchildren(node) - self.state.pop('function') - - def visit_AttributeNode(self, node): - if getattr(node.obj, 'name', None) == 'lib': - self.record_event(node, type='use', name=node.attribute) - else: - self.visitchildren(node) - - -def extract(path, **kwargs): - - name = os.path.splitext(os.path.relpath(path))[0].replace('/', '.') - - options = CompilationOptions() - options.include_path.append('include') - options.language_level = 2 - options.compiler_directives = dict( - c_string_type='str', - c_string_encoding='ascii', - ) - - context = Context( - options.include_path, - options.compiler_directives, - options.cplus, - options.language_level, - options=options, - ) - - tree = parse_from_strings( - name, open(path).read(), context, - level='module_pxd' if path.endswith('.pxd') else None, - **kwargs) - - extractor = Visitor({'file': path}) - extractor.visit(tree) - return extractor.events - - -def iter_cython(path): - '''Yield all ``.pyx`` and ``.pxd`` files in the given root.''' - for dir_path, dir_names, file_names in os.walk(path): - for file_name in file_names: - if file_name.startswith('.'): - continue - if os.path.splitext(file_name)[1] not in ('.pyx', '.pxd'): - continue - yield os.path.join(dir_path, file_name) - - -doxygen = {} -doxygen_base = 'https://ffmpeg.org/doxygen/trunk' -tagfile_path = 'docs/_build/doxygen/tagfile.xml' - -tagfile_json = tagfile_path + '.json' -if os.path.exists(tagfile_json): - print('Loading pre-parsed Doxygen tagfile:', tagfile_json, file=sys.stderr) - doxygen = json.load(open(tagfile_json)) - - -if not doxygen: - - print('Parsing Doxygen tagfile:', tagfile_path, file=sys.stderr) - if not os.path.exists(tagfile_path): - print(' MISSING!', file=sys.stderr) - else: - - root = etree.parse(tagfile_path) - - def inspect_member(node, name_prefix=''): - name = name_prefix + node.find('name').text - anchorfile = node.find('anchorfile').text - anchor = node.find('anchor').text - - url = '%s/%s#%s' % (doxygen_base, anchorfile, anchor) - - doxygen[name] = {'url': url} - - if node.attrib['kind'] == 'function': - ret_type = node.find('type').text - arglist = node.find('arglist').text - sig = '%s %s%s' % (ret_type, name, arglist) - doxygen[name]['sig'] = sig - - for struct in root.iter('compound'): - if struct.attrib['kind'] != 'struct': - continue - name_prefix = struct.find('name').text + '.' - for node in struct.iter('member'): - inspect_member(node, name_prefix) - - for node in root.iter('member'): - inspect_member(node) - - - json.dump(doxygen, open(tagfile_json, 'w'), sort_keys=True, indent=4) - - -print('Parsing Cython source for references...', file=sys.stderr) -lib_references = {} -for path in iter_cython('av'): - try: - events = extract(path) - except Exception as e: - print(" %s in %s" % (e.__class__.__name__, path), file=sys.stderr) - print(" %s" % e, file=sys.stderr) - continue - for event in events: - if event['type'] == 'use': - lib_references.setdefault(event['name'], []).append(event) - - - - - - - -defs_by_extern = {} -for path in iter_cython('include'): - - # This one has "include" directives, which is not supported when - # parsing from a string. - if path == 'include/libav.pxd': - continue - - # Extract all #: comments from the source files. - comments_by_line = {} - for i, line in enumerate(open(path)): - m = re.match(r'^\s*#: ?', line) - if m: - comment = line[m.end():].rstrip() - comments_by_line[i + 1] = line[m.end():] - - # Extract Cython definitions from the source files. - for event in extract(path): - - extern = event.get('extern_from') or path.replace('include/', '') - defs_by_extern.setdefault(extern, []).append(event) - - # Collect comments above and below - comments = event['_comments'] = [] - line = event['pos'][1] - 1 - while line in comments_by_line: - comments.insert(0, comments_by_line.pop(line)) - line -= 1 - line = event['end_pos'][1] + 1 - while line in comments_by_line: - comments.append(comments_by_line.pop(line)) - line += 1 - - # Figure out the Sphinx headline. - if event['type'] == 'function': - event['_sort_key'] = 2 - sig = doxygen.get(event['name'], {}).get('sig') - if sig: - sig = re.sub(r'\).+', ')', sig) # strip trailer - event['_headline'] = '.. c:function:: %s' % sig - else: - event['_headline'] = '.. c:function:: %s()' % event['name'] - - elif event['type'] == 'variable': - struct = event.get('struct') - if struct: - event['_headline'] = '.. c:member:: %s %s' % (event['vartype'], event['name']) - event['_sort_key'] = 1.1 - else: - event['_headline'] = '.. c:var:: %s' % event['name'] - event['_sort_key'] = 3 - - elif event['type'] == 'struct': - event['_headline'] = '.. c:type:: struct %s' % event['name'] - event['_sort_key'] = 1 - event['_doxygen_url'] = '%s/struct%s.html' % (doxygen_base, event['name']) - - else: - print('Unknown event type %s' % event['type'], file=sys.stderr) - - name = event['name'] - if event.get('struct'): - name = '%s.%s' % (event['struct'], name) - - # Doxygen URLs - event.setdefault('_doxygen_url', doxygen.get(name, {}).get('url')) - - # Find use references. - ref_events = lib_references.get(name, []) - if ref_events: - - ref_pairs = [] - for ref in sorted(ref_events, key=lambda e: e['name']): - - chunks = [ - ref.get('module'), - ref.get('class'), - ] - chunks = filter(None, chunks) - prefix = '.'.join(chunks) + '.' if chunks else '' - - if ref.get('property'): - ref_pairs.append((ref['property'], ':attr:`%s%s`' % (prefix, ref['property']))) - elif ref.get('function'): - name = ref['function'] - if name in ('__init__', '__cinit__', '__dealloc__'): - ref_pairs.append((name, ':class:`%s%s <%s>`' % (prefix, name, prefix.rstrip('.')))) - else: - ref_pairs.append((name, ':func:`%s%s`' % (prefix, name))) - else: - continue - - unique_refs = event['_references'] = [] - seen = set() - for name, ref in sorted(ref_pairs): - if name in seen: - continue - seen.add(name) - unique_refs.append(ref) - - - - -print(''' - -.. - This file is generated by includes.py; any modifications will be destroyed! - -Wrapped C Types and Functions -============================= - -''') - -for extern, events in sorted(defs_by_extern.items()): - did_header = False - - for event in events: - - headline = event.get('_headline') - comments = event.get('_comments') - refs = event.get('_references', []) - url = event.get('_doxygen_url') - indent = ' ' if event.get('struct') else '' - - if not headline: - continue - if ( - not filter(None, (x.strip() for x in comments if x.strip())) and - not refs and - event['type'] not in ('struct', ) - ): - pass - - if not did_header: - print('``%s``' % extern) - print('-' * (len(extern) + 4)) - print() - did_header = True - - if url: - print() - print(indent + '.. rst-class:: ffmpeg-quicklink') - print() - print(indent + ' `FFmpeg Docs <%s>`__' % url) - - print(indent + headline) - print() - - if comments: - for line in comments: - print(indent + ' ' + line) - print() - - if refs: - print(indent + ' Referenced by: ', end='') - for i, ref in enumerate(refs): - print((', ' if i else '') + ref, end='') - print('.') - - print() diff --git a/docs/development/includes.rst b/docs/development/includes.rst deleted file mode 100644 index 6b2c989cb..000000000 --- a/docs/development/includes.rst +++ /dev/null @@ -1,2 +0,0 @@ - -.. include:: ../_build/rst/development/includes.rst diff --git a/docs/generate-tagfile b/docs/generate-tagfile deleted file mode 100755 index 1f729de5c..000000000 --- a/docs/generate-tagfile +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python - -import argparse -import os -import subprocess - - -parser = argparse.ArgumentParser() -parser.add_argument("-l", "--library", required=True) -parser.add_argument("-o", "--output", required=True) -args = parser.parse_args() - -output = os.path.abspath(args.output) -outdir = os.path.dirname(output) -if not os.path.exists(outdir): - os.makedirs(outdir) - -proc = subprocess.Popen(["doxygen", "-"], stdin=subprocess.PIPE, cwd=args.library) -proc.communicate( - """ - -#@INCLUDE = doc/Doxyfile -GENERATE_TAGFILE = {} -GENERATE_HTML = no -GENERATE_LATEX = no -CASE_SENSE_NAMES = yes -INPUT = libavcodec libavdevice libavfilter libavformat libavresample libavutil libswresample libswscale - -""".format( - output - ).encode() -) From 663249d72616cfc08518d6bf295f98761a0bb4dd Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 18 Feb 2025 15:42:15 -0500 Subject: [PATCH 490/846] Set the default bit_rate to 0 --- av/container/output.pyx | 4 ++-- tests/test_encode.py | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/av/container/output.pyx b/av/container/output.pyx index 7140c64b5..2a12c40f2 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -76,7 +76,7 @@ cdef class OutputContainer(Container): codec_context.pix_fmt = lib.AV_PIX_FMT_YUV420P codec_context.width = kwargs.pop("width", 640) codec_context.height = kwargs.pop("height", 480) - codec_context.bit_rate = kwargs.pop("bit_rate", 1024000) + codec_context.bit_rate = kwargs.pop("bit_rate", 0) codec_context.bit_rate_tolerance = kwargs.pop("bit_rate_tolerance", 128000) try: to_avrational(kwargs.pop("time_base"), &codec_context.time_base) @@ -90,7 +90,7 @@ cdef class OutputContainer(Container): # Some sane audio defaults elif codec.type == lib.AVMEDIA_TYPE_AUDIO: codec_context.sample_fmt = codec.sample_fmts[0] - codec_context.bit_rate = kwargs.pop("bit_rate", 128000) + codec_context.bit_rate = kwargs.pop("bit_rate", 0) codec_context.bit_rate_tolerance = kwargs.pop("bit_rate_tolerance", 32000) try: to_avrational(kwargs.pop("time_base"), &codec_context.time_base) diff --git a/tests/test_encode.py b/tests/test_encode.py index c107aa044..23cbdc275 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -120,7 +120,6 @@ def test_default_options(self) -> None: assert stream.time_base is None # codec context properties - assert stream.bit_rate == 1024000 assert stream.format.height == 480 assert stream.format.name == "yuv420p" assert stream.format.width == 640 @@ -176,7 +175,6 @@ def test_default_options(self) -> None: assert stream.time_base is None # codec context properties - assert stream.bit_rate == 128000 assert stream.format.name == "s16" assert stream.sample_rate == 48000 From a2477d5b0670f4e56aac52323e9fa7f8039d29f5 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 19 Feb 2025 01:15:22 -0500 Subject: [PATCH 491/846] Use new ffmpeg binary --- scripts/ffmpeg-7.1.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ffmpeg-7.1.json b/scripts/ffmpeg-7.1.json index a9fdf419d..dd4f9a778 100644 --- a/scripts/ffmpeg-7.1.json +++ b/scripts/ffmpeg-7.1.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.1-3/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7a-community/ffmpeg-{platform}.tar.gz" } \ No newline at end of file From 8d9b94c38e22d1b845d1c2e1e071ccb2096a7e1a Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 19 Feb 2025 03:31:21 -0500 Subject: [PATCH 492/846] Add Codec.canonical_name --- CHANGELOG.rst | 1 + av/codec/codec.pyi | 2 ++ av/codec/codec.pyx | 9 +++++++++ tests/test_codec.py | 1 + 4 files changed, 13 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 79cca4c55..dc57483c0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -26,6 +26,7 @@ Features: - Add Bayer pixel formats by :gh-user:`z-khan` in (:pr:`1755`). - Add support for yuv422p10le pix_fmt by :gh-user:`WyattBlue` in (:pr:`1767`). - Add ``supported_np_pix_fmts`` by :gh-user:`WyattBlue` in (:pr:`1766`). +- Add ``Codec.canonical_name`` by :gh-user:`WyattBlue`. Misc. - Drop support for MacOS 11 by :gh-user:`WyattBlue` in (:pr:`1764`). diff --git a/av/codec/codec.pyi b/av/codec/codec.pyi index 9f80a4909..4270c641f 100644 --- a/av/codec/codec.pyi +++ b/av/codec/codec.pyi @@ -57,6 +57,8 @@ class Codec: @property def name(self) -> str: ... @property + def canonical_name(self) -> str: ... + @property def long_name(self) -> str: ... @property def type(self) -> Literal["video", "audio", "data", "subtitle", "attachment"]: ... diff --git a/av/codec/codec.pyx b/av/codec/codec.pyx index 3e9e028f8..a28db758e 100644 --- a/av/codec/codec.pyx +++ b/av/codec/codec.pyx @@ -1,3 +1,5 @@ +cimport libav as lib + from av.audio.format cimport get_audio_format from av.codec.hwaccel cimport wrap_hwconfig from av.descriptor cimport wrap_avclass @@ -144,6 +146,13 @@ cdef class Codec: @property def name(self): return self.ptr.name or "" + @property + def canonical_name(self): + """ + Returns the name of the codec, not a specific encoder. + """ + return lib.avcodec_get_name(self.ptr.id) + @property def long_name(self): return self.ptr.long_name or "" diff --git a/tests/test_codec.py b/tests/test_codec.py index 70e688435..9d72a243c 100644 --- a/tests/test_codec.py +++ b/tests/test_codec.py @@ -68,6 +68,7 @@ def test_codec_opus_decoder() -> None: def test_codec_opus_encoder() -> None: c = Codec("opus", "w") assert c.name in ("opus", "libopus") + assert c.canonical_name == "opus" assert c.long_name in ("Opus", "libopus Opus") assert c.type == "audio" assert c.is_encoder From fb6cf4ed7b41e863371f1640a2ff0a3ea703deb2 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 20 Feb 2025 14:04:40 -0500 Subject: [PATCH 493/846] Remove outdated installion section --- av/about.py | 2 +- docs/Makefile | 11 +++---- docs/conf.py | 42 ++++++++++++++++++++++--- docs/overview/installation.rst | 57 ---------------------------------- 4 files changed, 43 insertions(+), 69 deletions(-) diff --git a/av/about.py b/av/about.py index 217fb25cb..4c6a9313e 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "14.1.0" +__version__ = "14.2.0rc1" diff --git a/docs/Makefile b/docs/Makefile index 7efd36bac..e45e1d03a 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,5 +1,4 @@ SPHINXOPTS = -SPHINXBUILD = sphinx-build BUILDDIR = _build PYAV_PIP ?= pip PIP := $(PYAV_PIP) @@ -17,15 +16,13 @@ _build/rst/%.rst: %.py $(TAGFILE) $(shell find ../include ../av -name '*.pyx' -o python $< > $@.tmp mv $@.tmp $@ -clean: - rm -rf $(BUILDDIR) $(FFMPEGDIR) - -html: $(RENDERED) $(TAGFILE) +html: $(RENDERED) $(PIP) install -U sphinx - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + rm -rf $(BUILDDIR) + sphinx-build -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html test: - PYAV_SKIP_DOXYLINK=1 $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + PYAV_SKIP_DOXYLINK=1 sphinx-build -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest open: open _build/html/index.html diff --git a/docs/conf.py b/docs/conf.py index 1f2541f32..b88dc7bd3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -88,7 +88,6 @@ def sandboxed(*args, **kwargs): doctest_test_doctest_blocks = "" extlinks = { - "ffmpeg": ("https://ffmpeg.org/doxygen/trunk/%s.html", "%s"), "ffstruct": ("https://ffmpeg.org/doxygen/trunk/struct%s.html", "struct %s"), "issue": ("https://github.com/PyAV-Org/PyAV/issues/%s", "#%s"), "pr": ("https://github.com/PyAV-Org/PyAV/pull/%s", "#%s"), @@ -203,9 +202,7 @@ def makerow(*texts): seen = set() enum_items = [ - (name, item) - for name, item in vars(enum).items() - if isinstance(item, enum) + (name, item) for name, item in vars(enum).items() if isinstance(item, enum) ] for name, item in enum_items: if name.lower() in seen: @@ -226,8 +223,45 @@ def makerow(*texts): return [table] +def ffmpeg_role(name, rawtext, text, lineno, inliner, options={}, content=[]): + """ + Custom role for FFmpeg API links. + Converts :ffmpeg:`AVSomething` into proper FFmpeg API documentation links. + """ + + base_url = "https://ffmpeg.org/doxygen/7.0/struct{}.html" + + try: + struct_name, member = text.split(".") + except Exception: + struct_name = None + + if struct_name is None: + url = base_url.format(text) + else: + fragment = { + "AVCodecContext.thread_count": "#aa852b6227d0778b62e9cc4034ad3720c", + "AVCodecContext.thread_type": "#a7651614f4309122981d70e06a4b42fcb", + "AVCodecContext.skip_frame": "#af869b808363998c80adf7df6a944a5a6", + "AVCodec.capabilities": "#af51f7ff3dac8b730f46b9713e49a2518", + "AVCodecDescriptor.props": "#a9949288403a12812cd6e3892ac45f40f", + }.get(text, f"#{member}") + + url = base_url.format(struct_name) + fragment + + node = nodes.reference(rawtext, text, refuri=url, **options) + return [node], [] + + def setup(app): app.add_css_file("custom.css") + app.add_role("ffmpeg", ffmpeg_role) app.add_directive("flagtable", EnumTable) app.add_directive("enumtable", EnumTable) app.add_directive("pyinclude", PyInclude) + + return { + "version": "1.0", + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/docs/overview/installation.rst b/docs/overview/installation.rst index cf1364c1a..eea041203 100644 --- a/docs/overview/installation.rst +++ b/docs/overview/installation.rst @@ -21,63 +21,6 @@ Another way to install PyAV is via `conda-forge ` See the `Conda quick install `_ docs to get started with (mini)Conda. -Bring your own FFmpeg ---------------------- - -PyAV can also be compiled against your own build of FFmpeg (version ``7.0`` or higher). You can force installing PyAV from source by running: - -.. code-block:: bash - - pip install av --no-binary av - -PyAV depends upon several libraries from FFmpeg: - -- ``libavcodec`` -- ``libavdevice`` -- ``libavfilter`` -- ``libavformat`` -- ``libavutil`` -- ``libswresample`` -- ``libswscale`` - -and a few other tools in general: - -- ``pkg-config`` -- Python's development headers - - -MacOS -^^^^^ - -On **MacOS**, Homebrew_ saves the day:: - - brew install ffmpeg pkg-config - -.. _homebrew: http://brew.sh/ - - -Ubuntu >= 18.04 LTS -^^^^^^^^^^^^^^^^^^^ - -On **Ubuntu 18.04 LTS** everything can come from the default sources:: - - # General dependencies - sudo apt-get install -y python-dev pkg-config - - # Library components - sudo apt-get install -y \ - libavformat-dev libavcodec-dev libavdevice-dev \ - libavutil-dev libswscale-dev libswresample-dev libavfilter-dev - - -Windows -^^^^^^^ - -It is possible to build PyAV on Windows without Conda by installing FFmpeg yourself, e.g. from the `shared and dev packages `_. - -Unpack them somewhere (like ``C:\ffmpeg``), and then :ref:`tell PyAV where they are located `. - - Building from the latest source ------------------------------- From c04f6e6a19b42b5669835ca77fa4a0aa98170299 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 20 Feb 2025 15:07:24 -0500 Subject: [PATCH 494/846] Add more doc links --- .github/workflows/smoke.yml | 3 --- docs/conf.py | 7 +++++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 416f6fecb..40dcb3fe6 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -35,10 +35,8 @@ jobs: matrix: config: - {os: ubuntu-24.04, python: "3.12", ffmpeg: "7.1", extras: true} - - {os: ubuntu-24.04, python: "3.9", ffmpeg: "7.0.2"} - {os: ubuntu-24.04, python: "pypy3.10", ffmpeg: "7.1"} - {os: macos-14, python: "3.9", ffmpeg: "7.1"} - - {os: macos-14, python: "3.9", ffmpeg: "7.0.2"} env: PYAV_PYTHON: python${{ matrix.config.python }} @@ -112,7 +110,6 @@ jobs: matrix: config: - {os: windows-latest, python: "3.9", ffmpeg: "7.1"} - - {os: windows-latest, python: "3.9", ffmpeg: "7.0"} steps: - name: Checkout diff --git a/docs/conf.py b/docs/conf.py index b88dc7bd3..ba5e36500 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -243,8 +243,15 @@ def ffmpeg_role(name, rawtext, text, lineno, inliner, options={}, content=[]): "AVCodecContext.thread_count": "#aa852b6227d0778b62e9cc4034ad3720c", "AVCodecContext.thread_type": "#a7651614f4309122981d70e06a4b42fcb", "AVCodecContext.skip_frame": "#af869b808363998c80adf7df6a944a5a6", + "AVCodecContext.qmin": "#a3f63bc9141e25bf7f1cda0cef7cd4a60", + "AVCodecContext.qmax": "#ab015db3b7fcd227193a7c17283914187", "AVCodec.capabilities": "#af51f7ff3dac8b730f46b9713e49a2518", "AVCodecDescriptor.props": "#a9949288403a12812cd6e3892ac45f40f", + "AVCodecContext.bits_per_coded_sample": "#a3866500f51fabfa90faeae894c6e955c", + "AVFrame.color_range": "#a853afbad220bbc58549b4860732a3aa5", + "AVFrame.color_primaries": "#a59a3f830494f2ed1133103a1bc9481e7", + "AVFrame.color_trc": "#ab09abb126e3922bc1d010cf044087939", + "AVFrame.colorspace": "#a9262c231f1f64869439b4fe587fe1710", }.get(text, f"#{member}") url = base_url.format(struct_name) + fragment From 7495bfe1731912728836e38f60006c543f53297b Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 20 Feb 2025 22:09:00 -0500 Subject: [PATCH 495/846] Fix all broken links --- docs/conf.py | 11 +++++++++-- docs/cookbook/numpy.rst | 2 +- docs/overview/installation.rst | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index ba5e36500..07b942a31 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -88,7 +88,6 @@ def sandboxed(*args, **kwargs): doctest_test_doctest_blocks = "" extlinks = { - "ffstruct": ("https://ffmpeg.org/doxygen/trunk/struct%s.html", "struct %s"), "issue": ("https://github.com/PyAV-Org/PyAV/issues/%s", "#%s"), "pr": ("https://github.com/PyAV-Org/PyAV/pull/%s", "#%s"), "gh-user": ("https://github.com/%s", "@%s"), @@ -237,7 +236,15 @@ def ffmpeg_role(name, rawtext, text, lineno, inliner, options={}, content=[]): struct_name = None if struct_name is None: - url = base_url.format(text) + fragment = { + "avformat_seek_file": "group__lavf__decoding.html#ga3b40fc8d2fda6992ae6ea2567d71ba30", + "av_find_best_stream": "avformat_8c.html#a8d4609a8f685ad894c1503ffd1b610b4", + "av_frame_make_writable": "group__lavu__frame.html#gadd5417c06f5a6b419b0dbd8f0ff363fd", + "avformat_write_header": "group__lavf__encoding.html#ga18b7b10bb5b94c4842de18166bc677cb", + "av_guess_frame_rate": "group__lavf__misc.html#ga698e6aa73caa9616851092e2be15875d", + "av_guess_sample_aspect_ratio": "group__lavf__misc.html#gafa6fbfe5c1bf6792fd6e33475b6056bd", + }.get(text, f"struct{text}.html") + url = "https://ffmpeg.org/doxygen/7.0/" + fragment else: fragment = { "AVCodecContext.thread_count": "#aa852b6227d0778b62e9cc4034ad3720c", diff --git a/docs/cookbook/numpy.rst b/docs/cookbook/numpy.rst index d4887945c..6842efefc 100644 --- a/docs/cookbook/numpy.rst +++ b/docs/cookbook/numpy.rst @@ -7,7 +7,7 @@ Video Barcode A video barcode shows the change in colour and tone over time. Time is represented on the horizontal axis, while the vertical remains the vertical direction in the image. -See http://moviebarcode.tumblr.com/ for examples from Hollywood movies, and here is an example from a sunset timelapse: +See https://moviebarcode.tumblr.com/ for examples from Hollywood movies, and here is an example from a sunset timelapse: .. image:: ../_static/examples/numpy/barcode.jpg diff --git a/docs/overview/installation.rst b/docs/overview/installation.rst index eea041203..8eb5ce553 100644 --- a/docs/overview/installation.rst +++ b/docs/overview/installation.rst @@ -18,7 +18,7 @@ Another way to install PyAV is via `conda-forge ` conda install av -c conda-forge -See the `Conda quick install `_ docs to get started with (mini)Conda. +See the `Conda quick install `_ docs to get started with (mini)Conda. Building from the latest source From ca7f5ea2cbe694929fec2544735a86c9f90953c4 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 20 Feb 2025 22:34:55 -0500 Subject: [PATCH 496/846] Add copy button to code blocks --- CHANGELOG.rst | 3 ++- docs/Makefile | 2 +- docs/conf.py | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index dc57483c0..7c8ece6b6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -28,7 +28,8 @@ Features: - Add ``supported_np_pix_fmts`` by :gh-user:`WyattBlue` in (:pr:`1766`). - Add ``Codec.canonical_name`` by :gh-user:`WyattBlue`. -Misc. +Misc: + - Drop support for MacOS 11 by :gh-user:`WyattBlue` in (:pr:`1764`). - Add warnings when compiling from source by :gh-user:`WyattBlue` in (:pr:`1773`). diff --git a/docs/Makefile b/docs/Makefile index e45e1d03a..6654f1062 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -17,7 +17,7 @@ _build/rst/%.rst: %.py $(TAGFILE) $(shell find ../include ../av -name '*.pyx' -o mv $@.tmp $@ html: $(RENDERED) - $(PIP) install -U sphinx + $(PIP) install -U sphinx sphinx-copybutton rm -rf $(BUILDDIR) sphinx-build -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html diff --git a/docs/conf.py b/docs/conf.py index 07b942a31..b32212e97 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,6 +16,7 @@ "sphinx.ext.viewcode", "sphinx.ext.extlinks", "sphinx.ext.doctest", + "sphinx_copybutton", # Add copy button to code blocks ] # Add any paths that contain templates here, relative to this directory. From 318695612a05a599d86fb48255a0ce1b4e8d91d9 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 21 Feb 2025 11:38:17 -0500 Subject: [PATCH 497/846] Don't let sidebar text overflow --- av/stream.pyx | 14 ++++++-------- av/video/stream.pyx | 10 +++++----- docs/_static/custom.css | 7 +++++++ docs/conf.py | 7 +++++++ 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/av/stream.pyx b/av/stream.pyx index d0ecf37ad..90f10d038 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -202,7 +202,7 @@ cdef class Stream: """ The unit of time (in fractional seconds) in which timestamps are expressed. - :type: :class:`~fractions.Fraction` or ``None`` + :type: fractions.Fraction | None """ return avrational_to_fraction(&self.ptr.time_base) @@ -219,7 +219,7 @@ cdef class Stream: The presentation timestamp in :attr:`time_base` units of the first frame in this stream. - :type: :class:`int` or ``None`` + :type: int | None """ if self.ptr.start_time != lib.AV_NOPTS_VALUE: return self.ptr.start_time @@ -229,7 +229,7 @@ cdef class Stream: """ The duration of this stream in :attr:`time_base` units. - :type: :class:`int` or ``None`` + :type: int | None """ if self.ptr.duration != lib.AV_NOPTS_VALUE: @@ -242,7 +242,7 @@ cdef class Stream: Returns ``0`` if it is not known. - :type: :class:`int` + :type: int """ return self.ptr.nb_frames @@ -251,7 +251,7 @@ cdef class Stream: """ The language of the stream. - :type: :class:`str` or ``None`` + :type: str | None """ return self.metadata.get("language") @@ -264,8 +264,6 @@ cdef class Stream: """ The type of the stream. - Examples: ``'audio'``, ``'video'``, ``'subtitle'``. - - :type: str + :type: Literal["audio", "video", "subtitle", "data", "attachment"] """ return lib.av_get_media_type_string(self.ptr.codecpar.codec_type) diff --git a/av/video/stream.pyx b/av/video/stream.pyx index 6ab685167..383cc13a6 100644 --- a/av/video/stream.pyx +++ b/av/video/stream.pyx @@ -57,7 +57,7 @@ cdef class VideoStream(Stream): This is calculated when the file is opened by looking at the first few frames and averaging their rate. - :type: :class:`~fractions.Fraction` or ``None`` + :type: fractions.Fraction | None """ return avrational_to_fraction(&self.ptr.avg_frame_rate) @@ -70,7 +70,7 @@ cdef class VideoStream(Stream): frames can be represented accurately. See :ffmpeg:`AVStream.r_frame_rate` for more. - :type: :class:`~fractions.Fraction` or ``None`` + :type: fractions.Fraction | None """ return avrational_to_fraction(&self.ptr.r_frame_rate) @@ -81,7 +81,7 @@ cdef class VideoStream(Stream): This is a wrapper around :ffmpeg:`av_guess_frame_rate`, and uses multiple heuristics to decide what is "the" frame rate. - :type: :class:`~fractions.Fraction` or ``None`` + :type: fractions.Fraction | None """ # The two NULL arguments aren't used in FFmpeg >= 4.0 cdef lib.AVRational val = lib.av_guess_frame_rate(NULL, self.ptr, NULL) @@ -94,7 +94,7 @@ cdef class VideoStream(Stream): This is a wrapper around :ffmpeg:`av_guess_sample_aspect_ratio`, and uses multiple heuristics to decide what is "the" sample aspect ratio. - :type: :class:`~fractions.Fraction` or ``None`` + :type: fractions.Fraction | None """ cdef lib.AVRational sar = lib.av_guess_sample_aspect_ratio(self.container.ptr, self.ptr, NULL) return avrational_to_fraction(&sar) @@ -105,7 +105,7 @@ cdef class VideoStream(Stream): This is calculated from :meth:`.VideoStream.guessed_sample_aspect_ratio`. - :type: :class:`~fractions.Fraction` or ``None`` + :type: fractions.Fraction | None """ cdef lib.AVRational dar diff --git a/docs/_static/custom.css b/docs/_static/custom.css index b95a5f253..136af619b 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -1,3 +1,10 @@ +body { + text-rendering: optimizeLegibility; +} + +.sphinxsidebarwrapper { + overflow: scroll; +} .ffmpeg-quicklink { float: right; diff --git a/docs/conf.py b/docs/conf.py index b32212e97..d971572e1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -6,6 +6,9 @@ from docutils import nodes from sphinx.util.docutils import SphinxDirective +sys.path.insert(0, os.path.abspath("..")) + + # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ @@ -58,6 +61,10 @@ # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] +html_theme_options = { + "sidebarwidth": "250px", +} + doctest_global_setup = """ import errno From 524786b235006ff3cd81642c77a853541d1228a0 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 22 Feb 2025 19:18:17 -0500 Subject: [PATCH 498/846] Re-add publishing the sdist @jlaine and @hmaarrfk are responsible for maintaining this format. --- .github/workflows/tests.yml | 20 ++++++++++++- MANIFEST.in | 7 +++++ README.md | 15 +++++++++- docs/overview/installation.rst | 55 ++++++++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 MANIFEST.in diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e9e4003b9..a6c5232b3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,6 +4,24 @@ on: types: [published] workflow_dispatch: jobs: + package-source: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + - name: Build source package + run: | + pip install setuptools cython + python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.1.json /tmp/vendor + PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig python setup.py sdist + - name: Upload source package + uses: actions/upload-artifact@v4 + with: + name: dist-source + path: dist/ + package-wheel: runs-on: ${{ matrix.os }} strategy: @@ -64,7 +82,7 @@ jobs: publish: runs-on: ubuntu-latest - needs: [package-wheel] + needs: [package-source, package-wheel] steps: - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 000000000..b87a010d3 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,7 @@ +include *.txt *.md +recursive-include av *.pyx *.pxd +recursive-include docs *.rst *.py +recursive-include examples *.py +recursive-include include *.pxd *.h +recursive-include src/av *.c *.h +recursive-include tests *.py \ No newline at end of file diff --git a/README.md b/README.md index 232fd6743..59e291c74 100644 --- a/README.md +++ b/README.md @@ -32,10 +32,23 @@ conda install av -c conda-forge See the [Conda install][conda-install] docs to get started with (mini)Conda. +Alternative installation methods +-------------------------------- + +Due to the complexity of the dependencies, PyAV is not always the easiest Python package to install from source. If you want to use your existing ffmpeg (must be the correct major version), the source version of PyAV is on [PyPI][pypi]: + +> [!WARNING] +> You must be in a posix env, and have the correct version of ffmpeg installed on your system. + +```bash +pip install av --no-binary av +``` + + Installing From Source ---------------------- -Here's how to build PyAV from source source. You must use [MSYS2](https://www.msys2.org/) when using Windows. +Here's how to build PyAV from source. You must use [MSYS2](https://www.msys2.org/) when using Windows. ```bash git clone https://github.com/PyAV-Org/PyAV.git diff --git a/docs/overview/installation.rst b/docs/overview/installation.rst index 8eb5ce553..5fa7fc860 100644 --- a/docs/overview/installation.rst +++ b/docs/overview/installation.rst @@ -21,6 +21,61 @@ Another way to install PyAV is via `conda-forge ` See the `Conda quick install `_ docs to get started with (mini)Conda. +Bring your own FFmpeg +--------------------- + +PyAV can also be compiled against your own build of FFmpeg (version ``7.0`` or higher). You can force installing PyAV from source by running: + +.. code-block:: bash + pip install av --no-binary av +PyAV depends upon several libraries from FFmpeg: + +- ``libavcodec`` +- ``libavdevice`` +- ``libavfilter`` +- ``libavformat`` +- ``libavutil`` +- ``libswresample`` +- ``libswscale`` + +and a few other tools in general: + +- ``pkg-config`` +- Python's development headers + + +MacOS +^^^^^ + +On **MacOS**, Homebrew_ saves the day:: + + brew install ffmpeg pkg-config + +.. _homebrew: http://brew.sh/ + + +Ubuntu >= 18.04 LTS +^^^^^^^^^^^^^^^^^^^ + +On **Ubuntu 18.04 LTS** everything can come from the default sources:: + + # General dependencies + sudo apt-get install -y python-dev pkg-config + + # Library components + sudo apt-get install -y \ + libavformat-dev libavcodec-dev libavdevice-dev \ + libavutil-dev libswscale-dev libswresample-dev libavfilter-dev + + +Windows +^^^^^^^ + +It is possible to build PyAV on Windows without Conda by installing FFmpeg yourself, e.g. from the `shared and dev packages `_. + +Unpack them somewhere (like ``C:\ffmpeg``), and then :ref:`tell PyAV where they are located `. + + Building from the latest source ------------------------------- From bc0ca08670b2672fc7a7fc8c1c73f2803f5ff6ec Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 25 Feb 2025 08:11:21 -0500 Subject: [PATCH 499/846] Bump version to 14.2.0 --- CHANGELOG.rst | 5 ++--- av/about.py | 2 +- av/codec/context.pyx | 2 +- scripts/ffmpeg-7.1.json | 4 ++-- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7c8ece6b6..a5d57b4c7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,8 +17,8 @@ We are operating with `semantic versioning `_. are merged into the "default" branch. -v14.2.0 (Unreleased) --------------------- +v14.2.0 +------- Features: @@ -31,7 +31,6 @@ Features: Misc: - Drop support for MacOS 11 by :gh-user:`WyattBlue` in (:pr:`1764`). -- Add warnings when compiling from source by :gh-user:`WyattBlue` in (:pr:`1773`). v14.1.0 diff --git a/av/about.py b/av/about.py index 4c6a9313e..331dde6ff 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "14.2.0rc1" +__version__ = "14.2.0" diff --git a/av/codec/context.pyx b/av/codec/context.pyx index a56f14ddd..5ca8f24a4 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -218,7 +218,7 @@ cdef class CodecContext: self.ptr.time_base.num = 1 self.ptr.time_base.den = lib.AV_TIME_BASE - err_check(lib.avcodec_open2(self.ptr, self.codec.ptr, &options.ptr)) + err_check(lib.avcodec_open2(self.ptr, self.codec.ptr, &options.ptr), "avcodec_open2(" + self.codec.name + ")") self.is_open = True self.options = dict(options) diff --git a/scripts/ffmpeg-7.1.json b/scripts/ffmpeg-7.1.json index dd4f9a778..ee386661d 100644 --- a/scripts/ffmpeg-7.1.json +++ b/scripts/ffmpeg-7.1.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7a-community/ffmpeg-{platform}.tar.gz" -} \ No newline at end of file + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.1-4/ffmpeg-{platform}.tar.gz" +} From baa1bce146135e0cafe8a822fbf4ad1090ca06a0 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 9 Mar 2025 13:12:29 -0400 Subject: [PATCH 500/846] Update recording examples --- examples/basics/record_facecam.py | 8 +++++++- examples/basics/record_screen.py | 18 ++++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/examples/basics/record_facecam.py b/examples/basics/record_facecam.py index 2200bc546..0bba6b36a 100644 --- a/examples/basics/record_facecam.py +++ b/examples/basics/record_facecam.py @@ -18,7 +18,13 @@ ) output = av.open("out.mkv", "w") -output_stream = output.add_stream("h264", rate=30) +# Prefer x264, but use Apple hardware if not available. +try: + encoder = av.Codec("libx264", "w").name +except av.FFmpegError: + encoder = "h264_videotoolbox" + +output_stream = output.add_stream(encoder, rate=30) output_stream.width = input_.streams.video[0].width output_stream.height = input_.streams.video[0].height output_stream.pix_fmt = "yuv420p" diff --git a/examples/basics/record_screen.py b/examples/basics/record_screen.py index 92818e931..14fdfc428 100644 --- a/examples/basics/record_screen.py +++ b/examples/basics/record_screen.py @@ -13,15 +13,25 @@ input_ = av.open("1", format="avfoundation") output = av.open("out.mkv", "w") -output_stream = output.add_stream("h264", rate=30) +# Prefer x264, but use Apple hardware if not available. +try: + encoder = av.Codec("libx264", "w").name +except av.FFmpegError: + encoder = "h264_videotoolbox" + +output_stream = output.add_stream(encoder, rate=30) output_stream.width = input_.streams.video[0].width output_stream.height = input_.streams.video[0].height output_stream.pix_fmt = "yuv420p" try: - for frame in input_.decode(video=0): - packet = output_stream.encode(frame) - output.mux(packet) + while True: + try: + for frame in input_.decode(video=0): + packet = output_stream.encode(frame) + output.mux(packet) + except av.BlockingIOError: + pass except KeyboardInterrupt: print("Recording stopped by user") From 0897b7bb993db90d23e85c93c245c355762d3679 Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Tue, 11 Mar 2025 20:46:27 -0600 Subject: [PATCH 501/846] Fix typing because dts can be None --- av/packet.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/packet.pyi b/av/packet.pyi index 9bdbb8c62..baa234d7b 100644 --- a/av/packet.pyi +++ b/av/packet.pyi @@ -10,7 +10,7 @@ class Packet(Buffer): stream_index: int time_base: Fraction pts: int | None - dts: int + dts: int | None pos: int | None size: int duration: int | None From abf2f76464c24e915b6261cbbc632dabaa3b205f Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 12 Mar 2025 00:30:53 -0400 Subject: [PATCH 502/846] Remove unused variables --- av/container/core.pyx | 4 ++-- av/container/pyio.pyx | 12 ++++++------ av/logging.pyx | 2 +- av/subtitles/subtitle.pyx | 1 - av/video/frame.pyx | 1 - 5 files changed, 9 insertions(+), 11 deletions(-) diff --git a/av/container/core.pyx b/av/container/core.pyx index 3da402f2f..2b9a1244b 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -95,7 +95,7 @@ cdef int pyav_io_open_gil(lib.AVFormatContext *s, pb[0] = pyio_file.iocontext return 0 - except Exception as e: + except Exception: return stash_exception() @@ -117,7 +117,7 @@ cdef int pyav_io_close_gil(lib.AVFormatContext *s, lib.AVIOContext *pb) noexcept else: result = pyio_close_gil(pb) - except Exception as e: + except Exception: stash_exception() result = lib.AVERROR_UNKNOWN # Or another appropriate error code diff --git a/av/container/pyio.pyx b/av/container/pyio.pyx index 821ae9c0c..c8b82f96a 100644 --- a/av/container/pyio.pyx +++ b/av/container/pyio.pyx @@ -89,7 +89,7 @@ cdef int pyio_read_gil(void *opaque, uint8_t *buf, int buf_size) noexcept: if not res: return lib.AVERROR_EOF return len(res) - except Exception as e: + except Exception: return stash_exception() @@ -108,7 +108,7 @@ cdef int pyio_write_gil(void *opaque, const uint8_t *buf, int buf_size) noexcept bytes_written = ret_value if isinstance(ret_value, int) else buf_size self.pos += bytes_written return bytes_written - except Exception as e: + except Exception: return stash_exception() @@ -140,7 +140,7 @@ cdef int64_t pyio_seek_gil(void *opaque, int64_t offset, int whence): else: res = self.ftell() return res - except Exception as e: + except Exception: return stash_exception() @@ -148,7 +148,7 @@ cdef int pyio_close_gil(lib.AVIOContext *pb): try: return lib.avio_close(pb) - except Exception as e: + except Exception: stash_exception() @@ -158,12 +158,12 @@ cdef int pyio_close_custom_gil(lib.AVIOContext *pb): self = pb.opaque # Flush bytes in the AVIOContext buffers to the custom I/O - result = lib.avio_flush(pb) + lib.avio_flush(pb) if self.fclose is not None: self.fclose() return 0 - except Exception as e: + except Exception: stash_exception() diff --git a/av/logging.pyx b/av/logging.pyx index 66a5095ad..9cb232d2a 100644 --- a/av/logging.pyx +++ b/av/logging.pyx @@ -318,7 +318,7 @@ cdef void log_callback(void *ptr, int level, const char *format, lib.va_list arg with gil: try: log_callback_gil(level, name, message) - except Exception as e: + except Exception: fprintf(stderr, "av.logging: exception while handling %s[%d]: %s\n", name, level, message) # For some reason lib.PyErr_PrintEx(0) won't work. diff --git a/av/subtitles/subtitle.pyx b/av/subtitles/subtitle.pyx index 373bb529b..a713daa22 100644 --- a/av/subtitles/subtitle.pyx +++ b/av/subtitles/subtitle.pyx @@ -16,7 +16,6 @@ cdef class SubtitleSet: """ def __cinit__(self, SubtitleProxy proxy): self.proxy = proxy - cdef int i self.rects = tuple(build_subtitle(self, i) for i in range(self.proxy.struct.num_rects)) def __repr__(self): diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 4647ab81f..84a35cc49 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -55,7 +55,6 @@ cdef copy_bytes_to_plane(img_bytes, VideoPlane plane, unsigned int bytes_per_pix cdef const uint8_t[:] i_buf = img_bytes cdef size_t i_pos = 0 cdef size_t i_stride = plane.width * bytes_per_pixel - cdef size_t i_size = plane.height * i_stride cdef uint8_t[:] o_buf = plane cdef size_t o_pos = 0 From 85d4a5ab1962f0167cf9a7e0673d5f48203b7f8b Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 12 Mar 2025 00:37:47 -0400 Subject: [PATCH 503/846] Set binding=False This makes code faster and smaller, at the expense of disallowing overriding Cython class methods. --- setup.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/setup.py b/setup.py index bef384515..f1ae841dd 100644 --- a/setup.py +++ b/setup.py @@ -188,15 +188,18 @@ def parse_cflags(raw_flags): library_dirs=extension_extra["library_dirs"], ) +compiler_directives = { + "c_string_type": "str", + "c_string_encoding": "ascii", + "embedsignature": True, + "binding": False, + "language_level": 3, +} + # Add the cythonized loudnorm extension to ext_modules ext_modules = cythonize( loudnorm_extension, - compiler_directives={ - "c_string_type": "str", - "c_string_encoding": "ascii", - "embedsignature": True, - "language_level": 3, - }, + compiler_directives=compiler_directives, build_dir="src", include_path=["include"], ) @@ -223,12 +226,7 @@ def parse_cflags(raw_flags): library_dirs=extension_extra["library_dirs"], sources=[pyx_path], ), - compiler_directives={ - "c_string_type": "str", - "c_string_encoding": "ascii", - "embedsignature": True, - "language_level": 3, - }, + compiler_directives=compiler_directives, build_dir="src", include_path=["include"], ) From 086dda0bb84e922c8318550a41db5a936cdc20c3 Mon Sep 17 00:00:00 2001 From: Marcell Pardavi Date: Tue, 25 Mar 2025 14:18:29 +0100 Subject: [PATCH 504/846] Use Python 3 compatible stderr logging in Cython --- av/error.pyx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/av/error.pyx b/av/error.pyx index b8b415207..c3d8a3e7b 100644 --- a/av/error.pyx +++ b/av/error.pyx @@ -1,4 +1,5 @@ cimport libav as lib +from libc.stdio cimport fprintf, stderr from libc.stdlib cimport free, malloc from av.logging cimport get_last_error @@ -367,7 +368,7 @@ cdef int stash_exception(exc_info=None): existing = getattr(_local, "exc_info", None) if existing is not None: - print >> sys.stderr, "PyAV library exception being dropped:" + fprintf(stderr, "PyAV library exception being dropped:\n") traceback.print_exception(*existing) _err_count -= 1 # Balance out the +=1 that is coming. From bca1f10f3cebdd8723b29b929303851bca9dee1d Mon Sep 17 00:00:00 2001 From: Nikhil Idiculla Date: Wed, 26 Mar 2025 12:17:22 -0700 Subject: [PATCH 505/846] Add yuv420p10le pix_fmt --- av/video/frame.pyx | 36 ++++++++++++++++++++++++++++++++++-- tests/test_videoframe.py | 9 +++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 84a35cc49..785603a1a 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -21,7 +21,7 @@ supported_np_pix_fmts = { "gbrp14be", "gbrp14le", "gbrp16be", "gbrp16le", "gbrpf32be", "gbrpf32le", "gray", "gray16be", "gray16le", "gray8", "grayf32be", "grayf32le", "nv12", "pal8", "rgb24", "rgb48be", "rgb48le", "rgb8", "rgba", "rgba64be", "rgba64le", "yuv420p", - "yuv422p10le", "yuv444p", "yuv444p16be", "yuv444p16le", "yuva444p16be", + "yuv420p10le", "yuv422p10le", "yuv444p", "yuv444p16be", "yuv444p16le", "yuva444p16be", "yuva444p16le", "yuvj420p", "yuvj444p", "yuyv422", } @@ -321,7 +321,7 @@ cdef class VideoFrame(Frame): import numpy as np # check size - if frame.format.name in {"yuv420p", "yuvj420p", "yuyv422", "yuv422p10le"}: + if frame.format.name in {"yuv420p", "yuvj420p", "yuyv422", "yuv420p10le", "yuv422p10le"}: assert frame.width % 2 == 0, "the width has to be even for this pixel format" assert frame.height % 2 == 0, "the height has to be even for this pixel format" @@ -407,6 +407,16 @@ cdef class VideoFrame(Frame): useful_array(frame.planes[1]), useful_array(frame.planes[2]), ]).reshape(-1, frame.width) + if frame.format.name == "yuv420p10le": + # Read planes as uint16: + y = useful_array(frame.planes[0], 2, "uint16").reshape(frame.height, frame.width) + u = useful_array(frame.planes[1], 2, "uint16").reshape(frame.height // 2, frame.width // 2) + v = useful_array(frame.planes[2], 2, "uint16").reshape(frame.height // 2, frame.width // 2) + u_full = np.repeat(np.repeat(u, 2, axis=1), 2, axis=0) + v_full = np.repeat(np.repeat(u, 2, axis=1), 2, axis=0) + if channel_last: + return np.stack([y, u_full, v_full], axis=2) + return np.stack([y, u_full, v_full], axis=0) if frame.format.name == "yuv422p10le": # Read planes as uint16 at their original width y = useful_array(frame.planes[0], 2, "uint16").reshape(frame.height, frame.width) @@ -652,6 +662,28 @@ cdef class VideoFrame(Frame): copy_array_to_plane(flat[u_start:v_start], frame.planes[1], 1) copy_array_to_plane(flat[v_start:], frame.planes[2], 1) return frame + elif format == "yuv420p10le": + if not isinstance(array, np.ndarray) or array.dtype != np.uint16: + raise ValueError("Array must be uint16 type") + + # Convert to channel-first if needed: + if channel_last and array.shape[2] == 3: + array = np.moveaxis(array, 2, 0) + elif not (array.shape[0] == 3): + raise ValueError("Array must have shape (3, height, width) or (height, width, 3)") + + height, width = array.shape[1:] + if width % 2 != 0 or height % 2 != 0: + raise ValueError("Width and height must be even") + + frame = VideoFrame(width, height, format) + copy_array_to_plane(array[0], frame.planes[0], 2) + # Subsample U and V by taking every other row and column: + u = array[1, ::2, ::2].copy() # Need copy to ensure C-contiguous + v = array[2, ::2, ::2].copy() # Need copy to ensure C-contiguous + copy_array_to_plane(u, frame.planes[1], 2) + copy_array_to_plane(v, frame.planes[2], 2) + return frame elif format == "yuv422p10le": if not isinstance(array, np.ndarray) or array.dtype != np.uint16: raise ValueError("Array must be uint16 type") diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index bef69550b..9327a4ccd 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -485,6 +485,15 @@ def test_ndarray_yuv444p16() -> None: assertNdarraysEqual(frame.to_ndarray(), array) +def test_ndarray_yuv420p10le() -> None: + array = numpy.random.randint(0, 65536, size=(3, 480, 640), dtype=numpy.uint16) + for format in ("yuv420p10le",): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assert format in av.video.frame.supported_np_pix_fmts + + def test_ndarray_yuv422p10le() -> None: array = numpy.random.randint(0, 65536, size=(3, 480, 640), dtype=numpy.uint16) for format in ("yuv422p10le",): From 3353ec22d7a8828fe5185d432e965244f361655b Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Fri, 28 Mar 2025 12:12:29 -0600 Subject: [PATCH 506/846] Allow venv to be used for build --- scripts/activate.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/activate.sh b/scripts/activate.sh index 72bd44738..9a45b964e 100755 --- a/scripts/activate.sh +++ b/scripts/activate.sh @@ -23,7 +23,10 @@ if [[ ! "$PYAV_LIBRARY" ]]; then fi export PYAV_LIBRARY -if [[ ! "$PYAV_PYTHON" ]]; then +if [[ "$VIRTUAL_ENV" ]]; then + PYAV_PYTHON="${VIRTUAL_ENV}/bin/python3" + echo "Using activated venv: $VIRTUAL_ENV" +elif [[ ! "$PYAV_PYTHON" ]]; then PYAV_PYTHON="${PYAV_PYTHON-python3}" echo 'No $PYAV_PYTHON set; defaulting to python3.' fi @@ -44,6 +47,9 @@ if [[ "$GITHUB_ACTION" ]]; then # GitHub has a very self-contained environment. Lets just work in that. echo "We're on CI, so not setting up another virtualenv." +elif [[ "$VIRTUAL_ENV" ]]; then + # Using activated venv + true else export PYAV_VENV_NAME="$(uname -s).$(uname -r).$("$PYAV_PYTHON" -c ' From 1a04fab45f373f801889139f38033dad51474a2d Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Fri, 28 Mar 2025 12:11:26 -0600 Subject: [PATCH 507/846] Use commands that work on macOS --- scripts/activate.sh | 6 +++++- scripts/build-deps | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/activate.sh b/scripts/activate.sh index 9a45b964e..06e956028 100755 --- a/scripts/activate.sh +++ b/scripts/activate.sh @@ -6,7 +6,11 @@ if [[ "$0" == "${BASH_SOURCE[0]}" ]]; then exit 1 fi -export PYAV_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.."; pwd)" +if [[ "$(uname)" == "Darwin" ]]; then + export PYAV_ROOT="$(realpath -- "$(dirname -- "$(readlink -f -- "$0")")/..")" +else + export PYAV_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.."; pwd)" +fi if [[ ! "$PYAV_LIBRARY" ]]; then if [[ "$1" ]]; then diff --git a/scripts/build-deps b/scripts/build-deps index 9cc795305..4291140dd 100755 --- a/scripts/build-deps +++ b/scripts/build-deps @@ -47,7 +47,7 @@ cd "$PYAV_LIBRARY_ROOT" if [[ ! -d $PYAV_LIBRARY ]]; then url="https://ffmpeg.org/releases/$PYAV_LIBRARY.tar.gz" echo Downloading $url - wget --no-check-certificate "$url" || exit 1 + curl "$url" --output ${PYAV_LIBRARY}.tar.gz || exit 1 tar -xzf $PYAV_LIBRARY.tar.gz rm $PYAV_LIBRARY.tar.gz echo From a670d5fcbb165098c111316710bcfbb1de1f2599 Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Fri, 28 Mar 2025 13:51:48 -0600 Subject: [PATCH 508/846] Change check for zsh instead of macOS --- scripts/activate.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/activate.sh b/scripts/activate.sh index 06e956028..9dd41ba2b 100755 --- a/scripts/activate.sh +++ b/scripts/activate.sh @@ -6,7 +6,7 @@ if [[ "$0" == "${BASH_SOURCE[0]}" ]]; then exit 1 fi -if [[ "$(uname)" == "Darwin" ]]; then +if [[ -n "$ZSH_VERSION" ]]; then export PYAV_ROOT="$(realpath -- "$(dirname -- "$(readlink -f -- "$0")")/..")" else export PYAV_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.."; pwd)" From b6a3e5e779932e04e8a5462786566ca8c1086dde Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 28 Mar 2025 22:26:04 -0400 Subject: [PATCH 509/846] Mention bitrate in changelog --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a5d57b4c7..828230ac8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -31,6 +31,7 @@ Features: Misc: - Drop support for MacOS 11 by :gh-user:`WyattBlue` in (:pr:`1764`). +- Set default bitrate to 0, allowing variable bitrate by :gh-user:`WyattBlue`. v14.1.0 From 18de7be6a6f68544b82fcc8722c8a962df1b98c0 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 29 Mar 2025 09:45:33 -0400 Subject: [PATCH 510/846] Support writing files in Cython's pure python mode --- .github/workflows/smoke.yml | 3 +- .github/workflows/tests.yml | 2 +- Makefile | 2 +- av/filter/loudnorm.pxd | 15 ++++++++ av/filter/loudnorm.py | 48 +++++++++++++++++++++++++ av/filter/loudnorm.pyx | 69 ------------------------------------ av/{packet.pyx => packet.py} | 64 +++++++++++++++++---------------- pyproject.toml | 2 +- scripts/build | 2 +- setup.py | 22 +++++++----- 10 files changed, 117 insertions(+), 112 deletions(-) create mode 100644 av/filter/loudnorm.py delete mode 100644 av/filter/loudnorm.pyx rename av/{packet.pyx => packet.py} (79%) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 40dcb3fe6..5882bf1cf 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -121,8 +121,9 @@ jobs: . $CONDA/etc/profile.d/conda.sh conda config --set always_yes true conda config --add channels conda-forge + conda config --add channels scientific-python-nightly-wheels conda create -q -n pyav \ - cython \ + cython==3.1.0b0 \ numpy \ pillow \ pytest \ diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a6c5232b3..bf30096d5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,7 +13,7 @@ jobs: python-version: "3.13" - name: Build source package run: | - pip install setuptools cython + pip install -U --pre cython setuptools python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.1.json /tmp/vendor PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig python setup.py sdist - name: Upload source package diff --git a/Makefile b/Makefile index 667ff3116..84a36f2a8 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ default: build build: - $(PIP) install -U cython setuptools + $(PIP) install -U --pre cython setuptools CFLAGS=$(CFLAGS) LDFLAGS=$(LDFLAGS) $(PYTHON) setup.py build_ext --inplace --debug clean: diff --git a/av/filter/loudnorm.pxd b/av/filter/loudnorm.pxd index b08d3502f..2729dd8be 100644 --- a/av/filter/loudnorm.pxd +++ b/av/filter/loudnorm.pxd @@ -1,4 +1,19 @@ from av.audio.stream cimport AudioStream +cdef extern from "libavcodec/avcodec.h": + ctypedef struct AVCodecContext: + pass + +cdef extern from "libavformat/avformat.h": + ctypedef struct AVFormatContext: + pass + +cdef extern from "loudnorm_impl.h": + char* loudnorm_get_stats( + AVFormatContext* fmt_ctx, + int audio_stream_index, + const char* loudnorm_args + ) nogil + cpdef bytes stats(str loudnorm_args, AudioStream stream) diff --git a/av/filter/loudnorm.py b/av/filter/loudnorm.py new file mode 100644 index 000000000..0be991ab6 --- /dev/null +++ b/av/filter/loudnorm.py @@ -0,0 +1,48 @@ +import cython +from cython.cimports.av.audio.stream import AudioStream +from cython.cimports.av.container.core import Container +from cython.cimports.libc.stdlib import free + +from av.logging import get_level, set_level + + +@cython.ccall +def stats(loudnorm_args: str, stream: AudioStream) -> bytes: + """ + Get loudnorm statistics for an audio stream. + + Args: + loudnorm_args (str): Arguments for the loudnorm filter (e.g. "i=-24.0:lra=7.0:tp=-2.0") + stream (AudioStream): Input audio stream to analyze + + Returns: + bytes: JSON string containing the loudnorm statistics + """ + + if "print_format=json" not in loudnorm_args: + loudnorm_args = loudnorm_args + ":print_format=json" + + container: Container = stream.container + format_ptr: cython.pointer[AVFormatContext] = container.ptr + container.ptr = cython.NULL # Prevent double-free + + stream_index: cython.int = stream.index + py_args: bytes = loudnorm_args.encode("utf-8") + c_args: cython.p_const_char = py_args + result: cython.p_char + + # Save log level since C function overwrite it. + level = get_level() + + with cython.nogil: + result = loudnorm_get_stats(format_ptr, stream_index, c_args) + + if result == cython.NULL: + raise RuntimeError("Failed to get loudnorm stats") + + py_result = result[:] # Make a copy of the string + free(result) # Free the C string + + set_level(level) + + return py_result diff --git a/av/filter/loudnorm.pyx b/av/filter/loudnorm.pyx deleted file mode 100644 index 78f320a9e..000000000 --- a/av/filter/loudnorm.pyx +++ /dev/null @@ -1,69 +0,0 @@ -# av/filter/loudnorm.pyx - -cimport libav as lib -from cpython.bytes cimport PyBytes_FromString -from libc.stdlib cimport free - -from av.audio.codeccontext cimport AudioCodecContext -from av.audio.stream cimport AudioStream -from av.container.core cimport Container -from av.stream cimport Stream -from av.logging import get_level, set_level - - -cdef extern from "libavcodec/avcodec.h": - ctypedef struct AVCodecContext: - pass - -cdef extern from "libavformat/avformat.h": - ctypedef struct AVFormatContext: - pass - -cdef extern from "loudnorm_impl.h": - char* loudnorm_get_stats( - AVFormatContext* fmt_ctx, - int audio_stream_index, - const char* loudnorm_args - ) nogil - - -cpdef bytes stats(str loudnorm_args, AudioStream stream): - """ - Get loudnorm statistics for an audio stream. - - Args: - loudnorm_args (str): Arguments for the loudnorm filter (e.g. "i=-24.0:lra=7.0:tp=-2.0") - stream (AudioStream): Input audio stream to analyze - - Returns: - bytes: JSON string containing the loudnorm statistics - """ - - if "print_format=json" not in loudnorm_args: - loudnorm_args = loudnorm_args + ":print_format=json" - - cdef Container container = stream.container - cdef AVFormatContext* format_ptr = container.ptr - - container.ptr = NULL # Prevent double-free - - cdef int stream_index = stream.index - cdef bytes py_args = loudnorm_args.encode("utf-8") - cdef const char* c_args = py_args - cdef char* result - - # Save log level since C function overwrite it. - level = get_level() - - with nogil: - result = loudnorm_get_stats(format_ptr, stream_index, c_args) - - if result == NULL: - raise RuntimeError("Failed to get loudnorm stats") - - py_result = result[:] # Make a copy of the string - free(result) # Free the C string - - set_level(level) - - return py_result diff --git a/av/packet.pyx b/av/packet.py similarity index 79% rename from av/packet.pyx rename to av/packet.py index b5c9251eb..c49085c57 100644 --- a/av/packet.pyx +++ b/av/packet.py @@ -1,27 +1,30 @@ -cimport libav as lib +import cython +from cython.cimports import libav as lib +from cython.cimports.av.bytesource import bytesource +from cython.cimports.av.error import err_check +from cython.cimports.av.opaque import opaque_container +from cython.cimports.av.utils import avrational_to_fraction, to_avrational -from av.bytesource cimport bytesource -from av.error cimport err_check -from av.opaque cimport opaque_container -from av.utils cimport avrational_to_fraction, to_avrational - - -cdef class Packet(Buffer): +@cython.cclass +class Packet(Buffer): """A packet of encoded data within a :class:`~av.format.Stream`. This may, or may not include a complete object within a stream. :meth:`decode` must be called to extract encoded data. - """ def __cinit__(self, input=None): - with nogil: + with cython.nogil: self.ptr = lib.av_packet_alloc() + def __dealloc__(self): + with cython.nogil: + lib.av_packet_free(cython.address(self.ptr)) + def __init__(self, input=None): - cdef size_t size = 0 - cdef ByteSource source = None + size: cython.size_t = 0 + source: ByteSource = None if input is None: return @@ -41,24 +44,24 @@ def __init__(self, input=None): # instead of its data. # self.source = source - def __dealloc__(self): - with nogil: - lib.av_packet_free(&self.ptr) - def __repr__(self): stream = self._stream.index if self._stream else 0 return ( - f"" ) # Buffer protocol. - cdef size_t _buffer_size(self): + @cython.cfunc + def _buffer_size(self) -> cython.size_t: return self.ptr.size - cdef void* _buffer_ptr(self): + + @cython.cfunc + def _buffer_ptr(self) -> cython.p_void: return self.ptr.data - cdef _rebase_time(self, lib.AVRational dst): + @cython.cfunc + def _rebase_time(self, dst: lib.AVRational): if not dst.num: raise ValueError("Cannot rebase to zero time.") @@ -92,7 +95,7 @@ def stream(self): return self._stream @stream.setter - def stream(self, Stream stream): + def stream(self, stream: Stream): self._stream = stream self.ptr.stream_index = stream.ptr.index @@ -103,11 +106,11 @@ def time_base(self): :type: fractions.Fraction """ - return avrational_to_fraction(&self._time_base) + return avrational_to_fraction(cython.address(self._time_base)) @time_base.setter def time_base(self, value): - to_avrational(value, &self._time_base) + to_avrational(value, cython.address(self._time_base)) @property def pts(self): @@ -116,7 +119,7 @@ def pts(self): This is the time at which the packet should be shown to the user. - :type: int + :type: int | None """ if self.ptr.pts != lib.AV_NOPTS_VALUE: return self.ptr.pts @@ -133,7 +136,7 @@ def dts(self): """ The decoding timestamp in :attr:`time_base` units for this packet. - :type: int + :type: int | None """ if self.ptr.dts != lib.AV_NOPTS_VALUE: return self.ptr.dts @@ -152,7 +155,7 @@ def pos(self): Returns `None` if it is not known. - :type: int + :type: int | None """ if self.ptr.pos != -1: return self.ptr.pos @@ -221,14 +224,15 @@ def is_disposable(self): @property def opaque(self): - if self.ptr.opaque_ref is not NULL: - return opaque_container.get( self.ptr.opaque_ref.data) + if self.ptr.opaque_ref is not cython.NULL: + return opaque_container.get( + cython.cast(cython.p_char, self.ptr.opaque_ref.data) + ) @opaque.setter def opaque(self, v): - lib.av_buffer_unref(&self.ptr.opaque_ref) + lib.av_buffer_unref(cython.address(self.ptr.opaque_ref)) if v is None: return self.ptr.opaque_ref = opaque_container.add(v) - diff --git a/pyproject.toml b/pyproject.toml index 15ed77023..0bfb6cce9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>61", "cython>=3,<4"] +requires = ["setuptools>61", "cython>=3.1.0a1,<4"] [project] name = "av" diff --git a/scripts/build b/scripts/build index 7e27d7f74..3b9346d60 100755 --- a/scripts/build +++ b/scripts/build @@ -21,6 +21,6 @@ which ffmpeg || exit 2 ffmpeg -version || exit 3 echo -$PYAV_PIP install -U cython setuptools 2> /dev/null +$PYAV_PIP install -U --pre cython setuptools 2> /dev/null "$PYAV_PYTHON" scripts/comptime.py "$PYAV_PYTHON" setup.py config build_ext --inplace || exit 1 diff --git a/setup.py b/setup.py index f1ae841dd..d5ad4fb31 100644 --- a/setup.py +++ b/setup.py @@ -177,13 +177,15 @@ def parse_cflags(raw_flags): "library_dirs": [], } +IMPORT_NAME = "av" + loudnorm_extension = Extension( - "av.filter.loudnorm", + f"{IMPORT_NAME}.filter.loudnorm", sources=[ - "av/filter/loudnorm.pyx", - "av/filter/loudnorm_impl.c", + f"{IMPORT_NAME}/filter/loudnorm.py", + f"{IMPORT_NAME}/filter/loudnorm_impl.c", ], - include_dirs=["av/filter"] + extension_extra["include_dirs"], + include_dirs=[f"{IMPORT_NAME}/filter"] + extension_extra["include_dirs"], libraries=extension_extra["libraries"], library_dirs=extension_extra["library_dirs"], ) @@ -204,10 +206,14 @@ def parse_cflags(raw_flags): include_path=["include"], ) -for dirname, dirnames, filenames in os.walk("av"): +for dirname, dirnames, filenames in os.walk(IMPORT_NAME): for filename in filenames: # We are looking for Cython sources. - if filename.startswith(".") or os.path.splitext(filename)[1] != ".pyx": + if filename.startswith("."): + continue + if filename in {"__init__.py", "__main__.py", "about.py", "datasets.py"}: + continue + if os.path.splitext(filename)[1] not in {".pyx", ".py"}: continue pyx_path = os.path.join(dirname, filename) @@ -236,13 +242,13 @@ def parse_cflags(raw_flags): insert_enum_in_generated_files(cfile) -package_folders = pathlib.Path("av").glob("**/") +package_folders = pathlib.Path(IMPORT_NAME).glob("**/") package_data = { ".".join(pckg.parts): ["*.pxd", "*.pyi", "*.typed"] for pckg in package_folders } setup( - packages=find_packages(include=["av*"]), + packages=find_packages(include=[f"{IMPORT_NAME}*"]), package_data=package_data, ext_modules=ext_modules, ) From 29b44beb86796b841526b3be5b4771c29ded2cdc Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 29 Mar 2025 10:25:12 -0400 Subject: [PATCH 511/846] Use ruff for formatting --- .github/workflows/smoke.yml | 2 +- Makefile | 5 ++--- pyproject.toml | 9 --------- tests/test_decode.py | 6 +++--- 4 files changed, 6 insertions(+), 16 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 5882bf1cf..e8aac3330 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -123,7 +123,6 @@ jobs: conda config --add channels conda-forge conda config --add channels scientific-python-nightly-wheels conda create -q -n pyav \ - cython==3.1.0b0 \ numpy \ pillow \ pytest \ @@ -135,6 +134,7 @@ jobs: run: | . $CONDA/etc/profile.d/conda.sh conda activate pyav + pip install 'Cython==3.1.0a1' python scripts\\fetch-vendor.py --config-file scripts\\ffmpeg-${{ matrix.config.ffmpeg }}.json $CONDA_PREFIX\\Library python scripts\\comptime.py ${{ matrix.config.ffmpeg }} python setup.py build_ext --inplace --ffmpeg-dir=$CONDA_PREFIX\\Library diff --git a/Makefile b/Makefile index 84a36f2a8..4b4f58ab7 100644 --- a/Makefile +++ b/Makefile @@ -28,9 +28,8 @@ fate-suite: rsync -vrltLW rsync://fate-suite.ffmpeg.org/fate-suite/ tests/assets/fate-suite/ lint: - $(PIP) install -U black isort flake8 flake8-pyproject pillow numpy mypy==1.15.0 pytest - black --check av examples tests setup.py - flake8 av + $(PIP) install -U ruff isort pillow numpy mypy==1.15.0 pytest + ruff format --check av examples tests setup.py isort --check-only --diff av examples tests mypy av tests diff --git a/pyproject.toml b/pyproject.toml index 0bfb6cce9..458beee81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,12 +52,3 @@ homepage = "https://pyav.basswood-io.com" profile = "black" known_first_party = ["av"] skip = ["av/__init__.py"] - -[tool.flake8] -filename = ["*.py", "*.pyx", "*.pxd"] -ignore = ["E203", "W503"] -max-line-length = 142 -per-file-ignores = [ - "__init__.py:E402,F401", - "*.pyx,*.pxd:E211,E225,E227,E402,E999", -] \ No newline at end of file diff --git a/tests/test_decode.py b/tests/test_decode.py index c1846af69..7ad722bfd 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -218,9 +218,9 @@ def test_hardware_decode(self) -> None: ) HWACCEL_DEVICE_TYPE = os.environ["HWACCEL_DEVICE_TYPE"] - assert ( - HWACCEL_DEVICE_TYPE in hwdevices_available - ), f"{HWACCEL_DEVICE_TYPE} not available" + assert HWACCEL_DEVICE_TYPE in hwdevices_available, ( + f"{HWACCEL_DEVICE_TYPE} not available" + ) test_video_path = "tests/assets/black.mp4" make_h264_test_video(test_video_path) From dc0e6d0700c9487739292ace58a8d3d9ea4be80f Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 29 Mar 2025 10:40:10 -0400 Subject: [PATCH 512/846] Make audio/codeccontext pure --- .../{codeccontext.pyx => codeccontext.py} | 55 +++++++++---------- 1 file changed, 27 insertions(+), 28 deletions(-) rename av/audio/{codeccontext.pyx => codeccontext.py} (63%) diff --git a/av/audio/codeccontext.pyx b/av/audio/codeccontext.py similarity index 63% rename from av/audio/codeccontext.pyx rename to av/audio/codeccontext.py index 856af555c..2dc629917 100644 --- a/av/audio/codeccontext.pyx +++ b/av/audio/codeccontext.py @@ -1,21 +1,20 @@ -cimport libav as lib - -from av.audio.format cimport AudioFormat, get_audio_format -from av.audio.frame cimport AudioFrame, alloc_audio_frame -from av.audio.layout cimport AudioLayout, get_audio_layout -from av.codec.hwaccel cimport HWAccel -from av.frame cimport Frame -from av.packet cimport Packet - - -cdef class AudioCodecContext(CodecContext): - cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec, HWAccel hwaccel): - CodecContext._init(self, ptr, codec, hwaccel) - - cdef _prepare_frames_for_encode(self, Frame input_frame): - - cdef AudioFrame frame = input_frame - cdef bint allow_var_frame_size = self.ptr.codec.capabilities & lib.AV_CODEC_CAP_VARIABLE_FRAME_SIZE +import cython +from cython.cimports import libav as lib +from cython.cimports.av.audio.format import AudioFormat, get_audio_format +from cython.cimports.av.audio.frame import AudioFrame, alloc_audio_frame +from cython.cimports.av.audio.layout import AudioLayout, get_audio_layout +from cython.cimports.av.frame import Frame +from cython.cimports.av.packet import Packet + + +@cython.cclass +class AudioCodecContext(CodecContext): + @cython.cfunc + def _prepare_frames_for_encode(self, input_frame: Frame | None): + frame: AudioFrame | None = input_frame + allow_var_frame_size: cython.bint = ( + self.ptr.codec.capabilities & lib.AV_CODEC_CAP_VARIABLE_FRAME_SIZE + ) # Note that the resampler will simply return an input frame if there is # no resampling to be done. The control flow was just a little easier this way. @@ -24,22 +23,22 @@ format=self.format, layout=self.layout, rate=self.ptr.sample_rate, - frame_size=None if allow_var_frame_size else self.ptr.frame_size + frame_size=None if allow_var_frame_size else self.ptr.frame_size, ) frames = self.resampler.resample(frame) - - # flush if input frame is None if input_frame is None: - frames.append(None) + frames.append(None) # flush if input frame is None return frames - cdef Frame _alloc_next_frame(self): + @cython.cfunc + def _alloc_next_frame(self) -> Frame: return alloc_audio_frame() - cdef _setup_decoded_frame(self, Frame frame, Packet packet): + @cython.cfunc + def _setup_decoded_frame(self, frame: Frame, packet: Packet): CodecContext._setup_decoded_frame(self, frame, packet) - cdef AudioFrame aframe = frame + aframe: AudioFrame = frame aframe._init_user_attributes() @property @@ -61,7 +60,7 @@ def sample_rate(self): return self.ptr.sample_rate @sample_rate.setter - def sample_rate(self, int value): + def sample_rate(self, value: cython.int): self.ptr.sample_rate = value @property @@ -88,7 +87,7 @@ def layout(self): @layout.setter def layout(self, value): - cdef AudioLayout layout = AudioLayout(value) + layout: AudioLayout = AudioLayout(value) self.ptr.ch_layout = layout.layout @property @@ -102,5 +101,5 @@ def format(self): @format.setter def format(self, value): - cdef AudioFormat format = AudioFormat(value) + format: AudioFormat = AudioFormat(value) self.ptr.sample_fmt = format.sample_fmt From 4e01ca7474210b164fd630f47571455a42ce09f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Tue, 1 Apr 2025 09:28:04 +0200 Subject: [PATCH 513/846] Add typing overloads for `CodecContext.create` The return type of `CodecContext.create` can be narrowed based on the name of the requested codec, allowing us to know whether we are dealing with an `AudioCodecContext` or a `VideoCodecContext`. As the same logic is used by `OutputContainer.add_stream`, use a shared list of audio / video codec names. We add the following codec names: - audio : libopus, pcm_alaw, pcm_mulaw - video : libvpx, libx264 --- av/audio/__init__.pyi | 12 ++++++++++++ av/codec/context.pyi | 21 ++++++++++++++++++++- av/container/output.pyi | 9 +++++---- av/video/__init__.pyi | 13 +++++++++++++ 4 files changed, 50 insertions(+), 5 deletions(-) diff --git a/av/audio/__init__.pyi b/av/audio/__init__.pyi index 73f2eebdd..daefab6c9 100644 --- a/av/audio/__init__.pyi +++ b/av/audio/__init__.pyi @@ -1,4 +1,16 @@ +from typing import Literal + from .frame import AudioFrame from .stream import AudioStream +_AudioCodecName = Literal[ + "aac", + "libopus", + "mp2", + "mp3", + "pcm_alaw", + "pcm_mulaw", + "pcm_s16le", +] + __all__ = ("AudioFrame", "AudioStream") diff --git a/av/codec/context.pyi b/av/codec/context.pyi index ac57126ef..77810d9ed 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -1,8 +1,12 @@ from enum import Flag, IntEnum from fractions import Fraction -from typing import ClassVar, Literal, cast +from typing import ClassVar, Literal, cast, overload +from av.audio import _AudioCodecName +from av.audio.codeccontext import AudioCodecContext from av.packet import Packet +from av.video import _VideoCodecName +from av.video.codeccontext import VideoCodecContext from .codec import Codec from .hwaccel import HWAccel @@ -87,6 +91,21 @@ class CodecContext: @property def is_hwaccel(self) -> bool: ... def open(self, strict: bool = True) -> None: ... + @overload + @staticmethod + def create( + codec: _AudioCodecName, + mode: Literal["r", "w"] | None = None, + hwaccel: HWAccel | None = None, + ) -> AudioCodecContext: ... + @overload + @staticmethod + def create( + codec: _VideoCodecName, + mode: Literal["r", "w"] | None = None, + hwaccel: HWAccel | None = None, + ) -> VideoCodecContext: ... + @overload @staticmethod def create( codec: str | Codec, diff --git a/av/container/output.pyi b/av/container/output.pyi index 3169f486b..568345cd2 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -1,11 +1,12 @@ from fractions import Fraction -from typing import Literal, Sequence, TypeVar, Union, overload +from typing import Sequence, TypeVar, Union, overload +from av.audio import _AudioCodecName from av.audio.stream import AudioStream from av.data.stream import DataStream from av.packet import Packet -from av.stream import Stream from av.subtitles.stream import SubtitleStream +from av.video import _VideoCodecName from av.video.stream import VideoStream from .core import Container @@ -17,7 +18,7 @@ class OutputContainer(Container): @overload def add_stream( self, - codec_name: Literal["pcm_s16le", "aac", "mp3", "mp2"], + codec_name: _AudioCodecName, rate: int | None = None, options: dict[str, str] | None = None, **kwargs, @@ -25,7 +26,7 @@ class OutputContainer(Container): @overload def add_stream( self, - codec_name: Literal["h264", "hevc", "mpeg4", "png", "gif", "qtrle"], + codec_name: _VideoCodecName, rate: Fraction | int | None = None, options: dict[str, str] | None = None, **kwargs, diff --git a/av/video/__init__.pyi b/av/video/__init__.pyi index 8fa8fe7e5..58a19a63f 100644 --- a/av/video/__init__.pyi +++ b/av/video/__init__.pyi @@ -1,4 +1,17 @@ +from typing import Literal + from .frame import VideoFrame from .stream import VideoStream +_VideoCodecName = Literal[ + "gif", + "h264", + "hevc", + "libvpx", + "libx264", + "mpeg4", + "png", + "qtrle", +] + __all__ = ("VideoFrame", "VideoStream") From bc9ae5b9092f75520aa0e7f646c9d9ba355b7957 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 1 Apr 2025 23:25:28 -0400 Subject: [PATCH 514/846] Use ffmpeg 7.1.1 --- .github/workflows/smoke.yml | 6 +++--- CHANGELOG.rst | 7 +++++++ scripts/activate.sh | 2 +- scripts/ffmpeg-7.1.json | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index e8aac3330..e83adf62d 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -34,9 +34,9 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-24.04, python: "3.12", ffmpeg: "7.1", extras: true} - - {os: ubuntu-24.04, python: "pypy3.10", ffmpeg: "7.1"} - - {os: macos-14, python: "3.9", ffmpeg: "7.1"} + - {os: ubuntu-24.04, python: "3.12", ffmpeg: "7.1.1", extras: true} + - {os: ubuntu-24.04, python: "pypy3.10", ffmpeg: "7.1.1"} + - {os: macos-14, python: "3.9", ffmpeg: "7.1.1"} env: PYAV_PYTHON: python${{ matrix.config.python }} diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 828230ac8..663cf4839 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,6 +17,13 @@ We are operating with `semantic versioning `_. are merged into the "default" branch. +v14.2.1 +------- + +Fixes: + +- Uses ffmpeg 7.1.1, fixes deadlocks. + v14.2.0 ------- diff --git a/scripts/activate.sh b/scripts/activate.sh index 9dd41ba2b..ab22d79d9 100755 --- a/scripts/activate.sh +++ b/scripts/activate.sh @@ -21,7 +21,7 @@ if [[ ! "$PYAV_LIBRARY" ]]; then return 1 fi else - PYAV_LIBRARY=ffmpeg-7.1 + PYAV_LIBRARY=ffmpeg-7.1.1 echo "No \$PYAV_LIBRARY set; defaulting to $PYAV_LIBRARY" fi fi diff --git a/scripts/ffmpeg-7.1.json b/scripts/ffmpeg-7.1.json index ee386661d..2af06b2cb 100644 --- a/scripts/ffmpeg-7.1.json +++ b/scripts/ffmpeg-7.1.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.1-4/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.1.1-1/ffmpeg-{platform}.tar.gz" } From 6940c5e048fcf17894d5a3caa72849aaa2d63829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Sat, 5 Apr 2025 10:34:45 +0200 Subject: [PATCH 515/846] Provide binary wheels for musl-based Linux distros such as Alpine --- .github/workflows/tests.yml | 3 +-- CHANGELOG.rst | 4 ++++ scripts/fetch-vendor.py | 5 ++++- scripts/ffmpeg-7.1.json | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bf30096d5..a38af2c08 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -57,7 +57,6 @@ jobs: - name: Build wheels env: CIBW_ARCHS: ${{ matrix.arch }} - CIBW_BEFORE_ALL_LINUX: yum install -y alsa-lib libxcb CIBW_BEFORE_BUILD: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.1.json /tmp/vendor CIBW_BEFORE_BUILD_MACOS: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.1.json /tmp/vendor CIBW_BEFORE_BUILD_WINDOWS: python scripts\fetch-vendor.py --config-file scripts\ffmpeg-7.1.json C:\cibw\vendor @@ -65,7 +64,7 @@ jobs: CIBW_ENVIRONMENT_MACOS: PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig LDFLAGS=-headerpad_max_install_names CIBW_ENVIRONMENT_WINDOWS: INCLUDE=C:\\cibw\\vendor\\include LIB=C:\\cibw\\vendor\\lib PYAV_SKIP_TESTS=unicode_filename CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: delvewheel repair --add-path C:\cibw\vendor\bin -w {dest_dir} {wheel} - CIBW_SKIP: "pp* *-musllinux*" + CIBW_SKIP: "pp*" CIBW_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m pytest {package}/tests && mv {project}/av.disabled {project}/av CIBW_TEST_REQUIRES: pytest numpy # skip tests when there are no binary wheels of numpy diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 663cf4839..1580e0a63 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -20,6 +20,10 @@ We are operating with `semantic versioning `_. v14.2.1 ------- +Features: + +- Provide binary wheels for musl-based distros such as Alpine by :gh-user:`jlaine` in (:pr:`1856`). + Fixes: - Uses ffmpeg 7.1.1, fixes deadlocks. diff --git a/scripts/fetch-vendor.py b/scripts/fetch-vendor.py index 5041f4265..47aa2a421 100644 --- a/scripts/fetch-vendor.py +++ b/scripts/fetch-vendor.py @@ -11,7 +11,10 @@ def get_platform(): system = platform.system() machine = platform.machine() if system == "Linux": - return f"manylinux_{machine}" + if platform.libc_ver()[0] == "glibc": + return f"manylinux_{machine}" + else: + return f"musllinux_{machine}" elif system == "Darwin": # cibuildwheel sets ARCHFLAGS: # https://github.com/pypa/cibuildwheel/blob/5255155bc57eb6224354356df648dc42e31a0028/cibuildwheel/macos.py#L207-L220 diff --git a/scripts/ffmpeg-7.1.json b/scripts/ffmpeg-7.1.json index 2af06b2cb..42601a31d 100644 --- a/scripts/ffmpeg-7.1.json +++ b/scripts/ffmpeg-7.1.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.1.1-1/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.1.1-2/ffmpeg-{platform}.tar.gz" } From 81fd054d3d92714ed7ce973962397399e8af5b53 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 6 Apr 2025 02:31:22 -0400 Subject: [PATCH 516/846] Update authors --- AUTHORS.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 1792ba863..df46c6c11 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -16,12 +16,12 @@ All contributors (by number of commits): * Casper van der Wel * Philip de Nier * Tadas Dailyda +* Dave Johansen * JoeUgly <41972063+JoeUgly@users.noreply.github.com> * Justin Wong <46082645+uvjustin@users.noreply.github.com> * Mark Harfouche - Alba Mendez -- Dave Johansen - Xinran Xu ; `@xxr3376 `_ - Dan Allan ; `@danielballan `_ - Moonsik Park @@ -78,10 +78,12 @@ All contributors (by number of commits): * Johan Jeppsson Karlin * Philipp Klaus * Kim Minjong +* Marcell Pardavi * Matteo Destro * Mattias Wadman * Manuel Goacolou * Julian Schweizer +* Nikhil Idiculla * Ömer Sezgin Uğurlu * Orivej Desh * Philipp Krähenbühl From 4c29ccd4badbbdfb5b57ff0b114b7f6a21b79a2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Sun, 6 Apr 2025 11:27:47 +0200 Subject: [PATCH 517/846] Bump version to 14.3.0 --- CHANGELOG.rst | 10 +++++++--- av/about.py | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1580e0a63..71ddc1214 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,15 +17,19 @@ We are operating with `semantic versioning `_. are merged into the "default" branch. -v14.2.1 +v14.3.0 ------- Features: +- Support yuv420p10le in numpy conversion by :gh-user:`tsnl` in (:pr:`1833`). +- Add typing overloads for ``CodecContext.create`` by :gh-user:`jlaine` in (:pr:`1853`). - Provide binary wheels for musl-based distros such as Alpine by :gh-user:`jlaine` in (:pr:`1856`). Fixes: +- Fix ``Packet.dts`` typing by :gh-user:`daveisfera` in (:pr:`1815`). +- Use Python 3 compatible stderr logging by :gh-user:`pmarcelll` in (:pr:`1832`). - Uses ffmpeg 7.1.1, fixes deadlocks. v14.2.0 @@ -53,8 +57,8 @@ Features: - Add hardware decoding by :gh-user:`matthewlai` and :gh-user:`WyattBlue` in (:pr:`1685`). - Add ``Stream.disposition`` and ``Disposition`` enum by :gh-user:`WyattBlue` in (:pr:`1720`). - Add ``VideoFrame.rotation`` by :gh-user:`lgeiger` in (:pr:`1675`). -- Support grayf32le and gbrapf32le in numpy convertion by :gh-user:`robinechuca` in (:pr:`1712`). -- Support yuv[a]p16 formats in numpy convertion by :gh-user:`robinechuca` in (:pr:`1722`). +- Support grayf32le and gbrapf32le in numpy conversion by :gh-user:`robinechuca` in (:pr:`1712`). +- Support yuv[a]p16 formats in numpy conversion by :gh-user:`robinechuca` in (:pr:`1722`). v14.0.1 ------- diff --git a/av/about.py b/av/about.py index 331dde6ff..f79d47d48 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "14.2.0" +__version__ = "14.3.0" From 1cd9c8a4e5257fa3a267a5617490cd92fe352590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Sun, 6 Apr 2025 17:33:02 +0200 Subject: [PATCH 518/846] Update license information according to PEP 639 --- pyproject.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 458beee81..26ebae4ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ requires = ["setuptools>61", "cython>=3.1.0a1,<4"] name = "av" description = "Pythonic bindings for FFmpeg's libraries." readme = "README.md" -license = {text = "BSD-3-Clause"} +license = "BSD-3-Clause" authors = [ {name = "WyattBlue", email = "wyattblue@auto-editor.com"}, {name = "Jeremy Lainé", email = "jeremy.laine@m4x.org"}, @@ -14,7 +14,6 @@ requires-python = ">=3.9" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", - "License :: OSI Approved :: BSD License", "Natural Language :: English", "Operating System :: MacOS :: MacOS X", "Operating System :: POSIX", From cc45b2c504697402d0b6d15d6f4e8010c4dcdbb3 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 13 Apr 2025 12:15:10 +0200 Subject: [PATCH 519/846] Bump setuptools requirement for PEP 639 support --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 26ebae4ef..9710de837 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>61", "cython>=3.1.0a1,<4"] +requires = ["setuptools>=77.0", "cython>=3.1.0a1,<4"] [project] name = "av" From f3504bad6194263208a331a6687dc0328f4a9571 Mon Sep 17 00:00:00 2001 From: z-khan Date: Sat, 19 Apr 2025 23:08:12 +1000 Subject: [PATCH 520/846] update cuda config flags update cuda config flag to "--enable-cuda-nvcc" as per Video Codec SDK v13.0 https://docs.nvidia.com/video-technologies/video-codec-sdk/13.0/ffmpeg-with-nvidia-gpu/index.html --- scripts/build-deps | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/scripts/build-deps b/scripts/build-deps index 4291140dd..d278c27c0 100755 --- a/scripts/build-deps +++ b/scripts/build-deps @@ -29,9 +29,7 @@ if [[ -e /usr/local/cuda ]]; then fi PKG_CONFIG_PATH="/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH" - CONFFLAGS_NVIDIA="--enable-cuda \ - --enable-cuvid \ - --enable-nvenc \ + CONFFLAGS_NVIDIA="--enable-cuda-nvcc \ --enable-nonfree \ --enable-libnpp \ --extra-cflags=-I/usr/local/cuda/include \ From fc1402cd1d4dd8570782769d74e13144d5fd2ec6 Mon Sep 17 00:00:00 2001 From: z-khan Date: Thu, 24 Apr 2025 07:24:31 +1000 Subject: [PATCH 521/846] Add BGR48, BGRA64 pixel formats Extend `supported_np_pix_fmts` pixel formats with `bgr48be` `bgr48le` `bgra64be` `bgra64le`. Add handling for `argb` `abgr` `grayf32le` `grayf32be` pixel formats. --------- Co-authored-by: WyattBlue --- av/video/frame.pyx | 49 +++++++++++++------ tests/test_videoframe.py | 103 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 15 deletions(-) diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 785603a1a..04a14fd8b 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -16,7 +16,7 @@ cdef object _cinit_bypass_sentinel supported_np_pix_fmts = { "abgr", "argb", "bayer_bggr16be", "bayer_bggr16le", "bayer_bggr8", "bayer_gbrg16be", "bayer_gbrg16le", "bayer_gbrg8", "bayer_grbg16be", "bayer_grbg16le", "bayer_grbg8", - "bayer_rggb16be", "bayer_rggb16le", "bayer_rggb8", "bgr24", "bgr8", "bgra", + "bayer_rggb16be", "bayer_rggb16le", "bayer_rggb8", "bgr24", "bgr48be", "bgr48le", "bgr8", "bgra", "bgra64be", "bgra64le", "gbrapf32be", "gbrapf32le", "gbrp", "gbrp10be", "gbrp10le", "gbrp12be", "gbrp12le", "gbrp14be", "gbrp14le", "gbrp16be", "gbrp16le", "gbrpf32be", "gbrpf32le", "gray", "gray16be", "gray16le", "gray8", "grayf32be", "grayf32le", "nv12", "pal8", "rgb24", @@ -342,6 +342,8 @@ cdef class VideoFrame(Frame): "bayer_rggb16le": (2, "uint16"), "bayer_rggb16be": (2, "uint16"), "bgr24": (3, "uint8"), + "bgr48be": (6, "uint16"), + "bgr48le": (6, "uint16"), "bgr8": (1, "uint8"), "bgra": (4, "uint8"), "gbrapf32be": (4, "float32"), @@ -370,6 +372,8 @@ cdef class VideoFrame(Frame): "rgba": (4, "uint8"), "rgba64be": (8, "uint16"), "rgba64le": (8, "uint16"), + "bgra64be": (8, "uint16"), + "bgra64le": (8, "uint16"), "yuv444p": (1, "uint8"), "yuv444p16be": (2, "uint16"), "yuv444p16le": (2, "uint16"), @@ -467,43 +471,58 @@ cdef class VideoFrame(Frame): if not width: width = array.shape[1] - if format in ("rgb24", "bgr24"): + if format in {"rgb24", "bgr24"}: check_ndarray(array, "uint8", 3) check_ndarray_shape(array, array.shape[2] == 3) if array.strides[1:] != (3, 1): raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = (array.strides[0], ) + elif format in {"rgb48le", "rgb48be", "bgr48le", "bgr48be"}: + check_ndarray(array, "uint16", 3) + check_ndarray_shape(array, array.shape[2] == 3) + if array.strides[1:] != (6, 2): + raise ValueError("provided array does not have C_CONTIGUOUS rows") linesizes = (array.strides[0], ) - elif format in ("rgba", "bgra"): + elif format in {"rgba", "bgra", "argb", "abgr"}: check_ndarray(array, "uint8", 3) check_ndarray_shape(array, array.shape[2] == 4) if array.strides[1:] != (4, 1): raise ValueError("provided array does not have C_CONTIGUOUS rows") linesizes = (array.strides[0], ) - elif format in ("gray", "gray8", "rgb8", "bgr8"): + elif format in {"rgba64le", "rgba64be", "bgra64le", "bgra64be"}: + check_ndarray(array, "uint16", 3) + check_ndarray_shape(array, array.shape[2] == 4) + if array.strides[1:] != (8, 2): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = (array.strides[0], ) + elif format in {"gray", "gray8", "rgb8", "bgr8","bayer_bggr8", "bayer_rggb8", "bayer_gbrg8", "bayer_grbg8"}: check_ndarray(array, "uint8", 2) if array.strides[1] != 1: raise ValueError("provided array does not have C_CONTIGUOUS rows") linesizes = (array.strides[0], ) - elif format in ("yuv420p", "yuvj420p", "nv12"): + elif format in {"gray16le", "gray16be", "bayer_rggb16le", "bayer_gbrg16le", "bayer_grbg16le","bayer_bggr16be", "bayer_rggb16be", "bayer_gbrg16be", "bayer_grbg16be"}: + check_ndarray(array, "uint16", 2) + if array.strides[1] != 2: + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = (array.strides[0], ) + elif format in {"grayf32le", "grayf32be"}: + check_ndarray(array, "float32", 2) + if array.strides[1] != 4: + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = (array.strides[0], ) + elif format in {"yuv420p", "yuvj420p", "nv12"}: check_ndarray(array, "uint8", 2) check_ndarray_shape(array, array.shape[0] % 3 == 0) check_ndarray_shape(array, array.shape[1] % 2 == 0) height = height // 6 * 4 if array.strides[1] != 1: raise ValueError("provided array does not have C_CONTIGUOUS rows") - if format in ("yuv420p", "yuvj420p"): + if format in {"yuv420p", "yuvj420p"}: # For YUV420 planar formats, the UV plane stride is always half the Y stride. linesizes = (array.strides[0], array.strides[0] // 2, array.strides[0] // 2) else: # Planes where U and V are interleaved have the same stride as Y. linesizes = (array.strides[0], array.strides[0]) - elif format in {"bayer_bggr8", "bayer_rggb8", "bayer_gbrg8", "bayer_grbg8","bayer_bggr16le", "bayer_rggb16le", "bayer_gbrg16le", "bayer_grbg16le","bayer_bggr16be", "bayer_rggb16be", "bayer_gbrg16be", "bayer_grbg16be"}: - check_ndarray(array, "uint8" if format.endswith("8") else "uint16", 2) - - if array.strides[1] != (1 if format.endswith("8") else 2): - raise ValueError("provided array does not have C_CONTIGUOUS rows") - - linesizes = (array.strides[0],) else: raise ValueError(f"Conversion from numpy array with format `{format}` is not yet supported") @@ -717,13 +736,13 @@ cdef class VideoFrame(Frame): elif format in {"argb", "rgba", "abgr", "bgra"}: check_ndarray(array, "uint8", 3) check_ndarray_shape(array, array.shape[2] == 4) - elif format in {"rgb48be", "rgb48le"}: + elif format in {"rgb48be", "rgb48le","bgr48be", "bgr48le"}: check_ndarray(array, "uint16", 3) check_ndarray_shape(array, array.shape[2] == 3) frame = VideoFrame(array.shape[1], array.shape[0], format) copy_array_to_plane(byteswap_array(array, format.endswith("be")), frame.planes[0], 6) return frame - elif format in {"rgba64be", "rgba64le"}: + elif format in {"rgba64be", "rgba64le", "bgra64be", "bgra64le"}: check_ndarray(array, "uint16", 3) check_ndarray_shape(array, array.shape[2] == 4) frame = VideoFrame(array.shape[1], array.shape[0], format) diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 9327a4ccd..26549b31b 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -285,6 +285,35 @@ def test_ndarray_rgba_align() -> None: assertNdarraysEqual(frame.to_ndarray(), array) +def test_ndarray_bayer8() -> None: + array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) + for format in ("bayer_bggr8", "bayer_gbrg8", "bayer_grbg8", "bayer_rggb8"): + frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_bayer16() -> None: + array = numpy.random.randint(0, 65536, size=(480, 640), dtype=numpy.uint16) + for format in ( + "bayer_bggr16be", + "bayer_bggr16le", + "bayer_gbrg16be", + "bayer_gbrg16le", + "bayer_grbg16be", + "bayer_grbg16le", + "bayer_rggb16be", + "bayer_rggb16le", + ): + frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + def test_ndarray_gbrp() -> None: array = numpy.random.randint(0, 256, size=(480, 640, 3), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="gbrp") @@ -571,6 +600,17 @@ def test_ndarray_rgb48be() -> None: assertPixelValue16(frame.planes[0], array[0][0][0], "big") +def test_ndarray_bgr48be() -> None: + array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="bgr48be") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "bgr48be" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining blue value of first pixel + assertPixelValue16(frame.planes[0], array[0][0][0], "big") + + def test_ndarray_rgb48le() -> None: array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="rgb48le") @@ -582,6 +622,17 @@ def test_ndarray_rgb48le() -> None: assertPixelValue16(frame.planes[0], array[0][0][0], "little") +def test_ndarray_bgr48le() -> None: + array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="bgr48le") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "bgr48le" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining blue value of first pixel + assertPixelValue16(frame.planes[0], array[0][0][0], "little") + + def test_ndarray_rgb48le_align() -> None: array = numpy.random.randint(0, 65536, size=(238, 318, 3), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="rgb48le") @@ -593,6 +644,17 @@ def test_ndarray_rgb48le_align() -> None: assertPixelValue16(frame.planes[0], array[0][0][0], "little") +def test_ndarray_bgr48le_align() -> None: + array = numpy.random.randint(0, 65536, size=(238, 318, 3), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="bgr48le") + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == "bgr48le" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining blue value of first pixel + assertPixelValue16(frame.planes[0], array[0][0][0], "little") + + def test_ndarray_rgba64be() -> None: array = numpy.random.randint(0, 65536, size=(480, 640, 4), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="rgba64be") @@ -604,6 +666,17 @@ def test_ndarray_rgba64be() -> None: assertPixelValue16(frame.planes[0], array[0][0][0], "big") +def test_ndarray_bgra64be() -> None: + array = numpy.random.randint(0, 65536, size=(480, 640, 4), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="bgra64be") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "bgra64be" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining blue value of first pixel + assertPixelValue16(frame.planes[0], array[0][0][0], "big") + + def test_ndarray_rgba64le() -> None: array = numpy.random.randint(0, 65536, size=(480, 640, 4), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="rgba64le") @@ -615,6 +688,17 @@ def test_ndarray_rgba64le() -> None: assertPixelValue16(frame.planes[0], array[0][0][0], "little") +def test_ndarray_bgra64le() -> None: + array = numpy.random.randint(0, 65536, size=(480, 640, 4), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="bgra64le") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "bgra64le" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining blue value of first pixel + assertPixelValue16(frame.planes[0], array[0][0][0], "little") + + def test_ndarray_rgb8() -> None: array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="rgb8") @@ -805,6 +889,25 @@ def test_shares_memory_rgba() -> None: assertNdarraysEqual(frame.to_ndarray(), array) +def test_shares_memory_bayer8() -> None: + for format in ("bayer_rggb8", "bayer_bggr8", "bayer_grbg8", "bayer_gbrg8"): + array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + frame = VideoFrame.from_numpy_buffer(array, format) + assertNdarraysEqual(frame.to_ndarray(), array) + + array[...] = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + assertNdarraysEqual(frame.to_ndarray(), array) + + array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + array = array[:, :300] + assert not array.data.c_contiguous + frame = VideoFrame.from_numpy_buffer(array, format) + assertNdarraysEqual(frame.to_ndarray(), array) + + array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) + assertNdarraysEqual(frame.to_ndarray(), array) + + def test_shares_memory_yuv420p() -> None: array = numpy.random.randint(0, 256, size=(512 * 6 // 4, 256), dtype=numpy.uint8) frame = VideoFrame.from_numpy_buffer(array, "yuv420p") From d00c98d30631f65165140c69e5c7c61732d182c3 Mon Sep 17 00:00:00 2001 From: Max Ehrlich Date: Tue, 29 Apr 2025 20:51:22 +0000 Subject: [PATCH 522/846] Expose the duration field for frames Signed-off-by: Max Ehrlich --- av/frame.pyi | 1 + av/frame.pyx | 21 +++++++++++++++++++++ av/video/frame.pyi | 1 + include/libavcodec/avcodec.pxd | 2 ++ tests/test_videoframe.py | 15 +++++++++++++++ 5 files changed, 40 insertions(+) diff --git a/av/frame.pyi b/av/frame.pyi index 9af81dcfe..38a273afc 100644 --- a/av/frame.pyi +++ b/av/frame.pyi @@ -9,6 +9,7 @@ class SideData(TypedDict, total=False): class Frame: dts: int | None pts: int | None + duration: int | None time_base: Fraction side_data: SideData opaque: object diff --git a/av/frame.pyx b/av/frame.pyx index 57681bbcd..fefdd2dee 100644 --- a/av/frame.pyx +++ b/av/frame.pyx @@ -53,6 +53,9 @@ cdef class Frame: if self.ptr.pts != lib.AV_NOPTS_VALUE: self.ptr.pts = lib.av_rescale_q(self.ptr.pts, self._time_base, dst) + if self.ptr.duration != 0: + self.ptr.duration = lib.av_rescale_q(self.ptr.duration, self._time_base, dst) + self._time_base = dst @property @@ -95,6 +98,24 @@ cdef class Frame: else: self.ptr.pts = value + @property + def duration(self): + """ + The duration of the frame in :attr:`time_base` units + + :type: int + """ + if self.ptr.duration == 0: + return None + return self.ptr.duration + + @duration.setter + def duration(self, value): + if value is None: + self.ptr.duration = 0 + else: + self.ptr.duration = value + @property def time(self): """ diff --git a/av/video/frame.pyi b/av/video/frame.pyi index bba60cc5d..313e184f9 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -30,6 +30,7 @@ class PictureType(IntEnum): class VideoFrame(Frame): format: VideoFormat pts: int + duration: int planes: tuple[VideoPlane, ...] pict_type: int colorspace: int diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 5bed3583d..0c8713cf8 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -461,6 +461,8 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVColorTransferCharacteristic color_trc AVColorSpace colorspace + int64_t duration + cdef AVFrame* avcodec_alloc_frame() cdef struct AVPacket: diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 26549b31b..677bc1fc1 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -49,6 +49,21 @@ def test_opaque() -> None: assert type(frame.opaque) is tuple and len(frame.opaque) == 2 +def test_frame_duration_matches_packet() -> None: + with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: + packet_durations = [ + (p.pts, p.duration) for p in container.demux() if p.pts is not None + ] + packet_durations.sort(key=lambda x: x[0]) + + with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: + frame_durations = [(f.pts, f.duration) for f in container.decode(video=0)] + frame_durations.sort(key=lambda x: x[0]) + + assert len(packet_durations) == len(frame_durations) + assert all(pd[1] == fd[1] for pd, fd in zip(packet_durations, frame_durations)) + + def test_invalid_pixel_format() -> None: with pytest.raises(ValueError, match="not a pixel format: '__unknown_pix_fmt'"): VideoFrame(640, 480, "__unknown_pix_fmt") From f1070b88f359ceff4db8381f930a70eef3baeffe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Fri, 16 May 2025 15:34:30 +0200 Subject: [PATCH 523/846] Update FFmpeg build to use libvpx 1.15.1 and libxml2 2.14.3 libvpx 1.14.0 is vulnerable to CVE-2024-5197. libxml2 2.9.13 is vulnerable to CVE-2022-2309, CVE-2023-29469, CVE-2017-5130, CVE-2023-45322, CVE-2024-25062, CVE-2022-40303, CVE-2022-40304, CVE-2023-28484, CVE-2022-29824 Fixes: #1892 Fixes: #1894 --- scripts/ffmpeg-7.1.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ffmpeg-7.1.json b/scripts/ffmpeg-7.1.json index 42601a31d..26641cedc 100644 --- a/scripts/ffmpeg-7.1.json +++ b/scripts/ffmpeg-7.1.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.1.1-2/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.1.1-4/ffmpeg-{platform}.tar.gz" } From e6a399a8bcb13595a68589307349c19d528c502e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Fri, 16 May 2025 19:37:17 +0200 Subject: [PATCH 524/846] Bump version to 14.4.0 --- CHANGELOG.rst | 11 +++++++++++ av/about.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 71ddc1214..a340bb088 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,17 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. +v14.4.0 +------- + +Features: + +- Expose the duration field for frames by :gh-user:`Queuecumber` in (:pr:`1880`). +- Add support for BGR48 and BGRA64 pixel formats by :gh-user:`z-khan` in (:pr:`1873`). + +Fixes: + +- Fix security vulnerabilities in binary wheels by :gh-user:`jlaine` in (:pr:`1896`). v14.3.0 ------- diff --git a/av/about.py b/av/about.py index f79d47d48..15fd90e44 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "14.3.0" +__version__ = "14.4.0" From efe9ce263019a843443a735c55313bd740d45874 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 20 May 2025 23:42:54 -0400 Subject: [PATCH 525/846] Make container/output pure This also syncs the QoL changes I've made in basswood-av. "Pure" stands for Cython's "Pure Python" mode, a way to write Cython in the Python grammar. --- av/container/output.pxd | 1 - av/container/{output.pyx => output.py} | 236 +++++++++++++------------ 2 files changed, 127 insertions(+), 110 deletions(-) rename av/container/{output.pyx => output.py} (58%) diff --git a/av/container/output.pxd b/av/container/output.pxd index a4299891c..51d3f308e 100644 --- a/av/container/output.pxd +++ b/av/container/output.pxd @@ -5,7 +5,6 @@ from av.stream cimport Stream cdef class OutputContainer(Container): - cdef bint _started cdef bint _done cdef lib.AVPacket *packet_ptr diff --git a/av/container/output.pyx b/av/container/output.py similarity index 58% rename from av/container/output.pyx rename to av/container/output.py index 2a12c40f2..64a4f4a53 100644 --- a/av/container/output.pyx +++ b/av/container/output.py @@ -1,24 +1,22 @@ -import logging import os from fractions import Fraction -cimport libav as lib - -from av.codec.codec cimport Codec -from av.codec.context cimport CodecContext, wrap_codec_context -from av.container.streams cimport StreamContainer -from av.dictionary cimport _Dictionary -from av.error cimport err_check -from av.packet cimport Packet -from av.stream cimport Stream, wrap_stream -from av.utils cimport dict_to_avdict, to_avrational +import cython +from cython.cimports import libav as lib +from cython.cimports.av.codec.codec import Codec +from cython.cimports.av.codec.context import CodecContext, wrap_codec_context +from cython.cimports.av.container.streams import StreamContainer +from cython.cimports.av.dictionary import _Dictionary +from cython.cimports.av.error import err_check +from cython.cimports.av.packet import Packet +from cython.cimports.av.stream import Stream, wrap_stream +from cython.cimports.av.utils import dict_to_avdict, to_avrational from av.dictionary import Dictionary -log = logging.getLogger(__name__) - -cdef close_output(OutputContainer self): +@cython.cfunc +def close_output(self: OutputContainer): self.streams = StreamContainer() if self._started and not self._done: # We must only ever call av_write_trailer *once*, otherwise we get a @@ -28,23 +26,24 @@ self.err_check(lib.av_write_trailer(self.ptr)) finally: if self.file is None and not (self.ptr.oformat.flags & lib.AVFMT_NOFILE): - lib.avio_closep(&self.ptr.pb) + lib.avio_closep(cython.address(self.ptr.pb)) self._done = True -cdef class OutputContainer(Container): +@cython.cclass +class OutputContainer(Container): def __cinit__(self, *args, **kwargs): self.streams = StreamContainer() self.metadata = {} - with nogil: + with cython.nogil: self.packet_ptr = lib.av_packet_alloc() def __dealloc__(self): close_output(self) - with nogil: - lib.av_packet_free(&self.packet_ptr) + with cython.nogil: + lib.av_packet_free(cython.address(self.packet_ptr)) - def add_stream(self, codec_name, rate=None, dict options=None, **kwargs): + def add_stream(self, codec_name, rate=None, options: dict | None = None, **kwargs): """add_stream(codec_name, rate=None) Creates a new stream from a codec name and returns it. @@ -58,67 +57,69 @@ def add_stream(self, codec_name, rate=None, dict options=None, **kwargs): """ - cdef Codec codec_obj = Codec(codec_name, "w") - cdef const lib.AVCodec *codec = codec_obj.ptr + codec_obj: Codec = Codec(codec_name, "w") + codec: cython.pointer[cython.const[lib.AVCodec]] = codec_obj.ptr # Assert that this format supports the requested codec. - if not lib.avformat_query_codec(self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL): + if not lib.avformat_query_codec( + self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL + ): raise ValueError( f"{self.format.name!r} format does not support {codec_obj.name!r} codec" ) # Create new stream in the AVFormatContext, set AVCodecContext values. - cdef lib.AVStream *stream = lib.avformat_new_stream(self.ptr, codec) - cdef lib.AVCodecContext *codec_context = lib.avcodec_alloc_context3(codec) + stream: cython.pointer[lib.AVStream] = lib.avformat_new_stream(self.ptr, codec) + ctx: cython.pointer[lib.AVCodecContext] = lib.avcodec_alloc_context3(codec) # Now lets set some more sane video defaults if codec.type == lib.AVMEDIA_TYPE_VIDEO: - codec_context.pix_fmt = lib.AV_PIX_FMT_YUV420P - codec_context.width = kwargs.pop("width", 640) - codec_context.height = kwargs.pop("height", 480) - codec_context.bit_rate = kwargs.pop("bit_rate", 0) - codec_context.bit_rate_tolerance = kwargs.pop("bit_rate_tolerance", 128000) + ctx.pix_fmt = lib.AV_PIX_FMT_YUV420P + ctx.width = kwargs.pop("width", 640) + ctx.height = kwargs.pop("height", 480) + ctx.bit_rate = kwargs.pop("bit_rate", 0) + ctx.bit_rate_tolerance = kwargs.pop("bit_rate_tolerance", 128000) try: - to_avrational(kwargs.pop("time_base"), &codec_context.time_base) + to_avrational(kwargs.pop("time_base"), cython.address(ctx.time_base)) except KeyError: pass - to_avrational(rate or 24, &codec_context.framerate) + to_avrational(rate or 24, cython.address(ctx.framerate)) - stream.avg_frame_rate = codec_context.framerate - stream.time_base = codec_context.time_base + stream.avg_frame_rate = ctx.framerate + stream.time_base = ctx.time_base # Some sane audio defaults elif codec.type == lib.AVMEDIA_TYPE_AUDIO: - codec_context.sample_fmt = codec.sample_fmts[0] - codec_context.bit_rate = kwargs.pop("bit_rate", 0) - codec_context.bit_rate_tolerance = kwargs.pop("bit_rate_tolerance", 32000) + ctx.sample_fmt = codec.sample_fmts[0] + ctx.bit_rate = kwargs.pop("bit_rate", 0) + ctx.bit_rate_tolerance = kwargs.pop("bit_rate_tolerance", 32000) try: - to_avrational(kwargs.pop("time_base"), &codec_context.time_base) + to_avrational(kwargs.pop("time_base"), cython.address(ctx.time_base)) except KeyError: pass if rate is None: - codec_context.sample_rate = 48000 + ctx.sample_rate = 48000 elif type(rate) is int: - codec_context.sample_rate = rate + ctx.sample_rate = rate else: raise TypeError("audio stream `rate` must be: int | None") - stream.time_base = codec_context.time_base - lib.av_channel_layout_default(&codec_context.ch_layout, 2) + stream.time_base = ctx.time_base + lib.av_channel_layout_default(cython.address(ctx.ch_layout), 2) # Some formats want stream headers to be separate if self.ptr.oformat.flags & lib.AVFMT_GLOBALHEADER: - codec_context.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER + ctx.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER # Initialise stream codec parameters to populate the codec type. # # Subsequent changes to the codec context will be applied just before # encoding starts in `start_encoding()`. - err_check(lib.avcodec_parameters_from_context(stream.codecpar, codec_context)) + err_check(lib.avcodec_parameters_from_context(stream.codecpar, ctx)) # Construct the user-land stream - cdef CodecContext py_codec_context = wrap_codec_context(codec_context, codec, None) - cdef Stream py_stream = wrap_stream(self, stream, py_codec_context) + py_codec_context: CodecContext = wrap_codec_context(ctx, codec, None) + py_stream: Stream = wrap_stream(self, stream, py_codec_context) self.streams.add_stream(py_stream) if options: @@ -129,7 +130,7 @@ def add_stream(self, codec_name, rate=None, dict options=None, **kwargs): return py_stream - def add_stream_from_template(self, Stream template not None, opaque=None, **kwargs): + def add_stream_from_template(self, template: Stream, opaque=None, **kwargs): """ Creates a new stream from a template. Supports video, audio, and subtitle streams. @@ -138,43 +139,44 @@ def add_stream_from_template(self, Stream template not None, opaque=None, **kwar :param \\**kwargs: Set attributes for the stream. :rtype: The new :class:`~av.stream.Stream`. """ - cdef const lib.AVCodec *codec - cdef Codec codec_obj - if opaque is None: opaque = template.type != "video" + codec_obj: Codec if opaque: # Copy ctx from template. codec_obj = template.codec_context.codec - else: # Construct new codec object. + else: # Construct new codec object. codec_obj = Codec(template.codec_context.codec.name, "w") - codec = codec_obj.ptr + + codec: cython.pointer[cython.const[lib.AVCodec]] = codec_obj.ptr # Assert that this format supports the requested codec. - if not lib.avformat_query_codec(self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL): + if not lib.avformat_query_codec( + self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL + ): raise ValueError( f"{self.format.name!r} format does not support {codec_obj.name!r} codec" ) # Create new stream in the AVFormatContext, set AVCodecContext values. - cdef lib.AVStream *stream = lib.avformat_new_stream(self.ptr, codec) - cdef lib.AVCodecContext *codec_context = lib.avcodec_alloc_context3(codec) + stream: cython.pointer[lib.AVStream] = lib.avformat_new_stream(self.ptr, codec) + ctx: cython.pointer[lib.AVCodecContext] = lib.avcodec_alloc_context3(codec) - err_check(lib.avcodec_parameters_to_context(codec_context, template.ptr.codecpar)) + err_check(lib.avcodec_parameters_to_context(ctx, template.ptr.codecpar)) # Reset the codec tag assuming we are remuxing. - codec_context.codec_tag = 0 + ctx.codec_tag = 0 # Some formats want stream headers to be separate if self.ptr.oformat.flags & lib.AVFMT_GLOBALHEADER: - codec_context.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER + ctx.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER # Initialize stream codec parameters to populate the codec type. Subsequent changes to # the codec context will be applied just before encoding starts in `start_encoding()`. - err_check(lib.avcodec_parameters_from_context(stream.codecpar, codec_context)) + err_check(lib.avcodec_parameters_from_context(stream.codecpar, ctx)) # Construct the user-land stream - cdef CodecContext py_codec_context = wrap_codec_context(codec_context, codec, None) - cdef Stream py_stream = wrap_stream(self, stream, py_codec_context) + py_codec_context: CodecContext = wrap_codec_context(ctx, codec, None) + py_stream: Stream = wrap_stream(self, stream, py_codec_context) self.streams.add_stream(py_stream) for k, v in kwargs.items(): @@ -182,8 +184,7 @@ def add_stream_from_template(self, Stream template not None, opaque=None, **kwar return py_stream - - def add_data_stream(self, codec_name=None, dict options=None): + def add_data_stream(self, codec_name=None, options: dict | None = None): """add_data_stream(codec_name=None) Creates a new data stream and returns it. @@ -193,47 +194,49 @@ def add_data_stream(self, codec_name=None, dict options=None): :param dict options: Stream options. :rtype: The new :class:`~av.data.stream.DataStream`. """ - cdef const lib.AVCodec *codec = NULL + codec: cython.pointer[cython.const[lib.AVCodec]] = cython.NULL if codec_name is not None: codec = lib.avcodec_find_encoder_by_name(codec_name.encode()) - if codec == NULL: + if codec == cython.NULL: raise ValueError(f"Unknown data codec: {codec_name}") # Assert that this format supports the requested codec - if not lib.avformat_query_codec(self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL): + if not lib.avformat_query_codec( + self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL + ): raise ValueError( f"{self.format.name!r} format does not support {codec_name!r} codec" ) # Create new stream in the AVFormatContext - cdef lib.AVStream *stream = lib.avformat_new_stream(self.ptr, codec) - if stream == NULL: + stream: cython.pointer[lib.AVStream] = lib.avformat_new_stream(self.ptr, codec) + if stream == cython.NULL: raise MemoryError("Could not allocate stream") # Set up codec context if we have a codec - cdef lib.AVCodecContext *codec_context = NULL - if codec != NULL: - codec_context = lib.avcodec_alloc_context3(codec) - if codec_context == NULL: + ctx: cython.pointer[lib.AVCodecContext] = cython.NULL + if codec != cython.NULL: + ctx = lib.avcodec_alloc_context3(codec) + if ctx == cython.NULL: raise MemoryError("Could not allocate codec context") # Some formats want stream headers to be separate if self.ptr.oformat.flags & lib.AVFMT_GLOBALHEADER: - codec_context.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER + ctx.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER # Initialize stream codec parameters - err_check(lib.avcodec_parameters_from_context(stream.codecpar, codec_context)) + err_check(lib.avcodec_parameters_from_context(stream.codecpar, ctx)) else: # For raw data streams, just set the codec type stream.codecpar.codec_type = lib.AVMEDIA_TYPE_DATA # Construct the user-land stream - cdef CodecContext py_codec_context = None - if codec_context != NULL: - py_codec_context = wrap_codec_context(codec_context, codec, None) + py_codec_context: CodecContext | None = None + if ctx != cython.NULL: + py_codec_context = wrap_codec_context(ctx, codec, None) - cdef Stream py_stream = wrap_stream(self, stream, py_codec_context) + py_stream: Stream = wrap_stream(self, stream, py_codec_context) self.streams.add_stream(py_stream) if options: @@ -241,18 +244,18 @@ def add_data_stream(self, codec_name=None, dict options=None): return py_stream - cpdef start_encoding(self): + @cython.ccall + def start_encoding(self): """Write the file header! Called automatically.""" - if self._started: return # TODO: This does NOT handle options coming from 3 sources. # This is only a rough approximation of what would be cool to do. - used_options = set() + used_options: set = set() + stream: Stream # Finalize and open all streams. - cdef Stream stream for stream in self.streams: ctx = stream.codec_context # Skip codec context handling for data streams without codecs @@ -274,29 +277,38 @@ def add_data_stream(self, codec_name=None, dict options=None): stream._finalize_for_output() # Open the output file, if needed. - cdef bytes name_obj = os.fsencode(self.name if self.file is None else "") - cdef char *name = name_obj - if self.ptr.pb == NULL and not self.ptr.oformat.flags & lib.AVFMT_NOFILE: - err_check(lib.avio_open(&self.ptr.pb, name, lib.AVIO_FLAG_WRITE)) + name_obj: bytes = os.fsencode(self.name if self.file is None else "") + name: cython.p_char = name_obj + if self.ptr.pb == cython.NULL and not self.ptr.oformat.flags & lib.AVFMT_NOFILE: + err_check( + lib.avio_open(cython.address(self.ptr.pb), name, lib.AVIO_FLAG_WRITE) + ) # Copy the metadata dict. dict_to_avdict( - &self.ptr.metadata, self.metadata, + cython.address(self.ptr.metadata), + self.metadata, encoding=self.metadata_encoding, - errors=self.metadata_errors + errors=self.metadata_errors, ) - cdef _Dictionary all_options = Dictionary(self.options, self.container_options) - cdef _Dictionary options = all_options.copy() - self.err_check(lib.avformat_write_header(self.ptr, &options.ptr)) + all_options: _Dictionary = Dictionary(self.options, self.container_options) + options: _Dictionary = all_options.copy() + self.err_check(lib.avformat_write_header(self.ptr, cython.address(options.ptr))) # Track option usage... for k in all_options: if k not in options: used_options.add(k) + # ... and warn if any weren't used. - unused_options = {k: v for k, v in self.options.items() if k not in used_options} + unused_options = { + k: v for k, v in self.options.items() if k not in used_options + } if unused_options: + import logging + + log = logging.getLogger(__name__) log.warning("Some options were not used: %s" % unused_options) self._started = True @@ -306,21 +318,25 @@ def supported_codecs(self): """ Returns a set of all codecs this format supports. """ - result = set() - cdef const lib.AVCodec *codec = NULL - cdef void *opaque = NULL + result: set = set() + codec: cython.pointer[cython.const[lib.AVCodec]] = cython.NULL + opaque: cython.p_void = cython.NULL while True: - codec = lib.av_codec_iterate(&opaque) - if codec == NULL: + codec = lib.av_codec_iterate(cython.address(opaque)) + if codec == cython.NULL: break - if lib.avformat_query_codec(self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL) == 1: + if ( + lib.avformat_query_codec( + self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL + ) + == 1 + ): result.add(codec.name) return result - @property def default_video_codec(self): """ @@ -346,29 +362,31 @@ def close(self): close_output(self) def mux(self, packets): - # We accept either a Packet, or a sequence of packets. This should - # smooth out the transition to the new encode API which returns a - # sequence of packets. + # We accept either a Packet, or a sequence of packets. This should smooth out + # the transition to the new encode API which returns a sequence of packets. if isinstance(packets, Packet): self.mux_one(packets) else: for packet in packets: self.mux_one(packet) - def mux_one(self, Packet packet not None): + def mux_one(self, packet: Packet): self.start_encoding() # Assert the packet is in stream time. - if packet.ptr.stream_index < 0 or packet.ptr.stream_index >= self.ptr.nb_streams: + if ( + packet.ptr.stream_index < 0 + or cython.cast(cython.uint, packet.ptr.stream_index) >= self.ptr.nb_streams + ): raise ValueError("Bad Packet stream_index.") - cdef lib.AVStream *stream = self.ptr.streams[packet.ptr.stream_index] + + stream: cython.pointer[lib.AVStream] = self.ptr.streams[packet.ptr.stream_index] packet._rebase_time(stream.time_base) - # Make another reference to the packet, as av_interleaved_write_frame + # Make another reference to the packet, as `av_interleaved_write_frame()` # takes ownership of the reference. self.err_check(lib.av_packet_ref(self.packet_ptr, packet.ptr)) - cdef int ret - with nogil: - ret = lib.av_interleaved_write_frame(self.ptr, self.packet_ptr) + with cython.nogil: + ret: cython.int = lib.av_interleaved_write_frame(self.ptr, self.packet_ptr) self.err_check(ret) From 633f237a6916d17b85fe937f95e28af651fbaf0e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 21 May 2025 00:33:06 -0400 Subject: [PATCH 526/846] Copy template flags if creating a new Codec object This fixes #1903 --- av/container/output.py | 9 ++++++++- tests/test_remux.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 tests/test_remux.py diff --git a/av/container/output.py b/av/container/output.py index 64a4f4a53..d035b0265 100644 --- a/av/container/output.py +++ b/av/container/output.py @@ -130,7 +130,9 @@ def add_stream(self, codec_name, rate=None, options: dict | None = None, **kwarg return py_stream - def add_stream_from_template(self, template: Stream, opaque=None, **kwargs): + def add_stream_from_template( + self, template: Stream, opaque: bool | None = None, **kwargs + ): """ Creates a new stream from a template. Supports video, audio, and subtitle streams. @@ -170,6 +172,11 @@ def add_stream_from_template(self, template: Stream, opaque=None, **kwargs): if self.ptr.oformat.flags & lib.AVFMT_GLOBALHEADER: ctx.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER + # Copy flags If we're creating a new codec object. This fixes some muxing issues. + # Overwriting `ctx.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER` is intentional. + if not opaque: + ctx.flags = template.codec_context.flags + # Initialize stream codec parameters to populate the codec type. Subsequent changes to # the codec context will be applied just before encoding starts in `start_encoding()`. err_check(lib.avcodec_parameters_from_context(stream.codecpar, ctx)) diff --git a/tests/test_remux.py b/tests/test_remux.py new file mode 100644 index 000000000..1eca2dfd2 --- /dev/null +++ b/tests/test_remux.py @@ -0,0 +1,33 @@ +import av +import av.datasets + + +def test_video_remux() -> None: + input_path = av.datasets.curated("pexels/time-lapse-video-of-night-sky-857195.mp4") + input_ = av.open(input_path) + output = av.open("remuxed.mkv", "w") + + in_stream = input_.streams.video[0] + out_stream = output.add_stream_from_template(in_stream) + + for packet in input_.demux(in_stream): + if packet.dts is None: + continue + + packet.stream = out_stream + output.mux(packet) + + input_.close() + output.close() + + with av.open("remuxed.mkv") as container: + # Assert output is a valid media file + assert len(container.streams.video) == 1 + assert len(container.streams.audio) == 0 + assert container.streams.video[0].codec.name == "h264" + + packet_count = 0 + for packet in container.demux(video=0): + packet_count += 1 + + assert packet_count > 50 From 7299106973cd0b3a9ea0894f6436dc04762fbd81 Mon Sep 17 00:00:00 2001 From: Jiabei Zhu Date: Fri, 6 Jun 2025 20:24:08 -0400 Subject: [PATCH 527/846] Fix typo for AudioFormat.is_packed --- av/audio/format.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/audio/format.pyx b/av/audio/format.pyx index f86e7b604..c67f2fb9c 100644 --- a/av/audio/format.pyx +++ b/av/audio/format.pyx @@ -82,7 +82,7 @@ cdef class AudioFormat: @property def is_packed(self): - """Is this a planar format? + """Is this a packed format? Strictly opposite of :attr:`is_planar`. From 3739f5986acceaf8c56e35a7fbc35e0a2f684a62 Mon Sep 17 00:00:00 2001 From: zzjjbb <31069326+zzjjbb@users.noreply.github.com> Date: Fri, 6 Jun 2025 21:33:42 -0500 Subject: [PATCH 528/846] Fix memory error for AudioFifo properties Validate self.ptr before access: uninitialized properties should return None rather than crash the Python interpreter --- av/audio/fifo.pyx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/av/audio/fifo.pyx b/av/audio/fifo.pyx index 9b1380270..2ceb55f77 100644 --- a/av/audio/fifo.pyx +++ b/av/audio/fifo.pyx @@ -176,13 +176,19 @@ cdef class AudioFifo: @property def format(self): """The :class:`.AudioFormat` of this FIFO.""" + if not self.ptr: + raise AttributeError(f"'{__name__}.AudioFifo' object has no attribute 'format'") return self.template.format @property def layout(self): """The :class:`.AudioLayout` of this FIFO.""" + if not self.ptr: + raise AttributeError(f"'{__name__}.AudioFifo' object has no attribute 'layout'") return self.template.layout @property def sample_rate(self): + if not self.ptr: + raise AttributeError(f"'{__name__}.AudioFifo' object has no attribute 'sample_rate'") return self.template.sample_rate @property From 10b93f95c5422c7608418b322fa698bc651f50e7 Mon Sep 17 00:00:00 2001 From: Mattia Procopio Date: Tue, 10 Jun 2025 08:51:08 +0200 Subject: [PATCH 529/846] Try another port until one is nnot used --- tests/test_timeout.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/tests/test_timeout.py b/tests/test_timeout.py index 0640237f9..fa1d36d9a 100644 --- a/tests/test_timeout.py +++ b/tests/test_timeout.py @@ -7,7 +7,6 @@ from .common import TestCase, fate_suite -PORT = 8002 CONTENT = open(fate_suite("mpeg2/mpeg2_field_encoding.ts"), "rb").read() # Needs to be long enough for all host OSes to deal. TIMEOUT = 0.25 @@ -34,8 +33,17 @@ def log_message(self, format: object, *args: object) -> None: class TestTimeout(TestCase): + port = 8002 + def setUp(cls) -> None: - cls._server = HttpServer(("", PORT), SlowRequestHandler) + while True: + try: + cls._server = HttpServer(("", cls.port), SlowRequestHandler) + except OSError: + cls.port += 1 + else: + break + cls._thread = threading.Thread(target=cls._server.handle_request) cls._thread.daemon = True # Make sure the tests will exit. cls._thread.start() @@ -46,14 +54,16 @@ def tearDown(cls) -> None: def test_no_timeout(self) -> None: start = time.time() - av.open(f"http://localhost:{PORT}/mpeg2_field_encoding.ts") + av.open(f"http://localhost:{self.port}/mpeg2_field_encoding.ts") duration = time.time() - start assert duration > DELAY def test_open_timeout(self) -> None: with self.assertRaises(av.ExitError): start = time.time() - av.open(f"http://localhost:{PORT}/mpeg2_field_encoding.ts", timeout=TIMEOUT) + av.open( + f"http://localhost:{self.port}/mpeg2_field_encoding.ts", timeout=TIMEOUT + ) duration = time.time() - start assert duration < DELAY @@ -62,7 +72,7 @@ def test_open_timeout_2(self) -> None: with self.assertRaises(av.ExitError): start = time.time() av.open( - f"http://localhost:{PORT}/mpeg2_field_encoding.ts", + f"http://localhost:{self.port}/mpeg2_field_encoding.ts", timeout=(TIMEOUT, None), ) From 506f6ac4d5621e8d9ef39b32acd94ac0e6c3ad32 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 18 Jun 2025 21:01:08 -0400 Subject: [PATCH 530/846] Make SubtitleStream.decode() return list of subs.. Make `SubtitleStream.decode()` return list of subtitles directly, without the intermediate `SubtitleSet`. --- CHANGELOG.rst | 7 + av/subtitles/codeccontext.pxd | 3 +- av/subtitles/codeccontext.py | 55 +++++++ av/subtitles/codeccontext.pyi | 3 + av/subtitles/codeccontext.pyx | 23 --- av/subtitles/stream.py | 23 +++ av/subtitles/stream.pyi | 7 +- av/subtitles/stream.pyx | 23 --- av/subtitles/{subtitle.pyx => subtitle.py} | 163 ++++++++++++--------- tests/test_subtitles.py | 43 +++--- 10 files changed, 215 insertions(+), 135 deletions(-) create mode 100644 av/subtitles/codeccontext.py delete mode 100644 av/subtitles/codeccontext.pyx create mode 100644 av/subtitles/stream.py delete mode 100644 av/subtitles/stream.pyx rename av/subtitles/{subtitle.pyx => subtitle.py} (51%) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a340bb088..3a69a86ab 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,13 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. +v15.0.0 (Unreleased) +-------------------- + +Major: + +- Make ``SubtitleStream.decode()`` return the list of subtitles directly, without the intermediate ``SubtitleSet``. + v14.4.0 ------- diff --git a/av/subtitles/codeccontext.pxd b/av/subtitles/codeccontext.pxd index 42141aa4f..c94744e45 100644 --- a/av/subtitles/codeccontext.pxd +++ b/av/subtitles/codeccontext.pxd @@ -1,5 +1,6 @@ from av.codec.context cimport CodecContext +from av.packet cimport Packet cdef class SubtitleCodecContext(CodecContext): - pass + cpdef decode2(self, Packet packet) diff --git a/av/subtitles/codeccontext.py b/av/subtitles/codeccontext.py new file mode 100644 index 000000000..0b3dda063 --- /dev/null +++ b/av/subtitles/codeccontext.py @@ -0,0 +1,55 @@ +import cython +from cython.cimports import libav as lib +from cython.cimports.av.error import err_check +from cython.cimports.av.packet import Packet +from cython.cimports.av.subtitles.subtitle import SubtitleProxy, SubtitleSet + + +@cython.cclass +class SubtitleCodecContext(CodecContext): + @cython.cfunc + def _send_packet_and_recv(self, packet: Packet | None): + if packet is None: + raise RuntimeError("packet cannot be None") + + proxy: SubtitleProxy = SubtitleProxy() + got_frame: cython.int = 0 + + err_check( + lib.avcodec_decode_subtitle2( + self.ptr, + cython.address(proxy.struct), + cython.address(got_frame), + packet.ptr, + ) + ) + + if got_frame: + return SubtitleSet(proxy) + return [] + + @cython.ccall + def decode2(self, packet: Packet): + """ + Returns SubtitleSet if you really need it. + """ + if not self.codec.ptr: + raise ValueError("cannot decode unknown codec") + + self.open(strict=False) + + proxy: SubtitleProxy = SubtitleProxy() + got_frame: cython.int = 0 + + err_check( + lib.avcodec_decode_subtitle2( + self.ptr, + cython.address(proxy.struct), + cython.address(got_frame), + packet.ptr, + ) + ) + + if got_frame: + return SubtitleSet(proxy) + return None diff --git a/av/subtitles/codeccontext.pyi b/av/subtitles/codeccontext.pyi index 0762c19f0..90c700935 100644 --- a/av/subtitles/codeccontext.pyi +++ b/av/subtitles/codeccontext.pyi @@ -1,6 +1,9 @@ from typing import Literal from av.codec.context import CodecContext +from av.packet import Packet +from av.subtitles.subtitle import SubtitleSet class SubtitleCodecContext(CodecContext): type: Literal["subtitle"] + def decode2(self, packet: Packet) -> SubtitleSet | None: ... diff --git a/av/subtitles/codeccontext.pyx b/av/subtitles/codeccontext.pyx deleted file mode 100644 index c0712c92c..000000000 --- a/av/subtitles/codeccontext.pyx +++ /dev/null @@ -1,23 +0,0 @@ -cimport libav as lib - -from av.error cimport err_check -from av.packet cimport Packet -from av.subtitles.subtitle cimport SubtitleProxy, SubtitleSet - - -cdef class SubtitleCodecContext(CodecContext): - cdef _send_packet_and_recv(self, Packet packet): - if packet is None: - raise RuntimeError("packet cannot be None") - - cdef SubtitleProxy proxy = SubtitleProxy() - cdef int got_frame = 0 - - err_check( - lib.avcodec_decode_subtitle2(self.ptr, &proxy.struct, &got_frame, packet.ptr) - ) - - if got_frame: - return [SubtitleSet(proxy)] - else: - return [] diff --git a/av/subtitles/stream.py b/av/subtitles/stream.py new file mode 100644 index 000000000..525440e9d --- /dev/null +++ b/av/subtitles/stream.py @@ -0,0 +1,23 @@ +import cython +from cython.cimports.av.packet import Packet +from cython.cimports.av.stream import Stream + + +@cython.cclass +class SubtitleStream(Stream): + def __getattr__(self, name): + return getattr(self.codec_context, name) + + @cython.ccall + def decode(self, packet: Packet | None = None): + """ + Decode a :class:`.Packet` and returns a subtitle object. + + :rtype: list[AssSubtitle] | list[BitmapSubtitle] + + .. seealso:: This is a passthrough to :meth:`.CodecContext.decode`. + """ + if not packet: + packet = Packet() + + return self.codec_context.decode(packet) diff --git a/av/subtitles/stream.pyi b/av/subtitles/stream.pyi index cb1ac34a2..ac8083802 100644 --- a/av/subtitles/stream.pyi +++ b/av/subtitles/stream.pyi @@ -1,6 +1,9 @@ from av.packet import Packet from av.stream import Stream -from av.subtitles.subtitle import SubtitleSet +from av.subtitles.subtitle import AssSubtitle, BitmapSubtitle, SubtitleSet class SubtitleStream(Stream): - def decode(self, packet: Packet | None = None) -> list[SubtitleSet]: ... + def decode( + self, packet: Packet | None = None + ) -> list[AssSubtitle] | list[BitmapSubtitle]: ... + def decode2(self, packet: Packet) -> SubtitleSet | None: ... diff --git a/av/subtitles/stream.pyx b/av/subtitles/stream.pyx deleted file mode 100644 index 9f90b9871..000000000 --- a/av/subtitles/stream.pyx +++ /dev/null @@ -1,23 +0,0 @@ -from av.packet cimport Packet -from av.stream cimport Stream - - -cdef class SubtitleStream(Stream): - """ - A :class:`SubtitleStream` can contain many :class:`SubtitleSet` objects accessible via decoding. - """ - def __getattr__(self, name): - return getattr(self.codec_context, name) - - cpdef decode(self, Packet packet=None): - """ - Decode a :class:`.Packet` and return a list of :class:`.SubtitleSet`. - - :rtype: list[SubtitleSet] - - .. seealso:: This is a passthrough to :meth:`.CodecContext.decode`. - """ - if not packet: - packet = Packet() - - return self.codec_context.decode(packet) diff --git a/av/subtitles/subtitle.pyx b/av/subtitles/subtitle.py similarity index 51% rename from av/subtitles/subtitle.pyx rename to av/subtitles/subtitle.py index a713daa22..1acadf0b5 100644 --- a/av/subtitles/subtitle.pyx +++ b/av/subtitles/subtitle.py @@ -1,34 +1,49 @@ -from cpython cimport PyBuffer_FillInfo +import cython +from cython.cimports.cpython import PyBuffer_FillInfo, PyBytes_FromString +from cython.cimports.libc.stdint import uint64_t -cdef extern from "Python.h": - bytes PyBytes_FromString(char*) - - -cdef class SubtitleProxy: +@cython.cclass +class SubtitleProxy: def __dealloc__(self): - lib.avsubtitle_free(&self.struct) + lib.avsubtitle_free(cython.address(self.struct)) -cdef class SubtitleSet: +@cython.cclass +class SubtitleSet: """ A :class:`SubtitleSet` can contain many :class:`Subtitle` objects. + + Wraps :ffmpeg:`AVSubtitle`. """ - def __cinit__(self, SubtitleProxy proxy): + + def __cinit__(self, proxy: SubtitleProxy): self.proxy = proxy - self.rects = tuple(build_subtitle(self, i) for i in range(self.proxy.struct.num_rects)) + self.rects = tuple( + build_subtitle(self, i) for i in range(self.proxy.struct.num_rects) + ) def __repr__(self): - return f"<{self.__class__.__module__}.{self.__class__.__name__} at 0x{id(self):x}>" + return ( + f"<{self.__class__.__module__}.{self.__class__.__name__} at 0x{id(self):x}>" + ) @property - def format(self): return self.proxy.struct.format + def format(self): + return self.proxy.struct.format + @property - def start_display_time(self): return self.proxy.struct.start_display_time + def start_display_time(self): + return self.proxy.struct.start_display_time + @property - def end_display_time(self): return self.proxy.struct.end_display_time + def end_display_time(self): + return self.proxy.struct.end_display_time + @property - def pts(self): return self.proxy.struct.pts + def pts(self): + """Same as packet pts, in av.time_base.""" + return self.proxy.struct.pts def __len__(self): return len(self.rects) @@ -40,33 +55,37 @@ def __getitem__(self, i): return self.rects[i] -cdef Subtitle build_subtitle(SubtitleSet subtitle, int index): +@cython.cfunc +def build_subtitle(subtitle: SubtitleSet, index: cython.int) -> Subtitle: """Build an av.Stream for an existing AVStream. - The AVStream MUST be fully constructed and ready for use before this is - called. - + The AVStream MUST be fully constructed and ready for use before this is called. """ - - if index < 0 or index >= subtitle.proxy.struct.num_rects: + if index < 0 or cython.cast(cython.uint, index) >= subtitle.proxy.struct.num_rects: raise ValueError("subtitle rect index out of range") - cdef lib.AVSubtitleRect *ptr = subtitle.proxy.struct.rects[index] + + ptr: cython.pointer[lib.AVSubtitleRect] = subtitle.proxy.struct.rects[index] if ptr.type == lib.SUBTITLE_BITMAP: return BitmapSubtitle(subtitle, index) - elif ptr.type == lib.SUBTITLE_ASS or ptr.type == lib.SUBTITLE_TEXT: + if ptr.type == lib.SUBTITLE_ASS or ptr.type == lib.SUBTITLE_TEXT: return AssSubtitle(subtitle, index) - else: - raise ValueError("unknown subtitle type %r" % ptr.type) + + raise ValueError("unknown subtitle type %r" % ptr.type) -cdef class Subtitle: +@cython.cclass +class Subtitle: """ An abstract base class for each concrete type of subtitle. Wraps :ffmpeg:`AVSubtitleRect` """ - def __cinit__(self, SubtitleSet subtitle, int index): - if index < 0 or index >= subtitle.proxy.struct.num_rects: + + def __cinit__(self, subtitle: SubtitleSet, index: cython.int): + if ( + index < 0 + or cython.cast(cython.uint, index) >= subtitle.proxy.struct.num_rects + ): raise ValueError("subtitle rect index out of range") self.proxy = subtitle.proxy self.ptr = self.proxy.struct.rects[index] @@ -83,15 +102,14 @@ def __cinit__(self, SubtitleSet subtitle, int index): raise ValueError(f"unknown subtitle type {self.ptr.type!r}") def __repr__(self): - return f"<{self.__class__.__module__}.{self.__class__.__name__} at 0x{id(self):x}>" + return f"" -cdef class BitmapSubtitle(Subtitle): - def __cinit__(self, SubtitleSet subtitle, int index): +@cython.cclass +class BitmapSubtitle(Subtitle): + def __cinit__(self, subtitle: SubtitleSet, index: cython.int): self.planes = tuple( - BitmapSubtitlePlane(self, i) - for i in range(4) - if self.ptr.linesize[i] + BitmapSubtitlePlane(self, i) for i in range(4) if self.ptr.linesize[i] ) def __repr__(self): @@ -101,15 +119,24 @@ def __repr__(self): ) @property - def x(self): return self.ptr.x + def x(self): + return self.ptr.x + @property - def y(self): return self.ptr.y + def y(self): + return self.ptr.y + @property - def width(self): return self.ptr.w + def width(self): + return self.ptr.w + @property - def height(self): return self.ptr.h + def height(self): + return self.ptr.h + @property - def nb_colors(self): return self.ptr.nb_colors + def nb_colors(self): + return self.ptr.nb_colors def __len__(self): return len(self.planes) @@ -121,8 +148,9 @@ def __getitem__(self, i): return self.planes[i] -cdef class BitmapSubtitlePlane: - def __cinit__(self, BitmapSubtitle subtitle, int index): +@cython.cclass +class BitmapSubtitlePlane: + def __cinit__(self, subtitle: BitmapSubtitle, index: cython.int): if index >= 4: raise ValueError("BitmapSubtitles have only 4 planes") if not subtitle.ptr.linesize[index]: @@ -131,29 +159,28 @@ def __cinit__(self, BitmapSubtitle subtitle, int index): self.subtitle = subtitle self.index = index self.buffer_size = subtitle.ptr.w * subtitle.ptr.h - self._buffer = subtitle.ptr.data[index] + self._buffer = cython.cast(cython.p_void, subtitle.ptr.data[index]) # New-style buffer support. - def __getbuffer__(self, Py_buffer *view, int flags): + def __getbuffer__(self, view: cython.pointer[Py_buffer], flags: cython.int): PyBuffer_FillInfo(view, self, self._buffer, self.buffer_size, 0, flags) -cdef class AssSubtitle(Subtitle): +@cython.cclass +class AssSubtitle(Subtitle): """ Represents an ASS/Text subtitle format, as opposed to a bitmap Subtitle format. """ + def __repr__(self): - return ( - f"<{self.__class__.__module__}.{self.__class__.__name__} " - f"{self.text!r} at 0x{id(self):x}>" - ) + return f"" @property def ass(self): """ Returns the subtitle in the ASS/SSA format. Used by the vast majority of subtitle formats. """ - if self.ptr.ass is not NULL: + if self.ptr.ass is not cython.NULL: return PyBytes_FromString(self.ptr.ass) return b"" @@ -162,42 +189,44 @@ def dialogue(self): """ Extract the dialogue from the ass format. Strip comments. """ - comma_count = 0 - i = 0 - cdef bytes ass_text = self.ass - cdef bytes result = b"" - - while comma_count < 8 and i < len(ass_text): - if bytes([ass_text[i]]) == b",": + comma_count: cython.short = 0 + i: uint64_t = 0 + state: cython.bint = False + ass_text: bytes = self.ass + char, next_char = cython.declare(cython.char) + result: bytearray = bytearray() + text_len: cython.Py_ssize_t = len(ass_text) + + while comma_count < 8 and i < text_len: + if ass_text[i] == b","[0]: comma_count += 1 i += 1 - state = False - while i < len(ass_text): - char = bytes([ass_text[i]]) - next_char = b"" if i + 1 >= len(ass_text) else bytes([ass_text[i + 1]]) + while i < text_len: + char = ass_text[i] + next_char = 0 if i + 1 >= text_len else ass_text[i + 1] - if char == b"\\" and next_char == b"N": - result += b"\n" + if char == b"\\"[0] and next_char == b"N"[0]: + result.append(b"\n"[0]) i += 2 continue if not state: - if char == b"{" and next_char != b"\\": + if char == b"{"[0] and next_char != b"\\"[0]: state = True else: - result += char - elif char == b"}": + result.append(char) + elif char == b"}"[0]: state = False i += 1 - return result + return bytes(result) @property def text(self): """ Rarely used attribute. You're probably looking for dialogue. """ - if self.ptr.text is not NULL: + if self.ptr.text is not cython.NULL: return PyBytes_FromString(self.ptr.text) return b"" diff --git a/tests/test_subtitles.py b/tests/test_subtitles.py index e2b5ab512..66e6f9cfa 100644 --- a/tests/test_subtitles.py +++ b/tests/test_subtitles.py @@ -1,3 +1,5 @@ +from typing import cast + import av from av.subtitles.subtitle import AssSubtitle, BitmapSubtitle @@ -8,43 +10,46 @@ class TestSubtitle: def test_movtext(self) -> None: path = fate_suite("sub/MovText_capability_tester.mp4") - subs = [] + subs: list[AssSubtitle] = [] with av.open(path) as container: for packet in container.demux(): - subs.extend(packet.decode()) + subs.extend(cast(list[AssSubtitle], packet.decode())) assert len(subs) == 3 - subset = subs[0] - assert subset.format == 1 - assert subset.pts == 970000 - assert subset.start_display_time == 0 - assert subset.end_display_time == 1570 - - sub = subset[0] + sub = subs[0] assert isinstance(sub, AssSubtitle) assert sub.type == b"ass" assert sub.text == b"" assert sub.ass == b"0,0,Default,,0,0,0,,- Test 1.\\N- Test 2." assert sub.dialogue == b"- Test 1.\n- Test 2." + def test_subset(self) -> None: + path = fate_suite("sub/MovText_capability_tester.mp4") + + with av.open(path) as container: + subs = container.streams.subtitles[0] + for packet in container.demux(subs): + subset = subs.decode2(packet) + if subset is not None: + assert not isinstance(subset, av.subtitles.subtitle.Subtitle) + assert isinstance(subset, av.subtitles.subtitle.SubtitleSet) + assert subset.format == 1 + assert hasattr(subset, "pts") + assert subset.start_display_time == 0 + assert hasattr(subset, "end_display_time") + def test_vobsub(self) -> None: path = fate_suite("sub/vobsub.sub") - subs = [] + subs: list[BitmapSubtitle] = [] with av.open(path) as container: for packet in container.demux(): - subs.extend(packet.decode()) + subs.extend(cast(list[BitmapSubtitle], packet.decode())) assert len(subs) == 43 - subset = subs[0] - assert subset.format == 0 - assert subset.pts == 132499044 - assert subset.start_display_time == 0 - assert subset.end_display_time == 4960 - - sub = subset[0] + sub = subs[0] assert isinstance(sub, BitmapSubtitle) assert sub.type == b"bitmap" assert sub.x == 259 @@ -60,7 +65,7 @@ def test_vobsub(self) -> None: def test_subtitle_flush(self) -> None: path = fate_suite("sub/MovText_capability_tester.mp4") - subs = [] + subs: list[object] = [] with av.open(path) as container: stream = container.streams.subtitles[0] for packet in container.demux(stream): From 9dd293536d2da0f6380b7f44e4afe91408ab3d02 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 23 Jun 2025 19:55:34 -0400 Subject: [PATCH 531/846] Make audio/format pure --- av/audio/format.pxd | 4 ---- av/audio/{format.pyx => format.py} | 25 ++++++++++++------------- 2 files changed, 12 insertions(+), 17 deletions(-) rename av/audio/{format.pyx => format.py} (84%) diff --git a/av/audio/format.pxd b/av/audio/format.pxd index 4160aa85b..c4d4bc552 100644 --- a/av/audio/format.pxd +++ b/av/audio/format.pxd @@ -2,10 +2,6 @@ cimport libav as lib cdef class AudioFormat: - cdef lib.AVSampleFormat sample_fmt - cdef _init(self, lib.AVSampleFormat sample_fmt) - - cdef AudioFormat get_audio_format(lib.AVSampleFormat format) diff --git a/av/audio/format.pyx b/av/audio/format.py similarity index 84% rename from av/audio/format.pyx rename to av/audio/format.py index c67f2fb9c..7d5de1ad9 100644 --- a/av/audio/format.pyx +++ b/av/audio/format.py @@ -1,41 +1,40 @@ import sys +import cython -cdef str container_format_postfix = "le" if sys.byteorder == "little" else "be" +container_format_postfix: str = "le" if sys.byteorder == "little" else "be" +_cinit_bypass_sentinel = object() -cdef object _cinit_bypass_sentinel - -cdef AudioFormat get_audio_format(lib.AVSampleFormat c_format): +@cython.cfunc +def get_audio_format(c_format: lib.AVSampleFormat) -> AudioFormat: """Get an AudioFormat without going through a string.""" if c_format < 0: return None - cdef AudioFormat format = AudioFormat.__new__(AudioFormat, _cinit_bypass_sentinel) - format._init(c_format) + format: AudioFormat = AudioFormat(_cinit_bypass_sentinel) + format.sample_fmt = c_format return format -cdef class AudioFormat: +@cython.cclass +class AudioFormat: """Descriptor of audio formats.""" def __cinit__(self, name): if name is _cinit_bypass_sentinel: return - cdef lib.AVSampleFormat sample_fmt + sample_fmt: lib.AVSampleFormat if isinstance(name, AudioFormat): - sample_fmt = (name).sample_fmt + sample_fmt = cython.cast(AudioFormat, name).sample_fmt else: sample_fmt = lib.av_get_sample_fmt(name) if sample_fmt < 0: raise ValueError(f"Not a sample format: {name!r}") - self._init(sample_fmt) - - cdef _init(self, lib.AVSampleFormat sample_fmt): self.sample_fmt = sample_fmt def __repr__(self): @@ -49,7 +48,7 @@ def name(self): 's16p' """ - return lib.av_get_sample_fmt_name(self.sample_fmt) + return lib.av_get_sample_fmt_name(self.sample_fmt) @property def bytes(self): From 7b2ee3235c382a7e7855f9b00bb32530a57eb850 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 23 Jun 2025 20:11:36 -0400 Subject: [PATCH 532/846] Make audio/frame pure --- av/audio/frame.pyi | 8 +-- av/audio/frame.pyx | 120 ++++++++++++++++++++++++++------------------- 2 files changed, 74 insertions(+), 54 deletions(-) diff --git a/av/audio/frame.pyi b/av/audio/frame.pyi index 7f61e4e6d..6aeb86b4d 100644 --- a/av/audio/frame.pyi +++ b/av/audio/frame.pyi @@ -35,13 +35,15 @@ class AudioFrame(Frame): def __init__( self, - format: str = "s16", - layout: str = "stereo", + format: AudioFormat | str = "s16", + layout: AudioLayout | str = "stereo", samples: int = 0, align: int = 1, ) -> None: ... @staticmethod def from_ndarray( - array: _SupportedNDarray, format: str = "s16", layout: str = "stereo" + array: _SupportedNDarray, + format: AudioFormat | str = "s16", + layout: AudioLayout | str = "stereo", ) -> AudioFrame: ... def to_ndarray(self) -> _SupportedNDarray: ... diff --git a/av/audio/frame.pyx b/av/audio/frame.pyx index 14356cb4e..cf3ab5934 100644 --- a/av/audio/frame.pyx +++ b/av/audio/frame.pyx @@ -1,11 +1,16 @@ -from av.audio.format cimport get_audio_format -from av.audio.layout cimport get_audio_layout -from av.audio.plane cimport AudioPlane -from av.error cimport err_check -from av.utils cimport check_ndarray +import cython +from cython.cimports.av.audio.format import get_audio_format +from cython.cimports.av.audio.layout import get_audio_layout +from cython.cimports.av.audio.plane import AudioPlane +from cython.cimports.av.error import err_check +from cython.cimports.av.utils import check_ndarray +_cinit_bypass_sentinel = object() -cdef object _cinit_bypass_sentinel + +@cython.cfunc +def alloc_audio_frame() -> AudioFrame: + return AudioFrame(_cinit_bypass_sentinel) format_dtypes = { @@ -22,30 +27,28 @@ format_dtypes = { } -cdef AudioFrame alloc_audio_frame(): - """Get a mostly uninitialized AudioFrame. - - You MUST call AudioFrame._init(...) or AudioFrame._init_user_attributes() - before exposing to the user. - - """ - return AudioFrame.__new__(AudioFrame, _cinit_bypass_sentinel) - - -cdef class AudioFrame(Frame): +@cython.cclass +class AudioFrame(Frame): """A frame of audio.""" def __cinit__(self, format="s16", layout="stereo", samples=0, align=1): if format is _cinit_bypass_sentinel: return - cdef AudioFormat cy_format = AudioFormat(format) - cdef AudioLayout cy_layout = AudioLayout(layout) + cy_format: AudioFormat = AudioFormat(format) + cy_layout: AudioLayout = AudioLayout(layout) self._init(cy_format.sample_fmt, cy_layout.layout, samples, align) - cdef _init(self, lib.AVSampleFormat format, lib.AVChannelLayout layout, unsigned int nb_samples, unsigned int align): + @cython.cfunc + def _init( + self, + format: lib.AVSampleFormat, + layout: lib.AVChannelLayout, + nb_samples: cython.uint, + align: cython.uint, + ): self.ptr.nb_samples = nb_samples - self.ptr.format = format + self.ptr.format = format self.ptr.ch_layout = layout # Sometimes this is called twice. Oh well. @@ -53,41 +56,44 @@ cdef class AudioFrame(Frame): if self.layout.nb_channels != 0 and nb_samples: # Cleanup the old buffer. - lib.av_freep(&self._buffer) + lib.av_freep(cython.address(self._buffer)) # Get a new one. - self._buffer_size = err_check(lib.av_samples_get_buffer_size( - NULL, - self.layout.nb_channels, - nb_samples, - format, - align - )) - self._buffer = lib.av_malloc(self._buffer_size) + self._buffer_size = err_check( + lib.av_samples_get_buffer_size( + cython.NULL, self.layout.nb_channels, nb_samples, format, align + ) + ) + self._buffer = cython.cast( + cython.pointer[uint8_t], lib.av_malloc(self._buffer_size) + ) if not self._buffer: raise MemoryError("cannot allocate AudioFrame buffer") # Connect the data pointers to the buffer. - err_check(lib.avcodec_fill_audio_frame( - self.ptr, - self.layout.nb_channels, - self.ptr.format, - self._buffer, - self._buffer_size, - align - )) + err_check( + lib.avcodec_fill_audio_frame( + self.ptr, + self.layout.nb_channels, + cython.cast(lib.AVSampleFormat, self.ptr.format), + self._buffer, + self._buffer_size, + align, + ) + ) def __dealloc__(self): - lib.av_freep(&self._buffer) + lib.av_freep(cython.address(self._buffer)) - cdef _init_user_attributes(self): + @cython.cfunc + def _init_user_attributes(self): self.layout = get_audio_layout(self.ptr.ch_layout) - self.format = get_audio_format(self.ptr.format) + self.format = get_audio_format(cython.cast(lib.AVSampleFormat, self.ptr.format)) def __repr__(self): return ( - f"" ) @staticmethod @@ -97,6 +103,10 @@ cdef class AudioFrame(Frame): """ import numpy as np + py_format = format if isinstance(format, AudioFormat) else AudioFormat(format) + py_layout = layout if isinstance(layout, AudioLayout) else AudioLayout(layout) + format = py_format.name + # map avcodec type to numpy type try: dtype = np.dtype(format_dtypes[format]) @@ -106,18 +116,22 @@ cdef class AudioFrame(Frame): ) # check input format - nb_channels = AudioLayout(layout).nb_channels + nb_channels = py_layout.nb_channels check_ndarray(array, dtype, 2) - if AudioFormat(format).is_planar: + if py_format.is_planar: if array.shape[0] != nb_channels: - raise ValueError(f"Expected planar `array.shape[0]` to equal `{nb_channels}` but got `{array.shape[0]}`") + raise ValueError( + f"Expected planar `array.shape[0]` to equal `{nb_channels}` but got `{array.shape[0]}`" + ) samples = array.shape[1] else: if array.shape[0] != 1: - raise ValueError(f"Expected packed `array.shape[0]` to equal `1` but got `{array.shape[0]}`") + raise ValueError( + f"Expected packed `array.shape[0]` to equal `1` but got `{array.shape[0]}`" + ) samples = array.shape[1] // nb_channels - frame = AudioFrame(format=format, layout=layout, samples=samples) + frame = AudioFrame(format=py_format, layout=py_layout, samples=samples) for i, plane in enumerate(frame.planes): plane.update(array[i, :]) return frame @@ -129,7 +143,7 @@ cdef class AudioFrame(Frame): :type: tuple """ - cdef int plane_count = 0 + plane_count: cython.int = 0 while self.ptr.extended_data[plane_count]: plane_count += 1 @@ -177,11 +191,15 @@ cdef class AudioFrame(Frame): try: dtype = np.dtype(format_dtypes[self.format.name]) except KeyError: - raise ValueError(f"Conversion from {self.format.name!r} format to numpy array is not supported.") + raise ValueError( + f"Conversion from {self.format.name!r} format to numpy array is not supported." + ) if self.format.is_planar: count = self.samples else: count = self.samples * self.layout.nb_channels - return np.vstack([np.frombuffer(x, dtype=dtype, count=count) for x in self.planes]) + return np.vstack( + [np.frombuffer(x, dtype=dtype, count=count) for x in self.planes] + ) From ef999f7c2710afa9a290a45e2850178950388881 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 23 Jun 2025 20:22:51 -0400 Subject: [PATCH 533/846] Make audio/resampler pure --- av/audio/{frame.pyx => frame.py} | 0 av/audio/resampler.pxd | 5 +-- av/audio/{resampler.pyx => resampler.py} | 53 +++++++++++++----------- 3 files changed, 29 insertions(+), 29 deletions(-) rename av/audio/{frame.pyx => frame.py} (100%) rename av/audio/{resampler.pyx => resampler.py} (76%) diff --git a/av/audio/frame.pyx b/av/audio/frame.py similarity index 100% rename from av/audio/frame.pyx rename to av/audio/frame.py diff --git a/av/audio/resampler.pxd b/av/audio/resampler.pxd index d3601403d..20b74186e 100644 --- a/av/audio/resampler.pxd +++ b/av/audio/resampler.pxd @@ -5,9 +5,7 @@ from av.filter.graph cimport Graph cdef class AudioResampler: - cdef readonly bint is_passthrough - cdef AudioFrame template # Destination descriptors @@ -17,5 +15,4 @@ cdef class AudioResampler: cdef readonly unsigned int frame_size cdef Graph graph - - cpdef resample(self, AudioFrame) + cpdef list resample(self, AudioFrame) diff --git a/av/audio/resampler.pyx b/av/audio/resampler.py similarity index 76% rename from av/audio/resampler.pyx rename to av/audio/resampler.py index 69d790bad..d56ec06ed 100644 --- a/av/audio/resampler.pyx +++ b/av/audio/resampler.py @@ -1,12 +1,14 @@ -from av.filter.context cimport FilterContext +from errno import EAGAIN -import errno +import cython +from cython.cimports.av.filter.context import FilterContext +from cython.cimports.av.filter.graph import Graph -import av.filter +from av.error import FFmpegError -cdef class AudioResampler: - +@cython.cclass +class AudioResampler: """AudioResampler(format=None, layout=None, rate=None) :param AudioFormat format: The target format, or string that parses to one @@ -14,23 +16,23 @@ :param AudioLayout layout: The target layout, or an int/string that parses to one (e.g. ``"stereo"``). :param int rate: The target sample rate. - - """ def __cinit__(self, format=None, layout=None, rate=None, frame_size=None): if format is not None: - self.format = format if isinstance(format, AudioFormat) else AudioFormat(format) + self.format = ( + format if isinstance(format, AudioFormat) else AudioFormat(format) + ) if layout is not None: self.layout = AudioLayout(layout) - self.rate = int(rate) if rate else 0 + self.rate = int(rate) if rate else 0 self.frame_size = int(frame_size) if frame_size else 0 - self.graph = None - cpdef resample(self, AudioFrame frame): + @cython.ccall + def resample(self, frame: AudioFrame | None) -> list: """resample(frame) Convert the ``sample_rate``, ``channel_layout`` and/or ``format`` of @@ -60,30 +62,31 @@ def __cinit__(self, format=None, layout=None, rate=None, frame_size=None): # Check if we can passthrough or if there is actually work to do. if ( - frame.format.sample_fmt == self.format.sample_fmt and - frame.layout == self.layout and - frame.sample_rate == self.rate and - self.frame_size == 0 + frame.format.sample_fmt == self.format.sample_fmt + and frame.layout == self.layout + and frame.sample_rate == self.rate + and self.frame_size == 0 ): self.is_passthrough = True return [frame] # handle resampling with aformat filter # (similar to configure_output_audio_filter from ffmpeg) - self.graph = av.filter.Graph() + self.graph = Graph() extra_args = {} if frame.time_base is not None: - extra_args["time_base"] = str(frame.time_base) + extra_args["time_base"] = f"{frame.time_base}" + abuffer = self.graph.add( "abuffer", - sample_rate=str(frame.sample_rate), + sample_rate=f"{frame.sample_rate}", sample_fmt=AudioFormat(frame.format).name, channel_layout=frame.layout.name, **extra_args, ) aformat = self.graph.add( "aformat", - sample_rates=str(self.rate), + sample_rates=f"{self.rate}", sample_fmts=self.format.name, channel_layouts=self.layout.name, ) @@ -97,22 +100,22 @@ def __cinit__(self, format=None, layout=None, rate=None, frame_size=None): if frame is not None: if ( - frame.format.sample_fmt != self.template.format.sample_fmt or - frame.layout != self.template.layout or - frame.sample_rate != self.template.rate + frame.format.sample_fmt != self.template.format.sample_fmt + or frame.layout != self.template.layout + or frame.sample_rate != self.template.rate ): raise ValueError("Frame does not match AudioResampler setup.") self.graph.push(frame) - output = [] + output: list = [] while True: try: output.append(self.graph.pull()) except EOFError: break - except av.FFmpegError as e: - if e.errno != errno.EAGAIN: + except FFmpegError as e: + if e.errno != EAGAIN: raise break From 92ada7ced22fb8cfea102b541d5a5994a1b92d75 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 23 Jun 2025 20:33:00 -0400 Subject: [PATCH 534/846] Make audio/stream pure --- av/audio/{stream.pyx => stream.py} | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) rename av/audio/{stream.pyx => stream.py} (75%) diff --git a/av/audio/stream.pyx b/av/audio/stream.py similarity index 75% rename from av/audio/stream.pyx rename to av/audio/stream.py index 4d633edce..7c150c84b 100644 --- a/av/audio/stream.pyx +++ b/av/audio/stream.py @@ -1,9 +1,10 @@ -from av.packet cimport Packet +import cython +from cython.cimports.av.audio.frame import AudioFrame +from cython.cimports.av.packet import Packet -from .frame cimport AudioFrame - -cdef class AudioStream(Stream): +@cython.cclass +class AudioStream(Stream): def __repr__(self): form = self.format.name if self.format else None return ( @@ -14,7 +15,8 @@ def __repr__(self): def __getattr__(self, name): return getattr(self.codec_context, name) - cpdef encode(self, AudioFrame frame=None): + @cython.ccall + def encode(self, frame: AudioFrame | None = None): """ Encode an :class:`.AudioFrame` and return a list of :class:`.Packet`. @@ -24,14 +26,15 @@ def __getattr__(self, name): """ packets = self.codec_context.encode(frame) - cdef Packet packet + packet: Packet for packet in packets: packet._stream = self packet.ptr.stream_index = self.ptr.index return packets - cpdef decode(self, Packet packet=None): + @cython.ccall + def decode(self, packet: Packet | None = None): """ Decode a :class:`.Packet` and return a list of :class:`.AudioFrame`. From 5313c4104ed74350bc1c2b4751776cd177e6654a Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 23 Jun 2025 20:53:24 -0400 Subject: [PATCH 535/846] Make audio/plane pure --- av/audio/plane.pxd | 2 -- av/audio/plane.py | 13 +++++++++++++ av/audio/plane.pyx | 11 ----------- 3 files changed, 13 insertions(+), 13 deletions(-) create mode 100644 av/audio/plane.py delete mode 100644 av/audio/plane.pyx diff --git a/av/audio/plane.pxd b/av/audio/plane.pxd index 316c84031..de912ac22 100644 --- a/av/audio/plane.pxd +++ b/av/audio/plane.pxd @@ -2,7 +2,5 @@ from av.plane cimport Plane cdef class AudioPlane(Plane): - cdef readonly size_t buffer_size - cdef size_t _buffer_size(self) diff --git a/av/audio/plane.py b/av/audio/plane.py new file mode 100644 index 000000000..bdaf15708 --- /dev/null +++ b/av/audio/plane.py @@ -0,0 +1,13 @@ +import cython +from cython.cimports.av.audio.frame import AudioFrame + + +@cython.cclass +class AudioPlane(Plane): + def __cinit__(self, frame: AudioFrame, index: cython.int): + # Only the first linesize is ever populated, but it applies to every plane. + self.buffer_size = self.frame.ptr.linesize[0] + + @cython.cfunc + def _buffer_size(self) -> cython.size_t: + return self.buffer_size diff --git a/av/audio/plane.pyx b/av/audio/plane.pyx deleted file mode 100644 index 92c508cbd..000000000 --- a/av/audio/plane.pyx +++ /dev/null @@ -1,11 +0,0 @@ -from av.audio.frame cimport AudioFrame - - -cdef class AudioPlane(Plane): - - def __cinit__(self, AudioFrame frame, int index): - # Only the first linesize is ever populated, but it applies to every plane. - self.buffer_size = self.frame.ptr.linesize[0] - - cdef size_t _buffer_size(self): - return self.buffer_size From 44d8095f844f34498ab7b19d371fe2a391cdc8a4 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 23 Jun 2025 21:19:26 -0400 Subject: [PATCH 536/846] Make video/stream pure --- av/video/{stream.pyx => stream.py} | 60 ++++++++++++++++-------------- 1 file changed, 33 insertions(+), 27 deletions(-) rename av/video/{stream.pyx => stream.py} (65%) diff --git a/av/video/stream.pyx b/av/video/stream.py similarity index 65% rename from av/video/stream.pyx rename to av/video/stream.py index 383cc13a6..0a7032b09 100644 --- a/av/video/stream.pyx +++ b/av/video/stream.py @@ -1,12 +1,12 @@ -cimport libav as lib +import cython +from cython.cimports import libav as lib +from cython.cimports.av.packet import Packet +from cython.cimports.av.utils import avrational_to_fraction, to_avrational +from cython.cimports.av.video.frame import VideoFrame -from av.packet cimport Packet -from av.utils cimport avrational_to_fraction, to_avrational -from .frame cimport VideoFrame - - -cdef class VideoStream(Stream): +@cython.cclass +class VideoStream(Stream): def __repr__(self): return ( f"= 4.0 - cdef lib.AVRational val = lib.av_guess_frame_rate(NULL, self.ptr, NULL) - return avrational_to_fraction(&val) + val: lib.AVRational = lib.av_guess_frame_rate( + cython.NULL, self.ptr, cython.NULL + ) + return avrational_to_fraction(cython.address(val)) @property def sample_aspect_ratio(self): @@ -96,9 +98,11 @@ def sample_aspect_ratio(self): :type: fractions.Fraction | None """ - cdef lib.AVRational sar = lib.av_guess_sample_aspect_ratio(self.container.ptr, self.ptr, NULL) - return avrational_to_fraction(&sar) - + sar: lib.AVRational = lib.av_guess_sample_aspect_ratio( + self.container.ptr, self.ptr, cython.NULL + ) + return avrational_to_fraction(cython.address(sar)) + @property def display_aspect_ratio(self): """The guessed display aspect ratio (DAR) of this stream. @@ -107,11 +111,13 @@ def display_aspect_ratio(self): :type: fractions.Fraction | None """ - cdef lib.AVRational dar - + dar = cython.declare(lib.AVRational) lib.av_reduce( - &dar.num, &dar.den, + cython.address(dar.num), + cython.address(dar.den), self.format.width * self.sample_aspect_ratio.num, - self.format.height * self.sample_aspect_ratio.den, 1024*1024) + self.format.height * self.sample_aspect_ratio.den, + 1024 * 1024, + ) - return avrational_to_fraction(&dar) + return avrational_to_fraction(cython.address(dar)) From ae04e1efff870b969799aafa314ef95a0ab26c00 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 23 Jun 2025 22:24:04 -0400 Subject: [PATCH 537/846] video/frame: apply BasswoodAV changes This also applies "add GBRP, GBRAP, RGBA formats" by z-khan Co-authored-by: z-khan --- av/video/frame.pxd | 1 + av/video/{frame.pyx => frame.py} | 766 +++++++++++++++++++++++-------- av/video/frame.pyi | 9 +- tests/test_videoframe.py | 469 ++++++++++++------- 4 files changed, 886 insertions(+), 359 deletions(-) rename av/video/{frame.pyx => frame.py} (54%) diff --git a/av/video/frame.pxd b/av/video/frame.pxd index 779b23977..d352a7ab8 100644 --- a/av/video/frame.pxd +++ b/av/video/frame.pxd @@ -17,5 +17,6 @@ cdef class VideoFrame(Frame): cdef _init(self, lib.AVPixelFormat format, unsigned int width, unsigned int height) cdef _init_user_attributes(self) + cpdef save(self, object filepath) cdef VideoFrame alloc_video_frame() diff --git a/av/video/frame.pyx b/av/video/frame.py similarity index 54% rename from av/video/frame.pyx rename to av/video/frame.py index 04a14fd8b..bc4bf9154 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.py @@ -1,38 +1,115 @@ import sys from enum import IntEnum -from libc.stdint cimport uint8_t +import cython +from cython.cimports.av.error import err_check +from cython.cimports.av.sidedata.sidedata import get_display_rotation +from cython.cimports.av.utils import check_ndarray +from cython.cimports.av.video.format import get_pix_fmt, get_video_format +from cython.cimports.av.video.plane import VideoPlane +from cython.cimports.libc.stdint import uint8_t -from av.error cimport err_check -from av.sidedata.sidedata cimport get_display_rotation -from av.utils cimport check_ndarray -from av.video.format cimport get_pix_fmt, get_video_format -from av.video.plane cimport VideoPlane - - -cdef object _cinit_bypass_sentinel +_cinit_bypass_sentinel = object() # `pix_fmt`s supported by Frame.to_ndarray() and Frame.from_ndarray() supported_np_pix_fmts = { - "abgr", "argb", "bayer_bggr16be", "bayer_bggr16le", "bayer_bggr8", "bayer_gbrg16be", - "bayer_gbrg16le", "bayer_gbrg8", "bayer_grbg16be", "bayer_grbg16le", "bayer_grbg8", - "bayer_rggb16be", "bayer_rggb16le", "bayer_rggb8", "bgr24", "bgr48be", "bgr48le", "bgr8", "bgra", "bgra64be", "bgra64le", - "gbrapf32be", "gbrapf32le", "gbrp", "gbrp10be", "gbrp10le", "gbrp12be", "gbrp12le", - "gbrp14be", "gbrp14le", "gbrp16be", "gbrp16le", "gbrpf32be", "gbrpf32le", "gray", - "gray16be", "gray16le", "gray8", "grayf32be", "grayf32le", "nv12", "pal8", "rgb24", - "rgb48be", "rgb48le", "rgb8", "rgba", "rgba64be", "rgba64le", "yuv420p", - "yuv420p10le", "yuv422p10le", "yuv444p", "yuv444p16be", "yuv444p16le", "yuva444p16be", - "yuva444p16le", "yuvj420p", "yuvj444p", "yuyv422", + "abgr", + "argb", + "bayer_bggr16be", + "bayer_bggr16le", + "bayer_bggr8", + "bayer_gbrg16be", + "bayer_gbrg16le", + "bayer_gbrg8", + "bayer_grbg16be", + "bayer_grbg16le", + "bayer_grbg8", + "bayer_rggb16be", + "bayer_rggb16le", + "bayer_rggb8", + "bgr24", + "bgr48be", + "bgr48le", + "bgr8", + "bgra", + "bgra64be", + "bgra64le", + "gbrap", + "gbrap10be", + "gbrap10le", + "gbrap12be", + "gbrap12le", + "gbrap14be", + "gbrap14le", + "gbrap16be", + "gbrap16le", + "gbrapf32be", + "gbrapf32le", + "gbrp", + "gbrp10be", + "gbrp10le", + "gbrp12be", + "gbrp12le", + "gbrp14be", + "gbrp14le", + "gbrp16be", + "gbrp16le", + "gbrp9be", + "gbrp9le", + "gbrpf32be", + "gbrpf32le", + "gray", + "gray10be", + "gray10le", + "gray12be", + "gray12le", + "gray14be", + "gray14le", + "gray16be", + "gray16le", + "gray8", + "gray9be", + "gray9le", + "grayf32be", + "grayf32le", + "nv12", + "pal8", + "rgb24", + "rgb48be", + "rgb48le", + "rgb8", + "rgba", + "rgba64be", + "rgba64le", + "rgbaf16be", + "rgbaf16le", + "rgbaf32be", + "rgbaf32le", + "rgbf32be", + "rgbf32le", + "yuv420p", + "yuv422p10le", + "yuv444p", + "yuv444p16be", + "yuv444p16le", + "yuva444p16be", + "yuva444p16le", + "yuvj420p", + "yuvj444p", + "yuyv422", } -cdef VideoFrame alloc_video_frame(): + +@cython.cfunc +def alloc_video_frame() -> VideoFrame: """Get a mostly uninitialized VideoFrame. You MUST call VideoFrame._init(...) or VideoFrame._init_user_attributes() before exposing to the user. """ - return VideoFrame.__new__(VideoFrame, _cinit_bypass_sentinel) + return VideoFrame(_cinit_bypass_sentinel) + class PictureType(IntEnum): NONE = lib.AV_PICTURE_TYPE_NONE # Undefined @@ -44,23 +121,31 @@ class PictureType(IntEnum): SP = lib.AV_PICTURE_TYPE_SP # Switching predicted BI = lib.AV_PICTURE_TYPE_BI # BI type -cdef byteswap_array(array, bint big_endian): + +@cython.cfunc +def byteswap_array(array, big_endian: cython.bint): if (sys.byteorder == "big") != big_endian: return array.byteswap() - else: - return array - - -cdef copy_bytes_to_plane(img_bytes, VideoPlane plane, unsigned int bytes_per_pixel, bint flip_horizontal, bint flip_vertical): - cdef const uint8_t[:] i_buf = img_bytes - cdef size_t i_pos = 0 - cdef size_t i_stride = plane.width * bytes_per_pixel - - cdef uint8_t[:] o_buf = plane - cdef size_t o_pos = 0 - cdef size_t o_stride = abs(plane.line_size) - - cdef int start_row, end_row, step + return array + + +@cython.cfunc +def copy_bytes_to_plane( + img_bytes, + plane: VideoPlane, + bytes_per_pixel: cython.uint, + flip_horizontal: cython.bint, + flip_vertical: cython.bint, +): + i_buf: cython.const[uint8_t][:] = img_bytes + i_pos: cython.size_t = 0 + i_stride: cython.size_t = plane.width * bytes_per_pixel + + o_buf: uint8_t[:] = plane + o_pos: cython.size_t = 0 + o_stride: cython.size_t = abs(plane.line_size) + + start_row, end_row, step = cython.declare(cython.int) if flip_vertical: start_row = plane.height - 1 end_row = -1 @@ -70,56 +155,65 @@ class PictureType(IntEnum): end_row = plane.height step = 1 - cdef int i, j + i, j = cython.declare(cython.int) for row in range(start_row, end_row, step): i_pos = row * i_stride if flip_horizontal: for i in range(0, i_stride, bytes_per_pixel): for j in range(bytes_per_pixel): - o_buf[o_pos + i + j] = i_buf[i_pos + i_stride - i - bytes_per_pixel + j] + o_buf[o_pos + i + j] = i_buf[ + i_pos + i_stride - i - bytes_per_pixel + j + ] else: - o_buf[o_pos:o_pos + i_stride] = i_buf[i_pos:i_pos + i_stride] + o_buf[o_pos : o_pos + i_stride] = i_buf[i_pos : i_pos + i_stride] o_pos += o_stride -cdef copy_array_to_plane(array, VideoPlane plane, unsigned int bytes_per_pixel): - cdef bytes imgbytes = array.tobytes() +@cython.cfunc +def copy_array_to_plane(array, plane: VideoPlane, bytes_per_pixel: cython.uint): + imgbytes: bytes = array.tobytes() copy_bytes_to_plane(imgbytes, plane, bytes_per_pixel, False, False) -cdef useful_array(VideoPlane plane, unsigned int bytes_per_pixel=1, str dtype="uint8"): +@cython.cfunc +def useful_array( + plane: VideoPlane, bytes_per_pixel: cython.uint = 1, dtype: str = "uint8" +): """ Return the useful part of the VideoPlane as a single dimensional array. We are simply discarding any padding which was added for alignment. """ import numpy as np - cdef size_t total_line_size = abs(plane.line_size) - cdef size_t useful_line_size = plane.width * bytes_per_pixel + + total_line_size: cython.size_t = abs(plane.line_size) + useful_line_size: cython.size_t = plane.width * bytes_per_pixel arr = np.frombuffer(plane, np.uint8) if total_line_size != useful_line_size: arr = arr.reshape(-1, total_line_size)[:, 0:useful_line_size].reshape(-1) return arr.view(np.dtype(dtype)) -cdef check_ndarray_shape(object array, bint ok): +@cython.cfunc +def check_ndarray_shape(array: object, ok: cython.bint): if not ok: raise ValueError(f"Unexpected numpy array shape `{array.shape}`") -cdef class VideoFrame(Frame): +@cython.cclass +class VideoFrame(Frame): def __cinit__(self, width=0, height=0, format="yuv420p"): if width is _cinit_bypass_sentinel: return - cdef lib.AVPixelFormat c_format = get_pix_fmt(format) - + c_format: lib.AVPixelFormat = get_pix_fmt(format) self._init(c_format, width, height) - cdef _init(self, lib.AVPixelFormat format, unsigned int width, unsigned int height): - cdef int res = 0 + @cython.cfunc + def _init(self, format: lib.AVPixelFormat, width: cython.uint, height: cython.uint): + res: cython.int = 0 - with nogil: + with cython.nogil: self.ptr.width = width self.ptr.height = height self.ptr.format = format @@ -137,13 +231,18 @@ def __cinit__(self, width=0, height=0, format="yuv420p"): self._init_user_attributes() - cdef _init_user_attributes(self): - self.format = get_video_format(self.ptr.format, self.ptr.width, self.ptr.height) + @cython.cfunc + def _init_user_attributes(self): + self.format = get_video_format( + cython.cast(lib.AVPixelFormat, self.ptr.format), + self.ptr.width, + self.ptr.height, + ) def __dealloc__(self): # The `self._buffer` member is only set if *we* allocated the buffer in `_init`, # as opposed to a buffer allocated by a decoder. - lib.av_freep(&self._buffer) + lib.av_freep(cython.address(self._buffer)) # Let go of the reference from the numpy buffers if we made one self._np_buffer = None @@ -158,11 +257,10 @@ def planes(self): """ A tuple of :class:`.VideoPlane` objects. """ - # We need to detect which planes actually exist, but also contrain - # ourselves to the maximum plane count (as determined only by VideoFrames - # so far), in case the library implementation does not set the last - # plane to NULL. - cdef int max_plane_count = 0 + # We need to detect which planes actually exist, but also constrain ourselves to + # the maximum plane count (as determined only by VideoFrames so far), in case + # the library implementation does not set the last plane to NULL. + max_plane_count: cython.int = 0 for i in range(self.format.ptr.nb_components): count = self.format.ptr.comp[i].plane + 1 if max_plane_count < count: @@ -170,7 +268,7 @@ def planes(self): if self.format.name == "pal8": max_plane_count = 2 - cdef int plane_count = 0 + plane_count: cython.int = 0 while plane_count < max_plane_count and self.ptr.extended_data[plane_count]: plane_count += 1 return tuple([VideoPlane(self, i) for i in range(plane_count)]) @@ -267,6 +365,34 @@ def to_rgb(self, **kwargs): """ return self.reformat(format="rgb24", **kwargs) + @cython.ccall + def save(self, filepath: object): + """Save a VideoFrame as a JPG or PNG. + + :param filepath: str | Path + """ + is_jpg: cython.bint + + if filepath.endswith(".png"): + is_jpg = False + elif filepath.endswith(".jpg") or filepath.endswith(".jpeg"): + is_jpg = True + else: + raise ValueError("filepath must end with png or jpg.") + + encoder: str = "mjpeg" if is_jpg else "png" + pix_fmt: str = "yuvj420p" if is_jpg else "rgb24" + + from av.container.core import open + + with open(filepath, "w", options={"update": "1"}) as output: + output_stream = output.add_stream(encoder, pix_fmt=pix_fmt) + output_stream.width = self.width + output_stream.height = self.height + + output.mux(output_stream.encode(self.reformat(format=pix_fmt))) + output.mux(output_stream.encode(None)) + def to_image(self, **kwargs): """Get an RGB ``PIL.Image`` of this frame. @@ -276,23 +402,26 @@ def to_image(self, **kwargs): """ from PIL import Image - cdef VideoPlane plane = self.reformat(format="rgb24", **kwargs).planes[0] - cdef const uint8_t[:] i_buf = plane - cdef size_t i_pos = 0 - cdef size_t i_stride = plane.line_size + plane: VideoPlane = self.reformat(format="rgb24", **kwargs).planes[0] - cdef size_t o_pos = 0 - cdef size_t o_stride = plane.width * 3 - cdef size_t o_size = plane.height * o_stride - cdef bytearray o_buf = bytearray(o_size) + i_buf: cython.const[uint8_t][:] = plane + i_pos: cython.size_t = 0 + i_stride: cython.size_t = plane.line_size + + o_pos: cython.size_t = 0 + o_stride: cython.size_t = plane.width * 3 + o_size: cython.size_t = plane.height * o_stride + o_buf: bytearray = bytearray(o_size) while o_pos < o_size: - o_buf[o_pos:o_pos + o_stride] = i_buf[i_pos:i_pos + o_stride] + o_buf[o_pos : o_pos + o_stride] = i_buf[i_pos : i_pos + o_stride] i_pos += i_stride o_pos += o_stride - return Image.frombytes("RGB", (plane.width, plane.height), bytes(o_buf), "raw", "RGB", 0, 1) + return Image.frombytes( + "RGB", (plane.width, plane.height), bytes(o_buf), "raw", "RGB", 0, 1 + ) def to_ndarray(self, channel_last=False, **kwargs): """Get a numpy array of this frame. @@ -307,7 +436,7 @@ def to_ndarray(self, channel_last=False, **kwargs): .. note:: Numpy must be installed. - .. note:: For formats which return an array of ``uint16`` or ``float32``, + .. note:: For formats which return an array of ``uint16``, ``float16`` or ``float32``, the samples will be in the system's native byte order. .. note:: For ``pal8``, an ``(image, palette)`` tuple will be returned, @@ -316,14 +445,18 @@ def to_ndarray(self, channel_last=False, **kwargs): .. note:: For ``gbrp`` formats, channels are flipped to RGB order. """ - cdef VideoFrame frame = self.reformat(**kwargs) + frame: VideoFrame = self.reformat(**kwargs) import numpy as np # check size - if frame.format.name in {"yuv420p", "yuvj420p", "yuyv422", "yuv420p10le", "yuv422p10le"}: - assert frame.width % 2 == 0, "the width has to be even for this pixel format" - assert frame.height % 2 == 0, "the height has to be even for this pixel format" + if frame.format.name in {"yuv420p", "yuvj420p", "yuyv422", "yuv422p10le"}: + assert frame.width % 2 == 0, ( + "the width has to be even for this pixel format" + ) + assert frame.height % 2 == 0, ( + "the height has to be even for this pixel format" + ) # cases planes are simply concatenated in shape (height, width, channels) itemsize, dtype = { @@ -346,6 +479,17 @@ def to_ndarray(self, channel_last=False, **kwargs): "bgr48le": (6, "uint16"), "bgr8": (1, "uint8"), "bgra": (4, "uint8"), + "bgra64be": (8, "uint16"), + "bgra64le": (8, "uint16"), + "gbrap": (1, "uint8"), + "gbrap10be": (2, "uint16"), + "gbrap10le": (2, "uint16"), + "gbrap12be": (2, "uint16"), + "gbrap12le": (2, "uint16"), + "gbrap14be": (2, "uint16"), + "gbrap14le": (2, "uint16"), + "gbrap16be": (2, "uint16"), + "gbrap16le": (2, "uint16"), "gbrapf32be": (4, "float32"), "gbrapf32le": (4, "float32"), "gbrp": (1, "uint8"), @@ -357,12 +501,22 @@ def to_ndarray(self, channel_last=False, **kwargs): "gbrp14le": (2, "uint16"), "gbrp16be": (2, "uint16"), "gbrp16le": (2, "uint16"), + "gbrp9be": (2, "uint16"), + "gbrp9le": (2, "uint16"), "gbrpf32be": (4, "float32"), "gbrpf32le": (4, "float32"), "gray": (1, "uint8"), + "gray10be": (2, "uint16"), + "gray10le": (2, "uint16"), + "gray12be": (2, "uint16"), + "gray12le": (2, "uint16"), + "gray14be": (2, "uint16"), + "gray14le": (2, "uint16"), "gray16be": (2, "uint16"), "gray16le": (2, "uint16"), "gray8": (1, "uint8"), + "gray9be": (2, "uint16"), + "gray9le": (2, "uint16"), "grayf32be": (4, "float32"), "grayf32le": (4, "float32"), "rgb24": (3, "uint8"), @@ -372,8 +526,12 @@ def to_ndarray(self, channel_last=False, **kwargs): "rgba": (4, "uint8"), "rgba64be": (8, "uint16"), "rgba64le": (8, "uint16"), - "bgra64be": (8, "uint16"), - "bgra64le": (8, "uint16"), + "rgbaf16be": (8, "float16"), + "rgbaf16le": (8, "float16"), + "rgbaf32be": (16, "float32"), + "rgbaf32le": (16, "float32"), + "rgbf32be": (12, "float32"), + "rgbf32le": (12, "float32"), "yuv444p": (1, "uint8"), "yuv444p16be": (2, "uint16"), "yuv444p16le": (2, "uint16"), @@ -384,8 +542,9 @@ def to_ndarray(self, channel_last=False, **kwargs): }.get(frame.format.name, (None, None)) if itemsize is not None: layers = [ - useful_array(plan, itemsize, dtype) - .reshape(frame.height, frame.width, -1) + useful_array(plan, itemsize, dtype).reshape( + frame.height, frame.width, -1 + ) for plan in frame.planes ] if len(layers) == 1: # shortcut, avoid memory copy @@ -406,26 +565,24 @@ def to_ndarray(self, channel_last=False, **kwargs): # special cases if frame.format.name in {"yuv420p", "yuvj420p"}: - return np.hstack([ - useful_array(frame.planes[0]), - useful_array(frame.planes[1]), - useful_array(frame.planes[2]), - ]).reshape(-1, frame.width) - if frame.format.name == "yuv420p10le": - # Read planes as uint16: - y = useful_array(frame.planes[0], 2, "uint16").reshape(frame.height, frame.width) - u = useful_array(frame.planes[1], 2, "uint16").reshape(frame.height // 2, frame.width // 2) - v = useful_array(frame.planes[2], 2, "uint16").reshape(frame.height // 2, frame.width // 2) - u_full = np.repeat(np.repeat(u, 2, axis=1), 2, axis=0) - v_full = np.repeat(np.repeat(u, 2, axis=1), 2, axis=0) - if channel_last: - return np.stack([y, u_full, v_full], axis=2) - return np.stack([y, u_full, v_full], axis=0) + return np.hstack( + [ + useful_array(frame.planes[0]), + useful_array(frame.planes[1]), + useful_array(frame.planes[2]), + ] + ).reshape(-1, frame.width) if frame.format.name == "yuv422p10le": # Read planes as uint16 at their original width - y = useful_array(frame.planes[0], 2, "uint16").reshape(frame.height, frame.width) - u = useful_array(frame.planes[1], 2, "uint16").reshape(frame.height, frame.width // 2) - v = useful_array(frame.planes[2], 2, "uint16").reshape(frame.height, frame.width // 2) + y = useful_array(frame.planes[0], 2, "uint16").reshape( + frame.height, frame.width + ) + u = useful_array(frame.planes[1], 2, "uint16").reshape( + frame.height, frame.width // 2 + ) + v = useful_array(frame.planes[2], 2, "uint16").reshape( + frame.height, frame.width // 2 + ) # Double the width of U and V by repeating each value u_full = np.repeat(u, 2, axis=1) @@ -435,13 +592,20 @@ def to_ndarray(self, channel_last=False, **kwargs): return np.stack([y, u_full, v_full], axis=0) if frame.format.name == "pal8": image = useful_array(frame.planes[0]).reshape(frame.height, frame.width) - palette = np.frombuffer(frame.planes[1], "i4").astype(">i4").reshape(-1, 1).view(np.uint8) + palette = ( + np.frombuffer(frame.planes[1], "i4") + .astype(">i4") + .reshape(-1, 1) + .view(np.uint8) + ) return image, palette if frame.format.name == "nv12": - return np.hstack([ - useful_array(frame.planes[0]), - useful_array(frame.planes[1], 2), - ]).reshape(-1, frame.width) + return np.hstack( + [ + useful_array(frame.planes[0]), + useful_array(frame.planes[1], 2), + ] + ).reshape(-1, frame.width) raise ValueError( f"Conversion to numpy array with format `{frame.format.name}` is not yet supported" @@ -455,18 +619,29 @@ def from_image(img): if img.mode != "RGB": img = img.convert("RGB") - cdef VideoFrame frame = VideoFrame(img.size[0], img.size[1], "rgb24") + frame: VideoFrame = VideoFrame(img.size[0], img.size[1], "rgb24") copy_array_to_plane(img, frame.planes[0], 3) return frame @staticmethod def from_numpy_buffer(array, format="rgb24", width=0): - # Usually the width of the array is the same as the width of the image. But sometimes - # this is not possible, for example with yuv420p images that have padding. These are - # awkward because the UV rows at the bottom have padding bytes in the middle of the - # row as well as at the end. To cope with these, callers need to be able to pass the - # actual width to us. + """ + Construct a frame from a numpy buffer. + + :param int width: optional width of actual image, if different from the array width. + + .. note:: For formats which expect an array of ``uint16``, ``float16`` or ``float32``, + the samples must be in the system's native byte order. + + .. note:: for ``gbrp`` formats, channels are assumed to be given in RGB order. + + .. note:: For formats where width of the array is not the same as the width of the image, + for example with yuv420p images the UV rows at the bottom have padding bytes in the middle of the + row as well as at the end. To cope with these, callers need to be able to pass the actual width. + """ + import numpy as np + height = array.shape[0] if not width: width = array.shape[1] @@ -476,40 +651,169 @@ def from_numpy_buffer(array, format="rgb24", width=0): check_ndarray_shape(array, array.shape[2] == 3) if array.strides[1:] != (3, 1): raise ValueError("provided array does not have C_CONTIGUOUS rows") - linesizes = (array.strides[0], ) + linesizes = (array.strides[0],) elif format in {"rgb48le", "rgb48be", "bgr48le", "bgr48be"}: check_ndarray(array, "uint16", 3) check_ndarray_shape(array, array.shape[2] == 3) if array.strides[1:] != (6, 2): raise ValueError("provided array does not have C_CONTIGUOUS rows") - linesizes = (array.strides[0], ) + linesizes = (array.strides[0],) + elif format in {"rgbf32le", "rgbf32be"}: + check_ndarray(array, "float32", 3) + check_ndarray_shape(array, array.shape[2] == 3) + if array.strides[1:] != (12, 4): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = (array.strides[0],) elif format in {"rgba", "bgra", "argb", "abgr"}: check_ndarray(array, "uint8", 3) check_ndarray_shape(array, array.shape[2] == 4) if array.strides[1:] != (4, 1): raise ValueError("provided array does not have C_CONTIGUOUS rows") - linesizes = (array.strides[0], ) + linesizes = (array.strides[0],) elif format in {"rgba64le", "rgba64be", "bgra64le", "bgra64be"}: check_ndarray(array, "uint16", 3) check_ndarray_shape(array, array.shape[2] == 4) if array.strides[1:] != (8, 2): raise ValueError("provided array does not have C_CONTIGUOUS rows") - linesizes = (array.strides[0], ) - elif format in {"gray", "gray8", "rgb8", "bgr8","bayer_bggr8", "bayer_rggb8", "bayer_gbrg8", "bayer_grbg8"}: + linesizes = (array.strides[0],) + elif format in {"rgbaf16le", "rgbaf16be"}: + check_ndarray(array, "float16", 3) + check_ndarray_shape(array, array.shape[2] == 4) + if array.strides[1:] != (8, 2): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = (array.strides[0],) + elif format in {"rgbaf32le", "rgbaf32be"}: + check_ndarray(array, "float32", 3) + check_ndarray_shape(array, array.shape[2] == 4) + if array.strides[1:] != (16, 4): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = (array.strides[0],) + elif format in { + "gray", + "gray8", + "rgb8", + "bgr8", + "bayer_bggr8", + "bayer_gbrg8", + "bayer_grbg8", + "bayer_rggb8", + }: check_ndarray(array, "uint8", 2) if array.strides[1] != 1: raise ValueError("provided array does not have C_CONTIGUOUS rows") - linesizes = (array.strides[0], ) - elif format in {"gray16le", "gray16be", "bayer_rggb16le", "bayer_gbrg16le", "bayer_grbg16le","bayer_bggr16be", "bayer_rggb16be", "bayer_gbrg16be", "bayer_grbg16be"}: + linesizes = (array.strides[0],) + elif format in { + "gray9be", + "gray9le", + "gray10be", + "gray10le", + "gray12be", + "gray12le", + "gray14be", + "gray14le", + "gray16be", + "gray16le", + "bayer_bggr16be", + "bayer_bggr16le", + "bayer_gbrg16be", + "bayer_gbrg16le", + "bayer_grbg16be", + "bayer_grbg16le", + "bayer_rggb16be", + "bayer_rggb16le", + }: check_ndarray(array, "uint16", 2) if array.strides[1] != 2: raise ValueError("provided array does not have C_CONTIGUOUS rows") - linesizes = (array.strides[0], ) + linesizes = (array.strides[0],) elif format in {"grayf32le", "grayf32be"}: check_ndarray(array, "float32", 2) if array.strides[1] != 4: raise ValueError("provided array does not have C_CONTIGUOUS rows") - linesizes = (array.strides[0], ) + linesizes = (array.strides[0],) + elif format in {"gbrp"}: + check_ndarray(array, "uint8", 3) + check_ndarray_shape(array, array.shape[2] == 3) + if array.strides[1:] != (3, 1): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = ( + array.strides[0] // 3, + array.strides[0] // 3, + array.strides[0] // 3, + ) + elif format in { + "gbrp9be", + "gbrp9le", + "gbrp10be", + "gbrp10le", + "gbrp12be", + "gbrp12le", + "gbrp14be", + "gbrp14le", + "gbrp16be", + "gbrp16le", + }: + check_ndarray(array, "uint16", 3) + check_ndarray_shape(array, array.shape[2] == 3) + if array.strides[1:] != (6, 2): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = ( + array.strides[0] // 3, + array.strides[0] // 3, + array.strides[0] // 3, + ) + elif format in {"gbrpf32be", "gbrpf32le"}: + check_ndarray(array, "float32", 3) + check_ndarray_shape(array, array.shape[2] == 3) + if array.strides[1:] != (12, 4): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = ( + array.strides[0] // 3, + array.strides[0] // 3, + array.strides[0] // 3, + ) + elif format in {"gbrap"}: + check_ndarray(array, "uint8", 3) + check_ndarray_shape(array, array.shape[2] == 4) + if array.strides[1:] != (4, 1): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = ( + array.strides[0] // 4, + array.strides[0] // 4, + array.strides[0] // 4, + array.strides[0] // 4, + ) + elif format in { + "gbrap10be", + "gbrap10le", + "gbrap12be", + "gbrap12le", + "gbrap14be", + "gbrap14le", + "gbrap16be", + "gbrap16le", + }: + check_ndarray(array, "uint16", 3) + check_ndarray_shape(array, array.shape[2] == 4) + if array.strides[1:] != (8, 2): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = ( + array.strides[0] // 4, + array.strides[0] // 4, + array.strides[0] // 4, + array.strides[0] // 4, + ) + elif format in {"gbrapf32be", "gbrapf32le"}: + check_ndarray(array, "float32", 3) + check_ndarray_shape(array, array.shape[2] == 4) + if array.strides[1:] != (16, 4): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = ( + array.strides[0] // 4, + array.strides[0] // 4, + array.strides[0] // 4, + array.strides[0] // 4, + ) elif format in {"yuv420p", "yuvj420p", "nv12"}: check_ndarray(array, "uint8", 2) check_ndarray_shape(array, array.shape[0] % 3 == 0) @@ -519,26 +823,37 @@ def from_numpy_buffer(array, format="rgb24", width=0): raise ValueError("provided array does not have C_CONTIGUOUS rows") if format in {"yuv420p", "yuvj420p"}: # For YUV420 planar formats, the UV plane stride is always half the Y stride. - linesizes = (array.strides[0], array.strides[0] // 2, array.strides[0] // 2) + linesizes = ( + array.strides[0], + array.strides[0] // 2, + array.strides[0] // 2, + ) else: # Planes where U and V are interleaved have the same stride as Y. linesizes = (array.strides[0], array.strides[0]) else: - raise ValueError(f"Conversion from numpy array with format `{format}` is not yet supported") + raise ValueError( + f"Conversion from numpy array with format `{format}` is not yet supported" + ) + + if format.startswith("gbrap"): # rgba -> gbra + array = np.ascontiguousarray(np.moveaxis(array[..., [1, 2, 0, 3]], -1, 0)) + elif format.startswith("gbrp"): # rgb -> gbr + array = np.ascontiguousarray(np.moveaxis(array[..., [1, 2, 0]], -1, 0)) - frame = alloc_video_frame() + frame = VideoFrame(_cinit_bypass_sentinel) frame._image_fill_pointers_numpy(array, width, height, linesizes, format) return frame def _image_fill_pointers_numpy(self, buffer, width, height, linesizes, format): - cdef lib.AVPixelFormat c_format - cdef uint8_t * c_ptr - cdef size_t c_data + c_format: lib.AVPixelFormat + c_ptr: cython.pointer[uint8_t] + c_data: cython.size_t - # If you want to use the numpy notation - # then you need to include the following two lines at the top of the file + # If you want to use the numpy notation, then you need to include the following lines at the top of the file: # cimport numpy as cnp # cnp.import_array() + # And add the numpy include directories to the setup.py files # hint np.get_include() # cdef cnp.ndarray[ @@ -548,19 +863,15 @@ def _image_fill_pointers_numpy(self, buffer, width, height, linesizes, format): # c_ptr = &c_buffer[0] # c_ptr = ((buffer.ctypes.data)) - # Using buffer.ctypes.data helps avoid any kind of - # usage of the c-api from numpy, which avoid the need to add numpy - # as a compile time dependency - # Without this double cast, you get an error that looks like - # c_ptr = (buffer.ctypes.data) - # TypeError: expected bytes, int found + # Using buffer.ctypes.data helps avoid any kind of usage of the c-api from + # numpy, which avoid the need to add numpy as a compile time dependency. + c_data = buffer.ctypes.data - c_ptr = (c_data) + c_ptr = cython.cast(cython.pointer[uint8_t], c_data) c_format = get_pix_fmt(format) - lib.av_freep(&self._buffer) + lib.av_freep(cython.address(self._buffer)) - # Hold on to a reference for the numpy buffer - # so that it doesn't get accidentally garbage collected + # Hold on to a reference for the numpy buffer so that it doesn't get accidentally garbage collected self._np_buffer = buffer self.ptr.format = c_format self.ptr.width = width @@ -570,7 +881,7 @@ def _image_fill_pointers_numpy(self, buffer, width, height, linesizes, format): res = lib.av_image_fill_pointers( self.ptr.data, - self.ptr.format, + cython.cast(lib.AVPixelFormat, self.ptr.format), self.ptr.height, c_ptr, self.ptr.linesize, @@ -588,10 +899,11 @@ def from_ndarray(array, format="rgb24", channel_last=False): :param bool channel_last: If False (default), the shape for the yuv444p and yuvj444p is given by (channels, height, width) rather than (height, width, channels). - .. note:: For formats which expect an array of ``uint16``, + .. note:: For formats which expect an array of ``uint16``, ``float16`` or ``float32``, the samples must be in the system's native byte order. - .. note:: for ``pal8``, an ``(image, palette)`` pair must be passed. `palette` must have shape (256, 4) and is given in ARGB format (PyAV will swap bytes if needed). + .. note:: for ``pal8``, an ``(image, palette)`` pair must be passed. `palette` must + have shape (256, 4) and is given in ARGB format (PyAV will swap bytes if needed). .. note:: for ``gbrp`` formats, channels are assumed to be given in RGB order. @@ -600,45 +912,64 @@ def from_ndarray(array, format="rgb24", channel_last=False): # case layers are concatenated channels, itemsize, dtype = { - "yuv444p": (3, 1, "uint8"), - "yuvj444p": (3, 1, "uint8"), + "bayer_bggr16be": (1, 2, "uint16"), + "bayer_bggr16le": (1, 2, "uint16"), + "bayer_bggr8": (1, 1, "uint8"), + "bayer_gbrg16be": (1, 2, "uint16"), + "bayer_gbrg16le": (1, 2, "uint16"), + "bayer_gbrg8": (1, 1, "uint8"), + "bayer_grbg16be": (1, 2, "uint16"), + "bayer_grbg16le": (1, 2, "uint16"), + "bayer_grbg8": (1, 1, "uint8"), + "bayer_rggb16be": (1, 2, "uint16"), + "bayer_rggb16le": (1, 2, "uint16"), + "bayer_rggb8": (1, 1, "uint8"), + "bgr8": (1, 1, "uint8"), + "gbrap": (4, 1, "uint8"), + "gbrap10be": (4, 2, "uint16"), + "gbrap10le": (4, 2, "uint16"), + "gbrap12be": (4, 2, "uint16"), + "gbrap12le": (4, 2, "uint16"), + "gbrap14be": (4, 2, "uint16"), + "gbrap14le": (4, 2, "uint16"), + "gbrap16be": (4, 2, "uint16"), + "gbrap16le": (4, 2, "uint16"), + "gbrapf32be": (4, 4, "float32"), + "gbrapf32le": (4, 4, "float32"), "gbrp": (3, 1, "uint8"), "gbrp10be": (3, 2, "uint16"), - "gbrp12be": (3, 2, "uint16"), - "gbrp14be": (3, 2, "uint16"), - "gbrp16be": (3, 2, "uint16"), "gbrp10le": (3, 2, "uint16"), + "gbrp12be": (3, 2, "uint16"), "gbrp12le": (3, 2, "uint16"), + "gbrp14be": (3, 2, "uint16"), "gbrp14le": (3, 2, "uint16"), + "gbrp16be": (3, 2, "uint16"), "gbrp16le": (3, 2, "uint16"), + "gbrp9be": (3, 2, "uint16"), + "gbrp9le": (3, 2, "uint16"), "gbrpf32be": (3, 4, "float32"), "gbrpf32le": (3, 4, "float32"), "gray": (1, 1, "uint8"), - "gray8": (1, 1, "uint8"), - "rgb8": (1, 1, "uint8"), - "bgr8": (1, 1, "uint8"), + "gray10be": (1, 2, "uint16"), + "gray10le": (1, 2, "uint16"), + "gray12be": (1, 2, "uint16"), + "gray12le": (1, 2, "uint16"), + "gray14be": (1, 2, "uint16"), + "gray14le": (1, 2, "uint16"), "gray16be": (1, 2, "uint16"), "gray16le": (1, 2, "uint16"), + "gray8": (1, 1, "uint8"), + "gray9be": (1, 2, "uint16"), + "gray9le": (1, 2, "uint16"), "grayf32be": (1, 4, "float32"), "grayf32le": (1, 4, "float32"), - "gbrapf32be": (4, 4, "float32"), - "gbrapf32le": (4, 4, "float32"), + "rgb8": (1, 1, "uint8"), + "yuv444p": (3, 1, "uint8"), "yuv444p16be": (3, 2, "uint16"), "yuv444p16le": (3, 2, "uint16"), "yuva444p16be": (4, 2, "uint16"), - "yuva444p16le": (4, 2, "uint16"), - "bayer_bggr8": (1, 1, "uint8"), - "bayer_rggb8": (1, 1, "uint8"), - "bayer_grbg8": (1, 1, "uint8"), - "bayer_gbrg8": (1, 1, "uint8"), - "bayer_bggr16be": (1, 2, "uint16"), - "bayer_bggr16le": (1, 2, "uint16"), - "bayer_rggb16be": (1, 2, "uint16"), - "bayer_rggb16le": (1, 2, "uint16"), - "bayer_grbg16be": (1, 2, "uint16"), - "bayer_grbg16le": (1, 2, "uint16"), - "bayer_gbrg16be": (1, 2, "uint16"), - "bayer_gbrg16le": (1, 2, "uint16"), + "yuva444p16le": (4, 2, "uint16"), + "yuvj444p": (3, 1, "uint8"), }.get(format, (None, None, None)) if channels is not None: if array.ndim == 2: # (height, width) -> (height, width, 1) @@ -650,9 +981,14 @@ def from_ndarray(array, format="rgb24", channel_last=False): array = byteswap_array(array, format.endswith("be")) frame = VideoFrame(array.shape[1], array.shape[0], format) if frame.format.name.startswith("gbr"): # rgb -> gbr - array = np.concatenate([ # not inplace to avoid bad surprises - array[:, :, 1:3], array[:, :, 0:1], array[:, :, 3:], - ], axis=2) + array = np.concatenate( + [ # not inplace to avoid bad surprises + array[:, :, 1:3], + array[:, :, 0:1], + array[:, :, 3:], + ], + axis=2, + ) for i in range(channels): copy_array_to_plane(array[:, :, i], frame.planes[i], itemsize) return frame @@ -681,28 +1017,6 @@ def from_ndarray(array, format="rgb24", channel_last=False): copy_array_to_plane(flat[u_start:v_start], frame.planes[1], 1) copy_array_to_plane(flat[v_start:], frame.planes[2], 1) return frame - elif format == "yuv420p10le": - if not isinstance(array, np.ndarray) or array.dtype != np.uint16: - raise ValueError("Array must be uint16 type") - - # Convert to channel-first if needed: - if channel_last and array.shape[2] == 3: - array = np.moveaxis(array, 2, 0) - elif not (array.shape[0] == 3): - raise ValueError("Array must have shape (3, height, width) or (height, width, 3)") - - height, width = array.shape[1:] - if width % 2 != 0 or height % 2 != 0: - raise ValueError("Width and height must be even") - - frame = VideoFrame(width, height, format) - copy_array_to_plane(array[0], frame.planes[0], 2) - # Subsample U and V by taking every other row and column: - u = array[1, ::2, ::2].copy() # Need copy to ensure C-contiguous - v = array[2, ::2, ::2].copy() # Need copy to ensure C-contiguous - copy_array_to_plane(u, frame.planes[1], 2) - copy_array_to_plane(v, frame.planes[2], 2) - return frame elif format == "yuv422p10le": if not isinstance(array, np.ndarray) or array.dtype != np.uint16: raise ValueError("Array must be uint16 type") @@ -711,7 +1025,9 @@ def from_ndarray(array, format="rgb24", channel_last=False): if channel_last and array.shape[2] == 3: array = np.moveaxis(array, 2, 0) elif not (array.shape[0] == 3): - raise ValueError("Array must have shape (3, height, width) or (height, width, 3)") + raise ValueError( + "Array must have shape (3, height, width) or (height, width, 3)" + ) height, width = array.shape[1:] if width % 2 != 0 or height % 2 != 0: @@ -736,17 +1052,45 @@ def from_ndarray(array, format="rgb24", channel_last=False): elif format in {"argb", "rgba", "abgr", "bgra"}: check_ndarray(array, "uint8", 3) check_ndarray_shape(array, array.shape[2] == 4) - elif format in {"rgb48be", "rgb48le","bgr48be", "bgr48le"}: + elif format in {"rgb48be", "rgb48le", "bgr48be", "bgr48le"}: check_ndarray(array, "uint16", 3) check_ndarray_shape(array, array.shape[2] == 3) frame = VideoFrame(array.shape[1], array.shape[0], format) - copy_array_to_plane(byteswap_array(array, format.endswith("be")), frame.planes[0], 6) + copy_array_to_plane( + byteswap_array(array, format.endswith("be")), frame.planes[0], 6 + ) + return frame + elif format in {"rgbf32be", "rgbf32le"}: + check_ndarray(array, "float32", 3) + check_ndarray_shape(array, array.shape[2] == 3) + frame = VideoFrame(array.shape[1], array.shape[0], format) + copy_array_to_plane( + byteswap_array(array, format.endswith("be")), frame.planes[0], 12 + ) return frame elif format in {"rgba64be", "rgba64le", "bgra64be", "bgra64le"}: check_ndarray(array, "uint16", 3) check_ndarray_shape(array, array.shape[2] == 4) frame = VideoFrame(array.shape[1], array.shape[0], format) - copy_array_to_plane(byteswap_array(array, format.endswith("be")), frame.planes[0], 8) + copy_array_to_plane( + byteswap_array(array, format.endswith("be")), frame.planes[0], 8 + ) + return frame + elif format in {"rgbaf16be", "rgbaf16le"}: + check_ndarray(array, "float16", 3) + check_ndarray_shape(array, array.shape[2] == 4) + frame = VideoFrame(array.shape[1], array.shape[0], format) + copy_array_to_plane( + byteswap_array(array, format.endswith("be")), frame.planes[0], 8 + ) + return frame + elif format in {"rgbaf32be", "rgbaf32le"}: + check_ndarray(array, "float32", 3) + check_ndarray_shape(array, array.shape[2] == 4) + frame = VideoFrame(array.shape[1], array.shape[0], format) + copy_array_to_plane( + byteswap_array(array, format.endswith("be")), frame.planes[0], 16 + ) return frame elif format == "nv12": check_ndarray(array, "uint8", 2) @@ -760,20 +1104,52 @@ def from_ndarray(array, format="rgb24", channel_last=False): copy_array_to_plane(flat[uv_start:], frame.planes[1], 2) return frame else: - raise ValueError(f"Conversion from numpy array with format `{format}` is not yet supported") + raise ValueError( + f"Conversion from numpy array with format `{format}` is not yet supported" + ) frame = VideoFrame(array.shape[1], array.shape[0], format) - copy_array_to_plane(array, frame.planes[0], 1 if array.ndim == 2 else array.shape[2]) + copy_array_to_plane( + array, frame.planes[0], 1 if array.ndim == 2 else array.shape[2] + ) return frame @staticmethod - def from_bytes(img_bytes: bytes, width: int, height: int, format="rgba", flip_horizontal=False, flip_vertical=False): + def from_bytes( + img_bytes: bytes, + width: int, + height: int, + format="rgba", + flip_horizontal=False, + flip_vertical=False, + ): frame = VideoFrame(width, height, format) if format == "rgba": - copy_bytes_to_plane(img_bytes, frame.planes[0], 4, flip_horizontal, flip_vertical) - elif format in ("bayer_bggr8", "bayer_rggb8", "bayer_gbrg8", "bayer_grbg8","bayer_bggr16le", "bayer_rggb16le", "bayer_gbrg16le", "bayer_grbg16le","bayer_bggr16be", "bayer_rggb16be", "bayer_gbrg16be", "bayer_grbg16be"): - copy_bytes_to_plane(img_bytes, frame.planes[0], 1 if format.endswith("8") else 2, flip_horizontal, flip_vertical) + copy_bytes_to_plane( + img_bytes, frame.planes[0], 4, flip_horizontal, flip_vertical + ) + elif format in { + "bayer_bggr8", + "bayer_rggb8", + "bayer_gbrg8", + "bayer_grbg8", + "bayer_bggr16le", + "bayer_rggb16le", + "bayer_gbrg16le", + "bayer_grbg16le", + "bayer_bggr16be", + "bayer_rggb16be", + "bayer_gbrg16be", + "bayer_grbg16be", + }: + copy_bytes_to_plane( + img_bytes, + frame.planes[0], + 1 if format.endswith("8") else 2, + flip_horizontal, + flip_vertical, + ) else: raise NotImplementedError(f"Format '{format}' is not supported.") return frame diff --git a/av/video/frame.pyi b/av/video/frame.pyi index 313e184f9..c6868aabc 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -1,8 +1,8 @@ from enum import IntEnum +from pathlib import Path from typing import Any, ClassVar, Union import numpy as np -from PIL import Image from av.frame import Frame @@ -12,6 +12,7 @@ from .plane import VideoPlane _SupportedNDarray = Union[ np.ndarray[Any, np.dtype[np.uint8]], np.ndarray[Any, np.dtype[np.uint16]], + np.ndarray[Any, np.dtype[np.float16]], np.ndarray[Any, np.dtype[np.float32]], ] @@ -30,7 +31,6 @@ class PictureType(IntEnum): class VideoFrame(Frame): format: VideoFormat pts: int - duration: int planes: tuple[VideoPlane, ...] pict_type: int colorspace: int @@ -61,12 +61,13 @@ class VideoFrame(Frame): dst_color_range: int | str | None = None, ) -> VideoFrame: ... def to_rgb(self, **kwargs: Any) -> VideoFrame: ... - def to_image(self, **kwargs: Any) -> Image.Image: ... + def save(self, filepath: str | Path) -> None: ... + def to_image(self, **kwargs): ... def to_ndarray( self, channel_last: bool = False, **kwargs: Any ) -> _SupportedNDarray: ... @staticmethod - def from_image(img: Image.Image) -> VideoFrame: ... + def from_image(img): ... @staticmethod def from_numpy_buffer( array: _SupportedNDarray, format: str = "rgb24", width: int = 0 diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 677bc1fc1..9c9773ae8 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -7,16 +7,11 @@ import av from av import VideoFrame +from av.frame import Frame +from av.video.frame import supported_np_pix_fmts from av.video.reformatter import ColorRange, Colorspace, Interpolation -from .common import ( - TestCase, - assertImagesAlmostEqual, - assertNdarraysEqual, - fate_png, - fate_suite, - has_pillow, -) +from .common import TestCase, assertNdarraysEqual, fate_png, fate_suite def assertPixelValue16(plane, expected, byteorder: str) -> None: @@ -29,26 +24,6 @@ def assertPixelValue16(plane, expected, byteorder: str) -> None: assert view[1] == (expected >> 8 & 0xFF) -def test_opaque() -> None: - with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: - video_stream = container.streams.video[0] - - ctx = video_stream.codec_context - ctx.flags |= av.codec.context.Flags.copy_opaque - - assert video_stream.codec_context.copy_opaque - - for packet_idx, packet in enumerate(container.demux()): - packet.opaque = (time.time(), packet_idx) - for frame in packet.decode(): - assert isinstance(frame, av.frame.Frame) - - if frame.opaque is None: - continue - - assert type(frame.opaque) is tuple and len(frame.opaque) == 2 - - def test_frame_duration_matches_packet() -> None: with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: packet_durations = [ @@ -147,64 +122,24 @@ def test_memoryview_read() -> None: assert mem[:7] == b"0.234xx" -class TestVideoFrameImage(TestCase): - def setUp(self) -> None: - if not has_pillow: - pytest.skip() +def test_interpolation() -> None: + container = av.open(fate_png()) + for _ in container.decode(video=0): + frame = _ + break - def test_roundtrip(self) -> None: - import PIL.Image as Image + assert frame.width == 330 and frame.height == 330 - image = Image.open(fate_png()) - frame = VideoFrame.from_image(image) - img = frame.to_image() - img.save(self.sandboxed("roundtrip-high.jpg")) - assertImagesAlmostEqual(image, img) + img = frame.reformat(width=200, height=100, interpolation=Interpolation.BICUBIC) + assert img.width == 200 and img.height == 100 - def test_interpolation(self) -> None: - import PIL.Image as Image + img = frame.reformat(width=200, height=100, interpolation="BICUBIC") + assert img.width == 200 and img.height == 100 - image = Image.open(fate_png()) - frame = VideoFrame.from_image(image) - assert frame.width == 330 and frame.height == 330 - - img = frame.to_image(width=200, height=100, interpolation=Interpolation.BICUBIC) - assert img.width == 200 and img.height == 100 - - img = frame.to_image(width=200, height=100, interpolation="BICUBIC") - assert img.width == 200 and img.height == 100 - - img = frame.to_image( - width=200, height=100, interpolation=int(Interpolation.BICUBIC) - ) - assert img.width == 200 and img.height == 100 - - def test_to_image_rgb24(self) -> None: - sizes = [(318, 238), (320, 240), (500, 500)] - for width, height in sizes: - frame = VideoFrame(width, height, format="rgb24") - - # fill video frame data - for plane in frame.planes: - ba = bytearray(plane.buffer_size) - pos = 0 - for row in range(height): - for i in range(plane.line_size): - ba[pos] = i % 256 - pos += 1 - plane.update(ba) - - # construct expected image data - expected = bytearray(height * width * 3) - pos = 0 - for row in range(height): - for i in range(width * 3): - expected[pos] = i % 256 - pos += 1 - - img = frame.to_image() - assert img.size == (width, height) - assert img.tobytes() == expected + img = frame.reformat( + width=200, height=100, interpolation=int(Interpolation.BICUBIC) + ) + assert img.width == 200 and img.height == 100 def test_basic_to_ndarray() -> None: @@ -212,19 +147,11 @@ def test_basic_to_ndarray() -> None: assert array.shape == (480, 640, 3) -def test_to_image_with_dimensions() -> None: - if not has_pillow: - pytest.skip() - - img = VideoFrame(640, 480, format="rgb24").to_image(width=320, height=240) - assert img.size == (320, 240) - - def test_ndarray_gray() -> None: array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) for format in ("gray", "gray8"): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == "gray" assertNdarraysEqual(frame.to_ndarray(), array) @@ -234,17 +161,127 @@ def test_ndarray_gray_align() -> None: array = numpy.random.randint(0, 256, size=(238, 318), dtype=numpy.uint8) for format in ("gray", "gray8"): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 318 and frame.height == 238 assert frame.format.name == "gray" assertNdarraysEqual(frame.to_ndarray(), array) +def test_ndarray_gray9be() -> None: + array = numpy.random.randint(0, 512, size=(480, 640), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="gray9be") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "gray9be" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining value of first pixel + assertPixelValue16(frame.planes[0], array[0][0], "big") + + +def test_ndarray_gray9le() -> None: + array = numpy.random.randint(0, 512, size=(480, 640), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="gray9le") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "gray9le" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining value of first pixel + assertPixelValue16(frame.planes[0], array[0][0], "little") + + +def test_ndarray_gray10be() -> None: + array = numpy.random.randint(0, 1024, size=(480, 640), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="gray10be") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "gray10be" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining value of first pixel + assertPixelValue16(frame.planes[0], array[0][0], "big") + + +def test_ndarray_gray10le() -> None: + array = numpy.random.randint(0, 1024, size=(480, 640), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="gray10le") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "gray10le" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining value of first pixel + assertPixelValue16(frame.planes[0], array[0][0], "little") + + +def test_ndarray_gray12be() -> None: + array = numpy.random.randint(0, 4096, size=(480, 640), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="gray12be") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "gray12be" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining value of first pixel + assertPixelValue16(frame.planes[0], array[0][0], "big") + + +def test_ndarray_gray12le() -> None: + array = numpy.random.randint(0, 4096, size=(480, 640), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="gray12le") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "gray12le" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining value of first pixel + assertPixelValue16(frame.planes[0], array[0][0], "little") + + +def test_ndarray_gray14be() -> None: + array = numpy.random.randint(0, 16384, size=(480, 640), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="gray14be") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "gray14be" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining value of first pixel + assertPixelValue16(frame.planes[0], array[0][0], "big") + + +def test_ndarray_gray14le() -> None: + array = numpy.random.randint(0, 16384, size=(480, 640), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="gray14le") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "gray14le" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining value of first pixel + assertPixelValue16(frame.planes[0], array[0][0], "little") + + +def test_ndarray_gray16be() -> None: + array = numpy.random.randint(0, 65536, size=(480, 640), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="gray16be") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "gray16be" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining value of first pixel + assertPixelValue16(frame.planes[0], array[0][0], "big") + + +def test_ndarray_gray16le() -> None: + array = numpy.random.randint(0, 65536, size=(480, 640), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="gray16le") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "gray16le" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining value of first pixel + assertPixelValue16(frame.planes[0], array[0][0], "little") + + def test_ndarray_grayf32() -> None: array = numpy.random.random_sample(size=(480, 640)).astype(numpy.float32) for format in ("grayf32be", "grayf32le"): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -254,7 +291,7 @@ def test_ndarray_grayf32_align() -> None: array = numpy.random.random_sample(size=(238, 318)).astype(numpy.float32) for format in ("grayf32be", "grayf32le"): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 318 and frame.height == 238 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -264,7 +301,7 @@ def test_ndarray_rgb() -> None: array = numpy.random.randint(0, 256, size=(480, 640, 3), dtype=numpy.uint8) for format in ("rgb24", "bgr24"): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -274,17 +311,26 @@ def test_ndarray_rgb_align() -> None: array = numpy.random.randint(0, 256, size=(238, 318, 3), dtype=numpy.uint8) for format in ("rgb24", "bgr24"): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 318 and frame.height == 238 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) +def test_ndarray_rgbf32() -> None: + array = numpy.random.random_sample(size=(480, 640, 3)).astype(numpy.float32) + for format in ("rgbf32be", "rgbf32le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + def test_ndarray_rgba() -> None: array = numpy.random.randint(0, 256, size=(480, 640, 4), dtype=numpy.uint8) for format in ("argb", "rgba", "abgr", "bgra"): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -294,7 +340,7 @@ def test_ndarray_rgba_align() -> None: array = numpy.random.randint(0, 256, size=(238, 318, 4), dtype=numpy.uint8) for format in ("argb", "rgba", "abgr", "bgra"): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 318 and frame.height == 238 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -304,7 +350,7 @@ def test_ndarray_bayer8() -> None: array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) for format in ("bayer_bggr8", "bayer_gbrg8", "bayer_grbg8", "bayer_rggb8"): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -323,12 +369,126 @@ def test_ndarray_bayer16() -> None: "bayer_rggb16le", ): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) +def test_ndarray_gbrap() -> None: + array = numpy.random.randint(0, 256, size=(480, 640, 4), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="gbrap") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "gbrap" + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrap_align() -> None: + array = numpy.random.randint(0, 256, size=(238, 318, 4), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="gbrap") + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == "gbrap" + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrap10() -> None: + array = numpy.random.randint(0, 1024, size=(480, 640, 4), dtype=numpy.uint16) + for format in ("gbrap10be", "gbrap10le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert format in supported_np_pix_fmts + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrap10_align() -> None: + array = numpy.random.randint(0, 1024, size=(238, 318, 4), dtype=numpy.uint16) + for format in ("gbrap10be", "gbrap10le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert format in supported_np_pix_fmts + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrap12() -> None: + array = numpy.random.randint(0, 4096, size=(480, 640, 4), dtype=numpy.uint16) + for format in ("gbrap12be", "gbrap12le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert format in supported_np_pix_fmts + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrap12_align() -> None: + array = numpy.random.randint(0, 4096, size=(238, 318, 4), dtype=numpy.uint16) + for format in ("gbrap12be", "gbrap12le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert format in supported_np_pix_fmts + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrap14() -> None: + array = numpy.random.randint(0, 16384, size=(480, 640, 4), dtype=numpy.uint16) + for format in ("gbrap14be", "gbrap14le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert format in supported_np_pix_fmts + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrap14_align() -> None: + array = numpy.random.randint(0, 16384, size=(238, 318, 4), dtype=numpy.uint16) + for format in ("gbrap14be", "gbrap14le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert format in supported_np_pix_fmts + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrap16() -> None: + array = numpy.random.randint(0, 65536, size=(480, 640, 4), dtype=numpy.uint16) + for format in ("gbrap16be", "gbrap16le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert format in supported_np_pix_fmts + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrap16_align() -> None: + array = numpy.random.randint(0, 65536, size=(238, 318, 4), dtype=numpy.uint16) + for format in ("gbrap16be", "gbrap16le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert format in supported_np_pix_fmts + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrapf32() -> None: + array = numpy.random.random_sample(size=(480, 640, 4)).astype(numpy.float32) + for format in ("gbrapf32be", "gbrapf32le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrapf32_align() -> None: + array = numpy.random.random_sample(size=(238, 318, 4)).astype(numpy.float32) + for format in ("gbrapf32be", "gbrapf32le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + def test_ndarray_gbrp() -> None: array = numpy.random.randint(0, 256, size=(480, 640, 3), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="gbrp") @@ -345,11 +505,31 @@ def test_ndarray_gbrp_align() -> None: assertNdarraysEqual(frame.to_ndarray(), array) +def test_ndarray_gbrp9() -> None: + array = numpy.random.randint(0, 512, size=(480, 640, 3), dtype=numpy.uint16) + for format in ("gbrp9be", "gbrp9le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert format in supported_np_pix_fmts + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrp9_align() -> None: + array = numpy.random.randint(0, 512, size=(238, 318, 3), dtype=numpy.uint16) + for format in ("gbrp9be", "gbrp9le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert format in supported_np_pix_fmts + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + def test_ndarray_gbrp10() -> None: array = numpy.random.randint(0, 1024, size=(480, 640, 3), dtype=numpy.uint16) for format in ("gbrp10be", "gbrp10le"): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -359,7 +539,7 @@ def test_ndarray_gbrp10_align() -> None: array = numpy.random.randint(0, 1024, size=(238, 318, 3), dtype=numpy.uint16) for format in ("gbrp10be", "gbrp10le"): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 318 and frame.height == 238 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -369,7 +549,7 @@ def test_ndarray_gbrp12() -> None: array = numpy.random.randint(0, 4096, size=(480, 640, 3), dtype=numpy.uint16) for format in ("gbrp12be", "gbrp12le"): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -379,7 +559,7 @@ def test_ndarray_gbrp12_align() -> None: array = numpy.random.randint(0, 4096, size=(238, 318, 3), dtype=numpy.uint16) for format in ("gbrp12be", "gbrp12le"): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 318 and frame.height == 238 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -389,7 +569,7 @@ def test_ndarray_gbrp14() -> None: array = numpy.random.randint(0, 16384, size=(480, 640, 3), dtype=numpy.uint16) for format in ("gbrp14be", "gbrp14le"): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -399,7 +579,7 @@ def test_ndarray_gbrp14_align() -> None: array = numpy.random.randint(0, 16384, size=(238, 318, 3), dtype=numpy.uint16) for format in ("gbrp14be", "gbrp14le"): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 318 and frame.height == 238 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -409,7 +589,7 @@ def test_ndarray_gbrp16() -> None: array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) for format in ("gbrp16be", "gbrp16le"): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -419,7 +599,7 @@ def test_ndarray_gbrp16_align() -> None: array = numpy.random.randint(0, 65536, size=(238, 318, 3), dtype=numpy.uint16) for format in ("gbrp16be", "gbrp16le"): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 318 and frame.height == 238 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -443,24 +623,6 @@ def test_ndarray_gbrpf32_align() -> None: assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_gbrapf32() -> None: - array = numpy.random.random_sample(size=(480, 640, 4)).astype(numpy.float32) - for format in ("gbrapf32be", "gbrapf32le"): - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 640 and frame.height == 480 - assert frame.format.name == format - assertNdarraysEqual(frame.to_ndarray(), array) - - -def test_ndarray_gbrapf32_allign() -> None: - array = numpy.random.random_sample(size=(238, 318, 4)).astype(numpy.float32) - for format in ("gbrapf32be", "gbrapf32le"): - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 318 and frame.height == 238 - assert frame.format.name == format - assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_yuv420p() -> None: array = numpy.random.randint(0, 256, size=(720, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuv420p") @@ -529,22 +691,13 @@ def test_ndarray_yuv444p16() -> None: assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_yuv420p10le() -> None: - array = numpy.random.randint(0, 65536, size=(3, 480, 640), dtype=numpy.uint16) - for format in ("yuv420p10le",): - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 640 and frame.height == 480 - assert frame.format.name == format - assert format in av.video.frame.supported_np_pix_fmts - - def test_ndarray_yuv422p10le() -> None: array = numpy.random.randint(0, 65536, size=(3, 480, 640), dtype=numpy.uint16) for format in ("yuv422p10le",): frame = VideoFrame.from_ndarray(array, format=format) assert frame.width == 640 and frame.height == 480 assert frame.format.name == format - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts def test_ndarray_yuv444p16_align() -> None: @@ -582,28 +735,6 @@ def test_ndarray_yuyv422_align() -> None: assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_gray16be() -> None: - array = numpy.random.randint(0, 65536, size=(480, 640), dtype=numpy.uint16) - frame = VideoFrame.from_ndarray(array, format="gray16be") - assert frame.width == 640 and frame.height == 480 - assert frame.format.name == "gray16be" - assertNdarraysEqual(frame.to_ndarray(), array) - - # check endianness by examining value of first pixel - assertPixelValue16(frame.planes[0], array[0][0], "big") - - -def test_ndarray_gray16le() -> None: - array = numpy.random.randint(0, 65536, size=(480, 640), dtype=numpy.uint16) - frame = VideoFrame.from_ndarray(array, format="gray16le") - assert frame.width == 640 and frame.height == 480 - assert frame.format.name == "gray16le" - assertNdarraysEqual(frame.to_ndarray(), array) - - # check endianness by examining value of first pixel - assertPixelValue16(frame.planes[0], array[0][0], "little") - - def test_ndarray_rgb48be() -> None: array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="rgb48be") @@ -714,6 +845,24 @@ def test_ndarray_bgra64le() -> None: assertPixelValue16(frame.planes[0], array[0][0][0], "little") +def test_ndarray_rgbaf16() -> None: + array = numpy.random.random_sample(size=(480, 640, 4)).astype(numpy.float16) + for format in ("rgbaf16be", "rgbaf16le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_rgbaf32() -> None: + array = numpy.random.random_sample(size=(480, 640, 4)).astype(numpy.float32) + for format in ("rgbaf32be", "rgbaf32le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + def test_ndarray_rgb8() -> None: array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="rgb8") @@ -736,7 +885,7 @@ def test_ndarray_pal8(): frame = VideoFrame.from_ndarray((array, palette), format="pal8") assert frame.width == 640 and frame.height == 480 assert frame.format.name == "pal8" - assert frame.format.name in av.video.frame.supported_np_pix_fmts + assert frame.format.name in supported_np_pix_fmts returned = frame.to_ndarray() assert type(returned) is tuple and len(returned) == 2 assertNdarraysEqual(returned[0], array) @@ -748,7 +897,7 @@ def test_ndarray_nv12() -> None: frame = VideoFrame.from_ndarray(array, format="nv12") assert frame.width == 640 and frame.height == 480 assert frame.format.name == "nv12" - assert frame.format.name in av.video.frame.supported_np_pix_fmts + assert frame.format.name in supported_np_pix_fmts assertNdarraysEqual(frame.to_ndarray(), array) From ab4e548ba67a52515658257de3184de04a02c569 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 23 Jun 2025 22:46:37 -0400 Subject: [PATCH 538/846] Use mypy 1.16.1 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 4b4f58ab7..135d091ad 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ fate-suite: rsync -vrltLW rsync://fate-suite.ffmpeg.org/fate-suite/ tests/assets/fate-suite/ lint: - $(PIP) install -U ruff isort pillow numpy mypy==1.15.0 pytest + $(PIP) install -U ruff isort pillow numpy mypy==1.16.1 pytest ruff format --check av examples tests setup.py isort --check-only --diff av examples tests mypy av tests From a92b91c7837daa219f42191e67ebd4949d5edc38 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 25 Jun 2025 22:38:30 -0400 Subject: [PATCH 539/846] PyAV-ffmpeg 7.1.1-5 Deleting `test_writing_to_custom_io_dash` because it depends on libxml2, an insecure library. --- scripts/build-deps | 2 +- scripts/ffmpeg-7.1.json | 2 +- tests/test_python_io.py | 36 ------------------------------------ 3 files changed, 2 insertions(+), 38 deletions(-) diff --git a/scripts/build-deps b/scripts/build-deps index d278c27c0..f34ed5a5b 100755 --- a/scripts/build-deps +++ b/scripts/build-deps @@ -57,11 +57,11 @@ echo ./configure --disable-doc \ --disable-static \ --disable-stripping \ + --disable-libxml2 \ --enable-debug=3 \ --enable-gpl \ --enable-version3 \ --enable-libx264 \ - --enable-libxml2 \ --enable-shared \ --enable-sse \ --enable-avx \ diff --git a/scripts/ffmpeg-7.1.json b/scripts/ffmpeg-7.1.json index 26641cedc..8a606aea3 100644 --- a/scripts/ffmpeg-7.1.json +++ b/scripts/ffmpeg-7.1.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.1.1-4/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.1.1-5/ffmpeg-{platform}.tar.gz" } diff --git a/tests/test_python_io.py b/tests/test_python_io.py index 3169ae047..5a4d1becf 100644 --- a/tests/test_python_io.py +++ b/tests/test_python_io.py @@ -237,42 +237,6 @@ def test_writing_to_buffer_broken_with_close(self) -> None: with pytest.raises(OSError): container.close() - @run_in_sandbox - def test_writing_to_custom_io_dash(self) -> None: - # Custom I/O that opens file and logs calls - wrapped_custom_io = CustomIOLogger() - - output_filename = "custom_io_output.mpd" - - # Write a DASH package using the custom IO. Prefix the name with CUSTOM_IO_PROTOCOL to - # avoid temporary file and renaming. - with av.open( - CUSTOM_IO_PROTOCOL + output_filename, "w", io_open=wrapped_custom_io - ) as container: - write_rgb_rotate(container) - - # Check that at least 3 files were opened using the custom IO: - # "output_filename", init-stream0.m4s and chunk-stream-0x.m4s - assert len(wrapped_custom_io._log) >= 3 - assert len(wrapped_custom_io._method_log) >= 3 - - # Check that all files were written to - all_write = all( - method_log._filter("write") for method_log in wrapped_custom_io._method_log - ) - assert all_write - - # Check that all files were closed - all_closed = all( - method_log._filter("close") for method_log in wrapped_custom_io._method_log - ) - assert all_closed - - # Check contents. - # Note that the dash demuxer doesn't support custom I/O. - with av.open(output_filename, "r") as container: - assert_rgb_rotate(self, container, is_dash=True) - def test_writing_to_custom_io_image2(self) -> None: if not has_pillow: pytest.skip() From e343eae6045f5ca6b9a0f0bccb7537a2244159aa Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 26 Jun 2025 00:21:48 -0400 Subject: [PATCH 540/846] Get rid of setup.py warning --- setup.py | 33 ++++++++------------------------- 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/setup.py b/setup.py index d5ad4fb31..16ca7f59b 100644 --- a/setup.py +++ b/setup.py @@ -6,30 +6,19 @@ import shlex import subprocess import sys -from time import sleep + +if platform.system() == "Darwin": + major_version = int(platform.mac_ver()[0].split(".")[0]) + if major_version < 12: + raise OSError("You are using an EOL, unsupported, and out-of-date OS.") def is_virtualenv(): return sys.base_prefix != sys.prefix -if platform.system() == "Darwin": - major_version = int(platform.mac_ver()[0].split(".")[0]) - if major_version < 12: - print( - "\033[1;91mWarning!\033[0m You are using an EOL, unsupported, and out-of-date OS." - ) - sleep(3) - -print( - "\n\033[1;91mWarning!\033[0m You are installing from source.\n" - "It is \033[1;37mEXPECTED\033[0m that it will fail. You are \033[1;37mREQUIRED\033[0m" - " to use ffmpeg 7.\nYou \033[1;37mMUST\033[0m have Cython, pkg-config, and a C compiler.\n" -) -if os.getenv("GITHUB_ACTIONS") == "true" or is_virtualenv(): - pass -else: - print("\033[1;91mWarning!\033[0m You are not using a virtual environment") +if not (os.getenv("GITHUB_ACTIONS") == "true" or is_virtualenv()): + raise ValueError("You are not using a virtual environment") from Cython.Build import cythonize @@ -159,16 +148,10 @@ def parse_cflags(raw_flags): FFMPEG_DIR = arg.split("=")[1] del sys.argv[i] -# Do not cythonize or use pkg-config when cleaning. -use_pkg_config = platform.system() != "Windows" -if len(sys.argv) > 1 and sys.argv[1] == "clean": - cythonize = lambda ext, **kwargs: [ext] - use_pkg_config = False - # Locate ffmpeg libraries and headers. if FFMPEG_DIR is not None: extension_extra = get_config_from_directory(FFMPEG_DIR) -elif use_pkg_config: +elif platform.system() != "Windows": extension_extra = get_config_from_pkg_config() else: extension_extra = { From 77bb0ef18dff2423ae6622f7856d04336554125d Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 26 Jun 2025 00:30:58 -0400 Subject: [PATCH 541/846] Fix this warning: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit src/av/video/frame.c: In function ‘__pyx_f_2av_5video_5frame_copy_bytes_to_plane’: src/av/video/frame.c:18808:41: warning: comparison of integer expressions of different signedness: ‘int’ and ‘unsigned int’ [-Wsign-compare] 18808 | for (__pyx_t_15 = 0; __pyx_t_15 < __pyx_t_16; __pyx_t_15+=1) { | --- av/video/frame.py | 1 - 1 file changed, 1 deletion(-) diff --git a/av/video/frame.py b/av/video/frame.py index bc4bf9154..970ecc4b8 100644 --- a/av/video/frame.py +++ b/av/video/frame.py @@ -155,7 +155,6 @@ def copy_bytes_to_plane( end_row = plane.height step = 1 - i, j = cython.declare(cython.int) for row in range(start_row, end_row, step): i_pos = row * i_stride if flip_horizontal: From f964e7a3664cd6f443e0dbc50aa1067bd97d19b9 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 26 Jun 2025 00:49:41 -0400 Subject: [PATCH 542/846] Update AUTHORS.rst --- AUTHORS.rst | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index df46c6c11..b85c8ce1b 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -23,10 +23,11 @@ All contributors (by number of commits): - Alba Mendez - Xinran Xu ; `@xxr3376 `_ +- z-khan +- Marc Mueller <30130371+cdce8p@users.noreply.github.com> - Dan Allan ; `@danielballan `_ - Moonsik Park - Santtu Keskinen -- Marc Mueller <30130371+cdce8p@users.noreply.github.com> - Christoph Rackwitz - David Plowman - Alireza Davoudi ; `@adavoudi `_ @@ -38,8 +39,8 @@ All contributors (by number of commits): - Ulrik Mikaelsson ; `@rawler `_ - Wel C. van der - Will Patera ; `@willpatera `_ -- z-khan +* zzjjbb <31069326+zzjjbb@users.noreply.github.com> * Joe Schiff <41972063+JoeSchiff@users.noreply.github.com> * Dexer <73297572+DexerBR@users.noreply.github.com> * rutsh @@ -59,7 +60,6 @@ All contributors (by number of commits): * Radek Senfeld ; `@radek-senfeld `_ * robinechuca * Benjamin Chrétien <2742231+bchretien@users.noreply.github.com> -* zzjjbb <31069326+zzjjbb@users.noreply.github.com> * davidplowman <38045873+davidplowman@users.noreply.github.com> * Hanz <40712686+HanzCEO@users.noreply.github.com> * Artturin @@ -81,12 +81,14 @@ All contributors (by number of commits): * Marcell Pardavi * Matteo Destro * Mattias Wadman +* Max Ehrlich * Manuel Goacolou * Julian Schweizer * Nikhil Idiculla * Ömer Sezgin Uğurlu * Orivej Desh * Philipp Krähenbühl +* Mattia Procopio * ramoncaldeira * Roland van Laar * Santiago Castro @@ -106,3 +108,4 @@ All contributors (by number of commits): * Koichi Akabe * David Joy * Sviatoslav Sydorenko (Святослав Сидоренко) +* Jiabei Zhu From d6ae316897e9712f3d68e1055b510af0f51a37c0 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 26 Jun 2025 01:56:33 -0400 Subject: [PATCH 543/846] Consolidate stream classes into single module - Move AttachmentStream and DataStream from separate modules into av/stream.py - Convert stream.pyx from Cython to pure Python - Add missing 'options' property to Stream type hints This consolidation reduces code duplication and simplifies the module structure without changing the public API. --- av/attachments/__init__.py | 0 av/attachments/stream.pxd | 5 -- av/attachments/stream.pyi | 8 --- av/attachments/stream.pyx | 26 ------- av/container/output.pyi | 2 +- av/container/streams.pyi | 4 +- av/data/__init__.pxd | 0 av/data/__init__.py | 0 av/data/stream.pxd | 5 -- av/data/stream.pyi | 6 -- av/data/stream.pyx | 16 ----- av/stream.pxd | 8 ++- av/{stream.pyx => stream.py} | 127 ++++++++++++++++++++++++----------- av/stream.pyi | 12 +++- 14 files changed, 108 insertions(+), 111 deletions(-) delete mode 100644 av/attachments/__init__.py delete mode 100644 av/attachments/stream.pxd delete mode 100644 av/attachments/stream.pyi delete mode 100644 av/attachments/stream.pyx delete mode 100644 av/data/__init__.pxd delete mode 100644 av/data/__init__.py delete mode 100644 av/data/stream.pxd delete mode 100644 av/data/stream.pyi delete mode 100644 av/data/stream.pyx rename av/{stream.pyx => stream.py} (72%) diff --git a/av/attachments/__init__.py b/av/attachments/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/av/attachments/stream.pxd b/av/attachments/stream.pxd deleted file mode 100644 index 81f788b77..000000000 --- a/av/attachments/stream.pxd +++ /dev/null @@ -1,5 +0,0 @@ -from av.stream cimport Stream - - -cdef class AttachmentStream(Stream): - pass diff --git a/av/attachments/stream.pyi b/av/attachments/stream.pyi deleted file mode 100644 index 3d660e4a0..000000000 --- a/av/attachments/stream.pyi +++ /dev/null @@ -1,8 +0,0 @@ -from typing import Literal - -from av.stream import Stream - -class AttachmentStream(Stream): - type: Literal["attachment"] - @property - def mimetype(self) -> str | None: ... diff --git a/av/attachments/stream.pyx b/av/attachments/stream.pyx deleted file mode 100644 index de7d10119..000000000 --- a/av/attachments/stream.pyx +++ /dev/null @@ -1,26 +0,0 @@ -from av.stream cimport Stream - - -cdef class AttachmentStream(Stream): - """ - An :class:`AttachmentStream` represents a stream of attachment data within a media container. - Typically used to attach font files that are referenced in ASS/SSA Subtitle Streams. - """ - - @property - def name(self): - """ - Returns the file name of the attachment. - - :rtype: str | None - """ - return self.metadata.get("filename") - - @property - def mimetype(self): - """ - Returns the MIME type of the attachment. - - :rtype: str | None - """ - return self.metadata.get("mimetype") diff --git a/av/container/output.pyi b/av/container/output.pyi index 568345cd2..b370095de 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -3,8 +3,8 @@ from typing import Sequence, TypeVar, Union, overload from av.audio import _AudioCodecName from av.audio.stream import AudioStream -from av.data.stream import DataStream from av.packet import Packet +from av.stream import DataStream from av.subtitles.stream import SubtitleStream from av.video import _VideoCodecName from av.video.stream import VideoStream diff --git a/av/container/streams.pyi b/av/container/streams.pyi index fbaf1b67f..52b98818f 100644 --- a/av/container/streams.pyi +++ b/av/container/streams.pyi @@ -1,9 +1,7 @@ from typing import Iterator, Literal, overload -from av.attachments.stream import AttachmentStream from av.audio.stream import AudioStream -from av.data.stream import DataStream -from av.stream import Stream +from av.stream import AttachmentStream, DataStream, Stream from av.subtitles.stream import SubtitleStream from av.video.stream import VideoStream diff --git a/av/data/__init__.pxd b/av/data/__init__.pxd deleted file mode 100644 index e69de29bb..000000000 diff --git a/av/data/__init__.py b/av/data/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/av/data/stream.pxd b/av/data/stream.pxd deleted file mode 100644 index 012792a4a..000000000 --- a/av/data/stream.pxd +++ /dev/null @@ -1,5 +0,0 @@ -from av.stream cimport Stream - - -cdef class DataStream(Stream): - pass diff --git a/av/data/stream.pyi b/av/data/stream.pyi deleted file mode 100644 index 45a669d4f..000000000 --- a/av/data/stream.pyi +++ /dev/null @@ -1,6 +0,0 @@ -from av.frame import Frame -from av.packet import Packet -from av.stream import Stream - -class DataStream(Stream): - name: str | None diff --git a/av/data/stream.pyx b/av/data/stream.pyx deleted file mode 100644 index 4cc957762..000000000 --- a/av/data/stream.pyx +++ /dev/null @@ -1,16 +0,0 @@ -cimport libav as lib - - -cdef class DataStream(Stream): - def __repr__(self): - return ( - f"'} at 0x{id(self):x}>" - ) - - @property - def name(self): - cdef const lib.AVCodecDescriptor *desc = lib.avcodec_descriptor_get(self.ptr.codecpar.codec_id) - if desc == NULL: - return None - return desc.name diff --git a/av/stream.pxd b/av/stream.pxd index c847f641e..9aa6616e5 100644 --- a/av/stream.pxd +++ b/av/stream.pxd @@ -19,8 +19,14 @@ cdef class Stream: # Private API. cdef _init(self, Container, lib.AVStream*, CodecContext) cdef _finalize_for_output(self) - cdef _set_time_base(self, value) cdef _set_id(self, value) cdef Stream wrap_stream(Container, lib.AVStream*, CodecContext) + + +cdef class DataStream(Stream): + pass + +cdef class AttachmentStream(Stream): + pass diff --git a/av/stream.pyx b/av/stream.py similarity index 72% rename from av/stream.pyx rename to av/stream.py index 90f10d038..b5c22588b 100644 --- a/av/stream.pyx +++ b/av/stream.py @@ -1,10 +1,10 @@ -cimport libav as lib - from enum import Flag -from av.error cimport err_check -from av.packet cimport Packet -from av.utils cimport ( +import cython +from cython.cimports import libav as lib +from cython.cimports.av.error import err_check +from cython.cimports.av.packet import Packet +from cython.cimports.av.utils import ( avdict_to_dict, avrational_to_fraction, dict_to_avdict, @@ -34,35 +34,38 @@ class Disposition(Flag): multilayer = 1 << 21 -cdef object _cinit_bypass_sentinel = object() +_cinit_bypass_sentinel = cython.declare(object, object()) -cdef Stream wrap_stream(Container container, lib.AVStream *c_stream, CodecContext codec_context): - """Build an av.Stream for an existing AVStream. - The AVStream MUST be fully constructed and ready for use before this is - called. +@cython.cfunc +def wrap_stream( + container: Container, + c_stream: cython.pointer[lib.AVStream], + codec_context: CodecContext, +) -> Stream: + """Build an av.Stream for an existing AVStream. + The AVStream MUST be fully constructed and ready for use before this is called. """ # This better be the right one... assert container.ptr.streams[c_stream.index] == c_stream - cdef Stream py_stream + py_stream: Stream + + from av.audio.stream import AudioStream + from av.subtitles.stream import SubtitleStream + from av.video.stream import VideoStream if c_stream.codecpar.codec_type == lib.AVMEDIA_TYPE_VIDEO: - from av.video.stream import VideoStream py_stream = VideoStream.__new__(VideoStream, _cinit_bypass_sentinel) elif c_stream.codecpar.codec_type == lib.AVMEDIA_TYPE_AUDIO: - from av.audio.stream import AudioStream py_stream = AudioStream.__new__(AudioStream, _cinit_bypass_sentinel) elif c_stream.codecpar.codec_type == lib.AVMEDIA_TYPE_SUBTITLE: - from av.subtitles.stream import SubtitleStream py_stream = SubtitleStream.__new__(SubtitleStream, _cinit_bypass_sentinel) elif c_stream.codecpar.codec_type == lib.AVMEDIA_TYPE_ATTACHMENT: - from av.attachments.stream import AttachmentStream py_stream = AttachmentStream.__new__(AttachmentStream, _cinit_bypass_sentinel) elif c_stream.codecpar.codec_type == lib.AVMEDIA_TYPE_DATA: - from av.data.stream import DataStream py_stream = DataStream.__new__(DataStream, _cinit_bypass_sentinel) else: py_stream = Stream.__new__(Stream, _cinit_bypass_sentinel) @@ -71,7 +74,8 @@ class Disposition(Flag): return py_stream -cdef class Stream: +@cython.cclass +class Stream: """ A single stream of audio, video or subtitles within a :class:`.Container`. @@ -93,7 +97,13 @@ def __cinit__(self, name): return raise RuntimeError("cannot manually instantiate Stream") - cdef _init(self, Container container, lib.AVStream *stream, CodecContext codec_context): + @cython.cfunc + def _init( + self, + container: Container, + stream: cython.pointer[lib.AVStream], + codec_context: CodecContext, + ): self.container = container self.ptr = stream @@ -121,18 +131,19 @@ def __setattr__(self, name, value): if name == "disposition": self.ptr.disposition = value return + if name == "time_base": + to_avrational(value, cython.address(self.ptr.time_base)) + return # Convenience setter for codec context properties. if self.codec_context is not None: setattr(self.codec_context, name, value) - if name == "time_base": - self._set_time_base(value) - - cdef _finalize_for_output(self): - + @cython.cfunc + def _finalize_for_output(self): dict_to_avdict( - &self.ptr.metadata, self.metadata, + cython.address(self.ptr.metadata), + self.metadata, encoding=self.container.metadata_encoding, errors=self.container.metadata_errors, ) @@ -140,9 +151,12 @@ def __setattr__(self, name, value): if not self.ptr.time_base.num: self.ptr.time_base = self.codec_context.ptr.time_base - # It prefers if we pass it parameters via this other object. - # Lets just copy what we want. - err_check(lib.avcodec_parameters_from_context(self.ptr.codecpar, self.codec_context.ptr)) + # It prefers if we pass it parameters via this other object. Let's just copy what we want. + err_check( + lib.avcodec_parameters_from_context( + self.ptr.codecpar, self.codec_context.ptr + ) + ) @property def id(self): @@ -154,10 +168,8 @@ def id(self): """ return self.ptr.id - cdef _set_id(self, value): - """ - Setter used by __setattr__ for the id property. - """ + @cython.cfunc + def _set_id(self, value): if value is None: self.ptr.id = 0 else: @@ -196,7 +208,6 @@ def index(self): """ return self.ptr.index - @property def time_base(self): """ @@ -205,13 +216,7 @@ def time_base(self): :type: fractions.Fraction | None """ - return avrational_to_fraction(&self.ptr.time_base) - - cdef _set_time_base(self, value): - """ - Setter used by __setattr__ for the time_base property. - """ - to_avrational(value, &self.ptr.time_base) + return avrational_to_fraction(cython.address(self.ptr.time_base)) @property def start_time(self): @@ -267,3 +272,47 @@ def type(self): :type: Literal["audio", "video", "subtitle", "data", "attachment"] """ return lib.av_get_media_type_string(self.ptr.codecpar.codec_type) + + +@cython.cclass +class DataStream(Stream): + def __repr__(self): + return ( + f"'} at 0x{id(self):x}>" + ) + + @property + def name(self): + desc: cython.pointer[cython.const[lib.AVCodecDescriptor]] = ( + lib.avcodec_descriptor_get(self.ptr.codecpar.codec_id) + ) + if desc == cython.NULL: + return None + return desc.name + + +@cython.cclass +class AttachmentStream(Stream): + """ + An :class:`AttachmentStream` represents a stream of attachment data within a media container. + Typically used to attach font files that are referenced in ASS/SSA Subtitle Streams. + """ + + @property + def name(self): + """ + Returns the file name of the attachment. + + :rtype: str | None + """ + return self.metadata.get("filename") + + @property + def mimetype(self): + """ + Returns the MIME type of the attachment. + + :rtype: str | None + """ + return self.metadata.get("mimetype") diff --git a/av/stream.pyi b/av/stream.pyi index 88dc7c00b..18e6fdca9 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -1,6 +1,6 @@ from enum import Flag from fractions import Fraction -from typing import Literal, cast +from typing import Any, Literal, cast from .codec import Codec, CodecContext from .container import Container @@ -36,6 +36,7 @@ class Stream: profiles: list[str] profile: str | None index: int + options: dict[str, Any] time_base: Fraction | None average_rate: Fraction | None base_rate: Fraction | None @@ -46,3 +47,12 @@ class Stream: frames: int language: str | None type: Literal["video", "audio", "data", "subtitle", "attachment"] + +class DataStream(Stream): + type: Literal["data"] + name: str | None + +class AttachmentStream(Stream): + type: Literal["attachment"] + @property + def mimetype(self) -> str | None: ... From 8346c20885c3555d056772845d0e302311a2cbb1 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 26 Jun 2025 14:53:45 -0400 Subject: [PATCH 544/846] Fix grammar issues --- av/logging.pyx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/av/logging.pyx b/av/logging.pyx index 9cb232d2a..30a253fe0 100644 --- a/av/logging.pyx +++ b/av/logging.pyx @@ -1,6 +1,6 @@ """ -FFmpeg has a logging system that it uses extensively. It's very noisy so PyAV turns it -off by default. This, unfortunately has the effect of making raised errors having less +FFmpeg has a logging system that it uses extensively. It's very noisy, so PyAV turns it +off by default. This unfortunately has the effect of making raised errors have less detailed messages. It's therefore recommended to use VERBOSE when developing. .. _enable_logging: @@ -8,7 +8,7 @@ detailed messages. It's therefore recommended to use VERBOSE when developing. Enabling Logging ~~~~~~~~~~~~~~~~~ -You can hook the logging system with Python by setting the log level:: +You can hook into the logging system with Python by setting the log level:: import av @@ -25,10 +25,10 @@ quickly with:: logging.basicConfig() -Note that handling logs with Python sometimes doesn't play nice multi-threads workflows. +Note that handling logs with Python sometimes doesn't play nicely with multi-threaded workflows. An alternative is :func:`restore_default_callback`. -This will restores FFmpeg's logging default system, which prints to the terminal. +This restores FFmpeg's default logging system, which prints to the terminal. Like with setting the log level to ``None``, this may also result in raised errors having less detailed messages. From bc95b9ef38e29de0982f400b4b4b8d90278291f6 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 26 Jun 2025 16:41:09 -0400 Subject: [PATCH 545/846] Release v15 Release Candidate 1 --- CHANGELOG.rst | 20 +++++++++++++++++--- av/about.py | 2 +- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3a69a86ab..e3cb6ecf0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,12 +16,26 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. -v15.0.0 (Unreleased) --------------------- +v15.0.0rc1 +---------- Major: -- Make ``SubtitleStream.decode()`` return the list of subtitles directly, without the intermediate ``SubtitleSet``. +- Make ``SubtitleStream.decode()`` return the list of subtitles directly, making the API nicer. +- Binary wheels: Remove libxml2 because of security concerns. +- Binary wheels: Remove xz because lzma encoding is very niche in media decoding and lesser security concerns. + +Features: + +- Add GBRP, GBRAP, RGBA formats by :gh-user:`z-khan`. +- Add ``VideoFrame.save()``, a convenient way to save frames to PNGs or JPEGs, by :gh-user:`WyattBlue`. + +Fixes: + +- Binary wheels: Re-enable libx265, use version 4.1 by :gh-user:`WyattBlue`. +- Fix memory error for AudioFifo properties by :gh-user:`zzjjbb`. +- Copy template flags if creating a new Codec object by :gh-user:`WyattBlue` +- ``AudioFrame.from_ndarray()``: Support python classes for parameters too, by :gh-user:`WyattBlue`. v14.4.0 ------- diff --git a/av/about.py b/av/about.py index 15fd90e44..2607c1260 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "14.4.0" +__version__ = "15.0.0rc1" From d2e95a184b3e452d2e16fe61bf38aa0e2f06c643 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 27 Jun 2025 21:33:59 -0400 Subject: [PATCH 546/846] Any -> object --- av/stream.pyi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/av/stream.pyi b/av/stream.pyi index 18e6fdca9..e6188c7b0 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -1,6 +1,6 @@ from enum import Flag from fractions import Fraction -from typing import Any, Literal, cast +from typing import Literal, cast from .codec import Codec, CodecContext from .container import Container @@ -36,7 +36,7 @@ class Stream: profiles: list[str] profile: str | None index: int - options: dict[str, Any] + options: dict[str, object] time_base: Fraction | None average_rate: Fraction | None base_rate: Fraction | None From 9d223f176e1558085fd4aea91fd99fa64eb5e960 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 27 Jun 2025 21:44:21 -0400 Subject: [PATCH 547/846] Add annotations to encode_frames_with_qminmax --- av/stream.pyi | 3 +++ tests/test_encode.py | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/av/stream.pyi b/av/stream.pyi index e6188c7b0..8289a6358 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -48,6 +48,9 @@ class Stream: language: str | None type: Literal["video", "audio", "data", "subtitle", "attachment"] + # From context + codec_tag: str + class DataStream(Stream): type: Literal["data"] name: str | None diff --git a/tests/test_encode.py b/tests/test_encode.py index 23cbdc275..9284a9c82 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import io import math from fractions import Fraction @@ -409,7 +411,9 @@ def test_max_b_frames(self) -> None: assert actual_max_b_frames <= max_b_frames -def encode_frames_with_qminmax(frames: list, shape: tuple, qminmax: tuple) -> int: +def encode_frames_with_qminmax( + frames: list[VideoFrame], shape: tuple[int, int, int], qminmax: tuple[int, int] +) -> int: """ Encode a video with the given quantiser limits, and return how many enocded bytes we made in total. From ca2c8c84a22adab80b6baa5dfc266777c613aa2e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 27 Jun 2025 22:48:28 -0400 Subject: [PATCH 548/846] Drop support for MacOS 12 or under --- .github/workflows/tests.yml | 2 +- CHANGELOG.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a38af2c08..1de9f0036 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -53,7 +53,7 @@ jobs: - name: Set Minimum MacOS Target if: matrix.os == 'macos-13' || matrix.os == 'macos-14' run: | - echo "MACOSX_DEPLOYMENT_TARGET=12.0" >> $GITHUB_ENV + echo "MACOSX_DEPLOYMENT_TARGET=13.0" >> $GITHUB_ENV - name: Build wheels env: CIBW_ARCHS: ${{ matrix.arch }} diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e3cb6ecf0..b5ee063d7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -22,6 +22,7 @@ v15.0.0rc1 Major: - Make ``SubtitleStream.decode()`` return the list of subtitles directly, making the API nicer. +- Drop support for MacOS 12 or under. - Binary wheels: Remove libxml2 because of security concerns. - Binary wheels: Remove xz because lzma encoding is very niche in media decoding and lesser security concerns. From 3ebe7d25a82a80811145d0552835f04d8eba2ede Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 3 Jul 2025 01:02:31 -0400 Subject: [PATCH 549/846] pyav-ffmpeg 7.1.1-6 --- scripts/ffmpeg-7.1.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ffmpeg-7.1.json b/scripts/ffmpeg-7.1.json index 8a606aea3..82ee313d6 100644 --- a/scripts/ffmpeg-7.1.json +++ b/scripts/ffmpeg-7.1.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.1.1-5/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.1.1-6/ffmpeg-{platform}.tar.gz" } From 25f94f1b2bc9062f67739119766f1633dfee52c4 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 3 Jul 2025 03:02:38 -0400 Subject: [PATCH 550/846] Release 15.0.0 --- CHANGELOG.rst | 4 ++-- av/about.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b5ee063d7..9beb09ffd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,8 +16,8 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. -v15.0.0rc1 ----------- +v15.0.0 +------- Major: diff --git a/av/about.py b/av/about.py index 2607c1260..d5793afcb 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "15.0.0rc1" +__version__ = "15.0.0" From 2186a380ef7d7b0dcc3427d0208f31edccc821ea Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 4 Jul 2025 15:48:19 -0400 Subject: [PATCH 551/846] Remove checks in setup.py --- setup.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/setup.py b/setup.py index 16ca7f59b..a96e0a019 100644 --- a/setup.py +++ b/setup.py @@ -7,20 +7,6 @@ import subprocess import sys -if platform.system() == "Darwin": - major_version = int(platform.mac_ver()[0].split(".")[0]) - if major_version < 12: - raise OSError("You are using an EOL, unsupported, and out-of-date OS.") - - -def is_virtualenv(): - return sys.base_prefix != sys.prefix - - -if not (os.getenv("GITHUB_ACTIONS") == "true" or is_virtualenv()): - raise ValueError("You are not using a virtual environment") - - from Cython.Build import cythonize from Cython.Compiler.AutoDocTransforms import EmbedSignature from setuptools import Extension, find_packages, setup From 2c20d39559fa59004615b92b0af113c922f767c1 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 4 Jul 2025 21:23:08 -0400 Subject: [PATCH 552/846] Avoid setting channel variable --- av/audio/layout.pyx | 6 ++---- setup.py | 19 ------------------- 2 files changed, 2 insertions(+), 23 deletions(-) diff --git a/av/audio/layout.pyx b/av/audio/layout.pyx index ea259d0fd..0936075ef 100644 --- a/av/audio/layout.pyx +++ b/av/audio/layout.pyx @@ -52,16 +52,14 @@ cdef class AudioLayout: @property def channels(self): - cdef lib.AVChannel channel cdef char buf[16] cdef char buf2[128] results = [] for index in range(self.layout.nb_channels): - channel = lib.av_channel_layout_channel_from_index(&self.layout, index); - size = lib.av_channel_name(buf, sizeof(buf), channel) - 1 - size2 = lib.av_channel_description(buf2, sizeof(buf2), channel) - 1 + size = lib.av_channel_name(buf, sizeof(buf), lib.av_channel_layout_channel_from_index(&self.layout, index)) - 1 + size2 = lib.av_channel_description(buf2, sizeof(buf2), lib.av_channel_layout_channel_from_index(&self.layout, index)) - 1 results.append( AudioChannel( PyBytes_FromStringAndSize(buf, size).decode("utf-8"), diff --git a/setup.py b/setup.py index a96e0a019..ebc2146fe 100644 --- a/setup.py +++ b/setup.py @@ -26,21 +26,6 @@ old_embed_signature = EmbedSignature._embed_signature -def insert_enum_in_generated_files(source): - # Work around Cython failing to add `enum` to `AVChannel` type. - # TODO: Make Cython bug report - if source.endswith(".c"): - with open(source, "r") as file: - content = file.read() - - # Replace "AVChannel __pyx_v_channel;" with "enum AVChannel __pyx_v_channel;" - modified_content = re.sub( - r"\b(? Date: Fri, 4 Jul 2025 22:09:54 -0400 Subject: [PATCH 553/846] Remove attachment docs --- docs/api/attachments.rst | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 docs/api/attachments.rst diff --git a/docs/api/attachments.rst b/docs/api/attachments.rst deleted file mode 100644 index 6e59a202b..000000000 --- a/docs/api/attachments.rst +++ /dev/null @@ -1,8 +0,0 @@ - -Attachments -=========== - -.. automodule:: av.attachments.stream - - .. autoclass:: AttachmentStream - :members: From cf6d0520c3a806b6ab9ca5caf3398ac47c809aae Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 7 Jul 2025 11:53:20 -0400 Subject: [PATCH 554/846] Use AVPacket.time_base --- av/codec/context.pyx | 4 ++-- av/container/input.pyx | 4 ++-- av/packet.pxd | 6 ------ av/packet.py | 15 +++++++-------- include/libavcodec/avcodec.pxd | 6 +----- 5 files changed, 12 insertions(+), 23 deletions(-) diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 5ca8f24a4..49087e779 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -428,7 +428,7 @@ cdef class CodecContext: # # NOTE: if the CodecContext's time_base is altered during encoding, all bets # are off! - packet._time_base = self.ptr.time_base + packet.ptr.time_base = self.ptr.time_base cpdef decode(self, Packet packet=None): """Decode a list of :class:`.Frame` from the given :class:`.Packet`. @@ -469,7 +469,7 @@ cdef class CodecContext: # TODO: Somehow get this from the stream so we can not pass the # packet here (because flushing packets are bogus). if packet is not None: - frame._time_base = packet._time_base + frame._time_base = packet.ptr.time_base @property def name(self): diff --git a/av/container/input.pyx b/av/container/input.pyx index 1ba4750d7..943081e15 100644 --- a/av/container/input.pyx +++ b/av/container/input.pyx @@ -183,7 +183,7 @@ cdef class InputContainer(Container): if packet.ptr.stream_index < len(self.streams): packet._stream = self.streams[packet.ptr.stream_index] # Keep track of this so that remuxing is easier. - packet._time_base = packet._stream.ptr.time_base + packet.ptr.time_base = packet._stream.ptr.time_base yield packet # Flush! @@ -191,7 +191,7 @@ cdef class InputContainer(Container): if include_stream[i]: packet = Packet() packet._stream = self.streams[i] - packet._time_base = packet._stream.ptr.time_base + packet.ptr.time_base = packet._stream.ptr.time_base yield packet finally: diff --git a/av/packet.pxd b/av/packet.pxd index ca21e6b76..f1517a3d4 100644 --- a/av/packet.pxd +++ b/av/packet.pxd @@ -6,15 +6,9 @@ from av.stream cimport Stream cdef class Packet(Buffer): - cdef lib.AVPacket* ptr - cdef Stream _stream - - # We track our own time. - cdef lib.AVRational _time_base cdef _rebase_time(self, lib.AVRational) - # Hold onto the original reference. cdef ByteSource source cdef size_t _buffer_size(self) diff --git a/av/packet.py b/av/packet.py index c49085c57..81f1aaa4d 100644 --- a/av/packet.py +++ b/av/packet.py @@ -65,16 +65,15 @@ def _rebase_time(self, dst: lib.AVRational): if not dst.num: raise ValueError("Cannot rebase to zero time.") - if not self._time_base.num: - self._time_base = dst + if not self.ptr.time_base.num: + self.ptr.time_base = dst return - if self._time_base.num == dst.num and self._time_base.den == dst.den: + if self.ptr.time_base.num == dst.num and self.ptr.time_base.den == dst.den: return - lib.av_packet_rescale_ts(self.ptr, self._time_base, dst) - - self._time_base = dst + lib.av_packet_rescale_ts(self.ptr, self.ptr.time_base, dst) + self.ptr.time_base = dst def decode(self): """ @@ -106,11 +105,11 @@ def time_base(self): :type: fractions.Fraction """ - return avrational_to_fraction(cython.address(self._time_base)) + return avrational_to_fraction(cython.address(self.ptr.time_base)) @time_base.setter def time_base(self, value): - to_avrational(value, cython.address(self._time_base)) + to_avrational(value, cython.address(self.ptr.time_base)) @property def pts(self): diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 0c8713cf8..134ae6f07 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -466,19 +466,15 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef AVFrame* avcodec_alloc_frame() cdef struct AVPacket: - int64_t pts int64_t dts uint8_t *data - + AVRational time_base int size int stream_index int flags - int duration - int64_t pos - void *opaque AVBufferRef *opaque_ref From 93a100548daacd7ec01824b106440856f08bb153 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 16 Jul 2025 12:24:18 -0600 Subject: [PATCH 555/846] Install cython from scientific-python nightlies in smoke tests --- .github/workflows/smoke.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index e83adf62d..d6637ce62 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -127,14 +127,14 @@ jobs: pillow \ pytest \ python=${{ matrix.config.python }} \ - setuptools + setuptools \ + cython - name: Build shell: bash run: | . $CONDA/etc/profile.d/conda.sh conda activate pyav - pip install 'Cython==3.1.0a1' python scripts\\fetch-vendor.py --config-file scripts\\ffmpeg-${{ matrix.config.ffmpeg }}.json $CONDA_PREFIX\\Library python scripts\\comptime.py ${{ matrix.config.ffmpeg }} python setup.py build_ext --inplace --ffmpeg-dir=$CONDA_PREFIX\\Library From 8e973abffdeb7149e25e15019ea9ea20f12a30bd Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Mon, 21 Jul 2025 18:25:09 -0600 Subject: [PATCH 556/846] Add set_image for updating the image content of an existing frame --- av/video/frame.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/av/video/frame.py b/av/video/frame.py index 970ecc4b8..0d60fd8e9 100644 --- a/av/video/frame.py +++ b/av/video/frame.py @@ -610,16 +610,22 @@ def to_ndarray(self, channel_last=False, **kwargs): f"Conversion to numpy array with format `{frame.format.name}` is not yet supported" ) - @staticmethod - def from_image(img): + def set_image(self, img): """ - Construct a frame from a ``PIL.Image``. + Update content from a ``PIL.Image``. """ if img.mode != "RGB": img = img.convert("RGB") + copy_array_to_plane(img, self.planes[0], 3) + + @staticmethod + def from_image(img): + """ + Construct a frame from a ``PIL.Image``. + """ frame: VideoFrame = VideoFrame(img.size[0], img.size[1], "rgb24") - copy_array_to_plane(img, frame.planes[0], 3) + frame.set_image(img) return frame From e2ec340cab48201fc3a72aa029fdae4035f3c4ed Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 21 Jul 2025 21:24:05 -0400 Subject: [PATCH 557/846] Index opaque by id and make `key_frame` writable --- CHANGELOG.rst | 7 ++++ av/{frame.pyx => frame.py} | 85 +++++++++++++++++++++----------------- av/frame.pyi | 4 +- av/opaque.pxd | 6 +-- av/opaque.pyx | 44 +++++++++++++------- tests/test_videoframe.py | 15 +++++++ 6 files changed, 102 insertions(+), 59 deletions(-) rename av/{frame.pyx => frame.py} (67%) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9beb09ffd..395910dc4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,13 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. +v15.1.0 +------- + +Features: + +- Make the `Frame.key_frame` flag writable. + v15.0.0 ------- diff --git a/av/frame.pyx b/av/frame.py similarity index 67% rename from av/frame.pyx rename to av/frame.py index fefdd2dee..8100c39d6 100644 --- a/av/frame.pyx +++ b/av/frame.py @@ -1,11 +1,13 @@ -from av.error cimport err_check -from av.opaque cimport opaque_container -from av.utils cimport avrational_to_fraction, to_avrational +import cython +from cython.cimports.av.error import err_check +from cython.cimports.av.opaque import opaque_container +from cython.cimports.av.utils import avrational_to_fraction, to_avrational from av.sidedata.sidedata import SideDataContainer -cdef class Frame: +@cython.cclass +class Frame: """ Base class for audio and video frames. @@ -13,20 +15,19 @@ """ def __cinit__(self, *args, **kwargs): - with nogil: + with cython.nogil: self.ptr = lib.av_frame_alloc() def __dealloc__(self): - with nogil: - # This calls av_frame_unref, and then frees the pointer. - # Thats it. - lib.av_frame_free(&self.ptr) + with cython.nogil: + lib.av_frame_free(cython.address(self.ptr)) def __repr__(self): - return f"av.{self.__class__.__name__} pts={self.pts} at 0x{id(self):x}>" + return f"" - cdef _copy_internal_attributes(self, Frame source, bint data_layout=True): - """Mimic another frame.""" + @cython.cfunc + def _copy_internal_attributes(self, source: Frame, data_layout: cython.bint = True): + # Mimic another frame self._time_base = source._time_base lib.av_frame_copy_props(self.ptr, source.ptr) if data_layout: @@ -36,10 +37,12 @@ def __repr__(self): self.ptr.height = source.ptr.height self.ptr.ch_layout = source.ptr.ch_layout - cdef _init_user_attributes(self): + @cython.cfunc + def _init_user_attributes(self): pass # Dummy to match the API of the others. - cdef _rebase_time(self, lib.AVRational dst): + @cython.cfunc + def _rebase_time(self, dst: lib.AVRational): if not dst.num: raise ValueError("Cannot rebase to zero time.") @@ -54,7 +57,9 @@ def __repr__(self): self.ptr.pts = lib.av_rescale_q(self.ptr.pts, self._time_base, dst) if self.ptr.duration != 0: - self.ptr.duration = lib.av_rescale_q(self.ptr.duration, self._time_base, dst) + self.ptr.duration = lib.av_rescale_q( + self.ptr.duration, self._time_base, dst + ) self._time_base = dst @@ -65,7 +70,7 @@ def dts(self): (if frame threading isn't used) This is also the Presentation time of this frame calculated from only :attr:`.Packet.dts` values without pts values. - :type: int + :type: int | None """ if self.ptr.pkt_dts == lib.AV_NOPTS_VALUE: return None @@ -85,7 +90,7 @@ def pts(self): This is the time at which the frame should be shown to the user. - :type: int + :type: int | None """ if self.ptr.pts == lib.AV_NOPTS_VALUE: return None @@ -105,16 +110,11 @@ def duration(self): :type: int """ - if self.ptr.duration == 0: - return None return self.ptr.duration @duration.setter def duration(self, value): - if value is None: - self.ptr.duration = 0 - else: - self.ptr.duration = value + self.ptr.duration = value @property def time(self): @@ -123,26 +123,25 @@ def time(self): This is the time at which the frame should be shown to the user. - :type: float + :type: float | None """ if self.ptr.pts == lib.AV_NOPTS_VALUE: return None - else: - return float(self.ptr.pts) * self._time_base.num / self._time_base.den + return float(self.ptr.pts) * self._time_base.num / self._time_base.den @property def time_base(self): """ The unit of time (in fractional seconds) in which timestamps are expressed. - :type: fractions.Fraction + :type: fractions.Fraction | None """ if self._time_base.num: - return avrational_to_fraction(&self._time_base) + return avrational_to_fraction(cython.address(self._time_base)) @time_base.setter def time_base(self, value): - to_avrational(value, &self._time_base) + to_avrational(value, cython.address(self._time_base)) @property def is_corrupt(self): @@ -151,7 +150,9 @@ def is_corrupt(self): :type: bool """ - return self.ptr.decode_error_flags != 0 or bool(self.ptr.flags & lib.AV_FRAME_FLAG_CORRUPT) + return self.ptr.decode_error_flags != 0 or bool( + self.ptr.flags & lib.AV_FRAME_FLAG_CORRUPT + ) @property def key_frame(self): @@ -162,6 +163,13 @@ def key_frame(self): """ return bool(self.ptr.flags & lib.AV_FRAME_FLAG_KEY) + @key_frame.setter + def key_frame(self, v): + # PyAV makes no guarantees this does anything. + if v: + self.ptr.flags |= lib.AV_FRAME_FLAG_KEY + else: + self.ptr.flags &= ~lib.AV_FRAME_FLAG_KEY @property def side_data(self): @@ -174,20 +182,19 @@ def make_writable(self): Ensures that the frame data is writable. Copy the data to new buffer if it is not. This is a wrapper around :ffmpeg:`av_frame_make_writable`. """ - cdef int ret - - ret = lib.av_frame_make_writable(self.ptr) + ret: cython.int = lib.av_frame_make_writable(self.ptr) err_check(ret) @property def opaque(self): - if self.ptr.opaque_ref is not NULL: - return opaque_container.get( self.ptr.opaque_ref.data) + if self.ptr.opaque_ref is not cython.NULL: + return opaque_container.get( + cython.cast(cython.p_char, self.ptr.opaque_ref.data) + ) @opaque.setter def opaque(self, v): - lib.av_buffer_unref(&self.ptr.opaque_ref) + lib.av_buffer_unref(cython.address(self.ptr.opaque_ref)) - if v is None: - return - self.ptr.opaque_ref = opaque_container.add(v) + if v is not None: + self.ptr.opaque_ref = opaque_container.add(v) diff --git a/av/frame.pyi b/av/frame.pyi index 38a273afc..f085fc0f4 100644 --- a/av/frame.pyi +++ b/av/frame.pyi @@ -9,8 +9,8 @@ class SideData(TypedDict, total=False): class Frame: dts: int | None pts: int | None - duration: int | None - time_base: Fraction + duration: int + time_base: Fraction | None side_data: SideData opaque: object @property diff --git a/av/opaque.pxd b/av/opaque.pxd index f5c38d7fa..76174931f 100644 --- a/av/opaque.pxd +++ b/av/opaque.pxd @@ -2,11 +2,11 @@ cimport libav as lib cdef class OpaqueContainer: - cdef dict _by_name + cdef dict _objects cdef lib.AVBufferRef *add(self, object v) - cdef object get(self, bytes name) - cdef object pop(self, bytes name) + cdef object get(self, char *name) + cdef object pop(self, char *name) cdef OpaqueContainer opaque_container diff --git a/av/opaque.pyx b/av/opaque.pyx index 1e6769898..619169edb 100644 --- a/av/opaque.pyx +++ b/av/opaque.pyx @@ -1,7 +1,6 @@ cimport libav as lib from libc.stdint cimport uint8_t - -from uuid import uuid4 +from libc.string cimport memcpy cdef void key_free(void *opaque, uint8_t *data) noexcept nogil: @@ -11,22 +10,37 @@ cdef void key_free(void *opaque, uint8_t *data) noexcept nogil: cdef class OpaqueContainer: - """A container that holds references to Python objects, indexed by uuid""" - def __cinit__(self): - self._by_name = {} + self._objects = {} + + cdef lib.AVBufferRef *add(self, object v): + # Use object's memory address as key + cdef size_t key = id(v) + self._objects[key] = v + + cdef uint8_t *data = lib.av_malloc(sizeof(size_t)) + if data == NULL: + raise MemoryError("Failed to allocate memory for key") + + memcpy(data, &key, sizeof(size_t)) + + # Create the buffer with our free callback + cdef lib.AVBufferRef *buffer_ref = lib.av_buffer_create( + data, sizeof(size_t), key_free, NULL, 0 + ) + + if buffer_ref == NULL: + raise MemoryError("Failed to create AVBufferRef") - cdef lib.AVBufferRef *add(self, v): - cdef bytes uuid = str(uuid4()).encode("utf-8") - cdef lib.AVBufferRef *ref = lib.av_buffer_create(uuid, len(uuid), &key_free, NULL, 0) - self._by_name[uuid] = v - return ref + return buffer_ref - cdef object get(self, bytes name): - return self._by_name.get(name) + cdef object get(self, char *name): + cdef size_t key = (name)[0] + return self._objects.get(key) - cdef object pop(self, bytes name): - return self._by_name.pop(name) + cdef object pop(self, char *name): + cdef size_t key = (name)[0] + return self._objects.pop(key, None) -cdef opaque_container = OpaqueContainer() +cdef OpaqueContainer opaque_container = OpaqueContainer() diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 9c9773ae8..90d91ab45 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -122,6 +122,21 @@ def test_memoryview_read() -> None: assert mem[:7] == b"0.234xx" +def test_opaque() -> None: + frame = VideoFrame(640, 480, "rgb24") + frame.opaque = 3 + assert frame.opaque == 3 + frame.opaque = "a" + assert frame.opaque == "a" + + greeting = "Hello World!" + frame.opaque = greeting + assert frame.opaque is greeting + + frame.opaque = None + assert frame.opaque is None + + def test_interpolation() -> None: container = av.open(fate_png()) for _ in container.decode(video=0): From 3cb9b6383a2bdc626f005b5b13789d4c75fabfb5 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 21 Jul 2025 22:45:35 -0400 Subject: [PATCH 558/846] Fix opaque container on PyPy --- av/opaque.pyx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/av/opaque.pyx b/av/opaque.pyx index 619169edb..0c2a4507b 100644 --- a/av/opaque.pyx +++ b/av/opaque.pyx @@ -1,5 +1,5 @@ cimport libav as lib -from libc.stdint cimport uint8_t +from libc.stdint cimport intptr_t, uint8_t from libc.string cimport memcpy @@ -15,18 +15,18 @@ cdef class OpaqueContainer: cdef lib.AVBufferRef *add(self, object v): # Use object's memory address as key - cdef size_t key = id(v) + cdef intptr_t key = id(v) self._objects[key] = v - cdef uint8_t *data = lib.av_malloc(sizeof(size_t)) + cdef uint8_t *data = lib.av_malloc(sizeof(intptr_t)) if data == NULL: raise MemoryError("Failed to allocate memory for key") - memcpy(data, &key, sizeof(size_t)) + memcpy(data, &key, sizeof(intptr_t)) # Create the buffer with our free callback cdef lib.AVBufferRef *buffer_ref = lib.av_buffer_create( - data, sizeof(size_t), key_free, NULL, 0 + data, sizeof(intptr_t), key_free, NULL, 0 ) if buffer_ref == NULL: @@ -35,11 +35,11 @@ cdef class OpaqueContainer: return buffer_ref cdef object get(self, char *name): - cdef size_t key = (name)[0] + cdef intptr_t key = (name)[0] return self._objects.get(key) cdef object pop(self, char *name): - cdef size_t key = (name)[0] + cdef intptr_t key = (name)[0] return self._objects.pop(key, None) From 2f6c495778c083f7cb9f712beef025734cff9fb6 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 4 Aug 2025 16:28:03 -0400 Subject: [PATCH 559/846] Replace FFmpeg version macro with direct API call --- av/filter/pad.pyx | 4 +--- include/libavfilter/avfilter.pxd | 15 +-------------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/av/filter/pad.pyx b/av/filter/pad.pyx index 873b31b04..cf889ea69 100644 --- a/av/filter/pad.pyx +++ b/av/filter/pad.pyx @@ -72,9 +72,7 @@ cdef tuple alloc_filter_pads(Filter filter, const lib.AVFilterPad *ptr, bint is_ cdef int i = 0 cdef int count if context is None: - # This is a custom function defined using a macro in avfilter.pxd. Its usage - # can be changed after we stop supporting FFmpeg < 5.0. - count = lib.pyav_get_num_pads(filter.ptr, not is_input, ptr) + count = lib.avfilter_filter_pad_count(filter.ptr, not is_input) else: count = (context.ptr.nb_inputs if is_input else context.ptr.nb_outputs) diff --git a/include/libavfilter/avfilter.pxd b/include/libavfilter/avfilter.pxd index dd3e91ddf..90f349005 100644 --- a/include/libavfilter/avfilter.pxd +++ b/include/libavfilter/avfilter.pxd @@ -1,14 +1,4 @@ - cdef extern from "libavfilter/avfilter.h" nogil: - """ - #if (LIBAVFILTER_VERSION_INT >= 525156) - // avfilter_filter_pad_count is available since version 8.3.100 of libavfilter (FFmpeg 5.0) - #define _avfilter_get_num_pads(filter, is_output, pads) (avfilter_filter_pad_count(filter, is_output)) - #else - // avfilter_filter_pad_count has been deprecated as of version 8.3.100 of libavfilter (FFmpeg 5.0) - #define _avfilter_get_num_pads(filter, is_output, pads) (avfilter_pad_count(pads)) - #endif - """ cdef int avfilter_version() cdef char* avfilter_configuration() cdef char* avfilter_license() @@ -20,10 +10,9 @@ cdef extern from "libavfilter/avfilter.h" nogil: const char* avfilter_pad_get_name(const AVFilterPad *pads, int index) AVMediaType avfilter_pad_get_type(const AVFilterPad *pads, int index) - int pyav_get_num_pads "_avfilter_get_num_pads" (const AVFilter *filter, int is_output, const AVFilterPad *pads) + cdef unsigned avfilter_filter_pad_count(const AVFilter *filter, int is_output) cdef struct AVFilter: - AVClass *priv_class const char *name @@ -48,7 +37,6 @@ cdef extern from "libavfilter/avfilter.h" nogil: cdef struct AVFilterLink # Defined later. cdef struct AVFilterContext: - AVClass *av_class AVFilter *filter @@ -68,7 +56,6 @@ cdef extern from "libavfilter/avfilter.h" nogil: cdef AVClass* avfilter_get_class() cdef struct AVFilterLink: - AVFilterContext *src AVFilterPad *srcpad AVFilterContext *dst From 3de331d5621a1d491ba0d45fc1a096c7cbaa10d6 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 4 Aug 2025 17:19:32 -0400 Subject: [PATCH 560/846] Combine pad and link --- av/filter/context.pyi | 4 -- av/filter/context.pyx | 2 +- av/filter/filter.pyi | 5 --- av/filter/filter.pyx | 2 +- av/filter/link.pxd | 20 +++++++++- av/filter/link.pyi | 5 +-- av/filter/link.pyx | 77 +++++++++++++++++++++++++++++++++++- av/filter/pad.pxd | 23 ----------- av/filter/pad.pyi | 10 ----- av/filter/pad.pyx | 90 ------------------------------------------- tests/test_filters.py | 6 --- 11 files changed, 97 insertions(+), 147 deletions(-) delete mode 100644 av/filter/pad.pxd delete mode 100644 av/filter/pad.pyi delete mode 100644 av/filter/pad.pyx diff --git a/av/filter/context.pyi b/av/filter/context.pyi index 7c00087a9..13ee480c2 100644 --- a/av/filter/context.pyi +++ b/av/filter/context.pyi @@ -1,12 +1,8 @@ from av.filter import Graph from av.frame import Frame -from .pad import FilterContextPad - class FilterContext: name: str | None - inputs: tuple[FilterContextPad, ...] - outputs: tuple[FilterContextPad, ...] def init(self, args: str | None = None, **kwargs: str | None) -> None: ... def link_to( diff --git a/av/filter/context.pyx b/av/filter/context.pyx index b820d3d18..ba5981ff1 100644 --- a/av/filter/context.pyx +++ b/av/filter/context.pyx @@ -4,7 +4,7 @@ from av.audio.frame cimport alloc_audio_frame from av.dictionary cimport _Dictionary from av.dictionary import Dictionary from av.error cimport err_check -from av.filter.pad cimport alloc_filter_pads +from av.filter.link cimport alloc_filter_pads from av.frame cimport Frame from av.utils cimport avrational_to_fraction from av.video.frame cimport alloc_video_frame diff --git a/av/filter/filter.pyi b/av/filter/filter.pyi index 2751e973c..42d9cd7c9 100644 --- a/av/filter/filter.pyi +++ b/av/filter/filter.pyi @@ -1,12 +1,9 @@ from av.descriptor import Descriptor from av.option import Option -from .pad import FilterPad - class Filter: name: str description: str - descriptor: Descriptor options: tuple[Option, ...] | None flags: int @@ -15,8 +12,6 @@ class Filter: timeline_support: bool slice_threads: bool command_support: bool - inputs: tuple[FilterPad, ...] - outputs: tuple[FilterPad, ...] def __init__(self, name: str) -> None: ... diff --git a/av/filter/filter.pyx b/av/filter/filter.pyx index d4880dc15..5f4963b2a 100644 --- a/av/filter/filter.pyx +++ b/av/filter/filter.pyx @@ -1,7 +1,7 @@ cimport libav as lib from av.descriptor cimport wrap_avclass -from av.filter.pad cimport alloc_filter_pads +from av.filter.link cimport alloc_filter_pads cdef object _cinit_sentinel = object() diff --git a/av/filter/link.pxd b/av/filter/link.pxd index a6a4b1c09..08771362b 100644 --- a/av/filter/link.pxd +++ b/av/filter/link.pxd @@ -1,11 +1,12 @@ cimport libav as lib +from av.filter.context cimport FilterContext +from av.filter.filter cimport Filter from av.filter.graph cimport Graph -from av.filter.pad cimport FilterContextPad +from av.filter.link cimport FilterContextPad, FilterLink cdef class FilterLink: - cdef readonly Graph graph cdef lib.AVFilterLink *ptr @@ -14,3 +15,18 @@ cdef class FilterLink: cdef FilterLink wrap_filter_link(Graph graph, lib.AVFilterLink *ptr) + +cdef class FilterPad: + cdef readonly Filter filter + cdef readonly FilterContext context + cdef readonly bint is_input + cdef readonly int index + + cdef const lib.AVFilterPad *base_ptr + + +cdef class FilterContextPad(FilterPad): + cdef FilterLink _link + + +cdef tuple alloc_filter_pads(Filter, const lib.AVFilterPad *ptr, bint is_input, FilterContext context=?) diff --git a/av/filter/link.pyi b/av/filter/link.pyi index dd420ad91..9d199a272 100644 --- a/av/filter/link.pyi +++ b/av/filter/link.pyi @@ -1,5 +1,2 @@ -from .pad import FilterContextPad - class FilterLink: - input: FilterContextPad - output: FilterContextPad + pass diff --git a/av/filter/link.pyx b/av/filter/link.pyx index 78b7da30f..905082d15 100644 --- a/av/filter/link.pyx +++ b/av/filter/link.pyx @@ -7,7 +7,6 @@ cdef _cinit_sentinel = object() cdef class FilterLink: - def __cinit__(self, sentinel): if sentinel is not _cinit_sentinel: raise RuntimeError("cannot instantiate FilterLink") @@ -51,3 +50,79 @@ cdef FilterLink wrap_filter_link(Graph graph, lib.AVFilterLink *ptr): link.graph = graph link.ptr = ptr return link + + + +cdef class FilterPad: + def __cinit__(self, sentinel): + if sentinel is not _cinit_sentinel: + raise RuntimeError("cannot construct FilterPad") + + def __repr__(self): + _filter = self.filter.name + _io = "inputs" if self.is_input else "outputs" + + return f"" + + @property + def is_output(self): + return not self.is_input + + @property + def name(self): + return lib.avfilter_pad_get_name(self.base_ptr, self.index) + + +cdef class FilterContextPad(FilterPad): + def __repr__(self): + _filter = self.filter.name + _io = "inputs" if self.is_input else "outputs" + context = self.context.name + + return f"" + + @property + def link(self): + if self._link: + return self._link + cdef lib.AVFilterLink **links = self.context.ptr.inputs if self.is_input else self.context.ptr.outputs + cdef lib.AVFilterLink *link = links[self.index] + if not link: + return + self._link = wrap_filter_link(self.context.graph, link) + return self._link + + @property + def linked(self): + cdef FilterLink link = self.link + if link: + return link.input if self.is_input else link.output + + +cdef tuple alloc_filter_pads(Filter filter, const lib.AVFilterPad *ptr, bint is_input, FilterContext context=None): + if not ptr: + return () + + pads = [] + + # We need to be careful and check our bounds if we know what they are, + # since the arrays on a AVFilterContext are not NULL terminated. + cdef int i = 0 + cdef int count + if context is None: + count = lib.avfilter_filter_pad_count(filter.ptr, not is_input) + else: + count = (context.ptr.nb_inputs if is_input else context.ptr.nb_outputs) + + cdef FilterPad pad + while (i < count): + pad = FilterPad(_cinit_sentinel) if context is None else FilterContextPad(_cinit_sentinel) + pads.append(pad) + pad.filter = filter + pad.context = context + pad.is_input = is_input + pad.base_ptr = ptr + pad.index = i + i += 1 + + return tuple(pads) diff --git a/av/filter/pad.pxd b/av/filter/pad.pxd deleted file mode 100644 index 15ac950fc..000000000 --- a/av/filter/pad.pxd +++ /dev/null @@ -1,23 +0,0 @@ -cimport libav as lib - -from av.filter.context cimport FilterContext -from av.filter.filter cimport Filter -from av.filter.link cimport FilterLink - - -cdef class FilterPad: - - cdef readonly Filter filter - cdef readonly FilterContext context - cdef readonly bint is_input - cdef readonly int index - - cdef const lib.AVFilterPad *base_ptr - - -cdef class FilterContextPad(FilterPad): - - cdef FilterLink _link - - -cdef tuple alloc_filter_pads(Filter, const lib.AVFilterPad *ptr, bint is_input, FilterContext context=?) diff --git a/av/filter/pad.pyi b/av/filter/pad.pyi deleted file mode 100644 index 1a6c9bda6..000000000 --- a/av/filter/pad.pyi +++ /dev/null @@ -1,10 +0,0 @@ -from .link import FilterLink - -class FilterPad: - is_output: bool - name: str - type: str - -class FilterContextPad(FilterPad): - link: FilterLink | None - linked: FilterContextPad | None diff --git a/av/filter/pad.pyx b/av/filter/pad.pyx deleted file mode 100644 index cf889ea69..000000000 --- a/av/filter/pad.pyx +++ /dev/null @@ -1,90 +0,0 @@ -from av.filter.link cimport wrap_filter_link - - -cdef object _cinit_sentinel = object() - - -cdef class FilterPad: - def __cinit__(self, sentinel): - if sentinel is not _cinit_sentinel: - raise RuntimeError("cannot construct FilterPad") - - def __repr__(self): - _filter = self.filter.name - _io = "inputs" if self.is_input else "outputs" - - return f"" - - @property - def is_output(self): - return not self.is_input - - @property - def name(self): - return lib.avfilter_pad_get_name(self.base_ptr, self.index) - - @property - def type(self): - """ - The media type of this filter pad. - - Examples: `'audio'`, `'video'`, `'subtitle'`. - - :type: str - """ - return lib.av_get_media_type_string(lib.avfilter_pad_get_type(self.base_ptr, self.index)) - - -cdef class FilterContextPad(FilterPad): - def __repr__(self): - _filter = self.filter.name - _io = "inputs" if self.is_input else "outputs" - context = self.context.name - - return f"" - - @property - def link(self): - if self._link: - return self._link - cdef lib.AVFilterLink **links = self.context.ptr.inputs if self.is_input else self.context.ptr.outputs - cdef lib.AVFilterLink *link = links[self.index] - if not link: - return - self._link = wrap_filter_link(self.context.graph, link) - return self._link - - @property - def linked(self): - cdef FilterLink link = self.link - if link: - return link.input if self.is_input else link.output - - -cdef tuple alloc_filter_pads(Filter filter, const lib.AVFilterPad *ptr, bint is_input, FilterContext context=None): - if not ptr: - return () - - pads = [] - - # We need to be careful and check our bounds if we know what they are, - # since the arrays on a AVFilterContext are not NULL terminated. - cdef int i = 0 - cdef int count - if context is None: - count = lib.avfilter_filter_pad_count(filter.ptr, not is_input) - else: - count = (context.ptr.nb_inputs if is_input else context.ptr.nb_outputs) - - cdef FilterPad pad - while (i < count): - pad = FilterPad(_cinit_sentinel) if context is None else FilterContextPad(_cinit_sentinel) - pads.append(pad) - pad.filter = filter - pad.context = context - pad.is_input = is_input - pad.base_ptr = ptr - pad.index = i - i += 1 - - return tuple(pads) diff --git a/tests/test_filters.py b/tests/test_filters.py index 7722de735..886c22a01 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -51,18 +51,12 @@ def test_filter_descriptor(self) -> None: assert f.name == "testsrc" assert f.description == "Generate test pattern." assert not f.dynamic_inputs - assert len(f.inputs) == 0 assert not f.dynamic_outputs - assert len(f.outputs) == 1 - assert f.outputs[0].name == "default" - assert f.outputs[0].type == "video" def test_dynamic_filter_descriptor(self): f = Filter("split") assert not f.dynamic_inputs - assert len(f.inputs) == 1 assert f.dynamic_outputs - assert len(f.outputs) == 0 def test_generator_graph(self): graph = Graph() From eaa675d67ee8802ea651c49cadf3da5313ded417 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 4 Aug 2025 17:56:51 -0400 Subject: [PATCH 561/846] Remove FilterFlags --- av/filter/__init__.py | 2 +- av/filter/filter.pyi | 4 ---- av/filter/filter.pyx | 24 ------------------------ include/libavfilter/avfilter.pxd | 7 ------- tests/test_filters.py | 7 ------- 5 files changed, 1 insertion(+), 43 deletions(-) diff --git a/av/filter/__init__.py b/av/filter/__init__.py index 5dd4430d4..b63a95f25 100644 --- a/av/filter/__init__.py +++ b/av/filter/__init__.py @@ -1,3 +1,3 @@ -from .filter import Filter, FilterFlags, filter_descriptor, filters_available +from .filter import Filter, filter_descriptor, filters_available from .graph import Graph from .loudnorm import stats diff --git a/av/filter/filter.pyi b/av/filter/filter.pyi index 42d9cd7c9..e9f9737f3 100644 --- a/av/filter/filter.pyi +++ b/av/filter/filter.pyi @@ -7,10 +7,6 @@ class Filter: descriptor: Descriptor options: tuple[Option, ...] | None flags: int - dynamic_inputs: bool - dynamic_outputs: bool - timeline_support: bool - slice_threads: bool command_support: bool def __init__(self, name: str) -> None: ... diff --git a/av/filter/filter.pyx b/av/filter/filter.pyx index 5f4963b2a..d66b2e46a 100644 --- a/av/filter/filter.pyx +++ b/av/filter/filter.pyx @@ -13,14 +13,6 @@ cdef Filter wrap_filter(const lib.AVFilter *ptr): return filter_ -cpdef enum FilterFlags: - DYNAMIC_INPUTS = lib.AVFILTER_FLAG_DYNAMIC_INPUTS - DYNAMIC_OUTPUTS = lib.AVFILTER_FLAG_DYNAMIC_OUTPUTS - SLICE_THREADS = lib.AVFILTER_FLAG_SLICE_THREADS - SUPPORT_TIMELINE_GENERIC = lib.AVFILTER_FLAG_SUPPORT_TIMELINE_GENERIC - SUPPORT_TIMELINE_INTERNAL = lib.AVFILTER_FLAG_SUPPORT_TIMELINE_INTERNAL - - cdef class Filter: def __cinit__(self, name): if name is _cinit_sentinel: @@ -55,22 +47,6 @@ cdef class Filter: def flags(self): return self.ptr.flags - @property - def dynamic_inputs(self): - return bool(self.ptr.flags & lib.AVFILTER_FLAG_DYNAMIC_INPUTS) - - @property - def dynamic_outputs(self): - return bool(self.ptr.flags & lib.AVFILTER_FLAG_DYNAMIC_OUTPUTS) - - @property - def timeline_support(self): - return bool(self.ptr.flags & lib.AVFILTER_FLAG_SUPPORT_TIMELINE_GENERIC) - - @property - def slice_threads(self): - return bool(self.ptr.flags & lib.AVFILTER_FLAG_SLICE_THREADS) - @property def command_support(self): return self.ptr.process_command != NULL diff --git a/include/libavfilter/avfilter.pxd b/include/libavfilter/avfilter.pxd index 90f349005..de7bfb445 100644 --- a/include/libavfilter/avfilter.pxd +++ b/include/libavfilter/avfilter.pxd @@ -24,13 +24,6 @@ cdef extern from "libavfilter/avfilter.h" nogil: const AVFilterPad *outputs int (*process_command)(AVFilterContext *, const char *cmd, const char *arg, char *res, int res_len, int flags) - cdef enum: - AVFILTER_FLAG_DYNAMIC_INPUTS - AVFILTER_FLAG_DYNAMIC_OUTPUTS - AVFILTER_FLAG_SLICE_THREADS - AVFILTER_FLAG_SUPPORT_TIMELINE_GENERIC - AVFILTER_FLAG_SUPPORT_TIMELINE_INTERNAL - cdef AVFilter* avfilter_get_by_name(const char *name) cdef const AVFilter* av_filter_iterate(void **opaque) diff --git a/tests/test_filters.py b/tests/test_filters.py index 886c22a01..bd74a633a 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -50,13 +50,6 @@ def test_filter_descriptor(self) -> None: f = Filter("testsrc") assert f.name == "testsrc" assert f.description == "Generate test pattern." - assert not f.dynamic_inputs - assert not f.dynamic_outputs - - def test_dynamic_filter_descriptor(self): - f = Filter("split") - assert not f.dynamic_inputs - assert f.dynamic_outputs def test_generator_graph(self): graph = Graph() From 22d6a7ac30d10eba804cb59672d8bb8dd1623998 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 4 Aug 2025 19:28:45 -0400 Subject: [PATCH 562/846] Remove unused swr functions --- include/libswresample/swresample.pxd | 35 ---------------------------- 1 file changed, 35 deletions(-) diff --git a/include/libswresample/swresample.pxd b/include/libswresample/swresample.pxd index 65b8314df..5bd8a8629 100644 --- a/include/libswresample/swresample.pxd +++ b/include/libswresample/swresample.pxd @@ -1,39 +1,4 @@ -from libc.stdint cimport int64_t, uint8_t - - cdef extern from "libswresample/swresample.h" nogil: - cdef int swresample_version() cdef char* swresample_configuration() cdef char* swresample_license() - - cdef struct SwrContext: - pass - - cdef SwrContext* swr_alloc_set_opts( - SwrContext *ctx, - int64_t out_ch_layout, - AVSampleFormat out_sample_fmt, - int out_sample_rate, - int64_t in_ch_layout, - AVSampleFormat in_sample_fmt, - int in_sample_rate, - int log_offset, - void *log_ctx # logging context, can be NULL - ) - - cdef int swr_convert( - SwrContext *ctx, - uint8_t ** out_buffer, - int out_count, - uint8_t **in_buffer, - int in_count - ) - # Gets the delay the next input sample will - # experience relative to the next output sample. - cdef int64_t swr_get_delay(SwrContext *s, int64_t base) - - cdef SwrContext* swr_alloc() - cdef int swr_init(SwrContext* ctx) - cdef void swr_free(SwrContext **ctx) - cdef void swr_close(SwrContext *ctx) From 0b85c60defbea05ee8118a0605f7c31254589a0b Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 23 Aug 2025 12:23:26 -0400 Subject: [PATCH 563/846] Build with ffmpeg 8 --- .github/workflows/smoke.yml | 8 ++++---- av/codec/codec.pyx | 1 - av/codec/context.pyx | 6 +++--- av/container/core.pyx | 1 - av/filter/filter.pyi | 1 - av/filter/filter.pyx | 4 ---- av/format.pyx | 1 - include/libavcodec/avcodec.pxd | 10 +++------- include/libavfilter/avfilter.pxd | 8 ++------ include/libavformat/avformat.pxd | 2 -- scripts/activate.sh | 2 +- scripts/{ffmpeg-7.0.json => ffmpeg-8.0.json} | 4 ++-- 12 files changed, 15 insertions(+), 33 deletions(-) rename scripts/{ffmpeg-7.0.json => ffmpeg-8.0.json} (61%) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index d6637ce62..3c355a949 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -34,9 +34,9 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-24.04, python: "3.12", ffmpeg: "7.1.1", extras: true} - - {os: ubuntu-24.04, python: "pypy3.10", ffmpeg: "7.1.1"} - - {os: macos-14, python: "3.9", ffmpeg: "7.1.1"} + - {os: ubuntu-24.04, python: "3.12", ffmpeg: "8.0", extras: true} + - {os: ubuntu-24.04, python: "pypy3.10", ffmpeg: "8.0"} + - {os: macos-14, python: "3.9", ffmpeg: "8.0"} env: PYAV_PYTHON: python${{ matrix.config.python }} @@ -109,7 +109,7 @@ jobs: fail-fast: false matrix: config: - - {os: windows-latest, python: "3.9", ffmpeg: "7.1"} + - {os: windows-latest, python: "3.9", ffmpeg: "8.0"} steps: - name: Checkout diff --git a/av/codec/codec.pyx b/av/codec/codec.pyx index a28db758e..aeab61cc3 100644 --- a/av/codec/codec.pyx +++ b/av/codec/codec.pyx @@ -36,7 +36,6 @@ class Capabilities(IntEnum): delay = lib.AV_CODEC_CAP_DELAY small_last_frame = lib.AV_CODEC_CAP_SMALL_LAST_FRAME hwaccel_vdpau = 1 << 7 - subframes = lib.AV_CODEC_CAP_SUBFRAMES experimental = lib.AV_CODEC_CAP_EXPERIMENTAL channel_conf = lib.AV_CODEC_CAP_CHANNEL_CONF neg_linesizes = 1 << 11 diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 49087e779..1d9c412b2 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -494,7 +494,7 @@ cdef class CodecContext: # the codec itself. So use the descriptor here. desc = self.codec.desc cdef int i = 0 - while desc.profiles[i].profile != lib.FF_PROFILE_UNKNOWN: + while desc.profiles[i].profile != lib.AV_PROFILE_UNKNOWN: ret.append(desc.profiles[i].name) i += 1 @@ -509,7 +509,7 @@ cdef class CodecContext: # the codec itself. So use the descriptor here. desc = self.codec.desc cdef int i = 0 - while desc.profiles[i].profile != lib.FF_PROFILE_UNKNOWN: + while desc.profiles[i].profile != lib.AV_PROFILE_UNKNOWN: if desc.profiles[i].profile == self.ptr.profile: return desc.profiles[i].name i += 1 @@ -523,7 +523,7 @@ cdef class CodecContext: # the codec itself. So use the descriptor here. desc = self.codec.desc cdef int i = 0 - while desc.profiles[i].profile != lib.FF_PROFILE_UNKNOWN: + while desc.profiles[i].profile != lib.AV_PROFILE_UNKNOWN: if desc.profiles[i].name == value: self.ptr.profile = desc.profiles[i].profile return diff --git a/av/container/core.pyx b/av/container/core.pyx index 2b9a1244b..d8950c76f 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -138,7 +138,6 @@ class Flags(Flag): bitexact: "When muxing, try to avoid writing any random/volatile data to the output. This includes any random IDs, real-time timestamps/dates, muxer version, etc. This flag is mainly intended for testing." = lib.AVFMT_FLAG_BITEXACT sort_dts: "Try to interleave outputted packets by dts (using this flag can slow demuxing down)." = lib.AVFMT_FLAG_SORT_DTS fast_seek: "Enable fast, but inaccurate seeks for some formats." = lib.AVFMT_FLAG_FAST_SEEK - shortest: "Stop muxing when the shortest stream stops." = lib.AVFMT_FLAG_SHORTEST auto_bsf: "Add bitstream filters as requested by the muxer." = lib.AVFMT_FLAG_AUTO_BSF class AudioCodec(IntEnum): diff --git a/av/filter/filter.pyi b/av/filter/filter.pyi index e9f9737f3..f07f317fc 100644 --- a/av/filter/filter.pyi +++ b/av/filter/filter.pyi @@ -7,7 +7,6 @@ class Filter: descriptor: Descriptor options: tuple[Option, ...] | None flags: int - command_support: bool def __init__(self, name: str) -> None: ... diff --git a/av/filter/filter.pyx b/av/filter/filter.pyx index d66b2e46a..b6e7005f0 100644 --- a/av/filter/filter.pyx +++ b/av/filter/filter.pyx @@ -47,10 +47,6 @@ cdef class Filter: def flags(self): return self.ptr.flags - @property - def command_support(self): - return self.ptr.process_command != NULL - @property def inputs(self): if self._inputs is None: diff --git a/av/format.pyx b/av/format.pyx index 464e34f49..2eb386649 100644 --- a/av/format.pyx +++ b/av/format.pyx @@ -31,7 +31,6 @@ class Flags(Flag): no_bin_search: "Format does not allow to fall back on binary search via read_timestamp" = lib.AVFMT_NOBINSEARCH no_gen_search: "Format does not allow to fall back on generic search" = lib.AVFMT_NOGENSEARCH no_byte_seek: "Format does not allow seeking by bytes" = lib.AVFMT_NO_BYTE_SEEK - allow_flush: "Format allows flushing. If not set, the muxer will not receive a NULL packet in the write_packet function." = lib.AVFMT_ALLOW_FLUSH ts_nonstrict: "Format does not require strictly increasing timestamps, but they must still be monotonic." = lib.AVFMT_TS_NONSTRICT ts_negative: "Format allows muxing negative timestamps." = lib.AVFMT_TS_NEGATIVE # If not set the timestamp will be shifted in `av_write_frame()` and `av_interleaved_write_frame()` diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 134ae6f07..2bce8c2c1 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -80,14 +80,10 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef enum: AV_CODEC_CAP_DRAW_HORIZ_BAND AV_CODEC_CAP_DR1 - # AV_CODEC_CAP_HWACCEL AV_CODEC_CAP_DELAY AV_CODEC_CAP_SMALL_LAST_FRAME - # AV_CODEC_CAP_HWACCEL_VDPAU - AV_CODEC_CAP_SUBFRAMES AV_CODEC_CAP_EXPERIMENTAL AV_CODEC_CAP_CHANNEL_CONF - # AV_CODEC_CAP_NEG_LINESIZES AV_CODEC_CAP_FRAME_THREADS AV_CODEC_CAP_SLICE_THREADS AV_CODEC_CAP_PARAM_CHANGE @@ -98,6 +94,9 @@ cdef extern from "libavcodec/avcodec.h" nogil: AV_CODEC_CAP_HYBRID AV_CODEC_CAP_ENCODER_REORDERED_OPAQUE + cdef enum: + AV_PROFILE_UNKNOWN = -99 + cdef enum: FF_THREAD_FRAME FF_THREAD_SLICE @@ -156,9 +155,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: FF_COMPLIANCE_UNOFFICIAL FF_COMPLIANCE_EXPERIMENTAL - cdef enum: - FF_PROFILE_UNKNOWN = -99 - cdef enum AVCodecID: AV_CODEC_ID_NONE AV_CODEC_ID_MPEG2VIDEO diff --git a/include/libavfilter/avfilter.pxd b/include/libavfilter/avfilter.pxd index de7bfb445..b1ced248b 100644 --- a/include/libavfilter/avfilter.pxd +++ b/include/libavfilter/avfilter.pxd @@ -13,16 +13,12 @@ cdef extern from "libavfilter/avfilter.h" nogil: cdef unsigned avfilter_filter_pad_count(const AVFilter *filter, int is_output) cdef struct AVFilter: - AVClass *priv_class - const char *name const char *description - - const int flags - const AVFilterPad *inputs const AVFilterPad *outputs - int (*process_command)(AVFilterContext *, const char *cmd, const char *arg, char *res, int res_len, int flags) + const AVClass *priv_class + int flags cdef AVFilter* avfilter_get_by_name(const char *name) cdef const AVFilter* av_filter_iterate(void **opaque) diff --git a/include/libavformat/avformat.pxd b/include/libavformat/avformat.pxd index 6c23e99b4..09eede855 100644 --- a/include/libavformat/avformat.pxd +++ b/include/libavformat/avformat.pxd @@ -125,7 +125,6 @@ cdef extern from "libavformat/avformat.h" nogil: AVFMT_NOBINSEARCH AVFMT_NOGENSEARCH AVFMT_NO_BYTE_SEEK - AVFMT_ALLOW_FLUSH AVFMT_TS_NONSTRICT AVFMT_TS_NEGATIVE AVFMT_SEEK_TO_PTS @@ -145,7 +144,6 @@ cdef extern from "libavformat/avformat.h" nogil: AVFMT_FLAG_BITEXACT AVFMT_FLAG_SORT_DTS AVFMT_FLAG_FAST_SEEK - AVFMT_FLAG_SHORTEST AVFMT_FLAG_AUTO_BSF cdef int av_probe_input_buffer( diff --git a/scripts/activate.sh b/scripts/activate.sh index ab22d79d9..81240cafd 100755 --- a/scripts/activate.sh +++ b/scripts/activate.sh @@ -21,7 +21,7 @@ if [[ ! "$PYAV_LIBRARY" ]]; then return 1 fi else - PYAV_LIBRARY=ffmpeg-7.1.1 + PYAV_LIBRARY=ffmpeg-8.0 echo "No \$PYAV_LIBRARY set; defaulting to $PYAV_LIBRARY" fi fi diff --git a/scripts/ffmpeg-7.0.json b/scripts/ffmpeg-8.0.json similarity index 61% rename from scripts/ffmpeg-7.0.json rename to scripts/ffmpeg-8.0.json index 6cbe76b2c..915f5bfc2 100644 --- a/scripts/ffmpeg-7.0.json +++ b/scripts/ffmpeg-8.0.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.0.2-1/ffmpeg-{platform}.tar.gz" -} \ No newline at end of file + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0-1/ffmpeg-{platform}.tar.gz" +} From 724741146893ab0abfd289e38a93b1d9f60014ea Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 27 Aug 2025 18:31:44 -0400 Subject: [PATCH 564/846] Release 15.1.0 --- CHANGELOG.rst | 1 + av/about.py | 2 +- av/codec/context.pyi | 1 - av/codec/context.pyx | 1 - 4 files changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 395910dc4..ce6dc7356 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -21,6 +21,7 @@ v15.1.0 Features: +- Support FFmpeg 8 - Make the `Frame.key_frame` flag writable. v15.0.0 diff --git a/av/about.py b/av/about.py index d5793afcb..9a75d4360 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "15.0.0" +__version__ = "15.1.0" diff --git a/av/codec/context.pyi b/av/codec/context.pyi index 77810d9ed..e0c447186 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -25,7 +25,6 @@ class Flags(IntEnum): four_mv = cast(int, ...) output_corrupt = cast(int, ...) qpel = cast(int, ...) - drop_changed = cast(int, ...) recon_frame = cast(int, ...) copy_opaque = cast(int, ...) frame_duration = cast(int, ...) diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 1d9c412b2..ac294ee10 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -52,7 +52,6 @@ class Flags(IntEnum): four_mv = lib.AV_CODEC_FLAG_4MV output_corrupt = lib.AV_CODEC_FLAG_OUTPUT_CORRUPT qpel = lib.AV_CODEC_FLAG_QPEL - drop_changed = 1 << 5 recon_frame = lib.AV_CODEC_FLAG_RECON_FRAME copy_opaque = lib.AV_CODEC_FLAG_COPY_OPAQUE frame_duration = lib.AV_CODEC_FLAG_FRAME_DURATION From e3f2efd85e8c36c1f7b1a14660e05b5c7909a6e6 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 29 Aug 2025 23:49:14 -0400 Subject: [PATCH 565/846] Don't build i686 binaries Build is failing because it can't find libcurl: ValueError: Cannot repair wheel, because required library "libssl.so.10" could not be located Someone who cares about this can fix it --- .github/workflows/tests.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1de9f0036..788340c60 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -34,8 +34,6 @@ jobs: arch: x86_64 - os: ubuntu-24.04-arm arch: aarch64 - - os: ubuntu-24.04 - arch: i686 - os: ubuntu-24.04 arch: x86_64 - os: windows-latest @@ -67,8 +65,6 @@ jobs: CIBW_SKIP: "pp*" CIBW_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m pytest {package}/tests && mv {project}/av.disabled {project}/av CIBW_TEST_REQUIRES: pytest numpy - # skip tests when there are no binary wheels of numpy - CIBW_TEST_SKIP: "*_i686" run: | pip install cibuildwheel delvewheel cibuildwheel --output-dir dist From 7de657ac3cb702609082eaac9212bd37b58fc9ff Mon Sep 17 00:00:00 2001 From: Kim Minjong Date: Sat, 30 Aug 2025 22:53:00 +0900 Subject: [PATCH 566/846] Add process_command This allows dynamic control of filter parameters without stopping and restarting the filtering process. Wraps avfilter_process_command --------- Co-authored-by: WyattBlue --- av/filter/context.pyi | 3 +++ av/filter/context.pyx | 40 ++++++++++++++++++++++++++++++++ include/libavfilter/avfilter.pxd | 9 +++++++ 3 files changed, 52 insertions(+) diff --git a/av/filter/context.pyi b/av/filter/context.pyi index 13ee480c2..d37febdb9 100644 --- a/av/filter/context.pyi +++ b/av/filter/context.pyi @@ -12,3 +12,6 @@ class FilterContext: def graph(self) -> Graph: ... def push(self, frame: Frame) -> None: ... def pull(self) -> Frame: ... + def process_command( + self, cmd: str, arg: str | None = None, res_len: int = 1024, flags: int = 0 + ) -> str | None: ... diff --git a/av/filter/context.pyx b/av/filter/context.pyx index ba5981ff1..85c23863b 100644 --- a/av/filter/context.pyx +++ b/av/filter/context.pyx @@ -1,3 +1,5 @@ +cimport libav as lib + import weakref from av.audio.frame cimport alloc_audio_frame @@ -132,3 +134,41 @@ cdef class FilterContext: frame._init_user_attributes() frame.time_base = avrational_to_fraction(&self.ptr.inputs[0].time_base) return frame + + def process_command(self, cmd, arg=None, int res_len=1024, int flags=0): + if not cmd: + raise ValueError("Invalid cmd") + + cdef char *c_cmd = NULL + cdef char *c_arg = NULL + + c_cmd = cmd + if arg is not None: + c_arg = arg + + cdef char *c_res = NULL + cdef int ret + cdef bytearray res_buf = None + cdef unsigned char[:] view + cdef bytes b + cdef int nul + + if res_len > 0: + res_buf = bytearray(res_len) + view = res_buf + c_res = &view[0] + else: + c_res = NULL + + with nogil: + ret = lib.avfilter_process_command(self.ptr, c_cmd, c_arg, c_res, res_len, flags) + err_check(ret) + + if res_buf is not None: + b = bytes(res_buf) + nul = b.find(b'\x00') + if nul >= 0: + b = b[:nul] + if b: + return b.decode("utf-8", "strict") + return None diff --git a/include/libavfilter/avfilter.pxd b/include/libavfilter/avfilter.pxd index b1ced248b..8d4c05f6f 100644 --- a/include/libavfilter/avfilter.pxd +++ b/include/libavfilter/avfilter.pxd @@ -62,6 +62,15 @@ cdef extern from "libavfilter/avfilter.h" nogil: # custom cdef set pyav_get_available_filters() + int avfilter_process_command(AVFilterContext *filter, + const char *cmd, + const char *arg, + char *res, + int res_len, + int flags) + + cdef int AVFILTER_CMD_FLAG_FAST + cdef extern from "libavfilter/buffersink.h" nogil: cdef void av_buffersink_set_frame_size(AVFilterContext *ctx, unsigned frame_size) From a2a7996f346f9220c70b9f4db40d8fd47d4ed44c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 1 Sep 2025 09:01:40 -0400 Subject: [PATCH 567/846] Add ffmpeg-latest.json So no one forgets to update tests.yml in the future --- .github/workflows/smoke.yml | 2 +- .github/workflows/tests.yml | 8 ++++---- scripts/ffmpeg-latest.json | 3 +++ 3 files changed, 8 insertions(+), 5 deletions(-) create mode 100644 scripts/ffmpeg-latest.json diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 3c355a949..b6189e26b 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -109,7 +109,7 @@ jobs: fail-fast: false matrix: config: - - {os: windows-latest, python: "3.9", ffmpeg: "8.0"} + - {os: windows-latest, python: "3.9", ffmpeg: "latest"} steps: - name: Checkout diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 788340c60..94e5d58e1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,7 +14,7 @@ jobs: - name: Build source package run: | pip install -U --pre cython setuptools - python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.1.json /tmp/vendor + python scripts/fetch-vendor.py --config-file scripts/ffmpeg-latest.json /tmp/vendor PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig python setup.py sdist - name: Upload source package uses: actions/upload-artifact@v4 @@ -55,9 +55,9 @@ jobs: - name: Build wheels env: CIBW_ARCHS: ${{ matrix.arch }} - CIBW_BEFORE_BUILD: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.1.json /tmp/vendor - CIBW_BEFORE_BUILD_MACOS: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.1.json /tmp/vendor - CIBW_BEFORE_BUILD_WINDOWS: python scripts\fetch-vendor.py --config-file scripts\ffmpeg-7.1.json C:\cibw\vendor + CIBW_BEFORE_BUILD: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-latest.json /tmp/vendor + CIBW_BEFORE_BUILD_MACOS: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-latest.json /tmp/vendor + CIBW_BEFORE_BUILD_WINDOWS: python scripts\fetch-vendor.py --config-file scripts\ffmpeg-latest.json C:\cibw\vendor CIBW_ENVIRONMENT_LINUX: LD_LIBRARY_PATH=/tmp/vendor/lib:$LD_LIBRARY_PATH PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig CIBW_ENVIRONMENT_MACOS: PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig LDFLAGS=-headerpad_max_install_names CIBW_ENVIRONMENT_WINDOWS: INCLUDE=C:\\cibw\\vendor\\include LIB=C:\\cibw\\vendor\\lib PYAV_SKIP_TESTS=unicode_filename diff --git a/scripts/ffmpeg-latest.json b/scripts/ffmpeg-latest.json new file mode 100644 index 000000000..915f5bfc2 --- /dev/null +++ b/scripts/ffmpeg-latest.json @@ -0,0 +1,3 @@ +{ + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0-1/ffmpeg-{platform}.tar.gz" +} From 5f338961d0c7c2c87e32a8e89fd0fbce96a23772 Mon Sep 17 00:00:00 2001 From: DE-AI <81620697+DE-AI@users.noreply.github.com> Date: Mon, 1 Sep 2025 17:41:48 +0200 Subject: [PATCH 568/846] Binding to AVChapter and chapters functions in Container class --- Makefile | 2 +- av/container/core.pyi | 11 +++++++-- av/container/core.pyx | 18 +++++++++++++- include/libavformat/avformat.pxd | 11 +++++++++ tests/test_chapters.py | 41 ++++++++++++++++++++++++++++++++ 5 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 tests/test_chapters.py diff --git a/Makefile b/Makefile index 135d091ad..4858add5b 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ fate-suite: rsync -vrltLW rsync://fate-suite.ffmpeg.org/fate-suite/ tests/assets/fate-suite/ lint: - $(PIP) install -U ruff isort pillow numpy mypy==1.16.1 pytest + $(PIP) install -U ruff isort pillow numpy mypy==1.17.1 pytest ruff format --check av examples tests setup.py isort --check-only --diff av examples tests mypy av tests diff --git a/av/container/core.pyi b/av/container/core.pyi index d61d07110..8cd2a9dc5 100644 --- a/av/container/core.pyi +++ b/av/container/core.pyi @@ -2,7 +2,7 @@ from enum import Flag, IntEnum from fractions import Fraction from pathlib import Path from types import TracebackType -from typing import Any, Callable, ClassVar, Literal, Type, cast, overload +from typing import Any, Callable, ClassVar, Literal, Type, TypedDict, cast, overload from av.codec.hwaccel import HWAccel from av.format import ContainerFormat @@ -67,6 +67,13 @@ class AudioCodec(IntEnum): pcm_u8 = cast(int, ...) pcm_vidc = cast(int, ...) +class _Chapter(TypedDict): + id: int + start: int + end: int + time_base: Fraction | None + metadata: dict[str, str] + class Container: writeable: bool name: str @@ -86,7 +93,6 @@ class Container: open_timeout: Real | None read_timeout: Real | None flags: int - def __enter__(self) -> Container: ... def __exit__( self, @@ -96,6 +102,7 @@ class Container: ) -> bool: ... def set_timeout(self, timeout: Real | None) -> None: ... def start_timeout(self) -> None: ... + def chapters(self) -> list[_Chapter]: ... @overload def open( diff --git a/av/container/core.pyx b/av/container/core.pyx index d8950c76f..de7a07dda 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -15,7 +15,7 @@ from av.container.output cimport OutputContainer from av.container.pyio cimport pyio_close_custom_gil, pyio_close_gil from av.error cimport err_check, stash_exception from av.format cimport build_container_format -from av.utils cimport avdict_to_dict +from av.utils cimport avdict_to_dict, avrational_to_fraction from av.dictionary import Dictionary from av.logging import Capture as LogCapture @@ -330,6 +330,22 @@ cdef class Container: self._assert_open() self.ptr.flags = value + def chapters(self): + self._assert_open() + cdef list result = [] + cdef int i + + for i in range(self.ptr.nb_chapters): + ch = self.ptr.chapters[i] + result.append({ + "id": ch.id, + "start": ch.start, + "end": ch.end, + "time_base": avrational_to_fraction(&ch.time_base), + "metadata": avdict_to_dict(ch.metadata, self.metadata_encoding, self.metadata_errors), + }) + return result + def open( file, mode=None, diff --git a/include/libavformat/avformat.pxd b/include/libavformat/avformat.pxd index 09eede855..3816b46fa 100644 --- a/include/libavformat/avformat.pxd +++ b/include/libavformat/avformat.pxd @@ -44,6 +44,14 @@ cdef extern from "libavformat/avformat.h" nogil: AVRational r_frame_rate AVRational sample_aspect_ratio + cdef struct AVChapter: + int id + int64_t start + int64_t end + AVRational time_base + AVDictionary *metadata + + # http://ffmpeg.org/doxygen/trunk/structAVIOContext.html cdef struct AVIOContext: unsigned char* buffer @@ -173,6 +181,9 @@ cdef extern from "libavformat/avformat.h" nogil: unsigned int nb_streams AVStream **streams + unsigned int nb_chapters + AVChapter **chapters + AVInputFormat *iformat AVOutputFormat *oformat diff --git a/tests/test_chapters.py b/tests/test_chapters.py new file mode 100644 index 000000000..6a3d371b2 --- /dev/null +++ b/tests/test_chapters.py @@ -0,0 +1,41 @@ +from fractions import Fraction + +import av + +from .common import fate_suite + + +def test_chapters() -> None: + expected = [ + { + "id": 1, + "start": 0, + "end": 5000, + "time_base": Fraction(1, 1000), + "metadata": {"title": "start"}, + }, + { + "id": 2, + "start": 5000, + "end": 10500, + "time_base": Fraction(1, 1000), + "metadata": {"title": "Five Seconds"}, + }, + { + "id": 3, + "start": 10500, + "end": 15000, + "time_base": Fraction(1, 1000), + "metadata": {"title": "Ten point 5 seconds"}, + }, + { + "id": 4, + "start": 15000, + "end": 19849, + "time_base": Fraction(1, 1000), + "metadata": {"title": "15 sec - over soon"}, + }, + ] + path = fate_suite("vorbis/vorbis_chapter_extension_demo.ogg") + with av.open(path) as container: + assert container.chapters() == expected From 0d988f038042507a5c3fe854b24061749df30905 Mon Sep 17 00:00:00 2001 From: Kian-Meng Ang Date: Tue, 2 Sep 2025 00:59:48 +0800 Subject: [PATCH 569/846] Fix typos Found via `codespell -L wel,domin,ehr,bu,setts` --- AUTHORS.py | 2 +- CHANGELOG.rst | 2 +- av/container/core.pyx | 2 +- av/container/input.pyx | 2 +- av/error.pyx | 4 ++-- av/sidedata/motionvectors.pyx | 2 +- av/sidedata/sidedata.pyx | 2 +- av/video/codeccontext.pyx | 4 ++-- av/video/format.pyx | 2 +- docs/api/video.rst | 2 +- tests/test_bitstream.py | 2 +- tests/test_encode.py | 2 +- 12 files changed, 14 insertions(+), 14 deletions(-) diff --git a/AUTHORS.py b/AUTHORS.py index 797e423ed..ad0746641 100644 --- a/AUTHORS.py +++ b/AUTHORS.py @@ -94,7 +94,7 @@ github = github_map.get(email) # The '-' vs '*' is so that Sphinx treats them as different lists, and - # introduces a gap bettween them. + # introduces a gap between them. if github: print( "%s %s <%s>; `@%s `_" diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ce6dc7356..04130c2b7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -559,7 +559,7 @@ Build: Other: - Incremental improvements to docs and tests. -- Examples directory will now always be runnable as-is, and embeded in the docs (in a copy-pastable form). +- Examples directory will now always be runnable as-is, and embedded in the docs (in a copy-pastable form). v6.0.0 diff --git a/av/container/core.pyx b/av/container/core.pyx index de7a07dda..076faea15 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -130,7 +130,7 @@ class Flags(Flag): non_block: "Do not block when reading packets from input." = lib.AVFMT_FLAG_NONBLOCK ign_dts: "Ignore DTS on frames that contain both DTS & PTS." = lib.AVFMT_FLAG_IGNDTS no_fillin: "Do not infer any values from other values, just return what is stored in the container." = lib.AVFMT_FLAG_NOFILLIN - no_parse: "Do not use AVParsers, you also must set AVFMT_FLAG_NOFILLIN as the fillin code works on frames and no parsing -> no frames. Also seeking to frames can not work if parsing to find frame boundaries has been disabled." = lib.AVFMT_FLAG_NOPARSE + no_parse: "Do not use AVParsers, you also must set AVFMT_FLAG_NOFILLIN as the fill in code works on frames and no parsing -> no frames. Also seeking to frames can not work if parsing to find frame boundaries has been disabled." = lib.AVFMT_FLAG_NOPARSE no_buffer: "Do not buffer frames when possible." = lib.AVFMT_FLAG_NOBUFFER custom_io: "The caller has supplied a custom AVIOContext, don't avio_close() it." = lib.AVFMT_FLAG_CUSTOM_IO discard_corrupt: "Discard frames marked corrupted." = lib.AVFMT_FLAG_DISCARD_CORRUPT diff --git a/av/container/input.pyx b/av/container/input.pyx index 943081e15..56af7b05c 100644 --- a/av/container/input.pyx +++ b/av/container/input.pyx @@ -241,7 +241,7 @@ cdef class InputContainer(Container): In most cases, the defaults of ``backwards = True`` and ``any_frame = False`` are the best course of action, followed by you demuxing/decoding to - the position that you want. This is becase to properly decode video frames + the position that you want. This is because to properly decode video frames you need to start from the previous keyframe. .. seealso:: :ffmpeg:`avformat_seek_file` for discussion of the flags. diff --git a/av/error.pyx b/av/error.pyx index c3d8a3e7b..123ed735f 100644 --- a/av/error.pyx +++ b/av/error.pyx @@ -267,7 +267,7 @@ ErrorType = EnumType("ErrorType", (EnumItem, ), {"__module__": __name__}, [x[:2] for enum in ErrorType: - # Mimick the errno module. + # Mimic the errno module. globals()[enum.name] = enum if enum.value == c_PYAV_STASHED_ERROR: enum.strerror = PYAV_STASHED_ERROR_message @@ -275,7 +275,7 @@ for enum in ErrorType: enum.strerror = lib.av_err2str(-enum.value) -# Mimick the builtin exception types. +# Mimic the builtin exception types. # See https://www.python.org/dev/peps/pep-3151/#new-exception-classes # Use the named ones we have, otherwise default to OSError for anything in errno. diff --git a/av/sidedata/motionvectors.pyx b/av/sidedata/motionvectors.pyx index b0b0b705f..8a4200232 100644 --- a/av/sidedata/motionvectors.pyx +++ b/av/sidedata/motionvectors.pyx @@ -55,7 +55,7 @@ class MotionVectors(_MotionVectors, Sequence): cdef class MotionVector: def __init__(self, sentinel, _MotionVectors parent, int index): if sentinel is not _cinit_bypass_sentinel: - raise RuntimeError("cannot manually instatiate MotionVector") + raise RuntimeError("cannot manually instantiate MotionVector") self.parent = parent cdef lib.AVMotionVector *base = parent.ptr.data self.ptr = base + index diff --git a/av/sidedata/sidedata.pyx b/av/sidedata/sidedata.pyx index 24dbae119..65b1387f0 100644 --- a/av/sidedata/sidedata.pyx +++ b/av/sidedata/sidedata.pyx @@ -63,7 +63,7 @@ cdef int get_display_rotation(Frame frame): cdef class SideData(Buffer): def __init__(self, sentinel, Frame frame, int index): if sentinel is not _cinit_bypass_sentinel: - raise RuntimeError("cannot manually instatiate SideData") + raise RuntimeError("cannot manually instantiate SideData") self.frame = frame self.ptr = frame.ptr.side_data[index] self.metadata = wrap_dictionary(self.ptr.metadata) diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.pyx index c9d8eb4c0..e82e56e91 100644 --- a/av/video/codeccontext.pyx +++ b/av/video/codeccontext.pyx @@ -217,13 +217,13 @@ cdef class VideoCodecContext(CodecContext): :type: int """ if self.is_decoder: - raise RuntimeError("Cannnot access 'gop_size' as a decoder") + raise RuntimeError("Cannot access 'gop_size' as a decoder") return self.ptr.gop_size @gop_size.setter def gop_size(self, int value): if self.is_decoder: - raise RuntimeError("Cannnot access 'gop_size' as a decoder") + raise RuntimeError("Cannot access 'gop_size' as a decoder") self.ptr.gop_size = value @property diff --git a/av/video/format.pyx b/av/video/format.pyx index 8779f7151..4e66a8836 100644 --- a/av/video/format.pyx +++ b/av/video/format.pyx @@ -158,7 +158,7 @@ cdef class VideoFormatComponent: @property def is_luma(self): - """Is this compoment a luma channel?""" + """Is this component a luma channel?""" return self.index == 0 and ( self.format.ptr.nb_components == 1 or self.format.ptr.nb_components == 2 or diff --git a/docs/api/video.rst b/docs/api/video.rst index 1c56788a1..7e97e38c8 100644 --- a/docs/api/video.rst +++ b/docs/api/video.rst @@ -113,7 +113,7 @@ Enums .. autoclass:: av.video.reformatter.Colorspace Wraps the ``SWS_CS_*`` flags. There is a bit of overlap in - these names which comes from FFmpeg and backards compatibility. + these names which comes from FFmpeg and backwards compatibility. .. enumtable:: av.video.reformatter.Colorspace diff --git a/tests/test_bitstream.py b/tests/test_bitstream.py index 26e54599b..dde7723c7 100644 --- a/tests/test_bitstream.py +++ b/tests/test_bitstream.py @@ -17,7 +17,7 @@ def is_annexb(packet: Packet | bytes | None) -> bool: return data[:3] == b"\0\0\x01" or data[:4] == b"\0\0\0\x01" -def test_filters_availible() -> None: +def test_filters_available() -> None: assert "h264_mp4toannexb" in bitstream_filters_available diff --git a/tests/test_encode.py b/tests/test_encode.py index 9284a9c82..d64cadc20 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -415,7 +415,7 @@ def encode_frames_with_qminmax( frames: list[VideoFrame], shape: tuple[int, int, int], qminmax: tuple[int, int] ) -> int: """ - Encode a video with the given quantiser limits, and return how many enocded + Encode a video with the given quantiser limits, and return how many encoded bytes we made in total. frames: the frames to encode From 8105052b77c359fa8f26623e6ccd4d110b2ebf50 Mon Sep 17 00:00:00 2001 From: Kesh Ikuma <79113787+tikuma-lsuhsc@users.noreply.github.com> Date: Mon, 1 Sep 2025 22:23:58 -0500 Subject: [PATCH 570/846] Add packet side-data handling mechanism Adds ability to extract (copy) a side data from one packet and apply it to another packet --------- Co-authored-by: WyattBlue --- av/packet.pxd | 7 ++ av/packet.py | 219 +++++++++++++++++++++++++++++++++ av/packet.pyi | 64 ++++++++++ include/libavcodec/avcodec.pxd | 15 ++- tests/test_packet.py | 45 +++++++ 5 files changed, 349 insertions(+), 1 deletion(-) diff --git a/av/packet.pxd b/av/packet.pxd index f1517a3d4..8e8ad034d 100644 --- a/av/packet.pxd +++ b/av/packet.pxd @@ -1,3 +1,5 @@ +from cython.cimports.libc.stdint import uint8_t + cimport libav as lib from av.buffer cimport Buffer @@ -5,6 +7,11 @@ from av.bytesource cimport ByteSource from av.stream cimport Stream +cdef class PacketSideData: + cdef uint8_t *data + cdef size_t size + cdef lib.AVPacketSideDataType dtype + cdef class Packet(Buffer): cdef lib.AVPacket* ptr cdef Stream _stream diff --git a/av/packet.py b/av/packet.py index 81f1aaa4d..e1051b653 100644 --- a/av/packet.py +++ b/av/packet.py @@ -1,9 +1,183 @@ +from typing import Iterator, Literal, get_args + import cython from cython.cimports import libav as lib from cython.cimports.av.bytesource import bytesource from cython.cimports.av.error import err_check from cython.cimports.av.opaque import opaque_container from cython.cimports.av.utils import avrational_to_fraction, to_avrational +from cython.cimports.libc.string import memcpy + +# Check https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/packet.h#L41 +# for new additions in the future ffmpeg releases +# Note: the order must follow that of the AVPacketSideDataType enum def +PktSideDataT = Literal[ + "palette", + "new_extradata", + "param_change", + "h263_mb_info", + "replay_gain", + "display_matrix", + "stereo_3d", + "audio_service_type", + "quality_stats", + "fallback_track", + "cpb_properties", + "skip_samples", + "jp_dual_mono", + "strings_metadata", + "subtitle_position", + "matroska_block_additional", + "webvtt_identifier", + "webvtt_settings", + "metadata_update", + "mpegts_stream_id", + "mastering_display_metadata", + "spherical", + "content_light_level", + "a53_cc", + "encryption_init_info", + "encryption_info", + "afd", + "prft", + "icc_profile", + "dovi_conf", + "s12m_timecode", + "dynamic_hdr10_plus", + "iamf_mix_gain_param", + "iamf_info_param", + "iamf_recon_gain_info_param", + "ambient_viewing_environment", + "frame_cropping", + "lcevc", + "3d_reference_displays", + "rtcp_sr", +] + + +def packet_sidedata_type_to_literal(dtype: lib.AVPacketSideDataType) -> PktSideDataT: + return get_args(PktSideDataT)[cython.cast(int, dtype)] + + +def packet_sidedata_type_from_literal(dtype: PktSideDataT) -> lib.AVPacketSideDataType: + return get_args(PktSideDataT).index(dtype) + + +@cython.cclass +class PacketSideData: + @staticmethod + def from_packet(packet: Packet, data_type: PktSideDataT) -> PacketSideData: + """create new PacketSideData by copying an existing packet's side data + + :param packet: Source packet + :type packet: :class:`~av.packet.Packet` + :param data_type: side data type + :return: newly created copy of the side data if the side data of the + requested type is found in the packet, else an empty object + :rtype: :class:`~av.packet.PacketSideData` + """ + + dtype = packet_sidedata_type_from_literal(data_type) + return _packet_sidedata_from_packet(packet.ptr, dtype) + + def __cinit__(self, dtype: lib.AVPacketSideDataType, size: cython.size_t): + self.dtype = dtype + with cython.nogil: + if size: + self.data = cython.cast(cython.p_uchar, lib.av_malloc(size)) + if self.data == cython.NULL: + raise MemoryError("Failed to allocate memory") + else: + self.data = cython.NULL + self.size = size + + def __dealloc__(self): + with cython.nogil: + lib.av_freep(cython.address(self.data)) + + def to_packet(self, packet: Packet, move: cython.bint = False): + """copy or move side data to the specified packet + + :param packet: Target packet + :type packet: :class:`~av.packet.Packet` + :param move: True to move the data from this object to the packet, + defaults to False. + :type move: bool + """ + if self.size == 0: + # nothing to add, should clear existing side_data in packet? + return + + data = self.data + + with cython.nogil: + if not move: + data = cython.cast(cython.p_uchar, lib.av_malloc(self.size)) + if data == cython.NULL: + raise MemoryError("Failed to allocate memory") + memcpy(data, self.data, self.size) + + res = lib.av_packet_add_side_data(packet.ptr, self.dtype, data, self.size) + err_check(res) + + if move: + self.data = cython.NULL + self.size = 0 + + @property + def data_type(self) -> str: + """ + The type of this packet side data. + + :type: str + """ + return packet_sidedata_type_to_literal(self.dtype) + + @property + def data_desc(self) -> str: + """ + The description of this packet side data type. + + :type: str + """ + + return lib.av_packet_side_data_name(self.dtype) + + @property + def data_size(self) -> int: + """ + The size in bytes of this packet side data. + + :type: int + """ + return self.size + + def __bool__(self) -> bool: + """ + True if this object holds side data. + + :type: bool + """ + return self.data != cython.NULL + + +@cython.cfunc +def _packet_sidedata_from_packet( + packet: cython.pointer[lib.AVPacket], dtype: lib.AVPacketSideDataType +) -> PacketSideData: + with cython.nogil: + c_ptr = lib.av_packet_side_data_get( + packet.side_data, packet.side_data_elems, dtype + ) + found: cython.bint = c_ptr != cython.NULL + + sdata = PacketSideData(dtype, c_ptr.size if found else 0) + + with cython.nogil: + if found: + memcpy(sdata.data, c_ptr.data, c_ptr.size) + + return sdata @cython.cclass @@ -235,3 +409,48 @@ def opaque(self, v): if v is None: return self.ptr.opaque_ref = opaque_container.add(v) + + def has_sidedata(self, dtype: str) -> bool: + """True if this packet has the specified side data + + :param dtype: side data type + :type dtype: str + """ + + dtype2 = packet_sidedata_type_from_literal(dtype) + return ( + lib.av_packet_side_data_get( + self.ptr.side_data, self.ptr.side_data_elems, dtype2 + ) + != cython.NULL + ) + + def get_sidedata(self, dtype: str) -> PacketSideData: + """get a copy of the side data + + :param dtype: side data type (:method:`~av.packet.PacketSideData.sidedata_types` for the full list of options) + :type dtype: str + :return: newly created copy of the side data if the side data of the + requested type is found in the packet, else an empty object + :rtype: :class:`~av.packet.PacketSideData` + """ + return PacketSideData.from_packet(self, dtype) + + def set_sidedata(self, sidedata: PacketSideData, move: cython.bint = False): + """copy or move side data to this packet + + :param sidedata: Source packet side data + :type sidedata: :class:`~av.packet.PacketSideData` + :param move: If True, move the data from `sidedata` object, defaults to False + :type move: bool + """ + sidedata.to_packet(self, move) + + def iter_sidedata(self) -> Iterator[PacketSideData]: + """iterate over side data of this packet. + + :yield: :class:`~av.packet.PacketSideData` object + """ + + for i in range(self.ptr.side_data_elems): + yield _packet_sidedata_from_packet(self.ptr, self.ptr.side_data[i].type) diff --git a/av/packet.pyi b/av/packet.pyi index baa234d7b..8dab20065 100644 --- a/av/packet.pyi +++ b/av/packet.pyi @@ -1,10 +1,70 @@ from fractions import Fraction +from typing import Iterator, Literal from av.subtitles.subtitle import SubtitleSet from .buffer import Buffer from .stream import Stream +# Sync with definition in 'packet.py' +PktSideDataT = Literal[ + "palette", + "new_extradata", + "param_change", + "h263_mb_info", + "replay_gain", + "display_matrix", + "stereo_3d", + "audio_service_type", + "quality_stats", + "fallback_track", + "cpb_properties", + "skip_samples", + "jp_dual_mono", + "strings_metadata", + "subtitle_position", + "matroska_block_additional", + "webvtt_identifier", + "webvtt_settings", + "metadata_update", + "mpegts_stream_id", + "mastering_display_metadata", + "spherical", + "content_light_level", + "a53_cc", + "encryption_init_info", + "encryption_info", + "afd", + "prft", + "icc_profile", + "dovi_conf", + "s12m_timecode", + "dynamic_hdr10_plus", + "iamf_mix_gain_param", + "iamf_info_param", + "iamf_recon_gain_info_param", + "ambient_viewing_environment", + "frame_cropping", + "lcevc", + "3d_reference_displays", + "rtcp_sr", +] + +class PacketSideData: + @staticmethod + def from_packet(packet: Packet, dtype: PktSideDataT) -> PacketSideData: ... + def to_packet(self, packet: Packet, move: bool = False): ... + @property + def data_type(self) -> str: ... + @property + def data_desc(self) -> str: ... + @property + def data_size(self) -> int: ... + def __bool__(self) -> bool: ... + +def packet_sidedata_type_to_literal(dtype: int) -> PktSideDataT: ... +def packet_sidedata_type_from_literal(dtype: PktSideDataT) -> int: ... + class Packet(Buffer): stream: Stream stream_index: int @@ -23,3 +83,7 @@ class Packet(Buffer): def __init__(self, input: int | bytes | None = None) -> None: ... def decode(self) -> list[SubtitleSet]: ... + def has_sidedata(self, dtype: PktSideDataT) -> bool: ... + def get_sidedata(self, dtype: PktSideDataT) -> PacketSideData: ... + def set_sidedata(self, sidedata: PacketSideData, move: bool = False) -> None: ... + def iter_sidedata(self) -> Iterator[PacketSideData]: ... diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 2bce8c2c1..43ecb0260 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -1,4 +1,4 @@ -from libc.stdint cimport int8_t, int64_t, uint16_t, uint32_t +from libc.stdint cimport int8_t, int64_t, uint16_t, uint32_t, uint8_t cdef extern from "libavcodec/codec.h": struct AVCodecTag: @@ -17,6 +17,17 @@ cdef extern from "libavcodec/packet.h" nogil: int free_opaque ) + const AVPacketSideData *av_packet_side_data_get(const AVPacketSideData *sd, + int nb_sd, + AVPacketSideDataType type) + + uint8_t* av_packet_get_side_data(const AVPacket *pkt, AVPacketSideDataType type, + size_t *size) + + int av_packet_add_side_data(AVPacket *pkt, AVPacketSideDataType type, + uint8_t *data, size_t size) + + const char *av_packet_side_data_name(AVPacketSideDataType type) cdef extern from "libavutil/channel_layout.h": ctypedef enum AVChannelOrder: @@ -469,6 +480,8 @@ cdef extern from "libavcodec/avcodec.h" nogil: int size int stream_index int flags + AVPacketSideData *side_data + int side_data_elems int duration int64_t pos void *opaque diff --git a/tests/test_packet.py b/tests/test_packet.py index 423396f71..d5552238c 100644 --- a/tests/test_packet.py +++ b/tests/test_packet.py @@ -1,3 +1,5 @@ +from typing import get_args + import av from .common import fate_suite @@ -48,3 +50,46 @@ def test_set_duration(self) -> None: packet.duration += 10 assert packet.duration == old_duration + 10 + + +class TestPacketSideData: + def test_data_types(self) -> None: + dtypes = get_args(av.packet.PktSideDataT) + ffmpeg_ver = [int(v) for v in av.ffmpeg_version_info.split(".", 2)[:2]] + for dtype in dtypes: + av_enum = av.packet.packet_sidedata_type_from_literal(dtype) + assert dtype == av.packet.packet_sidedata_type_to_literal(av_enum) + + if (ffmpeg_ver[0] < 8 and dtype == "lcevc") or ( + ffmpeg_ver[0] < 9 and dtype == "rtcp_sr" + ): + break + + def test_iter(self) -> None: + with av.open(fate_suite("h264/extradata-reload-multi-stsd.mov")) as container: + for pkt in container.demux(): + for sdata in pkt.iter_sidedata(): + assert pkt.dts == 2 and sdata.data_type == "new_extradata" + + def test_palette(self) -> None: + with av.open(fate_suite("h264/extradata-reload-multi-stsd.mov")) as container: + iterpackets = container.demux() + pkt = next(pkt for pkt in iterpackets if pkt.has_sidedata("new_extradata")) + + sdata = pkt.get_sidedata("new_extradata") + assert sdata.data_type == "new_extradata" + assert bool(sdata) + assert sdata.data_size > 0 + assert sdata.data_desc == "New Extradata" + + nxt = next(iterpackets) # has no palette + + assert not nxt.has_sidedata("new_extradata") + + sdata1 = nxt.get_sidedata("new_extradata") + assert sdata1.data_type == "new_extradata" + assert not bool(sdata1) + assert sdata1.data_size == 0 + + nxt.set_sidedata(sdata, move=True) + assert not bool(sdata) From ac1fd125f354a31867f2ad519f8151ddc9f354e7 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 3 Sep 2025 09:51:42 -0600 Subject: [PATCH 571/846] Declare free-threaded support and support 3.13t --- .github/workflows/smoke.yml | 2 ++ .github/workflows/tests.yml | 1 + setup.py | 1 + 3 files changed, 4 insertions(+) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index b6189e26b..da2c4dee0 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -34,6 +34,7 @@ jobs: fail-fast: false matrix: config: + - {os: ubuntu-24.04, python: "3.14t", ffmpeg: "8.0", extras: true} - {os: ubuntu-24.04, python: "3.12", ffmpeg: "8.0", extras: true} - {os: ubuntu-24.04, python: "pypy3.10", ffmpeg: "8.0"} - {os: macos-14, python: "3.9", ffmpeg: "8.0"} @@ -50,6 +51,7 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.config.python }} + allow-prereleases: true - name: OS Packages run: | diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 94e5d58e1..cb38c2bfc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -65,6 +65,7 @@ jobs: CIBW_SKIP: "pp*" CIBW_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m pytest {package}/tests && mv {project}/av.disabled {project}/av CIBW_TEST_REQUIRES: pytest numpy + CIBW_ENABLE: cpython-freethreading run: | pip install cibuildwheel delvewheel cibuildwheel --output-dir dist diff --git a/setup.py b/setup.py index ebc2146fe..ce6bcc388 100644 --- a/setup.py +++ b/setup.py @@ -150,6 +150,7 @@ def parse_cflags(raw_flags): "embedsignature": True, "binding": False, "language_level": 3, + "freethreading_compatible": True, } # Add the cythonized loudnorm extension to ext_modules From 6dd3dfd3b9d045903dd9e5ec8e0b1546fc67894d Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 3 Sep 2025 16:45:19 -0400 Subject: [PATCH 572/846] Add changelog items --- CHANGELOG.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 04130c2b7..7a445525e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,20 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. +v16.0.0 +------- +(unreleased) + +Major: +- Drop Python 3.9, Support Python 3.14. + +Features: +- Declare free-threaded support and support 3.13t by :gh-user:`ngoldbaum` in (:pr:`2005`). +- Add ``Filter.Context.process_command()`` method by :gh-user:`caffeinism` in (:pr:`2000`). + +Fixes: +- Fix typos + v15.1.0 ------- From 1aced8f0ebf32f127ac3468c2ff7acb4ffe79ed4 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 3 Sep 2025 16:59:38 -0400 Subject: [PATCH 573/846] Make utils pure --- av/option.pyx | 10 ++++-- av/utils.pxd | 5 +-- av/utils.py | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++ av/utils.pyx | 78 ---------------------------------------------- 4 files changed, 95 insertions(+), 84 deletions(-) create mode 100644 av/utils.py delete mode 100644 av/utils.pyx diff --git a/av/option.pyx b/av/option.pyx index e58c4c13f..604d81a53 100644 --- a/av/option.pyx +++ b/av/option.pyx @@ -1,6 +1,5 @@ cimport libav as lib - -from av.utils cimport flag_in_bitfield +from libc.stdint cimport uint64_t from enum import Enum, Flag @@ -16,6 +15,13 @@ cdef Option wrap_option(tuple choices, const lib.AVOption *ptr): return obj +cdef flag_in_bitfield(uint64_t bitfield, uint64_t flag): + # Not every flag exists in every version of FFMpeg, so we define them to 0. + if not flag: + return None + return bool(bitfield & flag) + + class OptionType(Enum): FLAGS = lib.AV_OPT_TYPE_FLAGS INT = lib.AV_OPT_TYPE_INT diff --git a/av/utils.pxd b/av/utils.pxd index 9aeb4a2fb..60279e513 100644 --- a/av/utils.pxd +++ b/av/utils.pxd @@ -1,5 +1,4 @@ cimport libav as lib -from libc.stdint cimport uint64_t cdef dict avdict_to_dict(lib.AVDictionary *input, str encoding, str errors) @@ -7,6 +6,4 @@ cdef dict_to_avdict(lib.AVDictionary **dst, dict src, str encoding, str errors) cdef object avrational_to_fraction(const lib.AVRational *input) cdef void to_avrational(object frac, lib.AVRational *input) - -cdef check_ndarray(object array, object dtype, int ndim) -cdef flag_in_bitfield(uint64_t bitfield, uint64_t flag) +cdef void check_ndarray(object array, object dtype, int ndim) diff --git a/av/utils.py b/av/utils.py new file mode 100644 index 000000000..3516be0e3 --- /dev/null +++ b/av/utils.py @@ -0,0 +1,86 @@ +# type: ignore +from fractions import Fraction + +import cython +from cython.cimports import libav as lib +from cython.cimports.av.error import err_check + +# === DICTIONARIES === +# ==================== + + +@cython.cfunc +def _decode(s: cython.pointer[cython.char], encoding, errors) -> str: + return cython.cast(bytes, s).decode(encoding, errors) + + +@cython.cfunc +def _encode(s, encoding, errors) -> bytes: + return s.encode(encoding, errors) + + +@cython.cfunc +def avdict_to_dict( + input: cython.pointer[lib.AVDictionary], encoding: str, errors: str +) -> dict: + element: cython.pointer[lib.AVDictionaryEntry] = cython.NULL + output: dict = {} + while True: + element = lib.av_dict_get(input, "", element, lib.AV_DICT_IGNORE_SUFFIX) + if element == cython.NULL: + break + output[_decode(element.key, encoding, errors)] = _decode( + element.value, encoding, errors + ) + + return output + + +@cython.cfunc +def dict_to_avdict( + dst: cython.pointer[cython.pointer[lib.AVDictionary]], + src: dict, + encoding: str, + errors: str, +): + lib.av_dict_free(dst) + for key, value in src.items(): + err_check( + lib.av_dict_set( + dst, key.encode(encoding, errors), value.encode(encoding, errors), 0 + ) + ) + + +# === FRACTIONS === +# ================= + + +@cython.cfunc +def avrational_to_fraction( + input: cython.pointer[cython.const[lib.AVRational]], +) -> object: + if input.num and input.den: + return Fraction(input.num, input.den) + return None + + +@cython.cfunc +def to_avrational(frac: object, input: cython.pointer[lib.AVRational]) -> cython.void: + input.num = frac.numerator + input.den = frac.denominator + + +@cython.cfunc +def check_ndarray(array: object, dtype: object, ndim: cython.int) -> cython.void: + """ + Check a numpy array has the expected data type and number of dimensions. + """ + if array.dtype != dtype: + raise ValueError( + f"Expected numpy array with dtype `{dtype}` but got `{array.dtype}`" + ) + if array.ndim != ndim: + raise ValueError( + f"Expected numpy array with ndim `{ndim}` but got `{array.ndim}`" + ) diff --git a/av/utils.pyx b/av/utils.pyx deleted file mode 100644 index 190bbf4d7..000000000 --- a/av/utils.pyx +++ /dev/null @@ -1,78 +0,0 @@ -from libc.stdint cimport uint64_t - -from fractions import Fraction - -cimport libav as lib - -from av.error cimport err_check - -# === DICTIONARIES === -# ==================== - -cdef _decode(char *s, encoding, errors): - return (s).decode(encoding, errors) - -cdef bytes _encode(s, encoding, errors): - return s.encode(encoding, errors) - -cdef dict avdict_to_dict(lib.AVDictionary *input, str encoding, str errors): - cdef lib.AVDictionaryEntry *element = NULL - cdef dict output = {} - while True: - element = lib.av_dict_get(input, "", element, lib.AV_DICT_IGNORE_SUFFIX) - if element == NULL: - break - output[_decode(element.key, encoding, errors)] = _decode(element.value, encoding, errors) - return output - - -cdef dict_to_avdict(lib.AVDictionary **dst, dict src, str encoding, str errors): - lib.av_dict_free(dst) - for key, value in src.items(): - err_check( - lib.av_dict_set( - dst, - _encode(key, encoding, errors), - _encode(value, encoding, errors), - 0 - ) - ) - - -# === FRACTIONS === -# ================= - -cdef object avrational_to_fraction(const lib.AVRational *input): - if input.num and input.den: - return Fraction(input.num, input.den) - - -cdef void to_avrational(object frac, lib.AVRational *input): - input.num = frac.numerator - input.den = frac.denominator - - -# === OTHER === -# ============= - - -cdef check_ndarray(object array, object dtype, int ndim): - """ - Check a numpy array has the expected data type and number of dimensions. - """ - if array.dtype != dtype: - raise ValueError(f"Expected numpy array with dtype `{dtype}` but got `{array.dtype}`") - if array.ndim != ndim: - raise ValueError(f"Expected numpy array with ndim `{ndim}` but got `{array.ndim}`") - - -cdef flag_in_bitfield(uint64_t bitfield, uint64_t flag): - # Not every flag exists in every version of FFMpeg, so we define them to 0. - if not flag: - return None - return bool(bitfield & flag) - - -# === BACKWARDS COMPAT === - -from .error import err_check From 106089447cab9dc6d3ca2d1ab7e0aeff76d7a6a4 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 3 Sep 2025 18:19:58 -0400 Subject: [PATCH 574/846] Drop support for Python 3.9 --- .github/workflows/smoke.yml | 4 ++-- pyproject.toml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index da2c4dee0..fe5b82687 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -37,7 +37,7 @@ jobs: - {os: ubuntu-24.04, python: "3.14t", ffmpeg: "8.0", extras: true} - {os: ubuntu-24.04, python: "3.12", ffmpeg: "8.0", extras: true} - {os: ubuntu-24.04, python: "pypy3.10", ffmpeg: "8.0"} - - {os: macos-14, python: "3.9", ffmpeg: "8.0"} + - {os: macos-14, python: "3.10", ffmpeg: "8.0"} env: PYAV_PYTHON: python${{ matrix.config.python }} @@ -111,7 +111,7 @@ jobs: fail-fast: false matrix: config: - - {os: windows-latest, python: "3.9", ffmpeg: "latest"} + - {os: windows-latest, python: "3.10", ffmpeg: "latest"} steps: - name: Checkout diff --git a/pyproject.toml b/pyproject.toml index 9710de837..33cde6b2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=77.0", "cython>=3.1.0a1,<4"] +requires = ["setuptools>=77.0", "cython>=3.1.0,<4"] [project] name = "av" @@ -10,7 +10,7 @@ authors = [ {name = "WyattBlue", email = "wyattblue@auto-editor.com"}, {name = "Jeremy Lainé", email = "jeremy.laine@m4x.org"}, ] -requires-python = ">=3.9" +requires-python = ">=3.10" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", @@ -20,11 +20,11 @@ classifiers = [ "Operating System :: Unix", "Operating System :: Microsoft :: Windows", "Programming Language :: Cython", - "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", + "Programming Language :: Python :: 3.14", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Multimedia :: Sound/Audio", "Topic :: Multimedia :: Sound/Audio :: Conversion", From 4d603ec63b4d4f55e2bfe97bd2dd0f04cb41f361 Mon Sep 17 00:00:00 2001 From: DE-AI <81620697+DE-AI@users.noreply.github.com> Date: Thu, 4 Sep 2025 14:15:20 +0200 Subject: [PATCH 575/846] Implement set_chapters method --- av/container/core.pyi | 5 +++-- av/container/core.pyx | 51 +++++++++++++++++++++++++++++++++++++++++- tests/test_chapters.py | 17 ++++++++++++++ 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/av/container/core.pyi b/av/container/core.pyi index 8cd2a9dc5..f923ba01e 100644 --- a/av/container/core.pyi +++ b/av/container/core.pyi @@ -67,7 +67,7 @@ class AudioCodec(IntEnum): pcm_u8 = cast(int, ...) pcm_vidc = cast(int, ...) -class _Chapter(TypedDict): +class Chapter(TypedDict): id: int start: int end: int @@ -102,7 +102,8 @@ class Container: ) -> bool: ... def set_timeout(self, timeout: Real | None) -> None: ... def start_timeout(self) -> None: ... - def chapters(self) -> list[_Chapter]: ... + def chapters(self) -> list[Chapter]: ... + def set_chapters(self, chapters: list[Chapter]) -> None: ... @overload def open( diff --git a/av/container/core.pyx b/av/container/core.pyx index 076faea15..299ac6ed3 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -15,7 +15,12 @@ from av.container.output cimport OutputContainer from av.container.pyio cimport pyio_close_custom_gil, pyio_close_gil from av.error cimport err_check, stash_exception from av.format cimport build_container_format -from av.utils cimport avdict_to_dict, avrational_to_fraction +from av.utils cimport ( + avdict_to_dict, + avrational_to_fraction, + dict_to_avdict, + to_avrational, +) from av.dictionary import Dictionary from av.logging import Capture as LogCapture @@ -123,6 +128,17 @@ cdef int pyav_io_close_gil(lib.AVFormatContext *s, lib.AVIOContext *pb) noexcept return result +cdef void _free_chapters(lib.AVFormatContext *ctx) noexcept nogil: + cdef int i + if ctx.chapters != NULL: + for i in range(ctx.nb_chapters): + if ctx.chapters[i] != NULL: + if ctx.chapters[i].metadata != NULL: + lib.av_dict_free(&ctx.chapters[i].metadata) + lib.av_freep(&ctx.chapters[i]) + lib.av_freep(&ctx.chapters) + ctx.nb_chapters = 0 + class Flags(Flag): gen_pts: "Generate missing pts even if it requires parsing future frames." = lib.AVFMT_FLAG_GENPTS @@ -346,6 +362,39 @@ cdef class Container: }) return result + def set_chapters(self, chapters): + self._assert_open() + + cdef int count = len(chapters) + cdef int i + cdef lib.AVChapter **ch_array + cdef lib.AVChapter *ch + cdef dict entry + + with nogil: + _free_chapters(self.ptr) + + ch_array = lib.av_malloc(count * sizeof(lib.AVChapter *)) + if ch_array == NULL: + raise MemoryError("av_malloc failed for chapters") + + for i in range(count): + entry = chapters[i] + ch = lib.av_malloc(sizeof(lib.AVChapter)) + if ch == NULL: + raise MemoryError("av_malloc failed for chapter") + ch.id = entry["id"] + ch.start = entry["start"] + ch.end = entry["end"] + to_avrational(entry["time_base"], &ch.time_base) + ch.metadata = NULL + if "metadata" in entry: + dict_to_avdict(&ch.metadata, entry["metadata"], self.metadata_encoding, self.metadata_errors) + ch_array[i] = ch + + self.ptr.nb_chapters = count + self.ptr.chapters = ch_array + def open( file, mode=None, diff --git a/tests/test_chapters.py b/tests/test_chapters.py index 6a3d371b2..c8a3626ce 100644 --- a/tests/test_chapters.py +++ b/tests/test_chapters.py @@ -39,3 +39,20 @@ def test_chapters() -> None: path = fate_suite("vorbis/vorbis_chapter_extension_demo.ogg") with av.open(path) as container: assert container.chapters() == expected + + +def test_set_chapters() -> None: + chapters: list[av.container.Chapter] = [ + { + "id": 1, + "start": 0, + "end": 5000, + "time_base": Fraction(1, 1000), + "metadata": {"title": "start"}, + } + ] + + path = fate_suite("h264/interlaced_crop.mp4") + with av.open(path) as container: + container.set_chapters(chapters) + assert container.chapters() == chapters From 5076f8dc104001d31dd07c30999a07bbb95c86db Mon Sep 17 00:00:00 2001 From: Curtis Doty Date: Thu, 18 Sep 2025 07:41:56 -0700 Subject: [PATCH 576/846] Prevent rsync error on missing subdir --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 4858add5b..b37d83e57 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,7 @@ clean: fate-suite: # Grab ALL of the samples from the ffmpeg site. + mkdir -p tests/assets/fate-suite/ rsync -vrltLW rsync://fate-suite.ffmpeg.org/fate-suite/ tests/assets/fate-suite/ lint: From 84859928338bd9c7c42c41e9f7da34873acbffc2 Mon Sep 17 00:00:00 2001 From: velsinki <40809145+velsinki@users.noreply.github.com> Date: Thu, 18 Sep 2025 17:13:53 +0200 Subject: [PATCH 577/846] Remove VideoFrame.pts stub Since `VideoFrame` does not override the `pts` setter/getter, it can also be `None` here. Instead of writing it out in the stubs, we can just reuse the definition from `Frame` by removing the stub in `VideoFrame`. --- av/video/frame.pyi | 1 - 1 file changed, 1 deletion(-) diff --git a/av/video/frame.pyi b/av/video/frame.pyi index c6868aabc..a7575e3bd 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -30,7 +30,6 @@ class PictureType(IntEnum): class VideoFrame(Frame): format: VideoFormat - pts: int planes: tuple[VideoPlane, ...] pict_type: int colorspace: int From 4e786dd8251c1c6c1f93b1215b79828ec08de805 Mon Sep 17 00:00:00 2001 From: velsinki <40809145+velsinki@users.noreply.github.com> Date: Thu, 18 Sep 2025 17:18:47 +0200 Subject: [PATCH 578/846] Allow None in FilterContext.push type stub `None` is correctly handled to signify EOF to the buffer, but this should also be visible in the type stub. --- av/filter/context.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/filter/context.pyi b/av/filter/context.pyi index d37febdb9..2350019f3 100644 --- a/av/filter/context.pyi +++ b/av/filter/context.pyi @@ -10,7 +10,7 @@ class FilterContext: ) -> None: ... @property def graph(self) -> Graph: ... - def push(self, frame: Frame) -> None: ... + def push(self, frame: Frame | None) -> None: ... def pull(self) -> Frame: ... def process_command( self, cmd: str, arg: str | None = None, res_len: int = 1024, flags: int = 0 From c51a78db95bf41ba4c73bb3db4c45c1f7cd9ea38 Mon Sep 17 00:00:00 2001 From: velsinki <40809145+velsinki@users.noreply.github.com> Date: Thu, 18 Sep 2025 19:10:16 +0200 Subject: [PATCH 579/846] Fix VideoFrame.pts typing in tests Fixes 84859928338bd9c7c42c41e9f7da34873acbffc2 --- tests/test_seek.py | 2 ++ tests/test_videoframe.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_seek.py b/tests/test_seek.py index d0e9e1f2e..ac28f6580 100644 --- a/tests/test_seek.py +++ b/tests/test_seek.py @@ -104,6 +104,8 @@ def test_decode_half(self) -> None: frame_count = 0 for frame in container.decode(video_stream): + assert frame.pts is not None + if current_frame is None: current_frame = timestamp_to_frame(frame.pts, video_stream) else: diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 90d91ab45..a977cd828 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -32,7 +32,9 @@ def test_frame_duration_matches_packet() -> None: packet_durations.sort(key=lambda x: x[0]) with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: - frame_durations = [(f.pts, f.duration) for f in container.decode(video=0)] + frame_durations = [ + (f.pts, f.duration) for f in container.decode(video=0) if f.pts is not None + ] frame_durations.sort(key=lambda x: x[0]) assert len(packet_durations) == len(frame_durations) From cddcef804adc275096302f91e31e4c062a61f5f6 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 21 Sep 2025 23:36:02 -0400 Subject: [PATCH 580/846] Use macos-15-intel runner --- .github/workflows/tests.yml | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cb38c2bfc..d2ceecad6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -30,7 +30,7 @@ jobs: include: - os: macos-14 arch: arm64 - - os: macos-13 + - os: macos-15-intel arch: x86_64 - os: ubuntu-24.04-arm arch: aarch64 @@ -43,15 +43,10 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.13" - - name: Install packages - if: matrix.os == 'macos-13' - run: | - brew update - brew install pkg-config - name: Set Minimum MacOS Target - if: matrix.os == 'macos-13' || matrix.os == 'macos-14' + if: runner.os == 'macOS' run: | - echo "MACOSX_DEPLOYMENT_TARGET=13.0" >> $GITHUB_ENV + echo "MACOSX_DEPLOYMENT_TARGET=14.0" >> $GITHUB_ENV - name: Build wheels env: CIBW_ARCHS: ${{ matrix.arch }} From 45eaebac0c6601a48ba81fbdcd2dee59cc6d3578 Mon Sep 17 00:00:00 2001 From: Curtis Doty Date: Thu, 2 Oct 2025 13:58:21 -0700 Subject: [PATCH 581/846] Prevent segfault on data stream template --- av/container/output.py | 5 +++++ tests/test_streams.py | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/av/container/output.py b/av/container/output.py index d035b0265..7738086ad 100644 --- a/av/container/output.py +++ b/av/container/output.py @@ -144,6 +144,11 @@ def add_stream_from_template( if opaque is None: opaque = template.type != "video" + if template.codec_context is None: + raise ValueError( + f"template stream of type {template.type} has no codec context" + ) + codec_obj: Codec if opaque: # Copy ctx from template. codec_obj = template.codec_context.codec diff --git a/tests/test_streams.py b/tests/test_streams.py index c7b234d48..d07085de4 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -1,5 +1,6 @@ import os from fractions import Fraction +from typing import Any, cast import pytest @@ -146,3 +147,23 @@ def test_data_stream(self) -> None: assert repr.startswith("") container.close() + + def test_data_stream_from_template(self) -> None: + """Test that adding a data stream from a template raises ValueError.""" + + # Open an existing container with a data stream + input_container = av.open(fate_suite("mxf/track_01_v02.mxf")) + input_data_stream = input_container.streams.data[0] + + # Create a new container and ensure using a data stream as a template raises ValueError + output_container = av.open("out.mkv", "w") + with pytest.raises(ValueError): + # input_data_stream is a DataStream at runtime; the test asserts that + # using it as a template raises ValueError. The static type stubs + # intentionally restrict which Stream subclasses are valid templates, + # so cast to Any here to keep the runtime check while satisfying + # the type checker. + output_container.add_stream_from_template(cast(Any, input_data_stream)) + + input_container.close() + output_container.close() From d87faf7b32eae594af802f5cd368fa3072539d38 Mon Sep 17 00:00:00 2001 From: Curtis Doty Date: Thu, 2 Oct 2025 14:43:32 -0700 Subject: [PATCH 582/846] Your bash is not my bash Let's respect the environment. If a developer is running their own version of the shell, respect it. Don't hard-code. --- scripts/activate.sh | 3 +-- scripts/build | 2 +- scripts/build-deps | 2 +- scripts/test | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/scripts/activate.sh b/scripts/activate.sh index 81240cafd..95f60a33d 100755 --- a/scripts/activate.sh +++ b/scripts/activate.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#! /usr/bin/env bash # Make sure this is sourced. if [[ "$0" == "${BASH_SOURCE[0]}" ]]; then @@ -78,7 +78,6 @@ print("{}{}.{}".format(platform.python_implementation().lower(), *sys.version_in fi - # Just a flag so that we know this was supposedly run. export _PYAV_ACTIVATED=1 diff --git a/scripts/build b/scripts/build index 3b9346d60..4ee15d003 100755 --- a/scripts/build +++ b/scripts/build @@ -1,4 +1,4 @@ -#!/bin/bash +#! /usr/bin/env bash if [[ ! "$_PYAV_ACTIVATED" ]]; then export here="$(cd "$(dirname "${BASH_SOURCE[0]}")"; pwd)" diff --git a/scripts/build-deps b/scripts/build-deps index f34ed5a5b..5f32cf140 100755 --- a/scripts/build-deps +++ b/scripts/build-deps @@ -1,4 +1,4 @@ -#!/bin/bash +#! /usr/bin/env bash if [[ ! "$_PYAV_ACTIVATED" ]]; then export here="$(cd "$(dirname "${BASH_SOURCE[0]}")"; pwd)" diff --git a/scripts/test b/scripts/test index 8244778b3..e91288baf 100755 --- a/scripts/test +++ b/scripts/test @@ -1,4 +1,4 @@ -#!/bin/bash +#! /usr/bin/env bash # Exit as soon as something errors. set -e From 61e9fc6280668c6e3abfa7e476e7ed6e7a05f0ea Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 2 Oct 2025 19:13:27 -0400 Subject: [PATCH 583/846] CI: Fix windows workflow --- .github/workflows/tests.yml | 6 +++++- scripts/ffmpeg-8.0.json | 2 +- scripts/ffmpeg-latest.json | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d2ceecad6..c2b8d1189 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -46,7 +46,11 @@ jobs: - name: Set Minimum MacOS Target if: runner.os == 'macOS' run: | - echo "MACOSX_DEPLOYMENT_TARGET=14.0" >> $GITHUB_ENV + if [ "${{ matrix.arch }}" = "x86_64" ]; then + echo "MACOSX_DEPLOYMENT_TARGET=11.0" >> $GITHUB_ENV + else + echo "MACOSX_DEPLOYMENT_TARGET=14.0" >> $GITHUB_ENV + fi - name: Build wheels env: CIBW_ARCHS: ${{ matrix.arch }} diff --git a/scripts/ffmpeg-8.0.json b/scripts/ffmpeg-8.0.json index 915f5bfc2..fc5719b8a 100644 --- a/scripts/ffmpeg-8.0.json +++ b/scripts/ffmpeg-8.0.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0-1/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0-2/ffmpeg-{platform}.tar.gz" } diff --git a/scripts/ffmpeg-latest.json b/scripts/ffmpeg-latest.json index 915f5bfc2..fc5719b8a 100644 --- a/scripts/ffmpeg-latest.json +++ b/scripts/ffmpeg-latest.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0-1/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0-2/ffmpeg-{platform}.tar.gz" } From ca699102f14f09a72cf5865f2049ab88df72f8e0 Mon Sep 17 00:00:00 2001 From: Santtu Keskinen Date: Fri, 3 Oct 2025 20:35:43 +0300 Subject: [PATCH 584/846] Writable and copyable attachment and data streams --- av/container/output.py | 104 +++++++++++++++++++++++++++------ av/container/output.pyi | 7 ++- av/stream.py | 17 ++++++ av/stream.pyi | 2 + tests/test_streams.py | 124 +++++++++++++++++++++++++++++++++------- 5 files changed, 215 insertions(+), 39 deletions(-) diff --git a/av/container/output.py b/av/container/output.py index 7738086ad..145437752 100644 --- a/av/container/output.py +++ b/av/container/output.py @@ -134,7 +134,7 @@ def add_stream_from_template( self, template: Stream, opaque: bool | None = None, **kwargs ): """ - Creates a new stream from a template. Supports video, audio, and subtitle streams. + Creates a new stream from a template. Supports video, audio, subtitle, data and attachment streams. :param template: Copy codec from another :class:`~av.stream.Stream` instance. :param opaque: If True, copy opaque data from the template's codec context. @@ -145,9 +145,7 @@ def add_stream_from_template( opaque = template.type != "video" if template.codec_context is None: - raise ValueError( - f"template stream of type {template.type} has no codec context" - ) + return self._add_stream_without_codec_from_template(template, **kwargs) codec_obj: Codec if opaque: # Copy ctx from template. @@ -196,6 +194,79 @@ def add_stream_from_template( return py_stream + def _add_stream_without_codec_from_template( + self, template: Stream, **kwargs + ) -> Stream: + codec_type: cython.int = template.ptr.codecpar.codec_type + if codec_type not in {lib.AVMEDIA_TYPE_ATTACHMENT, lib.AVMEDIA_TYPE_DATA}: + raise ValueError( + f"template stream of type {template.type} has no codec context" + ) + + stream: cython.pointer[lib.AVStream] = lib.avformat_new_stream( + self.ptr, cython.NULL + ) + if stream == cython.NULL: + raise MemoryError("Could not allocate stream") + + err_check(lib.avcodec_parameters_copy(stream.codecpar, template.ptr.codecpar)) + + # Mirror basic properties that are not derived from a codec context. + stream.time_base = template.ptr.time_base + stream.start_time = template.ptr.start_time + stream.duration = template.ptr.duration + stream.disposition = template.ptr.disposition + + py_stream: Stream = wrap_stream(self, stream, None) + self.streams.add_stream(py_stream) + + py_stream.metadata = dict(template.metadata) + + for k, v in kwargs.items(): + setattr(py_stream, k, v) + + return py_stream + + def add_attachment(self, name: str, mimetype: str, data: bytes): + """ + Create an attachment stream and embed its payload into the container header. + + - Only supported by formats that support attachments (e.g. Matroska). + - No per-packet muxing is required; attachments are written at header time. + """ + # Create stream with no codec (attachments are codec-less). + stream: cython.pointer[lib.AVStream] = lib.avformat_new_stream( + self.ptr, cython.NULL + ) + if stream == cython.NULL: + raise MemoryError("Could not allocate stream") + + stream.codecpar.codec_type = lib.AVMEDIA_TYPE_ATTACHMENT + stream.codecpar.codec_id = lib.AV_CODEC_ID_NONE + + # Allocate and copy payload into codecpar.extradata. + payload_size: cython.size_t = len(data) + if payload_size: + buf = cython.cast(cython.p_uchar, lib.av_malloc(payload_size + 1)) + if buf == cython.NULL: + raise MemoryError("Could not allocate attachment data") + # Copy bytes. + for i in range(payload_size): + buf[i] = data[i] + buf[payload_size] = 0 + stream.codecpar.extradata = cython.cast(cython.p_uchar, buf) + stream.codecpar.extradata_size = payload_size + + # Wrap as user-land stream. + meta_ptr = cython.address(stream.metadata) + err_check(lib.av_dict_set(meta_ptr, b"filename", name.encode(), 0)) + mime_bytes = mimetype.encode() + err_check(lib.av_dict_set(meta_ptr, b"mimetype", mime_bytes, 0)) + + py_stream: Stream = wrap_stream(self, stream, None) + self.streams.add_stream(py_stream) + return py_stream + def add_data_stream(self, codec_name=None, options: dict | None = None): """add_data_stream(codec_name=None) @@ -270,21 +341,20 @@ def start_encoding(self): # Finalize and open all streams. for stream in self.streams: ctx = stream.codec_context - # Skip codec context handling for data streams without codecs + # Skip codec context handling for streams without codecs (e.g. data/attachments). if ctx is None: - if stream.type != "data": + if stream.type not in {"data", "attachment"}: raise ValueError(f"Stream {stream.index} has no codec context") - continue - - if not ctx.is_open: - for k, v in self.options.items(): - ctx.options.setdefault(k, v) - ctx.open() - - # Track option consumption. - for k in self.options: - if k not in ctx.options: - used_options.add(k) + else: + if not ctx.is_open: + for k, v in self.options.items(): + ctx.options.setdefault(k, v) + ctx.open() + + # Track option consumption. + for k in self.options: + if k not in ctx.options: + used_options.add(k) stream._finalize_for_output() diff --git a/av/container/output.pyi b/av/container/output.pyi index b370095de..b3cbe1873 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -4,14 +4,14 @@ from typing import Sequence, TypeVar, Union, overload from av.audio import _AudioCodecName from av.audio.stream import AudioStream from av.packet import Packet -from av.stream import DataStream +from av.stream import AttachmentStream, DataStream, Stream from av.subtitles.stream import SubtitleStream from av.video import _VideoCodecName from av.video.stream import VideoStream from .core import Container -_StreamT = TypeVar("_StreamT", bound=Union[VideoStream, AudioStream, SubtitleStream]) +_StreamT = TypeVar("_StreamT", bound=Stream) class OutputContainer(Container): def __enter__(self) -> OutputContainer: ... @@ -42,6 +42,9 @@ class OutputContainer(Container): def add_stream_from_template( self, template: _StreamT, opaque: bool | None = None, **kwargs ) -> _StreamT: ... + def add_attachment( + self, name: str, mimetype: str, data: bytes + ) -> AttachmentStream: ... def add_data_stream( self, codec_name: str | None = None, options: dict[str, str] | None = None ) -> DataStream: ... diff --git a/av/stream.py b/av/stream.py index b5c22588b..a43c43496 100644 --- a/av/stream.py +++ b/av/stream.py @@ -148,6 +148,9 @@ def _finalize_for_output(self): errors=self.container.metadata_errors, ) + if self.codec_context is None: + return + if not self.ptr.time_base.num: self.ptr.time_base = self.codec_context.ptr.time_base @@ -316,3 +319,17 @@ def mimetype(self): :rtype: str | None """ return self.metadata.get("mimetype") + + @property + def data(self): + """Return the raw attachment payload as bytes.""" + extradata: cython.p_uchar = self.ptr.codecpar.extradata + size: cython.Py_ssize_t = self.ptr.codecpar.extradata_size + if extradata == cython.NULL or size <= 0: + return b"" + + payload = bytearray(size) + for i in range(size): + payload[i] = extradata[i] + + return bytes(payload) diff --git a/av/stream.pyi b/av/stream.pyi index 8289a6358..f6ca196c8 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -59,3 +59,5 @@ class AttachmentStream(Stream): type: Literal["attachment"] @property def mimetype(self) -> str | None: ... + @property + def data(self) -> bytes: ... diff --git a/tests/test_streams.py b/tests/test_streams.py index d07085de4..f82ce384b 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -1,10 +1,10 @@ import os from fractions import Fraction -from typing import Any, cast import pytest import av +import av.datasets from .common import fate_suite @@ -13,7 +13,14 @@ class TestStreams: @pytest.fixture(autouse=True) def cleanup(self): yield - for file in ("data.ts", "out.mkv"): + for file in ( + "data.ts", + "data_source.ts", + "data_copy.ts", + "out.mkv", + "video_with_attachment.mkv", + "remuxed_attachment.mkv", + ): if os.path.exists(file): os.remove(file) @@ -149,21 +156,98 @@ def test_data_stream(self) -> None: container.close() def test_data_stream_from_template(self) -> None: - """Test that adding a data stream from a template raises ValueError.""" - - # Open an existing container with a data stream - input_container = av.open(fate_suite("mxf/track_01_v02.mxf")) - input_data_stream = input_container.streams.data[0] - - # Create a new container and ensure using a data stream as a template raises ValueError - output_container = av.open("out.mkv", "w") - with pytest.raises(ValueError): - # input_data_stream is a DataStream at runtime; the test asserts that - # using it as a template raises ValueError. The static type stubs - # intentionally restrict which Stream subclasses are valid templates, - # so cast to Any here to keep the runtime check while satisfying - # the type checker. - output_container.add_stream_from_template(cast(Any, input_data_stream)) - - input_container.close() - output_container.close() + source_path = "data_source.ts" + payloads = [b"payload-a", b"payload-b", b"payload-c"] + + with av.open(source_path, "w") as source: + source_stream = source.add_data_stream() + for i, payload in enumerate(payloads): + packet = av.Packet(payload) + packet.pts = i + packet.stream = source_stream + source.mux(packet) + + copied_payloads: list[bytes] = [] + + with av.open(source_path) as input_container: + input_data_stream = input_container.streams.data[0] + + with av.open("data_copy.ts", "w") as output_container: + output_data_stream = output_container.add_stream_from_template( + input_data_stream + ) + + for packet in input_container.demux(input_data_stream): + payload = bytes(packet) + if not payload: + continue + copied_payloads.append(payload) + clone = av.Packet(payload) + clone.pts = packet.pts + clone.dts = packet.dts + clone.time_base = packet.time_base + clone.stream = output_data_stream + output_container.mux(clone) + + with av.open("data_copy.ts") as remuxed: + output_stream = remuxed.streams.data[0] + assert output_stream.codec_context is None + + remuxed_payloads: list[bytes] = [] + for packet in remuxed.demux(output_stream): + payload = bytes(packet) + if payload: + remuxed_payloads.append(payload) + + assert remuxed_payloads == copied_payloads + + def test_attachment_stream(self) -> None: + input_path = av.datasets.curated( + "pexels/time-lapse-video-of-night-sky-857195.mp4" + ) + input_ = av.open(input_path) + out1_path = "video_with_attachment.mkv" + + with av.open(out1_path, "w") as out1: + out1.add_attachment( + name="attachment.txt", mimetype="text/plain", data=b"hello\n" + ) + + in_v = input_.streams.video[0] + out_v = out1.add_stream_from_template(in_v) + + for packet in input_.demux(in_v): + if packet.dts is None: + continue + packet.stream = out_v + out1.mux(packet) + + input_.close() + + with av.open(out1_path) as c: + attachments = c.streams.attachments + assert len(attachments) == 1 + att = attachments[0] + assert att.name == "attachment.txt" + assert att.mimetype == "text/plain" + assert att.data == b"hello\n" + + out2_path = "remuxed_attachment.mkv" + with av.open(out1_path) as ic, av.open(out2_path, "w") as oc: + stream_map = {} + for s in ic.streams: + stream_map[s.index] = oc.add_stream_from_template(s) + + for packet in ic.demux(ic.streams.video): + if packet.dts is None: + continue + packet.stream = stream_map[packet.stream.index] + oc.mux(packet) + + with av.open(out2_path) as c: + attachments = c.streams.attachments + assert len(attachments) == 1 + att = attachments[0] + assert att.name == "attachment.txt" + assert att.mimetype == "text/plain" + assert att.data == b"hello\n" From 00493da0f49746bf06619d92cdf957737aab205e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 4 Oct 2025 20:35:30 -0400 Subject: [PATCH 585/846] Release 16.0.0 --- AUTHORS.rst | 8 +++++++- CHANGELOG.rst | 12 ++++++++++-- av/about.py | 2 +- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index b85c8ce1b..5de3b44c8 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -28,6 +28,8 @@ All contributors (by number of commits): - Dan Allan ; `@danielballan `_ - Moonsik Park - Santtu Keskinen +- velsinki <40809145+velsinki@users.noreply.github.com> +- Curtis Doty - Christoph Rackwitz - David Plowman - Alireza Davoudi ; `@adavoudi `_ @@ -43,6 +45,7 @@ All contributors (by number of commits): * zzjjbb <31069326+zzjjbb@users.noreply.github.com> * Joe Schiff <41972063+JoeSchiff@users.noreply.github.com> * Dexer <73297572+DexerBR@users.noreply.github.com> +* DE-AI <81620697+DE-AI@users.noreply.github.com> * rutsh * Felix Vollmer * Santiago Castro @@ -50,11 +53,13 @@ All contributors (by number of commits): * Ihor Liubymov * Johannes Erdfelt * Karl Litterfeldt ; `@litterfeldt `_ +* Kim Minjong * Martin Larralde * Simon-Martin Schröder * Matteo Destro * mephi42 * Miles Kaufmann +* Nathan Goldbaum * Pablo Prietz * Andrew Wason * Radek Senfeld ; `@radek-senfeld `_ @@ -62,6 +67,7 @@ All contributors (by number of commits): * Benjamin Chrétien <2742231+bchretien@users.noreply.github.com> * davidplowman <38045873+davidplowman@users.noreply.github.com> * Hanz <40712686+HanzCEO@users.noreply.github.com> +* Kesh Ikuma <79113787+tikuma-lsuhsc@users.noreply.github.com> * Artturin * Ian Lee * Ryan Huang @@ -76,8 +82,8 @@ All contributors (by number of commits): * henri-gasc * Jonathan Martin * Johan Jeppsson Karlin +* Kian-Meng Ang * Philipp Klaus -* Kim Minjong * Marcell Pardavi * Matteo Destro * Mattias Wadman diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7a445525e..eb4d1b7e4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,16 +18,24 @@ We are operating with `semantic versioning `_. v16.0.0 ------- -(unreleased) Major: + - Drop Python 3.9, Support Python 3.14. +- Drop support for i686 Linux. Features: -- Declare free-threaded support and support 3.13t by :gh-user:`ngoldbaum` in (:pr:`2005`). + - Add ``Filter.Context.process_command()`` method by :gh-user:`caffeinism` in (:pr:`2000`). +- Add packet side-data handling mechanism by :gh-user:`tikuma-lsuhsc ` in (:pr:`2003`). +- Implemented set_chapters method by :gh-user:`DE-AI` in (:pr:`2004`). +- Declare free-threaded support and support 3.13t by :gh-user:`ngoldbaum` in (:pr:`2005`). +- Add writable and copyable attachment and data streams by :gh-user:`skeskinen` in (:pr:`2026`). Fixes: + +- Declare free-threaded support and support 3.13t by :gh-user:`ngoldbaum` in (:pr:`2005`). +- Allow ``None`` in ``FilterContext.push()`` type stub by :gh-user:`velsinki` in (:pr:`2015`). - Fix typos v15.1.0 diff --git a/av/about.py b/av/about.py index 9a75d4360..5e5e9109a 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "15.1.0" +__version__ = "16.0.0" From 8e9e59781c922b7a166334028fb3ad805622f7ee Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 10 Oct 2025 04:45:01 -0400 Subject: [PATCH 586/846] Fix #2029 Add new HWDeviceType enums, skip test when version isn't SemVar. --- av/about.py | 2 +- av/codec/hwaccel.pyx | 3 +++ tests/test_packet.py | 7 +++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/av/about.py b/av/about.py index 5e5e9109a..610c11155 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "16.0.0" +__version__ = "16.0.1" diff --git a/av/codec/hwaccel.pyx b/av/codec/hwaccel.pyx index 257e6e7b2..e9a6bd194 100644 --- a/av/codec/hwaccel.pyx +++ b/av/codec/hwaccel.pyx @@ -25,6 +25,9 @@ class HWDeviceType(IntEnum): mediacodec = lib.AV_HWDEVICE_TYPE_MEDIACODEC vulkan = lib.AV_HWDEVICE_TYPE_VULKAN d3d12va = lib.AV_HWDEVICE_TYPE_D3D12VA + amf = 13 # FFmpeg >=8 + ohcodec = 14 + # TODO: When ffmpeg major is changed, check this enum. class HWConfigMethod(IntEnum): none = 0 diff --git a/tests/test_packet.py b/tests/test_packet.py index d5552238c..b0920ed3f 100644 --- a/tests/test_packet.py +++ b/tests/test_packet.py @@ -1,4 +1,5 @@ from typing import get_args +from unittest import SkipTest import av @@ -55,6 +56,12 @@ def test_set_duration(self) -> None: class TestPacketSideData: def test_data_types(self) -> None: dtypes = get_args(av.packet.PktSideDataT) + + if av.ffmpeg_version_info.startswith("n") or av.ffmpeg_version_info.count( + "." + ) not in (1, 2): + raise SkipTest(f"Expect version to be SemVar: {av.ffmpeg_version_info}") + ffmpeg_ver = [int(v) for v in av.ffmpeg_version_info.split(".", 2)[:2]] for dtype in dtypes: av_enum = av.packet.packet_sidedata_type_from_literal(dtype) From 7687256d87f22f78cb75bb4aba94dc6e504c9983 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 13 Oct 2025 07:33:44 -0400 Subject: [PATCH 587/846] Release 16.0.1 --- CHANGELOG.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index eb4d1b7e4..dce62d944 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,13 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. +v16.0.1 +------- + +Fixes: + +- Add new hwaccel enums by :gh-user:`WyattBlue` in (:pr:`2030`). + v16.0.0 ------- From 5f61ed9ebeb51fd18f4d72594043f14092e59ae5 Mon Sep 17 00:00:00 2001 From: Curtis Doty Date: Sat, 22 Nov 2025 14:10:38 -0800 Subject: [PATCH 588/846] Add libjpeg-dev dep to smoke --- .github/workflows/smoke.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index fe5b82687..fe59b4821 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -1,5 +1,6 @@ name: smoke on: + workflow_dispatch: push: branches: main paths-ignore: @@ -58,8 +59,18 @@ jobs: case ${{ matrix.config.os }} in ubuntu-24.04) sudo apt-get update - sudo apt-get install autoconf automake build-essential cmake \ - libtool pkg-config nasm zlib1g-dev libvorbis-dev libx264-dev + sudo apt-get install \ + autoconf \ + automake \ + build-essential \ + cmake \ + libjpeg-dev \ + libtool \ + libvorbis-dev \ + libx264-dev \ + nasm \ + pkg-config \ + zlib1g-dev if [[ "${{ matrix.config.extras }}" ]]; then sudo apt-get install doxygen wget fi From 979880e73cad64661e89a6d516918a77bf13ca68 Mon Sep 17 00:00:00 2001 From: Curtis Doty Date: Sat, 22 Nov 2025 16:32:08 -0800 Subject: [PATCH 589/846] Refactor AUTHORS (#2043) --- AUTHORS.py | 191 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 131 insertions(+), 60 deletions(-) diff --git a/AUTHORS.py b/AUTHORS.py index ad0746641..1d68e08b1 100644 --- a/AUTHORS.py +++ b/AUTHORS.py @@ -1,16 +1,23 @@ +""" Generate the AUTHORS.rst file from git commit history. + +This module reads git commit logs and produces a formatted list of contributors +grouped by their contribution count, mapping email aliases and GitHub usernames. +""" + +from dataclasses import dataclass import math -import subprocess +import subprocess # noqa: S404 -print( - """Contributors -============ -All contributors (by number of commits): -""" -) +def main() -> None: + """ Generate and print the AUTHORS.rst content. """ + + contributors = get_git_contributors() + print_contributors(contributors) +# ------------------------------------------------------------------------------ -email_map = { +EMAIL_ALIASES: dict[str, str | None] = { # Maintainers. "git@mikeboers.com": "github@mikeboers.com", "mboers@keypics.com": "github@mikeboers.com", @@ -27,7 +34,7 @@ "61652821+laggykiller@users.noreply.github.com": "chaudominic2@gmail.com", } -name_map = { +CANONICAL_NAMES: dict[str, str] = { "caspervdw@gmail.com": "Casper van der Wel", "daniel.b.allan@gmail.com": "Dan Allan", "mgoacolou@cls.fr": "Manuel Goacolou", @@ -37,7 +44,7 @@ "xxr@megvii.com": "Xinran Xu", } -github_map = { +GITHUB_USERNAMES: dict[str, str] = { "billy.shambrook@gmail.com": "billyshambrook", "daniel.b.allan@gmail.com": "danielballan", "davoudialireza@gmail.com": "adavoudi", @@ -55,57 +62,121 @@ "xxr@megvii.com": "xxr3376", "chaudominic2@gmail.com": "laggykiller", "wyattblue@auto-editor.com": "WyattBlue", + "Curtis@GreenKey.net": "dotysan", } -email_count = {} -for line in ( - subprocess.check_output(["git", "log", "--format=%aN,%aE"]).decode().splitlines() -): - name, email = line.strip().rsplit(",", 1) - - email = email_map.get(email, email) - if not email: - continue - - names = name_map.setdefault(email, set()) - if isinstance(names, set): - names.add(name) - - email_count[email] = email_count.get(email, 0) + 1 - - -last = None -block_i = 0 -for email, count in sorted(email_count.items(), key=lambda x: (-x[1], x[0])): - # This is the natural log, because of course it should be. ;) - order = int(math.log(count)) - if last and last != order: - block_i += 1 - print() - last = order - - names = name_map[email] - if isinstance(names, set): - name = ", ".join(sorted(names)) - else: - name = names - - github = github_map.get(email) - - # The '-' vs '*' is so that Sphinx treats them as different lists, and - # introduces a gap between them. - if github: - print( - "%s %s <%s>; `@%s `_" - % ("-*"[block_i % 2], name, email, github, github) - ) - else: - print( - "%s %s <%s>" - % ( - "-*"[block_i % 2], - name, - email, +@dataclass +class Contributor: + """ Represents a contributor with their email, names, and GitHub username. """ + + email: str + names: set[str] + github: str | None = None + commit_count: int = 0 + + @property + def display_name(self) -> str: + """ Return the formatted display name for the contributor. + + Returns: + Comma-separated sorted list of contributor names. + """ + + return ", ".join(sorted(self.names)) + + def format_line(self, bullet: str) -> str: + """ Format the contributor line for RST output. + + Args: + bullet: The bullet character to use (- or *). + + Returns: + Formatted RST line with contributor info. + """ + + if self.github: + return ( + f"{bullet} {self.display_name} <{self.email}>; " + f"`@{self.github} `_" + ) + return f"{bullet} {self.display_name} <{self.email}>" + + +def get_git_contributors() -> dict[str, Contributor]: + """ Parse git log and return contributors grouped by canonical email. + + Returns: + Dictionary mapping canonical emails to Contributor objects. + """ + + contributors: dict[str, Contributor] = {} + git_log = subprocess.check_output( + ["git", "log", "--format=%aN,%aE"], # noqa: S607 + text=True, + ).splitlines() + + for line in git_log: + name, email = line.strip().rsplit(",", 1) + canonical_email = EMAIL_ALIASES.get(email, email) + + if not canonical_email: + continue + + if canonical_email not in contributors: + contributors[canonical_email] = Contributor( + email=canonical_email, + names=set(), + github=GITHUB_USERNAMES.get(canonical_email), ) - ) + + contributor = contributors[canonical_email] + contributor.names.add(name) + contributor.commit_count += 1 + + for email, canonical_name in CANONICAL_NAMES.items(): + if email in contributors: + contributors[email].names = {canonical_name} + + return contributors + + +def print_contributors(contributors: dict[str, Contributor]) -> None: + """Print contributors grouped by logarithmic order of commits. + + Args: + contributors: Dictionary of contributors to print. + """ + + print("""\ + Contributors + ============ + + All contributors (by number of commits): + """.replace(" ", "")) + + sorted_contributors = sorted( + contributors.values(), + key=lambda c: (-c.commit_count, c.email), + ) + + last_order: int | None = None + block_index = 0 + + for contributor in sorted_contributors: + # This is the natural log, because of course it should be. ;) + order = int(math.log(contributor.commit_count)) + + if last_order and last_order != order: + block_index += 1 + print() + last_order = order + + # The '-' vs '*' is so that Sphinx treats them as different lists, and + # introduces a gap between them. + bullet = "-*"[block_index % 2] + print(contributor.format_line(bullet)) + + +if __name__ == "__main__": + main() From b22003ada81ad33d8ddefb5bc6728787ad4fb68d Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 26 Nov 2025 20:27:33 -0500 Subject: [PATCH 590/846] Use checkout 6 --- .github/workflows/smoke.yml | 6 +++--- .github/workflows/tests.yml | 6 +++--- pyproject.toml | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index fe59b4821..f75bf1b06 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Python uses: actions/setup-python@v5 @@ -45,7 +45,7 @@ jobs: PYAV_LIBRARY: ffmpeg-${{ matrix.config.ffmpeg }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 name: Checkout - name: Python ${{ matrix.config.python }} @@ -126,7 +126,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Conda shell: bash diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c2b8d1189..7611e4f18 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,7 +7,7 @@ jobs: package-source: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: actions/setup-python@v5 with: python-version: "3.13" @@ -39,7 +39,7 @@ jobs: - os: windows-latest arch: AMD64 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: actions/setup-python@v5 with: python-version: "3.13" @@ -79,7 +79,7 @@ jobs: runs-on: ubuntu-latest needs: [package-source, package-wheel] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: actions/download-artifact@v4 with: merge-multiple: true diff --git a/pyproject.toml b/pyproject.toml index 33cde6b2c..24bdc90a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ zip-safe = false version = {attr = "av.about.__version__"} [project.urls] -"Bug Tracker" = "https://github.com/PyAV-Org/PyAV/discussions/new?category=4-bugs" +"Bug Tracker" = "https://github.com/PyAV-Org/PyAV/issues" "Source Code" = "https://github.com/PyAV-Org/PyAV" homepage = "https://pyav.basswood-io.com" From 4c4a54c0115abc71f04fd6e403b275ae846ce34b Mon Sep 17 00:00:00 2001 From: Santtu Keskinen Date: Thu, 4 Dec 2025 17:30:02 -0800 Subject: [PATCH 591/846] Add read/write access to PacketSideData --- av/packet.pxd | 6 +++- av/packet.py | 16 ++++++++++- av/packet.pyi | 2 +- tests/test_packet.py | 65 +++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 85 insertions(+), 4 deletions(-) diff --git a/av/packet.pxd b/av/packet.pxd index 8e8ad034d..2640a5aeb 100644 --- a/av/packet.pxd +++ b/av/packet.pxd @@ -7,11 +7,15 @@ from av.bytesource cimport ByteSource from av.stream cimport Stream -cdef class PacketSideData: +cdef class PacketSideData(Buffer): cdef uint8_t *data cdef size_t size cdef lib.AVPacketSideDataType dtype + cdef size_t _buffer_size(self) + cdef void* _buffer_ptr(self) + cdef bint _buffer_writable(self) + cdef class Packet(Buffer): cdef lib.AVPacket* ptr cdef Stream _stream diff --git a/av/packet.py b/av/packet.py index e1051b653..d21b432ea 100644 --- a/av/packet.py +++ b/av/packet.py @@ -2,6 +2,7 @@ import cython from cython.cimports import libav as lib +from cython.cimports.av.buffer import Buffer from cython.cimports.av.bytesource import bytesource from cython.cimports.av.error import err_check from cython.cimports.av.opaque import opaque_container @@ -64,7 +65,7 @@ def packet_sidedata_type_from_literal(dtype: PktSideDataT) -> lib.AVPacketSideDa @cython.cclass -class PacketSideData: +class PacketSideData(Buffer): @staticmethod def from_packet(packet: Packet, data_type: PktSideDataT) -> PacketSideData: """create new PacketSideData by copying an existing packet's side data @@ -152,6 +153,19 @@ def data_size(self) -> int: """ return self.size + # Buffer protocol implementation + @cython.cfunc + def _buffer_size(self) -> cython.size_t: + return self.size + + @cython.cfunc + def _buffer_ptr(self) -> cython.p_void: + return self.data + + @cython.cfunc + def _buffer_writable(self) -> cython.bint: + return True + def __bool__(self) -> bool: """ True if this object holds side data. diff --git a/av/packet.pyi b/av/packet.pyi index 8dab20065..6b1a271c2 100644 --- a/av/packet.pyi +++ b/av/packet.pyi @@ -50,7 +50,7 @@ PktSideDataT = Literal[ "rtcp_sr", ] -class PacketSideData: +class PacketSideData(Buffer): @staticmethod def from_packet(packet: Packet, dtype: PktSideDataT) -> PacketSideData: ... def to_packet(self, packet: Packet, move: bool = False): ... diff --git a/tests/test_packet.py b/tests/test_packet.py index b0920ed3f..044dfd907 100644 --- a/tests/test_packet.py +++ b/tests/test_packet.py @@ -1,9 +1,10 @@ +import struct from typing import get_args from unittest import SkipTest import av -from .common import fate_suite +from .common import fate_suite, sandboxed class TestProperties: @@ -100,3 +101,65 @@ def test_palette(self) -> None: nxt.set_sidedata(sdata, move=True) assert not bool(sdata) + + def test_buffer_protocol(self) -> None: + with av.open(fate_suite("h264/extradata-reload-multi-stsd.mov")) as container: + for pkt in container.demux(): + if pkt.has_sidedata("new_extradata"): + sdata = pkt.get_sidedata("new_extradata") + + raw = bytes(sdata) + assert len(raw) == sdata.data_size > 0 + assert sdata.buffer_size == sdata.data_size + assert sdata.buffer_ptr != 0 + assert bytes(memoryview(sdata)) == raw + + # Modify and verify changes stick + sdata.update(b"\xde\xad\xbe\xef" + raw[4:]) + assert bytes(sdata)[:4] == b"\xde\xad\xbe\xef" + + pkt.set_sidedata(sdata) + assert ( + bytes(pkt.get_sidedata("new_extradata"))[:4] + == b"\xde\xad\xbe\xef" + ) + return + + raise AssertionError("No packet with new_extradata side data found") + + def test_skip_samples_remux(self) -> None: + # Source file has skip_end=706 on last packet. Setting to 0 should + # result in 706 more decoded samples. And the file duration reported by + # the container should also increase. + output_path = sandboxed("skip_samples_modified.mkv") + + with av.open(fate_suite("mkv/codec_delay_opus.mkv")) as c: + original_samples = sum(f.samples for f in c.decode(c.streams.audio[0])) + + with av.open(fate_suite("mkv/codec_delay_opus.mkv")) as inp: + original_duration = inp.duration + audio_stream = inp.streams.audio[0] + with av.open(output_path, "w") as out: + out_stream = out.add_stream_from_template(audio_stream) + for pkt in inp.demux(audio_stream): + if pkt.dts is None: + continue + if pkt.has_sidedata("skip_samples"): + sdata = pkt.get_sidedata("skip_samples") + raw = bytes(sdata) + skip_end = struct.unpack(" original_duration From 6a0cf682201150abf735215e3834f4fd7d4716ba Mon Sep 17 00:00:00 2001 From: Santtu Keskinen Date: Wed, 3 Dec 2025 21:12:24 +0200 Subject: [PATCH 592/846] Add subtitle encoding support --- av/subtitles/codeccontext.pxd | 1 + av/subtitles/codeccontext.py | 96 ++++++++++++++++++++++ av/subtitles/codeccontext.pyi | 2 + av/subtitles/subtitle.py | 113 ++++++++++++++++++++++++-- av/subtitles/subtitle.pyi | 10 ++- include/libavcodec/avcodec.pxd | 4 + include/libavutil/avutil.pxd | 2 + tests/test_subtitles.py | 144 ++++++++++++++++++++++++++++++++- 8 files changed, 365 insertions(+), 7 deletions(-) diff --git a/av/subtitles/codeccontext.pxd b/av/subtitles/codeccontext.pxd index c94744e45..01c8fbf1f 100644 --- a/av/subtitles/codeccontext.pxd +++ b/av/subtitles/codeccontext.pxd @@ -3,4 +3,5 @@ from av.packet cimport Packet cdef class SubtitleCodecContext(CodecContext): + cdef bint subtitle_header_set cpdef decode2(self, Packet packet) diff --git a/av/subtitles/codeccontext.py b/av/subtitles/codeccontext.py index 0b3dda063..78c274df2 100644 --- a/av/subtitles/codeccontext.py +++ b/av/subtitles/codeccontext.py @@ -1,12 +1,108 @@ import cython from cython.cimports import libav as lib +from cython.cimports.av.bytesource import ByteSource, bytesource from cython.cimports.av.error import err_check from cython.cimports.av.packet import Packet from cython.cimports.av.subtitles.subtitle import SubtitleProxy, SubtitleSet +from cython.cimports.cpython.bytes import PyBytes_FromStringAndSize +from cython.cimports.libc.string import memcpy, strlen @cython.cclass class SubtitleCodecContext(CodecContext): + @property + def subtitle_header(self) -> bytes | None: + """Get the subtitle header data (ASS/SSA format for text subtitles).""" + if ( + self.ptr.subtitle_header == cython.NULL + or self.ptr.subtitle_header_size <= 0 + ): + return None + return PyBytes_FromStringAndSize( + cython.cast(cython.p_char, self.ptr.subtitle_header), + self.ptr.subtitle_header_size, + ) + + @subtitle_header.setter + def subtitle_header(self, data: bytes | None) -> None: + """Set the subtitle header data.""" + source: ByteSource + if data is None: + lib.av_freep(cython.address(self.ptr.subtitle_header)) + self.ptr.subtitle_header_size = 0 + else: + source = bytesource(data) + self.ptr.subtitle_header = cython.cast( + cython.p_uchar, + lib.av_realloc( + self.ptr.subtitle_header, + source.length + lib.AV_INPUT_BUFFER_PADDING_SIZE, + ), + ) + if not self.ptr.subtitle_header: + raise MemoryError("Cannot allocate subtitle_header") + memcpy(self.ptr.subtitle_header, source.ptr, source.length) + self.ptr.subtitle_header_size = source.length + self.subtitle_header_set = True + + def __dealloc__(self) -> None: + if self.ptr and self.subtitle_header_set: + lib.av_freep(cython.address(self.ptr.subtitle_header)) + + def encode_subtitle(self, subtitle: SubtitleSet) -> Packet: + """ + Encode a SubtitleSet into a Packet. + + Args: + subtitle: The SubtitleSet to encode + + Returns: + A Packet containing the encoded subtitle data + """ + if not self.codec.ptr: + raise ValueError("Cannot encode with unknown codec") + + self.open(strict=False) + + # Calculate buffer size from subtitle text length + buf_size: cython.size_t = 0 + i: cython.uint + for i in range(subtitle.proxy.struct.num_rects): + rect = subtitle.proxy.struct.rects[i] + if rect.ass != cython.NULL: + buf_size += strlen(rect.ass) + if rect.text != cython.NULL: + buf_size += strlen(rect.text) + buf_size += 1024 # padding for format overhead + + buf: cython.p_uchar = cython.cast(cython.p_uchar, lib.av_malloc(buf_size)) + if buf == cython.NULL: + raise MemoryError("Failed to allocate subtitle encode buffer") + + ret: cython.int = lib.avcodec_encode_subtitle( + self.ptr, + buf, + buf_size, + cython.address(subtitle.proxy.struct), + ) + + if ret < 0: + lib.av_free(buf) + err_check(ret, "avcodec_encode_subtitle()") + + packet: Packet = Packet(ret) + memcpy(packet.ptr.data, buf, ret) + lib.av_free(buf) + + packet.ptr.pts = subtitle.proxy.struct.pts + packet.ptr.dts = subtitle.proxy.struct.pts + packet.ptr.duration = ( + subtitle.proxy.struct.end_display_time + - subtitle.proxy.struct.start_display_time + ) + + return packet + @cython.cfunc def _send_packet_and_recv(self, packet: Packet | None): if packet is None: diff --git a/av/subtitles/codeccontext.pyi b/av/subtitles/codeccontext.pyi index 90c700935..7f8a49d09 100644 --- a/av/subtitles/codeccontext.pyi +++ b/av/subtitles/codeccontext.pyi @@ -6,4 +6,6 @@ from av.subtitles.subtitle import SubtitleSet class SubtitleCodecContext(CodecContext): type: Literal["subtitle"] + subtitle_header: bytes | None def decode2(self, packet: Packet) -> SubtitleSet | None: ... + def encode_subtitle(self, subtitle: SubtitleSet) -> Packet: ... diff --git a/av/subtitles/subtitle.py b/av/subtitles/subtitle.py index 1acadf0b5..68f85fd1d 100644 --- a/av/subtitles/subtitle.py +++ b/av/subtitles/subtitle.py @@ -1,6 +1,7 @@ import cython from cython.cimports.cpython import PyBuffer_FillInfo, PyBytes_FromString -from cython.cimports.libc.stdint import uint64_t +from cython.cimports.libc.stdint import int64_t, uint64_t +from cython.cimports.libc.string import memcpy, strlen @cython.cclass @@ -9,6 +10,9 @@ def __dealloc__(self): lib.avsubtitle_free(cython.address(self.struct)) +_cinit_bypass_sentinel = object() + + @cython.cclass class SubtitleSet: """ @@ -17,11 +21,94 @@ class SubtitleSet: Wraps :ffmpeg:`AVSubtitle`. """ - def __cinit__(self, proxy: SubtitleProxy): - self.proxy = proxy - self.rects = tuple( - build_subtitle(self, i) for i in range(self.proxy.struct.num_rects) + def __cinit__(self, proxy_or_sentinel=None): + if proxy_or_sentinel is _cinit_bypass_sentinel: + # Creating empty SubtitleSet for encoding + self.proxy = SubtitleProxy() + self.rects = () + elif isinstance(proxy_or_sentinel, SubtitleProxy): + # Creating from decoded subtitle + self.proxy = proxy_or_sentinel + self.rects = tuple( + build_subtitle(self, i) for i in range(self.proxy.struct.num_rects) + ) + else: + raise TypeError( + "SubtitleSet requires a SubtitleProxy or use SubtitleSet.create()" + ) + + @staticmethod + def create( + text: bytes, + start: int, + end: int, + pts: int = 0, + subtitle_format: int = 1, + ) -> "SubtitleSet": + """ + Create a SubtitleSet for encoding. + + Args: + text: The subtitle text in ASS dialogue format + (e.g. b"0,0,Default,,0,0,0,,Hello World") + start: Start display time as offset from pts (typically 0) + end: End display time as offset from pts (i.e., duration) + pts: Presentation timestamp in stream time_base units + subtitle_format: Subtitle format (default 1 for text) + + Note: + All timing values should be in stream time_base units. + For MKV (time_base=1/1000), units are milliseconds. + For MP4 (time_base=1/1000000), units are microseconds. + + Returns: + A SubtitleSet ready for encoding + """ + subset: SubtitleSet = SubtitleSet(_cinit_bypass_sentinel) + + subset.proxy.struct.format = subtitle_format + subset.proxy.struct.start_display_time = start + subset.proxy.struct.end_display_time = end + subset.proxy.struct.pts = pts + + subset.proxy.struct.num_rects = 1 + subset.proxy.struct.rects = cython.cast( + cython.pointer[cython.pointer[lib.AVSubtitleRect]], + lib.av_mallocz(cython.sizeof(cython.pointer[lib.AVSubtitleRect])), + ) + if subset.proxy.struct.rects == cython.NULL: + raise MemoryError("Failed to allocate subtitle rects array") + + rect: cython.pointer[lib.AVSubtitleRect] = cython.cast( + cython.pointer[lib.AVSubtitleRect], + lib.av_mallocz(cython.sizeof(lib.AVSubtitleRect)), ) + if rect == cython.NULL: + lib.av_free(subset.proxy.struct.rects) + subset.proxy.struct.rects = cython.NULL + raise MemoryError("Failed to allocate subtitle rect") + + subset.proxy.struct.rects[0] = rect + + rect.x = 0 + rect.y = 0 + rect.w = 0 + rect.h = 0 + rect.nb_colors = 0 + rect.type = lib.SUBTITLE_ASS + rect.text = cython.NULL + rect.flags = 0 + + text_len: cython.Py_ssize_t = len(text) + rect.ass = cython.cast(cython.p_char, lib.av_malloc(text_len + 1)) + if rect.ass == cython.NULL: + raise MemoryError("Failed to allocate subtitle text") + memcpy(rect.ass, cython.cast(cython.p_char, text), text_len) + rect.ass[text_len] = 0 + + subset.rects = (AssSubtitle(subset, 0),) + + return subset def __repr__(self): return ( @@ -32,19 +119,35 @@ def __repr__(self): def format(self): return self.proxy.struct.format + @format.setter + def format(self, value: int): + self.proxy.struct.format = value + @property def start_display_time(self): return self.proxy.struct.start_display_time + @start_display_time.setter + def start_display_time(self, value: int): + self.proxy.struct.start_display_time = value + @property def end_display_time(self): return self.proxy.struct.end_display_time + @end_display_time.setter + def end_display_time(self, value: int): + self.proxy.struct.end_display_time = value + @property def pts(self): """Same as packet pts, in av.time_base.""" return self.proxy.struct.pts + @pts.setter + def pts(self, value: int): + self.proxy.struct.pts = value + def __len__(self): return len(self.rects) diff --git a/av/subtitles/subtitle.pyi b/av/subtitles/subtitle.pyi index 2a35d0a55..d3d4201fd 100644 --- a/av/subtitles/subtitle.pyi +++ b/av/subtitles/subtitle.pyi @@ -5,8 +5,16 @@ class SubtitleSet: start_display_time: int end_display_time: int pts: int - rects: tuple[Subtitle] + rects: tuple[Subtitle, ...] + @staticmethod + def create( + text: bytes, + start: int, + end: int, + pts: int = 0, + subtitle_format: int = 1, + ) -> SubtitleSet: ... def __len__(self) -> int: ... def __iter__(self) -> Iterator[Subtitle]: ... def __getitem__(self, i: int) -> Subtitle: ... diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 43ecb0260..b2e78baf0 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -289,6 +289,10 @@ cdef extern from "libavcodec/avcodec.h" nogil: int extradata_size uint8_t *extradata + # Subtitle header (ASS/SSA format for text subtitles) + uint8_t *subtitle_header + int subtitle_header_size + int delay AVCodec *codec diff --git a/include/libavutil/avutil.pxd b/include/libavutil/avutil.pxd index 49be65f69..1bfbd2c5d 100644 --- a/include/libavutil/avutil.pxd +++ b/include/libavutil/avutil.pxd @@ -110,9 +110,11 @@ cdef extern from "libavutil/avutil.h" nogil: cdef double M_PI cdef void* av_malloc(size_t size) + cdef void* av_mallocz(size_t size) cdef void *av_calloc(size_t nmemb, size_t size) cdef void *av_realloc(void *ptr, size_t size) + cdef void av_free(void *ptr) cdef void av_freep(void *ptr) cdef int av_get_bytes_per_sample(AVSampleFormat sample_fmt) diff --git a/tests/test_subtitles.py b/tests/test_subtitles.py index 66e6f9cfa..d2a199924 100644 --- a/tests/test_subtitles.py +++ b/tests/test_subtitles.py @@ -1,9 +1,12 @@ +import io from typing import cast import av +from av.codec.context import CodecContext +from av.subtitles.codeccontext import SubtitleCodecContext from av.subtitles.subtitle import AssSubtitle, BitmapSubtitle -from .common import fate_suite +from .common import TestCase, fate_suite class TestSubtitle: @@ -73,3 +76,142 @@ def test_subtitle_flush(self) -> None: subs.extend(stream.decode()) assert len(subs) == 3 + + def test_subtitle_header_read(self) -> None: + """Test reading subtitle_header from a decoded subtitle stream.""" + path = fate_suite("sub/MovText_capability_tester.mp4") + + with av.open(path) as container: + stream = container.streams.subtitles[0] + ctx = cast(SubtitleCodecContext, stream.codec_context) + header = ctx.subtitle_header + assert header is None or isinstance(header, bytes) + + def test_subtitle_header_write(self) -> None: + """Test setting subtitle_header on encoder context.""" + ctx = cast(SubtitleCodecContext, CodecContext.create("mov_text", "w")) + assert ctx.subtitle_header is None + + ass_header = b"[Script Info]\nScriptType: v4.00+\n" + ctx.subtitle_header = ass_header + assert ctx.subtitle_header == ass_header + + new_header = b"[Script Info]\nScriptType: v4.00\n" + ctx.subtitle_header = new_header + assert ctx.subtitle_header == new_header + + ctx.subtitle_header = None + assert ctx.subtitle_header is None + + +class TestSubtitleEncoding(TestCase): + def test_subtitle_set_create(self) -> None: + """Test creating SubtitleSet for encoding.""" + from av.subtitles.subtitle import SubtitleSet + + text = b"0,0,Default,,0,0,0,,Hello World" + subtitle = SubtitleSet.create(text=text, start=0, end=2000, pts=0) + + assert subtitle.format == 1 + assert subtitle.start_display_time == 0 + assert subtitle.end_display_time == 2000 + assert subtitle.pts == 0 + assert len(subtitle) == 1 + assert cast(AssSubtitle, subtitle[0]).ass == text + + def test_subtitle_encode_mp4(self) -> None: + """Test encoding subtitles to MP4 container.""" + from av.subtitles.subtitle import SubtitleSet + + ass_header = b"""[Script Info] +ScriptType: v4.00+ +PlayResX: 640 +PlayResY: 480 + +[V4+ Styles] +Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding +Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1 + +[Events] +Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text +""" + + output = io.BytesIO() + with av.open(output, "w", format="mp4") as container: + # MP4 requires video for subtitles + video_stream = container.add_stream("libx264", rate=30) + video_stream.width = 640 + video_stream.height = 480 + video_stream.pix_fmt = "yuv420p" + + sub_stream = container.add_stream("mov_text") + sub_ctx = cast(SubtitleCodecContext, sub_stream.codec_context) + sub_ctx.subtitle_header = ass_header + + container.start_encoding() + + frame = av.VideoFrame(640, 480, "yuv420p") + frame.pts = 0 + for packet in video_stream.encode(frame): + container.mux(packet) + + time_base = sub_stream.time_base + assert time_base is not None + subtitle = SubtitleSet.create( + text=b"0,0,Default,,0,0,0,,Hello World", + start=0, + end=int(2 / time_base), + pts=0, + ) + packet = sub_ctx.encode_subtitle(subtitle) + packet.stream = sub_stream + container.mux(packet) + + for packet in video_stream.encode(): + container.mux(packet) + + output.seek(0) + with av.open(output) as container: + assert len(container.streams.subtitles) == 1 + + def test_subtitle_encode_mkv_srt(self) -> None: + """Test encoding SRT subtitles to MKV container.""" + from av.subtitles.subtitle import SubtitleSet + + minimal_header = b"[Script Info]\n" + + output = io.BytesIO() + with av.open(output, "w", format="matroska") as container: + sub_stream = container.add_stream("srt") + sub_ctx = cast(SubtitleCodecContext, sub_stream.codec_context) + sub_ctx.subtitle_header = minimal_header + + container.start_encoding() + + time_base = sub_stream.time_base + assert time_base is not None + for text, start_sec, duration_sec in [ + (b"0,0,Default,,0,0,0,,First subtitle", 0, 2), + (b"0,0,Default,,0,0,0,,Second subtitle", 2, 2), + (b"0,0,Default,,0,0,0,,Third subtitle", 4, 2), + ]: + subtitle = SubtitleSet.create( + text=text, + start=0, + end=int(duration_sec / time_base), + pts=int(start_sec / time_base), + ) + packet = sub_ctx.encode_subtitle(subtitle) + packet.stream = sub_stream + container.mux(packet) + + output.seek(0) + with av.open(output, mode="r") as input_container: + assert len(input_container.streams.subtitles) == 1 + subs: list[AssSubtitle] = [] + for packet in input_container.demux(): + subs.extend(cast(list[AssSubtitle], packet.decode())) + assert len(subs) == 3 + assert b"First subtitle" in subs[0].dialogue + assert b"Second subtitle" in subs[1].dialogue + assert b"Third subtitle" in subs[2].dialogue From d979acb9fe28890e35ae3f0cfe801dfab0479c50 Mon Sep 17 00:00:00 2001 From: Mattias Wadman Date: Fri, 12 Dec 2025 14:39:18 +0100 Subject: [PATCH 593/846] Add yuv422p support for video frame to_ndarray and from_ndarray --- av/video/frame.py | 23 +++++++++++++++++++++-- tests/test_videoframe.py | 8 ++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/av/video/frame.py b/av/video/frame.py index 0d60fd8e9..174683727 100644 --- a/av/video/frame.py +++ b/av/video/frame.py @@ -449,7 +449,13 @@ def to_ndarray(self, channel_last=False, **kwargs): import numpy as np # check size - if frame.format.name in {"yuv420p", "yuvj420p", "yuyv422", "yuv422p10le"}: + if frame.format.name in { + "yuv420p", + "yuvj420p", + "yuyv422", + "yuv422p10le", + "yuv422p", + }: assert frame.width % 2 == 0, ( "the width has to be even for this pixel format" ) @@ -563,7 +569,7 @@ def to_ndarray(self, channel_last=False, **kwargs): return array # special cases - if frame.format.name in {"yuv420p", "yuvj420p"}: + if frame.format.name in {"yuv420p", "yuvj420p", "yuv422p"}: return np.hstack( [ useful_array(frame.planes[0]), @@ -1022,6 +1028,19 @@ def from_ndarray(array, format="rgb24", channel_last=False): copy_array_to_plane(flat[u_start:v_start], frame.planes[1], 1) copy_array_to_plane(flat[v_start:], frame.planes[2], 1) return frame + elif format == "yuv422p": + check_ndarray(array, "uint8", 2) + check_ndarray_shape(array, array.shape[0] % 4 == 0) + check_ndarray_shape(array, array.shape[1] % 2 == 0) + + frame = VideoFrame(array.shape[1], array.shape[0] // 2, format) + u_start = frame.width * frame.height + v_start = u_start + u_start // 2 + flat = array.reshape(-1) + copy_array_to_plane(flat[0:u_start], frame.planes[0], 1) + copy_array_to_plane(flat[u_start:v_start], frame.planes[1], 1) + copy_array_to_plane(flat[v_start:], frame.planes[2], 1) + return frame elif format == "yuv422p10le": if not isinstance(array, np.ndarray) or array.dtype != np.uint16: raise ValueError("Array must be uint16 type") diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index a977cd828..80e59614f 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -648,6 +648,14 @@ def test_ndarray_yuv420p() -> None: assertNdarraysEqual(frame.to_ndarray(), array) +def test_ndarray_yuv422p() -> None: + array = numpy.random.randint(0, 256, size=(960, 640), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="yuv422p") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "yuv422p" + assertNdarraysEqual(frame.to_ndarray(), array) + + def test_ndarray_yuv420p_align() -> None: array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuv420p") From 340b40ee5075ec8408bb60e1c119eea3d4eb3b4c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 12 Dec 2025 11:49:15 -0500 Subject: [PATCH 594/846] Fix #2053 --- av/container/core.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/container/core.pyi b/av/container/core.pyi index f923ba01e..26e76ed2c 100644 --- a/av/container/core.pyi +++ b/av/container/core.pyi @@ -99,7 +99,7 @@ class Container: exc_type: Type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, - ) -> bool: ... + ) -> None: ... def set_timeout(self, timeout: Real | None) -> None: ... def start_timeout(self) -> None: ... def chapters(self) -> list[Chapter]: ... From f10e3aee0fa0315bfbdb1f64b901b406fa87c95b Mon Sep 17 00:00:00 2001 From: Nils DEYBACH <68770774+ndeybach@users.noreply.github.com> Date: Tue, 16 Dec 2025 12:14:46 +0100 Subject: [PATCH 595/846] Normalize line endings via .gitattributes --- .gitattributes | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..24b8f0829 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# POSIX-sensitive files: always LF +*.sh text eol=lf +Makefile text eol=lf \ No newline at end of file From 3e5b4a10eef4ad6e64782864bd4ce77baec7a9b9 Mon Sep 17 00:00:00 2001 From: Nils DEYBACH <68770774+ndeybach@users.noreply.github.com> Date: Tue, 16 Dec 2025 12:40:17 +0100 Subject: [PATCH 596/846] Add binding for "avcodec_find_best_pix_fmt_of_list" (#2058) --- av/codec/__init__.py | 10 +++++- av/codec/codec.pyi | 26 +++++++++++++- av/codec/codec.pyx | 59 +++++++++++++++++++++++++++++++- include/libavcodec/avcodec.pxd | 7 ++++ tests/test_codec.py | 62 ++++++++++++++++++++++++++++++++++ 5 files changed, 161 insertions(+), 3 deletions(-) diff --git a/av/codec/__init__.py b/av/codec/__init__.py index f35f9b7d4..178ed5106 100644 --- a/av/codec/__init__.py +++ b/av/codec/__init__.py @@ -1,4 +1,11 @@ -from .codec import Capabilities, Codec, Properties, codec_descriptor, codecs_available +from .codec import ( + Capabilities, + Codec, + Properties, + codec_descriptor, + codecs_available, + find_best_pix_fmt_of_list, +) from .context import CodecContext __all__ = ( @@ -7,5 +14,6 @@ "Properties", "codec_descriptor", "codecs_available", + "find_best_pix_fmt_of_list", "CodecContext", ) diff --git a/av/codec/codec.pyi b/av/codec/codec.pyi index 4270c641f..49b0bc43c 100644 --- a/av/codec/codec.pyi +++ b/av/codec/codec.pyi @@ -1,6 +1,6 @@ from enum import Flag, IntEnum from fractions import Fraction -from typing import ClassVar, Literal, cast, overload +from typing import ClassVar, Literal, Sequence, cast, overload from av.audio.codeccontext import AudioCodecContext from av.audio.format import AudioFormat @@ -113,3 +113,27 @@ codecs_available: set[str] def dump_codecs() -> None: ... def dump_hwconfigs() -> None: ... + +PixFmtLike = str | VideoFormat + +def find_best_pix_fmt_of_list( + pix_fmts: Sequence[PixFmtLike], + src_pix_fmt: PixFmtLike, + has_alpha: bool = False, +) -> tuple[VideoFormat | None, int]: + """ + Find the best pixel format to convert to given a source format. + + Wraps :ffmpeg:`avcodec_find_best_pix_fmt_of_list`. + + :param pix_fmts: Iterable of pixel formats to choose from (str or VideoFormat). + :param src_pix_fmt: Source pixel format (str or VideoFormat). + :param bool has_alpha: Whether the source alpha channel is used. + :return: (best_format, loss): best_format is the best matching pixel format from + the list, or None if no suitable format was found; loss is Combination of flags informing you what kind of losses will occur. + :rtype: (VideoFormat | None, int) + + Note on loss: it is a bitmask of FFmpeg loss flags describing what kinds of information would be lost converting from src_pix_fmt to best_format (e.g. loss of alpha, chroma, colorspace, resolution, bit depth, etc.). Multiple losses can be present at once, so the value is meant to be interpreted with bitwise & against FFmpeg's FF_LOSS_* constants. + For exact behavior see: libavutil/pixdesc.c/get_pix_fmt_score() in ffmpeg source code. + """ + ... diff --git a/av/codec/codec.pyx b/av/codec/codec.pyx index aeab61cc3..29040c64f 100644 --- a/av/codec/codec.pyx +++ b/av/codec/codec.pyx @@ -4,9 +4,10 @@ from av.audio.format cimport get_audio_format from av.codec.hwaccel cimport wrap_hwconfig from av.descriptor cimport wrap_avclass from av.utils cimport avrational_to_fraction -from av.video.format cimport get_video_format +from av.video.format cimport VideoFormat, get_pix_fmt, get_video_format from enum import Flag, IntEnum +from libc.stdlib cimport free, malloc cdef object _cinit_sentinel = object() @@ -387,3 +388,59 @@ def dump_hwconfigs(): print(" ", codec.name) for config in configs: print(" ", config) + + +def find_best_pix_fmt_of_list(pix_fmts, src_pix_fmt, has_alpha=False): + """ + Find the best pixel format to convert to given a source format. + + Wraps :ffmpeg:`avcodec_find_best_pix_fmt_of_list`. + + :param pix_fmts: Iterable of pixel formats to choose from (str or VideoFormat). + :param src_pix_fmt: Source pixel format (str or VideoFormat). + :param bool has_alpha: Whether the source alpha channel is used. + :return: (best_format, loss) + :rtype: (VideoFormat | None, int) + """ + cdef lib.AVPixelFormat src + cdef lib.AVPixelFormat best + cdef lib.AVPixelFormat *c_list = NULL + cdef Py_ssize_t n + cdef Py_ssize_t i + cdef object item + cdef int c_loss + + if pix_fmts is None: + raise TypeError("pix_fmts must not be None") + + pix_fmts = tuple(pix_fmts) + if not pix_fmts: + return None, 0 + + if isinstance(src_pix_fmt, VideoFormat): + src = (src_pix_fmt).pix_fmt + else: + src = get_pix_fmt(src_pix_fmt) + + n = len(pix_fmts) + c_list = malloc((n + 1) * sizeof(lib.AVPixelFormat)) + if c_list == NULL: + raise MemoryError() + + try: + for i in range(n): + item = pix_fmts[i] + if isinstance(item, VideoFormat): + c_list[i] = (item).pix_fmt + else: + c_list[i] = get_pix_fmt(item) + c_list[n] = lib.AV_PIX_FMT_NONE + + c_loss = 0 + best = lib.avcodec_find_best_pix_fmt_of_list( + c_list, src, 1 if has_alpha else 0, &c_loss + ) + return get_video_format(best, 0, 0), c_loss + finally: + if c_list != NULL: + free(c_list) diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index b2e78baf0..9f158b88a 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -75,6 +75,13 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef char* avcodec_configuration() cdef char* avcodec_license() + AVPixelFormat avcodec_find_best_pix_fmt_of_list( + const AVPixelFormat *pix_fmt_list, + AVPixelFormat src_pix_fmt, + int has_alpha, + int *loss_ptr, + ) + cdef size_t AV_INPUT_BUFFER_PADDING_SIZE cdef int64_t AV_NOPTS_VALUE diff --git a/tests/test_codec.py b/tests/test_codec.py index 9d72a243c..17432c926 100644 --- a/tests/test_codec.py +++ b/tests/test_codec.py @@ -1,6 +1,7 @@ import pytest from av import AudioFormat, Codec, VideoFormat, codecs_available +from av.codec import find_best_pix_fmt_of_list from av.codec.codec import UnknownCodecError @@ -89,3 +90,64 @@ def test_codec_opus_encoder() -> None: def test_codecs_available() -> None: assert codecs_available + + +def test_find_best_pix_fmt_of_list_empty() -> None: + best, loss = find_best_pix_fmt_of_list([], "rgb24") + assert best is None + assert loss == 0 + + +@pytest.mark.parametrize( + "pix_fmts,src_pix_fmt,expected_best", + [ + (["rgb24", "yuv420p"], "rgb24", "rgb24"), + (["rgb24"], "yuv420p", "rgb24"), + (["yuv420p"], "rgb24", "yuv420p"), + ([VideoFormat("yuv420p")], VideoFormat("rgb24"), "yuv420p"), + ( + ["yuv420p", "yuv444p", "gray", "rgb24", "rgba", "bgra", "yuyv422"], + "rgba", + "rgba", + ), + ], +) +def test_find_best_pix_fmt_of_list_best(pix_fmts, src_pix_fmt, expected_best) -> None: + best, loss = find_best_pix_fmt_of_list(pix_fmts, src_pix_fmt) + assert best is not None + assert best.name == expected_best + assert isinstance(loss, int) + + +@pytest.mark.parametrize( + "pix_fmts,src_pix_fmt", + [ + (["__unknown_pix_fmt"], "rgb24"), + (["rgb24"], "__unknown_pix_fmt"), + ], +) +def test_find_best_pix_fmt_of_list_unknown_pix_fmt(pix_fmts, src_pix_fmt) -> None: + with pytest.raises(ValueError, match="not a pixel format"): + find_best_pix_fmt_of_list(pix_fmts, src_pix_fmt) + + +@pytest.mark.parametrize( + "pix_fmts,src_pix_fmt", + [ + (["rgb24", "bgr24", "gray", "yuv420p", "yuv444p", "yuyv422"], "nv12"), + (["yuv420p", "yuv444p", "gray", "yuv420p"], "rgb24"), + (["rgb24", "rgba", "bgra", "rgb24", "gray"], "yuv420p"), + ], +) +def test_find_best_pix_fmt_of_list_picks_from_list(pix_fmts, src_pix_fmt) -> None: + best, loss = find_best_pix_fmt_of_list(pix_fmts, src_pix_fmt) + assert best is not None + assert best.name in set(pix_fmts) + assert isinstance(loss, int) + + +def test_find_best_pix_fmt_of_list_alpha_loss_flagged_when_used() -> None: + best, loss = find_best_pix_fmt_of_list(["rgb24"], "rgba", has_alpha=True) + assert best is not None + assert best.name == "rgb24" + assert loss != 0 From d88cec040cbe426e4f19ca41c6ac23cede0b8832 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 16 Dec 2025 20:59:10 -0500 Subject: [PATCH 597/846] Fix #2057 --- include/libavcodec/avcodec.pxd | 2 -- 1 file changed, 2 deletions(-) diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 9f158b88a..1edc26ef6 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -336,8 +336,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVBufferRef *hw_device_ctx AVPixelFormat (*get_format)(AVCodecContext *s, const AVPixelFormat *fmt) - # User Data - void *opaque cdef AVCodecContext* avcodec_alloc_context3(AVCodec *codec) cdef void avcodec_free_context(AVCodecContext **ctx) From 7a125bc4da02fef2d23115750a404717b55209a2 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 17 Dec 2025 00:08:22 -0500 Subject: [PATCH 598/846] opaque uintptr_t, fix #2036 --- av/opaque.pyx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/av/opaque.pyx b/av/opaque.pyx index 0c2a4507b..626b02985 100644 --- a/av/opaque.pyx +++ b/av/opaque.pyx @@ -1,5 +1,5 @@ cimport libav as lib -from libc.stdint cimport intptr_t, uint8_t +from libc.stdint cimport uint8_t, uintptr_t from libc.string cimport memcpy @@ -15,18 +15,18 @@ cdef class OpaqueContainer: cdef lib.AVBufferRef *add(self, object v): # Use object's memory address as key - cdef intptr_t key = id(v) + cdef uintptr_t key = id(v) self._objects[key] = v - cdef uint8_t *data = lib.av_malloc(sizeof(intptr_t)) + cdef uint8_t *data = lib.av_malloc(sizeof(uintptr_t)) if data == NULL: raise MemoryError("Failed to allocate memory for key") - memcpy(data, &key, sizeof(intptr_t)) + memcpy(data, &key, sizeof(uintptr_t)) # Create the buffer with our free callback cdef lib.AVBufferRef *buffer_ref = lib.av_buffer_create( - data, sizeof(intptr_t), key_free, NULL, 0 + data, sizeof(uintptr_t), key_free, NULL, 0 ) if buffer_ref == NULL: @@ -35,11 +35,11 @@ cdef class OpaqueContainer: return buffer_ref cdef object get(self, char *name): - cdef intptr_t key = (name)[0] + cdef uintptr_t key = (name)[0] return self._objects.get(key) cdef object pop(self, char *name): - cdef intptr_t key = (name)[0] + cdef uintptr_t key = (name)[0] return self._objects.pop(key, None) From 0f9fa2e217e9dda77f0462fe8cb0e8f4f54d5fb8 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 17 Dec 2025 01:26:49 -0500 Subject: [PATCH 599/846] Update changelog --- CHANGELOG.rst | 18 +++++++++++++++++- docs/api/filter.rst | 8 -------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index dce62d944..f02f3ea44 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,22 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. + +next +---- + +Features: + +- Add AMD AMF hardware decoding by :gh-user:`ladaapp2`. +- Add subtitle encoding support by :gh-user:`skeskinen` in (:pr:`2050`). +- Add read/write access to PacketSideData by :gh-user:`skeskinen` in (:pr:`2051`). +- Add yuv422p support for video frame to_ndarray and from_ndarray by :gh-user:`wader` in (:pr:`2054`). +- Add binding for ``avcodec_find_best_pix_fmt_of_list()`` by :gh-user:`ndeybach` (:pr:`2058`). + +Fixes: + +- Fix #2036, #2053, #2057 by :gh-user:`WyattBlue`. + v16.0.1 ------- @@ -34,7 +50,7 @@ Major: Features: - Add ``Filter.Context.process_command()`` method by :gh-user:`caffeinism` in (:pr:`2000`). -- Add packet side-data handling mechanism by :gh-user:`tikuma-lsuhsc ` in (:pr:`2003`). +- Add packet side-data handling mechanism by :gh-user:`tikuma-lsuhsc` in (:pr:`2003`). - Implemented set_chapters method by :gh-user:`DE-AI` in (:pr:`2004`). - Declare free-threaded support and support 3.13t by :gh-user:`ngoldbaum` in (:pr:`2005`). - Add writable and copyable attachment and data streams by :gh-user:`skeskinen` in (:pr:`2026`). diff --git a/docs/api/filter.rst b/docs/api/filter.rst index d126fda67..8674fdd9e 100644 --- a/docs/api/filter.rst +++ b/docs/api/filter.rst @@ -24,11 +24,3 @@ Filters .. autoclass:: FilterLink :members: - -.. automodule:: av.filter.pad - - .. autoclass:: FilterPad - :members: - - .. autoclass:: FilterContextPad - :members: From bf32ce5f1e0092a1732d3e2cef8622cbf74efb4e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 17 Dec 2025 01:31:14 -0500 Subject: [PATCH 600/846] Use ffmpeg 8.0.1 --- .github/workflows/smoke.yml | 11 +++++------ scripts/build | 1 - scripts/comptime.py | 32 -------------------------------- scripts/ffmpeg-8.0.json | 2 +- scripts/ffmpeg-latest.json | 2 +- 5 files changed, 7 insertions(+), 41 deletions(-) delete mode 100644 scripts/comptime.py diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index f75bf1b06..a8bed529b 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -35,10 +35,10 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-24.04, python: "3.14t", ffmpeg: "8.0", extras: true} - - {os: ubuntu-24.04, python: "3.12", ffmpeg: "8.0", extras: true} - - {os: ubuntu-24.04, python: "pypy3.10", ffmpeg: "8.0"} - - {os: macos-14, python: "3.10", ffmpeg: "8.0"} + - {os: ubuntu-24.04, python: "3.14t", ffmpeg: "8.0.1", extras: true} + - {os: ubuntu-24.04, python: "3.12", ffmpeg: "8.0.1", extras: true} + - {os: ubuntu-24.04, python: "pypy3.10", ffmpeg: "8.0.1"} + - {os: macos-14, python: "3.10", ffmpeg: "8.0.1"} env: PYAV_PYTHON: python${{ matrix.config.python }} @@ -76,7 +76,7 @@ jobs: fi ;; macos-14) - brew install automake libtool nasm libpng libvorbis libvpx opus x264 + brew install automake libtool libpng libvorbis libvpx opus x264 ;; esac @@ -149,7 +149,6 @@ jobs: . $CONDA/etc/profile.d/conda.sh conda activate pyav python scripts\\fetch-vendor.py --config-file scripts\\ffmpeg-${{ matrix.config.ffmpeg }}.json $CONDA_PREFIX\\Library - python scripts\\comptime.py ${{ matrix.config.ffmpeg }} python setup.py build_ext --inplace --ffmpeg-dir=$CONDA_PREFIX\\Library - name: Test diff --git a/scripts/build b/scripts/build index 4ee15d003..e3bd7a073 100755 --- a/scripts/build +++ b/scripts/build @@ -22,5 +22,4 @@ ffmpeg -version || exit 3 echo $PYAV_PIP install -U --pre cython setuptools 2> /dev/null -"$PYAV_PYTHON" scripts/comptime.py "$PYAV_PYTHON" setup.py config build_ext --inplace || exit 1 diff --git a/scripts/comptime.py b/scripts/comptime.py deleted file mode 100644 index a03a465f9..000000000 --- a/scripts/comptime.py +++ /dev/null @@ -1,32 +0,0 @@ -import os -import sys - -def replace_in_file(file_path): - try: - with open(file_path, "r", encoding="utf-8") as file: - content = file.read() - - modified_content = content.replace("# [FFMPEG6] ", "") - - with open(file_path, "w") as file: - file.write(modified_content) - except UnicodeDecodeError: - pass - - -def process_directory(directory): - for root, dirs, files in os.walk(directory): - for file in files: - file_path = os.path.join(root, file) - replace_in_file(file_path) - - -version = os.environ.get("PYAV_LIBRARY") -if version is None: - is_6 = sys.argv[1].startswith("6") -else: - is_6 = version.startswith("ffmpeg-6") - -if is_6: - process_directory("av") - process_directory("include") diff --git a/scripts/ffmpeg-8.0.json b/scripts/ffmpeg-8.0.json index fc5719b8a..f8af81c7a 100644 --- a/scripts/ffmpeg-8.0.json +++ b/scripts/ffmpeg-8.0.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0-2/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0.1-1/ffmpeg-{platform}.tar.gz" } diff --git a/scripts/ffmpeg-latest.json b/scripts/ffmpeg-latest.json index fc5719b8a..f8af81c7a 100644 --- a/scripts/ffmpeg-latest.json +++ b/scripts/ffmpeg-latest.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0-2/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0.1-1/ffmpeg-{platform}.tar.gz" } From 8fb7d1de07924649c22f940616fc9bb3b1356b85 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 17 Dec 2025 20:05:21 -0500 Subject: [PATCH 601/846] Return type should not be string literal --- av/subtitles/subtitle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/subtitles/subtitle.py b/av/subtitles/subtitle.py index 68f85fd1d..cdf0e16ec 100644 --- a/av/subtitles/subtitle.py +++ b/av/subtitles/subtitle.py @@ -44,7 +44,7 @@ def create( end: int, pts: int = 0, subtitle_format: int = 1, - ) -> "SubtitleSet": + ) -> SubtitleSet: """ Create a SubtitleSet for encoding. From c5cd12b6b561f1e88754d8179d7cdc9457511e15 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 3 Jan 2026 20:59:11 -0500 Subject: [PATCH 602/846] Fix #2049 --- av/container/output.py | 20 +++++++++++++----- tests/test_streams.py | 46 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/av/container/output.py b/av/container/output.py index 145437752..e57f269e8 100644 --- a/av/container/output.py +++ b/av/container/output.py @@ -278,15 +278,23 @@ def add_data_stream(self, codec_name=None, options: dict | None = None): :rtype: The new :class:`~av.data.stream.DataStream`. """ codec: cython.pointer[cython.const[lib.AVCodec]] = cython.NULL + codec_descriptor: cython.pointer[lib.AVCodecDescriptor] = cython.NULL if codec_name is not None: codec = lib.avcodec_find_encoder_by_name(codec_name.encode()) if codec == cython.NULL: - raise ValueError(f"Unknown data codec: {codec_name}") + codec = lib.avcodec_find_decoder_by_name(codec_name.encode()) + if codec == cython.NULL: + codec_descriptor = lib.avcodec_descriptor_get_by_name( + codec_name.encode() + ) + if codec_descriptor == cython.NULL: + raise ValueError(f"Unknown data codec: {codec_name}") - # Assert that this format supports the requested codec + # Verify format supports this codec + codec_id = codec.id if codec != cython.NULL else codec_descriptor.id if not lib.avformat_query_codec( - self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL + self.ptr.oformat, codec_id, lib.FF_COMPLIANCE_NORMAL ): raise ValueError( f"{self.format.name!r} format does not support {codec_name!r} codec" @@ -297,7 +305,7 @@ def add_data_stream(self, codec_name=None, options: dict | None = None): if stream == cython.NULL: raise MemoryError("Could not allocate stream") - # Set up codec context if we have a codec + # Set up codec context and parameters ctx: cython.pointer[lib.AVCodecContext] = cython.NULL if codec != cython.NULL: ctx = lib.avcodec_alloc_context3(codec) @@ -311,8 +319,10 @@ def add_data_stream(self, codec_name=None, options: dict | None = None): # Initialize stream codec parameters err_check(lib.avcodec_parameters_from_context(stream.codecpar, ctx)) else: - # For raw data streams, just set the codec type + # No codec available - set basic parameters for data stream stream.codecpar.codec_type = lib.AVMEDIA_TYPE_DATA + if codec_descriptor != cython.NULL: + stream.codecpar.codec_id = codec_descriptor.id # Construct the user-land stream py_codec_context: CodecContext | None = None diff --git a/tests/test_streams.py b/tests/test_streams.py index f82ce384b..9387d68cc 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -17,6 +17,8 @@ def cleanup(self): "data.ts", "data_source.ts", "data_copy.ts", + "data_with_codec.ts", + "data_invalid.ts", "out.mkv", "video_with_attachment.mkv", "remuxed_attachment.mkv", @@ -201,6 +203,50 @@ def test_data_stream_from_template(self) -> None: assert remuxed_payloads == copied_payloads + def test_data_stream_with_codec(self) -> None: + """Test adding a data stream with a specific codec name.""" + # Test that invalid codec names raise appropriate errors + with pytest.raises(ValueError, match="Unknown data codec"): + container = av.open("data_invalid.ts", "w") + try: + container.add_data_stream("not_a_real_codec_name_12345") + finally: + container.close() + + # Test that add_data_stream with codec parameter works correctly + # We use "bin_data" which is a data codec that's always available + output_path = "data_with_codec.ts" + with av.open(output_path, "w") as container: + # Try to create a data stream with a codec + # bin_data is a simple passthrough codec for binary data + data_stream = container.add_data_stream("bin_data") + klv_stream = container.add_data_stream("klv") + + assert data_stream.type == "data" + assert klv_stream.type == "data" + # Note: codec_context may be None for descriptor-only data codecs + + test_data = [b"test1", b"test2", b"test3"] + for i, data in enumerate(test_data): + packet = av.Packet(data) + packet.pts = i + packet.stream = data_stream + container.mux(packet) + + with av.open(output_path) as newcontainer: + data_stream = newcontainer.streams.data[0] + klv_stream = newcontainer.streams.data[1] + assert data_stream.type == "data" + assert klv_stream.type == "data" + assert "bin_data" in str(data_stream) + assert "klv" in str(klv_stream) + assert data_stream.name == "bin_data" + assert klv_stream.name == "klv" + try: + os.remove(output_path) + except Exception: + pass + def test_attachment_stream(self) -> None: input_path = av.datasets.curated( "pexels/time-lapse-video-of-night-sky-857195.mp4" From 17137ed8e21019b4a97f6ad787580a1dce436675 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 5 Jan 2026 23:09:41 -0500 Subject: [PATCH 603/846] Use latest ffmpeg --- CHANGELOG.rst | 5 +++-- scripts/ffmpeg-8.0.json | 2 +- scripts/ffmpeg-latest.json | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f02f3ea44..bd15c4e75 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,11 +17,12 @@ We are operating with `semantic versioning `_. are merged into the "default" branch. -next ----- +v16.1.0 +------- Features: +- Add support for Intel QSV codecs by :gh-user:`ladaapp`. - Add AMD AMF hardware decoding by :gh-user:`ladaapp2`. - Add subtitle encoding support by :gh-user:`skeskinen` in (:pr:`2050`). - Add read/write access to PacketSideData by :gh-user:`skeskinen` in (:pr:`2051`). diff --git a/scripts/ffmpeg-8.0.json b/scripts/ffmpeg-8.0.json index f8af81c7a..54968d400 100644 --- a/scripts/ffmpeg-8.0.json +++ b/scripts/ffmpeg-8.0.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0.1-1/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0.1-2/ffmpeg-{platform}.tar.gz" } diff --git a/scripts/ffmpeg-latest.json b/scripts/ffmpeg-latest.json index f8af81c7a..54968d400 100644 --- a/scripts/ffmpeg-latest.json +++ b/scripts/ffmpeg-latest.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0.1-1/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0.1-2/ffmpeg-{platform}.tar.gz" } From b9164b434f040f34314130e8afd0cd7eb4618802 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 6 Jan 2026 16:21:56 -0500 Subject: [PATCH 604/846] Use ffmpeg 8.0.1-3 --- scripts/fetch-vendor.py | 20 ++++++-------------- scripts/ffmpeg-7.1.json | 3 --- scripts/ffmpeg-8.0.json | 2 +- scripts/ffmpeg-latest.json | 2 +- 4 files changed, 8 insertions(+), 19 deletions(-) delete mode 100644 scripts/ffmpeg-7.1.json diff --git a/scripts/fetch-vendor.py b/scripts/fetch-vendor.py index 47aa2a421..2fb15c676 100644 --- a/scripts/fetch-vendor.py +++ b/scripts/fetch-vendor.py @@ -9,25 +9,18 @@ def get_platform(): system = platform.system() - machine = platform.machine() + is_arm64 = platform.machine() in ("arm64", "aarch64") if system == "Linux": if platform.libc_ver()[0] == "glibc": - return f"manylinux_{machine}" + return "manylinux-aarch64" if is_arm64 else "manylinux-x86_64" else: - return f"musllinux_{machine}" + return "musllinux-aarch64" if is_arm64 else "musllinux-x86_64" elif system == "Darwin": - # cibuildwheel sets ARCHFLAGS: - # https://github.com/pypa/cibuildwheel/blob/5255155bc57eb6224354356df648dc42e31a0028/cibuildwheel/macos.py#L207-L220 - if "ARCHFLAGS" in os.environ: - machine = os.environ["ARCHFLAGS"].split()[1] - return f"macosx_{machine}" + return "macos-arm64" if is_arm64 else "macos-x86_64" elif system == "Windows": - if struct.calcsize("P") * 8 == 64: - return "win_amd64" - else: - return "win32" + return "windows-aarch64" if is_arm64 else "windows-x86_64" else: - raise Exception(f"Unsupported system {system}") + return "unknown" parser = argparse.ArgumentParser(description="Fetch and extract tarballs") @@ -58,6 +51,5 @@ def get_platform(): ["curl", "--location", "--output", tarball_file, "--silent", tarball_url] ) -# extract tarball logging.info(f"Extracting {tarball_name}") subprocess.check_call(["tar", "-C", args.destination_dir, "-xf", tarball_file]) diff --git a/scripts/ffmpeg-7.1.json b/scripts/ffmpeg-7.1.json deleted file mode 100644 index 82ee313d6..000000000 --- a/scripts/ffmpeg-7.1.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.1.1-6/ffmpeg-{platform}.tar.gz" -} diff --git a/scripts/ffmpeg-8.0.json b/scripts/ffmpeg-8.0.json index 54968d400..5ec867122 100644 --- a/scripts/ffmpeg-8.0.json +++ b/scripts/ffmpeg-8.0.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0.1-2/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0.1-3/ffmpeg-{platform}.tar.gz" } diff --git a/scripts/ffmpeg-latest.json b/scripts/ffmpeg-latest.json index 54968d400..5ec867122 100644 --- a/scripts/ffmpeg-latest.json +++ b/scripts/ffmpeg-latest.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0.1-2/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0.1-3/ffmpeg-{platform}.tar.gz" } From a14f73069583a4fcc0d5093bde8058e6bd38dbbb Mon Sep 17 00:00:00 2001 From: benedikt-grl Date: Wed, 7 Jan 2026 20:42:44 +0100 Subject: [PATCH 605/846] Add support for VideoEncParams and VideoBlockParams (#2070) --- av/sidedata/encparams.pxd | 11 ++ av/sidedata/encparams.pyi | 27 ++++ av/sidedata/encparams.pyx | 165 +++++++++++++++++++++++++ av/sidedata/sidedata.pyx | 3 + include/libav.pxd | 1 + include/libavutil/video_enc_params.pxd | 25 ++++ tests/test_decode.py | 27 ++++ 7 files changed, 259 insertions(+) create mode 100644 av/sidedata/encparams.pxd create mode 100644 av/sidedata/encparams.pyi create mode 100644 av/sidedata/encparams.pyx create mode 100644 include/libavutil/video_enc_params.pxd diff --git a/av/sidedata/encparams.pxd b/av/sidedata/encparams.pxd new file mode 100644 index 000000000..031b59a42 --- /dev/null +++ b/av/sidedata/encparams.pxd @@ -0,0 +1,11 @@ +cimport libav as lib + +from av.sidedata.sidedata cimport SideData + + +cdef class VideoEncParams(SideData): + pass + + +cdef class VideoBlockParams: + cdef lib.AVVideoBlockParams *ptr diff --git a/av/sidedata/encparams.pyi b/av/sidedata/encparams.pyi new file mode 100644 index 000000000..de962ba51 --- /dev/null +++ b/av/sidedata/encparams.pyi @@ -0,0 +1,27 @@ +from enum import IntEnum +from typing import Any, cast + +import numpy as np + +class VideoEncParamsType(IntEnum): + NONE = cast(int, ...) + VP9 = cast(int, ...) + H264 = cast(int, ...) + MPEG2 = cast(int, ...) + +class VideoEncParams: + nb_blocks: int + blocks_offset: int + block_size: int + codec_type: VideoEncParamsType + qp: int + delta_qp: int + def block_params(self, idx: int) -> VideoBlockParams: ... + def qp_map(self) -> np.ndarray[Any, Any]: ... + +class VideoBlockParams: + src_x: int + src_y: int + w: int + h: int + delta_qp: int diff --git a/av/sidedata/encparams.pyx b/av/sidedata/encparams.pyx new file mode 100644 index 000000000..380dc53d4 --- /dev/null +++ b/av/sidedata/encparams.pyx @@ -0,0 +1,165 @@ +cimport libav as lib +from libc.stdint cimport int32_t, uint8_t + +from enum import IntEnum + +VideoEncParamsType = IntEnum( + "AVVideoEncParamsType", + { + "NONE": lib.AV_VIDEO_ENC_PARAMS_NONE, + "VP9": lib.AV_VIDEO_ENC_PARAMS_VP9, + "H264": lib.AV_VIDEO_ENC_PARAMS_H264, + "MPEG2": lib.AV_VIDEO_ENC_PARAMS_MPEG2, + }, +) + +cdef class VideoEncParams(SideData): + def __repr__(self): + return f"" + + @property + def nb_blocks(self): + """ + Number of blocks in the array + May be 0, in which case no per-block information is present. In this case + the values of blocks_offset / block_size are unspecified and should not + be accessed. + """ + return ( self.ptr.data).nb_blocks + + @property + def blocks_offset(self): + """ + Offset in bytes from the beginning of this structure at which the array of blocks starts. + """ + return ( self.ptr.data).blocks_offset + + @property + def block_size(self): + """ + Size of each block in bytes. May not match sizeof(AVVideoBlockParams). + """ + return ( self.ptr.data).block_size + + @property + def codec_type(self): + """ + Type of the parameters (the codec they are used with). + """ + cdef lib.AVVideoEncParamsType t = ( self.ptr.data).type + return VideoEncParamsType(t) + + @property + def qp(self): + """ + Base quantisation parameter for the frame. The final quantiser for a + given block in a given plane is obtained from this value, possibly + combined with `delta_qp` and the per-block delta in a manner + documented for each type. + """ + return ( self.ptr.data).qp + + @property + def delta_qp(self): + """ + Quantisation parameter offset from the base (per-frame) qp for a given + plane (first index) and AC/DC coefficients (second index). + """ + cdef lib.AVVideoEncParams *p = self.ptr.data + return [[p.delta_qp[i][j] for j in range(2)] for i in range(4)] + + def block_params(self, idx): + """ + Get the encoding parameters for a given block + """ + # Validate given index + if idx < 0 or idx >= self.nb_blocks: + raise ValueError("Expected idx in range [0, nb_blocks)") + + return VideoBlockParams(self, idx) + + def qp_map(self): + """ + Convenience method that creates a 2-D map with the quantization parameters per macroblock. + Only for MPEG2 and H264 encoded videos. + """ + import numpy as np + + cdef int mb_h = (self.frame.ptr.height + 15) // 16 + cdef int mb_w = (self.frame.ptr.width + 15) // 16 + cdef int nb_mb = mb_h * mb_w + cdef int block_idx + cdef int y + cdef int x + cdef VideoBlockParams block + + # Validate number of blocks + if self.nb_blocks != nb_mb: + raise RuntimeError("Expected frame size to match number of blocks in side data") + + # Validate type + cdef lib.AVVideoEncParamsType type = ( self.ptr.data).type + if type != lib.AVVideoEncParamsType.AV_VIDEO_ENC_PARAMS_MPEG2 and type != lib.AVVideoEncParamsType.AV_VIDEO_ENC_PARAMS_H264: + raise ValueError("Expected MPEG2 or H264") + + # Create a 2-D map with the number of macroblocks + cdef int32_t[:, ::1] map = np.empty((mb_h, mb_w), dtype=np.int32) + + # Fill map with quantization parameter per macroblock + for block_idx in range(nb_mb): + block = VideoBlockParams(self, block_idx) + y = block.src_y // 16 + x = block.src_x // 16 + map[y, x] = self.qp + block.delta_qp + + return np.asarray(map) + + +cdef class VideoBlockParams: + def __init__(self, VideoEncParams video_enc_params, int idx) -> None: + cdef uint8_t* base = video_enc_params.ptr.data + cdef Py_ssize_t offset = video_enc_params.blocks_offset + idx * video_enc_params.block_size + self.ptr = (base + offset) + + def __repr__(self): + return f"" + + @property + def src_x(self): + """ + Horizontal distance in luma pixels from the top-left corner of the visible frame + to the top-left corner of the block. + Can be negative if top/right padding is present on the coded frame. + """ + return self.ptr.src_x + + @property + def src_y(self): + """ + Vertical distance in luma pixels from the top-left corner of the visible frame + to the top-left corner of the block. + Can be negative if top/right padding is present on the coded frame. + """ + return self.ptr.src_y + + @property + def w(self): + """ + Width of the block in luma pixels + """ + return self.ptr.w + + @property + def h(self): + """ + Height of the block in luma pixels + """ + return self.ptr.h + + @property + def delta_qp(self): + """ + Difference between this block's final quantization parameter and the + corresponding per-frame value. + """ + return self.ptr.delta_qp diff --git a/av/sidedata/sidedata.pyx b/av/sidedata/sidedata.pyx index 65b1387f0..96c3d72f5 100644 --- a/av/sidedata/sidedata.pyx +++ b/av/sidedata/sidedata.pyx @@ -3,6 +3,7 @@ from libc.stdint cimport int32_t from collections.abc import Mapping from enum import Enum +from av.sidedata.encparams import VideoEncParams from av.sidedata.motionvectors import MotionVectors @@ -49,6 +50,8 @@ class Type(Enum): cdef SideData wrap_side_data(Frame frame, int index): if frame.ptr.side_data[index].type == lib.AV_FRAME_DATA_MOTION_VECTORS: return MotionVectors(_cinit_bypass_sentinel, frame, index) + elif frame.ptr.side_data[index].type == lib.AV_FRAME_DATA_VIDEO_ENC_PARAMS: + return VideoEncParams(_cinit_bypass_sentinel, frame, index) else: return SideData(_cinit_bypass_sentinel, frame, index) diff --git a/include/libav.pxd b/include/libav.pxd index 568913208..5e074a1b8 100644 --- a/include/libav.pxd +++ b/include/libav.pxd @@ -7,6 +7,7 @@ include "libavutil/frame.pxd" include "libavutil/hwcontext.pxd" include "libavutil/samplefmt.pxd" include "libavutil/motion_vector.pxd" +include "libavutil/video_enc_params.pxd" include "libavcodec/avcodec.pxd" include "libavcodec/bsf.pxd" diff --git a/include/libavutil/video_enc_params.pxd b/include/libavutil/video_enc_params.pxd new file mode 100644 index 000000000..aec452666 --- /dev/null +++ b/include/libavutil/video_enc_params.pxd @@ -0,0 +1,25 @@ +from libc.stdint cimport uint32_t, int32_t +from libc.stddef cimport size_t + + +cdef extern from "libavutil/video_enc_params.h" nogil: + cdef enum AVVideoEncParamsType: + AV_VIDEO_ENC_PARAMS_NONE + AV_VIDEO_ENC_PARAMS_VP9 + AV_VIDEO_ENC_PARAMS_H264 + AV_VIDEO_ENC_PARAMS_MPEG2 + + cdef struct AVVideoEncParams: + uint32_t nb_blocks + size_t blocks_offset + size_t block_size + AVVideoEncParamsType type + int32_t qp + int32_t delta_qp[4][2] + + cdef struct AVVideoBlockParams: + int32_t src_x + int32_t src_y + int32_t w + int32_t h + int32_t delta_qp \ No newline at end of file diff --git a/tests/test_decode.py b/tests/test_decode.py index 7ad722bfd..685b743f5 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -2,6 +2,7 @@ import os import pathlib from fractions import Fraction +from typing import cast import numpy as np import pytest @@ -138,6 +139,32 @@ def test_decoded_motion_vectors_no_flag(self) -> None: assert vectors is None return + def test_decoded_video_enc_params(self) -> None: + container = av.open(fate_suite("h264/interlaced_crop.mp4")) + stream = container.streams.video[0] + stream.codec_context.options = {"export_side_data": "venc_params"} + + for frame in container.decode(stream): + video_enc_params = cast( + av.sidedata.encparams.VideoEncParams, + frame.side_data.get("VIDEO_ENC_PARAMS"), + ) + assert video_enc_params is not None + assert video_enc_params.nb_blocks == 40 * 24 + + first_block = video_enc_params.block_params(0) + assert video_enc_params.qp + first_block.delta_qp == 29 + return + + def test_decoded_video_enc_params_no_flag(self) -> None: + container = av.open(fate_suite("h264/interlaced_crop.mp4")) + stream = container.streams.video[0] + # When no additional flag is given, there should be no side data with the video encoding params + + for frame in container.decode(stream): + video_enc_params = frame.side_data.get("VIDEO_ENC_PARAMS") + assert video_enc_params is None + def test_decode_video_corrupt(self) -> None: # write an empty file path = self.sandboxed("empty.h264") From 7e0138cd0b84a1ffcae0f39f0f94c7f3ab9673ed Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 8 Jan 2026 16:18:44 -0500 Subject: [PATCH 606/846] Update authors --- AUTHORS.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 5de3b44c8..ea0148bf8 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -21,15 +21,15 @@ All contributors (by number of commits): * Justin Wong <46082645+uvjustin@users.noreply.github.com> * Mark Harfouche +- Santtu Keskinen - Alba Mendez +- Curtis Doty ; `@dotysan `_ - Xinran Xu ; `@xxr3376 `_ - z-khan - Marc Mueller <30130371+cdce8p@users.noreply.github.com> - Dan Allan ; `@danielballan `_ - Moonsik Park -- Santtu Keskinen - velsinki <40809145+velsinki@users.noreply.github.com> -- Curtis Doty - Christoph Rackwitz - David Plowman - Alireza Davoudi ; `@adavoudi `_ @@ -44,6 +44,7 @@ All contributors (by number of commits): * zzjjbb <31069326+zzjjbb@users.noreply.github.com> * Joe Schiff <41972063+JoeSchiff@users.noreply.github.com> +* Nils DEYBACH <68770774+ndeybach@users.noreply.github.com> * Dexer <73297572+DexerBR@users.noreply.github.com> * DE-AI <81620697+DE-AI@users.noreply.github.com> * rutsh @@ -57,6 +58,7 @@ All contributors (by number of commits): * Martin Larralde * Simon-Martin Schröder * Matteo Destro +* Mattias Wadman * mephi42 * Miles Kaufmann * Nathan Goldbaum @@ -72,6 +74,7 @@ All contributors (by number of commits): * Ian Lee * Ryan Huang * Arthur Barros +* benedikt-grl * Carlos Ruiz * Carlos Ruiz * Maxime Desroches @@ -86,7 +89,6 @@ All contributors (by number of commits): * Philipp Klaus * Marcell Pardavi * Matteo Destro -* Mattias Wadman * Max Ehrlich * Manuel Goacolou * Julian Schweizer From bae50fa636afc1e5fd4ede2961c824aaa9a5a81d Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 9 Jan 2026 14:08:20 -0500 Subject: [PATCH 607/846] Release 16.1.0 --- av/about.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/about.py b/av/about.py index 610c11155..5d194c730 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "16.0.1" +__version__ = "16.1.0" From eb52a8b6eeabfbde37fbaf7e9e4fd929be471080 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E5=B0=8F=E7=99=BD?= <296015668@qq.com> Date: Sun, 11 Jan 2026 09:07:33 +0800 Subject: [PATCH 608/846] Update get_platform() to support more archs --- scripts/fetch-vendor.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/scripts/fetch-vendor.py b/scripts/fetch-vendor.py index 2fb15c676..41801df2b 100644 --- a/scripts/fetch-vendor.py +++ b/scripts/fetch-vendor.py @@ -9,12 +9,11 @@ def get_platform(): system = platform.system() - is_arm64 = platform.machine() in ("arm64", "aarch64") + machine = platform.machine().lower() + is_arm64 = machine in {"arm64", "aarch64"} if system == "Linux": - if platform.libc_ver()[0] == "glibc": - return "manylinux-aarch64" if is_arm64 else "manylinux-x86_64" - else: - return "musllinux-aarch64" if is_arm64 else "musllinux-x86_64" + prefix = "manylinux-" if platform.libc_ver()[0] == "glibc" else "musllinux-" + return prefix + machine elif system == "Darwin": return "macos-arm64" if is_arm64 else "macos-x86_64" elif system == "Windows": From c720be9fe790636216cce09945e39ba269434193 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 10 Jan 2026 19:46:28 -0500 Subject: [PATCH 609/846] Build limited API 3.11+, closes #2033 --- .github/workflows/smoke.yml | 7 +++---- .github/workflows/tests.yml | 7 +++---- pyproject.toml | 3 +-- setup.py | 11 +++++++++++ 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index a8bed529b..d6651f4ee 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -35,10 +35,9 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-24.04, python: "3.14t", ffmpeg: "8.0.1", extras: true} - {os: ubuntu-24.04, python: "3.12", ffmpeg: "8.0.1", extras: true} - - {os: ubuntu-24.04, python: "pypy3.10", ffmpeg: "8.0.1"} - - {os: macos-14, python: "3.10", ffmpeg: "8.0.1"} + # - {os: ubuntu-24.04, python: "pypy3.11", ffmpeg: "8.0.1"} + - {os: macos-14, python: "3.11", ffmpeg: "8.0.1"} env: PYAV_PYTHON: python${{ matrix.config.python }} @@ -122,7 +121,7 @@ jobs: fail-fast: false matrix: config: - - {os: windows-latest, python: "3.10", ffmpeg: "latest"} + - {os: windows-latest, python: "3.11", ffmpeg: "latest"} steps: - name: Checkout diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7611e4f18..efa8de710 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -40,9 +40,9 @@ jobs: arch: AMD64 steps: - uses: actions/checkout@v6 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: - python-version: "3.13" + python-version: "3.14" - name: Set Minimum MacOS Target if: runner.os == 'macOS' run: | @@ -61,10 +61,9 @@ jobs: CIBW_ENVIRONMENT_MACOS: PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig LDFLAGS=-headerpad_max_install_names CIBW_ENVIRONMENT_WINDOWS: INCLUDE=C:\\cibw\\vendor\\include LIB=C:\\cibw\\vendor\\lib PYAV_SKIP_TESTS=unicode_filename CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: delvewheel repair --add-path C:\cibw\vendor\bin -w {dest_dir} {wheel} - CIBW_SKIP: "pp*" + CIBW_SKIP: "pp* cp312* cp313* cp314*" CIBW_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m pytest {package}/tests && mv {project}/av.disabled {project}/av CIBW_TEST_REQUIRES: pytest numpy - CIBW_ENABLE: cpython-freethreading run: | pip install cibuildwheel delvewheel cibuildwheel --output-dir dist diff --git a/pyproject.toml b/pyproject.toml index 24bdc90a9..7ab4ae2ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ authors = [ {name = "WyattBlue", email = "wyattblue@auto-editor.com"}, {name = "Jeremy Lainé", email = "jeremy.laine@m4x.org"}, ] -requires-python = ">=3.10" +requires-python = ">=3.11" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", @@ -20,7 +20,6 @@ classifiers = [ "Operating System :: Unix", "Operating System :: Microsoft :: Windows", "Programming Language :: Cython", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", diff --git a/setup.py b/setup.py index ce6bcc388..1db60b649 100644 --- a/setup.py +++ b/setup.py @@ -21,6 +21,12 @@ "swresample", ] +if sys.implementation.name == "cpython": + py_limited_api = True + options = {"bdist_wheel": {"py_limited_api": "cp311"}} +else: + py_limited_api = False + options = {} # Monkey-patch Cython to not overwrite embedded signatures. old_embed_signature = EmbedSignature._embed_signature @@ -142,6 +148,8 @@ def parse_cflags(raw_flags): include_dirs=[f"{IMPORT_NAME}/filter"] + extension_extra["include_dirs"], libraries=extension_extra["libraries"], library_dirs=extension_extra["library_dirs"], + define_macros=[("Py_LIMITED_API", 0x030B0000)], + py_limited_api=py_limited_api, ) compiler_directives = { @@ -186,6 +194,8 @@ def parse_cflags(raw_flags): libraries=extension_extra["libraries"], library_dirs=extension_extra["library_dirs"], sources=[pyx_path], + define_macros=[("Py_LIMITED_API", 0x030B0000)], + py_limited_api=py_limited_api, ), compiler_directives=compiler_directives, build_dir="src", @@ -202,4 +212,5 @@ def parse_cflags(raw_flags): packages=find_packages(include=[f"{IMPORT_NAME}*"]), package_data=package_data, ext_modules=ext_modules, + options=options, ) From 3a42e8f9cec9a5813a06becd7f9ee14f5fd078e5 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 11 Jan 2026 02:24:19 -0500 Subject: [PATCH 610/846] Publish source code first --- .github/workflows/tests.yml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index efa8de710..ca29cb4b3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,19 +8,20 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v6 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: - python-version: "3.13" + python-version: "3.14" - name: Build source package run: | - pip install -U --pre cython setuptools + pip install -U cython setuptools python scripts/fetch-vendor.py --config-file scripts/ffmpeg-latest.json /tmp/vendor PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig python setup.py sdist - - name: Upload source package - uses: actions/upload-artifact@v4 + - name: Publish to PyPI + if: github.event_name == 'release' && github.event.action == 'published' + uses: pypa/gh-action-pypi-publish@release/v1 with: - name: dist-source - path: dist/ + user: __token__ + password: ${{ secrets.PYPI_TOKEN }} package-wheel: runs-on: ${{ matrix.os }} @@ -76,7 +77,7 @@ jobs: publish: runs-on: ubuntu-latest - needs: [package-source, package-wheel] + needs: [package-wheel] steps: - uses: actions/checkout@v6 - uses: actions/download-artifact@v4 From ed09666cc63329550d4957653c7067b3fca3a0d3 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 11 Jan 2026 05:29:16 -0500 Subject: [PATCH 611/846] Enable pypy for smoke again --- .github/workflows/smoke.yml | 2 +- setup.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index d6651f4ee..0b75e3a98 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -36,7 +36,7 @@ jobs: matrix: config: - {os: ubuntu-24.04, python: "3.12", ffmpeg: "8.0.1", extras: true} - # - {os: ubuntu-24.04, python: "pypy3.11", ffmpeg: "8.0.1"} + - {os: ubuntu-24.04, python: "pypy3.11", ffmpeg: "8.0.1"} - {os: macos-14, python: "3.11", ffmpeg: "8.0.1"} env: diff --git a/setup.py b/setup.py index 1db60b649..9be9acb27 100644 --- a/setup.py +++ b/setup.py @@ -24,9 +24,11 @@ if sys.implementation.name == "cpython": py_limited_api = True options = {"bdist_wheel": {"py_limited_api": "cp311"}} + define_macros = [("Py_LIMITED_API", 0x030B0000)] else: py_limited_api = False options = {} + define_macros = [] # Monkey-patch Cython to not overwrite embedded signatures. old_embed_signature = EmbedSignature._embed_signature @@ -148,7 +150,7 @@ def parse_cflags(raw_flags): include_dirs=[f"{IMPORT_NAME}/filter"] + extension_extra["include_dirs"], libraries=extension_extra["libraries"], library_dirs=extension_extra["library_dirs"], - define_macros=[("Py_LIMITED_API", 0x030B0000)], + define_macros=define_macros, py_limited_api=py_limited_api, ) @@ -194,7 +196,7 @@ def parse_cflags(raw_flags): libraries=extension_extra["libraries"], library_dirs=extension_extra["library_dirs"], sources=[pyx_path], - define_macros=[("Py_LIMITED_API", 0x030B0000)], + define_macros=define_macros, py_limited_api=py_limited_api, ), compiler_directives=compiler_directives, From cac1cab0aa8799379c3d918471de468d101fbe30 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 11 Jan 2026 15:16:28 -0500 Subject: [PATCH 612/846] Use ubuntu-slim for linting --- .github/workflows/smoke.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 0b75e3a98..fa78a285c 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -15,16 +15,14 @@ on: - '**.txt' jobs: style: - runs-on: ubuntu-24.04 + runs-on: ubuntu-slim steps: - name: Checkout uses: actions/checkout@v6 - - name: Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: - python-version: "3.12" - + python-version: "3.14" - name: Linters run: make lint @@ -48,7 +46,7 @@ jobs: name: Checkout - name: Python ${{ matrix.config.python }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.config.python }} allow-prereleases: true @@ -71,7 +69,7 @@ jobs: pkg-config \ zlib1g-dev if [[ "${{ matrix.config.extras }}" ]]; then - sudo apt-get install doxygen wget + sudo apt-get install doxygen fi ;; macos-14) From ec7a9d9054a93b687a247f5696105ce8b54249d9 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 11 Jan 2026 15:38:46 -0500 Subject: [PATCH 613/846] Make plane, dictionary pure python --- av/dictionary.pxd | 2 -- av/dictionary.py | 62 ++++++++++++++++++++++++++++++++++++++ av/dictionary.pyx | 57 ----------------------------------- av/plane.pxd | 2 -- av/{plane.pyx => plane.py} | 10 ++++-- 5 files changed, 69 insertions(+), 64 deletions(-) create mode 100644 av/dictionary.py delete mode 100644 av/dictionary.pyx rename av/{plane.pyx => plane.py} (72%) diff --git a/av/dictionary.pxd b/av/dictionary.pxd index 1c59df448..47c100adc 100644 --- a/av/dictionary.pxd +++ b/av/dictionary.pxd @@ -2,9 +2,7 @@ cimport libav as lib cdef class _Dictionary: - cdef lib.AVDictionary *ptr - cpdef _Dictionary copy(self) diff --git a/av/dictionary.py b/av/dictionary.py new file mode 100644 index 000000000..2080266ed --- /dev/null +++ b/av/dictionary.py @@ -0,0 +1,62 @@ +from collections.abc import MutableMapping + +import cython +from cython.cimports.av.error import err_check + + +@cython.cclass +class _Dictionary: + def __cinit__(self, *args, **kwargs): + for arg in args: + self.update(arg) + if kwargs: + self.update(kwargs) + + def __dealloc__(self): + if self.ptr != cython.NULL: + lib.av_dict_free(cython.address(self.ptr)) + + def __getitem__(self, key: cython.str): + element = cython.declare( + cython.pointer[lib.AVDictionaryEntry], + lib.av_dict_get(self.ptr, key, cython.NULL, 0), + ) + if element == cython.NULL: + raise KeyError(key) + return element.value + + def __setitem__(self, key: cython.str, value: cython.str): + err_check(lib.av_dict_set(cython.address(self.ptr), key, value, 0)) + + def __delitem__(self, key: cython.str): + err_check(lib.av_dict_set(cython.address(self.ptr), key, cython.NULL, 0)) + + def __len__(self): + return err_check(lib.av_dict_count(self.ptr)) + + def __iter__(self): + element = cython.declare(cython.pointer[lib.AVDictionaryEntry], cython.NULL) + while True: + element = lib.av_dict_get(self.ptr, "", element, lib.AV_DICT_IGNORE_SUFFIX) + if element == cython.NULL: + break + yield element.key + + def __repr__(self): + return f"bv.Dictionary({dict(self)!r})" + + def copy(self): + other = cython.declare(_Dictionary, Dictionary()) + lib.av_dict_copy(cython.address(other.ptr), self.ptr, 0) + return other + + +class Dictionary(_Dictionary, MutableMapping): + pass + + +@cython.cfunc +def wrap_dictionary(input_: cython.pointer[lib.AVDictionary]) -> _Dictionary: + output = cython.declare(_Dictionary, Dictionary()) + output.ptr = input_ + return output diff --git a/av/dictionary.pyx b/av/dictionary.pyx deleted file mode 100644 index 15de38381..000000000 --- a/av/dictionary.pyx +++ /dev/null @@ -1,57 +0,0 @@ -from collections.abc import MutableMapping - -from av.error cimport err_check - - -cdef class _Dictionary: - def __cinit__(self, *args, **kwargs): - for arg in args: - self.update(arg) - if kwargs: - self.update(kwargs) - - def __dealloc__(self): - if self.ptr != NULL: - lib.av_dict_free(&self.ptr) - - def __getitem__(self, str key): - cdef lib.AVDictionaryEntry *element = lib.av_dict_get(self.ptr, key, NULL, 0) - if element != NULL: - return element.value - else: - raise KeyError(key) - - def __setitem__(self, str key, str value): - err_check(lib.av_dict_set(&self.ptr, key, value, 0)) - - def __delitem__(self, str key): - err_check(lib.av_dict_set(&self.ptr, key, NULL, 0)) - - def __len__(self): - return err_check(lib.av_dict_count(self.ptr)) - - def __iter__(self): - cdef lib.AVDictionaryEntry *element = NULL - while True: - element = lib.av_dict_get(self.ptr, "", element, lib.AV_DICT_IGNORE_SUFFIX) - if element == NULL: - break - yield element.key - - def __repr__(self): - return f"av.Dictionary({dict(self)!r})" - - cpdef _Dictionary copy(self): - cdef _Dictionary other = Dictionary() - lib.av_dict_copy(&other.ptr, self.ptr, 0) - return other - - -class Dictionary(_Dictionary, MutableMapping): - pass - - -cdef _Dictionary wrap_dictionary(lib.AVDictionary *input_): - cdef _Dictionary output = Dictionary() - output.ptr = input_ - return output diff --git a/av/plane.pxd b/av/plane.pxd index df3847d7b..2066bfea8 100644 --- a/av/plane.pxd +++ b/av/plane.pxd @@ -3,9 +3,7 @@ from av.frame cimport Frame cdef class Plane(Buffer): - cdef Frame frame cdef int index - cdef size_t _buffer_size(self) cdef void* _buffer_ptr(self) diff --git a/av/plane.pyx b/av/plane.py similarity index 72% rename from av/plane.pyx rename to av/plane.py index c733b20a7..f84a99c3d 100644 --- a/av/plane.pyx +++ b/av/plane.py @@ -1,12 +1,15 @@ +import cython -cdef class Plane(Buffer): + +@cython.cclass +class Plane(Buffer): """ Base class for audio and video planes. See also :class:`~av.audio.plane.AudioPlane` and :class:`~av.video.plane.VideoPlane`. """ - def __cinit__(self, Frame frame, int index): + def __cinit__(self, frame: Frame, index: cython.int): self.frame = frame self.index = index @@ -16,5 +19,6 @@ def __repr__(self): f"buffer_ptr=0x{self.buffer_ptr:x}; at 0x{id(self):x}>" ) - cdef void* _buffer_ptr(self): + @cython.cfunc + def _buffer_ptr(self) -> cython.p_void: return self.frame.ptr.extended_data[self.index] From 01d950904d2cb2de620ec51849b3d32a4b1a0287 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 11 Jan 2026 16:03:18 -0500 Subject: [PATCH 614/846] Make error.pyx pure python --- av/error.pxd | 1 - av/{error.pyx => error.py} | 246 +++++++++++++++++-------------------- av/error.pyi | 2 +- tests/test_bitstream.py | 2 +- tests/test_errors.py | 8 +- tests/test_python_io.py | 3 +- 6 files changed, 122 insertions(+), 140 deletions(-) rename av/{error.pyx => error.py} (65%) diff --git a/av/error.pxd b/av/error.pxd index d9a542a36..17c37f609 100644 --- a/av/error.pxd +++ b/av/error.pxd @@ -1,3 +1,2 @@ - cdef int stash_exception(exc_info=*) cpdef int err_check(int res, filename=*) except -1 diff --git a/av/error.pyx b/av/error.py similarity index 65% rename from av/error.pyx rename to av/error.py index 123ed735f..8827aafc2 100644 --- a/av/error.pyx +++ b/av/error.py @@ -1,37 +1,47 @@ -cimport libav as lib -from libc.stdio cimport fprintf, stderr -from libc.stdlib cimport free, malloc - -from av.logging cimport get_last_error - import errno import os import sys import traceback from threading import local +import cython +from cython.cimports import libav as lib +from cython.cimports.av.logging import get_last_error +from cython.cimports.libc.stdio import fprintf, stderr +from cython.cimports.libc.stdlib import free, malloc + # Will get extended with all of the exceptions. __all__ = [ - "ErrorType", "FFmpegError", "LookupError", "HTTPError", "HTTPClientError", + "ErrorType", + "FFmpegError", + "LookupError", + "HTTPError", + "HTTPClientError", "UndefinedError", ] +sentinel = cython.declare(object, object()) -cpdef code_to_tag(int code): +@cython.ccall +def code_to_tag(code: cython.int) -> bytes: """Convert an integer error code into 4-byte tag. >>> code_to_tag(1953719668) b'test' """ - return bytes(( - code & 0xff, - (code >> 8) & 0xff, - (code >> 16) & 0xff, - (code >> 24) & 0xff, - )) - -cpdef tag_to_code(bytes tag): + return bytes( + ( + code & 0xFF, + (code >> 8) & 0xFF, + (code >> 16) & 0xFF, + (code >> 24) & 0xFF, + ) + ) + + +@cython.ccall +def tag_to_code(tag: bytes) -> cython.int: """Convert a 4-byte error tag into an integer code. >>> tag_to_code(b'test') @@ -40,12 +50,7 @@ """ if len(tag) != 4: raise ValueError("Error tags are 4 bytes.") - return ( - (tag[0]) + - (tag[1] << 8) + - (tag[2] << 16) + - (tag[3] << 24) - ) + return (tag[0]) + (tag[1] << 8) + (tag[2] << 16) + (tag[3] << 24) class FFmpegError(Exception): @@ -78,8 +83,8 @@ def __init__(self, code, message, filename=None, log=None): args.append(filename) if log: args.append(log) - super(FFmpegError, self).__init__(*args) - self.args = tuple(args) # FileNotFoundError/etc. only pulls 2 args. + super().__init__(*args) + self.args = tuple(args) @property def filename(self): @@ -104,14 +109,16 @@ def __str__(self): if self.filename: msg = f"{msg}: {self.filename!r}" if self.log: - msg = f"{msg}; last error log: [{self.log[1].strip()}] {self.log[2].strip()}" + msg = ( + f"{msg}; last error log: [{self.log[1].strip()}] {self.log[2].strip()}" + ) return msg # Our custom error, used in callbacks. -cdef int c_PYAV_STASHED_ERROR = tag_to_code(b"PyAV") -cdef str PYAV_STASHED_ERROR_message = "Error in PyAV callback" +c_PYAV_STASHED_ERROR: cython.int = tag_to_code(b"PyAV") +PYAV_STASHED_ERROR_message: str = "Error in PyAV callback" # Bases for the FFmpeg-based exceptions. @@ -128,6 +135,8 @@ class HTTPClientError(FFmpegError): # Tuples of (enum_name, enum_value, exc_name, exc_base). +# tuple[str, int, str | None, Exception | none] +# fmt: off _ffmpeg_specs = ( ("BSF_NOT_FOUND", -lib.AVERROR_BSF_NOT_FOUND, "BSFNotFoundError", LookupError), ("BUG", -lib.AVERROR_BUG, None, RuntimeError), @@ -156,8 +165,7 @@ class HTTPClientError(FFmpegError): ("HTTP_SERVER_ERROR", -lib.AVERROR_HTTP_SERVER_ERROR, "HTTPServerError", HTTPError), ("PYAV_CALLBACK", c_PYAV_STASHED_ERROR, "PyAVCallbackError", RuntimeError), ) - -cdef sentinel = object() +# fmt: on class EnumType(type): @@ -189,59 +197,24 @@ def __len__(self): def __iter__(self): return iter(self._all) - def __getitem__(self, key): - if isinstance(key, str): - return self._by_name[key] - if isinstance(key, int): - try: - return self._by_value[key] - except KeyError: - pass - - raise KeyError(key) - - if isinstance(key, self): - return key - - raise TypeError(f"{self.__name__} indices must be str, int, or itself") - - def _get(self, long value, bint create=False): - try: - return self._by_value[value] - except KeyError: - pass - - if not create: - return - - return self._create(f"{self.__name__.upper()}_{value}", value, by_value_only=True) - - def get(self, key, default=None, create=False): - try: - return self[key] - except KeyError: - if create: - return self._get(key, create=True) - return default - - -cdef class EnumItem: +@cython.cclass +class EnumItem: """An enumeration of FFmpeg's error types. -.. attribute:: tag + .. attribute:: tag - The FFmpeg byte tag for the error. + The FFmpeg byte tag for the error. -.. attribute:: strerror + .. attribute:: strerror - The error message that would be returned. + The error message that would be returned. + """ -""" - cdef readonly str name - cdef readonly int value + name = cython.declare(str, visibility="readonly") + value = cython.declare(cython.int, visibility="readonly") - def __cinit__(self, sentinel_, str name, int value, doc=None): + def __cinit__(self, sentinel_, name: str, value: cython.int, doc=None): if sentinel_ is not sentinel: raise RuntimeError(f"Cannot instantiate {self.__class__.__name__}.") @@ -263,42 +236,25 @@ def tag(self): return code_to_tag(self.value) -ErrorType = EnumType("ErrorType", (EnumItem, ), {"__module__": __name__}, [x[:2] for x in _ffmpeg_specs]) +ErrorType = EnumType( + "ErrorType", (EnumItem,), {"__module__": __name__}, [x[:2] for x in _ffmpeg_specs] +) for enum in ErrorType: - # Mimic the errno module. + # Mimick the errno module. globals()[enum.name] = enum if enum.value == c_PYAV_STASHED_ERROR: enum.strerror = PYAV_STASHED_ERROR_message else: enum.strerror = lib.av_err2str(-enum.value) - -# Mimic the builtin exception types. -# See https://www.python.org/dev/peps/pep-3151/#new-exception-classes -# Use the named ones we have, otherwise default to OSError for anything in errno. - -# See this command for the count of POSIX codes used: -# -# egrep -IR 'AVERROR\(E[A-Z]+\)' vendor/ffmpeg-4.2 |\ -# sed -E 's/.*AVERROR\((E[A-Z]+)\).*/\1/' | \ -# sort | uniq -c -# -# The biggest ones that don't map to PEP 3151 builtins: -# -# 2106 EINVAL -> ValueError -# 649 EIO -> IOError (if it is distinct from OSError) -# 4080 ENOMEM -> MemoryError -# 340 ENOSYS -> NotImplementedError -# 35 ERANGE -> OverflowError - -classes = {} +classes: dict = {} def _extend_builtin(name, codes): base = getattr(__builtins__, name, OSError) - cls = type(name, (FFmpegError, base), dict(__module__=__name__)) + cls = type(name, (FFmpegError, base), {"__module__": __name__}) # Register in builder. for code in codes: @@ -311,30 +267,56 @@ def _extend_builtin(name, codes): return cls -# PEP 3151 builtins. _extend_builtin("PermissionError", (errno.EACCES, errno.EPERM)) -_extend_builtin("BlockingIOError", (errno.EAGAIN, errno.EALREADY, errno.EINPROGRESS, errno.EWOULDBLOCK)) -_extend_builtin("ChildProcessError", (errno.ECHILD, )) -_extend_builtin("ConnectionAbortedError", (errno.ECONNABORTED, )) -_extend_builtin("ConnectionRefusedError", (errno.ECONNREFUSED, )) -_extend_builtin("ConnectionResetError", (errno.ECONNRESET, )) -_extend_builtin("FileExistsError", (errno.EEXIST, )) -_extend_builtin("InterruptedError", (errno.EINTR, )) -_extend_builtin("IsADirectoryError", (errno.EISDIR, )) -_extend_builtin("FileNotFoundError", (errno.ENOENT, )) -_extend_builtin("NotADirectoryError", (errno.ENOTDIR, )) +_extend_builtin( + "BlockingIOError", + (errno.EAGAIN, errno.EALREADY, errno.EINPROGRESS, errno.EWOULDBLOCK), +) +_extend_builtin("ChildProcessError", (errno.ECHILD,)) +_extend_builtin("ConnectionAbortedError", (errno.ECONNABORTED,)) +_extend_builtin("ConnectionRefusedError", (errno.ECONNREFUSED,)) +_extend_builtin("ConnectionResetError", (errno.ECONNRESET,)) +_extend_builtin("FileExistsError", (errno.EEXIST,)) +_extend_builtin("InterruptedError", (errno.EINTR,)) +_extend_builtin("IsADirectoryError", (errno.EISDIR,)) +_extend_builtin("FileNotFoundError", (errno.ENOENT,)) +_extend_builtin("NotADirectoryError", (errno.ENOTDIR,)) _extend_builtin("BrokenPipeError", (errno.EPIPE, errno.ESHUTDOWN)) -_extend_builtin("ProcessLookupError", (errno.ESRCH, )) -_extend_builtin("TimeoutError", (errno.ETIMEDOUT, )) +_extend_builtin("ProcessLookupError", (errno.ESRCH,)) +_extend_builtin("TimeoutError", (errno.ETIMEDOUT,)) +_extend_builtin("MemoryError", (errno.ENOMEM,)) +_extend_builtin("NotImplementedError", (errno.ENOSYS,)) +_extend_builtin("OverflowError", (errno.ERANGE,)) +_extend_builtin("OSError", [code for code in errno.errorcode if code not in classes]) -# Other obvious ones. -_extend_builtin("ValueError", (errno.EINVAL, )) -_extend_builtin("MemoryError", (errno.ENOMEM, )) -_extend_builtin("NotImplementedError", (errno.ENOSYS, )) -_extend_builtin("OverflowError", (errno.ERANGE, )) -# The rest of them (for now) -_extend_builtin("OSError", [code for code in errno.errorcode if code not in classes]) +class ArgumentError(FFmpegError): + def __str__(self): + msg = "" + if self.strerror is not None: + msg = f"{msg}{self.strerror}" + if self.filename: + msg = f"{msg}: {self.filename!r}" + if self.errno is not None: + msg = f"{msg} returned {self.errno}" + if self.log: + msg = ( + f"{msg}; last error log: [{self.log[1].strip()}] {self.log[2].strip()}" + ) + + return msg + + +class UndefinedError(FFmpegError): + """Fallback exception type in case FFmpeg returns an error we don't know about.""" + + pass + + +classes[errno.EINVAL] = ArgumentError +globals()["ArgumentError"] = ArgumentError +__all__.append("ArgumentError") + # Classes for the FFmpeg errors. for enum_name, code, name, base in _ffmpeg_specs: @@ -360,10 +342,12 @@ def _extend_builtin(name, codes): # Storage for stashing. -cdef object _local = local() -cdef int _err_count = 0 +_local: object = local() +_err_count: cython.int = 0 + -cdef int stash_exception(exc_info=None): +@cython.cfunc +def stash_exception(exc_info=None) -> cython.int: global _err_count existing = getattr(_local, "exc_info", None) @@ -380,9 +364,12 @@ def _extend_builtin(name, codes): return -c_PYAV_STASHED_ERROR -cdef int _last_log_count = 0 +_last_log_count: cython.int = 0 -cpdef int err_check(int res, filename=None) except -1: + +@cython.ccall +@cython.exceptval(-1, check=False) +def err_check(res: cython.int, filename=None) -> cython.int: """Raise appropriate exceptions from library return code.""" global _err_count @@ -394,7 +381,7 @@ def _extend_builtin(name, codes): if exc_info is not None: _err_count -= 1 _local.exc_info = None - raise exc_info[0], exc_info[1], exc_info[2] + raise exc_info[1].with_traceback(exc_info[2]) if res >= 0: return res @@ -407,9 +394,11 @@ def _extend_builtin(name, codes): else: log = None - cdef int code = -res - cdef char* error_buffer = malloc(lib.AV_ERROR_MAX_STRING_SIZE * sizeof(char)) - if error_buffer == NULL: + code: cython.int = -res + error_buffer: cython.p_char = cython.cast( + cython.p_char, malloc(lib.AV_ERROR_MAX_STRING_SIZE * cython.sizeof(char)) + ) + if error_buffer == cython.NULL: raise MemoryError() try: @@ -424,8 +413,3 @@ def _extend_builtin(name, codes): raise cls(code, message, filename, log) finally: free(error_buffer) - - -class UndefinedError(FFmpegError): - """Fallback exception type in case FFmpeg returns an error we don't know about.""" - pass diff --git a/av/error.pyi b/av/error.pyi index e18f7334c..354c5fd3a 100644 --- a/av/error.pyi +++ b/av/error.pyi @@ -69,4 +69,4 @@ class OSError(FFmpegError, builtins.OSError): ... class PermissionError(FFmpegError, builtins.PermissionError): ... class ProcessLookupError(FFmpegError, builtins.ProcessLookupError): ... class TimeoutError(FFmpegError, builtins.TimeoutError): ... -class ValueError(FFmpegError, builtins.ValueError): ... +class ArgumentError(FFmpegError): ... diff --git a/tests/test_bitstream.py b/tests/test_bitstream.py index dde7723c7..5646b46b6 100644 --- a/tests/test_bitstream.py +++ b/tests/test_bitstream.py @@ -97,7 +97,7 @@ def test_filter_flush() -> None: container.seek(0) # Without flushing, we expect to get an error: "A non-NULL packet sent after an EOF." - with pytest.raises(ValueError): + with pytest.raises(av.ArgumentError): for p in container.demux(stream): ctx.filter(p) diff --git a/tests/test_errors.py b/tests/test_errors.py index f654c6b2e..68e2681e1 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -7,7 +7,7 @@ def test_stringify() -> None: - for cls in (av.ValueError, av.FileNotFoundError, av.DecoderNotFoundError): + for cls in (av.FileNotFoundError, av.DecoderNotFoundError): e = cls(1, "foo") assert f"{e}" == "[Errno 1] foo" assert f"{e!r}" == f"{cls.__name__}(1, 'foo')" @@ -16,7 +16,7 @@ def test_stringify() -> None: == f"av.error.{cls.__name__}: [Errno 1] foo\n" ) - for cls in (av.ValueError, av.FileNotFoundError, av.DecoderNotFoundError): + for cls in (av.FileNotFoundError, av.DecoderNotFoundError): e = cls(1, "foo", "bar.txt") assert f"{e}" == "[Errno 1] foo: 'bar.txt'" assert f"{e!r}" == f"{cls.__name__}(1, 'foo', 'bar.txt')" @@ -27,9 +27,7 @@ def test_stringify() -> None: def test_bases() -> None: - assert issubclass(av.ValueError, ValueError) - assert issubclass(av.ValueError, av.FFmpegError) - + assert issubclass(av.ArgumentError, av.FFmpegError) assert issubclass(av.FileNotFoundError, FileNotFoundError) assert issubclass(av.FileNotFoundError, OSError) assert issubclass(av.FileNotFoundError, av.FFmpegError) diff --git a/tests/test_python_io.py b/tests/test_python_io.py index 5a4d1becf..c240a45e6 100644 --- a/tests/test_python_io.py +++ b/tests/test_python_io.py @@ -323,7 +323,8 @@ def test_writing_to_pipe_writeonly(self) -> None: buf = WriteOnlyPipe() with pytest.raises( - ValueError, match=escape("[mp4] muxer does not support non seekable output") + av.ArgumentError, + match=escape("[mp4] muxer does not support non seekable output"), ): write(buf) From ab03fa9c712975ad7264c7b496f43dde3769d9e3 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 16 Jan 2026 00:51:17 -0500 Subject: [PATCH 615/846] New changelog structure --- CHANGELOG.rst | 942 ++------------------------------------------------ 1 file changed, 31 insertions(+), 911 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index bd15c4e75..86eb44f2e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,18 +4,36 @@ Changelog We are operating with `semantic versioning `_. .. - Please try to update this file in the commits that make the changes. + Update this file in your commit that makes a change (besides maintainence). - To make merging/rebasing easier, we don't manually break lines in here - when they are too long, so any particular change is just one line. + To make merging/rebasing easier, don't manually break lines in here when they are too long. + To make tracking easier, please add either ``closes #123`` or ``fixes #123`` to the first line of the commit message, when closing/fixing a GitHub issue. + Changelog entries will be limited to the latest major version and (next) to prevent exponential growth in file storage. + Use the Oxford comma. - To make tracking easier, please add either ``closes #123`` or ``fixes #123`` - to the first line of the commit message. There are more syntaxes at: - . + Entries look like this: - Note that they these tags will not actually close the issue/PR until they - are merged into the "default" branch. + v21.67.42 + --------- + Major: + - Breaking changes (MAJOR) go here, including for binary wheels. + Features: + - Features (MINOR) changes go here. + + Fixes: + - Bug fixes (PATCH) go here. + - $CHANGE by :gh-user:`mikeboers` in (:pr:`1`). + +v17.0.0 (Next) +-------------- + +Major: + +- Limited API binary wheels are now built. +- 3.13t (free-threading) will be dropped because of storage limitations. +- When an FFmpeg C function indicates an error, raise av.ArgumentError instead of ValueError/av.ValueError. This helps disambiguate why an exception is being thrown. +- Save space by removing libaom (av1 encoder/decoder); dav1d, stvav1, and hardware, are available. v16.1.0 ------- @@ -62,907 +80,9 @@ Fixes: - Allow ``None`` in ``FilterContext.push()`` type stub by :gh-user:`velsinki` in (:pr:`2015`). - Fix typos -v15.1.0 -------- - -Features: - -- Support FFmpeg 8 -- Make the `Frame.key_frame` flag writable. - -v15.0.0 -------- - -Major: - -- Make ``SubtitleStream.decode()`` return the list of subtitles directly, making the API nicer. -- Drop support for MacOS 12 or under. -- Binary wheels: Remove libxml2 because of security concerns. -- Binary wheels: Remove xz because lzma encoding is very niche in media decoding and lesser security concerns. - -Features: - -- Add GBRP, GBRAP, RGBA formats by :gh-user:`z-khan`. -- Add ``VideoFrame.save()``, a convenient way to save frames to PNGs or JPEGs, by :gh-user:`WyattBlue`. - -Fixes: - -- Binary wheels: Re-enable libx265, use version 4.1 by :gh-user:`WyattBlue`. -- Fix memory error for AudioFifo properties by :gh-user:`zzjjbb`. -- Copy template flags if creating a new Codec object by :gh-user:`WyattBlue` -- ``AudioFrame.from_ndarray()``: Support python classes for parameters too, by :gh-user:`WyattBlue`. - -v14.4.0 -------- - -Features: - -- Expose the duration field for frames by :gh-user:`Queuecumber` in (:pr:`1880`). -- Add support for BGR48 and BGRA64 pixel formats by :gh-user:`z-khan` in (:pr:`1873`). - -Fixes: - -- Fix security vulnerabilities in binary wheels by :gh-user:`jlaine` in (:pr:`1896`). - -v14.3.0 -------- - -Features: - -- Support yuv420p10le in numpy conversion by :gh-user:`tsnl` in (:pr:`1833`). -- Add typing overloads for ``CodecContext.create`` by :gh-user:`jlaine` in (:pr:`1853`). -- Provide binary wheels for musl-based distros such as Alpine by :gh-user:`jlaine` in (:pr:`1856`). - -Fixes: - -- Fix ``Packet.dts`` typing by :gh-user:`daveisfera` in (:pr:`1815`). -- Use Python 3 compatible stderr logging by :gh-user:`pmarcelll` in (:pr:`1832`). -- Uses ffmpeg 7.1.1, fixes deadlocks. - -v14.2.0 -------- - -Features: - -- Add support for external flags in hwacccel by :gh-user:`materight` in (:pr:`1751`). -- Add Bayer pixel formats by :gh-user:`z-khan` in (:pr:`1755`). -- Add support for yuv422p10le pix_fmt by :gh-user:`WyattBlue` in (:pr:`1767`). -- Add ``supported_np_pix_fmts`` by :gh-user:`WyattBlue` in (:pr:`1766`). -- Add ``Codec.canonical_name`` by :gh-user:`WyattBlue`. - -Misc: - -- Drop support for MacOS 11 by :gh-user:`WyattBlue` in (:pr:`1764`). -- Set default bitrate to 0, allowing variable bitrate by :gh-user:`WyattBlue`. - - -v14.1.0 -------- - -Features: - -- Add hardware decoding by :gh-user:`matthewlai` and :gh-user:`WyattBlue` in (:pr:`1685`). -- Add ``Stream.disposition`` and ``Disposition`` enum by :gh-user:`WyattBlue` in (:pr:`1720`). -- Add ``VideoFrame.rotation`` by :gh-user:`lgeiger` in (:pr:`1675`). -- Support grayf32le and gbrapf32le in numpy conversion by :gh-user:`robinechuca` in (:pr:`1712`). -- Support yuv[a]p16 formats in numpy conversion by :gh-user:`robinechuca` in (:pr:`1722`). - -v14.0.1 -------- - -Fixes: - -- Include header files in source distribution by :gh-user:`hmaarrfk` in (:pr:`1662`). -- Cleanup ``AVStream.side_data`` leftovers by :gh-user:`lgeiger` in (:pr:`1674`). -- Address :issue:`1663` by :gh-user:`WyattBlue`. -- Make ``mp3`` work with ``OutputContainer.add_stream_from_template()``. - -v14.0.0 -------- - -Major: - -- Drop FFmpeg 6. -- Drop support for MacOS <11 in our binary wheels. -- Deleted PyAV's custom Enum class in favor of Python's standard Enums. -- Remove ``CodecContext.close()`` and ``Stream.side_data`` because it's deprecated in ffmpeg. -- Remove ``AVError`` alias (use ``FFmpegError`` directly instead). -- Remove the `template` arg from ``OutputContainer.add_stream()``. - -Features: - -- Add ``OutputContainer.add_stream_from_template()`` by :gh-user:`WyattBlue` and :gh-user:`cdce8p`. -- Add ``OutputContainer.add_data_stream()`` by :gh-user:`WyattBlue`. -- Add ``filter.loudnorm.stats()`` function that returns the stats of loudnorm for 2-pass filtering by :gh-user:`WyattBlue`. -- Add ``qmin`` and ``qmax`` parameters to the ``VideoCodecContext`` by :gh-user:`davidplowman` in (:pr:`1618`). -- Allow the profile of a codec to be set as well as queried by :gh-user:`davidplowman` in (:pr:`1625`). - -Fixes: - -- Make ``VideoFrame.from_numpy_buffer()`` support buffers with padding by :gh-user:`davidplowman` in (:pr:`1635`). -- Correct ``Colorspace``'s lowercase enums. -- Updated ``sidedata.Type`` enum. -- Ensure streams in StreamContainer are released. Fixes :issue:`1599`. - -v13.1.0 -------- - -Features: - -- Allow passing Python objects around using `Frame.opaque` and `Packet.opaque` by :gh-user:`CarlosRDomin` and :gh-user:`WyattBlue` in (:pr:`1533`). -- Allow extradata to be set by encoders by :gh-user:`daveisfera` in (:pr:`1526`). -- Add getting ffmpeg version info string by :gh-user:`hmaarrfk` in (:pr:`1564`). - -Fixes: - -- Remove the `deprecation` module in anticipation of `PEP 702 `_. -- Add type stubs to previously unannotated API sections. -- Improve type stubs for both `mypy` and `mypy --strict`. -- Permit only setting `time_base` with a Fraction, as mypy is unable to respect different types in getters vs setters. -- Declare `write_packet` function as const by :gh-user:`hmaarrfk` in (:pr:`1517`). - -v13.0.0 -------- - -Major: - -- Drop FFmpeg 5, Support FFmpeg 7. -- Drop Python 3.8, Support Python 3.13. -- Update FFmpeg to 7.0.2 for the binary wheels. -- Disallow initializing an AudioLayout object with an int. -- Disallow accessing gop_size, timebase as a decoder (Raised deprecation warning before). -- Remove `ticks_per_frame` property because it was deprecated in FFmpeg. - -Features: - -- Add AttachmentStream class. -- Add `best()` method to StreamContainer. -- Add `set_audio_frame_size()` method to Graph object. -- Add `nb_channels` property to AudioLayout object. -- Add `from_bytes()` method to VideoFrame object. - -Fixes: - -- Fix VideoCC's repl breaking when `self._format` is None. -- Fix getting `pix_fmt` property when VideoCC's `self._format` is None. - -v12.3.0 -------- - -Features: - -- Support libav's `av_log_set_level` by :gh-user:`materight` in (:pr:`1448`). -- Add Graph.link_nodes by :gh-user:`WyattBlue` in (:pr:`1449`). -- Add default codec properties by :gh-user:`WyattBlue` in (:pr:`1450`). -- Remove the xvid and ass packages in ffmpeg binaries because they were unused by :gh-user:`WyattBlue` in (:pr:`1462`). -- Add supported_codecs property to OutputContainer by :gh-user:`WyattBlue` in (:pr:`1465`). -- Add text and dialogue property to AssSubtitle, remove TextSubtitle by :gh-user:`WyattBlue` in (:pr:`1456`). - -Fixes: - -- Include libav headers in final distribution by :gh-user:`materight` in (:pr:`1455`). -- Fix segfault when calling subtitle_stream.decode() by :gh-user:`WyattBlue` in (:pr:`1460`). -- flushing subtitle decoder requires a new uninitialized packet by :gh-user:`moonsikpark` in (:pr:`1461`). -- Set default color range for VideoReformatter.format() by :gh-user:`elxy` in (:pr:`1458`). -- Resampler: format, layout accepts `str` `int` too by :gh-user:`WyattBlue` in (:pr:`1446`). - -v12.2.0 -------- - -Features: - -- Add a `make_writable` method to `Frame` instances (:issue:`1414`). -- Use `av_guess_sample_aspect_ratio` to report sample and display aspect ratios. - -Fixes: - -- Fix a crash when assigning an `AudioLayout` to `AudioCodecContext.layout` (:issue:`1434`). -- Remove a circular reference which caused `AudioSampler` to occupy memory until garbage collected (:issue:`1429`). -- Fix more type stubs, remove incorrect `__init__.pyi`. - -v12.1.0 -------- - -Features: - -- Build binary wheels with webp support. -- Allow disabling logs, disable logs by default. -- Add bitstream filters by :gh-user:`skeskinen` in (:pr:`1379` :issue:`1375`). -- Expose CodecContext flush_buffers by :gh-user:`skeskinen` in (:pr:`1382`). - -Fixes: - -- Fix type stubs, add missing type stubs. -- Add S12M_TIMECODE by :gh-user:`WyattBlue` in (:pr:`1381`). -- Subtitle.text now returns bytes by :gh-user:`WyattBlue` in (:pr:`1398`). -- Allow packet.duration to be writable by :gh-user:`WyattBlue` in (:pr:`1399`). -- Remove deprecated `VideoStream.frame_rate` by :gh-user:`WyattBlue` in (:pr:`1351`). -- Build with Arm for PyPy now by :gh-user:`WyattBlue` in (:pr:`1395`). -- Fix #1378 by :gh-user:`WyattBlue` in (:pr:`1400`). -- setup.py: use PKG_CONFIG env var to get the pkg-config to use by :gh-user:`Artturin` in (:pr:`1387`). - -v12.0.0 -------- - -Major: - -- Add type hints. -- Update FFmpeg to 6.1.1 for the binary wheels. -- Update libraries for the binary wheels (notably dav1d to 1.4.1). -- Deprecate VideoCodecContext.gop_size for decoders by :gh-user:`JoeSchiff` in (:pr:`1256`). -- Deprecate frame.index by :gh-user:`JoeSchiff` in (:pr:`1218`). - -Features: - -- Allow using pathlib.Path for av.open by :gh-user:`WyattBlue` in (:pr:`1231`). -- Add `max_b_frames` property to CodecContext by :gh-user:`davidplowman` in (:pr:`1119`). -- Add `encode_lazy` method to CodecContext by :gh-user:`rawler` in (:pr:`1092`). -- Add `color_range` to CodecContext/Frame by :gh-user:`johanjeppsson` in (:pr:`686`). -- Set `time_base` for AudioResampler by :gh-user:`daveisfera` in (:issue:`1209`). -- Add support for ffmpeg's AVCodecContext::delay by :gh-user:`JoeSchiff` in (:issue:`1279`). -- Add `color_primaries`, `color_trc`, `colorspace` to VideoStream by :gh-user:`WyattBlue` in (:pr:`1304`). -- Add `bits_per_coded_sample` to VideoCodecContext by :gh-user:`rvanlaar` in (:pr:`1203`). -- AssSubtitle.ass now returns as bytes by :gh-user:`WyattBlue` in (:pr:`1333`). -- Expose DISPLAYMATRIX side data by :gh-user:`hyenal` in (:pr:`1249`). - -Fixes: - -- Convert deprecated Cython extension class properties to decorator syntax by :gh-user:`JoeSchiff`. -- Check None packet when setting time_base after decode by :gh-user:`philipnbbc` in (:pr:`1281`). -- Remove deprecated `Buffer.to_bytes` by :gh-user:`WyattBlue` in (:pr:`1286`). -- Remove deprecated `Packet.decode_one` by :gh-user:`WyattBlue` in (:pr:`1301`). - -v11.0.0 -------- - -Major: - -- Add support for FFmpeg 6.0, drop support for FFmpeg < 5.0. -- Add support for Python 3.12, drop support for Python < 3.8. -- Build binary wheels against libvpx 1.13.1 to fix CVE-2023-5217. -- Build binary wheels against FFmpeg 6.0. - -Features: - -- Add support for the `ENCODER_FLUSH` encoder flag (:issue:`1067`). -- Add VideoFrame ndarray operations for yuv444p/yuvj444p formats (:issue:`788`). -- Add setters for `AVFrame.dts`, `AVPacket.is_keyframe` and `AVPacket.is_corrupt` (:issue:`1179`). - -Fixes: - -- Fix build using Cython 3 (:issue:`1140`). -- Populate new streams with codec parameters (:issue:`1044`). -- Explicitly set `python_requires` to avoid installing on incompatible Python (:issue:`1057`). -- Make `AudioFifo.__repr__` safe before the first frame (:issue:`1130`). -- Guard input container members against use after closes (:issue:`1137`). - -v10.0.0 -------- - -Major: - -- Add support for FFmpeg 5.0 and 5.1 (:issue:`817`). -- Drop support for FFmpeg < 4.3. -- Deprecate `CodecContext.time_base` for decoders (:issue:`966`). -- Deprecate `VideoStream.framerate` and `VideoStream.rate` (:issue:`1005`). -- Stop proxying `Codec` from `Stream` instances (:issue:`1037`). - -Features: - -- Update FFmpeg to 5.1.2 for the binary wheels. -- Provide binary wheels for Python 3.11 (:issue:`1019`). -- Add VideoFrame ndarray operations for gbrp formats (:issue:`986`). -- Add VideoFrame ndarray operations for gbrpf32 formats (:issue:`1028`). -- Add VideoFrame ndarray operations for nv12 format (:issue:`996`). - -Fixes: - -- Fix conversion to numpy array for multi-byte formats (:issue:`981`). -- Safely iterate over filter pads (:issue:`1000`). - -v9.2.0 ------- - -Features: - -- Update binary wheels to enable libvpx support. -- Add an `io_open` argument to `av.open` for multi-file custom I/O. -- Add support for AV_FRAME_DATA_SEI_UNREGISTERED (:issue:`723`). -- Ship .pxd files to allow other libraries to `cimport av` (:issue:`716`). - -Fixes: - -- Fix an `ImportError` when using Python 3.8/3.9 via Conda (:issue:`952`). -- Fix a muxing memory leak which was introduced in v9.1.0 (:issue:`959`). - -v9.1.1 ------- - -Fixes: - -- Update binary wheels to update dependencies on Windows, disable ALSA on Linux. - -v9.1.0 ------- - -Features: - -- Add VideoFrame ndarray operations for rgb48be, rgb48le, rgb64be, rgb64le pixel formats. -- Add VideoFrame ndarray operations for gray16be, gray16le pixel formats (:issue:`674`). -- Make it possible to use av.open() on a pipe (:issue:`738`). -- Use the "ASS without timings" format when decoding subtitles. - -Fixes: - -- Update binary wheels to fix security vulnerabilities (:issue:`921`) and enable ALSA on Linux (:issue:`941`). -- Fix crash when closing an output container an encountering an I/O error (:issue:`613`). -- Fix crash when probing corrupt raw format files (:issue:`590`). -- Fix crash when manipulating streams with an unknown codec (:issue:`689`). -- Remove obsolete KEEP_SIDE_DATA and MP4A_LATM flags which are gone in FFmpeg 5.0. -- Deprecate `to_bytes()` method of Packet, Plane and SideData, use `bytes(packet)` instead. - -v9.0.2 ------- - -Minor: - -- Update FFmpeg to 4.4.1 for the binary wheels. -- Fix framerate when writing video with FFmpeg 4.4 (:issue:`876`). - -v9.0.1 ------- - -Minor: - -- Update binary wheels to fix security vulnerabilities (:issue:`901`). - -v9.0.0 ------- - -Major: - -- Re-implement AudioResampler with aformat and buffersink (:issue:`761`). - AudioResampler.resample() now returns a list of frames. -- Remove deprecated methods: AudioFrame.to_nd_array, VideoFrame.to_nd_array and Stream.seek. - -Minor: - -- Provide binary wheels for macOS/arm64 and Linux/aarch64. -- Simplify setup.py, require Cython. -- Update the installation instructions in favor of PyPI. -- Fix VideoFrame.to_image with height & width (:issue:`878`). -- Fix setting Stream time_base (:issue:`784`). -- Replace deprecated av_init_packet with av_packet_alloc (:issue:`872`). -- Validate pixel format in VideoCodecContext.pix_fmt setter (:issue:`815`). -- Fix AudioFrame ndarray conversion endianness (:issue:`833`). -- Improve time_base support with filters (:issue:`765`). -- Allow flushing filters by sending `None` (:issue:`886`). -- Avoid unnecessary vsnprintf() calls in log_callback() (:issue:`877`). -- Make Frame.from_ndarray raise ValueError instead of AssertionError. - -v8.1.0 ------- - -Minor: - -- Update FFmpeg to 4.3.2 for the binary wheels. -- Provide binary wheels for Python 3.10 (:issue:`820`). -- Stop providing binary wheels for end-of-life Python 3.6. -- Fix args order in Frame.__repr__ (:issue:`749`). -- Fix documentation to remove unavailable QUIET log level (:issue:`719`). -- Expose codec_context.codec_tag (:issue:`741`). -- Add example for encoding with a custom PTS (:issue:`725`). -- Use av_packet_rescale_ts in Packet._rebase_time() (:issue:`737`). -- Do not hardcode errno values in test suite (:issue:`729`). -- Use av_guess_format for output container format (:issue:`691`). -- Fix setting CodecContext.extradata (:issue:`658`, :issue:`740`). -- Fix documentation code block indentation (:issue:`783`). -- Fix link to Conda installation instructions (:issue:`782`). -- Export AudioStream from av.audio (:issue:`775`). -- Fix setting CodecContext.extradata (:issue:`801`). - -v8.0.3 ------- - -Minor: - -- Update FFmpeg to 4.3.1 for the binary wheels. - -v8.0.2 ------- - -Minor: - -- Enable GnuTLS support in the FFmpeg build used for binary wheels (:issue:`675`). -- Make binary wheels compatible with Mac OS X 10.9+ (:issue:`662`). -- Drop Python 2.x buffer protocol code. -- Remove references to previous repository location. - -v8.0.1 ------- - -Minor: - -- Enable additional FFmpeg features in the binary wheels. -- Use os.fsencode for both input and output file names (:issue:`600`). - -v8.0.0 ------- - -Major: - -- Drop support for Python 2 and Python 3.4. -- Provide binary wheels for Linux, Mac and Windows. - -Minor: - -- Remove shims for obsolete FFmpeg versions (:issue:`588`). -- Add yuvj420p format for :meth:`VideoFrame.from_ndarray` and :meth:`VideoFrame.to_ndarray` (:issue:`583`). -- Add support for palette formats in :meth:`VideoFrame.from_ndarray` and :meth:`VideoFrame.to_ndarray` (:issue:`601`). -- Fix Python 3.8 deprecation warning related to abstract base classes (:issue:`616`). -- Remove ICC profiles from logos (:issue:`622`). - -Fixes: - -- Avoid infinite timeout in :func:`av.open` (:issue:`589`). - -v7.0.1 ------- - -Fixes: - -- Removed deprecated ``AV_FRAME_DATA_QP_TABLE_*`` enums. (:issue:`607`) - - -v7.0.0 ------- - -Major: - -- Drop support for FFmpeg < 4.0. (:issue:`559`) -- Introduce per-error exceptions, and mirror the builtin exception hierarchy. It is recommended to examine your error handling code, as common FFmpeg errors will result in `ValueError` baseclasses now. (:issue:`563`) -- Data stream's `encode` and `decode` return empty lists instead of none allowing common API use patterns with data streams. -- Remove ``whence`` parameter from :meth:`InputContainer.seek` as non-time seeking doesn't seem to actually be supported by any FFmpeg formats. - -Minor: - -- Users can disable the logging system to avoid lockups in sub-interpreters. (:issue:`545`) -- Filters support audio in general, and a new :meth:`.Graph.add_abuffer`. (:issue:`562`) -- :func:`av.open` supports `timeout` parameters. (:issue:`480` and :issue:`316`) -- Expose :attr:`Stream.base_rate` and :attr:`Stream.guessed_rate`. (:issue:`564`) -- :meth:`.VideoFrame.reformat` can specify interpolation. -- Expose many sets of flags. - -Fixes: - -- Fix typing in :meth:`.CodecContext.parse` and make it more robust. -- Fix wrong attribute in ByteSource. (:issue:`340`) -- Remove exception that would break audio remuxing. (:issue:`537`) -- Log messages include last FFmpeg error log in more helpful way. -- Use AVCodecParameters so FFmpeg doesn't complain. (:issue:`222`) - - -v6.2.0 ------- - -Major: - -- Allow :meth:`av.open` to be used as a context manager. -- Fix compatibility with PyPy, the full test suite now passes. (:issue:`130`) - -Minor: - -- Add :meth:`.InputContainer.close` method. (:issue:`317`, :issue:`456`) -- Ensure audio output gets flushes when using a FIFO. (:issue:`511`) -- Make Python I/O buffer size configurable. (:issue:`512`) -- Make :class:`.AudioFrame` and :class:`VideoFrame` more garbage-collector friendly by breaking a reference cycle. (:issue:`517`) - -Build: - -- Do not install the `scratchpad` package. - - -v6.1.2 ------- - -Micro: - -- Fix a numpy deprecation warning in :meth:`.AudioFrame.to_ndarray`. - - -v6.1.1 ------- - -Micro: - -- Fix alignment in :meth:`.VideoFrame.from_ndarray`. (:issue:`478`) -- Fix error message in :meth:`.Buffer.update`. - -Build: - -- Fix more compiler warnings. - - -v6.1.0 ------- - -Minor: - -- ``av.datasets`` for sample data that is pulled from either FFmpeg's FATE suite, or our documentation server. -- :meth:`.InputContainer.seek` gets a ``stream`` argument to specify the ``time_base`` the requested ``offset`` is in. - -Micro: - -- Avoid infinite look in ``Stream.__getattr__``. (:issue:`450`) -- Correctly handle Python I/O with no ``seek`` method. -- Remove ``Datastream.seek`` override (:issue:`299`) - -Build: - -- Assert building against compatible FFmpeg. (:issue:`401`) -- Lock down Cython language level to avoid build warnings. (:issue:`443`) - -Other: - -- Incremental improvements to docs and tests. -- Examples directory will now always be runnable as-is, and embedded in the docs (in a copy-pastable form). - - -v6.0.0 ------- - -Major: - -- Drop support for FFmpeg < 3.2. -- Remove ``VideoFrame.to_qimage`` method, as it is too tied to PyQt4. (:issue:`424`) - -Minor: - -- Add support for all known sample formats in :meth:`.AudioFrame.to_ndarray` and add :meth:`.AudioFrame.to_ndarray`. (:issue:`422`) -- Add support for more image formats in :meth:`.VideoFrame.to_ndarray` and :meth:`.VideoFrame.from_ndarray`. (:issue:`415`) - -Micro: - -- Fix a memory leak in :meth:`.OutputContainer.mux_one`. (:issue:`431`) -- Ensure :meth:`.OutputContainer.close` is called at destruction. (:issue:`427`) -- Fix a memory leak in :class:`.OutputContainer` initialisation. (:issue:`427`) -- Make all video frames created by PyAV use 8-byte alignment. (:issue:`425`) -- Behave properly in :meth:`.VideoFrame.to_image` and :meth:`.VideoFrame.from_image` when ``width != line_width``. (:issue:`425`) -- Fix manipulations on video frames whose width does not match the line stride. (:issue:`423`) -- Fix several :attr:`.Plane.line_size` misunderstandings. (:issue:`421`) -- Consistently decode dictionary contents. (:issue:`414`) -- Always use send/recv en/decoding mechanism. This removes the ``count`` parameter, which was not used in the send/recv pipeline. (:issue:`413`) -- Remove various deprecated iterators. (:issue:`412`) -- Fix a memory leak when using Python I/O. (:issue:`317`) -- Make :meth:`.OutputContainer.mux_one` call `av_interleaved_write_frame` with the GIL released. - -Build: - -- Remove the "reflection" mechanism, and rely on FFmpeg version we build against to decide which methods to call. (:issue:`416`) -- Fix many more ``const`` warnings. - - -v0.x.y ------- - -.. note:: - - Below here we used ``v0.x.y``. - - We incremented ``x`` to signal a major change (i.e. backwards - incompatibilities) and incremented ``y`` as a minor change (i.e. backwards - compatible features). - - Once we wanted more subtlety and felt we had matured enough, we jumped - past the implications of ``v1.0.0`` straight to ``v6.0.0`` - (as if we had not been stuck in ``v0.x.y`` all along). - - -v0.5.3 ------- - -Minor: - -- Expose :attr:`.VideoFrame.pict_type` as :class:`.PictureType` enum. - (:pr:`402`) -- Expose :attr:`.Codec.video_rates` and :attr:`.Codec.audio_rates`. - (:pr:`381`) - -Patch: - -- Fix :attr:`.Packet.time_base` handling during flush. - (:pr:`398`) -- :meth:`.VideoFrame.reformat` can throw exceptions when requested colorspace - transforms aren't possible. -- Wrapping the stream object used to overwrite the ``pix_fmt`` attribute. - (:pr:`390`) - -Runtime: - -- Deprecate ``VideoFrame.ptr`` in favour of :attr:`VideoFrame.buffer_ptr`. -- Deprecate ``Plane.update_buffer()`` and ``Packet.update_buffer`` in favour of - :meth:`.Plane.update`. - (:pr:`407`) -- Deprecate ``Plane.update_from_string()`` in favour of :meth:`.Plane.update`. - (:pr:`407`) -- Deprecate ``AudioFrame.to_nd_array()`` and ``VideoFrame.to_nd_array()`` in - favour of :meth:`.AudioFrame.to_ndarray` and :meth:`.VideoFrame.to_ndarray`. - (:pr:`404`) - -Build: - -- CI covers more cases, including macOS. - (:pr:`373` and :pr:`399`) -- Fix many compilation warnings. - (:issue:`379`, :pr:`380`, :pr:`387`, and :pr:`388`) - -Docs: - -- Docstrings for many commonly used attributes. - (:pr:`372` and :pr:`409`) - - -v0.5.2 ------- - -Build: - -- Fixed Windows build, which broke in v0.5.1. -- Compiler checks are not cached by default. This behaviour is retained if you - ``source scripts/activate.sh`` to develop PyAV. - (:issue:`256`) -- Changed to ``PYAV_SETUP_REFLECT_DEBUG=1`` from ``PYAV_DEBUG_BUILD=1``. - - -v0.5.1 ------- - -Build: - -- Set ``PYAV_DEBUG_BUILD=1`` to force a verbose reflection (mainly for being - installed via ``pip``, which is why this is worth a release). - - -v0.5.0 ------- - -Major: - -- Dropped support for Libav in general. - (:issue:`110`) -- No longer uses libavresample. - -Minor: - -- ``av.open`` has ``container_options`` and ``stream_options``. -- ``Frame`` includes ``pts`` in ``repr``. - -Patch: - -- EnumItem's hash calculation no longer overflows. - (:issue:`339`, :issue:`341` and :issue:`342`.) -- Frame.time_base was not being set in most cases during decoding. - (:issue:`364`) -- CodecContext.options no longer needs to be manually initialized. -- CodexContext.thread_type accepts its enums. - - -v0.4.1 ------- - -Minor: - -- Add `Frame.interlaced_frame` to indicate if the frame is interlaced. - (:issue:`327` by :gh-user:`MPGek`) -- Add FLTP support to ``Frame.to_nd_array()``. - (:issue:`288` by :gh-user:`rawler`) -- Expose ``CodecContext.extradata`` for codecs that have extra data, e.g. - Huffman tables. - (:issue:`287` by :gh-user:`adavoudi`) - -Patch: - -- Packets retain their refcount after muxing. - (:issue:`334`) -- `Codec` construction is more robust to find more codecs. - (:issue:`332` by :gh-user:`adavoudi`) -- Refined frame corruption detection. - (:issue:`291` by :gh-user:`Litterfeldt`) -- Unicode filenames are okay. - (:issue:`82`) - - -v0.4.0 ------- - -Major: - -- ``CodecContext`` has taken over encoding/decoding, and can work in isolation - of streams/containers. -- ``Stream.encode`` returns a list of packets, instead of a single packet. -- ``AudioFifo`` and ``AudioResampler`` will raise ``ValueError`` if input frames - inconsistent ``pts``. -- ``time_base`` use has been revisited across the codebase, and may not be converted - between ``Stream.time_base`` and ``CodecContext.time_base`` at the same times - in the transcoding pipeline. -- ``CodecContext.rate`` has been removed, but proxied to ``VideoCodecContext.framerate`` - and ``AudioCodecContext.sample_rate``. The definition is effectively inverted from - the old one (i.e. for 24fps it used to be ``1/24`` and is now ``24/1``). -- Fractions (e.g. ``time_base``, ``rate``) will be ``None`` if they are invalid. -- ``InputContainer.seek`` and ``Stream.seek`` will raise TypeError if given - a float, when previously they converted it from seconds. - -Minor: - -- Added ``Packet.is_keyframe`` and ``Packet.is_corrupt``. - (:issue:`226`) -- Many more ``time_base``, ``pts`` and other attributes are writable. -- ``Option`` exposes much more of the API (but not get/set). - (:issue:`243`) -- Expose metadata encoding controls. - (:issue:`250`) -- Expose ``CodecContext.skip_frame``. - (:issue:`259`) - -Patch: - -- Build doesn't fail if you don't have git installed. - (:issue:`184`) -- Developer environment works better with Python3. - (:issue:`248`) -- Fix Container deallocation resulting in segfaults. - (:issue:`253`) - - -v0.3.3 ------- - -Patch: - -- Fix segfault due to buffer overflow in handling of stream options. - (:issue:`163` and :issue:`169`) -- Fix segfault due to seek not properly checking if codecs were open before - using avcodec_flush_buffers. - (:issue:`201`) - - -v0.3.2 ------- - -Minor: - -- Expose basics of avfilter via ``Filter``. -- Add ``Packet.time_base``. -- Add ``AudioFrame.to_nd_array`` to match same on ``VideoFrame``. -- Update Windows build process. - -Patch: - -- Further improvements to the logging system. - (:issue:`128`) - - -v0.3.1 ------- - -Minor: - -- ``av.logging.set_log_after_shutdown`` renamed to ``set_print_after_shutdown`` -- Repeating log messages will be skipped, much like ffmpeg's does by default - -Patch: - -- Fix memory leak in logging system when under heavy logging loads while - threading. - (:issue:`128` with help from :gh-user:`mkassner` and :gh-user:`ksze`) - - -v0.3.0 ------- - -Major: - -- Python IO can write -- Improve build system to use Python's C compiler for function detection; - build system is much more robust -- MSVC support. - (:issue:`115` by :gh-user:`vidartf`) -- Continuous integration on Windows via AppVeyor. (by :gh-user:`vidartf`) - -Minor: - -- Add ``Packet.decode_one()`` to skip packet flushing for codecs that would - otherwise error -- ``StreamContainer`` for easier selection of streams -- Add buffer protocol support to Packet - -Patch: - -- Fix bug when using Python IO on files larger than 2GB. - (:issue:`109` by :gh-user:`xxr3376`) -- Fix usage of changed Pillow API - -Known Issues: - -- VideoFrame is suspected to leak memory in narrow cases on Linux. - (:issue:`128`) - - -v0.2.4 ------- - -- fix library search path for current Libav/Ubuntu 14.04. - (:issue:`97`) -- explicitly include all sources to combat 0.2.3 release problem. - (:issue:`100`) - - -v0.2.3 ------- - -.. warning:: There was an issue with the PyPI distribution in which it required - Cython to be installed. - -Major: - -- Python IO. -- Aggressively releases GIL -- Add experimental Windows build. - (:issue:`84`) - -Minor: - -- Several new Stream/Packet/Frame attributes - -Patch: - -- Fix segfault in audio handling. - (:issue:`86` and :issue:`93`) -- Fix use of PIL/Pillow API. - (:issue:`85`) -- Fix bad assumptions about plane counts. - (:issue:`76`) - - -v0.2.2 ------- - -- Cythonization in setup.py; mostly a development issue. -- Fix for av.InputContainer.size over 2**31. - - -v0.2.1 ------- - -- Python 3 compatibility! -- Build process fails if missing libraries. -- Fix linking of libavdevices. - - -v0.2.0 ------- - -.. warning:: This version has an issue linking in libavdevices, and very likely - will not work for you. - -It sure has been a long time since this was released, and there was a lot of -arbitrary changes that come with us wrapping an API as we are discovering it. -Changes include, but are not limited to: - -- Audio encoding. -- Exposing planes and buffers. -- Descriptors for channel layouts, video and audio formats, etc.. -- Seeking. -- Many many more properties on all of the objects. -- Device support (e.g. webcams). - - -v0.1.0 ------- +15.X and Below +-------------- +.. + or see older git commits -- FIRST PUBLIC RELEASE! -- Container/video/audio formats. -- Audio layouts. -- Decoding video/audio/subtitles. -- Encoding video. -- Audio FIFOs and resampling. +`15.X Changelog ` From 2b4088fc9089289e3f9fdaec7740dd7b8abe8532 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 16 Jan 2026 01:10:11 -0500 Subject: [PATCH 616/846] Update the copyright year --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index d971572e1..02c0d9f4b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,7 +27,7 @@ source_suffix = ".rst" master_doc = "index" project = "PyAV" -copyright = "2025, The PyAV Team" +copyright = "2026, The PyAV Team" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the From 04fe98051a03418644d413d5b37979ad53046326 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 16 Jan 2026 01:37:51 -0500 Subject: [PATCH 617/846] Build cp314t and cp310 along with limited wheels --- .github/workflows/smoke.yml | 2 ++ .github/workflows/tests.yml | 2 +- pyproject.toml | 3 ++- setup.py | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index fa78a285c..218e36241 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -7,12 +7,14 @@ on: - '**.md' - '**.rst' - '**.txt' + - '.github/workflows/tests.yml' pull_request: branches: main paths-ignore: - '**.md' - '**.rst' - '**.txt' + - '.github/workflows/tests.yml' jobs: style: runs-on: ubuntu-slim diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ca29cb4b3..e151e3c56 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -62,7 +62,7 @@ jobs: CIBW_ENVIRONMENT_MACOS: PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig LDFLAGS=-headerpad_max_install_names CIBW_ENVIRONMENT_WINDOWS: INCLUDE=C:\\cibw\\vendor\\include LIB=C:\\cibw\\vendor\\lib PYAV_SKIP_TESTS=unicode_filename CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: delvewheel repair --add-path C:\cibw\vendor\bin -w {dest_dir} {wheel} - CIBW_SKIP: "pp* cp312* cp313* cp314*" + CIBW_BUILD: "cp310* cp311* cp314t*" CIBW_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m pytest {package}/tests && mv {project}/av.disabled {project}/av CIBW_TEST_REQUIRES: pytest numpy run: | diff --git a/pyproject.toml b/pyproject.toml index 7ab4ae2ae..24bdc90a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ authors = [ {name = "WyattBlue", email = "wyattblue@auto-editor.com"}, {name = "Jeremy Lainé", email = "jeremy.laine@m4x.org"}, ] -requires-python = ">=3.11" +requires-python = ">=3.10" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", @@ -20,6 +20,7 @@ classifiers = [ "Operating System :: Unix", "Operating System :: Microsoft :: Windows", "Programming Language :: Cython", + "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", diff --git a/setup.py b/setup.py index 9be9acb27..0e00f9873 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ "swresample", ] -if sys.implementation.name == "cpython": +if sys.implementation.name == "cpython" and (3, 14) > sys.version_info > (3, 11): py_limited_api = True options = {"bdist_wheel": {"py_limited_api": "cp311"}} define_macros = [("Py_LIMITED_API", 0x030B0000)] From 27d2e2300045284811939ec94455c6be8d9e5aee Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 16 Jan 2026 02:31:37 -0500 Subject: [PATCH 618/846] Build windows aarch64 --- .github/workflows/tests.yml | 3 +++ scripts/activate.sh | 2 +- scripts/ffmpeg-8.0.json | 2 +- scripts/ffmpeg-latest.json | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e151e3c56..4b4e1f45d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -39,6 +39,8 @@ jobs: arch: x86_64 - os: windows-latest arch: AMD64 + - os: windows-11-arm + arch: ARM64 steps: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 @@ -63,6 +65,7 @@ jobs: CIBW_ENVIRONMENT_WINDOWS: INCLUDE=C:\\cibw\\vendor\\include LIB=C:\\cibw\\vendor\\lib PYAV_SKIP_TESTS=unicode_filename CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: delvewheel repair --add-path C:\cibw\vendor\bin -w {dest_dir} {wheel} CIBW_BUILD: "cp310* cp311* cp314t*" + CIBW_SKIP: "cp310-win_arm64" CIBW_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m pytest {package}/tests && mv {project}/av.disabled {project}/av CIBW_TEST_REQUIRES: pytest numpy run: | diff --git a/scripts/activate.sh b/scripts/activate.sh index 95f60a33d..a58a64b94 100755 --- a/scripts/activate.sh +++ b/scripts/activate.sh @@ -21,7 +21,7 @@ if [[ ! "$PYAV_LIBRARY" ]]; then return 1 fi else - PYAV_LIBRARY=ffmpeg-8.0 + PYAV_LIBRARY=ffmpeg-8.0.1 echo "No \$PYAV_LIBRARY set; defaulting to $PYAV_LIBRARY" fi fi diff --git a/scripts/ffmpeg-8.0.json b/scripts/ffmpeg-8.0.json index 5ec867122..8e56abfd1 100644 --- a/scripts/ffmpeg-8.0.json +++ b/scripts/ffmpeg-8.0.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0.1-3/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0.1-4/ffmpeg-{platform}.tar.gz" } diff --git a/scripts/ffmpeg-latest.json b/scripts/ffmpeg-latest.json index 5ec867122..8e56abfd1 100644 --- a/scripts/ffmpeg-latest.json +++ b/scripts/ffmpeg-latest.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0.1-3/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0.1-4/ffmpeg-{platform}.tar.gz" } From 906c4304aa59718b96ef759e973e8f3c8bcf0f59 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 19 Jan 2026 01:05:38 -0500 Subject: [PATCH 619/846] Make video/plane pure python --- av/video/{plane.pyx => plane.py} | 13 ++++++++----- av/video/reformatter.pyx | 8 +++----- 2 files changed, 11 insertions(+), 10 deletions(-) rename av/video/{plane.pyx => plane.py} (81%) diff --git a/av/video/plane.pyx b/av/video/plane.py similarity index 81% rename from av/video/plane.pyx rename to av/video/plane.py index 908b48716..495a9de4c 100644 --- a/av/video/plane.pyx +++ b/av/video/plane.py @@ -1,8 +1,10 @@ -from av.video.frame cimport VideoFrame +import cython +from cython.cimports.av.video.frame import VideoFrame -cdef class VideoPlane(Plane): - def __cinit__(self, VideoFrame frame, int index): +@cython.cclass +class VideoPlane(Plane): + def __cinit__(self, frame: VideoFrame, index: cython.int): # The palette plane has no associated component or linesize; set fields manually if frame.format.name == "pal8" and index == 1: self.width = 256 @@ -16,7 +18,7 @@ def __cinit__(self, VideoFrame frame, int index): self.width = component.width self.height = component.height break - else: + else: # nobreak raise RuntimeError(f"could not find plane {index} of {frame.format!r}") # Sometimes, linesize is negative (and that is meaningful). We are only @@ -24,7 +26,8 @@ def __cinit__(self, VideoFrame frame, int index): # ignore it's direction. self.buffer_size = abs(self.frame.ptr.linesize[self.index]) * self.height - cdef size_t _buffer_size(self): + @cython.cfunc + def _buffer_size(self) -> cython.size_t: return self.buffer_size @property diff --git a/av/video/reformatter.pyx b/av/video/reformatter.pyx index a0c576d12..4f6ec4c9f 100644 --- a/av/video/reformatter.pyx +++ b/av/video/reformatter.pyx @@ -165,12 +165,11 @@ cdef class VideoReformatter: if src_colorspace != dst_colorspace or src_color_range != dst_color_range: with nogil: - # Casts for const-ness, because Cython isn't expressive enough. ret = lib.sws_getColorspaceDetails( self.ptr, - &inv_tbl, + &inv_tbl, &src_colorspace_range, - &tbl, + &tbl, &dst_colorspace_range, &brightness, &contrast, @@ -210,8 +209,7 @@ cdef class VideoReformatter: with nogil: lib.sws_scale( self.ptr, - # Cast for const-ness, because Cython isn't expressive enough. - frame.ptr.data, + frame.ptr.data, frame.ptr.linesize, 0, # slice Y frame.ptr.height, From 205d548b64a73b3d438f6cf444159373a6ee9322 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 19 Jan 2026 01:32:40 -0500 Subject: [PATCH 620/846] Make vidoe/format pure --- av/video/format.pxd | 6 --- av/video/{format.pyx => format.py} | 84 ++++++++++++++++-------------- 2 files changed, 45 insertions(+), 45 deletions(-) rename av/video/{format.pyx => format.py} (69%) diff --git a/av/video/format.pxd b/av/video/format.pxd index a2efa9d1d..2c6323294 100644 --- a/av/video/format.pxd +++ b/av/video/format.pxd @@ -2,26 +2,20 @@ cimport libav as lib cdef class VideoFormat: - cdef lib.AVPixelFormat pix_fmt cdef const lib.AVPixFmtDescriptor *ptr cdef readonly unsigned int width, height - cdef readonly tuple components - cdef _init(self, lib.AVPixelFormat pix_fmt, unsigned int width, unsigned int height) - cpdef chroma_width(self, int luma_width=?) cpdef chroma_height(self, int luma_height=?) cdef class VideoFormatComponent: - cdef VideoFormat format cdef readonly unsigned int index cdef const lib.AVComponentDescriptor *ptr cdef VideoFormat get_video_format(lib.AVPixelFormat c_format, unsigned int width, unsigned int height) - cdef lib.AVPixelFormat get_pix_fmt(const char *name) except lib.AV_PIX_FMT_NONE diff --git a/av/video/format.pyx b/av/video/format.py similarity index 69% rename from av/video/format.pyx rename to av/video/format.py index 4e66a8836..50fb1a89d 100644 --- a/av/video/format.pyx +++ b/av/video/format.py @@ -1,31 +1,39 @@ +import cython +from cython import uint as cuint -cdef object _cinit_bypass_sentinel = object() +_cinit_bypass_sentinel = cython.declare(object, object()) -cdef VideoFormat get_video_format(lib.AVPixelFormat c_format, unsigned int width, unsigned int height): + +@cython.cfunc +def get_video_format( + c_format: lib.AVPixelFormat, width: cuint, height: cuint +) -> VideoFormat | None: if c_format == lib.AV_PIX_FMT_NONE: return None - cdef VideoFormat format = VideoFormat.__new__(VideoFormat, _cinit_bypass_sentinel) + format: VideoFormat = VideoFormat.__new__(VideoFormat, _cinit_bypass_sentinel) format._init(c_format, width, height) return format -cdef lib.AVPixelFormat get_pix_fmt(const char *name) except lib.AV_PIX_FMT_NONE: - """Wrapper for lib.av_get_pix_fmt with error checking.""" - cdef lib.AVPixelFormat pix_fmt = lib.av_get_pix_fmt(name) +@cython.cfunc +@cython.exceptval(lib.AV_PIX_FMT_NONE, check=False) +def get_pix_fmt(name: cython.p_const_char) -> lib.AVPixelFormat: + """Wrapper for lib.av_get_pix_fmt with error checking.""" + pix_fmt: lib.AVPixelFormat = lib.av_get_pix_fmt(name) if pix_fmt == lib.AV_PIX_FMT_NONE: raise ValueError("not a pixel format: %r" % name) - return pix_fmt -cdef class VideoFormat: +@cython.cclass +class VideoFormat: """ - >>> format = VideoFormat('rgb24') - >>> format.name - 'rgb24' + >>> format = VideoFormat('rgb24') + >>> format.name + 'rgb24' """ @@ -33,23 +41,22 @@ def __cinit__(self, name, width=0, height=0): if name is _cinit_bypass_sentinel: return - cdef VideoFormat other if isinstance(name, VideoFormat): - other = name + other: VideoFormat = cython.cast(VideoFormat, name) self._init(other.pix_fmt, width or other.width, height or other.height) return - cdef lib.AVPixelFormat pix_fmt = get_pix_fmt(name) + pix_fmt: lib.AVPixelFormat = get_pix_fmt(name) self._init(pix_fmt, width, height) - cdef _init(self, lib.AVPixelFormat pix_fmt, unsigned int width, unsigned int height): + @cython.cfunc + def _init(self, pix_fmt: lib.AVPixelFormat, width: cuint, height: cuint): self.pix_fmt = pix_fmt self.ptr = lib.av_pix_fmt_desc_get(pix_fmt) self.width = width self.height = height self.components = tuple( - VideoFormatComponent(self, i) - for i in range(self.ptr.nb_components) + VideoFormatComponent(self, i) for i in range(self.ptr.nb_components) ) def __repr__(self): @@ -64,54 +71,48 @@ def __int__(self): @property def name(self): """Canonical name of the pixel format.""" - return self.ptr.name + return cython.cast(str, self.ptr.name) @property def bits_per_pixel(self): return lib.av_get_bits_per_pixel(self.ptr) @property - def padded_bits_per_pixel(self): return lib.av_get_padded_bits_per_pixel(self.ptr) + def padded_bits_per_pixel(self): + return lib.av_get_padded_bits_per_pixel(self.ptr) @property def is_big_endian(self): """Pixel format is big-endian.""" return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_BE) - @property def has_palette(self): """Pixel format has a palette in data[1], values are indexes in this palette.""" return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_PAL) - @property def is_bit_stream(self): """All values of a component are bit-wise packed end to end.""" return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_BITSTREAM) - - # Skipping PIX_FMT_HWACCEL - # """Pixel format is an HW accelerated format.""" - @property def is_planar(self): """At least one pixel component is not in the first data plane.""" return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_PLANAR) - @property def is_rgb(self): """The pixel format contains RGB-like data (as opposed to YUV/grayscale).""" return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_RGB) - @property def is_bayer(self): """The pixel format contains Bayer data.""" return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_BAYER) - cpdef chroma_width(self, int luma_width=0): + @cython.ccall + def chroma_width(self, luma_width: cython.int = 0): """chroma_width(luma_width=0) Width of a chroma plane relative to a luma plane. @@ -122,7 +123,8 @@ def is_bayer(self): luma_width = luma_width or self.width return -((-luma_width) >> self.ptr.log2_chroma_w) if luma_width else 0 - cpdef chroma_height(self, int luma_height=0): + @cython.ccall + def chroma_height(self, luma_height: cython.int = 0): """chroma_height(luma_height=0) Height of a chroma plane relative to a luma plane. @@ -134,11 +136,12 @@ def is_bayer(self): return -((-luma_height) >> self.ptr.log2_chroma_h) if luma_height else 0 -cdef class VideoFormatComponent: - def __cinit__(self, VideoFormat format, size_t index): +@cython.cclass +class VideoFormatComponent: + def __cinit__(self, format: VideoFormat, index: cython.size_t): self.format = format self.index = index - self.ptr = &format.ptr.comp[index] + self.ptr = cython.address(format.ptr.comp[index]) @property def plane(self): @@ -153,22 +156,25 @@ def bits(self): @property def is_alpha(self): """Is this component an alpha channel?""" - return ((self.index == 1 and self.format.ptr.nb_components == 2) or - (self.index == 3 and self.format.ptr.nb_components == 4)) + return (self.index == 1 and self.format.ptr.nb_components == 2) or ( + self.index == 3 and self.format.ptr.nb_components == 4 + ) @property def is_luma(self): """Is this component a luma channel?""" return self.index == 0 and ( - self.format.ptr.nb_components == 1 or - self.format.ptr.nb_components == 2 or - not self.format.is_rgb + self.format.ptr.nb_components == 1 + or self.format.ptr.nb_components == 2 + or not self.format.is_rgb ) @property def is_chroma(self): """Is this component a chroma channel?""" - return (self.index == 1 or self.index == 2) and (self.format.ptr.log2_chroma_w or self.format.ptr.log2_chroma_h) + return (self.index == 1 or self.index == 2) and ( + self.format.ptr.log2_chroma_w or self.format.ptr.log2_chroma_h + ) @property def width(self): @@ -190,7 +196,7 @@ def height(self): names = set() -cdef const lib.AVPixFmtDescriptor *desc = NULL +desc = cython.declare(cython.pointer[lib.AVPixFmtDescriptor], cython.NULL) while True: desc = lib.av_pix_fmt_desc_next(desc) if not desc: From 7f521906daae0018340ce7fccec5078df01c2dcc Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 19 Jan 2026 01:43:46 -0500 Subject: [PATCH 621/846] Removed deprecated avformat_network_init func --- av/_core.pyx | 1 - include/libavformat/avformat.pxd | 1 - 2 files changed, 2 deletions(-) diff --git a/av/_core.pyx b/av/_core.pyx index edca772f5..b87ef8824 100644 --- a/av/_core.pyx +++ b/av/_core.pyx @@ -1,7 +1,6 @@ cimport libav as lib # Initialise libraries. -lib.avformat_network_init() lib.avdevice_register_all() # Exports. diff --git a/include/libavformat/avformat.pxd b/include/libavformat/avformat.pxd index 3816b46fa..3a2218f06 100644 --- a/include/libavformat/avformat.pxd +++ b/include/libavformat/avformat.pxd @@ -6,7 +6,6 @@ cdef extern from "libavformat/avformat.h" nogil: cdef int avformat_version() cdef char* avformat_configuration() cdef char* avformat_license() - cdef void avformat_network_init() cdef int64_t INT64_MIN From d3fbbd2ab737dcea6aa9d686b899ea2177fcc2b2 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 19 Jan 2026 01:47:43 -0500 Subject: [PATCH 622/846] Make av/core pure --- av/{_core.pyx => _core.py} | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) rename av/{_core.pyx => _core.py} (75%) diff --git a/av/_core.pyx b/av/_core.py similarity index 75% rename from av/_core.pyx rename to av/_core.py index b87ef8824..2384fa457 100644 --- a/av/_core.pyx +++ b/av/_core.py @@ -1,22 +1,23 @@ -cimport libav as lib +import cython +import cython.cimports.libav as lib -# Initialise libraries. lib.avdevice_register_all() # Exports. time_base = lib.AV_TIME_BASE -cdef decode_version(v): +@cython.cfunc +def decode_version(v): if v < 0: return (-1, -1, -1) - cdef int major = (v >> 16) & 0xff - cdef int minor = (v >> 8) & 0xff - cdef int micro = (v) & 0xff - + major: cython.int = (v >> 16) & 0xFF + minor: cython.int = (v >> 8) & 0xFF + micro: cython.int = (v) & 0xFF return (major, minor, micro) + # Return an informative version string. # This usually is the actual release version number or a git commit # description. This string has no fixed format and can change any time. It @@ -27,37 +28,37 @@ "libavutil": dict( version=decode_version(lib.avutil_version()), configuration=lib.avutil_configuration(), - license=lib.avutil_license() + license=lib.avutil_license(), ), "libavcodec": dict( version=decode_version(lib.avcodec_version()), configuration=lib.avcodec_configuration(), - license=lib.avcodec_license() + license=lib.avcodec_license(), ), "libavformat": dict( version=decode_version(lib.avformat_version()), configuration=lib.avformat_configuration(), - license=lib.avformat_license() + license=lib.avformat_license(), ), "libavdevice": dict( version=decode_version(lib.avdevice_version()), configuration=lib.avdevice_configuration(), - license=lib.avdevice_license() + license=lib.avdevice_license(), ), "libavfilter": dict( version=decode_version(lib.avfilter_version()), configuration=lib.avfilter_configuration(), - license=lib.avfilter_license() + license=lib.avfilter_license(), ), "libswscale": dict( version=decode_version(lib.swscale_version()), configuration=lib.swscale_configuration(), - license=lib.swscale_license() + license=lib.swscale_license(), ), "libswresample": dict( version=decode_version(lib.swresample_version()), configuration=lib.swresample_configuration(), - license=lib.swresample_license() + license=lib.swresample_license(), ), } From f9f80e681c310c669915a0387ddc5d0673e041f3 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 19 Jan 2026 02:21:09 -0500 Subject: [PATCH 623/846] Make filter/filter pure --- av/filter/filter.pxd | 2 -- av/filter/{filter.pyx => filter.py} | 34 +++++++++++++++-------------- av/utils.py | 5 ----- 3 files changed, 18 insertions(+), 23 deletions(-) rename av/filter/{filter.pyx => filter.py} (70%) diff --git a/av/filter/filter.pxd b/av/filter/filter.pxd index 27501ae57..0a7cd41a0 100644 --- a/av/filter/filter.pxd +++ b/av/filter/filter.pxd @@ -4,9 +4,7 @@ from av.descriptor cimport Descriptor cdef class Filter: - cdef const lib.AVFilter *ptr - cdef object _inputs cdef object _outputs cdef Descriptor _descriptor diff --git a/av/filter/filter.pyx b/av/filter/filter.py similarity index 70% rename from av/filter/filter.pyx rename to av/filter/filter.py index b6e7005f0..b348dd3e0 100644 --- a/av/filter/filter.pyx +++ b/av/filter/filter.py @@ -1,24 +1,26 @@ -cimport libav as lib +import cython +from cython.cimports import libav as lib +from cython.cimports.av.descriptor import wrap_avclass +from cython.cimports.av.filter.link import alloc_filter_pads -from av.descriptor cimport wrap_avclass -from av.filter.link cimport alloc_filter_pads +_cinit_sentinel = cython.declare(object, object()) -cdef object _cinit_sentinel = object() - - -cdef Filter wrap_filter(const lib.AVFilter *ptr): - cdef Filter filter_ = Filter(_cinit_sentinel) +@cython.cfunc +def wrap_filter(ptr: cython.pointer[cython.const[lib.AVFilter]]) -> Filter: + filter_: Filter = Filter(_cinit_sentinel) filter_.ptr = ptr return filter_ -cdef class Filter: +@cython.cclass +class Filter: def __cinit__(self, name): if name is _cinit_sentinel: return if not isinstance(name, str): raise TypeError("takes a filter name as a string") + self.ptr = lib.avfilter_get_by_name(name) if not self.ptr: raise ValueError(f"no filter {name}") @@ -60,19 +62,19 @@ def outputs(self): return self._outputs -cdef get_filter_names(): - names = set() - cdef const lib.AVFilter *ptr - cdef void *opaque = NULL +@cython.cfunc +def get_filter_names() -> set: + names: set = set() + ptr: cython.pointer[cython.const[lib.AVFilter]] + opaque: cython.p_void = cython.NULL while True: - ptr = lib.av_filter_iterate(&opaque) + ptr = lib.av_filter_iterate(cython.address(opaque)) if ptr: names.add(ptr.name) else: break return names -filters_available = get_filter_names() - +filters_available = get_filter_names() filter_descriptor = wrap_avclass(lib.avfilter_get_class()) diff --git a/av/utils.py b/av/utils.py index 3516be0e3..bdf11bbb5 100644 --- a/av/utils.py +++ b/av/utils.py @@ -14,11 +14,6 @@ def _decode(s: cython.pointer[cython.char], encoding, errors) -> str: return cython.cast(bytes, s).decode(encoding, errors) -@cython.cfunc -def _encode(s, encoding, errors) -> bytes: - return s.encode(encoding, errors) - - @cython.cfunc def avdict_to_dict( input: cython.pointer[lib.AVDictionary], encoding: str, errors: str From 26a489c520a6b453aa8f285d52e673fecbca9d0e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 19 Jan 2026 02:53:38 -0500 Subject: [PATCH 624/846] Make 'streams' tuple on demand --- av/container/streams.pxd | 15 +--- av/container/{streams.pyx => streams.py} | 100 +++++++++++++---------- 2 files changed, 60 insertions(+), 55 deletions(-) rename av/container/{streams.pyx => streams.py} (66%) diff --git a/av/container/streams.pxd b/av/container/streams.pxd index 097176e10..d62174992 100644 --- a/av/container/streams.pxd +++ b/av/container/streams.pxd @@ -2,20 +2,7 @@ cimport libav as lib from av.stream cimport Stream -from .core cimport Container - cdef class StreamContainer: cdef list _streams - - # For the different types. - cdef readonly tuple video - cdef readonly tuple audio - cdef readonly tuple subtitles - cdef readonly tuple attachments - cdef readonly tuple data - cdef readonly tuple other - - cdef add_stream(self, Stream stream) - cdef int _get_best_stream_index(self, Container container, lib.AVMediaType type_enum, Stream related) noexcept - + cdef void add_stream(self, Stream stream) diff --git a/av/container/streams.pyx b/av/container/streams.py similarity index 66% rename from av/container/streams.pyx rename to av/container/streams.py index 17e4992d8..027c0016b 100644 --- a/av/container/streams.pyx +++ b/av/container/streams.py @@ -1,4 +1,6 @@ -cimport libav as lib +import cython +import cython.cimports.libav as lib +from cython.cimports.av.stream import Stream def _flatten(input_): @@ -9,7 +11,9 @@ def _flatten(input_): else: yield x -cdef lib.AVMediaType _get_media_type_enum(str type): + +@cython.cfunc +def _get_media_type_enum(type: str) -> lib.AVMediaType: if type == "video": return lib.AVMEDIA_TYPE_VIDEO elif type == "audio": @@ -23,7 +27,28 @@ def _flatten(input_): else: raise ValueError(f"Invalid stream type: {type}") -cdef class StreamContainer: + +@cython.cfunc +@cython.exceptval(check=False) +def _get_best_stream_index( + fmtctx: cython.pointer[lib.AVFormatContext], + enumtype: lib.AVMediaType, + related: Stream | None, +) -> cython.int: + stream_index: cython.int + + if related is None: + stream_index = lib.av_find_best_stream(fmtctx, enumtype, -1, -1, cython.NULL, 0) + else: + stream_index = lib.av_find_best_stream( + fmtctx, enumtype, -1, related.ptr.index, cython.NULL, 0 + ) + + return stream_index + + +@cython.cclass +class StreamContainer: """ A tuple-like container of :class:`Stream`. @@ -40,31 +65,12 @@ def _flatten(input_): def __cinit__(self): self._streams = [] - self.video = () - self.audio = () - self.subtitles = () - self.data = () - self.attachments = () - self.other = () - - cdef add_stream(self, Stream stream): + @cython.cfunc + def add_stream(self, stream: Stream) -> cython.void: assert stream.ptr.index == len(self._streams) self._streams.append(stream) - if stream.ptr.codecpar.codec_type == lib.AVMEDIA_TYPE_VIDEO: - self.video = self.video + (stream, ) - elif stream.ptr.codecpar.codec_type == lib.AVMEDIA_TYPE_AUDIO: - self.audio = self.audio + (stream, ) - elif stream.ptr.codecpar.codec_type == lib.AVMEDIA_TYPE_SUBTITLE: - self.subtitles = self.subtitles + (stream, ) - elif stream.ptr.codecpar.codec_type == lib.AVMEDIA_TYPE_ATTACHMENT: - self.attachments = self.attachments + (stream, ) - elif stream.ptr.codecpar.codec_type == lib.AVMEDIA_TYPE_DATA: - self.data = self.data + (stream, ) - else: - self.other = self.other + (stream, ) - # Basic tuple interface. def __len__(self): return len(self._streams) @@ -78,6 +84,26 @@ def __getitem__(self, index): else: return self.get(index) + @property + def video(self): + return tuple(s for s in self._streams if s.type == "video") + + @property + def audio(self): + return tuple(s for s in self._streams if s.type == "audio") + + @property + def subtitles(self): + return tuple(s for s in self._streams if s.type == "subtitle") + + @property + def data(self): + return tuple(s for s in self._streams if s.type == "data") + + @property + def attachments(self): + return tuple(s for s in self._streams if s.type == "attachment") + def get(self, *args, **kwargs): """get(streams=None, video=None, audio=None, subtitles=None, data=None) @@ -120,7 +146,9 @@ def get(self, *args, **kwargs): elif isinstance(x, dict): for type_, indices in x.items(): - if type_ == "streams": # For compatibility with the pseudo signature + if ( + type_ == "streams" + ): # For compatibility with the pseudo signature streams = self._streams else: streams = getattr(self, type_) @@ -134,17 +162,7 @@ def get(self, *args, **kwargs): return selection or self._streams[:] - cdef int _get_best_stream_index(self, Container container, lib.AVMediaType type_enum, Stream related) noexcept: - cdef int stream_index - - if related is None: - stream_index = lib.av_find_best_stream(container.ptr, type_enum, -1, -1, NULL, 0) - else: - stream_index = lib.av_find_best_stream(container.ptr, type_enum, -1, related.ptr.index, NULL, 0) - - return stream_index - - def best(self, str type, /, Stream related = None): + def best(self, type: str, /, related: Stream | None = None): """best(type: Literal["video", "audio", "subtitle", "attachment", "data"], /, related: Stream | None) Finds the "best" stream in the file. Wraps :ffmpeg:`av_find_best_stream`. Example:: @@ -155,14 +173,14 @@ def best(self, str type, /, Stream related = None): :return: The best stream of the specified type :rtype: Stream | None """ - cdef type_enum = _get_media_type_enum(type) - if len(self._streams) == 0: return None - cdef container = self._streams[0].container - - cdef int stream_index = self._get_best_stream_index(container, type_enum, related) + first_stream: Stream = cython.cast(Stream, self._streams[0]) + container: cython.pointer[lib.AVFormatContext] = first_stream.container.ptr + stream_index: cython.int = _get_best_stream_index( + container, _get_media_type_enum(type), related + ) if stream_index < 0: return None From db710df28617d17010da672ca5d121b4e1043a9a Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 19 Jan 2026 03:58:39 -0500 Subject: [PATCH 625/846] Simplify StreamContainer.get method --- av/container/streams.py | 36 ++++++++++++++---------------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/av/container/streams.py b/av/container/streams.py index 027c0016b..0045b5c85 100644 --- a/av/container/streams.py +++ b/av/container/streams.py @@ -3,15 +3,6 @@ from cython.cimports.av.stream import Stream -def _flatten(input_): - for x in input_: - if isinstance(x, (tuple, list)): - for y in _flatten(x): - yield y - else: - yield x - - @cython.cfunc def _get_media_type_enum(type: str) -> lib.AVMediaType: if type == "video": @@ -131,35 +122,36 @@ def get(self, *args, **kwargs): If nothing is selected, then all streams are returned. """ + selection: list = [] - selection = [] - - for x in _flatten((args, kwargs)): + def process(x): if x is None: pass - elif isinstance(x, Stream): selection.append(x) - elif isinstance(x, int): selection.append(self._streams[x]) - + elif isinstance(x, (tuple, list)): + for item in x: + process(item) elif isinstance(x, dict): for type_, indices in x.items(): - if ( - type_ == "streams" - ): # For compatibility with the pseudo signature - streams = self._streams - else: - streams = getattr(self, type_) + # For compatibility with the pseudo signature + streams = ( + self._streams if type_ == "streams" else getattr(self, type_) + ) if not isinstance(indices, (tuple, list)): indices = [indices] for i in indices: selection.append(streams[i]) - else: raise TypeError("Argument must be Stream or int.", type(x)) + for arg in args: + process(arg) + if kwargs: + process(kwargs) + return selection or self._streams[:] def best(self, type: str, /, related: Stream | None = None): From dc4d6b4864eee80328ab3a9031ab5b5a150c88f1 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 19 Jan 2026 04:28:28 -0500 Subject: [PATCH 626/846] time.monotonic() is always available --- av/container/core.pxd | 1 - av/container/core.pyi | 2 -- av/container/core.pyx | 12 +++--------- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/av/container/core.pxd b/av/container/core.pxd index 87bb792b3..71d18ae91 100644 --- a/av/container/core.pxd +++ b/av/container/core.pxd @@ -14,7 +14,6 @@ ctypedef struct timeout_info: cdef class Container: - cdef readonly bint writeable cdef lib.AVFormatContext *ptr diff --git a/av/container/core.pyi b/av/container/core.pyi index 26e76ed2c..2ca547028 100644 --- a/av/container/core.pyi +++ b/av/container/core.pyi @@ -100,8 +100,6 @@ class Container: exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: ... - def set_timeout(self, timeout: Real | None) -> None: ... - def start_timeout(self) -> None: ... def chapters(self) -> list[Chapter]: ... def set_chapters(self, chapters: list[Chapter]) -> None: ... diff --git a/av/container/core.pyx b/av/container/core.pyx index 299ac6ed3..53c873372 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -28,20 +28,14 @@ from av.logging import Capture as LogCapture cdef object _cinit_sentinel = object() - -# We want to use the monotonic clock if it is available. -cdef object clock = getattr(time, "monotonic", time.time) - -cdef int interrupt_cb (void *p) noexcept nogil: +cdef int interrupt_cb(void *p) noexcept nogil: cdef timeout_info info = dereference( p) if info.timeout < 0: # timeout < 0 means no timeout return 0 cdef double current_time with gil: - current_time = clock() - - # Check if the clock has been changed. + current_time = time.monotonic() if current_time < info.start_time: # Raise this when we get back to Python. stash_exception((RuntimeError, RuntimeError("Clock has been changed to before timeout start"), None)) @@ -330,7 +324,7 @@ cdef class Container: self.interrupt_callback_info.timeout = timeout cdef start_timeout(self): - self.interrupt_callback_info.start_time = clock() + self.interrupt_callback_info.start_time = time.monotonic() cdef _assert_open(self): if self.ptr == NULL: From e3a0cbbbfc0936007c6dd1995f61a40efb1c515a Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 19 Jan 2026 05:00:38 -0500 Subject: [PATCH 627/846] Make codec/codec pure --- av/codec/{codec.pyx => codec.py} | 119 ++++++++++++++++++------------- 1 file changed, 68 insertions(+), 51 deletions(-) rename av/codec/{codec.pyx => codec.py} (82%) diff --git a/av/codec/codec.pyx b/av/codec/codec.py similarity index 82% rename from av/codec/codec.pyx rename to av/codec/codec.py index 29040c64f..806190d13 100644 --- a/av/codec/codec.pyx +++ b/av/codec/codec.py @@ -1,24 +1,26 @@ -cimport libav as lib - -from av.audio.format cimport get_audio_format -from av.codec.hwaccel cimport wrap_hwconfig -from av.descriptor cimport wrap_avclass -from av.utils cimport avrational_to_fraction -from av.video.format cimport VideoFormat, get_pix_fmt, get_video_format - from enum import Flag, IntEnum -from libc.stdlib cimport free, malloc +import cython +from cython.cimports import libav as lib +from cython.cimports.av.audio.format import get_audio_format +from cython.cimports.av.codec.hwaccel import wrap_hwconfig +from cython.cimports.av.descriptor import wrap_avclass +from cython.cimports.av.utils import avrational_to_fraction +from cython.cimports.av.video.format import VideoFormat, get_pix_fmt, get_video_format +from cython.cimports.libc.stdlib import free, malloc + +_cinit_sentinel = cython.declare(object, object()) -cdef object _cinit_sentinel = object() -cdef Codec wrap_codec(const lib.AVCodec *ptr): - cdef Codec codec = Codec(_cinit_sentinel) +@cython.cfunc +def wrap_codec(ptr: cython.pointer[cython.const[lib.AVCodec]]) -> Codec: + codec: Codec = Codec(_cinit_sentinel) codec.ptr = ptr codec.is_encoder = lib.av_codec_is_encoder(ptr) codec._init() return codec + class Properties(Flag): NONE = 0 INTRA_ONLY = lib.AV_CODEC_PROP_INTRA_ONLY @@ -57,7 +59,8 @@ class UnknownCodecError(ValueError): pass -cdef class Codec: +@cython.cclass +class Codec: """Codec(name, mode='r') :param str name: The codec name. @@ -105,7 +108,8 @@ def __cinit__(self, name, mode="r"): if (mode == "w") != self.is_encoder: raise RuntimeError("Found codec does not match mode.", name, mode) - cdef _init(self, name=None): + @cython.cfunc + def _init(self, name=None): if not self.ptr: raise UnknownCodecError(name) @@ -124,12 +128,13 @@ def __repr__(self): mode = self.mode return f"" - def create(self, kind = None): + def create(self, kind=None): """Create a :class:`.CodecContext` for this codec. :param str kind: Gives a hint to static type checkers for what exact CodecContext is used. """ from .context import CodecContext + return CodecContext.create(self) @property @@ -141,10 +146,12 @@ def is_decoder(self): return not self.is_encoder @property - def descriptor(self): return wrap_avclass(self.ptr.priv_class) + def descriptor(self): + return wrap_avclass(self.ptr.priv_class) @property - def name(self): return self.ptr.name or "" + def name(self): + return self.ptr.name or "" @property def canonical_name(self): @@ -154,7 +161,8 @@ def canonical_name(self): return lib.avcodec_get_name(self.ptr.id) @property - def long_name(self): return self.ptr.long_name or "" + def long_name(self): + return self.ptr.long_name or "" @property def type(self): @@ -167,7 +175,8 @@ def type(self): return lib.av_get_media_type_string(self.ptr.type) @property - def id(self): return self.ptr.id + def id(self): + return self.ptr.id @property def frame_rates(self): @@ -175,10 +184,12 @@ def frame_rates(self): if not self.ptr.supported_framerates: return - ret = [] - cdef int i = 0 + ret: list = [] + i: cython.int = 0 while self.ptr.supported_framerates[i].denum: - ret.append(avrational_to_fraction(&self.ptr.supported_framerates[i])) + ret.append( + avrational_to_fraction(cython.address(self.ptr.supported_framerates[i])) + ) i += 1 return ret @@ -188,8 +199,8 @@ def audio_rates(self): if not self.ptr.supported_samplerates: return - ret = [] - cdef int i = 0 + ret: list = [] + i: cython.int = 0 while self.ptr.supported_samplerates[i]: ret.append(self.ptr.supported_samplerates[i]) i += 1 @@ -201,8 +212,8 @@ def video_formats(self): if not self.ptr.pix_fmts: return - ret = [] - cdef int i = 0 + ret: list = [] + i: cython.int = 0 while self.ptr.pix_fmts[i] != -1: ret.append(get_video_format(self.ptr.pix_fmts[i], 0, 0)) i += 1 @@ -214,8 +225,8 @@ def audio_formats(self): if not self.ptr.sample_fmts: return - ret = [] - cdef int i = 0 + ret: list = [] + i: cython.int = 0 while self.ptr.sample_fmts[i] != -1: ret.append(get_audio_format(self.ptr.sample_fmts[i])) i += 1 @@ -225,9 +236,9 @@ def audio_formats(self): def hardware_configs(self): if self._hardware_configs: return self._hardware_configs - ret = [] - cdef int i = 0 - cdef const lib.AVCodecHWConfig *ptr + ret: list = [] + i: cython.int = 0 + ptr: cython.pointer[cython.const[lib.AVCodecHWConfig]] while True: ptr = lib.avcodec_get_hw_config(self.ptr, i) if not ptr: @@ -309,12 +320,14 @@ def delay(self): """ return bool(self.ptr.capabilities & lib.AV_CODEC_CAP_DELAY) -cdef get_codec_names(): - names = set() - cdef const lib.AVCodec *ptr - cdef void *opaque = NULL + +@cython.cfunc +def get_codec_names(): + names: cython.set = set() + ptr = cython.declare(cython.pointer[cython.const[lib.AVCodec]]) + opaque: cython.p_void = cython.NULL while True: - ptr = lib.av_codec_iterate(&opaque) + ptr = lib.av_codec_iterate(cython.address(opaque)) if ptr: names.add(ptr.name) else: @@ -373,6 +386,7 @@ def dump_codecs(): except Exception as e: print(f"...... {codec.name:<18} ERROR: {e}") + def dump_hwconfigs(): print("Hardware configs:") for name in sorted(codecs_available): @@ -402,13 +416,13 @@ def find_best_pix_fmt_of_list(pix_fmts, src_pix_fmt, has_alpha=False): :return: (best_format, loss) :rtype: (VideoFormat | None, int) """ - cdef lib.AVPixelFormat src - cdef lib.AVPixelFormat best - cdef lib.AVPixelFormat *c_list = NULL - cdef Py_ssize_t n - cdef Py_ssize_t i - cdef object item - cdef int c_loss + src: lib.AVPixelFormat + best: lib.AVPixelFormat + c_list: cython.pointer[lib.AVPixelFormat] = cython.NULL + n: cython.Py_ssize_t + i: cython.Py_ssize_t + item: object + c_loss: cython.int if pix_fmts is None: raise TypeError("pix_fmts must not be None") @@ -418,29 +432,32 @@ def find_best_pix_fmt_of_list(pix_fmts, src_pix_fmt, has_alpha=False): return None, 0 if isinstance(src_pix_fmt, VideoFormat): - src = (src_pix_fmt).pix_fmt + src = cython.cast(VideoFormat, src_pix_fmt).pix_fmt else: - src = get_pix_fmt(src_pix_fmt) + src = get_pix_fmt(cython.cast(str, src_pix_fmt)) n = len(pix_fmts) - c_list = malloc((n + 1) * sizeof(lib.AVPixelFormat)) - if c_list == NULL: + c_list = cython.cast( + cython.pointer[lib.AVPixelFormat], + malloc((n + 1) * cython.sizeof(lib.AVPixelFormat)), + ) + if c_list == cython.NULL: raise MemoryError() try: for i in range(n): item = pix_fmts[i] if isinstance(item, VideoFormat): - c_list[i] = (item).pix_fmt + c_list[i] = cython.cast(VideoFormat, item).pix_fmt else: - c_list[i] = get_pix_fmt(item) + c_list[i] = get_pix_fmt(cython.cast(str, item)) c_list[n] = lib.AV_PIX_FMT_NONE c_loss = 0 best = lib.avcodec_find_best_pix_fmt_of_list( - c_list, src, 1 if has_alpha else 0, &c_loss + c_list, src, 1 if has_alpha else 0, cython.address(c_loss) ) return get_video_format(best, 0, 0), c_loss finally: - if c_list != NULL: + if c_list != cython.NULL: free(c_list) From dc51a11619aea2ec97d85548da0beeca6efa873a Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 19 Jan 2026 05:11:48 -0500 Subject: [PATCH 628/846] Make audio/fifo pure --- av/audio/{fifo.pyx => fifo.py} | 98 +++++++++++++++++++--------------- 1 file changed, 55 insertions(+), 43 deletions(-) rename av/audio/{fifo.pyx => fifo.py} (68%) diff --git a/av/audio/fifo.pyx b/av/audio/fifo.py similarity index 68% rename from av/audio/fifo.pyx rename to av/audio/fifo.py index 2ceb55f77..14673527e 100644 --- a/av/audio/fifo.pyx +++ b/av/audio/fifo.py @@ -1,8 +1,10 @@ -from av.audio.frame cimport alloc_audio_frame -from av.error cimport err_check +import cython +from cython.cimports.av.audio.frame import alloc_audio_frame +from cython.cimports.av.error import err_check -cdef class AudioFifo: +@cython.cclass +class AudioFifo: """A simple audio sample FIFO (First In First Out) buffer.""" def __repr__(self): @@ -22,7 +24,8 @@ def __dealloc__(self): if self.ptr: lib.av_audio_fifo_free(self.ptr) - cpdef write(self, AudioFrame frame): + @cython.ccall + def write(self, frame: AudioFrame | None): """write(frame) Push a frame of samples into the queue. @@ -45,7 +48,6 @@ def __dealloc__(self): return if not self.ptr: - # Hold onto a copy of the attributes of the first frame to populate # output frames with. self.template = alloc_audio_frame() @@ -60,9 +62,10 @@ def __dealloc__(self): self.pts_per_sample = 0 self.ptr = lib.av_audio_fifo_alloc( - frame.ptr.format, + cython.cast(lib.AVSampleFormat, frame.ptr.format), frame.layout.nb_channels, - frame.ptr.nb_samples * 2, # Just a default number of samples; it will adjust. + frame.ptr.nb_samples + * 2, # Just a default number of samples; it will adjust. ) if not self.ptr: @@ -70,34 +73,43 @@ def __dealloc__(self): # Make sure nothing changed. elif ( - frame.ptr.format != self.template.ptr.format or - # TODO: frame.ptr.ch_layout != self.template.ptr.ch_layout or - frame.ptr.sample_rate != self.template.ptr.sample_rate or - (frame._time_base.num and self.template._time_base.num and ( - frame._time_base.num != self.template._time_base.num or - frame._time_base.den != self.template._time_base.den - )) + frame.ptr.format != self.template.ptr.format + or frame.ptr.sample_rate != self.template.ptr.sample_rate + or ( + frame._time_base.num + and self.template._time_base.num + and ( + frame._time_base.num != self.template._time_base.num + or frame._time_base.den != self.template._time_base.den + ) + ) ): raise ValueError("Frame does not match AudioFifo parameters.") # Assert that the PTS are what we expect. - cdef int64_t expected_pts + expected_pts = cython.declare(int64_t) if self.pts_per_sample and frame.ptr.pts != lib.AV_NOPTS_VALUE: - expected_pts = (self.pts_per_sample * self.samples_written) + expected_pts = cython.cast( + int64_t, self.pts_per_sample * self.samples_written + ) if frame.ptr.pts != expected_pts: raise ValueError( - "Frame.pts (%d) != expected (%d); fix or set to None." % (frame.ptr.pts, expected_pts) + "Frame.pts (%d) != expected (%d); fix or set to None." + % (frame.ptr.pts, expected_pts) ) - err_check(lib.av_audio_fifo_write( - self.ptr, - frame.ptr.extended_data, - frame.ptr.nb_samples, - )) + err_check( + lib.av_audio_fifo_write( + self.ptr, + cython.cast(cython.pointer[cython.p_void], frame.ptr.extended_data), + frame.ptr.nb_samples, + ) + ) self.samples_written += frame.ptr.nb_samples - cpdef read(self, int samples=0, bint partial=False): + @cython.ccall + def read(self, samples: cython.int = 0, partial: cython.bint = False): """read(samples=0, partial=False) Read samples from the queue. @@ -115,7 +127,7 @@ def __dealloc__(self): if not self.ptr: return - cdef int buffered_samples = lib.av_audio_fifo_size(self.ptr) + buffered_samples: cython.int = lib.av_audio_fifo_size(self.ptr) if buffered_samples < 1: return @@ -127,31 +139,35 @@ def __dealloc__(self): else: return - cdef AudioFrame frame = alloc_audio_frame() + frame: AudioFrame = alloc_audio_frame() frame._copy_internal_attributes(self.template) frame._init( - self.template.ptr.format, - self.template.ptr.ch_layout, + cython.cast(lib.AVSampleFormat, self.template.ptr.format), + cython.cast(lib.AVChannelLayout, self.template.ptr.ch_layout), samples, 1, # Align? ) - err_check(lib.av_audio_fifo_read( - self.ptr, - frame.ptr.extended_data, - samples, - )) + err_check( + lib.av_audio_fifo_read( + self.ptr, + cython.cast(cython.pointer[cython.p_void], frame.ptr.extended_data), + samples, + ) + ) if self.pts_per_sample: - frame.ptr.pts = (self.pts_per_sample * self.samples_read) + frame.ptr.pts = cython.cast( + uint64_t, self.pts_per_sample * self.samples_read + ) else: frame.ptr.pts = lib.AV_NOPTS_VALUE self.samples_read += samples - return frame - cpdef read_many(self, int samples, bint partial=False): + @cython.ccall + def read_many(self, samples: cython.int, partial: cython.bint = False): """read_many(samples, partial=False) Read as many frames as we can. @@ -162,8 +178,8 @@ def __dealloc__(self): """ - cdef AudioFrame frame - frames = [] + frame: AudioFrame + frames: list = [] while True: frame = self.read(samples, partial=partial) if frame is not None: @@ -176,19 +192,15 @@ def __dealloc__(self): @property def format(self): """The :class:`.AudioFormat` of this FIFO.""" - if not self.ptr: - raise AttributeError(f"'{__name__}.AudioFifo' object has no attribute 'format'") return self.template.format + @property def layout(self): """The :class:`.AudioLayout` of this FIFO.""" - if not self.ptr: - raise AttributeError(f"'{__name__}.AudioFifo' object has no attribute 'layout'") return self.template.layout + @property def sample_rate(self): - if not self.ptr: - raise AttributeError(f"'{__name__}.AudioFifo' object has no attribute 'sample_rate'") return self.template.sample_rate @property From 4f5480e8704bba5c629a0a505dac6779f3b48c3a Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 19 Jan 2026 05:18:34 -0500 Subject: [PATCH 629/846] Make audio/layout pure --- av/audio/layout.pxd | 1 - av/audio/layout.py | 101 ++++++++++++++++++++++++++++++++++++++++++++ av/audio/layout.pyx | 82 ----------------------------------- av/dictionary.py | 2 +- 4 files changed, 102 insertions(+), 84 deletions(-) create mode 100644 av/audio/layout.py delete mode 100644 av/audio/layout.pyx diff --git a/av/audio/layout.pxd b/av/audio/layout.pxd index c7a2368f1..d7e991e95 100644 --- a/av/audio/layout.pxd +++ b/av/audio/layout.pxd @@ -3,6 +3,5 @@ cimport libav as lib cdef class AudioLayout: cdef lib.AVChannelLayout layout - cdef _init(self, lib.AVChannelLayout layout) cdef AudioLayout get_audio_layout(lib.AVChannelLayout c_layout) diff --git a/av/audio/layout.py b/av/audio/layout.py new file mode 100644 index 000000000..43bedb9c7 --- /dev/null +++ b/av/audio/layout.py @@ -0,0 +1,101 @@ +from dataclasses import dataclass + +import cython +from cython.cimports import libav as lib +from cython.cimports.cpython.bytes import PyBytes_FromStringAndSize + + +@dataclass +class AudioChannel: + name: str + description: str + + def __repr__(self): + return f"" + + +_cinit_bypass_sentinel = cython.declare(object, object()) + + +@cython.cfunc +def get_audio_layout(c_layout: lib.AVChannelLayout) -> AudioLayout: + """Get an AudioLayout from Cython land.""" + layout: AudioLayout = AudioLayout(_cinit_bypass_sentinel) + layout.layout = c_layout + return layout + + +@cython.cclass +class AudioLayout: + def __cinit__(self, layout): + if layout is _cinit_bypass_sentinel: + return + + if type(layout) is str: + ret = lib.av_channel_layout_from_string(cython.address(c_layout), layout) + if ret != 0: + raise ValueError(f"Invalid layout: {layout}") + elif isinstance(layout, AudioLayout): + c_layout = cython.cast(AudioLayout, layout).layout + else: + raise TypeError( + f"layout must be of type: string | av.AudioLayout, got {type(layout)}" + ) + + self.layout = c_layout + + def __repr__(self): + return f"" + + def __eq__(self, other): + return ( + isinstance(other, AudioLayout) + and self.name == other.name + and self.nb_channels == other.nb_channels + ) + + @property + def nb_channels(self): + return self.layout.nb_channels + + @property + def channels(self): + buf: cython.char[16] + buf2: cython.char[128] + + results: list = [] + for index in range(self.layout.nb_channels): + size = lib.av_channel_name( + buf, + cython.sizeof(buf), + lib.av_channel_layout_channel_from_index( + cython.address(self.layout), index + ), + ) + size2 = lib.av_channel_description( + buf2, + cython.sizeof(buf2), + lib.av_channel_layout_channel_from_index( + cython.address(self.layout), index + ), + ) + results.append( + AudioChannel( + PyBytes_FromStringAndSize(buf, size - 1).decode("utf-8"), + PyBytes_FromStringAndSize(buf2, size2 - 1).decode("utf-8"), + ) + ) + + return tuple(results) + + @property + def name(self) -> str: + """The canonical name of the audio layout.""" + layout_name: cython.char[129] + ret: cython.int = lib.av_channel_layout_describe( + cython.address(self.layout), layout_name, cython.sizeof(layout_name) + ) + if ret < 0: + raise RuntimeError(f"Failed to get layout name: {ret}") + + return layout_name diff --git a/av/audio/layout.pyx b/av/audio/layout.pyx deleted file mode 100644 index 0936075ef..000000000 --- a/av/audio/layout.pyx +++ /dev/null @@ -1,82 +0,0 @@ -cimport libav as lib -from cpython.bytes cimport PyBytes_FromStringAndSize - -from dataclasses import dataclass - - -@dataclass -class AudioChannel: - name: str - description: str - - def __repr__(self): - return f"" - -cdef object _cinit_bypass_sentinel - -cdef AudioLayout get_audio_layout(lib.AVChannelLayout c_layout): - """Get an AudioLayout from Cython land.""" - cdef AudioLayout layout = AudioLayout.__new__(AudioLayout, _cinit_bypass_sentinel) - layout._init(c_layout) - return layout - - -cdef class AudioLayout: - def __init__(self, layout): - if layout is _cinit_bypass_sentinel: - return - - if type(layout) is str: - ret = lib.av_channel_layout_from_string(&c_layout, layout) - if ret != 0: - raise ValueError(f"Invalid layout: {layout}") - elif isinstance(layout, AudioLayout): - c_layout = (layout).layout - else: - raise TypeError(f"layout must be of type: string | av.AudioLayout, got {type(layout)}") - - self._init(c_layout) - - cdef _init(self, lib.AVChannelLayout layout): - self.layout = layout - - def __repr__(self): - return f"" - - def __eq__(self, other): - return isinstance(other, AudioLayout) and self.name == other.name and self.nb_channels == other.nb_channels - - @property - def nb_channels(self): - return self.layout.nb_channels - - @property - def channels(self): - cdef char buf[16] - cdef char buf2[128] - - results = [] - - for index in range(self.layout.nb_channels): - size = lib.av_channel_name(buf, sizeof(buf), lib.av_channel_layout_channel_from_index(&self.layout, index)) - 1 - size2 = lib.av_channel_description(buf2, sizeof(buf2), lib.av_channel_layout_channel_from_index(&self.layout, index)) - 1 - results.append( - AudioChannel( - PyBytes_FromStringAndSize(buf, size).decode("utf-8"), - PyBytes_FromStringAndSize(buf2, size2).decode("utf-8"), - ) - ) - - return tuple(results) - - @property - def name(self) -> str: - """The canonical name of the audio layout.""" - cdef char layout_name[128] - cdef int ret - - ret = lib.av_channel_layout_describe(&self.layout, layout_name, sizeof(layout_name)) - if ret < 0: - raise RuntimeError(f"Failed to get layout name: {ret}") - - return layout_name \ No newline at end of file diff --git a/av/dictionary.py b/av/dictionary.py index 2080266ed..2c08be63b 100644 --- a/av/dictionary.py +++ b/av/dictionary.py @@ -43,7 +43,7 @@ def __iter__(self): yield element.key def __repr__(self): - return f"bv.Dictionary({dict(self)!r})" + return f"av.Dictionary({dict(self)!r})" def copy(self): other = cython.declare(_Dictionary, Dictionary()) From f72c83c89cbca2af1fbbaa5387a42d0624dc2c21 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 21 Jan 2026 02:01:37 -0500 Subject: [PATCH 630/846] Make container/pyio pure --- av/container/{pyio.pyx => pyio.py} | 128 ++++++++++++++++++----------- 1 file changed, 81 insertions(+), 47 deletions(-) rename av/container/{pyio.pyx => pyio.py} (52%) diff --git a/av/container/pyio.pyx b/av/container/pyio.py similarity index 52% rename from av/container/pyio.pyx rename to av/container/pyio.py index c8b82f96a..ccacc01e8 100644 --- a/av/container/pyio.pyx +++ b/av/container/pyio.py @@ -1,16 +1,25 @@ -cimport libav as lib -from libc.string cimport memcpy +# type: ignore +import cython +from cython import NULL +from cython.cimports import libav as lib +from cython.cimports.av.error import stash_exception +from cython.cimports.libc.stdint import int64_t, uint8_t +from cython.cimports.libc.string import memcpy -from av.error cimport stash_exception +Buf = cython.typedef(cython.pointer[uint8_t]) +BufC = cython.typedef(cython.pointer[cython.const[uint8_t]]) -ctypedef int64_t (*seek_func_t)(void *opaque, int64_t offset, int whence) noexcept nogil +seek_func_t = cython.typedef( + "int64_t (*seek_func_t)(void *opaque, int64_t offset, int whence) noexcept nogil" +) -cdef class PyIOFile: +@cython.cclass +class PyIOFile: def __cinit__(self, file, buffer_size, writeable=None): self.file = file - cdef seek_func_t seek_func = NULL + seek_func: seek_func_t = NULL readable = getattr(self.file, "readable", None) writable = getattr(self.file, "writable", None) @@ -21,39 +30,43 @@ def __cinit__(self, file, buffer_size, writeable=None): self.ftell = getattr(self.file, "tell", None) self.fclose = getattr(self.file, "close", None) - # To be seekable the file object must have `seek` and `tell` methods. + # To be seekable, the file object must have `seek` and `tell` methods. # If it also has a `seekable` method, it must return True. if ( self.fseek is not None and self.ftell is not None and (seekable is None or seekable()) ): - seek_func = pyio_seek + seek_func: seek_func_t = pyio_seek if writeable is None: writeable = self.fwrite is not None if writeable: if self.fwrite is None or (writable is not None and not writable()): - raise ValueError("File object has no write() method, or writable() returned False.") + raise ValueError( + "File object has no write() method, or writable() returned False." + ) else: if self.fread is None or (readable is not None and not readable()): - raise ValueError("File object has no read() method, or readable() returned False.") + raise ValueError( + "File object has no read() method, or readable() returned False." + ) self.pos = 0 self.pos_is_valid = True # This is effectively the maximum size of reads. - self.buffer = lib.av_malloc(buffer_size) + self.buffer = cython.cast(cython.p_uchar, lib.av_malloc(buffer_size)) self.iocontext = lib.avio_alloc_context( self.buffer, buffer_size, writeable, - self, # User data. + cython.cast(cython.p_void, self), # User data. pyio_read, pyio_write, - seek_func + seek_func, ) if seek_func: @@ -61,30 +74,37 @@ def __cinit__(self, file, buffer_size, writeable=None): self.iocontext.max_packet_size = buffer_size def __dealloc__(self): - with nogil: + with cython.nogil: # FFmpeg will not release custom input, so it's up to us to free it. # Do not touch our original buffer as it may have been freed and replaced. if self.iocontext: - lib.av_freep(&self.iocontext.buffer) - lib.av_freep(&self.iocontext) + lib.av_freep(cython.address(self.iocontext.buffer)) + lib.av_freep(cython.address(self.iocontext)) - # We likely errored badly if we got here, and so are still - # responsible for our buffer. + # We likely errored badly if we got here, and so we are still responsible. else: - lib.av_freep(&self.buffer) + lib.av_freep(cython.address(self.buffer)) -cdef int pyio_read(void *opaque, uint8_t *buf, int buf_size) noexcept nogil: - with gil: +@cython.cfunc +@cython.nogil +@cython.exceptval(check=False) +def pyio_read(opaque: cython.p_void, buf: Buf, buf_size: cython.int) -> cython.int: + with cython.gil: return pyio_read_gil(opaque, buf, buf_size) -cdef int pyio_read_gil(void *opaque, uint8_t *buf, int buf_size) noexcept: - cdef PyIOFile self - cdef bytes res + +@cython.cfunc +@cython.exceptval(check=False) +def pyio_read_gil(opaque: cython.p_void, buf: Buf, buf_size: cython.int) -> cython.int: + self: PyIOFile + res: bytes try: - self = opaque + self = cython.cast(PyIOFile, opaque) res = self.fread(buf_size) - memcpy(buf, res, len(res)) + memcpy( + buf, cython.cast(cython.p_void, cython.cast(cython.p_char, res)), len(res) + ) self.pos += len(res) if not res: return lib.AVERROR_EOF @@ -93,16 +113,24 @@ def __dealloc__(self): return stash_exception() -cdef int pyio_write(void *opaque, const uint8_t *buf, int buf_size) noexcept nogil: - with gil: +@cython.cfunc +@cython.nogil +@cython.exceptval(check=False) +def pyio_write(opaque: cython.p_void, buf: BufC, buf_size: cython.int) -> cython.int: + with cython.gil: return pyio_write_gil(opaque, buf, buf_size) -cdef int pyio_write_gil(void *opaque, const uint8_t *buf, int buf_size) noexcept: - cdef PyIOFile self - cdef bytes bytes_to_write - cdef int bytes_written + +@cython.cfunc +@cython.exceptval(check=False) +def pyio_write_gil( + opaque: cython.p_void, buf: BufC, buf_size: cython.int +) -> cython.int: + self: PyIOFile + bytes_to_write: bytes + bytes_written: cython.int try: - self = opaque + self = cython.cast(PyIOFile, opaque) bytes_to_write = buf[:buf_size] ret_value = self.fwrite(bytes_to_write) bytes_written = ret_value if isinstance(ret_value, int) else buf_size @@ -112,19 +140,25 @@ def __dealloc__(self): return stash_exception() -cdef int64_t pyio_seek(void *opaque, int64_t offset, int whence) noexcept nogil: - # Seek takes the standard flags, but also a ad-hoc one which means that - # the library wants to know how large the file is. We are generally - # allowed to ignore this. +@cython.cfunc +@cython.nogil +@cython.exceptval(check=False) +def pyio_seek(opaque: cython.p_void, offset: int64_t, whence: cython.int) -> int64_t: + # Seek takes the standard flags, but also a ad-hoc one which means that the library + # wants to know how large the file is. We are generally allowed to ignore this. if whence == lib.AVSEEK_SIZE: return -1 - with gil: + with cython.gil: return pyio_seek_gil(opaque, offset, whence) -cdef int64_t pyio_seek_gil(void *opaque, int64_t offset, int whence): - cdef PyIOFile self + +@cython.cfunc +def pyio_seek_gil( + opaque: cython.p_void, offset: int64_t, whence: cython.int +) -> int64_t: + self: PyIOFile try: - self = opaque + self = cython.cast(PyIOFile, opaque) res = self.fseek(offset, whence) # Track the position for the user. @@ -144,18 +178,19 @@ def __dealloc__(self): return stash_exception() -cdef int pyio_close_gil(lib.AVIOContext *pb): +@cython.cfunc +def pyio_close_gil(pb: cython.pointer[lib.AVIOContext]) -> cython.int: try: return lib.avio_close(pb) - except Exception: stash_exception() -cdef int pyio_close_custom_gil(lib.AVIOContext *pb): - cdef PyIOFile self +@cython.cfunc +def pyio_close_custom_gil(pb: cython.pointer[lib.AVIOContext]) -> cython.int: + self: PyIOFile try: - self = pb.opaque + self = cython.cast(PyIOFile, pb.opaque) # Flush bytes in the AVIOContext buffers to the custom I/O lib.avio_flush(pb) @@ -164,6 +199,5 @@ def __dealloc__(self): self.fclose() return 0 - except Exception: stash_exception() From 6a966ec095b6dbe0b7901ac05ca5c01fddf61848 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 21 Jan 2026 02:15:35 -0500 Subject: [PATCH 631/846] Make codec/context pure --- av/codec/{context.pyx => context.py} | 254 ++++++++++++++++----------- 1 file changed, 153 insertions(+), 101 deletions(-) rename av/codec/{context.pyx => context.py} (76%) diff --git a/av/codec/context.pyx b/av/codec/context.py similarity index 76% rename from av/codec/context.pyx rename to av/codec/context.py index ac294ee10..0117d8034 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.py @@ -1,36 +1,42 @@ -cimport libav as lib -from libc.errno cimport EAGAIN -from libc.stdint cimport uint8_t -from libc.string cimport memcpy - -from av.bytesource cimport ByteSource, bytesource -from av.codec.codec cimport Codec, wrap_codec -from av.dictionary cimport _Dictionary -from av.error cimport err_check -from av.packet cimport Packet -from av.utils cimport avrational_to_fraction, to_avrational - from enum import Flag, IntEnum -from av.dictionary import Dictionary - +import cython +from cython.cimports import libav as lib +from cython.cimports.av.bytesource import ByteSource, bytesource +from cython.cimports.av.codec.codec import Codec, wrap_codec +from cython.cimports.av.dictionary import _Dictionary +from cython.cimports.av.error import err_check +from cython.cimports.av.packet import Packet +from cython.cimports.av.utils import avrational_to_fraction, to_avrational +from cython.cimports.libc.errno import EAGAIN +from cython.cimports.libc.stdint import uint8_t +from cython.cimports.libc.string import memcpy -cdef object _cinit_sentinel = object() +from av.dictionary import Dictionary +_cinit_sentinel = cython.declare(object, object()) -cdef CodecContext wrap_codec_context(lib.AVCodecContext *c_ctx, const lib.AVCodec *c_codec, HWAccel hwaccel): - """Build an av.CodecContext for an existing AVCodecContext.""" - cdef CodecContext py_ctx +@cython.cfunc +def wrap_codec_context( + c_ctx: cython.pointer[lib.AVCodecContext], + c_codec: cython.pointer[cython.const[lib.AVCodec]], + hwaccel: HWAccel, +) -> CodecContext: + """Build an bv.CodecContext for an existing AVCodecContext.""" + py_ctx: CodecContext if c_ctx.codec_type == lib.AVMEDIA_TYPE_VIDEO: from av.video.codeccontext import VideoCodecContext + py_ctx = VideoCodecContext(_cinit_sentinel) elif c_ctx.codec_type == lib.AVMEDIA_TYPE_AUDIO: from av.audio.codeccontext import AudioCodecContext + py_ctx = AudioCodecContext(_cinit_sentinel) elif c_ctx.codec_type == lib.AVMEDIA_TYPE_SUBTITLE: from av.subtitles.codeccontext import SubtitleCodecContext + py_ctx = SubtitleCodecContext(_cinit_sentinel) else: py_ctx = CodecContext(_cinit_sentinel) @@ -44,7 +50,10 @@ class ThreadType(Flag): NONE = 0 FRAME: "Decode more than one frame at once" = lib.FF_THREAD_FRAME SLICE: "Decode more than one part of a single frame at once" = lib.FF_THREAD_SLICE - AUTO: "Decode using both FRAME and SLICE methods." = lib.FF_THREAD_SLICE | lib.FF_THREAD_FRAME + AUTO: "Decode using both FRAME and SLICE methods." = ( + lib.FF_THREAD_SLICE | lib.FF_THREAD_FRAME + ) + class Flags(IntEnum): unaligned = lib.AV_CODEC_FLAG_UNALIGNED @@ -68,6 +77,7 @@ class Flags(IntEnum): interlaced_me = lib.AV_CODEC_FLAG_INTERLACED_ME closed_gop = lib.AV_CODEC_FLAG_CLOSED_GOP + class Flags2(IntEnum): fast = lib.AV_CODEC_FLAG2_FAST no_output = lib.AV_CODEC_FLAG2_NO_OUTPUT @@ -80,11 +90,14 @@ class Flags2(IntEnum): ro_flush_noop = lib.AV_CODEC_FLAG2_RO_FLUSH_NOOP -cdef class CodecContext: +@cython.cclass +class CodecContext: @staticmethod def create(codec, mode=None, hwaccel=None): - cdef Codec cy_codec = codec if isinstance(codec, Codec) else Codec(codec, mode) - cdef lib.AVCodecContext *c_ctx = lib.avcodec_alloc_context3(cy_codec.ptr) + cy_codec: Codec = codec if isinstance(codec, Codec) else Codec(codec, mode) + c_ctx: cython.pointer[lib.AVCodecContext] = lib.avcodec_alloc_context3( + cy_codec.ptr + ) return wrap_codec_context(c_ctx, cy_codec.ptr, hwaccel) def __cinit__(self, sentinel=None, *args, **kwargs): @@ -95,11 +108,17 @@ def __cinit__(self, sentinel=None, *args, **kwargs): self.stream_index = -1 # This is set by the container immediately. self.is_open = False - cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec, HWAccel hwaccel): + @cython.cfunc + def _init( + self, + ptr: cython.pointer[lib.AVCodecContext], + codec: cython.pointer[cython.const[lib.AVCodec]], + hwaccel: HWAccel, + ): self.ptr = ptr if self.ptr.codec and codec and self.ptr.codec != codec: raise RuntimeError("Wrapping CodecContext with mismatched codec.") - self.codec = wrap_codec(codec if codec != NULL else self.ptr.codec) + self.codec = wrap_codec(codec if codec != cython.NULL else self.ptr.codec) self.hwaccel = hwaccel # Set reasonable threading defaults. @@ -116,7 +135,7 @@ def flags(self): return self.ptr.flags @flags.setter - def flags(self, int value): + def flags(self, value: cython.int): self.ptr.flags = value @property @@ -156,25 +175,35 @@ def flags2(self): return self.ptr.flags2 @flags2.setter - def flags2(self, int value): + def flags2(self, value: cython.int): self.ptr.flags2 = value @property def extradata(self): - if self.ptr is NULL: + if self.ptr is cython.NULL: return None if self.ptr.extradata_size > 0: - return (self.ptr.extradata)[:self.ptr.extradata_size] + return cython.cast( + bytes, + cython.cast(cython.pointer[uint8_t], self.ptr.extradata)[ + : self.ptr.extradata_size + ], + ) return None @extradata.setter def extradata(self, data): if data is None: - lib.av_freep(&self.ptr.extradata) + lib.av_freep(cython.address(self.ptr.extradata)) self.ptr.extradata_size = 0 else: source = bytesource(data) - self.ptr.extradata = lib.av_realloc(self.ptr.extradata, source.length + lib.AV_INPUT_BUFFER_PADDING_SIZE) + self.ptr.extradata = cython.cast( + cython.pointer[uint8_t], + lib.av_realloc( + self.ptr.extradata, source.length + lib.AV_INPUT_BUFFER_PADDING_SIZE + ), + ) if not self.ptr.extradata: raise MemoryError("Cannot allocate extradata") memcpy(self.ptr.extradata, source.ptr, source.length) @@ -187,23 +216,24 @@ def extradata_size(self): @property def is_encoder(self): - if self.ptr is NULL: + if self.ptr is cython.NULL: return False return lib.av_codec_is_encoder(self.ptr.codec) @property def is_decoder(self): - if self.ptr is NULL: + if self.ptr is cython.NULL: return False return lib.av_codec_is_decoder(self.ptr.codec) - cpdef open(self, bint strict=True): + @cython.ccall + def open(self, strict: cython.bint = True): if self.is_open: if strict: raise ValueError("CodecContext is already open.") return - cdef _Dictionary options = Dictionary() + options: _Dictionary = Dictionary() options.update(self.options or {}) if not self.ptr.time_base.num and self.is_encoder: @@ -217,15 +247,18 @@ def is_decoder(self): self.ptr.time_base.num = 1 self.ptr.time_base.den = lib.AV_TIME_BASE - err_check(lib.avcodec_open2(self.ptr, self.codec.ptr, &options.ptr), "avcodec_open2(" + self.codec.name + ")") + err_check( + lib.avcodec_open2(self.ptr, self.codec.ptr, cython.address(options.ptr)), + f'avcodec_open2("{self.codec.name}", {self.options})', + ) self.is_open = True self.options = dict(options) def __dealloc__(self): if self.ptr and self.extradata_set: - lib.av_freep(&self.ptr.extradata) + lib.av_freep(cython.address(self.ptr.extradata)) if self.ptr: - lib.avcodec_free_context(&self.ptr) + lib.avcodec_free_context(cython.address(self.ptr)) if self.parser: lib.av_parser_close(self.parser) @@ -256,27 +289,29 @@ def parse(self, raw_input=None): if not self.parser: raise ValueError(f"No parser for {self.codec.name}") - cdef ByteSource source = bytesource(raw_input, allow_none=True) + source: ByteSource = bytesource(raw_input, allow_none=True) - cdef unsigned char *in_data = source.ptr if source is not None else NULL - cdef int in_size = source.length if source is not None else 0 + in_data: cython.p_uchar = source.ptr if source is not None else cython.NULL + in_size: cython.int = source.length if source is not None else 0 - cdef unsigned char *out_data - cdef int out_size - cdef int consumed - cdef Packet packet = None - - packets = [] + out_data: cython.p_uchar + out_size: cython.int + consumed: cython.int + packet: Packet = None + packets: list = [] while True: - with nogil: + with cython.nogil: consumed = lib.av_parser_parse2( self.parser, self.ptr, - &out_data, &out_size, - in_data, in_size, - lib.AV_NOPTS_VALUE, lib.AV_NOPTS_VALUE, - 0 + cython.address(out_data), + cython.address(out_size), + in_data, + in_size, + lib.AV_NOPTS_VALUE, + lib.AV_NOPTS_VALUE, + 0, ) err_check(consumed) @@ -317,12 +352,13 @@ def is_hwaccel(self): """ return self.hwaccel_ctx is not None - def _send_frame_and_recv(self, Frame frame): - cdef Packet packet - - cdef int res - with nogil: - res = lib.avcodec_send_frame(self.ptr, frame.ptr if frame is not None else NULL) + def _send_frame_and_recv(self, frame: Frame | None): + packet: Packet + res: cython.int + with cython.nogil: + res = lib.avcodec_send_frame( + self.ptr, frame.ptr if frame is not None else cython.NULL + ) err_check(res, "avcodec_send_frame()") packet = self._recv_packet() @@ -330,15 +366,17 @@ def _send_frame_and_recv(self, Frame frame): yield packet packet = self._recv_packet() - cdef _send_packet_and_recv(self, Packet packet): - cdef Frame frame - - cdef int res - with nogil: - res = lib.avcodec_send_packet(self.ptr, packet.ptr if packet is not None else NULL) + @cython.cfunc + def _send_packet_and_recv(self, packet: Packet | None): + frame: Frame + res: cython.int + with cython.nogil: + res = lib.avcodec_send_packet( + self.ptr, packet.ptr if packet is not None else cython.NULL + ) err_check(res, "avcodec_send_packet()") - out = [] + out: list = [] while True: frame = self._recv_frame() if frame: @@ -347,48 +385,56 @@ def _send_frame_and_recv(self, Frame frame): break return out - cdef _prepare_frames_for_encode(self, Frame frame): + @cython.cfunc + def _prepare_frames_for_encode(self, frame: Frame | None): return [frame] - cdef Frame _alloc_next_frame(self): + @cython.cfunc + def _alloc_next_frame(self) -> Frame: raise NotImplementedError("Base CodecContext cannot decode.") - cdef _recv_frame(self): + @cython.cfunc + def _recv_frame(self): if not self._next_frame: self._next_frame = self._alloc_next_frame() - cdef Frame frame = self._next_frame - cdef int res - with nogil: + frame: Frame = self._next_frame + res: cython.int + + with cython.nogil: res = lib.avcodec_receive_frame(self.ptr, frame.ptr) if res == -EAGAIN or res == lib.AVERROR_EOF: return - err_check(res, "avcodec_receive_frame()") + err_check(res, "avcodec_receive_frame()") frame = self._transfer_hwframe(frame) if not res: self._next_frame = None return frame - cdef _transfer_hwframe(self, Frame frame): + @cython.cfunc + def _transfer_hwframe(self, frame: Frame): return frame - cdef _recv_packet(self): - cdef Packet packet = Packet() + @cython.cfunc + def _recv_packet(self): + packet: Packet = Packet() + res: cython.int - cdef int res - with nogil: + with cython.nogil: res = lib.avcodec_receive_packet(self.ptr, packet.ptr) + if res == -EAGAIN or res == lib.AVERROR_EOF: return - err_check(res, "avcodec_receive_packet()") + err_check(res, "avcodec_receive_packet()") if not res: return packet - cdef _prepare_and_time_rebase_frames_for_encode(self, Frame frame): + @cython.cfunc + def _prepare_and_time_rebase_frames_for_encode(self, frame: Frame): if self.ptr.codec_type not in [lib.AVMEDIA_TYPE_VIDEO, lib.AVMEDIA_TYPE_AUDIO]: raise NotImplementedError("Encoding is only supported for audio and video.") @@ -404,7 +450,8 @@ def _send_frame_and_recv(self, Frame frame): return frames - cpdef encode(self, Frame frame=None): + @cython.ccall + def encode(self, frame: Frame | None = None): """Encode a list of :class:`.Packet` from the given :class:`.Frame`.""" res = [] for frame in self._prepare_and_time_rebase_frames_for_encode(frame): @@ -413,13 +460,14 @@ def _send_frame_and_recv(self, Frame frame): res.append(packet) return res - def encode_lazy(self, Frame frame=None): + def encode_lazy(self, frame: Frame | None = None): for frame in self._prepare_and_time_rebase_frames_for_encode(frame): for packet in self._send_frame_and_recv(frame): self._setup_encoded_packet(packet) yield packet - cdef _setup_encoded_packet(self, Packet packet): + @cython.cfunc + def _setup_encoded_packet(self, packet: Packet): # We coerced the frame's time_base into the CodecContext's during encoding, # and FFmpeg copied the frame's pts/dts to the packet, so keep track of # this time_base in case the frame needs to be muxed to a container with @@ -429,7 +477,8 @@ def encode_lazy(self, Frame frame=None): # are off! packet.ptr.time_base = self.ptr.time_base - cpdef decode(self, Packet packet=None): + @cython.ccall + def decode(self, packet: Packet | None = None): """Decode a list of :class:`.Frame` from the given :class:`.Packet`. If the packet is None, the buffers will be flushed. This is useful if @@ -437,20 +486,20 @@ def encode_lazy(self, Frame frame=None): (if they are encoded with a codec that has B-frames). """ - if not self.codec.ptr: raise ValueError("cannot decode unknown codec") self.open(strict=False) - res = [] + res: list = [] for frame in self._send_packet_and_recv(packet): if isinstance(frame, Frame): self._setup_decoded_frame(frame, packet) res.append(frame) return res - cpdef flush_buffers(self): + @cython.ccall + def flush_buffers(self): """Reset the internal codec state and discard all internal buffers. Should be called before you start decoding from a new position e.g. @@ -458,10 +507,11 @@ def encode_lazy(self, Frame frame=None): """ if self.is_open: - with nogil: + with cython.nogil: lib.avcodec_flush_buffers(self.ptr) - cdef _setup_decoded_frame(self, Frame frame, Packet packet): + @cython.cfunc + def _setup_decoded_frame(self, frame: Frame, packet: Packet | None): # Propagate our manual times. # While decoding, frame times are in stream time_base, which PyAV # is carrying around. @@ -485,15 +535,15 @@ def profiles(self): :type: list[str] """ - ret = [] + ret: list = [] if not self.ptr.codec or not self.codec.desc or not self.codec.desc.profiles: return ret # Profiles are always listed in the codec descriptor, but not necessarily in # the codec itself. So use the descriptor here. desc = self.codec.desc - cdef int i = 0 - while desc.profiles[i].profile != lib.AV_PROFILE_UNKNOWN: + i: cython.int = 0 + while desc.profiles[i].profile != lib.AV_PROFILE_UNKNOWN: ret.append(desc.profiles[i].name) i += 1 @@ -507,8 +557,8 @@ def profile(self): # Profiles are always listed in the codec descriptor, but not necessarily in # the codec itself. So use the descriptor here. desc = self.codec.desc - cdef int i = 0 - while desc.profiles[i].profile != lib.AV_PROFILE_UNKNOWN: + i: cython.int = 0 + while desc.profiles[i].profile != lib.AV_PROFILE_UNKNOWN: if desc.profiles[i].profile == self.ptr.profile: return desc.profiles[i].name i += 1 @@ -521,7 +571,7 @@ def profile(self, value): # Profiles are always listed in the codec descriptor, but not necessarily in # the codec itself. So use the descriptor here. desc = self.codec.desc - cdef int i = 0 + i: cython.int = 0 while desc.profiles[i].profile != lib.AV_PROFILE_UNKNOWN: if desc.profiles[i].name == value: self.ptr.profile = desc.profiles[i].profile @@ -532,24 +582,26 @@ def profile(self, value): def time_base(self): if self.is_decoder: raise RuntimeError("Cannot access 'time_base' as a decoder") - return avrational_to_fraction(&self.ptr.time_base) + return avrational_to_fraction(cython.address(self.ptr.time_base)) @time_base.setter def time_base(self, value): if self.is_decoder: raise RuntimeError("Cannot access 'time_base' as a decoder") - to_avrational(value, &self.ptr.time_base) + to_avrational(value, cython.address(self.ptr.time_base)) @property def codec_tag(self): return self.ptr.codec_tag.to_bytes(4, byteorder="little", signed=False).decode( - encoding="ascii") + encoding="ascii" + ) @codec_tag.setter def codec_tag(self, value): if isinstance(value, str) and len(value) == 4: - self.ptr.codec_tag = int.from_bytes(value.encode(encoding="ascii"), - byteorder="little", signed=False) + self.ptr.codec_tag = int.from_bytes( + value.encode(encoding="ascii"), byteorder="little", signed=False + ) else: raise ValueError("Codec tag should be a 4 character string.") @@ -558,7 +610,7 @@ def bit_rate(self): return self.ptr.bit_rate if self.ptr.bit_rate > 0 else None @bit_rate.setter - def bit_rate(self, int value): + def bit_rate(self, value: cython.int): self.ptr.bit_rate = value @property @@ -573,7 +625,7 @@ def bit_rate_tolerance(self): self.ptr.bit_rate_tolerance @bit_rate_tolerance.setter - def bit_rate_tolerance(self, int value): + def bit_rate_tolerance(self, value: cython.int): self.ptr.bit_rate_tolerance = value @property @@ -586,7 +638,7 @@ def thread_count(self): return self.ptr.thread_count @thread_count.setter - def thread_count(self, int value): + def thread_count(self, value: cython.int): if self.is_open: raise RuntimeError("Cannot change thread_count after codec is open.") self.ptr.thread_count = value From 0caf320e23b456f98727512f29b40c7d26336b39 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 21 Jan 2026 02:30:41 -0500 Subject: [PATCH 632/846] Make sidedata/sidedata pure --- av/sidedata/sidedata.pxd | 2 - av/sidedata/{sidedata.pyx => sidedata.py} | 56 +++++++++++++++-------- 2 files changed, 36 insertions(+), 22 deletions(-) rename av/sidedata/{sidedata.pyx => sidedata.py} (74%) diff --git a/av/sidedata/sidedata.pxd b/av/sidedata/sidedata.pxd index 5e6e5bf4c..37578a61d 100644 --- a/av/sidedata/sidedata.pxd +++ b/av/sidedata/sidedata.pxd @@ -1,4 +1,3 @@ - cimport libav as lib from av.buffer cimport Buffer @@ -18,6 +17,5 @@ cdef int get_display_rotation(Frame frame) cdef class _SideDataContainer: cdef Frame frame - cdef list _by_index cdef dict _by_type diff --git a/av/sidedata/sidedata.pyx b/av/sidedata/sidedata.py similarity index 74% rename from av/sidedata/sidedata.pyx rename to av/sidedata/sidedata.py index 96c3d72f5..24f58a3e0 100644 --- a/av/sidedata/sidedata.pyx +++ b/av/sidedata/sidedata.py @@ -1,13 +1,13 @@ -from libc.stdint cimport int32_t - from collections.abc import Mapping from enum import Enum +import cython +from cython.cimports.libc.stdint import int32_t + from av.sidedata.encparams import VideoEncParams from av.sidedata.motionvectors import MotionVectors - -cdef object _cinit_bypass_sentinel = object() +_cinit_bypass_sentinel = cython.declare(object, object()) class Type(Enum): @@ -17,6 +17,7 @@ class Type(Enum): From: https://github.com/FFmpeg/FFmpeg/blob/master/libavutil/frame.h """ + PANSCAN = lib.AV_FRAME_DATA_PANSCAN A53_CC = lib.AV_FRAME_DATA_A53_CC STEREO3D = lib.AV_FRAME_DATA_STEREO3D @@ -47,7 +48,8 @@ class Type(Enum): VIDEO_HINT = lib.AV_FRAME_DATA_VIDEO_HINT -cdef SideData wrap_side_data(Frame frame, int index): +@cython.cfunc +def wrap_side_data(frame: Frame, index: cython.int) -> SideData: if frame.ptr.side_data[index].type == lib.AV_FRAME_DATA_MOTION_VECTORS: return MotionVectors(_cinit_bypass_sentinel, frame, index) elif frame.ptr.side_data[index].type == lib.AV_FRAME_DATA_VIDEO_ENC_PARAMS: @@ -56,46 +58,60 @@ class Type(Enum): return SideData(_cinit_bypass_sentinel, frame, index) -cdef int get_display_rotation(Frame frame): +@cython.cfunc +def get_display_rotation(frame: Frame) -> cython.int: for i in range(frame.ptr.nb_side_data): if frame.ptr.side_data[i].type == lib.AV_FRAME_DATA_DISPLAYMATRIX: - return int(lib.av_display_rotation_get(frame.ptr.side_data[i].data)) + return int( + lib.av_display_rotation_get( + cython.cast( + cython.pointer[cython.const[int32_t]], + frame.ptr.side_data[i].data, + ) + ) + ) return 0 -cdef class SideData(Buffer): - def __init__(self, sentinel, Frame frame, int index): +@cython.cclass +class SideData(Buffer): + def __init__(self, sentinel, frame: Frame, index: cython.int): if sentinel is not _cinit_bypass_sentinel: - raise RuntimeError("cannot manually instantiate SideData") + raise RuntimeError("cannot manually instatiate SideData") self.frame = frame self.ptr = frame.ptr.side_data[index] self.metadata = wrap_dictionary(self.ptr.metadata) - cdef size_t _buffer_size(self): + @cython.cfunc + def _buffer_size(self) -> cython.size_t: return self.ptr.size - cdef void* _buffer_ptr(self): + @cython.cfunc + def _buffer_ptr(self) -> cython.p_void: return self.ptr.data - cdef bint _buffer_writable(self): + @cython.cfunc + def _buffer_writable(self) -> cython.bint: return False def __repr__(self): - return f"self.ptr.data:0x}>" + return f"" @property def type(self): return Type(self.ptr.type) -cdef class _SideDataContainer: - def __init__(self, Frame frame): +@cython.cclass +class _SideDataContainer: + def __init__(self, frame: Frame): self.frame = frame - self._by_index = [] - self._by_type = {} + self._by_index: list = [] + self._by_type: dict = {} + + i: cython.Py_ssize_t + data: SideData - cdef int i - cdef SideData data for i in range(self.frame.ptr.nb_side_data): data = wrap_side_data(frame, i) self._by_index.append(data) From 7fc0b943136ada169c51858e4f6aaf88271a3e3f Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 21 Jan 2026 02:57:52 -0500 Subject: [PATCH 633/846] Make sidedata/motionvectors pure --- av/sidedata/motionvectors.pxd | 8 +- av/sidedata/motionvectors.py | 135 ++++++++++++++++++++++++++++++++++ av/sidedata/motionvectors.pyi | 6 +- av/sidedata/motionvectors.pyx | 104 -------------------------- av/video/frame.py | 2 + 5 files changed, 142 insertions(+), 113 deletions(-) create mode 100644 av/sidedata/motionvectors.py delete mode 100644 av/sidedata/motionvectors.pyx diff --git a/av/sidedata/motionvectors.pxd b/av/sidedata/motionvectors.pxd index 993c5e283..078648325 100644 --- a/av/sidedata/motionvectors.pxd +++ b/av/sidedata/motionvectors.pxd @@ -4,13 +4,11 @@ from av.frame cimport Frame from av.sidedata.sidedata cimport SideData -cdef class _MotionVectors(SideData): - +cdef class MotionVectors(SideData): cdef dict _vectors - cdef int _len + cdef Py_ssize_t _len cdef class MotionVector: - - cdef _MotionVectors parent + cdef MotionVectors parent cdef lib.AVMotionVector *ptr diff --git a/av/sidedata/motionvectors.py b/av/sidedata/motionvectors.py new file mode 100644 index 000000000..820013e43 --- /dev/null +++ b/av/sidedata/motionvectors.py @@ -0,0 +1,135 @@ +from collections.abc import Sequence + +import cython +from cython.cimports import libav as lib +from cython.cimports.av.sidedata.sidedata import SideData + +_cinit_bypass_sentinel = cython.declare(object, object()) + + +@cython.cclass +class MotionVectors(SideData, Sequence): + def __init__(self, sentinel, frame: Frame, index: cython.int): + SideData.__init__(self, sentinel, frame, index) + self._vectors = {} + self._len = self.ptr.size // cython.sizeof(lib.AVMotionVector) + + def __repr__(self): + return ( + f"" + ) + + def __len__(self): + return self._len + + def __getitem__(self, index: cython.Py_ssize_t): + try: + return self._vectors[index] + except KeyError: + pass + + if index >= self._len: + raise IndexError(index) + + vector = self._vectors[index] = MotionVector( + _cinit_bypass_sentinel, self, index + ) + return vector + + def __iter__(self): + """Iterate over all motion vectors.""" + for i in range(self._len): + yield self[i] + + def to_ndarray(self): + """ + Convert motion vectors to a NumPy structured array. + + Returns a NumPy array with fields corresponding to the AVMotionVector structure. + """ + import numpy as np + + return np.frombuffer( + self, + dtype=np.dtype( + [ + ("source", "int32"), + ("w", "uint8"), + ("h", "uint8"), + ("src_x", "int16"), + ("src_y", "int16"), + ("dst_x", "int16"), + ("dst_y", "int16"), + ("flags", "uint64"), + ("motion_x", "int32"), + ("motion_y", "int32"), + ("motion_scale", "uint16"), + ], + align=True, + ), + ) + + +@cython.cclass +class MotionVector: + """ + Represents a single motion vector from video frame data. + + Motion vectors describe the motion of a block of pixels between frames. + """ + + def __init__(self, sentinel, parent: MotionVectors, index: cython.int): + if sentinel is not _cinit_bypass_sentinel: + raise RuntimeError("cannot manually instantiate MotionVector") + self.parent = parent + base: cython.pointer[lib.AVMotionVector] = cython.cast( + cython.pointer[lib.AVMotionVector], parent.ptr.data + ) + self.ptr = base + index + + def __repr__(self): + return ( + f"" + ) + + @property + def source(self): + return self.ptr.source + + @property + def w(self): + return self.ptr.w + + @property + def h(self): + return self.ptr.h + + @property + def src_x(self): + return self.ptr.src_x + + @property + def src_y(self): + return self.ptr.src_y + + @property + def dst_x(self): + return self.ptr.dst_x + + @property + def dst_y(self): + return self.ptr.dst_y + + @property + def motion_x(self): + return self.ptr.motion_x + + @property + def motion_y(self): + return self.ptr.motion_y + + @property + def motion_scale(self): + return self.ptr.motion_scale diff --git a/av/sidedata/motionvectors.pyi b/av/sidedata/motionvectors.pyi index eb514eb70..a1ab3c0fc 100644 --- a/av/sidedata/motionvectors.pyi +++ b/av/sidedata/motionvectors.pyi @@ -6,11 +6,9 @@ from .sidedata import SideData class MotionVectors(SideData, Sequence[MotionVector]): @overload - def __getitem__(self, index: int): ... + def __getitem__(self, index: int) -> MotionVector: ... @overload - def __getitem__(self, index: slice): ... - @overload - def __getitem__(self, index: int | slice): ... + def __getitem__(self, index: slice) -> list[MotionVector]: ... def __len__(self) -> int: ... def to_ndarray(self) -> np.ndarray[Any, Any]: ... diff --git a/av/sidedata/motionvectors.pyx b/av/sidedata/motionvectors.pyx deleted file mode 100644 index 8a4200232..000000000 --- a/av/sidedata/motionvectors.pyx +++ /dev/null @@ -1,104 +0,0 @@ -from collections.abc import Sequence - - -cdef object _cinit_bypass_sentinel = object() - - -# Cython doesn't let us inherit from the abstract Sequence, so we will subclass -# it later. -cdef class _MotionVectors(SideData): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._vectors = {} - self._len = self.ptr.size // sizeof(lib.AVMotionVector) - - def __repr__(self): - return f"self.ptr.data:0x}" - - def __getitem__(self, int index): - - try: - return self._vectors[index] - except KeyError: - pass - - if index >= self._len: - raise IndexError(index) - - vector = self._vectors[index] = MotionVector(_cinit_bypass_sentinel, self, index) - return vector - - def __len__(self): - return self._len - - def to_ndarray(self): - import numpy as np - return np.frombuffer(self, dtype=np.dtype([ - ("source", "int32"), - ("w", "uint8"), - ("h", "uint8"), - ("src_x", "int16"), - ("src_y", "int16"), - ("dst_x", "int16"), - ("dst_y", "int16"), - ("flags", "uint64"), - ("motion_x", "int32"), - ("motion_y", "int32"), - ("motion_scale", "uint16"), - ], align=True)) - - -class MotionVectors(_MotionVectors, Sequence): - pass - - -cdef class MotionVector: - def __init__(self, sentinel, _MotionVectors parent, int index): - if sentinel is not _cinit_bypass_sentinel: - raise RuntimeError("cannot manually instantiate MotionVector") - self.parent = parent - cdef lib.AVMotionVector *base = parent.ptr.data - self.ptr = base + index - - def __repr__(self): - return f"" - - @property - def source(self): - return self.ptr.source - - @property - def w(self): - return self.ptr.w - - @property - def h(self): - return self.ptr.h - - @property - def src_x(self): - return self.ptr.src_x - - @property - def src_y(self): - return self.ptr.src_y - - @property - def dst_x(self): - return self.ptr.dst_x - - @property - def dst_y(self): - return self.ptr.dst_y - - @property - def motion_x(self): - return self.ptr.motion_x - - @property - def motion_y(self): - return self.ptr.motion_y - - @property - def motion_scale(self): - return self.ptr.motion_scale diff --git a/av/video/frame.py b/av/video/frame.py index 174683727..1320afad3 100644 --- a/av/video/frame.py +++ b/av/video/frame.py @@ -158,7 +158,9 @@ def copy_bytes_to_plane( for row in range(start_row, end_row, step): i_pos = row * i_stride if flip_horizontal: + i: cython.Py_ssize_t for i in range(0, i_stride, bytes_per_pixel): + j: cython.Py_ssize_t for j in range(bytes_per_pixel): o_buf[o_pos + i + j] = i_buf[ i_pos + i_stride - i - bytes_per_pixel + j From 9e09e3c431bbab437e3e73b8e2b771a858cc780c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 21 Jan 2026 03:31:57 -0500 Subject: [PATCH 634/846] Fix sws_scale const correctness for strict GCC --- av/video/reformatter.pyx | 6 +++--- include/libswscale/swscale.pxd | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/av/video/reformatter.pyx b/av/video/reformatter.pyx index 4f6ec4c9f..465df6278 100644 --- a/av/video/reformatter.pyx +++ b/av/video/reformatter.pyx @@ -157,8 +157,8 @@ cdef class VideoReformatter: # We want to change the colorspace/color_range transforms. # We do that by grabbing all of the current settings, changing a # couple, and setting them all. We need a lot of state here. - cdef const int *inv_tbl - cdef const int *tbl + cdef int *inv_tbl + cdef int *tbl cdef int src_colorspace_range, dst_colorspace_range cdef int brightness, contrast, saturation cdef int ret @@ -209,7 +209,7 @@ cdef class VideoReformatter: with nogil: lib.sws_scale( self.ptr, - frame.ptr.data, + frame.ptr.data, frame.ptr.linesize, 0, # slice Y frame.ptr.height, diff --git a/include/libswscale/swscale.pxd b/include/libswscale/swscale.pxd index af7f7d134..a9698ecb3 100644 --- a/include/libswscale/swscale.pxd +++ b/include/libswscale/swscale.pxd @@ -49,11 +49,11 @@ cdef extern from "libswscale/swscale.h" nogil: cdef int sws_scale( SwsContext *ctx, - unsigned char **src_slice, - int *src_stride, + const unsigned char *const *src_slice, + const int *src_stride, int src_slice_y, int src_slice_h, - unsigned char **dst_slice, + unsigned char *const *dst_slice, int *dst_stride, ) From 8d4028e7f9a58a2674cc6aed3120a4b0a2776228 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 21 Jan 2026 12:55:36 -0500 Subject: [PATCH 635/846] Make container/input pure --- av/container/{input.pyx => input.py} | 141 +++++++++++++++------------ 1 file changed, 80 insertions(+), 61 deletions(-) rename av/container/{input.pyx => input.py} (71%) diff --git a/av/container/input.pyx b/av/container/input.py similarity index 71% rename from av/container/input.pyx rename to av/container/input.py index 56af7b05c..682158997 100644 --- a/av/container/input.pyx +++ b/av/container/input.py @@ -1,53 +1,59 @@ -from libc.stdint cimport int64_t -from libc.stdlib cimport free, malloc - -from av.codec.context cimport CodecContext, wrap_codec_context -from av.container.streams cimport StreamContainer -from av.dictionary cimport _Dictionary -from av.error cimport err_check -from av.packet cimport Packet -from av.stream cimport Stream, wrap_stream -from av.utils cimport avdict_to_dict +import cython +from cython.cimports.av.codec.context import CodecContext, wrap_codec_context +from cython.cimports.av.container.streams import StreamContainer +from cython.cimports.av.dictionary import _Dictionary +from cython.cimports.av.error import err_check +from cython.cimports.av.packet import Packet +from cython.cimports.av.stream import Stream, wrap_stream +from cython.cimports.av.utils import avdict_to_dict +from cython.cimports.libc.stdint import int64_t +from cython.cimports.libc.stdlib import free, malloc from av.dictionary import Dictionary -cdef close_input(InputContainer self): +@cython.cfunc +def close_input(self: InputContainer): self.streams = StreamContainer() if self.input_was_opened: - with nogil: + with cython.nogil: # This causes `self.ptr` to be set to NULL. - lib.avformat_close_input(&self.ptr) + lib.avformat_close_input(cython.address(self.ptr)) self.input_was_opened = False -cdef class InputContainer(Container): +@cython.cclass +class InputContainer(Container): def __cinit__(self, *args, **kwargs): - cdef CodecContext py_codec_context - cdef unsigned int i - cdef lib.AVStream *stream - cdef lib.AVCodec *codec - cdef lib.AVCodecContext *codec_context + py_codec_context: CodecContext + i: cython.uint + stream: cython.pointer[lib.AVStream] + codec: cython.pointer[lib.AVCodec] + codec_context: cython.pointer[lib.AVCodecContext] # If we have either the global `options`, or a `stream_options`, prepare # a mashup of those options for each stream. - cdef lib.AVDictionary **c_options = NULL - cdef _Dictionary base_dict, stream_dict + c_options: cython.pointer[cython.pointer[lib.AVDictionary]] = cython.NULL + base_dict: _Dictionary + stream_dict: _Dictionary if self.options or self.stream_options: base_dict = Dictionary(self.options) - c_options = malloc(self.ptr.nb_streams * sizeof(void*)) + c_options = cython.cast( + cython.pointer[cython.pointer[lib.AVDictionary]], + malloc(self.ptr.nb_streams * cython.sizeof(cython.p_void)), + ) for i in range(self.ptr.nb_streams): - c_options[i] = NULL + c_options[i] = cython.NULL if i < len(self.stream_options) and self.stream_options: stream_dict = base_dict.copy() stream_dict.update(self.stream_options[i]) - lib.av_dict_copy(&c_options[i], stream_dict.ptr, 0) + lib.av_dict_copy(cython.address(c_options[i]), stream_dict.ptr, 0) else: - lib.av_dict_copy(&c_options[i], base_dict.ptr, 0) + lib.av_dict_copy(cython.address(c_options[i]), base_dict.ptr, 0) self.set_timeout(self.open_timeout) self.start_timeout() - with nogil: + with cython.nogil: # This peeks are the first few frames to: # - set stream.disposition from codec.audio_service_type (not exposed); # - set stream.codec.bits_per_coded_sample; @@ -55,17 +61,14 @@ def __cinit__(self, *args, **kwargs): # - set stream.start_time; # - set stream.r_frame_rate to average value; # - open and closes codecs with the options provided. - ret = lib.avformat_find_stream_info( - self.ptr, - c_options - ) + ret = lib.avformat_find_stream_info(self.ptr, c_options) self.set_timeout(None) self.err_check(ret) - # Cleanup all of our options. + # Clean up all of our options. if c_options: for i in range(self.ptr.nb_streams): - lib.av_dict_free(&c_options[i]) + lib.av_dict_free(cython.address(c_options[i])) free(c_options) at_least_one_accelerated_context = False @@ -75,11 +78,15 @@ def __cinit__(self, *args, **kwargs): stream = self.ptr.streams[i] codec = lib.avcodec_find_decoder(stream.codecpar.codec_id) if codec: - # allocate and initialise decoder + # allocate and initialize decoder codec_context = lib.avcodec_alloc_context3(codec) - err_check(lib.avcodec_parameters_to_context(codec_context, stream.codecpar)) + err_check( + lib.avcodec_parameters_to_context(codec_context, stream.codecpar) + ) codec_context.pkt_timebase = stream.time_base - py_codec_context = wrap_codec_context(codec_context, codec, self.hwaccel) + py_codec_context = wrap_codec_context( + codec_context, codec, self.hwaccel + ) if py_codec_context.is_hwaccel: at_least_one_accelerated_context = True else: @@ -87,10 +94,18 @@ def __cinit__(self, *args, **kwargs): py_codec_context = None self.streams.add_stream(wrap_stream(self, stream, py_codec_context)) - if self.hwaccel and not self.hwaccel.allow_software_fallback and not at_least_one_accelerated_context: - raise RuntimeError("Hardware accelerated decode requested but no stream is compatible") + if ( + self.hwaccel + and not self.hwaccel.allow_software_fallback + and not at_least_one_accelerated_context + ): + raise RuntimeError( + "Hardware accelerated decode requested but no stream is compatible" + ) - self.metadata = avdict_to_dict(self.ptr.metadata, self.metadata_encoding, self.metadata_errors) + self.metadata = avdict_to_dict( + self.ptr.metadata, self.metadata_encoding, self.metadata_errors + ) def __dealloc__(self): close_input(self) @@ -141,19 +156,20 @@ def demux(self, *args, **kwargs): # For whatever reason, Cython does not like us directly passing kwargs # from one method to another. Without kwargs, it ends up passing a # NULL reference, which segfaults. So we force it to do something with it. - # This is likely a bug in Cython; see https://github.com/cython/cython/issues/2166 - # (and others). + # This is a bug in Cython; see https://github.com/cython/cython/issues/2166 id(kwargs) streams = self.streams.get(*args, **kwargs) - - cdef bint *include_stream = malloc(self.ptr.nb_streams * sizeof(bint)) - if include_stream == NULL: + include_stream: cython.pointer[cython.bint] = cython.cast( + cython.pointer[cython.bint], + malloc(self.ptr.nb_streams * cython.sizeof(bint)), + ) + if include_stream == cython.NULL: raise MemoryError() - cdef unsigned int i - cdef Packet packet - cdef int ret + i: cython.uint + packet: Packet + ret: cython.int self.set_timeout(self.read_timeout) try: @@ -169,7 +185,7 @@ def demux(self, *args, **kwargs): packet = Packet() try: self.start_timeout() - with nogil: + with cython.nogil: ret = lib.av_read_frame(self.ptr, packet.ptr) self.err_check(ret) except EOFError: @@ -217,8 +233,14 @@ def decode(self, *args, **kwargs): yield frame def seek( - self, offset, *, bint backward=True, bint any_frame=False, Stream stream=None, - bint unsupported_frame_offset=False, bint unsupported_byte_offset=False + self, + offset, + *, + backward: bint = True, + any_frame: bint = False, + stream: Stream | None = None, + unsupported_frame_offset: bint = False, + unsupported_byte_offset: bint = False, ): """seek(offset, *, backward=True, any_frame=False, stream=None) @@ -249,16 +271,12 @@ def seek( """ self._assert_open() - # We used to take floats here and assume they were in seconds. This - # was super confusing, so lets go in the complete opposite direction - # and reject non-ints. if not isinstance(offset, int): raise TypeError("Container.seek only accepts integer offset.", type(offset)) - cdef int64_t c_offset = offset - - cdef int flags = 0 - cdef int ret + c_offset: int64_t = offset + flags: cython.int = 0 + ret: cython.int if backward: flags |= lib.AVSEEK_FLAG_BACKWARD @@ -271,18 +289,19 @@ def seek( if unsupported_byte_offset: flags |= lib.AVSEEK_FLAG_BYTE - cdef int stream_index = stream.index if stream else -1 - with nogil: + stream_index: cython.int = stream.index if stream else -1 + with cython.nogil: ret = lib.av_seek_frame(self.ptr, stream_index, c_offset, flags) err_check(ret) self.flush_buffers() - cdef flush_buffers(self): + @cython.cfunc + def flush_buffers(self): self._assert_open() - cdef Stream stream - cdef CodecContext codec_context + stream: Stream + codec_context: CodecContext for stream in self.streams: codec_context = stream.codec_context From 714be3499c6ca9fb24e39268cfeca2f07793bfc2 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 21 Jan 2026 12:55:36 -0500 Subject: [PATCH 636/846] Make container/input pure --- av/filter/{link.pyx => link.py} | 82 ++++++++++++++++++++------------- 1 file changed, 50 insertions(+), 32 deletions(-) rename av/filter/{link.pyx => link.py} (57%) diff --git a/av/filter/link.pyx b/av/filter/link.py similarity index 57% rename from av/filter/link.pyx rename to av/filter/link.py index 905082d15..7914ba2b1 100644 --- a/av/filter/link.pyx +++ b/av/filter/link.py @@ -1,12 +1,12 @@ -cimport libav as lib +import cython +import cython.cimports.libav as lib +from cython.cimports.av.filter.graph import Graph -from av.filter.graph cimport Graph +_cinit_sentinel = cython.declare(object, object()) -cdef _cinit_sentinel = object() - - -cdef class FilterLink: +@cython.cclass +class FilterLink: def __cinit__(self, sentinel): if sentinel is not _cinit_sentinel: raise RuntimeError("cannot instantiate FilterLink") @@ -15,14 +15,14 @@ def __cinit__(self, sentinel): def input(self): if self._input: return self._input - cdef lib.AVFilterContext *cctx = self.ptr.src - cdef unsigned int i + cctx: cython.pointer[lib.AVFilterContext] = self.ptr.src + i: cython.Py_ssize_t for i in range(cctx.nb_outputs): if self.ptr == cctx.outputs[i]: break - else: + else: # nobreak raise RuntimeError("could not find link in context") - ctx = self.graph._context_by_ptr[cctx] + ctx = self.graph._context_by_ptr[cython.cast(cython.long, cctx)] self._input = ctx.outputs[i] return self._input @@ -30,30 +30,33 @@ def input(self): def output(self): if self._output: return self._output - cdef lib.AVFilterContext *cctx = self.ptr.dst - cdef unsigned int i + cctx: cython.pointer[lib.AVFilterContext] = self.ptr.dst + i: cython.Py_ssize_t for i in range(cctx.nb_inputs): if self.ptr == cctx.inputs[i]: break else: raise RuntimeError("could not find link in context") try: - ctx = self.graph._context_by_ptr[cctx] + ctx = self.graph._context_by_ptr[cython.cast(cython.long, cctx)] except KeyError: - raise RuntimeError("could not find context in graph", (cctx.name, cctx.filter.name)) + raise RuntimeError( + "could not find context in graph", (cctx.name, cctx.filter.name) + ) self._output = ctx.inputs[i] return self._output -cdef FilterLink wrap_filter_link(Graph graph, lib.AVFilterLink *ptr): - cdef FilterLink link = FilterLink(_cinit_sentinel) +@cython.cfunc +def wrap_filter_link(graph: Graph, ptr: cython.pointer[lib.AVFilterLink]) -> FilterLink: + link: FilterLink = FilterLink(_cinit_sentinel) link.graph = graph link.ptr = ptr return link - -cdef class FilterPad: +@cython.cclass +class FilterPad: def __cinit__(self, sentinel): if sentinel is not _cinit_sentinel: raise RuntimeError("cannot construct FilterPad") @@ -62,7 +65,9 @@ def __repr__(self): _filter = self.filter.name _io = "inputs" if self.is_input else "outputs" - return f"" + return ( + f"" + ) @property def is_output(self): @@ -73,7 +78,8 @@ def name(self): return lib.avfilter_pad_get_name(self.base_ptr, self.index) -cdef class FilterContextPad(FilterPad): +@cython.cclass +class FilterContextPad(FilterPad): def __repr__(self): _filter = self.filter.name _io = "inputs" if self.is_input else "outputs" @@ -85,8 +91,10 @@ def __repr__(self): def link(self): if self._link: return self._link - cdef lib.AVFilterLink **links = self.context.ptr.inputs if self.is_input else self.context.ptr.outputs - cdef lib.AVFilterLink *link = links[self.index] + links: cython.pointer[cython.pointer[lib.AVFilterLink]] = ( + self.context.ptr.inputs if self.is_input else self.context.ptr.outputs + ) + link: cython.pointer[lib.AVFilterLink] = links[self.index] if not link: return self._link = wrap_filter_link(self.context.graph, link) @@ -94,29 +102,39 @@ def link(self): @property def linked(self): - cdef FilterLink link = self.link + link: FilterLink = self.link if link: return link.input if self.is_input else link.output -cdef tuple alloc_filter_pads(Filter filter, const lib.AVFilterPad *ptr, bint is_input, FilterContext context=None): +@cython.cfunc +def alloc_filter_pads( + filter: Filter, + ptr: cython.pointer[cython.const[lib.AVFilterPad]], + is_input: cython.bint, + context: FilterContext | None = None, +) -> tuple: if not ptr: return () - pads = [] + pads: list = [] # We need to be careful and check our bounds if we know what they are, # since the arrays on a AVFilterContext are not NULL terminated. - cdef int i = 0 - cdef int count + i: cython.int = 0 + count: cython.int if context is None: count = lib.avfilter_filter_pad_count(filter.ptr, not is_input) else: - count = (context.ptr.nb_inputs if is_input else context.ptr.nb_outputs) - - cdef FilterPad pad - while (i < count): - pad = FilterPad(_cinit_sentinel) if context is None else FilterContextPad(_cinit_sentinel) + count = context.ptr.nb_inputs if is_input else context.ptr.nb_outputs + + pad: FilterPad + while i < count: + pad = ( + FilterPad(_cinit_sentinel) + if context is None + else FilterContextPad(_cinit_sentinel) + ) pads.append(pad) pad.filter = filter pad.context = context From 6232cdd79789f1c40b262d58082a2bc358f1293a Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 21 Jan 2026 14:37:15 -0500 Subject: [PATCH 637/846] Make av/logging pure --- av/{logging.pyx => logging.py} | 161 +++++++++++++++++++++------------ 1 file changed, 105 insertions(+), 56 deletions(-) rename av/{logging.pyx => logging.py} (69%) diff --git a/av/logging.pyx b/av/logging.py similarity index 69% rename from av/logging.pyx rename to av/logging.py index 30a253fe0..ade74c954 100644 --- a/av/logging.pyx +++ b/av/logging.py @@ -1,3 +1,4 @@ +# type: ignore """ FFmpeg has a logging system that it uses extensively. It's very noisy, so PyAV turns it off by default. This unfortunately has the effect of making raised errors have less @@ -38,14 +39,15 @@ """ -cimport libav as lib -from libc.stdio cimport fprintf, stderr -from libc.stdlib cimport free, malloc - import logging import sys from threading import Lock, get_ident +import cython +import cython.cimports.libav as lib +from cython.cimports.libc.stdio import fprintf, stderr +from cython.cimports.libc.stdlib import free, malloc + # Library levels. PANIC = lib.AV_LOG_PANIC # 0 FATAL = lib.AV_LOG_FATAL # 8 @@ -60,9 +62,9 @@ CRITICAL = FATAL -cpdef adapt_level(int level): +@cython.ccall +def adapt_level(level: cython.int): """Convert a library log level to a Python log level.""" - if level <= lib.AV_LOG_FATAL: # Includes PANIC return 50 # logging.CRITICAL elif level <= lib.AV_LOG_ERROR: @@ -79,7 +81,7 @@ return 1 -cdef object level_threshold = None +level_threshold = cython.declare(object, None) # ... but lets limit ourselves to WARNING (assuming nobody already did this). if "libav" not in logging.Logger.manager.loggerDict: @@ -133,10 +135,10 @@ def restore_default_callback(): lib.av_log_set_callback(lib.av_log_default_callback) -cdef bint skip_repeated = True -cdef skip_lock = Lock() -cdef object last_log = None -cdef int skip_count = 0 +skip_repeated = cython.declare(cython.bint, True) +skip_lock = cython.declare(object, Lock()) +last_log = cython.declare(object, None) +skip_count = cython.declare(cython.int, 0) def get_skip_repeated(): @@ -151,10 +153,12 @@ def set_skip_repeated(v): # For error reporting. -cdef object last_error = None -cdef int error_count = 0 +last_error = cython.declare(object, None) +error_count = cython.declare(cython.int, 0) + -cpdef get_last_error(): +@cython.ccall +def get_last_error(): """Get the last log that was at least ``ERROR``.""" if error_count: with skip_lock: @@ -163,10 +167,12 @@ def set_skip_repeated(v): return 0, None -cdef global_captures = [] -cdef thread_captures = {} +global_captures = cython.declare(list, []) +thread_captures = cython.declare(dict, {}) -cdef class Capture: + +@cython.cclass +class Capture: """A context manager for capturing logs. :param bool local: Should logs from all threads be captured, or just one @@ -181,12 +187,11 @@ def set_skip_repeated(v): """ - cdef readonly list logs - cdef list captures + logs = cython.declare(list, visibility="readonly") + captures = cython.declare(list, visibility="private") - def __init__(self, bint local=True): + def __init__(self, local: cython.bint = True): self.logs = [] - if local: self.captures = thread_captures.setdefault(get_ident(), []) else: @@ -197,56 +202,73 @@ def __enter__(self): return self.logs def __exit__(self, type_, value, traceback): - self.captures.pop(-1) + self.captures.pop() + + +log_context = cython.struct( + class_=cython.pointer[lib.AVClass], + name=cython.p_char, +) +item_name_func = cython.typedef("const char *(*item_name_func)(void *) noexcept nogil") -cdef struct log_context: - lib.AVClass *class_ - const char *name -cdef const char *log_context_name(void *ptr) noexcept nogil: - cdef log_context *obj = ptr +@cython.cfunc +@cython.nogil +@cython.exceptval(check=False) +def log_context_name(ptr: cython.p_void) -> cython.p_char: + obj: cython.pointer[log_context] = cython.cast(cython.pointer[log_context], ptr) return obj.name -cdef lib.AVClass log_class -log_class.item_name = log_context_name -cpdef log(int level, str name, str message): +log_class = cython.declare(lib.AVClass) +log_class.item_name = cython.cast(item_name_func, log_context_name) + + +@cython.ccall +def log(level: cython.int, name: str, message: str): """Send a log through the library logging system. This is mostly for testing. - """ - - cdef log_context *obj = malloc(sizeof(log_context)) - obj.class_ = &log_class + obj: cython.pointer[log_context] = cython.cast( + cython.pointer[log_context], malloc(cython.sizeof(log_context)) + ) + obj.class_ = cython.address(log_class) obj.name = name - cdef bytes message_bytes = message.encode("utf-8") - - lib.av_log(obj, level, "%s", message_bytes) + message_bytes: bytes = message.encode("utf-8") + + lib.av_log( + cython.cast(cython.p_void, obj), + level, + "%s", + cython.cast(cython.p_char, message_bytes), + ) free(obj) -cdef log_callback_gil(int level, const char *c_name, const char *c_message): +@cython.cfunc +def log_callback_gil( + level: cython.int, c_name: cython.p_const_char, c_message: cython.p_char +): global error_count global skip_count global last_log global last_error - name = c_name if c_name is not NULL else "" - message = (c_message).decode("utf8", "backslashreplace") + name = cython.cast(str, c_name) if c_name is not cython.NULL else "" + message = cython.cast(bytes, c_message).decode("utf8", "backslashreplace") log = (level, name, message) # We have to filter it ourselves, but we will still process it in general so # it is available to our error handling. # Note that FFmpeg's levels are backwards from Python's. - cdef bint is_interesting = level <= level_threshold + is_interesting: cython.bint = level <= level_threshold # Skip messages which are identical to the previous. # TODO: Be smarter about threads. - cdef bint is_repeated = False - - cdef object repeat_log = None + is_repeated: cython.bint = False + repeat_log: object = None with skip_lock: if is_interesting: @@ -263,7 +285,7 @@ def __exit__(self, type_, value, traceback): repeat_log = ( last_log[0], last_log[1], - "%s (repeated %d more times)" % (last_log[2], skip_count) + "%s (repeated %d more times)" % (last_log[2], skip_count), ) skip_count = 0 @@ -281,7 +303,8 @@ def __exit__(self, type_, value, traceback): log_callback_emit(log) -cdef log_callback_emit(log): +@cython.cfunc +def log_callback_emit(log): lib_level, name, message = log captures = thread_captures.get(get_ident()) or global_captures @@ -296,37 +319,63 @@ def __exit__(self, type_, value, traceback): logger.log(py_level, message.strip()) -cdef void log_callback(void *ptr, int level, const char *format, lib.va_list args) noexcept nogil: - cdef bint inited = lib.Py_IsInitialized() +@cython.cfunc +@cython.nogil +@cython.exceptval(check=False) +def log_callback( + ptr: cython.p_void, + level: cython.int, + format: cython.p_const_char, + args: lib.va_list, +) -> cython.void: + inited: cython.bint = lib.Py_IsInitialized() if not inited: return - with gil: + with cython.gil: if level > level_threshold and level != lib.AV_LOG_ERROR: return # Format the message. - cdef char message[1024] + message: cython.char[1024] lib.vsnprintf(message, 1023, format, args) # Get the name. - cdef const char *name = NULL - cdef lib.AVClass *cls = (ptr)[0] if ptr else NULL + name: cython.p_const_char = cython.NULL + cls: cython.pointer[lib.AVClass] = ( + cython.cast(cython.pointer[cython.pointer[lib.AVClass]], ptr)[0] + if ptr + else cython.NULL + ) if cls and cls.item_name: name = cls.item_name(ptr) - with gil: + with cython.gil: try: log_callback_gil(level, name, message) except Exception: - fprintf(stderr, "av.logging: exception while handling %s[%d]: %s\n", - name, level, message) + fprintf( + stderr, + "av.logging: exception while handling %s[%d]: %s\n", + name, + level, + message, + ) # For some reason lib.PyErr_PrintEx(0) won't work. exc, type_, tb = sys.exc_info() lib.PyErr_Display(exc, type_, tb) -cdef void nolog_callback(void *ptr, int level, const char *format, lib.va_list args) noexcept nogil: +@cython.cfunc +@cython.nogil +@cython.exceptval(check=False) +def nolog_callback( + ptr: cython.p_void, + level: cython.int, + format: cython.p_const_char, + args: lib.va_list, +) -> cython.void: pass + lib.av_log_set_callback(nolog_callback) From e15007e3b3baf49835c3fa31165be9ad348c94ce Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 21 Jan 2026 14:37:15 -0500 Subject: [PATCH 638/846] Make video/codeccontext pure --- av/audio/codeccontext.py | 2 +- av/codec/context.pxd | 2 +- av/codec/context.py | 2 +- av/container/input.py | 8 +- av/video/codeccontext.pxd | 7 - .../{codeccontext.pyx => codeccontext.py} | 156 ++++++++++-------- 6 files changed, 96 insertions(+), 81 deletions(-) rename av/video/{codeccontext.pyx => codeccontext.py} (69%) diff --git a/av/audio/codeccontext.py b/av/audio/codeccontext.py index 2dc629917..8174ba26d 100644 --- a/av/audio/codeccontext.py +++ b/av/audio/codeccontext.py @@ -10,7 +10,7 @@ @cython.cclass class AudioCodecContext(CodecContext): @cython.cfunc - def _prepare_frames_for_encode(self, input_frame: Frame | None): + def _prepare_frames_for_encode(self, input_frame: Frame | None) -> list: frame: AudioFrame | None = input_frame allow_var_frame_size: cython.bint = ( self.ptr.codec.capabilities & lib.AV_CODEC_CAP_VARIABLE_FRAME_SIZE diff --git a/av/codec/context.pxd b/av/codec/context.pxd index 7ba89dab7..a9722c384 100644 --- a/av/codec/context.pxd +++ b/av/codec/context.pxd @@ -40,7 +40,7 @@ cdef class CodecContext: # TODO: Remove the `Packet` from `_setup_decoded_frame` (because flushing packets # are bogus). It should take all info it needs from the context and/or stream. cdef _prepare_and_time_rebase_frames_for_encode(self, Frame frame) - cdef _prepare_frames_for_encode(self, Frame frame) + cdef list _prepare_frames_for_encode(self, Frame frame) cdef _setup_encoded_packet(self, Packet) cdef _setup_decoded_frame(self, Frame, Packet) diff --git a/av/codec/context.py b/av/codec/context.py index 0117d8034..4751e597f 100644 --- a/av/codec/context.py +++ b/av/codec/context.py @@ -386,7 +386,7 @@ def _send_packet_and_recv(self, packet: Packet | None): return out @cython.cfunc - def _prepare_frames_for_encode(self, frame: Frame | None): + def _prepare_frames_for_encode(self, frame: Frame | None) -> list: return [frame] @cython.cfunc diff --git a/av/container/input.py b/av/container/input.py index 682158997..938ac8dab 100644 --- a/av/container/input.py +++ b/av/container/input.py @@ -236,11 +236,11 @@ def seek( self, offset, *, - backward: bint = True, - any_frame: bint = False, + backward: cython.bint = True, + any_frame: cython.bint = False, stream: Stream | None = None, - unsupported_frame_offset: bint = False, - unsupported_byte_offset: bint = False, + unsupported_frame_offset: cython.bint = False, + unsupported_byte_offset: cython.bint = False, ): """seek(offset, *, backward=True, any_frame=False, stream=None) diff --git a/av/video/codeccontext.pxd b/av/video/codeccontext.pxd index 895ba74b1..8d128348a 100644 --- a/av/video/codeccontext.pxd +++ b/av/video/codeccontext.pxd @@ -16,18 +16,11 @@ cdef struct AVCodecPrivateData: cdef class VideoCodecContext(CodecContext): - cdef AVCodecPrivateData _private_data - cdef VideoFormat _format cdef _build_format(self) - cdef int last_w cdef int last_h cdef readonly VideoReformatter reformatter - - # For encoding. cdef readonly int encoded_frame_count - - # For decoding. cdef VideoFrame next_frame diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.py similarity index 69% rename from av/video/codeccontext.pyx rename to av/video/codeccontext.py index e82e56e91..b7842a7d5 100644 --- a/av/video/codeccontext.pyx +++ b/av/video/codeccontext.py @@ -1,41 +1,56 @@ -cimport libav as lib -from libc.stdint cimport int64_t - -from av.codec.context cimport CodecContext -from av.codec.hwaccel cimport HWAccel, HWConfig -from av.error cimport err_check -from av.frame cimport Frame -from av.packet cimport Packet -from av.utils cimport avrational_to_fraction, to_avrational -from av.video.format cimport VideoFormat, get_pix_fmt, get_video_format -from av.video.frame cimport VideoFrame, alloc_video_frame -from av.video.reformatter cimport VideoReformatter - - -cdef lib.AVPixelFormat _get_hw_format(lib.AVCodecContext *ctx, const lib.AVPixelFormat *pix_fmts) noexcept: +import cython +import cython.cimports.libav as lib +from cython.cimports.av.codec.context import CodecContext +from cython.cimports.av.codec.hwaccel import HWAccel, HWConfig +from cython.cimports.av.error import err_check +from cython.cimports.av.frame import Frame +from cython.cimports.av.packet import Packet +from cython.cimports.av.utils import avrational_to_fraction, to_avrational +from cython.cimports.av.video.format import VideoFormat, get_pix_fmt, get_video_format +from cython.cimports.av.video.frame import VideoFrame, alloc_video_frame +from cython.cimports.av.video.reformatter import VideoReformatter +from cython.cimports.libc.stdint import int64_t + + +@cython.cfunc +@cython.exceptval(check=False) +def _get_hw_format( + ctx: cython.pointer[lib.AVCodecContext], + pix_fmts: cython.pointer[cython.const[lib.AVPixelFormat]], +) -> lib.AVPixelFormat: # In the case where we requested accelerated decoding, the decoder first calls this function # with a list that includes both the hardware format and software formats. # First we try to pick the hardware format if it's in the list. # However, if the decoder fails to initialize the hardware, it will call this function again, # with only software formats in pix_fmts. We return ctx->sw_pix_fmt regardless in this case, # because that should be in the candidate list. If not, we are out of ideas anyways. - cdef AVCodecPrivateData* private_data = ctx.opaque - i = 0 + private_data: cython.pointer[AVCodecPrivateData] = cython.cast( + cython.pointer[AVCodecPrivateData], ctx.opaque + ) + i: cython.int = 0 while pix_fmts[i] != -1: if pix_fmts[i] == private_data.hardware_pix_fmt: return pix_fmts[i] i += 1 - return ctx.sw_pix_fmt if private_data.allow_software_fallback else lib.AV_PIX_FMT_NONE - + return ( + ctx.sw_pix_fmt if private_data.allow_software_fallback else lib.AV_PIX_FMT_NONE + ) -cdef class VideoCodecContext(CodecContext): +@cython.cclass +class VideoCodecContext(CodecContext): def __cinit__(self, *args, **kwargs): self.last_w = 0 self.last_h = 0 - cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec, HWAccel hwaccel): - CodecContext._init(self, ptr, codec, hwaccel) # TODO: Can this be `super`? + @cython.cfunc + def _init( + self, + ptr: cython.pointer[lib.AVCodecContext], + codec: cython.pointer[cython.const[lib.AVCodec]], + hwaccel: HWAccel | None, + ): + CodecContext._init(self, ptr, codec, hwaccel) if hwaccel is not None: try: @@ -43,9 +58,13 @@ def __cinit__(self, *args, **kwargs): self.ptr.hw_device_ctx = lib.av_buffer_ref(self.hwaccel_ctx.ptr) self.ptr.pix_fmt = self.hwaccel_ctx.config.ptr.pix_fmt self.ptr.get_format = _get_hw_format - self._private_data.hardware_pix_fmt = self.hwaccel_ctx.config.ptr.pix_fmt - self._private_data.allow_software_fallback = self.hwaccel.allow_software_fallback - self.ptr.opaque = &self._private_data + self._private_data.hardware_pix_fmt = ( + self.hwaccel_ctx.config.ptr.pix_fmt + ) + self._private_data.allow_software_fallback = ( + self.hwaccel.allow_software_fallback + ) + self.ptr.opaque = cython.address(self._private_data) except NotImplementedError: # Some streams may not have a hardware decoder. For example, many action # cam videos have a low resolution mjpeg stream, which is usually not @@ -60,20 +79,20 @@ def __cinit__(self, *args, **kwargs): self._build_format() self.encoded_frame_count = 0 - cdef _prepare_frames_for_encode(self, Frame input): - if not input: + @cython.cfunc + def _prepare_frames_for_encode(self, input: Frame | None) -> list: + if input is None or not input: return [None] - cdef VideoFrame vframe = input - if self._format is None: raise ValueError("self._format is None, cannot encode") + vframe: VideoFrame = input # Reformat if it doesn't match. if ( - vframe.format.pix_fmt != self._format.pix_fmt or - vframe.width != self.ptr.width or - vframe.height != self.ptr.height + vframe.format.pix_fmt != self._format.pix_fmt + or vframe.width != self.ptr.width + or vframe.height != self.ptr.height ): if not self.reformatter: self.reformatter = VideoReformatter() @@ -84,49 +103,50 @@ def __cinit__(self, *args, **kwargs): # There is no pts, so create one. if vframe.ptr.pts == lib.AV_NOPTS_VALUE: - vframe.ptr.pts = self.encoded_frame_count + vframe.ptr.pts = cython.cast(int64_t, self.encoded_frame_count) self.encoded_frame_count += 1 - return [vframe] - cdef Frame _alloc_next_frame(self): + @cython.cfunc + def _alloc_next_frame(self) -> Frame: return alloc_video_frame() - cdef _setup_decoded_frame(self, Frame frame, Packet packet): + @cython.cfunc + def _setup_decoded_frame(self, frame: Frame, packet: Packet): CodecContext._setup_decoded_frame(self, frame, packet) - cdef VideoFrame vframe = frame + vframe: VideoFrame = frame vframe._init_user_attributes() - cdef _transfer_hwframe(self, Frame frame): + @cython.cfunc + def _transfer_hwframe(self, frame: Frame): if self.hwaccel_ctx is None: return frame - if frame.ptr.format != self.hwaccel_ctx.config.ptr.pix_fmt: # If we get a software frame, that means we are in software fallback mode, and don't actually # need to transfer. return frame - cdef Frame frame_sw - - frame_sw = self._alloc_next_frame() - + frame_sw: Frame = self._alloc_next_frame() err_check(lib.av_hwframe_transfer_data(frame_sw.ptr, frame.ptr, 0)) - - # TODO: Is there anything else to transfer?! + # TODO: Is there anything else to transfer? frame_sw.pts = frame.pts - return frame_sw - cdef _build_format(self): - self._format = get_video_format(self.ptr.pix_fmt, self.ptr.width, self.ptr.height) + @cython.cfunc + def _build_format(self): + self._format = get_video_format( + cython.cast(lib.AVPixelFormat, self.ptr.pix_fmt), + self.ptr.width, + self.ptr.height, + ) @property def format(self): return self._format @format.setter - def format(self, VideoFormat format): + def format(self, format: VideoFormat): self.ptr.pix_fmt = format.pix_fmt self.ptr.width = format.width self.ptr.height = format.height @@ -134,23 +154,23 @@ def format(self, VideoFormat format): @property def width(self): - if self.ptr is NULL: + if self.ptr is cython.NULL: return 0 return self.ptr.width @width.setter - def width(self, unsigned int value): + def width(self, value: cython.uint): self.ptr.width = value self._build_format() @property def height(self): - if self.ptr is NULL: + if self.ptr is cython.NULL: return 0 return self.ptr.height @height.setter - def height(self, unsigned int value): + def height(self, value: cython.uint): self.ptr.height = value self._build_format() @@ -160,13 +180,13 @@ def bits_per_coded_sample(self): The number of bits per sample in the codedwords. It's mandatory for this to be set for some formats to decode properly. Wraps :ffmpeg:`AVCodecContext.bits_per_coded_sample`. - + :type: int """ return self.ptr.bits_per_coded_sample - + @bits_per_coded_sample.setter - def bits_per_coded_sample(self, int value): + def bits_per_coded_sample(self, value: cython.int): if self.is_encoder: raise ValueError("Not supported for encoders") @@ -194,11 +214,11 @@ def framerate(self): :type: fractions.Fraction """ - return avrational_to_fraction(&self.ptr.framerate) + return avrational_to_fraction(cython.address(self.ptr.framerate)) @framerate.setter def framerate(self, value): - to_avrational(value, &self.ptr.framerate) + to_avrational(value, cython.address(self.ptr.framerate)) @property def rate(self): @@ -213,7 +233,7 @@ def rate(self, value): def gop_size(self): """ Sets the number of frames between keyframes. Used only for encoding. - + :type: int """ if self.is_decoder: @@ -221,29 +241,31 @@ def gop_size(self): return self.ptr.gop_size @gop_size.setter - def gop_size(self, int value): + def gop_size(self, value: cython.int): if self.is_decoder: raise RuntimeError("Cannot access 'gop_size' as a decoder") self.ptr.gop_size = value @property def sample_aspect_ratio(self): - return avrational_to_fraction(&self.ptr.sample_aspect_ratio) + return avrational_to_fraction(cython.address(self.ptr.sample_aspect_ratio)) @sample_aspect_ratio.setter def sample_aspect_ratio(self, value): - to_avrational(value, &self.ptr.sample_aspect_ratio) + to_avrational(value, cython.address(self.ptr.sample_aspect_ratio)) @property def display_aspect_ratio(self): - cdef lib.AVRational dar - + dar: lib.AVRational lib.av_reduce( - &dar.num, &dar.den, + cython.address(dar.num), + cython.address(dar.den), self.ptr.width * self.ptr.sample_aspect_ratio.num, - self.ptr.height * self.ptr.sample_aspect_ratio.den, 1024*1024) + self.ptr.height * self.ptr.sample_aspect_ratio.den, + 1024 * 1024, + ) - return avrational_to_fraction(&dar) + return avrational_to_fraction(cython.address(dar)) @property def has_b_frames(self): From 2f9636935d0be91b41cbb040924f4c21eec2513e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 21 Jan 2026 16:43:06 -0500 Subject: [PATCH 639/846] Avoid using strncpy --- av/filter/loudnorm_impl.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/av/filter/loudnorm_impl.c b/av/filter/loudnorm_impl.c index f6e22e4ce..c4e182981 100644 --- a/av/filter/loudnorm_impl.c +++ b/av/filter/loudnorm_impl.c @@ -36,7 +36,9 @@ static void logging_callback(void *ptr, int level, const char *fmt, va_list vl) pthread_mutex_lock(&json_mutex); #endif - strncpy(json_buffer, json_start, sizeof(json_buffer) - 1); + size_t len = strnlen(json_start, sizeof(json_buffer) - 1); + memcpy(json_buffer, json_start, len); + json_buffer[len] = '\0'; json_captured = 1; #ifdef _WIN32 From 63210bf596bd989449c1e8962b17dc445022e764 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 21 Jan 2026 16:49:40 -0500 Subject: [PATCH 640/846] Restore guards (fix 'main' regression) --- av/audio/fifo.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/av/audio/fifo.py b/av/audio/fifo.py index 14673527e..953d24edc 100644 --- a/av/audio/fifo.py +++ b/av/audio/fifo.py @@ -192,15 +192,21 @@ def read_many(self, samples: cython.int, partial: cython.bint = False): @property def format(self): """The :class:`.AudioFormat` of this FIFO.""" + if not self.ptr: + raise AttributeError(f"'{__name__}.AudioFifo' object has no attribute 'format'") return self.template.format @property def layout(self): """The :class:`.AudioLayout` of this FIFO.""" + if not self.ptr: + raise AttributeError(f"'{__name__}.AudioFifo' object has no attribute 'layout'") return self.template.layout @property def sample_rate(self): + if not self.ptr: + raise AttributeError(f"'{__name__}.AudioFifo' object has no attribute 'sample_rate'") return self.template.sample_rate @property From 76d2cb7b303ee47d96142a303d8cd791ce75ade4 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 21 Jan 2026 16:57:36 -0500 Subject: [PATCH 641/846] Use newer mypy --- Makefile | 2 +- av/audio/fifo.py | 12 +++++++++--- tests/test_decode.py | 6 ++++-- tests/test_subtitles.py | 6 +++--- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index b37d83e57..a9fb0876f 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ fate-suite: rsync -vrltLW rsync://fate-suite.ffmpeg.org/fate-suite/ tests/assets/fate-suite/ lint: - $(PIP) install -U ruff isort pillow numpy mypy==1.17.1 pytest + $(PIP) install -U ruff isort pillow numpy mypy==1.19.1 pytest ruff format --check av examples tests setup.py isort --check-only --diff av examples tests mypy av tests diff --git a/av/audio/fifo.py b/av/audio/fifo.py index 953d24edc..ebd68adb8 100644 --- a/av/audio/fifo.py +++ b/av/audio/fifo.py @@ -193,20 +193,26 @@ def read_many(self, samples: cython.int, partial: cython.bint = False): def format(self): """The :class:`.AudioFormat` of this FIFO.""" if not self.ptr: - raise AttributeError(f"'{__name__}.AudioFifo' object has no attribute 'format'") + raise AttributeError( + f"'{__name__}.AudioFifo' object has no attribute 'format'" + ) return self.template.format @property def layout(self): """The :class:`.AudioLayout` of this FIFO.""" if not self.ptr: - raise AttributeError(f"'{__name__}.AudioFifo' object has no attribute 'layout'") + raise AttributeError( + f"'{__name__}.AudioFifo' object has no attribute 'layout'" + ) return self.template.layout @property def sample_rate(self): if not self.ptr: - raise AttributeError(f"'{__name__}.AudioFifo' object has no attribute 'sample_rate'") + raise AttributeError( + f"'{__name__}.AudioFifo' object has no attribute 'sample_rate'" + ) return self.template.sample_rate @property diff --git a/tests/test_decode.py b/tests/test_decode.py index 685b743f5..d7fffbd4c 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -8,6 +8,8 @@ import pytest import av +from av.sidedata.encparams import VideoEncParams +from av.subtitles.subtitle import SubtitleSet from .common import TestCase, fate_suite @@ -109,7 +111,7 @@ def test_decoded_time_base(self) -> None: for packet in container.demux(stream): for frame in packet.decode(): - assert not isinstance(frame, av.subtitles.subtitle.SubtitleSet) + assert not isinstance(frame, SubtitleSet) assert packet.time_base == frame.time_base assert stream.time_base == frame.time_base return @@ -146,7 +148,7 @@ def test_decoded_video_enc_params(self) -> None: for frame in container.decode(stream): video_enc_params = cast( - av.sidedata.encparams.VideoEncParams, + VideoEncParams, frame.side_data.get("VIDEO_ENC_PARAMS"), ) assert video_enc_params is not None diff --git a/tests/test_subtitles.py b/tests/test_subtitles.py index d2a199924..321b29d1e 100644 --- a/tests/test_subtitles.py +++ b/tests/test_subtitles.py @@ -4,7 +4,7 @@ import av from av.codec.context import CodecContext from av.subtitles.codeccontext import SubtitleCodecContext -from av.subtitles.subtitle import AssSubtitle, BitmapSubtitle +from av.subtitles.subtitle import AssSubtitle, BitmapSubtitle, Subtitle, SubtitleSet from .common import TestCase, fate_suite @@ -35,8 +35,8 @@ def test_subset(self) -> None: for packet in container.demux(subs): subset = subs.decode2(packet) if subset is not None: - assert not isinstance(subset, av.subtitles.subtitle.Subtitle) - assert isinstance(subset, av.subtitles.subtitle.SubtitleSet) + assert not isinstance(subset, Subtitle) + assert isinstance(subset, SubtitleSet) assert subset.format == 1 assert hasattr(subset, "pts") assert subset.start_display_time == 0 From 13488d2b480f21dffc1a52577ba0b05c65467c4f Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 22 Jan 2026 00:51:24 -0500 Subject: [PATCH 642/846] Make video/reformatter pure --- av/video/{reformatter.pyx => reformatter.py} | 143 +++++++++++-------- 1 file changed, 84 insertions(+), 59 deletions(-) rename av/video/{reformatter.pyx => reformatter.py} (66%) diff --git a/av/video/reformatter.pyx b/av/video/reformatter.py similarity index 66% rename from av/video/reformatter.pyx rename to av/video/reformatter.py index 465df6278..dc898be84 100644 --- a/av/video/reformatter.pyx +++ b/av/video/reformatter.py @@ -1,12 +1,11 @@ -cimport libav as lib -from libc.stdint cimport uint8_t - -from av.error cimport err_check -from av.video.format cimport VideoFormat -from av.video.frame cimport alloc_video_frame - from enum import IntEnum +import cython +import cython.cimports.libav as lib +from cython.cimports.av.error import err_check +from cython.cimports.av.video.format import VideoFormat +from cython.cimports.av.video.frame import alloc_video_frame + class Interpolation(IntEnum): FAST_BILINEAR: "Fast bilinear" = lib.SWS_FAST_BILINEAR @@ -38,6 +37,7 @@ class Colorspace(IntEnum): smpte240m = lib.SWS_CS_SMPTE240M default = lib.SWS_CS_DEFAULT + class ColorRange(IntEnum): UNSPECIFIED: "Unspecified" = lib.AVCOL_RANGE_UNSPECIFIED MPEG: "MPEG (limited) YUV range, 219*2^(n-8)" = lib.AVCOL_RANGE_MPEG @@ -58,7 +58,8 @@ def _resolve_enum_value(value, enum_class, default): raise ValueError(f"Cannot convert {value} to {enum_class.__name__}") -cdef class VideoReformatter: +@cython.cclass +class VideoReformatter: """An object for reformatting size and pixel format of :class:`.VideoFrame`. It is most efficient to have a reformatter object for each set of parameters @@ -67,13 +68,21 @@ def _resolve_enum_value(value, enum_class, default): """ def __dealloc__(self): - with nogil: + with cython.nogil: lib.sws_freeContext(self.ptr) - def reformat(self, VideoFrame frame not None, width=None, height=None, - format=None, src_colorspace=None, dst_colorspace=None, - interpolation=None, src_color_range=None, - dst_color_range=None): + def reformat( + self, + frame: VideoFrame, + width=None, + height=None, + format=None, + src_colorspace=None, + dst_colorspace=None, + interpolation=None, + src_color_range=None, + dst_color_range=None, + ): """Create a new :class:`VideoFrame` with the given width/height/format/colorspace. Returns the same frame untouched if nothing needs to be done to it. @@ -95,13 +104,24 @@ def reformat(self, VideoFrame frame not None, width=None, height=None, """ - cdef VideoFormat video_format = VideoFormat(format if format is not None else frame.format) - - cdef int c_src_colorspace = _resolve_enum_value(src_colorspace, Colorspace, frame.colorspace) - cdef int c_dst_colorspace = _resolve_enum_value(dst_colorspace, Colorspace, frame.colorspace) - cdef int c_interpolation = _resolve_enum_value(interpolation, Interpolation, int(Interpolation.BILINEAR)) - cdef int c_src_color_range = _resolve_enum_value(src_color_range, ColorRange, 0) - cdef int c_dst_color_range = _resolve_enum_value(dst_color_range, ColorRange, 0) + video_format: VideoFormat = VideoFormat( + format if format is not None else frame.format + ) + c_src_colorspace: cython.int = _resolve_enum_value( + src_colorspace, Colorspace, frame.colorspace + ) + c_dst_colorspace: cython.int = _resolve_enum_value( + dst_colorspace, Colorspace, frame.colorspace + ) + c_interpolation: cython.int = _resolve_enum_value( + interpolation, Interpolation, int(Interpolation.BILINEAR) + ) + c_src_color_range: cython.int = _resolve_enum_value( + src_color_range, ColorRange, 0 + ) + c_dst_color_range: cython.int = _resolve_enum_value( + dst_color_range, ColorRange, 0 + ) return self._reformat( frame, @@ -115,11 +135,19 @@ def reformat(self, VideoFrame frame not None, width=None, height=None, c_dst_color_range, ) - cdef _reformat(self, VideoFrame frame, int width, int height, - lib.AVPixelFormat dst_format, int src_colorspace, - int dst_colorspace, int interpolation, - int src_color_range, int dst_color_range): - + @cython.cfunc + def _reformat( + self, + frame: VideoFrame, + width: cython.int, + height: cython.int, + dst_format: lib.AVPixelFormat, + src_colorspace: cython.int, + dst_colorspace: cython.int, + interpolation: cython.int, + src_color_range: cython.int, + dst_color_range: cython.int, + ): if frame.ptr.format < 0: raise ValueError("Frame does not have format set.") @@ -127,19 +155,19 @@ def reformat(self, VideoFrame frame not None, width=None, height=None, src_color_range = 1 if src_color_range == ColorRange.JPEG.value else 0 dst_color_range = 1 if dst_color_range == ColorRange.JPEG.value else 0 - cdef lib.AVPixelFormat src_format = frame.ptr.format + src_format = cython.cast(lib.AVPixelFormat, frame.ptr.format) # Shortcut! if ( - dst_format == src_format and - width == frame.ptr.width and - height == frame.ptr.height and - dst_colorspace == src_colorspace and - src_color_range == dst_color_range + dst_format == src_format + and width == frame.ptr.width + and height == frame.ptr.height + and dst_colorspace == src_colorspace + and src_color_range == dst_color_range ): return frame - with nogil: + with cython.nogil: self.ptr = lib.sws_getCachedContext( self.ptr, frame.ptr.width, @@ -149,36 +177,37 @@ def reformat(self, VideoFrame frame not None, width=None, height=None, height, dst_format, interpolation, - NULL, - NULL, - NULL + cython.NULL, + cython.NULL, + cython.NULL, ) # We want to change the colorspace/color_range transforms. - # We do that by grabbing all of the current settings, changing a + # We do that by grabbing all the current settings, changing a # couple, and setting them all. We need a lot of state here. - cdef int *inv_tbl - cdef int *tbl - cdef int src_colorspace_range, dst_colorspace_range - cdef int brightness, contrast, saturation - cdef int ret + inv_tbl: cython.p_int + tbl: cython.p_int + src_colorspace_range: cython.int + dst_colorspace_range: cython.int + brightness: cython.int + contrast: cython.int + saturation: cython.int if src_colorspace != dst_colorspace or src_color_range != dst_color_range: - with nogil: + with cython.nogil: ret = lib.sws_getColorspaceDetails( self.ptr, - &inv_tbl, - &src_colorspace_range, - &tbl, - &dst_colorspace_range, - &brightness, - &contrast, - &saturation + cython.address(inv_tbl), + cython.address(src_colorspace_range), + cython.address(tbl), + cython.address(dst_colorspace_range), + cython.address(brightness), + cython.address(contrast), + cython.address(saturation), ) - err_check(ret) - with nogil: + with cython.nogil: # Grab the coefficients for the requested transforms. # The inv_table brings us to linear, and `tbl` to the new space. if src_colorspace != lib.SWS_CS_DEFAULT: @@ -186,7 +215,6 @@ def reformat(self, VideoFrame frame not None, width=None, height=None, if dst_colorspace != lib.SWS_CS_DEFAULT: tbl = lib.sws_getCoefficients(dst_colorspace) - # Apply! ret = lib.sws_setColorspaceDetails( self.ptr, inv_tbl, @@ -195,21 +223,18 @@ def reformat(self, VideoFrame frame not None, width=None, height=None, dst_color_range, brightness, contrast, - saturation + saturation, ) - err_check(ret) - # Create a new VideoFrame. - cdef VideoFrame new_frame = alloc_video_frame() + new_frame: VideoFrame = alloc_video_frame() new_frame._copy_internal_attributes(frame) new_frame._init(dst_format, width, height) - # Finally, scale the image. - with nogil: + with cython.nogil: lib.sws_scale( self.ptr, - frame.ptr.data, + frame.ptr.data, frame.ptr.linesize, 0, # slice Y frame.ptr.height, From 90bed5ad7a34a6fdc7797783a2949b4c7a615bc0 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 22 Jan 2026 01:00:47 -0500 Subject: [PATCH 643/846] Make av/buffer pure --- av/{buffer.pyx => buffer.py} | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) rename av/{buffer.pyx => buffer.py} (60%) diff --git a/av/buffer.pyx b/av/buffer.py similarity index 60% rename from av/buffer.pyx rename to av/buffer.py index 20c7c4877..047d1a157 100644 --- a/av/buffer.pyx +++ b/av/buffer.py @@ -1,25 +1,27 @@ -from cpython cimport PyBUF_WRITABLE, PyBuffer_FillInfo -from libc.string cimport memcpy +import cython +from cython.cimports.av.bytesource import ByteSource, bytesource +from cython.cimports.cpython import PyBUF_WRITABLE, PyBuffer_FillInfo +from cython.cimports.libc.string import memcpy -from av.bytesource cimport ByteSource, bytesource - -cdef class Buffer: +@cython.cclass +class Buffer: """A base class for PyAV objects which support the buffer protocol, such as :class:`.Packet` and :class:`.Plane`. """ - cdef size_t _buffer_size(self): + @cython.cfunc + def _buffer_size(self) -> cython.size_t: return 0 - cdef void* _buffer_ptr(self): - return NULL + def _buffer_ptr(self) -> cython.p_void: + return cython.NULL - cdef bint _buffer_writable(self): + def _buffer_writable(self) -> cython.bint: return True - def __getbuffer__(self, Py_buffer *view, int flags): + def __getbuffer__(self, view: cython.pointer[Py_buffer], flags: cython.int): if flags & PyBUF_WRITABLE and not self._buffer_writable(): raise ValueError("buffer is not writable") @@ -33,20 +35,20 @@ def buffer_size(self): @property def buffer_ptr(self): """The memory address of the buffer.""" - return self._buffer_ptr() + return cython.cast(cython.size_t, self._buffer_ptr()) def update(self, input): """Replace the data in this object with the given buffer. Accepts anything that supports the `buffer protocol `_, - e.g. bytes, Numpy arrays, other :class:`Buffer` objects, etc.. + e.g. bytes, NumPy arrays, other :class:`Buffer` objects, etc.. """ if not self._buffer_writable(): raise ValueError("buffer is not writable") - cdef ByteSource source = bytesource(input) - cdef size_t size = self._buffer_size() + source: ByteSource = bytesource(input) + size: cython.size_t = self._buffer_size() if source.length != size: raise ValueError(f"got {source.length} bytes; need {size} bytes") From 8aa3b368aa2987f680ff5f730ff06816b5172288 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 22 Jan 2026 01:05:26 -0500 Subject: [PATCH 644/846] Fix sign warning --- av/subtitles/subtitle.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/av/subtitles/subtitle.py b/av/subtitles/subtitle.py index cdf0e16ec..90eef433f 100644 --- a/av/subtitles/subtitle.py +++ b/av/subtitles/subtitle.py @@ -1,6 +1,5 @@ import cython from cython.cimports.cpython import PyBuffer_FillInfo, PyBytes_FromString -from cython.cimports.libc.stdint import int64_t, uint64_t from cython.cimports.libc.string import memcpy, strlen @@ -10,7 +9,7 @@ def __dealloc__(self): lib.avsubtitle_free(cython.address(self.struct)) -_cinit_bypass_sentinel = object() +_cinit_bypass_sentinel = cython.declare(object, object()) @cython.cclass @@ -293,7 +292,7 @@ def dialogue(self): Extract the dialogue from the ass format. Strip comments. """ comma_count: cython.short = 0 - i: uint64_t = 0 + i: cython.Py_ssize_t = 0 state: cython.bint = False ass_text: bytes = self.ass char, next_char = cython.declare(cython.char) From 68ec10f6680acf99503943ff03fea929a6e8ec80 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 22 Jan 2026 01:22:56 -0500 Subject: [PATCH 645/846] Make filter/graph pure --- av/filter/{graph.pyx => graph.py} | 120 +++++++++++++++++++----------- 1 file changed, 75 insertions(+), 45 deletions(-) rename av/filter/{graph.pyx => graph.py} (68%) diff --git a/av/filter/graph.pyx b/av/filter/graph.py similarity index 68% rename from av/filter/graph.pyx rename to av/filter/graph.py index c1a2d7a06..471b6ebc4 100644 --- a/av/filter/graph.pyx +++ b/av/filter/graph.py @@ -1,22 +1,23 @@ import warnings from fractions import Fraction -from av.audio.format cimport AudioFormat -from av.audio.frame cimport AudioFrame -from av.audio.layout cimport AudioLayout -from av.error cimport err_check -from av.filter.context cimport FilterContext, wrap_filter_context -from av.filter.filter cimport Filter, wrap_filter -from av.video.format cimport VideoFormat -from av.video.frame cimport VideoFrame - - -cdef class Graph: +import cython +from cython.cimports.av.audio.format import AudioFormat +from cython.cimports.av.audio.frame import AudioFrame +from cython.cimports.av.audio.layout import AudioLayout +from cython.cimports.av.error import err_check +from cython.cimports.av.filter.context import FilterContext, wrap_filter_context +from cython.cimports.av.filter.filter import Filter, wrap_filter +from cython.cimports.av.video.format import VideoFormat +from cython.cimports.av.video.frame import VideoFrame + + +@cython.cclass +class Graph: def __cinit__(self): self.ptr = lib.avfilter_graph_alloc() self.configured = False self._name_counts = {} - self._nb_filters_seen = 0 self._context_by_ptr = {} self._context_by_name = {} @@ -25,9 +26,10 @@ def __cinit__(self): def __dealloc__(self): if self.ptr: # This frees the graph, filter contexts, links, etc.. - lib.avfilter_graph_free(&self.ptr) + lib.avfilter_graph_free(cython.address(self.ptr)) - cdef str _get_unique_name(self, str name): + @cython.cfunc + def _get_unique_name(self, name: str) -> str: count = self._name_counts.get(name, 0) self._name_counts[name] = count + 1 if count: @@ -35,11 +37,12 @@ def __dealloc__(self): else: return name - cpdef configure(self, bint auto_buffer=True, bint force=False): + @cython.ccall + def configure(self, auto_buffer: cython.bint = True, force: cython.bint = False): if self.configured and not force: return - err_check(lib.avfilter_graph_config(self.ptr, NULL)) + err_check(lib.avfilter_graph_config(self.ptr, cython.NULL)) self.configured = True # We get auto-inserted stuff here. @@ -54,7 +57,7 @@ def link_nodes(self, *nodes): return self def add(self, filter, args=None, **kwargs): - cdef Filter cy_filter + cy_filter: Filter if isinstance(filter, str): cy_filter = Filter(filter) elif isinstance(filter, Filter): @@ -62,14 +65,15 @@ def add(self, filter, args=None, **kwargs): else: raise TypeError("filter must be a string or Filter") - cdef str name = self._get_unique_name(kwargs.pop("name", None) or cy_filter.name) - - cdef lib.AVFilterContext *ptr = lib.avfilter_graph_alloc_filter(self.ptr, cy_filter.ptr, name) + name: str = self._get_unique_name(kwargs.pop("name", None) or cy_filter.name) + ptr: cython.pointer[lib.AVFilterContext] = lib.avfilter_graph_alloc_filter( + self.ptr, cy_filter.ptr, name + ) if not ptr: raise RuntimeError("Could not allocate AVFilterContext") # Manually construct this context (so we can return it). - cdef FilterContext ctx = wrap_filter_context(self, cy_filter, ptr) + ctx: FilterContext = wrap_filter_context(self, cy_filter, ptr) ctx.init(args, **kwargs) self._register_context(ctx) @@ -80,28 +84,38 @@ def add(self, filter, args=None, **kwargs): return ctx - cdef _register_context(self, FilterContext ctx): - self._context_by_ptr[ctx.ptr] = ctx + @cython.cfunc + def _register_context(self, ctx: FilterContext): + self._context_by_ptr[cython.cast(cython.long, ctx.ptr)] = ctx self._context_by_name[ctx.ptr.name] = ctx self._context_by_type.setdefault(ctx.filter.ptr.name, []).append(ctx) - cdef _auto_register(self): - cdef int i - cdef lib.AVFilterContext *c_ctx - cdef Filter filter_ - cdef FilterContext py_ctx + @cython.cfunc + def _auto_register(self): + i: cython.int + c_ctx: cython.pointer[lib.AVFilterContext] + filter_: Filter + py_ctx: FilterContext # We assume that filters are never removed from the graph. At this # point we don't expose that in the API, so we should be okay... for i in range(self._nb_filters_seen, self.ptr.nb_filters): c_ctx = self.ptr.filters[i] - if c_ctx in self._context_by_ptr: + if cython.cast(cython.long, c_ctx) in self._context_by_ptr: continue filter_ = wrap_filter(c_ctx.filter) py_ctx = wrap_filter_context(self, filter_, c_ctx) self._register_context(py_ctx) self._nb_filters_seen = self.ptr.nb_filters - def add_buffer(self, template=None, width=None, height=None, format=None, name=None, time_base=None): + def add_buffer( + self, + template=None, + width=None, + height=None, + format=None, + name=None, + time_base=None, + ): if template is not None: if width is None: width = template.width @@ -119,9 +133,11 @@ def add_buffer(self, template=None, width=None, height=None, format=None, name=N if format is None: raise ValueError("missing format") if time_base is None: - warnings.warn("missing time_base. Guessing 1/1000 time base. " - "This is deprecated and may be removed in future releases.", - DeprecationWarning) + warnings.warn( + "missing time_base. Guessing 1/1000 time base. " + "This is deprecated and may be removed in future releases.", + DeprecationWarning, + ) time_base = Fraction(1, 1000) return self.add( @@ -133,7 +149,16 @@ def add_buffer(self, template=None, width=None, height=None, format=None, name=N pixel_aspect="1/1", ) - def add_abuffer(self, template=None, sample_rate=None, format=None, layout=None, channels=None, name=None, time_base=None): + def add_abuffer( + self, + template=None, + sample_rate=None, + format=None, + layout=None, + channels=None, + name=None, + time_base=None, + ): """ Convenience method for adding `abuffer `_. """ @@ -159,15 +184,15 @@ def add_abuffer(self, template=None, sample_rate=None, format=None, layout=None, if time_base is None: time_base = Fraction(1, sample_rate) - kwargs = dict( - sample_rate=str(sample_rate), - sample_fmt=AudioFormat(format).name, - time_base=str(time_base), - ) + kwargs = { + "sample_rate": f"{sample_rate}", + "sample_fmt": AudioFormat(format).name, + "time_base": f"{time_base}", + } if layout: kwargs["channel_layout"] = AudioLayout(layout).name if channels: - kwargs["channels"] = str(channels) + kwargs["channels"] = f"{channels}" return self.add("abuffer", name=name, **kwargs) @@ -182,27 +207,32 @@ def set_audio_frame_size(self, frame_size): if not sinks: raise ValueError("missing abuffersink filter") for sink in sinks: - lib.av_buffersink_set_frame_size((sink).ptr, frame_size) + lib.av_buffersink_set_frame_size( + cython.cast(FilterContext, sink).ptr, frame_size + ) def push(self, frame): if frame is None: - contexts = self._context_by_type.get("buffer", []) + self._context_by_type.get("abuffer", []) + contexts = self._context_by_type.get( + "buffer", [] + ) + self._context_by_type.get("abuffer", []) elif isinstance(frame, VideoFrame): contexts = self._context_by_type.get("buffer", []) elif isinstance(frame, AudioFrame): contexts = self._context_by_type.get("abuffer", []) else: - raise ValueError(f"can only AudioFrame, VideoFrame or None; got {type(frame)}") + raise ValueError( + f"can only AudioFrame, VideoFrame or None; got {type(frame)}" + ) for ctx in contexts: ctx.push(frame) - def vpush(self, VideoFrame frame): + def vpush(self, frame: VideoFrame | None): """Like `push`, but only for VideoFrames.""" for ctx in self._context_by_type.get("buffer", []): ctx.push(frame) - # TODO: Test complex filter graphs, add `at: int = 0` arg to pull() and vpull(). def pull(self): vsinks = self._context_by_type.get("buffersink", []) From ea4fd39719620fb9a41bc43edfc5034af7737961 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 22 Jan 2026 02:07:42 -0500 Subject: [PATCH 646/846] Make container/core pure --- av/container/{core.pyx => core.py} | 362 ++++++++++++++++++----------- 1 file changed, 229 insertions(+), 133 deletions(-) rename av/container/{core.pyx => core.py} (63%) diff --git a/av/container/core.pyx b/av/container/core.py similarity index 63% rename from av/container/core.pyx rename to av/container/core.py index 53c873372..ebdea47e6 100755 --- a/av/container/core.pyx +++ b/av/container/core.py @@ -1,118 +1,148 @@ -from cython.operator cimport dereference -from libc.stdint cimport int64_t - import os import time from enum import Flag, IntEnum from pathlib import Path -cimport libav as lib - -from av.codec.hwaccel cimport HWAccel -from av.container.core cimport timeout_info -from av.container.input cimport InputContainer -from av.container.output cimport OutputContainer -from av.container.pyio cimport pyio_close_custom_gil, pyio_close_gil -from av.error cimport err_check, stash_exception -from av.format cimport build_container_format -from av.utils cimport ( +import cython +import cython.cimports.libav as lib +from cython.cimports.av.codec.hwaccel import HWAccel +from cython.cimports.av.container.core import timeout_info +from cython.cimports.av.container.input import InputContainer +from cython.cimports.av.container.output import OutputContainer +from cython.cimports.av.container.pyio import pyio_close_custom_gil, pyio_close_gil +from cython.cimports.av.error import err_check, stash_exception +from cython.cimports.av.format import build_container_format +from cython.cimports.av.utils import ( avdict_to_dict, avrational_to_fraction, dict_to_avdict, to_avrational, ) +from cython.cimports.libc.stdint import int64_t +from cython.operator import dereference from av.dictionary import Dictionary from av.logging import Capture as LogCapture +_cinit_sentinel = cython.declare(object, object()) -cdef object _cinit_sentinel = object() +AVChapterPtrPtr = cython.typedef(cython.pointer[cython.pointer[lib.AVChapter]]) -cdef int interrupt_cb(void *p) noexcept nogil: - cdef timeout_info info = dereference( p) + +@cython.cfunc +@cython.nogil +@cython.exceptval(check=False) +def interrupt_cb(p: cython.p_void) -> cython.int: + info: timeout_info = dereference(cython.cast(cython.pointer[timeout_info], p)) if info.timeout < 0: # timeout < 0 means no timeout return 0 - cdef double current_time - with gil: + current_time: cython.double + with cython.gil: current_time = time.monotonic() if current_time < info.start_time: # Raise this when we get back to Python. - stash_exception((RuntimeError, RuntimeError("Clock has been changed to before timeout start"), None)) + stash_exception( + ( + RuntimeError, + RuntimeError("Clock has been changed to before timeout start"), + None, + ) + ) return 1 if current_time > info.start_time + info.timeout: return 1 - return 0 -cdef int pyav_io_open(lib.AVFormatContext *s, - lib.AVIOContext **pb, - const char *url, - int flags, - lib.AVDictionary **options) noexcept nogil: - with gil: +@cython.cfunc +@cython.nogil +@cython.exceptval(check=False) +def pyav_io_open( + s: cython.pointer[lib.AVFormatContext], + pb: cython.pointer[cython.pointer[lib.AVIOContext]], + url: cython.p_const_char, + flags: cython.int, + options: cython.pointer[cython.pointer[lib.AVDictionary]], +) -> cython.int: + with cython.gil: return pyav_io_open_gil(s, pb, url, flags, options) -cdef int pyav_io_open_gil(lib.AVFormatContext *s, - lib.AVIOContext **pb, - const char *url, - int flags, - lib.AVDictionary **options) noexcept: - cdef Container container - cdef object file - cdef PyIOFile pyio_file +@cython.cfunc +@cython.exceptval(check=False) +def pyav_io_open_gil( + s: cython.pointer[lib.AVFormatContext], + pb: cython.pointer[cython.pointer[lib.AVIOContext]], + url: cython.p_const_char, + flags: cython.int, + options: cython.pointer[cython.pointer[lib.AVDictionary]], +) -> cython.int: + container: Container + file: object + pyio_file: PyIOFile try: - container = dereference(s).opaque + container = cython.cast(Container, dereference(s).opaque) - if options is not NULL: + if options is not cython.NULL: options_dict = avdict_to_dict( - dereference(options), + dereference( + cython.cast( + cython.pointer[cython.pointer[lib.AVDictionary]], options + ) + ), encoding=container.metadata_encoding, - errors=container.metadata_errors + errors=container.metadata_errors, ) else: options_dict = {} file = container.io_open( - url if url is not NULL else "", - flags, - options_dict + cython.cast(str, url) if url is not cython.NULL else "", flags, options_dict ) pyio_file = PyIOFile( - file, - container.buffer_size, - (flags & lib.AVIO_FLAG_WRITE) != 0 + file, container.buffer_size, (flags & lib.AVIO_FLAG_WRITE) != 0 ) # Add it to the container to avoid it being deallocated - container.open_files[pyio_file.iocontext.opaque] = pyio_file - + container.open_files[cython.cast(int64_t, pyio_file.iocontext.opaque)] = ( + pyio_file + ) pb[0] = pyio_file.iocontext return 0 - except Exception: return stash_exception() -cdef int pyav_io_close(lib.AVFormatContext *s, lib.AVIOContext *pb) noexcept nogil: - with gil: +@cython.cfunc +@cython.nogil +@cython.exceptval(check=False) +def pyav_io_close( + s: cython.pointer[lib.AVFormatContext], pb: cython.pointer[lib.AVIOContext] +) -> cython.int: + with cython.gil: return pyav_io_close_gil(s, pb) -cdef int pyav_io_close_gil(lib.AVFormatContext *s, lib.AVIOContext *pb) noexcept: - cdef Container container - cdef int result = 0 - try: - container = dereference(s).opaque - if container.open_files is not None and pb.opaque in container.open_files: +@cython.cfunc +@cython.exceptval(check=False) +def pyav_io_close_gil( + s: cython.pointer[lib.AVFormatContext], pb: cython.pointer[lib.AVIOContext] +) -> cython.int: + container: Container + result: cython.int = 0 + try: + container = cython.cast(Container, dereference(s).opaque) + if ( + container.open_files is not None + and cython.cast(int64_t, pb.opaque) in container.open_files + ): result = pyio_close_custom_gil(pb) # Remove it from the container so that it can be deallocated - del container.open_files[pb.opaque] + del container.open_files[cython.cast(int64_t, pb.opaque)] else: result = pyio_close_gil(pb) @@ -122,18 +152,25 @@ return result -cdef void _free_chapters(lib.AVFormatContext *ctx) noexcept nogil: - cdef int i - if ctx.chapters != NULL: - for i in range(ctx.nb_chapters): - if ctx.chapters[i] != NULL: - if ctx.chapters[i].metadata != NULL: - lib.av_dict_free(&ctx.chapters[i].metadata) - lib.av_freep(&ctx.chapters[i]) - lib.av_freep(&ctx.chapters) - ctx.nb_chapters = 0 + +@cython.cfunc +@cython.nogil +@cython.exceptval(check=False) +def _free_chapters(ctx: cython.pointer[lib.AVFormatContext]) -> cython.void: + i: cython.Py_ssize_t + if ctx.chapters != cython.NULL: + for i in range(ctx.nb_chapters): + if ctx.chapters[i] != cython.NULL: + if ctx.chapters[i].metadata != cython.NULL: + lib.av_dict_free(cython.address(ctx.chapters[i].metadata)) + lib.av_freep( + cython.cast(cython.pp_void, cython.address(ctx.chapters[i])) + ) + lib.av_freep(cython.cast(cython.pp_void, cython.address(ctx.chapters))) + ctx.nb_chapters = 0 +# fmt: off class Flags(Flag): gen_pts: "Generate missing pts even if it requires parsing future frames." = lib.AVFMT_FLAG_GENPTS ign_idx: "Ignore index." = lib.AVFMT_FLAG_IGNIDX @@ -187,15 +224,27 @@ class AudioCodec(IntEnum): pcm_u32le = lib.AV_CODEC_ID_PCM_U32LE # PCM unsigned 32-bit little-endian. pcm_u8 = lib.AV_CODEC_ID_PCM_U8 # PCM unsigned 8-bit. pcm_vidc = lib.AV_CODEC_ID_PCM_VIDC # PCM VIDC. - - -cdef class Container: - def __cinit__(self, sentinel, file_, format_name, options, - container_options, stream_options, hwaccel, - metadata_encoding, metadata_errors, - buffer_size, open_timeout, read_timeout, - io_open): - +# fmt: on + + +@cython.cclass +class Container: + def __cinit__( + self, + sentinel, + file_, + format_name, + options, + container_options, + stream_options, + hwaccel, + metadata_encoding, + metadata_errors, + buffer_size, + open_timeout, + read_timeout, + io_open, + ): if sentinel is not _cinit_sentinel: raise RuntimeError("cannot construct base Container") @@ -230,28 +279,29 @@ def __cinit__(self, sentinel, file_, format_name, options, self.format = ContainerFormat(format_name) self.input_was_opened = False - cdef int res + res: cython.int + name_obj: bytes = os.fsencode(self.name) + name: cython.p_char = name_obj + ofmt: cython.pointer[lib.AVOutputFormat] - cdef bytes name_obj = os.fsencode(self.name) - cdef char *name = name_obj - - cdef lib.AVOutputFormat *ofmt if self.writeable: - - ofmt = self.format.optr if self.format else lib.av_guess_format(NULL, name, NULL) - if ofmt == NULL: + ofmt = ( + self.format.optr + if self.format + else lib.av_guess_format(cython.NULL, name, cython.NULL) + ) + if ofmt == cython.NULL: raise ValueError("Could not determine output format") - with nogil: + with cython.nogil: # This does not actually open the file. res = lib.avformat_alloc_output_context2( - &self.ptr, + cython.address(self.ptr), ofmt, - NULL, + cython.NULL, name, ) self.err_check(res) - else: # We need the context before we open the input AND setup Python IO. self.ptr = lib.avformat_alloc_context() @@ -259,13 +309,15 @@ def __cinit__(self, sentinel, file_, format_name, options, # Setup interrupt callback if self.open_timeout is not None or self.read_timeout is not None: self.ptr.interrupt_callback.callback = interrupt_cb - self.ptr.interrupt_callback.opaque = &self.interrupt_callback_info + self.ptr.interrupt_callback.opaque = cython.address( + self.interrupt_callback_info + ) if acodec is not None: self.ptr.audio_codec_id = getattr(AudioCodec, acodec) self.ptr.flags |= lib.AVFMT_FLAG_GENPTS - self.ptr.opaque = self + self.ptr.opaque = cython.cast(cython.p_void, self) # Setup Python IO. self.open_files = {} @@ -278,16 +330,18 @@ def __cinit__(self, sentinel, file_, format_name, options, self.ptr.io_close2 = pyav_io_close self.ptr.flags |= lib.AVFMT_FLAG_CUSTOM_IO - cdef lib.AVInputFormat *ifmt - cdef _Dictionary c_options + ifmt: cython.pointer[lib.AVInputFormat] + c_options: _Dictionary if not self.writeable: - ifmt = self.format.iptr if self.format else NULL + ifmt = self.format.iptr if self.format else cython.NULL c_options = Dictionary(self.options, self.container_options) self.set_timeout(self.open_timeout) self.start_timeout() - with nogil: - res = lib.avformat_open_input(&self.ptr, name, ifmt, &c_options.ptr) + with cython.nogil: + res = lib.avformat_open_input( + cython.address(self.ptr), name, ifmt, cython.address(c_options.ptr) + ) self.set_timeout(None) self.err_check(res) self.input_was_opened = True @@ -296,7 +350,7 @@ def __cinit__(self, sentinel, file_, format_name, options, self.format = build_container_format(self.ptr.iformat, self.ptr.oformat) def __dealloc__(self): - with nogil: + with cython.nogil: lib.avformat_free_context(self.ptr) def __enter__(self): @@ -308,7 +362,9 @@ def __exit__(self, exc_type, exc_val, exc_tb): def __repr__(self): return f"" - cdef int err_check(self, int value) except -1: + @cython.cfunc + @cython.exceptval(-1, check=False) + def err_check(self, value: cython.int) -> cython.int: return err_check(value, filename=self.name) def dumps_format(self): @@ -317,17 +373,20 @@ def dumps_format(self): lib.av_dump_format(self.ptr, 0, "", isinstance(self, OutputContainer)) return "".join(log[2] for log in logs) - cdef set_timeout(self, timeout): + @cython.cfunc + def set_timeout(self, timeout): if timeout is None: self.interrupt_callback_info.timeout = -1.0 else: self.interrupt_callback_info.timeout = timeout - cdef start_timeout(self): + @cython.cfunc + def start_timeout(self): self.interrupt_callback_info.start_time = time.monotonic() - cdef _assert_open(self): - if self.ptr == NULL: + @cython.cfunc + def _assert_open(self): + if self.ptr == cython.NULL: raise AssertionError("Container is not open") @property @@ -336,59 +395,74 @@ def flags(self): return self.ptr.flags @flags.setter - def flags(self, int value): + def flags(self, value: cython.int): self._assert_open() self.ptr.flags = value def chapters(self): self._assert_open() - cdef list result = [] - cdef int i - + result: list = [] + i: cython.Py_ssize_t for i in range(self.ptr.nb_chapters): ch = self.ptr.chapters[i] - result.append({ - "id": ch.id, - "start": ch.start, - "end": ch.end, - "time_base": avrational_to_fraction(&ch.time_base), - "metadata": avdict_to_dict(ch.metadata, self.metadata_encoding, self.metadata_errors), - }) + result.append( + { + "id": ch.id, + "start": ch.start, + "end": ch.end, + "time_base": avrational_to_fraction(cython.address(ch.time_base)), + "metadata": avdict_to_dict( + ch.metadata, self.metadata_encoding, self.metadata_errors + ), + } + ) return result def set_chapters(self, chapters): self._assert_open() - cdef int count = len(chapters) - cdef int i - cdef lib.AVChapter **ch_array - cdef lib.AVChapter *ch - cdef dict entry + count: cython.Py_ssize_t = len(chapters) + i: cython.Py_ssize_t + ch_array: AVChapterPtrPtr + ch: cython.pointer[lib.AVChapter] + entry: dict - with nogil: + with cython.nogil: _free_chapters(self.ptr) - ch_array = lib.av_malloc(count * sizeof(lib.AVChapter *)) - if ch_array == NULL: + ch_array = cython.cast( + AVChapterPtrPtr, + lib.av_malloc(count * cython.sizeof(cython.pointer[lib.AVChapter])), + ) + if ch_array == cython.NULL: raise MemoryError("av_malloc failed for chapters") for i in range(count): entry = chapters[i] - ch = lib.av_malloc(sizeof(lib.AVChapter)) - if ch == NULL: + ch = cython.cast( + cython.pointer[lib.AVChapter], + lib.av_malloc(cython.sizeof(lib.AVChapter)), + ) + if ch == cython.NULL: raise MemoryError("av_malloc failed for chapter") ch.id = entry["id"] - ch.start = entry["start"] - ch.end = entry["end"] - to_avrational(entry["time_base"], &ch.time_base) - ch.metadata = NULL + ch.start = cython.cast(int64_t, entry["start"]) + ch.end = cython.cast(int64_t, entry["end"]) + to_avrational(entry["time_base"], cython.address(ch.time_base)) + ch.metadata = cython.NULL if "metadata" in entry: - dict_to_avdict(&ch.metadata, entry["metadata"], self.metadata_encoding, self.metadata_errors) + dict_to_avdict( + cython.address(ch.metadata), + entry["metadata"], + self.metadata_encoding, + self.metadata_errors, + ) ch_array[i] = ch self.ptr.nb_chapters = count self.ptr.chapters = ch_array + def open( file, mode=None, @@ -401,7 +475,7 @@ def open( buffer_size=32768, timeout=None, io_open=None, - hwaccel=None + hwaccel=None, ): """open(file, mode='r', **kwargs) @@ -473,16 +547,38 @@ def open( read_timeout = timeout if mode.startswith("r"): - return InputContainer(_cinit_sentinel, file, format, options, - container_options, stream_options, hwaccel, metadata_encoding, metadata_errors, - buffer_size, open_timeout, read_timeout, io_open, + return InputContainer( + _cinit_sentinel, + file, + format, + options, + container_options, + stream_options, + hwaccel, + metadata_encoding, + metadata_errors, + buffer_size, + open_timeout, + read_timeout, + io_open, ) if stream_options: raise ValueError( "Provide stream options via Container.add_stream(..., options={})." ) - return OutputContainer(_cinit_sentinel, file, format, options, - container_options, stream_options, None, metadata_encoding, metadata_errors, - buffer_size, open_timeout, read_timeout, io_open, + return OutputContainer( + _cinit_sentinel, + file, + format, + options, + container_options, + stream_options, + None, + metadata_encoding, + metadata_errors, + buffer_size, + open_timeout, + read_timeout, + io_open, ) From 5e9cc00c653a996c1e4979d79a1c3ba0d42e9dac Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 22 Jan 2026 20:01:01 -0500 Subject: [PATCH 647/846] Make filter/context pure --- av/filter/{context.pyx => context.py} | 124 +++++++++++++++----------- 1 file changed, 71 insertions(+), 53 deletions(-) rename av/filter/{context.pyx => context.py} (55%) diff --git a/av/filter/context.pyx b/av/filter/context.py similarity index 55% rename from av/filter/context.pyx rename to av/filter/context.py index 85c23863b..5ca6d6e47 100644 --- a/av/filter/context.pyx +++ b/av/filter/context.py @@ -1,57 +1,69 @@ -cimport libav as lib - import weakref -from av.audio.frame cimport alloc_audio_frame -from av.dictionary cimport _Dictionary -from av.dictionary import Dictionary -from av.error cimport err_check -from av.filter.link cimport alloc_filter_pads -from av.frame cimport Frame -from av.utils cimport avrational_to_fraction -from av.video.frame cimport alloc_video_frame +import cython +import cython.cimports.libav as lib +from cython.cimports.av.audio.frame import alloc_audio_frame +from cython.cimports.av.dictionary import _Dictionary +from cython.cimports.av.error import err_check +from cython.cimports.av.filter.link import alloc_filter_pads +from cython.cimports.av.frame import Frame +from cython.cimports.av.utils import avrational_to_fraction +from cython.cimports.av.video.frame import alloc_video_frame +from av.dictionary import Dictionary -cdef object _cinit_sentinel = object() +_cinit_sentinel = cython.declare(object, object()) -cdef FilterContext wrap_filter_context(Graph graph, Filter filter, lib.AVFilterContext *ptr): - cdef FilterContext self = FilterContext(_cinit_sentinel) +@cython.cfunc +def wrap_filter_context( + graph: Graph, filter: Filter, ptr: cython.pointer[lib.AVFilterContext] +) -> FilterContext: + self: FilterContext = FilterContext(_cinit_sentinel) self._graph = weakref.ref(graph) self.filter = filter self.ptr = ptr return self -cdef class FilterContext: +@cython.cclass +class FilterContext: def __cinit__(self, sentinel): if sentinel is not _cinit_sentinel: raise RuntimeError("cannot construct FilterContext") def __repr__(self): - if self.ptr != NULL: - name = repr(self.ptr.name) if self.ptr.name != NULL else "" + if self.ptr != cython.NULL: + name = repr(self.ptr.name) if self.ptr.name != cython.NULL else "" else: name = "None" - parent = self.filter.ptr.name if self.filter and self.filter.ptr != NULL else None + parent = ( + self.filter.ptr.name + if self.filter and self.filter.ptr != cython.NULL + else None + ) return f"" @property def name(self): - if self.ptr.name != NULL: + if self.ptr.name != cython.NULL: return self.ptr.name @property def inputs(self): if self._inputs is None: - self._inputs = alloc_filter_pads(self.filter, self.ptr.input_pads, True, self) + self._inputs = alloc_filter_pads( + self.filter, self.ptr.input_pads, True, self + ) return self._inputs @property def outputs(self): if self._outputs is None: - self._outputs = alloc_filter_pads(self.filter, self.ptr.output_pads, False, self) + self._outputs = alloc_filter_pads( + self.filter, self.ptr.output_pads, False, self + ) return self._outputs def init(self, args=None, **kwargs): @@ -60,40 +72,45 @@ def init(self, args=None, **kwargs): if args and kwargs: raise ValueError("cannot init from args and kwargs") - cdef _Dictionary dict_ = None - cdef char *c_args = NULL + dict_: _Dictionary = None + c_args: cython.p_char = cython.NULL if args or not kwargs: if args: c_args = args err_check(lib.avfilter_init_str(self.ptr, c_args)) else: dict_ = Dictionary(kwargs) - err_check(lib.avfilter_init_dict(self.ptr, &dict_.ptr)) + err_check(lib.avfilter_init_dict(self.ptr, cython.address(dict_.ptr))) self.inited = True if dict_: raise ValueError(f"unused config: {', '.join(sorted(dict_))}") - def link_to(self, FilterContext input_, int output_idx=0, int input_idx=0): + def link_to( + self, + input_: FilterContext, + output_idx: cython.int = 0, + input_idx: cython.int = 0, + ): err_check(lib.avfilter_link(self.ptr, output_idx, input_.ptr, input_idx)) - + @property def graph(self): - if (graph := self._graph()): + if graph := self._graph(): return graph else: raise RuntimeError("graph is unallocated") - def push(self, Frame frame): - cdef int res + def push(self, frame: Frame | None): + res: cython.int if frame is None: - with nogil: - res = lib.av_buffersrc_write_frame(self.ptr, NULL) + with cython.nogil: + res = lib.av_buffersrc_write_frame(self.ptr, cython.NULL) err_check(res) return elif self.filter.name in ("abuffer", "buffer"): - with nogil: + with cython.nogil: res = lib.av_buffersrc_write_frame(self.ptr, frame.ptr) err_check(res) return @@ -108,9 +125,8 @@ def push(self, Frame frame): self.inputs[0].linked.context.push(frame) def pull(self): - cdef Frame frame - cdef int res - + frame: Frame + res: cython.int if self.filter.name == "buffersink": frame = alloc_video_frame() elif self.filter.name == "abuffersink": @@ -127,46 +143,48 @@ def pull(self): self.graph.configure() - with nogil: + with cython.nogil: res = lib.av_buffersink_get_frame(self.ptr, frame.ptr) err_check(res) frame._init_user_attributes() - frame.time_base = avrational_to_fraction(&self.ptr.inputs[0].time_base) + frame.time_base = avrational_to_fraction( + cython.address(self.ptr.inputs[0].time_base) + ) return frame - def process_command(self, cmd, arg=None, int res_len=1024, int flags=0): + def process_command( + self, cmd, arg=None, res_len: cython.int = 1024, flags: cython.int = 0 + ): if not cmd: raise ValueError("Invalid cmd") - cdef char *c_cmd = NULL - cdef char *c_arg = NULL - - c_cmd = cmd + c_arg: cython.p_char = cython.NULL + c_cmd: cython.p_char = cmd if arg is not None: c_arg = arg - cdef char *c_res = NULL - cdef int ret - cdef bytearray res_buf = None - cdef unsigned char[:] view - cdef bytes b - cdef int nul + c_res: cython.p_char = cython.NULL + ret: cython.int + res_buf: bytearray = None + view: cython.uchar[:] + b: bytes + nul: cython.int if res_len > 0: res_buf = bytearray(res_len) view = res_buf - c_res = &view[0] - else: - c_res = NULL + c_res = cython.cast(cython.p_char, cython.address(view[0])) - with nogil: - ret = lib.avfilter_process_command(self.ptr, c_cmd, c_arg, c_res, res_len, flags) + with cython.nogil: + ret = lib.avfilter_process_command( + self.ptr, c_cmd, c_arg, c_res, res_len, flags + ) err_check(ret) if res_buf is not None: b = bytes(res_buf) - nul = b.find(b'\x00') + nul = b.find(b"\x00") if nul >= 0: b = b[:nul] if b: From 69c34042b17d7d92102255818d63cded0bee6ac0 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 22 Jan 2026 20:39:22 -0500 Subject: [PATCH 648/846] Remove unused C defs --- include/libavcodec/avcodec.pxd | 132 ++++++++++++--------------------- 1 file changed, 46 insertions(+), 86 deletions(-) diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 1edc26ef6..1bf0d674a 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -257,84 +257,63 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVClass *av_class AVMediaType codec_type - char codec_name[32] - unsigned int codec_tag + AVCodec *codec AVCodecID codec_id - - int flags - int flags2 - int thread_count - int thread_type - - int profile - AVDiscard skip_frame - - AVFrame* coded_frame + unsigned int codec_tag void* opaque int bit_rate - int bit_rate_tolerance - int mb_decision - - int bits_per_coded_sample - int global_quality - int compression_level - - int qmin - int qmax - int rc_max_rate - int rc_min_rate - int rc_buffer_size - float rc_max_available_vbv_use - float rc_min_vbv_overflow_use - - AVRational framerate - AVRational pkt_timebase - AVRational time_base - - int extradata_size + int flags + int flags2 uint8_t *extradata - - # Subtitle header (ASS/SSA format for text subtitles) - uint8_t *subtitle_header - int subtitle_header_size - + int extradata_size + AVRational time_base + AVRational pkt_timebase + AVRational framerate int delay - - AVCodec *codec - - # Video. int width int height int coded_width int coded_height - + AVRational sample_aspect_ratio AVPixelFormat pix_fmt AVPixelFormat sw_pix_fmt - AVRational sample_aspect_ratio - int gop_size # The number of pictures in a group of pictures, or 0 for intra_only. - int max_b_frames - int has_b_frames - AVColorRange color_range AVColorPrimaries color_primaries AVColorTransferCharacteristic color_trc AVColorSpace colorspace + AVColorRange color_range - # Audio. - AVSampleFormat sample_fmt + int has_b_frames + AVPixelFormat (*get_format)(AVCodecContext *s, const AVPixelFormat *fmt) + int max_b_frames + int mb_decision + int gop_size int sample_rate + AVSampleFormat sample_fmt AVChannelLayout ch_layout int frame_size - #: .. todo:: ``get_buffer`` is deprecated for get_buffer2 in newer versions of FFmpeg. - int get_buffer(AVCodecContext *ctx, AVFrame *frame) - void release_buffer(AVCodecContext *ctx, AVFrame *frame) + int bit_rate_tolerance + int global_quality + int compression_level + int qmin + int qmax + int rc_buffer_size + int rc_max_rate + int rc_min_rate - # Hardware acceleration AVHWAccel *hwaccel AVBufferRef *hw_device_ctx - AVPixelFormat (*get_format)(AVCodecContext *s, const AVPixelFormat *fmt) + + int thread_count + int thread_type + int bits_per_coded_sample + int profile + AVDiscard skip_frame + + int subtitle_header_size + uint8_t *subtitle_header cdef AVCodecContext* avcodec_alloc_context3(AVCodec *codec) @@ -355,8 +334,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef char* avcodec_get_name(AVCodecID id) - cdef char* av_get_profile_name(AVCodec *codec, int profile) - cdef int avcodec_open2( AVCodecContext *ctx, AVCodec *codec, @@ -446,56 +423,48 @@ cdef extern from "libavcodec/avcodec.h" nogil: uint8_t *data[4] int linesize[4] uint8_t **extended_data - - int format # Should be AVPixelFormat or AVSampleFormat - AVPictureType pict_type - int width int height - - int nb_side_data - AVFrameSideData **side_data - int nb_samples - int sample_rate - - AVChannelLayout ch_layout + int format # -1 if unknown, AVPixelFormat or AVSampleFormat + AVPictureType pict_type int64_t pts int64_t pkt_dts - - int pkt_size - - uint8_t **base void *opaque - AVBufferRef *opaque_ref - AVDictionary *metadata + int sample_rate + int nb_side_data + AVFrameSideData **side_data int flags - int decode_error_flags AVColorRange color_range AVColorPrimaries color_primaries AVColorTransferCharacteristic color_trc AVColorSpace colorspace + AVDictionary *metadata + int decode_error_flags + + AVBufferRef *opaque_ref + AVChannelLayout ch_layout int64_t duration cdef AVFrame* avcodec_alloc_frame() cdef struct AVPacket: + void *buf # ptr[AVBufferRef] int64_t pts int64_t dts uint8_t *data - AVRational time_base int size int stream_index int flags AVPacketSideData *side_data int side_data_elems - int duration + int64_t duration int64_t pos void *opaque AVBufferRef *opaque_ref - + AVRational time_base cdef int avcodec_fill_audio_frame( AVFrame *frame, @@ -556,15 +525,8 @@ cdef extern from "libavcodec/avcodec.h" nogil: ) cdef void avsubtitle_free(AVSubtitle*) - - cdef void avcodec_get_frame_defaults(AVFrame* frame) - cdef void avcodec_flush_buffers(AVCodecContext *ctx) - # TODO: avcodec_default_get_buffer is deprecated for avcodec_default_get_buffer2 in newer versions of FFmpeg - cdef int avcodec_default_get_buffer(AVCodecContext *ctx, AVFrame *frame) - cdef void avcodec_default_release_buffer(AVCodecContext *ctx, AVFrame *frame) - # === New-style Transcoding cdef int avcodec_send_packet(AVCodecContext *avctx, AVPacket *packet) cdef int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame) @@ -576,8 +538,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef struct AVCodecParser: int codec_ids[5] - cdef AVCodecParser* av_parser_next(AVCodecParser *c) - cdef struct AVCodecParserContext: pass From d391fcaf1fc7b63d72c5416ddb3f6f659d56e810 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 22 Jan 2026 21:17:48 -0500 Subject: [PATCH 649/846] Remove old channel stuff --- include/libavcodec/avcodec.pxd | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 1bf0d674a..11cff34a1 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -1,22 +1,10 @@ -from libc.stdint cimport int8_t, int64_t, uint16_t, uint32_t, uint8_t +from libc.stdint cimport int64_t, uint16_t, uint32_t, uint8_t cdef extern from "libavcodec/codec.h": struct AVCodecTag: pass -cdef extern from "libavcodec/codec_id.h": - AVCodecID av_codec_get_id(const AVCodecTag *const *tags, uint32_t tag) - - cdef extern from "libavcodec/packet.h" nogil: - AVPacketSideData* av_packet_side_data_new( - AVPacketSideData **sides, - int *nb_sides, - AVPacketSideDataType type, - size_t size, - int free_opaque - ) - const AVPacketSideData *av_packet_side_data_get(const AVPacketSideData *sd, int nb_sd, AVPacketSideDataType type) @@ -41,12 +29,6 @@ cdef extern from "libavutil/channel_layout.h": AV_CHAN_FRONT_LEFT AV_CHAN_FRONT_RIGHT AV_CHAN_FRONT_CENTER - # ... other channel enum values ... - - ctypedef struct AVChannelCustom: - AVChannel id - char name[16] - void *opaque ctypedef struct AVChannelLayout: AVChannelOrder order From e4bfc349252ab633f4a1707462157265e069e364 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 22 Jan 2026 22:19:28 -0500 Subject: [PATCH 650/846] Use zero-copy for Packet init from buffer data Instead of allocating new memory and copying data when creating a Packet from a buffer (bytes, numpy array, etc.), reference the source buffer directly using av_buffer_create with a noop_free callback. The ByteSource is stored in self.source to keep the Python buffer alive. Co-Authored-By: Claude Opus 4.5 --- av/opaque.pxd | 4 ++++ av/opaque.pyx | 4 ++++ av/packet.py | 27 ++++++++++++++++----------- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/av/opaque.pxd b/av/opaque.pxd index 76174931f..808f6338e 100644 --- a/av/opaque.pxd +++ b/av/opaque.pxd @@ -1,4 +1,8 @@ cimport libav as lib +from libc.stdint cimport uint8_t + + +cdef void noop_free(void *opaque, uint8_t *data) noexcept nogil cdef class OpaqueContainer: diff --git a/av/opaque.pyx b/av/opaque.pyx index 626b02985..583ea5577 100644 --- a/av/opaque.pyx +++ b/av/opaque.pyx @@ -3,6 +3,10 @@ from libc.stdint cimport uint8_t, uintptr_t from libc.string cimport memcpy +cdef void noop_free(void *opaque, uint8_t *data) noexcept nogil: + pass + + cdef void key_free(void *opaque, uint8_t *data) noexcept nogil: cdef char *name = data with gil: diff --git a/av/packet.py b/av/packet.py index d21b432ea..867d46211 100644 --- a/av/packet.py +++ b/av/packet.py @@ -3,9 +3,9 @@ import cython from cython.cimports import libav as lib from cython.cimports.av.buffer import Buffer -from cython.cimports.av.bytesource import bytesource +from cython.cimports.av.bytesource import ByteSource, bytesource from cython.cimports.av.error import err_check -from cython.cimports.av.opaque import opaque_container +from cython.cimports.av.opaque import noop_free, opaque_container from cython.cimports.av.utils import avrational_to_fraction, to_avrational from cython.cimports.libc.string import memcpy @@ -213,24 +213,29 @@ def __dealloc__(self): def __init__(self, input=None): size: cython.size_t = 0 source: ByteSource = None + buf: cython.pointer[lib.AVBufferRef] if input is None: return if isinstance(input, int): size = input + if size: + err_check(lib.av_new_packet(self.ptr, size)) else: source = bytesource(input) size = source.length - - if size: - err_check(lib.av_new_packet(self.ptr, size)) - - if source is not None: - self.update(source) - # TODO: Hold onto the source, and copy its pointer - # instead of its data. - # self.source = source + if size: + # Create a buffer that references the source data directly. + # The noop_free callback is used because Python manages the memory + # via self.source keeping the ByteSource alive. + buf = lib.av_buffer_create(source.ptr, size, noop_free, cython.NULL, 0) + if buf == cython.NULL: + raise MemoryError("Could not allocate AVBufferRef") + self.ptr.buf = buf + self.ptr.data = source.ptr + self.ptr.size = size + self.source = source def __repr__(self): stream = self._stream.index if self._stream else 0 From 744b6131713e21f40ad186f1b391d5799010c9d9 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 22 Jan 2026 22:47:20 -0500 Subject: [PATCH 651/846] Make av.ArgumentError a subclass of ValueError Make av.ArgumentError a subclass of ValueError until the next major version (18.0). --- av/error.py | 2 +- tests/test_errors.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/av/error.py b/av/error.py index 8827aafc2..77fce88d1 100644 --- a/av/error.py +++ b/av/error.py @@ -290,7 +290,7 @@ def _extend_builtin(name, codes): _extend_builtin("OSError", [code for code in errno.errorcode if code not in classes]) -class ArgumentError(FFmpegError): +class ArgumentError(FFmpegError, ValueError): def __str__(self): msg = "" if self.strerror is not None: diff --git a/tests/test_errors.py b/tests/test_errors.py index 68e2681e1..fdfe0f49d 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -28,6 +28,8 @@ def test_stringify() -> None: def test_bases() -> None: assert issubclass(av.ArgumentError, av.FFmpegError) + # Deprecated, assert false for Major 18.0: + assert issubclass(av.ArgumentError, ValueError) assert issubclass(av.FileNotFoundError, FileNotFoundError) assert issubclass(av.FileNotFoundError, OSError) assert issubclass(av.FileNotFoundError, av.FFmpegError) From 825aabb9ac352a3032b8d7c560170657945637a9 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 22 Jan 2026 23:34:07 -0500 Subject: [PATCH 652/846] Make av/format pure --- av/{format.pyx => format.py} | 76 ++++++++++++++++++++---------------- 1 file changed, 43 insertions(+), 33 deletions(-) rename av/{format.pyx => format.py} (74%) diff --git a/av/format.pyx b/av/format.py similarity index 74% rename from av/format.pyx rename to av/format.py index 2eb386649..a0bf0bd90 100644 --- a/av/format.pyx +++ b/av/format.py @@ -1,22 +1,28 @@ -cimport libav as lib - -from av.descriptor cimport wrap_avclass - from enum import Flag +import cython +import cython.cimports.libav as lib +from cython.cimports.av.descriptor import wrap_avclass -cdef object _cinit_bypass_sentinel = object() +_cinit_bypass_sentinel = cython.declare(object, object()) -cdef ContainerFormat build_container_format(lib.AVInputFormat* iptr, lib.AVOutputFormat* optr): + +@cython.cfunc +def build_container_format( + iptr: cython.pointer[lib.AVInputFormat], optr: cython.pointer[lib.AVOutputFormat] +) -> ContainerFormat: if not iptr and not optr: raise ValueError("needs input format or output format") - cdef ContainerFormat format = ContainerFormat.__new__(ContainerFormat, _cinit_bypass_sentinel) + format: ContainerFormat = ContainerFormat.__new__( + ContainerFormat, _cinit_bypass_sentinel + ) format.iptr = iptr format.optr = optr format.name = optr.name if optr else iptr.name return format +# fmt: off class Flags(Flag): no_file = lib.AVFMT_NOFILE need_number: "Needs '%d' in filename." = lib.AVFMT_NEEDNUMBER @@ -36,8 +42,11 @@ class Flags(Flag): # If not set the timestamp will be shifted in `av_write_frame()` and `av_interleaved_write_frame()` # so they start from 0. The user or muxer can override this through AVFormatContext.avoid_negative_ts seek_to_pts: "Seeking is based on PTS" = lib.AVFMT_SEEK_TO_PTS +# fmt: on + -cdef class ContainerFormat: +@cython.cclass +class ContainerFormat: """Descriptor of a container format. :param str name: The name of the format. @@ -59,7 +68,7 @@ def __cinit__(self, name, mode=None): self.iptr = lib.av_find_input_format(name) if mode is None or mode == "w": - self.optr = lib.av_guess_format(name, NULL, NULL) + self.optr = lib.av_guess_format(name, cython.NULL, cython.NULL) if not self.iptr and not self.optr: raise ValueError(f"no container format {name!r}") @@ -81,30 +90,30 @@ def options(self): @property def input(self): """An input-only view of this format.""" - if self.iptr == NULL: + if self.iptr == cython.NULL: return None - elif self.optr == NULL: + elif self.optr == cython.NULL: return self else: - return build_container_format(self.iptr, NULL) + return build_container_format(self.iptr, cython.NULL) @property def output(self): """An output-only view of this format.""" - if self.optr == NULL: + if self.optr == cython.NULL: return None - elif self.iptr == NULL: + elif self.iptr == cython.NULL: return self else: - return build_container_format(NULL, self.optr) + return build_container_format(cython.NULL, self.optr) @property def is_input(self): - return self.iptr != NULL + return self.iptr != cython.NULL @property def is_output(self): - return self.optr != NULL + return self.optr != cython.NULL @property def long_name(self): @@ -114,7 +123,7 @@ def long_name(self): @property def extensions(self): - cdef set exts = set() + exts: set = set() if self.iptr and self.iptr.extensions: exts.update(self.iptr.extensions.split(",")) if self.optr and self.optr.extensions: @@ -128,9 +137,8 @@ def flags(self): :rtype: int """ - return ( - (self.iptr.flags if self.iptr else 0) | - (self.optr.flags if self.optr else 0) + return (self.iptr.flags if self.iptr else 0) | ( + self.optr.flags if self.optr else 0 ) @property @@ -138,32 +146,34 @@ def no_file(self): return bool(self.flags & lib.AVFMT_NOFILE) -cdef get_output_format_names(): - names = set() - cdef const lib.AVOutputFormat *ptr - cdef void *opaque = NULL +@cython.cfunc +def get_output_format_names() -> set: + names: set = set() + ptr: cython.pointer[cython.const[lib.AVOutputFormat]] + opaque: cython.p_void = cython.NULL while True: - ptr = lib.av_muxer_iterate(&opaque) + ptr = lib.av_muxer_iterate(cython.address(opaque)) if ptr: names.add(ptr.name) else: break return names -cdef get_input_format_names(): - names = set() - cdef const lib.AVInputFormat *ptr - cdef void *opaque = NULL + +@cython.cfunc +def get_input_format_names() -> set: + names: set = set() + ptr: cython.pointer[cython.const[lib.AVInputFormat]] + opaque: cython.p_void = cython.NULL while True: - ptr = lib.av_demuxer_iterate(&opaque) + ptr = lib.av_demuxer_iterate(cython.address(opaque)) if ptr: names.add(ptr.name) else: break return names + formats_available = get_output_format_names() formats_available.update(get_input_format_names()) - - format_descriptor = wrap_avclass(lib.avformat_get_class()) From baeb028de953185db3c39fa60835399db80a5fc1 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 23 Jan 2026 13:57:37 -0500 Subject: [PATCH 653/846] Make av/bitstream pure --- av/bitstream.py | 115 +++++++++++++++++++++++++++++++++++++++ av/bitstream.pyx | 95 -------------------------------- av/container/streams.pyi | 1 - 3 files changed, 115 insertions(+), 96 deletions(-) create mode 100644 av/bitstream.py delete mode 100644 av/bitstream.pyx diff --git a/av/bitstream.py b/av/bitstream.py new file mode 100644 index 000000000..3c0bff1bd --- /dev/null +++ b/av/bitstream.py @@ -0,0 +1,115 @@ +import cython +import cython.cimports.libav as lib +from cython.cimports.av.error import err_check +from cython.cimports.av.packet import Packet +from cython.cimports.av.stream import Stream +from cython.cimports.libc.errno import EAGAIN + + +@cython.cclass +class BitStreamFilterContext: + """ + Initializes a bitstream filter: a way to directly modify packet data. + + Wraps :ffmpeg:`AVBSFContext` + + :param Stream in_stream: A stream that defines the input codec for the bitfilter. + :param Stream out_stream: A stream whose codec is overwritten using the output parameters from the bitfilter. + """ + + def __cinit__( + self, + filter_description, + in_stream: Stream | None = None, + out_stream: Stream | None = None, + ): + res: cython.int + filter_str: cython.p_char = filter_description + + with cython.nogil: + res = lib.av_bsf_list_parse_str(filter_str, cython.address(self.ptr)) + err_check(res) + + if in_stream is not None: + with cython.nogil: + res = lib.avcodec_parameters_copy( + self.ptr.par_in, in_stream.ptr.codecpar + ) + err_check(res) + + with cython.nogil: + res = lib.av_bsf_init(self.ptr) + err_check(res) + + if out_stream is not None: + with cython.nogil: + res = lib.avcodec_parameters_copy( + out_stream.ptr.codecpar, self.ptr.par_out + ) + err_check(res) + lib.avcodec_parameters_to_context( + out_stream.codec_context.ptr, out_stream.ptr.codecpar + ) + + def __dealloc__(self): + if self.ptr: + lib.av_bsf_free(cython.address(self.ptr)) + + @cython.ccall + def filter(self, packet: Packet | None = None): + """ + Processes a packet based on the filter_description set during initialization. + Multiple packets may be created. + + :type: list[Packet] + """ + res: cython.int + new_packet: Packet + + with cython.nogil: + res = lib.av_bsf_send_packet( + self.ptr, packet.ptr if packet is not None else cython.NULL + ) + err_check(res) + + output: list = [] + while True: + new_packet = Packet() + with cython.nogil: + res = lib.av_bsf_receive_packet(self.ptr, new_packet.ptr) + + if res == -EAGAIN or res == lib.AVERROR_EOF: + return output + + err_check(res) + if res: + return output + + output.append(new_packet) + + @cython.ccall + def flush(self): + """ + Reset the internal state of the filter. + Should be called e.g. when seeking. + Can be used to make the filter usable again after draining it with EOF marker packet. + """ + lib.av_bsf_flush(self.ptr) + + +@cython.cfunc +def get_filter_names() -> set: + names: set = set() + ptr: cython.pointer[cython.const[lib.AVBitStreamFilter]] + opaque: cython.p_void = cython.NULL + while True: + ptr = lib.av_bsf_iterate(cython.address(opaque)) + if ptr: + names.add(ptr.name) + else: + break + + return names + + +bitstream_filters_available = get_filter_names() diff --git a/av/bitstream.pyx b/av/bitstream.pyx deleted file mode 100644 index b5361f8c2..000000000 --- a/av/bitstream.pyx +++ /dev/null @@ -1,95 +0,0 @@ -cimport libav as lib -from libc.errno cimport EAGAIN - -from av.error cimport err_check -from av.packet cimport Packet -from av.stream cimport Stream - - -cdef class BitStreamFilterContext: - """ - Initializes a bitstream filter: a way to directly modify packet data. - - Wraps :ffmpeg:`AVBSFContext` - - :param Stream in_stream: A stream that defines the input codec for the bitfilter. - :param Stream out_stream: A stream whose codec is overwritten using the output parameters from the bitfilter. - """ - def __cinit__(self, filter_description, Stream in_stream=None, Stream out_stream=None): - cdef int res - cdef char *filter_str = filter_description - - with nogil: - res = lib.av_bsf_list_parse_str(filter_str, &self.ptr) - err_check(res) - - if in_stream is not None: - with nogil: - res = lib.avcodec_parameters_copy(self.ptr.par_in, in_stream.ptr.codecpar) - err_check(res) - - with nogil: - res = lib.av_bsf_init(self.ptr) - err_check(res) - - if out_stream is not None: - with nogil: - res = lib.avcodec_parameters_copy(out_stream.ptr.codecpar, self.ptr.par_out) - err_check(res) - lib.avcodec_parameters_to_context(out_stream.codec_context.ptr, out_stream.ptr.codecpar) - - def __dealloc__(self): - if self.ptr: - lib.av_bsf_free(&self.ptr) - - cpdef filter(self, Packet packet=None): - """ - Processes a packet based on the filter_description set during initialization. - Multiple packets may be created. - - :type: list[Packet] - """ - cdef int res - cdef Packet new_packet - - with nogil: - res = lib.av_bsf_send_packet(self.ptr, packet.ptr if packet is not None else NULL) - err_check(res) - - output = [] - while True: - new_packet = Packet() - with nogil: - res = lib.av_bsf_receive_packet(self.ptr, new_packet.ptr) - - if res == -EAGAIN or res == lib.AVERROR_EOF: - return output - - err_check(res) - if res: - return output - - output.append(new_packet) - - cpdef flush(self): - """ - Reset the internal state of the filter. - Should be called e.g. when seeking. - Can be used to make the filter usable again after draining it with EOF marker packet. - """ - lib.av_bsf_flush(self.ptr) - -cdef get_filter_names(): - names = set() - cdef const lib.AVBitStreamFilter *ptr - cdef void *opaque = NULL - while True: - ptr = lib.av_bsf_iterate(&opaque) - if ptr: - names.add(ptr.name) - else: - break - - return names - -bitstream_filters_available = get_filter_names() diff --git a/av/container/streams.pyi b/av/container/streams.pyi index 52b98818f..9e0df35a4 100644 --- a/av/container/streams.pyi +++ b/av/container/streams.pyi @@ -11,7 +11,6 @@ class StreamContainer: subtitles: tuple[SubtitleStream, ...] attachments: tuple[AttachmentStream, ...] data: tuple[DataStream, ...] - other: tuple[Stream, ...] def __init__(self) -> None: ... def __len__(self) -> int: ... From 24dcbf0f01f7267c437020a792fe6ef4a87746d3 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 23 Jan 2026 14:12:28 -0500 Subject: [PATCH 654/846] Remove unused libavdevice bindings --- include/libavdevice/avdevice.pxd | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/include/libavdevice/avdevice.pxd b/include/libavdevice/avdevice.pxd index bc9b4adea..31c654b10 100644 --- a/include/libavdevice/avdevice.pxd +++ b/include/libavdevice/avdevice.pxd @@ -1,12 +1,5 @@ - cdef extern from "libavdevice/avdevice.h" nogil: - - cdef int avdevice_version() + cdef int avdevice_version() cdef char* avdevice_configuration() cdef char* avdevice_license() - void avdevice_register_all() - - AVInputFormat * av_input_audio_device_next(AVInputFormat *d) - AVInputFormat * av_input_video_device_next(AVInputFormat *d) - AVOutputFormat * av_output_audio_device_next(AVOutputFormat *d) - AVOutputFormat * av_output_video_device_next(AVOutputFormat *d) + void avdevice_register_all() From ca6b49a33df8dfcc743406153f96d354848aab9d Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 23 Jan 2026 14:32:48 -0500 Subject: [PATCH 655/846] Remove unused libavutil bindings --- include/libav.pxd | 1 - include/libavutil/avutil.pxd | 119 ++------------------------- include/libavutil/channel_layout.pxd | 11 --- 3 files changed, 6 insertions(+), 125 deletions(-) delete mode 100644 include/libavutil/channel_layout.pxd diff --git a/include/libav.pxd b/include/libav.pxd index 5e074a1b8..15e086f93 100644 --- a/include/libav.pxd +++ b/include/libav.pxd @@ -1,6 +1,5 @@ include "libavutil/avutil.pxd" include "libavutil/buffer.pxd" -include "libavutil/channel_layout.pxd" include "libavutil/dict.pxd" include "libavutil/error.pxd" include "libavutil/frame.pxd" diff --git a/include/libavutil/avutil.pxd b/include/libavutil/avutil.pxd index 1bfbd2c5d..790469ff7 100644 --- a/include/libavutil/avutil.pxd +++ b/include/libavutil/avutil.pxd @@ -11,7 +11,6 @@ cdef extern from "libavutil/rational.h" nogil: cdef int av_reduce(int *dst_num, int *dst_den, int64_t num, int64_t den, int64_t max) cdef extern from "libavutil/avutil.h" nogil: - cdef const char* av_version_info() cdef int avutil_version() cdef char* avutil_configuration() @@ -107,13 +106,9 @@ cdef extern from "libavutil/avutil.h" nogil: AVCOL_TRC_ARIB_STD_B67 AVCOL_TRC_NB - cdef double M_PI - cdef void* av_malloc(size_t size) cdef void* av_mallocz(size_t size) - cdef void *av_calloc(size_t nmemb, size_t size) - cdef void *av_realloc(void *ptr, size_t size) - + cdef void* av_realloc(void *ptr, size_t size) cdef void av_free(void *ptr) cdef void av_freep(void *ptr) @@ -126,56 +121,15 @@ cdef extern from "libavutil/avutil.h" nogil: AVSampleFormat sample_fmt, int align ) - - # See: http://ffmpeg.org/doxygen/trunk/structAVRational.html ctypedef struct AVRational: int num int den - cdef AVRational AV_TIME_BASE_Q - - # Rescales from one time base to another - cdef int64_t av_rescale_q( - int64_t a, # time stamp - AVRational bq, # source time base - AVRational cq # target time base - ) - - # Rescale a 64-bit integer with specified rounding. - # A simple a*b/c isn't possible as it can overflow - cdef int64_t av_rescale_rnd( - int64_t a, - int64_t b, - int64_t c, - int r # should be AVRounding, but then we can't use bitwise logic. - ) - - cdef int64_t av_rescale_q_rnd( - int64_t a, - AVRational bq, - AVRational cq, - int r # should be AVRounding, but then we can't use bitwise logic. - ) - - cdef int64_t av_rescale( - int64_t a, - int64_t b, - int64_t c - ) - - cdef char* av_strdup(char *s) - - cdef int av_opt_set_int( - void *obj, - char *name, - int64_t value, - int search_flags - ) - + cdef int64_t av_rescale_q(int64_t a, AVRational bq, AVRational cq) + cdef int64_t av_rescale(int64_t a, int64_t b, int64_t c) cdef const char* av_get_media_type_string(AVMediaType media_type) cdef extern from "libavutil/pixdesc.h" nogil: - # See: http://ffmpeg.org/doxygen/trunk/structAVComponentDescriptor.html cdef struct AVComponentDescriptor: unsigned int plane @@ -215,56 +169,25 @@ cdef extern from "libavutil/pixdesc.h" nogil: int av_get_padded_bits_per_pixel(AVPixFmtDescriptor *pixdesc) -cdef extern from "libavutil/channel_layout.h" nogil: - - # Layouts. - cdef uint64_t av_get_channel_layout(char* name) - cdef int av_get_channel_layout_nb_channels(uint64_t channel_layout) - cdef int64_t av_get_default_channel_layout(int nb_channels) - - # Channels. - cdef uint64_t av_channel_layout_extract_channel(uint64_t layout, int index) - cdef char* av_get_channel_name(uint64_t channel) - cdef char* av_get_channel_description(uint64_t channel) - - cdef extern from "libavutil/audio_fifo.h" nogil: - cdef struct AVAudioFifo: pass cdef void av_audio_fifo_free(AVAudioFifo *af) - cdef AVAudioFifo* av_audio_fifo_alloc( - AVSampleFormat sample_fmt, - int channels, - int nb_samples + AVSampleFormat sample_fmt, int channels, int nb_samples ) - - cdef int av_audio_fifo_write( - AVAudioFifo *af, - void **data, - int nb_samples - ) - - cdef int av_audio_fifo_read( - AVAudioFifo *af, - void **data, - int nb_samples - ) - + cdef int av_audio_fifo_write(AVAudioFifo *af, void **data, int nb_samples) + cdef int av_audio_fifo_read(AVAudioFifo *af, void **data, int nb_samples) cdef int av_audio_fifo_size(AVAudioFifo *af) - cdef int av_audio_fifo_space (AVAudioFifo *af) cdef extern from "stdarg.h" nogil: - # For logging. Should really be in another PXD. ctypedef struct va_list: pass cdef extern from "Python.h" nogil: - # For logging. See av/logging.pyx for an explanation. cdef int Py_AddPendingCall(void *, void *) void PyErr_PrintEx(int set_sys_last_vars) int Py_IsInitialized() @@ -310,7 +233,6 @@ cdef extern from "libavutil/opt.h" nogil: AV_OPT_FLAG_FILTERING_PARAM cdef struct AVOption: - const char *name const char *help AVOptionType type @@ -325,7 +247,6 @@ cdef extern from "libavutil/opt.h" nogil: cdef extern from "libavutil/imgutils.h" nogil: - cdef int av_image_alloc( uint8_t *pointers[4], int linesizes[4], @@ -341,37 +262,12 @@ cdef extern from "libavutil/imgutils.h" nogil: uint8_t *ptr, const int linesizes[4] ) - cdef int av_image_fill_linesizes( - int linesizes[4], - AVPixelFormat pix_fmt, - int width, - ) - cdef extern from "libavutil/log.h" nogil: - - cdef enum AVClassCategory: - AV_CLASS_CATEGORY_NA - AV_CLASS_CATEGORY_INPUT - AV_CLASS_CATEGORY_OUTPUT - AV_CLASS_CATEGORY_MUXER - AV_CLASS_CATEGORY_DEMUXER - AV_CLASS_CATEGORY_ENCODER - AV_CLASS_CATEGORY_DECODER - AV_CLASS_CATEGORY_FILTER - AV_CLASS_CATEGORY_BITSTREAM_FILTER - AV_CLASS_CATEGORY_SWSCALER - AV_CLASS_CATEGORY_SWRESAMPLER - AV_CLASS_CATEGORY_NB - cdef struct AVClass: - const char *class_name const char *(*item_name)(void*) nogil - - AVClassCategory category int parent_log_context_offset - const AVOption *option cdef enum: @@ -386,10 +282,7 @@ cdef extern from "libavutil/log.h" nogil: AV_LOG_TRACE AV_LOG_MAX_OFFSET - # Send a log. void av_log(void *ptr, int level, const char *fmt, ...) - - # Get the logs. ctypedef void(*av_log_callback)(void *, int, const char *, va_list) void av_log_default_callback(void *, int, const char *, va_list) void av_log_set_callback (av_log_callback callback) diff --git a/include/libavutil/channel_layout.pxd b/include/libavutil/channel_layout.pxd deleted file mode 100644 index 1459fbd22..000000000 --- a/include/libavutil/channel_layout.pxd +++ /dev/null @@ -1,11 +0,0 @@ -cdef extern from "libavutil/channel_layout.h" nogil: - - # This is not a comprehensive list. - cdef uint64_t AV_CH_LAYOUT_MONO - cdef uint64_t AV_CH_LAYOUT_STEREO - cdef uint64_t AV_CH_LAYOUT_2POINT1 - cdef uint64_t AV_CH_LAYOUT_4POINT0 - cdef uint64_t AV_CH_LAYOUT_5POINT0_BACK - cdef uint64_t AV_CH_LAYOUT_5POINT1_BACK - cdef uint64_t AV_CH_LAYOUT_6POINT1 - cdef uint64_t AV_CH_LAYOUT_7POINT1 From 346ae8cecbb4dddc5ed967bdaa6703008b5fcd1f Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 23 Jan 2026 14:51:30 -0500 Subject: [PATCH 656/846] Remove various unused bindings --- include/libavcodec/avcodec.pxd | 15 --------------- include/libavcodec/hwaccel.pxd | 8 +------- include/libavfilter/avfiltergraph.pxd | 13 ------------- include/libavfilter/buffersink.pxd | 6 +----- include/libavformat/avformat.pxd | 19 ------------------- include/libavutil/frame.pxd | 7 ------- include/libavutil/samplefmt.pxd | 27 --------------------------- include/libswscale/swscale.pxd | 13 ------------- 8 files changed, 2 insertions(+), 106 deletions(-) diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 11cff34a1..d74906b98 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -1,9 +1,5 @@ from libc.stdint cimport int64_t, uint16_t, uint32_t, uint8_t -cdef extern from "libavcodec/codec.h": - struct AVCodecTag: - pass - cdef extern from "libavcodec/packet.h" nogil: const AVPacketSideData *av_packet_side_data_get(const AVPacketSideData *sd, int nb_sd, @@ -430,8 +426,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVChannelLayout ch_layout int64_t duration - cdef AVFrame* avcodec_alloc_frame() - cdef struct AVPacket: void *buf # ptr[AVBufferRef] int64_t pts @@ -457,8 +451,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: int align ) - cdef void avcodec_free_frame(AVFrame **frame) - cdef AVPacket* av_packet_alloc() cdef void av_packet_free(AVPacket **) cdef int av_new_packet(AVPacket*, int) @@ -532,13 +524,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: int64_t pts, int64_t dts, int64_t pos ) - cdef int av_parser_change( - AVCodecParserContext *s, - AVCodecContext *avctx, - uint8_t **poutbuf, int *poutbuf_size, - const uint8_t *buf, int buf_size, - int keyframe - ) cdef void av_parser_close(AVCodecParserContext *s) cdef struct AVCodecParameters: diff --git a/include/libavcodec/hwaccel.pxd b/include/libavcodec/hwaccel.pxd index cb9ac41b6..2023efd64 100644 --- a/include/libavcodec/hwaccel.pxd +++ b/include/libavcodec/hwaccel.pxd @@ -9,11 +9,5 @@ cdef extern from "libavcodec/avcodec.h" nogil: int methods AVHWDeviceType device_type cdef const AVCodecHWConfig* avcodec_get_hw_config(const AVCodec *codec, int index) - cdef enum: - AV_HWACCEL_CODEC_CAP_EXPERIMENTAL cdef struct AVHWAccel: - char *name - AVMediaType type - AVCodecID id - AVPixelFormat pix_fmt - int capabilities + pass diff --git a/include/libavfilter/avfiltergraph.pxd b/include/libavfilter/avfiltergraph.pxd index b773063f9..f27dcbbcc 100644 --- a/include/libavfilter/avfiltergraph.pxd +++ b/include/libavfilter/avfiltergraph.pxd @@ -1,6 +1,4 @@ - cdef extern from "libavfilter/avfilter.h" nogil: - cdef struct AVFilterGraph: int nb_filters AVFilterContext **filters @@ -14,13 +12,6 @@ cdef extern from "libavfilter/avfilter.h" nogil: cdef AVFilterGraph* avfilter_graph_alloc() cdef void avfilter_graph_free(AVFilterGraph **ptr) - cdef int avfilter_graph_parse2( - AVFilterGraph *graph, - const char *filter_str, - AVFilterInOut **inputs, - AVFilterInOut **outputs - ) - cdef AVFilterContext* avfilter_graph_alloc_filter( AVFilterGraph *graph, const AVFilter *filter, @@ -44,7 +35,3 @@ cdef extern from "libavfilter/avfilter.h" nogil: ) cdef int avfilter_graph_config(AVFilterGraph *graph, void *logctx) - - cdef char* avfilter_graph_dump(AVFilterGraph *graph, const char *options) - - cdef void avfilter_inout_free(AVFilterInOut **inout_list) diff --git a/include/libavfilter/buffersink.pxd b/include/libavfilter/buffersink.pxd index 84ea56c68..ef843cb7a 100644 --- a/include/libavfilter/buffersink.pxd +++ b/include/libavfilter/buffersink.pxd @@ -1,6 +1,2 @@ cdef extern from "libavfilter/buffersink.h" nogil: - - int av_buffersink_get_frame( - AVFilterContext *ctx, - AVFrame *frame - ) + int av_buffersink_get_frame(AVFilterContext *ctx, AVFrame *frame) diff --git a/include/libavformat/avformat.pxd b/include/libavformat/avformat.pxd index 3a2218f06..c2e549533 100644 --- a/include/libavformat/avformat.pxd +++ b/include/libavformat/avformat.pxd @@ -93,16 +93,6 @@ cdef extern from "libavformat/avformat.h" nogil: # const AVCodecTag* const *codec_tag const AVClass *priv_class - cdef struct AVProbeData: - unsigned char *buf - int buf_size - const char *filename - - cdef AVInputFormat* av_probe_input_format( - AVProbeData *pd, - int is_opened - ) - # http://ffmpeg.org/doxygen/trunk/structAVOutputFormat.html cdef struct AVOutputFormat: const char *name @@ -153,15 +143,6 @@ cdef extern from "libavformat/avformat.h" nogil: AVFMT_FLAG_FAST_SEEK AVFMT_FLAG_AUTO_BSF - cdef int av_probe_input_buffer( - AVIOContext *pb, - AVInputFormat **fmt, - const char *filename, - void *logctx, - unsigned int offset, - unsigned int max_probe_size - ) - cdef int av_find_best_stream( AVFormatContext *ic, AVMediaType type, diff --git a/include/libavutil/frame.pxd b/include/libavutil/frame.pxd index aa8dc3a00..c7f60e93c 100644 --- a/include/libavutil/frame.pxd +++ b/include/libavutil/frame.pxd @@ -1,14 +1,7 @@ cdef extern from "libavutil/frame.h" nogil: - cdef AVFrame* av_frame_alloc() cdef void av_frame_free(AVFrame**) - cdef int av_frame_ref(AVFrame *dst, const AVFrame *src) - cdef AVFrame* av_frame_clone(const AVFrame *src) cdef void av_frame_unref(AVFrame *frame) - cdef void av_frame_move_ref(AVFrame *dst, AVFrame *src) - cdef int av_frame_get_buffer(AVFrame *frame, int align) - cdef int av_frame_is_writable(AVFrame *frame) cdef int av_frame_make_writable(AVFrame *frame) - cdef int av_frame_copy(AVFrame *dst, const AVFrame *src) cdef int av_frame_copy_props(AVFrame *dst, const AVFrame *src) cdef AVFrameSideData* av_frame_get_side_data(AVFrame *frame, AVFrameSideDataType type) diff --git a/include/libavutil/samplefmt.pxd b/include/libavutil/samplefmt.pxd index a26c6ecfd..c70272936 100644 --- a/include/libavutil/samplefmt.pxd +++ b/include/libavutil/samplefmt.pxd @@ -26,15 +26,6 @@ cdef extern from "libavutil/samplefmt.h" nogil: cdef AVSampleFormat av_get_packed_sample_fmt(AVSampleFormat sample_fmt) cdef AVSampleFormat av_get_planar_sample_fmt(AVSampleFormat sample_fmt) - cdef int av_samples_alloc( - uint8_t** audio_data, - int* linesize, - int nb_channels, - int nb_samples, - AVSampleFormat sample_fmt, - int align - ) - cdef int av_samples_get_buffer_size( int *linesize, int nb_channels, @@ -42,21 +33,3 @@ cdef extern from "libavutil/samplefmt.h" nogil: AVSampleFormat sample_fmt, int align ) - - cdef int av_samples_fill_arrays( - uint8_t **audio_data, - int *linesize, - const uint8_t *buf, - int nb_channels, - int nb_samples, - AVSampleFormat sample_fmt, - int align - ) - - cdef int av_samples_set_silence( - uint8_t **audio_data, - int offset, - int nb_samples, - int nb_channels, - AVSampleFormat sample_fmt - ) diff --git a/include/libswscale/swscale.pxd b/include/libswscale/swscale.pxd index a9698ecb3..2d855d95a 100644 --- a/include/libswscale/swscale.pxd +++ b/include/libswscale/swscale.pxd @@ -34,19 +34,6 @@ cdef extern from "libswscale/swscale.h" nogil: cdef int SWS_CS_SMPTE240M cdef int SWS_CS_DEFAULT - cdef SwsContext* sws_getContext( - int src_width, - int src_height, - AVPixelFormat src_format, - int dst_width, - int dst_height, - AVPixelFormat dst_format, - int flags, - SwsFilter *src_filter, - SwsFilter *dst_filter, - double *param, - ) - cdef int sws_scale( SwsContext *ctx, const unsigned char *const *src_slice, From f978d2ca0a64e3783db1a95a647b45b821bc5d6e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 23 Jan 2026 19:39:50 -0500 Subject: [PATCH 657/846] Fix setuptools warning about av.filter package Add explicit package discovery configuration to pyproject.toml to align with setup.py's find_packages() call, preventing the "Package 'av.filter' is absent from the packages configuration" warning during pip builds. Co-Authored-By: Claude Opus 4.5 --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 24bdc90a9..ce542dc46 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,9 @@ dynamic = ["version"] [tool.setuptools] zip-safe = false +[tool.setuptools.packages.find] +include = ["av*"] + [tool.setuptools.dynamic] version = {attr = "av.about.__version__"} From 56aca85eaa1e8e035dd2082a15eea2a8c1aff212 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 23 Jan 2026 21:13:39 -0500 Subject: [PATCH 658/846] Bump dev version --- av/about.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/about.py b/av/about.py index 5d194c730..fea979679 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "16.1.0" +__version__ = "17.0.0pre" From 92b011bc37433f653f68a69036614b82797e9875 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 23 Jan 2026 21:53:48 -0500 Subject: [PATCH 659/846] Add missing consts for Ubuntu --- .github/workflows/smoke.yml | 1 + .github/workflows/tests.yml | 24 ++++++++++++++++-------- av/video/reformatter.py | 4 ++-- include/libswscale/swscale.pxd | 15 ++++----------- 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 218e36241..04f519a35 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -36,6 +36,7 @@ jobs: matrix: config: - {os: ubuntu-24.04, python: "3.12", ffmpeg: "8.0.1", extras: true} + - {os: ubuntu-24.04, python: "3.10", ffmpeg: "8.0.1"} - {os: ubuntu-24.04, python: "pypy3.11", ffmpeg: "8.0.1"} - {os: macos-14, python: "3.11", ffmpeg: "8.0.1"} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4b4e1f45d..9cbdb6715 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,12 +16,11 @@ jobs: pip install -U cython setuptools python scripts/fetch-vendor.py --config-file scripts/ffmpeg-latest.json /tmp/vendor PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig python setup.py sdist - - name: Publish to PyPI - if: github.event_name == 'release' && github.event.action == 'published' - uses: pypa/gh-action-pypi-publish@release/v1 + - name: Upload source package + uses: actions/upload-artifact@v6 with: - user: __token__ - password: ${{ secrets.PYPI_TOKEN }} + name: dist-source + path: dist/ package-wheel: runs-on: ${{ matrix.os }} @@ -73,20 +72,29 @@ jobs: cibuildwheel --output-dir dist shell: bash - name: Upload wheels - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: dist-${{ matrix.os }}-${{ matrix.arch }} path: dist/ publish: runs-on: ubuntu-latest - needs: [package-wheel] + needs: [package-source, package-wheel] steps: - uses: actions/checkout@v6 - - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v7 with: merge-multiple: true path: dist/ + + - name: Publish to GitHub + if: github.event_name == 'release' + uses: softprops/action-gh-release@v1 + with: + files: dist/* + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Publish to PyPI if: github.event_name == 'release' && github.event.action == 'published' uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/av/video/reformatter.py b/av/video/reformatter.py index dc898be84..e3e83d2d9 100644 --- a/av/video/reformatter.py +++ b/av/video/reformatter.py @@ -234,8 +234,8 @@ def _reformat( with cython.nogil: lib.sws_scale( self.ptr, - frame.ptr.data, - frame.ptr.linesize, + cython.cast("const unsigned char *const *", frame.ptr.data), + cython.cast("const int *", frame.ptr.linesize), 0, # slice Y frame.ptr.height, new_frame.ptr.data, diff --git a/include/libswscale/swscale.pxd b/include/libswscale/swscale.pxd index 2d855d95a..ffc0eb6b0 100644 --- a/include/libswscale/swscale.pxd +++ b/include/libswscale/swscale.pxd @@ -1,6 +1,6 @@ +from libc.stdint cimport uint8_t cdef extern from "libswscale/swscale.h" nogil: - cdef int swscale_version() cdef char* swscale_configuration() cdef char* swscale_license() @@ -13,7 +13,6 @@ cdef extern from "libswscale/swscale.h" nogil: cdef struct SwsFilter: pass - # Flags. cdef int SWS_FAST_BILINEAR cdef int SWS_BILINEAR cdef int SWS_BICUBIC @@ -25,7 +24,6 @@ cdef extern from "libswscale/swscale.h" nogil: cdef int SWS_SINC cdef int SWS_LANCZOS cdef int SWS_SPLINE - cdef int SWS_CS_ITU709 cdef int SWS_CS_FCC cdef int SWS_CS_ITU601 @@ -36,16 +34,14 @@ cdef extern from "libswscale/swscale.h" nogil: cdef int sws_scale( SwsContext *ctx, - const unsigned char *const *src_slice, + const uint8_t *const *src_slice, const int *src_stride, int src_slice_y, int src_slice_h, unsigned char *const *dst_slice, - int *dst_stride, + const int *dst_stride, ) - cdef void sws_freeContext(SwsContext *ctx) - cdef SwsContext *sws_getCachedContext( SwsContext *context, int src_width, @@ -59,9 +55,7 @@ cdef extern from "libswscale/swscale.h" nogil: SwsFilter *dst_filter, double *param, ) - - cdef int* sws_getCoefficients(int colorspace) - + cdef const int* sws_getCoefficients(int colorspace) cdef int sws_getColorspaceDetails( SwsContext *context, int **inv_table, @@ -72,7 +66,6 @@ cdef extern from "libswscale/swscale.h" nogil: int *contrast, int *saturation ) - cdef int sws_setColorspaceDetails( SwsContext *context, const int inv_table[4], From c7c422c86ca704094d80a8b9e4ef8736599c187c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 24 Jan 2026 00:31:19 -0500 Subject: [PATCH 660/846] 8.0.1-5 --- scripts/ffmpeg-8.0.json | 2 +- scripts/ffmpeg-latest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/ffmpeg-8.0.json b/scripts/ffmpeg-8.0.json index 8e56abfd1..a7df98bb9 100644 --- a/scripts/ffmpeg-8.0.json +++ b/scripts/ffmpeg-8.0.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0.1-4/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0.1-5/ffmpeg-{platform}.tar.gz" } diff --git a/scripts/ffmpeg-latest.json b/scripts/ffmpeg-latest.json index 8e56abfd1..a7df98bb9 100644 --- a/scripts/ffmpeg-latest.json +++ b/scripts/ffmpeg-latest.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0.1-4/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0.1-5/ffmpeg-{platform}.tar.gz" } From efe1e4c7ff4c5b28af8b0c8d455107d5ae7a6197 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 24 Jan 2026 01:51:51 -0500 Subject: [PATCH 661/846] Don't make MotionVectors a subclass of Sequence It breaked limited api. --- av/sidedata/motionvectors.py | 4 +--- av/sidedata/motionvectors.pyi | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/av/sidedata/motionvectors.py b/av/sidedata/motionvectors.py index 820013e43..04e06f25e 100644 --- a/av/sidedata/motionvectors.py +++ b/av/sidedata/motionvectors.py @@ -1,5 +1,3 @@ -from collections.abc import Sequence - import cython from cython.cimports import libav as lib from cython.cimports.av.sidedata.sidedata import SideData @@ -8,7 +6,7 @@ @cython.cclass -class MotionVectors(SideData, Sequence): +class MotionVectors(SideData): def __init__(self, sentinel, frame: Frame, index: cython.int): SideData.__init__(self, sentinel, frame, index) self._vectors = {} diff --git a/av/sidedata/motionvectors.pyi b/av/sidedata/motionvectors.pyi index a1ab3c0fc..37b0002c0 100644 --- a/av/sidedata/motionvectors.pyi +++ b/av/sidedata/motionvectors.pyi @@ -4,7 +4,7 @@ import numpy as np from .sidedata import SideData -class MotionVectors(SideData, Sequence[MotionVector]): +class MotionVectors(SideData): @overload def __getitem__(self, index: int) -> MotionVector: ... @overload From 3e8638169d7af4ee3bd53d7835b6e85626fad039 Mon Sep 17 00:00:00 2001 From: Nick <24689722+ntjohnson1@users.noreply.github.com> Date: Sun, 25 Jan 2026 19:13:09 -0500 Subject: [PATCH 662/846] Fix packet typing based on stream and specify InputContainer.demux based on incoming stream (#2134) --- av/container/input.pyi | 45 ++++++++++++++++++++++++++++++++++++++++-- av/packet.pyi | 38 +++++++++++++++++++++++++++-------- tests/test_decode.py | 5 +++++ tests/test_streams.py | 20 +++++++++++-------- 4 files changed, 90 insertions(+), 18 deletions(-) diff --git a/av/container/input.pyi b/av/container/input.pyi index 90154c331..b9d93c6b4 100644 --- a/av/container/input.pyi +++ b/av/container/input.pyi @@ -3,7 +3,7 @@ from typing import Any, Iterator, overload from av.audio.frame import AudioFrame from av.audio.stream import AudioStream from av.packet import Packet -from av.stream import Stream +from av.stream import AttachmentStream, DataStream, Stream from av.subtitles.stream import SubtitleStream from av.subtitles.subtitle import SubtitleSet from av.video.frame import VideoFrame @@ -19,7 +19,48 @@ class InputContainer(Container): def __enter__(self) -> InputContainer: ... def close(self) -> None: ... - def demux(self, *args: Any, **kwargs: Any) -> Iterator[Packet]: ... + @overload + def demux(self, video_stream: VideoStream) -> Iterator[Packet[VideoStream]]: ... + @overload + def demux( + self, video_streams: tuple[VideoStream, ...] + ) -> Iterator[Packet[VideoStream]]: ... + @overload + def demux(self, *, video: Any) -> Iterator[Packet[VideoStream]]: ... + @overload + def demux(self, audio_stream: AudioStream) -> Iterator[Packet[AudioStream]]: ... + @overload + def demux( + self, audio_streams: tuple[AudioStream, ...] + ) -> Iterator[Packet[AudioStream]]: ... + @overload + def demux(self, *, audio: Any) -> Iterator[Packet[AudioStream]]: ... + @overload + def demux( + self, subtitle_stream: SubtitleStream + ) -> Iterator[Packet[SubtitleStream]]: ... + @overload + def demux( + self, subtitle_streams: tuple[SubtitleStream, ...] + ) -> Iterator[Packet[SubtitleStream]]: ... + @overload + def demux(self, data_stream: DataStream) -> Iterator[Packet[DataStream]]: ... + @overload + def demux( + self, data_streams: tuple[DataStream, ...] + ) -> Iterator[Packet[DataStream]]: ... + @overload + def demux(self, *, data: Any) -> Iterator[Packet[DataStream]]: ... + @overload + def demux( + self, attachment_stream: AttachmentStream + ) -> Iterator[Packet[AttachmentStream]]: ... + @overload + def demux( + self, attachment_streams: tuple[AttachmentStream, ...] + ) -> Iterator[Packet[AttachmentStream]]: ... + @overload + def demux(self, *args: Any, **kwargs: Any) -> Iterator[Packet[Stream]]: ... @overload def decode(self, video: int) -> Iterator[VideoFrame]: ... @overload diff --git a/av/packet.pyi b/av/packet.pyi index 6b1a271c2..d129e5e7f 100644 --- a/av/packet.pyi +++ b/av/packet.pyi @@ -1,7 +1,13 @@ from fractions import Fraction -from typing import Iterator, Literal +from typing import Generic, Iterator, Literal, TypeVar, overload -from av.subtitles.subtitle import SubtitleSet +from av.audio.frame import AudioFrame +from av.audio.stream import AudioStream +from av.stream import AttachmentStream, DataStream, Stream +from av.subtitles.stream import SubtitleStream +from av.subtitles.subtitle import AssSubtitle, BitmapSubtitle, SubtitleSet +from av.video.frame import VideoFrame +from av.video.stream import VideoStream from .buffer import Buffer from .stream import Stream @@ -52,8 +58,8 @@ PktSideDataT = Literal[ class PacketSideData(Buffer): @staticmethod - def from_packet(packet: Packet, dtype: PktSideDataT) -> PacketSideData: ... - def to_packet(self, packet: Packet, move: bool = False): ... + def from_packet(packet: Packet[Stream], dtype: PktSideDataT) -> PacketSideData: ... + def to_packet(self, packet: Packet[Stream], move: bool = False): ... @property def data_type(self) -> str: ... @property @@ -65,8 +71,11 @@ class PacketSideData(Buffer): def packet_sidedata_type_to_literal(dtype: int) -> PktSideDataT: ... def packet_sidedata_type_from_literal(dtype: PktSideDataT) -> int: ... -class Packet(Buffer): - stream: Stream +# TypeVar for stream types - bound to Stream so it can be any stream type +StreamT = TypeVar("StreamT", bound=Stream) + +class Packet(Buffer, Generic[StreamT]): + stream: StreamT stream_index: int time_base: Fraction pts: int | None @@ -81,8 +90,21 @@ class Packet(Buffer): is_trusted: bool is_disposable: bool - def __init__(self, input: int | bytes | None = None) -> None: ... - def decode(self) -> list[SubtitleSet]: ... + def __init__(self: Packet[Stream], input: int | bytes | None = None) -> None: ... + + # Overloads that return the same type as the stream's decode method + @overload + def decode(self: Packet[VideoStream]) -> list[VideoFrame]: ... + @overload + def decode(self: Packet[AudioStream]) -> list[AudioFrame]: ... + @overload + def decode( + self: Packet[SubtitleStream], + ) -> list[AssSubtitle] | list[BitmapSubtitle]: ... + @overload + def decode( + self, + ) -> list[VideoFrame | AudioFrame | AssSubtitle | BitmapSubtitle]: ... def has_sidedata(self, dtype: PktSideDataT) -> bool: ... def get_sidedata(self, dtype: PktSideDataT) -> PacketSideData: ... def set_sidedata(self, sidedata: PacketSideData, move: bool = False) -> None: ... diff --git a/tests/test_decode.py b/tests/test_decode.py index d7fffbd4c..3277dd776 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -72,11 +72,13 @@ def test_decode_audio_corrupt(self) -> None: packet_count = 0 frame_count = 0 + audio_frame: av.AudioFrame | None = None with av.open(path) as container: for packet in container.demux(audio=0): for frame in packet.decode(): frame_count += 1 + audio_frame = frame packet_count += 1 assert packet_count == 1 @@ -109,8 +111,11 @@ def test_decoded_time_base(self) -> None: assert stream.time_base == Fraction(1, 25) + video_frame: av.VideoFrame | None = None + for packet in container.demux(stream): for frame in packet.decode(): + video_frame = frame assert not isinstance(frame, SubtitleSet) assert packet.time_base == frame.time_base assert stream.time_base == frame.time_base diff --git a/tests/test_streams.py b/tests/test_streams.py index 9387d68cc..4f7defef0 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -179,15 +179,15 @@ def test_data_stream_from_template(self) -> None: input_data_stream ) - for packet in input_container.demux(input_data_stream): - payload = bytes(packet) + for data_packet in input_container.demux(input_data_stream): + payload = bytes(data_packet) if not payload: continue copied_payloads.append(payload) clone = av.Packet(payload) - clone.pts = packet.pts - clone.dts = packet.dts - clone.time_base = packet.time_base + clone.pts = data_packet.pts + clone.dts = data_packet.dts + clone.time_base = data_packet.time_base clone.stream = output_data_stream output_container.mux(clone) @@ -196,8 +196,8 @@ def test_data_stream_from_template(self) -> None: assert output_stream.codec_context is None remuxed_payloads: list[bytes] = [] - for packet in remuxed.demux(output_stream): - payload = bytes(packet) + for data_packet in remuxed.demux(output_stream): + payload = bytes(data_packet) if payload: remuxed_payloads.append(payload) @@ -287,7 +287,11 @@ def test_attachment_stream(self) -> None: for packet in ic.demux(ic.streams.video): if packet.dts is None: continue - packet.stream = stream_map[packet.stream.index] + updated_stream = stream_map.get(packet.stream.index) + if isinstance(updated_stream, av.video.stream.VideoStream): + packet.stream = updated_stream + else: + raise ValueError("Expected a VideoStream") oc.mux(packet) with av.open(out2_path) as c: From f462b75df51983c456526ca5ecadce420ed152bd Mon Sep 17 00:00:00 2001 From: Max Ehrlich Date: Tue, 27 Jan 2026 18:32:59 -0500 Subject: [PATCH 663/846] Expose AVIndexEntry (#2136) --- av/indexentries.pxd | 9 ++++ av/indexentries.py | 91 ++++++++++++++++++++++++++++++++ av/indexentries.pyi | 14 +++++ av/indexentry.pxd | 9 ++++ av/indexentry.py | 63 ++++++++++++++++++++++ av/indexentry.pyi | 8 +++ av/stream.pxd | 3 ++ av/stream.py | 2 + av/stream.pyi | 2 + include/libavformat/avformat.pxd | 15 ++++++ tests/test_indexentries.py | 79 +++++++++++++++++++++++++++ 11 files changed, 295 insertions(+) create mode 100644 av/indexentries.pxd create mode 100644 av/indexentries.py create mode 100644 av/indexentries.pyi create mode 100644 av/indexentry.pxd create mode 100644 av/indexentry.py create mode 100644 av/indexentry.pyi create mode 100644 tests/test_indexentries.py diff --git a/av/indexentries.pxd b/av/indexentries.pxd new file mode 100644 index 000000000..44575476a --- /dev/null +++ b/av/indexentries.pxd @@ -0,0 +1,9 @@ +cimport libav as lib + + +cdef class IndexEntries: + cdef lib.AVStream *stream_ptr + cdef _init(self, lib.AVStream *ptr) + + +cdef IndexEntries wrap_index_entries(lib.AVStream *ptr) diff --git a/av/indexentries.py b/av/indexentries.py new file mode 100644 index 000000000..ecbc83d27 --- /dev/null +++ b/av/indexentries.py @@ -0,0 +1,91 @@ +import cython +import cython.cimports.libav as lib +from cython.cimports.av.indexentry import wrap_index_entry +from cython.cimports.libc.stdint import int64_t + +_cinit_bypass_sentinel = cython.declare(object, object()) + + +@cython.cfunc +def wrap_index_entries(ptr: cython.pointer[lib.AVStream]) -> IndexEntries: + obj: IndexEntries = IndexEntries(_cinit_bypass_sentinel) + obj._init(ptr) + return obj + + +@cython.cclass +class IndexEntries: + """A sequence-like view of FFmpeg's per-stream index entries. + + Exposed as :attr:`~av.stream.Stream.index_entries`. + + The index is provided by the demuxer and may be empty or incomplete depending + on the container format. This is useful for fast multi-seek loops (e.g., decoding + at a lower-than-native framerate). + """ + + def __cinit__(self, sentinel): + if sentinel is _cinit_bypass_sentinel: + return + raise RuntimeError("cannot manually instantiate IndexEntries") + + @cython.cfunc + def _init(self, ptr: cython.pointer[lib.AVStream]): + self.stream_ptr = ptr + + def __repr__(self): + return f"" + + def __len__(self) -> int: + with cython.nogil: + return lib.avformat_index_get_entries_count(self.stream_ptr) + + def __iter__(self): + for i in range(len(self)): + yield self[i] + + def __getitem__(self, index): + if isinstance(index, int): + n = len(self) + if index < 0: + index += n + if index < 0 or index >= n: + raise IndexError(f"Index entries {index} out of bounds for size {n}") + + c_idx = cython.declare(cython.int, index) + with cython.nogil: + entry = lib.avformat_index_get_entry(self.stream_ptr, c_idx) + + if entry == cython.NULL: + raise IndexError("index entry not found") + + return wrap_index_entry(entry) + + elif isinstance(index, slice): + start, stop, step = index.indices(len(self)) + return [self[i] for i in range(start, stop, step)] + + else: + raise TypeError("Index must be an integer or a slice") + + def search_timestamp( + self, timestamp, *, backward: bool = True, any_frame: bool = False + ): + """Search the underlying index for ``timestamp``. + + This wraps FFmpeg's ``av_index_search_timestamp``. + + Returns an index into this object, or ``-1`` if no match is found. + """ + c_timestamp = cython.declare(int64_t, timestamp) + flags = cython.declare(cython.int, 0) + + if backward: + flags |= lib.AVSEEK_FLAG_BACKWARD + if any_frame: + flags |= lib.AVSEEK_FLAG_ANY + + with cython.nogil: + idx = lib.av_index_search_timestamp(self.stream_ptr, c_timestamp, flags) + + return idx diff --git a/av/indexentries.pyi b/av/indexentries.pyi new file mode 100644 index 000000000..c5d495711 --- /dev/null +++ b/av/indexentries.pyi @@ -0,0 +1,14 @@ +from typing import Iterator, overload + +from av.indexentry import IndexEntry + +class IndexEntries: + def __len__(self) -> int: ... + def __iter__(self) -> Iterator[IndexEntry]: ... + @overload + def __getitem__(self, index: int) -> IndexEntry: ... + @overload + def __getitem__(self, index: slice) -> list[IndexEntry]: ... + def search_timestamp( + self, timestamp, *, backward: bool = True, any_frame: bool = False + ) -> int: ... diff --git a/av/indexentry.pxd b/av/indexentry.pxd new file mode 100644 index 000000000..55bac3ff4 --- /dev/null +++ b/av/indexentry.pxd @@ -0,0 +1,9 @@ +cimport libav as lib + + +cdef class IndexEntry: + cdef lib.AVIndexEntry *ptr + cdef _init(self, lib.AVIndexEntry *ptr) + + +cdef IndexEntry wrap_index_entry(lib.AVIndexEntry *ptr) diff --git a/av/indexentry.py b/av/indexentry.py new file mode 100644 index 000000000..4d793f0cd --- /dev/null +++ b/av/indexentry.py @@ -0,0 +1,63 @@ +import cython +import cython.cimports.libav as lib + +_cinit_bypass_sentinel = cython.declare(object, object()) + + +@cython.cfunc +def wrap_index_entry(ptr: cython.pointer[lib.AVIndexEntry]) -> IndexEntry: + obj: IndexEntry = IndexEntry(_cinit_bypass_sentinel) + obj._init(ptr) + return obj + + +@cython.cclass +class IndexEntry: + """A single entry from a stream's index. + + This is a thin wrapper around FFmpeg's ``AVIndexEntry``. + + The exact meaning of the fields depends on the container/demuxer. + """ + + def __cinit__(self, sentinel): + if sentinel is not _cinit_bypass_sentinel: + raise RuntimeError("cannot manually instantiate IndexEntry") + + @cython.cfunc + def _init(self, ptr: cython.pointer[lib.AVIndexEntry]): + self.ptr = ptr + + def __repr__(self): + return ( + f"" + ) + + @property + def pos(self): + return self.ptr.pos + + @property + def timestamp(self): + return self.ptr.timestamp + + @property + def flags(self): + return self.ptr.flags + + @property + def is_keyframe(self): + return bool(self.ptr.flags & lib.AVINDEX_KEYFRAME) + + @property + def is_discard(self): + return bool(self.ptr.flags & lib.AVINDEX_DISCARD_FRAME) + + @property + def size(self): + return self.ptr.size + + @property + def min_distance(self): + return self.ptr.min_distance diff --git a/av/indexentry.pyi b/av/indexentry.pyi new file mode 100644 index 000000000..5365bc768 --- /dev/null +++ b/av/indexentry.pyi @@ -0,0 +1,8 @@ +class IndexEntry: + pos: int + timestamp: int + flags: int + is_keyframe: bool + is_discard: bool + size: int + min_distance: int diff --git a/av/stream.pxd b/av/stream.pxd index 9aa6616e5..ecdbe8029 100644 --- a/av/stream.pxd +++ b/av/stream.pxd @@ -3,6 +3,7 @@ cimport libav as lib from av.codec.context cimport CodecContext from av.container.core cimport Container from av.frame cimport Frame +from av.indexentries cimport IndexEntries from av.packet cimport Packet @@ -16,6 +17,8 @@ cdef class Stream: # CodecContext attributes. cdef readonly CodecContext codec_context + cdef readonly IndexEntries index_entries + # Private API. cdef _init(self, Container, lib.AVStream*, CodecContext) cdef _finalize_for_output(self) diff --git a/av/stream.py b/av/stream.py index a43c43496..868def2ab 100644 --- a/av/stream.py +++ b/av/stream.py @@ -3,6 +3,7 @@ import cython from cython.cimports import libav as lib from cython.cimports.av.error import err_check +from cython.cimports.av.indexentries import wrap_index_entries from cython.cimports.av.packet import Packet from cython.cimports.av.utils import ( avdict_to_dict, @@ -106,6 +107,7 @@ def _init( ): self.container = container self.ptr = stream + self.index_entries = wrap_index_entries(self.ptr) self.codec_context = codec_context if self.codec_context: diff --git a/av/stream.pyi b/av/stream.pyi index f6ca196c8..8790d2601 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -4,6 +4,7 @@ from typing import Literal, cast from .codec import Codec, CodecContext from .container import Container +from .indexentries import IndexEntries class Disposition(Flag): default = cast(int, ...) @@ -32,6 +33,7 @@ class Stream: codec: Codec codec_context: CodecContext metadata: dict[str, str] + index_entries: IndexEntries id: int profiles: list[str] profile: str | None diff --git a/include/libavformat/avformat.pxd b/include/libavformat/avformat.pxd index c2e549533..1fda271f8 100644 --- a/include/libavformat/avformat.pxd +++ b/include/libavformat/avformat.pxd @@ -331,3 +331,18 @@ cdef extern from "libavformat/avformat.h" nogil: # custom cdef set pyav_get_available_formats() + + cdef struct AVIndexEntry: + int64_t pos + int64_t timestamp + int flags + int size + int min_distance + + cdef enum: + AVINDEX_KEYFRAME + AVINDEX_DISCARD_FRAME + + cdef AVIndexEntry *avformat_index_get_entry(AVStream *st, int idx) + cdef int avformat_index_get_entries_count(AVStream *st) + cdef int av_index_search_timestamp(AVStream *st, int64_t timestamp, int flags) diff --git a/tests/test_indexentries.py b/tests/test_indexentries.py new file mode 100644 index 000000000..c0d5cd393 --- /dev/null +++ b/tests/test_indexentries.py @@ -0,0 +1,79 @@ +import av + +from .common import TestCase, fate_suite + + +class TestIndexEntries(TestCase): + def test_index_entries_len_mp4(self) -> None: + with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: + stream = container.streams.video[0] + assert len(stream.index_entries) == stream.frames + + def test_index_entries_len_webm(self) -> None: + with av.open( + fate_suite("vp9-test-vectors/vp90-2-00-quantizer-00.webm") + ) as container: + stream = container.streams.video[0] + index_entries_len_before_demux = len(stream.index_entries) + + keyframes = len([p for p in container.demux(video=0) if p.is_keyframe]) + assert index_entries_len_before_demux == keyframes + + def test_index_entries_search_timestamp_options_mp4(self) -> None: + with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: + stream = container.streams.video[0] + fi = stream.index_entries + + target_ts = -1 + + assert fi.search_timestamp(target_ts) == 0 + assert fi.search_timestamp(target_ts, any_frame=True) == 1 + assert fi.search_timestamp(target_ts, backward=False) == 21 + assert fi.search_timestamp(target_ts, backward=False, any_frame=True) == 1 + + e0 = fi[0] + e1 = fi[1] + e21 = fi[21] + assert e0.timestamp == -2 and e0.is_keyframe + assert e1.timestamp == -1 and not e1.is_keyframe + assert e21.timestamp == 19 and e21.is_keyframe + + def test_index_entries_matches_packet_mp4(self) -> None: + with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: + stream = container.streams.video[0] + for i, packet in enumerate(container.demux(video=0)): + if packet.dts is not None: + assert stream.index_entries[i].timestamp == packet.dts + + def test_index_entries_in_bounds(self) -> None: + with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: + stream = container.streams.video[0] + first = stream.index_entries[0] + first_neg = stream.index_entries[-len(stream.index_entries)] + last = stream.index_entries[-1] + last_pos = stream.index_entries[len(stream.index_entries) - 1] + assert first.timestamp == first_neg.timestamp + assert last.timestamp == last_pos.timestamp + + def test_index_entries_out_of_bounds(self) -> None: + with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: + stream = container.streams.video[0] + with self.assertRaises(IndexError): + _ = stream.index_entries[len(stream.index_entries)] + + with self.assertRaises(IndexError): + _ = stream.index_entries[-len(stream.index_entries) - 1] + + def test_index_entries_slice(self) -> None: + with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: + stream = container.streams.video[0] + + individual_indices = [stream.index_entries[i] for i in range(1, 5)] + slice_indices = stream.index_entries[1:5] + assert len(individual_indices) == len(slice_indices) == 4 + assert all( + [ + i.timestamp == j.timestamp + for i, j in zip(individual_indices, slice_indices) + ] + ) From 1f475254db3eb839ae557fee324bd50555b8e4bb Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 31 Jan 2026 12:36:12 -0500 Subject: [PATCH 664/846] Combine av/buffer and av/bytesource --- av/buffer.pxd | 12 ++++++++- av/buffer.py | 47 ++++++++++++++++++++++++++++++++++-- av/bytesource.pxd | 14 ----------- av/bytesource.pyx | 43 --------------------------------- av/codec/context.pxd | 2 +- av/codec/context.py | 2 +- av/packet.pxd | 3 +-- av/packet.py | 3 +-- av/subtitles/codeccontext.py | 2 +- 9 files changed, 61 insertions(+), 67 deletions(-) delete mode 100644 av/bytesource.pxd delete mode 100644 av/bytesource.pyx diff --git a/av/buffer.pxd b/av/buffer.pxd index cfab07ca0..07ae34ae5 100644 --- a/av/buffer.pxd +++ b/av/buffer.pxd @@ -1,6 +1,16 @@ +from cpython.buffer cimport Py_buffer -cdef class Buffer: +cdef class ByteSource: + cdef object owner + cdef bint has_view + cdef Py_buffer view + cdef unsigned char *ptr + cdef size_t length + +cdef ByteSource bytesource(object, bint allow_none=*) + +cdef class Buffer: cdef size_t _buffer_size(self) cdef void* _buffer_ptr(self) cdef bint _buffer_writable(self) diff --git a/av/buffer.py b/av/buffer.py index 047d1a157..5b179f322 100644 --- a/av/buffer.py +++ b/av/buffer.py @@ -1,9 +1,53 @@ import cython -from cython.cimports.av.bytesource import ByteSource, bytesource from cython.cimports.cpython import PyBUF_WRITABLE, PyBuffer_FillInfo +from cython.cimports.cpython.buffer import ( + PyBUF_SIMPLE, + PyBuffer_Release, + PyObject_CheckBuffer, + PyObject_GetBuffer, +) from cython.cimports.libc.string import memcpy +@cython.cclass +class ByteSource: + def __cinit__(self, owner): + self.owner = owner + + try: + self.ptr = owner + except TypeError: + pass + else: + self.length = len(owner) + return + + if PyObject_CheckBuffer(owner): + # Can very likely use PyBUF_ND instead of PyBUF_SIMPLE + res = PyObject_GetBuffer(owner, cython.address(self.view), PyBUF_SIMPLE) + if not res: + self.has_view = True + self.ptr = cython.cast(cython.p_uchar, self.view.buf) + self.length = self.view.len + return + + raise TypeError("expected bytes, bytearray or memoryview") + + def __dealloc__(self): + if self.has_view: + PyBuffer_Release(cython.address(self.view)) + + +@cython.cfunc +def bytesource(obj, allow_none: cython.bint = False) -> ByteSource | None: + if allow_none and obj is None: + return None + elif isinstance(obj, ByteSource): + return obj + else: + return ByteSource(obj) + + @cython.cclass class Buffer: """A base class for PyAV objects which support the buffer protocol, such @@ -29,7 +73,6 @@ def __getbuffer__(self, view: cython.pointer[Py_buffer], flags: cython.int): @property def buffer_size(self): - """The size of the buffer in bytes.""" return self._buffer_size() @property diff --git a/av/bytesource.pxd b/av/bytesource.pxd deleted file mode 100644 index 050baab35..000000000 --- a/av/bytesource.pxd +++ /dev/null @@ -1,14 +0,0 @@ -from cpython.buffer cimport Py_buffer - - -cdef class ByteSource: - - cdef object owner - - cdef bint has_view - cdef Py_buffer view - - cdef unsigned char *ptr - cdef size_t length - -cdef ByteSource bytesource(object, bint allow_none=*) diff --git a/av/bytesource.pyx b/av/bytesource.pyx deleted file mode 100644 index 9192c6d1a..000000000 --- a/av/bytesource.pyx +++ /dev/null @@ -1,43 +0,0 @@ -from cpython.buffer cimport ( - PyBUF_SIMPLE, - PyBuffer_Release, - PyObject_CheckBuffer, - PyObject_GetBuffer, -) - - -cdef class ByteSource: - def __cinit__(self, owner): - self.owner = owner - - try: - self.ptr = owner - except TypeError: - pass - else: - self.length = len(owner) - return - - if PyObject_CheckBuffer(owner): - # Can very likely use PyBUF_ND instead of PyBUF_SIMPLE - res = PyObject_GetBuffer(owner, &self.view, PyBUF_SIMPLE) - if not res: - self.has_view = True - self.ptr = self.view.buf - self.length = self.view.len - return - - raise TypeError("expected bytes, bytearray or memoryview") - - def __dealloc__(self): - if self.has_view: - PyBuffer_Release(&self.view) - - -cdef ByteSource bytesource(obj, bint allow_none=False): - if allow_none and obj is None: - return - elif isinstance(obj, ByteSource): - return obj - else: - return ByteSource(obj) diff --git a/av/codec/context.pxd b/av/codec/context.pxd index a9722c384..3d87023d7 100644 --- a/av/codec/context.pxd +++ b/av/codec/context.pxd @@ -1,7 +1,7 @@ cimport libav as lib from libc.stdint cimport int64_t -from av.bytesource cimport ByteSource +from av.buffer cimport ByteSource from av.codec.codec cimport Codec from av.codec.hwaccel cimport HWAccel from av.frame cimport Frame diff --git a/av/codec/context.py b/av/codec/context.py index 4751e597f..27bcf9493 100644 --- a/av/codec/context.py +++ b/av/codec/context.py @@ -2,7 +2,7 @@ import cython from cython.cimports import libav as lib -from cython.cimports.av.bytesource import ByteSource, bytesource +from cython.cimports.av.buffer import ByteSource, bytesource from cython.cimports.av.codec.codec import Codec, wrap_codec from cython.cimports.av.dictionary import _Dictionary from cython.cimports.av.error import err_check diff --git a/av/packet.pxd b/av/packet.pxd index 2640a5aeb..9bd077d79 100644 --- a/av/packet.pxd +++ b/av/packet.pxd @@ -2,8 +2,7 @@ from cython.cimports.libc.stdint import uint8_t cimport libav as lib -from av.buffer cimport Buffer -from av.bytesource cimport ByteSource +from av.buffer cimport Buffer, ByteSource from av.stream cimport Stream diff --git a/av/packet.py b/av/packet.py index 867d46211..09bd0e1bd 100644 --- a/av/packet.py +++ b/av/packet.py @@ -2,8 +2,7 @@ import cython from cython.cimports import libav as lib -from cython.cimports.av.buffer import Buffer -from cython.cimports.av.bytesource import ByteSource, bytesource +from cython.cimports.av.buffer import Buffer, ByteSource, bytesource from cython.cimports.av.error import err_check from cython.cimports.av.opaque import noop_free, opaque_container from cython.cimports.av.utils import avrational_to_fraction, to_avrational diff --git a/av/subtitles/codeccontext.py b/av/subtitles/codeccontext.py index 78c274df2..6db7309dd 100644 --- a/av/subtitles/codeccontext.py +++ b/av/subtitles/codeccontext.py @@ -1,6 +1,6 @@ import cython from cython.cimports import libav as lib -from cython.cimports.av.bytesource import ByteSource, bytesource +from cython.cimports.av.buffer import ByteSource, bytesource from cython.cimports.av.error import err_check from cython.cimports.av.packet import Packet from cython.cimports.av.subtitles.subtitle import SubtitleProxy, SubtitleSet From d83393f0a53fc18757f6b5323795aad8931373e7 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 31 Jan 2026 13:12:35 -0500 Subject: [PATCH 665/846] Combine av/indexenr* to av/index.py --- av/{indexentries.pxd => index.pxd} | 5 ++- av/{indexentries.py => index.py} | 60 +++++++++++++++++++++++++++- av/{indexentries.pyi => index.pyi} | 9 ++++- av/indexentry.pxd | 9 ----- av/indexentry.py | 63 ------------------------------ av/indexentry.pyi | 8 ---- av/stream.pxd | 2 +- av/stream.py | 2 +- av/stream.pyi | 2 +- 9 files changed, 74 insertions(+), 86 deletions(-) rename av/{indexentries.pxd => index.pxd} (64%) rename av/{indexentries.py => index.py} (66%) rename av/{indexentries.pyi => index.pyi} (74%) delete mode 100644 av/indexentry.pxd delete mode 100644 av/indexentry.py delete mode 100644 av/indexentry.pyi diff --git a/av/indexentries.pxd b/av/index.pxd similarity index 64% rename from av/indexentries.pxd rename to av/index.pxd index 44575476a..f8ab87835 100644 --- a/av/indexentries.pxd +++ b/av/index.pxd @@ -1,9 +1,12 @@ cimport libav as lib +cdef class IndexEntry: + cdef lib.AVIndexEntry *ptr + cdef _init(self, lib.AVIndexEntry *ptr) + cdef class IndexEntries: cdef lib.AVStream *stream_ptr cdef _init(self, lib.AVStream *ptr) - cdef IndexEntries wrap_index_entries(lib.AVStream *ptr) diff --git a/av/indexentries.py b/av/index.py similarity index 66% rename from av/indexentries.py rename to av/index.py index ecbc83d27..4cba7d37e 100644 --- a/av/indexentries.py +++ b/av/index.py @@ -1,11 +1,69 @@ import cython import cython.cimports.libav as lib -from cython.cimports.av.indexentry import wrap_index_entry from cython.cimports.libc.stdint import int64_t _cinit_bypass_sentinel = cython.declare(object, object()) +@cython.cfunc +def wrap_index_entry(ptr: cython.pointer[lib.AVIndexEntry]) -> IndexEntry: + obj: IndexEntry = IndexEntry(_cinit_bypass_sentinel) + obj._init(ptr) + return obj + + +@cython.cclass +class IndexEntry: + """A single entry from a stream's index. + + This is a thin wrapper around FFmpeg's ``AVIndexEntry``. + + The exact meaning of the fields depends on the container/demuxer. + """ + + def __cinit__(self, sentinel): + if sentinel is not _cinit_bypass_sentinel: + raise RuntimeError("cannot manually instantiate IndexEntry") + + @cython.cfunc + def _init(self, ptr: cython.pointer[lib.AVIndexEntry]): + self.ptr = ptr + + def __repr__(self): + return ( + f"" + ) + + @property + def pos(self): + return self.ptr.pos + + @property + def timestamp(self): + return self.ptr.timestamp + + @property + def flags(self): + return self.ptr.flags + + @property + def is_keyframe(self): + return bool(self.ptr.flags & lib.AVINDEX_KEYFRAME) + + @property + def is_discard(self): + return bool(self.ptr.flags & lib.AVINDEX_DISCARD_FRAME) + + @property + def size(self): + return self.ptr.size + + @property + def min_distance(self): + return self.ptr.min_distance + + @cython.cfunc def wrap_index_entries(ptr: cython.pointer[lib.AVStream]) -> IndexEntries: obj: IndexEntries = IndexEntries(_cinit_bypass_sentinel) diff --git a/av/indexentries.pyi b/av/index.pyi similarity index 74% rename from av/indexentries.pyi rename to av/index.pyi index c5d495711..b74262609 100644 --- a/av/indexentries.pyi +++ b/av/index.pyi @@ -1,6 +1,13 @@ from typing import Iterator, overload -from av.indexentry import IndexEntry +class IndexEntry: + pos: int + timestamp: int + flags: int + is_keyframe: bool + is_discard: bool + size: int + min_distance: int class IndexEntries: def __len__(self) -> int: ... diff --git a/av/indexentry.pxd b/av/indexentry.pxd deleted file mode 100644 index 55bac3ff4..000000000 --- a/av/indexentry.pxd +++ /dev/null @@ -1,9 +0,0 @@ -cimport libav as lib - - -cdef class IndexEntry: - cdef lib.AVIndexEntry *ptr - cdef _init(self, lib.AVIndexEntry *ptr) - - -cdef IndexEntry wrap_index_entry(lib.AVIndexEntry *ptr) diff --git a/av/indexentry.py b/av/indexentry.py deleted file mode 100644 index 4d793f0cd..000000000 --- a/av/indexentry.py +++ /dev/null @@ -1,63 +0,0 @@ -import cython -import cython.cimports.libav as lib - -_cinit_bypass_sentinel = cython.declare(object, object()) - - -@cython.cfunc -def wrap_index_entry(ptr: cython.pointer[lib.AVIndexEntry]) -> IndexEntry: - obj: IndexEntry = IndexEntry(_cinit_bypass_sentinel) - obj._init(ptr) - return obj - - -@cython.cclass -class IndexEntry: - """A single entry from a stream's index. - - This is a thin wrapper around FFmpeg's ``AVIndexEntry``. - - The exact meaning of the fields depends on the container/demuxer. - """ - - def __cinit__(self, sentinel): - if sentinel is not _cinit_bypass_sentinel: - raise RuntimeError("cannot manually instantiate IndexEntry") - - @cython.cfunc - def _init(self, ptr: cython.pointer[lib.AVIndexEntry]): - self.ptr = ptr - - def __repr__(self): - return ( - f"" - ) - - @property - def pos(self): - return self.ptr.pos - - @property - def timestamp(self): - return self.ptr.timestamp - - @property - def flags(self): - return self.ptr.flags - - @property - def is_keyframe(self): - return bool(self.ptr.flags & lib.AVINDEX_KEYFRAME) - - @property - def is_discard(self): - return bool(self.ptr.flags & lib.AVINDEX_DISCARD_FRAME) - - @property - def size(self): - return self.ptr.size - - @property - def min_distance(self): - return self.ptr.min_distance diff --git a/av/indexentry.pyi b/av/indexentry.pyi deleted file mode 100644 index 5365bc768..000000000 --- a/av/indexentry.pyi +++ /dev/null @@ -1,8 +0,0 @@ -class IndexEntry: - pos: int - timestamp: int - flags: int - is_keyframe: bool - is_discard: bool - size: int - min_distance: int diff --git a/av/stream.pxd b/av/stream.pxd index ecdbe8029..89d41e559 100644 --- a/av/stream.pxd +++ b/av/stream.pxd @@ -3,7 +3,7 @@ cimport libav as lib from av.codec.context cimport CodecContext from av.container.core cimport Container from av.frame cimport Frame -from av.indexentries cimport IndexEntries +from av.index cimport IndexEntries from av.packet cimport Packet diff --git a/av/stream.py b/av/stream.py index 868def2ab..05a45d92a 100644 --- a/av/stream.py +++ b/av/stream.py @@ -3,7 +3,7 @@ import cython from cython.cimports import libav as lib from cython.cimports.av.error import err_check -from cython.cimports.av.indexentries import wrap_index_entries +from cython.cimports.av.index import wrap_index_entries from cython.cimports.av.packet import Packet from cython.cimports.av.utils import ( avdict_to_dict, diff --git a/av/stream.pyi b/av/stream.pyi index 8790d2601..3c4e55f6a 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -4,7 +4,7 @@ from typing import Literal, cast from .codec import Codec, CodecContext from .container import Container -from .indexentries import IndexEntries +from .index import IndexEntries class Disposition(Flag): default = cast(int, ...) From d89e4a2bed1827edb2608e695b8b8290ae372c10 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 31 Jan 2026 13:39:04 -0500 Subject: [PATCH 666/846] Make codec/hwaccel pure --- av/codec/{hwaccel.pyx => hwaccel.py} | 83 +++++++++++++++++----------- 1 file changed, 50 insertions(+), 33 deletions(-) rename av/codec/{hwaccel.pyx => hwaccel.py} (67%) diff --git a/av/codec/hwaccel.pyx b/av/codec/hwaccel.py similarity index 67% rename from av/codec/hwaccel.pyx rename to av/codec/hwaccel.py index e9a6bd194..6508a5867 100644 --- a/av/codec/hwaccel.pyx +++ b/av/codec/hwaccel.py @@ -1,12 +1,12 @@ import weakref from enum import IntEnum -cimport libav as lib - -from av.codec.codec cimport Codec -from av.dictionary cimport _Dictionary -from av.error cimport err_check -from av.video.format cimport get_video_format +import cython +import cython.cimports.libav as lib +from cython.cimports.av.codec.codec import Codec +from cython.cimports.av.dictionary import _Dictionary +from cython.cimports.av.error import err_check +from cython.cimports.av.video.format import get_video_format from av.dictionary import Dictionary @@ -29,34 +29,41 @@ class HWDeviceType(IntEnum): ohcodec = 14 # TODO: When ffmpeg major is changed, check this enum. + class HWConfigMethod(IntEnum): none = 0 - hw_device_ctx = lib.AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX # This is the only one we support. + hw_device_ctx = ( + lib.AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX + ) # This is the only one we support. hw_frame_ctx = lib.AV_CODEC_HW_CONFIG_METHOD_HW_FRAMES_CTX internal = lib.AV_CODEC_HW_CONFIG_METHOD_INTERNAL ad_hoc = lib.AV_CODEC_HW_CONFIG_METHOD_AD_HOC -cdef object _cinit_sentinel = object() -cdef object _singletons = weakref.WeakValueDictionary() +_cinit_sentinel = cython.declare(object, object()) +_singletons = cython.declare(object, weakref.WeakValueDictionary()) + -cdef HWConfig wrap_hwconfig(lib.AVCodecHWConfig *ptr): +@cython.cfunc +def wrap_hwconfig(ptr: cython.pointer[lib.AVCodecHWConfig]) -> HWConfig: try: - return _singletons[ptr] + return _singletons[cython.cast(cython.int, ptr)] except KeyError: pass - cdef HWConfig config = HWConfig(_cinit_sentinel) + config: HWConfig = HWConfig(_cinit_sentinel) config._init(ptr) - _singletons[ptr] = config + _singletons[cython.cast(cython.int, ptr)] = config return config -cdef class HWConfig: +@cython.cclass +class HWConfig: def __init__(self, sentinel): if sentinel is not _cinit_sentinel: raise RuntimeError("Cannot instantiate CodecContext") - cdef void _init(self, lib.AVCodecHWConfig *ptr): + @cython.cfunc + def _init(self, ptr: cython.pointer[lib.AVCodecHWConfig]) -> cython.void: self.ptr = ptr def __repr__(self): @@ -64,7 +71,7 @@ def __repr__(self): f"self.ptr:x}>" + f"is_supported={self.is_supported} at 0x{cython.cast(int, self.ptr):x}>" ) @property @@ -84,21 +91,28 @@ def is_supported(self): return bool(self.ptr.methods & lib.AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX) -cpdef hwdevices_available(): - result = [] - - cdef lib.AVHWDeviceType x = lib.AV_HWDEVICE_TYPE_NONE +@cython.ccall +def hwdevices_available(): + result: list = [] + x: lib.AVHWDeviceType = lib.AV_HWDEVICE_TYPE_NONE while True: x = lib.av_hwdevice_iterate_types(x) if x == lib.AV_HWDEVICE_TYPE_NONE: break result.append(lib.av_hwdevice_get_type_name(HWDeviceType(x))) - return result -cdef class HWAccel: - def __init__(self, device_type, device=None, allow_software_fallback=True, options=None, flags=None): +@cython.cclass +class HWAccel: + def __init__( + self, + device_type, + device=None, + allow_software_fallback=True, + options=None, + flags=None, + ): if isinstance(device_type, HWDeviceType): self._device_type = device_type elif isinstance(device_type, str): @@ -112,11 +126,11 @@ def __init__(self, device_type, device=None, allow_software_fallback=True, optio self.allow_software_fallback = allow_software_fallback self.options = {} if not options else dict(options) self.flags = 0 if not flags else flags - self.ptr = NULL + self.ptr = cython.NULL self.config = None - def _initialize_hw_context(self, Codec codec not None): - cdef HWConfig config + def _initialize_hw_context(self, codec: Codec): + config: HWConfig for config in codec.hardware_configs: if not (config.ptr.methods & lib.AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX): continue @@ -127,20 +141,23 @@ def _initialize_hw_context(self, Codec codec not None): raise NotImplementedError(f"No supported hardware config for {codec}") self.config = config - - cdef char *c_device = NULL + c_device: cython.p_char = cython.NULL if self._device: device_bytes = self._device.encode() c_device = device_bytes - cdef _Dictionary c_options = Dictionary(self.options) + c_options: _Dictionary = Dictionary(self.options) err_check( lib.av_hwdevice_ctx_create( - &self.ptr, config.ptr.device_type, c_device, c_options.ptr, self.flags + cython.address(self.ptr), + config.ptr.device_type, + c_device, + c_options.ptr, + self.flags, ) ) - def create(self, Codec codec not None): + def create(self, codec: Codec): """Create a new hardware accelerator context with the given codec""" if self.ptr: raise RuntimeError("Hardware context already initialized") @@ -149,11 +166,11 @@ def create(self, Codec codec not None): device_type=self._device_type, device=self._device, allow_software_fallback=self.allow_software_fallback, - options=self.options + options=self.options, ) ret._initialize_hw_context(codec) return ret def __dealloc__(self): if self.ptr: - lib.av_buffer_unref(&self.ptr) + lib.av_buffer_unref(cython.address(self.ptr)) From a388589a73b0f983e511765cc9279b82f8bb3d5a Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 1 Feb 2026 12:41:26 -0500 Subject: [PATCH 667/846] Make av/descriptor pure --- av/descriptor.py | 75 +++++++++++++++++++++++++++++++++++++++++++++++ av/descriptor.pyx | 59 ------------------------------------- 2 files changed, 75 insertions(+), 59 deletions(-) create mode 100644 av/descriptor.py delete mode 100644 av/descriptor.pyx diff --git a/av/descriptor.py b/av/descriptor.py new file mode 100644 index 000000000..20cd1a787 --- /dev/null +++ b/av/descriptor.py @@ -0,0 +1,75 @@ +import cython +import cython.cimports.libav as lib +from cython.cimports.av.option import ( + Option, + OptionChoice, + wrap_option, + wrap_option_choice, +) + +_cinit_sentinel = cython.declare(object, object()) + + +@cython.cfunc +def wrap_avclass(ptr: cython.pointer[cython.const[lib.AVClass]]) -> Descriptor | None: + if ptr == cython.NULL: + return None + obj: Descriptor = Descriptor(_cinit_sentinel) + obj.ptr = ptr + return obj + + +@cython.cclass +class Descriptor: + def __cinit__(self, sentinel): + if sentinel is not _cinit_sentinel: + raise RuntimeError("Cannot construct av.Descriptor") + + @property + def name(self): + return self.ptr.class_name if self.ptr.class_name else None + + @property + def options(self): + ptr: cython.pointer[cython.const[lib.AVOption]] = self.ptr.option + choice_ptr: cython.pointer[cython.const[lib.AVOption]] + option: Option + option_choice: OptionChoice + choice_is_default: cython.bint + if self._options is None: + options: list = [] + ptr = self.ptr.option + while ptr != cython.NULL and ptr.name != cython.NULL: + if ptr.type == lib.AV_OPT_TYPE_CONST: + ptr += 1 + continue + choices: list = [] + if ( + ptr.unit != cython.NULL + ): # option has choices (matching const options) + choice_ptr = self.ptr.option + while choice_ptr != cython.NULL and choice_ptr.name != cython.NULL: + if ( + choice_ptr.type != lib.AV_OPT_TYPE_CONST + or choice_ptr.unit != ptr.unit + ): + choice_ptr += 1 + continue + choice_is_default = ( + choice_ptr.default_val.i64 == ptr.default_val.i64 + or ptr.type == lib.AV_OPT_TYPE_FLAGS + and choice_ptr.default_val.i64 & ptr.default_val.i64 + ) + option_choice = wrap_option_choice( + choice_ptr, choice_is_default + ) + choices.append(option_choice) + choice_ptr += 1 + option = wrap_option(tuple(choices), ptr) + options.append(option) + ptr += 1 + self._options = tuple(options) + return self._options + + def __repr__(self): + return f"<{self.__class__.__name__} {self.name} at 0x{id(self):x}>" diff --git a/av/descriptor.pyx b/av/descriptor.pyx deleted file mode 100644 index 720db1625..000000000 --- a/av/descriptor.pyx +++ /dev/null @@ -1,59 +0,0 @@ -cimport libav as lib - -from .option cimport Option, OptionChoice, wrap_option, wrap_option_choice - - -cdef object _cinit_sentinel = object() - -cdef Descriptor wrap_avclass(const lib.AVClass *ptr): - if ptr == NULL: - return None - cdef Descriptor obj = Descriptor(_cinit_sentinel) - obj.ptr = ptr - return obj - - -cdef class Descriptor: - def __cinit__(self, sentinel): - if sentinel is not _cinit_sentinel: - raise RuntimeError("Cannot construct av.Descriptor") - - @property - def name(self): - return self.ptr.class_name if self.ptr.class_name else None - - @property - def options(self): - cdef const lib.AVOption *ptr = self.ptr.option - cdef const lib.AVOption *choice_ptr - cdef Option option - cdef OptionChoice option_choice - cdef bint choice_is_default - if self._options is None: - options = [] - ptr = self.ptr.option - while ptr != NULL and ptr.name != NULL: - if ptr.type == lib.AV_OPT_TYPE_CONST: - ptr += 1 - continue - choices = [] - if ptr.unit != NULL: # option has choices (matching const options) - choice_ptr = self.ptr.option - while choice_ptr != NULL and choice_ptr.name != NULL: - if choice_ptr.type != lib.AV_OPT_TYPE_CONST or choice_ptr.unit != ptr.unit: - choice_ptr += 1 - continue - choice_is_default = (choice_ptr.default_val.i64 == ptr.default_val.i64 or - ptr.type == lib.AV_OPT_TYPE_FLAGS and - choice_ptr.default_val.i64 & ptr.default_val.i64) - option_choice = wrap_option_choice(choice_ptr, choice_is_default) - choices.append(option_choice) - choice_ptr += 1 - option = wrap_option(tuple(choices), ptr) - options.append(option) - ptr += 1 - self._options = tuple(options) - return self._options - - def __repr__(self): - return f"<{self.__class__.__name__} {self.name} at 0x{id(self):x}>" From 1746ef49c0f4e9d77eb762e52ffe6a80b90af471 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 1 Feb 2026 13:11:14 -0500 Subject: [PATCH 668/846] Make av/opaque pure --- av/opaque.py | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++ av/opaque.pyx | 50 ---------------------------------------- 2 files changed, 63 insertions(+), 50 deletions(-) create mode 100644 av/opaque.py delete mode 100644 av/opaque.pyx diff --git a/av/opaque.py b/av/opaque.py new file mode 100644 index 000000000..7d295699a --- /dev/null +++ b/av/opaque.py @@ -0,0 +1,63 @@ +# type:ignore +import cython +import cython.cimports.libav as lib +from cython import NULL, sizeof +from cython.cimports.libc.stdint import uint8_t, uintptr_t +from cython.cimports.libc.string import memcpy + +u8ptr = cython.typedef(cython.pointer[uint8_t]) + + +@cython.cfunc +@cython.exceptval(check=False) +@cython.nogil +def noop_free(opaque: cython.p_void, data: u8ptr) -> cython.void: + pass + + +@cython.cfunc +@cython.exceptval(check=False) +@cython.nogil +def key_free(opaque: cython.p_void, data: u8ptr) -> cython.void: + name: cython.p_char = cython.cast(cython.p_char, data) + with cython.gil: + opaque_container.pop(name) + + +@cython.cclass +class OpaqueContainer: + def __cinit__(self): + self._objects = {} + + @cython.cfunc + def add(self, v: object) -> cython.pointer[lib.AVBufferRef]: + # Use object's memory address as key + key: uintptr_t = cython.cast(cython.longlong, id(v)) + self._objects[key] = v + + data: u8ptr = cython.cast(u8ptr, lib.av_malloc(sizeof(uintptr_t))) + if data == NULL: + raise MemoryError("Failed to allocate memory for key") + + memcpy(data, cython.address(key), sizeof(uintptr_t)) + + # Create the buffer with our free callback + buffer_ref: cython.pointer[lib.AVBufferRef] = lib.av_buffer_create( + data, sizeof(uintptr_t), key_free, NULL, 0 + ) + + if buffer_ref == NULL: + raise MemoryError("Failed to create AVBufferRef") + + return buffer_ref + + def get(self, name) -> object: + key: uintptr_t = cython.cast(cython.pointer[uintptr_t], name)[0] + return self._objects.get(key) + + def pop(self, name) -> object: + key: uintptr_t = cython.cast(cython.pointer[uintptr_t], name)[0] + return self._objects.pop(key, None) + + +opaque_container: OpaqueContainer = OpaqueContainer() diff --git a/av/opaque.pyx b/av/opaque.pyx deleted file mode 100644 index 583ea5577..000000000 --- a/av/opaque.pyx +++ /dev/null @@ -1,50 +0,0 @@ -cimport libav as lib -from libc.stdint cimport uint8_t, uintptr_t -from libc.string cimport memcpy - - -cdef void noop_free(void *opaque, uint8_t *data) noexcept nogil: - pass - - -cdef void key_free(void *opaque, uint8_t *data) noexcept nogil: - cdef char *name = data - with gil: - opaque_container.pop(name) - - -cdef class OpaqueContainer: - def __cinit__(self): - self._objects = {} - - cdef lib.AVBufferRef *add(self, object v): - # Use object's memory address as key - cdef uintptr_t key = id(v) - self._objects[key] = v - - cdef uint8_t *data = lib.av_malloc(sizeof(uintptr_t)) - if data == NULL: - raise MemoryError("Failed to allocate memory for key") - - memcpy(data, &key, sizeof(uintptr_t)) - - # Create the buffer with our free callback - cdef lib.AVBufferRef *buffer_ref = lib.av_buffer_create( - data, sizeof(uintptr_t), key_free, NULL, 0 - ) - - if buffer_ref == NULL: - raise MemoryError("Failed to create AVBufferRef") - - return buffer_ref - - cdef object get(self, char *name): - cdef uintptr_t key = (name)[0] - return self._objects.get(key) - - cdef object pop(self, char *name): - cdef uintptr_t key = (name)[0] - return self._objects.pop(key, None) - - -cdef OpaqueContainer opaque_container = OpaqueContainer() From 604fa0eb85838c075efbf9eee358e3256f1e5389 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 1 Feb 2026 13:38:04 -0500 Subject: [PATCH 669/846] Make av/option pure --- av/opaque.pxd | 1 - av/option.pxd | 7 --- av/{option.pyx => option.py} | 107 +++++++++++++++++++++-------------- 3 files changed, 65 insertions(+), 50 deletions(-) rename av/{option.pyx => option.py} (68%) diff --git a/av/opaque.pxd b/av/opaque.pxd index 808f6338e..5a0a423e3 100644 --- a/av/opaque.pxd +++ b/av/opaque.pxd @@ -7,7 +7,6 @@ cdef void noop_free(void *opaque, uint8_t *data) noexcept nogil cdef class OpaqueContainer: cdef dict _objects - cdef lib.AVBufferRef *add(self, object v) cdef object get(self, char *name) cdef object pop(self, char *name) diff --git a/av/option.pxd b/av/option.pxd index 9087b811c..4c8eaffbf 100644 --- a/av/option.pxd +++ b/av/option.pxd @@ -2,20 +2,13 @@ cimport libav as lib cdef class BaseOption: - cdef const lib.AVOption *ptr - cdef class Option(BaseOption): - cdef readonly tuple choices - cdef class OptionChoice(BaseOption): - cdef readonly bint is_default - cdef Option wrap_option(tuple choices, const lib.AVOption *ptr) - cdef OptionChoice wrap_option_choice(const lib.AVOption *ptr, bint is_default) diff --git a/av/option.pyx b/av/option.py similarity index 68% rename from av/option.pyx rename to av/option.py index 604d81a53..6a20af14f 100644 --- a/av/option.pyx +++ b/av/option.py @@ -1,24 +1,27 @@ -cimport libav as lib -from libc.stdint cimport uint64_t - from enum import Enum, Flag +import cython +import cython.cimports.libav as lib +from cython import NULL, bint +from cython.cimports.libc.stdint import uint64_t + +_cinit_sentinel = cython.declare(object, object()) -cdef object _cinit_sentinel = object() -cdef Option wrap_option(tuple choices, const lib.AVOption *ptr): +@cython.cfunc +def wrap_option( + choices: tuple, ptr: cython.pointer[cython.const[lib.AVOption]] +) -> Option: if ptr == NULL: return None - cdef Option obj = Option(_cinit_sentinel) + obj: Option = Option(_cinit_sentinel) obj.ptr = ptr obj.choices = choices return obj -cdef flag_in_bitfield(uint64_t bitfield, uint64_t flag): - # Not every flag exists in every version of FFMpeg, so we define them to 0. - if not flag: - return None +@cython.cfunc +def flag_in_bitfield(bitfield: uint64_t, flag: uint64_t) -> bool: return bool(bitfield & flag) @@ -43,16 +46,6 @@ class OptionType(Enum): CHANNEL_LAYOUT = lib.AV_OPT_TYPE_CHLAYOUT BOOL = lib.AV_OPT_TYPE_BOOL -cdef tuple _INT_TYPES = ( - lib.AV_OPT_TYPE_FLAGS, - lib.AV_OPT_TYPE_INT, - lib.AV_OPT_TYPE_INT64, - lib.AV_OPT_TYPE_PIXEL_FMT, - lib.AV_OPT_TYPE_SAMPLE_FMT, - lib.AV_OPT_TYPE_DURATION, - lib.AV_OPT_TYPE_CHLAYOUT, - lib.AV_OPT_TYPE_BOOL, -) class OptionFlags(Flag): ENCODING_PARAM = lib.AV_OPT_FLAG_ENCODING_PARAM @@ -65,7 +58,23 @@ class OptionFlags(Flag): FILTERING_PARAM = lib.AV_OPT_FLAG_FILTERING_PARAM -cdef class BaseOption: +_INT_TYPES = cython.declare( + tuple, + ( + lib.AV_OPT_TYPE_FLAGS, + lib.AV_OPT_TYPE_INT, + lib.AV_OPT_TYPE_INT64, + lib.AV_OPT_TYPE_PIXEL_FMT, + lib.AV_OPT_TYPE_SAMPLE_FMT, + lib.AV_OPT_TYPE_DURATION, + lib.AV_OPT_TYPE_CHLAYOUT, + lib.AV_OPT_TYPE_BOOL, + ), +) + + +@cython.cclass +class BaseOption: def __cinit__(self, sentinel): if sentinel is not _cinit_sentinel: raise RuntimeError(f"Cannot construct av.{self.__class__.__name__}") @@ -82,34 +91,41 @@ def help(self): def flags(self): return self.ptr.flags - # Option flags @property def is_encoding_param(self): return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_ENCODING_PARAM) + @property def is_decoding_param(self): return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_DECODING_PARAM) + @property def is_audio_param(self): return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_AUDIO_PARAM) + @property def is_video_param(self): return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_VIDEO_PARAM) + @property def is_subtitle_param(self): return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_SUBTITLE_PARAM) + @property def is_export(self): return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_EXPORT) + @property def is_readonly(self): return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_READONLY) + @property def is_filtering_param(self): return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_FILTERING_PARAM) -cdef class Option(BaseOption): +@cython.cclass +class Option(BaseOption): @property def type(self): return OptionType(self.ptr.type) @@ -126,45 +142,52 @@ def offset(self): def default(self): if self.ptr.type in _INT_TYPES: return self.ptr.default_val.i64 - if self.ptr.type in (lib.AV_OPT_TYPE_DOUBLE, lib.AV_OPT_TYPE_FLOAT, - lib.AV_OPT_TYPE_RATIONAL): + if self.ptr.type in ( + lib.AV_OPT_TYPE_DOUBLE, + lib.AV_OPT_TYPE_FLOAT, + lib.AV_OPT_TYPE_RATIONAL, + ): return self.ptr.default_val.dbl - if self.ptr.type in (lib.AV_OPT_TYPE_STRING, lib.AV_OPT_TYPE_BINARY, - lib.AV_OPT_TYPE_IMAGE_SIZE, lib.AV_OPT_TYPE_VIDEO_RATE, - lib.AV_OPT_TYPE_COLOR): + if self.ptr.type in ( + lib.AV_OPT_TYPE_STRING, + lib.AV_OPT_TYPE_BINARY, + lib.AV_OPT_TYPE_IMAGE_SIZE, + lib.AV_OPT_TYPE_VIDEO_RATE, + lib.AV_OPT_TYPE_COLOR, + ): return self.ptr.default_val.str if self.ptr.default_val.str != NULL else "" - def _norm_range(self, value): - if self.ptr.type in _INT_TYPES: - return int(value) - return value - @property def min(self): - return self._norm_range(self.ptr.min) + if self.ptr.type in _INT_TYPES: + return int(self.ptr.min) + return self.ptr.min @property def max(self): - return self._norm_range(self.ptr.max) + if self.ptr.type in _INT_TYPES: + return int(self.ptr.max) + return self.ptr.max def __repr__(self): - return ( - f"" - ) + return f"" -cdef OptionChoice wrap_option_choice(const lib.AVOption *ptr, bint is_default): +@cython.cfunc +def wrap_option_choice( + ptr: cython.pointer[cython.const[lib.AVOption]], is_default: bint +) -> OptionChoice | None: if ptr == NULL: return None - cdef OptionChoice obj = OptionChoice(_cinit_sentinel) + obj: OptionChoice = OptionChoice(_cinit_sentinel) obj.ptr = ptr obj.is_default = is_default return obj -cdef class OptionChoice(BaseOption): +@cython.cclass +class OptionChoice(BaseOption): """ Represents AV_OPT_TYPE_CONST options which are essentially choices of non-const option with same unit. From 5ec88e884b39b1a3cfd82b4ecc6a85ce9a489edd Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 1 Feb 2026 13:49:14 -0500 Subject: [PATCH 670/846] Fix pointer casting warnings --- av/sidedata/motionvectors.py | 3 ++- av/sidedata/sidedata.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/av/sidedata/motionvectors.py b/av/sidedata/motionvectors.py index 04e06f25e..dcf0e9913 100644 --- a/av/sidedata/motionvectors.py +++ b/av/sidedata/motionvectors.py @@ -1,6 +1,7 @@ import cython from cython.cimports import libav as lib from cython.cimports.av.sidedata.sidedata import SideData +from cython.cimports.libc.stdint import uintptr_t _cinit_bypass_sentinel = cython.declare(object, object()) @@ -15,7 +16,7 @@ def __init__(self, sentinel, frame: Frame, index: cython.int): def __repr__(self): return ( f"" + f"of {len(self)} vectors at 0x{cython.cast(uintptr_t, self.ptr.data):0x}>" ) def __len__(self): diff --git a/av/sidedata/sidedata.py b/av/sidedata/sidedata.py index 24f58a3e0..f19aeac06 100644 --- a/av/sidedata/sidedata.py +++ b/av/sidedata/sidedata.py @@ -2,7 +2,7 @@ from enum import Enum import cython -from cython.cimports.libc.stdint import int32_t +from cython.cimports.libc.stdint import int32_t, uintptr_t from av.sidedata.encparams import VideoEncParams from av.sidedata.motionvectors import MotionVectors @@ -95,7 +95,7 @@ def _buffer_writable(self) -> cython.bint: return False def __repr__(self): - return f"" + return f"" @property def type(self): From e8c0269b6a8373f75f57178457173c0315b7c456 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 1 Feb 2026 14:17:51 -0500 Subject: [PATCH 671/846] AVIndex, fix const warns --- av/index.pxd | 4 ++-- av/index.py | 14 ++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/av/index.pxd b/av/index.pxd index f8ab87835..d7098c2f0 100644 --- a/av/index.pxd +++ b/av/index.pxd @@ -2,8 +2,8 @@ cimport libav as lib cdef class IndexEntry: - cdef lib.AVIndexEntry *ptr - cdef _init(self, lib.AVIndexEntry *ptr) + cdef const lib.AVIndexEntry *ptr + cdef _init(self, const lib.AVIndexEntry *ptr) cdef class IndexEntries: cdef lib.AVStream *stream_ptr diff --git a/av/index.py b/av/index.py index 4cba7d37e..504da9576 100644 --- a/av/index.py +++ b/av/index.py @@ -6,7 +6,7 @@ @cython.cfunc -def wrap_index_entry(ptr: cython.pointer[lib.AVIndexEntry]) -> IndexEntry: +def wrap_index_entry(ptr: cython.pointer[cython.const[lib.AVIndexEntry]]) -> IndexEntry: obj: IndexEntry = IndexEntry(_cinit_bypass_sentinel) obj._init(ptr) return obj @@ -26,7 +26,7 @@ def __cinit__(self, sentinel): raise RuntimeError("cannot manually instantiate IndexEntry") @cython.cfunc - def _init(self, ptr: cython.pointer[lib.AVIndexEntry]): + def _init(self, ptr: cython.pointer[cython.const[lib.AVIndexEntry]]): self.ptr = ptr def __repr__(self): @@ -110,19 +110,17 @@ def __getitem__(self, index): if index < 0 or index >= n: raise IndexError(f"Index entries {index} out of bounds for size {n}") - c_idx = cython.declare(cython.int, index) + c_idx: cython.int = index + entry: cython.pointer[cython.const[lib.AVIndexEntry]] with cython.nogil: entry = lib.avformat_index_get_entry(self.stream_ptr, c_idx) - if entry == cython.NULL: raise IndexError("index entry not found") return wrap_index_entry(entry) - elif isinstance(index, slice): start, stop, step = index.indices(len(self)) return [self[i] for i in range(start, stop, step)] - else: raise TypeError("Index must be an integer or a slice") @@ -135,8 +133,8 @@ def search_timestamp( Returns an index into this object, or ``-1`` if no match is found. """ - c_timestamp = cython.declare(int64_t, timestamp) - flags = cython.declare(cython.int, 0) + c_timestamp: int64_t = timestamp + flags: cython.int = 0 if backward: flags |= lib.AVSEEK_FLAG_BACKWARD From 8cd8926843125a2ab27d7618d435765fb324bf34 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 1 Feb 2026 14:22:27 -0500 Subject: [PATCH 672/846] Update installation.rst --- docs/overview/installation.rst | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/docs/overview/installation.rst b/docs/overview/installation.rst index 5fa7fc860..35da507d8 100644 --- a/docs/overview/installation.rst +++ b/docs/overview/installation.rst @@ -24,7 +24,7 @@ See the `Conda quick install = 18.04 LTS -^^^^^^^^^^^^^^^^^^^ - -On **Ubuntu 18.04 LTS** everything can come from the default sources:: - - # General dependencies - sudo apt-get install -y python-dev pkg-config - - # Library components - sudo apt-get install -y \ - libavformat-dev libavcodec-dev libavdevice-dev \ - libavutil-dev libswscale-dev libswresample-dev libavfilter-dev - - Windows ^^^^^^^ From 9aa5af99fdd963b4ddfa7058bc733b060f409358 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 1 Feb 2026 14:31:13 -0500 Subject: [PATCH 673/846] Fix doc warnings --- av/packet.py | 2 +- av/subtitles/subtitle.py | 21 +++++++++------------ docs/overview/installation.rst | 2 ++ 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/av/packet.py b/av/packet.py index 09bd0e1bd..5a97817e3 100644 --- a/av/packet.py +++ b/av/packet.py @@ -446,7 +446,7 @@ def has_sidedata(self, dtype: str) -> bool: def get_sidedata(self, dtype: str) -> PacketSideData: """get a copy of the side data - :param dtype: side data type (:method:`~av.packet.PacketSideData.sidedata_types` for the full list of options) + :param dtype: side data type (:meth:`~av.packet.PacketSideData.sidedata_types` for the full list of options) :type dtype: str :return: newly created copy of the side data if the side data of the requested type is found in the packet, else an empty object diff --git a/av/subtitles/subtitle.py b/av/subtitles/subtitle.py index 90eef433f..83f8041a1 100644 --- a/av/subtitles/subtitle.py +++ b/av/subtitles/subtitle.py @@ -47,21 +47,18 @@ def create( """ Create a SubtitleSet for encoding. - Args: - text: The subtitle text in ASS dialogue format - (e.g. b"0,0,Default,,0,0,0,,Hello World") - start: Start display time as offset from pts (typically 0) - end: End display time as offset from pts (i.e., duration) - pts: Presentation timestamp in stream time_base units - subtitle_format: Subtitle format (default 1 for text) - - Note: + :param text: The subtitle text in ASS dialogue format + (e.g. ``b"0,0,Default,,0,0,0,,Hello World"``) + :param start: Start display time as offset from pts (typically 0) + :param end: End display time as offset from pts (i.e., duration) + :param pts: Presentation timestamp in stream time_base units + :param subtitle_format: Subtitle format (default 1 for text) + :return: A SubtitleSet ready for encoding + + .. note:: All timing values should be in stream time_base units. For MKV (time_base=1/1000), units are milliseconds. For MP4 (time_base=1/1000000), units are microseconds. - - Returns: - A SubtitleSet ready for encoding """ subset: SubtitleSet = SubtitleSet(_cinit_bypass_sentinel) diff --git a/docs/overview/installation.rst b/docs/overview/installation.rst index 35da507d8..b4399ba48 100644 --- a/docs/overview/installation.rst +++ b/docs/overview/installation.rst @@ -27,7 +27,9 @@ Bring your own FFmpeg PyAV can also be compiled against your own build of FFmpeg (version ``8.0`` or higher). You can force installing PyAV from source by running: .. code-block:: bash + pip install av --no-binary av + PyAV depends upon several libraries from FFmpeg: - ``libavcodec`` From 5f0e312585be2d65f6373d66551bfe0a81b2aa2e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 1 Feb 2026 15:05:49 -0500 Subject: [PATCH 674/846] Make av/sidedata/encparams pure --- av/sidedata/{encparams.pyx => encparams.py} | 103 ++++++++++++-------- 1 file changed, 61 insertions(+), 42 deletions(-) rename av/sidedata/{encparams.pyx => encparams.py} (60%) diff --git a/av/sidedata/encparams.pyx b/av/sidedata/encparams.py similarity index 60% rename from av/sidedata/encparams.pyx rename to av/sidedata/encparams.py index 380dc53d4..399189a6b 100644 --- a/av/sidedata/encparams.pyx +++ b/av/sidedata/encparams.py @@ -1,19 +1,20 @@ -cimport libav as lib -from libc.stdint cimport int32_t, uint8_t - from enum import IntEnum -VideoEncParamsType = IntEnum( - "AVVideoEncParamsType", - { - "NONE": lib.AV_VIDEO_ENC_PARAMS_NONE, - "VP9": lib.AV_VIDEO_ENC_PARAMS_VP9, - "H264": lib.AV_VIDEO_ENC_PARAMS_H264, - "MPEG2": lib.AV_VIDEO_ENC_PARAMS_MPEG2, - }, -) - -cdef class VideoEncParams(SideData): +import cython +from cython.cimports import libav as lib +from cython.cimports.av.sidedata.sidedata import SideData +from cython.cimports.libc.stdint import uint8_t + + +class VideoEncParamsType(IntEnum): + NONE = lib.AV_VIDEO_ENC_PARAMS_NONE + VP9 = lib.AV_VIDEO_ENC_PARAMS_VP9 + H264 = lib.AV_VIDEO_ENC_PARAMS_H264 + MPEG2 = lib.AV_VIDEO_ENC_PARAMS_MPEG2 + + +@cython.cclass +class VideoEncParams(SideData): def __repr__(self): return f"" @@ -25,29 +26,37 @@ def nb_blocks(self): the values of blocks_offset / block_size are unspecified and should not be accessed. """ - return ( self.ptr.data).nb_blocks + return cython.cast( + cython.pointer[lib.AVVideoEncParams], self.ptr.data + ).nb_blocks @property def blocks_offset(self): """ Offset in bytes from the beginning of this structure at which the array of blocks starts. """ - return ( self.ptr.data).blocks_offset + return cython.cast( + cython.pointer[lib.AVVideoEncParams], self.ptr.data + ).blocks_offset @property def block_size(self): """ Size of each block in bytes. May not match sizeof(AVVideoBlockParams). """ - return ( self.ptr.data).block_size + return cython.cast( + cython.pointer[lib.AVVideoEncParams], self.ptr.data + ).block_size @property def codec_type(self): """ Type of the parameters (the codec they are used with). """ - cdef lib.AVVideoEncParamsType t = ( self.ptr.data).type - return VideoEncParamsType(t) + t: lib.AVVideoEncParamsType = cython.cast( + cython.pointer[lib.AVVideoEncParams], self.ptr.data + ).type + return VideoEncParamsType(cython.cast(cython.int, t)) @property def qp(self): @@ -57,15 +66,17 @@ def qp(self): combined with `delta_qp` and the per-block delta in a manner documented for each type. """ - return ( self.ptr.data).qp - + return cython.cast(cython.pointer[lib.AVVideoEncParams], self.ptr.data).qp + @property def delta_qp(self): """ Quantisation parameter offset from the base (per-frame) qp for a given plane (first index) and AC/DC coefficients (second index). """ - cdef lib.AVVideoEncParams *p = self.ptr.data + p: cython.pointer[lib.AVVideoEncParams] = cython.cast( + cython.pointer[lib.AVVideoEncParams], self.ptr.data + ) return [[p.delta_qp[i][j] for j in range(2)] for i in range(4)] def block_params(self, idx): @@ -85,25 +96,28 @@ def qp_map(self): """ import numpy as np - cdef int mb_h = (self.frame.ptr.height + 15) // 16 - cdef int mb_w = (self.frame.ptr.width + 15) // 16 - cdef int nb_mb = mb_h * mb_w - cdef int block_idx - cdef int y - cdef int x - cdef VideoBlockParams block + mb_h: cython.int = (self.frame.ptr.height + 15) // 16 + mb_w: cython.int = (self.frame.ptr.width + 15) // 16 + nb_mb: cython.int = mb_h * mb_w + block_idx, x, y = cython.declare(cython.int) + block: VideoBlockParams - # Validate number of blocks if self.nb_blocks != nb_mb: - raise RuntimeError("Expected frame size to match number of blocks in side data") - - # Validate type - cdef lib.AVVideoEncParamsType type = ( self.ptr.data).type - if type != lib.AVVideoEncParamsType.AV_VIDEO_ENC_PARAMS_MPEG2 and type != lib.AVVideoEncParamsType.AV_VIDEO_ENC_PARAMS_H264: + raise RuntimeError( + "Expected frame size to match number of blocks in side data" + ) + + type: lib.AVVideoEncParamsType = cython.cast( + cython.pointer[lib.AVVideoEncParams], self.ptr.data + ).type + if ( + type != lib.AVVideoEncParamsType.AV_VIDEO_ENC_PARAMS_MPEG2 + and type != lib.AVVideoEncParamsType.AV_VIDEO_ENC_PARAMS_H264 + ): raise ValueError("Expected MPEG2 or H264") # Create a 2-D map with the number of macroblocks - cdef int32_t[:, ::1] map = np.empty((mb_h, mb_w), dtype=np.int32) + map = np.empty((mb_h, mb_w), dtype=np.int32) # Fill map with quantization parameter per macroblock for block_idx in range(nb_mb): @@ -112,14 +126,19 @@ def qp_map(self): x = block.src_x // 16 map[y, x] = self.qp + block.delta_qp - return np.asarray(map) + return map -cdef class VideoBlockParams: - def __init__(self, VideoEncParams video_enc_params, int idx) -> None: - cdef uint8_t* base = video_enc_params.ptr.data - cdef Py_ssize_t offset = video_enc_params.blocks_offset + idx * video_enc_params.block_size - self.ptr = (base + offset) +@cython.cclass +class VideoBlockParams: + def __init__(self, video_enc_params: VideoEncParams, idx: cython.int) -> None: + base: cython.pointer[uint8_t] = cython.cast( + cython.pointer[uint8_t], video_enc_params.ptr.data + ) + offset: cython.Py_ssize_t = ( + video_enc_params.blocks_offset + idx * video_enc_params.block_size + ) + self.ptr = cython.cast(cython.pointer[lib.AVVideoBlockParams], base + offset) def __repr__(self): return f"" From e6bfe92755c7e7c1bbf8d1774e1be3b1358fb9f9 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 3 Feb 2026 20:37:02 -0500 Subject: [PATCH 675/846] Remove unused c defs --- av/logging.pxd | 4 +++ av/logging.py | 6 ++-- include/libavcodec/bsf.pxd | 28 +++------------ include/libavutil/avutil.pxd | 54 ++--------------------------- include/libavutil/dict.pxd | 30 +++------------- include/libavutil/error.pxd | 13 ------- include/libavutil/hwcontext.pxd | 1 - include/libavutil/motion_vector.pxd | 1 - include/libavutil/samplefmt.pxd | 15 +++----- 9 files changed, 22 insertions(+), 130 deletions(-) diff --git a/av/logging.pxd b/av/logging.pxd index a886a0f20..8bd9fb730 100644 --- a/av/logging.pxd +++ b/av/logging.pxd @@ -1,2 +1,6 @@ +cdef extern from "Python.h" nogil: + void PyErr_PrintEx(int set_sys_last_vars) + int Py_IsInitialized() + void PyErr_Display(object, object, object) cpdef get_last_error() diff --git a/av/logging.py b/av/logging.py index ade74c954..a937b26a2 100644 --- a/av/logging.py +++ b/av/logging.py @@ -328,7 +328,7 @@ def log_callback( format: cython.p_const_char, args: lib.va_list, ) -> cython.void: - inited: cython.bint = lib.Py_IsInitialized() + inited: cython.bint = Py_IsInitialized() if not inited: return @@ -361,9 +361,9 @@ def log_callback( level, message, ) - # For some reason lib.PyErr_PrintEx(0) won't work. + # For some reason PyErr_PrintEx(0) won't work. exc, type_, tb = sys.exc_info() - lib.PyErr_Display(exc, type_, tb) + PyErr_Display(exc, type_, tb) @cython.cfunc diff --git a/include/libavcodec/bsf.pxd b/include/libavcodec/bsf.pxd index 4a558b478..b4c41f7f4 100644 --- a/include/libavcodec/bsf.pxd +++ b/include/libavcodec/bsf.pxd @@ -1,6 +1,4 @@ - cdef extern from "libavcodec/bsf.h" nogil: - cdef struct AVBitStreamFilter: const char *name AVCodecID *codec_ids @@ -13,28 +11,10 @@ cdef extern from "libavcodec/bsf.h" nogil: const AVCodecParameters *par_in const AVCodecParameters *par_out - cdef const AVBitStreamFilter* av_bsf_get_by_name(const char *name) - - cdef int av_bsf_list_parse_str( - const char *str, - AVBSFContext **bsf - ) - + cdef int av_bsf_list_parse_str(const char *str, AVBSFContext **bsf) cdef int av_bsf_init(AVBSFContext *ctx) cdef void av_bsf_free(AVBSFContext **ctx) - cdef AVBitStreamFilter* av_bsf_iterate(void **opaque) - - cdef int av_bsf_send_packet( - AVBSFContext *ctx, - AVPacket *pkt - ) - - cdef int av_bsf_receive_packet( - AVBSFContext *ctx, - AVPacket *pkt - ) - - cdef void av_bsf_flush( - AVBSFContext *ctx - ) + cdef int av_bsf_send_packet(AVBSFContext *ctx, AVPacket *pkt) + cdef int av_bsf_receive_packet(AVBSFContext *ctx, AVPacket *pkt) + cdef void av_bsf_flush(AVBSFContext *ctx) diff --git a/include/libavutil/avutil.pxd b/include/libavutil/avutil.pxd index 790469ff7..23b4fa6fc 100644 --- a/include/libavutil/avutil.pxd +++ b/include/libavutil/avutil.pxd @@ -1,9 +1,6 @@ from libc.stdint cimport int64_t, uint8_t, uint64_t, int32_t -cdef extern from "libavutil/mathematics.h" nogil: - pass - cdef extern from "libavutil/display.h" nogil: cdef double av_display_rotation_get(const int32_t matrix[9]) @@ -12,7 +9,7 @@ cdef extern from "libavutil/rational.h" nogil: cdef extern from "libavutil/avutil.h" nogil: cdef const char* av_version_info() - cdef int avutil_version() + cdef int avutil_version() cdef char* avutil_configuration() cdef char* avutil_license() @@ -34,15 +31,6 @@ cdef extern from "libavutil/avutil.h" nogil: PIX_FMT_RGB24 PIX_FMT_RGBA - cdef enum AVRounding: - AV_ROUND_ZERO - AV_ROUND_INF - AV_ROUND_DOWN - AV_ROUND_UP - AV_ROUND_NEAR_INF - # This is nice, but only in FFMpeg: - # AV_ROUND_PASS_MINMAX - cdef enum AVColorSpace: AVCOL_SPC_RGB AVCOL_SPC_BT709 @@ -83,37 +71,14 @@ cdef extern from "libavutil/avutil.h" nogil: AVCOL_PRI_NB cdef enum AVColorTransferCharacteristic: - AVCOL_TRC_RESERVED0 - AVCOL_TRC_BT709 - AVCOL_TRC_UNSPECIFIED - AVCOL_TRC_RESERVED - AVCOL_TRC_GAMMA22 - AVCOL_TRC_GAMMA28 - AVCOL_TRC_SMPTE170M - AVCOL_TRC_SMPTE240M - AVCOL_TRC_LINEAR - AVCOL_TRC_LOG - AVCOL_TRC_LOG_SQRT - AVCOL_TRC_IEC61966_2_4 - AVCOL_TRC_BT1361_ECG - AVCOL_TRC_IEC61966_2_1 - AVCOL_TRC_BT2020_10 - AVCOL_TRC_BT2020_12 - AVCOL_TRC_SMPTE2084 - AVCOL_TRC_SMPTEST2084 - AVCOL_TRC_SMPTE428 - AVCOL_TRC_SMPTEST428_1 - AVCOL_TRC_ARIB_STD_B67 - AVCOL_TRC_NB + pass cdef void* av_malloc(size_t size) cdef void* av_mallocz(size_t size) cdef void* av_realloc(void *ptr, size_t size) cdef void av_free(void *ptr) cdef void av_freep(void *ptr) - cdef int av_get_bytes_per_sample(AVSampleFormat sample_fmt) - cdef int av_samples_get_buffer_size( int *linesize, int nb_channels, @@ -124,7 +89,6 @@ cdef extern from "libavutil/avutil.h" nogil: ctypedef struct AVRational: int num int den - cdef AVRational AV_TIME_BASE_Q cdef int64_t av_rescale_q(int64_t a, AVRational bq, AVRational cq) cdef int64_t av_rescale(int64_t a, int64_t b, int64_t c) cdef const char* av_get_media_type_string(AVMediaType media_type) @@ -142,13 +106,9 @@ cdef extern from "libavutil/pixdesc.h" nogil: AV_PIX_FMT_FLAG_BE AV_PIX_FMT_FLAG_PAL AV_PIX_FMT_FLAG_BITSTREAM - AV_PIX_FMT_FLAG_HWACCEL AV_PIX_FMT_FLAG_PLANAR AV_PIX_FMT_FLAG_RGB - AV_PIX_FMT_FLAG_PSEUDOPAL - AV_PIX_FMT_FLAG_ALPHA AV_PIX_FMT_FLAG_BAYER - AV_PIX_FMT_FLAG_FLOAT # See: http://ffmpeg.org/doxygen/trunk/structAVPixFmtDescriptor.html cdef struct AVPixFmtDescriptor: @@ -161,10 +121,8 @@ cdef extern from "libavutil/pixdesc.h" nogil: cdef AVPixFmtDescriptor* av_pix_fmt_desc_get(AVPixelFormat pix_fmt) cdef AVPixFmtDescriptor* av_pix_fmt_desc_next(AVPixFmtDescriptor *prev) - cdef char * av_get_pix_fmt_name(AVPixelFormat pix_fmt) cdef AVPixelFormat av_get_pix_fmt(char* name) - int av_get_bits_per_pixel(AVPixFmtDescriptor *pixdesc) int av_get_padded_bits_per_pixel(AVPixFmtDescriptor *pixdesc) @@ -186,14 +144,6 @@ cdef extern from "stdarg.h" nogil: ctypedef struct va_list: pass - -cdef extern from "Python.h" nogil: - cdef int Py_AddPendingCall(void *, void *) - void PyErr_PrintEx(int set_sys_last_vars) - int Py_IsInitialized() - void PyErr_Display(object, object, object) - - cdef extern from "libavutil/opt.h" nogil: cdef enum AVOptionType: AV_OPT_TYPE_FLAGS diff --git a/include/libavutil/dict.pxd b/include/libavutil/dict.pxd index c2b141101..19b0d9086 100644 --- a/include/libavutil/dict.pxd +++ b/include/libavutil/dict.pxd @@ -1,38 +1,18 @@ cdef extern from "libavutil/dict.h" nogil: - # See: http://ffmpeg.org/doxygen/trunk/structAVDictionary.html ctypedef struct AVDictionary: pass - - cdef void av_dict_free(AVDictionary **) - # See: http://ffmpeg.org/doxygen/trunk/structAVDictionaryEntry.html ctypedef struct AVDictionaryEntry: char *key char *value - cdef int AV_DICT_IGNORE_SUFFIX - + cdef void av_dict_free(AVDictionary **) cdef AVDictionaryEntry* av_dict_get( - AVDictionary *dict, - char *key, - AVDictionaryEntry *prev, - int flags, + AVDictionary *dict, char *key, AVDictionaryEntry *prev, int flags ) - cdef int av_dict_set( - AVDictionary **pm, - const char *key, - const char *value, - int flags - ) - - cdef int av_dict_count( - AVDictionary *m - ) - - cdef int av_dict_copy( - AVDictionary **dst, - AVDictionary *src, - int flags + AVDictionary **pm, const char *key, const char *value, int flags ) + cdef int av_dict_count(AVDictionary *m) + cdef int av_dict_copy(AVDictionary **dst, AVDictionary *src, int flags) diff --git a/include/libavutil/error.pxd b/include/libavutil/error.pxd index 2122772e3..17aad3fd9 100644 --- a/include/libavutil/error.pxd +++ b/include/libavutil/error.pxd @@ -1,9 +1,4 @@ cdef extern from "libavutil/error.h" nogil: - - # Not actually from here, but whatever. - cdef int ENOMEM - cdef int EAGAIN - cdef int AVERROR_BSF_NOT_FOUND cdef int AVERROR_BUG cdef int AVERROR_BUFFER_TOO_SMALL @@ -23,21 +18,13 @@ cdef extern from "libavutil/error.h" nogil: cdef int AVERROR_EXPERIMENTAL cdef int AVERROR_INPUT_CHANGED cdef int AVERROR_OUTPUT_CHANGED - cdef int AVERROR_HTTP_BAD_REQUEST cdef int AVERROR_HTTP_UNAUTHORIZED cdef int AVERROR_HTTP_FORBIDDEN cdef int AVERROR_HTTP_NOT_FOUND cdef int AVERROR_HTTP_OTHER_4XX cdef int AVERROR_HTTP_SERVER_ERROR - - cdef int AVERROR_NOMEM "AVERROR(ENOMEM)" - - # cdef int FFERRTAG(int, int, int, int) - cdef int AVERROR(int error) - cdef int AV_ERROR_MAX_STRING_SIZE - cdef int av_strerror(int errno, char *output, size_t output_size) cdef char* av_err2str(int errnum) diff --git a/include/libavutil/hwcontext.pxd b/include/libavutil/hwcontext.pxd index beda15a2c..dc6402a6d 100644 --- a/include/libavutil/hwcontext.pxd +++ b/include/libavutil/hwcontext.pxd @@ -1,5 +1,4 @@ cdef extern from "libavutil/hwcontext.h" nogil: - enum AVHWDeviceType: AV_HWDEVICE_TYPE_NONE AV_HWDEVICE_TYPE_VDPAU diff --git a/include/libavutil/motion_vector.pxd b/include/libavutil/motion_vector.pxd index 457d7149e..4938a5dff 100644 --- a/include/libavutil/motion_vector.pxd +++ b/include/libavutil/motion_vector.pxd @@ -2,7 +2,6 @@ from libc.stdint cimport int16_t, int32_t, uint8_t, uint16_t, uint64_t cdef extern from "libavutil/motion_vector.h" nogil: - cdef struct AVMotionVector: int32_t source uint8_t w diff --git a/include/libavutil/samplefmt.pxd b/include/libavutil/samplefmt.pxd index c70272936..5ee25dafd 100644 --- a/include/libavutil/samplefmt.pxd +++ b/include/libavutil/samplefmt.pxd @@ -1,5 +1,4 @@ cdef extern from "libavutil/samplefmt.h" nogil: - cdef enum AVSampleFormat: AV_SAMPLE_FMT_NONE AV_SAMPLE_FMT_U8 @@ -12,20 +11,14 @@ cdef extern from "libavutil/samplefmt.h" nogil: AV_SAMPLE_FMT_S32P AV_SAMPLE_FMT_FLTP AV_SAMPLE_FMT_DBLP - AV_SAMPLE_FMT_NB # Number. + AV_SAMPLE_FMT_NB - # Find by name. cdef AVSampleFormat av_get_sample_fmt(char* name) - - # Inspection. - cdef char * av_get_sample_fmt_name(AVSampleFormat sample_fmt) - cdef int av_get_bytes_per_sample(AVSampleFormat sample_fmt) - cdef int av_sample_fmt_is_planar(AVSampleFormat sample_fmt) - - # Alternative forms. + cdef char *av_get_sample_fmt_name(AVSampleFormat sample_fmt) + cdef int av_get_bytes_per_sample(AVSampleFormat sample_fmt) + cdef int av_sample_fmt_is_planar(AVSampleFormat sample_fmt) cdef AVSampleFormat av_get_packed_sample_fmt(AVSampleFormat sample_fmt) cdef AVSampleFormat av_get_planar_sample_fmt(AVSampleFormat sample_fmt) - cdef int av_samples_get_buffer_size( int *linesize, int nb_channels, From 0a42b4faec47d4640ea12c3a6eb4511683dfa9ab Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 3 Feb 2026 21:42:05 -0500 Subject: [PATCH 676/846] Combine all libavutil files into one --- include/libav.pxd | 9 - include/libavcodec/avcodec.pxd | 124 ++-------- include/libavfilter/avfilter.pxd | 3 +- include/libavfilter/buffersrc.pxd | 6 +- include/libavformat/avformat.pxd | 146 ++---------- include/libavutil/avutil.pxd | 318 ++++++++++++++++++------- include/libavutil/buffer.pxd | 18 -- include/libavutil/dict.pxd | 18 -- include/libavutil/error.pxd | 30 --- include/libavutil/frame.pxd | 7 - include/libavutil/hwcontext.pxd | 23 -- include/libavutil/motion_vector.pxd | 16 -- include/libavutil/samplefmt.pxd | 28 --- include/libavutil/video_enc_params.pxd | 25 -- include/libswresample/swresample.pxd | 2 +- 15 files changed, 275 insertions(+), 498 deletions(-) delete mode 100644 include/libavutil/buffer.pxd delete mode 100644 include/libavutil/dict.pxd delete mode 100644 include/libavutil/error.pxd delete mode 100644 include/libavutil/frame.pxd delete mode 100644 include/libavutil/hwcontext.pxd delete mode 100644 include/libavutil/motion_vector.pxd delete mode 100644 include/libavutil/samplefmt.pxd delete mode 100644 include/libavutil/video_enc_params.pxd diff --git a/include/libav.pxd b/include/libav.pxd index 15e086f93..a66626c3d 100644 --- a/include/libav.pxd +++ b/include/libav.pxd @@ -1,13 +1,4 @@ include "libavutil/avutil.pxd" -include "libavutil/buffer.pxd" -include "libavutil/dict.pxd" -include "libavutil/error.pxd" -include "libavutil/frame.pxd" -include "libavutil/hwcontext.pxd" -include "libavutil/samplefmt.pxd" -include "libavutil/motion_vector.pxd" -include "libavutil/video_enc_params.pxd" - include "libavcodec/avcodec.pxd" include "libavcodec/bsf.pxd" include "libavcodec/hwaccel.pxd" diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index d74906b98..b75769945 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -1,31 +1,25 @@ from libc.stdint cimport int64_t, uint16_t, uint32_t, uint8_t cdef extern from "libavcodec/packet.h" nogil: - const AVPacketSideData *av_packet_side_data_get(const AVPacketSideData *sd, - int nb_sd, - AVPacketSideDataType type) - - uint8_t* av_packet_get_side_data(const AVPacket *pkt, AVPacketSideDataType type, - size_t *size) - - int av_packet_add_side_data(AVPacket *pkt, AVPacketSideDataType type, - uint8_t *data, size_t size) - + const AVPacketSideData *av_packet_side_data_get( + const AVPacketSideData *sd, int nb_sd, AVPacketSideDataType type + ) + uint8_t* av_packet_get_side_data( + const AVPacket *pkt, AVPacketSideDataType type, size_t *size + ) + int av_packet_add_side_data( + AVPacket *pkt, AVPacketSideDataType type, uint8_t *data, size_t size + ) const char *av_packet_side_data_name(AVPacketSideDataType type) cdef extern from "libavutil/channel_layout.h": ctypedef enum AVChannelOrder: - AV_CHANNEL_ORDER_UNSPEC - AV_CHANNEL_ORDER_NATIVE - AV_CHANNEL_ORDER_CUSTOM - AV_CHANNEL_ORDER_AMBISONIC - + pass ctypedef enum AVChannel: AV_CHAN_NONE = -1 AV_CHAN_FRONT_LEFT AV_CHAN_FRONT_RIGHT AV_CHAN_FRONT_CENTER - ctypedef struct AVChannelLayout: AVChannelOrder order int nb_channels @@ -36,10 +30,7 @@ cdef extern from "libavutil/channel_layout.h": void *opaque int av_channel_layout_default(AVChannelLayout *ch_layout, int nb_channels) - int av_channel_layout_from_mask(AVChannelLayout *channel_layout, uint64_t mask) int av_channel_layout_from_string(AVChannelLayout *channel_layout, const char *str) - void av_channel_layout_uninit(AVChannelLayout *channel_layout) - int av_channel_layout_copy(AVChannelLayout *dst, const AVChannelLayout *src) int av_channel_layout_describe(const AVChannelLayout *channel_layout, char *buf, size_t buf_size) int av_channel_name(char *buf, size_t buf_size, AVChannel channel_id) int av_channel_description(char *buf, size_t buf_size, AVChannel channel_id) @@ -48,8 +39,7 @@ cdef extern from "libavutil/channel_layout.h": cdef extern from "libavcodec/avcodec.h" nogil: cdef set pyav_get_available_codecs() - - cdef int avcodec_version() + cdef int avcodec_version() cdef char* avcodec_configuration() cdef char* avcodec_license() @@ -293,72 +283,24 @@ cdef extern from "libavcodec/avcodec.h" nogil: int subtitle_header_size uint8_t *subtitle_header - cdef AVCodecContext* avcodec_alloc_context3(AVCodec *codec) cdef void avcodec_free_context(AVCodecContext **ctx) - cdef AVClass* avcodec_get_class() - cdef AVCodec* avcodec_find_decoder(AVCodecID id) cdef AVCodec* avcodec_find_encoder(AVCodecID id) - cdef AVCodec* avcodec_find_decoder_by_name(char *name) cdef AVCodec* avcodec_find_encoder_by_name(char *name) - cdef const AVCodec* av_codec_iterate(void **opaque) - - cdef AVCodecDescriptor* avcodec_descriptor_get (AVCodecID id) - cdef AVCodecDescriptor* avcodec_descriptor_get_by_name (char *name) - + cdef AVCodecDescriptor* avcodec_descriptor_get(AVCodecID id) + cdef AVCodecDescriptor* avcodec_descriptor_get_by_name(char *name) cdef char* avcodec_get_name(AVCodecID id) - - cdef int avcodec_open2( - AVCodecContext *ctx, - AVCodec *codec, - AVDictionary **options, - ) - - cdef int AV_NUM_DATA_POINTERS - + cdef int avcodec_open2(AVCodecContext *ctx, AVCodec *codec, AVDictionary **options) cdef enum AVPacketSideDataType: - AV_PKT_DATA_PALETTE - AV_PKT_DATA_NEW_EXTRADATA - AV_PKT_DATA_PARAM_CHANGE - AV_PKT_DATA_H263_MB_INFO - AV_PKT_DATA_REPLAYGAIN - AV_PKT_DATA_DISPLAYMATRIX - AV_PKT_DATA_STEREO3D - AV_PKT_DATA_AUDIO_SERVICE_TYPE - AV_PKT_DATA_QUALITY_STATS - AV_PKT_DATA_FALLBACK_TRACK - AV_PKT_DATA_CPB_PROPERTIES - AV_PKT_DATA_SKIP_SAMPLES - AV_PKT_DATA_JP_DUALMONO - AV_PKT_DATA_STRINGS_METADATA - AV_PKT_DATA_SUBTITLE_POSITION - AV_PKT_DATA_MATROSKA_BLOCKADDITIONAL - AV_PKT_DATA_WEBVTT_IDENTIFIER - AV_PKT_DATA_WEBVTT_SETTINGS - AV_PKT_DATA_METADATA_UPDATE - AV_PKT_DATA_MPEGTS_STREAM_ID - AV_PKT_DATA_MASTERING_DISPLAY_METADATA - AV_PKT_DATA_SPHERICAL - AV_PKT_DATA_CONTENT_LIGHT_LEVEL - AV_PKT_DATA_A53_CC - AV_PKT_DATA_ENCRYPTION_INIT_INFO - AV_PKT_DATA_ENCRYPTION_INFO - AV_PKT_DATA_AFD - AV_PKT_DATA_PRFT - AV_PKT_DATA_ICC_PROFILE - AV_PKT_DATA_DOVI_CONF - AV_PKT_DATA_S12M_TIMECODE - AV_PKT_DATA_DYNAMIC_HDR10_PLUS - AV_PKT_DATA_NB - + pass cdef struct AVPacketSideData: - uint8_t *data; - size_t size; - AVPacketSideDataType type; + uint8_t *data + size_t size + AVPacketSideDataType type cdef enum AVFrameSideDataType: AV_FRAME_DATA_PANSCAN @@ -427,7 +369,7 @@ cdef extern from "libavcodec/avcodec.h" nogil: int64_t duration cdef struct AVPacket: - void *buf # ptr[AVBufferRef] + void *buf int64_t pts int64_t dts uint8_t *data @@ -450,7 +392,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: int buf_size, int align ) - cdef AVPacket* av_packet_alloc() cdef void av_packet_free(AVPacket **) cdef int av_new_packet(AVPacket*, int) @@ -485,30 +426,18 @@ cdef extern from "libavcodec/avcodec.h" nogil: int64_t pts cdef int avcodec_decode_subtitle2( - AVCodecContext *ctx, - AVSubtitle *sub, - int *done, - AVPacket *pkt, + AVCodecContext *ctx, AVSubtitle *sub, int *done, AVPacket *pkt, ) - cdef int avcodec_encode_subtitle( - AVCodecContext *avctx, - uint8_t *buf, - int buf_size, - AVSubtitle *sub + AVCodecContext *avctx, uint8_t *buf, int buf_size, AVSubtitle *sub ) - cdef void avsubtitle_free(AVSubtitle*) cdef void avcodec_flush_buffers(AVCodecContext *ctx) - - # === New-style Transcoding cdef int avcodec_send_packet(AVCodecContext *avctx, AVPacket *packet) cdef int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame) cdef int avcodec_send_frame(AVCodecContext *avctx, AVFrame *frame) cdef int avcodec_receive_packet(AVCodecContext *avctx, AVPacket *avpkt) - # === Parsers - cdef struct AVCodecParser: int codec_ids[5] @@ -529,20 +458,15 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef struct AVCodecParameters: AVMediaType codec_type AVCodecID codec_id - AVPacketSideData *coded_side_data - int nb_coded_side_data uint8_t *extradata int extradata_size cdef int avcodec_parameters_copy( - AVCodecParameters *dst, - const AVCodecParameters *src + AVCodecParameters *dst, const AVCodecParameters *src ) cdef int avcodec_parameters_from_context( - AVCodecParameters *par, - const AVCodecContext *codec, + AVCodecParameters *par, const AVCodecContext *codec ) cdef int avcodec_parameters_to_context( - AVCodecContext *codec, - const AVCodecParameters *par + AVCodecContext *codec, const AVCodecParameters *par ) diff --git a/include/libavfilter/avfilter.pxd b/include/libavfilter/avfilter.pxd index 8d4c05f6f..db67507b4 100644 --- a/include/libavfilter/avfilter.pxd +++ b/include/libavfilter/avfilter.pxd @@ -1,10 +1,9 @@ cdef extern from "libavfilter/avfilter.h" nogil: - cdef int avfilter_version() + cdef int avfilter_version() cdef char* avfilter_configuration() cdef char* avfilter_license() cdef struct AVFilterPad: - # This struct is opaque. pass const char* avfilter_pad_get_name(const AVFilterPad *pads, int index) diff --git a/include/libavfilter/buffersrc.pxd b/include/libavfilter/buffersrc.pxd index db057662a..1e0f70771 100644 --- a/include/libavfilter/buffersrc.pxd +++ b/include/libavfilter/buffersrc.pxd @@ -1,6 +1,2 @@ cdef extern from "libavfilter/buffersrc.h" nogil: - - int av_buffersrc_write_frame( - AVFilterContext *ctx, - const AVFrame *frame - ) + int av_buffersrc_write_frame(AVFilterContext *ctx, const AVFrame *frame) diff --git a/include/libavformat/avformat.pxd b/include/libavformat/avformat.pxd index 1fda271f8..89e63af2f 100644 --- a/include/libavformat/avformat.pxd +++ b/include/libavformat/avformat.pxd @@ -2,19 +2,15 @@ from libc.stdint cimport int64_t, uint64_t cdef extern from "libavformat/avformat.h" nogil: - - cdef int avformat_version() + cdef int avformat_version() cdef char* avformat_configuration() cdef char* avformat_license() - cdef int64_t INT64_MIN - cdef int AV_TIME_BASE cdef int AVSEEK_FLAG_BACKWARD cdef int AVSEEK_FLAG_BYTE cdef int AVSEEK_FLAG_ANY cdef int AVSEEK_FLAG_FRAME - cdef int AVIO_FLAG_WRITE cdef enum AVMediaType: @@ -24,16 +20,13 @@ cdef extern from "libavformat/avformat.h" nogil: AVMEDIA_TYPE_DATA AVMEDIA_TYPE_SUBTITLE AVMEDIA_TYPE_ATTACHMENT - AVMEDIA_TYPE_NB cdef struct AVStream: int index int id int disposition - AVCodecParameters *codecpar AVRational time_base - int64_t start_time int64_t duration int64_t nb_frames @@ -50,7 +43,6 @@ cdef extern from "libavformat/avformat.h" nogil: AVRational time_base AVDictionary *metadata - # http://ffmpeg.org/doxygen/trunk/structAVIOContext.html cdef struct AVIOContext: unsigned char* buffer @@ -66,12 +58,7 @@ cdef extern from "libavformat/avformat.h" nogil: int (*callback)(void*) void *opaque - cdef int AVIO_FLAG_DIRECT cdef int AVIO_SEEKABLE_NORMAL - - cdef int SEEK_SET - cdef int SEEK_CUR - cdef int SEEK_END cdef int AVSEEK_SIZE cdef AVIOContext* avio_alloc_context( @@ -90,7 +77,6 @@ cdef extern from "libavformat/avformat.h" nogil: const char *long_name const char *extensions int flags - # const AVCodecTag* const *codec_tag const AVClass *priv_class # http://ffmpeg.org/doxygen/trunk/structAVOutputFormat.html @@ -102,7 +88,6 @@ cdef extern from "libavformat/avformat.h" nogil: AVCodecID audio_codec AVCodecID subtitle_codec int flags - # const AVCodecTag* const *codec_tag const AVClass *priv_class int avformat_query_codec(const AVOutputFormat *oformat, AVCodecID codec_id, int std_compliance) @@ -156,33 +141,22 @@ cdef extern from "libavformat/avformat.h" nogil: # http://ffmpeg.org/doxygen/trunk/structAVFormatContext.html cdef struct AVFormatContext: - - # Streams. unsigned int nb_streams AVStream **streams - unsigned int nb_chapters AVChapter **chapters - AVInputFormat *iformat AVOutputFormat *oformat - AVIOContext *pb AVIOInterruptCB interrupt_callback - AVDictionary *metadata - char filename int64_t start_time int64_t duration int bit_rate - int flags - int64_t max_analyze_duration - AVCodecID audio_codec_id void *opaque - int (*io_open)( AVFormatContext *s, AVIOContext **pb, @@ -190,120 +164,47 @@ cdef extern from "libavformat/avformat.h" nogil: int flags, AVDictionary **options ) - int (*io_close2)( - AVFormatContext *s, - AVIOContext *pb - ) + int (*io_close2)(AVFormatContext *s, AVIOContext *pb) cdef AVFormatContext* avformat_alloc_context() - - # .. c:function:: avformat_open_input(...) - # - # Options are passed via :func:`av.open`. - # - # .. seealso:: FFmpeg's docs: :ffmpeg:`avformat_open_input` - # cdef int avformat_open_input( - AVFormatContext **ctx, # NULL will allocate for you. + AVFormatContext **ctx, char *filename, - AVInputFormat *format, # Can be NULL. - AVDictionary **options # Can be NULL. + AVInputFormat *format, + AVDictionary **options ) cdef int avformat_close_input(AVFormatContext **ctx) - - # .. c:function:: avformat_write_header(...) - # - # Options are passed via :func:`av.open`; called in - # :meth:`av.container.OutputContainer.start_encoding`. - # - # .. seealso:: FFmpeg's docs: :ffmpeg:`avformat_write_header` - # - cdef int avformat_write_header( - AVFormatContext *ctx, - AVDictionary **options # Can be NULL - ) - + cdef int avformat_write_header(AVFormatContext *ctx, AVDictionary **options) cdef int av_write_trailer(AVFormatContext *ctx) - - cdef int av_interleaved_write_frame( - AVFormatContext *ctx, - AVPacket *pkt - ) - - cdef int av_write_frame( - AVFormatContext *ctx, - AVPacket *pkt - ) - - cdef int avio_open( - AVIOContext **s, - char *url, - int flags - ) - - cdef int64_t avio_size( - AVIOContext *s - ) - + cdef int av_interleaved_write_frame(AVFormatContext *ctx, AVPacket *pkt) + cdef int av_write_frame(AVFormatContext *ctx, AVPacket *pkt) + cdef int avio_open(AVIOContext **s, char *url, int flags) + cdef int64_t avio_size(AVIOContext *s) cdef AVOutputFormat* av_guess_format( - char *short_name, - char *filename, - char *mime_type + char *short_name, char *filename, char *mime_type ) - cdef int avformat_query_codec( - AVOutputFormat *ofmt, - AVCodecID codec_id, - int std_compliance + AVOutputFormat *ofmt, AVCodecID codec_id, int std_compliance ) - cdef void avio_flush(AVIOContext *s) - cdef int avio_close(AVIOContext *s) - cdef int avio_closep(AVIOContext **s) - - cdef int avformat_find_stream_info( - AVFormatContext *ctx, - AVDictionary **options, # Can be NULL. - ) - - cdef AVStream* avformat_new_stream( - AVFormatContext *ctx, - AVCodec *c - ) - + cdef int avformat_find_stream_info(AVFormatContext *ctx, AVDictionary **options) + cdef AVStream* avformat_new_stream(AVFormatContext *ctx, AVCodec *c) cdef int avformat_alloc_output_context2( AVFormatContext **ctx, AVOutputFormat *oformat, char *format_name, char *filename ) - cdef int avformat_free_context(AVFormatContext *ctx) - cdef AVClass* avformat_get_class() - - cdef void av_dump_format( - AVFormatContext *ctx, - int index, - char *url, - int is_output, - ) - - cdef int av_read_frame( - AVFormatContext *ctx, - AVPacket *packet, - ) - + cdef void av_dump_format(AVFormatContext *ctx, int index, char *url, int is_output) + cdef int av_read_frame(AVFormatContext *ctx, AVPacket *packet) cdef int av_seek_frame( - AVFormatContext *ctx, - int stream_index, - int64_t timestamp, - int flags + AVFormatContext *ctx, int stream_index, int64_t timestamp, int flags ) - cdef int avformat_seek_file( AVFormatContext *ctx, int stream_index, @@ -312,24 +213,15 @@ cdef extern from "libavformat/avformat.h" nogil: int64_t max_ts, int flags ) - cdef AVRational av_guess_frame_rate( - AVFormatContext *ctx, - AVStream *stream, - AVFrame *frame + AVFormatContext *ctx, AVStream *stream, AVFrame *frame ) - cdef AVRational av_guess_sample_aspect_ratio( - AVFormatContext *ctx, - AVStream *stream, - AVFrame *frame + AVFormatContext *ctx, AVStream *stream, AVFrame *frame ) - cdef const AVInputFormat* av_demuxer_iterate(void **opaque) cdef const AVOutputFormat* av_muxer_iterate(void **opaque) - # custom - cdef set pyav_get_available_formats() cdef struct AVIndexEntry: diff --git a/include/libavutil/avutil.pxd b/include/libavutil/avutil.pxd index 23b4fa6fc..9d8486e1a 100644 --- a/include/libavutil/avutil.pxd +++ b/include/libavutil/avutil.pxd @@ -1,11 +1,16 @@ -from libc.stdint cimport int64_t, uint8_t, uint64_t, int32_t +from libc.stdint cimport int16_t, int32_t, int64_t, uint8_t, uint16_t, uint32_t, uint64_t -cdef extern from "libavutil/display.h" nogil: - cdef double av_display_rotation_get(const int32_t matrix[9]) - -cdef extern from "libavutil/rational.h" nogil: - cdef int av_reduce(int *dst_num, int *dst_den, int64_t num, int64_t den, int64_t max) +cdef extern from "libavutil/audio_fifo.h" nogil: + cdef struct AVAudioFifo: + pass + cdef void av_audio_fifo_free(AVAudioFifo *af) + cdef AVAudioFifo* av_audio_fifo_alloc( + AVSampleFormat sample_fmt, int channels, int nb_samples + ) + cdef int av_audio_fifo_write(AVAudioFifo *af, void **data, int nb_samples) + cdef int av_audio_fifo_read(AVAudioFifo *af, void **data, int nb_samples) + cdef int av_audio_fifo_size(AVAudioFifo *af) cdef extern from "libavutil/avutil.h" nogil: cdef const char* av_version_info() @@ -26,10 +31,6 @@ cdef extern from "libavutil/avutil.h" nogil: cdef enum AVPixelFormat: AV_PIX_FMT_NONE AV_PIX_FMT_YUV420P - AV_PIX_FMT_RGBA - AV_PIX_FMT_RGB24 - PIX_FMT_RGB24 - PIX_FMT_RGBA cdef enum AVColorSpace: AVCOL_SPC_RGB @@ -93,56 +94,156 @@ cdef extern from "libavutil/avutil.h" nogil: cdef int64_t av_rescale(int64_t a, int64_t b, int64_t c) cdef const char* av_get_media_type_string(AVMediaType media_type) -cdef extern from "libavutil/pixdesc.h" nogil: - # See: http://ffmpeg.org/doxygen/trunk/structAVComponentDescriptor.html - cdef struct AVComponentDescriptor: - unsigned int plane - unsigned int step - unsigned int offset - unsigned int shift - unsigned int depth +cdef extern from "libavutil/buffer.h" nogil: + AVBufferRef *av_buffer_create(uint8_t *data, size_t size, void (*free)(void *opaque, uint8_t *data), void *opaque, int flags) + AVBufferRef* av_buffer_ref(AVBufferRef *buf) + void av_buffer_unref(AVBufferRef **buf) + cdef struct AVBuffer: + uint8_t *data + int size + void (*free)(void *opaque, uint8_t *data) + void *opaque + int flags + cdef struct AVBufferRef: + AVBuffer *buffer + uint8_t *data + int size - cdef enum AVPixFmtFlags: - AV_PIX_FMT_FLAG_BE - AV_PIX_FMT_FLAG_PAL - AV_PIX_FMT_FLAG_BITSTREAM - AV_PIX_FMT_FLAG_PLANAR - AV_PIX_FMT_FLAG_RGB - AV_PIX_FMT_FLAG_BAYER +cdef extern from "libavutil/dict.h" nogil: + # See: http://ffmpeg.org/doxygen/trunk/structAVDictionary.html + ctypedef struct AVDictionary: + pass + # See: http://ffmpeg.org/doxygen/trunk/structAVDictionaryEntry.html + ctypedef struct AVDictionaryEntry: + char *key + char *value + cdef int AV_DICT_IGNORE_SUFFIX + cdef void av_dict_free(AVDictionary **) + cdef AVDictionaryEntry* av_dict_get( + AVDictionary *dict, char *key, AVDictionaryEntry *prev, int flags + ) + cdef int av_dict_set( + AVDictionary **pm, const char *key, const char *value, int flags + ) + cdef int av_dict_count(AVDictionary *m) + cdef int av_dict_copy(AVDictionary **dst, AVDictionary *src, int flags) - # See: http://ffmpeg.org/doxygen/trunk/structAVPixFmtDescriptor.html - cdef struct AVPixFmtDescriptor: - const char *name - uint8_t nb_components - uint8_t log2_chroma_w - uint8_t log2_chroma_h - uint8_t flags - AVComponentDescriptor comp[4] +cdef extern from "libavutil/display.h" nogil: + cdef double av_display_rotation_get(const int32_t matrix[9]) - cdef AVPixFmtDescriptor* av_pix_fmt_desc_get(AVPixelFormat pix_fmt) - cdef AVPixFmtDescriptor* av_pix_fmt_desc_next(AVPixFmtDescriptor *prev) - cdef char * av_get_pix_fmt_name(AVPixelFormat pix_fmt) - cdef AVPixelFormat av_get_pix_fmt(char* name) - int av_get_bits_per_pixel(AVPixFmtDescriptor *pixdesc) - int av_get_padded_bits_per_pixel(AVPixFmtDescriptor *pixdesc) +cdef extern from "libavutil/error.h" nogil: + cdef int AVERROR_BSF_NOT_FOUND + cdef int AVERROR_BUG + cdef int AVERROR_BUFFER_TOO_SMALL + cdef int AVERROR_DECODER_NOT_FOUND + cdef int AVERROR_DEMUXER_NOT_FOUND + cdef int AVERROR_ENCODER_NOT_FOUND + cdef int AVERROR_EOF + cdef int AVERROR_EXIT + cdef int AVERROR_EXTERNAL + cdef int AVERROR_FILTER_NOT_FOUND + cdef int AVERROR_INVALIDDATA + cdef int AVERROR_MUXER_NOT_FOUND + cdef int AVERROR_OPTION_NOT_FOUND + cdef int AVERROR_PATCHWELCOME + cdef int AVERROR_PROTOCOL_NOT_FOUND + cdef int AVERROR_UNKNOWN + cdef int AVERROR_EXPERIMENTAL + cdef int AVERROR_INPUT_CHANGED + cdef int AVERROR_OUTPUT_CHANGED + cdef int AVERROR_HTTP_BAD_REQUEST + cdef int AVERROR_HTTP_UNAUTHORIZED + cdef int AVERROR_HTTP_FORBIDDEN + cdef int AVERROR_HTTP_NOT_FOUND + cdef int AVERROR_HTTP_OTHER_4XX + cdef int AVERROR_HTTP_SERVER_ERROR + cdef int AV_ERROR_MAX_STRING_SIZE + cdef int av_strerror(int errno, char *output, size_t output_size) + cdef char* av_err2str(int errnum) +cdef extern from "libavutil/frame.h" nogil: + cdef AVFrame* av_frame_alloc() + cdef void av_frame_free(AVFrame**) + cdef void av_frame_unref(AVFrame *frame) + cdef int av_frame_make_writable(AVFrame *frame) + cdef int av_frame_copy_props(AVFrame *dst, const AVFrame *src) + cdef AVFrameSideData* av_frame_get_side_data(AVFrame *frame, AVFrameSideDataType type) -cdef extern from "libavutil/audio_fifo.h" nogil: - cdef struct AVAudioFifo: - pass +cdef extern from "libavutil/hwcontext.h" nogil: + enum AVHWDeviceType: + AV_HWDEVICE_TYPE_NONE + AV_HWDEVICE_TYPE_VDPAU + AV_HWDEVICE_TYPE_CUDA + AV_HWDEVICE_TYPE_VAAPI + AV_HWDEVICE_TYPE_DXVA2 + AV_HWDEVICE_TYPE_QSV + AV_HWDEVICE_TYPE_VIDEOTOOLBOX + AV_HWDEVICE_TYPE_D3D11VA + AV_HWDEVICE_TYPE_DRM + AV_HWDEVICE_TYPE_OPENCL + AV_HWDEVICE_TYPE_MEDIACODEC + AV_HWDEVICE_TYPE_VULKAN + AV_HWDEVICE_TYPE_D3D12VA - cdef void av_audio_fifo_free(AVAudioFifo *af) - cdef AVAudioFifo* av_audio_fifo_alloc( - AVSampleFormat sample_fmt, int channels, int nb_samples + cdef int av_hwdevice_ctx_create(AVBufferRef **device_ctx, AVHWDeviceType type, const char *device, AVDictionary *opts, int flags) + cdef AVHWDeviceType av_hwdevice_find_type_by_name(const char *name) + cdef const char *av_hwdevice_get_type_name(AVHWDeviceType type) + cdef AVHWDeviceType av_hwdevice_iterate_types(AVHWDeviceType prev) + cdef int av_hwframe_transfer_data(AVFrame *dst, const AVFrame *src, int flags) + +cdef extern from "libavutil/imgutils.h" nogil: + cdef int av_image_alloc( + uint8_t *pointers[4], + int linesizes[4], + int width, + int height, + AVPixelFormat pix_fmt, + int align + ) + cdef int av_image_fill_pointers( + uint8_t *pointers[4], + AVPixelFormat pix_fmt, + int height, + uint8_t *ptr, + const int linesizes[4] ) - cdef int av_audio_fifo_write(AVAudioFifo *af, void **data, int nb_samples) - cdef int av_audio_fifo_read(AVAudioFifo *af, void **data, int nb_samples) - cdef int av_audio_fifo_size(AVAudioFifo *af) +cdef extern from "libavutil/log.h" nogil: + cdef struct AVClass: + const char *class_name + const char *(*item_name)(void*) nogil + const AVOption *option -cdef extern from "stdarg.h" nogil: - ctypedef struct va_list: - pass + cdef enum: + AV_LOG_QUIET + AV_LOG_PANIC + AV_LOG_FATAL + AV_LOG_ERROR + AV_LOG_WARNING + AV_LOG_INFO + AV_LOG_VERBOSE + AV_LOG_DEBUG + AV_LOG_TRACE + + void av_log(void *ptr, int level, const char *fmt, ...) + ctypedef void(*av_log_callback)(void *, int, const char *, va_list) + void av_log_default_callback(void *, int, const char *, va_list) + void av_log_set_callback (av_log_callback callback) + void av_log_set_level(int level) + +cdef extern from "libavutil/motion_vector.h" nogil: + cdef struct AVMotionVector: + int32_t source + uint8_t w + uint8_t h + int16_t src_x + int16_t src_y + int16_t dst_x + int16_t dst_y + uint64_t flags + int32_t motion_x + int32_t motion_y + uint16_t motion_scale cdef extern from "libavutil/opt.h" nogil: cdef enum AVOptionType: @@ -187,53 +288,92 @@ cdef extern from "libavutil/opt.h" nogil: const char *help AVOptionType type int offset - AVOption_default_val default_val - double min double max int flags const char *unit +cdef extern from "libavutil/pixdesc.h" nogil: + # See: http://ffmpeg.org/doxygen/trunk/structAVComponentDescriptor.html + cdef struct AVComponentDescriptor: + unsigned int plane + unsigned int step + unsigned int offset + unsigned int shift + unsigned int depth -cdef extern from "libavutil/imgutils.h" nogil: - cdef int av_image_alloc( - uint8_t *pointers[4], - int linesizes[4], - int width, - int height, - AVPixelFormat pix_fmt, + cdef enum AVPixFmtFlags: + AV_PIX_FMT_FLAG_BE + AV_PIX_FMT_FLAG_PAL + AV_PIX_FMT_FLAG_BITSTREAM + AV_PIX_FMT_FLAG_PLANAR + AV_PIX_FMT_FLAG_RGB + AV_PIX_FMT_FLAG_BAYER + + # See: http://ffmpeg.org/doxygen/trunk/structAVPixFmtDescriptor.html + cdef struct AVPixFmtDescriptor: + const char *name + uint8_t nb_components + uint8_t log2_chroma_w + uint8_t log2_chroma_h + uint8_t flags + AVComponentDescriptor comp[4] + + cdef AVPixFmtDescriptor* av_pix_fmt_desc_get(AVPixelFormat pix_fmt) + cdef AVPixFmtDescriptor* av_pix_fmt_desc_next(AVPixFmtDescriptor *prev) + cdef char * av_get_pix_fmt_name(AVPixelFormat pix_fmt) + cdef AVPixelFormat av_get_pix_fmt(char* name) + int av_get_bits_per_pixel(AVPixFmtDescriptor *pixdesc) + int av_get_padded_bits_per_pixel(AVPixFmtDescriptor *pixdesc) + +cdef extern from "libavutil/rational.h" nogil: + cdef int av_reduce(int *dst_num, int *dst_den, int64_t num, int64_t den, int64_t max) + +cdef extern from "libavutil/samplefmt.h" nogil: + cdef enum AVSampleFormat: + AV_SAMPLE_FMT_U8 + AV_SAMPLE_FMT_S16 + AV_SAMPLE_FMT_S32 + AV_SAMPLE_FMT_FLT + AV_SAMPLE_FMT_DBL + + cdef AVSampleFormat av_get_sample_fmt(char* name) + cdef char *av_get_sample_fmt_name(AVSampleFormat sample_fmt) + cdef int av_get_bytes_per_sample(AVSampleFormat sample_fmt) + cdef int av_sample_fmt_is_planar(AVSampleFormat sample_fmt) + cdef AVSampleFormat av_get_packed_sample_fmt(AVSampleFormat sample_fmt) + cdef AVSampleFormat av_get_planar_sample_fmt(AVSampleFormat sample_fmt) + cdef int av_samples_get_buffer_size( + int *linesize, + int nb_channels, + int nb_samples, + AVSampleFormat sample_fmt, int align ) - cdef int av_image_fill_pointers( - uint8_t *pointers[4], - AVPixelFormat pix_fmt, - int height, - uint8_t *ptr, - const int linesizes[4] - ) -cdef extern from "libavutil/log.h" nogil: - cdef struct AVClass: - const char *class_name - const char *(*item_name)(void*) nogil - int parent_log_context_offset - const AVOption *option +cdef extern from "libavutil/video_enc_params.h" nogil: + cdef enum AVVideoEncParamsType: + AV_VIDEO_ENC_PARAMS_NONE + AV_VIDEO_ENC_PARAMS_VP9 + AV_VIDEO_ENC_PARAMS_H264 + AV_VIDEO_ENC_PARAMS_MPEG2 - cdef enum: - AV_LOG_QUIET - AV_LOG_PANIC - AV_LOG_FATAL - AV_LOG_ERROR - AV_LOG_WARNING - AV_LOG_INFO - AV_LOG_VERBOSE - AV_LOG_DEBUG - AV_LOG_TRACE - AV_LOG_MAX_OFFSET + cdef struct AVVideoEncParams: + uint32_t nb_blocks + size_t blocks_offset + size_t block_size + AVVideoEncParamsType type + int32_t qp + int32_t delta_qp[4][2] - void av_log(void *ptr, int level, const char *fmt, ...) - ctypedef void(*av_log_callback)(void *, int, const char *, va_list) - void av_log_default_callback(void *, int, const char *, va_list) - void av_log_set_callback (av_log_callback callback) - void av_log_set_level(int level) + cdef struct AVVideoBlockParams: + int32_t src_x + int32_t src_y + int32_t w + int32_t h + int32_t delta_qp + +cdef extern from "stdarg.h" nogil: + ctypedef struct va_list: + pass diff --git a/include/libavutil/buffer.pxd b/include/libavutil/buffer.pxd deleted file mode 100644 index d4ff4cd17..000000000 --- a/include/libavutil/buffer.pxd +++ /dev/null @@ -1,18 +0,0 @@ -from libc.stdint cimport intptr_t, uint8_t - -cdef extern from "libavutil/buffer.h" nogil: - AVBufferRef *av_buffer_create(uint8_t *data, size_t size, void (*free)(void *opaque, uint8_t *data), void *opaque, int flags) - AVBufferRef* av_buffer_ref(AVBufferRef *buf) - void av_buffer_unref(AVBufferRef **buf) - - cdef struct AVBuffer: - uint8_t *data - int size - intptr_t refcount - void (*free)(void *opaque, uint8_t *data) - void *opaque - int flags - cdef struct AVBufferRef: - AVBuffer *buffer - uint8_t *data - int size diff --git a/include/libavutil/dict.pxd b/include/libavutil/dict.pxd deleted file mode 100644 index 19b0d9086..000000000 --- a/include/libavutil/dict.pxd +++ /dev/null @@ -1,18 +0,0 @@ -cdef extern from "libavutil/dict.h" nogil: - # See: http://ffmpeg.org/doxygen/trunk/structAVDictionary.html - ctypedef struct AVDictionary: - pass - # See: http://ffmpeg.org/doxygen/trunk/structAVDictionaryEntry.html - ctypedef struct AVDictionaryEntry: - char *key - char *value - cdef int AV_DICT_IGNORE_SUFFIX - cdef void av_dict_free(AVDictionary **) - cdef AVDictionaryEntry* av_dict_get( - AVDictionary *dict, char *key, AVDictionaryEntry *prev, int flags - ) - cdef int av_dict_set( - AVDictionary **pm, const char *key, const char *value, int flags - ) - cdef int av_dict_count(AVDictionary *m) - cdef int av_dict_copy(AVDictionary **dst, AVDictionary *src, int flags) diff --git a/include/libavutil/error.pxd b/include/libavutil/error.pxd deleted file mode 100644 index 17aad3fd9..000000000 --- a/include/libavutil/error.pxd +++ /dev/null @@ -1,30 +0,0 @@ -cdef extern from "libavutil/error.h" nogil: - cdef int AVERROR_BSF_NOT_FOUND - cdef int AVERROR_BUG - cdef int AVERROR_BUFFER_TOO_SMALL - cdef int AVERROR_DECODER_NOT_FOUND - cdef int AVERROR_DEMUXER_NOT_FOUND - cdef int AVERROR_ENCODER_NOT_FOUND - cdef int AVERROR_EOF - cdef int AVERROR_EXIT - cdef int AVERROR_EXTERNAL - cdef int AVERROR_FILTER_NOT_FOUND - cdef int AVERROR_INVALIDDATA - cdef int AVERROR_MUXER_NOT_FOUND - cdef int AVERROR_OPTION_NOT_FOUND - cdef int AVERROR_PATCHWELCOME - cdef int AVERROR_PROTOCOL_NOT_FOUND - cdef int AVERROR_UNKNOWN - cdef int AVERROR_EXPERIMENTAL - cdef int AVERROR_INPUT_CHANGED - cdef int AVERROR_OUTPUT_CHANGED - cdef int AVERROR_HTTP_BAD_REQUEST - cdef int AVERROR_HTTP_UNAUTHORIZED - cdef int AVERROR_HTTP_FORBIDDEN - cdef int AVERROR_HTTP_NOT_FOUND - cdef int AVERROR_HTTP_OTHER_4XX - cdef int AVERROR_HTTP_SERVER_ERROR - cdef int AVERROR(int error) - cdef int AV_ERROR_MAX_STRING_SIZE - cdef int av_strerror(int errno, char *output, size_t output_size) - cdef char* av_err2str(int errnum) diff --git a/include/libavutil/frame.pxd b/include/libavutil/frame.pxd deleted file mode 100644 index c7f60e93c..000000000 --- a/include/libavutil/frame.pxd +++ /dev/null @@ -1,7 +0,0 @@ -cdef extern from "libavutil/frame.h" nogil: - cdef AVFrame* av_frame_alloc() - cdef void av_frame_free(AVFrame**) - cdef void av_frame_unref(AVFrame *frame) - cdef int av_frame_make_writable(AVFrame *frame) - cdef int av_frame_copy_props(AVFrame *dst, const AVFrame *src) - cdef AVFrameSideData* av_frame_get_side_data(AVFrame *frame, AVFrameSideDataType type) diff --git a/include/libavutil/hwcontext.pxd b/include/libavutil/hwcontext.pxd deleted file mode 100644 index dc6402a6d..000000000 --- a/include/libavutil/hwcontext.pxd +++ /dev/null @@ -1,23 +0,0 @@ -cdef extern from "libavutil/hwcontext.h" nogil: - enum AVHWDeviceType: - AV_HWDEVICE_TYPE_NONE - AV_HWDEVICE_TYPE_VDPAU - AV_HWDEVICE_TYPE_CUDA - AV_HWDEVICE_TYPE_VAAPI - AV_HWDEVICE_TYPE_DXVA2 - AV_HWDEVICE_TYPE_QSV - AV_HWDEVICE_TYPE_VIDEOTOOLBOX - AV_HWDEVICE_TYPE_D3D11VA - AV_HWDEVICE_TYPE_DRM - AV_HWDEVICE_TYPE_OPENCL - AV_HWDEVICE_TYPE_MEDIACODEC - AV_HWDEVICE_TYPE_VULKAN - AV_HWDEVICE_TYPE_D3D12VA - - cdef int av_hwdevice_ctx_create(AVBufferRef **device_ctx, AVHWDeviceType type, const char *device, AVDictionary *opts, int flags) - - cdef AVHWDeviceType av_hwdevice_find_type_by_name(const char *name) - cdef const char *av_hwdevice_get_type_name(AVHWDeviceType type) - cdef AVHWDeviceType av_hwdevice_iterate_types(AVHWDeviceType prev) - - cdef int av_hwframe_transfer_data(AVFrame *dst, const AVFrame *src, int flags) diff --git a/include/libavutil/motion_vector.pxd b/include/libavutil/motion_vector.pxd deleted file mode 100644 index 4938a5dff..000000000 --- a/include/libavutil/motion_vector.pxd +++ /dev/null @@ -1,16 +0,0 @@ -from libc.stdint cimport int16_t, int32_t, uint8_t, uint16_t, uint64_t - - -cdef extern from "libavutil/motion_vector.h" nogil: - cdef struct AVMotionVector: - int32_t source - uint8_t w - uint8_t h - int16_t src_x - int16_t src_y - int16_t dst_x - int16_t dst_y - uint64_t flags - int32_t motion_x - int32_t motion_y - uint16_t motion_scale diff --git a/include/libavutil/samplefmt.pxd b/include/libavutil/samplefmt.pxd deleted file mode 100644 index 5ee25dafd..000000000 --- a/include/libavutil/samplefmt.pxd +++ /dev/null @@ -1,28 +0,0 @@ -cdef extern from "libavutil/samplefmt.h" nogil: - cdef enum AVSampleFormat: - AV_SAMPLE_FMT_NONE - AV_SAMPLE_FMT_U8 - AV_SAMPLE_FMT_S16 - AV_SAMPLE_FMT_S32 - AV_SAMPLE_FMT_FLT - AV_SAMPLE_FMT_DBL - AV_SAMPLE_FMT_U8P - AV_SAMPLE_FMT_S16P - AV_SAMPLE_FMT_S32P - AV_SAMPLE_FMT_FLTP - AV_SAMPLE_FMT_DBLP - AV_SAMPLE_FMT_NB - - cdef AVSampleFormat av_get_sample_fmt(char* name) - cdef char *av_get_sample_fmt_name(AVSampleFormat sample_fmt) - cdef int av_get_bytes_per_sample(AVSampleFormat sample_fmt) - cdef int av_sample_fmt_is_planar(AVSampleFormat sample_fmt) - cdef AVSampleFormat av_get_packed_sample_fmt(AVSampleFormat sample_fmt) - cdef AVSampleFormat av_get_planar_sample_fmt(AVSampleFormat sample_fmt) - cdef int av_samples_get_buffer_size( - int *linesize, - int nb_channels, - int nb_samples, - AVSampleFormat sample_fmt, - int align - ) diff --git a/include/libavutil/video_enc_params.pxd b/include/libavutil/video_enc_params.pxd deleted file mode 100644 index aec452666..000000000 --- a/include/libavutil/video_enc_params.pxd +++ /dev/null @@ -1,25 +0,0 @@ -from libc.stdint cimport uint32_t, int32_t -from libc.stddef cimport size_t - - -cdef extern from "libavutil/video_enc_params.h" nogil: - cdef enum AVVideoEncParamsType: - AV_VIDEO_ENC_PARAMS_NONE - AV_VIDEO_ENC_PARAMS_VP9 - AV_VIDEO_ENC_PARAMS_H264 - AV_VIDEO_ENC_PARAMS_MPEG2 - - cdef struct AVVideoEncParams: - uint32_t nb_blocks - size_t blocks_offset - size_t block_size - AVVideoEncParamsType type - int32_t qp - int32_t delta_qp[4][2] - - cdef struct AVVideoBlockParams: - int32_t src_x - int32_t src_y - int32_t w - int32_t h - int32_t delta_qp \ No newline at end of file diff --git a/include/libswresample/swresample.pxd b/include/libswresample/swresample.pxd index 5bd8a8629..8e6f6649b 100644 --- a/include/libswresample/swresample.pxd +++ b/include/libswresample/swresample.pxd @@ -1,4 +1,4 @@ cdef extern from "libswresample/swresample.h" nogil: - cdef int swresample_version() + cdef int swresample_version() cdef char* swresample_configuration() cdef char* swresample_license() From a59df296cb4be24a0c8b0279b1cfdc4ade0d3a87 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 3 Feb 2026 12:27:31 -0500 Subject: [PATCH 677/846] Fix #2149 Set colorspace and color_range ... Set colorspace and color_range metadata on reformatted frames When VideoFrame.reformat() is called with explicit dst_colorspace or dst_color_range parameters, the output frame's metadata now correctly reflects these values. Previously, the swscale conversion was performed but the frame metadata was not updated. Changes: - Add SWS_CS_* to AVColorSpace mapping for correct metadata translation - Only set frame metadata when user explicitly specifies dst parameters - Preserve source frame metadata when dst params are not specified Co-Authored-By: Claude Opus 4.5 --- av/video/reformatter.pxd | 3 ++- av/video/reformatter.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/av/video/reformatter.pxd b/av/video/reformatter.pxd index 7682fab6d..d031f4dcf 100644 --- a/av/video/reformatter.pxd +++ b/av/video/reformatter.pxd @@ -10,4 +10,5 @@ cdef class VideoReformatter: cdef _reformat(self, VideoFrame frame, int width, int height, lib.AVPixelFormat format, int src_colorspace, int dst_colorspace, int interpolation, - int src_color_range, int dst_color_range) + int src_color_range, int dst_color_range, + bint set_dst_colorspace, bint set_dst_color_range) diff --git a/av/video/reformatter.py b/av/video/reformatter.py index e3e83d2d9..786543744 100644 --- a/av/video/reformatter.py +++ b/av/video/reformatter.py @@ -58,6 +58,21 @@ def _resolve_enum_value(value, enum_class, default): raise ValueError(f"Cannot convert {value} to {enum_class.__name__}") +# Mapping from SWS_CS_* (swscale colorspace) to AVColorSpace (frame metadata). +# Note: SWS_CS_ITU601, SWS_CS_ITU624, SWS_CS_SMPTE170M, and SWS_CS_DEFAULT all have +# the same value (5), so we map 5 -> AVCOL_SPC_SMPTE170M as the most common case. +# SWS_CS_DEFAULT is handled specially by not setting frame metadata. +_SWS_CS_TO_AVCOL_SPC = cython.declare( + dict, + { + lib.SWS_CS_ITU709: lib.AVCOL_SPC_BT709, + lib.SWS_CS_FCC: lib.AVCOL_SPC_FCC, + lib.SWS_CS_ITU601: lib.AVCOL_SPC_SMPTE170M, + lib.SWS_CS_SMPTE240M: lib.AVCOL_SPC_SMPTE240M, + }, +) + + @cython.cclass class VideoReformatter: """An object for reformatting size and pixel format of :class:`.VideoFrame`. @@ -123,6 +138,10 @@ def reformat( dst_color_range, ColorRange, 0 ) + # Track whether user explicitly specified destination metadata + set_dst_colorspace: cython.bint = dst_colorspace is not None + set_dst_color_range: cython.bint = dst_color_range is not None + return self._reformat( frame, width or frame.ptr.width, @@ -133,6 +152,8 @@ def reformat( c_interpolation, c_src_color_range, c_dst_color_range, + set_dst_colorspace, + set_dst_color_range, ) @cython.cfunc @@ -147,10 +168,16 @@ def _reformat( interpolation: cython.int, src_color_range: cython.int, dst_color_range: cython.int, + set_dst_colorspace: cython.bint, + set_dst_color_range: cython.bint, ): if frame.ptr.format < 0: raise ValueError("Frame does not have format set.") + # Save original values to set on the output frame (before swscale conversion) + frame_dst_colorspace = dst_colorspace + frame_dst_color_range = dst_color_range + # The definition of color range in pixfmt.h and swscale.h is different. src_color_range = 1 if src_color_range == ColorRange.JPEG.value else 0 dst_color_range = 1 if dst_color_range == ColorRange.JPEG.value else 0 @@ -231,6 +258,16 @@ def _reformat( new_frame._copy_internal_attributes(frame) new_frame._init(dst_format, width, height) + # Set the colorspace and color_range on the output frame only if explicitly specified + if set_dst_colorspace and frame_dst_colorspace in _SWS_CS_TO_AVCOL_SPC: + new_frame.ptr.colorspace = cython.cast( + lib.AVColorSpace, _SWS_CS_TO_AVCOL_SPC[frame_dst_colorspace] + ) + if set_dst_color_range: + new_frame.ptr.color_range = cython.cast( + lib.AVColorRange, frame_dst_color_range + ) + with cython.nogil: lib.sws_scale( self.ptr, From 3a02c09f5b4cef62d843e1d106af766458d31490 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 3 Feb 2026 22:38:00 -0500 Subject: [PATCH 678/846] One Dictionary class --- av/codec/context.py | 6 ++---- av/codec/hwaccel.py | 6 ++---- av/container/core.pxd | 2 +- av/container/core.py | 3 +-- av/container/input.py | 8 +++----- av/container/output.py | 8 +++----- av/dictionary.pxd | 7 +++---- av/dictionary.py | 36 ++++++++++++++++++++++++------------ av/dictionary.pyi | 13 ++++++++++--- av/filter/context.py | 6 ++---- av/sidedata/sidedata.pxd | 6 ++---- 11 files changed, 53 insertions(+), 48 deletions(-) diff --git a/av/codec/context.py b/av/codec/context.py index 27bcf9493..c348353cd 100644 --- a/av/codec/context.py +++ b/av/codec/context.py @@ -4,7 +4,7 @@ from cython.cimports import libav as lib from cython.cimports.av.buffer import ByteSource, bytesource from cython.cimports.av.codec.codec import Codec, wrap_codec -from cython.cimports.av.dictionary import _Dictionary +from cython.cimports.av.dictionary import Dictionary from cython.cimports.av.error import err_check from cython.cimports.av.packet import Packet from cython.cimports.av.utils import avrational_to_fraction, to_avrational @@ -12,8 +12,6 @@ from cython.cimports.libc.stdint import uint8_t from cython.cimports.libc.string import memcpy -from av.dictionary import Dictionary - _cinit_sentinel = cython.declare(object, object()) @@ -233,7 +231,7 @@ def open(self, strict: cython.bint = True): raise ValueError("CodecContext is already open.") return - options: _Dictionary = Dictionary() + options: Dictionary = Dictionary() options.update(self.options or {}) if not self.ptr.time_base.num and self.is_encoder: diff --git a/av/codec/hwaccel.py b/av/codec/hwaccel.py index 6508a5867..ffa196e48 100644 --- a/av/codec/hwaccel.py +++ b/av/codec/hwaccel.py @@ -4,12 +4,10 @@ import cython import cython.cimports.libav as lib from cython.cimports.av.codec.codec import Codec -from cython.cimports.av.dictionary import _Dictionary +from cython.cimports.av.dictionary import Dictionary from cython.cimports.av.error import err_check from cython.cimports.av.video.format import get_video_format -from av.dictionary import Dictionary - class HWDeviceType(IntEnum): none = lib.AV_HWDEVICE_TYPE_NONE @@ -145,7 +143,7 @@ def _initialize_hw_context(self, codec: Codec): if self._device: device_bytes = self._device.encode() c_device = device_bytes - c_options: _Dictionary = Dictionary(self.options) + c_options: Dictionary = Dictionary(self.options) err_check( lib.av_hwdevice_ctx_create( diff --git a/av/container/core.pxd b/av/container/core.pxd index 71d18ae91..1067b678d 100644 --- a/av/container/core.pxd +++ b/av/container/core.pxd @@ -3,7 +3,7 @@ cimport libav as lib from av.codec.hwaccel cimport HWAccel from av.container.pyio cimport PyIOFile from av.container.streams cimport StreamContainer -from av.dictionary cimport _Dictionary +from av.dictionary cimport Dictionary from av.format cimport ContainerFormat from av.stream cimport Stream diff --git a/av/container/core.py b/av/container/core.py index ebdea47e6..babb037e9 100755 --- a/av/container/core.py +++ b/av/container/core.py @@ -21,7 +21,6 @@ from cython.cimports.libc.stdint import int64_t from cython.operator import dereference -from av.dictionary import Dictionary from av.logging import Capture as LogCapture _cinit_sentinel = cython.declare(object, object()) @@ -331,7 +330,7 @@ def __cinit__( self.ptr.flags |= lib.AVFMT_FLAG_CUSTOM_IO ifmt: cython.pointer[lib.AVInputFormat] - c_options: _Dictionary + c_options: Dictionary if not self.writeable: ifmt = self.format.iptr if self.format else cython.NULL c_options = Dictionary(self.options, self.container_options) diff --git a/av/container/input.py b/av/container/input.py index 938ac8dab..6a24d71b4 100644 --- a/av/container/input.py +++ b/av/container/input.py @@ -1,7 +1,7 @@ import cython from cython.cimports.av.codec.context import CodecContext, wrap_codec_context from cython.cimports.av.container.streams import StreamContainer -from cython.cimports.av.dictionary import _Dictionary +from cython.cimports.av.dictionary import Dictionary from cython.cimports.av.error import err_check from cython.cimports.av.packet import Packet from cython.cimports.av.stream import Stream, wrap_stream @@ -9,8 +9,6 @@ from cython.cimports.libc.stdint import int64_t from cython.cimports.libc.stdlib import free, malloc -from av.dictionary import Dictionary - @cython.cfunc def close_input(self: InputContainer): @@ -34,8 +32,8 @@ def __cinit__(self, *args, **kwargs): # If we have either the global `options`, or a `stream_options`, prepare # a mashup of those options for each stream. c_options: cython.pointer[cython.pointer[lib.AVDictionary]] = cython.NULL - base_dict: _Dictionary - stream_dict: _Dictionary + base_dict: Dictionary + stream_dict: Dictionary if self.options or self.stream_options: base_dict = Dictionary(self.options) c_options = cython.cast( diff --git a/av/container/output.py b/av/container/output.py index e57f269e8..29e8460fa 100644 --- a/av/container/output.py +++ b/av/container/output.py @@ -6,14 +6,12 @@ from cython.cimports.av.codec.codec import Codec from cython.cimports.av.codec.context import CodecContext, wrap_codec_context from cython.cimports.av.container.streams import StreamContainer -from cython.cimports.av.dictionary import _Dictionary +from cython.cimports.av.dictionary import Dictionary from cython.cimports.av.error import err_check from cython.cimports.av.packet import Packet from cython.cimports.av.stream import Stream, wrap_stream from cython.cimports.av.utils import dict_to_avdict, to_avrational -from av.dictionary import Dictionary - @cython.cfunc def close_output(self: OutputContainer): @@ -384,8 +382,8 @@ def start_encoding(self): errors=self.metadata_errors, ) - all_options: _Dictionary = Dictionary(self.options, self.container_options) - options: _Dictionary = all_options.copy() + all_options: Dictionary = Dictionary(self.options, self.container_options) + options: Dictionary = all_options.copy() self.err_check(lib.avformat_write_header(self.ptr, cython.address(options.ptr))) # Track option usage... diff --git a/av/dictionary.pxd b/av/dictionary.pxd index 47c100adc..d1eb3c9d1 100644 --- a/av/dictionary.pxd +++ b/av/dictionary.pxd @@ -1,9 +1,8 @@ cimport libav as lib -cdef class _Dictionary: +cdef class Dictionary: cdef lib.AVDictionary *ptr - cpdef _Dictionary copy(self) + cpdef Dictionary copy(self) - -cdef _Dictionary wrap_dictionary(lib.AVDictionary *input_) +cdef Dictionary wrap_dictionary(lib.AVDictionary *input_) diff --git a/av/dictionary.py b/av/dictionary.py index 2c08be63b..4735e9fde 100644 --- a/av/dictionary.py +++ b/av/dictionary.py @@ -1,11 +1,9 @@ -from collections.abc import MutableMapping - import cython from cython.cimports.av.error import err_check @cython.cclass -class _Dictionary: +class Dictionary: def __cinit__(self, *args, **kwargs): for arg in args: self.update(arg) @@ -17,9 +15,8 @@ def __dealloc__(self): lib.av_dict_free(cython.address(self.ptr)) def __getitem__(self, key: cython.str): - element = cython.declare( - cython.pointer[lib.AVDictionaryEntry], - lib.av_dict_get(self.ptr, key, cython.NULL, 0), + element: cython.pointer[lib.AVDictionaryEntry] = lib.av_dict_get( + self.ptr, key, cython.NULL, 0 ) if element == cython.NULL: raise KeyError(key) @@ -35,7 +32,7 @@ def __len__(self): return err_check(lib.av_dict_count(self.ptr)) def __iter__(self): - element = cython.declare(cython.pointer[lib.AVDictionaryEntry], cython.NULL) + element: cython.pointer[lib.AVDictionaryEntry] = cython.NULL while True: element = lib.av_dict_get(self.ptr, "", element, lib.AV_DICT_IGNORE_SUFFIX) if element == cython.NULL: @@ -46,17 +43,32 @@ def __repr__(self): return f"av.Dictionary({dict(self)!r})" def copy(self): - other = cython.declare(_Dictionary, Dictionary()) + other: Dictionary = Dictionary() lib.av_dict_copy(cython.address(other.ptr), self.ptr, 0) return other + def pop(self, key: str): + value = self[key] + del self[key] + return value -class Dictionary(_Dictionary, MutableMapping): - pass + def update(self, other=(), /, **kwds): + if isinstance(other, Dictionary): + lib.av_dict_copy( + cython.address(self.ptr), cython.cast(Dictionary, other).ptr, 0 + ) + elif hasattr(other, "keys"): + for key in other.keys(): + self[key] = other[key] + else: + for key, value in other: + self[key] = value + for key, value in kwds.items(): + self[key] = value @cython.cfunc -def wrap_dictionary(input_: cython.pointer[lib.AVDictionary]) -> _Dictionary: - output = cython.declare(_Dictionary, Dictionary()) +def wrap_dictionary(input_: cython.pointer[lib.AVDictionary]) -> Dictionary: + output: Dictionary = Dictionary() output.ptr = input_ return output diff --git a/av/dictionary.pyi b/av/dictionary.pyi index a6868bea2..3d81e7835 100644 --- a/av/dictionary.pyi +++ b/av/dictionary.pyi @@ -1,10 +1,17 @@ -from collections.abc import MutableMapping -from typing import Iterator +from typing import Iterable, Iterator, Mapping -class Dictionary(MutableMapping[str, str]): +class Dictionary: def __getitem__(self, key: str) -> str: ... def __setitem__(self, key: str, value: str) -> None: ... def __delitem__(self, key: str) -> None: ... def __len__(self) -> int: ... def __iter__(self) -> Iterator[str]: ... def __repr__(self) -> str: ... + def copy(self) -> Dictionary: ... + def pop(self, key: str) -> str: ... + def update( + self, + other: Mapping[str, str] | Iterable[tuple[str, str]] = (), + /, + **kwds: str, + ) -> None: ... diff --git a/av/filter/context.py b/av/filter/context.py index 5ca6d6e47..bb2068ae9 100644 --- a/av/filter/context.py +++ b/av/filter/context.py @@ -3,15 +3,13 @@ import cython import cython.cimports.libav as lib from cython.cimports.av.audio.frame import alloc_audio_frame -from cython.cimports.av.dictionary import _Dictionary +from cython.cimports.av.dictionary import Dictionary from cython.cimports.av.error import err_check from cython.cimports.av.filter.link import alloc_filter_pads from cython.cimports.av.frame import Frame from cython.cimports.av.utils import avrational_to_fraction from cython.cimports.av.video.frame import alloc_video_frame -from av.dictionary import Dictionary - _cinit_sentinel = cython.declare(object, object()) @@ -72,7 +70,7 @@ def init(self, args=None, **kwargs): if args and kwargs: raise ValueError("cannot init from args and kwargs") - dict_: _Dictionary = None + dict_: Dictionary = None c_args: cython.p_char = cython.NULL if args or not kwargs: if args: diff --git a/av/sidedata/sidedata.pxd b/av/sidedata/sidedata.pxd index 37578a61d..d97903c16 100644 --- a/av/sidedata/sidedata.pxd +++ b/av/sidedata/sidedata.pxd @@ -1,18 +1,16 @@ cimport libav as lib from av.buffer cimport Buffer -from av.dictionary cimport _Dictionary, wrap_dictionary +from av.dictionary cimport Dictionary, wrap_dictionary from av.frame cimport Frame cdef class SideData(Buffer): cdef Frame frame cdef lib.AVFrameSideData *ptr - cdef _Dictionary metadata - + cdef Dictionary metadata cdef SideData wrap_side_data(Frame frame, int index) - cdef int get_display_rotation(Frame frame) cdef class _SideDataContainer: From d74d09c683fed4c37d16397ad5eb7c6b739b6b36 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 4 Feb 2026 11:15:02 -0500 Subject: [PATCH 679/846] Fix #2153 (regression) --- av/codec/codec.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/av/codec/codec.py b/av/codec/codec.py index 806190d13..82f5a4bda 100644 --- a/av/codec/codec.py +++ b/av/codec/codec.py @@ -245,9 +245,8 @@ def hardware_configs(self): break ret.append(wrap_hwconfig(ptr)) i += 1 - ret = tuple(ret) - self._hardware_configs = ret - return ret + self._hardware_configs = tuple(ret) + return self._hardware_configs @property def properties(self): From f6de2c5c4eb0e6c90bc9e1549b7d64cb34bebd14 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 7 Feb 2026 23:09:05 -0500 Subject: [PATCH 680/846] Remove unused imports --- av/__init__.py | 1 - av/audio/__init__.py | 4 ++-- av/audio/resampler.py | 1 - av/container/__init__.py | 4 ++-- av/container/core.py | 1 - av/container/output.py | 1 - av/container/output.pyi | 2 +- av/descriptor.pyi | 2 -- av/error.pyi | 1 - av/filter/__init__.py | 4 ++-- av/packet.pyi | 4 ++-- av/sidedata/motionvectors.pyi | 2 +- av/stream.py | 1 - av/subtitles/subtitle.py | 2 +- av/video/__init__.py | 4 ++-- av/video/codeccontext.py | 2 +- av/video/frame.pyi | 2 +- av/video/stream.py | 2 +- docs/conf.py | 2 -- scripts/fetch-vendor.py | 1 - tests/common.py | 1 - tests/test_bitstream.py | 2 +- tests/test_python_io.py | 2 +- tests/test_streams.py | 1 - tests/test_videoframe.py | 7 ++----- 25 files changed, 20 insertions(+), 36 deletions(-) diff --git a/av/__init__.py b/av/__init__.py index e2f9e5a6d..cbc3c8a2f 100644 --- a/av/__init__.py +++ b/av/__init__.py @@ -17,7 +17,6 @@ from av.bitstream import BitStreamFilterContext, bitstream_filters_available from av.codec.codec import Codec, codecs_available from av.codec.context import CodecContext -from av.codec.hwaccel import HWConfig from av.container import open from av.format import ContainerFormat, formats_available from av.packet import Packet diff --git a/av/audio/__init__.py b/av/audio/__init__.py index 74ddf6964..f57d62046 100644 --- a/av/audio/__init__.py +++ b/av/audio/__init__.py @@ -1,2 +1,2 @@ -from .frame import AudioFrame -from .stream import AudioStream +from .frame import AudioFrame as AudioFrame +from .stream import AudioStream as AudioStream diff --git a/av/audio/resampler.py b/av/audio/resampler.py index d56ec06ed..b0572607f 100644 --- a/av/audio/resampler.py +++ b/av/audio/resampler.py @@ -1,7 +1,6 @@ from errno import EAGAIN import cython -from cython.cimports.av.filter.context import FilterContext from cython.cimports.av.filter.graph import Graph from av.error import FFmpegError diff --git a/av/container/__init__.py b/av/container/__init__.py index 98d49dd4e..c5dde46ea 100644 --- a/av/container/__init__.py +++ b/av/container/__init__.py @@ -1,3 +1,3 @@ from .core import Container, Flags, open -from .input import InputContainer -from .output import OutputContainer +from .input import InputContainer as InputContainer +from .output import OutputContainer as OutputContainer diff --git a/av/container/core.py b/av/container/core.py index babb037e9..f4ef1a277 100755 --- a/av/container/core.py +++ b/av/container/core.py @@ -5,7 +5,6 @@ import cython import cython.cimports.libav as lib -from cython.cimports.av.codec.hwaccel import HWAccel from cython.cimports.av.container.core import timeout_info from cython.cimports.av.container.input import InputContainer from cython.cimports.av.container.output import OutputContainer diff --git a/av/container/output.py b/av/container/output.py index 29e8460fa..1e02168cb 100644 --- a/av/container/output.py +++ b/av/container/output.py @@ -1,5 +1,4 @@ import os -from fractions import Fraction import cython from cython.cimports import libav as lib diff --git a/av/container/output.pyi b/av/container/output.pyi index b3cbe1873..3fa18243c 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -1,5 +1,5 @@ from fractions import Fraction -from typing import Sequence, TypeVar, Union, overload +from typing import Sequence, TypeVar, overload from av.audio import _AudioCodecName from av.audio.stream import AudioStream diff --git a/av/descriptor.pyi b/av/descriptor.pyi index ae1998391..4aa2144a3 100644 --- a/av/descriptor.pyi +++ b/av/descriptor.pyi @@ -1,5 +1,3 @@ -from typing import NoReturn - from .option import Option class Descriptor: diff --git a/av/error.pyi b/av/error.pyi index 354c5fd3a..c708a80f6 100644 --- a/av/error.pyi +++ b/av/error.pyi @@ -1,5 +1,4 @@ import builtins -from enum import Enum classes: dict[int, Exception] diff --git a/av/filter/__init__.py b/av/filter/__init__.py index b63a95f25..14c8563ce 100644 --- a/av/filter/__init__.py +++ b/av/filter/__init__.py @@ -1,3 +1,3 @@ from .filter import Filter, filter_descriptor, filters_available -from .graph import Graph -from .loudnorm import stats +from .graph import Graph as Graph +from .loudnorm import stats as stats diff --git a/av/packet.pyi b/av/packet.pyi index d129e5e7f..3a0af39ef 100644 --- a/av/packet.pyi +++ b/av/packet.pyi @@ -3,9 +3,9 @@ from typing import Generic, Iterator, Literal, TypeVar, overload from av.audio.frame import AudioFrame from av.audio.stream import AudioStream -from av.stream import AttachmentStream, DataStream, Stream +from av.stream import Stream from av.subtitles.stream import SubtitleStream -from av.subtitles.subtitle import AssSubtitle, BitmapSubtitle, SubtitleSet +from av.subtitles.subtitle import AssSubtitle, BitmapSubtitle from av.video.frame import VideoFrame from av.video.stream import VideoStream diff --git a/av/sidedata/motionvectors.pyi b/av/sidedata/motionvectors.pyi index 37b0002c0..6b9971fb4 100644 --- a/av/sidedata/motionvectors.pyi +++ b/av/sidedata/motionvectors.pyi @@ -1,4 +1,4 @@ -from typing import Any, Sequence, overload +from typing import Any, overload import numpy as np diff --git a/av/stream.py b/av/stream.py index 05a45d92a..f4dcb38d7 100644 --- a/av/stream.py +++ b/av/stream.py @@ -4,7 +4,6 @@ from cython.cimports import libav as lib from cython.cimports.av.error import err_check from cython.cimports.av.index import wrap_index_entries -from cython.cimports.av.packet import Packet from cython.cimports.av.utils import ( avdict_to_dict, avrational_to_fraction, diff --git a/av/subtitles/subtitle.py b/av/subtitles/subtitle.py index 83f8041a1..91a8aa142 100644 --- a/av/subtitles/subtitle.py +++ b/av/subtitles/subtitle.py @@ -1,6 +1,6 @@ import cython from cython.cimports.cpython import PyBuffer_FillInfo, PyBytes_FromString -from cython.cimports.libc.string import memcpy, strlen +from cython.cimports.libc.string import memcpy @cython.cclass diff --git a/av/video/__init__.py b/av/video/__init__.py index 4a25d8837..f781103fa 100644 --- a/av/video/__init__.py +++ b/av/video/__init__.py @@ -1,2 +1,2 @@ -from .frame import VideoFrame -from .stream import VideoStream +from .frame import VideoFrame as VideoFrame +from .stream import VideoStream as VideoStream diff --git a/av/video/codeccontext.py b/av/video/codeccontext.py index b7842a7d5..21f3d0448 100644 --- a/av/video/codeccontext.py +++ b/av/video/codeccontext.py @@ -1,7 +1,7 @@ import cython import cython.cimports.libav as lib from cython.cimports.av.codec.context import CodecContext -from cython.cimports.av.codec.hwaccel import HWAccel, HWConfig +from cython.cimports.av.codec.hwaccel import HWAccel from cython.cimports.av.error import err_check from cython.cimports.av.frame import Frame from cython.cimports.av.packet import Packet diff --git a/av/video/frame.pyi b/av/video/frame.pyi index a7575e3bd..a8d2c1ec9 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -1,6 +1,6 @@ from enum import IntEnum from pathlib import Path -from typing import Any, ClassVar, Union +from typing import Any, Union import numpy as np diff --git a/av/video/stream.py b/av/video/stream.py index 0a7032b09..658df3d5c 100644 --- a/av/video/stream.py +++ b/av/video/stream.py @@ -1,7 +1,7 @@ import cython from cython.cimports import libav as lib from cython.cimports.av.packet import Packet -from cython.cimports.av.utils import avrational_to_fraction, to_avrational +from cython.cimports.av.utils import avrational_to_fraction from cython.cimports.av.video.frame import VideoFrame diff --git a/docs/conf.py b/docs/conf.py index 02c0d9f4b..cfb4b4082 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,8 +1,6 @@ import os -import re import sys -import sphinx from docutils import nodes from sphinx.util.docutils import SphinxDirective diff --git a/scripts/fetch-vendor.py b/scripts/fetch-vendor.py index 41801df2b..b59ec3b7d 100644 --- a/scripts/fetch-vendor.py +++ b/scripts/fetch-vendor.py @@ -3,7 +3,6 @@ import json import os import platform -import struct import subprocess diff --git a/tests/common.py b/tests/common.py index ce1f2848b..d0cdac2e0 100644 --- a/tests/common.py +++ b/tests/common.py @@ -4,7 +4,6 @@ import errno import functools import os -import types import typing from typing import TYPE_CHECKING from unittest import TestCase as _Base diff --git a/tests/test_bitstream.py b/tests/test_bitstream.py index 5646b46b6..f94ed73af 100644 --- a/tests/test_bitstream.py +++ b/tests/test_bitstream.py @@ -6,7 +6,7 @@ from av import Packet from av.bitstream import BitStreamFilterContext, bitstream_filters_available -from .common import TestCase, fate_suite +from .common import fate_suite def is_annexb(packet: Packet | bytes | None) -> bool: diff --git a/tests/test_python_io.py b/tests/test_python_io.py index c240a45e6..a56196bde 100644 --- a/tests/test_python_io.py +++ b/tests/test_python_io.py @@ -11,7 +11,7 @@ import av -from .common import TestCase, fate_png, fate_suite, has_pillow, run_in_sandbox +from .common import TestCase, fate_png, fate_suite, has_pillow from .test_encode import assert_rgb_rotate, write_rgb_rotate if TYPE_CHECKING: diff --git a/tests/test_streams.py b/tests/test_streams.py index 4f7defef0..88279cb63 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -1,5 +1,4 @@ import os -from fractions import Fraction import pytest diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 80e59614f..5d2368a2f 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -1,17 +1,14 @@ -import time from fractions import Fraction -from unittest import SkipTest import numpy import pytest import av from av import VideoFrame -from av.frame import Frame from av.video.frame import supported_np_pix_fmts -from av.video.reformatter import ColorRange, Colorspace, Interpolation +from av.video.reformatter import Colorspace, Interpolation -from .common import TestCase, assertNdarraysEqual, fate_png, fate_suite +from .common import assertNdarraysEqual, fate_png, fate_suite def assertPixelValue16(plane, expected, byteorder: str) -> None: From 085b4d291c403a6c24b8df1c38eeecbfa6bea164 Mon Sep 17 00:00:00 2001 From: Kim Minjong Date: Sun, 8 Feb 2026 14:07:15 +0900 Subject: [PATCH 681/846] Preserving hardware memory during hw decoding, exporting/importing via dlpack (#2155) Add DLPack support for zero-copy GPU frame exchange between PyAV and frameworks like PyTorch/CuPy, avoiding GPU->CPU->GPU round-trips when using cuvid decode + GPU processing + nvenc encode. - Implement __dlpack__ and __dlpack_device__ on VideoPlane - Add VideoFrame.from_dlpack for importing DLPack tensors (nv12/p010le/p016le) - Add CudaContext for managing CUDA device/frames contexts - Add HWAccel.is_hw_owned to preserve frames on GPU memory - Switch frame buffer management from av_image_alloc to av_frame_get_buffer - Handle hw_frames_ctx in planes, reformatter, and to_ndarray paths --- av/codec/hwaccel.pxd | 2 + av/codec/hwaccel.py | 16 +- av/codec/hwaccel.pyi | 9 +- av/video/codeccontext.py | 4 + av/video/frame.pxd | 18 +- av/video/frame.py | 461 ++++++++++++++++++++++++++++--- av/video/frame.pyi | 18 ++ av/video/plane.pxd | 32 ++- av/video/plane.py | 265 +++++++++++++++++- av/video/plane.pyi | 4 + av/video/reformatter.py | 17 ++ include/libavcodec/avcodec.pxd | 8 +- include/libavutil/avutil.pxd | 15 + tests/test_dlpack.py | 486 +++++++++++++++++++++++++++++++++ 14 files changed, 1305 insertions(+), 50 deletions(-) create mode 100644 tests/test_dlpack.py diff --git a/av/codec/hwaccel.pxd b/av/codec/hwaccel.pxd index 46efdaf3b..af8815cb6 100644 --- a/av/codec/hwaccel.pxd +++ b/av/codec/hwaccel.pxd @@ -16,6 +16,8 @@ cdef class HWAccel: cdef readonly Codec codec cdef readonly HWConfig config cdef lib.AVBufferRef *ptr + cdef readonly int device_id + cdef readonly bint is_hw_owned cdef public bint allow_software_fallback cdef public dict options cdef public int flags diff --git a/av/codec/hwaccel.py b/av/codec/hwaccel.py index ffa196e48..991848995 100644 --- a/av/codec/hwaccel.py +++ b/av/codec/hwaccel.py @@ -110,6 +110,7 @@ def __init__( allow_software_fallback=True, options=None, flags=None, + is_hw_owned=False, ): if isinstance(device_type, HWDeviceType): self._device_type = device_type @@ -120,9 +121,17 @@ def __init__( else: raise ValueError("Unknown type for device_type") - self._device = device + self.is_hw_owned = is_hw_owned + self.device_id = 0 + if self._device_type == HWDeviceType.cuda and device: + self.device_id = int(device) + + self._device = None if device is None else f"{device}" self.allow_software_fallback = allow_software_fallback + self.options = {} if not options else dict(options) + if self._device_type == HWDeviceType.cuda and self.is_hw_owned: + self.options.setdefault("primary_ctx", "1") self.flags = 0 if not flags else flags self.ptr = cython.NULL self.config = None @@ -135,7 +144,7 @@ def _initialize_hw_context(self, codec: Codec): if self._device_type and config.device_type != self._device_type: continue break - else: + else: # nobreak raise NotImplementedError(f"No supported hardware config for {codec}") self.config = config @@ -155,7 +164,7 @@ def _initialize_hw_context(self, codec: Codec): ) ) - def create(self, codec: Codec): + def create(self, codec: Codec) -> HWAccel: """Create a new hardware accelerator context with the given codec""" if self.ptr: raise RuntimeError("Hardware context already initialized") @@ -165,6 +174,7 @@ def create(self, codec: Codec): device=self._device, allow_software_fallback=self.allow_software_fallback, options=self.options, + is_hw_owned=self.is_hw_owned, ) ret._initialize_hw_context(codec) return ret diff --git a/av/codec/hwaccel.pyi b/av/codec/hwaccel.pyi index 8bdc0a6e0..247771a3b 100644 --- a/av/codec/hwaccel.pyi +++ b/av/codec/hwaccel.pyi @@ -37,13 +37,20 @@ class HWConfig: def is_supported(self) -> bool: ... class HWAccel: + options: dict[str, object] + + @property + def is_hw_owned(self) -> bool: ... + @property + def device_id(self) -> int: ... def __init__( self, device_type: str | HWDeviceType, - device: str | None = None, + device: str | int | None = None, allow_software_fallback: bool = False, options: dict[str, object] | None = None, flags: int | None = None, + is_hw_owned: bool = False, ) -> None: ... def create(self, codec: Codec) -> HWAccel: ... diff --git a/av/video/codeccontext.py b/av/video/codeccontext.py index 21f3d0448..020edf37c 100644 --- a/av/video/codeccontext.py +++ b/av/video/codeccontext.py @@ -127,6 +127,10 @@ def _transfer_hwframe(self, frame: Frame): # need to transfer. return frame + if self.hwaccel_ctx.is_hw_owned: + cython.cast(VideoFrame, frame)._device_id = self.hwaccel_ctx.device_id + return frame + frame_sw: Frame = self._alloc_next_frame() err_check(lib.av_hwframe_transfer_data(frame_sw.ptr, frame.ptr, 0)) # TODO: Is there anything else to transfer? diff --git a/av/video/frame.pxd b/av/video/frame.pxd index d352a7ab8..90547cb9f 100644 --- a/av/video/frame.pxd +++ b/av/video/frame.pxd @@ -6,15 +6,21 @@ from av.video.format cimport VideoFormat from av.video.reformatter cimport VideoReformatter -cdef class VideoFrame(Frame): - # This is the buffer that is used to back everything in the AVFrame. - # We don't ever actually access it directly. - cdef uint8_t *_buffer - cdef object _np_buffer +cdef class CudaContext: + cdef readonly int device_id + cdef readonly bint primary_ctx + cdef lib.AVBufferRef* _device_ref + cdef dict _frames_cache + cdef lib.AVBufferRef* _get_device_ref(self) + cdef public lib.AVBufferRef* get_frames_ctx( + self, lib.AVPixelFormat sw_fmt, int width, int height + ) +cdef class VideoFrame(Frame): + cdef CudaContext _cuda_ctx cdef VideoReformatter reformatter cdef readonly VideoFormat format - + cdef readonly int _device_id cdef _init(self, lib.AVPixelFormat format, unsigned int width, unsigned int height) cdef _init_user_attributes(self) cpdef save(self, object filepath) diff --git a/av/video/frame.py b/av/video/frame.py index 1320afad3..1fa30f92f 100644 --- a/av/video/frame.py +++ b/av/video/frame.py @@ -2,12 +2,177 @@ from enum import IntEnum import cython +import cython.cimports.libav as lib +from cython.cimports.av.dictionary import Dictionary from cython.cimports.av.error import err_check from cython.cimports.av.sidedata.sidedata import get_display_rotation from cython.cimports.av.utils import check_ndarray from cython.cimports.av.video.format import get_pix_fmt, get_video_format -from cython.cimports.av.video.plane import VideoPlane -from cython.cimports.libc.stdint import uint8_t +from cython.cimports.av.video.plane import DLManagedTensor, VideoPlane, kCPU, kCuda +from cython.cimports.cpython.exc import PyErr_Clear +from cython.cimports.cpython.pycapsule import ( + PyCapsule_GetPointer, + PyCapsule_IsValid, + PyCapsule_SetName, +) +from cython.cimports.cpython.ref import Py_DECREF, Py_INCREF +from cython.cimports.libc.stdint import int64_t, uint8_t + + +@cython.cclass +class CudaContext: + def __cinit__(self, device_id: cython.int = 0, primary_ctx: cython.bint = True): + self.device_id = device_id + self.primary_ctx = primary_ctx + self._device_ref = cython.NULL + self._frames_cache = {} + + def __dealloc__(self): + ref: cython.pointer[lib.AVBufferRef] + + for v in self._frames_cache.values(): + ref = cython.cast( + cython.pointer[lib.AVBufferRef], + cython.cast(cython.size_t, v), + ) + lib.av_buffer_unref(cython.address(ref)) + self._frames_cache.clear() + + ref = self._device_ref + if ref != cython.NULL: + lib.av_buffer_unref(cython.address(ref)) + self._device_ref = cython.NULL + + @cython.cfunc + def _get_device_ref(self) -> cython.pointer[lib.AVBufferRef]: + device_ref: cython.pointer[lib.AVBufferRef] = self._device_ref + if device_ref != cython.NULL: + return device_ref + + device_ref = cython.NULL + device_bytes = f"{self.device_id}".encode() + c_device: cython.p_char = device_bytes + options: Dictionary = Dictionary( + {"primary_ctx": "1" if self.primary_ctx else "0"} + ) + err_check( + lib.av_hwdevice_ctx_create( + cython.address(device_ref), + lib.AV_HWDEVICE_TYPE_CUDA, + c_device, + options.ptr, + 0, + ) + ) + self._device_ref = device_ref + return device_ref + + @cython.cfunc + def get_frames_ctx( + self, + sw_fmt: lib.AVPixelFormat, + width: cython.int, + height: cython.int, + ) -> cython.pointer[lib.AVBufferRef]: + key = (int(sw_fmt), int(width), int(height)) + cached = self._frames_cache.get(key) + cached_ref: cython.pointer[lib.AVBufferRef] + out_ref: cython.pointer[lib.AVBufferRef] + + if cached is not None: + cached_ref = cython.cast( + cython.pointer[lib.AVBufferRef], + cython.cast(cython.size_t, cached), + ) + out_ref = lib.av_buffer_ref(cached_ref) + if out_ref == cython.NULL: + raise MemoryError("av_buffer_ref() failed") + return out_ref + + device_ref = self._get_device_ref() + + frames_ref: cython.pointer[lib.AVBufferRef] = lib.av_hwframe_ctx_alloc( + device_ref + ) + if frames_ref == cython.NULL: + raise MemoryError("av_hwframe_ctx_alloc() failed") + + try: + frames_ctx: cython.pointer[lib.AVHWFramesContext] = cython.cast( + cython.pointer[lib.AVHWFramesContext], frames_ref.data + ) + frames_ctx.format = get_pix_fmt(b"cuda") + frames_ctx.sw_format = sw_fmt + frames_ctx.width = int(width) + frames_ctx.height = int(height) + err_check(lib.av_hwframe_ctx_init(frames_ref)) + except Exception: + lib.av_buffer_unref(cython.address(frames_ref)) + raise + + out_ref = lib.av_buffer_ref(frames_ref) + if out_ref == cython.NULL: + lib.av_buffer_unref(cython.address(frames_ref)) + raise MemoryError("av_buffer_ref() failed") + + self._frames_cache[key] = cython.cast(cython.size_t, frames_ref) + return out_ref + + +@cython.cfunc +def _consume_dlpack(obj: object, stream: object) -> cython.pointer[DLManagedTensor]: + capsule: object + managed: cython.pointer[DLManagedTensor] + + if hasattr(obj, "__dlpack__"): + capsule = obj.__dlpack__() if stream is None else obj.__dlpack__(stream=stream) + else: + capsule = obj + + if not PyCapsule_IsValid(capsule, b"dltensor"): + PyErr_Clear() + raise TypeError( + "expected a DLPack capsule or an object implementing __dlpack__" + ) + + managed = cython.cast( + cython.pointer[DLManagedTensor], + PyCapsule_GetPointer(capsule, b"dltensor"), + ) + if managed == cython.NULL: + raise ValueError("PyCapsule_GetPointer returned NULL") + + if PyCapsule_SetName(capsule, b"used_dltensor") != 0: + raise RuntimeError("PyCapsule_SetName failed") + + return managed + + +@cython.cfunc +@cython.nogil +@cython.exceptval(check=False) +def _dlpack_avbuffer_free( + opaque: cython.p_void, + data: cython.pointer[uint8_t], +) -> cython.void: + managed: cython.pointer[DLManagedTensor] = cython.cast( + cython.pointer[DLManagedTensor], opaque + ) + if managed != cython.NULL: + managed.deleter(managed) + + +@cython.cfunc +@cython.nogil +@cython.exceptval(check=False) +def _numpy_avbuffer_free( + opaque: cython.p_void, + data: cython.pointer[uint8_t], +) -> cython.void: + if opaque != cython.NULL: + with cython.gil: + Py_DECREF(cython.cast(object, opaque)) + _cinit_bypass_sentinel = object() @@ -222,10 +387,7 @@ def _init(self, format: lib.AVPixelFormat, width: cython.uint, height: cython.ui # We enforce aligned buffers, otherwise `sws_scale` can perform # poorly or even cause out-of-bounds reads and writes. if width and height: - res = lib.av_image_alloc( - self.ptr.data, self.ptr.linesize, width, height, format, 16 - ) - self._buffer = self.ptr.data[0] + res = lib.av_frame_get_buffer(self.ptr, 16) if res: err_check(res) @@ -241,11 +403,7 @@ def _init_user_attributes(self): ) def __dealloc__(self): - # The `self._buffer` member is only set if *we* allocated the buffer in `_init`, - # as opposed to a buffer allocated by a decoder. - lib.av_freep(cython.address(self._buffer)) - # Let go of the reference from the numpy buffers if we made one - self._np_buffer = None + lib.av_frame_unref(self.ptr) def __repr__(self): return ( @@ -261,12 +419,21 @@ def planes(self): # We need to detect which planes actually exist, but also constrain ourselves to # the maximum plane count (as determined only by VideoFrames so far), in case # the library implementation does not set the last plane to NULL. + fmt = self.format + if self.ptr.hw_frames_ctx: + frames_ctx: cython.pointer[lib.AVHWFramesContext] = cython.cast( + cython.pointer[lib.AVHWFramesContext], self.ptr.hw_frames_ctx.data + ) + fmt = get_video_format( + frames_ctx.sw_format, self.ptr.width, self.ptr.height + ) + max_plane_count: cython.int = 0 - for i in range(self.format.ptr.nb_components): - count = self.format.ptr.comp[i].plane + 1 + for i in range(fmt.ptr.nb_components): + count = fmt.ptr.comp[i].plane + 1 if max_plane_count < count: max_plane_count = count - if self.format.name == "pal8": + if fmt.name == "pal8": max_plane_count = 2 plane_count: cython.int = 0 @@ -446,7 +613,18 @@ def to_ndarray(self, channel_last=False, **kwargs): .. note:: For ``gbrp`` formats, channels are flipped to RGB order. """ - frame: VideoFrame = self.reformat(**kwargs) + kwargs2 = dict(kwargs) + if self.ptr.hw_frames_ctx and "format" not in kwargs2: + frames_ctx: cython.pointer[lib.AVHWFramesContext] = cython.cast( + cython.pointer[lib.AVHWFramesContext], self.ptr.hw_frames_ctx.data + ) + kwargs2["format"] = get_video_format( + frames_ctx.sw_format, self.ptr.width, self.ptr.height + ).name + + frame: VideoFrame = self.reformat(**kwargs2) + if frame.ptr.hw_frames_ctx: + raise ValueError("Cannot convert a hardware frame to numpy directly.") import numpy as np @@ -859,10 +1037,6 @@ def from_numpy_buffer(array, format="rgb24", width=0): return frame def _image_fill_pointers_numpy(self, buffer, width, height, linesizes, format): - c_format: lib.AVPixelFormat - c_ptr: cython.pointer[uint8_t] - c_data: cython.size_t - # If you want to use the numpy notation, then you need to include the following lines at the top of the file: # cimport numpy as cnp # cnp.import_array() @@ -879,29 +1053,42 @@ def _image_fill_pointers_numpy(self, buffer, width, height, linesizes, format): # Using buffer.ctypes.data helps avoid any kind of usage of the c-api from # numpy, which avoid the need to add numpy as a compile time dependency. - c_data = buffer.ctypes.data - c_ptr = cython.cast(cython.pointer[uint8_t], c_data) - c_format = get_pix_fmt(format) - lib.av_freep(cython.address(self._buffer)) + c_data: cython.Py_ssize_t = buffer.ctypes.data + c_ptr: cython.pointer[uint8_t] = cython.cast(cython.pointer[uint8_t], c_data) + c_format: lib.AVPixelFormat = get_pix_fmt(format) + lib.av_frame_unref(self.ptr) # Hold on to a reference for the numpy buffer so that it doesn't get accidentally garbage collected - self._np_buffer = buffer self.ptr.format = c_format self.ptr.width = width self.ptr.height = height for i, linesize in enumerate(linesizes): self.ptr.linesize[i] = linesize - res = lib.av_image_fill_pointers( - self.ptr.data, - cython.cast(lib.AVPixelFormat, self.ptr.format), - self.ptr.height, + required = err_check( + lib.av_image_fill_pointers( + self.ptr.data, + cython.cast(lib.AVPixelFormat, self.ptr.format), + self.ptr.height, + c_ptr, + self.ptr.linesize, + ) + ) + + py_buf = cython.cast(object, buffer) + Py_INCREF(py_buf) + + self.ptr.buf[0] = lib.av_buffer_create( c_ptr, - self.ptr.linesize, + required, + _numpy_avbuffer_free, + cython.cast(cython.p_void, py_buf), + 0, ) + if self.ptr.buf[0] == cython.NULL: + Py_DECREF(py_buf) + raise MemoryError("av_buffer_create failed") - if res: - err_check(res) self._init_user_attributes() @staticmethod @@ -1179,3 +1366,215 @@ def from_bytes( else: raise NotImplementedError(f"Format '{format}' is not supported.") return frame + + @staticmethod + def from_dlpack( + planes, + format: str = "nv12", + width: int = 0, + height: int = 0, + stream=None, + device_id: int | None = None, + primary_ctx: bool = True, + cuda_context=None, + ): + if not isinstance(planes, (tuple, list)): + planes = (planes,) + + if len(planes) != 2: + raise ValueError( + "from_dlpack currently supports 2-plane formats only (nv12/p010le/p016le)" + ) + + sw_fmt: lib.AVPixelFormat = get_pix_fmt(format) + nv12 = get_pix_fmt(b"nv12") + p010le = get_pix_fmt(b"p010le") + p016le = get_pix_fmt(b"p016le") + + if sw_fmt not in (nv12, p010le, p016le): + raise NotImplementedError("from_dlpack supports nv12, p010le, p016le only") + + expected_bits = 8 if sw_fmt == nv12 else 16 + itemsize = 1 if expected_bits == 8 else 2 + + m0: cython.pointer[DLManagedTensor] = cython.NULL + m1: cython.pointer[DLManagedTensor] = cython.NULL + frame: VideoFrame = None + + try: + m0 = _consume_dlpack(planes[0], stream) + m1 = _consume_dlpack(planes[1], stream) + + dev_type0 = m0.dl_tensor.device_type + dev_type1 = m1.dl_tensor.device_type + if dev_type0 != dev_type1: + raise ValueError("plane tensors must have the same device_type") + if dev_type0 not in (kCuda, kCPU): + raise NotImplementedError( + "only CPU and CUDA DLPack tensors are supported" + ) + + dev0 = m0.dl_tensor.device_id + dev1 = m1.dl_tensor.device_id + if dev0 != dev1: + raise ValueError("plane tensors must be on the same CUDA device") + if dev_type0 == kCuda: + if device_id is None: + device_id = dev0 + elif device_id != dev0: + raise ValueError( + "device_id does not match the DLPack tensor device_id" + ) + else: + if device_id not in (None, 0): + raise ValueError("device_id must be 0 for CPU tensors") + device_id = 0 + if dev_type0 == kCPU and (dev0 != 0 or dev1 != 0): + raise ValueError("CPU DLPack tensors must have device_id == 0") + + if ( + m0.dl_tensor.dtype.code != 1 + or m0.dl_tensor.dtype.bits != expected_bits + or m0.dl_tensor.dtype.lanes != 1 + ): + raise TypeError("unexpected dtype for plane 0") + if ( + m1.dl_tensor.dtype.code != 1 + or m1.dl_tensor.dtype.bits != expected_bits + or m1.dl_tensor.dtype.lanes != 1 + ): + raise TypeError("unexpected dtype for plane 1") + + if m0.dl_tensor.ndim != 2: + raise ValueError("plane 0 must be 2D (H, W)") + + y_h = cython.cast(int64_t, m0.dl_tensor.shape[0]) + y_w = cython.cast(int64_t, m0.dl_tensor.shape[1]) + + if width == 0 and height == 0: + width = cython.cast(int, y_w) + height = cython.cast(int, y_h) + elif width == 0 or height == 0: + raise ValueError("either specify both width/height or neither") + else: + if y_w != width or y_h != height: + raise ValueError("plane 0 shape does not match width/height") + + if width % 2 or height % 2: + raise ValueError("width/height must be even for nv12/p010le/p016le") + + if m0.dl_tensor.strides != cython.NULL: + if m0.dl_tensor.strides[1] != 1: + raise ValueError("plane 0 must be contiguous in the last dimension") + y_pitch_elems = cython.cast(int64_t, m0.dl_tensor.strides[0]) + else: + y_pitch_elems = cython.cast(int64_t, width) + + y_linesize = cython.cast(int, y_pitch_elems * itemsize) + y_size = cython.cast(int, y_linesize * height) + + uv_ndim = m1.dl_tensor.ndim + uv_h_expected = height // 2 + + if uv_ndim == 2: + uv_h = cython.cast(int, m1.dl_tensor.shape[0]) + uv_w = cython.cast(int, m1.dl_tensor.shape[1]) + if uv_h != uv_h_expected or uv_w != width: + raise ValueError("plane 1 must have shape (H/2, W) for 2D UV") + if m1.dl_tensor.strides != cython.NULL: + if m1.dl_tensor.strides[1] != 1: + raise ValueError( + "plane 1 must be contiguous in the last dimension" + ) + uv_pitch_elems = cython.cast(int64_t, m1.dl_tensor.strides[0]) + else: + uv_pitch_elems = cython.cast(int64_t, uv_w) + elif uv_ndim == 3: + uv_h = cython.cast(int, m1.dl_tensor.shape[0]) + uv_w2 = cython.cast(int, m1.dl_tensor.shape[1]) + uv_c = cython.cast(int, m1.dl_tensor.shape[2]) + if uv_h != uv_h_expected or uv_w2 != (width // 2) or uv_c != 2: + raise ValueError("plane 1 must have shape (H/2, W/2, 2) for 3D UV") + if m1.dl_tensor.strides != cython.NULL: + if m1.dl_tensor.strides[2] != 1 or m1.dl_tensor.strides[1] != 2: + raise ValueError( + "unexpected UV plane strides for (H/2, W/2, 2)" + ) + uv_pitch_elems = cython.cast(int64_t, m1.dl_tensor.strides[0]) + else: + uv_pitch_elems = cython.cast(int64_t, width) + else: + raise ValueError("plane 1 must be 2D or 3D") + + uv_linesize = cython.cast(int, uv_pitch_elems * itemsize) + uv_size = cython.cast(int, uv_linesize * (height // 2)) + + frame = alloc_video_frame() + frame.ptr.width = width + frame.ptr.height = height + if dev_type0 == kCuda: + ctx: CudaContext + frames_ref: cython.pointer[lib.AVBufferRef] + if cuda_context is None: + ctx = CudaContext(device_id=device_id, primary_ctx=primary_ctx) + else: + if not isinstance(cuda_context, CudaContext): + raise TypeError("cuda_context must be a CudaContext") + if int(cuda_context.device_id) != int(device_id): + raise ValueError( + "cuda_context.device_id does not match the DLPack tensor device_id" + ) + if bool(cuda_context.primary_ctx) != bool(primary_ctx): + raise ValueError( + "cuda_context.primary_ctx does not match primary_ctx" + ) + ctx = cython.cast(CudaContext, cuda_context) + + frames_ref = ctx.get_frames_ctx(sw_fmt, width, height) + frame.ptr.format = get_pix_fmt(b"cuda") + frame.ptr.hw_frames_ctx = frames_ref + frame._device_id = device_id + frame._cuda_ctx = ctx + else: + frame.ptr.format = sw_fmt + + y_ptr = cython.cast( + cython.pointer[uint8_t], m0.dl_tensor.data + ) + cython.cast(cython.size_t, m0.dl_tensor.byte_offset) + uv_ptr = cython.cast( + cython.pointer[uint8_t], m1.dl_tensor.data + ) + cython.cast(cython.size_t, m1.dl_tensor.byte_offset) + + frame.ptr.buf[0] = lib.av_buffer_create( + y_ptr, y_size, _dlpack_avbuffer_free, cython.cast(cython.p_void, m0), 0 + ) + if frame.ptr.buf[0] == cython.NULL: + raise MemoryError("av_buffer_create failed for plane 0") + frame.ptr.data[0] = y_ptr + frame.ptr.linesize[0] = y_linesize + m0 = cython.NULL + + frame.ptr.buf[1] = lib.av_buffer_create( + uv_ptr, + uv_size, + _dlpack_avbuffer_free, + cython.cast(cython.p_void, m1), + 0, + ) + if frame.ptr.buf[1] == cython.NULL: + raise MemoryError("av_buffer_create failed for plane 1") + frame.ptr.data[1] = uv_ptr + frame.ptr.linesize[1] = uv_linesize + m1 = cython.NULL + + frame._init_user_attributes() + return frame + + except Exception: + if frame is not None: + lib.av_frame_unref(frame.ptr) + if m0 != cython.NULL: + m0.deleter(m0) + if m1 != cython.NULL: + m1.deleter(m1) + raise diff --git a/av/video/frame.pyi b/av/video/frame.pyi index a8d2c1ec9..570e227b0 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -28,6 +28,13 @@ class PictureType(IntEnum): SP = 6 BI = 7 +class CudaContext: + @property + def device_id(self) -> int: ... + @property + def primary_ctx(self) -> bool: ... + def __init__(self, device_id: int = 0, primary_ctx: bool = True) -> None: ... + class VideoFrame(Frame): format: VideoFormat planes: tuple[VideoPlane, ...] @@ -84,3 +91,14 @@ class VideoFrame(Frame): flip_horizontal: bool = False, flip_vertical: bool = False, ) -> VideoFrame: ... + @staticmethod + def from_dlpack( + planes: object | tuple[object, ...], + format: str = "nv12", + width: int = 0, + height: int = 0, + stream: int | None = None, + device_id: int | None = None, + primary_ctx: bool = True, + cuda_context: CudaContext | None = None, + ) -> VideoFrame: ... diff --git a/av/video/plane.pxd b/av/video/plane.pxd index f9abf22b6..80ff79050 100644 --- a/av/video/plane.pxd +++ b/av/video/plane.pxd @@ -1,8 +1,38 @@ +from libc.stdint cimport int64_t, uint8_t, uint16_t, uint64_t + from av.plane cimport Plane from av.video.format cimport VideoFormatComponent cdef class VideoPlane(Plane): - cdef readonly size_t buffer_size cdef readonly unsigned int width, height + + +cdef enum DeviceType: + kCPU = 1 + kCuda = 2 + +cdef struct DLDataType: + uint8_t code + uint8_t bits + uint16_t lanes + +cdef struct DLTensor: + void* data + int device_type + int device_id + int ndim + DLDataType dtype + int64_t* shape + int64_t* strides + uint64_t byte_offset + +cdef struct DLManagedTensor + +ctypedef void (*DLManagedTensorDeleter)(DLManagedTensor*) noexcept nogil + +cdef struct DLManagedTensor: + DLTensor dl_tensor + void* manager_ctx + DLManagedTensorDeleter deleter diff --git a/av/video/plane.py b/av/video/plane.py index 495a9de4c..c4908c21d 100644 --- a/av/video/plane.py +++ b/av/video/plane.py @@ -1,25 +1,45 @@ import cython +import cython.cimports.libav as lib +from cython.cimports.av.error import err_check +from cython.cimports.av.video.format import get_pix_fmt, get_video_format from cython.cimports.av.video.frame import VideoFrame +from cython.cimports.cpython import PyBUF_WRITABLE, PyBuffer_FillInfo +from cython.cimports.cpython.buffer import Py_buffer +from cython.cimports.cpython.pycapsule import ( + PyCapsule_GetPointer, + PyCapsule_IsValid, + PyCapsule_New, +) +from cython.cimports.libc.stdlib import free, malloc @cython.cclass class VideoPlane(Plane): def __cinit__(self, frame: VideoFrame, index: cython.int): # The palette plane has no associated component or linesize; set fields manually - if frame.format.name == "pal8" and index == 1: + fmt = frame.format + if frame.ptr.hw_frames_ctx: + frames_ctx: cython.pointer[lib.AVHWFramesContext] = cython.cast( + cython.pointer[lib.AVHWFramesContext], frame.ptr.hw_frames_ctx.data + ) + fmt = get_video_format( + frames_ctx.sw_format, frame.ptr.width, frame.ptr.height + ) + + if fmt.name == "pal8" and index == 1: self.width = 256 self.height = 1 self.buffer_size = 256 * 4 return - for i in range(frame.format.ptr.nb_components): - if frame.format.ptr.comp[i].plane == index: - component = frame.format.components[i] + for i in range(fmt.ptr.nb_components): + if fmt.ptr.comp[i].plane == index: + component = fmt.components[i] self.width = component.width self.height = component.height break - else: # nobreak - raise RuntimeError(f"could not find plane {index} of {frame.format!r}") + else: + raise RuntimeError(f"could not find plane {index} of {fmt!r}") # Sometimes, linesize is negative (and that is meaningful). We are only # insisting that the buffer size be based on the extent of linesize, and @@ -38,3 +58,236 @@ def line_size(self): :type: int """ return self.frame.ptr.linesize[self.index] + + @cython.cfunc + def _buffer_writable(self) -> cython.bint: + if self.frame.ptr.hw_frames_ctx: + return False + return True + + def __getbuffer__(self, view: cython.pointer[Py_buffer], flags: cython.int): + if self.frame.ptr.hw_frames_ctx: + raise TypeError( + "Hardware frame planes do not support the Python buffer protocol. " + "Use DLPack (__dlpack__) or download to a software frame." + ) + if flags & PyBUF_WRITABLE and not self._buffer_writable(): + raise ValueError("buffer is not writable") + PyBuffer_FillInfo(view, self, self._buffer_ptr(), self._buffer_size(), 0, flags) + + def __dlpack_device__(self): + if self.frame.ptr.hw_frames_ctx: + if cython.cast(lib.AVPixelFormat, self.frame.ptr.format) != get_pix_fmt( + b"cuda" + ): + raise NotImplementedError( + "DLPack export is only implemented for CUDA hw frames" + ) + return (kCuda, self.frame._device_id) + return (kCPU, 0) + + def __dlpack__(self, *, stream: int | None = None): + if self.frame.ptr.buf[0] == cython.NULL: + raise TypeError( + "DLPack export requires a refcounted AVFrame (frame.buf[0] is NULL)" + ) + + sw_fmt: lib.AVPixelFormat + device_type: cython.int + device_id: cython.int + + if self.frame.ptr.hw_frames_ctx: + if cython.cast(lib.AVPixelFormat, self.frame.ptr.format) != get_pix_fmt( + b"cuda" + ): + raise NotImplementedError( + "DLPack export is only implemented for CUDA hw frames" + ) + if stream is not None: + raise NotImplementedError( + "CUDA stream synchronization is not supported. " + "Pass stream=None and synchronize before calling __dlpack__." + ) + + frames_ctx: cython.pointer[lib.AVHWFramesContext] = cython.cast( + cython.pointer[lib.AVHWFramesContext], self.frame.ptr.hw_frames_ctx.data + ) + sw_fmt = frames_ctx.sw_format + device_type = kCuda + device_id = self.frame._device_id + else: + sw_fmt = cython.cast(lib.AVPixelFormat, self.frame.ptr.format) + device_type = kCPU + device_id = 0 + + line_size = self.line_size + if line_size < 0: + raise NotImplementedError( + "negative linesize is not supported for DLPack export" + ) + + nv12 = get_pix_fmt(b"nv12") + p010le = get_pix_fmt(b"p010le") + p016le = get_pix_fmt(b"p016le") + + ndim, bits, itemsize = cython.declare(cython.int) + s0, s1, s2 = cython.declare(int64_t) + st0, st1, st2 = cython.declare(int64_t) + + if sw_fmt == nv12: + itemsize = 1 + bits = 8 + if self.index == 0: + ndim = 2 + s0 = self.frame.ptr.height + s1 = self.frame.ptr.width + st0 = line_size + st1 = 1 + elif self.index == 1: + ndim = 3 + s0 = self.frame.ptr.height // 2 + s1 = self.frame.ptr.width // 2 + s2 = 2 + st0 = line_size + st1 = 2 + st2 = 1 + else: + raise ValueError("invalid plane index for NV12") + elif sw_fmt == p010le or sw_fmt == p016le: + itemsize = 2 + bits = 16 + if line_size % itemsize: + raise ValueError("linesize is not aligned to dtype") + if self.index == 0: + ndim = 2 + s0 = self.frame.ptr.height + s1 = self.frame.ptr.width + st0 = line_size // itemsize + st1 = 1 + elif self.index == 1: + ndim = 3 + s0 = self.frame.ptr.height // 2 + s1 = self.frame.ptr.width // 2 + s2 = 2 + st0 = line_size // itemsize + st1 = 2 + st2 = 1 + else: + raise ValueError("invalid plane index for P010/P016") + else: + raise NotImplementedError("unsupported sw_format for DLPack export") + + frame_ref: cython.pointer[lib.AVFrame] = lib.av_frame_alloc() + if frame_ref == cython.NULL: + raise MemoryError("av_frame_alloc() failed") + err_check(lib.av_frame_ref(frame_ref, self.frame.ptr)) + + shape = cython.cast( + cython.pointer[int64_t], malloc(ndim * cython.sizeof(int64_t)) + ) + strides = cython.cast( + cython.pointer[int64_t], malloc(ndim * cython.sizeof(int64_t)) + ) + if shape == cython.NULL or strides == cython.NULL: + if shape != cython.NULL: + free(shape) + if strides != cython.NULL: + free(strides) + lib.av_frame_free(cython.address(frame_ref)) + raise MemoryError("malloc() failed") + + if ndim == 2: + shape[0] = s0 + shape[1] = s1 + strides[0] = st0 + strides[1] = st1 + else: + shape[0] = s0 + shape[1] = s1 + shape[2] = s2 + strides[0] = st0 + strides[1] = st1 + strides[2] = st2 + + ctx = cython.cast( + cython.pointer[cython.p_void], malloc(3 * cython.sizeof(cython.p_void)) + ) + if ctx == cython.NULL: + free(shape) + free(strides) + lib.av_frame_free(cython.address(frame_ref)) + raise MemoryError("malloc() failed") + + ctx[0] = cython.cast(cython.p_void, frame_ref) + ctx[1] = cython.cast(cython.p_void, shape) + ctx[2] = cython.cast(cython.p_void, strides) + + managed = cython.cast( + cython.pointer[DLManagedTensor], malloc(cython.sizeof(DLManagedTensor)) + ) + if managed == cython.NULL: + free(ctx) + free(shape) + free(strides) + lib.av_frame_free(cython.address(frame_ref)) + raise MemoryError("malloc() failed") + + managed.dl_tensor.data = cython.cast(cython.p_void, frame_ref.data[self.index]) + managed.dl_tensor.device_type = device_type + managed.dl_tensor.device_id = device_id + managed.dl_tensor.ndim = ndim + managed.dl_tensor.dtype = DLDataType(code=1, bits=bits, lanes=1) + managed.dl_tensor.shape = shape + managed.dl_tensor.strides = strides + managed.dl_tensor.byte_offset = 0 + managed.manager_ctx = cython.cast(cython.p_void, ctx) + managed.deleter = _dlpack_managed_tensor_deleter + + try: + capsule = PyCapsule_New( + cython.cast(cython.p_void, managed), + b"dltensor", + _dlpack_capsule_destructor, + ) + except Exception: + _dlpack_managed_tensor_deleter(managed) + raise + + return capsule + + +@cython.cfunc +@cython.nogil +@cython.exceptval(check=False) +def _dlpack_managed_tensor_deleter( + managed: cython.pointer[DLManagedTensor], +) -> cython.void: + if managed == cython.NULL: + return + ctx = cython.cast(cython.pointer[cython.p_void], managed.manager_ctx) + if ctx != cython.NULL: + frame_ref = cython.cast(cython.pointer[lib.AVFrame], ctx[0]) + shape = cython.cast(cython.pointer[int64_t], ctx[1]) + strides = cython.cast(cython.pointer[int64_t], ctx[2]) + + if frame_ref != cython.NULL: + lib.av_frame_free(cython.address(frame_ref)) + if shape != cython.NULL: + free(shape) + if strides != cython.NULL: + free(strides) + free(ctx) + + free(managed) + + +@cython.cfunc +@cython.exceptval(check=False) +def _dlpack_capsule_destructor(capsule: object) -> cython.void: + if PyCapsule_IsValid(capsule, b"dltensor"): + managed = cython.cast( + cython.pointer[DLManagedTensor], + PyCapsule_GetPointer(capsule, b"dltensor"), + ) + if managed != cython.NULL: + managed.deleter(managed) diff --git a/av/video/plane.pyi b/av/video/plane.pyi index e4a0a206c..fcbf8e6ed 100644 --- a/av/video/plane.pyi +++ b/av/video/plane.pyi @@ -1,3 +1,5 @@ +from types import CapsuleType + from av.plane import Plane from .frame import VideoFrame @@ -9,3 +11,5 @@ class VideoPlane(Plane): buffer_size: int def __init__(self, frame: VideoFrame, index: int) -> None: ... + def __dlpack_device__(self) -> tuple[int, int]: ... + def __dlpack__(self, *, stream: int | None = None) -> CapsuleType: ... diff --git a/av/video/reformatter.py b/av/video/reformatter.py index 786543744..5a30d4d9b 100644 --- a/av/video/reformatter.py +++ b/av/video/reformatter.py @@ -185,6 +185,23 @@ def _reformat( src_format = cython.cast(lib.AVPixelFormat, frame.ptr.format) # Shortcut! + if frame.ptr.hw_frames_ctx: + if ( + dst_format == src_format + and width == frame.ptr.width + and height == frame.ptr.height + and dst_colorspace == src_colorspace + and src_color_range == dst_color_range + ): + return frame + + frame_sw = alloc_video_frame() + err_check(lib.av_hwframe_transfer_data(frame_sw.ptr, frame.ptr, 0)) + frame_sw.pts = frame.pts + frame_sw._init_user_attributes() + frame = frame_sw + src_format = cython.cast(lib.AVPixelFormat, frame.ptr.format) + if ( dst_format == src_format and width == frame.ptr.width diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index b75769945..9b7304d05 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -1,4 +1,5 @@ -from libc.stdint cimport int64_t, uint16_t, uint32_t, uint8_t +from libc.stdint cimport int64_t, uint8_t, uint16_t, uint32_t + cdef extern from "libavcodec/packet.h" nogil: const AVPacketSideData *av_packet_side_data_get( @@ -353,8 +354,10 @@ cdef extern from "libavcodec/avcodec.h" nogil: int64_t pkt_dts void *opaque int sample_rate - int nb_side_data + AVBufferRef *buf[8] + AVFrameSideData **side_data + int nb_side_data int flags AVColorRange color_range AVColorPrimaries color_primaries @@ -364,6 +367,7 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVDictionary *metadata int decode_error_flags + AVBufferRef *hw_frames_ctx AVBufferRef *opaque_ref AVChannelLayout ch_layout int64_t duration diff --git a/include/libavutil/avutil.pxd b/include/libavutil/avutil.pxd index 9d8486e1a..6ca52e162 100644 --- a/include/libavutil/avutil.pxd +++ b/include/libavutil/avutil.pxd @@ -164,7 +164,9 @@ cdef extern from "libavutil/error.h" nogil: cdef extern from "libavutil/frame.h" nogil: cdef AVFrame* av_frame_alloc() cdef void av_frame_free(AVFrame**) + cdef int av_frame_ref(AVFrame *dst, const AVFrame *src) cdef void av_frame_unref(AVFrame *frame) + cdef int av_frame_get_buffer(AVFrame *frame, int align) cdef int av_frame_make_writable(AVFrame *frame) cdef int av_frame_copy_props(AVFrame *dst, const AVFrame *src) cdef AVFrameSideData* av_frame_get_side_data(AVFrame *frame, AVFrameSideDataType type) @@ -185,12 +187,25 @@ cdef extern from "libavutil/hwcontext.h" nogil: AV_HWDEVICE_TYPE_VULKAN AV_HWDEVICE_TYPE_D3D12VA + ctypedef struct AVHWFramesContext: + const void *av_class + AVBufferRef *device_ref + void *device_ctx + void *hwctx + AVPixelFormat format + AVPixelFormat sw_format + int width + int height + cdef int av_hwdevice_ctx_create(AVBufferRef **device_ctx, AVHWDeviceType type, const char *device, AVDictionary *opts, int flags) cdef AVHWDeviceType av_hwdevice_find_type_by_name(const char *name) cdef const char *av_hwdevice_get_type_name(AVHWDeviceType type) cdef AVHWDeviceType av_hwdevice_iterate_types(AVHWDeviceType prev) cdef int av_hwframe_transfer_data(AVFrame *dst, const AVFrame *src, int flags) + cdef AVBufferRef *av_hwframe_ctx_alloc(AVBufferRef *device_ref) + cdef int av_hwframe_ctx_init(AVBufferRef *ref) + cdef extern from "libavutil/imgutils.h" nogil: cdef int av_image_alloc( uint8_t *pointers[4], diff --git a/tests/test_dlpack.py b/tests/test_dlpack.py new file mode 100644 index 000000000..e017affd2 --- /dev/null +++ b/tests/test_dlpack.py @@ -0,0 +1,486 @@ +import gc + +import numpy +import pytest + +import av +from av import VideoFrame +from av.codec.hwaccel import HWAccel + +from .common import assertNdarraysEqual, fate_png + + +def _make_u8(shape: tuple[int, ...]) -> numpy.ndarray: + return numpy.arange(int(numpy.prod(shape)), dtype=numpy.uint8).reshape(shape) + + +def _make_u16(shape: tuple[int, ...]) -> numpy.ndarray: + return numpy.arange(int(numpy.prod(shape)), dtype=numpy.uint16).reshape(shape) + + +def _plane_to_2d(plane, height: int, width: int, dtype) -> numpy.ndarray: + itemsize = numpy.dtype(dtype).itemsize + assert plane.line_size % itemsize == 0 + pitch_elems = plane.line_size // itemsize + arr = numpy.frombuffer(memoryview(plane), dtype=dtype).reshape(height, pitch_elems) + return arr[:, :width] + + +def _get_cuda_backend(): + try: + import cupy # type: ignore + + try: + if cupy.cuda.runtime.getDeviceCount() > 0: + return ("cupy", cupy) + except Exception: + pass + except Exception: + pass + + try: + import torch # type: ignore + + if torch.cuda.is_available(): + return ("torch", torch) + except Exception: + pass + + return None + + +def test_hwaccel_validation_and_primary_ctx() -> None: + hw = HWAccel(device_type="cuda") + assert hw.is_hw_owned == False + assert "primary_ctx" not in hw.options + + hw = HWAccel(device_type="cuda", is_hw_owned=True) + assert hw.is_hw_owned == True + assert hw.options.get("primary_ctx") == "1" + + hw = HWAccel(device_type="cuda", is_hw_owned=True, options={"primary_ctx": "0"}) + assert hw.options.get("primary_ctx") == "0" + + +def test_video_frame_from_dlpack_nv12_cpu_basic_zero_copy_and_lifetime() -> None: + width, height = 64, 48 + y = _make_u8((height, width)) + uv = _make_u8((height // 2, width // 2, 2)) + + frame = VideoFrame.from_dlpack((y, uv), format="nv12") + + assert frame.format.name == "nv12" + assert frame.width == width + assert frame.height == height + assert len(frame.planes) == 2 + assert frame.planes[0].width == width + assert frame.planes[0].height == height + assert frame.planes[1].width == width // 2 + assert frame.planes[1].height == height // 2 + assert frame.planes[0].line_size == width + assert frame.planes[1].line_size == width + + y_plane = _plane_to_2d(frame.planes[0], height, width, numpy.uint8) + uv_plane = _plane_to_2d(frame.planes[1], height // 2, width, numpy.uint8) + assertNdarraysEqual(y_plane, y) + assertNdarraysEqual(uv_plane, uv.reshape(height // 2, width)) + + y[0, 0] = 123 + uv[0, 0, 0] = 11 + uv[0, 0, 1] = 22 + + expected_y_bytes = y.tobytes() + expected_uv_bytes = uv.reshape(height // 2, width).tobytes() + + assert memoryview(frame.planes[0])[0] == 123 + assert memoryview(frame.planes[1])[0] == 11 + assert memoryview(frame.planes[1])[1] == 22 + + del y + del uv + gc.collect() + + assert bytes(frame.planes[0]) == expected_y_bytes + assert bytes(frame.planes[1]) == expected_uv_bytes + + +def test_video_frame_from_dlpack_nv12_cpu_with_pitch_and_dlpack_export() -> None: + width, height = 64, 48 + pad = 16 + + y_base = _make_u8((height, width + pad)) + y = y_base[:, :width] + uv_base = _make_u8((height // 2, (width + pad) // 2, 2)) + uv = uv_base[:, : width // 2, :] + + frame = VideoFrame.from_dlpack((y, uv), format="nv12") + + assert frame.planes[0].line_size == width + pad + assert frame.planes[1].line_size == width + pad + assert frame.planes[0].buffer_size == (width + pad) * height + assert frame.planes[1].buffer_size == (width + pad) * (height // 2) + + y_plane = _plane_to_2d(frame.planes[0], height, width, numpy.uint8) + uv_plane = _plane_to_2d(frame.planes[1], height // 2, width, numpy.uint8) + assertNdarraysEqual(y_plane, y) + assertNdarraysEqual(uv_plane, uv.reshape(height // 2, width)) + + assert frame.planes[0].__dlpack_device__() == (1, 0) + + y_dl = numpy.from_dlpack(frame.planes[0]) + uv_dl = numpy.from_dlpack(frame.planes[1]) + + assert y_dl.shape == (height, width) + assert y_dl.dtype == numpy.uint8 + assert y_dl.strides == (width + pad, 1) + assertNdarraysEqual(y_dl, y) + + assert uv_dl.shape == (height // 2, width // 2, 2) + assert uv_dl.dtype == numpy.uint8 + assert uv_dl.strides == (width + pad, 2, 1) + assertNdarraysEqual(uv_dl, uv) + + expected_y = numpy.array(y, copy=True) + expected_uv = numpy.array(uv, copy=True) + + del frame + del y + del uv + del y_base + del uv_base + gc.collect() + + assertNdarraysEqual(y_dl, expected_y) + assertNdarraysEqual(uv_dl, expected_uv) + + +def test_video_frame_from_dlpack_nv12_cpu_accepts_uv_2d() -> None: + width, height = 64, 48 + y = _make_u8((height, width)) + uv2d = _make_u8((height // 2, width)) + + frame = VideoFrame.from_dlpack((y, uv2d), format="nv12") + + uv_plane = _plane_to_2d(frame.planes[1], height // 2, width, numpy.uint8) + assertNdarraysEqual(uv_plane, uv2d) + + uv_dl = numpy.from_dlpack(frame.planes[1]) + assert uv_dl.shape == (height // 2, width // 2, 2) + assertNdarraysEqual(uv_dl, uv2d.reshape(height // 2, width // 2, 2)) + + +def test_video_frame_from_dlpack_accepts_video_plane_objects() -> None: + width, height = 64, 48 + y = _make_u8((height, width)) + uv = _make_u8((height // 2, width // 2, 2)) + + frame1 = VideoFrame.from_dlpack((y, uv), format="nv12") + frame2 = VideoFrame.from_dlpack((frame1.planes[0], frame1.planes[1]), format="nv12") + + assert bytes(frame2.planes[0]) == bytes(frame1.planes[0]) + assert bytes(frame2.planes[1]) == bytes(frame1.planes[1]) + + +@pytest.mark.parametrize("fmt", ["p010le", "p016le"]) +def test_video_frame_from_dlpack_p010_p016_cpu(fmt: str) -> None: + width, height = 64, 48 + y = _make_u16((height, width)) + uv = _make_u16((height // 2, width // 2, 2)) + + frame = VideoFrame.from_dlpack((y, uv), format=fmt) + + assert frame.format.name == fmt + assert len(frame.planes) == 2 + assert frame.planes[0].line_size == width * 2 + assert frame.planes[1].line_size == width * 2 + + y_plane = _plane_to_2d(frame.planes[0], height, width, numpy.uint16) + uv_plane = _plane_to_2d(frame.planes[1], height // 2, width, numpy.uint16) + assertNdarraysEqual(y_plane, y) + assertNdarraysEqual(uv_plane, uv.reshape(height // 2, width)) + + y_dl = numpy.from_dlpack(frame.planes[0]) + uv_dl = numpy.from_dlpack(frame.planes[1]) + + assert y_dl.dtype == numpy.uint16 + assert y_dl.shape == (height, width) + assert y_dl.strides == (width * 2, 2) + assertNdarraysEqual(y_dl, y) + + assert uv_dl.dtype == numpy.uint16 + assert uv_dl.shape == (height // 2, width // 2, 2) + assert uv_dl.strides == (width * 2, 4, 2) + assertNdarraysEqual(uv_dl, uv) + + +def test_video_plane_dlpack_export_keeps_frame_alive_after_gc() -> None: + container = av.open(fate_png()) + frame = next(container.decode(video=0)) + frame_nv12 = frame.reformat(format="nv12") + + width = frame_nv12.width + height = frame_nv12.height + line_size = frame_nv12.planes[0].line_size + expected = _plane_to_2d(frame_nv12.planes[0], height, width, numpy.uint8).copy() + + y_dl = numpy.from_dlpack(frame_nv12.planes[0]) + assert y_dl.shape == (height, width) + assert y_dl.strides == (line_size, 1) + + del frame_nv12 + del frame + del container + gc.collect() + + assertNdarraysEqual(y_dl, expected) + + +def test_video_plane_dlpack_unsupported_format_raises() -> None: + rgb = numpy.zeros((16, 16, 3), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(rgb, format="rgb24") + assert frame.planes[0].__dlpack_device__() == (1, 0) + + with pytest.raises( + NotImplementedError, match="unsupported sw_format for DLPack export" + ): + frame.planes[0].__dlpack__() + + +def test_video_frame_from_dlpack_requires_two_planes() -> None: + y = numpy.zeros((4, 4), dtype=numpy.uint8) + with pytest.raises(ValueError, match="2-plane"): + VideoFrame.from_dlpack(y, format="nv12") + + +def test_video_frame_from_dlpack_rejects_unsupported_format() -> None: + width, height = 64, 48 + y = numpy.zeros((height, width), dtype=numpy.uint8) + uv = numpy.zeros((height // 2, width // 2, 2), dtype=numpy.uint8) + + with pytest.raises(NotImplementedError, match="supports nv12, p010le, p016le only"): + VideoFrame.from_dlpack((y, uv), format="yuv420p") + + +def test_video_frame_from_dlpack_rejects_device_id_for_cpu() -> None: + width, height = 64, 48 + y = numpy.zeros((height, width), dtype=numpy.uint8) + uv = numpy.zeros((height // 2, width // 2, 2), dtype=numpy.uint8) + + with pytest.raises(ValueError, match="device_id must be 0 for CPU tensors"): + VideoFrame.from_dlpack((y, uv), format="nv12", device_id=1) + + +def test_video_frame_from_dlpack_requires_both_width_height_or_neither() -> None: + width, height = 64, 48 + y = numpy.zeros((height, width), dtype=numpy.uint8) + uv = numpy.zeros((height // 2, width // 2, 2), dtype=numpy.uint8) + + with pytest.raises(ValueError, match="either specify both width/height or neither"): + VideoFrame.from_dlpack((y, uv), format="nv12", width=width, height=0) + + +def test_video_frame_from_dlpack_rejects_plane0_shape_mismatch_with_width_height() -> ( + None +): + width, height = 64, 48 + y = numpy.zeros((height, width), dtype=numpy.uint8) + uv = numpy.zeros((height // 2, width // 2, 2), dtype=numpy.uint8) + + with pytest.raises(ValueError, match="plane 0 shape does not match width/height"): + VideoFrame.from_dlpack((y, uv), format="nv12", width=width + 2, height=height) + + +def test_video_frame_from_dlpack_rejects_odd_dimensions() -> None: + width, height = 63, 48 + y = numpy.zeros((height, width), dtype=numpy.uint8) + uv = numpy.zeros((height // 2, width), dtype=numpy.uint8) + + with pytest.raises(ValueError, match="width/height must be even"): + VideoFrame.from_dlpack((y, uv), format="nv12") + + +def test_video_frame_from_dlpack_rejects_noncontiguous_plane0_last_dim() -> None: + width, height = 64, 48 + y_full = numpy.zeros((height, width * 2), dtype=numpy.uint8) + y = y_full[:, ::2] + uv = numpy.zeros((height // 2, width // 2, 2), dtype=numpy.uint8) + + with pytest.raises( + ValueError, match="plane 0 must be contiguous in the last dimension" + ): + VideoFrame.from_dlpack((y, uv), format="nv12") + + +def test_video_frame_from_dlpack_rejects_noncontiguous_uv_plane_last_dim_2d() -> None: + width, height = 64, 48 + y = numpy.zeros((height, width), dtype=numpy.uint8) + uv_full = numpy.zeros((height // 2, width * 2), dtype=numpy.uint8) + uv = uv_full[:, ::2] + + with pytest.raises( + ValueError, match="plane 1 must be contiguous in the last dimension" + ): + VideoFrame.from_dlpack((y, uv), format="nv12") + + +def test_video_frame_from_dlpack_rejects_unexpected_uv_strides_3d() -> None: + width, height = 64, 48 + y = numpy.zeros((height, width), dtype=numpy.uint8) + uv_full = numpy.zeros((height // 2, width // 2, 4), dtype=numpy.uint8) + uv = uv_full[:, :, :2] + + with pytest.raises(ValueError, match="unexpected UV plane strides"): + VideoFrame.from_dlpack((y, uv), format="nv12") + + +def test_video_frame_from_dlpack_rejects_wrong_dtype_plane0() -> None: + width, height = 64, 48 + y = numpy.zeros((height, width), dtype=numpy.uint16) + uv = numpy.zeros((height // 2, width // 2, 2), dtype=numpy.uint8) + + with pytest.raises(TypeError, match="unexpected dtype for plane 0"): + VideoFrame.from_dlpack((y, uv), format="nv12") + + +def test_video_frame_from_dlpack_rejects_wrong_dtype_plane1() -> None: + width, height = 64, 48 + y = numpy.zeros((height, width), dtype=numpy.uint8) + uv = numpy.zeros((height // 2, width // 2, 2), dtype=numpy.uint16) + + with pytest.raises(TypeError, match="unexpected dtype for plane 1"): + VideoFrame.from_dlpack((y, uv), format="nv12") + + +def test_video_frame_from_dlpack_p010le_requires_uint16() -> None: + width, height = 64, 48 + y = numpy.zeros((height, width), dtype=numpy.uint8) + uv = numpy.zeros((height // 2, width // 2, 2), dtype=numpy.uint8) + + with pytest.raises(TypeError, match="unexpected dtype for plane 0"): + VideoFrame.from_dlpack((y, uv), format="p010le") + + +def test_video_frame_from_dlpack_rejects_plane0_ndim_not_2() -> None: + y = numpy.zeros((4, 4, 1), dtype=numpy.uint8) + uv = numpy.zeros((2, 4), dtype=numpy.uint8) + + with pytest.raises(ValueError, match="plane 0 must be 2D"): + VideoFrame.from_dlpack((y, uv), format="nv12", width=4, height=4) + + +def test_video_frame_from_dlpack_rejects_plane1_ndim_not_2_or_3() -> None: + width, height = 64, 48 + y = numpy.zeros((height, width), dtype=numpy.uint8) + uv = numpy.zeros((height // 2, width, 1, 1), dtype=numpy.uint8) + + with pytest.raises(ValueError, match="plane 1 must be 2D or 3D"): + VideoFrame.from_dlpack((y, uv), format="nv12") + + +def test_video_frame_from_dlpack_reusing_capsule_raises_typeerror() -> None: + width, height = 64, 48 + y = numpy.zeros((height, width), dtype=numpy.uint8) + uv = numpy.zeros((height // 2, width // 2, 2), dtype=numpy.uint8) + + cap0 = y.__dlpack__() + cap1 = uv.__dlpack__() + + VideoFrame.from_dlpack((cap0, cap1), format="nv12", width=width, height=height) + + with pytest.raises(TypeError, match="expected a DLPack capsule"): + VideoFrame.from_dlpack((cap0, cap1), format="nv12", width=width, height=height) + + +def test_video_frame_from_dlpack_invalid_plane_object_raises_typeerror() -> None: + with pytest.raises(TypeError, match="expected a DLPack capsule"): + VideoFrame.from_dlpack((object(), object()), format="nv12", width=64, height=48) + + +def test_video_frame_from_dlpack_cuda_hw_frame_behavior_if_available() -> None: + backend = _get_cuda_backend() + if backend is None: + pytest.skip("CUDA backend (cupy/torch) not available.") + + width, height = 64, 48 + name, mod = backend + + try: + if name == "cupy": + try: + ndev = int(mod.cuda.runtime.getDeviceCount()) + except Exception: + ndev = 1 + + device_id = 1 if ndev > 1 else 0 + with mod.cuda.Device(device_id): + y = mod.arange(height * width, dtype=mod.uint8).reshape(height, width) + uv = mod.arange( + (height // 2) * (width // 2) * 2, dtype=mod.uint8 + ).reshape(height // 2, width // 2, 2) + expected_device = y.__dlpack_device__() + frame = VideoFrame.from_dlpack((y, uv), format="nv12") + + assert frame.format.name == "cuda" + assert len(frame.planes) == 2 + + with pytest.raises( + TypeError, match="Hardware frame planes do not support" + ): + memoryview(frame.planes[0]) + + assert frame.planes[0].__dlpack_device__() == expected_device + + cap_y = frame.planes[0].__dlpack__() + if hasattr(mod, "fromDlpack"): + y2 = mod.fromDlpack(cap_y) + else: + y2 = mod.from_dlpack(cap_y) + + assert y2.shape == y.shape + assert mod.all(y2 == y).item() + + with pytest.raises( + ValueError, + match="Cannot convert a hardware frame to numpy directly", + ): + frame.to_ndarray(format="cuda") + + else: + try: + ndev = int(mod.cuda.device_count()) + except Exception: + ndev = 1 + + device_id = 1 if ndev > 1 else 0 + device = f"cuda:{device_id}" + + y = mod.arange(height * width, device=device, dtype=mod.uint8).reshape( + height, width + ) + uv = mod.arange( + (height // 2) * (width // 2) * 2, device=device, dtype=mod.uint8 + ).reshape(height // 2, width // 2, 2) + + expected_device = y.__dlpack_device__() + frame = VideoFrame.from_dlpack((y, uv), format="nv12") + + assert frame.format.name == "cuda" + assert len(frame.planes) == 2 + + with pytest.raises(TypeError, match="Hardware frame planes do not support"): + memoryview(frame.planes[0]) + + assert frame.planes[0].__dlpack_device__() == expected_device + + cap_y = frame.planes[0].__dlpack__() + y2 = mod.utils.dlpack.from_dlpack(cap_y) + + assert tuple(y2.shape) == tuple(y.shape) + assert mod.equal(y2, y) + + with pytest.raises( + ValueError, match="Cannot convert a hardware frame to numpy directly" + ): + frame.to_ndarray(format="cuda") + except av.FFmpegError as e: + pytest.skip(f"CUDA hwcontext not available in this build/runtime: {e}") From 1553e302a40baaac3feae384f7f19eeff3771d45 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 8 Feb 2026 00:22:25 -0500 Subject: [PATCH 682/846] Update changelog --- CHANGELOG.rst | 17 ++++++++++++++--- av/video/reformatter.py | 8 ++++++-- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 86eb44f2e..6a803b6a5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,7 +4,7 @@ Changelog We are operating with `semantic versioning `_. .. - Update this file in your commit that makes a change (besides maintainence). + Update this file in your commit that makes a change (besides maintenance). To make merging/rebasing easier, don't manually break lines in here when they are too long. To make tracking easier, please add either ``closes #123`` or ``fixes #123`` to the first line of the commit message, when closing/fixing a GitHub issue. @@ -33,7 +33,18 @@ Major: - Limited API binary wheels are now built. - 3.13t (free-threading) will be dropped because of storage limitations. - When an FFmpeg C function indicates an error, raise av.ArgumentError instead of ValueError/av.ValueError. This helps disambiguate why an exception is being thrown. -- Save space by removing libaom (av1 encoder/decoder); dav1d, stvav1, and hardware, are available. +- Save space by removing libaom (av1 encoder/decoder); dav1d, stvav1, and hardware, are available alternatives. + +Features: + +- Use zero-copy for Packet init from buffer data by :gh-user:`WyattBlue` in (:pr:`2199`). +- Expose AVIndexEntry by :gh-user:`Queuecumber` in (:pr:`2136`). +- Preserving hardware memory during cuvid decoding, exporting/importing via dlpack by :gh-user:`WyattBlue` in (:pr:`2155`). + +Fixes: + +- Fix :issue:`2149` by :gh-user:`WyattBlue` in (:pr:`2155`). +- Fix packet typing based on stream and specify InputContainer.demux based on incoming stream by :gh-user:`ntjohnson1` in (:pr:`2134`). v16.1.0 ------- @@ -45,7 +56,7 @@ Features: - Add subtitle encoding support by :gh-user:`skeskinen` in (:pr:`2050`). - Add read/write access to PacketSideData by :gh-user:`skeskinen` in (:pr:`2051`). - Add yuv422p support for video frame to_ndarray and from_ndarray by :gh-user:`wader` in (:pr:`2054`). -- Add binding for ``avcodec_find_best_pix_fmt_of_list()`` by :gh-user:`ndeybach` (:pr:`2058`). +- Add binding for ``avcodec_find_best_pix_fmt_of_list()`` by :gh-user:`ndeybach` in (:pr:`2058`). Fixes: diff --git a/av/video/reformatter.py b/av/video/reformatter.py index 5a30d4d9b..8005b7ed7 100644 --- a/av/video/reformatter.py +++ b/av/video/reformatter.py @@ -255,9 +255,13 @@ def _reformat( # Grab the coefficients for the requested transforms. # The inv_table brings us to linear, and `tbl` to the new space. if src_colorspace != lib.SWS_CS_DEFAULT: - inv_tbl = lib.sws_getCoefficients(src_colorspace) + inv_tbl = cython.cast( + cython.p_int, lib.sws_getCoefficients(src_colorspace) + ) if dst_colorspace != lib.SWS_CS_DEFAULT: - tbl = lib.sws_getCoefficients(dst_colorspace) + tbl = cython.cast( + cython.p_int, lib.sws_getCoefficients(dst_colorspace) + ) ret = lib.sws_setColorspaceDetails( self.ptr, From 642c6dee3ceed90d416b47428fc2e2139d7af802 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 8 Feb 2026 16:04:10 -0500 Subject: [PATCH 683/846] Make `include/` smaller Since almost everything needs libav, put as little as possible to reduce the need to recompile everything. `swscale` can be returned back to `include/` if another file besides `reformatter` needs it. --- av/_core.pxd | 15 +++++ av/_core.py | 20 +++---- av/logging.pxd | 6 ++ av/logging.py | 3 +- av/subtitles/subtitle.pxd | 2 - av/utils.py | 7 --- av/video/reformatter.pxd | 72 +++++++++++++++++++++++- av/video/reformatter.py | 77 ++++++++++++------------- include/{libavcodec => }/avcodec.pxd | 68 ++++++++++++++++------ include/{libavfilter => }/avfilter.pxd | 53 ++++++++++++----- include/{libavformat => }/avformat.pxd | 0 include/{libavutil => }/avutil.pxd | 0 include/libav.pxd | 23 ++------ include/libavcodec/bsf.pxd | 20 ------- include/libavcodec/hwaccel.pxd | 13 ----- include/libavdevice/avdevice.pxd | 5 -- include/libavfilter/avfiltergraph.pxd | 37 ------------ include/libavfilter/buffersink.pxd | 2 - include/libavfilter/buffersrc.pxd | 2 - include/libswresample/swresample.pxd | 4 -- include/libswscale/swscale.pxd | 78 -------------------------- 21 files changed, 232 insertions(+), 275 deletions(-) create mode 100644 av/_core.pxd rename include/{libavcodec => }/avcodec.pxd (91%) rename include/{libavfilter => }/avfilter.pxd (58%) rename include/{libavformat => }/avformat.pxd (100%) rename include/{libavutil => }/avutil.pxd (100%) delete mode 100644 include/libavcodec/bsf.pxd delete mode 100644 include/libavcodec/hwaccel.pxd delete mode 100644 include/libavdevice/avdevice.pxd delete mode 100644 include/libavfilter/avfiltergraph.pxd delete mode 100644 include/libavfilter/buffersink.pxd delete mode 100644 include/libavfilter/buffersrc.pxd delete mode 100644 include/libswresample/swresample.pxd delete mode 100644 include/libswscale/swscale.pxd diff --git a/av/_core.pxd b/av/_core.pxd new file mode 100644 index 000000000..49f665b6e --- /dev/null +++ b/av/_core.pxd @@ -0,0 +1,15 @@ +cdef extern from "libavdevice/avdevice.h" nogil: + cdef int avdevice_version() + cdef char* avdevice_configuration() + cdef char* avdevice_license() + void avdevice_register_all() + +cdef extern from "libswscale/swscale.h" nogil: + cdef int swscale_version() + cdef char* swscale_configuration() + cdef char* swscale_license() + +cdef extern from "libswresample/swresample.h" nogil: + cdef int swresample_version() + cdef char* swresample_configuration() + cdef char* swresample_license() diff --git a/av/_core.py b/av/_core.py index 2384fa457..d9ad01074 100644 --- a/av/_core.py +++ b/av/_core.py @@ -1,7 +1,7 @@ import cython import cython.cimports.libav as lib -lib.avdevice_register_all() +avdevice_register_all() # Exports. time_base = lib.AV_TIME_BASE @@ -41,9 +41,9 @@ def decode_version(v): license=lib.avformat_license(), ), "libavdevice": dict( - version=decode_version(lib.avdevice_version()), - configuration=lib.avdevice_configuration(), - license=lib.avdevice_license(), + version=decode_version(avdevice_version()), + configuration=avdevice_configuration(), + license=avdevice_license(), ), "libavfilter": dict( version=decode_version(lib.avfilter_version()), @@ -51,14 +51,14 @@ def decode_version(v): license=lib.avfilter_license(), ), "libswscale": dict( - version=decode_version(lib.swscale_version()), - configuration=lib.swscale_configuration(), - license=lib.swscale_license(), + version=decode_version(swscale_version()), + configuration=swscale_configuration(), + license=swscale_license(), ), "libswresample": dict( - version=decode_version(lib.swresample_version()), - configuration=lib.swresample_configuration(), - license=lib.swresample_license(), + version=decode_version(swresample_version()), + configuration=swresample_configuration(), + license=swresample_license(), ), } diff --git a/av/logging.pxd b/av/logging.pxd index 8bd9fb730..b5a99be02 100644 --- a/av/logging.pxd +++ b/av/logging.pxd @@ -1,6 +1,12 @@ +cimport libav as lib + + cdef extern from "Python.h" nogil: void PyErr_PrintEx(int set_sys_last_vars) int Py_IsInitialized() void PyErr_Display(object, object, object) +cdef extern from "stdio.h" nogil: + cdef int vsnprintf(char *output, int n, const char *format, lib.va_list args) + cpdef get_last_error() diff --git a/av/logging.py b/av/logging.py index a937b26a2..dbd247d56 100644 --- a/av/logging.py +++ b/av/logging.py @@ -44,7 +44,6 @@ from threading import Lock, get_ident import cython -import cython.cimports.libav as lib from cython.cimports.libc.stdio import fprintf, stderr from cython.cimports.libc.stdlib import free, malloc @@ -338,7 +337,7 @@ def log_callback( # Format the message. message: cython.char[1024] - lib.vsnprintf(message, 1023, format, args) + vsnprintf(message, 1023, format, args) # Get the name. name: cython.p_const_char = cython.NULL diff --git a/av/subtitles/subtitle.pxd b/av/subtitles/subtitle.pxd index 508eb9034..4f8556742 100644 --- a/av/subtitles/subtitle.pxd +++ b/av/subtitles/subtitle.pxd @@ -4,12 +4,10 @@ cimport libav as lib cdef class SubtitleProxy: cdef lib.AVSubtitle struct - cdef class SubtitleSet: cdef SubtitleProxy proxy cdef readonly tuple rects - cdef class Subtitle: cdef SubtitleProxy proxy cdef lib.AVSubtitleRect *ptr diff --git a/av/utils.py b/av/utils.py index bdf11bbb5..300d315f2 100644 --- a/av/utils.py +++ b/av/utils.py @@ -5,9 +5,6 @@ from cython.cimports import libav as lib from cython.cimports.av.error import err_check -# === DICTIONARIES === -# ==================== - @cython.cfunc def _decode(s: cython.pointer[cython.char], encoding, errors) -> str: @@ -47,10 +44,6 @@ def dict_to_avdict( ) -# === FRACTIONS === -# ================= - - @cython.cfunc def avrational_to_fraction( input: cython.pointer[cython.const[lib.AVRational]], diff --git a/av/video/reformatter.pxd b/av/video/reformatter.pxd index d031f4dcf..433824e8c 100644 --- a/av/video/reformatter.pxd +++ b/av/video/reformatter.pxd @@ -1,12 +1,80 @@ cimport libav as lib +from libc.stdint cimport uint8_t from av.video.frame cimport VideoFrame -cdef class VideoReformatter: +cdef extern from "libswscale/swscale.h" nogil: + cdef struct SwsContext: + pass + cdef struct SwsFilter: + pass + cdef int SWS_FAST_BILINEAR + cdef int SWS_BILINEAR + cdef int SWS_BICUBIC + cdef int SWS_X + cdef int SWS_POINT + cdef int SWS_AREA + cdef int SWS_BICUBLIN + cdef int SWS_GAUSS + cdef int SWS_SINC + cdef int SWS_LANCZOS + cdef int SWS_SPLINE + cdef int SWS_CS_ITU709 + cdef int SWS_CS_FCC + cdef int SWS_CS_ITU601 + cdef int SWS_CS_ITU624 + cdef int SWS_CS_SMPTE170M + cdef int SWS_CS_SMPTE240M + cdef int SWS_CS_DEFAULT - cdef lib.SwsContext *ptr + cdef int sws_scale( + SwsContext *ctx, + const uint8_t *const *src_slice, + const int *src_stride, + int src_slice_y, + int src_slice_h, + unsigned char *const *dst_slice, + const int *dst_stride, + ) + cdef void sws_freeContext(SwsContext *ctx) + cdef SwsContext *sws_getCachedContext( + SwsContext *context, + int src_width, + int src_height, + lib.AVPixelFormat src_format, + int dst_width, + int dst_height, + lib.AVPixelFormat dst_format, + int flags, + SwsFilter *src_filter, + SwsFilter *dst_filter, + double *param, + ) + cdef const int* sws_getCoefficients(int colorspace) + cdef int sws_getColorspaceDetails( + SwsContext *context, + int **inv_table, + int *srcRange, + int **table, + int *dstRange, + int *brightness, + int *contrast, + int *saturation + ) + cdef int sws_setColorspaceDetails( + SwsContext *context, + const int inv_table[4], + int srcRange, + const int table[4], + int dstRange, + int brightness, + int contrast, + int saturation + ) +cdef class VideoReformatter: + cdef SwsContext *ptr cdef _reformat(self, VideoFrame frame, int width, int height, lib.AVPixelFormat format, int src_colorspace, int dst_colorspace, int interpolation, diff --git a/av/video/reformatter.py b/av/video/reformatter.py index 8005b7ed7..b72162ede 100644 --- a/av/video/reformatter.py +++ b/av/video/reformatter.py @@ -1,41 +1,40 @@ from enum import IntEnum import cython -import cython.cimports.libav as lib from cython.cimports.av.error import err_check from cython.cimports.av.video.format import VideoFormat from cython.cimports.av.video.frame import alloc_video_frame class Interpolation(IntEnum): - FAST_BILINEAR: "Fast bilinear" = lib.SWS_FAST_BILINEAR - BILINEAR: "Bilinear" = lib.SWS_BILINEAR - BICUBIC: "Bicubic" = lib.SWS_BICUBIC - X: "Experimental" = lib.SWS_X - POINT: "Nearest neighbor / point" = lib.SWS_POINT - AREA: "Area averaging" = lib.SWS_AREA - BICUBLIN: "Luma bicubic / chroma bilinear" = lib.SWS_BICUBLIN - GAUSS: "Gaussian" = lib.SWS_GAUSS - SINC: "Sinc" = lib.SWS_SINC - LANCZOS: "Bicubic spline" = lib.SWS_LANCZOS + FAST_BILINEAR: "Fast bilinear" = SWS_FAST_BILINEAR + BILINEAR: "Bilinear" = SWS_BILINEAR + BICUBIC: "Bicubic" = SWS_BICUBIC + X: "Experimental" = SWS_X + POINT: "Nearest neighbor / point" = SWS_POINT + AREA: "Area averaging" = SWS_AREA + BICUBLIN: "Luma bicubic / chroma bilinear" = SWS_BICUBLIN + GAUSS: "Gaussian" = SWS_GAUSS + SINC: "Sinc" = SWS_SINC + LANCZOS: "Bicubic spline" = SWS_LANCZOS class Colorspace(IntEnum): - ITU709 = lib.SWS_CS_ITU709 - FCC = lib.SWS_CS_FCC - ITU601 = lib.SWS_CS_ITU601 - ITU624 = lib.SWS_CS_ITU624 - SMPTE170M = lib.SWS_CS_SMPTE170M - SMPTE240M = lib.SWS_CS_SMPTE240M - DEFAULT = lib.SWS_CS_DEFAULT + ITU709 = SWS_CS_ITU709 + FCC = SWS_CS_FCC + ITU601 = SWS_CS_ITU601 + ITU624 = SWS_CS_ITU624 + SMPTE170M = SWS_CS_SMPTE170M + SMPTE240M = SWS_CS_SMPTE240M + DEFAULT = SWS_CS_DEFAULT # Lowercase for b/c. - itu709 = lib.SWS_CS_ITU709 - fcc = lib.SWS_CS_FCC - itu601 = lib.SWS_CS_ITU601 - itu624 = lib.SWS_CS_ITU624 - smpte170m = lib.SWS_CS_SMPTE170M - smpte240m = lib.SWS_CS_SMPTE240M - default = lib.SWS_CS_DEFAULT + itu709 = SWS_CS_ITU709 + fcc = SWS_CS_FCC + itu601 = SWS_CS_ITU601 + itu624 = SWS_CS_ITU624 + smpte170m = SWS_CS_SMPTE170M + smpte240m = SWS_CS_SMPTE240M + default = SWS_CS_DEFAULT class ColorRange(IntEnum): @@ -65,10 +64,10 @@ def _resolve_enum_value(value, enum_class, default): _SWS_CS_TO_AVCOL_SPC = cython.declare( dict, { - lib.SWS_CS_ITU709: lib.AVCOL_SPC_BT709, - lib.SWS_CS_FCC: lib.AVCOL_SPC_FCC, - lib.SWS_CS_ITU601: lib.AVCOL_SPC_SMPTE170M, - lib.SWS_CS_SMPTE240M: lib.AVCOL_SPC_SMPTE240M, + SWS_CS_ITU709: lib.AVCOL_SPC_BT709, + SWS_CS_FCC: lib.AVCOL_SPC_FCC, + SWS_CS_ITU601: lib.AVCOL_SPC_SMPTE170M, + SWS_CS_SMPTE240M: lib.AVCOL_SPC_SMPTE240M, }, ) @@ -84,7 +83,7 @@ class VideoReformatter: def __dealloc__(self): with cython.nogil: - lib.sws_freeContext(self.ptr) + sws_freeContext(self.ptr) def reformat( self, @@ -212,7 +211,7 @@ def _reformat( return frame with cython.nogil: - self.ptr = lib.sws_getCachedContext( + self.ptr = sws_getCachedContext( self.ptr, frame.ptr.width, frame.ptr.height, @@ -239,7 +238,7 @@ def _reformat( if src_colorspace != dst_colorspace or src_color_range != dst_color_range: with cython.nogil: - ret = lib.sws_getColorspaceDetails( + ret = sws_getColorspaceDetails( self.ptr, cython.address(inv_tbl), cython.address(src_colorspace_range), @@ -254,16 +253,14 @@ def _reformat( with cython.nogil: # Grab the coefficients for the requested transforms. # The inv_table brings us to linear, and `tbl` to the new space. - if src_colorspace != lib.SWS_CS_DEFAULT: + if src_colorspace != SWS_CS_DEFAULT: inv_tbl = cython.cast( - cython.p_int, lib.sws_getCoefficients(src_colorspace) - ) - if dst_colorspace != lib.SWS_CS_DEFAULT: - tbl = cython.cast( - cython.p_int, lib.sws_getCoefficients(dst_colorspace) + cython.p_int, sws_getCoefficients(src_colorspace) ) + if dst_colorspace != SWS_CS_DEFAULT: + tbl = cython.cast(cython.p_int, sws_getCoefficients(dst_colorspace)) - ret = lib.sws_setColorspaceDetails( + ret = sws_setColorspaceDetails( self.ptr, inv_tbl, src_color_range, @@ -290,7 +287,7 @@ def _reformat( ) with cython.nogil: - lib.sws_scale( + sws_scale( self.ptr, cython.cast("const unsigned char *const *", frame.ptr.data), cython.cast("const int *", frame.ptr.linesize), diff --git a/include/libavcodec/avcodec.pxd b/include/avcodec.pxd similarity index 91% rename from include/libavcodec/avcodec.pxd rename to include/avcodec.pxd index 9b7304d05..09c7dabf0 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/avcodec.pxd @@ -1,19 +1,6 @@ -from libc.stdint cimport int64_t, uint8_t, uint16_t, uint32_t +from libc.stdint cimport int64_t, uint8_t, uint16_t, uint32_t, uint64_t - -cdef extern from "libavcodec/packet.h" nogil: - const AVPacketSideData *av_packet_side_data_get( - const AVPacketSideData *sd, int nb_sd, AVPacketSideDataType type - ) - uint8_t* av_packet_get_side_data( - const AVPacket *pkt, AVPacketSideDataType type, size_t *size - ) - int av_packet_add_side_data( - AVPacket *pkt, AVPacketSideDataType type, uint8_t *data, size_t size - ) - const char *av_packet_side_data_name(AVPacketSideDataType type) - -cdef extern from "libavutil/channel_layout.h": +cdef extern from "libavutil/channel_layout.h" nogil: ctypedef enum AVChannelOrder: pass ctypedef enum AVChannel: @@ -37,7 +24,6 @@ cdef extern from "libavutil/channel_layout.h": int av_channel_description(char *buf, size_t buf_size, AVChannel channel_id) AVChannel av_channel_layout_channel_from_index(AVChannelLayout *channel_layout, unsigned int idx) - cdef extern from "libavcodec/avcodec.h" nogil: cdef set pyav_get_available_codecs() cdef int avcodec_version() @@ -54,7 +40,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef size_t AV_INPUT_BUFFER_PADDING_SIZE cdef int64_t AV_NOPTS_VALUE - # AVCodecDescriptor.props cdef enum: AV_CODEC_PROP_INTRA_ONLY AV_CODEC_PROP_LOSSY @@ -63,7 +48,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: AV_CODEC_PROP_BITMAP_SUB AV_CODEC_PROP_TEXT_SUB - # AVCodec.capabilities cdef enum: AV_CODEC_CAP_DRAW_HORIZ_BAND AV_CODEC_CAP_DR1 @@ -222,6 +206,20 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVCodecDescriptor* avcodec_descriptor_get(AVCodecID) + cdef enum: + AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX + AV_CODEC_HW_CONFIG_METHOD_HW_FRAMES_CTX + AV_CODEC_HW_CONFIG_METHOD_INTERNAL + AV_CODEC_HW_CONFIG_METHOD_AD_HOC + + cdef struct AVCodecHWConfig: + AVPixelFormat pix_fmt + int methods + AVHWDeviceType device_type + cdef const AVCodecHWConfig* avcodec_get_hw_config(const AVCodec *codec, int index) + cdef struct AVHWAccel: + pass + cdef struct AVCodecContext: AVClass *av_class @@ -474,3 +472,37 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef int avcodec_parameters_to_context( AVCodecContext *codec, const AVCodecParameters *par ) + + +cdef extern from "libavcodec/bsf.h" nogil: + cdef struct AVBitStreamFilter: + const char *name + AVCodecID *codec_ids + + cdef struct AVCodecParameters: + pass + + cdef struct AVBSFContext: + const AVBitStreamFilter *filter + const AVCodecParameters *par_in + const AVCodecParameters *par_out + + cdef int av_bsf_list_parse_str(const char *str, AVBSFContext **bsf) + cdef int av_bsf_init(AVBSFContext *ctx) + cdef void av_bsf_free(AVBSFContext **ctx) + cdef AVBitStreamFilter* av_bsf_iterate(void **opaque) + cdef int av_bsf_send_packet(AVBSFContext *ctx, AVPacket *pkt) + cdef int av_bsf_receive_packet(AVBSFContext *ctx, AVPacket *pkt) + cdef void av_bsf_flush(AVBSFContext *ctx) + +cdef extern from "libavcodec/packet.h" nogil: + const AVPacketSideData *av_packet_side_data_get( + const AVPacketSideData *sd, int nb_sd, AVPacketSideDataType type + ) + uint8_t* av_packet_get_side_data( + const AVPacket *pkt, AVPacketSideDataType type, size_t *size + ) + int av_packet_add_side_data( + AVPacket *pkt, AVPacketSideDataType type, uint8_t *data, size_t size + ) + const char *av_packet_side_data_name(AVPacketSideDataType type) diff --git a/include/libavfilter/avfilter.pxd b/include/avfilter.pxd similarity index 58% rename from include/libavfilter/avfilter.pxd rename to include/avfilter.pxd index db67507b4..5eb11ecc2 100644 --- a/include/libavfilter/avfilter.pxd +++ b/include/avfilter.pxd @@ -22,8 +22,6 @@ cdef extern from "libavfilter/avfilter.h" nogil: cdef AVFilter* avfilter_get_by_name(const char *name) cdef const AVFilter* av_filter_iterate(void **opaque) - cdef struct AVFilterLink # Defined later. - cdef struct AVFilterContext: AVClass *av_class AVFilter *filter @@ -48,7 +46,6 @@ cdef extern from "libavfilter/avfilter.h" nogil: AVFilterPad *srcpad AVFilterContext *dst AVFilterPad *dstpad - AVMediaType Type int w int h @@ -58,18 +55,46 @@ cdef extern from "libavfilter/avfilter.h" nogil: int format AVRational time_base - # custom - cdef set pyav_get_available_filters() - - int avfilter_process_command(AVFilterContext *filter, - const char *cmd, - const char *arg, - char *res, - int res_len, - int flags) - - cdef int AVFILTER_CMD_FLAG_FAST + cdef struct AVFilterGraph: + int nb_filters + AVFilterContext **filters + cdef struct AVFilterInOut: + char *name + AVFilterContext *filter_ctx + int pad_idx + AVFilterInOut *next + + cdef AVFilterGraph* avfilter_graph_alloc() + cdef void avfilter_graph_free(AVFilterGraph **ptr) + cdef AVFilterContext* avfilter_graph_alloc_filter( + AVFilterGraph *graph, + const AVFilter *filter, + const char *name + ) + cdef int avfilter_graph_create_filter( + AVFilterContext **filt_ctx, + AVFilter *filt, + const char *name, + const char *args, + void *opaque, + AVFilterGraph *graph_ctx + ) + cdef int avfilter_link( + AVFilterContext *src, + unsigned int srcpad, + AVFilterContext *dst, + unsigned int dstpad + ) + cdef int avfilter_graph_config(AVFilterGraph *graph, void *logctx) + int avfilter_process_command( + AVFilterContext *filter, const char *cmd, const char *arg, char *res, + int res_len, int flags, + ) cdef extern from "libavfilter/buffersink.h" nogil: cdef void av_buffersink_set_frame_size(AVFilterContext *ctx, unsigned frame_size) + int av_buffersink_get_frame(AVFilterContext *ctx, AVFrame *frame) + +cdef extern from "libavfilter/buffersrc.h" nogil: + int av_buffersrc_write_frame(AVFilterContext *ctx, const AVFrame *frame) diff --git a/include/libavformat/avformat.pxd b/include/avformat.pxd similarity index 100% rename from include/libavformat/avformat.pxd rename to include/avformat.pxd diff --git a/include/libavutil/avutil.pxd b/include/avutil.pxd similarity index 100% rename from include/libavutil/avutil.pxd rename to include/avutil.pxd diff --git a/include/libav.pxd b/include/libav.pxd index a66626c3d..9157a8b33 100644 --- a/include/libav.pxd +++ b/include/libav.pxd @@ -1,19 +1,4 @@ -include "libavutil/avutil.pxd" -include "libavcodec/avcodec.pxd" -include "libavcodec/bsf.pxd" -include "libavcodec/hwaccel.pxd" - -include "libavdevice/avdevice.pxd" -include "libavformat/avformat.pxd" -include "libswresample/swresample.pxd" -include "libswscale/swscale.pxd" - -include "libavfilter/avfilter.pxd" -include "libavfilter/avfiltergraph.pxd" -include "libavfilter/buffersink.pxd" -include "libavfilter/buffersrc.pxd" - - -cdef extern from "stdio.h" nogil: - cdef int snprintf(char *output, int n, const char *format, ...) - cdef int vsnprintf(char *output, int n, const char *format, va_list args) +include "avutil.pxd" +include "avcodec.pxd" +include "avformat.pxd" +include "avfilter.pxd" diff --git a/include/libavcodec/bsf.pxd b/include/libavcodec/bsf.pxd deleted file mode 100644 index b4c41f7f4..000000000 --- a/include/libavcodec/bsf.pxd +++ /dev/null @@ -1,20 +0,0 @@ -cdef extern from "libavcodec/bsf.h" nogil: - cdef struct AVBitStreamFilter: - const char *name - AVCodecID *codec_ids - - cdef struct AVCodecParameters: - pass - - cdef struct AVBSFContext: - const AVBitStreamFilter *filter - const AVCodecParameters *par_in - const AVCodecParameters *par_out - - cdef int av_bsf_list_parse_str(const char *str, AVBSFContext **bsf) - cdef int av_bsf_init(AVBSFContext *ctx) - cdef void av_bsf_free(AVBSFContext **ctx) - cdef AVBitStreamFilter* av_bsf_iterate(void **opaque) - cdef int av_bsf_send_packet(AVBSFContext *ctx, AVPacket *pkt) - cdef int av_bsf_receive_packet(AVBSFContext *ctx, AVPacket *pkt) - cdef void av_bsf_flush(AVBSFContext *ctx) diff --git a/include/libavcodec/hwaccel.pxd b/include/libavcodec/hwaccel.pxd deleted file mode 100644 index 2023efd64..000000000 --- a/include/libavcodec/hwaccel.pxd +++ /dev/null @@ -1,13 +0,0 @@ -cdef extern from "libavcodec/avcodec.h" nogil: - cdef enum: - AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX, - AV_CODEC_HW_CONFIG_METHOD_HW_FRAMES_CTX, - AV_CODEC_HW_CONFIG_METHOD_INTERNAL, - AV_CODEC_HW_CONFIG_METHOD_AD_HOC, - cdef struct AVCodecHWConfig: - AVPixelFormat pix_fmt - int methods - AVHWDeviceType device_type - cdef const AVCodecHWConfig* avcodec_get_hw_config(const AVCodec *codec, int index) - cdef struct AVHWAccel: - pass diff --git a/include/libavdevice/avdevice.pxd b/include/libavdevice/avdevice.pxd deleted file mode 100644 index 31c654b10..000000000 --- a/include/libavdevice/avdevice.pxd +++ /dev/null @@ -1,5 +0,0 @@ -cdef extern from "libavdevice/avdevice.h" nogil: - cdef int avdevice_version() - cdef char* avdevice_configuration() - cdef char* avdevice_license() - void avdevice_register_all() diff --git a/include/libavfilter/avfiltergraph.pxd b/include/libavfilter/avfiltergraph.pxd deleted file mode 100644 index f27dcbbcc..000000000 --- a/include/libavfilter/avfiltergraph.pxd +++ /dev/null @@ -1,37 +0,0 @@ -cdef extern from "libavfilter/avfilter.h" nogil: - cdef struct AVFilterGraph: - int nb_filters - AVFilterContext **filters - - cdef struct AVFilterInOut: - char *name - AVFilterContext *filter_ctx - int pad_idx - AVFilterInOut *next - - cdef AVFilterGraph* avfilter_graph_alloc() - cdef void avfilter_graph_free(AVFilterGraph **ptr) - - cdef AVFilterContext* avfilter_graph_alloc_filter( - AVFilterGraph *graph, - const AVFilter *filter, - const char *name - ) - - cdef int avfilter_graph_create_filter( - AVFilterContext **filt_ctx, - AVFilter *filt, - const char *name, - const char *args, - void *opaque, - AVFilterGraph *graph_ctx - ) - - cdef int avfilter_link( - AVFilterContext *src, - unsigned int srcpad, - AVFilterContext *dst, - unsigned int dstpad - ) - - cdef int avfilter_graph_config(AVFilterGraph *graph, void *logctx) diff --git a/include/libavfilter/buffersink.pxd b/include/libavfilter/buffersink.pxd deleted file mode 100644 index ef843cb7a..000000000 --- a/include/libavfilter/buffersink.pxd +++ /dev/null @@ -1,2 +0,0 @@ -cdef extern from "libavfilter/buffersink.h" nogil: - int av_buffersink_get_frame(AVFilterContext *ctx, AVFrame *frame) diff --git a/include/libavfilter/buffersrc.pxd b/include/libavfilter/buffersrc.pxd deleted file mode 100644 index 1e0f70771..000000000 --- a/include/libavfilter/buffersrc.pxd +++ /dev/null @@ -1,2 +0,0 @@ -cdef extern from "libavfilter/buffersrc.h" nogil: - int av_buffersrc_write_frame(AVFilterContext *ctx, const AVFrame *frame) diff --git a/include/libswresample/swresample.pxd b/include/libswresample/swresample.pxd deleted file mode 100644 index 8e6f6649b..000000000 --- a/include/libswresample/swresample.pxd +++ /dev/null @@ -1,4 +0,0 @@ -cdef extern from "libswresample/swresample.h" nogil: - cdef int swresample_version() - cdef char* swresample_configuration() - cdef char* swresample_license() diff --git a/include/libswscale/swscale.pxd b/include/libswscale/swscale.pxd deleted file mode 100644 index ffc0eb6b0..000000000 --- a/include/libswscale/swscale.pxd +++ /dev/null @@ -1,78 +0,0 @@ -from libc.stdint cimport uint8_t - -cdef extern from "libswscale/swscale.h" nogil: - cdef int swscale_version() - cdef char* swscale_configuration() - cdef char* swscale_license() - - # See: http://ffmpeg.org/doxygen/trunk/structSwsContext.html - cdef struct SwsContext: - pass - - # See: http://ffmpeg.org/doxygen/trunk/structSwsFilter.html - cdef struct SwsFilter: - pass - - cdef int SWS_FAST_BILINEAR - cdef int SWS_BILINEAR - cdef int SWS_BICUBIC - cdef int SWS_X - cdef int SWS_POINT - cdef int SWS_AREA - cdef int SWS_BICUBLIN - cdef int SWS_GAUSS - cdef int SWS_SINC - cdef int SWS_LANCZOS - cdef int SWS_SPLINE - cdef int SWS_CS_ITU709 - cdef int SWS_CS_FCC - cdef int SWS_CS_ITU601 - cdef int SWS_CS_ITU624 - cdef int SWS_CS_SMPTE170M - cdef int SWS_CS_SMPTE240M - cdef int SWS_CS_DEFAULT - - cdef int sws_scale( - SwsContext *ctx, - const uint8_t *const *src_slice, - const int *src_stride, - int src_slice_y, - int src_slice_h, - unsigned char *const *dst_slice, - const int *dst_stride, - ) - cdef void sws_freeContext(SwsContext *ctx) - cdef SwsContext *sws_getCachedContext( - SwsContext *context, - int src_width, - int src_height, - AVPixelFormat src_format, - int dst_width, - int dst_height, - AVPixelFormat dst_format, - int flags, - SwsFilter *src_filter, - SwsFilter *dst_filter, - double *param, - ) - cdef const int* sws_getCoefficients(int colorspace) - cdef int sws_getColorspaceDetails( - SwsContext *context, - int **inv_table, - int *srcRange, - int **table, - int *dstRange, - int *brightness, - int *contrast, - int *saturation - ) - cdef int sws_setColorspaceDetails( - SwsContext *context, - const int inv_table[4], - int srcRange, - const int table[4], - int dstRange, - int brightness, - int contrast, - int saturation - ) From f8dddb9e98d50c974e9774a9cde2aa72fd6aef7a Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 10 Feb 2026 10:04:28 -0500 Subject: [PATCH 684/846] Fix #2161 null check for get_media_type_str --- av/codec/codec.py | 3 ++- av/codec/codec.pyi | 4 +++- av/stream.py | 3 ++- av/stream.pyi | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/av/codec/codec.py b/av/codec/codec.py index 82f5a4bda..f70717905 100644 --- a/av/codec/codec.py +++ b/av/codec/codec.py @@ -172,7 +172,8 @@ def type(self): E.g: ``'audio'``, ``'video'``, ``'subtitle'``. """ - return lib.av_get_media_type_string(self.ptr.type) + media_type = lib.av_get_media_type_string(self.ptr.type) + return "unknown" if media_type == cython.NULL else media_type @property def id(self): diff --git a/av/codec/codec.pyi b/av/codec/codec.pyi index 49b0bc43c..221872c33 100644 --- a/av/codec/codec.pyi +++ b/av/codec/codec.pyi @@ -61,7 +61,9 @@ class Codec: @property def long_name(self) -> str: ... @property - def type(self) -> Literal["video", "audio", "data", "subtitle", "attachment"]: ... + def type( + self, + ) -> Literal["video", "audio", "data", "subtitle", "attachment", "unknown"]: ... @property def id(self) -> int: ... frame_rates: list[Fraction] | None diff --git a/av/stream.py b/av/stream.py index f4dcb38d7..f8ce178b2 100644 --- a/av/stream.py +++ b/av/stream.py @@ -275,7 +275,8 @@ def type(self): :type: Literal["audio", "video", "subtitle", "data", "attachment"] """ - return lib.av_get_media_type_string(self.ptr.codecpar.codec_type) + media_type = lib.av_get_media_type_string(self.ptr.codecpar.codec_type) + return "unknown" if media_type == cython.NULL else media_type @cython.cclass diff --git a/av/stream.pyi b/av/stream.pyi index 3c4e55f6a..6f4459020 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -48,7 +48,7 @@ class Stream: disposition: Disposition frames: int language: str | None - type: Literal["video", "audio", "data", "subtitle", "attachment"] + type: Literal["video", "audio", "data", "subtitle", "attachment", "unknown"] # From context codec_tag: str From 4b39d2bd27b759bdb19eb9fda54245d48e6adb56 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 10 Feb 2026 10:14:58 -0500 Subject: [PATCH 685/846] Test with unknown stream --- tests/test_streams.py | 68 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/tests/test_streams.py b/tests/test_streams.py index 88279cb63..a9f5e5bdc 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -1,4 +1,6 @@ +import io import os +import struct import pytest @@ -8,6 +10,63 @@ from .common import fate_suite +def _crc32_mpeg(data: bytes) -> int: + crc = 0xFFFFFFFF + for byte in data: + crc ^= byte << 24 + for _ in range(8): + if crc & 0x80000000: + crc = (crc << 1) ^ 0x04C11DB7 + else: + crc <<= 1 + crc &= 0xFFFFFFFF + return crc + + +def _make_ts_packet(pid: int, payload: bytes, pusi: bool = False, cc: int = 0) -> bytes: + pid_hi = (pid >> 8) & 0x1F + pid_lo = pid & 0xFF + if pusi: + pid_hi |= 0x40 + header = bytes([0x47, pid_hi, pid_lo, 0x10 | (cc & 0x0F)]) + pkt = header + payload + return (pkt + b"\xff" * (188 - len(pkt)))[:188] + + +def _make_unknown_stream_ts() -> bytes: + """Build a minimal MPEG-TS byte string whose only stream has AVMEDIA_TYPE_UNKNOWN. + + Uses stream_type=0x82 in the PMT, which FFmpeg does not map to any known + media type, resulting in ``stream.type == "unknown"``. + """ + pat_data = bytes( + [0x00, 0xB0, 0x0D, 0x00, 0x01, 0xC1, 0x00, 0x00, 0x00, 0x01, 0xE1, 0x00] + ) + pat_data += struct.pack(">I", _crc32_mpeg(pat_data)) + pat_payload = bytes([0x00]) + pat_data + + # fmt: off + pmt_data = bytes([0x02, 0xB0, 0x12, 0x00, 0x01, 0xC1, 0x00, 0x00, + 0xE1, 0x02, 0xF0, 0x00, 0x82, 0xE1, 0x02, 0xF0, 0x00, + ]) + # fmt: on + pmt_data += struct.pack(">I", _crc32_mpeg(pmt_data)) + pmt_payload = bytes([0x00]) + pmt_data + + pes_header = bytes([0x00, 0x00, 0x01, 0xBD, 0x00, 0x0A, 0x80, 0x00, 0x00]) + pes_data = pes_header + b"\xaa" * (184 - len(pes_header)) + + packets: list[bytes] = [] + for i in range(2): + packets.append(_make_ts_packet(0x0000, pat_payload, pusi=True, cc=i)) + for i in range(2): + packets.append(_make_ts_packet(0x0100, pmt_payload, pusi=True, cc=i)) + for i in range(200): + packets.append(_make_ts_packet(0x0102, pes_data, pusi=(i % 10 == 0), cc=i % 16)) + + return b"".join(packets) + + class TestStreams: @pytest.fixture(autouse=True) def cleanup(self): @@ -300,3 +359,12 @@ def test_attachment_stream(self) -> None: assert att.name == "attachment.txt" assert att.mimetype == "text/plain" assert att.data == b"hello\n" + + def test_unknown_stream_type(self) -> None: + ts_data = _make_unknown_stream_ts() + + with av.open(io.BytesIO(ts_data), format="mpegts") as container: + assert len(container.streams) == 1 + stream = container.streams[0] + assert stream.type == "unknown" + assert type(stream) is av.stream.Stream From 80da805da2ec36d53f3acefdabc29eafa15c9bdd Mon Sep 17 00:00:00 2001 From: Benedikt Lorch Date: Wed, 18 Feb 2026 09:23:34 +0100 Subject: [PATCH 686/846] Switch height and width --- examples/numpy/generate_video.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/numpy/generate_video.py b/examples/numpy/generate_video.py index 8b490ea1f..27112d19f 100644 --- a/examples/numpy/generate_video.py +++ b/examples/numpy/generate_video.py @@ -14,7 +14,7 @@ stream.pix_fmt = "yuv420p" for frame_i in range(total_frames): - img = np.empty((480, 320, 3)) + img = np.empty((320, 480, 3)) img[:, :, 0] = 0.5 + 0.5 * np.sin(2 * np.pi * (0 / 3 + frame_i / total_frames)) img[:, :, 1] = 0.5 + 0.5 * np.sin(2 * np.pi * (1 / 3 + frame_i / total_frames)) img[:, :, 2] = 0.5 + 0.5 * np.sin(2 * np.pi * (2 / 3 + frame_i / total_frames)) From afae8b647ff69515494f24ec61bcbb8a4def6415 Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Thu, 19 Feb 2026 05:26:48 +0000 Subject: [PATCH 687/846] Reduce overhead of `to_ndarray` (#2165) Move pix-format dtype dict out of function call. Prevent some unneeded NumPy copies --- av/video/frame.py | 269 ++++++++++++++++++++++------------------------ 1 file changed, 128 insertions(+), 141 deletions(-) diff --git a/av/video/frame.py b/av/video/frame.py index 1fa30f92f..29d5e5460 100644 --- a/av/video/frame.py +++ b/av/video/frame.py @@ -264,6 +264,90 @@ def _numpy_avbuffer_free( "yuyv422", } +# Mapping from format name to (itemsize, dtype) for formats where planes +# are simply concatenated into shape (height, width, channels). +_np_pix_fmt_dtypes: dict[str, tuple[cython.uint, str]] = { + "abgr": (4, "uint8"), + "argb": (4, "uint8"), + "bayer_bggr8": (1, "uint8"), + "bayer_gbrg8": (1, "uint8"), + "bayer_grbg8": (1, "uint8"), + "bayer_rggb8": (1, "uint8"), + "bayer_bggr16le": (2, "uint16"), + "bayer_bggr16be": (2, "uint16"), + "bayer_gbrg16le": (2, "uint16"), + "bayer_gbrg16be": (2, "uint16"), + "bayer_grbg16le": (2, "uint16"), + "bayer_grbg16be": (2, "uint16"), + "bayer_rggb16le": (2, "uint16"), + "bayer_rggb16be": (2, "uint16"), + "bgr24": (3, "uint8"), + "bgr48be": (6, "uint16"), + "bgr48le": (6, "uint16"), + "bgr8": (1, "uint8"), + "bgra": (4, "uint8"), + "bgra64be": (8, "uint16"), + "bgra64le": (8, "uint16"), + "gbrap": (1, "uint8"), + "gbrap10be": (2, "uint16"), + "gbrap10le": (2, "uint16"), + "gbrap12be": (2, "uint16"), + "gbrap12le": (2, "uint16"), + "gbrap14be": (2, "uint16"), + "gbrap14le": (2, "uint16"), + "gbrap16be": (2, "uint16"), + "gbrap16le": (2, "uint16"), + "gbrapf32be": (4, "float32"), + "gbrapf32le": (4, "float32"), + "gbrp": (1, "uint8"), + "gbrp10be": (2, "uint16"), + "gbrp10le": (2, "uint16"), + "gbrp12be": (2, "uint16"), + "gbrp12le": (2, "uint16"), + "gbrp14be": (2, "uint16"), + "gbrp14le": (2, "uint16"), + "gbrp16be": (2, "uint16"), + "gbrp16le": (2, "uint16"), + "gbrp9be": (2, "uint16"), + "gbrp9le": (2, "uint16"), + "gbrpf32be": (4, "float32"), + "gbrpf32le": (4, "float32"), + "gray": (1, "uint8"), + "gray10be": (2, "uint16"), + "gray10le": (2, "uint16"), + "gray12be": (2, "uint16"), + "gray12le": (2, "uint16"), + "gray14be": (2, "uint16"), + "gray14le": (2, "uint16"), + "gray16be": (2, "uint16"), + "gray16le": (2, "uint16"), + "gray8": (1, "uint8"), + "gray9be": (2, "uint16"), + "gray9le": (2, "uint16"), + "grayf32be": (4, "float32"), + "grayf32le": (4, "float32"), + "rgb24": (3, "uint8"), + "rgb48be": (6, "uint16"), + "rgb48le": (6, "uint16"), + "rgb8": (1, "uint8"), + "rgba": (4, "uint8"), + "rgba64be": (8, "uint16"), + "rgba64le": (8, "uint16"), + "rgbaf16be": (8, "float16"), + "rgbaf16le": (8, "float16"), + "rgbaf32be": (16, "float32"), + "rgbaf32le": (16, "float32"), + "rgbf32be": (12, "float32"), + "rgbf32le": (12, "float32"), + "yuv444p": (1, "uint8"), + "yuv444p16be": (2, "uint16"), + "yuv444p16le": (2, "uint16"), + "yuva444p16be": (2, "uint16"), + "yuva444p16le": (2, "uint16"), + "yuvj444p": (1, "uint8"), + "yuyv422": (2, "uint8"), +} + @cython.cfunc def alloc_video_frame() -> VideoFrame: @@ -354,9 +438,10 @@ def useful_array( total_line_size: cython.size_t = abs(plane.line_size) useful_line_size: cython.size_t = plane.width * bytes_per_pixel + if total_line_size == useful_line_size: + return np.frombuffer(plane, dtype=dtype) arr = np.frombuffer(plane, np.uint8) - if total_line_size != useful_line_size: - arr = arr.reshape(-1, total_line_size)[:, 0:useful_line_size].reshape(-1) + arr = arr.reshape(-1, total_line_size)[:, 0:useful_line_size].reshape(-1) return arr.view(np.dtype(dtype)) @@ -613,161 +698,66 @@ def to_ndarray(self, channel_last=False, **kwargs): .. note:: For ``gbrp`` formats, channels are flipped to RGB order. """ - kwargs2 = dict(kwargs) - if self.ptr.hw_frames_ctx and "format" not in kwargs2: + if self.ptr.hw_frames_ctx and "format" not in kwargs: frames_ctx: cython.pointer[lib.AVHWFramesContext] = cython.cast( cython.pointer[lib.AVHWFramesContext], self.ptr.hw_frames_ctx.data ) - kwargs2["format"] = get_video_format( + kwargs = dict(kwargs) + kwargs["format"] = get_video_format( frames_ctx.sw_format, self.ptr.width, self.ptr.height ).name - frame: VideoFrame = self.reformat(**kwargs2) + frame: VideoFrame = self.reformat(**kwargs) if frame.ptr.hw_frames_ctx: raise ValueError("Cannot convert a hardware frame to numpy directly.") import numpy as np # check size - if frame.format.name in { - "yuv420p", - "yuvj420p", - "yuyv422", - "yuv422p10le", - "yuv422p", - }: - assert frame.width % 2 == 0, ( - "the width has to be even for this pixel format" - ) - assert frame.height % 2 == 0, ( - "the height has to be even for this pixel format" - ) + format_name = frame.format.name + height, width = frame.ptr.height, frame.ptr.width + planes: tuple[VideoPlane, ...] = frame.planes + if format_name in {"yuv420p", "yuvj420p", "yuyv422", "yuv422p10le", "yuv422p"}: + assert width % 2 == 0, "the width has to be even for this pixel format" + assert height % 2 == 0, "the height has to be even for this pixel format" # cases planes are simply concatenated in shape (height, width, channels) - itemsize, dtype = { - "abgr": (4, "uint8"), - "argb": (4, "uint8"), - "bayer_bggr8": (1, "uint8"), - "bayer_gbrg8": (1, "uint8"), - "bayer_grbg8": (1, "uint8"), - "bayer_rggb8": (1, "uint8"), - "bayer_bggr16le": (2, "uint16"), - "bayer_bggr16be": (2, "uint16"), - "bayer_gbrg16le": (2, "uint16"), - "bayer_gbrg16be": (2, "uint16"), - "bayer_grbg16le": (2, "uint16"), - "bayer_grbg16be": (2, "uint16"), - "bayer_rggb16le": (2, "uint16"), - "bayer_rggb16be": (2, "uint16"), - "bgr24": (3, "uint8"), - "bgr48be": (6, "uint16"), - "bgr48le": (6, "uint16"), - "bgr8": (1, "uint8"), - "bgra": (4, "uint8"), - "bgra64be": (8, "uint16"), - "bgra64le": (8, "uint16"), - "gbrap": (1, "uint8"), - "gbrap10be": (2, "uint16"), - "gbrap10le": (2, "uint16"), - "gbrap12be": (2, "uint16"), - "gbrap12le": (2, "uint16"), - "gbrap14be": (2, "uint16"), - "gbrap14le": (2, "uint16"), - "gbrap16be": (2, "uint16"), - "gbrap16le": (2, "uint16"), - "gbrapf32be": (4, "float32"), - "gbrapf32le": (4, "float32"), - "gbrp": (1, "uint8"), - "gbrp10be": (2, "uint16"), - "gbrp10le": (2, "uint16"), - "gbrp12be": (2, "uint16"), - "gbrp12le": (2, "uint16"), - "gbrp14be": (2, "uint16"), - "gbrp14le": (2, "uint16"), - "gbrp16be": (2, "uint16"), - "gbrp16le": (2, "uint16"), - "gbrp9be": (2, "uint16"), - "gbrp9le": (2, "uint16"), - "gbrpf32be": (4, "float32"), - "gbrpf32le": (4, "float32"), - "gray": (1, "uint8"), - "gray10be": (2, "uint16"), - "gray10le": (2, "uint16"), - "gray12be": (2, "uint16"), - "gray12le": (2, "uint16"), - "gray14be": (2, "uint16"), - "gray14le": (2, "uint16"), - "gray16be": (2, "uint16"), - "gray16le": (2, "uint16"), - "gray8": (1, "uint8"), - "gray9be": (2, "uint16"), - "gray9le": (2, "uint16"), - "grayf32be": (4, "float32"), - "grayf32le": (4, "float32"), - "rgb24": (3, "uint8"), - "rgb48be": (6, "uint16"), - "rgb48le": (6, "uint16"), - "rgb8": (1, "uint8"), - "rgba": (4, "uint8"), - "rgba64be": (8, "uint16"), - "rgba64le": (8, "uint16"), - "rgbaf16be": (8, "float16"), - "rgbaf16le": (8, "float16"), - "rgbaf32be": (16, "float32"), - "rgbaf32le": (16, "float32"), - "rgbf32be": (12, "float32"), - "rgbf32le": (12, "float32"), - "yuv444p": (1, "uint8"), - "yuv444p16be": (2, "uint16"), - "yuv444p16le": (2, "uint16"), - "yuva444p16be": (2, "uint16"), - "yuva444p16le": (2, "uint16"), - "yuvj444p": (1, "uint8"), - "yuyv422": (2, "uint8"), - }.get(frame.format.name, (None, None)) - if itemsize is not None: - layers = [ - useful_array(plan, itemsize, dtype).reshape( - frame.height, frame.width, -1 + if format_name in _np_pix_fmt_dtypes: + itemsize: cython.uint + itemsize, dtype = _np_pix_fmt_dtypes[format_name] + if len(planes) == 1: # shortcut, avoid memory copy + array = useful_array(planes[0], itemsize, dtype).reshape( + height, width, -1 ) - for plan in frame.planes - ] - if len(layers) == 1: # shortcut, avoid memory copy - array = layers[0] else: # general case - array = np.concatenate(layers, axis=2) - array = byteswap_array(array, frame.format.name.endswith("be")) + array = np.empty((height, width, len(planes)), dtype=dtype) + for i, plane in enumerate(planes): + array[:, :, i] = useful_array(plane, itemsize, dtype).reshape( + height, width + ) + array = byteswap_array(array, format_name.endswith("be")) if array.shape[2] == 1: # skip last channel for gray images return array.squeeze(2) - if frame.format.name.startswith("gbr"): # gbr -> rgb - buffer = array[:, :, 0].copy() - array[:, :, 0] = array[:, :, 2] - array[:, :, 2] = array[:, :, 1] - array[:, :, 1] = buffer - if not channel_last and frame.format.name in {"yuv444p", "yuvj444p"}: + if format_name.startswith("gbr"): # gbr -> rgb + array[:, :, :3] = array[:, :, [2, 0, 1]] + if not channel_last and format_name in {"yuv444p", "yuvj444p"}: array = np.moveaxis(array, 2, 0) return array # special cases - if frame.format.name in {"yuv420p", "yuvj420p", "yuv422p"}: + if format_name in {"yuv420p", "yuvj420p", "yuv422p"}: return np.hstack( [ - useful_array(frame.planes[0]), - useful_array(frame.planes[1]), - useful_array(frame.planes[2]), + useful_array(planes[0]), + useful_array(planes[1]), + useful_array(planes[2]), ] - ).reshape(-1, frame.width) - if frame.format.name == "yuv422p10le": + ).reshape(-1, width) + if format_name == "yuv422p10le": # Read planes as uint16 at their original width - y = useful_array(frame.planes[0], 2, "uint16").reshape( - frame.height, frame.width - ) - u = useful_array(frame.planes[1], 2, "uint16").reshape( - frame.height, frame.width // 2 - ) - v = useful_array(frame.planes[2], 2, "uint16").reshape( - frame.height, frame.width // 2 - ) + y = useful_array(planes[0], 2, "uint16").reshape(height, width) + u = useful_array(planes[1], 2, "uint16").reshape(height, width // 2) + v = useful_array(planes[2], 2, "uint16").reshape(height, width // 2) # Double the width of U and V by repeating each value u_full = np.repeat(u, 2, axis=1) @@ -775,25 +765,22 @@ def to_ndarray(self, channel_last=False, **kwargs): if channel_last: return np.stack([y, u_full, v_full], axis=2) return np.stack([y, u_full, v_full], axis=0) - if frame.format.name == "pal8": - image = useful_array(frame.planes[0]).reshape(frame.height, frame.width) + if format_name == "pal8": + image = useful_array(planes[0]).reshape(height, width) palette = ( - np.frombuffer(frame.planes[1], "i4") + np.frombuffer(planes[1], "i4") .astype(">i4") .reshape(-1, 1) .view(np.uint8) ) return image, palette - if frame.format.name == "nv12": + if format_name == "nv12": return np.hstack( - [ - useful_array(frame.planes[0]), - useful_array(frame.planes[1], 2), - ] - ).reshape(-1, frame.width) + [useful_array(planes[0]), useful_array(planes[1], 2)] + ).reshape(-1, width) raise ValueError( - f"Conversion to numpy array with format `{frame.format.name}` is not yet supported" + f"Conversion to numpy array with format `{format_name}` is not yet supported" ) def set_image(self, img): From eafc98f00fc5af2e2de6765f18222d3b6b577596 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 19 Feb 2026 00:29:39 -0500 Subject: [PATCH 688/846] Use cython.declare to make some vars not public --- av/audio/format.py | 6 +- av/audio/frame.py | 2 +- av/video/frame.py | 167 +++++++++++++++++++++++---------------------- 3 files changed, 90 insertions(+), 85 deletions(-) diff --git a/av/audio/format.py b/av/audio/format.py index 7d5de1ad9..97e1b59a2 100644 --- a/av/audio/format.py +++ b/av/audio/format.py @@ -2,8 +2,10 @@ import cython -container_format_postfix: str = "le" if sys.byteorder == "little" else "be" -_cinit_bypass_sentinel = object() +container_format_postfix = cython.declare( + str, "le" if sys.byteorder == "little" else "be" +) +_cinit_bypass_sentinel = cython.declare(object, object()) @cython.cfunc diff --git a/av/audio/frame.py b/av/audio/frame.py index cf3ab5934..0ff969475 100644 --- a/av/audio/frame.py +++ b/av/audio/frame.py @@ -5,7 +5,7 @@ from cython.cimports.av.error import err_check from cython.cimports.av.utils import check_ndarray -_cinit_bypass_sentinel = object() +_cinit_bypass_sentinel = cython.declare(object, object()) @cython.cfunc diff --git a/av/video/frame.py b/av/video/frame.py index 29d5e5460..f6ae147cc 100644 --- a/av/video/frame.py +++ b/av/video/frame.py @@ -174,7 +174,7 @@ def _numpy_avbuffer_free( Py_DECREF(cython.cast(object, opaque)) -_cinit_bypass_sentinel = object() +_cinit_bypass_sentinel = cython.declare(object, object()) # `pix_fmt`s supported by Frame.to_ndarray() and Frame.from_ndarray() supported_np_pix_fmts = { @@ -266,87 +266,90 @@ def _numpy_avbuffer_free( # Mapping from format name to (itemsize, dtype) for formats where planes # are simply concatenated into shape (height, width, channels). -_np_pix_fmt_dtypes: dict[str, tuple[cython.uint, str]] = { - "abgr": (4, "uint8"), - "argb": (4, "uint8"), - "bayer_bggr8": (1, "uint8"), - "bayer_gbrg8": (1, "uint8"), - "bayer_grbg8": (1, "uint8"), - "bayer_rggb8": (1, "uint8"), - "bayer_bggr16le": (2, "uint16"), - "bayer_bggr16be": (2, "uint16"), - "bayer_gbrg16le": (2, "uint16"), - "bayer_gbrg16be": (2, "uint16"), - "bayer_grbg16le": (2, "uint16"), - "bayer_grbg16be": (2, "uint16"), - "bayer_rggb16le": (2, "uint16"), - "bayer_rggb16be": (2, "uint16"), - "bgr24": (3, "uint8"), - "bgr48be": (6, "uint16"), - "bgr48le": (6, "uint16"), - "bgr8": (1, "uint8"), - "bgra": (4, "uint8"), - "bgra64be": (8, "uint16"), - "bgra64le": (8, "uint16"), - "gbrap": (1, "uint8"), - "gbrap10be": (2, "uint16"), - "gbrap10le": (2, "uint16"), - "gbrap12be": (2, "uint16"), - "gbrap12le": (2, "uint16"), - "gbrap14be": (2, "uint16"), - "gbrap14le": (2, "uint16"), - "gbrap16be": (2, "uint16"), - "gbrap16le": (2, "uint16"), - "gbrapf32be": (4, "float32"), - "gbrapf32le": (4, "float32"), - "gbrp": (1, "uint8"), - "gbrp10be": (2, "uint16"), - "gbrp10le": (2, "uint16"), - "gbrp12be": (2, "uint16"), - "gbrp12le": (2, "uint16"), - "gbrp14be": (2, "uint16"), - "gbrp14le": (2, "uint16"), - "gbrp16be": (2, "uint16"), - "gbrp16le": (2, "uint16"), - "gbrp9be": (2, "uint16"), - "gbrp9le": (2, "uint16"), - "gbrpf32be": (4, "float32"), - "gbrpf32le": (4, "float32"), - "gray": (1, "uint8"), - "gray10be": (2, "uint16"), - "gray10le": (2, "uint16"), - "gray12be": (2, "uint16"), - "gray12le": (2, "uint16"), - "gray14be": (2, "uint16"), - "gray14le": (2, "uint16"), - "gray16be": (2, "uint16"), - "gray16le": (2, "uint16"), - "gray8": (1, "uint8"), - "gray9be": (2, "uint16"), - "gray9le": (2, "uint16"), - "grayf32be": (4, "float32"), - "grayf32le": (4, "float32"), - "rgb24": (3, "uint8"), - "rgb48be": (6, "uint16"), - "rgb48le": (6, "uint16"), - "rgb8": (1, "uint8"), - "rgba": (4, "uint8"), - "rgba64be": (8, "uint16"), - "rgba64le": (8, "uint16"), - "rgbaf16be": (8, "float16"), - "rgbaf16le": (8, "float16"), - "rgbaf32be": (16, "float32"), - "rgbaf32le": (16, "float32"), - "rgbf32be": (12, "float32"), - "rgbf32le": (12, "float32"), - "yuv444p": (1, "uint8"), - "yuv444p16be": (2, "uint16"), - "yuv444p16le": (2, "uint16"), - "yuva444p16be": (2, "uint16"), - "yuva444p16le": (2, "uint16"), - "yuvj444p": (1, "uint8"), - "yuyv422": (2, "uint8"), -} +_np_pix_fmt_dtypes = cython.declare( + dict[str, tuple[cython.uint, str]], + { + "abgr": (4, "uint8"), + "argb": (4, "uint8"), + "bayer_bggr8": (1, "uint8"), + "bayer_gbrg8": (1, "uint8"), + "bayer_grbg8": (1, "uint8"), + "bayer_rggb8": (1, "uint8"), + "bayer_bggr16le": (2, "uint16"), + "bayer_bggr16be": (2, "uint16"), + "bayer_gbrg16le": (2, "uint16"), + "bayer_gbrg16be": (2, "uint16"), + "bayer_grbg16le": (2, "uint16"), + "bayer_grbg16be": (2, "uint16"), + "bayer_rggb16le": (2, "uint16"), + "bayer_rggb16be": (2, "uint16"), + "bgr24": (3, "uint8"), + "bgr48be": (6, "uint16"), + "bgr48le": (6, "uint16"), + "bgr8": (1, "uint8"), + "bgra": (4, "uint8"), + "bgra64be": (8, "uint16"), + "bgra64le": (8, "uint16"), + "gbrap": (1, "uint8"), + "gbrap10be": (2, "uint16"), + "gbrap10le": (2, "uint16"), + "gbrap12be": (2, "uint16"), + "gbrap12le": (2, "uint16"), + "gbrap14be": (2, "uint16"), + "gbrap14le": (2, "uint16"), + "gbrap16be": (2, "uint16"), + "gbrap16le": (2, "uint16"), + "gbrapf32be": (4, "float32"), + "gbrapf32le": (4, "float32"), + "gbrp": (1, "uint8"), + "gbrp10be": (2, "uint16"), + "gbrp10le": (2, "uint16"), + "gbrp12be": (2, "uint16"), + "gbrp12le": (2, "uint16"), + "gbrp14be": (2, "uint16"), + "gbrp14le": (2, "uint16"), + "gbrp16be": (2, "uint16"), + "gbrp16le": (2, "uint16"), + "gbrp9be": (2, "uint16"), + "gbrp9le": (2, "uint16"), + "gbrpf32be": (4, "float32"), + "gbrpf32le": (4, "float32"), + "gray": (1, "uint8"), + "gray10be": (2, "uint16"), + "gray10le": (2, "uint16"), + "gray12be": (2, "uint16"), + "gray12le": (2, "uint16"), + "gray14be": (2, "uint16"), + "gray14le": (2, "uint16"), + "gray16be": (2, "uint16"), + "gray16le": (2, "uint16"), + "gray8": (1, "uint8"), + "gray9be": (2, "uint16"), + "gray9le": (2, "uint16"), + "grayf32be": (4, "float32"), + "grayf32le": (4, "float32"), + "rgb24": (3, "uint8"), + "rgb48be": (6, "uint16"), + "rgb48le": (6, "uint16"), + "rgb8": (1, "uint8"), + "rgba": (4, "uint8"), + "rgba64be": (8, "uint16"), + "rgba64le": (8, "uint16"), + "rgbaf16be": (8, "float16"), + "rgbaf16le": (8, "float16"), + "rgbaf32be": (16, "float32"), + "rgbaf32le": (16, "float32"), + "rgbf32be": (12, "float32"), + "rgbf32le": (12, "float32"), + "yuv444p": (1, "uint8"), + "yuv444p16be": (2, "uint16"), + "yuv444p16le": (2, "uint16"), + "yuva444p16be": (2, "uint16"), + "yuva444p16le": (2, "uint16"), + "yuvj444p": (1, "uint8"), + "yuyv422": (2, "uint8"), + }, +) @cython.cfunc From 06298f666d2dae277603a4081118943dcf29889d Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Thu, 19 Feb 2026 13:36:39 +0000 Subject: [PATCH 689/846] Remove cython workaround from input container --- av/container/input.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/av/container/input.py b/av/container/input.py index 6a24d71b4..2412f7744 100644 --- a/av/container/input.py +++ b/av/container/input.py @@ -151,13 +151,7 @@ def demux(self, *args, **kwargs): """ self._assert_open() - # For whatever reason, Cython does not like us directly passing kwargs - # from one method to another. Without kwargs, it ends up passing a - # NULL reference, which segfaults. So we force it to do something with it. - # This is a bug in Cython; see https://github.com/cython/cython/issues/2166 - id(kwargs) - - streams = self.streams.get(*args, **kwargs) + streams: list[Stream] = self.streams.get(*args, **kwargs) include_stream: cython.pointer[cython.bint] = cython.cast( cython.pointer[cython.bint], malloc(self.ptr.nb_streams * cython.sizeof(bint)), @@ -225,7 +219,6 @@ def decode(self, *args, **kwargs): """ self._assert_open() - id(kwargs) # Avoid Cython bug; see demux(). for packet in self.demux(*args, **kwargs): for frame in packet.decode(): yield frame From 52c1f10ac7b3d4309289b2bd9e06772e69c47e12 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 27 Feb 2026 00:46:22 -0500 Subject: [PATCH 690/846] Use new config api --- av/codec/codec.py | 86 +++++++++++++++++++++++++----------------- av/container/output.py | 12 +++++- include/avcodec.pxd | 22 +++++++---- 3 files changed, 78 insertions(+), 42 deletions(-) diff --git a/av/codec/codec.py b/av/codec/codec.py index f70717905..930a9ecec 100644 --- a/av/codec/codec.py +++ b/av/codec/codec.py @@ -182,56 +182,74 @@ def id(self): @property def frame_rates(self): """A list of supported frame rates (:class:`fractions.Fraction`), or ``None``.""" - if not self.ptr.supported_framerates: + out: cython.p_void = cython.NULL + num: cython.int = 0 + lib.avcodec_get_supported_config( + cython.NULL, + self.ptr, + lib.AV_CODEC_CONFIG_FRAME_RATE, + 0, + cython.address(out), + cython.address(num), + ) + if not out: return - - ret: list = [] - i: cython.int = 0 - while self.ptr.supported_framerates[i].denum: - ret.append( - avrational_to_fraction(cython.address(self.ptr.supported_framerates[i])) - ) - i += 1 - return ret + rates = cython.cast(cython.pointer[lib.AVRational], out) + return [avrational_to_fraction(cython.address(rates[i])) for i in range(num)] @property def audio_rates(self): """A list of supported audio sample rates (``int``), or ``None``.""" - if not self.ptr.supported_samplerates: + out: cython.p_void = cython.NULL + num: cython.int = 0 + lib.avcodec_get_supported_config( + cython.NULL, + self.ptr, + lib.AV_CODEC_CONFIG_SAMPLE_RATE, + 0, + cython.address(out), + cython.address(num), + ) + if not out: return - - ret: list = [] - i: cython.int = 0 - while self.ptr.supported_samplerates[i]: - ret.append(self.ptr.supported_samplerates[i]) - i += 1 - return ret + rates = cython.cast(cython.pointer[cython.int], out) + return [rates[i] for i in range(num)] @property def video_formats(self): """A list of supported :class:`.VideoFormat`, or ``None``.""" - if not self.ptr.pix_fmts: + out: cython.p_void = cython.NULL + num: cython.int = 0 + lib.avcodec_get_supported_config( + cython.NULL, + self.ptr, + lib.AV_CODEC_CONFIG_PIX_FORMAT, + 0, + cython.address(out), + cython.address(num), + ) + if not out: return - - ret: list = [] - i: cython.int = 0 - while self.ptr.pix_fmts[i] != -1: - ret.append(get_video_format(self.ptr.pix_fmts[i], 0, 0)) - i += 1 - return ret + fmts = cython.cast(cython.pointer[lib.AVPixelFormat], out) + return [get_video_format(fmts[i], 0, 0) for i in range(num)] @property def audio_formats(self): """A list of supported :class:`.AudioFormat`, or ``None``.""" - if not self.ptr.sample_fmts: + out: cython.p_void = cython.NULL + num: cython.int = 0 + lib.avcodec_get_supported_config( + cython.NULL, + self.ptr, + lib.AV_CODEC_CONFIG_SAMPLE_FORMAT, + 0, + cython.address(out), + cython.address(num), + ) + if not out: return - - ret: list = [] - i: cython.int = 0 - while self.ptr.sample_fmts[i] != -1: - ret.append(get_audio_format(self.ptr.sample_fmts[i])) - i += 1 - return ret + fmts = cython.cast(cython.pointer[lib.AVSampleFormat], out) + return [get_audio_format(fmts[i]) for i in range(num)] @property def hardware_configs(self): diff --git a/av/container/output.py b/av/container/output.py index 1e02168cb..600d84720 100644 --- a/av/container/output.py +++ b/av/container/output.py @@ -87,7 +87,17 @@ def add_stream(self, codec_name, rate=None, options: dict | None = None, **kwarg # Some sane audio defaults elif codec.type == lib.AVMEDIA_TYPE_AUDIO: - ctx.sample_fmt = codec.sample_fmts[0] + out: cython.p_void = cython.NULL + lib.avcodec_get_supported_config( + cython.NULL, + codec, + lib.AV_CODEC_CONFIG_SAMPLE_FORMAT, + 0, + cython.address(out), + cython.NULL, + ) + if out: + ctx.sample_fmt = cython.cast(cython.pointer[lib.AVSampleFormat], out)[0] ctx.bit_rate = kwargs.pop("bit_rate", 0) ctx.bit_rate_tolerance = kwargs.pop("bit_rate_tolerance", 32000) try: diff --git a/include/avcodec.pxd b/include/avcodec.pxd index 09c7dabf0..98114fa12 100644 --- a/include/avcodec.pxd +++ b/include/avcodec.pxd @@ -179,19 +179,27 @@ cdef extern from "libavcodec/avcodec.h" nogil: char *long_name AVMediaType type AVCodecID id - int capabilities - - AVRational* supported_framerates - AVSampleFormat* sample_fmts - AVPixelFormat* pix_fmts - int* supported_samplerates - AVClass *priv_class cdef int av_codec_is_encoder(AVCodec*) cdef int av_codec_is_decoder(AVCodec*) + cdef enum AVCodecConfig: + AV_CODEC_CONFIG_PIX_FORMAT + AV_CODEC_CONFIG_FRAME_RATE + AV_CODEC_CONFIG_SAMPLE_RATE + AV_CODEC_CONFIG_SAMPLE_FORMAT + + cdef int avcodec_get_supported_config( + const AVCodecContext *avctx, + const AVCodec *codec, + AVCodecConfig config, + unsigned flags, + const void **out_configs, + int *out_num_configs, + ) + cdef struct AVProfile: int profile char *name From 9d7ae38d48ff19f95c73ea96d566a73619275b32 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 27 Feb 2026 01:13:00 -0500 Subject: [PATCH 691/846] Fix pointer-to-int truncation in hwaccel singleton cache --- av/codec/hwaccel.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/av/codec/hwaccel.py b/av/codec/hwaccel.py index 991848995..96175ff15 100644 --- a/av/codec/hwaccel.py +++ b/av/codec/hwaccel.py @@ -45,12 +45,12 @@ class HWConfigMethod(IntEnum): @cython.cfunc def wrap_hwconfig(ptr: cython.pointer[lib.AVCodecHWConfig]) -> HWConfig: try: - return _singletons[cython.cast(cython.int, ptr)] + return _singletons[cython.cast(cython.Py_ssize_t, ptr)] except KeyError: pass config: HWConfig = HWConfig(_cinit_sentinel) config._init(ptr) - _singletons[cython.cast(cython.int, ptr)] = config + _singletons[cython.cast(cython.Py_ssize_t, ptr)] = config return config @@ -69,7 +69,7 @@ def __repr__(self): f"" + f"is_supported={self.is_supported} at 0x{cython.cast(cython.Py_ssize_t, self.ptr):x}>" ) @property From d06308b616ecc41732cadfdba2aa3196253c0985 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 27 Feb 2026 01:29:42 -0500 Subject: [PATCH 692/846] Fix C compiler warnings --- av/codec/codec.py | 8 ++++---- av/codec/hwaccel.pxd | 6 +++--- av/codec/hwaccel.py | 6 ++++-- av/container/core.py | 4 ++-- av/container/input.py | 2 +- av/container/output.py | 6 ++++-- av/container/pyio.py | 2 +- av/format.pxd | 6 +++--- av/format.py | 3 ++- av/video/format.py | 2 +- include/avcodec.pxd | 16 ++++++++-------- include/avformat.pxd | 14 +++++++------- include/avutil.pxd | 8 ++++---- 13 files changed, 44 insertions(+), 39 deletions(-) diff --git a/av/codec/codec.py b/av/codec/codec.py index 930a9ecec..552af8284 100644 --- a/av/codec/codec.py +++ b/av/codec/codec.py @@ -182,7 +182,7 @@ def id(self): @property def frame_rates(self): """A list of supported frame rates (:class:`fractions.Fraction`), or ``None``.""" - out: cython.p_void = cython.NULL + out: cython.pointer[cython.const[cython.void]] = cython.NULL num: cython.int = 0 lib.avcodec_get_supported_config( cython.NULL, @@ -200,7 +200,7 @@ def frame_rates(self): @property def audio_rates(self): """A list of supported audio sample rates (``int``), or ``None``.""" - out: cython.p_void = cython.NULL + out: cython.pointer[cython.const[cython.void]] = cython.NULL num: cython.int = 0 lib.avcodec_get_supported_config( cython.NULL, @@ -218,7 +218,7 @@ def audio_rates(self): @property def video_formats(self): """A list of supported :class:`.VideoFormat`, or ``None``.""" - out: cython.p_void = cython.NULL + out: cython.pointer[cython.const[cython.void]] = cython.NULL num: cython.int = 0 lib.avcodec_get_supported_config( cython.NULL, @@ -236,7 +236,7 @@ def video_formats(self): @property def audio_formats(self): """A list of supported :class:`.AudioFormat`, or ``None``.""" - out: cython.p_void = cython.NULL + out: cython.pointer[cython.const[cython.void]] = cython.NULL num: cython.int = 0 lib.avcodec_get_supported_config( cython.NULL, diff --git a/av/codec/hwaccel.pxd b/av/codec/hwaccel.pxd index af8815cb6..fd9bb1720 100644 --- a/av/codec/hwaccel.pxd +++ b/av/codec/hwaccel.pxd @@ -5,10 +5,10 @@ from av.codec.codec cimport Codec cdef class HWConfig: cdef object __weakref__ - cdef lib.AVCodecHWConfig *ptr - cdef void _init(self, lib.AVCodecHWConfig *ptr) + cdef const lib.AVCodecHWConfig *ptr + cdef void _init(self, const lib.AVCodecHWConfig *ptr) -cdef HWConfig wrap_hwconfig(lib.AVCodecHWConfig *ptr) +cdef HWConfig wrap_hwconfig(const lib.AVCodecHWConfig *ptr) cdef class HWAccel: cdef int _device_type diff --git a/av/codec/hwaccel.py b/av/codec/hwaccel.py index 96175ff15..0155bdae4 100644 --- a/av/codec/hwaccel.py +++ b/av/codec/hwaccel.py @@ -43,7 +43,7 @@ class HWConfigMethod(IntEnum): @cython.cfunc -def wrap_hwconfig(ptr: cython.pointer[lib.AVCodecHWConfig]) -> HWConfig: +def wrap_hwconfig(ptr: cython.pointer[cython.const[lib.AVCodecHWConfig]]) -> HWConfig: try: return _singletons[cython.cast(cython.Py_ssize_t, ptr)] except KeyError: @@ -61,7 +61,9 @@ def __init__(self, sentinel): raise RuntimeError("Cannot instantiate CodecContext") @cython.cfunc - def _init(self, ptr: cython.pointer[lib.AVCodecHWConfig]) -> cython.void: + def _init( + self, ptr: cython.pointer[cython.const[lib.AVCodecHWConfig]] + ) -> cython.void: self.ptr = ptr def __repr__(self): diff --git a/av/container/core.py b/av/container/core.py index f4ef1a277..a651c8055 100755 --- a/av/container/core.py +++ b/av/container/core.py @@ -280,7 +280,7 @@ def __cinit__( res: cython.int name_obj: bytes = os.fsencode(self.name) name: cython.p_char = name_obj - ofmt: cython.pointer[lib.AVOutputFormat] + ofmt: cython.pointer[cython.const[lib.AVOutputFormat]] if self.writeable: ofmt = ( @@ -328,7 +328,7 @@ def __cinit__( self.ptr.io_close2 = pyav_io_close self.ptr.flags |= lib.AVFMT_FLAG_CUSTOM_IO - ifmt: cython.pointer[lib.AVInputFormat] + ifmt: cython.pointer[cython.const[lib.AVInputFormat]] c_options: Dictionary if not self.writeable: ifmt = self.format.iptr if self.format else cython.NULL diff --git a/av/container/input.py b/av/container/input.py index 2412f7744..25cebe386 100644 --- a/av/container/input.py +++ b/av/container/input.py @@ -26,7 +26,7 @@ def __cinit__(self, *args, **kwargs): py_codec_context: CodecContext i: cython.uint stream: cython.pointer[lib.AVStream] - codec: cython.pointer[lib.AVCodec] + codec: cython.pointer[cython.const[lib.AVCodec]] codec_context: cython.pointer[lib.AVCodecContext] # If we have either the global `options`, or a `stream_options`, prepare diff --git a/av/container/output.py b/av/container/output.py index 600d84720..b36cf307f 100644 --- a/av/container/output.py +++ b/av/container/output.py @@ -87,7 +87,7 @@ def add_stream(self, codec_name, rate=None, options: dict | None = None, **kwarg # Some sane audio defaults elif codec.type == lib.AVMEDIA_TYPE_AUDIO: - out: cython.p_void = cython.NULL + out: cython.pointer[cython.const[cython.void]] = cython.NULL lib.avcodec_get_supported_config( cython.NULL, codec, @@ -285,7 +285,9 @@ def add_data_stream(self, codec_name=None, options: dict | None = None): :rtype: The new :class:`~av.data.stream.DataStream`. """ codec: cython.pointer[cython.const[lib.AVCodec]] = cython.NULL - codec_descriptor: cython.pointer[lib.AVCodecDescriptor] = cython.NULL + codec_descriptor: cython.pointer[cython.const[lib.AVCodecDescriptor]] = ( + cython.NULL + ) if codec_name is not None: codec = lib.avcodec_find_encoder_by_name(codec_name.encode()) diff --git a/av/container/pyio.py b/av/container/pyio.py index ccacc01e8..6c248ba13 100644 --- a/av/container/pyio.py +++ b/av/container/pyio.py @@ -183,7 +183,7 @@ def pyio_close_gil(pb: cython.pointer[lib.AVIOContext]) -> cython.int: try: return lib.avio_close(pb) except Exception: - stash_exception() + return stash_exception() @cython.cfunc diff --git a/av/format.pxd b/av/format.pxd index 31cac50aa..4109f325c 100644 --- a/av/format.pxd +++ b/av/format.pxd @@ -5,8 +5,8 @@ cdef class ContainerFormat: cdef readonly str name - cdef lib.AVInputFormat *iptr - cdef lib.AVOutputFormat *optr + cdef const lib.AVInputFormat *iptr + cdef const lib.AVOutputFormat *optr -cdef ContainerFormat build_container_format(lib.AVInputFormat*, lib.AVOutputFormat*) +cdef ContainerFormat build_container_format(const lib.AVInputFormat*, const lib.AVOutputFormat*) diff --git a/av/format.py b/av/format.py index a0bf0bd90..aad23050d 100644 --- a/av/format.py +++ b/av/format.py @@ -9,7 +9,8 @@ @cython.cfunc def build_container_format( - iptr: cython.pointer[lib.AVInputFormat], optr: cython.pointer[lib.AVOutputFormat] + iptr: cython.pointer[cython.const[lib.AVInputFormat]], + optr: cython.pointer[cython.const[lib.AVOutputFormat]], ) -> ContainerFormat: if not iptr and not optr: raise ValueError("needs input format or output format") diff --git a/av/video/format.py b/av/video/format.py index 50fb1a89d..dbc37ed64 100644 --- a/av/video/format.py +++ b/av/video/format.py @@ -196,7 +196,7 @@ def height(self): names = set() -desc = cython.declare(cython.pointer[lib.AVPixFmtDescriptor], cython.NULL) +desc = cython.declare(cython.pointer[cython.const[lib.AVPixFmtDescriptor]], cython.NULL) while True: desc = lib.av_pix_fmt_desc_next(desc) if not desc: diff --git a/include/avcodec.pxd b/include/avcodec.pxd index 98114fa12..f471d2e75 100644 --- a/include/avcodec.pxd +++ b/include/avcodec.pxd @@ -290,18 +290,18 @@ cdef extern from "libavcodec/avcodec.h" nogil: int subtitle_header_size uint8_t *subtitle_header - cdef AVCodecContext* avcodec_alloc_context3(AVCodec *codec) + cdef AVCodecContext* avcodec_alloc_context3(const AVCodec *codec) cdef void avcodec_free_context(AVCodecContext **ctx) cdef AVClass* avcodec_get_class() - cdef AVCodec* avcodec_find_decoder(AVCodecID id) - cdef AVCodec* avcodec_find_encoder(AVCodecID id) - cdef AVCodec* avcodec_find_decoder_by_name(char *name) - cdef AVCodec* avcodec_find_encoder_by_name(char *name) + cdef const AVCodec* avcodec_find_decoder(AVCodecID id) + cdef const AVCodec* avcodec_find_encoder(AVCodecID id) + cdef const AVCodec* avcodec_find_decoder_by_name(char *name) + cdef const AVCodec* avcodec_find_encoder_by_name(char *name) cdef const AVCodec* av_codec_iterate(void **opaque) - cdef AVCodecDescriptor* avcodec_descriptor_get(AVCodecID id) - cdef AVCodecDescriptor* avcodec_descriptor_get_by_name(char *name) + cdef const AVCodecDescriptor* avcodec_descriptor_get(AVCodecID id) + cdef const AVCodecDescriptor* avcodec_descriptor_get_by_name(char *name) cdef char* avcodec_get_name(AVCodecID id) - cdef int avcodec_open2(AVCodecContext *ctx, AVCodec *codec, AVDictionary **options) + cdef int avcodec_open2(AVCodecContext *ctx, const AVCodec *codec, AVDictionary **options) cdef enum AVPacketSideDataType: pass cdef struct AVPacketSideData: diff --git a/include/avformat.pxd b/include/avformat.pxd index 89e63af2f..94bf27b2e 100644 --- a/include/avformat.pxd +++ b/include/avformat.pxd @@ -137,7 +137,7 @@ cdef extern from "libavformat/avformat.h" nogil: int flags ) - cdef AVInputFormat* av_find_input_format(const char *name) + cdef const AVInputFormat* av_find_input_format(const char *name) # http://ffmpeg.org/doxygen/trunk/structAVFormatContext.html cdef struct AVFormatContext: @@ -145,8 +145,8 @@ cdef extern from "libavformat/avformat.h" nogil: AVStream **streams unsigned int nb_chapters AVChapter **chapters - AVInputFormat *iformat - AVOutputFormat *oformat + const AVInputFormat *iformat + const AVOutputFormat *oformat AVIOContext *pb AVIOInterruptCB interrupt_callback AVDictionary *metadata @@ -170,7 +170,7 @@ cdef extern from "libavformat/avformat.h" nogil: cdef int avformat_open_input( AVFormatContext **ctx, char *filename, - AVInputFormat *format, + const AVInputFormat *format, AVDictionary **options ) @@ -181,7 +181,7 @@ cdef extern from "libavformat/avformat.h" nogil: cdef int av_write_frame(AVFormatContext *ctx, AVPacket *pkt) cdef int avio_open(AVIOContext **s, char *url, int flags) cdef int64_t avio_size(AVIOContext *s) - cdef AVOutputFormat* av_guess_format( + cdef const AVOutputFormat* av_guess_format( char *short_name, char *filename, char *mime_type ) cdef int avformat_query_codec( @@ -191,10 +191,10 @@ cdef extern from "libavformat/avformat.h" nogil: cdef int avio_close(AVIOContext *s) cdef int avio_closep(AVIOContext **s) cdef int avformat_find_stream_info(AVFormatContext *ctx, AVDictionary **options) - cdef AVStream* avformat_new_stream(AVFormatContext *ctx, AVCodec *c) + cdef AVStream* avformat_new_stream(AVFormatContext *ctx, const AVCodec *c) cdef int avformat_alloc_output_context2( AVFormatContext **ctx, - AVOutputFormat *oformat, + const AVOutputFormat *oformat, char *format_name, char *filename ) diff --git a/include/avutil.pxd b/include/avutil.pxd index 6ca52e162..c1320a97d 100644 --- a/include/avutil.pxd +++ b/include/avutil.pxd @@ -335,12 +335,12 @@ cdef extern from "libavutil/pixdesc.h" nogil: uint8_t flags AVComponentDescriptor comp[4] - cdef AVPixFmtDescriptor* av_pix_fmt_desc_get(AVPixelFormat pix_fmt) - cdef AVPixFmtDescriptor* av_pix_fmt_desc_next(AVPixFmtDescriptor *prev) + cdef const AVPixFmtDescriptor* av_pix_fmt_desc_get(AVPixelFormat pix_fmt) + cdef const AVPixFmtDescriptor* av_pix_fmt_desc_next(const AVPixFmtDescriptor *prev) cdef char * av_get_pix_fmt_name(AVPixelFormat pix_fmt) cdef AVPixelFormat av_get_pix_fmt(char* name) - int av_get_bits_per_pixel(AVPixFmtDescriptor *pixdesc) - int av_get_padded_bits_per_pixel(AVPixFmtDescriptor *pixdesc) + int av_get_bits_per_pixel(const AVPixFmtDescriptor *pixdesc) + int av_get_padded_bits_per_pixel(const AVPixFmtDescriptor *pixdesc) cdef extern from "libavutil/rational.h" nogil: cdef int av_reduce(int *dst_num, int *dst_den, int64_t num, int64_t den, int64_t max) From 6b68c8c20e9a81aba5ba90a377d2c21818f1cac8 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 27 Feb 2026 21:45:48 -0500 Subject: [PATCH 693/846] We don't need this type of comment --- av/container/input.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/av/container/input.py b/av/container/input.py index 25cebe386..b5833726a 100644 --- a/av/container/input.py +++ b/av/container/input.py @@ -52,13 +52,6 @@ def __cinit__(self, *args, **kwargs): self.set_timeout(self.open_timeout) self.start_timeout() with cython.nogil: - # This peeks are the first few frames to: - # - set stream.disposition from codec.audio_service_type (not exposed); - # - set stream.codec.bits_per_coded_sample; - # - set stream.duration; - # - set stream.start_time; - # - set stream.r_frame_rate to average value; - # - open and closes codecs with the options provided. ret = lib.avformat_find_stream_info(self.ptr, c_options) self.set_timeout(None) self.err_check(ret) @@ -76,7 +69,6 @@ def __cinit__(self, *args, **kwargs): stream = self.ptr.streams[i] codec = lib.avcodec_find_decoder(stream.codecpar.codec_id) if codec: - # allocate and initialize decoder codec_context = lib.avcodec_alloc_context3(codec) err_check( lib.avcodec_parameters_to_context(codec_context, stream.codecpar) From ccf04edc851c1b2dea20564ba5539542b42aac9c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 27 Feb 2026 22:01:44 -0500 Subject: [PATCH 694/846] Use declare to hide internal vars --- av/error.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/av/error.py b/av/error.py index 77fce88d1..669c8e55a 100644 --- a/av/error.py +++ b/av/error.py @@ -10,7 +10,7 @@ from cython.cimports.libc.stdio import fprintf, stderr from cython.cimports.libc.stdlib import free, malloc -# Will get extended with all of the exceptions. +# Will get extended with all the exceptions. __all__ = [ "ErrorType", "FFmpegError", @@ -117,8 +117,8 @@ def __str__(self): # Our custom error, used in callbacks. -c_PYAV_STASHED_ERROR: cython.int = tag_to_code(b"PyAV") -PYAV_STASHED_ERROR_message: str = "Error in PyAV callback" +c_PYAV_STASHED_ERROR = cython.declare(cython.int, tag_to_code(b"PyAV")) +PYAV_STASHED_ERROR_message = cython.declare(str, "Error in PyAV callback") # Bases for the FFmpeg-based exceptions. @@ -342,8 +342,9 @@ class UndefinedError(FFmpegError): # Storage for stashing. -_local: object = local() -_err_count: cython.int = 0 +_local = cython.declare(object, local()) +_err_count = cython.declare(cython.int, 0) +_last_log_count = cython.declare(cython.int, 0) @cython.cfunc @@ -364,9 +365,6 @@ def stash_exception(exc_info=None) -> cython.int: return -c_PYAV_STASHED_ERROR -_last_log_count: cython.int = 0 - - @cython.ccall @cython.exceptval(-1, check=False) def err_check(res: cython.int, filename=None) -> cython.int: From f239f75b05a615d4432bbf7713c6e70cd3636991 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 27 Feb 2026 22:29:21 -0500 Subject: [PATCH 695/846] Add enumerate_input_devices and enumerate_output_devices API Uses avdevice_list_input_sources/avdevice_list_output_sinks (the modern FFmpeg API) with a log-capture fallback for formats like avfoundation that log devices instead of implementing get_device_list. Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.rst | 1 + av/__init__.py | 4 + av/_core.pxd | 6 - av/_core.py | 8 +- av/device.py | 187 ++++++++++++++++++++++++++++++ av/device.pyi | 19 +++ examples/basics/record_facecam.py | 4 +- examples/basics/record_screen.py | 4 +- include/avdevice.pxd | 30 +++++ include/libav.pxd | 1 + 10 files changed, 250 insertions(+), 14 deletions(-) create mode 100644 av/device.py create mode 100644 av/device.pyi create mode 100644 include/avdevice.pxd diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6a803b6a5..7d507d46d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -40,6 +40,7 @@ Features: - Use zero-copy for Packet init from buffer data by :gh-user:`WyattBlue` in (:pr:`2199`). - Expose AVIndexEntry by :gh-user:`Queuecumber` in (:pr:`2136`). - Preserving hardware memory during cuvid decoding, exporting/importing via dlpack by :gh-user:`WyattBlue` in (:pr:`2155`). +- Add enumerate_input_devices and enumerate_output_devices API by :gh-user:`WyattBlue` in (:pr:`2155`). Fixes: diff --git a/av/__init__.py b/av/__init__.py index cbc3c8a2f..9d8148082 100644 --- a/av/__init__.py +++ b/av/__init__.py @@ -18,6 +18,7 @@ from av.codec.codec import Codec, codecs_available from av.codec.context import CodecContext from av.container import open +from av.device import DeviceInfo, enumerate_input_devices, enumerate_output_devices from av.format import ContainerFormat, formats_available from av.packet import Packet from av.error import * # noqa: F403; This is limited to exception types. @@ -44,6 +45,9 @@ "codecs_available", "CodecContext", "open", + "DeviceInfo", + "enumerate_input_devices", + "enumerate_output_devices", "ContainerFormat", "formats_available", "Packet", diff --git a/av/_core.pxd b/av/_core.pxd index 49f665b6e..43d1e716d 100644 --- a/av/_core.pxd +++ b/av/_core.pxd @@ -1,9 +1,3 @@ -cdef extern from "libavdevice/avdevice.h" nogil: - cdef int avdevice_version() - cdef char* avdevice_configuration() - cdef char* avdevice_license() - void avdevice_register_all() - cdef extern from "libswscale/swscale.h" nogil: cdef int swscale_version() cdef char* swscale_configuration() diff --git a/av/_core.py b/av/_core.py index d9ad01074..6ad38df5b 100644 --- a/av/_core.py +++ b/av/_core.py @@ -1,7 +1,7 @@ import cython import cython.cimports.libav as lib -avdevice_register_all() +lib.avdevice_register_all() # Exports. time_base = lib.AV_TIME_BASE @@ -41,9 +41,9 @@ def decode_version(v): license=lib.avformat_license(), ), "libavdevice": dict( - version=decode_version(avdevice_version()), - configuration=avdevice_configuration(), - license=avdevice_license(), + version=decode_version(lib.avdevice_version()), + configuration=lib.avdevice_configuration(), + license=lib.avdevice_license(), ), "libavfilter": dict( version=decode_version(lib.avfilter_version()), diff --git a/av/device.py b/av/device.py new file mode 100644 index 000000000..7be7e68a6 --- /dev/null +++ b/av/device.py @@ -0,0 +1,187 @@ +import re + +import cython +import cython.cimports.libav as lib +from cython.cimports.av.error import err_check + + +class DeviceInfo: + """Information about an input or output device. + + :param str name: The device identifier, for use as the first argument to :func:`av.open`. + :param str description: Human-readable description of the device. + :param bool is_default: Whether this is the default device. + :param list media_types: Media types this device provides, e.g. ``["video"]``, ``["audio"]``, + or ``["video", "audio"]``. + + """ + + name: str + description: str + is_default: bool + media_types: list[str] + + def __init__( + self, + name: str, + description: str, + is_default: bool, + media_types: list[str], + ) -> None: + self.name = name + self.description = description + self.is_default = is_default + self.media_types = media_types + + def __repr__(self) -> str: + default = " (default)" if self.is_default else "" + return f"" + + +@cython.cfunc +def _build_device_list(device_list: cython.pointer[lib.AVDeviceInfoList]) -> list: + devices: list = [] + i: cython.int + j: cython.int + device_info: cython.pointer[lib.AVDeviceInfo] + mt: lib.AVMediaType + s: cython.p_const_char + + for i in range(device_list.nb_devices): + device_info = device_list.devices[i] + + media_types: list = [] + for j in range(device_info.nb_media_types): + mt = device_info.media_types[j] + s = lib.av_get_media_type_string(mt) + if s: + media_types.append(s.decode()) + + devices.append( + DeviceInfo( + name=device_info.device_name.decode() + if device_info.device_name + else "", + description=device_info.device_description.decode() + if device_info.device_description + else "", + is_default=(i == device_list.default_device), + media_types=media_types, + ) + ) + + return devices + + +def _enumerate_via_log_fallback(format_name: str) -> list[DeviceInfo]: + """Fallback for formats (e.g. avfoundation) that log devices instead of + implementing get_device_list. Opens the format with list_devices=1 and + parses the INFO log output.""" + from av import logging as avlogging + + fmt: cython.pointer[cython.const[lib.AVInputFormat]] = lib.av_find_input_format( + format_name + ) + + opts: cython.pointer[lib.AVDictionary] = cython.NULL + lib.av_dict_set(cython.address(opts), b"list_devices", b"1", 0) + + ctx: cython.pointer[lib.AVFormatContext] = cython.NULL + + # Temporarily enable INFO logging so Capture receives device list messages. + old_level = avlogging.get_level() + avlogging.set_level(avlogging.INFO) + devices: list[DeviceInfo] = [] + try: + with avlogging.Capture() as logs: + lib.avformat_open_input(cython.address(ctx), b"", fmt, cython.address(opts)) + if ctx: + lib.avformat_close_input(cython.address(ctx)) + + current_media_type = "video" + for _level, _name, message in logs: + message = message.strip() + if "video devices" in message.lower(): + current_media_type = "video" + elif "audio devices" in message.lower(): + current_media_type = "audio" + else: + m = re.match(r"\[(\d+)\] (.+)", message) + if m: + devices.append( + DeviceInfo( + name=m.group(1), + description=m.group(2), + is_default=False, + media_types=[current_media_type], + ) + ) + finally: + avlogging.set_level(old_level) + lib.av_dict_free(cython.address(opts)) + + return devices + + +def enumerate_input_devices(format_name: str) -> list[DeviceInfo]: + """List the available input devices for a given format. + + :param str format_name: The format name, e.g. ``"avfoundation"``, ``"dshow"``, ``"v4l2"``. + :rtype: list[DeviceInfo] + :raises ValueError: If *format_name* is not a known input format. + :raises av.FFmpegError: If the device does not support enumeration. + + Example:: + + for device in av.enumerate_input_devices("avfoundation"): + print(device.name, device.description) + + """ + fmt: cython.pointer[cython.const[lib.AVInputFormat]] = lib.av_find_input_format( + format_name + ) + if not fmt: + raise ValueError(f"no such input format: {format_name!r}") + + device_list: cython.pointer[lib.AVDeviceInfoList] = cython.NULL + try: + err_check( + lib.avdevice_list_input_sources( + fmt, cython.NULL, cython.NULL, cython.address(device_list) + ) + ) + return _build_device_list(device_list) + except NotImplementedError: + # Format doesn't implement get_device_list (e.g. avfoundation). + # Fall back to opening with list_devices=1 and parsing the log output. + return _enumerate_via_log_fallback(format_name) + finally: + lib.avdevice_free_list_devices(cython.address(device_list)) + + +def enumerate_output_devices(format_name: str) -> list[DeviceInfo]: + """List the available output devices for a given format. + + :param str format_name: The format name, e.g. ``"audiotoolbox"``. + :rtype: list[DeviceInfo] + :raises ValueError: If *format_name* is not a known output format. + :raises av.FFmpegError: If the device does not support enumeration. + + """ + fmt: cython.pointer[cython.const[lib.AVOutputFormat]] = lib.av_guess_format( + format_name, cython.NULL, cython.NULL + ) + if not fmt: + raise ValueError(f"no such output format: {format_name!r}") + + device_list: cython.pointer[lib.AVDeviceInfoList] = cython.NULL + err_check( + lib.avdevice_list_output_sinks( + fmt, cython.NULL, cython.NULL, cython.address(device_list) + ) + ) + + try: + return _build_device_list(device_list) + finally: + lib.avdevice_free_list_devices(cython.address(device_list)) diff --git a/av/device.pyi b/av/device.pyi new file mode 100644 index 000000000..33c556994 --- /dev/null +++ b/av/device.pyi @@ -0,0 +1,19 @@ +__all__ = ("DeviceInfo", "enumerate_input_devices", "enumerate_output_devices") + +class DeviceInfo: + name: str + description: str + is_default: bool + media_types: list[str] + + def __init__( + self, + name: str, + description: str, + is_default: bool, + media_types: list[str], + ) -> None: ... + def __repr__(self) -> str: ... + +def enumerate_input_devices(format_name: str) -> list[DeviceInfo]: ... +def enumerate_output_devices(format_name: str) -> list[DeviceInfo]: ... diff --git a/examples/basics/record_facecam.py b/examples/basics/record_facecam.py index 0bba6b36a..e554d25d2 100644 --- a/examples/basics/record_facecam.py +++ b/examples/basics/record_facecam.py @@ -5,9 +5,9 @@ """ This is written for MacOS. Other platforms will need to init `input_` differently. -You may need to change the file "0". Use this command to list all devices: +You may need to change the file "0". Use this API to list all devices: - ffmpeg -f avfoundation -list_devices true -i "" + av.enumerate_input_devices("avfoundation") """ diff --git a/examples/basics/record_screen.py b/examples/basics/record_screen.py index 14fdfc428..76b50a7e2 100644 --- a/examples/basics/record_screen.py +++ b/examples/basics/record_screen.py @@ -4,9 +4,9 @@ """ This is written for MacOS. Other platforms will need a different file, format pair. -You may need to change the file "1". Use this command to list all devices: +You may need to change the file "1". Use this API to list all devices: - ffmpeg -f avfoundation -list_devices true -i "" + av.enumerate_input_devices("avfoundation") """ diff --git a/include/avdevice.pxd b/include/avdevice.pxd new file mode 100644 index 000000000..9631337fa --- /dev/null +++ b/include/avdevice.pxd @@ -0,0 +1,30 @@ +cdef extern from "libavdevice/avdevice.h" nogil: + cdef int avdevice_version() + cdef char* avdevice_configuration() + cdef char* avdevice_license() + void avdevice_register_all() + + cdef struct AVDeviceInfo: + char *device_name + char *device_description + int nb_media_types + AVMediaType *media_types + + cdef struct AVDeviceInfoList: + AVDeviceInfo **devices + int nb_devices + int default_device + + int avdevice_list_input_sources( + const AVInputFormat *device, + const char *device_name, + AVDictionary *device_options, + AVDeviceInfoList **device_list, + ) + int avdevice_list_output_sinks( + const AVOutputFormat *device, + const char *device_name, + AVDictionary *device_options, + AVDeviceInfoList **device_list, + ) + void avdevice_free_list_devices(AVDeviceInfoList **device_list) diff --git a/include/libav.pxd b/include/libav.pxd index 9157a8b33..333815d80 100644 --- a/include/libav.pxd +++ b/include/libav.pxd @@ -2,3 +2,4 @@ include "avutil.pxd" include "avcodec.pxd" include "avformat.pxd" include "avfilter.pxd" +include "avdevice.pxd" From 0a1ed6f67b3085959335134f60ff0748d41cad7f Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 27 Feb 2026 22:38:04 -0500 Subject: [PATCH 696/846] Add tests for enumerate_input/output_devices Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.rst | 2 +- tests/test_device.py | 66 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 tests/test_device.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7d507d46d..404b819d9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -40,7 +40,7 @@ Features: - Use zero-copy for Packet init from buffer data by :gh-user:`WyattBlue` in (:pr:`2199`). - Expose AVIndexEntry by :gh-user:`Queuecumber` in (:pr:`2136`). - Preserving hardware memory during cuvid decoding, exporting/importing via dlpack by :gh-user:`WyattBlue` in (:pr:`2155`). -- Add enumerate_input_devices and enumerate_output_devices API by :gh-user:`WyattBlue` in (:pr:`2155`). +- Add enumerate_input_devices and enumerate_output_devices API by :gh-user:`WyattBlue` in (:pr:`2174`). Fixes: diff --git a/tests/test_device.py b/tests/test_device.py new file mode 100644 index 000000000..4fffaf697 --- /dev/null +++ b/tests/test_device.py @@ -0,0 +1,66 @@ +import sys + +import pytest + +import av.error +from av.device import DeviceInfo, enumerate_input_devices, enumerate_output_devices + + +def test_device_info_attributes() -> None: + d = DeviceInfo("0", "FaceTime HD Camera", True, ["video"]) + assert d.name == "0" + assert d.description == "FaceTime HD Camera" + assert d.is_default is True + assert d.media_types == ["video"] + + +def test_device_info_repr_default() -> None: + d = DeviceInfo("0", "FaceTime HD Camera", True, ["video"]) + assert repr(d) == "" + + +def test_device_info_repr_non_default() -> None: + d = DeviceInfo("1", "Built-in Microphone", False, ["audio"]) + assert repr(d) == "" + + +def test_enumerate_input_devices_unknown_format() -> None: + with pytest.raises(ValueError, match="no such input format"): + enumerate_input_devices("not_a_real_format_xyz") + + +def test_enumerate_output_devices_unknown_format() -> None: + with pytest.raises(ValueError, match="no such output format"): + enumerate_output_devices("not_a_real_format_xyz") + + +def _assert_valid_device_list(devices: list[DeviceInfo]) -> None: + assert isinstance(devices, list) + for device in devices: + assert isinstance(device, DeviceInfo) + assert isinstance(device.name, str) + assert isinstance(device.description, str) + assert isinstance(device.is_default, bool) + assert isinstance(device.media_types, list) + assert all(isinstance(mt, str) for mt in device.media_types) + + +@pytest.mark.skipif(sys.platform != "darwin", reason="avfoundation is macOS only") +def test_enumerate_input_devices_avfoundation() -> None: + _assert_valid_device_list(enumerate_input_devices("avfoundation")) + + +@pytest.mark.skipif(sys.platform != "linux", reason="v4l2 is Linux only") +def test_enumerate_input_devices_v4l2() -> None: + try: + _assert_valid_device_list(enumerate_input_devices("video4linux2")) + except av.error.OSError: + pytest.skip("v4l2 device enumeration not available") + + +@pytest.mark.skipif(sys.platform != "win32", reason="dshow is Windows only") +def test_enumerate_input_devices_dshow() -> None: + try: + _assert_valid_device_list(enumerate_input_devices("dshow")) + except av.error.OSError: + pytest.skip("dshow device enumeration not available") From 8760006d5b08c7a313886011cd2473041b6d9ca5 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 28 Feb 2026 00:20:59 -0500 Subject: [PATCH 697/846] Add ColorTrc, ColorPrimaries, etc., closes #1968 - Add ColorTrc and ColorPrimaries IntEnum classes to av.video.reformatter - Add color_trc and color_primaries as read/write properties on VideoFrame - Add dst_color_trc and dst_color_primaries parameters to `VideoFrame.reformat()` Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.rst | 1 + av/video/frame.py | 26 ++++++++++ av/video/frame.pyi | 5 ++ av/video/reformatter.pxd | 4 +- av/video/reformatter.py | 101 ++++++++++++++++++++++++++++++++++----- av/video/reformatter.pyi | 35 ++++++++++++++ include/avutil.pxd | 23 +++++++-- tests/test_colorspace.py | 65 ++++++++++++++++++++++++- 8 files changed, 243 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 404b819d9..b8f944112 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -41,6 +41,7 @@ Features: - Expose AVIndexEntry by :gh-user:`Queuecumber` in (:pr:`2136`). - Preserving hardware memory during cuvid decoding, exporting/importing via dlpack by :gh-user:`WyattBlue` in (:pr:`2155`). - Add enumerate_input_devices and enumerate_output_devices API by :gh-user:`WyattBlue` in (:pr:`2174`). +- Add ``ColorTrc`` and ``ColorPrimaries`` enums; add ``color_trc`` and ``color_primaries`` properties to ``VideoFrame``; add ``dst_color_trc`` and ``dst_color_primaries`` parameters to ``VideoFrame.reformat()``, addressing :issue:`1968` by :gh-user:`WyattBlue` in (:pr:`2175`). Fixes: diff --git a/av/video/frame.py b/av/video/frame.py index f6ae147cc..586d317b5 100644 --- a/av/video/frame.py +++ b/av/video/frame.py @@ -595,6 +595,32 @@ def color_range(self): def color_range(self, value): self.ptr.color_range = value + @property + def color_trc(self): + """Transfer characteristic of frame. + + Wraps :ffmpeg:`AVFrame.color_trc`. + + """ + return self.ptr.color_trc + + @color_trc.setter + def color_trc(self, value): + self.ptr.color_trc = value + + @property + def color_primaries(self): + """Color primaries of frame. + + Wraps :ffmpeg:`AVFrame.color_primaries`. + + """ + return self.ptr.color_primaries + + @color_primaries.setter + def color_primaries(self, value): + self.ptr.color_primaries = value + def reformat(self, *args, **kwargs): """reformat(width=None, height=None, format=None, src_colorspace=None, dst_colorspace=None, interpolation=None) diff --git a/av/video/frame.pyi b/av/video/frame.pyi index 570e227b0..ba3c92fed 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -8,6 +8,7 @@ from av.frame import Frame from .format import VideoFormat from .plane import VideoPlane +from .reformatter import ColorPrimaries, ColorTrc _SupportedNDarray = Union[ np.ndarray[Any, np.dtype[np.uint8]], @@ -41,6 +42,8 @@ class VideoFrame(Frame): pict_type: int colorspace: int color_range: int + color_trc: int + color_primaries: int @property def time(self) -> float: ... @@ -65,6 +68,8 @@ class VideoFrame(Frame): interpolation: int | str | None = None, src_color_range: int | str | None = None, dst_color_range: int | str | None = None, + dst_color_trc: int | ColorTrc | None = None, + dst_color_primaries: int | ColorPrimaries | None = None, ) -> VideoFrame: ... def to_rgb(self, **kwargs: Any) -> VideoFrame: ... def save(self, filepath: str | Path) -> None: ... diff --git a/av/video/reformatter.pxd b/av/video/reformatter.pxd index 433824e8c..f57ba7e77 100644 --- a/av/video/reformatter.pxd +++ b/av/video/reformatter.pxd @@ -79,4 +79,6 @@ cdef class VideoReformatter: lib.AVPixelFormat format, int src_colorspace, int dst_colorspace, int interpolation, int src_color_range, int dst_color_range, - bint set_dst_colorspace, bint set_dst_color_range) + bint set_dst_colorspace, bint set_dst_color_range, + int dst_color_trc, int dst_color_primaries, + bint set_dst_color_trc, bint set_dst_color_primaries) diff --git a/av/video/reformatter.py b/av/video/reformatter.py index b72162ede..9c201bbe9 100644 --- a/av/video/reformatter.py +++ b/av/video/reformatter.py @@ -44,7 +44,55 @@ class ColorRange(IntEnum): NB: "Not part of ABI" = lib.AVCOL_RANGE_NB -def _resolve_enum_value(value, enum_class, default): +class ColorTrc(IntEnum): + """Transfer characteristic (gamma curve) of a video frame. + + Maps to FFmpeg's ``AVColorTransferCharacteristic``. + """ + + BT709: "BT.709" = lib.AVCOL_TRC_BT709 + UNSPECIFIED: "Unspecified" = lib.AVCOL_TRC_UNSPECIFIED + GAMMA22: "Gamma 2.2 (BT.470M)" = lib.AVCOL_TRC_GAMMA22 + GAMMA28: "Gamma 2.8 (BT.470BG)" = lib.AVCOL_TRC_GAMMA28 + SMPTE170M: "SMPTE 170M" = lib.AVCOL_TRC_SMPTE170M + SMPTE240M: "SMPTE 240M" = lib.AVCOL_TRC_SMPTE240M + LINEAR: "Linear" = lib.AVCOL_TRC_LINEAR + LOG: "Logarithmic (100:1 range)" = lib.AVCOL_TRC_LOG + LOG_SQRT: "Logarithmic (100*sqrt(10):1 range)" = lib.AVCOL_TRC_LOG_SQRT + IEC61966_2_4: "IEC 61966-2-4 (sRGB)" = lib.AVCOL_TRC_IEC61966_2_4 + BT1361_ECG: "BT.1361 extended colour gamut" = lib.AVCOL_TRC_BT1361_ECG + IEC61966_2_1: "IEC 61966-2-1 (sYCC)" = lib.AVCOL_TRC_IEC61966_2_1 + BT2020_10: "BT.2020 10-bit" = lib.AVCOL_TRC_BT2020_10 + BT2020_12: "BT.2020 12-bit" = lib.AVCOL_TRC_BT2020_12 + SMPTE2084: "SMPTE 2084 (PQ, HDR10)" = lib.AVCOL_TRC_SMPTE2084 + SMPTE428: "SMPTE 428-1" = lib.AVCOL_TRC_SMPTE428 + ARIB_STD_B67: "ARIB STD-B67 (HLG)" = lib.AVCOL_TRC_ARIB_STD_B67 + + +class ColorPrimaries(IntEnum): + """Color primaries of a video frame. + + Maps to FFmpeg's ``AVColorPrimaries``. + """ + + BT709: "BT.709 / sRGB / sYCC" = lib.AVCOL_PRI_BT709 + UNSPECIFIED: "Unspecified" = lib.AVCOL_PRI_UNSPECIFIED + BT470M: "BT.470M" = lib.AVCOL_PRI_BT470M + BT470BG: "BT.470BG / BT.601-6 625" = lib.AVCOL_PRI_BT470BG + SMPTE170M: "SMPTE 170M / BT.601-6 525" = lib.AVCOL_PRI_SMPTE170M + SMPTE240M: "SMPTE 240M" = lib.AVCOL_PRI_SMPTE240M + FILM: "Generic film (Illuminant C)" = lib.AVCOL_PRI_FILM + BT2020: "BT.2020 / BT.2100" = lib.AVCOL_PRI_BT2020 + SMPTE428: "SMPTE 428-1 / XYZ" = lib.AVCOL_PRI_SMPTE428 + SMPTE431: "SMPTE 431-2 (DCI-P3)" = lib.AVCOL_PRI_SMPTE431 + SMPTE432: "SMPTE 432-1 (Display P3)" = lib.AVCOL_PRI_SMPTE432 + EBU3213: "EBU 3213-E / JEDEC P22" = lib.AVCOL_PRI_EBU3213 + + +@cython.cfunc +def _resolve_enum_value( + value: object, enum_class: object, default: cython.int +) -> cython.int: # Helper function to resolve enum values from different input types. if value is None: return default @@ -96,6 +144,8 @@ def reformat( interpolation=None, src_color_range=None, dst_color_range=None, + dst_color_trc=None, + dst_color_primaries=None, ): """Create a new :class:`VideoFrame` with the given width/height/format/colorspace. @@ -112,34 +162,43 @@ def reformat( :param interpolation: The interpolation method to use, or ``None`` for ``BILINEAR``. :type interpolation: :class:`Interpolation` or ``str`` :param src_color_range: Current color range, or ``None`` for the ``UNSPECIFIED``. - :type src_color_range: :class:`color range` or ``str`` + :type src_color_range: :class:`ColorRange` or ``str`` :param dst_color_range: Desired color range, or ``None`` for the ``UNSPECIFIED``. - :type dst_color_range: :class:`color range` or ``str`` + :type dst_color_range: :class:`ColorRange` or ``str`` + :param dst_color_trc: Desired transfer characteristic to tag on the output frame, + or ``None`` to preserve the source frame's value. This sets frame metadata only; + it does not perform a pixel-level transfer function conversion. + :type dst_color_trc: :class:`ColorTrc` or ``int`` + :param dst_color_primaries: Desired color primaries to tag on the output frame, + or ``None`` to preserve the source frame's value. + :type dst_color_primaries: :class:`ColorPrimaries` or ``int`` """ video_format: VideoFormat = VideoFormat( format if format is not None else frame.format ) - c_src_colorspace: cython.int = _resolve_enum_value( + c_src_colorspace = _resolve_enum_value( src_colorspace, Colorspace, frame.colorspace ) - c_dst_colorspace: cython.int = _resolve_enum_value( + c_dst_colorspace = _resolve_enum_value( dst_colorspace, Colorspace, frame.colorspace ) - c_interpolation: cython.int = _resolve_enum_value( + c_interpolation = _resolve_enum_value( interpolation, Interpolation, int(Interpolation.BILINEAR) ) - c_src_color_range: cython.int = _resolve_enum_value( - src_color_range, ColorRange, 0 - ) - c_dst_color_range: cython.int = _resolve_enum_value( - dst_color_range, ColorRange, 0 + c_src_color_range = _resolve_enum_value(src_color_range, ColorRange, 0) + c_dst_color_range = _resolve_enum_value(dst_color_range, ColorRange, 0) + c_dst_color_trc = _resolve_enum_value(dst_color_trc, ColorTrc, 0) + c_dst_color_primaries = _resolve_enum_value( + dst_color_primaries, ColorPrimaries, 0 ) # Track whether user explicitly specified destination metadata set_dst_colorspace: cython.bint = dst_colorspace is not None set_dst_color_range: cython.bint = dst_color_range is not None + set_dst_color_trc: cython.bint = dst_color_trc is not None + set_dst_color_primaries: cython.bint = dst_color_primaries is not None return self._reformat( frame, @@ -153,6 +212,10 @@ def reformat( c_dst_color_range, set_dst_colorspace, set_dst_color_range, + c_dst_color_trc, + c_dst_color_primaries, + set_dst_color_trc, + set_dst_color_primaries, ) @cython.cfunc @@ -169,6 +232,10 @@ def _reformat( dst_color_range: cython.int, set_dst_colorspace: cython.bint, set_dst_color_range: cython.bint, + dst_color_trc: cython.int, + dst_color_primaries: cython.int, + set_dst_color_trc: cython.bint, + set_dst_color_primaries: cython.bint, ): if frame.ptr.format < 0: raise ValueError("Frame does not have format set.") @@ -191,6 +258,8 @@ def _reformat( and height == frame.ptr.height and dst_colorspace == src_colorspace and src_color_range == dst_color_range + and not set_dst_color_trc + and not set_dst_color_primaries ): return frame @@ -207,6 +276,8 @@ def _reformat( and height == frame.ptr.height and dst_colorspace == src_colorspace and src_color_range == dst_color_range + and not set_dst_color_trc + and not set_dst_color_primaries ): return frame @@ -285,6 +356,14 @@ def _reformat( new_frame.ptr.color_range = cython.cast( lib.AVColorRange, frame_dst_color_range ) + if set_dst_color_trc: + new_frame.ptr.color_trc = cython.cast( + lib.AVColorTransferCharacteristic, dst_color_trc + ) + if set_dst_color_primaries: + new_frame.ptr.color_primaries = cython.cast( + lib.AVColorPrimaries, dst_color_primaries + ) with cython.nogil: sws_scale( diff --git a/av/video/reformatter.pyi b/av/video/reformatter.pyi index 5d83fcbe3..b1e51e984 100644 --- a/av/video/reformatter.pyi +++ b/av/video/reformatter.pyi @@ -38,6 +38,39 @@ class ColorRange(IntEnum): JPEG = 2 NB = 3 +class ColorTrc(IntEnum): + BT709 = cast(int, ...) + UNSPECIFIED = cast(int, ...) + GAMMA22 = cast(int, ...) + GAMMA28 = cast(int, ...) + SMPTE170M = cast(int, ...) + SMPTE240M = cast(int, ...) + LINEAR = cast(int, ...) + LOG = cast(int, ...) + LOG_SQRT = cast(int, ...) + IEC61966_2_4 = cast(int, ...) + BT1361_ECG = cast(int, ...) + IEC61966_2_1 = cast(int, ...) + BT2020_10 = cast(int, ...) + BT2020_12 = cast(int, ...) + SMPTE2084 = cast(int, ...) + SMPTE428 = cast(int, ...) + ARIB_STD_B67 = cast(int, ...) + +class ColorPrimaries(IntEnum): + BT709 = cast(int, ...) + UNSPECIFIED = cast(int, ...) + BT470M = cast(int, ...) + BT470BG = cast(int, ...) + SMPTE170M = cast(int, ...) + SMPTE240M = cast(int, ...) + FILM = cast(int, ...) + BT2020 = cast(int, ...) + SMPTE428 = cast(int, ...) + SMPTE431 = cast(int, ...) + SMPTE432 = cast(int, ...) + EBU3213 = cast(int, ...) + class VideoReformatter: def reformat( self, @@ -50,4 +83,6 @@ class VideoReformatter: interpolation: int | str | None = None, src_color_range: int | str | None = None, dst_color_range: int | str | None = None, + dst_color_trc: int | ColorTrc | None = None, + dst_color_primaries: int | ColorPrimaries | None = None, ) -> VideoFrame: ... diff --git a/include/avutil.pxd b/include/avutil.pxd index c1320a97d..3ee78d2f5 100644 --- a/include/avutil.pxd +++ b/include/avutil.pxd @@ -53,10 +53,8 @@ cdef extern from "libavutil/avutil.h" nogil: AVCOL_RANGE_NB cdef enum AVColorPrimaries: - AVCOL_PRI_RESERVED0 AVCOL_PRI_BT709 AVCOL_PRI_UNSPECIFIED - AVCOL_PRI_RESERVED AVCOL_PRI_BT470M AVCOL_PRI_BT470BG AVCOL_PRI_SMPTE170M @@ -69,10 +67,27 @@ cdef extern from "libavutil/avutil.h" nogil: AVCOL_PRI_SMPTE432 AVCOL_PRI_EBU3213 AVCOL_PRI_JEDEC_P22 - AVCOL_PRI_NB cdef enum AVColorTransferCharacteristic: - pass + AVCOL_TRC_BT709 + AVCOL_TRC_UNSPECIFIED + AVCOL_TRC_GAMMA22 + AVCOL_TRC_GAMMA28 + AVCOL_TRC_SMPTE170M + AVCOL_TRC_SMPTE240M + AVCOL_TRC_LINEAR + AVCOL_TRC_LOG + AVCOL_TRC_LOG_SQRT + AVCOL_TRC_IEC61966_2_4 + AVCOL_TRC_BT1361_ECG + AVCOL_TRC_IEC61966_2_1 + AVCOL_TRC_BT2020_10 + AVCOL_TRC_BT2020_12 + AVCOL_TRC_SMPTE2084 + AVCOL_TRC_SMPTEST2084 + AVCOL_TRC_SMPTE428 + AVCOL_TRC_SMPTEST428_1 + AVCOL_TRC_ARIB_STD_B67 cdef void* av_malloc(size_t size) cdef void* av_mallocz(size_t size) diff --git a/tests/test_colorspace.py b/tests/test_colorspace.py index c76416c80..a5352fc7d 100644 --- a/tests/test_colorspace.py +++ b/tests/test_colorspace.py @@ -1,5 +1,10 @@ import av -from av.video.reformatter import ColorRange, Colorspace +from av.video.reformatter import ( + ColorPrimaries, + ColorRange, + Colorspace, + ColorTrc, +) from .common import fate_suite @@ -38,3 +43,61 @@ def test_sky_timelapse() -> None: assert stream.codec_context.color_primaries == 1 assert stream.codec_context.color_trc == 1 assert stream.codec_context.colorspace == 1 + + +def test_frame_color_trc_property() -> None: + frame = av.VideoFrame(width=64, height=64, format="rgb24") + assert frame.color_trc == ColorTrc.UNSPECIFIED + + frame.color_trc = ColorTrc.IEC61966_2_4 + assert frame.color_trc == ColorTrc.IEC61966_2_4 + + frame.color_trc = ColorTrc.BT709 + assert frame.color_trc == ColorTrc.BT709 + + +def test_frame_color_primaries_property() -> None: + frame = av.VideoFrame(width=64, height=64, format="rgb24") + assert frame.color_primaries == ColorPrimaries.UNSPECIFIED + + frame.color_primaries = ColorPrimaries.BT709 + assert frame.color_primaries == ColorPrimaries.BT709 + assert frame.color_primaries == 1 # AVCOL_PRI_BT709 + + +def test_reformat_dst_color_trc() -> None: + # Reformat a frame and tag it with sRGB transfer characteristic. + frame = av.VideoFrame(width=64, height=64, format="yuv420p") + rgb = frame.reformat( + format="rgb24", + dst_colorspace=Colorspace.ITU709, + dst_color_trc=ColorTrc.IEC61966_2_4, + ) + assert rgb.format.name == "rgb24" + assert rgb.colorspace == Colorspace.ITU709 + assert rgb.color_trc == ColorTrc.IEC61966_2_4 + + +def test_reformat_dst_color_primaries() -> None: + frame = av.VideoFrame(width=64, height=64, format="yuv420p") + rgb = frame.reformat( + format="rgb24", + dst_color_primaries=ColorPrimaries.BT709, + ) + assert rgb.color_primaries == ColorPrimaries.BT709 + + +def test_reformat_preserves_color_trc() -> None: + # When dst_color_trc is not specified, the source frame's value is preserved. + frame = av.VideoFrame(width=64, height=64, format="yuv420p") + frame.color_trc = ColorTrc.BT709 + rgb = frame.reformat(format="rgb24") + assert rgb.color_trc == ColorTrc.BT709 + + +def test_reformat_preserves_color_primaries() -> None: + # When dst_color_primaries is not specified, the source frame's value is preserved. + frame = av.VideoFrame(width=64, height=64, format="yuv420p") + frame.color_primaries = ColorPrimaries.BT709 + rgb = frame.reformat(format="rgb24") + assert rgb.color_primaries == ColorPrimaries.BT709 From 69427a5824361ac97d008cf1bf12de2a4d7d569b Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 28 Feb 2026 00:39:05 -0500 Subject: [PATCH 698/846] Update authors --- AUTHORS.rst | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index ea0148bf8..42e899c83 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -4,8 +4,8 @@ Contributors All contributors (by number of commits): - Mike Boers ; `@mikeboers `_ +- WyattBlue ; `@WyattBlue `_ -* WyattBlue ; `@WyattBlue `_ * Jeremy Lainé ; `@jlaine `_ - Mark Reid ; `@markreidvfx `_ @@ -24,6 +24,7 @@ All contributors (by number of commits): - Santtu Keskinen - Alba Mendez - Curtis Doty ; `@dotysan `_ +- Lukas Geiger - Xinran Xu ; `@xxr3376 `_ - z-khan - Marc Mueller <30130371+cdce8p@users.noreply.github.com> @@ -34,8 +35,8 @@ All contributors (by number of commits): - David Plowman - Alireza Davoudi ; `@adavoudi `_ - Jonathan Drolet -- Lukas Geiger - Matthew Lai +- Kim Minjong - Moritz Kassner ; `@mkassner `_ - Thomas A Caswell ; `@tacaswell `_ - Ulrik Mikaelsson ; `@rawler `_ @@ -49,12 +50,12 @@ All contributors (by number of commits): * DE-AI <81620697+DE-AI@users.noreply.github.com> * rutsh * Felix Vollmer +* Benedikt Lorch, benedikt-grl * Santiago Castro * Christian Clauss * Ihor Liubymov * Johannes Erdfelt * Karl Litterfeldt ; `@litterfeldt `_ -* Kim Minjong * Martin Larralde * Simon-Martin Schröder * Matteo Destro @@ -66,7 +67,9 @@ All contributors (by number of commits): * Andrew Wason * Radek Senfeld ; `@radek-senfeld `_ * robinechuca +* Nick <24689722+ntjohnson1@users.noreply.github.com> * Benjamin Chrétien <2742231+bchretien@users.noreply.github.com> +* 吴小白 <296015668@qq.com> * davidplowman <38045873+davidplowman@users.noreply.github.com> * Hanz <40712686+HanzCEO@users.noreply.github.com> * Kesh Ikuma <79113787+tikuma-lsuhsc@users.noreply.github.com> @@ -74,7 +77,6 @@ All contributors (by number of commits): * Ian Lee * Ryan Huang * Arthur Barros -* benedikt-grl * Carlos Ruiz * Carlos Ruiz * Maxime Desroches @@ -97,6 +99,7 @@ All contributors (by number of commits): * Orivej Desh * Philipp Krähenbühl * Mattia Procopio +* Max Ehrlich * ramoncaldeira * Roland van Laar * Santiago Castro From 99087dd38ca6a91d07204c001cd9137422684126 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 28 Feb 2026 00:43:15 -0500 Subject: [PATCH 699/846] Point to .../doxygen/8.0/ in doc links --- docs/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index cfb4b4082..b551eb08e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -234,7 +234,7 @@ def ffmpeg_role(name, rawtext, text, lineno, inliner, options={}, content=[]): Converts :ffmpeg:`AVSomething` into proper FFmpeg API documentation links. """ - base_url = "https://ffmpeg.org/doxygen/7.0/struct{}.html" + base_url = "https://ffmpeg.org/doxygen/8.0/struct{}.html" try: struct_name, member = text.split(".") @@ -250,7 +250,7 @@ def ffmpeg_role(name, rawtext, text, lineno, inliner, options={}, content=[]): "av_guess_frame_rate": "group__lavf__misc.html#ga698e6aa73caa9616851092e2be15875d", "av_guess_sample_aspect_ratio": "group__lavf__misc.html#gafa6fbfe5c1bf6792fd6e33475b6056bd", }.get(text, f"struct{text}.html") - url = "https://ffmpeg.org/doxygen/7.0/" + fragment + url = "https://ffmpeg.org/doxygen/8.0/" + fragment else: fragment = { "AVCodecContext.thread_count": "#aa852b6227d0778b62e9cc4034ad3720c", From 3d45d47c6188792c949e1fbbe849a515e731f2c7 Mon Sep 17 00:00:00 2001 From: laggykiller Date: Fri, 6 Mar 2026 04:08:19 +0800 Subject: [PATCH 700/846] Implement `keys()` method in `av.dictionary.Dictionary` to allow calling `dict()` on it --- av/dictionary.py | 3 +++ av/dictionary.pyi | 1 + 2 files changed, 4 insertions(+) diff --git a/av/dictionary.py b/av/dictionary.py index 4735e9fde..bdd02d055 100644 --- a/av/dictionary.py +++ b/av/dictionary.py @@ -42,6 +42,9 @@ def __iter__(self): def __repr__(self): return f"av.Dictionary({dict(self)!r})" + def keys(self): + return list(self) + def copy(self): other: Dictionary = Dictionary() lib.av_dict_copy(cython.address(other.ptr), self.ptr, 0) diff --git a/av/dictionary.pyi b/av/dictionary.pyi index 3d81e7835..7994c1306 100644 --- a/av/dictionary.pyi +++ b/av/dictionary.pyi @@ -7,6 +7,7 @@ class Dictionary: def __len__(self) -> int: ... def __iter__(self) -> Iterator[str]: ... def __repr__(self) -> str: ... + def keys(self) -> Iterable[str]: ... def copy(self) -> Dictionary: ... def pop(self, key: str) -> str: ... def update( From 9c05d4ec6c9ad879e4f96f07464f2519bc6cbf89 Mon Sep 17 00:00:00 2001 From: Santtu Keskinen Date: Thu, 5 Mar 2026 17:00:46 -0500 Subject: [PATCH 701/846] Add reorder_depth setter Co-Authored-By: WyattBlue --- av/video/codeccontext.py | 14 ++++++++++++++ av/video/codeccontext.pyi | 1 + 2 files changed, 15 insertions(+) diff --git a/av/video/codeccontext.py b/av/video/codeccontext.py index 020edf37c..8ef7801bd 100644 --- a/av/video/codeccontext.py +++ b/av/video/codeccontext.py @@ -278,6 +278,20 @@ def has_b_frames(self): """ return bool(self.ptr.has_b_frames) + @property + def reorder_depth(self): + """Raw ``has_b_frames`` value from FFmpeg (int, not bool). + + After :meth:`flush_buffers`, FFmpeg may reset the internal reorder + heuristic. Set this to the known reorder depth *after* seeking to + avoid dropped hierarchical B-frames. + """ + return self.ptr.has_b_frames + + @reorder_depth.setter + def reorder_depth(self, value: cython.int): + self.ptr.has_b_frames = value + @property def coded_width(self): """ diff --git a/av/video/codeccontext.pyi b/av/video/codeccontext.pyi index da72053c4..14c520beb 100644 --- a/av/video/codeccontext.pyi +++ b/av/video/codeccontext.pyi @@ -19,6 +19,7 @@ class VideoCodecContext(CodecContext): sample_aspect_ratio: Fraction | None display_aspect_ratio: Fraction | None has_b_frames: bool + reorder_depth: int max_b_frames: int coded_width: int coded_height: int From 1c7beae9b234b006a05a83f399482290ff4c9090 Mon Sep 17 00:00:00 2001 From: Clay Castronovo <42858023+clayy24@users.noreply.github.com> Date: Fri, 6 Mar 2026 13:45:24 -0700 Subject: [PATCH 702/846] Fix typo --- av/container/input.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/container/input.py b/av/container/input.py index b5833726a..304fe3e3f 100644 --- a/av/container/input.py +++ b/av/container/input.py @@ -227,7 +227,7 @@ def seek( ): """seek(offset, *, backward=True, any_frame=False, stream=None) - Seek to a (key)frame nearsest to the given timestamp. + Seek to a (key)frame nearest to the given timestamp. :param int offset: Time to seek to, expressed in``stream.time_base`` if ``stream`` is given, otherwise in :data:`av.time_base`. From 7ac99aab8d9ba33b43ffe8bc389522b2e8519c9b Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 10 Mar 2026 00:28:21 -0400 Subject: [PATCH 703/846] Fix memory growth when remuxing with add_stream_from_template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit start_encoding() was calling avcodec_open2() on codec contexts created by add_stream_from_template(), fully initializing the codec (e.g. libx264 allocates x264_t, thread pools, reference frames) even when the stream is only used for remuxing and never calls encode()/decode(). After freeing, the C heap retains this memory, causing RSS to grow with each output segment. Add a _template_initialized flag to CodecContext, set by add_stream_from_template(). In start_encoding(), contexts with this flag skip avcodec_open2() — the codec opens lazily via encode() or decode() if actually needed. Fixes #2135 Co-Authored-By: Claude Sonnet 4.6 --- av/codec/context.pxd | 4 ++++ av/container/output.py | 13 ++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/av/codec/context.pxd b/av/codec/context.pxd index 3d87023d7..08d24761e 100644 --- a/av/codec/context.pxd +++ b/av/codec/context.pxd @@ -14,6 +14,10 @@ cdef class CodecContext: # Whether AVCodecContext.extradata should be de-allocated upon destruction. cdef bint extradata_set + # True when created via add_stream_from_template(); start_encoding() skips + # avcodec_open2() and lets encode()/decode() open the codec lazily if needed. + cdef readonly bint _template_initialized + # Used as a signal that this is within a stream, and also for us to access that # stream. This is set "manually" by the stream after constructing this object. cdef int stream_index diff --git a/av/container/output.py b/av/container/output.py index b36cf307f..b5bafc660 100644 --- a/av/container/output.py +++ b/av/container/output.py @@ -193,6 +193,7 @@ def add_stream_from_template( # Construct the user-land stream py_codec_context: CodecContext = wrap_codec_context(ctx, codec, None) + py_codec_context._template_initialized = True py_stream: Stream = wrap_stream(self, stream, py_codec_context) self.streams.add_stream(py_stream) @@ -368,12 +369,14 @@ def start_encoding(self): if not ctx.is_open: for k, v in self.options.items(): ctx.options.setdefault(k, v) - ctx.open() - # Track option consumption. - for k in self.options: - if k not in ctx.options: - used_options.add(k) + if not ctx._template_initialized: + ctx.open() + + # Track option consumption. + for k in self.options: + if k not in ctx.options: + used_options.add(k) stream._finalize_for_output() From 0f462e244138770de303c7ef13edc4d21440c422 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 10 Mar 2026 01:49:24 -0400 Subject: [PATCH 704/846] Be super safe not to break FIPS Disable openssl explicitly to make sure source builds don't have FIPS problems. Probably overkill, but whatever. --- CHANGELOG.rst | 1 + scripts/build-deps | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b8f944112..e02cb1dd8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -47,6 +47,7 @@ Fixes: - Fix :issue:`2149` by :gh-user:`WyattBlue` in (:pr:`2155`). - Fix packet typing based on stream and specify InputContainer.demux based on incoming stream by :gh-user:`ntjohnson1` in (:pr:`2134`). +- Explicitly disable OpenSSL in source builds (``scripts/build-deps``) to prevent accidental OpenSSL linkage that breaks FIPS-enabled systems, addressing :issue:`1972`. v16.1.0 ------- diff --git a/scripts/build-deps b/scripts/build-deps index 5f32cf140..5b9a299c3 100755 --- a/scripts/build-deps +++ b/scripts/build-deps @@ -58,6 +58,7 @@ echo ./configure --disable-static \ --disable-stripping \ --disable-libxml2 \ + --disable-openssl \ --enable-debug=3 \ --enable-gpl \ --enable-version3 \ From ad89f031201cf379ca3d0c0b29277fc3e0a26dcf Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 10 Mar 2026 02:14:54 -0400 Subject: [PATCH 705/846] Add OutputContainer.add_mux_stream() for codec-context-free streams Adds `add_mux_stream(codec_name, rate=None, **kwargs)` to `OutputContainer`, allowing users to create a stream with only `codecpar` set (codec id, type, width, height, sample_rate) and no `CodecContext`. This is useful when muxing pre-encoded packets from an external source where no encoding or decoding is needed, separating the muxer role from the encoder role. Also relaxes `start_encoding()` to allow any stream type without a codec context (previously only data/attachment streams were permitted), and guards `VideoStream`/`AudioStream` repr and `__getattr__` against `codec_context=None`. Two missing fields (`AVMediaType type` on `AVCodecDescriptor`, and `width`, `height`, `sample_rate` on `AVCodecParameters`) are added to the pxd declarations so they can be accessed from Cython. Closes #1970 Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.rst | 1 + av/audio/stream.py | 6 +++ av/container/output.py | 109 +++++++++++++++++++++++++++++++++------- av/container/output.pyi | 6 +++ av/video/stream.py | 7 ++- include/avcodec.pxd | 4 ++ tests/test_remux.py | 48 ++++++++++++++++++ 7 files changed, 161 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e02cb1dd8..b5f2811cd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -37,6 +37,7 @@ Major: Features: +- Add ``OutputContainer.add_mux_stream()`` for creating codec-context-free streams, enabling muxing of pre-encoded packets without an encoder, addressing :issue:`1970` by :gh-user:`WyattBlue`. - Use zero-copy for Packet init from buffer data by :gh-user:`WyattBlue` in (:pr:`2199`). - Expose AVIndexEntry by :gh-user:`Queuecumber` in (:pr:`2136`). - Preserving hardware memory during cuvid decoding, exporting/importing via dlpack by :gh-user:`WyattBlue` in (:pr:`2155`). diff --git a/av/audio/stream.py b/av/audio/stream.py index 7c150c84b..e32f7f759 100644 --- a/av/audio/stream.py +++ b/av/audio/stream.py @@ -6,6 +6,8 @@ @cython.cclass class AudioStream(Stream): def __repr__(self): + if self.codec_context is None: + return f" at 0x{id(self):x}>" form = self.format.name if self.format else None return ( f" Stream: + """add_mux_stream(codec_name, rate=None) + + Creates a new stream for muxing pre-encoded data without creating a + :class:`.CodecContext`. Use this when you want to mux packets that were + already encoded externally and no encoding/decoding is needed. + + :param codec_name: The name of a codec. + :type codec_name: str + :param \\**kwargs: Set attributes for the stream (e.g. ``width``, ``height``, + ``time_base``). + :rtype: The new :class:`~av.stream.Stream`. + + """ + # Find the codec to get its id and type (try encoder first, then decoder). + codec_name_bytes: bytes = codec_name.encode() + codec: cython.pointer[cython.const[lib.AVCodec]] = ( + lib.avcodec_find_encoder_by_name(codec_name_bytes) + ) + codec_descriptor: cython.pointer[cython.const[lib.AVCodecDescriptor]] = ( + cython.NULL + ) + if codec == cython.NULL: + codec = lib.avcodec_find_decoder_by_name(codec_name_bytes) + if codec == cython.NULL: + codec_descriptor = lib.avcodec_descriptor_get_by_name(codec_name_bytes) + if codec_descriptor == cython.NULL: + raise ValueError(f"Unknown codec: {codec_name!r}") + + codec_id: lib.AVCodecID + codec_type: lib.AVMediaType + if codec != cython.NULL: + codec_id = codec.id + codec_type = codec.type + else: + codec_id = codec_descriptor.id + codec_type = codec_descriptor.type + + # Assert that this format supports the requested codec. + if not lib.avformat_query_codec( + self.ptr.oformat, codec_id, lib.FF_COMPLIANCE_NORMAL + ): + raise ValueError( + f"{self.format.name!r} format does not support {codec_name!r} codec" + ) + + # Create stream with no codec context. + stream: cython.pointer[lib.AVStream] = lib.avformat_new_stream( + self.ptr, cython.NULL + ) + if stream == cython.NULL: + raise MemoryError("Could not allocate stream") + + stream.codecpar.codec_id = codec_id + stream.codecpar.codec_type = codec_type + + if codec_type == lib.AVMEDIA_TYPE_VIDEO: + stream.codecpar.width = kwargs.pop("width", 0) + stream.codecpar.height = kwargs.pop("height", 0) + if rate is not None: + to_avrational(rate, cython.address(stream.avg_frame_rate)) + elif codec_type == lib.AVMEDIA_TYPE_AUDIO: + if rate is not None: + if type(rate) is int: + stream.codecpar.sample_rate = rate + else: + raise TypeError("audio stream `rate` must be: int | None") + + # Construct the user-land stream (no codec context). + py_stream: Stream = wrap_stream(self, stream, None) + self.streams.add_stream(py_stream) + + for k, v in kwargs.items(): + setattr(py_stream, k, v) + + return py_stream + def add_stream_from_template( self, template: Stream, opaque: bool | None = None, **kwargs ): @@ -291,13 +368,12 @@ def add_data_stream(self, codec_name=None, options: dict | None = None): ) if codec_name is not None: - codec = lib.avcodec_find_encoder_by_name(codec_name.encode()) + codec_name_bytes: bytes = codec_name.encode() + codec = lib.avcodec_find_encoder_by_name(codec_name_bytes) if codec == cython.NULL: - codec = lib.avcodec_find_decoder_by_name(codec_name.encode()) + codec = lib.avcodec_find_decoder_by_name(codec_name_bytes) if codec == cython.NULL: - codec_descriptor = lib.avcodec_descriptor_get_by_name( - codec_name.encode() - ) + codec_descriptor = lib.avcodec_descriptor_get_by_name(codec_name_bytes) if codec_descriptor == cython.NULL: raise ValueError(f"Unknown data codec: {codec_name}") @@ -361,22 +437,17 @@ def start_encoding(self): # Finalize and open all streams. for stream in self.streams: ctx = stream.codec_context - # Skip codec context handling for streams without codecs (e.g. data/attachments). - if ctx is None: - if stream.type not in {"data", "attachment"}: - raise ValueError(f"Stream {stream.index} has no codec context") - else: - if not ctx.is_open: - for k, v in self.options.items(): - ctx.options.setdefault(k, v) + if ctx is not None and not ctx.is_open: + for k, v in self.options.items(): + ctx.options.setdefault(k, v) - if not ctx._template_initialized: - ctx.open() + if not ctx._template_initialized: + ctx.open() - # Track option consumption. - for k in self.options: - if k not in ctx.options: - used_options.add(k) + # Track option consumption. + for k in self.options: + if k not in ctx.options: + used_options.add(k) stream._finalize_for_output() diff --git a/av/container/output.pyi b/av/container/output.pyi index 3fa18243c..de2e4950a 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -39,6 +39,12 @@ class OutputContainer(Container): options: dict[str, str] | None = None, **kwargs, ) -> VideoStream | AudioStream | SubtitleStream: ... + def add_mux_stream( + self, + codec_name: str, + rate: Fraction | int | None = None, + **kwargs, + ) -> Stream: ... def add_stream_from_template( self, template: _StreamT, opaque: bool | None = None, **kwargs ) -> _StreamT: ... diff --git a/av/video/stream.py b/av/video/stream.py index 658df3d5c..d5b2c106d 100644 --- a/av/video/stream.py +++ b/av/video/stream.py @@ -8,6 +8,8 @@ @cython.cclass class VideoStream(Stream): def __repr__(self): + if self.codec_context is None: + return f" at 0x{id(self):x}>" return ( f" None: packet_count += 1 assert packet_count > 50 + + +def test_add_mux_stream_video() -> None: + """add_mux_stream creates a video stream without a CodecContext.""" + input_path = av.datasets.curated("pexels/time-lapse-video-of-night-sky-857195.mp4") + + buf = io.BytesIO() + with av.open(input_path) as input_: + in_stream = input_.streams.video[0] + width = in_stream.codec_context.width + height = in_stream.codec_context.height + + with av.open(buf, "w", format="mp4") as output: + out_stream = output.add_mux_stream( + in_stream.codec_context.name, width=width, height=height + ) + assert out_stream.codec_context is None + assert out_stream.type == "video" + + out_stream.time_base = in_stream.time_base + + for packet in input_.demux(in_stream): + if packet.dts is None: + continue + packet.stream = out_stream + output.mux(packet) + + buf.seek(0) + with av.open(buf) as result: + assert len(result.streams.video) == 1 + assert result.streams.video[0].codec_context.width == width + assert result.streams.video[0].codec_context.height == height + + +def test_add_mux_stream_no_codec_context() -> None: + """add_mux_stream streams have no codec context and repr does not crash.""" + buf = io.BytesIO() + with av.open(buf, "w", format="mp4") as output: + video_stream = output.add_mux_stream("h264", width=1920, height=1080) + audio_stream = output.add_mux_stream("aac", rate=44100) + + assert video_stream.codec_context is None + assert audio_stream.codec_context is None + # repr should not crash + assert "video/" in repr(video_stream) + assert "audio/" in repr(audio_stream) From af163b7e6a6e821617399cb2a5c39179992853c6 Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Wed, 11 Mar 2026 02:50:38 +0000 Subject: [PATCH 706/846] Replace `sws_scale` with modern `sws_scale_frame` for reformatting --- av/video/frame.py | 2 +- av/video/reformatter.pxd | 55 ++-------------- av/video/reformatter.py | 133 +++++++++++---------------------------- 3 files changed, 44 insertions(+), 146 deletions(-) diff --git a/av/video/frame.py b/av/video/frame.py index 586d317b5..d2c549f19 100644 --- a/av/video/frame.py +++ b/av/video/frame.py @@ -472,7 +472,7 @@ def _init(self, format: lib.AVPixelFormat, width: cython.uint, height: cython.ui self.ptr.height = height self.ptr.format = format - # We enforce aligned buffers, otherwise `sws_scale` can perform + # We enforce aligned buffers, otherwise `sws_scale_frame` can perform # poorly or even cause out-of-bounds reads and writes. if width and height: res = lib.av_frame_get_buffer(self.ptr, 16) diff --git a/av/video/reformatter.pxd b/av/video/reformatter.pxd index f57ba7e77..7729c559c 100644 --- a/av/video/reformatter.pxd +++ b/av/video/reformatter.pxd @@ -1,14 +1,13 @@ cimport libav as lib -from libc.stdint cimport uint8_t from av.video.frame cimport VideoFrame cdef extern from "libswscale/swscale.h" nogil: cdef struct SwsContext: - pass - cdef struct SwsFilter: - pass + unsigned flags + int threads + cdef int SWS_FAST_BILINEAR cdef int SWS_BILINEAR cdef int SWS_BICUBIC @@ -28,50 +27,9 @@ cdef extern from "libswscale/swscale.h" nogil: cdef int SWS_CS_SMPTE240M cdef int SWS_CS_DEFAULT - cdef int sws_scale( - SwsContext *ctx, - const uint8_t *const *src_slice, - const int *src_stride, - int src_slice_y, - int src_slice_h, - unsigned char *const *dst_slice, - const int *dst_stride, - ) - cdef void sws_freeContext(SwsContext *ctx) - cdef SwsContext *sws_getCachedContext( - SwsContext *context, - int src_width, - int src_height, - lib.AVPixelFormat src_format, - int dst_width, - int dst_height, - lib.AVPixelFormat dst_format, - int flags, - SwsFilter *src_filter, - SwsFilter *dst_filter, - double *param, - ) - cdef const int* sws_getCoefficients(int colorspace) - cdef int sws_getColorspaceDetails( - SwsContext *context, - int **inv_table, - int *srcRange, - int **table, - int *dstRange, - int *brightness, - int *contrast, - int *saturation - ) - cdef int sws_setColorspaceDetails( - SwsContext *context, - const int inv_table[4], - int srcRange, - const int table[4], - int dstRange, - int brightness, - int contrast, - int saturation - ) + cdef SwsContext *sws_alloc_context() + cdef void sws_free_context(SwsContext **ctx) + cdef int sws_scale_frame(SwsContext *c, lib.AVFrame *dst, const lib.AVFrame *src) cdef class VideoReformatter: cdef SwsContext *ptr @@ -79,6 +37,5 @@ cdef class VideoReformatter: lib.AVPixelFormat format, int src_colorspace, int dst_colorspace, int interpolation, int src_color_range, int dst_color_range, - bint set_dst_colorspace, bint set_dst_color_range, int dst_color_trc, int dst_color_primaries, bint set_dst_color_trc, bint set_dst_color_primaries) diff --git a/av/video/reformatter.py b/av/video/reformatter.py index 9c201bbe9..29d4c582c 100644 --- a/av/video/reformatter.py +++ b/av/video/reformatter.py @@ -105,6 +105,19 @@ def _resolve_enum_value( raise ValueError(f"Cannot convert {value} to {enum_class.__name__}") +@cython.cfunc +def _set_frame_colorspace( + frame: VideoFrame, colorspace: cython.int, color_range: cython.int +): + """Set AVFrame colorspace/range from SWS_CS_* and AVColorRange values.""" + if colorspace in _SWS_CS_TO_AVCOL_SPC: + frame.ptr.colorspace = cython.cast( + lib.AVColorSpace, _SWS_CS_TO_AVCOL_SPC[colorspace] + ) + if color_range != lib.AVCOL_RANGE_UNSPECIFIED: + frame.ptr.color_range = cython.cast(lib.AVColorRange, color_range) + + # Mapping from SWS_CS_* (swscale colorspace) to AVColorSpace (frame metadata). # Note: SWS_CS_ITU601, SWS_CS_ITU624, SWS_CS_SMPTE170M, and SWS_CS_DEFAULT all have # the same value (5), so we map 5 -> AVCOL_SPC_SMPTE170M as the most common case. @@ -131,7 +144,7 @@ class VideoReformatter: def __dealloc__(self): with cython.nogil: - sws_freeContext(self.ptr) + sws_free_context(cython.address(self.ptr)) def reformat( self, @@ -195,8 +208,6 @@ def reformat( ) # Track whether user explicitly specified destination metadata - set_dst_colorspace: cython.bint = dst_colorspace is not None - set_dst_color_range: cython.bint = dst_color_range is not None set_dst_color_trc: cython.bint = dst_color_trc is not None set_dst_color_primaries: cython.bint = dst_color_primaries is not None @@ -210,8 +221,6 @@ def reformat( c_interpolation, c_src_color_range, c_dst_color_range, - set_dst_colorspace, - set_dst_color_range, c_dst_color_trc, c_dst_color_primaries, set_dst_color_trc, @@ -230,8 +239,6 @@ def _reformat( interpolation: cython.int, src_color_range: cython.int, dst_color_range: cython.int, - set_dst_colorspace: cython.bint, - set_dst_color_range: cython.bint, dst_color_trc: cython.int, dst_color_primaries: cython.int, set_dst_color_trc: cython.bint, @@ -240,14 +247,6 @@ def _reformat( if frame.ptr.format < 0: raise ValueError("Frame does not have format set.") - # Save original values to set on the output frame (before swscale conversion) - frame_dst_colorspace = dst_colorspace - frame_dst_color_range = dst_color_range - - # The definition of color range in pixfmt.h and swscale.h is different. - src_color_range = 1 if src_color_range == ColorRange.JPEG.value else 0 - dst_color_range = 1 if dst_color_range == ColorRange.JPEG.value else 0 - src_format = cython.cast(lib.AVPixelFormat, frame.ptr.format) # Shortcut! @@ -281,81 +280,34 @@ def _reformat( ): return frame - with cython.nogil: - self.ptr = sws_getCachedContext( - self.ptr, - frame.ptr.width, - frame.ptr.height, - src_format, - width, - height, - dst_format, - interpolation, - cython.NULL, - cython.NULL, - cython.NULL, - ) - - # We want to change the colorspace/color_range transforms. - # We do that by grabbing all the current settings, changing a - # couple, and setting them all. We need a lot of state here. - inv_tbl: cython.p_int - tbl: cython.p_int - src_colorspace_range: cython.int - dst_colorspace_range: cython.int - brightness: cython.int - contrast: cython.int - saturation: cython.int - - if src_colorspace != dst_colorspace or src_color_range != dst_color_range: - with cython.nogil: - ret = sws_getColorspaceDetails( - self.ptr, - cython.address(inv_tbl), - cython.address(src_colorspace_range), - cython.address(tbl), - cython.address(dst_colorspace_range), - cython.address(brightness), - cython.address(contrast), - cython.address(saturation), - ) - err_check(ret) - - with cython.nogil: - # Grab the coefficients for the requested transforms. - # The inv_table brings us to linear, and `tbl` to the new space. - if src_colorspace != SWS_CS_DEFAULT: - inv_tbl = cython.cast( - cython.p_int, sws_getCoefficients(src_colorspace) - ) - if dst_colorspace != SWS_CS_DEFAULT: - tbl = cython.cast(cython.p_int, sws_getCoefficients(dst_colorspace)) - - ret = sws_setColorspaceDetails( - self.ptr, - inv_tbl, - src_color_range, - tbl, - dst_color_range, - brightness, - contrast, - saturation, - ) - err_check(ret) + if self.ptr == cython.NULL: + self.ptr = sws_alloc_context() + if self.ptr == cython.NULL: + raise MemoryError("Could not allocate SwsContext") + # TODO(lgeiger): Enable multi-threading for sws_scale_frame. + self.ptr.threads = 1 + self.ptr.flags = cython.cast(cython.uint, interpolation) new_frame: VideoFrame = alloc_video_frame() new_frame._copy_internal_attributes(frame) new_frame._init(dst_format, width, height) - # Set the colorspace and color_range on the output frame only if explicitly specified - if set_dst_colorspace and frame_dst_colorspace in _SWS_CS_TO_AVCOL_SPC: - new_frame.ptr.colorspace = cython.cast( - lib.AVColorSpace, _SWS_CS_TO_AVCOL_SPC[frame_dst_colorspace] - ) - if set_dst_color_range: - new_frame.ptr.color_range = cython.cast( - lib.AVColorRange, frame_dst_color_range - ) + # Set source frame colorspace/range so sws_scale_frame can read it + frame_src_colorspace: lib.AVColorSpace = frame.ptr.colorspace + frame_src_color_range: lib.AVColorRange = frame.ptr.color_range + _set_frame_colorspace(frame, src_colorspace, src_color_range) + _set_frame_colorspace(new_frame, dst_colorspace, dst_color_range) + + with cython.nogil: + ret = sws_scale_frame(self.ptr, new_frame.ptr, frame.ptr) + + # Restore source frame colorspace/range to avoid side effects + frame.ptr.colorspace = frame_src_colorspace + frame.ptr.color_range = frame_src_color_range + + err_check(ret) + + # Set metadata-only properties on the output frame if explicitly specified if set_dst_color_trc: new_frame.ptr.color_trc = cython.cast( lib.AVColorTransferCharacteristic, dst_color_trc @@ -365,15 +317,4 @@ def _reformat( lib.AVColorPrimaries, dst_color_primaries ) - with cython.nogil: - sws_scale( - self.ptr, - cython.cast("const unsigned char *const *", frame.ptr.data), - cython.cast("const int *", frame.ptr.linesize), - 0, # slice Y - frame.ptr.height, - new_frame.ptr.data, - new_frame.ptr.linesize, - ) - return new_frame From 0e7acc1ac87e6e3b52b5583d44dc912b3b4b2a33 Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Wed, 11 Mar 2026 03:08:08 +0000 Subject: [PATCH 707/846] Enable multithreaded reformatting --- av/video/codeccontext.py | 6 +++++- av/video/frame.py | 2 +- av/video/frame.pyi | 1 + av/video/reformatter.pxd | 3 ++- av/video/reformatter.py | 9 +++++++-- av/video/reformatter.pyi | 1 + 6 files changed, 17 insertions(+), 5 deletions(-) diff --git a/av/video/codeccontext.py b/av/video/codeccontext.py index 8ef7801bd..e62bf8fae 100644 --- a/av/video/codeccontext.py +++ b/av/video/codeccontext.py @@ -98,7 +98,11 @@ def _prepare_frames_for_encode(self, input: Frame | None) -> list: self.reformatter = VideoReformatter() vframe = self.reformatter.reformat( - vframe, self.ptr.width, self.ptr.height, self._format + vframe, + self.ptr.width, + self.ptr.height, + self._format, + threads=self.ptr.thread_count, ) # There is no pts, so create one. diff --git a/av/video/frame.py b/av/video/frame.py index d2c549f19..74be1128d 100644 --- a/av/video/frame.py +++ b/av/video/frame.py @@ -622,7 +622,7 @@ def color_primaries(self, value): self.ptr.color_primaries = value def reformat(self, *args, **kwargs): - """reformat(width=None, height=None, format=None, src_colorspace=None, dst_colorspace=None, interpolation=None) + """reformat(width=None, height=None, format=None, src_colorspace=None, dst_colorspace=None, interpolation=None, threads=None) Create a new :class:`VideoFrame` with the given width/height/format/colorspace. diff --git a/av/video/frame.pyi b/av/video/frame.pyi index ba3c92fed..12a85182b 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -70,6 +70,7 @@ class VideoFrame(Frame): dst_color_range: int | str | None = None, dst_color_trc: int | ColorTrc | None = None, dst_color_primaries: int | ColorPrimaries | None = None, + threads: int | None = None, ) -> VideoFrame: ... def to_rgb(self, **kwargs: Any) -> VideoFrame: ... def save(self, filepath: str | Path) -> None: ... diff --git a/av/video/reformatter.pxd b/av/video/reformatter.pxd index 7729c559c..e68a70105 100644 --- a/av/video/reformatter.pxd +++ b/av/video/reformatter.pxd @@ -38,4 +38,5 @@ cdef class VideoReformatter: int dst_colorspace, int interpolation, int src_color_range, int dst_color_range, int dst_color_trc, int dst_color_primaries, - bint set_dst_color_trc, bint set_dst_color_primaries) + bint set_dst_color_trc, bint set_dst_color_primaries, + int threads) diff --git a/av/video/reformatter.py b/av/video/reformatter.py index 29d4c582c..982c86450 100644 --- a/av/video/reformatter.py +++ b/av/video/reformatter.py @@ -159,6 +159,7 @@ def reformat( dst_color_range=None, dst_color_trc=None, dst_color_primaries=None, + threads=None, ): """Create a new :class:`VideoFrame` with the given width/height/format/colorspace. @@ -185,6 +186,8 @@ def reformat( :param dst_color_primaries: Desired color primaries to tag on the output frame, or ``None`` to preserve the source frame's value. :type dst_color_primaries: :class:`ColorPrimaries` or ``int`` + :param int threads: How many threads to use for scaling, or ``0`` for automatic + selection based on the number of available CPUs. Defaults to ``0`` (auto). """ @@ -206,6 +209,7 @@ def reformat( c_dst_color_primaries = _resolve_enum_value( dst_color_primaries, ColorPrimaries, 0 ) + c_threads: cython.int = threads if threads is not None else 0 # Track whether user explicitly specified destination metadata set_dst_color_trc: cython.bint = dst_color_trc is not None @@ -225,6 +229,7 @@ def reformat( c_dst_color_primaries, set_dst_color_trc, set_dst_color_primaries, + c_threads, ) @cython.cfunc @@ -243,6 +248,7 @@ def _reformat( dst_color_primaries: cython.int, set_dst_color_trc: cython.bint, set_dst_color_primaries: cython.bint, + threads: cython.int, ): if frame.ptr.format < 0: raise ValueError("Frame does not have format set.") @@ -284,8 +290,7 @@ def _reformat( self.ptr = sws_alloc_context() if self.ptr == cython.NULL: raise MemoryError("Could not allocate SwsContext") - # TODO(lgeiger): Enable multi-threading for sws_scale_frame. - self.ptr.threads = 1 + self.ptr.threads = threads self.ptr.flags = cython.cast(cython.uint, interpolation) new_frame: VideoFrame = alloc_video_frame() diff --git a/av/video/reformatter.pyi b/av/video/reformatter.pyi index b1e51e984..c9071df49 100644 --- a/av/video/reformatter.pyi +++ b/av/video/reformatter.pyi @@ -85,4 +85,5 @@ class VideoReformatter: dst_color_range: int | str | None = None, dst_color_trc: int | ColorTrc | None = None, dst_color_primaries: int | ColorPrimaries | None = None, + threads: int | None = None, ) -> VideoFrame: ... From 4baa626fe940e4aa019f17411beb009942ad7f3d Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Wed, 11 Mar 2026 04:40:30 +0000 Subject: [PATCH 708/846] Reduce Python interactions --- av/video/reformatter.py | 42 +++++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/av/video/reformatter.py b/av/video/reformatter.py index 982c86450..a29283717 100644 --- a/av/video/reformatter.py +++ b/av/video/reformatter.py @@ -1,6 +1,7 @@ from enum import IntEnum import cython +import cython.cimports.libav as lib from cython.cimports.av.error import err_check from cython.cimports.av.video.format import VideoFormat from cython.cimports.av.video.frame import alloc_video_frame @@ -107,30 +108,25 @@ def _resolve_enum_value( @cython.cfunc def _set_frame_colorspace( - frame: VideoFrame, colorspace: cython.int, color_range: cython.int + frame: cython.pointer(lib.AVFrame), + colorspace: cython.int, + color_range: cython.int, ): """Set AVFrame colorspace/range from SWS_CS_* and AVColorRange values.""" - if colorspace in _SWS_CS_TO_AVCOL_SPC: - frame.ptr.colorspace = cython.cast( - lib.AVColorSpace, _SWS_CS_TO_AVCOL_SPC[colorspace] - ) if color_range != lib.AVCOL_RANGE_UNSPECIFIED: - frame.ptr.color_range = cython.cast(lib.AVColorRange, color_range) - - -# Mapping from SWS_CS_* (swscale colorspace) to AVColorSpace (frame metadata). -# Note: SWS_CS_ITU601, SWS_CS_ITU624, SWS_CS_SMPTE170M, and SWS_CS_DEFAULT all have -# the same value (5), so we map 5 -> AVCOL_SPC_SMPTE170M as the most common case. -# SWS_CS_DEFAULT is handled specially by not setting frame metadata. -_SWS_CS_TO_AVCOL_SPC = cython.declare( - dict, - { - SWS_CS_ITU709: lib.AVCOL_SPC_BT709, - SWS_CS_FCC: lib.AVCOL_SPC_FCC, - SWS_CS_ITU601: lib.AVCOL_SPC_SMPTE170M, - SWS_CS_SMPTE240M: lib.AVCOL_SPC_SMPTE240M, - }, -) + frame.color_range = cython.cast(lib.AVColorRange, color_range) + # Mapping from SWS_CS_* (swscale colorspace) to AVColorSpace (frame metadata). + # Note: SWS_CS_ITU601, SWS_CS_ITU624, SWS_CS_SMPTE170M, and SWS_CS_DEFAULT all have + # the same value (5), so we map 5 -> AVCOL_SPC_SMPTE170M as the most common case. + # SWS_CS_DEFAULT is handled specially by not setting frame metadata. + if colorspace == SWS_CS_ITU709: + frame.colorspace = lib.AVCOL_SPC_BT709 + elif colorspace == SWS_CS_FCC: + frame.colorspace = lib.AVCOL_SPC_FCC + elif colorspace == SWS_CS_ITU601: + frame.colorspace = lib.AVCOL_SPC_SMPTE170M + elif colorspace == SWS_CS_SMPTE240M: + frame.colorspace = lib.AVCOL_SPC_SMPTE240M @cython.cclass @@ -300,8 +296,8 @@ def _reformat( # Set source frame colorspace/range so sws_scale_frame can read it frame_src_colorspace: lib.AVColorSpace = frame.ptr.colorspace frame_src_color_range: lib.AVColorRange = frame.ptr.color_range - _set_frame_colorspace(frame, src_colorspace, src_color_range) - _set_frame_colorspace(new_frame, dst_colorspace, dst_color_range) + _set_frame_colorspace(frame.ptr, src_colorspace, src_color_range) + _set_frame_colorspace(new_frame.ptr, dst_colorspace, dst_color_range) with cython.nogil: ret = sws_scale_frame(self.ptr, new_frame.ptr, frame.ptr) From e0103cf7082f17919be0fd8970438e56f6baa07d Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Wed, 11 Mar 2026 12:53:37 +0000 Subject: [PATCH 709/846] Reduce Python overhead in reformat --- av/video/reformatter.py | 59 ++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/av/video/reformatter.py b/av/video/reformatter.py index a29283717..202c32115 100644 --- a/av/video/reformatter.py +++ b/av/video/reformatter.py @@ -3,7 +3,7 @@ import cython import cython.cimports.libav as lib from cython.cimports.av.error import err_check -from cython.cimports.av.video.format import VideoFormat +from cython.cimports.av.video.format import VideoFormat, get_pix_fmt from cython.cimports.av.video.frame import alloc_video_frame @@ -91,6 +91,7 @@ class ColorPrimaries(IntEnum): @cython.cfunc +@cython.inline def _resolve_enum_value( value: object, enum_class: object, default: cython.int ) -> cython.int: @@ -106,6 +107,16 @@ def _resolve_enum_value( raise ValueError(f"Cannot convert {value} to {enum_class.__name__}") +@cython.cfunc +@cython.inline +def _resolve_format(format: object, default: lib.AVPixelFormat) -> lib.AVPixelFormat: + if format is None: + return default + if isinstance(format, VideoFormat): + return cython.cast(VideoFormat, format).pix_fmt + return get_pix_fmt(format) + + @cython.cfunc def _set_frame_colorspace( frame: cython.pointer(lib.AVFrame), @@ -186,18 +197,15 @@ def reformat( selection based on the number of available CPUs. Defaults to ``0`` (auto). """ - - video_format: VideoFormat = VideoFormat( - format if format is not None else frame.format - ) + c_dst_format = _resolve_format(format, frame.format.pix_fmt) c_src_colorspace = _resolve_enum_value( - src_colorspace, Colorspace, frame.colorspace + src_colorspace, Colorspace, frame.ptr.colorspace ) c_dst_colorspace = _resolve_enum_value( - dst_colorspace, Colorspace, frame.colorspace + dst_colorspace, Colorspace, frame.ptr.colorspace ) c_interpolation = _resolve_enum_value( - interpolation, Interpolation, int(Interpolation.BILINEAR) + interpolation, Interpolation, SWS_BILINEAR ) c_src_color_range = _resolve_enum_value(src_color_range, ColorRange, 0) c_dst_color_range = _resolve_enum_value(dst_color_range, ColorRange, 0) @@ -206,6 +214,8 @@ def reformat( dst_color_primaries, ColorPrimaries, 0 ) c_threads: cython.int = threads if threads is not None else 0 + c_width: cython.int = width if width is not None else frame.ptr.width + c_height: cython.int = height if height is not None else frame.ptr.height # Track whether user explicitly specified destination metadata set_dst_color_trc: cython.bint = dst_color_trc is not None @@ -213,9 +223,9 @@ def reformat( return self._reformat( frame, - width or frame.ptr.width, - height or frame.ptr.height, - video_format.pix_fmt, + c_width, + c_height, + c_dst_format, c_src_colorspace, c_dst_colorspace, c_interpolation, @@ -252,25 +262,6 @@ def _reformat( src_format = cython.cast(lib.AVPixelFormat, frame.ptr.format) # Shortcut! - if frame.ptr.hw_frames_ctx: - if ( - dst_format == src_format - and width == frame.ptr.width - and height == frame.ptr.height - and dst_colorspace == src_colorspace - and src_color_range == dst_color_range - and not set_dst_color_trc - and not set_dst_color_primaries - ): - return frame - - frame_sw = alloc_video_frame() - err_check(lib.av_hwframe_transfer_data(frame_sw.ptr, frame.ptr, 0)) - frame_sw.pts = frame.pts - frame_sw._init_user_attributes() - frame = frame_sw - src_format = cython.cast(lib.AVPixelFormat, frame.ptr.format) - if ( dst_format == src_format and width == frame.ptr.width @@ -282,6 +273,14 @@ def _reformat( ): return frame + if frame.ptr.hw_frames_ctx: + frame_sw = alloc_video_frame() + err_check(lib.av_hwframe_transfer_data(frame_sw.ptr, frame.ptr, 0)) + frame_sw.pts = frame.pts + frame_sw._init_user_attributes() + frame = frame_sw + src_format = cython.cast(lib.AVPixelFormat, frame.ptr.format) + if self.ptr == cython.NULL: self.ptr = sws_alloc_context() if self.ptr == cython.NULL: From 20f6b7e7f8e70722c3dd57cc1827c6c6efed4b1f Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Wed, 11 Mar 2026 15:53:28 +0000 Subject: [PATCH 710/846] Add missing `SWS_SPLINE` interpolation (#2188) --- av/video/reformatter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/av/video/reformatter.py b/av/video/reformatter.py index 202c32115..96c1b1742 100644 --- a/av/video/reformatter.py +++ b/av/video/reformatter.py @@ -17,7 +17,8 @@ class Interpolation(IntEnum): BICUBLIN: "Luma bicubic / chroma bilinear" = SWS_BICUBLIN GAUSS: "Gaussian" = SWS_GAUSS SINC: "Sinc" = SWS_SINC - LANCZOS: "Bicubic spline" = SWS_LANCZOS + LANCZOS: "3-tap sinc/sinc" = SWS_LANCZOS + SPLINE: "Cubic Keys spline" = SWS_SPLINE class Colorspace(IntEnum): From c8356fc788e6b3da52de0e34388e959e09214229 Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Wed, 11 Mar 2026 19:52:05 +0000 Subject: [PATCH 711/846] Prevent data copy in `VideoFrame.to_ndarray()` for padded frames (#2190) --- av/video/frame.py | 94 ++++++++++++++++++++++++++++------------------- av/video/plane.py | 2 +- 2 files changed, 57 insertions(+), 39 deletions(-) diff --git a/av/video/frame.py b/av/video/frame.py index 74be1128d..5212db292 100644 --- a/av/video/frame.py +++ b/av/video/frame.py @@ -374,9 +374,13 @@ class PictureType(IntEnum): BI = lib.AV_PICTURE_TYPE_BI # BI type +_is_big_endian = cython.declare(cython.bint, sys.byteorder == "big") + + @cython.cfunc +@cython.inline def byteswap_array(array, big_endian: cython.bint): - if (sys.byteorder == "big") != big_endian: + if _is_big_endian != big_endian: return array.byteswap() return array @@ -429,23 +433,31 @@ def copy_array_to_plane(array, plane: VideoPlane, bytes_per_pixel: cython.uint): @cython.cfunc +@cython.inline def useful_array( plane: VideoPlane, bytes_per_pixel: cython.uint = 1, dtype: str = "uint8" ): """ - Return the useful part of the VideoPlane as a single dimensional array. + Return the useful part of the VideoPlane as a strided array. - We are simply discarding any padding which was added for alignment. + We are simply creating a view that discards any padding which was added for + alignment. """ import numpy as np - total_line_size: cython.size_t = abs(plane.line_size) - useful_line_size: cython.size_t = plane.width * bytes_per_pixel - if total_line_size == useful_line_size: - return np.frombuffer(plane, dtype=dtype) - arr = np.frombuffer(plane, np.uint8) - arr = arr.reshape(-1, total_line_size)[:, 0:useful_line_size].reshape(-1) - return arr.view(np.dtype(dtype)) + dtype_obj = np.dtype(dtype) + total_line_size = abs(plane.frame.ptr.linesize[plane.index]) + itemsize = dtype_obj.itemsize + channels = bytes_per_pixel // itemsize + + if channels == 1: + shape = (plane.height, plane.width) + strides = (total_line_size, itemsize) + else: + shape = (plane.height, plane.width, channels) + strides = (total_line_size, bytes_per_pixel, itemsize) + + return np.ndarray(shape, dtype=dtype_obj, buffer=plane, strides=strides) @cython.cfunc @@ -527,6 +539,8 @@ def planes(self): plane_count: cython.int = 0 while plane_count < max_plane_count and self.ptr.extended_data[plane_count]: plane_count += 1 + if plane_count == 1: + return (VideoPlane(self, 0),) return tuple([VideoPlane(self, i) for i in range(plane_count)]) @property @@ -744,49 +758,50 @@ def to_ndarray(self, channel_last=False, **kwargs): # check size format_name = frame.format.name - height, width = frame.ptr.height, frame.ptr.width planes: tuple[VideoPlane, ...] = frame.planes - if format_name in {"yuv420p", "yuvj420p", "yuyv422", "yuv422p10le", "yuv422p"}: - assert width % 2 == 0, "the width has to be even for this pixel format" - assert height % 2 == 0, "the height has to be even for this pixel format" - # cases planes are simply concatenated in shape (height, width, channels) if format_name in _np_pix_fmt_dtypes: + if format_name == "yuyv422": + assert frame.ptr.width % 2 == 0, "width has to be even for yuyv422" + assert frame.ptr.height % 2 == 0, "height has to be even for yuyv422" itemsize: cython.uint itemsize, dtype = _np_pix_fmt_dtypes[format_name] - if len(planes) == 1: # shortcut, avoid memory copy - array = useful_array(planes[0], itemsize, dtype).reshape( - height, width, -1 - ) + num_planes: cython.size_t = len(planes) + if num_planes == 1: # shortcut, avoid memory copy + array = useful_array(planes[0], itemsize, dtype) else: # general case - array = np.empty((height, width, len(planes)), dtype=dtype) - for i, plane in enumerate(planes): - array[:, :, i] = useful_array(plane, itemsize, dtype).reshape( - height, width - ) + array = np.empty( + (frame.ptr.height, frame.ptr.width, num_planes), dtype=dtype + ) + if format_name.startswith("gbr"): + plane_indices = (2, 0, 1, *range(3, num_planes)) + else: + plane_indices = range(num_planes) + for i, p_idx in enumerate(plane_indices): + array[:, :, i] = useful_array(planes[p_idx], itemsize, dtype) array = byteswap_array(array, format_name.endswith("be")) - if array.shape[2] == 1: # skip last channel for gray images - return array.squeeze(2) - if format_name.startswith("gbr"): # gbr -> rgb - array[:, :, :3] = array[:, :, [2, 0, 1]] if not channel_last and format_name in {"yuv444p", "yuvj444p"}: array = np.moveaxis(array, 2, 0) return array # special cases if format_name in {"yuv420p", "yuvj420p", "yuv422p"}: + assert frame.ptr.width % 2 == 0, "width has to be even for this format" + assert frame.ptr.height % 2 == 0, "height has to be even for this format" return np.hstack( [ - useful_array(planes[0]), - useful_array(planes[1]), - useful_array(planes[2]), + useful_array(planes[0]).reshape(-1), + useful_array(planes[1]).reshape(-1), + useful_array(planes[2]).reshape(-1), ] - ).reshape(-1, width) + ).reshape(-1, frame.ptr.width) if format_name == "yuv422p10le": + assert frame.ptr.width % 2 == 0, "width has to be even for this format" + assert frame.ptr.height % 2 == 0, "height has to be even for this format" # Read planes as uint16 at their original width - y = useful_array(planes[0], 2, "uint16").reshape(height, width) - u = useful_array(planes[1], 2, "uint16").reshape(height, width // 2) - v = useful_array(planes[2], 2, "uint16").reshape(height, width // 2) + y = useful_array(planes[0], 2, "uint16") + u = useful_array(planes[1], 2, "uint16") + v = useful_array(planes[2], 2, "uint16") # Double the width of U and V by repeating each value u_full = np.repeat(u, 2, axis=1) @@ -795,7 +810,7 @@ def to_ndarray(self, channel_last=False, **kwargs): return np.stack([y, u_full, v_full], axis=2) return np.stack([y, u_full, v_full], axis=0) if format_name == "pal8": - image = useful_array(planes[0]).reshape(height, width) + image = useful_array(planes[0]) palette = ( np.frombuffer(planes[1], "i4") .astype(">i4") @@ -805,8 +820,11 @@ def to_ndarray(self, channel_last=False, **kwargs): return image, palette if format_name == "nv12": return np.hstack( - [useful_array(planes[0]), useful_array(planes[1], 2)] - ).reshape(-1, width) + [ + useful_array(planes[0]).reshape(-1), + useful_array(planes[1], 2).reshape(-1), + ] + ).reshape(-1, frame.ptr.width) raise ValueError( f"Conversion to numpy array with format `{format_name}` is not yet supported" diff --git a/av/video/plane.py b/av/video/plane.py index c4908c21d..2b169f9cc 100644 --- a/av/video/plane.py +++ b/av/video/plane.py @@ -26,7 +26,7 @@ def __cinit__(self, frame: VideoFrame, index: cython.int): frames_ctx.sw_format, frame.ptr.width, frame.ptr.height ) - if fmt.name == "pal8" and index == 1: + if index == 1 and fmt.name == "pal8": self.width = 256 self.height = 1 self.buffer_size = 256 * 4 From 1088b71254d155f5f68d72f2259b7b8ec40e2992 Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Thu, 12 Mar 2026 12:35:26 +0000 Subject: [PATCH 712/846] Only call reformat if necessary --- av/video/frame.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/video/frame.py b/av/video/frame.py index 5212db292..1f345d365 100644 --- a/av/video/frame.py +++ b/av/video/frame.py @@ -750,7 +750,7 @@ def to_ndarray(self, channel_last=False, **kwargs): frames_ctx.sw_format, self.ptr.width, self.ptr.height ).name - frame: VideoFrame = self.reformat(**kwargs) + frame: VideoFrame = self.reformat(**kwargs) if len(kwargs) > 0 else self if frame.ptr.hw_frames_ctx: raise ValueError("Cannot convert a hardware frame to numpy directly.") From 25ee779d7a33bdc1974ccdd3274681802e8dfcf2 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 14 Mar 2026 01:23:30 -0400 Subject: [PATCH 713/846] Update authors --- AUTHORS.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 42e899c83..788979104 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -19,12 +19,12 @@ All contributors (by number of commits): * Dave Johansen * JoeUgly <41972063+JoeUgly@users.noreply.github.com> * Justin Wong <46082645+uvjustin@users.noreply.github.com> +* Lukas Geiger * Mark Harfouche +* Santtu Keskinen -- Santtu Keskinen - Alba Mendez - Curtis Doty ; `@dotysan `_ -- Lukas Geiger - Xinran Xu ; `@xxr3376 `_ - z-khan - Marc Mueller <30130371+cdce8p@users.noreply.github.com> @@ -72,6 +72,7 @@ All contributors (by number of commits): * 吴小白 <296015668@qq.com> * davidplowman <38045873+davidplowman@users.noreply.github.com> * Hanz <40712686+HanzCEO@users.noreply.github.com> +* Clay Castronovo <42858023+clayy24@users.noreply.github.com> * Kesh Ikuma <79113787+tikuma-lsuhsc@users.noreply.github.com> * Artturin * Ian Lee From 82bc94fa4a4950c3ba7950e7bf2dc143b7aa0a9a Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 14 Mar 2026 09:49:45 -0400 Subject: [PATCH 714/846] Write v17 changelog --- AUTHORS.rst | 2 +- CHANGELOG.rst | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 788979104..29f6e93ab 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -15,11 +15,11 @@ All contributors (by number of commits): * Billy Shambrook ; `@billyshambrook `_ * Casper van der Wel * Philip de Nier +* Lukas Geiger * Tadas Dailyda * Dave Johansen * JoeUgly <41972063+JoeUgly@users.noreply.github.com> * Justin Wong <46082645+uvjustin@users.noreply.github.com> -* Lukas Geiger * Mark Harfouche * Santtu Keskinen diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b5f2811cd..32baafdaf 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -25,8 +25,8 @@ We are operating with `semantic versioning `_. - Bug fixes (PATCH) go here. - $CHANGE by :gh-user:`mikeboers` in (:pr:`1`). -v17.0.0 (Next) --------------- +v17.0.0 +------- Major: @@ -43,12 +43,16 @@ Features: - Preserving hardware memory during cuvid decoding, exporting/importing via dlpack by :gh-user:`WyattBlue` in (:pr:`2155`). - Add enumerate_input_devices and enumerate_output_devices API by :gh-user:`WyattBlue` in (:pr:`2174`). - Add ``ColorTrc`` and ``ColorPrimaries`` enums; add ``color_trc`` and ``color_primaries`` properties to ``VideoFrame``; add ``dst_color_trc`` and ``dst_color_primaries`` parameters to ``VideoFrame.reformat()``, addressing :issue:`1968` by :gh-user:`WyattBlue` in (:pr:`2175`). +- Add multithreaded reformatting and reduce Python overhead by replacing ``sws_scale`` with ``sws_scale_frame``, skipping unnecessary reformats, and minimizing Python interactions by :gh-user:`lgeiger`. +- Prevent data copy in ``VideoFrame.to_ndarray()`` for padded frames by :gh-user:`lgeiger` in (:pr:`2190`). Fixes: - Fix :issue:`2149` by :gh-user:`WyattBlue` in (:pr:`2155`). - Fix packet typing based on stream and specify InputContainer.demux based on incoming stream by :gh-user:`ntjohnson1` in (:pr:`2134`). +- Fix memory growth when remuxing with ``add_stream_from_template`` by skipping ``avcodec_open2`` for template-initialized codec contexts, addressing :issue:`2135` by :gh-user:`WyattBlue`. - Explicitly disable OpenSSL in source builds (``scripts/build-deps``) to prevent accidental OpenSSL linkage that breaks FIPS-enabled systems, addressing :issue:`1972`. +- Add missing ``SWS_SPLINE`` interpolation by :gh-user:`lgeiger` in (:pr:`2188`). v16.1.0 ------- From 1f2b3ad8268b2bbf116567e2f60ac3570d041d92 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 14 Mar 2026 09:53:28 -0400 Subject: [PATCH 715/846] Release 17.0.0 --- av/about.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/about.py b/av/about.py index fea979679..a08b09c58 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "17.0.0pre" +__version__ = "17.0.0" From 764fed1ff3a218c7d4253d65a4a3d8289ac6df28 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 14 Mar 2026 12:35:48 -0400 Subject: [PATCH 716/846] Next dev version --- CHANGELOG.rst | 7 +++++++ av/about.py | 2 +- av/frame.py | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 32baafdaf..30a085fb2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -25,6 +25,13 @@ We are operating with `semantic versioning `_. - Bug fixes (PATCH) go here. - $CHANGE by :gh-user:`mikeboers` in (:pr:`1`). +v17.0.1 (next) +-------------- + +Fixes: + +- A cdivision decorator for faster division. + v17.0.0 ------- diff --git a/av/about.py b/av/about.py index a08b09c58..b7aebaa88 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "17.0.0" +__version__ = "17.0.1pre" diff --git a/av/frame.py b/av/frame.py index 8100c39d6..7e43de597 100644 --- a/av/frame.py +++ b/av/frame.py @@ -117,6 +117,7 @@ def duration(self, value): self.ptr.duration = value @property + @cython.cdivision(True) def time(self): """ The presentation time in seconds for this frame. From 9833d023587cc476caf41c3c3eb3fab062c294c0 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 14 Mar 2026 14:40:31 -0400 Subject: [PATCH 717/846] Update Interpolation descs with FFmpeg master --- av/video/reformatter.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/av/video/reformatter.py b/av/video/reformatter.py index 96c1b1742..67f4bd0ad 100644 --- a/av/video/reformatter.py +++ b/av/video/reformatter.py @@ -10,15 +10,15 @@ class Interpolation(IntEnum): FAST_BILINEAR: "Fast bilinear" = SWS_FAST_BILINEAR BILINEAR: "Bilinear" = SWS_BILINEAR - BICUBIC: "Bicubic" = SWS_BICUBIC + BICUBIC: "2-tap cubic B-spline" = SWS_BICUBIC X: "Experimental" = SWS_X POINT: "Nearest neighbor / point" = SWS_POINT AREA: "Area averaging" = SWS_AREA - BICUBLIN: "Luma bicubic / chroma bilinear" = SWS_BICUBLIN - GAUSS: "Gaussian" = SWS_GAUSS - SINC: "Sinc" = SWS_SINC + BICUBLIN: "Bicubic luma / Bilinear chroma" = SWS_BICUBLIN + GAUSS: "Gaussian approximation" = SWS_GAUSS + SINC: "Unwindowed Sinc" = SWS_SINC LANCZOS: "3-tap sinc/sinc" = SWS_LANCZOS - SPLINE: "Cubic Keys spline" = SWS_SPLINE + SPLINE: "Unwindowed natural cubic spline" = SWS_SPLINE class Colorspace(IntEnum): From 1e020e72d98897a7eab1979b3c21ab0ba4c00e57 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 19 Mar 2026 16:12:30 -0400 Subject: [PATCH 718/846] Test with ffmpeg 8.1 --- .github/workflows/smoke.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 04f519a35..02de77f0c 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -37,8 +37,10 @@ jobs: config: - {os: ubuntu-24.04, python: "3.12", ffmpeg: "8.0.1", extras: true} - {os: ubuntu-24.04, python: "3.10", ffmpeg: "8.0.1"} + - {os: ubuntu-24.04, python: "3.13", ffmpeg: "8.1"} - {os: ubuntu-24.04, python: "pypy3.11", ffmpeg: "8.0.1"} - {os: macos-14, python: "3.11", ffmpeg: "8.0.1"} + - {os: macos-14, python: "3.14", ffmpeg: "8.1"} env: PYAV_PYTHON: python${{ matrix.config.python }} From 57d8f4d7215a7b7cc7d01415613fc6aa2831a8d3 Mon Sep 17 00:00:00 2001 From: Leon White Date: Thu, 19 Mar 2026 15:57:48 +0100 Subject: [PATCH 719/846] Fix TestPacketSideData.test_skip_samples_remux running against FFmpeg 8.1 --- tests/test_packet.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_packet.py b/tests/test_packet.py index 044dfd907..badec11c8 100644 --- a/tests/test_packet.py +++ b/tests/test_packet.py @@ -148,9 +148,10 @@ def test_skip_samples_remux(self) -> None: sdata = pkt.get_sidedata("skip_samples") raw = bytes(sdata) skip_end = struct.unpack(" 0: + assert skip_end == 706 + sdata.update(raw[:4] + struct.pack(" Date: Thu, 19 Mar 2026 18:06:23 +0100 Subject: [PATCH 720/846] Fix changed rounding in in NEON-accelerated yuv420p->rgb24 in FFmpeg 8.1 --- tests/test_videoframe.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 5d2368a2f..8552683e8 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -1293,4 +1293,6 @@ def test_reformat_pixel_format_align() -> None: expected_rgb[:, :, 2] = 255 frame_rgb = frame_yuv.reformat(format="rgb24") - assertNdarraysEqual(frame_rgb.to_ndarray(), expected_rgb) + result = frame_rgb.to_ndarray() + assert result.shape == expected_rgb.shape + assert numpy.abs(result.astype(int) - expected_rgb.astype(int)).max() <= 1 From c2a197fb88f91f8d69838e29b05ac32bc1d7315c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 22 Mar 2026 01:28:09 -0400 Subject: [PATCH 721/846] Make VideoFormat.components lazy --- av/video/format.pxd | 1 - av/video/format.py | 9 ++++++--- av/video/format.pyi | 8 ++++---- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/av/video/format.pxd b/av/video/format.pxd index 2c6323294..679525a2a 100644 --- a/av/video/format.pxd +++ b/av/video/format.pxd @@ -5,7 +5,6 @@ cdef class VideoFormat: cdef lib.AVPixelFormat pix_fmt cdef const lib.AVPixFmtDescriptor *ptr cdef readonly unsigned int width, height - cdef readonly tuple components cdef _init(self, lib.AVPixelFormat pix_fmt, unsigned int width, unsigned int height) cpdef chroma_width(self, int luma_width=?) cpdef chroma_height(self, int luma_height=?) diff --git a/av/video/format.py b/av/video/format.py index dbc37ed64..4b6291e71 100644 --- a/av/video/format.py +++ b/av/video/format.py @@ -55,9 +55,6 @@ def _init(self, pix_fmt: lib.AVPixelFormat, width: cuint, height: cuint): self.ptr = lib.av_pix_fmt_desc_get(pix_fmt) self.width = width self.height = height - self.components = tuple( - VideoFormatComponent(self, i) for i in range(self.ptr.nb_components) - ) def __repr__(self): if self.width or self.height: @@ -73,6 +70,12 @@ def name(self): """Canonical name of the pixel format.""" return cython.cast(str, self.ptr.name) + @property + def components(self): + return tuple( + VideoFormatComponent(self, i) for i in range(self.ptr.nb_components) + ) + @property def bits_per_pixel(self): return lib.av_get_bits_per_pixel(self.ptr) diff --git a/av/video/format.pyi b/av/video/format.pyi index e102ef4c0..1ee0bc7a7 100644 --- a/av/video/format.pyi +++ b/av/video/format.pyi @@ -6,14 +6,14 @@ class VideoFormat: has_palette: bool is_bit_stream: bool is_planar: bool + width: int + height: int + @property + def components(self) -> tuple[VideoFormatComponent, ...]: ... @property def is_rgb(self) -> bool: ... @property def is_bayer(self) -> bool: ... - width: int - height: int - components: tuple[VideoFormatComponent, ...] - def __init__(self, name: str, width: int = 0, height: int = 0) -> None: ... def chroma_width(self, luma_width: int = 0) -> int: ... def chroma_height(self, luma_height: int = 0) -> int: ... From c0f29479ff30580e1098a67be42088a624171ae0 Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Mon, 23 Mar 2026 10:49:51 +0000 Subject: [PATCH 722/846] Graph: Add helper function to improve cython type inference --- av/filter/graph.pxd | 6 +++--- av/filter/graph.py | 27 +++++++++++++++------------ 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/av/filter/graph.pxd b/av/filter/graph.pxd index 2e52bd6ec..e85f67ce4 100644 --- a/av/filter/graph.pxd +++ b/av/filter/graph.pxd @@ -13,10 +13,10 @@ cdef class Graph: cdef dict _name_counts cdef str _get_unique_name(self, str name) + cdef list[FilterContext] _get_context_by_type(self, str type) cdef _register_context(self, FilterContext) cdef _auto_register(self) cdef int _nb_filters_seen - cdef dict _context_by_ptr - cdef dict _context_by_name - cdef dict _context_by_type + cdef dict[long, FilterContext] _context_by_ptr + cdef dict[str, list[FilterContext]] _context_by_type diff --git a/av/filter/graph.py b/av/filter/graph.py index 471b6ebc4..5cd3bbf06 100644 --- a/av/filter/graph.py +++ b/av/filter/graph.py @@ -20,7 +20,6 @@ def __cinit__(self): self._name_counts = {} self._nb_filters_seen = 0 self._context_by_ptr = {} - self._context_by_name = {} self._context_by_type = {} def __dealloc__(self): @@ -37,6 +36,11 @@ def _get_unique_name(self, name: str) -> str: else: return name + @cython.cfunc + @cython.inline + def _get_context_by_type(self, type_: str) -> list[FilterContext]: + return self._context_by_type.get(type_, []) + @cython.ccall def configure(self, auto_buffer: cython.bint = True, force: cython.bint = False): if self.configured and not force: @@ -87,7 +91,6 @@ def add(self, filter, args=None, **kwargs): @cython.cfunc def _register_context(self, ctx: FilterContext): self._context_by_ptr[cython.cast(cython.long, ctx.ptr)] = ctx - self._context_by_name[ctx.ptr.name] = ctx self._context_by_type.setdefault(ctx.filter.ptr.name, []).append(ctx) @cython.cfunc @@ -203,7 +206,7 @@ def set_audio_frame_size(self, frame_size): """ if not self.configured: raise ValueError("graph not configured") - sinks = self._context_by_type.get("abuffersink", []) + sinks = self._get_context_by_type("abuffersink") if not sinks: raise ValueError("missing abuffersink filter") for sink in sinks: @@ -213,13 +216,13 @@ def set_audio_frame_size(self, frame_size): def push(self, frame): if frame is None: - contexts = self._context_by_type.get( - "buffer", [] - ) + self._context_by_type.get("abuffer", []) + contexts = self._get_context_by_type("buffer") + self._get_context_by_type( + "abuffer" + ) elif isinstance(frame, VideoFrame): - contexts = self._context_by_type.get("buffer", []) + contexts = self._get_context_by_type("buffer") elif isinstance(frame, AudioFrame): - contexts = self._context_by_type.get("abuffer", []) + contexts = self._get_context_by_type("abuffer") else: raise ValueError( f"can only AudioFrame, VideoFrame or None; got {type(frame)}" @@ -230,13 +233,13 @@ def push(self, frame): def vpush(self, frame: VideoFrame | None): """Like `push`, but only for VideoFrames.""" - for ctx in self._context_by_type.get("buffer", []): + for ctx in self._get_context_by_type("buffer"): ctx.push(frame) # TODO: Test complex filter graphs, add `at: int = 0` arg to pull() and vpull(). def pull(self): - vsinks = self._context_by_type.get("buffersink", []) - asinks = self._context_by_type.get("abuffersink", []) + vsinks = self._get_context_by_type("buffersink") + asinks = self._get_context_by_type("abuffersink") nsinks = len(vsinks) + len(asinks) if nsinks != 1: @@ -246,7 +249,7 @@ def pull(self): def vpull(self): """Like `pull`, but only for VideoFrames.""" - vsinks = self._context_by_type.get("buffersink", []) + vsinks = self._get_context_by_type("buffersink") nsinks = len(vsinks) if nsinks != 1: raise ValueError(f"can only auto-pull with single sink; found {nsinks}") From 37a6ecee134c6b9dd932640de0c34b77a5fa6e29 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 28 Mar 2026 01:58:55 -0400 Subject: [PATCH 723/846] Close #2214 --- av/container/core.py | 3 +++ av/container/core.pyi | 1 + av/container/input.pyi | 1 - av/container/output.pyi | 1 - 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/av/container/core.py b/av/container/core.py index a651c8055..e8aa51854 100755 --- a/av/container/core.py +++ b/av/container/core.py @@ -357,6 +357,9 @@ def __enter__(self): def __exit__(self, exc_type, exc_val, exc_tb): self.close() + def close(self): + raise NotImplementedError + def __repr__(self): return f"" diff --git a/av/container/core.pyi b/av/container/core.pyi index 2ca547028..cb891cbdc 100644 --- a/av/container/core.pyi +++ b/av/container/core.pyi @@ -100,6 +100,7 @@ class Container: exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: ... + def close(self) -> None: ... def chapters(self) -> list[Chapter]: ... def set_chapters(self, chapters: list[Chapter]) -> None: ... diff --git a/av/container/input.pyi b/av/container/input.pyi index b9d93c6b4..a5f829fef 100644 --- a/av/container/input.pyi +++ b/av/container/input.pyi @@ -18,7 +18,6 @@ class InputContainer(Container): size: int def __enter__(self) -> InputContainer: ... - def close(self) -> None: ... @overload def demux(self, video_stream: VideoStream) -> Iterator[Packet[VideoStream]]: ... @overload diff --git a/av/container/output.pyi b/av/container/output.pyi index de2e4950a..f7781be29 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -55,7 +55,6 @@ class OutputContainer(Container): self, codec_name: str | None = None, options: dict[str, str] | None = None ) -> DataStream: ... def start_encoding(self) -> None: ... - def close(self) -> None: ... def mux(self, packets: Packet | Sequence[Packet]) -> None: ... def mux_one(self, packet: Packet) -> None: ... @property From 74534ecb2fc01d8218be1432dbeffb2a1b54e07d Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Thu, 2 Apr 2026 14:50:47 +0100 Subject: [PATCH 724/846] Break reference cycle `StreamContainer.get()` --- av/container/streams.py | 53 +++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/av/container/streams.py b/av/container/streams.py index 0045b5c85..a84e88792 100644 --- a/av/container/streams.py +++ b/av/container/streams.py @@ -1,3 +1,5 @@ +from typing import Iterator + import cython import cython.cimports.libav as lib from cython.cimports.av.stream import Stream @@ -95,6 +97,27 @@ def data(self): def attachments(self): return tuple(s for s in self._streams if s.type == "attachment") + def _get_streams(self, x) -> Iterator[Stream]: + if x is None: + pass + elif isinstance(x, Stream): + yield x + elif isinstance(x, int): + yield self._streams[x] + elif isinstance(x, (tuple, list)): + for item in x: + yield from self._get_streams(item) + elif isinstance(x, dict): + for type_, indices in x.items(): + # For compatibility with the pseudo signature + streams = self._streams if type_ == "streams" else getattr(self, type_) + if not isinstance(indices, (tuple, list)): + indices = [indices] + for i in indices: + yield streams[i] + else: + raise TypeError("Argument must be Stream or int.", type(x)) + def get(self, *args, **kwargs): """get(streams=None, video=None, audio=None, subtitles=None, data=None) @@ -122,35 +145,9 @@ def get(self, *args, **kwargs): If nothing is selected, then all streams are returned. """ - selection: list = [] - - def process(x): - if x is None: - pass - elif isinstance(x, Stream): - selection.append(x) - elif isinstance(x, int): - selection.append(self._streams[x]) - elif isinstance(x, (tuple, list)): - for item in x: - process(item) - elif isinstance(x, dict): - for type_, indices in x.items(): - # For compatibility with the pseudo signature - streams = ( - self._streams if type_ == "streams" else getattr(self, type_) - ) - if not isinstance(indices, (tuple, list)): - indices = [indices] - for i in indices: - selection.append(streams[i]) - else: - raise TypeError("Argument must be Stream or int.", type(x)) - - for arg in args: - process(arg) + selection = list(self._get_streams(args)) if kwargs: - process(kwargs) + selection.extend(self._get_streams(kwargs)) return selection or self._streams[:] From af3a66f32e9a61f7f33c1dfd00a5d94246481c8e Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Thu, 2 Apr 2026 15:54:57 +0100 Subject: [PATCH 725/846] Improve `VideoPlane` init --- av/video/plane.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/av/video/plane.py b/av/video/plane.py index 2b169f9cc..99c159652 100644 --- a/av/video/plane.py +++ b/av/video/plane.py @@ -1,7 +1,11 @@ import cython import cython.cimports.libav as lib from cython.cimports.av.error import err_check -from cython.cimports.av.video.format import get_pix_fmt, get_video_format +from cython.cimports.av.video.format import ( + VideoFormatComponent, + get_pix_fmt, + get_video_format, +) from cython.cimports.av.video.frame import VideoFrame from cython.cimports.cpython import PyBUF_WRITABLE, PyBuffer_FillInfo from cython.cimports.cpython.buffer import Py_buffer @@ -34,7 +38,7 @@ def __cinit__(self, frame: VideoFrame, index: cython.int): for i in range(fmt.ptr.nb_components): if fmt.ptr.comp[i].plane == index: - component = fmt.components[i] + component = VideoFormatComponent(fmt, i) self.width = component.width self.height = component.height break From bcc3cee651e14441e5d8c71ad75319887ccd7249 Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Tue, 7 Apr 2026 16:37:44 +0100 Subject: [PATCH 726/846] Expose `threads` in filter graph --- av/filter/graph.py | 17 +++++++++++++++++ av/filter/graph.pyi | 1 + include/avfilter.pxd | 1 + tests/test_filters.py | 23 +++++++++++++++++++++-- 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/av/filter/graph.py b/av/filter/graph.py index 5cd3bbf06..507fbf015 100644 --- a/av/filter/graph.py +++ b/av/filter/graph.py @@ -27,6 +27,23 @@ def __dealloc__(self): # This frees the graph, filter contexts, links, etc.. lib.avfilter_graph_free(cython.address(self.ptr)) + @property + def threads(self): + """Maximum number of threads used by filters in this graph. + + Set to 0 for automatic thread count. Must be set before adding any + filters to the graph. + + Wraps :ffmpeg:`AVFilterGraph.nb_threads`. + """ + return self.ptr.nb_threads + + @threads.setter + def threads(self, value: cython.int): + if self.ptr.nb_filters: + raise RuntimeError("Cannot change threads after filters have been added.") + self.ptr.nb_threads = value + @cython.cfunc def _get_unique_name(self, name: str) -> str: count = self._name_counts.get(name, 0) diff --git a/av/filter/graph.pyi b/av/filter/graph.pyi index e170c2ce7..758813ea2 100644 --- a/av/filter/graph.pyi +++ b/av/filter/graph.pyi @@ -14,6 +14,7 @@ from .filter import Filter class Graph: configured: bool + threads: int def __init__(self) -> None: ... def configure(self, auto_buffer: bool = True, force: bool = False) -> None: ... diff --git a/include/avfilter.pxd b/include/avfilter.pxd index 5eb11ecc2..afb953fdd 100644 --- a/include/avfilter.pxd +++ b/include/avfilter.pxd @@ -58,6 +58,7 @@ cdef extern from "libavfilter/avfilter.h" nogil: cdef struct AVFilterGraph: int nb_filters AVFilterContext **filters + int nb_threads cdef struct AVFilterInOut: char *name diff --git a/tests/test_filters.py b/tests/test_filters.py index bd74a633a..51522e7de 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -193,11 +193,10 @@ def test_audio_buffer_volume_filter(self): assert np.allclose(input_data * 0.5, output_data) - def test_video_buffer(self): + def _test_video_buffer(self, graph): input_container = av.open(format="lavfi", file="color=c=pink:duration=1:r=30") input_video_stream = input_container.streams.video[0] - graph = av.filter.Graph() buffer = graph.add_buffer(template=input_video_stream) bwdif = graph.add("bwdif", "send_field:tff:all") buffersink = graph.add("buffersink") @@ -223,6 +222,14 @@ def test_video_buffer(self): assert filtered_frames[1].pts == (frame.pts - 1) * 2 + 1 assert filtered_frames[1].time_base == Fraction(1, 60) + def test_video_buffer(self): + self._test_video_buffer(av.filter.Graph()) + + def test_video_buffer_threading(self): + graph = av.filter.Graph() + graph.threads = 4 + self._test_video_buffer(graph) + def test_EOF(self) -> None: input_container = av.open(format="lavfi", file="color=c=pink:duration=1:r=30") video_stream = input_container.streams.video[0] @@ -246,3 +253,15 @@ def test_EOF(self) -> None: assert isinstance(palette_frame, av.VideoFrame) assert palette_frame.width == 16 assert palette_frame.height == 16 + + def test_graph_threads(self) -> None: + graph = Graph() + assert graph.threads == 0 + + graph.threads = 4 + assert graph.threads == 4 + + graph.add("testsrc") + + with self.assertRaises(RuntimeError): + graph.threads = 2 From 6895df9108b64be5aa5b2013b16369f68d54cb8f Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 7 Apr 2026 11:31:01 -0400 Subject: [PATCH 727/846] Fix crash with container closing with GC --- .github/workflows/smoke.yml | 1 - av/container/output.py | 7 ++++++- tests/test_open.py | 18 ++++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 02de77f0c..6fb33ad02 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -38,7 +38,6 @@ jobs: - {os: ubuntu-24.04, python: "3.12", ffmpeg: "8.0.1", extras: true} - {os: ubuntu-24.04, python: "3.10", ffmpeg: "8.0.1"} - {os: ubuntu-24.04, python: "3.13", ffmpeg: "8.1"} - - {os: ubuntu-24.04, python: "pypy3.11", ffmpeg: "8.0.1"} - {os: macos-14, python: "3.11", ffmpeg: "8.0.1"} - {os: macos-14, python: "3.14", ffmpeg: "8.1"} diff --git a/av/container/output.py b/av/container/output.py index b2a643a7a..1e9ac3201 100644 --- a/av/container/output.py +++ b/av/container/output.py @@ -35,8 +35,13 @@ def __cinit__(self, *args, **kwargs): with cython.nogil: self.packet_ptr = lib.av_packet_alloc() + def __del__(self): + try: + close_output(self) + except Exception: + pass + def __dealloc__(self): - close_output(self) with cython.nogil: lib.av_packet_free(cython.address(self.packet_ptr)) diff --git a/tests/test_open.py b/tests/test_open.py index 9341e1f56..60cb60072 100644 --- a/tests/test_open.py +++ b/tests/test_open.py @@ -1,3 +1,5 @@ +import gc +import io from pathlib import Path import av @@ -35,3 +37,19 @@ def test_str_output() -> None: container = av.open(path, "w") assert type(container) is av.container.OutputContainer + + +def _container_no_close() -> None: + buf = io.BytesIO() + container = av.open(buf, mode="w", format="mp4") + stream = container.add_stream("mpeg4", rate=24) + stream.width = 320 + stream.height = 240 + stream.pix_fmt = "yuv420p" + container.start_encoding() + + +def test_container_no_close() -> None: + # Do not close so that container is freed through GC. + _container_no_close() + gc.collect() From 4afc5068185e78995d026abcc551b9a40e2e42d7 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 7 Apr 2026 23:23:38 -0400 Subject: [PATCH 728/846] Save 12 bytes for each OutputContainer --- av/container/core.pxd | 5 ++--- av/container/core.py | 18 +++++++++++------- av/container/core.pyi | 4 ++-- av/container/input.pxd | 1 - av/container/input.py | 6 +++--- av/container/output.pxd | 3 --- av/container/output.py | 10 +++++----- 7 files changed, 23 insertions(+), 24 deletions(-) diff --git a/av/container/core.pxd b/av/container/core.pxd index 1067b678d..7cba26407 100644 --- a/av/container/core.pxd +++ b/av/container/core.pxd @@ -1,4 +1,5 @@ cimport libav as lib +from libc.stdint cimport uint8_t from av.codec.hwaccel cimport HWAccel from av.container.pyio cimport PyIOFile @@ -14,16 +15,13 @@ ctypedef struct timeout_info: cdef class Container: - cdef readonly bint writeable cdef lib.AVFormatContext *ptr - cdef readonly object name cdef readonly str metadata_encoding cdef readonly str metadata_errors cdef readonly PyIOFile file cdef int buffer_size - cdef bint input_was_opened cdef readonly object io_open cdef readonly object open_files @@ -39,6 +37,7 @@ cdef class Container: cdef readonly dict metadata # Private API. + cdef uint8_t _myflag # enum: writeable, input_was_opened, started, done cdef _assert_open(self) cdef int err_check(self, int value) except -1 diff --git a/av/container/core.py b/av/container/core.py index e8aa51854..ab6c889b7 100755 --- a/av/container/core.py +++ b/av/container/core.py @@ -246,8 +246,8 @@ def __cinit__( if sentinel is not _cinit_sentinel: raise RuntimeError("cannot construct base Container") - self.writeable = isinstance(self, OutputContainer) - if not self.writeable and not isinstance(self, InputContainer): + writeable: cython.bint = isinstance(self, OutputContainer) + if not writeable and not isinstance(self, InputContainer): raise RuntimeError("Container cannot be directly extended.") if isinstance(file_, str): @@ -276,13 +276,13 @@ def __cinit__( format_name, acodec = format_name.split(":") self.format = ContainerFormat(format_name) - self.input_was_opened = False res: cython.int name_obj: bytes = os.fsencode(self.name) name: cython.p_char = name_obj ofmt: cython.pointer[cython.const[lib.AVOutputFormat]] - if self.writeable: + if writeable: + self._myflag |= 1 # enum.writeable = True ofmt = ( self.format.optr if self.format @@ -320,7 +320,7 @@ def __cinit__( # Setup Python IO. self.open_files = {} if not isinstance(file_, basestring): - self.file = PyIOFile(file_, buffer_size, self.writeable) + self.file = PyIOFile(file_, buffer_size, writeable) self.ptr.pb = self.file.iocontext if io_open is not None: @@ -330,7 +330,7 @@ def __cinit__( ifmt: cython.pointer[cython.const[lib.AVInputFormat]] c_options: Dictionary - if not self.writeable: + if not writeable: ifmt = self.format.iptr if self.format else cython.NULL c_options = Dictionary(self.options, self.container_options) @@ -342,7 +342,7 @@ def __cinit__( ) self.set_timeout(None) self.err_check(res) - self.input_was_opened = True + self._myflag |= 2 # enum.input_was_opened = True if format_name is None: self.format = build_container_format(self.ptr.iformat, self.ptr.oformat) @@ -400,6 +400,10 @@ def flags(self, value: cython.int): self._assert_open() self.ptr.flags = value + @property + def input_was_opened(self): + return self._myflag & 2 + def chapters(self): self._assert_open() result: list = [] diff --git a/av/container/core.pyi b/av/container/core.pyi index cb891cbdc..f62b6c813 100644 --- a/av/container/core.pyi +++ b/av/container/core.pyi @@ -75,13 +75,11 @@ class Chapter(TypedDict): metadata: dict[str, str] class Container: - writeable: bool name: str metadata_encoding: str metadata_errors: str file: Any buffer_size: int - input_was_opened: bool io_open: Any open_files: Any format: ContainerFormat @@ -100,6 +98,8 @@ class Container: exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: ... + @property + def input_was_opened(self) -> bool: ... def close(self) -> None: ... def chapters(self) -> list[Chapter]: ... def set_chapters(self, chapters: list[Chapter]) -> None: ... diff --git a/av/container/input.pxd b/av/container/input.pxd index 8c369d8ad..2e65025b9 100644 --- a/av/container/input.pxd +++ b/av/container/input.pxd @@ -5,5 +5,4 @@ from av.stream cimport Stream cdef class InputContainer(Container): - cdef flush_buffers(self) diff --git a/av/container/input.py b/av/container/input.py index 304fe3e3f..97037457f 100644 --- a/av/container/input.py +++ b/av/container/input.py @@ -13,11 +13,11 @@ @cython.cfunc def close_input(self: InputContainer): self.streams = StreamContainer() - if self.input_was_opened: - with cython.nogil: + with cython.nogil: + if self._myflag & 2: # This causes `self.ptr` to be set to NULL. lib.avformat_close_input(cython.address(self.ptr)) - self.input_was_opened = False + self._myflag &= ~2 # enum.input_was_opened = False @cython.cclass diff --git a/av/container/output.pxd b/av/container/output.pxd index 51d3f308e..71edc3bf2 100644 --- a/av/container/output.pxd +++ b/av/container/output.pxd @@ -5,8 +5,5 @@ from av.stream cimport Stream cdef class OutputContainer(Container): - cdef bint _started - cdef bint _done cdef lib.AVPacket *packet_ptr - cpdef start_encoding(self) diff --git a/av/container/output.py b/av/container/output.py index 1e9ac3201..61a370c06 100644 --- a/av/container/output.py +++ b/av/container/output.py @@ -15,16 +15,16 @@ @cython.cfunc def close_output(self: OutputContainer): self.streams = StreamContainer() - if self._started and not self._done: + if self._myflag & 12 == 4: # enum.started and not enum.done # We must only ever call av_write_trailer *once*, otherwise we get a # segmentation fault. Therefore no matter whether it succeeds or not - # we must absolutely set self._done. + # we must absolutely set enum.done. try: self.err_check(lib.av_write_trailer(self.ptr)) finally: if self.file is None and not (self.ptr.oformat.flags & lib.AVFMT_NOFILE): lib.avio_closep(cython.address(self.ptr.pb)) - self._done = True + self._myflag |= 8 # enum.done = True @cython.cclass @@ -431,7 +431,7 @@ def add_data_stream(self, codec_name=None, options: dict | None = None): @cython.ccall def start_encoding(self): """Write the file header! Called automatically.""" - if self._started: + if self._myflag & 4: # started return # TODO: This does NOT handle options coming from 3 sources. @@ -491,7 +491,7 @@ def start_encoding(self): log = logging.getLogger(__name__) log.warning("Some options were not used: %s" % unused_options) - self._started = True + self._myflag |= 4 @property def supported_codecs(self): From 9636aacb4246ea080fec9c9c162cef11a83381e1 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 8 Apr 2026 00:43:33 -0400 Subject: [PATCH 729/846] Make VideoCodecContext 12 bytes smaller --- av/codec/context.pxd | 13 +++++-------- av/codec/context.py | 4 +--- av/container/output.py | 4 ++-- av/video/codeccontext.pxd | 2 -- av/video/codeccontext.py | 4 ---- 5 files changed, 8 insertions(+), 19 deletions(-) diff --git a/av/codec/context.pxd b/av/codec/context.pxd index 08d24761e..7fd26d56e 100644 --- a/av/codec/context.pxd +++ b/av/codec/context.pxd @@ -1,5 +1,5 @@ cimport libav as lib -from libc.stdint cimport int64_t +from libc.stdint cimport int64_t, uint8_t from av.buffer cimport ByteSource from av.codec.codec cimport Codec @@ -11,13 +11,6 @@ from av.packet cimport Packet cdef class CodecContext: cdef lib.AVCodecContext *ptr - # Whether AVCodecContext.extradata should be de-allocated upon destruction. - cdef bint extradata_set - - # True when created via add_stream_from_template(); start_encoding() skips - # avcodec_open2() and lets encode()/decode() open the codec lazily if needed. - cdef readonly bint _template_initialized - # Used as a signal that this is within a stream, and also for us to access that # stream. This is set "manually" by the stream after constructing this object. cdef int stream_index @@ -40,6 +33,10 @@ cdef class CodecContext: # Used by hardware-accelerated decode. cdef HWAccel hwaccel_ctx + cdef uint8_t _ctxflags # ctxEnum: template_initialized + # True when created via add_stream_from_template(); start_encoding() skips + # avcodec_open2() and lets encode()/decode() open the codec lazily if needed. + # Used by both transcode APIs to setup user-land objects. # TODO: Remove the `Packet` from `_setup_decoded_frame` (because flushing packets # are bogus). It should take all info it needs from the context and/or stream. diff --git a/av/codec/context.py b/av/codec/context.py index c348353cd..fc9721754 100644 --- a/av/codec/context.py +++ b/av/codec/context.py @@ -206,7 +206,6 @@ def extradata(self, data): raise MemoryError("Cannot allocate extradata") memcpy(self.ptr.extradata, source.ptr, source.length) self.ptr.extradata_size = source.length - self.extradata_set = True @property def extradata_size(self): @@ -253,9 +252,8 @@ def open(self, strict: cython.bint = True): self.options = dict(options) def __dealloc__(self): - if self.ptr and self.extradata_set: - lib.av_freep(cython.address(self.ptr.extradata)) if self.ptr: + lib.av_freep(cython.address(self.ptr.extradata)) lib.avcodec_free_context(cython.address(self.ptr)) if self.parser: lib.av_parser_close(self.parser) diff --git a/av/container/output.py b/av/container/output.py index 61a370c06..79389c2cd 100644 --- a/av/container/output.py +++ b/av/container/output.py @@ -275,7 +275,7 @@ def add_stream_from_template( # Construct the user-land stream py_codec_context: CodecContext = wrap_codec_context(ctx, codec, None) - py_codec_context._template_initialized = True + py_codec_context._ctxflags |= 1 # _template_initialized = True py_stream: Stream = wrap_stream(self, stream, py_codec_context) self.streams.add_stream(py_stream) @@ -446,7 +446,7 @@ def start_encoding(self): for k, v in self.options.items(): ctx.options.setdefault(k, v) - if not ctx._template_initialized: + if not (ctx._ctxflags & 1): # template_initialized ctx.open() # Track option consumption. diff --git a/av/video/codeccontext.pxd b/av/video/codeccontext.pxd index 8d128348a..bbefcda56 100644 --- a/av/video/codeccontext.pxd +++ b/av/video/codeccontext.pxd @@ -19,8 +19,6 @@ cdef class VideoCodecContext(CodecContext): cdef AVCodecPrivateData _private_data cdef VideoFormat _format cdef _build_format(self) - cdef int last_w - cdef int last_h cdef readonly VideoReformatter reformatter cdef readonly int encoded_frame_count cdef VideoFrame next_frame diff --git a/av/video/codeccontext.py b/av/video/codeccontext.py index e62bf8fae..a054d48d7 100644 --- a/av/video/codeccontext.py +++ b/av/video/codeccontext.py @@ -39,10 +39,6 @@ def _get_hw_format( @cython.cclass class VideoCodecContext(CodecContext): - def __cinit__(self, *args, **kwargs): - self.last_w = 0 - self.last_h = 0 - @cython.cfunc def _init( self, From defe9241f8a737ba4c35a2543a41c1de9da933ea Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 8 Apr 2026 01:07:11 -0400 Subject: [PATCH 730/846] VideoCodecContext: build format lazily --- av/video/codeccontext.pxd | 2 -- av/video/codeccontext.py | 28 +++++++++------------------- 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/av/video/codeccontext.pxd b/av/video/codeccontext.pxd index bbefcda56..831e5c659 100644 --- a/av/video/codeccontext.pxd +++ b/av/video/codeccontext.pxd @@ -17,8 +17,6 @@ cdef struct AVCodecPrivateData: cdef class VideoCodecContext(CodecContext): cdef AVCodecPrivateData _private_data - cdef VideoFormat _format - cdef _build_format(self) cdef readonly VideoReformatter reformatter cdef readonly int encoded_frame_count cdef VideoFrame next_frame diff --git a/av/video/codeccontext.py b/av/video/codeccontext.py index a054d48d7..49ed82f52 100644 --- a/av/video/codeccontext.py +++ b/av/video/codeccontext.py @@ -72,7 +72,6 @@ def _init( # is_hwaccel() function on each stream's codec context. self.hwaccel_ctx = None - self._build_format() self.encoded_frame_count = 0 @cython.cfunc @@ -80,13 +79,10 @@ def _prepare_frames_for_encode(self, input: Frame | None) -> list: if input is None or not input: return [None] - if self._format is None: - raise ValueError("self._format is None, cannot encode") - vframe: VideoFrame = input # Reformat if it doesn't match. if ( - vframe.format.pix_fmt != self._format.pix_fmt + vframe.format.pix_fmt != self.pix_fmt or vframe.width != self.ptr.width or vframe.height != self.ptr.height ): @@ -97,7 +93,7 @@ def _prepare_frames_for_encode(self, input: Frame | None) -> list: vframe, self.ptr.width, self.ptr.height, - self._format, + self.format, threads=self.ptr.thread_count, ) @@ -137,24 +133,19 @@ def _transfer_hwframe(self, frame: Frame): frame_sw.pts = frame.pts return frame_sw - @cython.cfunc - def _build_format(self): - self._format = get_video_format( + @property + def format(self): + return get_video_format( cython.cast(lib.AVPixelFormat, self.ptr.pix_fmt), self.ptr.width, self.ptr.height, ) - @property - def format(self): - return self._format - @format.setter def format(self, format: VideoFormat): self.ptr.pix_fmt = format.pix_fmt self.ptr.width = format.width self.ptr.height = format.height - self._build_format() # Kinda wasteful. @property def width(self): @@ -165,7 +156,6 @@ def width(self): @width.setter def width(self, value: cython.uint): self.ptr.width = value - self._build_format() @property def height(self): @@ -176,7 +166,6 @@ def height(self): @height.setter def height(self, value: cython.uint): self.ptr.height = value - self._build_format() @property def bits_per_coded_sample(self): @@ -195,7 +184,6 @@ def bits_per_coded_sample(self, value: cython.int): raise ValueError("Not supported for encoders") self.ptr.bits_per_coded_sample = value - self._build_format() @property def pix_fmt(self): @@ -204,12 +192,14 @@ def pix_fmt(self): :type: str | None """ - return getattr(self._format, "name", None) + desc: cython.pointer[cython.const[lib.AVPixFmtDescriptor]] = ( + lib.av_pix_fmt_desc_get(cython.cast(lib.AVPixelFormat, self.ptr.pix_fmt)) + ) + return cython.cast(str, desc.name) @pix_fmt.setter def pix_fmt(self, value): self.ptr.pix_fmt = get_pix_fmt(value) - self._build_format() @property def framerate(self): From 40c6ea5f2aa228912fc802fa1650d865a5fe7a24 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 8 Apr 2026 01:33:23 -0400 Subject: [PATCH 731/846] Output Container: avoid swallowing warning --- av/container/output.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/av/container/output.py b/av/container/output.py index 79389c2cd..d0c51cc6b 100644 --- a/av/container/output.py +++ b/av/container/output.py @@ -16,6 +16,11 @@ def close_output(self: OutputContainer): self.streams = StreamContainer() if self._myflag & 12 == 4: # enum.started and not enum.done + # If the underlying Python IO file was already closed (e.g. during GC + # finalization where cycle ordering is undefined), skip the trailer. + if self.file is not None and getattr(self.file.file, "closed", False): + self._myflag |= 8 # enum.done = True + return # We must only ever call av_write_trailer *once*, otherwise we get a # segmentation fault. Therefore no matter whether it succeeds or not # we must absolutely set enum.done. @@ -36,10 +41,7 @@ def __cinit__(self, *args, **kwargs): self.packet_ptr = lib.av_packet_alloc() def __del__(self): - try: - close_output(self) - except Exception: - pass + close_output(self) def __dealloc__(self): with cython.nogil: From 236722263ed85c9254cb4bc572f55a527322aff4 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 8 Apr 2026 02:08:58 -0400 Subject: [PATCH 732/846] activate.sh: Use ffmpeg 8.1 --- scripts/activate.sh | 3 +-- scripts/build | 4 ---- scripts/build-deps | 12 ++++++++---- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/scripts/activate.sh b/scripts/activate.sh index a58a64b94..8df33716c 100755 --- a/scripts/activate.sh +++ b/scripts/activate.sh @@ -21,7 +21,7 @@ if [[ ! "$PYAV_LIBRARY" ]]; then return 1 fi else - PYAV_LIBRARY=ffmpeg-8.0.1 + PYAV_LIBRARY=ffmpeg-8.1 echo "No \$PYAV_LIBRARY set; defaulting to $PYAV_LIBRARY" fi fi @@ -38,7 +38,6 @@ fi # Hack for PyPy on GitHub Actions. # This is because PYAV_PYTHON is constructed from "python${{ matrix.config.python }}" # resulting in "pythonpypy3", which won't work. -# It would be nice to clean this up, but I want it to work ASAP. if [[ "$PYAV_PYTHON" == *pypy* ]]; then PYAV_PYTHON=python fi diff --git a/scripts/build b/scripts/build index e3bd7a073..dff74a880 100755 --- a/scripts/build +++ b/scripts/build @@ -17,9 +17,5 @@ echo PKG_CONFIG_PATH: $PKG_CONFIG_PATH echo LD_LIBRARY_PATH: $LD_LIBRARY_PATH echo -which ffmpeg || exit 2 -ffmpeg -version || exit 3 -echo - $PYAV_PIP install -U --pre cython setuptools 2> /dev/null "$PYAV_PYTHON" setup.py config build_ext --inplace || exit 1 diff --git a/scripts/build-deps b/scripts/build-deps index 5b9a299c3..d89b1e406 100755 --- a/scripts/build-deps +++ b/scripts/build-deps @@ -55,15 +55,19 @@ cd $PYAV_LIBRARY echo ./configure ./configure \ --disable-doc \ - --disable-static \ + --disable-programs \ --disable-stripping \ + --disable-static \ + --enable-shared \ + --enable-gpl \ + --enable-version3 \ --disable-libxml2 \ + --disable-xlib \ --disable-openssl \ --enable-debug=3 \ - --enable-gpl \ - --enable-version3 \ --enable-libx264 \ - --enable-shared \ + --disable-bsfs \ + --enable-bsf=chomp,h264_mp4toannexb,setts \ --enable-sse \ --enable-avx \ --enable-avx2 \ From 0d9d5ccafe0f156f151abf8c5bd4c7636ae72f34 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 9 Apr 2026 18:10:01 -0400 Subject: [PATCH 733/846] Test writing packets to data stream --- tests/test_packet.py | 55 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/tests/test_packet.py b/tests/test_packet.py index badec11c8..f1c862d0b 100644 --- a/tests/test_packet.py +++ b/tests/test_packet.py @@ -1,12 +1,67 @@ +import fractions +import io +import json import struct from typing import get_args from unittest import SkipTest +import numpy + import av from .common import fate_suite, sandboxed +class TestDataStreams: + def generate_container_with_data_packets(self): + file = io.BytesIO() + packet_datas_expected = dict[fractions.Fraction, bytes]() + + container = av.open(file, format="mp4", mode="w") + stream = container.add_data_stream("bin_data") + vstream = container.add_stream("h264") + + for i in range(10): + packet_data = json.dumps("This may not work", ensure_ascii=False).encode( + "utf-8" + ) + if i % 2 == 0: + packet_data = b"This works fine" + packet = av.Packet(packet_data) + vframe = av.VideoFrame.from_ndarray( + numpy.random.randint(0, 255, size=(120, 120, 3), dtype=numpy.uint8) + ) + packet.pts = i + packet.dts = i + packet.duration = 1 + packet.stream = stream + container.mux(packet) + container.mux(vstream.encode(vframe)) + # NOTE test passes if this is used + # packet_datas_expected[packet.pts * packet.time_base] = bytes(packet_data) + + container.close() + + container = av.open(file, format="mp4", mode="r") + return container, packet_datas_expected + + def test_data_packet_bytes(self): + container, packet_datas_expected = self.generate_container_with_data_packets() + + packet_datas = dict[fractions.Fraction, bytes]() + for packet in container.demux(container.streams.data[0]): + if packet.pts is None: + continue + assert bytes(packet) in ( + b"This works fine", + json.dumps("This may not work", ensure_ascii=False).encode("utf-8"), + ), bytes(packet) + # NOTE test passes if this is used + # packet_datas[packet.pts * packet.time_base] = bytes(packet) + + assert packet_datas == packet_datas_expected + + class TestProperties: def test_is_keyframe(self) -> None: with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: From 0d4149f0265237391d286058c720de984500bf23 Mon Sep 17 00:00:00 2001 From: bdavid-evertz Date: Thu, 9 Apr 2026 18:29:14 -0400 Subject: [PATCH 734/846] Fix #2223 --- av/packet.pxd | 2 -- av/packet.py | 29 ++++++++++++++++++++++++----- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/av/packet.pxd b/av/packet.pxd index 9bd077d79..1546a1315 100644 --- a/av/packet.pxd +++ b/av/packet.pxd @@ -19,7 +19,5 @@ cdef class Packet(Buffer): cdef lib.AVPacket* ptr cdef Stream _stream cdef _rebase_time(self, lib.AVRational) - # Hold onto the original reference. - cdef ByteSource source cdef size_t _buffer_size(self) cdef void* _buffer_ptr(self) diff --git a/av/packet.py b/av/packet.py index 5a97817e3..b25f61c93 100644 --- a/av/packet.py +++ b/av/packet.py @@ -4,8 +4,10 @@ from cython.cimports import libav as lib from cython.cimports.av.buffer import Buffer, ByteSource, bytesource from cython.cimports.av.error import err_check -from cython.cimports.av.opaque import noop_free, opaque_container +from cython.cimports.av.opaque import opaque_container from cython.cimports.av.utils import avrational_to_fraction, to_avrational +from cython.cimports.cpython.ref import Py_DECREF, Py_INCREF +from cython.cimports.libc.stdint import uint8_t from cython.cimports.libc.string import memcpy # Check https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/packet.h#L41 @@ -193,6 +195,18 @@ def _packet_sidedata_from_packet( return sdata +@cython.cfunc +@cython.nogil +@cython.exceptval(check=False) +def _python_free( + opaque: cython.p_void, + data: cython.pointer[uint8_t], +) -> cython.void: + if opaque != cython.NULL: + with cython.gil: + Py_DECREF(cython.cast(object, opaque)) + + @cython.cclass class Packet(Buffer): """A packet of encoded data within a :class:`~av.format.Stream`. @@ -226,15 +240,20 @@ def __init__(self, input=None): size = source.length if size: # Create a buffer that references the source data directly. - # The noop_free callback is used because Python manages the memory - # via self.source keeping the ByteSource alive. - buf = lib.av_buffer_create(source.ptr, size, noop_free, cython.NULL, 0) + Py_INCREF(source) + buf = lib.av_buffer_create( + source.ptr, + size, + _python_free, + cython.cast(cython.p_void, source), + 0, + ) if buf == cython.NULL: + Py_DECREF(source) raise MemoryError("Could not allocate AVBufferRef") self.ptr.buf = buf self.ptr.data = source.ptr self.ptr.size = size - self.source = source def __repr__(self): stream = self._stream.index if self._stream else 0 From c2fb45a747f5dc1ce1c6feb4d0b94a173e2b9f74 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 9 Apr 2026 18:55:15 -0400 Subject: [PATCH 735/846] Remove now unused noop_free() --- av/opaque.pxd | 4 ---- av/opaque.py | 7 ------- 2 files changed, 11 deletions(-) diff --git a/av/opaque.pxd b/av/opaque.pxd index 5a0a423e3..998e13397 100644 --- a/av/opaque.pxd +++ b/av/opaque.pxd @@ -1,8 +1,4 @@ cimport libav as lib -from libc.stdint cimport uint8_t - - -cdef void noop_free(void *opaque, uint8_t *data) noexcept nogil cdef class OpaqueContainer: diff --git a/av/opaque.py b/av/opaque.py index 7d295699a..bd25fd61f 100644 --- a/av/opaque.py +++ b/av/opaque.py @@ -8,13 +8,6 @@ u8ptr = cython.typedef(cython.pointer[uint8_t]) -@cython.cfunc -@cython.exceptval(check=False) -@cython.nogil -def noop_free(opaque: cython.p_void, data: u8ptr) -> cython.void: - pass - - @cython.cfunc @cython.exceptval(check=False) @cython.nogil From b5eb9be5312a8609f0c9acdabb3651712d2b5913 Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Fri, 17 Apr 2026 01:24:17 +0100 Subject: [PATCH 736/846] Remove `_send_packet_and_recv` to simplify `decode()` --- av/codec/context.pxd | 1 - av/codec/context.py | 39 +++++++++++++----------------------- av/subtitles/codeccontext.py | 13 +++++++++--- 3 files changed, 24 insertions(+), 29 deletions(-) diff --git a/av/codec/context.pxd b/av/codec/context.pxd index 7fd26d56e..cc12836d7 100644 --- a/av/codec/context.pxd +++ b/av/codec/context.pxd @@ -52,7 +52,6 @@ cdef class CodecContext: # send/recv buffer may be limited to a single frame. Ergo, we need to flush # the buffer as often as possible. cdef _recv_packet(self) - cdef _send_packet_and_recv(self, Packet packet) cdef _recv_frame(self) cdef _transfer_hwframe(self, Frame frame) diff --git a/av/codec/context.py b/av/codec/context.py index fc9721754..10b255b2b 100644 --- a/av/codec/context.py +++ b/av/codec/context.py @@ -362,25 +362,6 @@ def _send_frame_and_recv(self, frame: Frame | None): yield packet packet = self._recv_packet() - @cython.cfunc - def _send_packet_and_recv(self, packet: Packet | None): - frame: Frame - res: cython.int - with cython.nogil: - res = lib.avcodec_send_packet( - self.ptr, packet.ptr if packet is not None else cython.NULL - ) - err_check(res, "avcodec_send_packet()") - - out: list = [] - while True: - frame = self._recv_frame() - if frame: - out.append(frame) - else: - break - return out - @cython.cfunc def _prepare_frames_for_encode(self, frame: Frame | None) -> list: return [frame] @@ -487,12 +468,20 @@ def decode(self, packet: Packet | None = None): self.open(strict=False) - res: list = [] - for frame in self._send_packet_and_recv(packet): - if isinstance(frame, Frame): - self._setup_decoded_frame(frame, packet) - res.append(frame) - return res + res: cython.int + with cython.nogil: + res = lib.avcodec_send_packet( + self.ptr, packet.ptr if packet is not None else cython.NULL + ) + err_check(res, "avcodec_send_packet()") + + out: list = [] + frame = self._recv_frame() + while frame: + self._setup_decoded_frame(frame, packet) + out.append(frame) + frame = self._recv_frame() + return out @cython.ccall def flush_buffers(self): diff --git a/av/subtitles/codeccontext.py b/av/subtitles/codeccontext.py index 6db7309dd..a3a1a550c 100644 --- a/av/subtitles/codeccontext.py +++ b/av/subtitles/codeccontext.py @@ -1,6 +1,7 @@ import cython from cython.cimports import libav as lib from cython.cimports.av.buffer import ByteSource, bytesource +from cython.cimports.av.codec.context import CodecContext from cython.cimports.av.error import err_check from cython.cimports.av.packet import Packet from cython.cimports.av.subtitles.subtitle import SubtitleProxy, SubtitleSet @@ -103,11 +104,17 @@ def encode_subtitle(self, subtitle: SubtitleSet) -> Packet: return packet - @cython.cfunc - def _send_packet_and_recv(self, packet: Packet | None): + @cython.ccall + def decode(self, packet: Packet | None = None): + """Decode a subtitle packet, returning a list of :class:`.Subtitle` objects + if a subtitle was decoded, or an empty list otherwise.""" + if not self.codec.ptr: + raise ValueError("cannot decode unknown codec") + if packet is None: raise RuntimeError("packet cannot be None") + self.open(strict=False) proxy: SubtitleProxy = SubtitleProxy() got_frame: cython.int = 0 @@ -121,7 +128,7 @@ def _send_packet_and_recv(self, packet: Packet | None): ) if got_frame: - return SubtitleSet(proxy) + return list(SubtitleSet(proxy)) return [] @cython.ccall From 8b6f0dcc9c8662855cef60828e04ebb22b4d53ec Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Fri, 17 Apr 2026 02:18:43 +0100 Subject: [PATCH 737/846] Reuse a `AVPacket` read buffer when demuxing --- av/container/input.py | 18 ++++++++++++++---- include/avcodec.pxd | 2 ++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/av/container/input.py b/av/container/input.py index 97037457f..699497dca 100644 --- a/av/container/input.py +++ b/av/container/input.py @@ -153,6 +153,7 @@ def demux(self, *args, **kwargs): i: cython.uint packet: Packet + read_packet: Packet ret: cython.int self.set_timeout(self.read_timeout) @@ -165,22 +166,31 @@ def demux(self, *args, **kwargs): raise ValueError(f"stream index {i} out of range") include_stream[i] = True + # Pre-allocate a AVPacket that is reused as the read buffer. + read_packet = Packet() while True: - packet = Packet() + # Reset the read buffer + with cython.nogil: + lib.av_packet_unref(read_packet.ptr) try: self.start_timeout() with cython.nogil: - ret = lib.av_read_frame(self.ptr, packet.ptr) + ret = lib.av_read_frame(self.ptr, read_packet.ptr) self.err_check(ret) except EOFError: break - if include_stream[packet.ptr.stream_index]: + if include_stream[read_packet.ptr.stream_index]: # If AVFMTCTX_NOHEADER is set in ctx_flags, then new streams # may also appear in av_read_frame(). # http://ffmpeg.org/doxygen/trunk/structAVFormatContext.html # TODO: find better way to handle this - if packet.ptr.stream_index < len(self.streams): + if read_packet.ptr.stream_index < len(self.streams): + # Move the encoded data out of the read buffer into a + # fresh Packet for the caller. + packet = Packet() + with cython.nogil: + lib.av_packet_move_ref(packet.ptr, read_packet.ptr) packet._stream = self.streams[packet.ptr.stream_index] # Keep track of this so that remuxing is easier. packet.ptr.time_base = packet._stream.ptr.time_base diff --git a/include/avcodec.pxd b/include/avcodec.pxd index 8c610ea67..28dfcfed9 100644 --- a/include/avcodec.pxd +++ b/include/avcodec.pxd @@ -407,6 +407,8 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef void av_packet_free(AVPacket **) cdef int av_new_packet(AVPacket*, int) cdef int av_packet_ref(AVPacket *dst, const AVPacket *src) + cdef void av_packet_unref(AVPacket *pkt) + cdef void av_packet_move_ref(AVPacket *dst, AVPacket *src) cdef void av_packet_rescale_ts(AVPacket *pkt, AVRational src_tb, AVRational dst_tb) cdef enum AVSubtitleType: From b82546f682d0e63542a81a326e39b90323f5a0ac Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Fri, 17 Apr 2026 02:57:58 +0100 Subject: [PATCH 738/846] Use `AVPacket` directly Co-authored-by: WyattBlue --- av/container/input.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/av/container/input.py b/av/container/input.py index 699497dca..c7e82a84b 100644 --- a/av/container/input.py +++ b/av/container/input.py @@ -153,7 +153,7 @@ def demux(self, *args, **kwargs): i: cython.uint packet: Packet - read_packet: Packet + read_packet: cython.pointer[lib.AVPacket] ret: cython.int self.set_timeout(self.read_timeout) @@ -167,30 +167,34 @@ def demux(self, *args, **kwargs): include_stream[i] = True # Pre-allocate a AVPacket that is reused as the read buffer. - read_packet = Packet() + with cython.nogil: + read_packet = lib.av_packet_alloc() + if read_packet == cython.NULL: + raise MemoryError("Could not allocate packet") + while True: # Reset the read buffer with cython.nogil: - lib.av_packet_unref(read_packet.ptr) + lib.av_packet_unref(read_packet) try: self.start_timeout() with cython.nogil: - ret = lib.av_read_frame(self.ptr, read_packet.ptr) + ret = lib.av_read_frame(self.ptr, read_packet) self.err_check(ret) except EOFError: break - if include_stream[read_packet.ptr.stream_index]: + if include_stream[read_packet.stream_index]: # If AVFMTCTX_NOHEADER is set in ctx_flags, then new streams # may also appear in av_read_frame(). # http://ffmpeg.org/doxygen/trunk/structAVFormatContext.html # TODO: find better way to handle this - if read_packet.ptr.stream_index < len(self.streams): + if read_packet.stream_index < len(self.streams): # Move the encoded data out of the read buffer into a # fresh Packet for the caller. packet = Packet() with cython.nogil: - lib.av_packet_move_ref(packet.ptr, read_packet.ptr) + lib.av_packet_move_ref(packet.ptr, read_packet) packet._stream = self.streams[packet.ptr.stream_index] # Keep track of this so that remuxing is easier. packet.ptr.time_base = packet._stream.ptr.time_base @@ -207,6 +211,8 @@ def demux(self, *args, **kwargs): finally: self.set_timeout(None) free(include_stream) + if read_packet != cython.NULL: + lib.av_packet_free(cython.address(read_packet)) def decode(self, *args, **kwargs): """decode(streams=None, video=None, audio=None, subtitles=None, data=None) From e50b0fbf38b021eac9a7546ddaebb84a1e6dd64d Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 16 Apr 2026 23:56:24 -0400 Subject: [PATCH 739/846] Update changelog --- CHANGELOG.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 30a085fb2..863b3d236 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -31,6 +31,13 @@ v17.0.1 (next) Fixes: - A cdivision decorator for faster division. +- Make VideoFormat.components lazy by :gh-user:`WyattBlue`. +- Break reference cycle ``StreamContainer.get()`` by :gh-user:`lgeiger`. +- Expose threads in filter graph by :gh-user:`lgeiger`. +- Fix crash with container closing with GC by :gh-user:`WyattBlue`. +- Fix regression in 17.0.0 :issue:`2223`. +- Remove ``_send_packet_and_recv`` to simplify ``decode()`` by :gh-user:`lgeiger`. +- Reuse a ``AVPacket`` read buffer when demuxing by :gh-user:`lgeiger` and :gh-user:`WyattBlue`. v17.0.0 ------- From da9ce591e9c73653b85a0134c387925a6837c10d Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 18 Apr 2026 11:34:49 -0400 Subject: [PATCH 740/846] Use `layout_compare()` for `AudioLayout.__eq__()` And use AVCodecContext.frame_num instead of keeping track ourselves. --- CHANGELOG.rst | 5 ++++- av/audio/layout.py | 10 +++++++--- av/video/codeccontext.pxd | 1 - av/video/codeccontext.py | 7 +------ include/avcodec.pxd | 11 ++--------- 5 files changed, 14 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 863b3d236..63d3dfd78 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -31,13 +31,16 @@ v17.0.1 (next) Fixes: - A cdivision decorator for faster division. +- Adjust tests so they work with 8.1 by :gh-user:`strophy`. - Make VideoFormat.components lazy by :gh-user:`WyattBlue`. - Break reference cycle ``StreamContainer.get()`` by :gh-user:`lgeiger`. - Expose threads in filter graph by :gh-user:`lgeiger`. - Fix crash with container closing with GC by :gh-user:`WyattBlue`. -- Fix regression in 17.0.0 :issue:`2223`. +- Fix "Writing packets to data stream broken in av>=17" regression :issue:`2223` by :gh-user:`WyattBlue`. - Remove ``_send_packet_and_recv`` to simplify ``decode()`` by :gh-user:`lgeiger`. - Reuse a ``AVPacket`` read buffer when demuxing by :gh-user:`lgeiger` and :gh-user:`WyattBlue`. +- Use ``AVCodecContext.frame_num`` instead of counting ourselves by :gh-user:`WyattBlue`. +- Use ``av_channel_layout_compare()`` for ``AudioLayout.__eq__()`` by :gh-user:`WyattBlue`. v17.0.0 ------- diff --git a/av/audio/layout.py b/av/audio/layout.py index 43bedb9c7..e7163e33b 100644 --- a/av/audio/layout.py +++ b/av/audio/layout.py @@ -48,10 +48,14 @@ def __repr__(self): return f"" def __eq__(self, other): + if not isinstance(other, AudioLayout): + return False + c_other: lib.AVChannelLayout = cython.cast(AudioLayout, other).layout return ( - isinstance(other, AudioLayout) - and self.name == other.name - and self.nb_channels == other.nb_channels + lib.av_channel_layout_compare( + cython.address(self.layout), cython.address(c_other) + ) + == 0 ) @property diff --git a/av/video/codeccontext.pxd b/av/video/codeccontext.pxd index 831e5c659..3489fdb7a 100644 --- a/av/video/codeccontext.pxd +++ b/av/video/codeccontext.pxd @@ -18,5 +18,4 @@ cdef struct AVCodecPrivateData: cdef class VideoCodecContext(CodecContext): cdef AVCodecPrivateData _private_data cdef readonly VideoReformatter reformatter - cdef readonly int encoded_frame_count cdef VideoFrame next_frame diff --git a/av/video/codeccontext.py b/av/video/codeccontext.py index 49ed82f52..c9ac25788 100644 --- a/av/video/codeccontext.py +++ b/av/video/codeccontext.py @@ -72,15 +72,12 @@ def _init( # is_hwaccel() function on each stream's codec context. self.hwaccel_ctx = None - self.encoded_frame_count = 0 - @cython.cfunc def _prepare_frames_for_encode(self, input: Frame | None) -> list: if input is None or not input: return [None] vframe: VideoFrame = input - # Reformat if it doesn't match. if ( vframe.format.pix_fmt != self.pix_fmt or vframe.width != self.ptr.width @@ -97,11 +94,9 @@ def _prepare_frames_for_encode(self, input: Frame | None) -> list: threads=self.ptr.thread_count, ) - # There is no pts, so create one. if vframe.ptr.pts == lib.AV_NOPTS_VALUE: - vframe.ptr.pts = cython.cast(int64_t, self.encoded_frame_count) + vframe.ptr.pts = self.ptr.frame_num - self.encoded_frame_count += 1 return [vframe] @cython.cfunc diff --git a/include/avcodec.pxd b/include/avcodec.pxd index 28dfcfed9..c273443f0 100644 --- a/include/avcodec.pxd +++ b/include/avcodec.pxd @@ -1,31 +1,23 @@ from libc.stdint cimport int64_t, uint8_t, uint16_t, uint32_t, uint64_t cdef extern from "libavutil/channel_layout.h" nogil: - ctypedef enum AVChannelOrder: - pass ctypedef enum AVChannel: AV_CHAN_NONE = -1 AV_CHAN_FRONT_LEFT AV_CHAN_FRONT_RIGHT AV_CHAN_FRONT_CENTER ctypedef struct AVChannelLayout: - AVChannelOrder order int nb_channels - uint64_t mask - # union: - # uint64_t mask - # AVChannelCustom *map - void *opaque int av_channel_layout_default(AVChannelLayout *ch_layout, int nb_channels) int av_channel_layout_from_string(AVChannelLayout *channel_layout, const char *str) int av_channel_layout_describe(const AVChannelLayout *channel_layout, char *buf, size_t buf_size) int av_channel_name(char *buf, size_t buf_size, AVChannel channel_id) int av_channel_description(char *buf, size_t buf_size, AVChannel channel_id) + int av_channel_layout_compare(AVChannelLayout *chl, AVChannelLayout *chl1) AVChannel av_channel_layout_channel_from_index(AVChannelLayout *channel_layout, unsigned int idx) cdef extern from "libavcodec/avcodec.h" nogil: - cdef set pyav_get_available_codecs() cdef int avcodec_version() cdef char* avcodec_configuration() cdef char* avcodec_license() @@ -290,6 +282,7 @@ cdef extern from "libavcodec/avcodec.h" nogil: int subtitle_header_size uint8_t *subtitle_header + int64_t frame_num cdef AVCodecContext* avcodec_alloc_context3(const AVCodec *codec) cdef void avcodec_free_context(AVCodecContext **ctx) From 0d2ab042072d026cfe7f16bff775c5b754dee8b3 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 18 Apr 2026 11:56:00 -0400 Subject: [PATCH 741/846] Update authors --- AUTHORS.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 29f6e93ab..536dcd037 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -10,12 +10,12 @@ All contributors (by number of commits): - Mark Reid ; `@markreidvfx `_ +* Lukas Geiger * Vidar Tonaas Fauske ; `@vidartf `_ * laggykiller ; `@laggykiller `_ * Billy Shambrook ; `@billyshambrook `_ * Casper van der Wel * Philip de Nier -* Lukas Geiger * Tadas Dailyda * Dave Johansen * JoeUgly <41972063+JoeUgly@users.noreply.github.com> @@ -56,6 +56,7 @@ All contributors (by number of commits): * Ihor Liubymov * Johannes Erdfelt * Karl Litterfeldt ; `@litterfeldt `_ +* Leon White * Martin Larralde * Simon-Martin Schröder * Matteo Destro @@ -78,6 +79,7 @@ All contributors (by number of commits): * Ian Lee * Ryan Huang * Arthur Barros +* bdavid-evertz * Carlos Ruiz * Carlos Ruiz * Maxime Desroches From fa90286add4ebbaa2e141b5d6d1ba3e290973a38 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 18 Apr 2026 12:23:03 -0400 Subject: [PATCH 742/846] Release 17.0.1 --- av/about.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/about.py b/av/about.py index b7aebaa88..dba3a77b1 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "17.0.1pre" +__version__ = "17.0.1" From 59dfb48fc1efbedc80dc2f6599d7862e301ee021 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 18 Apr 2026 12:33:42 -0400 Subject: [PATCH 743/846] Next dev version --- CHANGELOG.rst | 8 +++++++- av/about.py | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 63d3dfd78..fa41c6682 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -25,11 +25,17 @@ We are operating with `semantic versioning `_. - Bug fixes (PATCH) go here. - $CHANGE by :gh-user:`mikeboers` in (:pr:`1`). -v17.0.1 (next) +v17.0.2 (next) -------------- Fixes: + +v17.0.1 +------- + +Fixes: + - A cdivision decorator for faster division. - Adjust tests so they work with 8.1 by :gh-user:`strophy`. - Make VideoFormat.components lazy by :gh-user:`WyattBlue`. diff --git a/av/about.py b/av/about.py index dba3a77b1..4fe5d9474 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "17.0.1" +__version__ = "17.0.2pre" From a2a961668b044e914a1cc79e55b23c679a2a1277 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 19 Apr 2026 13:13:38 -0400 Subject: [PATCH 744/846] Test with ffmpeg 8.1 binary --- scripts/ffmpeg-8.1.json | 3 +++ scripts/ffmpeg-latest.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 scripts/ffmpeg-8.1.json diff --git a/scripts/ffmpeg-8.1.json b/scripts/ffmpeg-8.1.json new file mode 100644 index 000000000..12fabf478 --- /dev/null +++ b/scripts/ffmpeg-8.1.json @@ -0,0 +1,3 @@ +{ + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.1-1/ffmpeg-{platform}.tar.gz" +} diff --git a/scripts/ffmpeg-latest.json b/scripts/ffmpeg-latest.json index a7df98bb9..12fabf478 100644 --- a/scripts/ffmpeg-latest.json +++ b/scripts/ffmpeg-latest.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0.1-5/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.1-1/ffmpeg-{platform}.tar.gz" } From 8ad40fac99a8d3b2b35237b18817848e83b27ff0 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 22 Apr 2026 23:42:28 -0400 Subject: [PATCH 745/846] Fix #2010 Also raise error if format lazy adds streams. --- av/container/input.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/av/container/input.py b/av/container/input.py index c7e82a84b..81d167c0d 100644 --- a/av/container/input.py +++ b/av/container/input.py @@ -34,13 +34,21 @@ def __cinit__(self, *args, **kwargs): c_options: cython.pointer[cython.pointer[lib.AVDictionary]] = cython.NULL base_dict: Dictionary stream_dict: Dictionary - if self.options or self.stream_options: + nb_streams_before: cython.uint = self.ptr.nb_streams + if self.stream_options and nb_streams_before == 0: + raise ValueError( + "stream_options were provided, but this format does not expose " + "its streams before avformat_find_stream_info (e.g. MPEG). " + "Per-stream options cannot be applied." + ) + # Only allocate c_options when streams are already known. + if (self.options or self.stream_options) and nb_streams_before > 0: base_dict = Dictionary(self.options) c_options = cython.cast( cython.pointer[cython.pointer[lib.AVDictionary]], - malloc(self.ptr.nb_streams * cython.sizeof(cython.p_void)), + malloc(nb_streams_before * cython.sizeof(cython.p_void)), ) - for i in range(self.ptr.nb_streams): + for i in range(nb_streams_before): c_options[i] = cython.NULL if i < len(self.stream_options) and self.stream_options: stream_dict = base_dict.copy() @@ -56,9 +64,8 @@ def __cinit__(self, *args, **kwargs): self.set_timeout(None) self.err_check(ret) - # Clean up all of our options. if c_options: - for i in range(self.ptr.nb_streams): + for i in range(nb_streams_before): lib.av_dict_free(cython.address(c_options[i])) free(c_options) From 4be3259603c38b142dfd9933ab2506122f7d81e1 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 23 Apr 2026 00:03:10 -0400 Subject: [PATCH 746/846] Fix more wrong uses of malloc --- av/container/core.py | 5 +++++ av/container/input.py | 2 ++ tests/test_chapters.py | 8 ++++++++ 3 files changed, 15 insertions(+) diff --git a/av/container/core.py b/av/container/core.py index ab6c889b7..4195be248 100755 --- a/av/container/core.py +++ b/av/container/core.py @@ -435,6 +435,11 @@ def set_chapters(self, chapters): with cython.nogil: _free_chapters(self.ptr) + if count == 0: + self.ptr.nb_chapters = 0 + self.ptr.chapters = cython.NULL + return + ch_array = cython.cast( AVChapterPtrPtr, lib.av_malloc(count * cython.sizeof(cython.pointer[lib.AVChapter])), diff --git a/av/container/input.py b/av/container/input.py index 81d167c0d..63792e02a 100644 --- a/av/container/input.py +++ b/av/container/input.py @@ -151,6 +151,8 @@ def demux(self, *args, **kwargs): self._assert_open() streams: list[Stream] = self.streams.get(*args, **kwargs) + if self.ptr.nb_streams == 0: + return include_stream: cython.pointer[cython.bint] = cython.cast( cython.pointer[cython.bint], malloc(self.ptr.nb_streams * cython.sizeof(bint)), diff --git a/tests/test_chapters.py b/tests/test_chapters.py index c8a3626ce..21272f527 100644 --- a/tests/test_chapters.py +++ b/tests/test_chapters.py @@ -56,3 +56,11 @@ def test_set_chapters() -> None: with av.open(path) as container: container.set_chapters(chapters) assert container.chapters() == chapters + + +def test_set_chapters_empty() -> None: + path = fate_suite("vorbis/vorbis_chapter_extension_demo.ogg") + with av.open(path) as container: + assert len(container.chapters()) == 4 + container.set_chapters([]) + assert container.chapters() == [] From b7ad8dc621f480c0d086680641e361902cfea176 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 23 Apr 2026 13:35:50 -0400 Subject: [PATCH 747/846] Set vcodec on container, closes #2243 --- av/container/core.py | 10 ++++++++++ av/container/core.pyi | 1 + include/avformat.pxd | 7 ++++--- tests/test_containerformat.py | 19 +++++++++++++++++++ 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/av/container/core.py b/av/container/core.py index 4195be248..8aeaa7a3e 100755 --- a/av/container/core.py +++ b/av/container/core.py @@ -390,6 +390,16 @@ def _assert_open(self): if self.ptr == cython.NULL: raise AssertionError("Container is not open") + @property + def video_codec_id(self): + self._assert_open() + return self.ptr.video_codec_id + + @video_codec_id.setter + def video_codec_id(self, value: lib.AVCodecID): + self._assert_open() + self.ptr.video_codec_id = value + @property def flags(self): self._assert_open() diff --git a/av/container/core.pyi b/av/container/core.pyi index f62b6c813..81e4936e4 100644 --- a/av/container/core.pyi +++ b/av/container/core.pyi @@ -91,6 +91,7 @@ class Container: open_timeout: Real | None read_timeout: Real | None flags: int + video_codec_id: int def __enter__(self) -> Container: ... def __exit__( self, diff --git a/include/avformat.pxd b/include/avformat.pxd index 94bf27b2e..5aca5e287 100644 --- a/include/avformat.pxd +++ b/include/avformat.pxd @@ -71,7 +71,7 @@ cdef extern from "libavformat/avformat.h" nogil: int64_t(*seek)(void *opaque, int64_t offset, int whence) ) - # http://ffmpeg.org/doxygen/trunk/structAVInputFormat.html + # https://ffmpeg.org/doxygen/trunk/structAVInputFormat.html cdef struct AVInputFormat: const char *name const char *long_name @@ -79,13 +79,13 @@ cdef extern from "libavformat/avformat.h" nogil: int flags const AVClass *priv_class - # http://ffmpeg.org/doxygen/trunk/structAVOutputFormat.html + # https://ffmpeg.org/doxygen/trunk/structAVOutputFormat.html cdef struct AVOutputFormat: const char *name const char *long_name const char *extensions - AVCodecID video_codec AVCodecID audio_codec + AVCodecID video_codec AVCodecID subtitle_codec int flags const AVClass *priv_class @@ -156,6 +156,7 @@ cdef extern from "libavformat/avformat.h" nogil: int bit_rate int flags AVCodecID audio_codec_id + AVCodecID video_codec_id void *opaque int (*io_open)( AVFormatContext *s, diff --git a/tests/test_containerformat.py b/tests/test_containerformat.py index 43f1eca9f..ca0b72fd7 100644 --- a/tests/test_containerformat.py +++ b/tests/test_containerformat.py @@ -1,5 +1,7 @@ from av import ContainerFormat, formats_available, open +from .common import fate_suite + def test_matroska() -> None: with open("test.mkv", "w") as container: @@ -56,5 +58,22 @@ def test_stream_segment() -> None: assert fmt.no_file +def test_video_codec_id() -> None: + # Regression test for https://github.com/PyAV-Org/PyAV/issues/2243 + # video_codec_id allows overriding the codec used when opening a container. + AV_CODEC_ID_NONE = 0 + AV_CODEC_ID_H264 = 27 + + with open(fate_suite("h264/interlaced_crop.mp4")) as container: + assert container.video_codec_id == AV_CODEC_ID_NONE + container.video_codec_id = AV_CODEC_ID_H264 + assert container.video_codec_id == AV_CODEC_ID_H264 + + with open("test.mkv", "w") as container: + assert container.video_codec_id == AV_CODEC_ID_NONE + container.video_codec_id = AV_CODEC_ID_H264 + assert container.video_codec_id == AV_CODEC_ID_H264 + + def test_formats_available() -> None: assert formats_available From 7dd26053b481b9b23fac75831a3c8a066b53a422 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 26 Apr 2026 17:18:53 -0400 Subject: [PATCH 748/846] Expose `global_quality` in CodecContext --- CHANGELOG.rst | 3 +++ av/codec/context.py | 17 +++++++++++++++++ av/codec/context.pyi | 1 + include/avutil.pxd | 2 ++ tests/test_codec_context.py | 5 +++++ 5 files changed, 28 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index fa41c6682..d89ead006 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -28,6 +28,9 @@ We are operating with `semantic versioning `_. v17.0.2 (next) -------------- +Features: +- Expose ``AVCodecContext.global_quality`` by :gh-user:`WyattBlue` in (:pr:`2246`). + Fixes: diff --git a/av/codec/context.py b/av/codec/context.py index 10b255b2b..0e8692d80 100644 --- a/av/codec/context.py +++ b/av/codec/context.py @@ -590,6 +590,23 @@ def codec_tag(self, value): else: raise ValueError("Codec tag should be a 4 character string.") + @property + @cython.cdivision(True) + def global_quality(self): + """Global quality for codecs which cannot change it per frame. + + Stored internally in lambda units; this property converts to/from + QP units using ``FF_QP2LAMBDA``. + + Wraps :ffmpeg:`AVCodecContext.global_quality`. + + """ + return self.ptr.global_quality // lib.FF_QP2LAMBDA + + @global_quality.setter + def global_quality(self, value: cython.int): + self.ptr.global_quality = value * lib.FF_QP2LAMBDA + @property def bit_rate(self): return self.ptr.bit_rate if self.ptr.bit_rate > 0 else None diff --git a/av/codec/context.pyi b/av/codec/context.pyi index e0c447186..808bb292b 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -62,6 +62,7 @@ class CodecContext: extradata: bytes | None time_base: Fraction codec_tag: str + global_quality: int bit_rate: int | None bit_rate_tolerance: int thread_count: int diff --git a/include/avutil.pxd b/include/avutil.pxd index 3ee78d2f5..d7a1fb765 100644 --- a/include/avutil.pxd +++ b/include/avutil.pxd @@ -18,6 +18,8 @@ cdef extern from "libavutil/avutil.h" nogil: cdef char* avutil_configuration() cdef char* avutil_license() + int FF_QP2LAMBDA + cdef enum AVPictureType: AV_PICTURE_TYPE_NONE AV_PICTURE_TYPE_I diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 1173a5865..d0629480e 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -68,6 +68,11 @@ def iter_raw_frames( class TestCodecContext(TestCase): + def test_global_quality(self): + ctx = Codec("mpeg4", "w").create() + ctx.global_quality = 5 + assert ctx.global_quality == 5 + def test_skip_frame_default(self): ctx = Codec("png", "w").create() assert ctx.skip_frame == "DEFAULT" From b251531036e45292c5d7312700a8c124a53bc175 Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Tue, 28 Apr 2026 18:48:25 -0600 Subject: [PATCH 749/846] Copy time_base in `add_stream_from_template()` (#2249) --- av/container/output.py | 4 ++++ tests/test_remux.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/av/container/output.py b/av/container/output.py index d0c51cc6b..4e868d60c 100644 --- a/av/container/output.py +++ b/av/container/output.py @@ -262,6 +262,10 @@ def add_stream_from_template( # Reset the codec tag assuming we are remuxing. ctx.codec_tag = 0 + # Copy the template's stream time_base + stream.time_base = template.ptr.time_base + ctx.time_base = template.ptr.time_base + # Some formats want stream headers to be separate if self.ptr.oformat.flags & lib.AVFMT_GLOBALHEADER: ctx.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER diff --git a/tests/test_remux.py b/tests/test_remux.py index 37a571883..9c4aecfe8 100644 --- a/tests/test_remux.py +++ b/tests/test_remux.py @@ -3,6 +3,8 @@ import av import av.datasets +from .common import fate_suite + def test_video_remux() -> None: input_path = av.datasets.curated("pexels/time-lapse-video-of-night-sky-857195.mp4") @@ -79,3 +81,30 @@ def test_add_mux_stream_no_codec_context() -> None: # repr should not crash assert "video/" in repr(video_stream) assert "audio/" in repr(audio_stream) + + +def test_add_stream_from_template_copies_time_base() -> None: + """add_stream_from_template must propagate the source stream's time_base. + + AVCodecParameters does not carry time_base, so without an explicit copy + the output stream's time_base stays as None + """ + video_path = av.datasets.curated("pexels/time-lapse-video-of-night-sky-857195.mp4") + with ( + av.open(video_path) as input_, + av.open(io.BytesIO(), "w", format="mp4") as output, + ): + in_video = input_.streams.video[0] + out_video = output.add_stream_from_template(in_video) + assert out_video.time_base is not None + assert out_video.time_base == in_video.time_base + + audio_path = fate_suite("audio-reference/chorusnoise_2ch_44kHz_s16.wav") + with ( + av.open(audio_path) as input_, + av.open(io.BytesIO(), "w", format="wav") as output, + ): + in_audio = input_.streams.audio[0] + out_audio = output.add_stream_from_template(in_audio) + assert out_audio.time_base is not None + assert out_audio.time_base == in_audio.time_base From cf289734f8ac78b1344b3773978f89e2871a4d9e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 30 Apr 2026 17:57:27 -0400 Subject: [PATCH 750/846] Remove strlen usage by using a fixed buffer --- av/subtitles/codeccontext.py | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/av/subtitles/codeccontext.py b/av/subtitles/codeccontext.py index a3a1a550c..c2a9fb4ae 100644 --- a/av/subtitles/codeccontext.py +++ b/av/subtitles/codeccontext.py @@ -6,7 +6,7 @@ from cython.cimports.av.packet import Packet from cython.cimports.av.subtitles.subtitle import SubtitleProxy, SubtitleSet from cython.cimports.cpython.bytes import PyBytes_FromStringAndSize -from cython.cimports.libc.string import memcpy, strlen +from cython.cimports.libc.string import memcpy @cython.cclass @@ -65,28 +65,16 @@ def encode_subtitle(self, subtitle: SubtitleSet) -> Packet: self.open(strict=False) - # Calculate buffer size from subtitle text length - buf_size: cython.size_t = 0 - i: cython.uint - for i in range(subtitle.proxy.struct.num_rects): - rect = subtitle.proxy.struct.rects[i] - if rect.ass != cython.NULL: - buf_size += strlen(rect.ass) - if rect.text != cython.NULL: - buf_size += strlen(rect.text) - buf_size += 1024 # padding for format overhead - - buf: cython.p_uchar = cython.cast(cython.p_uchar, lib.av_malloc(buf_size)) + buf: cython.p_uchar = cython.cast(cython.p_uchar, lib.av_malloc(1024 * 1024)) if buf == cython.NULL: - raise MemoryError("Failed to allocate subtitle encode buffer") + raise MemoryError() ret: cython.int = lib.avcodec_encode_subtitle( self.ptr, buf, - buf_size, + 1024 * 1024, cython.address(subtitle.proxy.struct), ) - if ret < 0: lib.av_free(buf) err_check(ret, "avcodec_encode_subtitle()") @@ -94,7 +82,6 @@ def encode_subtitle(self, subtitle: SubtitleSet) -> Packet: packet: Packet = Packet(ret) memcpy(packet.ptr.data, buf, ret) lib.av_free(buf) - packet.ptr.pts = subtitle.proxy.struct.pts packet.ptr.dts = subtitle.proxy.struct.pts packet.ptr.duration = ( From 5fe3c409e201ae5049599a75a5efdf7e4eb8261c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 1 May 2026 10:18:46 -0400 Subject: [PATCH 751/846] Resolve #2247 --- av/codec/context.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/av/codec/context.py b/av/codec/context.py index 0e8692d80..43d39c0ed 100644 --- a/av/codec/context.py +++ b/av/codec/context.py @@ -462,6 +462,14 @@ def decode(self, packet: Packet | None = None): you do not want the library to automatically re-order frames for you (if they are encoded with a codec that has B-frames). + .. warning:: + + This method is **not thread-safe**. Calling :meth:`decode` concurrently + from multiple threads on the same :class:`CodecContext` will corrupt + internal FFmpeg state and likely cause a crash (segfault). FFmpeg 8.1 + enforces this more strictly than earlier releases. If you need to decode + from multiple threads, give each thread its own :class:`CodecContext`. + """ if not self.codec.ptr: raise ValueError("cannot decode unknown codec") From 130f74ec43da6ca66babd4ad78e3023a215587b4 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 2 May 2026 13:38:29 -0400 Subject: [PATCH 752/846] User newer build of ffmpeg 8.1 --- scripts/ffmpeg-8.1.json | 2 +- scripts/ffmpeg-latest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/ffmpeg-8.1.json b/scripts/ffmpeg-8.1.json index 12fabf478..24fab9018 100644 --- a/scripts/ffmpeg-8.1.json +++ b/scripts/ffmpeg-8.1.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.1-1/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.1-3/ffmpeg-{platform}.tar.gz" } diff --git a/scripts/ffmpeg-latest.json b/scripts/ffmpeg-latest.json index 12fabf478..24fab9018 100644 --- a/scripts/ffmpeg-latest.json +++ b/scripts/ffmpeg-latest.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.1-1/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.1-3/ffmpeg-{platform}.tar.gz" } From 4c5bac1a42b9b4f65c909683096919b1110296a6 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 6 May 2026 17:22:26 -0400 Subject: [PATCH 753/846] Use ffmpeg 8.1.1 --- scripts/activate.sh | 2 +- scripts/ffmpeg-8.1.json | 2 +- scripts/ffmpeg-latest.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/activate.sh b/scripts/activate.sh index 8df33716c..6ed8f5ac8 100755 --- a/scripts/activate.sh +++ b/scripts/activate.sh @@ -21,7 +21,7 @@ if [[ ! "$PYAV_LIBRARY" ]]; then return 1 fi else - PYAV_LIBRARY=ffmpeg-8.1 + PYAV_LIBRARY=ffmpeg-8.1.1 echo "No \$PYAV_LIBRARY set; defaulting to $PYAV_LIBRARY" fi fi diff --git a/scripts/ffmpeg-8.1.json b/scripts/ffmpeg-8.1.json index 24fab9018..76ab6c2e4 100644 --- a/scripts/ffmpeg-8.1.json +++ b/scripts/ffmpeg-8.1.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.1-3/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.1.1-1/ffmpeg-{platform}.tar.gz" } diff --git a/scripts/ffmpeg-latest.json b/scripts/ffmpeg-latest.json index 24fab9018..76ab6c2e4 100644 --- a/scripts/ffmpeg-latest.json +++ b/scripts/ffmpeg-latest.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.1-3/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.1.1-1/ffmpeg-{platform}.tar.gz" } From f9f07306a8c547c05b1c3f420e1f927d1af8079e Mon Sep 17 00:00:00 2001 From: HotariTobu Date: Thu, 7 May 2026 21:03:43 +0900 Subject: [PATCH 754/846] Make Disposition an IntFlag Stream.disposition is typed as Disposition but the runtime setter assigns to a C int field, calling int(value). Plain Flag members are not int subclasses, so this raises TypeError. IntFlag members are int subclasses; bitwise / membership semantics are preserved. Co-Authored-By: Claude Opus 4.7 (1M context) --- av/stream.py | 4 ++-- av/stream.pyi | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/av/stream.py b/av/stream.py index f8ce178b2..a1a615aae 100644 --- a/av/stream.py +++ b/av/stream.py @@ -1,4 +1,4 @@ -from enum import Flag +from enum import IntFlag import cython from cython.cimports import libav as lib @@ -12,7 +12,7 @@ ) -class Disposition(Flag): +class Disposition(IntFlag): default = 1 << 0 dub = 1 << 1 original = 1 << 2 diff --git a/av/stream.pyi b/av/stream.pyi index 6f4459020..680166fd6 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -1,4 +1,4 @@ -from enum import Flag +from enum import IntFlag from fractions import Fraction from typing import Literal, cast @@ -6,7 +6,7 @@ from .codec import Codec, CodecContext from .container import Container from .index import IndexEntries -class Disposition(Flag): +class Disposition(IntFlag): default = cast(int, ...) dub = cast(int, ...) original = cast(int, ...) From 923be68ac0ad65bf15fd8fbb3513f0f10b3e9034 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 8 May 2026 18:54:52 -0400 Subject: [PATCH 755/846] Use uint8_t instead of bint for include_stream flag array --- av/container/input.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/av/container/input.py b/av/container/input.py index 63792e02a..ef4a5fb00 100644 --- a/av/container/input.py +++ b/av/container/input.py @@ -6,7 +6,7 @@ from cython.cimports.av.packet import Packet from cython.cimports.av.stream import Stream, wrap_stream from cython.cimports.av.utils import avdict_to_dict -from cython.cimports.libc.stdint import int64_t +from cython.cimports.libc.stdint import int64_t, uint8_t from cython.cimports.libc.stdlib import free, malloc @@ -153,9 +153,9 @@ def demux(self, *args, **kwargs): streams: list[Stream] = self.streams.get(*args, **kwargs) if self.ptr.nb_streams == 0: return - include_stream: cython.pointer[cython.bint] = cython.cast( - cython.pointer[cython.bint], - malloc(self.ptr.nb_streams * cython.sizeof(bint)), + include_stream: cython.pointer[uint8_t] = cython.cast( + cython.pointer[uint8_t], + malloc(self.ptr.nb_streams * cython.sizeof(uint8_t)), ) if include_stream == cython.NULL: raise MemoryError() @@ -168,12 +168,12 @@ def demux(self, *args, **kwargs): self.set_timeout(self.read_timeout) try: for i in range(self.ptr.nb_streams): - include_stream[i] = False + include_stream[i] = 0 for stream in streams: i = stream.index if i >= self.ptr.nb_streams: raise ValueError(f"stream index {i} out of range") - include_stream[i] = True + include_stream[i] = 1 # Pre-allocate a AVPacket that is reused as the read buffer. with cython.nogil: From 0f5a9398a0093ce2daa33d83a508765626b1c93e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 8 May 2026 19:08:19 -0400 Subject: [PATCH 756/846] Mark leaf cclasses as @cython.final Final classes can't be subclassed, letting Cython skip vtable lookups on method calls and enable other dispatch optimizations. Applied only to classes that aren't extended anywhere in the codebase. --- av/audio/codeccontext.py | 1 + av/audio/fifo.py | 1 + av/audio/format.py | 1 + av/audio/frame.py | 1 + av/audio/layout.py | 1 + av/audio/plane.py | 1 + av/audio/resampler.py | 1 + av/audio/stream.py | 1 + av/bitstream.py | 1 + av/buffer.py | 1 + av/codec/codec.py | 1 + av/codec/hwaccel.py | 2 ++ av/container/input.py | 1 + av/container/output.py | 1 + av/container/pyio.py | 1 + av/container/streams.py | 1 + av/descriptor.py | 1 + av/dictionary.py | 1 + av/filter/context.py | 1 + av/filter/filter.py | 1 + av/filter/graph.py | 1 + av/filter/link.py | 2 ++ av/format.py | 1 + av/index.py | 2 ++ av/logging.py | 1 + av/opaque.py | 1 + av/option.py | 2 ++ av/packet.py | 2 ++ av/sidedata/encparams.py | 2 ++ av/sidedata/motionvectors.py | 2 ++ av/stream.py | 2 ++ av/subtitles/codeccontext.py | 1 + av/subtitles/stream.py | 1 + av/subtitles/subtitle.py | 5 +++++ av/video/codeccontext.py | 1 + av/video/format.py | 2 ++ av/video/frame.py | 2 ++ av/video/plane.py | 1 + av/video/reformatter.py | 1 + av/video/stream.py | 1 + 40 files changed, 54 insertions(+) diff --git a/av/audio/codeccontext.py b/av/audio/codeccontext.py index 8174ba26d..4e7118d34 100644 --- a/av/audio/codeccontext.py +++ b/av/audio/codeccontext.py @@ -7,6 +7,7 @@ from cython.cimports.av.packet import Packet +@cython.final @cython.cclass class AudioCodecContext(CodecContext): @cython.cfunc diff --git a/av/audio/fifo.py b/av/audio/fifo.py index ebd68adb8..0fd128ee7 100644 --- a/av/audio/fifo.py +++ b/av/audio/fifo.py @@ -3,6 +3,7 @@ from cython.cimports.av.error import err_check +@cython.final @cython.cclass class AudioFifo: """A simple audio sample FIFO (First In First Out) buffer.""" diff --git a/av/audio/format.py b/av/audio/format.py index 97e1b59a2..d398f6924 100644 --- a/av/audio/format.py +++ b/av/audio/format.py @@ -20,6 +20,7 @@ def get_audio_format(c_format: lib.AVSampleFormat) -> AudioFormat: return format +@cython.final @cython.cclass class AudioFormat: """Descriptor of audio formats.""" diff --git a/av/audio/frame.py b/av/audio/frame.py index 0ff969475..c843c9efd 100644 --- a/av/audio/frame.py +++ b/av/audio/frame.py @@ -27,6 +27,7 @@ def alloc_audio_frame() -> AudioFrame: } +@cython.final @cython.cclass class AudioFrame(Frame): """A frame of audio.""" diff --git a/av/audio/layout.py b/av/audio/layout.py index e7163e33b..344efa220 100644 --- a/av/audio/layout.py +++ b/av/audio/layout.py @@ -25,6 +25,7 @@ def get_audio_layout(c_layout: lib.AVChannelLayout) -> AudioLayout: return layout +@cython.final @cython.cclass class AudioLayout: def __cinit__(self, layout): diff --git a/av/audio/plane.py b/av/audio/plane.py index bdaf15708..e68b5403e 100644 --- a/av/audio/plane.py +++ b/av/audio/plane.py @@ -2,6 +2,7 @@ from cython.cimports.av.audio.frame import AudioFrame +@cython.final @cython.cclass class AudioPlane(Plane): def __cinit__(self, frame: AudioFrame, index: cython.int): diff --git a/av/audio/resampler.py b/av/audio/resampler.py index b0572607f..fb6d98910 100644 --- a/av/audio/resampler.py +++ b/av/audio/resampler.py @@ -6,6 +6,7 @@ from av.error import FFmpegError +@cython.final @cython.cclass class AudioResampler: """AudioResampler(format=None, layout=None, rate=None) diff --git a/av/audio/stream.py b/av/audio/stream.py index e32f7f759..38009db19 100644 --- a/av/audio/stream.py +++ b/av/audio/stream.py @@ -3,6 +3,7 @@ from cython.cimports.av.packet import Packet +@cython.final @cython.cclass class AudioStream(Stream): def __repr__(self): diff --git a/av/bitstream.py b/av/bitstream.py index 3c0bff1bd..db808ab35 100644 --- a/av/bitstream.py +++ b/av/bitstream.py @@ -6,6 +6,7 @@ from cython.cimports.libc.errno import EAGAIN +@cython.final @cython.cclass class BitStreamFilterContext: """ diff --git a/av/buffer.py b/av/buffer.py index 5b179f322..3fefceee4 100644 --- a/av/buffer.py +++ b/av/buffer.py @@ -9,6 +9,7 @@ from cython.cimports.libc.string import memcpy +@cython.final @cython.cclass class ByteSource: def __cinit__(self, owner): diff --git a/av/codec/codec.py b/av/codec/codec.py index 552af8284..681e8cd1d 100644 --- a/av/codec/codec.py +++ b/av/codec/codec.py @@ -59,6 +59,7 @@ class UnknownCodecError(ValueError): pass +@cython.final @cython.cclass class Codec: """Codec(name, mode='r') diff --git a/av/codec/hwaccel.py b/av/codec/hwaccel.py index 0155bdae4..69f742b29 100644 --- a/av/codec/hwaccel.py +++ b/av/codec/hwaccel.py @@ -54,6 +54,7 @@ def wrap_hwconfig(ptr: cython.pointer[cython.const[lib.AVCodecHWConfig]]) -> HWC return config +@cython.final @cython.cclass class HWConfig: def __init__(self, sentinel): @@ -103,6 +104,7 @@ def hwdevices_available(): return result +@cython.final @cython.cclass class HWAccel: def __init__( diff --git a/av/container/input.py b/av/container/input.py index ef4a5fb00..f5fc52547 100644 --- a/av/container/input.py +++ b/av/container/input.py @@ -20,6 +20,7 @@ def close_input(self: InputContainer): self._myflag &= ~2 # enum.input_was_opened = False +@cython.final @cython.cclass class InputContainer(Container): def __cinit__(self, *args, **kwargs): diff --git a/av/container/output.py b/av/container/output.py index 4e868d60c..239e550f2 100644 --- a/av/container/output.py +++ b/av/container/output.py @@ -32,6 +32,7 @@ def close_output(self: OutputContainer): self._myflag |= 8 # enum.done = True +@cython.final @cython.cclass class OutputContainer(Container): def __cinit__(self, *args, **kwargs): diff --git a/av/container/pyio.py b/av/container/pyio.py index 6c248ba13..d5c420ea4 100644 --- a/av/container/pyio.py +++ b/av/container/pyio.py @@ -14,6 +14,7 @@ ) +@cython.final @cython.cclass class PyIOFile: def __cinit__(self, file, buffer_size, writeable=None): diff --git a/av/container/streams.py b/av/container/streams.py index a84e88792..e588694b4 100644 --- a/av/container/streams.py +++ b/av/container/streams.py @@ -40,6 +40,7 @@ def _get_best_stream_index( return stream_index +@cython.final @cython.cclass class StreamContainer: """ diff --git a/av/descriptor.py b/av/descriptor.py index 20cd1a787..faaa0f57c 100644 --- a/av/descriptor.py +++ b/av/descriptor.py @@ -19,6 +19,7 @@ def wrap_avclass(ptr: cython.pointer[cython.const[lib.AVClass]]) -> Descriptor | return obj +@cython.final @cython.cclass class Descriptor: def __cinit__(self, sentinel): diff --git a/av/dictionary.py b/av/dictionary.py index bdd02d055..c9bd76acb 100644 --- a/av/dictionary.py +++ b/av/dictionary.py @@ -2,6 +2,7 @@ from cython.cimports.av.error import err_check +@cython.final @cython.cclass class Dictionary: def __cinit__(self, *args, **kwargs): diff --git a/av/filter/context.py b/av/filter/context.py index bb2068ae9..4873b3ff5 100644 --- a/av/filter/context.py +++ b/av/filter/context.py @@ -24,6 +24,7 @@ def wrap_filter_context( return self +@cython.final @cython.cclass class FilterContext: def __cinit__(self, sentinel): diff --git a/av/filter/filter.py b/av/filter/filter.py index b348dd3e0..bd4d21896 100644 --- a/av/filter/filter.py +++ b/av/filter/filter.py @@ -13,6 +13,7 @@ def wrap_filter(ptr: cython.pointer[cython.const[lib.AVFilter]]) -> Filter: return filter_ +@cython.final @cython.cclass class Filter: def __cinit__(self, name): diff --git a/av/filter/graph.py b/av/filter/graph.py index 507fbf015..c1b61eb20 100644 --- a/av/filter/graph.py +++ b/av/filter/graph.py @@ -12,6 +12,7 @@ from cython.cimports.av.video.frame import VideoFrame +@cython.final @cython.cclass class Graph: def __cinit__(self): diff --git a/av/filter/link.py b/av/filter/link.py index 7914ba2b1..09a078d5a 100644 --- a/av/filter/link.py +++ b/av/filter/link.py @@ -5,6 +5,7 @@ _cinit_sentinel = cython.declare(object, object()) +@cython.final @cython.cclass class FilterLink: def __cinit__(self, sentinel): @@ -78,6 +79,7 @@ def name(self): return lib.avfilter_pad_get_name(self.base_ptr, self.index) +@cython.final @cython.cclass class FilterContextPad(FilterPad): def __repr__(self): diff --git a/av/format.py b/av/format.py index aad23050d..a8222577b 100644 --- a/av/format.py +++ b/av/format.py @@ -46,6 +46,7 @@ class Flags(Flag): # fmt: on +@cython.final @cython.cclass class ContainerFormat: """Descriptor of a container format. diff --git a/av/index.py b/av/index.py index 504da9576..b652bfdc1 100644 --- a/av/index.py +++ b/av/index.py @@ -12,6 +12,7 @@ def wrap_index_entry(ptr: cython.pointer[cython.const[lib.AVIndexEntry]]) -> Ind return obj +@cython.final @cython.cclass class IndexEntry: """A single entry from a stream's index. @@ -71,6 +72,7 @@ def wrap_index_entries(ptr: cython.pointer[lib.AVStream]) -> IndexEntries: return obj +@cython.final @cython.cclass class IndexEntries: """A sequence-like view of FFmpeg's per-stream index entries. diff --git a/av/logging.py b/av/logging.py index dbd247d56..2b565b9e8 100644 --- a/av/logging.py +++ b/av/logging.py @@ -170,6 +170,7 @@ def get_last_error(): thread_captures = cython.declare(dict, {}) +@cython.final @cython.cclass class Capture: """A context manager for capturing logs. diff --git a/av/opaque.py b/av/opaque.py index bd25fd61f..3a57989e4 100644 --- a/av/opaque.py +++ b/av/opaque.py @@ -17,6 +17,7 @@ def key_free(opaque: cython.p_void, data: u8ptr) -> cython.void: opaque_container.pop(name) +@cython.final @cython.cclass class OpaqueContainer: def __cinit__(self): diff --git a/av/option.py b/av/option.py index 6a20af14f..8ed3c8fb1 100644 --- a/av/option.py +++ b/av/option.py @@ -124,6 +124,7 @@ def is_filtering_param(self): return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_FILTERING_PARAM) +@cython.final @cython.cclass class Option(BaseOption): @property @@ -186,6 +187,7 @@ def wrap_option_choice( return obj +@cython.final @cython.cclass class OptionChoice(BaseOption): """ diff --git a/av/packet.py b/av/packet.py index b25f61c93..a69cf38c1 100644 --- a/av/packet.py +++ b/av/packet.py @@ -65,6 +65,7 @@ def packet_sidedata_type_from_literal(dtype: PktSideDataT) -> lib.AVPacketSideDa return get_args(PktSideDataT).index(dtype) +@cython.final @cython.cclass class PacketSideData(Buffer): @staticmethod @@ -207,6 +208,7 @@ def _python_free( Py_DECREF(cython.cast(object, opaque)) +@cython.final @cython.cclass class Packet(Buffer): """A packet of encoded data within a :class:`~av.format.Stream`. diff --git a/av/sidedata/encparams.py b/av/sidedata/encparams.py index 399189a6b..d0e78e8a1 100644 --- a/av/sidedata/encparams.py +++ b/av/sidedata/encparams.py @@ -13,6 +13,7 @@ class VideoEncParamsType(IntEnum): MPEG2 = lib.AV_VIDEO_ENC_PARAMS_MPEG2 +@cython.final @cython.cclass class VideoEncParams(SideData): def __repr__(self): @@ -129,6 +130,7 @@ def qp_map(self): return map +@cython.final @cython.cclass class VideoBlockParams: def __init__(self, video_enc_params: VideoEncParams, idx: cython.int) -> None: diff --git a/av/sidedata/motionvectors.py b/av/sidedata/motionvectors.py index dcf0e9913..369832ba6 100644 --- a/av/sidedata/motionvectors.py +++ b/av/sidedata/motionvectors.py @@ -6,6 +6,7 @@ _cinit_bypass_sentinel = cython.declare(object, object()) +@cython.final @cython.cclass class MotionVectors(SideData): def __init__(self, sentinel, frame: Frame, index: cython.int): @@ -70,6 +71,7 @@ def to_ndarray(self): ) +@cython.final @cython.cclass class MotionVector: """ diff --git a/av/stream.py b/av/stream.py index a1a615aae..23749e699 100644 --- a/av/stream.py +++ b/av/stream.py @@ -279,6 +279,7 @@ def type(self): return "unknown" if media_type == cython.NULL else media_type +@cython.final @cython.cclass class DataStream(Stream): def __repr__(self): @@ -297,6 +298,7 @@ def name(self): return desc.name +@cython.final @cython.cclass class AttachmentStream(Stream): """ diff --git a/av/subtitles/codeccontext.py b/av/subtitles/codeccontext.py index c2a9fb4ae..acad69fb7 100644 --- a/av/subtitles/codeccontext.py +++ b/av/subtitles/codeccontext.py @@ -9,6 +9,7 @@ from cython.cimports.libc.string import memcpy +@cython.final @cython.cclass class SubtitleCodecContext(CodecContext): @property diff --git a/av/subtitles/stream.py b/av/subtitles/stream.py index 525440e9d..484eff5be 100644 --- a/av/subtitles/stream.py +++ b/av/subtitles/stream.py @@ -3,6 +3,7 @@ from cython.cimports.av.stream import Stream +@cython.final @cython.cclass class SubtitleStream(Stream): def __getattr__(self, name): diff --git a/av/subtitles/subtitle.py b/av/subtitles/subtitle.py index 91a8aa142..24bf8cbe5 100644 --- a/av/subtitles/subtitle.py +++ b/av/subtitles/subtitle.py @@ -3,6 +3,7 @@ from cython.cimports.libc.string import memcpy +@cython.final @cython.cclass class SubtitleProxy: def __dealloc__(self): @@ -12,6 +13,7 @@ def __dealloc__(self): _cinit_bypass_sentinel = cython.declare(object, object()) +@cython.final @cython.cclass class SubtitleSet: """ @@ -204,6 +206,7 @@ def __repr__(self): return f"" +@cython.final @cython.cclass class BitmapSubtitle(Subtitle): def __cinit__(self, subtitle: SubtitleSet, index: cython.int): @@ -247,6 +250,7 @@ def __getitem__(self, i): return self.planes[i] +@cython.final @cython.cclass class BitmapSubtitlePlane: def __cinit__(self, subtitle: BitmapSubtitle, index: cython.int): @@ -265,6 +269,7 @@ def __getbuffer__(self, view: cython.pointer[Py_buffer], flags: cython.int): PyBuffer_FillInfo(view, self, self._buffer, self.buffer_size, 0, flags) +@cython.final @cython.cclass class AssSubtitle(Subtitle): """ diff --git a/av/video/codeccontext.py b/av/video/codeccontext.py index c9ac25788..eb1c9a365 100644 --- a/av/video/codeccontext.py +++ b/av/video/codeccontext.py @@ -37,6 +37,7 @@ def _get_hw_format( ) +@cython.final @cython.cclass class VideoCodecContext(CodecContext): @cython.cfunc diff --git a/av/video/format.py b/av/video/format.py index 4b6291e71..ce2246836 100644 --- a/av/video/format.py +++ b/av/video/format.py @@ -27,6 +27,7 @@ def get_pix_fmt(name: cython.p_const_char) -> lib.AVPixelFormat: return pix_fmt +@cython.final @cython.cclass class VideoFormat: """ @@ -139,6 +140,7 @@ def chroma_height(self, luma_height: cython.int = 0): return -((-luma_height) >> self.ptr.log2_chroma_h) if luma_height else 0 +@cython.final @cython.cclass class VideoFormatComponent: def __cinit__(self, format: VideoFormat, index: cython.size_t): diff --git a/av/video/frame.py b/av/video/frame.py index 1f345d365..605ec9a9c 100644 --- a/av/video/frame.py +++ b/av/video/frame.py @@ -19,6 +19,7 @@ from cython.cimports.libc.stdint import int64_t, uint8_t +@cython.final @cython.cclass class CudaContext: def __cinit__(self, device_id: cython.int = 0, primary_ctx: cython.bint = True): @@ -466,6 +467,7 @@ def check_ndarray_shape(array: object, ok: cython.bint): raise ValueError(f"Unexpected numpy array shape `{array.shape}`") +@cython.final @cython.cclass class VideoFrame(Frame): def __cinit__(self, width=0, height=0, format="yuv420p"): diff --git a/av/video/plane.py b/av/video/plane.py index 99c159652..0652f108f 100644 --- a/av/video/plane.py +++ b/av/video/plane.py @@ -17,6 +17,7 @@ from cython.cimports.libc.stdlib import free, malloc +@cython.final @cython.cclass class VideoPlane(Plane): def __cinit__(self, frame: VideoFrame, index: cython.int): diff --git a/av/video/reformatter.py b/av/video/reformatter.py index 67f4bd0ad..5778711d8 100644 --- a/av/video/reformatter.py +++ b/av/video/reformatter.py @@ -141,6 +141,7 @@ def _set_frame_colorspace( frame.colorspace = lib.AVCOL_SPC_SMPTE240M +@cython.final @cython.cclass class VideoReformatter: """An object for reformatting size and pixel format of :class:`.VideoFrame`. diff --git a/av/video/stream.py b/av/video/stream.py index d5b2c106d..c37f45a3e 100644 --- a/av/video/stream.py +++ b/av/video/stream.py @@ -5,6 +5,7 @@ from cython.cimports.av.video.frame import VideoFrame +@cython.final @cython.cclass class VideoStream(Stream): def __repr__(self): From 08d9f504d4dc71d3658becfc1fcb5a2839c3cbe8 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 8 May 2026 19:39:16 -0400 Subject: [PATCH 757/846] Remove undertested option/descriptor API --- av/codec/__init__.py | 2 - av/codec/codec.py | 6 -- av/codec/codec.pyi | 6 -- av/descriptor.pxd | 20 ----- av/descriptor.py | 76 ---------------- av/descriptor.pyi | 5 -- av/filter/__init__.py | 2 +- av/filter/filter.pxd | 3 - av/filter/filter.py | 14 --- av/filter/filter.pyi | 5 -- av/format.py | 13 --- av/option.pxd | 14 --- av/option.py | 203 ------------------------------------------ av/option.pyi | 55 ------------ tests/test_options.py | 17 ---- 15 files changed, 1 insertion(+), 440 deletions(-) delete mode 100644 av/descriptor.pxd delete mode 100644 av/descriptor.py delete mode 100644 av/descriptor.pyi delete mode 100644 av/option.pxd delete mode 100644 av/option.py delete mode 100644 av/option.pyi delete mode 100644 tests/test_options.py diff --git a/av/codec/__init__.py b/av/codec/__init__.py index 178ed5106..f8d4d75ae 100644 --- a/av/codec/__init__.py +++ b/av/codec/__init__.py @@ -2,7 +2,6 @@ Capabilities, Codec, Properties, - codec_descriptor, codecs_available, find_best_pix_fmt_of_list, ) @@ -12,7 +11,6 @@ "Capabilities", "Codec", "Properties", - "codec_descriptor", "codecs_available", "find_best_pix_fmt_of_list", "CodecContext", diff --git a/av/codec/codec.py b/av/codec/codec.py index 681e8cd1d..aff456ecb 100644 --- a/av/codec/codec.py +++ b/av/codec/codec.py @@ -4,7 +4,6 @@ from cython.cimports import libav as lib from cython.cimports.av.audio.format import get_audio_format from cython.cimports.av.codec.hwaccel import wrap_hwconfig -from cython.cimports.av.descriptor import wrap_avclass from cython.cimports.av.utils import avrational_to_fraction from cython.cimports.av.video.format import VideoFormat, get_pix_fmt, get_video_format from cython.cimports.libc.stdlib import free, malloc @@ -146,10 +145,6 @@ def mode(self): def is_decoder(self): return not self.is_encoder - @property - def descriptor(self): - return wrap_avclass(self.ptr.priv_class) - @property def name(self): return self.ptr.name or "" @@ -355,7 +350,6 @@ def get_codec_names(): codecs_available = get_codec_names() -codec_descriptor = wrap_avclass(lib.avcodec_get_class()) def dump_codecs(): diff --git a/av/codec/codec.pyi b/av/codec/codec.pyi index 221872c33..1459bc777 100644 --- a/av/codec/codec.pyi +++ b/av/codec/codec.pyi @@ -4,7 +4,6 @@ from typing import ClassVar, Literal, Sequence, cast, overload from av.audio.codeccontext import AudioCodecContext from av.audio.format import AudioFormat -from av.descriptor import Descriptor from av.subtitles.codeccontext import SubtitleCodecContext from av.video.codeccontext import VideoCodecContext from av.video.format import VideoFormat @@ -53,7 +52,6 @@ class Codec: def is_decoder(self) -> bool: ... @property def mode(self) -> Literal["r", "w"]: ... - descriptor: Descriptor @property def name(self) -> str: ... @property @@ -107,10 +105,6 @@ class Codec: VideoCodecContext | AudioCodecContext | SubtitleCodecContext | CodecContext ): ... -class codec_descriptor: - name: str - options: tuple[int, ...] - codecs_available: set[str] def dump_codecs() -> None: ... diff --git a/av/descriptor.pxd b/av/descriptor.pxd deleted file mode 100644 index 404f646af..000000000 --- a/av/descriptor.pxd +++ /dev/null @@ -1,20 +0,0 @@ -cimport libav as lib - - -cdef class Descriptor: - - # These are present as: - # - AVCodecContext.av_class (same as avcodec_get_class()) - # - AVFormatContext.av_class (same as avformat_get_class()) - # - AVFilterContext.av_class (same as avfilter_get_class()) - # - AVCodec.priv_class - # - AVOutputFormat.priv_class - # - AVInputFormat.priv_class - # - AVFilter.priv_class - - cdef const lib.AVClass *ptr - - cdef object _options # Option list cache. - - -cdef Descriptor wrap_avclass(const lib.AVClass*) diff --git a/av/descriptor.py b/av/descriptor.py deleted file mode 100644 index faaa0f57c..000000000 --- a/av/descriptor.py +++ /dev/null @@ -1,76 +0,0 @@ -import cython -import cython.cimports.libav as lib -from cython.cimports.av.option import ( - Option, - OptionChoice, - wrap_option, - wrap_option_choice, -) - -_cinit_sentinel = cython.declare(object, object()) - - -@cython.cfunc -def wrap_avclass(ptr: cython.pointer[cython.const[lib.AVClass]]) -> Descriptor | None: - if ptr == cython.NULL: - return None - obj: Descriptor = Descriptor(_cinit_sentinel) - obj.ptr = ptr - return obj - - -@cython.final -@cython.cclass -class Descriptor: - def __cinit__(self, sentinel): - if sentinel is not _cinit_sentinel: - raise RuntimeError("Cannot construct av.Descriptor") - - @property - def name(self): - return self.ptr.class_name if self.ptr.class_name else None - - @property - def options(self): - ptr: cython.pointer[cython.const[lib.AVOption]] = self.ptr.option - choice_ptr: cython.pointer[cython.const[lib.AVOption]] - option: Option - option_choice: OptionChoice - choice_is_default: cython.bint - if self._options is None: - options: list = [] - ptr = self.ptr.option - while ptr != cython.NULL and ptr.name != cython.NULL: - if ptr.type == lib.AV_OPT_TYPE_CONST: - ptr += 1 - continue - choices: list = [] - if ( - ptr.unit != cython.NULL - ): # option has choices (matching const options) - choice_ptr = self.ptr.option - while choice_ptr != cython.NULL and choice_ptr.name != cython.NULL: - if ( - choice_ptr.type != lib.AV_OPT_TYPE_CONST - or choice_ptr.unit != ptr.unit - ): - choice_ptr += 1 - continue - choice_is_default = ( - choice_ptr.default_val.i64 == ptr.default_val.i64 - or ptr.type == lib.AV_OPT_TYPE_FLAGS - and choice_ptr.default_val.i64 & ptr.default_val.i64 - ) - option_choice = wrap_option_choice( - choice_ptr, choice_is_default - ) - choices.append(option_choice) - choice_ptr += 1 - option = wrap_option(tuple(choices), ptr) - options.append(option) - ptr += 1 - self._options = tuple(options) - return self._options - - def __repr__(self): - return f"<{self.__class__.__name__} {self.name} at 0x{id(self):x}>" diff --git a/av/descriptor.pyi b/av/descriptor.pyi deleted file mode 100644 index 4aa2144a3..000000000 --- a/av/descriptor.pyi +++ /dev/null @@ -1,5 +0,0 @@ -from .option import Option - -class Descriptor: - name: str - options: tuple[Option, ...] diff --git a/av/filter/__init__.py b/av/filter/__init__.py index 14c8563ce..cffc49e7d 100644 --- a/av/filter/__init__.py +++ b/av/filter/__init__.py @@ -1,3 +1,3 @@ -from .filter import Filter, filter_descriptor, filters_available +from .filter import Filter, filters_available from .graph import Graph as Graph from .loudnorm import stats as stats diff --git a/av/filter/filter.pxd b/av/filter/filter.pxd index 0a7cd41a0..d74065a19 100644 --- a/av/filter/filter.pxd +++ b/av/filter/filter.pxd @@ -1,13 +1,10 @@ cimport libav as lib -from av.descriptor cimport Descriptor - cdef class Filter: cdef const lib.AVFilter *ptr cdef object _inputs cdef object _outputs - cdef Descriptor _descriptor cdef Filter wrap_filter(const lib.AVFilter *ptr) diff --git a/av/filter/filter.py b/av/filter/filter.py index bd4d21896..acbbc111a 100644 --- a/av/filter/filter.py +++ b/av/filter/filter.py @@ -1,6 +1,5 @@ import cython from cython.cimports import libav as lib -from cython.cimports.av.descriptor import wrap_avclass from cython.cimports.av.filter.link import alloc_filter_pads _cinit_sentinel = cython.declare(object, object()) @@ -26,18 +25,6 @@ def __cinit__(self, name): if not self.ptr: raise ValueError(f"no filter {name}") - @property - def descriptor(self): - if self._descriptor is None: - self._descriptor = wrap_avclass(self.ptr.priv_class) - return self._descriptor - - @property - def options(self): - if self.descriptor is None: - return - return self.descriptor.options - @property def name(self): return self.ptr.name @@ -78,4 +65,3 @@ def get_filter_names() -> set: filters_available = get_filter_names() -filter_descriptor = wrap_avclass(lib.avfilter_get_class()) diff --git a/av/filter/filter.pyi b/av/filter/filter.pyi index f07f317fc..090d4f1d0 100644 --- a/av/filter/filter.pyi +++ b/av/filter/filter.pyi @@ -1,11 +1,6 @@ -from av.descriptor import Descriptor -from av.option import Option - class Filter: name: str description: str - descriptor: Descriptor - options: tuple[Option, ...] | None flags: int def __init__(self, name: str) -> None: ... diff --git a/av/format.py b/av/format.py index a8222577b..fec69f7a0 100644 --- a/av/format.py +++ b/av/format.py @@ -2,7 +2,6 @@ import cython import cython.cimports.libav as lib -from cython.cimports.av.descriptor import wrap_avclass _cinit_bypass_sentinel = cython.declare(object, object()) @@ -78,17 +77,6 @@ def __cinit__(self, name, mode=None): def __repr__(self): return f"" - @property - def descriptor(self): - if self.iptr: - return wrap_avclass(self.iptr.priv_class) - else: - return wrap_avclass(self.optr.priv_class) - - @property - def options(self): - return self.descriptor.options - @property def input(self): """An input-only view of this format.""" @@ -178,4 +166,3 @@ def get_input_format_names() -> set: formats_available = get_output_format_names() formats_available.update(get_input_format_names()) -format_descriptor = wrap_avclass(lib.avformat_get_class()) diff --git a/av/option.pxd b/av/option.pxd deleted file mode 100644 index 4c8eaffbf..000000000 --- a/av/option.pxd +++ /dev/null @@ -1,14 +0,0 @@ -cimport libav as lib - - -cdef class BaseOption: - cdef const lib.AVOption *ptr - -cdef class Option(BaseOption): - cdef readonly tuple choices - -cdef class OptionChoice(BaseOption): - cdef readonly bint is_default - -cdef Option wrap_option(tuple choices, const lib.AVOption *ptr) -cdef OptionChoice wrap_option_choice(const lib.AVOption *ptr, bint is_default) diff --git a/av/option.py b/av/option.py deleted file mode 100644 index 8ed3c8fb1..000000000 --- a/av/option.py +++ /dev/null @@ -1,203 +0,0 @@ -from enum import Enum, Flag - -import cython -import cython.cimports.libav as lib -from cython import NULL, bint -from cython.cimports.libc.stdint import uint64_t - -_cinit_sentinel = cython.declare(object, object()) - - -@cython.cfunc -def wrap_option( - choices: tuple, ptr: cython.pointer[cython.const[lib.AVOption]] -) -> Option: - if ptr == NULL: - return None - obj: Option = Option(_cinit_sentinel) - obj.ptr = ptr - obj.choices = choices - return obj - - -@cython.cfunc -def flag_in_bitfield(bitfield: uint64_t, flag: uint64_t) -> bool: - return bool(bitfield & flag) - - -class OptionType(Enum): - FLAGS = lib.AV_OPT_TYPE_FLAGS - INT = lib.AV_OPT_TYPE_INT - INT64 = lib.AV_OPT_TYPE_INT64 - DOUBLE = lib.AV_OPT_TYPE_DOUBLE - FLOAT = lib.AV_OPT_TYPE_FLOAT - STRING = lib.AV_OPT_TYPE_STRING - RATIONAL = lib.AV_OPT_TYPE_RATIONAL - BINARY = lib.AV_OPT_TYPE_BINARY - DICT = lib.AV_OPT_TYPE_DICT - UINT64 = lib.AV_OPT_TYPE_UINT64 - CONST = lib.AV_OPT_TYPE_CONST - IMAGE_SIZE = lib.AV_OPT_TYPE_IMAGE_SIZE - PIXEL_FMT = lib.AV_OPT_TYPE_PIXEL_FMT - SAMPLE_FMT = lib.AV_OPT_TYPE_SAMPLE_FMT - VIDEO_RATE = lib.AV_OPT_TYPE_VIDEO_RATE - DURATION = lib.AV_OPT_TYPE_DURATION - COLOR = lib.AV_OPT_TYPE_COLOR - CHANNEL_LAYOUT = lib.AV_OPT_TYPE_CHLAYOUT - BOOL = lib.AV_OPT_TYPE_BOOL - - -class OptionFlags(Flag): - ENCODING_PARAM = lib.AV_OPT_FLAG_ENCODING_PARAM - DECODING_PARAM = lib.AV_OPT_FLAG_DECODING_PARAM - AUDIO_PARAM = lib.AV_OPT_FLAG_AUDIO_PARAM - VIDEO_PARAM = lib.AV_OPT_FLAG_VIDEO_PARAM - SUBTITLE_PARAM = lib.AV_OPT_FLAG_SUBTITLE_PARAM - EXPORT = lib.AV_OPT_FLAG_EXPORT - READONLY = lib.AV_OPT_FLAG_READONLY - FILTERING_PARAM = lib.AV_OPT_FLAG_FILTERING_PARAM - - -_INT_TYPES = cython.declare( - tuple, - ( - lib.AV_OPT_TYPE_FLAGS, - lib.AV_OPT_TYPE_INT, - lib.AV_OPT_TYPE_INT64, - lib.AV_OPT_TYPE_PIXEL_FMT, - lib.AV_OPT_TYPE_SAMPLE_FMT, - lib.AV_OPT_TYPE_DURATION, - lib.AV_OPT_TYPE_CHLAYOUT, - lib.AV_OPT_TYPE_BOOL, - ), -) - - -@cython.cclass -class BaseOption: - def __cinit__(self, sentinel): - if sentinel is not _cinit_sentinel: - raise RuntimeError(f"Cannot construct av.{self.__class__.__name__}") - - @property - def name(self): - return self.ptr.name - - @property - def help(self): - return self.ptr.help if self.ptr.help != NULL else "" - - @property - def flags(self): - return self.ptr.flags - - @property - def is_encoding_param(self): - return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_ENCODING_PARAM) - - @property - def is_decoding_param(self): - return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_DECODING_PARAM) - - @property - def is_audio_param(self): - return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_AUDIO_PARAM) - - @property - def is_video_param(self): - return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_VIDEO_PARAM) - - @property - def is_subtitle_param(self): - return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_SUBTITLE_PARAM) - - @property - def is_export(self): - return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_EXPORT) - - @property - def is_readonly(self): - return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_READONLY) - - @property - def is_filtering_param(self): - return flag_in_bitfield(self.ptr.flags, lib.AV_OPT_FLAG_FILTERING_PARAM) - - -@cython.final -@cython.cclass -class Option(BaseOption): - @property - def type(self): - return OptionType(self.ptr.type) - - @property - def offset(self): - """ - This can be used to find aliases of an option. - Options in a particular descriptor with the same offset are aliases. - """ - return self.ptr.offset - - @property - def default(self): - if self.ptr.type in _INT_TYPES: - return self.ptr.default_val.i64 - if self.ptr.type in ( - lib.AV_OPT_TYPE_DOUBLE, - lib.AV_OPT_TYPE_FLOAT, - lib.AV_OPT_TYPE_RATIONAL, - ): - return self.ptr.default_val.dbl - if self.ptr.type in ( - lib.AV_OPT_TYPE_STRING, - lib.AV_OPT_TYPE_BINARY, - lib.AV_OPT_TYPE_IMAGE_SIZE, - lib.AV_OPT_TYPE_VIDEO_RATE, - lib.AV_OPT_TYPE_COLOR, - ): - return self.ptr.default_val.str if self.ptr.default_val.str != NULL else "" - - @property - def min(self): - if self.ptr.type in _INT_TYPES: - return int(self.ptr.min) - return self.ptr.min - - @property - def max(self): - if self.ptr.type in _INT_TYPES: - return int(self.ptr.max) - return self.ptr.max - - def __repr__(self): - return f"" - - -@cython.cfunc -def wrap_option_choice( - ptr: cython.pointer[cython.const[lib.AVOption]], is_default: bint -) -> OptionChoice | None: - if ptr == NULL: - return None - - obj: OptionChoice = OptionChoice(_cinit_sentinel) - obj.ptr = ptr - obj.is_default = is_default - return obj - - -@cython.final -@cython.cclass -class OptionChoice(BaseOption): - """ - Represents AV_OPT_TYPE_CONST options which are essentially - choices of non-const option with same unit. - """ - - @property - def value(self): - return self.ptr.default_val.i64 - - def __repr__(self): - return f"" diff --git a/av/option.pyi b/av/option.pyi deleted file mode 100644 index f989a1138..000000000 --- a/av/option.pyi +++ /dev/null @@ -1,55 +0,0 @@ -from enum import Enum, Flag -from typing import cast - -class OptionType(Enum): - FLAGS = cast(int, ...) - INT = cast(int, ...) - INT64 = cast(int, ...) - DOUBLE = cast(int, ...) - FLOAT = cast(int, ...) - STRING = cast(int, ...) - RATIONAL = cast(int, ...) - BINARY = cast(int, ...) - DICT = cast(int, ...) - CONST = cast(int, ...) - IMAGE_SIZE = cast(int, ...) - PIXEL_FMT = cast(int, ...) - SAMPLE_FMT = cast(int, ...) - VIDEO_RATE = cast(int, ...) - DURATION = cast(int, ...) - COLOR = cast(int, ...) - CHANNEL_LAYOUT = cast(int, ...) - BOOL = cast(int, ...) - -class OptionFlags(Flag): - ENCODING_PARAM = cast(int, ...) - DECODING_PARAM = cast(int, ...) - AUDIO_PARAM = cast(int, ...) - VIDEO_PARAM = cast(int, ...) - SUBTITLE_PARAM = cast(int, ...) - EXPORT = cast(int, ...) - READONLY = cast(int, ...) - FILTERING_PARAM = cast(int, ...) - -class BaseOption: - name: str - help: str - flags: int - is_encoding_param: bool - is_decoding_param: bool - is_audio_param: bool - is_video_param: bool - is_subtitle_param: bool - is_export: bool - is_readonly: bool - is_filtering_param: bool - -class Option(BaseOption): - type: OptionType - offset: int - default: int - min: int - max: int - -class OptionChoice(BaseOption): - value: int diff --git a/tests/test_options.py b/tests/test_options.py deleted file mode 100644 index f5593a77d..000000000 --- a/tests/test_options.py +++ /dev/null @@ -1,17 +0,0 @@ -from av import ContainerFormat -from av.option import Option, OptionType - - -class TestOptions: - def test_mov_options(self) -> None: - mov = ContainerFormat("mov") - options = mov.descriptor.options # type: ignore - by_name = {opt.name: opt for opt in options} - - opt = by_name.get("use_absolute_path") - - assert isinstance(opt, Option) - assert opt.name == "use_absolute_path" - - # This was not a good option to actually test. - assert opt.type in (OptionType.BOOL, OptionType.INT) From 59a644bd42c298680061adddc0e98ecc797a0773 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 8 May 2026 21:04:38 -0400 Subject: [PATCH 758/846] Write changelog --- CHANGELOG.rst | 6 ++++-- av/about.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d89ead006..83dee5c8f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -25,14 +25,16 @@ We are operating with `semantic versioning `_. - Bug fixes (PATCH) go here. - $CHANGE by :gh-user:`mikeboers` in (:pr:`1`). -v17.0.2 (next) +v17.1.0 (next) -------------- Features: +- Use FFmpeg 8.1.1 in the binary wheels. - Expose ``AVCodecContext.global_quality`` by :gh-user:`WyattBlue` in (:pr:`2246`). Fixes: - +- Add ``cython.final`` to leaf classes, ensuring that they are not subclassed. +- Warn that ``CodecContext.decode()`` is not memory safe in some cases. v17.0.1 ------- diff --git a/av/about.py b/av/about.py index 4fe5d9474..e16231267 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "17.0.2pre" +__version__ = "17.1.0pre" From 2474f35ccce24727dfc76c7b92348ea7447595a6 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 31 May 2026 02:26:03 -0400 Subject: [PATCH 759/846] Fix #2264 --- CHANGELOG.rst | 1 + av/device.py | 8 +++----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 83dee5c8f..ff213ae87 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -35,6 +35,7 @@ Features: Fixes: - Add ``cython.final`` to leaf classes, ensuring that they are not subclassed. - Warn that ``CodecContext.decode()`` is not memory safe in some cases. +- Fix ``enumerate_input_devices`` and ``enumerate_output_devices`` raising ``AttributeError`` (:issue:`2264`). v17.0.1 ------- diff --git a/av/device.py b/av/device.py index 7be7e68a6..9868f1565 100644 --- a/av/device.py +++ b/av/device.py @@ -55,14 +55,12 @@ def _build_device_list(device_list: cython.pointer[lib.AVDeviceInfoList]) -> lis mt = device_info.media_types[j] s = lib.av_get_media_type_string(mt) if s: - media_types.append(s.decode()) + media_types.append(s) devices.append( DeviceInfo( - name=device_info.device_name.decode() - if device_info.device_name - else "", - description=device_info.device_description.decode() + name=device_info.device_name if device_info.device_name else "", + description=device_info.device_description if device_info.device_description else "", is_default=(i == device_list.default_device), From 76ed4be4382875dd9129cbd84e2148e7f607ebbf Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 31 May 2026 03:11:17 -0400 Subject: [PATCH 760/846] Add AGENTS.md --- AGENTS.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..fd712fed0 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,23 @@ +# Dev Workflow + +Run `source ./scripts/activate.sh` if not in virtualenv + +# 1. Make changes + +# 2. Build + +make + +# 3. Test + +make test # All tests +python -m pytest tests/some_file.py # Individual file +python -m pytest -k "substring" + +# 4. Lint before commiting + +make lint + +# Gotchas + +- Uses Cython so look out for UB From e558775fd6f7351f5b109c1df41b0cd23883b6d0 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 31 May 2026 11:31:05 -0400 Subject: [PATCH 761/846] Add HTTPTooManyRequestsError, closes #2267 --- CHANGELOG.rst | 1 + av/error.py | 1 + av/error.pyi | 1 + include/avutil.pxd | 1 + tests/test_errors.py | 13 +++++++++++++ 5 files changed, 17 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ff213ae87..76e7b2127 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -36,6 +36,7 @@ Fixes: - Add ``cython.final`` to leaf classes, ensuring that they are not subclassed. - Warn that ``CodecContext.decode()`` is not memory safe in some cases. - Fix ``enumerate_input_devices`` and ``enumerate_output_devices`` raising ``AttributeError`` (:issue:`2264`). +- Map HTTP 429 to ``HTTPTooManyRequestsError`` instead of ``UndefinedError`` (:issue:`2267`). v17.0.1 ------- diff --git a/av/error.py b/av/error.py index 669c8e55a..e6cca095e 100644 --- a/av/error.py +++ b/av/error.py @@ -161,6 +161,7 @@ class HTTPClientError(FFmpegError): ("HTTP_UNAUTHORIZED", -lib.AVERROR_HTTP_UNAUTHORIZED, "HTTPUnauthorizedError", HTTPClientError), ("HTTP_FORBIDDEN", -lib.AVERROR_HTTP_FORBIDDEN, "HTTPForbiddenError", HTTPClientError), ("HTTP_NOT_FOUND", -lib.AVERROR_HTTP_NOT_FOUND, "HTTPNotFoundError", HTTPClientError), + ("HTTP_TOO_MANY_REQUESTS", -lib.AVERROR_HTTP_TOO_MANY_REQUESTS, "HTTPTooManyRequestsError", HTTPClientError), ("HTTP_OTHER_4XX", -lib.AVERROR_HTTP_OTHER_4XX, "HTTPOtherClientError", HTTPClientError), ("HTTP_SERVER_ERROR", -lib.AVERROR_HTTP_SERVER_ERROR, "HTTPServerError", HTTPError), ("PYAV_CALLBACK", c_PYAV_STASHED_ERROR, "PyAVCallbackError", RuntimeError), diff --git a/av/error.pyi b/av/error.pyi index c708a80f6..1191d962f 100644 --- a/av/error.pyi +++ b/av/error.pyi @@ -46,6 +46,7 @@ class HTTPBadRequestError(HTTPClientError): ... class HTTPUnauthorizedError(HTTPClientError): ... class HTTPForbiddenError(HTTPClientError): ... class HTTPNotFoundError(HTTPClientError): ... +class HTTPTooManyRequestsError(HTTPClientError): ... class HTTPOtherClientError(HTTPClientError): ... class HTTPServerError(HTTPError): ... class PyAVCallbackError(FFmpegError, builtins.RuntimeError): ... diff --git a/include/avutil.pxd b/include/avutil.pxd index d7a1fb765..ede8f6fbe 100644 --- a/include/avutil.pxd +++ b/include/avutil.pxd @@ -172,6 +172,7 @@ cdef extern from "libavutil/error.h" nogil: cdef int AVERROR_HTTP_UNAUTHORIZED cdef int AVERROR_HTTP_FORBIDDEN cdef int AVERROR_HTTP_NOT_FOUND + cdef int AVERROR_HTTP_TOO_MANY_REQUESTS cdef int AVERROR_HTTP_OTHER_4XX cdef int AVERROR_HTTP_SERVER_ERROR cdef int AV_ERROR_MAX_STRING_SIZE diff --git a/tests/test_errors.py b/tests/test_errors.py index fdfe0f49d..f4dbf8fbc 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -63,3 +63,16 @@ def test_buffertoosmall() -> None: assert e.errno == BUFFER_TOO_SMALL else: assert False, "No exception raised!" + + +def test_http_too_many_requests() -> None: + """HTTP 429 maps to HTTPTooManyRequestsError, not UndefinedError.""" + + HTTP_TOO_MANY_REQUESTS = av.error.tag_to_code(b"\xf8429") + try: + av.error.err_check(-HTTP_TOO_MANY_REQUESTS) + except av.error.HTTPTooManyRequestsError as e: + assert e.errno == HTTP_TOO_MANY_REQUESTS + assert isinstance(e, av.error.HTTPClientError) + else: + assert False, "No exception raised!" From 779e496a1e2ccc7ba2d4a695a4a5237e1c368c5a Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 31 May 2026 18:10:25 -0400 Subject: [PATCH 762/846] Build linux armv7 --- .github/workflows/tests.yml | 8 ++++++++ av/subtitles/codeccontext.pxd | 1 + scripts/ffmpeg-8.1.json | 2 +- scripts/ffmpeg-latest.json | 2 +- setup.py | 8 ++++++++ 5 files changed, 19 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9cbdb6715..7098f7cf3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -34,6 +34,8 @@ jobs: arch: x86_64 - os: ubuntu-24.04-arm arch: aarch64 + - os: ubuntu-24.04 + arch: armv7l - os: ubuntu-24.04 arch: x86_64 - os: windows-latest @@ -45,6 +47,11 @@ jobs: - uses: actions/setup-python@v6 with: python-version: "3.14" + - name: Set up QEMU + if: matrix.arch == 'armv7l' + uses: docker/setup-qemu-action@v3 + with: + platforms: arm - name: Set Minimum MacOS Target if: runner.os == 'macOS' run: | @@ -67,6 +74,7 @@ jobs: CIBW_SKIP: "cp310-win_arm64" CIBW_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m pytest {package}/tests && mv {project}/av.disabled {project}/av CIBW_TEST_REQUIRES: pytest numpy + CIBW_TEST_SKIP: "*_armv7l" run: | pip install cibuildwheel delvewheel cibuildwheel --output-dir dist diff --git a/av/subtitles/codeccontext.pxd b/av/subtitles/codeccontext.pxd index 01c8fbf1f..0be45ed10 100644 --- a/av/subtitles/codeccontext.pxd +++ b/av/subtitles/codeccontext.pxd @@ -4,4 +4,5 @@ from av.packet cimport Packet cdef class SubtitleCodecContext(CodecContext): cdef bint subtitle_header_set + cpdef decode(self, Packet packet=?) cpdef decode2(self, Packet packet) diff --git a/scripts/ffmpeg-8.1.json b/scripts/ffmpeg-8.1.json index 76ab6c2e4..715c177bf 100644 --- a/scripts/ffmpeg-8.1.json +++ b/scripts/ffmpeg-8.1.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.1.1-1/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.1.1-2/ffmpeg-{platform}.tar.gz" } diff --git a/scripts/ffmpeg-latest.json b/scripts/ffmpeg-latest.json index 76ab6c2e4..715c177bf 100644 --- a/scripts/ffmpeg-latest.json +++ b/scripts/ffmpeg-latest.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.1.1-1/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.1.1-2/ffmpeg-{platform}.tar.gz" } diff --git a/setup.py b/setup.py index 0e00f9873..3a307c4c6 100644 --- a/setup.py +++ b/setup.py @@ -141,6 +141,12 @@ def parse_cflags(raw_flags): IMPORT_NAME = "av" +# Newer compilers treat incompatible pointer types as an error by default; +# downgrade it back to a warning so FFmpeg API churn doesn't break the build. +extra_compile_args = [] +if platform.system() != "Windows": + extra_compile_args.append("-Wno-error=incompatible-pointer-types") + loudnorm_extension = Extension( f"{IMPORT_NAME}.filter.loudnorm", sources=[ @@ -151,6 +157,7 @@ def parse_cflags(raw_flags): libraries=extension_extra["libraries"], library_dirs=extension_extra["library_dirs"], define_macros=define_macros, + extra_compile_args=extra_compile_args, py_limited_api=py_limited_api, ) @@ -197,6 +204,7 @@ def parse_cflags(raw_flags): library_dirs=extension_extra["library_dirs"], sources=[pyx_path], define_macros=define_macros, + extra_compile_args=extra_compile_args, py_limited_api=py_limited_api, ), compiler_directives=compiler_directives, From 6d968c915a917f2b01ce49b6a060b929e11a2b1c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 31 May 2026 20:45:41 -0400 Subject: [PATCH 763/846] Fix windows tarballs time metadata --- scripts/fetch-vendor.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/scripts/fetch-vendor.py b/scripts/fetch-vendor.py index b59ec3b7d..7161a0023 100644 --- a/scripts/fetch-vendor.py +++ b/scripts/fetch-vendor.py @@ -4,6 +4,7 @@ import os import platform import subprocess +import time def get_platform(): @@ -51,3 +52,13 @@ def get_platform(): logging.info(f"Extracting {tarball_name}") subprocess.check_call(["tar", "-C", args.destination_dir, "-xf", tarball_file]) + +# Some tarball members carry pre-1980 mtimes, which the ZIP format (and thus +# delvewheel's wheel repackaging) cannot represent. Bump any such file to now. +ZIP_EPOCH = 315532800 # 1980-01-01 00:00:00 UTC +now = time.time() +for root, _, files in os.walk(args.destination_dir): + for name in files: + path = os.path.join(root, name) + if os.path.getmtime(path) < ZIP_EPOCH: + os.utime(path, (now, now)) From d49f573dd5bf5c30ea90bb0089fd28372173c7c9 Mon Sep 17 00:00:00 2001 From: Joshua Date: Mon, 1 Jun 2026 13:39:43 -0700 Subject: [PATCH 764/846] Fix subtitle UTF-8 handling --- av/subtitles/subtitle.py | 2 +- tests/test_subtitles.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/av/subtitles/subtitle.py b/av/subtitles/subtitle.py index 24bf8cbe5..5c58647a9 100644 --- a/av/subtitles/subtitle.py +++ b/av/subtitles/subtitle.py @@ -297,7 +297,7 @@ def dialogue(self): i: cython.Py_ssize_t = 0 state: cython.bint = False ass_text: bytes = self.ass - char, next_char = cython.declare(cython.char) + char, next_char = cython.declare(cython.uchar) result: bytearray = bytearray() text_len: cython.Py_ssize_t = len(ass_text) diff --git a/tests/test_subtitles.py b/tests/test_subtitles.py index 321b29d1e..dd13d7756 100644 --- a/tests/test_subtitles.py +++ b/tests/test_subtitles.py @@ -119,6 +119,16 @@ def test_subtitle_set_create(self) -> None: assert len(subtitle) == 1 assert cast(AssSubtitle, subtitle[0]).ass == text + def test_subtitle_dialogue_extended_chars(self) -> None: + """Test handling of extended UTF-8 characters in subtitle dialogue.""" + from av.subtitles.subtitle import SubtitleSet + + text = "0,0,Default,,0,0,0,,♪ Hey, hey, hey ♪".encode("utf-8") + subtitle = SubtitleSet.create(text=text, start=0, end=2000, pts=0) + sub = cast(AssSubtitle, subtitle[0]) + + assert sub.dialogue == "♪ Hey, hey, hey ♪".encode("utf-8") + def test_subtitle_encode_mp4(self) -> None: """Test encoding subtitles to MP4 container.""" from av.subtitles.subtitle import SubtitleSet From dfc137328f0c749ac0b8adecf075c77f919f56b4 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 1 Jun 2026 23:50:04 -0400 Subject: [PATCH 765/846] Expose Stream.discard, closes #2272 --- CHANGELOG.rst | 1 + av/stream.py | 28 +++++++++++++++++++++++++++- av/stream.pyi | 12 +++++++++++- docs/api/stream.rst | 2 ++ include/avformat.pxd | 1 + tests/test_streams.py | 39 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 81 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 76e7b2127..15a7fff89 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -31,6 +31,7 @@ v17.1.0 (next) Features: - Use FFmpeg 8.1.1 in the binary wheels. - Expose ``AVCodecContext.global_quality`` by :gh-user:`WyattBlue` in (:pr:`2246`). +- Expose ``Stream.discard`` so demuxing and seeking can skip unwanted streams (:issue:`2272`). Fixes: - Add ``cython.final`` to leaf classes, ensuring that they are not subclassed. diff --git a/av/stream.py b/av/stream.py index 23749e699..f96030e26 100644 --- a/av/stream.py +++ b/av/stream.py @@ -1,4 +1,4 @@ -from enum import IntFlag +from enum import IntEnum, IntFlag import cython from cython.cimports import libav as lib @@ -34,6 +34,16 @@ class Disposition(IntFlag): multilayer = 1 << 21 +class Discard(IntEnum): + none = lib.AVDISCARD_NONE + default = lib.AVDISCARD_DEFAULT + nonref = lib.AVDISCARD_NONREF + bidir = lib.AVDISCARD_BIDIR + nonintra = lib.AVDISCARD_NONINTRA + nonkey = lib.AVDISCARD_NONKEY + all = lib.AVDISCARD_ALL + + _cinit_bypass_sentinel = cython.declare(object, object()) @@ -132,6 +142,9 @@ def __setattr__(self, name, value): if name == "disposition": self.ptr.disposition = value return + if name == "discard": + self.ptr.discard = Discard(value).value + return if name == "time_base": to_avrational(value, cython.address(self.ptr.time_base)) return @@ -268,6 +281,19 @@ def language(self): def disposition(self): return Disposition(self.ptr.disposition) + @property + def discard(self): + """ + Controls which packets of this stream are discarded by the demuxer. + + Set this to e.g. :attr:`Discard.all` on streams you don't need so that + :meth:`.Container.demux` and :meth:`.Container.seek` skip them, avoiding + the cost of synchronizing streams you never read. + + :type: Discard + """ + return Discard(self.ptr.discard) + @property def type(self): """ diff --git a/av/stream.pyi b/av/stream.pyi index 680166fd6..f9148021f 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -1,4 +1,4 @@ -from enum import IntFlag +from enum import IntEnum, IntFlag from fractions import Fraction from typing import Literal, cast @@ -27,6 +27,15 @@ class Disposition(IntFlag): still_image = cast(int, ...) multilayer = cast(int, ...) +class Discard(IntEnum): + none = cast(int, ...) + default = cast(int, ...) + nonref = cast(int, ...) + bidir = cast(int, ...) + nonintra = cast(int, ...) + nonkey = cast(int, ...) + all = cast(int, ...) + class Stream: name: str | None container: Container @@ -46,6 +55,7 @@ class Stream: start_time: int | None duration: int | None disposition: Disposition + discard: Discard frames: int language: str | None type: Literal["video", "audio", "data", "subtitle", "attachment", "unknown"] diff --git a/docs/api/stream.rst b/docs/api/stream.rst index 99a30b136..98d8fc43d 100644 --- a/docs/api/stream.rst +++ b/docs/api/stream.rst @@ -92,5 +92,7 @@ Others .. autoattribute:: Stream.language +.. autoattribute:: Stream.discard + diff --git a/include/avformat.pxd b/include/avformat.pxd index 5aca5e287..1cd07d814 100644 --- a/include/avformat.pxd +++ b/include/avformat.pxd @@ -25,6 +25,7 @@ cdef extern from "libavformat/avformat.h" nogil: int index int id int disposition + AVDiscard discard AVCodecParameters *codecpar AVRational time_base int64_t start_time diff --git a/tests/test_streams.py b/tests/test_streams.py index a9f5e5bdc..1afc67222 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -136,6 +136,45 @@ def test_selection(self) -> None: data = container.streams.data[0] assert data == container.streams.best("data") + def test_discard(self) -> None: + from av.stream import Discard + + container = av.open( + fate_suite("amv/MTV_high_res_320x240_sample_Penguin_Joke_MTV_from_WMV.amv") + ) + audio = container.streams.audio[0] + + # Default discard policy. + assert audio.discard == Discard.default + + # Setter accepts the enum and round-trips. + audio.discard = Discard.all + assert audio.discard == Discard.all + + audio.discard = Discard.nonkey + assert audio.discard == Discard.nonkey + container.close() + + # Discarding a stream makes demux skip (almost) all of its packets. + def audio_packets(discard: Discard | None) -> int: + c = av.open( + fate_suite( + "amv/MTV_high_res_320x240_sample_Penguin_Joke_MTV_from_WMV.amv" + ) + ) + if discard is not None: + c.streams.audio[0].discard = discard + count = sum( + 1 for p in c.demux() if p.dts is not None and p.stream.type == "audio" + ) + c.close() + return count + + baseline = audio_packets(None) + discarded = audio_packets(Discard.all) + assert baseline > 0 + assert discarded < baseline + def test_printing_video_stream(self) -> None: input_ = av.open( fate_suite("amv/MTV_high_res_320x240_sample_Penguin_Joke_MTV_from_WMV.amv") From 37591e4036ef6a14a52449edfe6a2adcc2f653c9 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 2 Jun 2026 03:46:34 -0400 Subject: [PATCH 766/846] Fix to_ndarray crash on negative linesize, closes #2213 --- CHANGELOG.rst | 1 + av/video/frame.py | 23 +++++++++++++++++---- av/video/plane.py | 9 ++++++++- tests/test_videoframe.py | 43 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 15a7fff89..068fbd80d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -38,6 +38,7 @@ Fixes: - Warn that ``CodecContext.decode()`` is not memory safe in some cases. - Fix ``enumerate_input_devices`` and ``enumerate_output_devices`` raising ``AttributeError`` (:issue:`2264`). - Map HTTP 429 to ``HTTPTooManyRequestsError`` instead of ``UndefinedError`` (:issue:`2267`). +- Fix crash in ``VideoFrame.to_ndarray()`` and ``to_image()`` on bottom-up frames with a negative ``line_size`` (:issue:`2213`). v17.0.1 ------- diff --git a/av/video/frame.py b/av/video/frame.py index 605ec9a9c..1a7b5364d 100644 --- a/av/video/frame.py +++ b/av/video/frame.py @@ -447,7 +447,8 @@ def useful_array( import numpy as np dtype_obj = np.dtype(dtype) - total_line_size = abs(plane.frame.ptr.linesize[plane.index]) + line_size = plane.frame.ptr.linesize[plane.index] + total_line_size = abs(line_size) itemsize = dtype_obj.itemsize channels = bytes_per_pixel // itemsize @@ -458,6 +459,13 @@ def useful_array( shape = (plane.height, plane.width, channels) strides = (total_line_size, bytes_per_pixel, itemsize) + if line_size < 0: + offset = (plane.height - 1) * total_line_size + strides = (-total_line_size, *strides[1:]) + return np.ndarray( + shape, dtype=dtype_obj, buffer=plane, offset=offset, strides=strides + ) + return np.ndarray(shape, dtype=dtype_obj, buffer=plane, strides=strides) @@ -704,17 +712,24 @@ def to_image(self, **kwargs): plane: VideoPlane = self.reformat(format="rgb24", **kwargs).planes[0] i_buf: cython.const[uint8_t][:] = plane - i_pos: cython.size_t = 0 - i_stride: cython.size_t = plane.line_size + line_size: cython.int = plane.line_size + i_stride: cython.size_t = abs(line_size) o_pos: cython.size_t = 0 o_stride: cython.size_t = plane.width * 3 o_size: cython.size_t = plane.height * o_stride o_buf: bytearray = bytearray(o_size) + # For bottom-up frames (negative line_size) the buffer protocol exposes + # rows from the lowest address, so the top display row is at the far end. + i_pos: cython.size_t = (plane.height - 1) * i_stride if line_size < 0 else 0 + while o_pos < o_size: o_buf[o_pos : o_pos + o_stride] = i_buf[i_pos : i_pos + o_stride] - i_pos += i_stride + if line_size < 0: + i_pos -= i_stride + else: + i_pos += i_stride o_pos += o_stride return Image.frombytes( diff --git a/av/video/plane.py b/av/video/plane.py index 0652f108f..15fe70a94 100644 --- a/av/video/plane.py +++ b/av/video/plane.py @@ -78,7 +78,14 @@ def __getbuffer__(self, view: cython.pointer[Py_buffer], flags: cython.int): ) if flags & PyBUF_WRITABLE and not self._buffer_writable(): raise ValueError("buffer is not writable") - PyBuffer_FillInfo(view, self, self._buffer_ptr(), self._buffer_size(), 0, flags) + + ptr: cython.p_void = self._buffer_ptr() + line_size: cython.int = self.frame.ptr.linesize[self.index] + if line_size < 0: + height: cython.int = self.height + ptr = cython.cast(cython.p_char, ptr) + (height - 1) * line_size + + PyBuffer_FillInfo(view, self, ptr, self._buffer_size(), 0, flags) def __dlpack_device__(self): if self.frame.ptr.hw_frames_ctx: diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 8552683e8..26386adb0 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -161,6 +161,49 @@ def test_basic_to_ndarray() -> None: assert array.shape == (480, 640, 3) +def _vflip(frame: VideoFrame) -> VideoFrame: + """Vertically flip a frame, which yields a bottom-up frame with a negative + ``line_size`` (the same layout DirectShow produces, see GH-2213).""" + graph = av.filter.Graph() + src = graph.add_buffer( + template=None, + width=frame.width, + height=frame.height, + format=frame.format, + time_base=Fraction(1, 1000), + ) + vflip = graph.add("vflip") + sink = graph.add("buffersink") + src.link_to(vflip) + vflip.link_to(sink) + graph.configure() + graph.push(frame) + out = graph.pull() + assert isinstance(out, VideoFrame) + return out + + +@pytest.mark.parametrize("format", ["rgb24", "bgr24", "gray"]) +def test_negative_linesize_to_ndarray(format: str) -> None: + # Bottom-up packed frames have a negative line_size; to_ndarray() must read + # them without crashing (GH-2213) and in the correct top-down order. + height, width = 6, 4 + if format == "gray": + array = numpy.arange(height * width, dtype=numpy.uint8).reshape(height, width) + else: + array = numpy.zeros((height, width, 3), dtype=numpy.uint8) + for row in range(height): + array[row, :, :] = row * 10 + + frame = _vflip(VideoFrame.from_ndarray(array, format=format)) + assert frame.planes[0].line_size < 0 + + result = frame.to_ndarray(format=format) + assertNdarraysEqual(result, array[::-1]) + # Fully materializing the array used to segfault on a bottom-up frame. + assert result.copy().sum() == int(array.sum()) + + def test_ndarray_gray() -> None: array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) for format in ("gray", "gray8"): From e75eaf4503968627640ad2ac685d1187eb562cb5 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 4 Jun 2026 03:28:33 -0400 Subject: [PATCH 767/846] Fix #2276 --- av/logging.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/av/logging.py b/av/logging.py index 2b565b9e8..295779444 100644 --- a/av/logging.py +++ b/av/logging.py @@ -81,6 +81,7 @@ def adapt_level(level: cython.int): level_threshold = cython.declare(object, None) +c_level_threshold = cython.declare(cython.int, lib.AV_LOG_QUIET) # ... but lets limit ourselves to WARNING (assuming nobody already did this). if "libav" not in logging.Logger.manager.loggerDict: @@ -107,12 +108,14 @@ def set_level(level): When developing your application. """ global level_threshold + global c_level_threshold if level is None: level_threshold = level lib.av_log_set_callback(nolog_callback) elif type(level) is int: level_threshold = level + c_level_threshold = level lib.av_log_set_callback(log_callback) else: raise ValueError("level must be: int | None") @@ -331,10 +334,8 @@ def log_callback( inited: cython.bint = Py_IsInitialized() if not inited: return - - with cython.gil: - if level > level_threshold and level != lib.AV_LOG_ERROR: - return + if level > c_level_threshold and level != lib.AV_LOG_ERROR: + return # Format the message. message: cython.char[1024] From ce055443002d27bc119bde253a55b4359b7ef897 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 4 Jun 2026 03:42:34 -0400 Subject: [PATCH 768/846] Assign inferred timing to packets, closes #1919 Assign parser-inferred timing to packets in `CodecContext.parse` --- av/codec/context.py | 14 ++++++++++++++ include/avcodec.pxd | 8 +++++++- tests/test_codec_context.py | 16 ++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/av/codec/context.py b/av/codec/context.py index 43d39c0ed..2c82f2de8 100644 --- a/av/codec/context.py +++ b/av/codec/context.py @@ -272,6 +272,10 @@ def parse(self, raw_input=None): It will return all packets that are fully contained within the given input, and will buffer partial packets until they are complete. + Any timing information the parser is able to infer (``pts``, ``dts``, + ``duration``, ``pos`` and the keyframe flag) is assigned onto the + returned packets. Fields the parser cannot determine are left unset. + :param ByteSource raw_input: A chunk of a byte-stream to process. Anything that can be turned into a :class:`.ByteSource` is fine. ``None`` or empty inputs will flush the parser's buffers. @@ -327,6 +331,16 @@ def parse(self, raw_input=None): packet = Packet(out_size) memcpy(packet.ptr.data, out_data, out_size) + # Propagate the timing information the parser inferred for + # this frame onto the packet (mirrors FFmpeg's parse_packet). + packet.ptr.pts = self.parser.pts + packet.ptr.dts = self.parser.dts + packet.ptr.pos = self.parser.pos + if self.parser.duration: + packet.ptr.duration = self.parser.duration + if self.parser.key_frame == 1: + packet.ptr.flags |= lib.AV_PKT_FLAG_KEY + packets.append(packet) if not in_size: diff --git a/include/avcodec.pxd b/include/avcodec.pxd index c273443f0..21b5e8324 100644 --- a/include/avcodec.pxd +++ b/include/avcodec.pxd @@ -448,7 +448,13 @@ cdef extern from "libavcodec/avcodec.h" nogil: int codec_ids[5] cdef struct AVCodecParserContext: - pass + int64_t pts + int64_t dts + int64_t pos + int64_t last_pos + int64_t offset + int duration + int key_frame cdef AVCodecParserContext *av_parser_init(int codec_id) cdef int av_parser_parse2( diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index d0629480e..1f9dc2ee0 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -211,6 +211,22 @@ def _assert_parse(self, codec_name: str, path: str) -> None: assert len(parsed_source) == len(full_source) assert full_source == parsed_source + def test_parse_assigns_packet_timing(self) -> None: + # Regression test for #1919: the parser-inferred timing information + # should be propagated onto the returned packets. + path = fate_suite("mpeg2/mpeg2_field_encoding.ts") + full_source = b"".join(bytes(p) for p in av.open(path).demux(video=0)) + + ctx = Codec("mpeg2video").create() + packets = [] + for i in range(0, len(full_source), 4096): + packets.extend(ctx.parse(full_source[i : i + 4096])) + packets.extend(ctx.parse()) + + # The parser is able to determine the byte position for this stream, + # so at least some packets should carry it through. + assert any(p.pos is not None for p in packets) + class TestEncoding(TestCase): def test_encoding_png(self) -> None: From 5acc3d102df066ddf327da925376432c9d823bc0 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 4 Jun 2026 04:25:54 -0400 Subject: [PATCH 769/846] mypy==2.1.0 --- Makefile | 2 +- tests/test_streams.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index a9fb0876f..d6edf1893 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ fate-suite: rsync -vrltLW rsync://fate-suite.ffmpeg.org/fate-suite/ tests/assets/fate-suite/ lint: - $(PIP) install -U ruff isort pillow numpy mypy==1.19.1 pytest + $(PIP) install -U ruff isort pillow numpy mypy==2.1.0 pytest ruff format --check av examples tests setup.py isort --check-only --diff av examples tests mypy av tests diff --git a/tests/test_streams.py b/tests/test_streams.py index 1afc67222..a78952013 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -245,8 +245,8 @@ def test_data_stream(self) -> None: # Verify we can read back all the packets, ignoring empty ones packets = [p for p in container.demux(data) if bytes(p)] assert len(packets) == len(test_data) - for packet, original_data in zip(packets, test_data): - assert bytes(packet) == original_data + for read_packet, original_data in zip(packets, test_data): + assert bytes(read_packet) == original_data # Test string representation repr = f"{data_stream}" From 4b2c5d1e68c6956c78f2a6aadad0cc0017ad61a5 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 4 Jun 2026 04:29:02 -0400 Subject: [PATCH 770/846] Write remux test output to sandbox instead of project root --- tests/test_remux.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_remux.py b/tests/test_remux.py index 9c4aecfe8..72247c45f 100644 --- a/tests/test_remux.py +++ b/tests/test_remux.py @@ -3,13 +3,14 @@ import av import av.datasets -from .common import fate_suite +from .common import fate_suite, sandboxed def test_video_remux() -> None: input_path = av.datasets.curated("pexels/time-lapse-video-of-night-sky-857195.mp4") + output_path = sandboxed("remuxed.mkv") input_ = av.open(input_path) - output = av.open("remuxed.mkv", "w") + output = av.open(output_path, "w") in_stream = input_.streams.video[0] out_stream = output.add_stream_from_template(in_stream) @@ -24,7 +25,7 @@ def test_video_remux() -> None: input_.close() output.close() - with av.open("remuxed.mkv") as container: + with av.open(output_path) as container: # Assert output is a valid media file assert len(container.streams.video) == 1 assert len(container.streams.audio) == 0 From bc504556cfaf0886e97c0b90cdbd3386714a3b66 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 4 Jun 2026 04:29:56 -0400 Subject: [PATCH 771/846] Update authors.rst --- AUTHORS.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index 536dcd037..6f4446459 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -89,6 +89,8 @@ All contributors (by number of commits): * Gemfield * henri-gasc * Jonathan Martin +* HotariTobu +* Joshua * Johan Jeppsson Karlin * Kian-Meng Ang * Philipp Klaus From 870dd7090bc30be2841163cab0c90e5ebd6b7f7b Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Fri, 5 Jun 2026 18:03:13 +0100 Subject: [PATCH 772/846] Fix FFmpegError memory leak --- av/error.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/av/error.py b/av/error.py index e6cca095e..32fb4d171 100644 --- a/av/error.py +++ b/av/error.py @@ -75,15 +75,15 @@ class FFmpegError(Exception): """ def __init__(self, code, message, filename=None, log=None): - self.errno = code - self.strerror = message - args = [code, message] if filename or log: args.append(filename) if log: args.append(log) super().__init__(*args) + + self.errno = code + self.strerror = message self.args = tuple(args) @property From 209388f57d640ff150e997ed3c11835db302a792 Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Fri, 5 Jun 2026 20:54:58 +0100 Subject: [PATCH 773/846] Fix memory leak in `AudioLayout` by uninitializing channel layout --- av/audio/layout.py | 3 +++ include/avcodec.pxd | 1 + 2 files changed, 4 insertions(+) diff --git a/av/audio/layout.py b/av/audio/layout.py index 344efa220..f3adb3514 100644 --- a/av/audio/layout.py +++ b/av/audio/layout.py @@ -28,6 +28,9 @@ def get_audio_layout(c_layout: lib.AVChannelLayout) -> AudioLayout: @cython.final @cython.cclass class AudioLayout: + def __dealloc__(self): + lib.av_channel_layout_uninit(cython.address(self.layout)) + def __cinit__(self, layout): if layout is _cinit_bypass_sentinel: return diff --git a/include/avcodec.pxd b/include/avcodec.pxd index 21b5e8324..f2123ce0d 100644 --- a/include/avcodec.pxd +++ b/include/avcodec.pxd @@ -16,6 +16,7 @@ cdef extern from "libavutil/channel_layout.h" nogil: int av_channel_description(char *buf, size_t buf_size, AVChannel channel_id) int av_channel_layout_compare(AVChannelLayout *chl, AVChannelLayout *chl1) AVChannel av_channel_layout_channel_from_index(AVChannelLayout *channel_layout, unsigned int idx) + void av_channel_layout_uninit(AVChannelLayout *channel_layout) cdef extern from "libavcodec/avcodec.h" nogil: cdef int avcodec_version() From 75e6854648a89b4038cc6e8e1790406a7af94f23 Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Fri, 5 Jun 2026 21:40:23 +0100 Subject: [PATCH 774/846] Fix memory leak when setting opaque on Frame --- av/opaque.py | 1 + 1 file changed, 1 insertion(+) diff --git a/av/opaque.py b/av/opaque.py index 3a57989e4..d6696289b 100644 --- a/av/opaque.py +++ b/av/opaque.py @@ -15,6 +15,7 @@ def key_free(opaque: cython.p_void, data: u8ptr) -> cython.void: name: cython.p_char = cython.cast(cython.p_char, data) with cython.gil: opaque_container.pop(name) + lib.av_free(data) @cython.final From 4af11a26fed1e6347d8748d5b48a679a5f9536b1 Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Fri, 5 Jun 2026 21:23:03 +0100 Subject: [PATCH 775/846] Break reference cycle between `FilterLink` and `Graph` --- av/filter/link.pxd | 2 +- av/filter/link.py | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/av/filter/link.pxd b/av/filter/link.pxd index 08771362b..7f6aad2aa 100644 --- a/av/filter/link.pxd +++ b/av/filter/link.pxd @@ -7,7 +7,7 @@ from av.filter.link cimport FilterContextPad, FilterLink cdef class FilterLink: - cdef readonly Graph graph + cdef object _graph cdef lib.AVFilterLink *ptr cdef FilterContextPad _input diff --git a/av/filter/link.py b/av/filter/link.py index 09a078d5a..0e5b680ce 100644 --- a/av/filter/link.py +++ b/av/filter/link.py @@ -1,3 +1,5 @@ +import weakref + import cython import cython.cimports.libav as lib from cython.cimports.av.filter.graph import Graph @@ -12,6 +14,13 @@ def __cinit__(self, sentinel): if sentinel is not _cinit_sentinel: raise RuntimeError("cannot instantiate FilterLink") + @property + def graph(self) -> Graph: + if g := self._graph(): + return g + else: + raise RuntimeError("graph is unallocated") + @property def input(self): if self._input: @@ -23,7 +32,8 @@ def input(self): break else: # nobreak raise RuntimeError("could not find link in context") - ctx = self.graph._context_by_ptr[cython.cast(cython.long, cctx)] + graph: Graph = self.graph + ctx = graph._context_by_ptr[cython.cast(cython.long, cctx)] self._input = ctx.outputs[i] return self._input @@ -39,7 +49,8 @@ def output(self): else: raise RuntimeError("could not find link in context") try: - ctx = self.graph._context_by_ptr[cython.cast(cython.long, cctx)] + graph: Graph = self.graph + ctx = graph._context_by_ptr[cython.cast(cython.long, cctx)] except KeyError: raise RuntimeError( "could not find context in graph", (cctx.name, cctx.filter.name) @@ -51,7 +62,7 @@ def output(self): @cython.cfunc def wrap_filter_link(graph: Graph, ptr: cython.pointer[lib.AVFilterLink]) -> FilterLink: link: FilterLink = FilterLink(_cinit_sentinel) - link.graph = graph + link._graph = weakref.ref(graph) link.ptr = ptr return link From 28d50ddc0ae99b6980c05d1aec74ffe33b7020f5 Mon Sep 17 00:00:00 2001 From: Mark Harfouche Date: Sat, 6 Jun 2026 20:18:12 -0400 Subject: [PATCH 776/846] Allow setting the rotation sidedata via new FFMPEG APIs (#2287) Add VideoStream.set_display_matrix() and set_display_rotation() to write the container display matrix (e.g. the MP4/MOV tkhd matrix) when encoding. - set_display_matrix(): the 9 raw AV_PKT_DATA_DISPLAYMATRIX integers, or None. - set_display_rotation(degrees, hflip, vflip): convenience wrapper; degrees is counter-clockwise to match VideoFrame.rotation, and the flips reach all eight EXIF orientations. The matrix is cached as an int32_t[9] member and injected as coded side data in _finalize_for_output(), after avcodec_parameters_from_context() clobbers it and before the header is written. Lives on VideoStream, so audio and subtitle streams don't expose it. Wires up the needed FFmpeg declarations and tests the eight EXIF orientations round-tripping through the container. --- Co-authored-by: WyattBlue --- CHANGELOG.rst | 1 + av/video/stream.pxd | 9 ++ av/video/stream.py | 59 ++++++++ av/video/stream.pyi | 5 + include/avcodec.pxd | 8 +- include/avutil.pxd | 2 + tests/test_display_matrix.py | 252 +++++++++++++++++++++++++++++++++++ 7 files changed, 335 insertions(+), 1 deletion(-) create mode 100644 tests/test_display_matrix.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 068fbd80d..5f61756df 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -32,6 +32,7 @@ Features: - Use FFmpeg 8.1.1 in the binary wheels. - Expose ``AVCodecContext.global_quality`` by :gh-user:`WyattBlue` in (:pr:`2246`). - Expose ``Stream.discard`` so demuxing and seeking can skip unwanted streams (:issue:`2272`). +- Add ``Stream.set_display_matrix()`` and ``Stream.set_display_rotation()`` to write the container display (rotation) matrix on output streams by :gh-user:`hmaarrfk` in (:pr:`2287`). Fixes: - Add ``cython.final`` to leaf classes, ensuring that they are not subclassed. diff --git a/av/video/stream.pxd b/av/video/stream.pxd index f0dcfb9b2..1a553f34c 100644 --- a/av/video/stream.pxd +++ b/av/video/stream.pxd @@ -1,3 +1,5 @@ +from libc.stdint cimport int32_t, uint8_t + from av.packet cimport Packet from av.stream cimport Stream @@ -5,5 +7,12 @@ from .frame cimport VideoFrame cdef class VideoStream(Stream): + # Display matrix (9 int32, native-endian) written as AV_PKT_DATA_DISPLAYMATRIX + # coded side data at mux time, applied only when _has_display_matrix is set. + cdef int32_t _display_matrix[9] + cdef uint8_t _has_display_matrix + + cdef _apply_display_matrix(self) + cpdef encode(self, VideoFrame frame=?) cpdef decode(self, Packet packet=?) diff --git a/av/video/stream.py b/av/video/stream.py index c37f45a3e..bb8b06fc0 100644 --- a/av/video/stream.py +++ b/av/video/stream.py @@ -1,8 +1,11 @@ import cython from cython.cimports import libav as lib from cython.cimports.av.packet import Packet +from cython.cimports.av.stream import Stream from cython.cimports.av.utils import avrational_to_fraction from cython.cimports.av.video.frame import VideoFrame +from cython.cimports.libc.stdint import int32_t +from cython.cimports.libc.string import memcpy @cython.final @@ -56,6 +59,62 @@ def decode(self, packet: Packet | None = None): """ return self.codec_context.decode(packet) + @cython.cfunc + def _finalize_for_output(self): + Stream._finalize_for_output(self) + # avcodec_parameters_from_context() overwrites codecpar.coded_side_data, + # so inject the display matrix after it, before avformat_write_header(). + if self.codec_context is not None and self._has_display_matrix: + self._apply_display_matrix() + + @cython.cfunc + def _apply_display_matrix(self): + n: cython.int = 9 * cython.sizeof(int32_t) + sd: cython.pointer[lib.AVPacketSideData] = lib.av_packet_side_data_new( + cython.address(self.ptr.codecpar.coded_side_data), + cython.address(self.ptr.codecpar.nb_coded_side_data), + lib.AV_PKT_DATA_DISPLAYMATRIX, + n, + 0, + ) + if sd == cython.NULL: + raise MemoryError("could not allocate display matrix side data") + + memcpy(sd.data, self._display_matrix, n) + + def set_display_matrix(self, matrix): + """Set the display matrix written to the container as coded side data. + + ``matrix`` is a sequence of 9 integers in FFmpeg's ``AV_PKT_DATA_DISPLAYMATRIX`` + layout, or ``None`` to clear. Must be called before the first frame is + encoded. See :meth:`set_display_rotation` for a higher-level helper. + """ + if matrix is None: + self._has_display_matrix = False + return + + vals = [int(v) for v in matrix] + if len(vals) != 9: + raise ValueError("display matrix must have exactly 9 elements") + i: cython.int + for i in range(9): + self._display_matrix[i] = vals[i] + self._has_display_matrix = True + + def set_display_rotation(self, degrees, hflip=False, vflip=False): + """Set the container display matrix from a rotation and optional flips. + + ``degrees`` is a counter-clockwise rotation (matching the value read back + from :attr:`VideoFrame.rotation`); ``hflip`` / ``vflip`` mirror after it. + Together these express all eight EXIF orientations. Must be called before + the first frame is encoded. + """ + # av_display_rotation_set() takes a clockwise angle; negate so our public + # `degrees` is counter-clockwise, matching VideoFrame.rotation on read. + lib.av_display_rotation_set(self._display_matrix, -float(degrees)) + lib.av_display_matrix_flip(self._display_matrix, bool(hflip), bool(vflip)) + self._has_display_matrix = True + @property def average_rate(self): """ diff --git a/av/video/stream.pyi b/av/video/stream.pyi index dd670d3cf..4e2a61e46 100644 --- a/av/video/stream.pyi +++ b/av/video/stream.pyi @@ -1,3 +1,4 @@ +from collections.abc import Sequence from fractions import Fraction from typing import Iterator, Literal @@ -20,6 +21,10 @@ class VideoStream(Stream): def encode(self, frame: VideoFrame | None = None) -> list[Packet]: ... def encode_lazy(self, frame: VideoFrame | None = None) -> Iterator[Packet]: ... def decode(self, packet: Packet | None = None) -> list[VideoFrame]: ... + def set_display_matrix(self, matrix: Sequence[int] | None) -> None: ... + def set_display_rotation( + self, degrees: float, hflip: bool = ..., vflip: bool = ... + ) -> None: ... # from codec context format: VideoFormat diff --git a/include/avcodec.pxd b/include/avcodec.pxd index f2123ce0d..5d13abed1 100644 --- a/include/avcodec.pxd +++ b/include/avcodec.pxd @@ -298,7 +298,7 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef char* avcodec_get_name(AVCodecID id) cdef int avcodec_open2(AVCodecContext *ctx, const AVCodec *codec, AVDictionary **options) cdef enum AVPacketSideDataType: - pass + AV_PKT_DATA_DISPLAYMATRIX cdef struct AVPacketSideData: uint8_t *data size_t size @@ -476,6 +476,8 @@ cdef extern from "libavcodec/avcodec.h" nogil: int width int height int sample_rate + AVPacketSideData *coded_side_data + int nb_coded_side_data cdef int avcodec_parameters_copy( AVCodecParameters *dst, const AVCodecParameters *src @@ -513,6 +515,10 @@ cdef extern from "libavcodec/packet.h" nogil: const AVPacketSideData *av_packet_side_data_get( const AVPacketSideData *sd, int nb_sd, AVPacketSideDataType type ) + AVPacketSideData *av_packet_side_data_new( + AVPacketSideData **psd, int *pnb_sd, + AVPacketSideDataType type, size_t size, int flags + ) uint8_t* av_packet_get_side_data( const AVPacket *pkt, AVPacketSideDataType type, size_t *size ) diff --git a/include/avutil.pxd b/include/avutil.pxd index ede8f6fbe..7b0a9e311 100644 --- a/include/avutil.pxd +++ b/include/avutil.pxd @@ -147,6 +147,8 @@ cdef extern from "libavutil/dict.h" nogil: cdef extern from "libavutil/display.h" nogil: cdef double av_display_rotation_get(const int32_t matrix[9]) + cdef void av_display_rotation_set(int32_t matrix[9], double angle) + cdef void av_display_matrix_flip(int32_t matrix[9], int hflip, int vflip) cdef extern from "libavutil/error.h" nogil: cdef int AVERROR_BSF_NOT_FOUND diff --git a/tests/test_display_matrix.py b/tests/test_display_matrix.py new file mode 100644 index 000000000..25c02af6c --- /dev/null +++ b/tests/test_display_matrix.py @@ -0,0 +1,252 @@ +from __future__ import annotations + +import io +import struct +from typing import cast + +import numpy as np +import pytest + +import av +from av.sidedata.sidedata import SideData +from av.video.stream import VideoStream + +WIDTH = 320 +HEIGHT = 240 +DURATION = 10 + +# The 8 EXIF orientations as 3x3 transformation matrices, built the same way as +# the application code: a 90 deg rotation generator (R) and a horizontal-flip +# generator (F). Orientations 2, 4, 5, 7 are reflections, which a scalar +# rotation cannot represent -- so these are verified by comparing the full +# matrix that round-trips through the container. +_R = np.asarray([[0, -1, 0], [1, 0, 0], [0, 0, 1]], dtype=float) # exif 8 +_F = np.asarray([[-1, 0, 0], [0, 1, 0], [0, 0, 1]], dtype=float) # exif 2 + +EXIF_MATRICES = { + 1: np.eye(3), + 2: _F, + 3: _R @ _R, + 4: _F @ _R @ _R, + 5: _F @ _R @ _R @ _R, + 6: _R @ _R @ _R, + 7: _F @ _R, + 8: _R, +} + +# Pure-rotation orientations also have a well-defined scalar rotation, reported +# by av_display_rotation_get() (counter-clockwise, range [-180, 180]). +EXPECTED_ROTATION = {1: 0, 3: 180, 6: -90, 8: 90} + +# Each EXIF orientation expressed through the convenience API as +# (degrees_ccw, hflip, vflip). Verified to reproduce EXIF_MATRICES exactly. +EXIF_VIA_ROTATION = { + 1: (0, False, False), + 2: (0, True, False), + 3: (0, True, True), + 4: (0, False, True), + 5: (90, True, False), + 6: (90, True, True), + 7: (90, False, True), + 8: (90, False, False), +} + +# One encoder per codec family we care about, plus the near-universal mpeg4 +# baseline. Unavailable encoders are skipped at runtime so the suite stays +# portable across FFmpeg builds. +CODECS = ["mpeg4", "libx264", "libopenh264", "libx265", "libsvtav1", "libaom-av1"] + + +def matrix_to_ints(matrix: np.ndarray) -> list[int]: + """Convert a 3x3 matrix to FFmpeg's AV_PKT_DATA_DISPLAYMATRIX integers. + + Layout (a, b, u, c, d, v, x, y, w): 16.16 fixed point everywhere except + u, v, w (indices 2, 5, 8) which are 2.30. + """ + flat = [float(v) for v in matrix.reshape(-1)] + return [ + int(round(v * (1 << 30))) if i in (2, 5, 8) else int(round(v * (1 << 16))) + for i, v in enumerate(flat) + ] + + +def _has_encoder(name: str) -> bool: + try: + av.codec.Codec(name, "w") + except Exception: + return False + return True + + +def _encode(codec_name: str, matrix: list[int] | None) -> io.BytesIO: + buf = io.BytesIO() + container = av.open(buf, "w", format="mp4") + stream = cast(VideoStream, container.add_stream(codec_name, rate=24)) + stream.width = WIDTH + stream.height = HEIGHT + stream.pix_fmt = "yuv420p" + + if matrix is not None: + stream.set_display_matrix(matrix) + + for i in range(DURATION): + img = np.full((HEIGHT, WIDTH, 3), (i * 8) % 256, dtype=np.uint8) + frame = av.VideoFrame.from_ndarray(img, format="rgb24") + for packet in stream.encode(frame): + container.mux(packet) + for packet in stream.encode(): + container.mux(packet) + container.close() + + buf.seek(0) + return buf + + +def _read_frame(buf: io.BytesIO) -> av.VideoFrame: + with av.open(buf, "r", format="mp4") as container: + return next(container.decode(video=0)) + + +def _read_matrix(frame: av.VideoFrame) -> list[int] | None: + sd = frame.side_data.get("DISPLAYMATRIX") + if sd is None: + return None + return list(struct.unpack("=9i", bytes(cast(SideData, sd)))) + + +@pytest.mark.parametrize("codec_name", CODECS) +@pytest.mark.parametrize("orientation", sorted(EXIF_MATRICES)) +def test_exif_orientation_roundtrip(codec_name: str, orientation: int) -> None: + if not _has_encoder(codec_name): + pytest.skip(f"encoder {codec_name} not available") + + expected = matrix_to_ints(EXIF_MATRICES[orientation]) + frame = _read_frame(_encode(codec_name, expected)) + got = _read_matrix(frame) + + identity = matrix_to_ints(np.eye(3)) + if expected == identity: + # Identity is the container default; demuxers emit no side data for it. + assert got is None + assert frame.rotation == 0 + else: + assert got == expected, f"exif {orientation}: wrote {expected}, read {got}" + + if orientation in EXPECTED_ROTATION: + rotation = frame.rotation + # 180 may come back negated; rotations are exact otherwise. + if abs(EXPECTED_ROTATION[orientation]) == 180: + assert abs(rotation) == 180 + else: + assert rotation == EXPECTED_ROTATION[orientation] + + +@pytest.mark.parametrize("degrees,expected", [(0, 0), (90, 90), (180, 180), (270, -90)]) +def test_set_display_rotation_roundtrip(degrees: int, expected: int) -> None: + # The public angle is counter-clockwise, matching VideoFrame.rotation. + buf = io.BytesIO() + container = av.open(buf, "w", format="mp4") + stream = container.add_stream("mpeg4", rate=24) + stream.width = WIDTH + stream.height = HEIGHT + stream.pix_fmt = "yuv420p" + stream.set_display_rotation(degrees) + for i in range(DURATION): + frame = av.VideoFrame.from_ndarray( + np.zeros((HEIGHT, WIDTH, 3), dtype=np.uint8), format="rgb24" + ) + for packet in stream.encode(frame): + container.mux(packet) + for packet in stream.encode(): + container.mux(packet) + container.close() + + buf.seek(0) + rotation = _read_frame(buf).rotation + if abs(expected) == 180: + assert abs(rotation) == 180 + else: + assert rotation == expected + + +@pytest.mark.parametrize("orientation", sorted(EXIF_VIA_ROTATION)) +def test_convenience_reaches_all_exif_orientations(orientation: int) -> None: + # set_display_rotation(degrees, hflip, vflip) must reproduce the exact same + # matrix as the explicit EXIF table for every one of the 8 orientations. + degrees, hflip, vflip = EXIF_VIA_ROTATION[orientation] + expected = matrix_to_ints(EXIF_MATRICES[orientation]) + + buf = io.BytesIO() + container = av.open(buf, "w", format="mp4") + stream = container.add_stream("mpeg4", rate=24) + stream.width = WIDTH + stream.height = HEIGHT + stream.pix_fmt = "yuv420p" + stream.set_display_rotation(degrees, hflip=hflip, vflip=vflip) + for i in range(DURATION): + frame = av.VideoFrame.from_ndarray( + np.zeros((HEIGHT, WIDTH, 3), dtype=np.uint8), format="rgb24" + ) + for packet in stream.encode(frame): + container.mux(packet) + for packet in stream.encode(): + container.mux(packet) + container.close() + + buf.seek(0) + got = _read_matrix(_read_frame(buf)) + if expected == matrix_to_ints(np.eye(3)): + assert got is None # identity emits no side data + else: + assert got == expected, f"exif {orientation}: wrote {expected}, read {got}" + + +def test_matrix_and_rotation_setters_are_mutually_exclusive() -> None: + # Setting one path must clear the other so they don't both apply. + buf = io.BytesIO() + with av.open(buf, "w", format="mp4") as container: + stream = container.add_stream("mpeg4", rate=24) + stream.width = WIDTH + stream.height = HEIGHT + stream.pix_fmt = "yuv420p" + stream.set_display_rotation(90) + stream.set_display_matrix(None) # clears both paths + frame = av.VideoFrame.from_ndarray( + np.zeros((HEIGHT, WIDTH, 3), dtype=np.uint8), format="rgb24" + ) + for packet in stream.encode(frame): + container.mux(packet) + for packet in stream.encode(): + container.mux(packet) + + buf.seek(0) + assert _read_matrix(_read_frame(buf)) is None + + +def test_set_display_matrix_validates_length() -> None: + buf = io.BytesIO() + with av.open(buf, "w", format="mp4") as container: + stream = container.add_stream("mpeg4", rate=24) + with pytest.raises(ValueError): + stream.set_display_matrix([0, 1, 2]) + + +def test_set_display_matrix_none_clears() -> None: + buf = io.BytesIO() + with av.open(buf, "w", format="mp4") as container: + stream = container.add_stream("mpeg4", rate=24) + stream.set_display_matrix(matrix_to_ints(EXIF_MATRICES[6])) + stream.set_display_matrix(None) # clear before encoding + stream.width = WIDTH + stream.height = HEIGHT + stream.pix_fmt = "yuv420p" + frame = av.VideoFrame.from_ndarray( + np.zeros((HEIGHT, WIDTH, 3), dtype=np.uint8), format="rgb24" + ) + for packet in stream.encode(frame): + container.mux(packet) + for packet in stream.encode(): + container.mux(packet) + + buf.seek(0) + assert _read_matrix(_read_frame(buf)) is None From 1940fd450e6165da950f540591da21b608d04a62 Mon Sep 17 00:00:00 2001 From: Kazuki Oikawa Date: Sun, 7 Jun 2026 09:10:03 +0900 Subject: [PATCH 777/846] Fix #2264 (Re-revised) device_name/device_description may contains UTF-8. In commit 2474f35, directly cast to `str` but this assumes ASCII, so it will raises exception if UTF-8 is included. In this commit, casts to `bytes` and then decodes UTF8. --- av/device.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/av/device.py b/av/device.py index 9868f1565..b5d5d110c 100644 --- a/av/device.py +++ b/av/device.py @@ -59,8 +59,10 @@ def _build_device_list(device_list: cython.pointer[lib.AVDeviceInfoList]) -> lis devices.append( DeviceInfo( - name=device_info.device_name if device_info.device_name else "", - description=device_info.device_description + name=cython.cast(bytes, device_info.device_name).decode() + if device_info.device_name + else "", + description=cython.cast(bytes, device_info.device_description).decode() if device_info.device_description else "", is_default=(i == device_list.default_device), From 7edf3fc72af6efd5de8643f24d272179fa9d12cf Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 6 Jun 2026 21:13:44 -0400 Subject: [PATCH 778/846] Remove stale autodoc directives for deleted attributes --- docs/api/codec.rst | 1 - docs/api/container.rst | 1 - 2 files changed, 2 deletions(-) diff --git a/docs/api/codec.rst b/docs/api/codec.rst index 7e06ff8cc..43bb235c2 100644 --- a/docs/api/codec.rst +++ b/docs/api/codec.rst @@ -15,7 +15,6 @@ Descriptors .. autoattribute:: Codec.is_decoder .. autoattribute:: Codec.is_encoder -.. autoattribute:: Codec.descriptor .. autoattribute:: Codec.name .. autoattribute:: Codec.long_name .. autoattribute:: Codec.type diff --git a/docs/api/container.rst b/docs/api/container.rst index f4c9732c9..71739fe3f 100644 --- a/docs/api/container.rst +++ b/docs/api/container.rst @@ -60,7 +60,6 @@ Formats .. autoattribute:: ContainerFormat.name .. autoattribute:: ContainerFormat.long_name -.. autoattribute:: ContainerFormat.options .. autoattribute:: ContainerFormat.input .. autoattribute:: ContainerFormat.output .. autoattribute:: ContainerFormat.is_input From 5f5535cb017954148d077d6598485c9944cee1ed Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 6 Jun 2026 23:19:43 -0400 Subject: [PATCH 779/846] Fix remux examples dropping keyframes with no DTS (#1917) The remux examples skipped demux's flushing packet with `packet.dts is None`. That assumption is wrong: a valid keyframe can demux with no DTS (e.g. the leading reordered packets of a B-frame stream in a PTS-only container like MKV). For such files the keyframe was dropped, producing an undecodable video that only played the audio. Skip the empty flushing packet with `packet.size == 0` instead, and apply the same fix to the remux loops in the test suite. Adds a regression test that encodes a B-frame MKV (whose first packet is a keyframe with no DTS) and asserts the remux keeps it. --- examples/basics/remux.py | 7 +++- examples/subtitles/remux.py | 2 +- tests/test_encode.py | 2 +- tests/test_packet.py | 2 +- tests/test_remux.py | 83 ++++++++++++++++++++++++++++++++++++- tests/test_streams.py | 4 +- 6 files changed, 91 insertions(+), 9 deletions(-) diff --git a/examples/basics/remux.py b/examples/basics/remux.py index 64ac00c99..13247c41a 100644 --- a/examples/basics/remux.py +++ b/examples/basics/remux.py @@ -14,8 +14,11 @@ for packet in input_.demux(in_stream): print(packet) - # We need to skip the "flushing" packets that `demux` generates. - if packet.dts is None: + # We need to skip the empty "flushing" packet that `demux` generates at the + # end. Don't test `packet.dts is None` here: a valid keyframe can legitimately + # have no DTS (e.g. the leading reordered packets of a B-frame stream in MKV), + # and skipping it would drop the keyframe and corrupt the output. + if packet.size == 0: continue # We need to assign the packet to the new stream. diff --git a/examples/subtitles/remux.py b/examples/subtitles/remux.py index 32ee7d6d3..4fdc71f1d 100644 --- a/examples/subtitles/remux.py +++ b/examples/subtitles/remux.py @@ -9,7 +9,7 @@ out_stream = output.add_stream_from_template(in_stream) for packet in input_.demux(in_stream): - if packet.dts is None: + if packet.size == 0: continue packet.stream = out_stream output.mux(packet) diff --git a/tests/test_encode.py b/tests/test_encode.py index d64cadc20..240c67872 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -233,7 +233,7 @@ def test_subtitle_muxing(self) -> None: out_stream = output.add_stream_from_template(in_stream) for packet in input_.demux(in_stream): - if packet.dts is None: + if packet.size == 0: continue packet.stream = out_stream output.mux(packet) diff --git a/tests/test_packet.py b/tests/test_packet.py index f1c862d0b..c4503caad 100644 --- a/tests/test_packet.py +++ b/tests/test_packet.py @@ -197,7 +197,7 @@ def test_skip_samples_remux(self) -> None: with av.open(output_path, "w") as out: out_stream = out.add_stream_from_template(audio_stream) for pkt in inp.demux(audio_stream): - if pkt.dts is None: + if pkt.size == 0: continue if pkt.has_sidedata("skip_samples"): sdata = pkt.get_sidedata("skip_samples") diff --git a/tests/test_remux.py b/tests/test_remux.py index 72247c45f..85f00563b 100644 --- a/tests/test_remux.py +++ b/tests/test_remux.py @@ -1,5 +1,8 @@ import io +import numpy as np +import pytest + import av import av.datasets @@ -16,7 +19,7 @@ def test_video_remux() -> None: out_stream = output.add_stream_from_template(in_stream) for packet in input_.demux(in_stream): - if packet.dts is None: + if packet.size == 0: # skip the flushing packet, not keyframes with no DTS continue packet.stream = out_stream @@ -58,7 +61,7 @@ def test_add_mux_stream_video() -> None: out_stream.time_base = in_stream.time_base for packet in input_.demux(in_stream): - if packet.dts is None: + if packet.size == 0: continue packet.stream = out_stream output.mux(packet) @@ -109,3 +112,79 @@ def test_add_stream_from_template_copies_time_base() -> None: out_audio = output.add_stream_from_template(in_audio) assert out_audio.time_base is not None assert out_audio.time_base == in_audio.time_base + + +def _make_b_frame_mkv(n: int = 48) -> io.BytesIO: + """Encode `n` frames with B-frames into an in-memory MKV. + + Matroska stores only presentation timestamps, so when this is demuxed again + libavformat cannot reconstruct a DTS for the leading reordered packets and + leaves it as None -- including on the very first packet, which is the + keyframe. That is exactly the layout that broke the remux example in #1917. + """ + buf = io.BytesIO() + with av.open(buf, "w", format="matroska") as out: + stream = out.add_stream("h264", rate=30) + stream.width, stream.height, stream.pix_fmt = 160, 120, "yuv420p" + stream.options = {"bf": "3", "g": "30"} + for i in range(n): + img = np.full((120, 160, 3), (i * 5) % 256, dtype="uint8") + frame = av.VideoFrame.from_ndarray(img, format="rgb24") + for packet in stream.encode(frame): + out.mux(packet) + for packet in stream.encode(None): + out.mux(packet) + buf.seek(0) + return buf + + +def _decoded_frame_count(buf: io.BytesIO) -> int: + buf.seek(0) + with av.open(buf, "r") as container: + return sum(1 for _ in container.decode(video=0)) + + +def test_remux_keeps_keyframe_with_none_dts() -> None: + """Regression test for #1917. + + A keyframe can legitimately demux with ``dts is None`` (B-frame stream in a + PTS-only container like MKV). The remux loop must skip only the empty + flushing packet (``size == 0``), not every ``dts is None`` packet, otherwise + the keyframe is dropped and the output is undecodable. + """ + if av.codec.Codec("h264", "w").name != "libx264": + pytest.skip("requires libx264") + + source = _make_b_frame_mkv() + expected_frames = _decoded_frame_count(source) + assert expected_frames > 0 + + # Precondition: the first packet really is a keyframe without a DTS, which is + # what the old `dts is None` filter would have wrongly discarded. + source.seek(0) + with av.open(source, "r") as input_: + first = next(p for p in input_.demux(input_.streams.video[0]) if p.size) + assert first.is_keyframe + assert first.dts is None + + source.seek(0) + output = io.BytesIO() + with ( + av.open(source, "r") as input_, + av.open(output, "w", format="matroska") as out, + ): + in_video = input_.streams.video[0] + out_video = out.add_stream_from_template(in_video) + for packet in input_.demux(in_video): + if packet.size == 0: # the flushing packet, not a keyframe with no DTS + continue + packet.stream = out_video + out.mux(packet) + + # The keyframe survived: every frame still decodes and the first packet of + # the remuxed stream is a keyframe. + assert _decoded_frame_count(output) == expected_frames + output.seek(0) + with av.open(output, "r") as container: + first_out = next(p for p in container.demux(video=0) if p.size) + assert first_out.is_keyframe diff --git a/tests/test_streams.py b/tests/test_streams.py index a78952013..9e4941c3a 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -360,7 +360,7 @@ def test_attachment_stream(self) -> None: out_v = out1.add_stream_from_template(in_v) for packet in input_.demux(in_v): - if packet.dts is None: + if packet.size == 0: continue packet.stream = out_v out1.mux(packet) @@ -382,7 +382,7 @@ def test_attachment_stream(self) -> None: stream_map[s.index] = oc.add_stream_from_template(s) for packet in ic.demux(ic.streams.video): - if packet.dts is None: + if packet.size == 0: continue updated_stream = stream_map.get(packet.stream.index) if isinstance(updated_stream, av.video.stream.VideoStream): From ef72887eb5be25e2fe23bca92fd11dcc92e4c7d8 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 6 Jun 2026 23:49:50 -0400 Subject: [PATCH 780/846] Update Authors --- AUTHORS.rst | 226 ++++++++++++++++++++++++++-------------------------- 1 file changed, 114 insertions(+), 112 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 6f4446459..5238156e2 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -11,117 +11,119 @@ All contributors (by number of commits): - Mark Reid ; `@markreidvfx `_ * Lukas Geiger -* Vidar Tonaas Fauske ; `@vidartf `_ -* laggykiller ; `@laggykiller `_ -* Billy Shambrook ; `@billyshambrook `_ -* Casper van der Wel -* Philip de Nier -* Tadas Dailyda -* Dave Johansen -* JoeUgly <41972063+JoeUgly@users.noreply.github.com> -* Justin Wong <46082645+uvjustin@users.noreply.github.com> -* Mark Harfouche -* Santtu Keskinen -- Alba Mendez -- Curtis Doty ; `@dotysan `_ -- Xinran Xu ; `@xxr3376 `_ -- z-khan -- Marc Mueller <30130371+cdce8p@users.noreply.github.com> -- Dan Allan ; `@danielballan `_ -- Moonsik Park -- velsinki <40809145+velsinki@users.noreply.github.com> -- Christoph Rackwitz -- David Plowman -- Alireza Davoudi ; `@adavoudi `_ -- Jonathan Drolet -- Matthew Lai -- Kim Minjong -- Moritz Kassner ; `@mkassner `_ -- Thomas A Caswell ; `@tacaswell `_ -- Ulrik Mikaelsson ; `@rawler `_ -- Wel C. van der -- Will Patera ; `@willpatera `_ +- Vidar Tonaas Fauske ; `@vidartf `_ +- laggykiller ; `@laggykiller `_ +- Billy Shambrook ; `@billyshambrook `_ +- Casper van der Wel +- Philip de Nier +- Tadas Dailyda +- Dave Johansen +- Mark Harfouche +- JoeUgly <41972063+JoeUgly@users.noreply.github.com> +- Justin Wong <46082645+uvjustin@users.noreply.github.com> +- Santtu Keskinen -* zzjjbb <31069326+zzjjbb@users.noreply.github.com> -* Joe Schiff <41972063+JoeSchiff@users.noreply.github.com> -* Nils DEYBACH <68770774+ndeybach@users.noreply.github.com> -* Dexer <73297572+DexerBR@users.noreply.github.com> -* DE-AI <81620697+DE-AI@users.noreply.github.com> -* rutsh -* Felix Vollmer -* Benedikt Lorch, benedikt-grl -* Santiago Castro -* Christian Clauss -* Ihor Liubymov -* Johannes Erdfelt -* Karl Litterfeldt ; `@litterfeldt `_ -* Leon White -* Martin Larralde -* Simon-Martin Schröder -* Matteo Destro -* Mattias Wadman -* mephi42 -* Miles Kaufmann -* Nathan Goldbaum -* Pablo Prietz -* Andrew Wason -* Radek Senfeld ; `@radek-senfeld `_ -* robinechuca -* Nick <24689722+ntjohnson1@users.noreply.github.com> -* Benjamin Chrétien <2742231+bchretien@users.noreply.github.com> -* 吴小白 <296015668@qq.com> -* davidplowman <38045873+davidplowman@users.noreply.github.com> -* Hanz <40712686+HanzCEO@users.noreply.github.com> -* Clay Castronovo <42858023+clayy24@users.noreply.github.com> -* Kesh Ikuma <79113787+tikuma-lsuhsc@users.noreply.github.com> -* Artturin -* Ian Lee -* Ryan Huang -* Arthur Barros -* bdavid-evertz -* Carlos Ruiz -* Carlos Ruiz -* Maxime Desroches -* egao1980 -* Eric Kalosa-Kenyon -* elxy -* Gemfield -* henri-gasc -* Jonathan Martin -* HotariTobu -* Joshua -* Johan Jeppsson Karlin -* Kian-Meng Ang -* Philipp Klaus -* Marcell Pardavi -* Matteo Destro -* Max Ehrlich -* Manuel Goacolou -* Julian Schweizer -* Nikhil Idiculla -* Ömer Sezgin Uğurlu -* Orivej Desh -* Philipp Krähenbühl -* Mattia Procopio -* Max Ehrlich -* ramoncaldeira -* Roland van Laar -* Santiago Castro -* Kengo Sawatsu -* FirefoxMetzger -* hyenal -* Brendan Long ; `@brendanlong `_ -* Семён Марьясин -* Stephen.Y -* Tom Flanagan -* Tim O'Shea -* Tim Ahpee -* Jonas Tingeborn -* Pino Toscano -* Ulrik Mikaelsson -* Vasiliy Kotov -* Koichi Akabe -* David Joy -* Sviatoslav Sydorenko (Святослав Сидоренко) -* Jiabei Zhu +* Alba Mendez +* Curtis Doty ; `@dotysan `_ +* Xinran Xu ; `@xxr3376 `_ +* z-khan +* Marc Mueller <30130371+cdce8p@users.noreply.github.com> +* Dan Allan ; `@danielballan `_ +* Moonsik Park +* velsinki <40809145+velsinki@users.noreply.github.com> +* Christoph Rackwitz +* David Plowman +* Alireza Davoudi ; `@adavoudi `_ +* Jonathan Drolet +* Matthew Lai +* Kim Minjong +* Moritz Kassner ; `@mkassner `_ +* Thomas A Caswell ; `@tacaswell `_ +* Ulrik Mikaelsson ; `@rawler `_ +* Wel C. van der +* Will Patera ; `@willpatera `_ + +- zzjjbb <31069326+zzjjbb@users.noreply.github.com> +- Joe Schiff <41972063+JoeSchiff@users.noreply.github.com> +- Nils DEYBACH <68770774+ndeybach@users.noreply.github.com> +- Dexer <73297572+DexerBR@users.noreply.github.com> +- DE-AI <81620697+DE-AI@users.noreply.github.com> +- rutsh +- Felix Vollmer +- Benedikt Lorch, benedikt-grl +- Santiago Castro +- Christian Clauss +- Ihor Liubymov +- Johannes Erdfelt +- Karl Litterfeldt ; `@litterfeldt `_ +- Leon White +- Martin Larralde +- Simon-Martin Schröder +- Matteo Destro +- Mattias Wadman +- mephi42 +- Miles Kaufmann +- Nathan Goldbaum +- Pablo Prietz +- Andrew Wason +- Radek Senfeld ; `@radek-senfeld `_ +- robinechuca +- Nick <24689722+ntjohnson1@users.noreply.github.com> +- Benjamin Chrétien <2742231+bchretien@users.noreply.github.com> +- 吴小白 <296015668@qq.com> +- davidplowman <38045873+davidplowman@users.noreply.github.com> +- Hanz <40712686+HanzCEO@users.noreply.github.com> +- Clay Castronovo <42858023+clayy24@users.noreply.github.com> +- Kesh Ikuma <79113787+tikuma-lsuhsc@users.noreply.github.com> +- Artturin +- Ian Lee +- Ryan Huang +- Arthur Barros +- bdavid-evertz +- Carlos Ruiz +- Carlos Ruiz +- Maxime Desroches +- egao1980 +- Eric Kalosa-Kenyon +- elxy +- Gemfield +- henri-gasc +- Jonathan Martin +- HotariTobu +- Joshua +- Johan Jeppsson Karlin +- Kazuki Oikawa +- Kian-Meng Ang +- Philipp Klaus +- Marcell Pardavi +- Matteo Destro +- Max Ehrlich +- Manuel Goacolou +- Julian Schweizer +- Nikhil Idiculla +- Ömer Sezgin Uğurlu +- Orivej Desh +- Philipp Krähenbühl +- Mattia Procopio +- Max Ehrlich +- ramoncaldeira +- Roland van Laar +- Santiago Castro +- Kengo Sawatsu +- FirefoxMetzger +- hyenal +- Brendan Long ; `@brendanlong `_ +- Семён Марьясин +- Stephen.Y +- Tom Flanagan +- Tim O'Shea +- Tim Ahpee +- Jonas Tingeborn +- Pino Toscano +- Ulrik Mikaelsson +- Vasiliy Kotov +- Koichi Akabe +- David Joy +- Sviatoslav Sydorenko (Святослав Сидоренко) +- Jiabei Zhu From 97241598792f4c980c32458187154c7c999d68b3 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 7 Jun 2026 00:03:49 -0400 Subject: [PATCH 781/846] Release 17.1.0 --- CHANGELOG.rst | 31 +++++++++++++++++++++++-------- av/about.py | 2 +- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5f61756df..ba11e5168 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -25,21 +25,36 @@ We are operating with `semantic versioning `_. - Bug fixes (PATCH) go here. - $CHANGE by :gh-user:`mikeboers` in (:pr:`1`). -v17.1.0 (next) --------------- +v17.1.0 +------- + +Breaking: +- Remove the undertested ``av.option`` and ``av.descriptor`` APIs, along with the related ``Codec`` and ``Filter`` descriptor accessors. Features: -- Use FFmpeg 8.1.1 in the binary wheels. +- Use FFmpeg 8.1.1 in the binary wheels by :gh-user:`WyattBlue`. +- Build Linux ARMv7 binary wheels by :gh-user:`WyattBlue`. - Expose ``AVCodecContext.global_quality`` by :gh-user:`WyattBlue` in (:pr:`2246`). -- Expose ``Stream.discard`` so demuxing and seeking can skip unwanted streams (:issue:`2272`). +- Expose ``Stream.discard`` so demuxing and seeking can skip unwanted streams by :gh-user:`WyattBlue` (:issue:`2272`). - Add ``Stream.set_display_matrix()`` and ``Stream.set_display_rotation()`` to write the container display (rotation) matrix on output streams by :gh-user:`hmaarrfk` in (:pr:`2287`). +- Add ``Container.video_codec_id`` to force a specific video codec on a container by :gh-user:`WyattBlue` (:issue:`2243`). Fixes: -- Add ``cython.final`` to leaf classes, ensuring that they are not subclassed. +- Add ``cython.final`` to leaf classes, ensuring that they are not subclassed by :gh-user:`WyattBlue`. - Warn that ``CodecContext.decode()`` is not memory safe in some cases. -- Fix ``enumerate_input_devices`` and ``enumerate_output_devices`` raising ``AttributeError`` (:issue:`2264`). -- Map HTTP 429 to ``HTTPTooManyRequestsError`` instead of ``UndefinedError`` (:issue:`2267`). -- Fix crash in ``VideoFrame.to_ndarray()`` and ``to_image()`` on bottom-up frames with a negative ``line_size`` (:issue:`2213`). +- Fix memory leaks in ``FFmpegError``, ``AudioLayout`` channel layouts, and ``Frame.opaque``, and break a reference cycle between ``FilterLink`` and ``Graph`` by :gh-user:`lgeiger`. +- Reduce excessive logging lock contention by :gh-user:`WyattBlue` (:issue:`2276`). +- Fix a crash when accessing ``Stream`` from multiple threads under FFmpeg 8.1 by :gh-user:`WyattBlue` (:issue:`2247`). +- Fix a crash during ``InputContainer`` initialization by :gh-user:`WyattBlue` (:issue:`2010`). +- Fix ``enumerate_input_devices`` and ``enumerate_output_devices`` raising ``AttributeError`` by :gh-user:`WyattBlue` and :gh-user:`kazuki` (:issue:`2264`). +- Map HTTP 429 to ``HTTPTooManyRequestsError`` instead of ``UndefinedError`` by :gh-user:`WyattBlue` (:issue:`2267`). +- Fix crash in ``VideoFrame.to_ndarray()`` and ``to_image()`` on bottom-up frames with a negative ``line_size`` by :gh-user:`WyattBlue` (:issue:`2213`). +- Make ``Disposition`` an ``IntFlag`` so ``Stream.disposition`` can be assigned without raising ``TypeError`` by :gh-user:`HotariTobu`. +- Assign parser-inferred ``pts``, ``dts``, and ``duration`` to packets from ``CodecContext.parse()`` by :gh-user:`WyattBlue` (:issue:`1919`). +- Copy ``time_base`` in ``add_stream_from_template()`` by :gh-user:`daveisfera` in (:pr:`2249`). +- Fix the remux examples dropping keyframes that demux with no DTS, which produced audio-only output by :gh-user:`WyattBlue` (:issue:`1917`). +- Fix subtitle UTF-8 handling by :gh-user:`jbree` in (:pr:`2271`). +- Fix several incorrect ``malloc`` size calculations by :gh-user:`WyattBlue`. v17.0.1 ------- diff --git a/av/about.py b/av/about.py index e16231267..1bef25448 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "17.1.0pre" +__version__ = "17.1.0" From 188f1b3512fcc18f2a3df51778fe20a681fbd084 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 7 Jun 2026 00:31:31 -0400 Subject: [PATCH 782/846] Bump to next dev version --- CHANGELOG.rst | 21 +++++++++++++++++++++ av/about.py | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ba11e5168..8364a5bad 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,22 +16,42 @@ We are operating with `semantic versioning `_. v21.67.42 --------- Major: + - Breaking changes (MAJOR) go here, including for binary wheels. Features: + - Features (MINOR) changes go here. Fixes: + - Bug fixes (PATCH) go here. - $CHANGE by :gh-user:`mikeboers` in (:pr:`1`). +v18.0.0 (next) +-------------- + +Breaking: + +- Remove Python 3.10 + +Features: + +- Nothing (yet) + +Fixes: + +- Nothing (yet) + v17.1.0 ------- Breaking: + - Remove the undertested ``av.option`` and ``av.descriptor`` APIs, along with the related ``Codec`` and ``Filter`` descriptor accessors. Features: + - Use FFmpeg 8.1.1 in the binary wheels by :gh-user:`WyattBlue`. - Build Linux ARMv7 binary wheels by :gh-user:`WyattBlue`. - Expose ``AVCodecContext.global_quality`` by :gh-user:`WyattBlue` in (:pr:`2246`). @@ -40,6 +60,7 @@ Features: - Add ``Container.video_codec_id`` to force a specific video codec on a container by :gh-user:`WyattBlue` (:issue:`2243`). Fixes: + - Add ``cython.final`` to leaf classes, ensuring that they are not subclassed by :gh-user:`WyattBlue`. - Warn that ``CodecContext.decode()`` is not memory safe in some cases. - Fix memory leaks in ``FFmpegError``, ``AudioLayout`` channel layouts, and ``Frame.opaque``, and break a reference cycle between ``FilterLink`` and ``Graph`` by :gh-user:`lgeiger`. diff --git a/av/about.py b/av/about.py index 1bef25448..e12ca9a2e 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "17.1.0" +__version__ = "18.0.0pre" From fc463fa21dbbe0ff316824a73b311d67f8bfc0c3 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 8 Jun 2026 18:46:29 -0400 Subject: [PATCH 783/846] Expose libswresample options in AudioResampler closes #2262 Add an `options` parameter to `AudioResampler` so callers can pass `libswresample` options (e.g. `resampler`, `filter_size`, `phase_shift`, `cutoff`, `precision`) through to the underlying resampler. When options are supplied, the conversion is performed by an explicit `aresample` filter (which owns the SwrContext) instead of the one FFmpeg auto-inserts before `aformat`. Options are forwarded as filter options, which search child contexts and reach the SwrContext. No new C bindings are required. --- CHANGELOG.rst | 2 +- av/audio/resampler.pxd | 1 + av/audio/resampler.py | 24 ++++++++++++++++--- av/audio/resampler.pyi | 3 +++ tests/test_audioresampler.py | 45 ++++++++++++++++++++++++++++++++++++ 5 files changed, 71 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8364a5bad..a18a60f07 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -37,7 +37,7 @@ Breaking: Features: -- Nothing (yet) +- Add ``options`` parameter to ``AudioResampler`` for passing ``libswresample`` options (e.g. ``resampler``, ``filter_size``, ``cutoff``) by :gh-user:`WyattBlue` (:issue:`2262`). Fixes: diff --git a/av/audio/resampler.pxd b/av/audio/resampler.pxd index 20b74186e..2846bdfb6 100644 --- a/av/audio/resampler.pxd +++ b/av/audio/resampler.pxd @@ -13,6 +13,7 @@ cdef class AudioResampler: cdef readonly AudioLayout layout cdef readonly int rate cdef readonly unsigned int frame_size + cdef readonly dict options cdef Graph graph cpdef list resample(self, AudioFrame) diff --git a/av/audio/resampler.py b/av/audio/resampler.py index fb6d98910..785e522b9 100644 --- a/av/audio/resampler.py +++ b/av/audio/resampler.py @@ -9,16 +9,23 @@ @cython.final @cython.cclass class AudioResampler: - """AudioResampler(format=None, layout=None, rate=None) + """AudioResampler(format=None, layout=None, rate=None, frame_size=None, options=None) :param AudioFormat format: The target format, or string that parses to one (e.g. ``"s16"``). :param AudioLayout layout: The target layout, or an int/string that parses to one (e.g. ``"stereo"``). :param int rate: The target sample rate. + :param int frame_size: The number of samples per output frame. + :param dict options: ``libswresample`` options passed to the underlying + ``aresample`` filter (e.g. ``{"resampler": "soxr", "precision": "28"}``). + See the `FFmpeg resampler documentation + `_ for the full list. """ - def __cinit__(self, format=None, layout=None, rate=None, frame_size=None): + def __cinit__( + self, format=None, layout=None, rate=None, frame_size=None, options=None + ): if format is not None: self.format = ( format if isinstance(format, AudioFormat) else AudioFormat(format) @@ -29,6 +36,7 @@ def __cinit__(self, format=None, layout=None, rate=None, frame_size=None): self.rate = int(rate) if rate else 0 self.frame_size = int(frame_size) if frame_size else 0 + self.options = {str(k): str(v) for k, v in options.items()} if options else {} self.graph = None @cython.ccall @@ -91,7 +99,17 @@ def resample(self, frame: AudioFrame | None) -> list: channel_layouts=self.layout.name, ) abuffersink = self.graph.add("abuffersink") - abuffer.link_to(aformat) + + # When libswresample options are given, do the conversion with an + # explicit aresample filter (which owns the SwrContext) instead of + # relying on the one FFmpeg auto-inserts before aformat. + if self.options: + aresample = self.graph.add("aresample", **self.options) + abuffer.link_to(aresample) + aresample.link_to(aformat) + else: + abuffer.link_to(aformat) + aformat.link_to(abuffersink) self.graph.configure() diff --git a/av/audio/resampler.pyi b/av/audio/resampler.pyi index cbf2134aa..80c16cf08 100644 --- a/av/audio/resampler.pyi +++ b/av/audio/resampler.pyi @@ -8,6 +8,8 @@ class AudioResampler: rate: int frame_size: int format: AudioFormat + layout: AudioLayout + options: dict[str, str] graph: Graph | None def __init__( @@ -16,5 +18,6 @@ class AudioResampler: layout: str | int | AudioLayout | None = None, rate: int | None = None, frame_size: int | None = None, + options: dict[str, str] | None = None, ) -> None: ... def resample(self, frame: AudioFrame | None) -> list[AudioFrame]: ... diff --git a/tests/test_audioresampler.py b/tests/test_audioresampler.py index cad8ebe34..65fdfdddb 100644 --- a/tests/test_audioresampler.py +++ b/tests/test_audioresampler.py @@ -269,6 +269,51 @@ def test_pts_missing_time_base() -> None: assert oframe.samples == 16 +def test_swr_options() -> None: + """ + libswresample options are passed through to the underlying aresample filter. + """ + resampler = AudioResampler( + "fltp", + "mono", + 16000, + options={"filter_size": "32", "phase_shift": "12", "cutoff": "0.95"}, + ) + assert resampler.options == { + "filter_size": "32", + "phase_shift": "12", + "cutoff": "0.95", + } + + iframe = AudioFrame("s16", "stereo", 1024) + iframe.sample_rate = 48000 + iframe.time_base = Fraction(1, 48000) + iframe.pts = 0 + + oframes = resampler.resample(iframe) + assert len(oframes) == 1 + + oframe = oframes[0] + assert oframe.sample_rate == 16000 + assert oframe.format.name == "fltp" + assert oframe.layout.name == "mono" + + +def test_swr_options_invalid() -> None: + """ + An unknown option is reported rather than silently ignored. + """ + resampler = AudioResampler("s16", "mono", 44100, options={"not_a_real_option": "1"}) + + iframe = AudioFrame("s16", "stereo", 1024) + iframe.sample_rate = 48000 + iframe.time_base = Fraction(1, 48000) + iframe.pts = 0 + + with pytest.raises(ValueError, match="unused config: not_a_real_option"): + resampler.resample(iframe) + + def test_mismatched_input() -> None: """ Consecutive frames must have the same layout, sample format and sample rate. From 748bf36bd4acb110ad9a1379508433aff376fe3a Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 10 Jun 2026 23:26:44 -0400 Subject: [PATCH 784/846] Require Python 3.11 and modernize syntax --- .github/workflows/smoke.yml | 2 +- .github/workflows/tests.yml | 3 +-- av/audio/codeccontext.pyi | 3 ++- av/audio/fifo.py | 4 ++-- av/audio/frame.pyi | 16 ++++++++-------- av/codec/codec.py | 16 ++++------------ av/codec/codec.pyi | 3 ++- av/container/core.pyi | 7 ++++--- av/container/input.py | 3 +-- av/container/input.pyi | 4 ++-- av/container/output.py | 2 +- av/container/output.pyi | 4 ++-- av/container/streams.py | 2 +- av/container/streams.pyi | 3 ++- av/datasets.py | 2 +- av/dictionary.pyi | 2 +- av/filter/graph.py | 2 +- av/index.pyi | 3 ++- av/logging.py | 2 +- av/logging.pyi | 3 ++- av/packet.py | 3 ++- av/packet.pyi | 3 ++- av/sidedata/sidedata.pyi | 4 ++-- av/subtitles/subtitle.py | 2 +- av/subtitles/subtitle.pyi | 3 ++- av/video/codeccontext.pyi | 3 ++- av/video/format.py | 2 +- av/video/frame.pyi | 14 +++++++------- av/video/stream.pyi | 4 ++-- examples/basics/parse.py | 2 +- examples/basics/thread_type.py | 4 ++-- pyproject.toml | 3 +-- setup.py | 2 +- tests/common.py | 9 +++------ tests/test_codec_context.py | 11 ++++------- tests/test_subtitles.py | 4 ++-- 36 files changed, 76 insertions(+), 83 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 6fb33ad02..b4962872f 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -36,7 +36,7 @@ jobs: matrix: config: - {os: ubuntu-24.04, python: "3.12", ffmpeg: "8.0.1", extras: true} - - {os: ubuntu-24.04, python: "3.10", ffmpeg: "8.0.1"} + - {os: ubuntu-24.04, python: "3.11", ffmpeg: "8.0.1"} - {os: ubuntu-24.04, python: "3.13", ffmpeg: "8.1"} - {os: macos-14, python: "3.11", ffmpeg: "8.0.1"} - {os: macos-14, python: "3.14", ffmpeg: "8.1"} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7098f7cf3..9f20ee93f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -70,8 +70,7 @@ jobs: CIBW_ENVIRONMENT_MACOS: PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig LDFLAGS=-headerpad_max_install_names CIBW_ENVIRONMENT_WINDOWS: INCLUDE=C:\\cibw\\vendor\\include LIB=C:\\cibw\\vendor\\lib PYAV_SKIP_TESTS=unicode_filename CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: delvewheel repair --add-path C:\cibw\vendor\bin -w {dest_dir} {wheel} - CIBW_BUILD: "cp310* cp311* cp314t*" - CIBW_SKIP: "cp310-win_arm64" + CIBW_BUILD: "cp311* cp314t*" CIBW_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m pytest {package}/tests && mv {project}/av.disabled {project}/av CIBW_TEST_REQUIRES: pytest numpy CIBW_TEST_SKIP: "*_armv7l" diff --git a/av/audio/codeccontext.pyi b/av/audio/codeccontext.pyi index b3ec3ce6e..e871db698 100644 --- a/av/audio/codeccontext.pyi +++ b/av/audio/codeccontext.pyi @@ -1,4 +1,5 @@ -from typing import Iterator, Literal +from collections.abc import Iterator +from typing import Literal from av.codec.context import CodecContext from av.packet import Packet diff --git a/av/audio/fifo.py b/av/audio/fifo.py index 0fd128ee7..00db125b7 100644 --- a/av/audio/fifo.py +++ b/av/audio/fifo.py @@ -95,8 +95,8 @@ def write(self, frame: AudioFrame | None): ) if frame.ptr.pts != expected_pts: raise ValueError( - "Frame.pts (%d) != expected (%d); fix or set to None." - % (frame.ptr.pts, expected_pts) + f"Frame.pts ({frame.ptr.pts}) != expected ({expected_pts}); " + "fix or set to None." ) err_check( diff --git a/av/audio/frame.pyi b/av/audio/frame.pyi index 6aeb86b4d..8efb26760 100644 --- a/av/audio/frame.pyi +++ b/av/audio/frame.pyi @@ -1,4 +1,4 @@ -from typing import Any, Union +from typing import Any import numpy as np @@ -9,13 +9,13 @@ from .layout import AudioLayout from .plane import AudioPlane format_dtypes: dict[str, str] -_SupportedNDarray = Union[ - np.ndarray[Any, np.dtype[np.float64]], # f8 - np.ndarray[Any, np.dtype[np.float32]], # f4 - np.ndarray[Any, np.dtype[np.int32]], # i4 - np.ndarray[Any, np.dtype[np.int16]], # i2 - np.ndarray[Any, np.dtype[np.uint8]], # u1 -] +_SupportedNDarray = ( + np.ndarray[Any, np.dtype[np.float64]] # f8 + | np.ndarray[Any, np.dtype[np.float32]] # f4 + | np.ndarray[Any, np.dtype[np.int32]] # i4 + | np.ndarray[Any, np.dtype[np.int16]] # i2 + | np.ndarray[Any, np.dtype[np.uint8]] # u1 +) class _Format: def __get__(self, i: object | None, owner: type | None = None) -> AudioFormat: ... diff --git a/av/codec/codec.py b/av/codec/codec.py index aff456ecb..616980641 100644 --- a/av/codec/codec.py +++ b/av/codec/codec.py @@ -116,7 +116,7 @@ def _init(self, name=None): if not self.desc: self.desc = lib.avcodec_descriptor_get(self.ptr.id) if not self.desc: - raise RuntimeError("No codec descriptor for %r." % name) + raise RuntimeError(f"No codec descriptor for {name!r}.") self.is_encoder = lib.av_codec_is_encoder(self.ptr) @@ -384,17 +384,9 @@ def dump_codecs(): try: print( - " %s%s%s%s%s%s %-18s %s" - % ( - ".D"[bool(d_codec)], - ".E"[bool(e_codec)], - codec.type[0].upper(), - ".I"[codec.intra_only], - ".L"[codec.lossy], - ".S"[codec.lossless], - codec.name, - codec.long_name, - ) + f" {'.D'[bool(d_codec)]}{'.E'[bool(e_codec)]}{codec.type[0].upper()}" + f"{'.I'[codec.intra_only]}{'.L'[codec.lossy]}{'.S'[codec.lossless]}" + f" {codec.name:<18} {codec.long_name}" ) except Exception as e: print(f"...... {codec.name:<18} ERROR: {e}") diff --git a/av/codec/codec.pyi b/av/codec/codec.pyi index 1459bc777..32c83371e 100644 --- a/av/codec/codec.pyi +++ b/av/codec/codec.pyi @@ -1,6 +1,7 @@ +from collections.abc import Sequence from enum import Flag, IntEnum from fractions import Fraction -from typing import ClassVar, Literal, Sequence, cast, overload +from typing import ClassVar, Literal, cast, overload from av.audio.codeccontext import AudioCodecContext from av.audio.format import AudioFormat diff --git a/av/container/core.pyi b/av/container/core.pyi index 81e4936e4..ec4a84d89 100644 --- a/av/container/core.pyi +++ b/av/container/core.pyi @@ -1,8 +1,9 @@ +from collections.abc import Callable from enum import Flag, IntEnum from fractions import Fraction from pathlib import Path from types import TracebackType -from typing import Any, Callable, ClassVar, Literal, Type, TypedDict, cast, overload +from typing import Any, ClassVar, Literal, Self, TypedDict, cast, overload from av.codec.hwaccel import HWAccel from av.format import ContainerFormat @@ -92,10 +93,10 @@ class Container: read_timeout: Real | None flags: int video_codec_id: int - def __enter__(self) -> Container: ... + def __enter__(self) -> Self: ... def __exit__( self, - exc_type: Type[BaseException] | None, + exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: ... diff --git a/av/container/input.py b/av/container/input.py index f5fc52547..4881e720d 100644 --- a/av/container/input.py +++ b/av/container/input.py @@ -238,8 +238,7 @@ def decode(self, *args, **kwargs): """ self._assert_open() for packet in self.demux(*args, **kwargs): - for frame in packet.decode(): - yield frame + yield from packet.decode() def seek( self, diff --git a/av/container/input.pyi b/av/container/input.pyi index a5f829fef..88dd07d69 100644 --- a/av/container/input.pyi +++ b/av/container/input.pyi @@ -1,4 +1,5 @@ -from typing import Any, Iterator, overload +from collections.abc import Iterator +from typing import Any, overload from av.audio.frame import AudioFrame from av.audio.stream import AudioStream @@ -17,7 +18,6 @@ class InputContainer(Container): bit_rate: int size: int - def __enter__(self) -> InputContainer: ... @overload def demux(self, video_stream: VideoStream) -> Iterator[Packet[VideoStream]]: ... @overload diff --git a/av/container/output.py b/av/container/output.py index 239e550f2..1b8f846e9 100644 --- a/av/container/output.py +++ b/av/container/output.py @@ -496,7 +496,7 @@ def start_encoding(self): import logging log = logging.getLogger(__name__) - log.warning("Some options were not used: %s" % unused_options) + log.warning(f"Some options were not used: {unused_options}") self._myflag |= 4 diff --git a/av/container/output.pyi b/av/container/output.pyi index f7781be29..ea42b1439 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -1,5 +1,6 @@ +from collections.abc import Sequence from fractions import Fraction -from typing import Sequence, TypeVar, overload +from typing import TypeVar, overload from av.audio import _AudioCodecName from av.audio.stream import AudioStream @@ -14,7 +15,6 @@ from .core import Container _StreamT = TypeVar("_StreamT", bound=Stream) class OutputContainer(Container): - def __enter__(self) -> OutputContainer: ... @overload def add_stream( self, diff --git a/av/container/streams.py b/av/container/streams.py index e588694b4..3b1dca00b 100644 --- a/av/container/streams.py +++ b/av/container/streams.py @@ -1,4 +1,4 @@ -from typing import Iterator +from collections.abc import Iterator import cython import cython.cimports.libav as lib diff --git a/av/container/streams.pyi b/av/container/streams.pyi index 9e0df35a4..bdab1ea54 100644 --- a/av/container/streams.pyi +++ b/av/container/streams.pyi @@ -1,4 +1,5 @@ -from typing import Iterator, Literal, overload +from collections.abc import Iterator +from typing import Literal, overload from av.audio.stream import AudioStream from av.stream import AttachmentStream, DataStream, Stream diff --git a/av/datasets.py b/av/datasets.py index 5954a9c98..237b3bf04 100644 --- a/av/datasets.py +++ b/av/datasets.py @@ -2,7 +2,7 @@ import logging import os import sys -from typing import Iterator +from collections.abc import Iterator from urllib.request import urlopen log = logging.getLogger(__name__) diff --git a/av/dictionary.pyi b/av/dictionary.pyi index 7994c1306..f68003c6a 100644 --- a/av/dictionary.pyi +++ b/av/dictionary.pyi @@ -1,4 +1,4 @@ -from typing import Iterable, Iterator, Mapping +from collections.abc import Iterable, Iterator, Mapping class Dictionary: def __getitem__(self, key: str) -> str: ... diff --git a/av/filter/graph.py b/av/filter/graph.py index c1b61eb20..a7648f967 100644 --- a/av/filter/graph.py +++ b/av/filter/graph.py @@ -50,7 +50,7 @@ def _get_unique_name(self, name: str) -> str: count = self._name_counts.get(name, 0) self._name_counts[name] = count + 1 if count: - return "%s_%s" % (name, count) + return f"{name}_{count}" else: return name diff --git a/av/index.pyi b/av/index.pyi index b74262609..1ccec7a66 100644 --- a/av/index.pyi +++ b/av/index.pyi @@ -1,4 +1,5 @@ -from typing import Iterator, overload +from collections.abc import Iterator +from typing import overload class IndexEntry: pos: int diff --git a/av/logging.py b/av/logging.py index 295779444..1d484b679 100644 --- a/av/logging.py +++ b/av/logging.py @@ -288,7 +288,7 @@ def log_callback_gil( repeat_log = ( last_log[0], last_log[1], - "%s (repeated %d more times)" % (last_log[2], skip_count), + f"{last_log[2]} (repeated {skip_count} more times)", ) skip_count = 0 diff --git a/av/logging.pyi b/av/logging.pyi index 8c32de77d..6193ad7b8 100644 --- a/av/logging.pyi +++ b/av/logging.pyi @@ -1,4 +1,5 @@ -from typing import Any, Callable +from collections.abc import Callable +from typing import Any PANIC: int FATAL: int diff --git a/av/packet.py b/av/packet.py index a69cf38c1..7886b37ad 100644 --- a/av/packet.py +++ b/av/packet.py @@ -1,4 +1,5 @@ -from typing import Iterator, Literal, get_args +from collections.abc import Iterator +from typing import Literal, get_args import cython from cython.cimports import libav as lib diff --git a/av/packet.pyi b/av/packet.pyi index 3a0af39ef..805a3cf97 100644 --- a/av/packet.pyi +++ b/av/packet.pyi @@ -1,5 +1,6 @@ +from collections.abc import Iterator from fractions import Fraction -from typing import Generic, Iterator, Literal, TypeVar, overload +from typing import Generic, Literal, TypeVar, overload from av.audio.frame import AudioFrame from av.audio.stream import AudioStream diff --git a/av/sidedata/sidedata.pyi b/av/sidedata/sidedata.pyi index 0093fabd0..3f8ef398e 100644 --- a/av/sidedata/sidedata.pyi +++ b/av/sidedata/sidedata.pyi @@ -1,6 +1,6 @@ -from collections.abc import Mapping +from collections.abc import Iterator, Mapping, Sequence from enum import Enum -from typing import ClassVar, Iterator, Sequence, cast, overload +from typing import ClassVar, cast, overload from av.buffer import Buffer from av.frame import Frame diff --git a/av/subtitles/subtitle.py b/av/subtitles/subtitle.py index 5c58647a9..2d2b53df6 100644 --- a/av/subtitles/subtitle.py +++ b/av/subtitles/subtitle.py @@ -172,7 +172,7 @@ def build_subtitle(subtitle: SubtitleSet, index: cython.int) -> Subtitle: if ptr.type == lib.SUBTITLE_ASS or ptr.type == lib.SUBTITLE_TEXT: return AssSubtitle(subtitle, index) - raise ValueError("unknown subtitle type %r" % ptr.type) + raise ValueError(f"unknown subtitle type {ptr.type!r}") @cython.cclass diff --git a/av/subtitles/subtitle.pyi b/av/subtitles/subtitle.pyi index d3d4201fd..5f277eba0 100644 --- a/av/subtitles/subtitle.pyi +++ b/av/subtitles/subtitle.pyi @@ -1,4 +1,5 @@ -from typing import Iterator, Literal +from collections.abc import Iterator +from typing import Literal class SubtitleSet: format: int diff --git a/av/video/codeccontext.pyi b/av/video/codeccontext.pyi index 14c520beb..280f5cdc0 100644 --- a/av/video/codeccontext.pyi +++ b/av/video/codeccontext.pyi @@ -1,5 +1,6 @@ +from collections.abc import Iterator from fractions import Fraction -from typing import Iterator, Literal +from typing import Literal from av.codec.context import CodecContext from av.packet import Packet diff --git a/av/video/format.py b/av/video/format.py index ce2246836..654f0f7fe 100644 --- a/av/video/format.py +++ b/av/video/format.py @@ -23,7 +23,7 @@ def get_pix_fmt(name: cython.p_const_char) -> lib.AVPixelFormat: pix_fmt: lib.AVPixelFormat = lib.av_get_pix_fmt(name) if pix_fmt == lib.AV_PIX_FMT_NONE: - raise ValueError("not a pixel format: %r" % name) + raise ValueError(f"not a pixel format: {name!r}") return pix_fmt diff --git a/av/video/frame.pyi b/av/video/frame.pyi index 12a85182b..9e98a7033 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -1,6 +1,6 @@ from enum import IntEnum from pathlib import Path -from typing import Any, Union +from typing import Any import numpy as np @@ -10,12 +10,12 @@ from .format import VideoFormat from .plane import VideoPlane from .reformatter import ColorPrimaries, ColorTrc -_SupportedNDarray = Union[ - np.ndarray[Any, np.dtype[np.uint8]], - np.ndarray[Any, np.dtype[np.uint16]], - np.ndarray[Any, np.dtype[np.float16]], - np.ndarray[Any, np.dtype[np.float32]], -] +_SupportedNDarray = ( + np.ndarray[Any, np.dtype[np.uint8]] + | np.ndarray[Any, np.dtype[np.uint16]] + | np.ndarray[Any, np.dtype[np.float16]] + | np.ndarray[Any, np.dtype[np.float32]] +) supported_np_pix_fmts: set[str] diff --git a/av/video/stream.pyi b/av/video/stream.pyi index 4e2a61e46..e6797f413 100644 --- a/av/video/stream.pyi +++ b/av/video/stream.pyi @@ -1,6 +1,6 @@ -from collections.abc import Sequence +from collections.abc import Iterator, Sequence from fractions import Fraction -from typing import Iterator, Literal +from typing import Literal from av.codec.context import ThreadType from av.packet import Packet diff --git a/examples/basics/parse.py b/examples/basics/parse.py index f4baaecb7..5d052449f 100644 --- a/examples/basics/parse.py +++ b/examples/basics/parse.py @@ -31,7 +31,7 @@ chunk = fh.read(1 << 16) packets = codec.parse(chunk) - print("Parsed {} packets from {} bytes:".format(len(packets), len(chunk))) + print(f"Parsed {len(packets)} packets from {len(chunk)} bytes:") for packet in packets: print(" ", packet) diff --git a/examples/basics/thread_type.py b/examples/basics/thread_type.py index 966a8c8c0..fab539b26 100644 --- a/examples/basics/thread_type.py +++ b/examples/basics/thread_type.py @@ -38,5 +38,5 @@ container.close() -print("Decoded with default threading in {:.2f}s.".format(default_time)) -print("Decoded with auto threading in {:.2f}s.".format(auto_time)) +print(f"Decoded with default threading in {default_time:.2f}s.") +print(f"Decoded with auto threading in {auto_time:.2f}s.") diff --git a/pyproject.toml b/pyproject.toml index ce542dc46..54fb5c736 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ authors = [ {name = "WyattBlue", email = "wyattblue@auto-editor.com"}, {name = "Jeremy Lainé", email = "jeremy.laine@m4x.org"}, ] -requires-python = ">=3.10" +requires-python = ">=3.11" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", @@ -20,7 +20,6 @@ classifiers = [ "Operating System :: Unix", "Operating System :: Microsoft :: Windows", "Programming Language :: Cython", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", diff --git a/setup.py b/setup.py index 3a307c4c6..4d3475cfe 100644 --- a/setup.py +++ b/setup.py @@ -94,7 +94,7 @@ def get_config_from_pkg_config(): known, unknown = parse_cflags(raw_cflags.decode("utf-8")) if unknown: - print("pkg-config returned flags we don't understand: {}".format(unknown)) + print(f"pkg-config returned flags we don't understand: {unknown}") if "-pthread" in unknown: print("Building PyAV against static FFmpeg libraries is not supported.") exit(1) diff --git a/tests/common.py b/tests/common.py index d0cdac2e0..19984c77c 100644 --- a/tests/common.py +++ b/tests/common.py @@ -20,7 +20,8 @@ has_pillow = False if TYPE_CHECKING: - from typing import Any, Callable, TypeVar + from collections.abc import Callable + from typing import Any, TypeVar from PIL.Image import Image @@ -105,11 +106,7 @@ def assertNdarraysEqual(a: np.ndarray, b: np.ndarray) -> None: msg = "" for equal in it: if not equal: - msg += "- arrays differ at index {}; {} {}\n".format( - it.multi_index, - a[it.multi_index], - b[it.multi_index], - ) + msg += f"- arrays differ at index {it.multi_index}; {a[it.multi_index]} {b[it.multi_index]}\n" assert False, f"ndarrays contents differ\n{msg}" diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 1f9dc2ee0..dcd212b9e 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -1,8 +1,9 @@ from __future__ import annotations import os +from collections.abc import Iterator from fractions import Fraction -from typing import Iterator, TypedDict, overload +from typing import TypedDict, overload import pytest @@ -273,12 +274,8 @@ def image_sequence_encode(self, codec_name: str) -> None: new_packet = new_packets[0] path = self.sandboxed( - "%s/encoder.%04d.%s" - % ( - codec_name, - frame_count, - codec_name if codec_name != "mjpeg" else "jpg", - ) + f"{codec_name}/encoder.{frame_count:04d}." + f"{codec_name if codec_name != 'mjpeg' else 'jpg'}" ) path_list.append(path) with open(path, "wb") as f: diff --git a/tests/test_subtitles.py b/tests/test_subtitles.py index dd13d7756..c3d462b59 100644 --- a/tests/test_subtitles.py +++ b/tests/test_subtitles.py @@ -123,11 +123,11 @@ def test_subtitle_dialogue_extended_chars(self) -> None: """Test handling of extended UTF-8 characters in subtitle dialogue.""" from av.subtitles.subtitle import SubtitleSet - text = "0,0,Default,,0,0,0,,♪ Hey, hey, hey ♪".encode("utf-8") + text = "0,0,Default,,0,0,0,,♪ Hey, hey, hey ♪".encode() subtitle = SubtitleSet.create(text=text, start=0, end=2000, pts=0) sub = cast(AssSubtitle, subtitle[0]) - assert sub.dialogue == "♪ Hey, hey, hey ♪".encode("utf-8") + assert sub.dialogue == "♪ Hey, hey, hey ♪".encode() def test_subtitle_encode_mp4(self) -> None: """Test encoding subtitles to MP4 container.""" From 56e2f882310fa0d2a70fa13a5513777d8c1671b1 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 11 Jun 2026 16:58:56 -0400 Subject: [PATCH 785/846] Test with 8.0.2 instead of 8.0.1 --- .github/workflows/smoke.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index b4962872f..067947b31 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -35,11 +35,11 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-24.04, python: "3.12", ffmpeg: "8.0.1", extras: true} - - {os: ubuntu-24.04, python: "3.11", ffmpeg: "8.0.1"} - - {os: ubuntu-24.04, python: "3.13", ffmpeg: "8.1"} - - {os: macos-14, python: "3.11", ffmpeg: "8.0.1"} - - {os: macos-14, python: "3.14", ffmpeg: "8.1"} + - {os: ubuntu-24.04, python: "3.14", ffmpeg: "8.1.1", extras: true} + - {os: ubuntu-24.04, python: "3.11", ffmpeg: "8.0.2"} + - {os: ubuntu-24.04, python: "3.13", ffmpeg: "8.1.1"} + - {os: macos-14, python: "3.12", ffmpeg: "8.0.2"} + - {os: macos-14, python: "3.14", ffmpeg: "8.1.1"} env: PYAV_PYTHON: python${{ matrix.config.python }} From 09413b2d6b5a37b603817744ded8f58359306c36 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 13 Jun 2026 19:23:48 -0400 Subject: [PATCH 786/846] Support yuv420p10le with ndarray conversions Add yuv420p10le to VideoFrame.to_ndarray and from_ndarray, reading the planes as uint16 in the same flat layout used by 8-bit yuv420p. closes #1981 --- CHANGELOG.rst | 1 + av/video/frame.py | 24 ++++++++++++++++++++++++ tests/test_videoframe.py | 17 +++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a18a60f07..67e265cf6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -38,6 +38,7 @@ Breaking: Features: - Add ``options`` parameter to ``AudioResampler`` for passing ``libswresample`` options (e.g. ``resampler``, ``filter_size``, ``cutoff``) by :gh-user:`WyattBlue` (:issue:`2262`). +- Support ``yuv420p10le`` in ``VideoFrame.to_ndarray`` and ``VideoFrame.from_ndarray`` by :gh-user:`WyattBlue` (:issue:`1981`). Fixes: diff --git a/av/video/frame.py b/av/video/frame.py index 1a7b5364d..0ae5ade9d 100644 --- a/av/video/frame.py +++ b/av/video/frame.py @@ -254,6 +254,7 @@ def _numpy_avbuffer_free( "rgbf32be", "rgbf32le", "yuv420p", + "yuv420p10le", "yuv422p10le", "yuv444p", "yuv444p16be", @@ -812,6 +813,16 @@ def to_ndarray(self, channel_last=False, **kwargs): useful_array(planes[2]).reshape(-1), ] ).reshape(-1, frame.ptr.width) + if format_name == "yuv420p10le": + assert frame.ptr.width % 2 == 0, "width has to be even for this format" + assert frame.ptr.height % 2 == 0, "height has to be even for this format" + return np.hstack( + [ + useful_array(planes[0], 2, "uint16").reshape(-1), + useful_array(planes[1], 2, "uint16").reshape(-1), + useful_array(planes[2], 2, "uint16").reshape(-1), + ] + ).reshape(-1, frame.ptr.width) if format_name == "yuv422p10le": assert frame.ptr.width % 2 == 0, "width has to be even for this format" assert frame.ptr.height % 2 == 0, "height has to be even for this format" @@ -1268,6 +1279,19 @@ def from_ndarray(array, format="rgb24", channel_last=False): copy_array_to_plane(flat[u_start:v_start], frame.planes[1], 1) copy_array_to_plane(flat[v_start:], frame.planes[2], 1) return frame + elif format == "yuv420p10le": + check_ndarray(array, "uint16", 2) + check_ndarray_shape(array, array.shape[0] % 3 == 0) + check_ndarray_shape(array, array.shape[1] % 2 == 0) + + frame = VideoFrame(array.shape[1], (array.shape[0] * 2) // 3, format) + u_start = frame.width * frame.height + v_start = 5 * u_start // 4 + flat = array.reshape(-1) + copy_array_to_plane(flat[0:u_start], frame.planes[0], 2) + copy_array_to_plane(flat[u_start:v_start], frame.planes[1], 2) + copy_array_to_plane(flat[v_start:], frame.planes[2], 2) + return frame elif format == "yuv422p": check_ndarray(array, "uint8", 2) check_ndarray_shape(array, array.shape[0] % 4 == 0) diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 26386adb0..ed2b27fd4 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -688,6 +688,23 @@ def test_ndarray_yuv420p() -> None: assertNdarraysEqual(frame.to_ndarray(), array) +def test_ndarray_yuv420p10le() -> None: + array = numpy.random.randint(0, 1024, size=(720, 640), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="yuv420p10le") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "yuv420p10le" + assert "yuv420p10le" in supported_np_pix_fmts + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_yuv420p10le_align() -> None: + array = numpy.random.randint(0, 1024, size=(357, 318), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="yuv420p10le") + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == "yuv420p10le" + assertNdarraysEqual(frame.to_ndarray(), array) + + def test_ndarray_yuv422p() -> None: array = numpy.random.randint(0, 256, size=(960, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuv422p") From 97cccc81f0f76a6fc649ea4d173e451f61d06e0c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 13 Jun 2026 19:30:50 -0400 Subject: [PATCH 787/846] Use cdivision and handle zero-size frames in ndarray conversions Add @cython.cdivision(True) to VideoFrame.to_ndarray/from_ndarray and guard the planar YUV paths against zero width/height frames, which have no allocated planes and previously raised a cryptic IndexError. --- av/video/frame.py | 31 +++++++++++++++++-------------- tests/test_videoframe.py | 16 ++++++++++++++++ 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/av/video/frame.py b/av/video/frame.py index 0ae5ade9d..e530f9026 100644 --- a/av/video/frame.py +++ b/av/video/frame.py @@ -737,6 +737,7 @@ def to_image(self, **kwargs): "RGB", (plane.width, plane.height), bytes(o_buf), "raw", "RGB", 0, 1 ) + @cython.cdivision(True) def to_ndarray(self, channel_last=False, **kwargs): """Get a numpy array of this frame. @@ -803,24 +804,19 @@ def to_ndarray(self, channel_last=False, **kwargs): return array # special cases - if format_name in {"yuv420p", "yuvj420p", "yuv422p"}: + if format_name in {"yuv420p", "yuvj420p", "yuv422p", "yuv420p10le"}: assert frame.ptr.width % 2 == 0, "width has to be even for this format" assert frame.ptr.height % 2 == 0, "height has to be even for this format" + is_10bit: cython.bint = format_name == "yuv420p10le" + itemsize = 2 if is_10bit else 1 + dtype = "uint16" if is_10bit else "uint8" + if frame.ptr.width == 0 or frame.ptr.height == 0: + return np.empty((0, frame.ptr.width), dtype=dtype) return np.hstack( [ - useful_array(planes[0]).reshape(-1), - useful_array(planes[1]).reshape(-1), - useful_array(planes[2]).reshape(-1), - ] - ).reshape(-1, frame.ptr.width) - if format_name == "yuv420p10le": - assert frame.ptr.width % 2 == 0, "width has to be even for this format" - assert frame.ptr.height % 2 == 0, "height has to be even for this format" - return np.hstack( - [ - useful_array(planes[0], 2, "uint16").reshape(-1), - useful_array(planes[1], 2, "uint16").reshape(-1), - useful_array(planes[2], 2, "uint16").reshape(-1), + useful_array(planes[0], itemsize, dtype).reshape(-1), + useful_array(planes[1], itemsize, dtype).reshape(-1), + useful_array(planes[2], itemsize, dtype).reshape(-1), ] ).reshape(-1, frame.ptr.width) if format_name == "yuv422p10le": @@ -1154,6 +1150,7 @@ def _image_fill_pointers_numpy(self, buffer, width, height, linesizes, format): self._init_user_attributes() @staticmethod + @cython.cdivision(True) def from_ndarray(array, format="rgb24", channel_last=False): """ Construct a frame from a numpy array. @@ -1272,6 +1269,8 @@ def from_ndarray(array, format="rgb24", channel_last=False): check_ndarray_shape(array, array.shape[1] % 2 == 0) frame = VideoFrame(array.shape[1], (array.shape[0] * 2) // 3, format) + if frame.width == 0 or frame.height == 0: + return frame u_start = frame.width * frame.height v_start = 5 * u_start // 4 flat = array.reshape(-1) @@ -1285,6 +1284,8 @@ def from_ndarray(array, format="rgb24", channel_last=False): check_ndarray_shape(array, array.shape[1] % 2 == 0) frame = VideoFrame(array.shape[1], (array.shape[0] * 2) // 3, format) + if frame.width == 0 or frame.height == 0: + return frame u_start = frame.width * frame.height v_start = 5 * u_start // 4 flat = array.reshape(-1) @@ -1298,6 +1299,8 @@ def from_ndarray(array, format="rgb24", channel_last=False): check_ndarray_shape(array, array.shape[1] % 2 == 0) frame = VideoFrame(array.shape[1], array.shape[0] // 2, format) + if frame.width == 0 or frame.height == 0: + return frame u_start = frame.width * frame.height v_start = u_start + u_start // 2 flat = array.reshape(-1) diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index ed2b27fd4..826a13c5d 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -705,6 +705,22 @@ def test_ndarray_yuv420p10le_align() -> None: assertNdarraysEqual(frame.to_ndarray(), array) +def test_ndarray_yuv420p10le_zero_size() -> None: + # A frame with zero width and/or height has no allocated planes; conversions + # should degrade gracefully instead of raising IndexError. + for w, h in ((0, 0), (0, 480), (640, 0)): + frame = VideoFrame(w, h, "yuv420p10le") + array = frame.to_ndarray() + assert array.dtype == numpy.uint16 + assert array.size == 0 + + empty = numpy.empty((0, 0), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(empty, format="yuv420p10le") + assert frame.width == 0 and frame.height == 0 + assert frame.format.name == "yuv420p10le" + assertNdarraysEqual(frame.to_ndarray(), empty) + + def test_ndarray_yuv422p() -> None: array = numpy.random.randint(0, 256, size=(960, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuv422p") From db104c80d66977beb9db9d584fe6bc469ad6beb9 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 13 Jun 2026 19:56:52 -0400 Subject: [PATCH 788/846] Keep decoded frames when a packet ends with undecodable bytes A single packet may contain a valid frame followed by undecodable bytes FFmpeg yields the good frame and only reports the error on the next receive, but decode() raised InvalidDataError before returning, discarding frames it had already decoded. Catch the error in the receive loop and return what was decoded, only re-raising when no frame was produced at all. Fixes #2044 --- av/codec/context.py | 14 +++++-- tests/test_codec_context.py | 74 +++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 3 deletions(-) diff --git a/av/codec/context.py b/av/codec/context.py index 2c82f2de8..9b51c1430 100644 --- a/av/codec/context.py +++ b/av/codec/context.py @@ -12,6 +12,8 @@ from cython.cimports.libc.stdint import uint8_t from cython.cimports.libc.string import memcpy +from av.error import InvalidDataError + _cinit_sentinel = cython.declare(object, object()) @@ -498,11 +500,17 @@ def decode(self, packet: Packet | None = None): err_check(res, "avcodec_send_packet()") out: list = [] - frame = self._recv_frame() - while frame: + while True: + try: + frame = self._recv_frame() + except InvalidDataError: + if out: + break + raise + if frame is None: + break self._setup_decoded_frame(frame, packet) out.append(frame) - frame = self._recv_frame() return out @cython.ccall diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index dcd212b9e..164ce73fb 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -13,6 +13,7 @@ AudioFrame, AudioLayout, AudioResampler, + AudioStream, Codec, Packet, VideoCodecContext, @@ -184,6 +185,79 @@ def test_bits_per_coded_sample(self): with pytest.raises(ValueError): stream.codec_context.bits_per_coded_sample = 32 + def test_decode_keeps_frames_before_error(self) -> None: + # Regression test for #2044: a single packet may contain a valid frame + # followed by undecodable bytes (e.g. a truncated final FLAC frame). + # FFmpeg yields the good frame and only reports the error on the next + # receive, so decode() must return what it already decoded instead of + # raising and discarding it. + import io + + import numpy as np + + # Build an in-memory FLAC stream with several independent frames. + buf = io.BytesIO() + with av.open(buf, "w", format="flac") as output: + stream = output.add_stream("flac", rate=44100) + assert isinstance(stream, AudioStream) + stream.format = "s16" + stream.layout = "mono" + n = 0 + for _ in range(8): + samples = 4096 + t = (np.arange(n, n + samples) / 44100).astype(np.float32) + sig = (np.sin(2 * np.pi * 440 * t) * 16000).astype(np.int16) + frame = AudioFrame.from_ndarray( + sig.reshape(1, -1), format="s16", layout="mono" + ) + frame.rate = 44100 + frame.pts = n + for packet in stream.encode(frame): + output.mux(packet) + n += samples + for packet in stream.encode(None): + output.mux(packet) + + # Re-read the raw (parser-split) frame packets and decoder extradata. + buf.seek(0) + with av.open(buf, "r") as container: + audio = container.streams.audio[0] + extradata = audio.codec_context.extradata + packets = [bytes(p) for p in container.demux(audio) if p.size] + + assert len(packets) >= 3 + + def make_ctx() -> AudioCodecContext: + ctx = Codec("flac", "r").create("audio") + assert isinstance(ctx, AudioCodecContext) + ctx.extradata = extradata + return ctx + + # A leading frame that decodes cleanly on its own. + good_frames = make_ctx().decode(Packet(packets[0])) + good_samples = sum(f.samples for f in good_frames) + assert good_samples > 0 + + # Trailing bytes that are undecodable on their own. + corrupt: bytes | None = None + for raw in packets[1:]: + chunk = raw[: len(raw) // 2] + try: + make_ctx().decode(Packet(chunk)) + except av.error.InvalidDataError: + corrupt = chunk + break + assert corrupt is not None, "could not construct an undecodable chunk" + + # [valid frame][undecodable bytes] in one packet must still yield the + # valid frame rather than raising and dropping everything. + frames = make_ctx().decode(Packet(packets[0] + corrupt)) + assert sum(f.samples for f in frames) >= good_samples + + # A packet that is *only* undecodable bytes still raises. + with pytest.raises(av.error.InvalidDataError): + make_ctx().decode(Packet(corrupt)) + def test_parse(self) -> None: # This one parses into a single packet. self._assert_parse("mpeg4", fate_suite("h264/interlaced_crop.mp4")) From cecb3ec41b29d1c9991271425c765fe8c8598da9 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 13 Jun 2026 20:14:36 -0400 Subject: [PATCH 789/846] Support DLPack export for any CPU VideoPlane VideoPlane.__dlpack__() only handled nv12/p010le/p016le and raised NotImplementedError otherwise, so common CPU formats (packed RGB, planar YUV, gray, ...) needed a NumPy intermediate to export. Add a generic CPU path that describes the plane from its pixel-format descriptor: single-component planes export as 2D (H, W), planes that interleave components as 3D (H, W, C). This reproduces the existing nv12/p010le/p016le layouts exactly. Bitstream, palette, Bayer, big-endian 16-bit, and sub-byte packings are rejected; CUDA export is unchanged. Fixes #2217 --- av/video/plane.py | 70 +++++++++++++++++++++++++++++++++++++++++++- tests/test_dlpack.py | 43 +++++++++++++++++++++++++-- 2 files changed, 109 insertions(+), 4 deletions(-) diff --git a/av/video/plane.py b/av/video/plane.py index 15fe70a94..c6d3c095c 100644 --- a/av/video/plane.py +++ b/av/video/plane.py @@ -186,8 +186,76 @@ def __dlpack__(self, *, stream: int | None = None): st2 = 1 else: raise ValueError("invalid plane index for P010/P016") - else: + elif device_type != kCPU: raise NotImplementedError("unsupported sw_format for DLPack export") + else: + # Generic CPU export. Describe the plane straight from its + # pixel-format descriptor: planes holding a single component (the + # Y/U/V planes of planar YUV, gray, ...) become 2D (H, W), while + # planes that interleave several components (packed RGB, the chroma + # plane of NV12, ...) become 3D (H, W, C). + desc: cython.pointer[lib.AVPixFmtDescriptor] = lib.av_pix_fmt_desc_get( + sw_fmt + ) + if desc == cython.NULL: + raise NotImplementedError("unknown pixel format for DLPack export") + if desc.flags & ( + lib.AV_PIX_FMT_FLAG_BITSTREAM + | lib.AV_PIX_FMT_FLAG_PAL + | lib.AV_PIX_FMT_FLAG_BAYER + ): + raise NotImplementedError( + "bitstream, palette, and Bayer formats are not supported for " + "DLPack export" + ) + + step_bytes: cython.int = 0 + ncomp: cython.int = 0 + i: cython.int + for i in range(desc.nb_components): + if desc.comp[i].plane != self.index: + continue + if ncomp == 0: + step_bytes = desc.comp[i].step + elif cython.cast(cython.int, desc.comp[i].step) != step_bytes: + raise NotImplementedError( + "mixed component step is not supported for DLPack export" + ) + ncomp += 1 + + if ncomp == 0: + raise ValueError(f"plane {self.index} has no components") + if step_bytes % ncomp: + raise NotImplementedError( + "unsupported component packing for DLPack export" + ) + itemsize = step_bytes // ncomp + if itemsize != 1 and itemsize != 2: + raise NotImplementedError( + "only 8- and 16-bit components are supported for DLPack export" + ) + if itemsize == 2 and desc.flags & lib.AV_PIX_FMT_FLAG_BE: + raise NotImplementedError( + "big-endian formats are not supported for DLPack export" + ) + bits = itemsize * 8 + if line_size % itemsize: + raise ValueError("linesize is not aligned to dtype") + + if ncomp == 1: + ndim = 2 + s0 = self.height + s1 = self.width + st0 = line_size // itemsize + st1 = 1 + else: + ndim = 3 + s0 = self.height + s1 = self.width + s2 = ncomp + st0 = line_size // itemsize + st1 = ncomp + st2 = 1 frame_ref: cython.pointer[lib.AVFrame] = lib.av_frame_alloc() if frame_ref == cython.NULL: diff --git a/tests/test_dlpack.py b/tests/test_dlpack.py index e017affd2..003e696f5 100644 --- a/tests/test_dlpack.py +++ b/tests/test_dlpack.py @@ -235,13 +235,50 @@ def test_video_plane_dlpack_export_keeps_frame_alive_after_gc() -> None: assertNdarraysEqual(y_dl, expected) -def test_video_plane_dlpack_unsupported_format_raises() -> None: - rgb = numpy.zeros((16, 16, 3), dtype=numpy.uint8) +def test_video_plane_dlpack_export_packed_rgb_cpu() -> None: + # Packed formats interleave several components in one plane and export as + # a 3D (H, W, C) tensor (issue #2217). + rgb = (numpy.arange(16 * 24 * 3) % 251).astype(numpy.uint8).reshape(16, 24, 3) frame = VideoFrame.from_ndarray(rgb, format="rgb24") + plane = frame.planes[0] + assert plane.__dlpack_device__() == (1, 0) + + arr = numpy.from_dlpack(plane) + assert arr.shape == (16, 24, 3) + assert arr.strides == (plane.line_size, 3, 1) + assert arr.dtype == numpy.uint8 + assertNdarraysEqual(arr, rgb) + + +def test_video_plane_dlpack_export_planar_yuv_cpu() -> None: + # Planar formats expose each single-component plane as a 2D (H, W) tensor + # (issue #2217). + frame = VideoFrame(16, 16, "yuv420p") + for index, (h, w) in enumerate([(16, 16), (8, 8), (8, 8)]): + plane = frame.planes[index] + assert plane.__dlpack_device__() == (1, 0) + arr = numpy.from_dlpack(plane) + assert arr.shape == (h, w) + assert arr.strides == (plane.line_size, 1) + assert arr.dtype == numpy.uint8 + + +def test_video_plane_dlpack_export_planar_yuv16_cpu() -> None: + # 16-bit planar formats export as uint16. + frame = VideoFrame(16, 16, "yuv420p10le") + arr = numpy.from_dlpack(frame.planes[0]) + assert arr.shape == (16, 16) + assert arr.dtype == numpy.uint16 + + +def test_video_plane_dlpack_unsupported_format_raises() -> None: + # Palette formats still cannot be exported. + frame = VideoFrame(16, 16, "pal8") assert frame.planes[0].__dlpack_device__() == (1, 0) with pytest.raises( - NotImplementedError, match="unsupported sw_format for DLPack export" + NotImplementedError, + match="bitstream, palette, and Bayer formats are not supported", ): frame.planes[0].__dlpack__() From 107817d197ef64b3fe4073ebcdf9240d44c6e789 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 13 Jun 2026 20:22:11 -0400 Subject: [PATCH 790/846] Support DLPack import for planar formats like yuv420p VideoFrame.from_dlpack() still only accepted the 2-plane nv12 family, so a decoded yuv420p frame could not be rebuilt from the plane tensors the previous commit can now export. Add a zero-copy CPU path for planar formats whose planes each hold a single component: yuv420p/yuv422p/yuv444p, gray, gbrp, and their 16-bit little-endian variants. Plane count, per-plane shape, and chroma subsampling come from the pixel-format descriptor. Continues #2217 --- av/video/frame.py | 163 +++++++++++++++++++++++++++++++++++++++++-- av/video/plane.py | 4 +- tests/test_dlpack.py | 101 +++++++++++++++++++++++++-- 3 files changed, 255 insertions(+), 13 deletions(-) diff --git a/av/video/frame.py b/av/video/frame.py index e530f9026..78b97a550 100644 --- a/av/video/frame.py +++ b/av/video/frame.py @@ -1459,18 +1459,20 @@ def from_dlpack( if not isinstance(planes, (tuple, list)): planes = (planes,) - if len(planes) != 2: - raise ValueError( - "from_dlpack currently supports 2-plane formats only (nv12/p010le/p016le)" - ) - sw_fmt: lib.AVPixelFormat = get_pix_fmt(format) nv12 = get_pix_fmt(b"nv12") p010le = get_pix_fmt(b"p010le") p016le = get_pix_fmt(b"p016le") if sw_fmt not in (nv12, p010le, p016le): - raise NotImplementedError("from_dlpack supports nv12, p010le, p016le only") + return VideoFrame._from_dlpack_planar( + planes, sw_fmt, format, width, height, stream, device_id + ) + + if len(planes) != 2: + raise ValueError( + "from_dlpack currently supports 2-plane formats only (nv12/p010le/p016le)" + ) expected_bits = 8 if sw_fmt == nv12 else 16 itemsize = 1 if expected_bits == 8 else 2 @@ -1656,3 +1658,152 @@ def from_dlpack( if m1 != cython.NULL: m1.deleter(m1) raise + + @staticmethod + def _from_dlpack_planar(planes, sw_fmt, format, width, height, stream, device_id): + # CPU-only import for planar formats whose planes each hold a single + # component: yuv420p/yuv422p/yuv444p, gray, gbrp, and their 16-bit + # little-endian variants. nv12/p010le/p016le keep their dedicated + # 2-plane path in from_dlpack(). + desc: cython.pointer[cython.const[lib.AVPixFmtDescriptor]] = ( + lib.av_pix_fmt_desc_get(sw_fmt) + ) + if desc == cython.NULL: + raise NotImplementedError(f"unknown pixel format {format!r}") + if desc.flags & ( + lib.AV_PIX_FMT_FLAG_BITSTREAM + | lib.AV_PIX_FMT_FLAG_PAL + | lib.AV_PIX_FMT_FLAG_BAYER + ): + raise NotImplementedError( + f"from_dlpack does not support bitstream, palette, or Bayer " + f"formats ({format!r})" + ) + + i: cython.int + nb_planes: cython.int = 0 + for i in range(desc.nb_components): + if cython.cast(cython.int, desc.comp[i].plane) + 1 > nb_planes: + nb_planes = desc.comp[i].plane + 1 + + p: cython.int + count: cython.int + comp_of_plane = [0] * nb_planes + for p in range(nb_planes): + count = 0 + for i in range(desc.nb_components): + if cython.cast(cython.int, desc.comp[i].plane) == p: + comp_of_plane[p] = i + count += 1 + if count != 1: + raise NotImplementedError( + "from_dlpack supports nv12/p010le/p016le and planar " + f"single-component formats; {format!r} is not supported" + ) + + if len(planes) != nb_planes: + raise ValueError( + f"{format!r} requires {nb_planes} plane(s), got {len(planes)}" + ) + + itemsize: cython.int = desc.comp[0].step + if itemsize != 1 and itemsize != 2: + raise NotImplementedError( + "only 8- and 16-bit components are supported for DLPack import" + ) + if itemsize == 2 and desc.flags & lib.AV_PIX_FMT_FLAG_BE: + raise NotImplementedError( + "big-endian formats are not supported for DLPack import" + ) + expected_bits: cython.int = itemsize * 8 + + if device_id not in (None, 0): + raise ValueError("device_id must be 0 for CPU tensors") + + log2_w: cython.int = desc.log2_chroma_w + log2_h: cython.int = desc.log2_chroma_h + + frame: VideoFrame = None + m: cython.pointer[DLManagedTensor] = cython.NULL + try: + frame = alloc_video_frame() + frame.ptr.format = sw_fmt + + for p in range(nb_planes): + m = _consume_dlpack(planes[p], stream) + + if m.dl_tensor.device_type != kCPU: + raise NotImplementedError( + "only CPU DLPack tensors are supported for this format" + ) + if m.dl_tensor.device_id != 0: + raise ValueError("CPU DLPack tensors must have device_id == 0") + + if ( + m.dl_tensor.dtype.code != 1 + or m.dl_tensor.dtype.bits != expected_bits + or m.dl_tensor.dtype.lanes != 1 + ): + raise TypeError(f"unexpected dtype for plane {p}") + + if m.dl_tensor.ndim != 2: + raise ValueError(f"plane {p} must be 2D (H, W)") + + ph = cython.cast(int64_t, m.dl_tensor.shape[0]) + pw = cython.cast(int64_t, m.dl_tensor.shape[1]) + + if p == 0: + if width == 0 and height == 0: + width = cython.cast(int, pw) + height = cython.cast(int, ph) + elif width == 0 or height == 0: + raise ValueError("either specify both width/height or neither") + elif pw != width or ph != height: + raise ValueError("plane 0 shape does not match width/height") + if (log2_w and width % 2) or (log2_h and height % 2): + raise ValueError(f"width/height must be even for {format!r}") + frame.ptr.width = width + frame.ptr.height = height + + comp_idx = comp_of_plane[p] + is_chroma = (comp_idx == 1 or comp_idx == 2) and (log2_w or log2_h) + exp_w = (-((-width) >> log2_w)) if is_chroma else width + exp_h = (-((-height) >> log2_h)) if is_chroma else height + + if pw != exp_w or ph != exp_h: + raise ValueError(f"plane {p} must have shape ({exp_h}, {exp_w})") + + if m.dl_tensor.strides != cython.NULL: + if m.dl_tensor.strides[1] != 1: + raise ValueError( + f"plane {p} must be contiguous in the last dimension" + ) + pitch_elems = cython.cast(int64_t, m.dl_tensor.strides[0]) + else: + pitch_elems = cython.cast(int64_t, exp_w) + + linesize = cython.cast(int, pitch_elems * itemsize) + size = cython.cast(int, linesize * exp_h) + + ptr = cython.cast( + cython.pointer[uint8_t], m.dl_tensor.data + ) + cython.cast(cython.size_t, m.dl_tensor.byte_offset) + + frame.ptr.buf[p] = lib.av_buffer_create( + ptr, size, _dlpack_avbuffer_free, cython.cast(cython.p_void, m), 0 + ) + if frame.ptr.buf[p] == cython.NULL: + raise MemoryError(f"av_buffer_create failed for plane {p}") + frame.ptr.data[p] = ptr + frame.ptr.linesize[p] = linesize + m = cython.NULL + + frame._init_user_attributes() + return frame + + except Exception: + if frame is not None: + lib.av_frame_unref(frame.ptr) + if m != cython.NULL: + m.deleter(m) + raise diff --git a/av/video/plane.py b/av/video/plane.py index c6d3c095c..51a3a13f1 100644 --- a/av/video/plane.py +++ b/av/video/plane.py @@ -194,8 +194,8 @@ def __dlpack__(self, *, stream: int | None = None): # Y/U/V planes of planar YUV, gray, ...) become 2D (H, W), while # planes that interleave several components (packed RGB, the chroma # plane of NV12, ...) become 3D (H, W, C). - desc: cython.pointer[lib.AVPixFmtDescriptor] = lib.av_pix_fmt_desc_get( - sw_fmt + desc: cython.pointer[cython.const[lib.AVPixFmtDescriptor]] = ( + lib.av_pix_fmt_desc_get(sw_fmt) ) if desc == cython.NULL: raise NotImplementedError("unknown pixel format for DLPack export") diff --git a/tests/test_dlpack.py b/tests/test_dlpack.py index 003e696f5..13e05fbbc 100644 --- a/tests/test_dlpack.py +++ b/tests/test_dlpack.py @@ -289,13 +289,104 @@ def test_video_frame_from_dlpack_requires_two_planes() -> None: VideoFrame.from_dlpack(y, format="nv12") -def test_video_frame_from_dlpack_rejects_unsupported_format() -> None: +def test_video_frame_from_dlpack_rejects_packed_format() -> None: + # Packed formats interleave several components in one plane and cannot be + # imported (only planar single-component formats and the nv12 family are). + rgb = numpy.zeros((16, 16, 3), dtype=numpy.uint8) + + with pytest.raises(NotImplementedError, match="is not supported"): + VideoFrame.from_dlpack((rgb,), format="rgb24") + + +@pytest.mark.parametrize( + "fmt,dtype,planes_hw", + [ + ("yuv420p", numpy.uint8, [(48, 64), (24, 32), (24, 32)]), + ("yuv422p", numpy.uint8, [(48, 64), (48, 32), (48, 32)]), + ("yuv444p", numpy.uint8, [(48, 64), (48, 64), (48, 64)]), + ("gray", numpy.uint8, [(48, 64)]), + ("yuv420p10le", numpy.uint16, [(48, 64), (24, 32), (24, 32)]), + ], +) +def test_video_frame_from_dlpack_planar_cpu(fmt, dtype, planes_hw) -> None: + # Issue #2217: planar formats whose planes each hold one component round + # trip through DLPack without a NumPy intermediate. width, height = 64, 48 - y = numpy.zeros((height, width), dtype=numpy.uint8) - uv = numpy.zeros((height // 2, width // 2, 2), dtype=numpy.uint8) + make = _make_u16 if dtype == numpy.uint16 else _make_u8 + src = [make((h, w)) for (h, w) in planes_hw] + + frame = VideoFrame.from_dlpack(tuple(src), format=fmt) + + assert frame.format.name == fmt + assert frame.width == width and frame.height == height + assert len(frame.planes) == len(src) + + for i, plane in enumerate(frame.planes): + arr = numpy.from_dlpack(plane) + assert arr.dtype == dtype + assert arr.shape == planes_hw[i] + assertNdarraysEqual(arr, src[i]) + + +def test_video_frame_from_dlpack_yuv420p_zero_copy_and_lifetime() -> None: + width, height = 64, 48 + y = _make_u8((height, width)) + u = _make_u8((height // 2, width // 2)) + v = _make_u8((height // 2, width // 2)) + + frame = VideoFrame.from_dlpack((y, u, v), format="yuv420p") + + # Mutating the source is visible through the frame (zero copy). + y[0, 0] = 200 + assert memoryview(frame.planes[0])[0] == 200 + + expected = [y.copy(), u.copy(), v.copy()] + del y, u, v + gc.collect() + + for i, plane in enumerate(frame.planes): + assertNdarraysEqual(numpy.from_dlpack(plane), expected[i]) + + +def test_video_frame_from_dlpack_yuv420p_with_pitch() -> None: + width, height = 64, 48 + pad = 16 + + y = _make_u8((height, width + pad))[:, :width] + u = _make_u8((height // 2, (width + pad) // 2))[:, : width // 2] + v = _make_u8((height // 2, (width + pad) // 2))[:, : width // 2] + + frame = VideoFrame.from_dlpack((y, u, v), format="yuv420p") + + assert frame.planes[0].line_size == width + pad + assert frame.planes[1].line_size == (width + pad) // 2 + assertNdarraysEqual(numpy.from_dlpack(frame.planes[0]), y) + assertNdarraysEqual(numpy.from_dlpack(frame.planes[1]), u) + assertNdarraysEqual(numpy.from_dlpack(frame.planes[2]), v) + + +def test_video_frame_from_dlpack_planar_wrong_plane_count() -> None: + y = numpy.zeros((48, 64), dtype=numpy.uint8) + u = numpy.zeros((24, 32), dtype=numpy.uint8) + + with pytest.raises(ValueError, match=r"requires 3 plane\(s\), got 2"): + VideoFrame.from_dlpack((y, u), format="yuv420p") + + +def test_video_frame_from_dlpack_planar_rejects_odd_dimensions() -> None: + y = numpy.zeros((48, 63), dtype=numpy.uint8) + u = numpy.zeros((24, 32), dtype=numpy.uint8) + v = numpy.zeros((24, 32), dtype=numpy.uint8) + + with pytest.raises(ValueError, match="must be even"): + VideoFrame.from_dlpack((y, u, v), format="yuv420p") + + +def test_video_frame_from_dlpack_planar_rejects_palette() -> None: + idx = numpy.zeros((16, 16), dtype=numpy.uint8) - with pytest.raises(NotImplementedError, match="supports nv12, p010le, p016le only"): - VideoFrame.from_dlpack((y, uv), format="yuv420p") + with pytest.raises(NotImplementedError, match="palette"): + VideoFrame.from_dlpack((idx,), format="pal8") def test_video_frame_from_dlpack_rejects_device_id_for_cpu() -> None: From b7551508a29d60a10ab08ea2cfdf318bf6918c4b Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 13 Jun 2026 22:14:29 -0400 Subject: [PATCH 791/846] Extract codec extradata in add_mux_stream for Matroska Muxing packets into Matroska failed in avformat_write_header because the muxer needs CodecPrivate (the codec extradata) before the first packet. For codec-less mux streams whose format embeds a global header, buffer packets and run the extract_extradata bitstream filter to recover the parameter sets from the bitstream, then set them on codecpar before the header is written. Streams give up after their first packet, so already length-prefixed packets fall back to the muxer's default behavior with no regression. closes #2198 --- CHANGELOG.rst | 2 +- av/container/core.pxd | 2 +- av/container/output.pxd | 6 ++ av/container/output.py | 122 ++++++++++++++++++++++++++++++++++++++++ include/avcodec.pxd | 1 + scripts/build-deps | 2 +- tests/test_remux.py | 42 ++++++++++++++ 7 files changed, 174 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 67e265cf6..3efcf11f0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -42,7 +42,7 @@ Features: Fixes: -- Nothing (yet) +- Fix ``add_mux_stream`` producing unwritable Matroska files by extracting codec extradata from the bitstream before the header is written by :gh-user:`WyattBlue` (:issue:`2198`). v17.1.0 ------- diff --git a/av/container/core.pxd b/av/container/core.pxd index 7cba26407..badd84abb 100644 --- a/av/container/core.pxd +++ b/av/container/core.pxd @@ -37,7 +37,7 @@ cdef class Container: cdef readonly dict metadata # Private API. - cdef uint8_t _myflag # enum: writeable, input_was_opened, started, done + cdef uint8_t _myflag # enum: writeable, input_was_opened, started, done, extradata_planned cdef _assert_open(self) cdef int err_check(self, int value) except -1 diff --git a/av/container/output.pxd b/av/container/output.pxd index 71edc3bf2..2ef93206f 100644 --- a/av/container/output.pxd +++ b/av/container/output.pxd @@ -1,9 +1,15 @@ cimport libav as lib from av.container.core cimport Container +from av.packet cimport Packet from av.stream cimport Stream cdef class OutputContainer(Container): cdef lib.AVPacket *packet_ptr + cdef dict _extradata_bsfs + cdef list _buffered_packets + cdef _mux_one(self, Packet packet) + cdef _buffer_for_extradata(self, Packet packet) + cdef _try_extract_extradata(self, Packet packet) cpdef start_encoding(self) diff --git a/av/container/output.py b/av/container/output.py index 1b8f846e9..8c56ef080 100644 --- a/av/container/output.py +++ b/av/container/output.py @@ -2,6 +2,7 @@ import cython from cython.cimports import libav as lib +from cython.cimports.av.bitstream import BitStreamFilterContext from cython.cimports.av.codec.codec import Codec from cython.cimports.av.codec.context import CodecContext, wrap_codec_context from cython.cimports.av.container.streams import StreamContainer @@ -10,10 +11,39 @@ from cython.cimports.av.packet import Packet from cython.cimports.av.stream import Stream, wrap_stream from cython.cimports.av.utils import dict_to_avdict, to_avrational +from cython.cimports.libc.stdint import uint8_t +from cython.cimports.libc.string import memcpy, memset + + +@cython.cfunc +def _set_codecpar_extradata( + stream: cython.pointer[lib.AVStream], + data: cython.pointer[uint8_t], + size: cython.int, +): + buf: cython.p_uchar = cython.cast( + cython.p_uchar, lib.av_malloc(size + lib.AV_INPUT_BUFFER_PADDING_SIZE) + ) + if buf == cython.NULL: + raise MemoryError("Could not allocate extradata") + + memcpy(buf, data, size) + memset(buf + size, 0, lib.AV_INPUT_BUFFER_PADDING_SIZE) + + lib.av_freep(cython.address(stream.codecpar.extradata)) + stream.codecpar.extradata = buf + stream.codecpar.extradata_size = size @cython.cfunc def close_output(self: OutputContainer): + if self.packet_ptr != cython.NULL and self._buffered_packets: + buffered: list = self._buffered_packets + self._buffered_packets = [] + packet: Packet + for packet in buffered: + self._mux_one(packet) + self.streams = StreamContainer() if self._myflag & 12 == 4: # enum.started and not enum.done # If the underlying Python IO file was already closed (e.g. during GC @@ -38,6 +68,8 @@ class OutputContainer(Container): def __cinit__(self, *args, **kwargs): self.streams = StreamContainer() self.metadata = {} + self._extradata_bsfs = {} + self._buffered_packets = [] with cython.nogil: self.packet_ptr = lib.av_packet_alloc() @@ -558,6 +590,13 @@ def mux(self, packets): self.mux_one(packet) def mux_one(self, packet: Packet): + if not (self._myflag & 4) and self._buffer_for_extradata(packet): + return + + self._mux_one(packet) + + @cython.cfunc + def _mux_one(self, packet: Packet): self.start_encoding() # Assert the packet is in stream time. @@ -577,3 +616,86 @@ def mux_one(self, packet: Packet): with cython.nogil: ret: cython.int = lib.av_interleaved_write_frame(self.ptr, self.packet_ptr) self.err_check(ret) + + @cython.cfunc + def _buffer_for_extradata(self, packet: Packet): + """Buffer ``packet`` until extradata is known for all mux streams that + need it. Returns True if the packet was buffered (caller should stop).""" + if not (self._myflag & 16): # extradata_planned + self._myflag |= 16 + if self.ptr.oformat.flags & lib.AVFMT_GLOBALHEADER: + stream: Stream + for stream in self.streams: + if ( + stream.codec_context is not None + or stream.ptr.codecpar.extradata != cython.NULL + ): + continue + try: + bsf = BitStreamFilterContext( + "extract_extradata", in_stream=stream + ) + except Exception: + continue # Codec does not support extradata extraction. + self._extradata_bsfs[stream.ptr.index] = bsf + + if not self._extradata_bsfs: + return False # Nothing to wait for; mux normally. + + self._try_extract_extradata(packet) + self._buffered_packets.append(packet) + if self._extradata_bsfs: + return True # Still waiting on some stream's extradata. + + # All extradata is resolved: write the header and flush buffered packets. + buffered: list = self._buffered_packets + self._buffered_packets = [] + buffered_packet: Packet + for buffered_packet in buffered: + self._mux_one(buffered_packet) + return True + + @cython.cfunc + def _try_extract_extradata(self, packet: Packet): + idx: cython.int = packet.ptr.stream_index + if idx not in self._extradata_bsfs: + return + + bsf_wrapper: BitStreamFilterContext = self._extradata_bsfs[idx] + bsf: cython.pointer[lib.AVBSFContext] = bsf_wrapper.ptr + + tmp: cython.pointer[lib.AVPacket] = lib.av_packet_alloc() + if tmp == cython.NULL: + raise MemoryError("Could not allocate packet") + + size: cython.size_t = 0 + sd: cython.pointer[uint8_t] + try: + # Clone the packet so the filter does not consume the caller's data. + if lib.av_packet_ref(tmp, packet.ptr) < 0: + return + + if lib.av_bsf_send_packet(bsf, tmp) < 0: + lib.av_packet_unref(tmp) # send failed; we still own the ref + return + + # The filter rejects packets that are already length-prefixed rather + # than annex-b, returning an error here; treat that and EOF/EAGAIN + # alike as "no in-band extradata". + while lib.av_bsf_receive_packet(bsf, tmp) == 0: + sd = lib.av_packet_get_side_data( + tmp, lib.AV_PKT_DATA_NEW_EXTRADATA, cython.address(size) + ) + if sd != cython.NULL and size > 0: + _set_codecpar_extradata( + self.ptr.streams[idx], sd, cython.cast(cython.int, size) + ) + lib.av_packet_unref(tmp) + break + lib.av_packet_unref(tmp) + finally: + lib.av_packet_free(cython.address(tmp)) + # A stream's first packet is the only reliable place to find in-band + # parameter sets, so stop waiting on this stream regardless of the + # result, falling back to the muxer's default behavior. + del self._extradata_bsfs[idx] diff --git a/include/avcodec.pxd b/include/avcodec.pxd index 5d13abed1..534f1ae1b 100644 --- a/include/avcodec.pxd +++ b/include/avcodec.pxd @@ -298,6 +298,7 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef char* avcodec_get_name(AVCodecID id) cdef int avcodec_open2(AVCodecContext *ctx, const AVCodec *codec, AVDictionary **options) cdef enum AVPacketSideDataType: + AV_PKT_DATA_NEW_EXTRADATA AV_PKT_DATA_DISPLAYMATRIX cdef struct AVPacketSideData: uint8_t *data diff --git a/scripts/build-deps b/scripts/build-deps index d89b1e406..ee90471c1 100755 --- a/scripts/build-deps +++ b/scripts/build-deps @@ -67,7 +67,7 @@ echo ./configure --enable-debug=3 \ --enable-libx264 \ --disable-bsfs \ - --enable-bsf=chomp,h264_mp4toannexb,setts \ + --enable-bsf=chomp,extract_extradata,h264_mp4toannexb,setts \ --enable-sse \ --enable-avx \ --enable-avx2 \ diff --git a/tests/test_remux.py b/tests/test_remux.py index 85f00563b..c5045e979 100644 --- a/tests/test_remux.py +++ b/tests/test_remux.py @@ -1,4 +1,5 @@ import io +from fractions import Fraction import numpy as np import pytest @@ -87,6 +88,47 @@ def test_add_mux_stream_no_codec_context() -> None: assert "audio/" in repr(audio_stream) +def test_add_mux_stream_matroska_extradata() -> None: + """Regression test for #2198. + + Muxing pre-encoded H.264 packets into Matroska used to fail in + ``avformat_write_header`` because the muxer needs ``CodecPrivate`` (the codec + extradata) before the first packet is written. When the packets are annex-b + with in-band parameter sets but the stream has no extradata, + ``add_mux_stream`` extracts it from the bitstream so the file is written and + stays decodable. + """ + if av.codec.Codec("h264", "w").name != "libx264": + pytest.skip("requires libx264") + + # Encode without a global header, so the packets carry annex-b parameter + # sets in-band and the codec context exposes no extradata to copy. + cc = av.CodecContext.create("libx264", "w") + cc.width, cc.height, cc.pix_fmt = 320, 240, "yuv420p" + cc.time_base = Fraction(1, 24) + cc.framerate = Fraction(24, 1) + + packets = [] + for i in range(24): + frame = av.VideoFrame.from_ndarray( + np.zeros((240, 320, 3), dtype="uint8"), format="rgb24" + ) + frame.pts = i + frame.time_base = Fraction(1, 24) + packets.extend(cc.encode(frame)) + packets.extend(cc.encode(None)) + assert cc.extradata is None # nothing to copy onto the stream + + buf = io.BytesIO() + with av.open(buf, "w", format="matroska") as output: + out_stream = output.add_mux_stream("libx264", rate=24, width=320, height=240) + for packet in packets: + packet.stream = out_stream + output.mux(packet) + + assert _decoded_frame_count(buf) == 24 + + def test_add_stream_from_template_copies_time_base() -> None: """add_stream_from_template must propagate the source stream's time_base. From 5b48f2d4357e1b208ce6956959079c81d78d8991 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 13 Jun 2026 23:01:27 -0400 Subject: [PATCH 792/846] Build native-arch extension in macOS smoke job actions/setup-python provides the python.org universal2 CPython, so setuptools compiled the extensions for arm64 and x86_64. Also stop installing libvorbis. --- .github/workflows/smoke.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 067947b31..17c24fded 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -67,7 +67,6 @@ jobs: cmake \ libjpeg-dev \ libtool \ - libvorbis-dev \ libx264-dev \ nasm \ pkg-config \ @@ -77,7 +76,7 @@ jobs: fi ;; macos-14) - brew install automake libtool libpng libvorbis libvpx opus x264 + brew install automake libtool libpng libvpx opus x264 ;; esac @@ -88,6 +87,7 @@ jobs: - name: Build run: | + [ "${{ runner.os }}" = "macOS" ] && export ARCHFLAGS="-arch $(uname -m)" . scripts/activate.sh ffmpeg-${{ matrix.config.ffmpeg }} scripts/build From f16cd610f779f6c916c15278c042f8b9bae7f586 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 13 Jun 2026 23:34:58 -0400 Subject: [PATCH 793/846] Disable most filters for smoke builds --- .github/workflows/smoke.yml | 3 +-- scripts/build-deps | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 17c24fded..78d80a072 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -65,7 +65,6 @@ jobs: automake \ build-essential \ cmake \ - libjpeg-dev \ libtool \ libx264-dev \ nasm \ @@ -76,7 +75,7 @@ jobs: fi ;; macos-14) - brew install automake libtool libpng libvpx opus x264 + brew install automake libtool opus x264 ;; esac diff --git a/scripts/build-deps b/scripts/build-deps index ee90471c1..c6ab3d73f 100755 --- a/scripts/build-deps +++ b/scripts/build-deps @@ -68,6 +68,8 @@ echo ./configure --enable-libx264 \ --disable-bsfs \ --enable-bsf=chomp,extract_extradata,h264_mp4toannexb,setts \ + --disable-filters \ + --enable-filter=abuffer,abuffersink,aformat,aresample,atempo,buffer,buffersink,bwdif,color,lutrgb,palettegen,scale,testsrc,vflip,volume \ --enable-sse \ --enable-avx \ --enable-avx2 \ From baebfed6ac8ce6726cfba236f90930ab2892b69c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 13 Jun 2026 23:53:08 -0400 Subject: [PATCH 794/846] Enable loudnorm filter in smoke builds and harden its impl test_streams.py::test_loudnorm calls av.filter.loudnorm, so add loudnorm to the trimmed smoke-build filter list; without it the filter lookup returned NULL. That NULL also exposed a crash: loudnorm_get_stats() built an abuffer -> loudnorm -> abuffersink graph but ignored every return code, so a NULL loudnorm context was passed to avfilter_link() and segfaulted the interpreter. Bail out early if a required filter is missing, check the create_filter/link/graph_config return codes so a NULL context can never be linked, free the graph on the error path, and skip the 5s JSON wait when no graph ran. On failure the function returns NULL, which the Python wrapper already turns into a RuntimeError. --- av/filter/loudnorm_impl.c | 110 +++++++++++++++++++++++--------------- scripts/build-deps | 2 +- 2 files changed, 69 insertions(+), 43 deletions(-) diff --git a/av/filter/loudnorm_impl.c b/av/filter/loudnorm_impl.c index c4e182981..19cb1c554 100644 --- a/av/filter/loudnorm_impl.c +++ b/av/filter/loudnorm_impl.c @@ -57,6 +57,20 @@ char* loudnorm_get_stats( const char* loudnorm_args ) { char* result = NULL; + + // Bail out cleanly if FFmpeg was built without a filter we depend on, + // instead of dereferencing a NULL filter context further down. The caller + // turns a NULL return into a Python exception. + if (!avfilter_get_by_name("abuffer") || + !avfilter_get_by_name("abuffersink") || + !avfilter_get_by_name("loudnorm")) { + av_log(NULL, AV_LOG_ERROR, + "loudnorm: a required filter (abuffer/abuffersink/loudnorm) is not " + "available in this FFmpeg build\n"); + avformat_close_input(&fmt_ctx); + return NULL; + } + json_captured = 0; // Reset the captured flag memset(json_buffer, 0, sizeof(json_buffer)); // Clear the buffer @@ -76,7 +90,10 @@ char* loudnorm_get_stats( AVCodec *codec = NULL; AVCodecContext *codec_ctx = NULL; + AVPacket *packet = NULL; + AVFrame *frame = NULL, *filt_frame = NULL; int ret; + int graph_configured = 0; AVCodecParameters *codecpar = fmt_ctx->streams[audio_stream_index]->codecpar; codec = (AVCodec *)avcodec_find_decoder(codecpar->codec_id); @@ -98,20 +115,25 @@ char* loudnorm_get_stats( av_get_sample_fmt_name(codec_ctx->sample_fmt), ch_layout_str); - avfilter_graph_create_filter(&src_ctx, avfilter_get_by_name("abuffer"), - "src", args, NULL, filter_graph); - avfilter_graph_create_filter(&sink_ctx, avfilter_get_by_name("abuffersink"), - "sink", NULL, NULL, filter_graph); - avfilter_graph_create_filter(&loudnorm_ctx, avfilter_get_by_name("loudnorm"), - "loudnorm", loudnorm_args, NULL, filter_graph); + if (avfilter_graph_create_filter(&src_ctx, avfilter_get_by_name("abuffer"), + "src", args, NULL, filter_graph) < 0 || + avfilter_graph_create_filter(&sink_ctx, avfilter_get_by_name("abuffersink"), + "sink", NULL, NULL, filter_graph) < 0 || + avfilter_graph_create_filter(&loudnorm_ctx, avfilter_get_by_name("loudnorm"), + "loudnorm", loudnorm_args, NULL, filter_graph) < 0) { + goto end; + } - avfilter_link(src_ctx, 0, loudnorm_ctx, 0); - avfilter_link(loudnorm_ctx, 0, sink_ctx, 0); - avfilter_graph_config(filter_graph, NULL); + if (avfilter_link(src_ctx, 0, loudnorm_ctx, 0) < 0 || + avfilter_link(loudnorm_ctx, 0, sink_ctx, 0) < 0 || + avfilter_graph_config(filter_graph, NULL) < 0) { + goto end; + } + graph_configured = 1; - AVPacket *packet = av_packet_alloc(); - AVFrame *frame = av_frame_alloc(); - AVFrame *filt_frame = av_frame_alloc(); + packet = av_packet_alloc(); + frame = av_frame_alloc(); + filt_frame = av_frame_alloc(); while ((ret = av_read_frame(fmt_ctx, packet)) >= 0) { if (packet->stream_index != audio_stream_index) { @@ -157,46 +179,50 @@ char* loudnorm_get_stats( av_frame_unref(filt_frame); } - // Pushes graph - avfilter_graph_free(&filter_graph); - end: + // Freeing the graph uninits the loudnorm filter, which is what makes it + // emit its JSON stats through our log callback. Safe to call on NULL. + avfilter_graph_free(&filter_graph); avcodec_free_context(&codec_ctx); avformat_close_input(&fmt_ctx); av_frame_free(&filt_frame); av_frame_free(&frame); av_packet_free(&packet); - #ifdef _WIN32 - EnterCriticalSection(&json_mutex); - while (!json_captured) { - if (!SleepConditionVariableCS(&json_cond, &json_mutex, 5000)) { // 5 second timeout - fprintf(stderr, "Timeout waiting for JSON data\n"); - break; + // If the graph never configured we produced no stats; don't block waiting + // for JSON that will never arrive. Leave result NULL so the caller raises. + if (graph_configured) { + #ifdef _WIN32 + EnterCriticalSection(&json_mutex); + while (!json_captured) { + if (!SleepConditionVariableCS(&json_cond, &json_mutex, 5000)) { // 5 second timeout + fprintf(stderr, "Timeout waiting for JSON data\n"); + break; + } } - } - if (json_captured) { - result = _strdup(json_buffer); // Use _strdup on Windows - } - LeaveCriticalSection(&json_mutex); - #else - struct timespec timeout; - clock_gettime(CLOCK_REALTIME, &timeout); - timeout.tv_sec += 5; // 5 second timeout - - pthread_mutex_lock(&json_mutex); - while (json_captured == 0) { - int ret = pthread_cond_timedwait(&json_cond, &json_mutex, &timeout); - if (ret == ETIMEDOUT) { - fprintf(stderr, "Timeout waiting for JSON data\n"); - break; + if (json_captured) { + result = _strdup(json_buffer); // Use _strdup on Windows } + LeaveCriticalSection(&json_mutex); + #else + struct timespec timeout; + clock_gettime(CLOCK_REALTIME, &timeout); + timeout.tv_sec += 5; // 5 second timeout + + pthread_mutex_lock(&json_mutex); + while (json_captured == 0) { + int ret = pthread_cond_timedwait(&json_cond, &json_mutex, &timeout); + if (ret == ETIMEDOUT) { + fprintf(stderr, "Timeout waiting for JSON data\n"); + break; + } + } + if (json_captured) { + result = strdup(json_buffer); + } + pthread_mutex_unlock(&json_mutex); + #endif } - if (json_captured) { - result = strdup(json_buffer); - } - pthread_mutex_unlock(&json_mutex); - #endif av_log_set_callback(av_log_default_callback); return result; diff --git a/scripts/build-deps b/scripts/build-deps index c6ab3d73f..4449f2bc3 100755 --- a/scripts/build-deps +++ b/scripts/build-deps @@ -69,7 +69,7 @@ echo ./configure --disable-bsfs \ --enable-bsf=chomp,extract_extradata,h264_mp4toannexb,setts \ --disable-filters \ - --enable-filter=abuffer,abuffersink,aformat,aresample,atempo,buffer,buffersink,bwdif,color,lutrgb,palettegen,scale,testsrc,vflip,volume \ + --enable-filter=abuffer,abuffersink,aformat,aresample,atempo,buffer,buffersink,bwdif,color,loudnorm,lutrgb,palettegen,scale,testsrc,vflip,volume \ --enable-sse \ --enable-avx \ --enable-avx2 \ From 07b5e98d2d16af9c50e87d17d7c573fa9b45c6ed Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 15 Jun 2026 07:32:15 -0400 Subject: [PATCH 795/846] Add more interpolation flags closes #2307 --- av/video/reformatter.pxd | 7 +++++++ av/video/reformatter.py | 15 ++++++++++++--- av/video/reformatter.pyi | 15 +++++++++++---- tests/test_videoframe.py | 7 +++++++ 4 files changed, 37 insertions(+), 7 deletions(-) diff --git a/av/video/reformatter.pxd b/av/video/reformatter.pxd index e68a70105..fe4b68db5 100644 --- a/av/video/reformatter.pxd +++ b/av/video/reformatter.pxd @@ -19,6 +19,13 @@ cdef extern from "libswscale/swscale.h" nogil: cdef int SWS_SINC cdef int SWS_LANCZOS cdef int SWS_SPLINE + cdef int SWS_PRINT_INFO + cdef int SWS_FULL_CHR_H_INT + cdef int SWS_FULL_CHR_H_INP + cdef int SWS_DIRECT_BGR + cdef int SWS_ACCURATE_RND + cdef int SWS_BITEXACT + cdef int SWS_ERROR_DIFFUSION cdef int SWS_CS_ITU709 cdef int SWS_CS_FCC cdef int SWS_CS_ITU601 diff --git a/av/video/reformatter.py b/av/video/reformatter.py index 5778711d8..fbbef01b9 100644 --- a/av/video/reformatter.py +++ b/av/video/reformatter.py @@ -1,4 +1,4 @@ -from enum import IntEnum +from enum import IntEnum, IntFlag import cython import cython.cimports.libav as lib @@ -7,7 +7,7 @@ from cython.cimports.av.video.frame import alloc_video_frame -class Interpolation(IntEnum): +class Interpolation(IntFlag): FAST_BILINEAR: "Fast bilinear" = SWS_FAST_BILINEAR BILINEAR: "Bilinear" = SWS_BILINEAR BICUBIC: "2-tap cubic B-spline" = SWS_BICUBIC @@ -19,6 +19,13 @@ class Interpolation(IntEnum): SINC: "Unwindowed Sinc" = SWS_SINC LANCZOS: "3-tap sinc/sinc" = SWS_LANCZOS SPLINE: "Unwindowed natural cubic spline" = SWS_SPLINE + PRINT_INFO: "Emit verbose scaler info to the log" = SWS_PRINT_INFO + FULL_CHR_H_INT: "Full chroma interpolation" = SWS_FULL_CHR_H_INT + FULL_CHR_H_INP: "Full chroma input" = SWS_FULL_CHR_H_INP + DIRECT_BGR: "Direct BGR" = SWS_DIRECT_BGR + ACCURATE_RND: "Accurate rounding" = SWS_ACCURATE_RND + BITEXACT: "Bit-exact output" = SWS_BITEXACT + ERROR_DIFFUSION: "Error diffusion dither" = SWS_ERROR_DIFFUSION class Colorspace(IntEnum): @@ -182,7 +189,9 @@ def reformat( :type src_colorspace: :class:`Colorspace` or ``str`` :param dst_colorspace: Desired colorspace, or ``None`` for the frame colorspace. :type dst_colorspace: :class:`Colorspace` or ``str`` - :param interpolation: The interpolation method to use, or ``None`` for ``BILINEAR``. + :param interpolation: The scaling algorithm to use, or ``None`` for ``BILINEAR``. + Option flags such as ``ACCURATE_RND`` or ``BITEXACT`` may be combined with + the algorithm using ``|``, e.g. ``Interpolation.BILINEAR | Interpolation.ACCURATE_RND``. :type interpolation: :class:`Interpolation` or ``str`` :param src_color_range: Current color range, or ``None`` for the ``UNSPECIFIED``. :type src_color_range: :class:`ColorRange` or ``str`` diff --git a/av/video/reformatter.pyi b/av/video/reformatter.pyi index c9071df49..30508a32b 100644 --- a/av/video/reformatter.pyi +++ b/av/video/reformatter.pyi @@ -1,10 +1,10 @@ -from enum import IntEnum +from enum import IntEnum, IntFlag from typing import cast from .frame import VideoFrame -class Interpolation(IntEnum): - FAST_BILINEAER = cast(int, ...) +class Interpolation(IntFlag): + FAST_BILINEAR = cast(int, ...) BILINEAR = cast(int, ...) BICUBIC = cast(int, ...) X = cast(int, ...) @@ -15,6 +15,13 @@ class Interpolation(IntEnum): SINC = cast(int, ...) LANCZOS = cast(int, ...) SPLINE = cast(int, ...) + PRINT_INFO = cast(int, ...) + FULL_CHR_H_INT = cast(int, ...) + FULL_CHR_H_INP = cast(int, ...) + DIRECT_BGR = cast(int, ...) + ACCURATE_RND = cast(int, ...) + BITEXACT = cast(int, ...) + ERROR_DIFFUSION = cast(int, ...) class Colorspace(IntEnum): ITU709 = cast(int, ...) @@ -80,7 +87,7 @@ class VideoReformatter: format: str | None = None, src_colorspace: int | None = None, dst_colorspace: int | None = None, - interpolation: int | str | None = None, + interpolation: Interpolation | int | str | None = None, src_color_range: int | str | None = None, dst_color_range: int | str | None = None, dst_color_trc: int | ColorTrc | None = None, diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 826a13c5d..acd3f53ab 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -155,6 +155,13 @@ def test_interpolation() -> None: ) assert img.width == 200 and img.height == 100 + # Option flags can be combined with the scaling algorithm. + combined = Interpolation.BILINEAR | Interpolation.ACCURATE_RND + assert isinstance(combined, Interpolation) + assert combined & Interpolation.ACCURATE_RND + img = frame.reformat(width=200, height=100, interpolation=combined) + assert img.width == 200 and img.height == 100 + def test_basic_to_ndarray() -> None: array = VideoFrame(640, 480, "rgb24").to_ndarray() From b8ab4cc10834f44229e340712877b1e5111136c1 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 15 Jun 2026 15:06:11 -0400 Subject: [PATCH 796/846] pyav.basswood-io.com => pyav.basswood.io --- CHANGELOG.rst | 2 +- README.md | 4 ++-- docs/Makefile | 2 +- pyproject.toml | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3efcf11f0..5b9530be4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -174,4 +174,4 @@ Fixes: .. or see older git commits -`15.X Changelog ` +`15.X Changelog ` diff --git a/README.md b/README.md index 59e291c74..9ca80440d 100644 --- a/README.md +++ b/README.md @@ -78,8 +78,8 @@ Have fun, [read the docs][docs], [come chat with us][discuss], and good luck! [conda-badge]: https://img.shields.io/conda/vn/conda-forge/av.svg?colorB=CCB39A [conda]: https://anaconda.org/conda-forge/av -[docs-badge]: https://img.shields.io/badge/docs-on%20pyav.basswood--io.com-blue.svg -[docs]: https://pyav.basswood-io.com +[docs-badge]: https://img.shields.io/badge/docs-on%20pyav.basswood.io-blue.svg +[docs]: https://pyav.basswood.io [pypi-badge]: https://img.shields.io/pypi/v/av.svg?colorB=CCB39A [pypi]: https://pypi.org/project/av [discuss]: https://github.com/PyAV-Org/PyAV/discussions diff --git a/docs/Makefile b/docs/Makefile index 6654f1062..e12e6f701 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -28,5 +28,5 @@ open: open _build/html/index.html upload: - rsync -avxP --delete _build/html/ root@basswood-io.com:/var/www/pyav/docs/develop + rsync -avxP --delete _build/html/ root@basswood.io:/var/www/pyav/docs/develop diff --git a/pyproject.toml b/pyproject.toml index 54fb5c736..035e4b489 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,7 @@ version = {attr = "av.about.__version__"} [project.urls] "Bug Tracker" = "https://github.com/PyAV-Org/PyAV/issues" "Source Code" = "https://github.com/PyAV-Org/PyAV" -homepage = "https://pyav.basswood-io.com" +homepage = "https://pyav.basswood.io" [project.scripts] "pyav" = "av.__main__:main" From 1566a4bdc465440025623d770a9208d41bebbaf1 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 15 Jun 2026 15:52:12 -0400 Subject: [PATCH 797/846] Use utf8 c_string_encoding Switch the Cython c_string_encoding directive from "ascii" to "utf8" so implicit char* <-> str conversions (e.g. av.Dictionary options, FFmpeg error strings) no longer raise UnicodeDecode/EncodeError on non-ASCII data. --- setup.py | 2 +- tests/test_dictionary.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4d3475cfe..2cd23d5a8 100644 --- a/setup.py +++ b/setup.py @@ -163,7 +163,7 @@ def parse_cflags(raw_flags): compiler_directives = { "c_string_type": "str", - "c_string_encoding": "ascii", + "c_string_encoding": "utf8", "embedsignature": True, "binding": False, "language_level": 3, diff --git a/tests/test_dictionary.py b/tests/test_dictionary.py index e577c7a07..14e698d73 100644 --- a/tests/test_dictionary.py +++ b/tests/test_dictionary.py @@ -15,3 +15,12 @@ def test_dictionary() -> None: assert d.pop("key") == "value" pytest.raises(KeyError, d.pop, "key") assert len(d) == 0 + + +def test_dictionary_non_ascii() -> None: + d = Dictionary() + d["café"] = "naïve 日本語 🎵" + + assert d["café"] == "naïve 日本語 🎵" + assert "café" in d + assert list(d) == ["café"] From ff7f6bd5b959ace03b23d20525db3d47c1272064 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 15 Jun 2026 16:31:51 -0400 Subject: [PATCH 798/846] Cross-compile armv7l wheels with zig instead of QEMU armv7l was the only target built under QEMU emulation. Replace it with a native x86_64 job that cross-compiles with zig: target Python trees are copied out of the manylinux/musllinux armv7l images via docker cp (no emulation), and CPython's cross-build env vars produce correct abi3 and free-threaded wheel tags. FFmpeg is still fetched prebuilt for armv7l. --- .github/workflows/tests.yml | 43 ++++++++--- scripts/auditwheel-cross.py | 28 ++++++++ scripts/build-armv7l-cross.sh | 130 ++++++++++++++++++++++++++++++++++ scripts/fetch-vendor.py | 6 ++ 4 files changed, 198 insertions(+), 9 deletions(-) create mode 100644 scripts/auditwheel-cross.py create mode 100755 scripts/build-armv7l-cross.sh diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9f20ee93f..b0be4a542 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -34,8 +34,6 @@ jobs: arch: x86_64 - os: ubuntu-24.04-arm arch: aarch64 - - os: ubuntu-24.04 - arch: armv7l - os: ubuntu-24.04 arch: x86_64 - os: windows-latest @@ -47,11 +45,6 @@ jobs: - uses: actions/setup-python@v6 with: python-version: "3.14" - - name: Set up QEMU - if: matrix.arch == 'armv7l' - uses: docker/setup-qemu-action@v3 - with: - platforms: arm - name: Set Minimum MacOS Target if: runner.os == 'macOS' run: | @@ -73,7 +66,6 @@ jobs: CIBW_BUILD: "cp311* cp314t*" CIBW_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m pytest {package}/tests && mv {project}/av.disabled {project}/av CIBW_TEST_REQUIRES: pytest numpy - CIBW_TEST_SKIP: "*_armv7l" run: | pip install cibuildwheel delvewheel cibuildwheel --output-dir dist @@ -84,9 +76,42 @@ jobs: name: dist-${{ matrix.os }}-${{ matrix.arch }} path: dist/ + # armv7l (32-bit ARM) is cross-compiled with zig on a native x86_64 runner + # instead of emulated with QEMU. cibuildwheel cannot cross-compile, so this + # target uses its own driver (scripts/build-armv7l-cross.sh). + package-wheel-armv7l: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v6 + - name: Set up host Python 3.11 + uses: actions/setup-python@v6 + with: + python-version: "3.11" + - name: Set up host Python 3.14t + uses: actions/setup-python@v6 + with: + python-version: "3.14t" + - name: Install build tooling + run: | + for py in python3.11 python3.14t; do + $py -m pip install -U "cython>=3.1.0,<4" "setuptools>=77.0" wheel + done + python3.11 -m pip install -U ziglang auditwheel + - name: Cross-compile armv7l wheels with zig + env: + HOST_PY311: python3.11 + HOST_PY314T: python3.14t + TOOLS_PY: python3.11 + run: bash scripts/build-armv7l-cross.sh + - name: Upload wheels + uses: actions/upload-artifact@v6 + with: + name: dist-armv7l + path: dist/ + publish: runs-on: ubuntu-latest - needs: [package-source, package-wheel] + needs: [package-source, package-wheel, package-wheel-armv7l] steps: - uses: actions/checkout@v6 - uses: actions/download-artifact@v7 diff --git a/scripts/auditwheel-cross.py b/scripts/auditwheel-cross.py new file mode 100644 index 000000000..13c529dc7 --- /dev/null +++ b/scripts/auditwheel-cross.py @@ -0,0 +1,28 @@ +"""Run ``auditwheel`` for a foreign architecture/libc from an x86_64 host. + +auditwheel builds its ``--plat`` choices from the host's detected architecture +and libc, so on an x86_64 runner it rejects e.g. ``manylinux_2_31_armv7l``. The +repair logic itself re-derives the architecture and libc from the wheel and works +cross-arch, so we only need to override the host detection used to build the CLI +choice list. + +Usage:: + + python auditwheel-cross.py +""" + +import sys + +from auditwheel.architecture import Architecture +from auditwheel.libc import Libc + +_arch = Architecture(sys.argv[1]) +_libc = {"glibc": Libc.GLIBC, "musl": Libc.MUSL}[sys.argv[2]] +del sys.argv[1:3] + +Architecture.detect = staticmethod(lambda *, bits=None: _arch) +Libc.detect = staticmethod(lambda: _libc) + +from auditwheel.main import main # noqa: E402 (import after patching detection) + +sys.exit(main()) diff --git a/scripts/build-armv7l-cross.sh b/scripts/build-armv7l-cross.sh new file mode 100755 index 000000000..69eb3bfea --- /dev/null +++ b/scripts/build-armv7l-cross.sh @@ -0,0 +1,130 @@ +#! /usr/bin/env bash +# +# Cross-compile PyAV's armv7l (32-bit ARM) wheels with zig on an x86_64 host, +# without QEMU. Only PyAV's Cython->C extensions are compiled; FFmpeg is fetched +# prebuilt for armv7l. zig is the cross compiler, and CPython's cross-build env +# vars (_PYTHON_HOST_PLATFORM / _PYTHON_SYSCONFIGDATA_NAME) make sysconfig report +# armv7l without running any armv7l code. The target Python trees + system libs +# are copied out of the manylinux armv7l image with `docker cp` (no execution). +# +# Builds two manylinux (glibc) wheels: cp311-abi3 (covers 3.11-3.13) and cp314t. +# musllinux armv7l is skipped: the musl FFmpeg needs system libs (libdrm, +# libxcb*, libbz2) the musllinux image doesn't ship for auditwheel to bundle. + +set -euo pipefail + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +root="$(cd "$here/.." && pwd)" +cd "$root" + +# Host interpreters that drive each build (must match the wheel's Python: 3.11 +# emits cp311-abi3, 3.14t emits cp314t). zig/auditwheel run from TOOLS_PY. +HOST_PY311="${HOST_PY311:-python3.11}" +HOST_PY314T="${HOST_PY314T:-python3.14t}" +TOOLS_PY="${TOOLS_PY:-python3}" + +DIST_DIR="${DIST_DIR:-$root/dist}" +WORK_DIR="${WORK_DIR:-$root/build/armv7l-cross}" +IMAGE="quay.io/pypa/manylinux_2_31_armv7l" +TRIPLE="arm-linux-gnueabihf.2.31" # pin glibc to the manylinux_2_31 policy +PLAT="manylinux_2_31_armv7l" +FFMPEG_CONFIG="scripts/ffmpeg-latest.json" + +PY_DIR="$WORK_DIR/py" +BIN_DIR="$WORK_DIR/bin" +mkdir -p "$DIST_DIR" "$BIN_DIR" "$PY_DIR" + +# --- zig compiler wrappers ------------------------------------------------ +# setuptools invokes $CC / $LDSHARED as plain commands. Resolve the zig binary +# once and call it directly (not "python -m ziglang"): the build sets PYTHONPATH +# to the target stdlib, which would crash a per-compile Python. The wrappers also +# drop a few gcc-only flags clang (zig) rejects. +ZIG_BIN="$("$TOOLS_PY" -c 'import os, ziglang; print(os.path.join(os.path.dirname(ziglang.__file__), "zig"))')" + +write_cc() { + cat >"$1" <"$BIN_DIR/zig-ar" +chmod +x "$BIN_DIR/zig-ar" + +# --- copy the target Python trees + system libs out of the image ---------- +extract_image() { + if [[ -d "$PY_DIR/_internal" ]]; then + return # already extracted + fi + echo "Pulling $IMAGE" + docker pull --platform linux/arm/v7 "$IMAGE" + local cid + cid="$(docker create --platform linux/arm/v7 "$IMAGE")" + # /opt/python/ are symlinks into /opt/_internal/; copy both. Also + # grab the armhf system libs (libxcb, ...) FFmpeg needs so auditwheel can + # bundle them. docker cp reads the filesystem; it runs no armv7l code. + docker cp "$cid:/opt/python" "$PY_DIR" + docker cp "$cid:/opt/_internal" "$PY_DIR" + docker cp "$cid:/usr/lib/arm-linux-gnueabihf" "$PY_DIR/syslib" + docker rm -f "$cid" >/dev/null +} + +# --- build + repair one wheel --------------------------------------------- +# args: +build_one() { + local host_py="$1" py_glob="$2" + + # Resolve /opt/python/ symlink to its real tree under /opt/_internal. + local link + link="$(find "$PY_DIR/python" -maxdepth 1 -name "$py_glob" | head -1)" + [[ -n "$link" ]] || { echo "no python matching '$py_glob':" >&2; ls "$PY_DIR/python" >&2; exit 1; } + local pytree="$PY_DIR/_internal/$(basename "$(readlink "$link")")" + + local inc=( "$pytree"/include/python3.* ) + local scd=( "$pytree"/lib/python3.*/_sysconfigdata_*.py ) + echo "=== building for $(basename "$pytree") (host $host_py) ===" + + # FFmpeg's pkg-config files bake in prefix=/tmp/vendor, so extract there. + rm -rf /tmp/vendor + PYAV_VENDOR_PLATFORM=manylinux-armv7l "$TOOLS_PY" scripts/fetch-vendor.py \ + --config-file "$FFMPEG_CONFIG" /tmp/vendor + + local raw="$WORK_DIR/raw/$(basename "$pytree")" + rm -rf "$raw"; mkdir -p "$raw" + + # The _PYTHON_* vars make sysconfig report armv7l; zig does the compiling. + env \ + CC="$BIN_DIR/zig-cc" CXX="$BIN_DIR/zig-cxx" AR="$BIN_DIR/zig-ar" \ + LDSHARED="$BIN_DIR/zig-cc -shared" \ + _PYTHON_HOST_PLATFORM=linux-armv7l \ + _PYTHON_SYSCONFIGDATA_NAME="$(basename "${scd[0]}" .py)" \ + PYTHONPATH="$(dirname "${scd[0]}")" \ + CFLAGS="-I${inc[0]} -Wno-error=incompatible-pointer-types" \ + PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig \ + LD_LIBRARY_PATH=/tmp/vendor/lib \ + "$host_py" -m pip wheel . --no-build-isolation --no-deps -w "$raw" + + # Bundle FFmpeg + its armhf system deps and stamp the platform tag. The shim + # lets auditwheel accept the armv7l --plat from an x86_64 host; --ldpaths + # points it at the target's libs instead of the host search path. + "$TOOLS_PY" scripts/auditwheel-cross.py armv7l glibc repair \ + --ldpaths "/tmp/vendor/lib:$PY_DIR/syslib" \ + --plat "$PLAT" -w "$DIST_DIR" "$raw"/*.whl +} + +extract_image +build_one "$HOST_PY311" "cp311-cp311" +build_one "$HOST_PY314T" "cp314*-cp314t" + +echo +echo "Built armv7l wheels:" +ls -1 "$DIST_DIR"/*armv7l*.whl diff --git a/scripts/fetch-vendor.py b/scripts/fetch-vendor.py index 7161a0023..b046c1884 100644 --- a/scripts/fetch-vendor.py +++ b/scripts/fetch-vendor.py @@ -8,6 +8,12 @@ def get_platform(): + # Allow forcing the target platform so we can fetch e.g. an armv7l build + # while running on an x86_64 host (cross-compilation). + forced = os.environ.get("PYAV_VENDOR_PLATFORM") + if forced: + return forced + system = platform.system() machine = platform.machine().lower() is_arm64 = machine in {"arm64", "aarch64"} From a3424b7126f326304ada96cdfaff0f5d9191e957 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 15 Jun 2026 20:46:30 -0400 Subject: [PATCH 799/846] Fix armv7l wheels and add armv7 smoke test (#2313) The cross-built armv7l wheels were broken on real hardware. Fixes: - Align extensions to the 4K page size (-Wl,-z,max-page-size=4096); lld's default 64K alignment with 4K file offsets is rejected by the loader. - Pin patchelf <0.18 for auditwheel (ubuntu-24.04 ships 0.18.0, which corrupts ELF files with large p_align, breaking the bundled FFmpeg libs). - Build extensions as optimized release (-O2 -DNDEBUG); the CFLAGS override had dropped these, tripping a free-threaded Py_SET_REFCNT assertion. - Add an armv7 smoke job: cross-build the wheels, then import them under QEMU. Validated on a real BeagleBone (armv7, Debian 13). --- .github/workflows/smoke.yml | 42 +++++++++++++++++++++++++++++++++++ .github/workflows/tests.yml | 4 +++- scripts/build-armv7l-cross.sh | 18 +++++++++++---- 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 78d80a072..5d1efbaa1 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -157,3 +157,45 @@ jobs: . $CONDA/etc/profile.d/conda.sh conda activate pyav python -m pytest + + armv7: + name: "armv7 cross-build + qemu smoke" + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v6 + - name: Set up host Python 3.11 + uses: actions/setup-python@v6 + with: + python-version: "3.11" + - name: Set up host Python 3.14t + uses: actions/setup-python@v6 + with: + python-version: "3.14t" + - name: Install build tooling + run: | + for py in python3.11 python3.14t; do + $py -m pip install -U "cython>=3.1.0,<4" "setuptools>=77.0" wheel + done + # patchelf 0.18 (ubuntu-24.04's system version) corrupts ELF files + # with large p_align; pin <0.18 for auditwheel to use. + python3.11 -m pip install -U ziglang auditwheel 'patchelf<0.18' + - name: Cross-compile armv7l wheels with zig + env: + HOST_PY311: python3.11 + HOST_PY314T: python3.14t + TOOLS_PY: python3.11 + run: bash scripts/build-armv7l-cross.sh + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: arm + - name: Import-test wheels under emulation + run: | + docker run --rm --platform linux/arm/v7 -v "$PWD/dist:/dist:ro" \ + quay.io/pypa/manylinux_2_31_armv7l bash -ceu ' + for py in /opt/python/cp311-cp311 /opt/python/cp314-cp314t; do + echo "::group::$("$py/bin/python" --version)" + "$py/bin/pip" install --no-index --find-links /dist av + "$py/bin/python" -m av --version + echo "::endgroup::" + done' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b0be4a542..030cddb90 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -96,7 +96,9 @@ jobs: for py in python3.11 python3.14t; do $py -m pip install -U "cython>=3.1.0,<4" "setuptools>=77.0" wheel done - python3.11 -m pip install -U ziglang auditwheel + # patchelf 0.18 (ubuntu-24.04's system version) corrupts ELF files + # with large p_align; pin <0.18 for auditwheel to use. + python3.11 -m pip install -U ziglang auditwheel 'patchelf<0.18' - name: Cross-compile armv7l wheels with zig env: HOST_PY311: python3.11 diff --git a/scripts/build-armv7l-cross.sh b/scripts/build-armv7l-cross.sh index 69eb3bfea..f304d51fd 100755 --- a/scripts/build-armv7l-cross.sh +++ b/scripts/build-armv7l-cross.sh @@ -102,21 +102,31 @@ build_one() { rm -rf "$raw"; mkdir -p "$raw" # The _PYTHON_* vars make sysconfig report armv7l; zig does the compiling. + # max-page-size=4096 aligns our extensions to the armv7l (4K) page size; + # lld's default 64K alignment uses 4K file offsets, which the loader rejects. + # Setting CFLAGS replaces the target's default OPT flags, so -O2 -DNDEBUG + # must be passed explicitly (without NDEBUG the free-threaded build trips a + # Py_SET_REFCNT assertion that release wheels compile out). env \ CC="$BIN_DIR/zig-cc" CXX="$BIN_DIR/zig-cxx" AR="$BIN_DIR/zig-ar" \ - LDSHARED="$BIN_DIR/zig-cc -shared" \ + LDSHARED="$BIN_DIR/zig-cc -shared -Wl,-z,max-page-size=4096" \ _PYTHON_HOST_PLATFORM=linux-armv7l \ _PYTHON_SYSCONFIGDATA_NAME="$(basename "${scd[0]}" .py)" \ PYTHONPATH="$(dirname "${scd[0]}")" \ - CFLAGS="-I${inc[0]} -Wno-error=incompatible-pointer-types" \ + CFLAGS="-I${inc[0]} -O2 -DNDEBUG -Wno-error=incompatible-pointer-types" \ PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig \ LD_LIBRARY_PATH=/tmp/vendor/lib \ "$host_py" -m pip wheel . --no-build-isolation --no-deps -w "$raw" # Bundle FFmpeg + its armhf system deps and stamp the platform tag. The shim # lets auditwheel accept the armv7l --plat from an x86_64 host; --ldpaths - # points it at the target's libs instead of the host search path. - "$TOOLS_PY" scripts/auditwheel-cross.py armv7l glibc repair \ + # points it at the target's libs instead of the host search path. Force the + # pip patchelf (<0.18, on PATH via the scripts dir) ahead of the system one: + # ubuntu-24.04 ships patchelf 0.18.0, which silently corrupts ELF files with + # large p_align, breaking the bundled FFmpeg libs at runtime. + local pe_dir + pe_dir="$("$TOOLS_PY" -c 'import sysconfig; print(sysconfig.get_path("scripts"))')" + PATH="$pe_dir:$PATH" "$TOOLS_PY" scripts/auditwheel-cross.py armv7l glibc repair \ --ldpaths "/tmp/vendor/lib:$PY_DIR/syslib" \ --plat "$PLAT" -w "$DIST_DIR" "$raw"/*.whl } From d1ca67e47b1208ab70edfecb6f66a6fd57aaab5b Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 16 Jun 2026 01:00:26 -0400 Subject: [PATCH 800/846] Add `at` parameter to Graph.push and Graph.vpush Push a frame to a single buffer source by index instead of broadcasting to every matching source, which is needed for multi-input filters like overlay. push(None, at=i) flushes that one source. --- CHANGELOG.rst | 1 + av/filter/graph.py | 23 +++++++++++++++++++--- av/filter/graph.pyi | 4 ++-- scripts/build-deps | 2 +- tests/test_filters.py | 44 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 68 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5b9530be4..797133111 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -39,6 +39,7 @@ Features: - Add ``options`` parameter to ``AudioResampler`` for passing ``libswresample`` options (e.g. ``resampler``, ``filter_size``, ``cutoff``) by :gh-user:`WyattBlue` (:issue:`2262`). - Support ``yuv420p10le`` in ``VideoFrame.to_ndarray`` and ``VideoFrame.from_ndarray`` by :gh-user:`WyattBlue` (:issue:`1981`). +- Add ``at`` parameter to ``Graph.push`` and ``Graph.vpush`` to push a frame to a single buffer source by index, for multi-input filters like ``overlay`` by :gh-user:`WyattBlue`. Fixes: diff --git a/av/filter/graph.py b/av/filter/graph.py index a7648f967..8967cce5f 100644 --- a/av/filter/graph.py +++ b/av/filter/graph.py @@ -232,7 +232,7 @@ def set_audio_frame_size(self, frame_size): cython.cast(FilterContext, sink).ptr, frame_size ) - def push(self, frame): + def push(self, frame, at: cython.int = -1): if frame is None: contexts = self._get_context_by_type("buffer") + self._get_context_by_type( "abuffer" @@ -246,12 +246,29 @@ def push(self, frame): f"can only AudioFrame, VideoFrame or None; got {type(frame)}" ) + if at >= 0: + if at >= len(contexts): + raise IndexError( + f"buffer source index {at} out of range; found {len(contexts)}" + ) + contexts[at].push(frame) + return + for ctx in contexts: ctx.push(frame) - def vpush(self, frame: VideoFrame | None): + def vpush(self, frame: VideoFrame | None, at: cython.int = -1): """Like `push`, but only for VideoFrames.""" - for ctx in self._get_context_by_type("buffer"): + contexts = self._get_context_by_type("buffer") + if at >= 0: + if at >= len(contexts): + raise IndexError( + f"buffer source index {at} out of range; found {len(contexts)}" + ) + contexts[at].push(frame) + return + + for ctx in contexts: ctx.push(frame) # TODO: Test complex filter graphs, add `at: int = 0` arg to pull() and vpull(). diff --git a/av/filter/graph.pyi b/av/filter/graph.pyi index 758813ea2..8f3231fc2 100644 --- a/av/filter/graph.pyi +++ b/av/filter/graph.pyi @@ -42,7 +42,7 @@ class Graph: time_base: Fraction | None = None, ) -> FilterContext: ... def set_audio_frame_size(self, frame_size: int) -> None: ... - def push(self, frame: None | AudioFrame | VideoFrame) -> None: ... + def push(self, frame: None | AudioFrame | VideoFrame, at: int = -1) -> None: ... def pull(self) -> VideoFrame | AudioFrame: ... - def vpush(self, frame: VideoFrame | None) -> None: ... + def vpush(self, frame: VideoFrame | None, at: int = -1) -> None: ... def vpull(self) -> VideoFrame: ... diff --git a/scripts/build-deps b/scripts/build-deps index 4449f2bc3..c2602e728 100755 --- a/scripts/build-deps +++ b/scripts/build-deps @@ -69,7 +69,7 @@ echo ./configure --disable-bsfs \ --enable-bsf=chomp,extract_extradata,h264_mp4toannexb,setts \ --disable-filters \ - --enable-filter=abuffer,abuffersink,aformat,aresample,atempo,buffer,buffersink,bwdif,color,loudnorm,lutrgb,palettegen,scale,testsrc,vflip,volume \ + --enable-filter=abuffer,abuffersink,aformat,aresample,atempo,buffer,buffersink,bwdif,color,loudnorm,lutrgb,overlay,palettegen,scale,testsrc,vflip,volume \ --enable-sse \ --enable-avx \ --enable-avx2 \ diff --git a/tests/test_filters.py b/tests/test_filters.py index 51522e7de..97038828a 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -254,6 +254,50 @@ def test_EOF(self) -> None: assert palette_frame.width == 16 assert palette_frame.height == 16 + def test_push_at_index(self) -> None: + # overlay has two video buffer sources; `at` targets a single one, + # instead of broadcasting the same frame to both (like auto-editor's + # pushIdx/flushIdx). + width, height = 16, 16 + + base = VideoFrame(width, height, "yuv420p") + for plane in base.planes: + plane.update(bytes(plane.buffer_size)) + base.pts = 0 + base.time_base = Fraction(1, 30) + + top = VideoFrame(width, height, "yuv420p") + for i, plane in enumerate(top.planes): + plane.update(bytes([200 if i == 0 else 128]) * plane.buffer_size) + top.pts = 0 + top.time_base = Fraction(1, 30) + + graph = Graph() + b0 = graph.add_buffer( + width=width, height=height, format=base.format, time_base=base.time_base + ) + b1 = graph.add_buffer( + width=width, height=height, format=top.format, time_base=top.time_base + ) + overlay = graph.add("overlay", "x=0:y=0") + sink = graph.add("buffersink") + b0.link_to(overlay, 0, 0) + b1.link_to(overlay, 0, 1) + overlay.link_to(sink) + graph.configure() + + graph.push(base, at=0) + graph.push(top, at=1) + graph.push(None, at=0) + graph.push(None, at=1) + + out = graph.vpull() + assert isinstance(out, av.VideoFrame) + assert (out.width, out.height) == (width, height) + + with self.assertRaises(IndexError): + graph.push(base, at=2) + def test_graph_threads(self) -> None: graph = Graph() assert graph.threads == 0 From 0fbedb20de51846087ef3e70f9a8fffa5475e8aa Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 16 Jun 2026 01:16:06 -0400 Subject: [PATCH 801/846] Make OutputContainer.metadata lazy --- av/container/core.pxd | 2 +- av/container/core.py | 8 ++++++++ av/container/input.py | 2 +- av/container/output.py | 1 - 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/av/container/core.pxd b/av/container/core.pxd index badd84abb..bd298a7a0 100644 --- a/av/container/core.pxd +++ b/av/container/core.pxd @@ -34,7 +34,7 @@ cdef class Container: cdef HWAccel hwaccel cdef readonly StreamContainer streams - cdef readonly dict metadata + cdef dict _metadata # Private API. cdef uint8_t _myflag # enum: writeable, input_was_opened, started, done, extradata_planned diff --git a/av/container/core.py b/av/container/core.py index 8aeaa7a3e..c6cdfe7fe 100755 --- a/av/container/core.py +++ b/av/container/core.py @@ -414,6 +414,14 @@ def flags(self, value: cython.int): def input_was_opened(self): return self._myflag & 2 + @property + def metadata(self) -> dict: + # Lazily created so output containers that never touch metadata don't + # allocate a dict. Input containers populate ``_metadata`` eagerly. + if self._metadata is None: + self._metadata = {} + return self._metadata + def chapters(self): self._assert_open() result: list = [] diff --git a/av/container/input.py b/av/container/input.py index 4881e720d..18558126c 100644 --- a/av/container/input.py +++ b/av/container/input.py @@ -101,7 +101,7 @@ def __cinit__(self, *args, **kwargs): "Hardware accelerated decode requested but no stream is compatible" ) - self.metadata = avdict_to_dict( + self._metadata = avdict_to_dict( self.ptr.metadata, self.metadata_encoding, self.metadata_errors ) diff --git a/av/container/output.py b/av/container/output.py index 8c56ef080..4a9de9f73 100644 --- a/av/container/output.py +++ b/av/container/output.py @@ -67,7 +67,6 @@ def close_output(self: OutputContainer): class OutputContainer(Container): def __cinit__(self, *args, **kwargs): self.streams = StreamContainer() - self.metadata = {} self._extradata_bsfs = {} self._buffered_packets = [] with cython.nogil: From d9d12c54347336bce010a7721b8a0df7f33dbf8e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 16 Jun 2026 01:24:16 -0400 Subject: [PATCH 802/846] Speed up Graph push/pull per-frame paths Cache a one-byte filter kind on FilterContext (source / video sink / audio sink / other) at wrap time, so push/pull no longer reconvert filter.name (a C string) to a Python str and do tuple-membership tests on every frame. Cache the buffer/abuffer source contexts on Graph so push/vpush index a list attribute directly instead of looking them up in _context_by_type each call. Pure internal change, no API or behavior difference. On a trivial buffer->buffersink graph the push+pull round trip drops from ~2.0 to ~1.25 us/frame. --- av/filter/context.pxd | 4 +--- av/filter/context.py | 21 ++++++++++++++++++--- av/filter/graph.pxd | 2 ++ av/filter/graph.py | 19 ++++++++++++------- 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/av/filter/context.pxd b/av/filter/context.pxd index ae9f27c99..d67e7e1e7 100644 --- a/av/filter/context.pxd +++ b/av/filter/context.pxd @@ -5,15 +5,13 @@ from av.filter.graph cimport Graph cdef class FilterContext: - cdef lib.AVFilterContext *ptr cdef readonly object _graph cdef readonly Filter filter - cdef object _inputs cdef object _outputs - cdef bint inited + cdef unsigned char _kind cdef FilterContext wrap_filter_context(Graph graph, Filter filter, lib.AVFilterContext *ptr) diff --git a/av/filter/context.py b/av/filter/context.py index 4873b3ff5..1dfd6e7a4 100644 --- a/av/filter/context.py +++ b/av/filter/context.py @@ -12,6 +12,11 @@ _cinit_sentinel = cython.declare(object, object()) +_KIND_OTHER = cython.declare(cython.uchar, 0) +_KIND_SOURCE = cython.declare(cython.uchar, 1) # buffer / abuffer +_KIND_VIDEO_SINK = cython.declare(cython.uchar, 2) # buffersink +_KIND_AUDIO_SINK = cython.declare(cython.uchar, 3) # abuffersink + @cython.cfunc def wrap_filter_context( @@ -21,6 +26,16 @@ def wrap_filter_context( self._graph = weakref.ref(graph) self.filter = filter self.ptr = ptr + + name: str = filter.name + if name == "buffer" or name == "abuffer": + self._kind = _KIND_SOURCE + elif name == "buffersink": + self._kind = _KIND_VIDEO_SINK + elif name == "abuffersink": + self._kind = _KIND_AUDIO_SINK + else: + self._kind = _KIND_OTHER return self @@ -108,7 +123,7 @@ def push(self, frame: Frame | None): res = lib.av_buffersrc_write_frame(self.ptr, cython.NULL) err_check(res) return - elif self.filter.name in ("abuffer", "buffer"): + elif self._kind == _KIND_SOURCE: with cython.nogil: res = lib.av_buffersrc_write_frame(self.ptr, frame.ptr) err_check(res) @@ -126,9 +141,9 @@ def push(self, frame: Frame | None): def pull(self): frame: Frame res: cython.int - if self.filter.name == "buffersink": + if self._kind == _KIND_VIDEO_SINK: frame = alloc_video_frame() - elif self.filter.name == "abuffersink": + elif self._kind == _KIND_AUDIO_SINK: frame = alloc_audio_frame() else: # Delegate to the output. diff --git a/av/filter/graph.pxd b/av/filter/graph.pxd index e85f67ce4..11e3644c6 100644 --- a/av/filter/graph.pxd +++ b/av/filter/graph.pxd @@ -20,3 +20,5 @@ cdef class Graph: cdef int _nb_filters_seen cdef dict[long, FilterContext] _context_by_ptr cdef dict[str, list[FilterContext]] _context_by_type + cdef list[FilterContext] _video_sources + cdef list[FilterContext] _audio_sources diff --git a/av/filter/graph.py b/av/filter/graph.py index 8967cce5f..e7ce7bc9b 100644 --- a/av/filter/graph.py +++ b/av/filter/graph.py @@ -22,6 +22,8 @@ def __cinit__(self): self._nb_filters_seen = 0 self._context_by_ptr = {} self._context_by_type = {} + self._video_sources = [] + self._audio_sources = [] def __dealloc__(self): if self.ptr: @@ -108,8 +110,13 @@ def add(self, filter, args=None, **kwargs): @cython.cfunc def _register_context(self, ctx: FilterContext): + name: str = ctx.filter.ptr.name self._context_by_ptr[cython.cast(cython.long, ctx.ptr)] = ctx - self._context_by_type.setdefault(ctx.filter.ptr.name, []).append(ctx) + self._context_by_type.setdefault(name, []).append(ctx) + if name == "buffer": + self._video_sources.append(ctx) + elif name == "abuffer": + self._audio_sources.append(ctx) @cython.cfunc def _auto_register(self): @@ -234,13 +241,11 @@ def set_audio_frame_size(self, frame_size): def push(self, frame, at: cython.int = -1): if frame is None: - contexts = self._get_context_by_type("buffer") + self._get_context_by_type( - "abuffer" - ) + contexts = self._video_sources + self._audio_sources elif isinstance(frame, VideoFrame): - contexts = self._get_context_by_type("buffer") + contexts = self._video_sources elif isinstance(frame, AudioFrame): - contexts = self._get_context_by_type("abuffer") + contexts = self._audio_sources else: raise ValueError( f"can only AudioFrame, VideoFrame or None; got {type(frame)}" @@ -259,7 +264,7 @@ def push(self, frame, at: cython.int = -1): def vpush(self, frame: VideoFrame | None, at: cython.int = -1): """Like `push`, but only for VideoFrames.""" - contexts = self._get_context_by_type("buffer") + contexts = self._video_sources if at >= 0: if at >= len(contexts): raise IndexError( From 68c1fe6640ae651d6fbc2e8da946f83d2e1dacba Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 16 Jun 2026 01:48:22 -0400 Subject: [PATCH 803/846] Fix unsound pointer-to-long casts that truncate on LLP64 Casting AVFilterContext* to C long truncated the high 32 bits on LLP64 platforms (64-bit Windows), so distinct contexts could collide on the same _context_by_ptr key. Key on size_t instead, which is pointer-width everywhere. Also route object addresses through uintptr_t rather than signed long long in OpaqueContainer.add. --- av/filter/graph.pxd | 2 +- av/filter/graph.py | 4 ++-- av/filter/link.py | 4 ++-- av/opaque.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/av/filter/graph.pxd b/av/filter/graph.pxd index 11e3644c6..f67053016 100644 --- a/av/filter/graph.pxd +++ b/av/filter/graph.pxd @@ -18,7 +18,7 @@ cdef class Graph: cdef _register_context(self, FilterContext) cdef _auto_register(self) cdef int _nb_filters_seen - cdef dict[long, FilterContext] _context_by_ptr + cdef dict[size_t, FilterContext] _context_by_ptr cdef dict[str, list[FilterContext]] _context_by_type cdef list[FilterContext] _video_sources cdef list[FilterContext] _audio_sources diff --git a/av/filter/graph.py b/av/filter/graph.py index e7ce7bc9b..7463c5a5c 100644 --- a/av/filter/graph.py +++ b/av/filter/graph.py @@ -111,7 +111,7 @@ def add(self, filter, args=None, **kwargs): @cython.cfunc def _register_context(self, ctx: FilterContext): name: str = ctx.filter.ptr.name - self._context_by_ptr[cython.cast(cython.long, ctx.ptr)] = ctx + self._context_by_ptr[cython.cast(cython.size_t, ctx.ptr)] = ctx self._context_by_type.setdefault(name, []).append(ctx) if name == "buffer": self._video_sources.append(ctx) @@ -128,7 +128,7 @@ def _auto_register(self): # point we don't expose that in the API, so we should be okay... for i in range(self._nb_filters_seen, self.ptr.nb_filters): c_ctx = self.ptr.filters[i] - if cython.cast(cython.long, c_ctx) in self._context_by_ptr: + if cython.cast(cython.size_t, c_ctx) in self._context_by_ptr: continue filter_ = wrap_filter(c_ctx.filter) py_ctx = wrap_filter_context(self, filter_, c_ctx) diff --git a/av/filter/link.py b/av/filter/link.py index 0e5b680ce..2b687e194 100644 --- a/av/filter/link.py +++ b/av/filter/link.py @@ -33,7 +33,7 @@ def input(self): else: # nobreak raise RuntimeError("could not find link in context") graph: Graph = self.graph - ctx = graph._context_by_ptr[cython.cast(cython.long, cctx)] + ctx = graph._context_by_ptr[cython.cast(cython.size_t, cctx)] self._input = ctx.outputs[i] return self._input @@ -50,7 +50,7 @@ def output(self): raise RuntimeError("could not find link in context") try: graph: Graph = self.graph - ctx = graph._context_by_ptr[cython.cast(cython.long, cctx)] + ctx = graph._context_by_ptr[cython.cast(cython.size_t, cctx)] except KeyError: raise RuntimeError( "could not find context in graph", (cctx.name, cctx.filter.name) diff --git a/av/opaque.py b/av/opaque.py index d6696289b..e55468c44 100644 --- a/av/opaque.py +++ b/av/opaque.py @@ -27,7 +27,7 @@ def __cinit__(self): @cython.cfunc def add(self, v: object) -> cython.pointer[lib.AVBufferRef]: # Use object's memory address as key - key: uintptr_t = cython.cast(cython.longlong, id(v)) + key: uintptr_t = cython.cast(uintptr_t, id(v)) self._objects[key] = v data: u8ptr = cython.cast(u8ptr, lib.av_malloc(sizeof(uintptr_t))) From 9f28bacb3fd22bb514de4e0bfcd655f125508d3d Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 16 Jun 2026 01:54:42 -0400 Subject: [PATCH 804/846] Silence MSVC C4267 size_t->int narrowing in Packet.__init__ av_new_packet() and AVPacket.size take int, but size stays size_t for av_buffer_create(). FFmpeg caps packet sizes at int range, so make the narrowing explicit to suppress the warning on Windows. --- av/packet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/av/packet.py b/av/packet.py index 7886b37ad..8be300661 100644 --- a/av/packet.py +++ b/av/packet.py @@ -237,7 +237,7 @@ def __init__(self, input=None): if isinstance(input, int): size = input if size: - err_check(lib.av_new_packet(self.ptr, size)) + err_check(lib.av_new_packet(self.ptr, cython.cast(cython.int, size))) else: source = bytesource(input) size = source.length @@ -256,7 +256,7 @@ def __init__(self, input=None): raise MemoryError("Could not allocate AVBufferRef") self.ptr.buf = buf self.ptr.data = source.ptr - self.ptr.size = size + self.ptr.size = cython.cast(cython.int, size) def __repr__(self): stream = self._stream.index if self._stream else 0 From 0b3ba521823c04e8cc4bf0f5be21c2a1ee094724 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 16 Jun 2026 02:02:07 -0400 Subject: [PATCH 805/846] Fix MSVC narrowing warnings in audio/codec/container - AudioFrame._init: explicit int cast on _buffer_size -> avcodec_fill_audio_frame (C4267) - CodecContext: explicit int casts on extradata_size and parser in_size (C4267) - avcodec.pxd: bit_rate, rc_max_rate, rc_min_rate are int64_t in FFmpeg, not int; widen bit_rate setter to match (C4244, also fixes >2^31 truncation) - avformat.pxd: AVChapter.id is int64_t, not int (C4244, fixes id truncation) - Container.set_chapters: explicit uint cast on nb_chapters = count (C4244) --- av/audio/frame.py | 2 +- av/codec/context.py | 8 +++++--- av/container/core.py | 2 +- include/avcodec.pxd | 6 +++--- include/avformat.pxd | 2 +- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/av/audio/frame.py b/av/audio/frame.py index c843c9efd..214a947e3 100644 --- a/av/audio/frame.py +++ b/av/audio/frame.py @@ -78,7 +78,7 @@ def _init( self.layout.nb_channels, cython.cast(lib.AVSampleFormat, self.ptr.format), self._buffer, - self._buffer_size, + cython.cast(cython.int, self._buffer_size), align, ) ) diff --git a/av/codec/context.py b/av/codec/context.py index 9b51c1430..6e10aaf9b 100644 --- a/av/codec/context.py +++ b/av/codec/context.py @@ -207,7 +207,7 @@ def extradata(self, data): if not self.ptr.extradata: raise MemoryError("Cannot allocate extradata") memcpy(self.ptr.extradata, source.ptr, source.length) - self.ptr.extradata_size = source.length + self.ptr.extradata_size = cython.cast(cython.int, source.length) @property def extradata_size(self): @@ -294,7 +294,9 @@ def parse(self, raw_input=None): source: ByteSource = bytesource(raw_input, allow_none=True) in_data: cython.p_uchar = source.ptr if source is not None else cython.NULL - in_size: cython.int = source.length if source is not None else 0 + in_size: cython.int = ( + cython.cast(cython.int, source.length) if source is not None else 0 + ) out_data: cython.p_uchar out_size: cython.int @@ -642,7 +644,7 @@ def bit_rate(self): return self.ptr.bit_rate if self.ptr.bit_rate > 0 else None @bit_rate.setter - def bit_rate(self, value: cython.int): + def bit_rate(self, value: cython.longlong): self.ptr.bit_rate = value @property diff --git a/av/container/core.py b/av/container/core.py index c6cdfe7fe..bd82e86a4 100755 --- a/av/container/core.py +++ b/av/container/core.py @@ -487,7 +487,7 @@ def set_chapters(self, chapters): ) ch_array[i] = ch - self.ptr.nb_chapters = count + self.ptr.nb_chapters = cython.cast(cython.uint, count) self.ptr.chapters = ch_array diff --git a/include/avcodec.pxd b/include/avcodec.pxd index 534f1ae1b..4e43b5bf1 100644 --- a/include/avcodec.pxd +++ b/include/avcodec.pxd @@ -232,7 +232,7 @@ cdef extern from "libavcodec/avcodec.h" nogil: void* opaque - int bit_rate + int64_t bit_rate int flags int flags2 uint8_t *extradata @@ -269,8 +269,8 @@ cdef extern from "libavcodec/avcodec.h" nogil: int qmin int qmax int rc_buffer_size - int rc_max_rate - int rc_min_rate + int64_t rc_max_rate + int64_t rc_min_rate AVHWAccel *hwaccel AVBufferRef *hw_device_ctx diff --git a/include/avformat.pxd b/include/avformat.pxd index 1cd07d814..06262c41a 100644 --- a/include/avformat.pxd +++ b/include/avformat.pxd @@ -38,7 +38,7 @@ cdef extern from "libavformat/avformat.h" nogil: AVRational sample_aspect_ratio cdef struct AVChapter: - int id + int64_t id int64_t start int64_t end AVRational time_base From d2c4df21b6f5a428bc67ea20a42b714d7bea2939 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 16 Jun 2026 02:06:54 -0400 Subject: [PATCH 806/846] Fix MSVC narrowing warnings in pyio - PyIOFile.pos is a file position; declare it int64_t instead of long so it doesn't truncate past 2GB on Win64 (C4244 on pos = / += offset) - pyio_read_gil: explicit int cast on the len(res) return (C4244) --- av/container/pyio.pxd | 2 +- av/container/pyio.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/av/container/pyio.pxd b/av/container/pyio.pxd index 80edc8a6b..c1f91b7b6 100644 --- a/av/container/pyio.pxd +++ b/av/container/pyio.pxd @@ -20,5 +20,5 @@ cdef class PyIOFile: # Custom IO for above. cdef lib.AVIOContext *iocontext cdef unsigned char *buffer - cdef long pos + cdef int64_t pos cdef bint pos_is_valid diff --git a/av/container/pyio.py b/av/container/pyio.py index d5c420ea4..34a497246 100644 --- a/av/container/pyio.py +++ b/av/container/pyio.py @@ -109,7 +109,7 @@ def pyio_read_gil(opaque: cython.p_void, buf: Buf, buf_size: cython.int) -> cyth self.pos += len(res) if not res: return lib.AVERROR_EOF - return len(res) + return cython.cast(cython.int, len(res)) except Exception: return stash_exception() From 6b84db2a6c873821764a3f85febacff72537757e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 16 Jun 2026 02:12:36 -0400 Subject: [PATCH 807/846] Fix remaining MSVC warnings --- av/container/output.py | 2 +- av/sidedata/sidedata.py | 2 +- av/subtitles/codeccontext.py | 2 +- av/video/format.py | 2 +- include/avcodec.pxd | 2 +- include/avformat.pxd | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/av/container/output.py b/av/container/output.py index 4a9de9f73..c17b32ba4 100644 --- a/av/container/output.py +++ b/av/container/output.py @@ -383,7 +383,7 @@ def add_attachment(self, name: str, mimetype: str, data: bytes): buf[i] = data[i] buf[payload_size] = 0 stream.codecpar.extradata = cython.cast(cython.p_uchar, buf) - stream.codecpar.extradata_size = payload_size + stream.codecpar.extradata_size = cython.cast(cython.int, payload_size) # Wrap as user-land stream. meta_ptr = cython.address(stream.metadata) diff --git a/av/sidedata/sidedata.py b/av/sidedata/sidedata.py index f19aeac06..d0fa7b0d1 100644 --- a/av/sidedata/sidedata.py +++ b/av/sidedata/sidedata.py @@ -109,7 +109,7 @@ def __init__(self, frame: Frame): self._by_index: list = [] self._by_type: dict = {} - i: cython.Py_ssize_t + i: cython.int data: SideData for i in range(self.frame.ptr.nb_side_data): diff --git a/av/subtitles/codeccontext.py b/av/subtitles/codeccontext.py index acad69fb7..618badfb1 100644 --- a/av/subtitles/codeccontext.py +++ b/av/subtitles/codeccontext.py @@ -44,7 +44,7 @@ def subtitle_header(self, data: bytes | None) -> None: if not self.ptr.subtitle_header: raise MemoryError("Cannot allocate subtitle_header") memcpy(self.ptr.subtitle_header, source.ptr, source.length) - self.ptr.subtitle_header_size = source.length + self.ptr.subtitle_header_size = cython.cast(cython.int, source.length) self.subtitle_header_set = True def __dealloc__(self) -> None: diff --git a/av/video/format.py b/av/video/format.py index 654f0f7fe..3ab505051 100644 --- a/av/video/format.py +++ b/av/video/format.py @@ -143,7 +143,7 @@ def chroma_height(self, luma_height: cython.int = 0): @cython.final @cython.cclass class VideoFormatComponent: - def __cinit__(self, format: VideoFormat, index: cython.size_t): + def __cinit__(self, format: VideoFormat, index: cython.uint): self.format = format self.index = index self.ptr = cython.address(format.ptr.comp[index]) diff --git a/include/avcodec.pxd b/include/avcodec.pxd index 4e43b5bf1..5671caf41 100644 --- a/include/avcodec.pxd +++ b/include/avcodec.pxd @@ -338,7 +338,7 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef struct AVFrameSideData: AVFrameSideDataType type uint8_t *data - int size + size_t size AVDictionary *metadata # See: http://ffmpeg.org/doxygen/trunk/structAVFrame.html diff --git a/include/avformat.pxd b/include/avformat.pxd index 06262c41a..54103f723 100644 --- a/include/avformat.pxd +++ b/include/avformat.pxd @@ -154,7 +154,7 @@ cdef extern from "libavformat/avformat.h" nogil: char filename int64_t start_time int64_t duration - int bit_rate + int64_t bit_rate int flags AVCodecID audio_codec_id AVCodecID video_codec_id From d548708499933397b6cefb248b7c55b00f26297c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 18 Jun 2026 00:12:46 -0400 Subject: [PATCH 808/846] Add PixFmtLoss enum for find_best_pix_fmt_of_list Wrap the loss bitmask returned by find_best_pix_fmt_of_list in a PixFmtLoss IntFlag instead of a plain int. Being an int subclass this is backward compatible, while making the FF_LOSS_* flags easier to work with. closes #2300 --- CHANGELOG.rst | 1 + av/codec/__init__.py | 2 ++ av/codec/codec.py | 24 ++++++++++++++++++++---- av/codec/codec.pyi | 24 +++++++++++++++++++----- docs/api/codec.rst | 13 +++++++++++++ tests/test_codec.py | 16 +++++++++++++++- 6 files changed, 70 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 797133111..1e7a60595 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -40,6 +40,7 @@ Features: - Add ``options`` parameter to ``AudioResampler`` for passing ``libswresample`` options (e.g. ``resampler``, ``filter_size``, ``cutoff``) by :gh-user:`WyattBlue` (:issue:`2262`). - Support ``yuv420p10le`` in ``VideoFrame.to_ndarray`` and ``VideoFrame.from_ndarray`` by :gh-user:`WyattBlue` (:issue:`1981`). - Add ``at`` parameter to ``Graph.push`` and ``Graph.vpush`` to push a frame to a single buffer source by index, for multi-input filters like ``overlay`` by :gh-user:`WyattBlue`. +- ``find_best_pix_fmt_of_list`` now returns the loss as a ``PixFmtLoss`` ``enum.IntFlag`` instead of a plain ``int`` by :gh-user:`WyattBlue` (:issue:`2300`). Fixes: diff --git a/av/codec/__init__.py b/av/codec/__init__.py index f8d4d75ae..f8f589de8 100644 --- a/av/codec/__init__.py +++ b/av/codec/__init__.py @@ -1,6 +1,7 @@ from .codec import ( Capabilities, Codec, + PixFmtLoss, Properties, codecs_available, find_best_pix_fmt_of_list, @@ -10,6 +11,7 @@ __all__ = ( "Capabilities", "Codec", + "PixFmtLoss", "Properties", "codecs_available", "find_best_pix_fmt_of_list", diff --git a/av/codec/codec.py b/av/codec/codec.py index 616980641..edda0c29d 100644 --- a/av/codec/codec.py +++ b/av/codec/codec.py @@ -1,4 +1,4 @@ -from enum import Flag, IntEnum +from enum import Flag, IntEnum, IntFlag import cython from cython.cimports import libav as lib @@ -54,6 +54,22 @@ class Capabilities(IntEnum): encoder_recon_frame = 1 << 22 +class PixFmtLoss(IntFlag): + """Flags describing what is lost when converting between pixel formats. + + Returned by :func:`find_best_pix_fmt_of_list`. Mirrors FFmpeg's + ``FF_LOSS_*`` flags. + """ + + NONE = 0 + RESOLUTION = 0x0001 # loss due to resolution change + DEPTH = 0x0002 # loss due to color depth change + COLORSPACE = 0x0004 # loss due to color space conversion + ALPHA = 0x0008 # loss of alpha bit + COLORQUANT = 0x0010 # loss due to color quantization + CHROMA = 0x0020 # loss of chroma (e.g. RGB to gray conversion) + + class UnknownCodecError(ValueError): pass @@ -419,7 +435,7 @@ def find_best_pix_fmt_of_list(pix_fmts, src_pix_fmt, has_alpha=False): :param src_pix_fmt: Source pixel format (str or VideoFormat). :param bool has_alpha: Whether the source alpha channel is used. :return: (best_format, loss) - :rtype: (VideoFormat | None, int) + :rtype: (VideoFormat | None, PixFmtLoss) """ src: lib.AVPixelFormat best: lib.AVPixelFormat @@ -434,7 +450,7 @@ def find_best_pix_fmt_of_list(pix_fmts, src_pix_fmt, has_alpha=False): pix_fmts = tuple(pix_fmts) if not pix_fmts: - return None, 0 + return None, PixFmtLoss.NONE if isinstance(src_pix_fmt, VideoFormat): src = cython.cast(VideoFormat, src_pix_fmt).pix_fmt @@ -462,7 +478,7 @@ def find_best_pix_fmt_of_list(pix_fmts, src_pix_fmt, has_alpha=False): best = lib.avcodec_find_best_pix_fmt_of_list( c_list, src, 1 if has_alpha else 0, cython.address(c_loss) ) - return get_video_format(best, 0, 0), c_loss + return get_video_format(best, 0, 0), PixFmtLoss(c_loss) finally: if c_list != cython.NULL: free(c_list) diff --git a/av/codec/codec.pyi b/av/codec/codec.pyi index 32c83371e..a9876c4f5 100644 --- a/av/codec/codec.pyi +++ b/av/codec/codec.pyi @@ -1,5 +1,5 @@ from collections.abc import Sequence -from enum import Flag, IntEnum +from enum import Flag, IntEnum, IntFlag from fractions import Fraction from typing import ClassVar, Literal, cast, overload @@ -44,6 +44,15 @@ class Capabilities(IntEnum): encoder_flush = cast(int, ...) encoder_recon_frame = cast(int, ...) +class PixFmtLoss(IntFlag): + NONE = cast(ClassVar[PixFmtLoss], ...) + RESOLUTION = cast(ClassVar[PixFmtLoss], ...) + DEPTH = cast(ClassVar[PixFmtLoss], ...) + COLORSPACE = cast(ClassVar[PixFmtLoss], ...) + ALPHA = cast(ClassVar[PixFmtLoss], ...) + COLORQUANT = cast(ClassVar[PixFmtLoss], ...) + CHROMA = cast(ClassVar[PixFmtLoss], ...) + class UnknownCodecError(ValueError): ... class Codec: @@ -117,7 +126,7 @@ def find_best_pix_fmt_of_list( pix_fmts: Sequence[PixFmtLike], src_pix_fmt: PixFmtLike, has_alpha: bool = False, -) -> tuple[VideoFormat | None, int]: +) -> tuple[VideoFormat | None, PixFmtLoss]: """ Find the best pixel format to convert to given a source format. @@ -127,10 +136,15 @@ def find_best_pix_fmt_of_list( :param src_pix_fmt: Source pixel format (str or VideoFormat). :param bool has_alpha: Whether the source alpha channel is used. :return: (best_format, loss): best_format is the best matching pixel format from - the list, or None if no suitable format was found; loss is Combination of flags informing you what kind of losses will occur. - :rtype: (VideoFormat | None, int) + the list, or None if no suitable format was found; loss is a combination of + :class:`PixFmtLoss` flags informing you what kind of losses will occur. + :rtype: (VideoFormat | None, PixFmtLoss) - Note on loss: it is a bitmask of FFmpeg loss flags describing what kinds of information would be lost converting from src_pix_fmt to best_format (e.g. loss of alpha, chroma, colorspace, resolution, bit depth, etc.). Multiple losses can be present at once, so the value is meant to be interpreted with bitwise & against FFmpeg's FF_LOSS_* constants. + Note on loss: it is an :class:`enum.IntFlag` describing what kinds of information + would be lost converting from src_pix_fmt to best_format (e.g. loss of alpha, + chroma, colorspace, resolution, bit depth, etc.). Multiple losses can be present + at once, so the value can be tested with bitwise & against the :class:`PixFmtLoss` + members. For exact behavior see: libavutil/pixdesc.c/get_pix_fmt_score() in ffmpeg source code. """ ... diff --git a/docs/api/codec.rst b/docs/api/codec.rst index 43bb235c2..7b6c261e3 100644 --- a/docs/api/codec.rst +++ b/docs/api/codec.rst @@ -43,6 +43,19 @@ Flags Note that ``ffmpeg -codecs`` prefers the properties versions of ``INTRA_ONLY`` and ``LOSSLESS``. +Pixel Format Selection +---------------------- + +.. autofunction:: find_best_pix_fmt_of_list + +.. autoclass:: PixFmtLoss + + Wraps FFmpeg's ``FF_LOSS_*`` flags. Returned by + :func:`find_best_pix_fmt_of_list` to describe what is lost when converting + from the source pixel format to the chosen one. Being an + :class:`enum.IntFlag`, members can be combined and tested with bitwise + operators. + Contexts -------- diff --git a/tests/test_codec.py b/tests/test_codec.py index 17432c926..2f8b95e0b 100644 --- a/tests/test_codec.py +++ b/tests/test_codec.py @@ -1,7 +1,7 @@ import pytest from av import AudioFormat, Codec, VideoFormat, codecs_available -from av.codec import find_best_pix_fmt_of_list +from av.codec import PixFmtLoss, find_best_pix_fmt_of_list from av.codec.codec import UnknownCodecError @@ -96,6 +96,7 @@ def test_find_best_pix_fmt_of_list_empty() -> None: best, loss = find_best_pix_fmt_of_list([], "rgb24") assert best is None assert loss == 0 + assert loss is PixFmtLoss.NONE @pytest.mark.parametrize( @@ -151,3 +152,16 @@ def test_find_best_pix_fmt_of_list_alpha_loss_flagged_when_used() -> None: assert best is not None assert best.name == "rgb24" assert loss != 0 + assert isinstance(loss, PixFmtLoss) + assert loss & PixFmtLoss.ALPHA + + +def test_find_best_pix_fmt_of_list_loss_flags() -> None: + # An identical format loses nothing. + _, loss = find_best_pix_fmt_of_list(["yuv420p"], "yuv420p") + assert loss is PixFmtLoss.NONE + + # Converting color to grayscale drops the chroma planes. + _, loss = find_best_pix_fmt_of_list(["gray"], "rgb24") + assert isinstance(loss, PixFmtLoss) + assert loss & PixFmtLoss.CHROMA From 8f233b7d6a94a0f3d8e25045b9640433251820b6 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 18 Jun 2026 08:02:33 -0400 Subject: [PATCH 809/846] Use ffmpeg 8.1.2 --- .github/workflows/smoke.yml | 8 ++++---- scripts/activate.sh | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 5d1efbaa1..c31c8e468 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -35,11 +35,11 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-24.04, python: "3.14", ffmpeg: "8.1.1", extras: true} + - {os: ubuntu-24.04, python: "3.14", ffmpeg: "8.1.2", extras: true} - {os: ubuntu-24.04, python: "3.11", ffmpeg: "8.0.2"} - - {os: ubuntu-24.04, python: "3.13", ffmpeg: "8.1.1"} + - {os: ubuntu-24.04, python: "3.13", ffmpeg: "8.1.2"} - {os: macos-14, python: "3.12", ffmpeg: "8.0.2"} - - {os: macos-14, python: "3.14", ffmpeg: "8.1.1"} + - {os: macos-14, python: "3.14", ffmpeg: "8.1.2"} env: PYAV_PYTHON: python${{ matrix.config.python }} @@ -186,7 +186,7 @@ jobs: TOOLS_PY: python3.11 run: bash scripts/build-armv7l-cross.sh - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v4 with: platforms: arm - name: Import-test wheels under emulation diff --git a/scripts/activate.sh b/scripts/activate.sh index 6ed8f5ac8..78eb08620 100755 --- a/scripts/activate.sh +++ b/scripts/activate.sh @@ -21,7 +21,7 @@ if [[ ! "$PYAV_LIBRARY" ]]; then return 1 fi else - PYAV_LIBRARY=ffmpeg-8.1.1 + PYAV_LIBRARY=ffmpeg-8.1.2 echo "No \$PYAV_LIBRARY set; defaulting to $PYAV_LIBRARY" fi fi From d1f701dc36cfcfb2fc13d591079f69fcc49568c0 Mon Sep 17 00:00:00 2001 From: Carlos Ruiz Date: Fri, 19 Jun 2026 20:16:27 -0700 Subject: [PATCH 810/846] Preserve frame props after hardware downloads --- av/video/codeccontext.py | 3 +-- av/video/reformatter.py | 3 +-- tests/test_decode.py | 54 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/av/video/codeccontext.py b/av/video/codeccontext.py index eb1c9a365..fab4774a3 100644 --- a/av/video/codeccontext.py +++ b/av/video/codeccontext.py @@ -125,8 +125,7 @@ def _transfer_hwframe(self, frame: Frame): frame_sw: Frame = self._alloc_next_frame() err_check(lib.av_hwframe_transfer_data(frame_sw.ptr, frame.ptr, 0)) - # TODO: Is there anything else to transfer? - frame_sw.pts = frame.pts + frame_sw._copy_internal_attributes(frame, data_layout=False) return frame_sw @property diff --git a/av/video/reformatter.py b/av/video/reformatter.py index fbbef01b9..435038dea 100644 --- a/av/video/reformatter.py +++ b/av/video/reformatter.py @@ -287,8 +287,7 @@ def _reformat( if frame.ptr.hw_frames_ctx: frame_sw = alloc_video_frame() err_check(lib.av_hwframe_transfer_data(frame_sw.ptr, frame.ptr, 0)) - frame_sw.pts = frame.pts - frame_sw._init_user_attributes() + frame_sw._copy_internal_attributes(frame, data_layout=False) frame = frame_sw src_format = cython.cast(lib.AVPixelFormat, frame.ptr.format) diff --git a/tests/test_decode.py b/tests/test_decode.py index 3277dd776..ee4142cc4 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -272,3 +272,57 @@ def test_hardware_decode(self) -> None: frame_count += 1 assert frame_count == video_stream.frames + + +@pytest.mark.parametrize("is_hw_owned", [False, True]) +def test_hardware_decode_download_preserves_frame_props(is_hw_owned: bool) -> None: + hwdevices_available = av.codec.hwaccel.hwdevices_available() + if "HWACCEL_DEVICE_TYPE" not in os.environ: + pytest.skip( + "Set the HWACCEL_DEVICE_TYPE to run this test. " + f"Options are {' '.join(hwdevices_available)}" + ) + + hwaccel_device_type = os.environ["HWACCEL_DEVICE_TYPE"] + assert hwaccel_device_type in hwdevices_available, ( + f"{hwaccel_device_type} not available" + ) + + test_video_path = fate_suite("hevc/hdr10_plus_h265_sample.hevc") + cpu_frame = decode_first_video_frame(test_video_path) + hw_frame = decode_first_video_frame( + test_video_path, + av.codec.hwaccel.HWAccel( + device_type=hwaccel_device_type, + is_hw_owned=is_hw_owned, + allow_software_fallback=False, + ), + ) + + # Ensure that hardware decoding preserves the frame properties, see #2231 + assert_video_frame_color_props_match(hw_frame, cpu_frame) + + # Ensure that reformatting also preserves them, even for hardware frames + cpu_frame = cpu_frame.reformat(format="bgr24") + hw_frame = hw_frame.reformat(format="bgr24") + assert_video_frame_color_props_match(hw_frame, cpu_frame) + + +def decode_first_video_frame( + path: str, hwaccel: av.codec.hwaccel.HWAccel | None = None +) -> av.VideoFrame: + with av.open(path, hwaccel=hwaccel) as container: + for packet in container.demux(video=0): + frames = packet.decode() + if frames: + return frames[0] + raise AssertionError("expected at least one decoded frame") + + +def assert_video_frame_color_props_match( + actual: av.VideoFrame, expected: av.VideoFrame +) -> None: + assert actual.color_range == expected.color_range + assert actual.colorspace == expected.colorspace + assert actual.color_primaries == expected.color_primaries + assert actual.color_trc == expected.color_trc From 41441a52418cfc772d559ca9bc79049a7be15277 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 21 Jun 2026 19:05:03 -0700 Subject: [PATCH 811/846] Update to 8.0.3 for smoke tests --- .github/workflows/smoke.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index c31c8e468..7eadf3733 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -36,9 +36,9 @@ jobs: matrix: config: - {os: ubuntu-24.04, python: "3.14", ffmpeg: "8.1.2", extras: true} - - {os: ubuntu-24.04, python: "3.11", ffmpeg: "8.0.2"} + - {os: ubuntu-24.04, python: "3.11", ffmpeg: "8.0.3"} - {os: ubuntu-24.04, python: "3.13", ffmpeg: "8.1.2"} - - {os: macos-14, python: "3.12", ffmpeg: "8.0.2"} + - {os: macos-14, python: "3.12", ffmpeg: "8.0.3"} - {os: macos-14, python: "3.14", ffmpeg: "8.1.2"} env: From fb43dfc69f067996f998cfd26e59616e90ea7529 Mon Sep 17 00:00:00 2001 From: Carlos Ruiz Date: Fri, 19 Jun 2026 22:56:41 -0700 Subject: [PATCH 812/846] Apply reformat shortcut to hardware frames too --- av/video/reformatter.py | 13 +++++++++++++ tests/test_decode.py | 42 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/av/video/reformatter.py b/av/video/reformatter.py index 435038dea..e95c871a3 100644 --- a/av/video/reformatter.py +++ b/av/video/reformatter.py @@ -291,6 +291,19 @@ def _reformat( frame = frame_sw src_format = cython.cast(lib.AVPixelFormat, frame.ptr.format) + # Check for shortcut again, in case dst_format matches the downloaded frame's sw_format + if ( + dst_format == src_format + and width == frame.ptr.width + and height == frame.ptr.height + and dst_colorspace == src_colorspace + and src_color_range == dst_color_range + and not set_dst_color_trc + and not set_dst_color_primaries + ): + frame._init_user_attributes() + return frame + if self.ptr == cython.NULL: self.ptr = sws_alloc_context() if self.ptr == cython.NULL: diff --git a/tests/test_decode.py b/tests/test_decode.py index ee4142cc4..814e0fc0e 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -308,6 +308,48 @@ def test_hardware_decode_download_preserves_frame_props(is_hw_owned: bool) -> No assert_video_frame_color_props_match(hw_frame, cpu_frame) +def test_hardware_frame_reformat_matches_downloaded_frame() -> None: + hwdevices_available = av.codec.hwaccel.hwdevices_available() + if "HWACCEL_DEVICE_TYPE" not in os.environ: + pytest.skip( + "Set the HWACCEL_DEVICE_TYPE to run this test. " + f"Options are {' '.join(hwdevices_available)}" + ) + + hwaccel_device_type = os.environ["HWACCEL_DEVICE_TYPE"] + assert hwaccel_device_type in hwdevices_available, ( + f"{hwaccel_device_type} not available" + ) + + test_video_path = fate_suite("h264/interlaced_crop.mp4") + downloaded_frame = decode_first_video_frame( + test_video_path, + av.codec.hwaccel.HWAccel( + device_type=hwaccel_device_type, + is_hw_owned=False, + allow_software_fallback=False, + ), + ) + hw_frame = decode_first_video_frame( + test_video_path, + av.codec.hwaccel.HWAccel( + device_type=hwaccel_device_type, + is_hw_owned=True, + allow_software_fallback=False, + ), + ) + assert downloaded_frame.format.name != hw_frame.format.name # E.g. cuda vs nv12 + + # Download hw_frame to CPU and ensure the contents match downloaded_frame + sw_format = downloaded_frame.format.name + reformatted_frame = hw_frame.reformat(format=sw_format) + assert reformatted_frame.format.name == downloaded_frame.format.name + assert np.array_equal( + reformatted_frame.to_ndarray(format=sw_format), + downloaded_frame.to_ndarray(format=sw_format), + ) + + def decode_first_video_frame( path: str, hwaccel: av.codec.hwaccel.HWAccel | None = None ) -> av.VideoFrame: From 8005a4df9e86a10815412efe1defcb15532f0e9a Mon Sep 17 00:00:00 2001 From: mark-oshea Date: Tue, 23 Jun 2026 16:36:44 -0700 Subject: [PATCH 813/846] Add Colorspace.BT2020 for BT.2020 YUV matrix conversion --- CHANGELOG.rst | 1 + av/video/reformatter.pxd | 1 + av/video/reformatter.py | 4 ++++ av/video/reformatter.pyi | 2 ++ tests/test_colorspace.py | 20 ++++++++++++++++++++ 5 files changed, 28 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1e7a60595..b855a7fb5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -41,6 +41,7 @@ Features: - Support ``yuv420p10le`` in ``VideoFrame.to_ndarray`` and ``VideoFrame.from_ndarray`` by :gh-user:`WyattBlue` (:issue:`1981`). - Add ``at`` parameter to ``Graph.push`` and ``Graph.vpush`` to push a frame to a single buffer source by index, for multi-input filters like ``overlay`` by :gh-user:`WyattBlue`. - ``find_best_pix_fmt_of_list`` now returns the loss as a ``PixFmtLoss`` ``enum.IntFlag`` instead of a plain ``int`` by :gh-user:`WyattBlue` (:issue:`2300`). +- Add ``Colorspace.BT2020`` by :gh-user:`mark-oshea`. Fixes: diff --git a/av/video/reformatter.pxd b/av/video/reformatter.pxd index fe4b68db5..5315a7c10 100644 --- a/av/video/reformatter.pxd +++ b/av/video/reformatter.pxd @@ -33,6 +33,7 @@ cdef extern from "libswscale/swscale.h" nogil: cdef int SWS_CS_SMPTE170M cdef int SWS_CS_SMPTE240M cdef int SWS_CS_DEFAULT + cdef int SWS_CS_BT2020 cdef SwsContext *sws_alloc_context() cdef void sws_free_context(SwsContext **ctx) diff --git a/av/video/reformatter.py b/av/video/reformatter.py index e95c871a3..eabcefe46 100644 --- a/av/video/reformatter.py +++ b/av/video/reformatter.py @@ -36,6 +36,7 @@ class Colorspace(IntEnum): SMPTE170M = SWS_CS_SMPTE170M SMPTE240M = SWS_CS_SMPTE240M DEFAULT = SWS_CS_DEFAULT + BT2020 = SWS_CS_BT2020 # Lowercase for b/c. itu709 = SWS_CS_ITU709 fcc = SWS_CS_FCC @@ -44,6 +45,7 @@ class Colorspace(IntEnum): smpte170m = SWS_CS_SMPTE170M smpte240m = SWS_CS_SMPTE240M default = SWS_CS_DEFAULT + bt2020 = SWS_CS_BT2020 class ColorRange(IntEnum): @@ -146,6 +148,8 @@ def _set_frame_colorspace( frame.colorspace = lib.AVCOL_SPC_SMPTE170M elif colorspace == SWS_CS_SMPTE240M: frame.colorspace = lib.AVCOL_SPC_SMPTE240M + elif colorspace == SWS_CS_BT2020: + frame.colorspace = lib.AVCOL_SPC_BT2020_NCL @cython.final diff --git a/av/video/reformatter.pyi b/av/video/reformatter.pyi index 30508a32b..480860d0b 100644 --- a/av/video/reformatter.pyi +++ b/av/video/reformatter.pyi @@ -31,6 +31,7 @@ class Colorspace(IntEnum): SMPTE170M = cast(int, ...) SMPTE240M = cast(int, ...) DEFAULT = cast(int, ...) + BT2020 = cast(int, ...) itu709 = cast(int, ...) fcc = cast(int, ...) itu601 = cast(int, ...) @@ -38,6 +39,7 @@ class Colorspace(IntEnum): smpte170m = cast(int, ...) smpte240m = cast(int, ...) default = cast(int, ...) + bt2020 = cast(int, ...) class ColorRange(IntEnum): UNSPECIFIED = 0 diff --git a/tests/test_colorspace.py b/tests/test_colorspace.py index a5352fc7d..6afd42cf0 100644 --- a/tests/test_colorspace.py +++ b/tests/test_colorspace.py @@ -1,3 +1,5 @@ +import pytest + import av from av.video.reformatter import ( ColorPrimaries, @@ -101,3 +103,21 @@ def test_reformat_preserves_color_primaries() -> None: frame.color_primaries = ColorPrimaries.BT709 rgb = frame.reformat(format="rgb24") assert rgb.color_primaries == ColorPrimaries.BT709 + + +@pytest.mark.parametrize( + ("colorspace", "expected"), + [ + (Colorspace.ITU709, Colorspace.ITU709), + (Colorspace.FCC, Colorspace.FCC), + (Colorspace.ITU601, 6), # AVCOL_SPC_SMPTE170M + (Colorspace.SMPTE240M, Colorspace.SMPTE240M), + (Colorspace.BT2020, Colorspace.BT2020), + ], +) +def test_reformat_dst_colorspace_metadata( + colorspace: Colorspace, expected: Colorspace | int +) -> None: + frame = av.VideoFrame(width=64, height=64, format="yuv420p") + rgb = frame.reformat(format="rgb24", dst_colorspace=colorspace) + assert rgb.colorspace == expected From b0560f28513f7b19b58bfccf821ef063deec9495 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 23 Jun 2026 21:03:25 -0400 Subject: [PATCH 814/846] Support HW encoding via add_stream(hwaccel=...) Previously PyAV only supported hardware decoding. Passing a hardware encoder (e.g. h264_vaapi) software frames failed at avcodec_open2 with EINVAL because no hw_device_ctx/hw_frames_ctx was ever set up. add_stream now accepts a hwaccel argument. For an encoder it attaches the device context, lazily builds the hw_frames_ctx in open() once width/height/pix_fmt are known, and uploads software frames to the device in _prepare_frames_for_encode. HWAccel setup now also accepts encoders, which advertise the HW_FRAMES_CTX method rather than HW_DEVICE_CTX. fixes #2156 --- CHANGELOG.rst | 1 + av/codec/context.pxd | 1 + av/codec/context.py | 48 ++++++++++++++++++ av/codec/hwaccel.py | 15 ++++-- av/codec/hwaccel.pyi | 2 +- av/container/output.py | 18 +++++-- av/container/output.pyi | 3 ++ av/video/codeccontext.pxd | 1 + av/video/codeccontext.py | 101 +++++++++++++++++++++++++++++--------- include/avcodec.pxd | 1 + include/avutil.pxd | 2 + tests/test_encode.py | 65 ++++++++++++++++++++++++ 12 files changed, 227 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b855a7fb5..85107acc3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -34,6 +34,7 @@ v18.0.0 (next) Breaking: - Remove Python 3.10 +- Support HW encoding via a ``hwaccel`` parameter on ``OutputContainer.add_stream`` (e.g. ``h264_vaapi``, ``h264_nvenc``, ``h264_videotoolbox``); software frames passed to ``encode`` are uploaded to the device automatically by :gh-user:`WyattBlue` (:issue:`2156`). Features: diff --git a/av/codec/context.pxd b/av/codec/context.pxd index cc12836d7..8108ecb1a 100644 --- a/av/codec/context.pxd +++ b/av/codec/context.pxd @@ -41,6 +41,7 @@ cdef class CodecContext: # TODO: Remove the `Packet` from `_setup_decoded_frame` (because flushing packets # are bogus). It should take all info it needs from the context and/or stream. cdef _prepare_and_time_rebase_frames_for_encode(self, Frame frame) + cdef void _setup_encode_hwframes(self) cdef list _prepare_frames_for_encode(self, Frame frame) cdef _setup_encoded_packet(self, Packet) cdef _setup_decoded_frame(self, Frame, Packet) diff --git a/av/codec/context.py b/av/codec/context.py index 6e10aaf9b..f842f80c1 100644 --- a/av/codec/context.py +++ b/av/codec/context.py @@ -246,6 +246,8 @@ def open(self, strict: cython.bint = True): self.ptr.time_base.num = 1 self.ptr.time_base.den = lib.AV_TIME_BASE + self._setup_encode_hwframes() + err_check( lib.avcodec_open2(self.ptr, self.codec.ptr, cython.address(options.ptr)), f'avcodec_open2("{self.codec.name}", {self.options})', @@ -380,6 +382,52 @@ def _send_frame_and_recv(self, frame: Frame | None): yield packet packet = self._recv_packet() + @cython.cfunc + def _setup_encode_hwframes(self) -> cython.void: + # Build the hardware frames context for hardware-accelerated encoding. + # + # Unlike the device context (attached at construction time), the frames + # context depends on the final width/height/pixel format, which the user + # sets after add_stream(). We therefore defer it until just before the + # codec is opened. + if self.hwaccel_ctx is None or not self.is_encoder: + return + if self.ptr.hw_frames_ctx: + return # Already set up. + + hw_format: lib.AVPixelFormat = self.hwaccel_ctx.config.ptr.pix_fmt + sw_format: lib.AVPixelFormat = cython.cast(lib.AVPixelFormat, self.ptr.pix_fmt) + + # The codec context's pix_fmt holds the *software* format the user feeds in. + # If they left it as the hardware format (or unset), pick a sane default. + if sw_format == hw_format or sw_format == lib.AV_PIX_FMT_NONE: + sw_format = lib.av_get_pix_fmt(b"nv12") + + frames_ref: cython.pointer[lib.AVBufferRef] = lib.av_hwframe_ctx_alloc( + self.hwaccel_ctx.ptr + ) + if frames_ref == cython.NULL: + raise MemoryError("av_hwframe_ctx_alloc() failed") + + try: + frames_ctx: cython.pointer[lib.AVHWFramesContext] = cython.cast( + cython.pointer[lib.AVHWFramesContext], frames_ref.data + ) + frames_ctx.format = hw_format + frames_ctx.sw_format = sw_format + frames_ctx.width = self.ptr.width + frames_ctx.height = self.ptr.height + frames_ctx.initial_pool_size = 32 + err_check(lib.av_hwframe_ctx_init(frames_ref)) + except Exception: + lib.av_buffer_unref(cython.address(frames_ref)) + raise + + # Ownership of frames_ref transfers to the codec context. + self.ptr.hw_frames_ctx = frames_ref + self.ptr.sw_pix_fmt = sw_format + self.ptr.pix_fmt = hw_format + @cython.cfunc def _prepare_frames_for_encode(self, frame: Frame | None) -> list: return [frame] diff --git a/av/codec/hwaccel.py b/av/codec/hwaccel.py index 69f742b29..392bd39ac 100644 --- a/av/codec/hwaccel.py +++ b/av/codec/hwaccel.py @@ -140,10 +140,17 @@ def __init__( self.ptr = cython.NULL self.config = None - def _initialize_hw_context(self, codec: Codec): + def _initialize_hw_context(self, codec: Codec, for_encoding: bool = False): + # Decoders advertise the device-context method, while encoders (e.g. + # h264_vaapi) advertise the frames-context method. Accept either one when + # setting up an encoder. + supported_methods: cython.int = lib.AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX + if for_encoding: + supported_methods |= lib.AV_CODEC_HW_CONFIG_METHOD_HW_FRAMES_CTX + config: HWConfig for config in codec.hardware_configs: - if not (config.ptr.methods & lib.AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX): + if not (config.ptr.methods & supported_methods): continue if self._device_type and config.device_type != self._device_type: continue @@ -168,7 +175,7 @@ def _initialize_hw_context(self, codec: Codec): ) ) - def create(self, codec: Codec) -> HWAccel: + def create(self, codec: Codec, for_encoding: bool = False) -> HWAccel: """Create a new hardware accelerator context with the given codec""" if self.ptr: raise RuntimeError("Hardware context already initialized") @@ -180,7 +187,7 @@ def create(self, codec: Codec) -> HWAccel: options=self.options, is_hw_owned=self.is_hw_owned, ) - ret._initialize_hw_context(codec) + ret._initialize_hw_context(codec, for_encoding=for_encoding) return ret def __dealloc__(self): diff --git a/av/codec/hwaccel.pyi b/av/codec/hwaccel.pyi index 247771a3b..a9135f683 100644 --- a/av/codec/hwaccel.pyi +++ b/av/codec/hwaccel.pyi @@ -52,6 +52,6 @@ class HWAccel: flags: int | None = None, is_hw_owned: bool = False, ) -> None: ... - def create(self, codec: Codec) -> HWAccel: ... + def create(self, codec: Codec, for_encoding: bool = False) -> HWAccel: ... def hwdevices_available() -> list[str]: ... diff --git a/av/container/output.py b/av/container/output.py index c17b32ba4..ebf9269e8 100644 --- a/av/container/output.py +++ b/av/container/output.py @@ -5,6 +5,7 @@ from cython.cimports.av.bitstream import BitStreamFilterContext from cython.cimports.av.codec.codec import Codec from cython.cimports.av.codec.context import CodecContext, wrap_codec_context +from cython.cimports.av.codec.hwaccel import HWAccel from cython.cimports.av.container.streams import StreamContainer from cython.cimports.av.dictionary import Dictionary from cython.cimports.av.error import err_check @@ -79,8 +80,15 @@ def __dealloc__(self): with cython.nogil: lib.av_packet_free(cython.address(self.packet_ptr)) - def add_stream(self, codec_name, rate=None, options: dict | None = None, **kwargs): - """add_stream(codec_name, rate=None) + def add_stream( + self, + codec_name, + rate=None, + options: dict | None = None, + hwaccel: HWAccel | None = None, + **kwargs, + ): + """add_stream(codec_name, rate=None, *, hwaccel=None) Creates a new stream from a codec name and returns it. Supports video, audio, and subtitle streams. @@ -88,6 +96,10 @@ def add_stream(self, codec_name, rate=None, options: dict | None = None, **kwarg :param codec_name: The name of a codec. :type codec_name: str :param dict options: Stream options. + :param HWAccel hwaccel: Optional settings for hardware-accelerated encoding. + Only applies to video streams (e.g. ``h264_vaapi``); software frames + passed to :meth:`~av.codec.context.CodecContext.encode` are uploaded to + the device automatically. :param \\**kwargs: Set attributes for the stream. :rtype: The new :class:`~av.stream.Stream`. @@ -164,7 +176,7 @@ def add_stream(self, codec_name, rate=None, options: dict | None = None, **kwarg err_check(lib.avcodec_parameters_from_context(stream.codecpar, ctx)) # Construct the user-land stream - py_codec_context: CodecContext = wrap_codec_context(ctx, codec, None) + py_codec_context: CodecContext = wrap_codec_context(ctx, codec, hwaccel) py_stream: Stream = wrap_stream(self, stream, py_codec_context) self.streams.add_stream(py_stream) diff --git a/av/container/output.pyi b/av/container/output.pyi index ea42b1439..5ed78a2b2 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -4,6 +4,7 @@ from typing import TypeVar, overload from av.audio import _AudioCodecName from av.audio.stream import AudioStream +from av.codec.hwaccel import HWAccel from av.packet import Packet from av.stream import AttachmentStream, DataStream, Stream from av.subtitles.stream import SubtitleStream @@ -29,6 +30,7 @@ class OutputContainer(Container): codec_name: _VideoCodecName, rate: Fraction | int | None = None, options: dict[str, str] | None = None, + hwaccel: HWAccel | None = None, **kwargs, ) -> VideoStream: ... @overload @@ -37,6 +39,7 @@ class OutputContainer(Container): codec_name: str, rate: Fraction | int | None = None, options: dict[str, str] | None = None, + hwaccel: HWAccel | None = None, **kwargs, ) -> VideoStream | AudioStream | SubtitleStream: ... def add_mux_stream( diff --git a/av/video/codeccontext.pxd b/av/video/codeccontext.pxd index 3489fdb7a..d15a9fc02 100644 --- a/av/video/codeccontext.pxd +++ b/av/video/codeccontext.pxd @@ -19,3 +19,4 @@ cdef class VideoCodecContext(CodecContext): cdef AVCodecPrivateData _private_data cdef readonly VideoReformatter reformatter cdef VideoFrame next_frame + cdef VideoFrame _encode_upload_frame(self, VideoFrame vframe) diff --git a/av/video/codeccontext.py b/av/video/codeccontext.py index fab4774a3..457ef43e9 100644 --- a/av/video/codeccontext.py +++ b/av/video/codeccontext.py @@ -49,29 +49,79 @@ def _init( ): CodecContext._init(self, ptr, codec, hwaccel) - if hwaccel is not None: - try: - self.hwaccel_ctx = hwaccel.create(self.codec) - self.ptr.hw_device_ctx = lib.av_buffer_ref(self.hwaccel_ctx.ptr) - self.ptr.pix_fmt = self.hwaccel_ctx.config.ptr.pix_fmt - self.ptr.get_format = _get_hw_format - self._private_data.hardware_pix_fmt = ( - self.hwaccel_ctx.config.ptr.pix_fmt - ) - self._private_data.allow_software_fallback = ( - self.hwaccel.allow_software_fallback - ) - self.ptr.opaque = cython.address(self._private_data) - except NotImplementedError: - # Some streams may not have a hardware decoder. For example, many action - # cam videos have a low resolution mjpeg stream, which is usually not - # compatible with hardware decoders. - # The user may have passed in a hwaccel because they want to decode the main - # stream with it, so we shouldn't abort even if we find a stream that can't - # be HW decoded. - # If the user wants to make sure hwaccel is actually used, they can check with the - # is_hwaccel() function on each stream's codec context. - self.hwaccel_ctx = None + if hwaccel is None: + return + + if self.is_encoder: + # Hardware-accelerated encoding. We only attach the device context here; + # the hardware frames context depends on the final width/height/pixel + # format (set by the user after add_stream()), so it is built lazily in + # CodecContext.open() via _setup_encode_hwframes(). + self.hwaccel_ctx = hwaccel.create(self.codec, for_encoding=True) + self.ptr.hw_device_ctx = lib.av_buffer_ref(self.hwaccel_ctx.ptr) + return + + try: + self.hwaccel_ctx = hwaccel.create(self.codec) + self.ptr.hw_device_ctx = lib.av_buffer_ref(self.hwaccel_ctx.ptr) + self.ptr.pix_fmt = self.hwaccel_ctx.config.ptr.pix_fmt + self.ptr.get_format = _get_hw_format + self._private_data.hardware_pix_fmt = self.hwaccel_ctx.config.ptr.pix_fmt + self._private_data.allow_software_fallback = ( + self.hwaccel.allow_software_fallback + ) + self.ptr.opaque = cython.address(self._private_data) + except NotImplementedError: + # Some streams may not have a hardware decoder. For example, many action + # cam videos have a low resolution mjpeg stream, which is usually not + # compatible with hardware decoders. + # The user may have passed in a hwaccel because they want to decode the main + # stream with it, so we shouldn't abort even if we find a stream that can't + # be HW decoded. + # If the user wants to make sure hwaccel is actually used, they can check with the + # is_hwaccel() function on each stream's codec context. + self.hwaccel_ctx = None + + @cython.cfunc + def _encode_upload_frame(self, vframe: VideoFrame) -> VideoFrame: + # Upload a software frame onto the device for hardware-accelerated encoding. + frames_ctx: cython.pointer[lib.AVHWFramesContext] = cython.cast( + cython.pointer[lib.AVHWFramesContext], self.ptr.hw_frames_ctx.data + ) + + # If the user already handed us a matching hardware frame, pass it through. + if vframe.ptr.format == frames_ctx.format: + return vframe + + # Convert to the frames context's software format and size before uploading, + # since av_hwframe_transfer_data() does not change pixel format or scale. + if ( + vframe.ptr.format != frames_ctx.sw_format + or vframe.ptr.width != frames_ctx.width + or vframe.ptr.height != frames_ctx.height + ): + if not self.reformatter: + self.reformatter = VideoReformatter() + vframe = self.reformatter.reformat( + vframe, + frames_ctx.width, + frames_ctx.height, + get_video_format( + frames_ctx.sw_format, frames_ctx.width, frames_ctx.height + ), + threads=self.ptr.thread_count, + ) + + hwframe: VideoFrame = alloc_video_frame() + err_check(lib.av_hwframe_get_buffer(self.ptr.hw_frames_ctx, hwframe.ptr, 0)) + err_check(lib.av_hwframe_transfer_data(hwframe.ptr, vframe.ptr, 0)) + hwframe._copy_internal_attributes(vframe, data_layout=False) + hwframe._init_user_attributes() + + if hwframe.ptr.pts == lib.AV_NOPTS_VALUE: + hwframe.ptr.pts = self.ptr.frame_num + + return hwframe @cython.cfunc def _prepare_frames_for_encode(self, input: Frame | None) -> list: @@ -79,6 +129,11 @@ def _prepare_frames_for_encode(self, input: Frame | None) -> list: return [None] vframe: VideoFrame = input + + # Hardware-accelerated encoding: upload the (software) frame to the device. + if self.ptr.hw_frames_ctx != cython.NULL: + return [self._encode_upload_frame(vframe)] + if ( vframe.format.pix_fmt != self.pix_fmt or vframe.width != self.ptr.width diff --git a/include/avcodec.pxd b/include/avcodec.pxd index 5671caf41..c94deaaa9 100644 --- a/include/avcodec.pxd +++ b/include/avcodec.pxd @@ -274,6 +274,7 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVHWAccel *hwaccel AVBufferRef *hw_device_ctx + AVBufferRef *hw_frames_ctx int thread_count int thread_type diff --git a/include/avutil.pxd b/include/avutil.pxd index 7b0a9e311..8911adcc0 100644 --- a/include/avutil.pxd +++ b/include/avutil.pxd @@ -216,12 +216,14 @@ cdef extern from "libavutil/hwcontext.h" nogil: AVPixelFormat sw_format int width int height + int initial_pool_size cdef int av_hwdevice_ctx_create(AVBufferRef **device_ctx, AVHWDeviceType type, const char *device, AVDictionary *opts, int flags) cdef AVHWDeviceType av_hwdevice_find_type_by_name(const char *name) cdef const char *av_hwdevice_get_type_name(AVHWDeviceType type) cdef AVHWDeviceType av_hwdevice_iterate_types(AVHWDeviceType prev) cdef int av_hwframe_transfer_data(AVFrame *dst, const AVFrame *src, int flags) + cdef int av_hwframe_get_buffer(AVBufferRef *hwframe_ctx, AVFrame *frame, int flags) cdef AVBufferRef *av_hwframe_ctx_alloc(AVBufferRef *device_ref) cdef int av_hwframe_ctx_init(AVBufferRef *ref) diff --git a/tests/test_encode.py b/tests/test_encode.py index 240c67872..bf432a495 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -2,12 +2,14 @@ import io import math +import os from fractions import Fraction import numpy as np import pytest import av +import av.codec.hwaccel from av import AudioFrame, VideoFrame from av.audio.stream import AudioStream from av.video.stream import VideoStream @@ -504,3 +506,66 @@ def test_profiles(self) -> None: stream.profile = profile print("Set", profile, "got", stream.profile) assert stream.profile == profile + + +# Map a hardware device type to a video encoder that uses it. +_HWACCEL_ENCODERS = { + "vaapi": "h264_vaapi", + "cuda": "h264_nvenc", + "qsv": "h264_qsv", + "videotoolbox": "h264_videotoolbox", +} + + +def test_hardware_encode() -> None: + hwdevices_available = av.codec.hwaccel.hwdevices_available() + if "HWACCEL_DEVICE_TYPE" not in os.environ: + pytest.skip( + "Set the HWACCEL_DEVICE_TYPE to run this test. " + f"Options are {' '.join(hwdevices_available)}" + ) + + device_type = os.environ["HWACCEL_DEVICE_TYPE"] + assert device_type in hwdevices_available, f"{device_type} not available" + + encoder = _HWACCEL_ENCODERS.get(device_type) + if encoder is None: + pytest.skip(f"No hardware encoder mapped for {device_type}") + + width, height, n_frames = 320, 240, 24 + hwaccel = av.codec.hwaccel.HWAccel( + device_type=device_type, allow_software_fallback=False + ) + + file = io.BytesIO() + container = av.open(file, mode="w", format="mp4") + stream = container.add_stream(encoder, rate=30, hwaccel=hwaccel) + assert isinstance(stream, VideoStream) + stream.width = width + stream.height = height + stream.pix_fmt = "nv12" + + # Feed plain software frames; PyAV uploads them to the device for us. + muxed = 0 + for i in range(n_frames): + array = np.full((height, width, 3), i * 8 % 256, dtype=np.uint8) + frame = VideoFrame.from_ndarray(array, format="rgb24") + for packet in stream.encode(frame): + container.mux(packet) + muxed += 1 + + # The hardware frames context must have been set up during open(). + assert stream.codec_context.is_hwaccel + + for packet in stream.encode(): + container.mux(packet) + muxed += 1 + container.close() + + assert muxed > 0 + + # The result must be a valid, decodable H.264 stream. + file.seek(0) + with av.open(file, "r") as in_container: + decoded = sum(1 for _ in in_container.decode(video=0)) + assert decoded == n_frames From 934717aa536c853bfc28864d31b5ff86c499cbf9 Mon Sep 17 00:00:00 2001 From: Carlos Ruiz Date: Tue, 23 Jun 2026 15:39:10 -0700 Subject: [PATCH 815/846] Leverage sws_is_noop for video reformat shortcuts --- av/video/reformatter.pxd | 2 +- av/video/reformatter.py | 93 ++++++++++++++-------------------------- 2 files changed, 34 insertions(+), 61 deletions(-) diff --git a/av/video/reformatter.pxd b/av/video/reformatter.pxd index 5315a7c10..1645d3943 100644 --- a/av/video/reformatter.pxd +++ b/av/video/reformatter.pxd @@ -37,6 +37,7 @@ cdef extern from "libswscale/swscale.h" nogil: cdef SwsContext *sws_alloc_context() cdef void sws_free_context(SwsContext **ctx) + cdef int sws_is_noop(const lib.AVFrame *dst, const lib.AVFrame *src) cdef int sws_scale_frame(SwsContext *c, lib.AVFrame *dst, const lib.AVFrame *src) cdef class VideoReformatter: @@ -46,5 +47,4 @@ cdef class VideoReformatter: int dst_colorspace, int interpolation, int src_color_range, int dst_color_range, int dst_color_trc, int dst_color_primaries, - bint set_dst_color_trc, bint set_dst_color_primaries, int threads) diff --git a/av/video/reformatter.py b/av/video/reformatter.py index eabcefe46..e658ff6b4 100644 --- a/av/video/reformatter.py +++ b/av/video/reformatter.py @@ -224,18 +224,16 @@ def reformat( ) c_src_color_range = _resolve_enum_value(src_color_range, ColorRange, 0) c_dst_color_range = _resolve_enum_value(dst_color_range, ColorRange, 0) - c_dst_color_trc = _resolve_enum_value(dst_color_trc, ColorTrc, 0) + c_dst_color_trc = _resolve_enum_value( + dst_color_trc, ColorTrc, frame.ptr.color_trc + ) c_dst_color_primaries = _resolve_enum_value( - dst_color_primaries, ColorPrimaries, 0 + dst_color_primaries, ColorPrimaries, frame.ptr.color_primaries ) c_threads: cython.int = threads if threads is not None else 0 c_width: cython.int = width if width is not None else frame.ptr.width c_height: cython.int = height if height is not None else frame.ptr.height - # Track whether user explicitly specified destination metadata - set_dst_color_trc: cython.bint = dst_color_trc is not None - set_dst_color_primaries: cython.bint = dst_color_primaries is not None - return self._reformat( frame, c_width, @@ -248,8 +246,6 @@ def reformat( c_dst_color_range, c_dst_color_trc, c_dst_color_primaries, - set_dst_color_trc, - set_dst_color_primaries, c_threads, ) @@ -267,46 +263,41 @@ def _reformat( dst_color_range: cython.int, dst_color_trc: cython.int, dst_color_primaries: cython.int, - set_dst_color_trc: cython.bint, - set_dst_color_primaries: cython.bint, threads: cython.int, ): - if frame.ptr.format < 0: - raise ValueError("Frame does not have format set.") - - src_format = cython.cast(lib.AVPixelFormat, frame.ptr.format) - - # Shortcut! - if ( - dst_format == src_format - and width == frame.ptr.width - and height == frame.ptr.height - and dst_colorspace == src_colorspace - and src_color_range == dst_color_range - and not set_dst_color_trc - and not set_dst_color_primaries - ): - return frame - if frame.ptr.hw_frames_ctx: frame_sw = alloc_video_frame() err_check(lib.av_hwframe_transfer_data(frame_sw.ptr, frame.ptr, 0)) frame_sw._copy_internal_attributes(frame, data_layout=False) + frame_sw._init_user_attributes() frame = frame_sw - src_format = cython.cast(lib.AVPixelFormat, frame.ptr.format) - - # Check for shortcut again, in case dst_format matches the downloaded frame's sw_format - if ( - dst_format == src_format - and width == frame.ptr.width - and height == frame.ptr.height - and dst_colorspace == src_colorspace - and src_color_range == dst_color_range - and not set_dst_color_trc - and not set_dst_color_primaries - ): - frame._init_user_attributes() - return frame + + new_frame: VideoFrame = alloc_video_frame() + new_frame._copy_internal_attributes(frame, data_layout=False) + new_frame.ptr.format = dst_format + new_frame.ptr.width = width + new_frame.ptr.height = height + new_frame.ptr.color_trc = cython.cast( + lib.AVColorTransferCharacteristic, dst_color_trc + ) + new_frame.ptr.color_primaries = cython.cast( + lib.AVColorPrimaries, dst_color_primaries + ) + + # Translate source and destination colorspace/range from SWS_CS_* to AVCOL_* + # so sws_is_noop and sws_scale_frame understand them + frame_src_colorspace: lib.AVColorSpace = frame.ptr.colorspace + frame_src_color_range: lib.AVColorRange = frame.ptr.color_range + _set_frame_colorspace(frame.ptr, src_colorspace, src_color_range) + _set_frame_colorspace(new_frame.ptr, dst_colorspace, dst_color_range) + + # Shortcut if sws_scale_frame would be a no-op + is_noop: cython.bint = sws_is_noop(new_frame.ptr, frame.ptr) != 0 + if is_noop: + # Restore source frame colorspace/range to avoid side effects + frame.ptr.colorspace = frame_src_colorspace + frame.ptr.color_range = frame_src_color_range + return frame if self.ptr == cython.NULL: self.ptr = sws_alloc_context() @@ -315,16 +306,8 @@ def _reformat( self.ptr.threads = threads self.ptr.flags = cython.cast(cython.uint, interpolation) - new_frame: VideoFrame = alloc_video_frame() - new_frame._copy_internal_attributes(frame) + # Allocate frame buffers and perform the conversion new_frame._init(dst_format, width, height) - - # Set source frame colorspace/range so sws_scale_frame can read it - frame_src_colorspace: lib.AVColorSpace = frame.ptr.colorspace - frame_src_color_range: lib.AVColorRange = frame.ptr.color_range - _set_frame_colorspace(frame.ptr, src_colorspace, src_color_range) - _set_frame_colorspace(new_frame.ptr, dst_colorspace, dst_color_range) - with cython.nogil: ret = sws_scale_frame(self.ptr, new_frame.ptr, frame.ptr) @@ -334,14 +317,4 @@ def _reformat( err_check(ret) - # Set metadata-only properties on the output frame if explicitly specified - if set_dst_color_trc: - new_frame.ptr.color_trc = cython.cast( - lib.AVColorTransferCharacteristic, dst_color_trc - ) - if set_dst_color_primaries: - new_frame.ptr.color_primaries = cython.cast( - lib.AVColorPrimaries, dst_color_primaries - ) - return new_frame From 42c164472bb84be451149241d06f71fbf721f9dc Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 24 Jun 2026 16:42:15 -0400 Subject: [PATCH 816/846] Encode GPU frames with pix_fmt=cuda, expose Encode GPU frames with pix_fmt=cuda, expose VideoFrame.sw_format A hardware frame (e.g. a CUDA frame from DLPack) carries its own frames context. nvenc and friends require hw_frames_ctx to be set before avcodec_open2, so adopt the frame's before opening the encoder. This lets GPU frames encode on-device with no download/upload round-trip. Also expose VideoFrame.sw_format, which returns the underlying software pixel format (nv12, yuv444p, p010le, ...) for a hardware frame and None otherwise. closes #2199 --- CHANGELOG.rst | 1 + av/codec/context.py | 11 +++++++++++ av/video/frame.py | 16 ++++++++++++++++ av/video/frame.pyi | 2 ++ tests/test_dlpack.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 73 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 85107acc3..25fd9e9ac 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -47,6 +47,7 @@ Features: Fixes: - Fix ``add_mux_stream`` producing unwritable Matroska files by extracting codec extradata from the bitstream before the header is written by :gh-user:`WyattBlue` (:issue:`2198`). +- Encode GPU frames (e.g. CUDA frames from DLPack) directly with ``pix_fmt="cuda"`` by adopting the frame's ``hw_frames_ctx`` before opening the encoder by :gh-user:`WyattBlue` (:issue:`2199`). v17.1.0 ------- diff --git a/av/codec/context.py b/av/codec/context.py index f842f80c1..27ab8bbef 100644 --- a/av/codec/context.py +++ b/av/codec/context.py @@ -481,6 +481,17 @@ def _prepare_and_time_rebase_frames_for_encode(self, frame: Frame): if self.ptr.codec_type not in [lib.AVMEDIA_TYPE_VIDEO, lib.AVMEDIA_TYPE_AUDIO]: raise NotImplementedError("Encoding is only supported for audio and video.") + # A hardware frame (e.g. a CUDA frame from DLPack) carries its own frames + # context. Encoders like h264_nvenc require hw_frames_ctx to be set before + # avcodec_open2, so adopt the frame's if we don't already have one. + if ( + not self.is_open + and frame is not None + and frame.ptr.hw_frames_ctx != cython.NULL + and self.ptr.hw_frames_ctx == cython.NULL + ): + self.ptr.hw_frames_ctx = lib.av_buffer_ref(frame.ptr.hw_frames_ctx) + self.open(strict=False) frames = self._prepare_frames_for_encode(frame) diff --git a/av/video/frame.py b/av/video/frame.py index 78b97a550..6d6c3f044 100644 --- a/av/video/frame.py +++ b/av/video/frame.py @@ -516,6 +516,22 @@ def _init_user_attributes(self): def __dealloc__(self): lib.av_frame_unref(self.ptr) + @property + def sw_format(self): + """ + For a hardware frame (e.g. ``format.name == "cuda"``), the underlying + software pixel format (``nv12``, ``yuv444p``, ``p010le``, ...). ``None`` + for a regular software frame. + + :type: VideoFormat | None + """ + if not self.ptr.hw_frames_ctx: + return None + frames_ctx: cython.pointer[lib.AVHWFramesContext] = cython.cast( + cython.pointer[lib.AVHWFramesContext], self.ptr.hw_frames_ctx.data + ) + return get_video_format(frames_ctx.sw_format, self.ptr.width, self.ptr.height) + def __repr__(self): return ( f" VideoFormat | None: ... @property def time(self) -> float: ... @property diff --git a/tests/test_dlpack.py b/tests/test_dlpack.py index 13e05fbbc..2e74f342f 100644 --- a/tests/test_dlpack.py +++ b/tests/test_dlpack.py @@ -1,4 +1,6 @@ import gc +from fractions import Fraction +from typing import cast import numpy import pytest @@ -6,6 +8,7 @@ import av from av import VideoFrame from av.codec.hwaccel import HWAccel +from av.video.codeccontext import VideoCodecContext from .common import assertNdarraysEqual, fate_png @@ -283,6 +286,10 @@ def test_video_plane_dlpack_unsupported_format_raises() -> None: frame.planes[0].__dlpack__() +def test_sw_format_none_for_software_frame() -> None: + assert VideoFrame(16, 16, "yuv420p").sw_format is None + + def test_video_frame_from_dlpack_requires_two_planes() -> None: y = numpy.zeros((4, 4), dtype=numpy.uint8) with pytest.raises(ValueError, match="2-plane"): @@ -612,3 +619,39 @@ def test_video_frame_from_dlpack_cuda_hw_frame_behavior_if_available() -> None: frame.to_ndarray(format="cuda") except av.FFmpegError as e: pytest.skip(f"CUDA hwcontext not available in this build/runtime: {e}") + + +def test_encode_cuda_frame_with_nvenc_if_available() -> None: + # Issue #2199: a CUDA frame from DLPack should encode on the GPU directly. + # Its hw_frames_ctx must propagate to the encoder before avcodec_open2. + backend = _get_cuda_backend() + if backend is None: + pytest.skip("CUDA backend (cupy/torch) not available.") + + name, mod = backend + width, height = 256, 256 + + try: + if name == "torch": + y = mod.zeros((height, width), dtype=mod.uint8, device="cuda") + uv = mod.zeros((height // 2, width // 2, 2), dtype=mod.uint8, device="cuda") + else: + y = mod.zeros((height, width), dtype=mod.uint8) + uv = mod.zeros((height // 2, width // 2, 2), dtype=mod.uint8) + + frame = VideoFrame.from_dlpack((y, uv), format="nv12") + assert frame.format.name == "cuda" + assert frame.sw_format is not None and frame.sw_format.name == "nv12" + + cc = cast(VideoCodecContext, av.CodecContext.create("h264_nvenc", "w")) + cc.width = width + cc.height = height + cc.time_base = Fraction(1, 24) + cc.framerate = Fraction(24, 1) + cc.pix_fmt = "cuda" + + packets = cc.encode(frame) + packets += cc.encode(None) # flush + assert any(p.size for p in packets) + except av.FFmpegError as e: + pytest.skip(f"nvenc/CUDA not available in this build/runtime: {e}") From a4167d5f4869b3fca277b02ff11ae9d4b0b7bb81 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 24 Jun 2026 16:53:50 -0400 Subject: [PATCH 817/846] Avoid reassigns --- av/container/pyio.py | 2 +- av/opaque.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/av/container/pyio.py b/av/container/pyio.py index 34a497246..0f7af6eb7 100644 --- a/av/container/pyio.py +++ b/av/container/pyio.py @@ -38,7 +38,7 @@ def __cinit__(self, file, buffer_size, writeable=None): and self.ftell is not None and (seekable is None or seekable()) ): - seek_func: seek_func_t = pyio_seek + seek_func = pyio_seek if writeable is None: writeable = self.fwrite is not None diff --git a/av/opaque.py b/av/opaque.py index e55468c44..281033258 100644 --- a/av/opaque.py +++ b/av/opaque.py @@ -55,4 +55,4 @@ def pop(self, name) -> object: return self._objects.pop(key, None) -opaque_container: OpaqueContainer = OpaqueContainer() +opaque_container = OpaqueContainer() From dd22c3792ac3217afc09409bc2b6d14e0c427a59 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 24 Jun 2026 17:19:59 -0400 Subject: [PATCH 818/846] Simplify code --- av/datasets.py | 8 +--- av/error.py | 92 ++++++++------------------------------- av/filter/loudnorm_impl.c | 83 +++++------------------------------ 3 files changed, 29 insertions(+), 154 deletions(-) diff --git a/av/datasets.py b/av/datasets.py index 237b3bf04..8547c1507 100644 --- a/av/datasets.py +++ b/av/datasets.py @@ -1,6 +1,7 @@ import errno import logging import os +import shutil import sys from collections.abc import Iterator from urllib.request import urlopen @@ -85,12 +86,7 @@ def cached_download(url: str, name: str) -> str: tmp_path = path + ".tmp" with open(tmp_path, "wb") as fh: - while True: - chunk = response.read(8196) - if chunk: - fh.write(chunk) - else: - break + shutil.copyfileobj(response, fh) os.rename(tmp_path, path) diff --git a/av/error.py b/av/error.py index 32fb4d171..941dfcbb8 100644 --- a/av/error.py +++ b/av/error.py @@ -1,3 +1,4 @@ +import enum import errno import os import sys @@ -19,7 +20,6 @@ "HTTPClientError", "UndefinedError", ] -sentinel = cython.declare(object, object()) @cython.ccall @@ -169,86 +169,28 @@ class HTTPClientError(FFmpegError): # fmt: on -class EnumType(type): - def __new__(mcl, name, bases, attrs, *args): - # Just adapting the method signature. - return super().__new__(mcl, name, bases, attrs) - - def __init__(self, name, bases, attrs, items): - self._by_name = {} - self._by_value = {} - self._all = [] - - for spec in items: - self._create(*spec) - - def _create(self, name, value, doc=None, by_value_only=False): - # We only have one instance per value. - try: - item = self._by_value[value] - except KeyError: - item = self(sentinel, name, value, doc) - self._by_value[value] = item - - return item - - def __len__(self): - return len(self._all) - - def __iter__(self): - return iter(self._all) - - -@cython.cclass -class EnumItem: - """An enumeration of FFmpeg's error types. - - .. attribute:: tag - - The FFmpeg byte tag for the error. - - .. attribute:: strerror - - The error message that would be returned. - """ - - name = cython.declare(str, visibility="readonly") - value = cython.declare(cython.int, visibility="readonly") - - def __cinit__(self, sentinel_, name: str, value: cython.int, doc=None): - if sentinel_ is not sentinel: - raise RuntimeError(f"Cannot instantiate {self.__class__.__name__}.") - - self.name = name - self.value = value - self.__doc__ = doc - - def __repr__(self): - return f"<{self.__class__.__module__}.{self.__class__.__name__}:{self.name}(0x{self.value:x})>" - - def __str__(self): - return self.name +ErrorType = enum.IntEnum( + "ErrorType", + [(name, value) for name, value, *_ in _ffmpeg_specs], + module=__name__, +) +ErrorType.__doc__ = "An enumeration of FFmpeg's error types." - def __int__(self): - return self.value - @property - def tag(self): - return code_to_tag(self.value) +def _error_type_tag(self) -> bytes: + """The FFmpeg byte tag for the error.""" + return code_to_tag(self.value) -ErrorType = EnumType( - "ErrorType", (EnumItem,), {"__module__": __name__}, [x[:2] for x in _ffmpeg_specs] -) +def _error_type_strerror(self) -> str: + """The error message that would be returned.""" + if self.value == c_PYAV_STASHED_ERROR: + return PYAV_STASHED_ERROR_message + return lib.av_err2str(-self.value) -for enum in ErrorType: - # Mimick the errno module. - globals()[enum.name] = enum - if enum.value == c_PYAV_STASHED_ERROR: - enum.strerror = PYAV_STASHED_ERROR_message - else: - enum.strerror = lib.av_err2str(-enum.value) +ErrorType.tag = property(_error_type_tag) +ErrorType.strerror = property(_error_type_strerror) classes: dict = {} diff --git a/av/filter/loudnorm_impl.c b/av/filter/loudnorm_impl.c index 19cb1c554..5d43c28b8 100644 --- a/av/filter/loudnorm_impl.c +++ b/av/filter/loudnorm_impl.c @@ -3,51 +3,23 @@ #include #include #include +#include #include -#ifdef _WIN32 - #include -#else - #include -#endif - -#ifdef _WIN32 - static CRITICAL_SECTION json_mutex; - static CONDITION_VARIABLE json_cond; - static int mutex_initialized = 0; -#else - static pthread_mutex_t json_mutex = PTHREAD_MUTEX_INITIALIZER; - static pthread_cond_t json_cond = PTHREAD_COND_INITIALIZER; -#endif - static char json_buffer[2048] = {0}; static int json_captured = 0; -// Custom logging callback +// Custom logging callback. The loudnorm filter prints its stats as a JSON line. static void logging_callback(void *ptr, int level, const char *fmt, va_list vl) { char line[2048]; vsnprintf(line, sizeof(line), fmt, vl); const char *json_start = strstr(line, "{"); if (json_start) { - #ifdef _WIN32 - EnterCriticalSection(&json_mutex); - #else - pthread_mutex_lock(&json_mutex); - #endif - size_t len = strnlen(json_start, sizeof(json_buffer) - 1); memcpy(json_buffer, json_start, len); json_buffer[len] = '\0'; json_captured = 1; - - #ifdef _WIN32 - WakeConditionVariable(&json_cond); - LeaveCriticalSection(&json_mutex); - #else - pthread_cond_signal(&json_cond); - pthread_mutex_unlock(&json_mutex); - #endif } } @@ -74,15 +46,6 @@ char* loudnorm_get_stats( json_captured = 0; // Reset the captured flag memset(json_buffer, 0, sizeof(json_buffer)); // Clear the buffer - #ifdef _WIN32 - // Initialize synchronization objects if needed - if (!mutex_initialized) { - InitializeCriticalSection(&json_mutex); - InitializeConditionVariable(&json_cond); - mutex_initialized = 1; - } - #endif - av_log_set_callback(logging_callback); AVFilterGraph *filter_graph = NULL; @@ -181,7 +144,8 @@ char* loudnorm_get_stats( end: // Freeing the graph uninits the loudnorm filter, which is what makes it - // emit its JSON stats through our log callback. Safe to call on NULL. + // emit its JSON stats through our log callback. This happens synchronously + // on this thread, so json_buffer is populated by the time the call returns. avfilter_graph_free(&filter_graph); avcodec_free_context(&codec_ctx); avformat_close_input(&fmt_ctx); @@ -189,39 +153,12 @@ char* loudnorm_get_stats( av_frame_free(&frame); av_packet_free(&packet); - // If the graph never configured we produced no stats; don't block waiting - // for JSON that will never arrive. Leave result NULL so the caller raises. - if (graph_configured) { - #ifdef _WIN32 - EnterCriticalSection(&json_mutex); - while (!json_captured) { - if (!SleepConditionVariableCS(&json_cond, &json_mutex, 5000)) { // 5 second timeout - fprintf(stderr, "Timeout waiting for JSON data\n"); - break; - } - } - if (json_captured) { - result = _strdup(json_buffer); // Use _strdup on Windows - } - LeaveCriticalSection(&json_mutex); - #else - struct timespec timeout; - clock_gettime(CLOCK_REALTIME, &timeout); - timeout.tv_sec += 5; // 5 second timeout - - pthread_mutex_lock(&json_mutex); - while (json_captured == 0) { - int ret = pthread_cond_timedwait(&json_cond, &json_mutex, &timeout); - if (ret == ETIMEDOUT) { - fprintf(stderr, "Timeout waiting for JSON data\n"); - break; - } - } - if (json_captured) { - result = strdup(json_buffer); - } - pthread_mutex_unlock(&json_mutex); - #endif + if (graph_configured && json_captured) { +#ifdef _WIN32 + result = _strdup(json_buffer); +#else + result = strdup(json_buffer); +#endif } av_log_set_callback(av_log_default_callback); From 3c95a75394ddf0957f97f5bae339dafb757c02ad Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 24 Jun 2026 19:51:04 -0400 Subject: [PATCH 819/846] actions/checkout 6 => 7 --- .github/workflows/smoke.yml | 8 ++++---- .github/workflows/tests.yml | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 7eadf3733..9ff147c7d 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-slim steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Python uses: actions/setup-python@v6 with: @@ -46,7 +46,7 @@ jobs: PYAV_LIBRARY: ffmpeg-${{ matrix.config.ffmpeg }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 name: Checkout - name: Python ${{ matrix.config.python }} @@ -126,7 +126,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up Conda shell: bash @@ -162,7 +162,7 @@ jobs: name: "armv7 cross-build + qemu smoke" runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Set up host Python 3.11 uses: actions/setup-python@v6 with: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 030cddb90..6fb97330a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,7 +7,7 @@ jobs: package-source: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/setup-python@v6 with: python-version: "3.14" @@ -41,7 +41,7 @@ jobs: - os: windows-11-arm arch: ARM64 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/setup-python@v6 with: python-version: "3.14" @@ -82,7 +82,7 @@ jobs: package-wheel-armv7l: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Set up host Python 3.11 uses: actions/setup-python@v6 with: @@ -115,7 +115,7 @@ jobs: runs-on: ubuntu-latest needs: [package-source, package-wheel, package-wheel-armv7l] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/download-artifact@v7 with: merge-multiple: true From d0d7746530ddfa933f80bb9a45a0e946521c1590 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 24 Jun 2026 21:17:23 -0400 Subject: [PATCH 820/846] Expose sw_format on VideoCodecContext --- av/video/codeccontext.py | 16 ++++++++++++++++ av/video/codeccontext.pyi | 1 + 2 files changed, 17 insertions(+) diff --git a/av/video/codeccontext.py b/av/video/codeccontext.py index 457ef43e9..ef39c0870 100644 --- a/av/video/codeccontext.py +++ b/av/video/codeccontext.py @@ -251,6 +251,22 @@ def pix_fmt(self): def pix_fmt(self, value): self.ptr.pix_fmt = get_pix_fmt(value) + @property + def sw_format(self): + """ + For a hardware context (e.g. ``format.name == "cuda"``), the underlying + software pixel format (``nv12``, ``p010le``, ...). ``None`` for a regular + software context. + + :type: VideoFormat | None + """ + if not self.ptr.hw_frames_ctx: + return None + frames_ctx: cython.pointer[lib.AVHWFramesContext] = cython.cast( + cython.pointer[lib.AVHWFramesContext], self.ptr.hw_frames_ctx.data + ) + return get_video_format(frames_ctx.sw_format, self.ptr.width, self.ptr.height) + @property def framerate(self): """ diff --git a/av/video/codeccontext.pyi b/av/video/codeccontext.pyi index 280f5cdc0..6b5e27d59 100644 --- a/av/video/codeccontext.pyi +++ b/av/video/codeccontext.pyi @@ -14,6 +14,7 @@ class VideoCodecContext(CodecContext): height: int bits_per_coded_sample: int pix_fmt: str | None + sw_format: VideoFormat | None framerate: Fraction rate: Fraction gop_size: int From e5104e4d3975b7f21f8de649a56ad2d2a0f3417f Mon Sep 17 00:00:00 2001 From: Carlos Ruiz Date: Mon, 29 Jun 2026 11:47:11 -0700 Subject: [PATCH 821/846] Allow configuring hardware encoder sw_format --- av/codec/codec.pyi | 2 ++ av/codec/context.py | 11 +++++++-- av/codec/hwaccel.pyi | 2 +- av/video/codeccontext.py | 10 ++++++++ av/video/codeccontext.pyi | 5 +++- tests/test_encode.py | 49 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 75 insertions(+), 4 deletions(-) diff --git a/av/codec/codec.pyi b/av/codec/codec.pyi index a9876c4f5..8631790b2 100644 --- a/av/codec/codec.pyi +++ b/av/codec/codec.pyi @@ -10,6 +10,7 @@ from av.video.codeccontext import VideoCodecContext from av.video.format import VideoFormat from .context import CodecContext +from .hwaccel import HWConfig class Properties(Flag): NONE = cast(ClassVar[Properties], ...) @@ -78,6 +79,7 @@ class Codec: audio_rates: list[int] | None video_formats: list[VideoFormat] | None audio_formats: list[AudioFormat] | None + hardware_configs: list[HWConfig] @property def properties(self) -> int: ... diff --git a/av/codec/context.py b/av/codec/context.py index 27ab8bbef..6fefb1857 100644 --- a/av/codec/context.py +++ b/av/codec/context.py @@ -396,9 +396,16 @@ def _setup_encode_hwframes(self) -> cython.void: return # Already set up. hw_format: lib.AVPixelFormat = self.hwaccel_ctx.config.ptr.pix_fmt - sw_format: lib.AVPixelFormat = cython.cast(lib.AVPixelFormat, self.ptr.pix_fmt) + sw_format: lib.AVPixelFormat = cython.cast( + lib.AVPixelFormat, self.ptr.sw_pix_fmt + ) + + # The codec context's sw_pix_fmt holds the software format the user + # wants the hardware frames context to use. Fall back to pix_fmt to + # preserve the existing stream.pix_fmt configuration path. + if sw_format == lib.AV_PIX_FMT_NONE: + sw_format = cython.cast(lib.AVPixelFormat, self.ptr.pix_fmt) - # The codec context's pix_fmt holds the *software* format the user feeds in. # If they left it as the hardware format (or unset), pick a sane default. if sw_format == hw_format or sw_format == lib.AV_PIX_FMT_NONE: sw_format = lib.av_get_pix_fmt(b"nv12") diff --git a/av/codec/hwaccel.pyi b/av/codec/hwaccel.pyi index a9135f683..b3b0c3ff0 100644 --- a/av/codec/hwaccel.pyi +++ b/av/codec/hwaccel.pyi @@ -30,7 +30,7 @@ class HWConfig: @property def device_type(self) -> HWDeviceType: ... @property - def format(self) -> VideoFormat: ... + def format(self) -> VideoFormat | None: ... @property def methods(self) -> HWConfigMethod: ... @property diff --git a/av/video/codeccontext.py b/av/video/codeccontext.py index ef39c0870..f213b9860 100644 --- a/av/video/codeccontext.py +++ b/av/video/codeccontext.py @@ -261,12 +261,22 @@ def sw_format(self): :type: VideoFormat | None """ if not self.ptr.hw_frames_ctx: + if self.ptr.sw_pix_fmt != lib.AV_PIX_FMT_NONE: + return get_video_format( + cython.cast(lib.AVPixelFormat, self.ptr.sw_pix_fmt), + self.ptr.width, + self.ptr.height, + ) return None frames_ctx: cython.pointer[lib.AVHWFramesContext] = cython.cast( cython.pointer[lib.AVHWFramesContext], self.ptr.hw_frames_ctx.data ) return get_video_format(frames_ctx.sw_format, self.ptr.width, self.ptr.height) + @sw_format.setter + def sw_format(self, value): + self.ptr.sw_pix_fmt = get_pix_fmt(value) + @property def framerate(self): """ diff --git a/av/video/codeccontext.pyi b/av/video/codeccontext.pyi index 6b5e27d59..603dc4dcb 100644 --- a/av/video/codeccontext.pyi +++ b/av/video/codeccontext.pyi @@ -14,7 +14,10 @@ class VideoCodecContext(CodecContext): height: int bits_per_coded_sample: int pix_fmt: str | None - sw_format: VideoFormat | None + @property + def sw_format(self) -> VideoFormat | None: ... + @sw_format.setter + def sw_format(self, value: str) -> None: ... framerate: Fraction rate: Fraction gop_size: int diff --git a/tests/test_encode.py b/tests/test_encode.py index bf432a495..294cc815b 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -517,6 +517,13 @@ def test_profiles(self) -> None: } +def get_hwaccel_format(encoder: str, device_type: str) -> str: + for config in av.Codec(encoder, "w").hardware_configs: + if config.device_type.name == device_type and config.format is not None: + return config.format.name + pytest.skip(f"No hardware format for {device_type} on {encoder}") + + def test_hardware_encode() -> None: hwdevices_available = av.codec.hwaccel.hwdevices_available() if "HWACCEL_DEVICE_TYPE" not in os.environ: @@ -569,3 +576,45 @@ def test_hardware_encode() -> None: with av.open(file, "r") as in_container: decoded = sum(1 for _ in in_container.decode(video=0)) assert decoded == n_frames + + +def test_hardware_encode_honors_sw_format() -> None: + hwdevices_available = av.codec.hwaccel.hwdevices_available() + if "HWACCEL_DEVICE_TYPE" not in os.environ: + pytest.skip( + "Set the HWACCEL_DEVICE_TYPE to run this test. " + f"Options are {' '.join(hwdevices_available)}" + ) + + device_type = os.environ["HWACCEL_DEVICE_TYPE"] + assert device_type in hwdevices_available, f"{device_type} not available" + + encoder = _HWACCEL_ENCODERS.get(device_type) + if encoder is None: + pytest.skip(f"No hardware encoder mapped for {device_type}") + hw_format = get_hwaccel_format(encoder, device_type) + + hwaccel = av.codec.hwaccel.HWAccel( + device_type=device_type, allow_software_fallback=False + ) + container = av.open(io.BytesIO(), mode="w", format="mp4") + stream = container.add_stream(encoder, rate=30, hwaccel=hwaccel) + assert isinstance(stream, VideoStream) + stream.width = 320 + stream.height = 240 + stream.pix_fmt = hw_format + stream.codec_context.sw_format = "yuv420p" + + assert stream.codec_context.sw_format is not None + assert stream.codec_context.sw_format.name == "yuv420p" + + frame = VideoFrame(320, 240, "rgb24") + for packet in stream.encode(frame): + container.mux(packet) + + assert stream.codec_context.pix_fmt == hw_format + assert stream.codec_context.sw_format is not None + assert stream.codec_context.sw_format.name == "yuv420p" + for packet in stream.encode(): + container.mux(packet) + container.close() From f5f5f7b0258a2610fcdfdc399cb92d491a6b467c Mon Sep 17 00:00:00 2001 From: Carlos Domin Date: Mon, 29 Jun 2026 17:18:49 -0700 Subject: [PATCH 822/846] Return None for unset video codec pix_fmt Signed-off-by: Carlos Domin --- av/video/codeccontext.py | 2 ++ tests/test_codec_context.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/av/video/codeccontext.py b/av/video/codeccontext.py index f213b9860..d3f36514c 100644 --- a/av/video/codeccontext.py +++ b/av/video/codeccontext.py @@ -245,6 +245,8 @@ def pix_fmt(self): desc: cython.pointer[cython.const[lib.AVPixFmtDescriptor]] = ( lib.av_pix_fmt_desc_get(cython.cast(lib.AVPixelFormat, self.ptr.pix_fmt)) ) + if desc == cython.NULL: + return None return cython.cast(str, desc.name) @pix_fmt.setter diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 164ce73fb..730b50bbe 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -150,6 +150,8 @@ def test_encoder_extradata(self) -> None: def test_encoder_pix_fmt(self) -> None: ctx = av.codec.Codec("h264", "w").create("video") + assert ctx.pix_fmt is None + # valid format ctx.pix_fmt = "yuv420p" assert ctx.pix_fmt == "yuv420p" From daa823166e9315fd443f9fcdb18e0821163dda46 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 29 Jun 2026 23:27:16 -0400 Subject: [PATCH 823/846] We set `-Wno-incompatible-pointer-types` in setup.py --- Makefile | 2 +- scripts/build-armv7l-cross.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index d6edf1893..4fb974eb2 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ LDFLAGS ?= "" -CFLAGS ?= "-O0 -Wno-incompatible-pointer-types -Wno-unreachable-code" +CFLAGS ?= "-O0 -Wno-unreachable-code" PYAV_PYTHON ?= python PYAV_PIP ?= pip diff --git a/scripts/build-armv7l-cross.sh b/scripts/build-armv7l-cross.sh index f304d51fd..f7e97ec42 100755 --- a/scripts/build-armv7l-cross.sh +++ b/scripts/build-armv7l-cross.sh @@ -113,7 +113,7 @@ build_one() { _PYTHON_HOST_PLATFORM=linux-armv7l \ _PYTHON_SYSCONFIGDATA_NAME="$(basename "${scd[0]}" .py)" \ PYTHONPATH="$(dirname "${scd[0]}")" \ - CFLAGS="-I${inc[0]} -O2 -DNDEBUG -Wno-error=incompatible-pointer-types" \ + CFLAGS="-I${inc[0]} -O2 -DNDEBUG" \ PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig \ LD_LIBRARY_PATH=/tmp/vendor/lib \ "$host_py" -m pip wheel . --no-build-isolation --no-deps -w "$raw" From 69ecd269c5b37caeafdff17aec0f0fefba834e61 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 29 Jun 2026 23:31:55 -0400 Subject: [PATCH 824/846] Update ffmpeg binary --- scripts/ffmpeg-8.1.json | 2 +- scripts/ffmpeg-latest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/ffmpeg-8.1.json b/scripts/ffmpeg-8.1.json index 715c177bf..58dc5ae29 100644 --- a/scripts/ffmpeg-8.1.json +++ b/scripts/ffmpeg-8.1.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.1.1-2/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.1.2-1/ffmpeg-{platform}.tar.gz" } diff --git a/scripts/ffmpeg-latest.json b/scripts/ffmpeg-latest.json index 715c177bf..58dc5ae29 100644 --- a/scripts/ffmpeg-latest.json +++ b/scripts/ffmpeg-latest.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.1.1-2/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.1.2-1/ffmpeg-{platform}.tar.gz" } From 52b8f6bdcd865f42c96844dfe8421675c37063cc Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 1 Jul 2026 15:42:56 -0400 Subject: [PATCH 825/846] Make transfer/primaries conversion opt-in in reformat, fixes #2208 sws_scale_frame (used since 17.0.0) validates color_trc/color_primaries and rejects RESERVED and other unsupported values (e.g. LOG) with EOPNOTSUPP, regressing plain reformat/to_ndarray to rgb24 on VP9 and NVDEC frames. The pre-17.0 sws_scale ignored these fields. Neutralize color_trc/color_primaries to UNSPECIFIED for the scale unless a destination value is explicitly requested, while preserving the source's tags on the returned frame. The YUV->RGB matrix and explicit conversions are unaffected. --- CHANGELOG.rst | 1 + av/video/reformatter.py | 57 +++++++++++++++++++++++++++++++++------- tests/test_colorspace.py | 47 +++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 25fd9e9ac..3152c1f56 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -46,6 +46,7 @@ Features: Fixes: +- Fix ``VideoFrame.reformat`` (and ``to_ndarray``/``to_rgb``/``to_image``) raising ``OSError`` ``Operation not supported`` on frames tagged with reserved or otherwise unsupported ``color_primaries``/``color_trc`` values (e.g. VP9 and NVDEC output); a transfer/primaries conversion is now only performed when explicitly requested by :gh-user:`WyattBlue` (:issue:`2208`). - Fix ``add_mux_stream`` producing unwritable Matroska files by extracting codec extradata from the bitstream before the header is written by :gh-user:`WyattBlue` (:issue:`2198`). - Encode GPU frames (e.g. CUDA frames from DLPack) directly with ``pix_fmt="cuda"`` by adopting the frame's ``hw_frames_ctx`` before opening the encoder by :gh-user:`WyattBlue` (:issue:`2199`). diff --git a/av/video/reformatter.py b/av/video/reformatter.py index e658ff6b4..59725571b 100644 --- a/av/video/reformatter.py +++ b/av/video/reformatter.py @@ -224,11 +224,14 @@ def reformat( ) c_src_color_range = _resolve_enum_value(src_color_range, ColorRange, 0) c_dst_color_range = _resolve_enum_value(dst_color_range, ColorRange, 0) + # Default to UNSPECIFIED (not the source's value) so that a transfer / + # primaries conversion is only performed when explicitly requested. See + # _reformat for why. c_dst_color_trc = _resolve_enum_value( - dst_color_trc, ColorTrc, frame.ptr.color_trc + dst_color_trc, ColorTrc, lib.AVCOL_TRC_UNSPECIFIED ) c_dst_color_primaries = _resolve_enum_value( - dst_color_primaries, ColorPrimaries, frame.ptr.color_primaries + dst_color_primaries, ColorPrimaries, lib.AVCOL_PRI_UNSPECIFIED ) c_threads: cython.int = threads if threads is not None else 0 c_width: cython.int = width if width is not None else frame.ptr.width @@ -277,12 +280,37 @@ def _reformat( new_frame.ptr.format = dst_format new_frame.ptr.width = width new_frame.ptr.height = height - new_frame.ptr.color_trc = cython.cast( - lib.AVColorTransferCharacteristic, dst_color_trc - ) - new_frame.ptr.color_primaries = cython.cast( - lib.AVColorPrimaries, dst_color_primaries + + # A transfer-characteristic / primaries conversion is opt-in. Unlike the + # pre-17.0 sws_scale, sws_scale_frame inspects color_trc/color_primaries + # and rejects RESERVED (and other unsupported) values with EOPNOTSUPP, + # which regressed plain reformats of e.g. VP9 / NVDEC frames (#2208). So + # only feed these fields to swscale when the caller explicitly requested a + # destination value; otherwise neutralize them for the scale (as the old + # sws_scale effectively did) while still preserving the source's tags on + # the returned frame's metadata. + convert_trc: cython.bint = dst_color_trc != lib.AVCOL_TRC_UNSPECIFIED + convert_primaries: cython.bint = ( + dst_color_primaries != lib.AVCOL_PRI_UNSPECIFIED ) + frame_src_color_trc: lib.AVColorTransferCharacteristic = frame.ptr.color_trc + frame_src_color_primaries: lib.AVColorPrimaries = frame.ptr.color_primaries + + if convert_trc: + new_frame.ptr.color_trc = cython.cast( + lib.AVColorTransferCharacteristic, dst_color_trc + ) + else: + frame.ptr.color_trc = lib.AVCOL_TRC_UNSPECIFIED + new_frame.ptr.color_trc = lib.AVCOL_TRC_UNSPECIFIED + + if convert_primaries: + new_frame.ptr.color_primaries = cython.cast( + lib.AVColorPrimaries, dst_color_primaries + ) + else: + frame.ptr.color_primaries = lib.AVCOL_PRI_UNSPECIFIED + new_frame.ptr.color_primaries = lib.AVCOL_PRI_UNSPECIFIED # Translate source and destination colorspace/range from SWS_CS_* to AVCOL_* # so sws_is_noop and sws_scale_frame understand them @@ -294,9 +322,11 @@ def _reformat( # Shortcut if sws_scale_frame would be a no-op is_noop: cython.bint = sws_is_noop(new_frame.ptr, frame.ptr) != 0 if is_noop: - # Restore source frame colorspace/range to avoid side effects + # Restore source frame metadata to avoid side effects frame.ptr.colorspace = frame_src_colorspace frame.ptr.color_range = frame_src_color_range + frame.ptr.color_trc = frame_src_color_trc + frame.ptr.color_primaries = frame_src_color_primaries return frame if self.ptr == cython.NULL: @@ -311,9 +341,18 @@ def _reformat( with cython.nogil: ret = sws_scale_frame(self.ptr, new_frame.ptr, frame.ptr) - # Restore source frame colorspace/range to avoid side effects + # Restore source frame metadata to avoid side effects frame.ptr.colorspace = frame_src_colorspace frame.ptr.color_range = frame_src_color_range + frame.ptr.color_trc = frame_src_color_trc + frame.ptr.color_primaries = frame_src_color_primaries + + # Preserve the source's transfer/primaries on the output when no explicit + # conversion was requested (the scale ran with neutralized tags). + if not convert_trc: + new_frame.ptr.color_trc = frame_src_color_trc + if not convert_primaries: + new_frame.ptr.color_primaries = frame_src_color_primaries err_check(ret) diff --git a/tests/test_colorspace.py b/tests/test_colorspace.py index 6afd42cf0..1dec93e20 100644 --- a/tests/test_colorspace.py +++ b/tests/test_colorspace.py @@ -121,3 +121,50 @@ def test_reformat_dst_colorspace_metadata( frame = av.VideoFrame(width=64, height=64, format="yuv420p") rgb = frame.reformat(format="rgb24", dst_colorspace=colorspace) assert rgb.colorspace == expected + + +# RESERVED0 (0) and RESERVED (3) primaries/transfer values, plus a couple of +# transfer functions swscale can't handle (LOG / LOG_SQRT). Real VP9 and NVDEC +# streams routinely tag frames with these. sws_scale_frame (used since 17.0.0) +# validates these fields and rejects them with EOPNOTSUPP, which regressed a +# plain reformat/to_ndarray to "rgb24" (#2208). The pre-17.0 sws_scale ignored +# them, and a transfer/primaries conversion should stay opt-in. +@pytest.mark.parametrize( + ("color_primaries", "color_trc"), + [ + (3, 3), # RESERVED / RESERVED + (0, 0), # RESERVED0 / RESERVED0 + (3, 2), # reserved primaries only + (2, 3), # reserved transfer only + (2, 9), # AVCOL_TRC_LOG (unsupported by swscale) + (2, 10), # AVCOL_TRC_LOG_SQRT (unsupported by swscale) + ], +) +def test_reformat_unsupported_color_metadata( + color_primaries: int, color_trc: int +) -> None: + frame = av.VideoFrame(width=64, height=64, format="yuv420p") + frame.colorspace = Colorspace.ITU709 + frame.color_primaries = color_primaries + frame.color_trc = color_trc + + # Neither of these should raise OSError(EOPNOTSUPP). + rgb = frame.reformat(format="rgb24") + assert rgb.format.name == "rgb24" + array = frame.to_ndarray(format="rgb24") + assert array.shape == (64, 64, 3) + + # The reformat must not mutate the source frame's metadata. + assert frame.color_primaries == color_primaries + assert frame.color_trc == color_trc + + # The BT.709 matrix is still applied even though the transfer/primaries are + # unsupported: a neutral gray must stay gray. + gray = av.VideoFrame(width=64, height=64, format="yuv420p") + gray.colorspace = Colorspace.ITU709 + gray.color_primaries = color_primaries + gray.color_trc = color_trc + for plane, value in zip(gray.planes, (128, 128, 128)): + plane.update(bytes([value]) * plane.buffer_size) + out = gray.to_ndarray(format="rgb24") + assert out.min() == out.max() == out[0, 0, 0] From debd890b7f2fa461f57133bdb904546b7fccec3d Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 1 Jul 2026 16:47:31 -0400 Subject: [PATCH 826/846] Accept Codec or codec name as BitStreamFilterContext in_stream in_stream now takes a Stream, a Codec, a codec-name str, or None. A Stream still copies the full input codec parameters, while a Codec or name only pins par_in->codec_id/codec_type, which is enough for codec-specific filters to initialize (filters that also need extradata still require a Stream). Also corrects the AVBSFContext.par_in/par_out declarations to non-const to match FFmpeg's header. --- CHANGELOG.rst | 1 + av/bitstream.py | 21 +++++++++++++++++---- av/bitstream.pyi | 3 ++- include/avcodec.pxd | 4 ++-- tests/test_bitstream.py | 16 ++++++++++++++++ 5 files changed, 38 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3152c1f56..19ac4ed87 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -43,6 +43,7 @@ Features: - Add ``at`` parameter to ``Graph.push`` and ``Graph.vpush`` to push a frame to a single buffer source by index, for multi-input filters like ``overlay`` by :gh-user:`WyattBlue`. - ``find_best_pix_fmt_of_list`` now returns the loss as a ``PixFmtLoss`` ``enum.IntFlag`` instead of a plain ``int`` by :gh-user:`WyattBlue` (:issue:`2300`). - Add ``Colorspace.BT2020`` by :gh-user:`mark-oshea`. +- ``BitStreamFilterContext`` now accepts a ``Codec`` or a codec-name ``str`` as ``in_stream`` to pin the input codec without a full ``Stream`` by :gh-user:`WyattBlue`. Fixes: diff --git a/av/bitstream.py b/av/bitstream.py index db808ab35..822e710a5 100644 --- a/av/bitstream.py +++ b/av/bitstream.py @@ -1,5 +1,6 @@ import cython import cython.cimports.libav as lib +from cython.cimports.av.codec.codec import Codec from cython.cimports.av.error import err_check from cython.cimports.av.packet import Packet from cython.cimports.av.stream import Stream @@ -14,14 +15,18 @@ class BitStreamFilterContext: Wraps :ffmpeg:`AVBSFContext` - :param Stream in_stream: A stream that defines the input codec for the bitfilter. + :param in_stream: Defines the input codec for the bitfilter. A :class:`.Stream` + copies the full input codec parameters, while a :class:`.Codec` or a codec-name + ``str`` only pins the input codec, which is all a codec-specific filter (such as + ``h264_mp4toannexb``) needs to initialize. + :type in_stream: :class:`.Stream`, :class:`.Codec`, str, or None :param Stream out_stream: A stream whose codec is overwritten using the output parameters from the bitfilter. """ def __cinit__( self, filter_description, - in_stream: Stream | None = None, + in_stream: Stream | Codec | str | None = None, out_stream: Stream | None = None, ): res: cython.int @@ -31,12 +36,20 @@ def __cinit__( res = lib.av_bsf_list_parse_str(filter_str, cython.address(self.ptr)) err_check(res) - if in_stream is not None: + if isinstance(in_stream, Stream): with cython.nogil: res = lib.avcodec_parameters_copy( - self.ptr.par_in, in_stream.ptr.codecpar + self.ptr.par_in, cython.cast(Stream, in_stream).ptr.codecpar ) err_check(res) + elif in_stream is not None: + # A Codec or codec name only pins the input codec, which is enough for + # codec-specific filters (e.g. h264_mp4toannexb) to initialize. + codec: Codec = ( + in_stream if isinstance(in_stream, Codec) else Codec(in_stream) + ) + self.ptr.par_in.codec_id = codec.ptr.id + self.ptr.par_in.codec_type = codec.ptr.type with cython.nogil: res = lib.av_bsf_init(self.ptr) diff --git a/av/bitstream.pyi b/av/bitstream.pyi index 477c65f2d..34adc16bf 100644 --- a/av/bitstream.pyi +++ b/av/bitstream.pyi @@ -1,3 +1,4 @@ +from .codec import Codec from .packet import Packet from .stream import Stream @@ -5,7 +6,7 @@ class BitStreamFilterContext: def __init__( self, filter_description: str | bytes, - in_stream: Stream | None = None, + in_stream: Stream | Codec | str | None = None, out_stream: Stream | None = None, ): ... def filter(self, packet: Packet | None) -> list[Packet]: ... diff --git a/include/avcodec.pxd b/include/avcodec.pxd index c94deaaa9..02e9ac9ca 100644 --- a/include/avcodec.pxd +++ b/include/avcodec.pxd @@ -502,8 +502,8 @@ cdef extern from "libavcodec/bsf.h" nogil: cdef struct AVBSFContext: const AVBitStreamFilter *filter - const AVCodecParameters *par_in - const AVCodecParameters *par_out + AVCodecParameters *par_in + AVCodecParameters *par_out cdef int av_bsf_list_parse_str(const char *str, AVBSFContext **bsf) cdef int av_bsf_init(AVBSFContext *ctx) diff --git a/tests/test_bitstream.py b/tests/test_bitstream.py index f94ed73af..820704e25 100644 --- a/tests/test_bitstream.py +++ b/tests/test_bitstream.py @@ -72,6 +72,22 @@ def test_filter_h264_mp4toannexb() -> None: assert is_annexb(p) +def test_filter_in_stream_codec_and_name() -> None: + # A Codec or codec-name str can stand in for a Stream to pin the input codec, + # which is all a codec-specific filter needs to initialize. + # (Filters that also need the stream's extradata, such as h264_mp4toannexb, + # still require a full Stream to convert correctly.) + for in_stream in ("h264", av.Codec("h264", "r")): + ctx = BitStreamFilterContext("h264_mp4toannexb", in_stream) + assert isinstance(ctx, BitStreamFilterContext) + + +def test_filter_in_stream_wrong_codec() -> None: + # h264_mp4toannexb only supports h264, so a mismatched codec is rejected. + with pytest.raises(av.ArgumentError): + BitStreamFilterContext("h264_mp4toannexb", "hevc") + + def test_filter_output_parameters() -> None: with av.open(fate_suite("h264/interlaced_crop.mp4"), "r") as container: stream = container.streams.video[0] From 54a4395bb4cdd9cdd53ff6216c50b69f6475c13d Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 2 Jul 2026 02:10:15 -0400 Subject: [PATCH 827/846] Release 18.0.0 --- CHANGELOG.rst | 10 +++++++--- av/about.py | 2 +- av/codec/hwaccel.py | 25 +++++++++++++++++++++++++ av/filter/graph.py | 11 ++++++++++- docs/api/codec.rst | 34 ++++++++++++++++++++++++++++++++++ 5 files changed, 77 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 19ac4ed87..0c86d12e4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -28,16 +28,18 @@ We are operating with `semantic versioning `_. - Bug fixes (PATCH) go here. - $CHANGE by :gh-user:`mikeboers` in (:pr:`1`). -v18.0.0 (next) --------------- +v18.0.0 +------- Breaking: - Remove Python 3.10 -- Support HW encoding via a ``hwaccel`` parameter on ``OutputContainer.add_stream`` (e.g. ``h264_vaapi``, ``h264_nvenc``, ``h264_videotoolbox``); software frames passed to ``encode`` are uploaded to the device automatically by :gh-user:`WyattBlue` (:issue:`2156`). Features: +- Support HW encoding via a ``hwaccel`` parameter on ``OutputContainer.add_stream`` (e.g. ``h264_vaapi``, ``h264_nvenc``, ``h264_videotoolbox``); software frames passed to ``encode`` are uploaded to the device automatically by :gh-user:`WyattBlue` (:issue:`2156`). +- Use FFmpeg 8.1.2 in the binary wheels by :gh-user:`WyattBlue`. +- Expose ``sw_format`` on ``VideoCodecContext``, and allow configuring the software format of hardware encoders by :gh-user:`WyattBlue`. - Add ``options`` parameter to ``AudioResampler`` for passing ``libswresample`` options (e.g. ``resampler``, ``filter_size``, ``cutoff``) by :gh-user:`WyattBlue` (:issue:`2262`). - Support ``yuv420p10le`` in ``VideoFrame.to_ndarray`` and ``VideoFrame.from_ndarray`` by :gh-user:`WyattBlue` (:issue:`1981`). - Add ``at`` parameter to ``Graph.push`` and ``Graph.vpush`` to push a frame to a single buffer source by index, for multi-input filters like ``overlay`` by :gh-user:`WyattBlue`. @@ -50,6 +52,8 @@ Fixes: - Fix ``VideoFrame.reformat`` (and ``to_ndarray``/``to_rgb``/``to_image``) raising ``OSError`` ``Operation not supported`` on frames tagged with reserved or otherwise unsupported ``color_primaries``/``color_trc`` values (e.g. VP9 and NVDEC output); a transfer/primaries conversion is now only performed when explicitly requested by :gh-user:`WyattBlue` (:issue:`2208`). - Fix ``add_mux_stream`` producing unwritable Matroska files by extracting codec extradata from the bitstream before the header is written by :gh-user:`WyattBlue` (:issue:`2198`). - Encode GPU frames (e.g. CUDA frames from DLPack) directly with ``pix_fmt="cuda"`` by adopting the frame's ``hw_frames_ctx`` before opening the encoder by :gh-user:`WyattBlue` (:issue:`2199`). +- Fix a crash when reading ``VideoCodecContext.pix_fmt`` before it is set; it now returns ``None`` by :gh-user:`CarlosRDomin`. +- Preserve frame attributes (``pts``, ``time_base``, colorspace, etc.) when downloading hardware frames to system memory by :gh-user:`CarlosRDomin`. v17.1.0 ------- diff --git a/av/about.py b/av/about.py index e12ca9a2e..c6a8b8ed8 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "18.0.0pre" +__version__ = "18.0.0" diff --git a/av/codec/hwaccel.py b/av/codec/hwaccel.py index 392bd39ac..93e7accec 100644 --- a/av/codec/hwaccel.py +++ b/av/codec/hwaccel.py @@ -94,6 +94,8 @@ def is_supported(self): @cython.ccall def hwdevices_available(): + """Return the names of the hardware device types FFmpeg was built with, + e.g. ``["cuda", "videotoolbox"]``.""" result: list = [] x: lib.AVHWDeviceType = lib.AV_HWDEVICE_TYPE_NONE while True: @@ -107,6 +109,29 @@ def hwdevices_available(): @cython.final @cython.cclass class HWAccel: + """HWAccel(device_type, device=None, allow_software_fallback=True, options=None, flags=None, is_hw_owned=False) + + Settings for hardware-accelerated decoding and encoding. Pass an instance to + :func:`av.open` to decode on the device, or to + :meth:`OutputContainer.add_stream ` + to encode on it. + + :param device_type: The kind of device, e.g. ``"cuda"``, ``"vaapi"``, + ``"videotoolbox"``. See :func:`hwdevices_available` for the types + supported by the loaded FFmpeg. + :type device_type: str or HWDeviceType + :param str device: An optional device identifier, e.g. a DRI render node path + (``"/dev/dri/renderD128"``) for VAAPI or a device index (``"1"``) for CUDA. + Uses the default device if ``None``. + :param bool allow_software_fallback: Whether decoding falls back to a software + decoder when the hardware decoder cannot handle the stream. + :param dict options: Options passed to ``av_hwdevice_ctx_create``. + :param int flags: Flags passed to ``av_hwdevice_ctx_create``. + :param bool is_hw_owned: If True, decoded frames stay on the device (for + consumption via DLPack, for example) instead of being downloaded to + system memory. + """ + def __init__( self, device_type, diff --git a/av/filter/graph.py b/av/filter/graph.py index 7463c5a5c..3e4898ab6 100644 --- a/av/filter/graph.py +++ b/av/filter/graph.py @@ -240,6 +240,15 @@ def set_audio_frame_size(self, frame_size): ) def push(self, frame, at: cython.int = -1): + """Push a frame into the graph's buffer source(s). + + :param frame: An :class:`.AudioFrame` or :class:`.VideoFrame` to push into + the matching buffer sources, or ``None`` to signal end of stream to + every buffer source. + :param int at: Index of a single buffer source to push to, for graphs with + multiple inputs (e.g. ``overlay``). The default of ``-1`` pushes to + every buffer source matching the frame's type. + """ if frame is None: contexts = self._video_sources + self._audio_sources elif isinstance(frame, VideoFrame): @@ -263,7 +272,7 @@ def push(self, frame, at: cython.int = -1): ctx.push(frame) def vpush(self, frame: VideoFrame | None, at: cython.int = -1): - """Like `push`, but only for VideoFrames.""" + """Like :meth:`push`, but only for :class:`.VideoFrame`.""" contexts = self._video_sources if at >= 0: if at >= len(contexts): diff --git a/docs/api/codec.rst b/docs/api/codec.rst index 7b6c261e3..a3f9b27a2 100644 --- a/docs/api/codec.rst +++ b/docs/api/codec.rst @@ -128,3 +128,37 @@ Enums and Flags .. enumtable:: av.codec.context.ThreadType +Hardware Acceleration +--------------------- + +.. currentmodule:: av.codec.hwaccel +.. automodule:: av.codec.hwaccel + +.. autoclass:: HWAccel + +.. autofunction:: hwdevices_available + +To decode on a hardware device, pass an :class:`HWAccel` to :func:`av.open`:: + + import av + from av.codec.hwaccel import HWAccel + + hwaccel = HWAccel(device_type="videotoolbox") + with av.open("input.mp4", hwaccel=hwaccel) as container: + for frame in container.decode(video=0): + ... # Frames are downloaded to system memory by default. + +To encode on a hardware device, pass one to :meth:`OutputContainer.add_stream +` with a hardware encoder. Software +frames passed to ``encode`` are uploaded to the device automatically:: + + with av.open("output.mp4", "w") as container: + stream = container.add_stream( + "h264_videotoolbox", rate=30, hwaccel=HWAccel(device_type="videotoolbox") + ) + ... + +See ``examples/basics/hw_decode.py`` for a complete example, including +recommended device types per platform. + + From 44cbe6414c863cfb27b56b228b3458bef405f8ef Mon Sep 17 00:00:00 2001 From: Yozer Date: Mon, 13 Jul 2026 21:38:50 +0200 Subject: [PATCH 828/846] Support current CUDA contexts --- av/video/frame.pxd | 1 + av/video/frame.py | 37 ++++++++++++++++++++++++++++++++----- av/video/frame.pyi | 10 +++++++++- docs/api/video.rst | 4 ++++ tests/test_dlpack.py | 34 ++++++++++++++++++++++++++++++++-- 5 files changed, 78 insertions(+), 8 deletions(-) diff --git a/av/video/frame.pxd b/av/video/frame.pxd index 90547cb9f..0a96311e0 100644 --- a/av/video/frame.pxd +++ b/av/video/frame.pxd @@ -9,6 +9,7 @@ from av.video.reformatter cimport VideoReformatter cdef class CudaContext: cdef readonly int device_id cdef readonly bint primary_ctx + cdef readonly bint current_ctx cdef lib.AVBufferRef* _device_ref cdef dict _frames_cache cdef lib.AVBufferRef* _get_device_ref(self) diff --git a/av/video/frame.py b/av/video/frame.py index 6d6c3f044..511cb028b 100644 --- a/av/video/frame.py +++ b/av/video/frame.py @@ -22,11 +22,28 @@ @cython.final @cython.cclass class CudaContext: - def __cinit__(self, device_id: cython.int = 0, primary_ctx: cython.bint = True): + """A reusable FFmpeg CUDA context for frames imported with DLPack. + + :param int device_id: CUDA device ordinal. + :param bool primary_ctx: Retain the device's primary CUDA context. + :param bool current_ctx: Wrap the CUDA context current on the calling thread + when this object is first used. This is mutually exclusive with + ``primary_ctx``. + """ + + def __cinit__( + self, + device_id: cython.int = 0, + primary_ctx: cython.bint = True, + current_ctx: cython.bint = False, + ): self.device_id = device_id self.primary_ctx = primary_ctx + self.current_ctx = current_ctx self._device_ref = cython.NULL self._frames_cache = {} + if primary_ctx and current_ctx: + raise ValueError("primary_ctx and current_ctx are mutually exclusive") def __dealloc__(self): ref: cython.pointer[lib.AVBufferRef] @@ -53,9 +70,10 @@ def _get_device_ref(self) -> cython.pointer[lib.AVBufferRef]: device_ref = cython.NULL device_bytes = f"{self.device_id}".encode() c_device: cython.p_char = device_bytes - options: Dictionary = Dictionary( - {"primary_ctx": "1" if self.primary_ctx else "0"} - ) + options_dict = {"primary_ctx": "1" if self.primary_ctx else "0"} + if self.current_ctx: + options_dict["current_ctx"] = "1" + options: Dictionary = Dictionary(options_dict) err_check( lib.av_hwdevice_ctx_create( cython.address(device_ref), @@ -1471,6 +1489,7 @@ def from_dlpack( device_id: int | None = None, primary_ctx: bool = True, cuda_context=None, + current_ctx: bool = False, ): if not isinstance(planes, (tuple, list)): planes = (planes,) @@ -1612,7 +1631,11 @@ def from_dlpack( ctx: CudaContext frames_ref: cython.pointer[lib.AVBufferRef] if cuda_context is None: - ctx = CudaContext(device_id=device_id, primary_ctx=primary_ctx) + ctx = CudaContext( + device_id=device_id, + primary_ctx=primary_ctx, + current_ctx=current_ctx, + ) else: if not isinstance(cuda_context, CudaContext): raise TypeError("cuda_context must be a CudaContext") @@ -1624,6 +1647,10 @@ def from_dlpack( raise ValueError( "cuda_context.primary_ctx does not match primary_ctx" ) + if bool(cuda_context.current_ctx) != bool(current_ctx): + raise ValueError( + "cuda_context.current_ctx does not match current_ctx" + ) ctx = cython.cast(CudaContext, cuda_context) frames_ref = ctx.get_frames_ctx(sw_fmt, width, height) diff --git a/av/video/frame.pyi b/av/video/frame.pyi index 3c8ef6a79..14197f24f 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -34,7 +34,14 @@ class CudaContext: def device_id(self) -> int: ... @property def primary_ctx(self) -> bool: ... - def __init__(self, device_id: int = 0, primary_ctx: bool = True) -> None: ... + @property + def current_ctx(self) -> bool: ... + def __init__( + self, + device_id: int = 0, + primary_ctx: bool = True, + current_ctx: bool = False, + ) -> None: ... class VideoFrame(Frame): format: VideoFormat @@ -109,4 +116,5 @@ class VideoFrame(Frame): device_id: int | None = None, primary_ctx: bool = True, cuda_context: CudaContext | None = None, + current_ctx: bool = False, ) -> VideoFrame: ... diff --git a/docs/api/video.rst b/docs/api/video.rst index 7e97e38c8..8d349fc1b 100644 --- a/docs/api/video.rst +++ b/docs/api/video.rst @@ -33,6 +33,9 @@ Video Frames .. automodule:: av.video.frame +.. autoclass:: CudaContext + :members: + .. autoclass:: VideoFrame A single video frame. @@ -80,6 +83,7 @@ Conversions .. automethod:: VideoFrame.from_image .. automethod:: VideoFrame.from_ndarray +.. automethod:: VideoFrame.from_dlpack diff --git a/tests/test_dlpack.py b/tests/test_dlpack.py index 2e74f342f..cfff0540c 100644 --- a/tests/test_dlpack.py +++ b/tests/test_dlpack.py @@ -531,6 +531,19 @@ def test_video_frame_from_dlpack_invalid_plane_object_raises_typeerror() -> None VideoFrame.from_dlpack((object(), object()), format="nv12", width=64, height=48) +def test_cuda_context_flags() -> None: + default_ctx = av.video.frame.CudaContext() + assert default_ctx.primary_ctx is True + assert default_ctx.current_ctx is False + + with pytest.raises(ValueError, match="mutually exclusive"): + av.video.frame.CudaContext(primary_ctx=True, current_ctx=True) + + ctx = av.video.frame.CudaContext(primary_ctx=False, current_ctx=True) + assert ctx.primary_ctx is False + assert ctx.current_ctx is True + + def test_video_frame_from_dlpack_cuda_hw_frame_behavior_if_available() -> None: backend = _get_cuda_backend() if backend is None: @@ -621,7 +634,10 @@ def test_video_frame_from_dlpack_cuda_hw_frame_behavior_if_available() -> None: pytest.skip(f"CUDA hwcontext not available in this build/runtime: {e}") -def test_encode_cuda_frame_with_nvenc_if_available() -> None: +@pytest.mark.parametrize( + "use_current_ctx", [False, True], ids=["primary-context", "current-context"] +) +def test_encode_cuda_frame_with_nvenc_if_available(use_current_ctx: bool) -> None: # Issue #2199: a CUDA frame from DLPack should encode on the GPU directly. # Its hw_frames_ctx must propagate to the encoder before avcodec_open2. backend = _get_cuda_backend() @@ -639,7 +655,21 @@ def test_encode_cuda_frame_with_nvenc_if_available() -> None: y = mod.zeros((height, width), dtype=mod.uint8) uv = mod.zeros((height // 2, width // 2, 2), dtype=mod.uint8) - frame = VideoFrame.from_dlpack((y, uv), format="nv12") + if use_current_ctx: + current_ctx = av.video.frame.CudaContext( + device_id=int(y.__dlpack_device__()[1]), + primary_ctx=False, + current_ctx=True, + ) + frame = VideoFrame.from_dlpack( + (y, uv), + format="nv12", + primary_ctx=False, + cuda_context=current_ctx, + current_ctx=True, + ) + else: + frame = VideoFrame.from_dlpack((y, uv), format="nv12") assert frame.format.name == "cuda" assert frame.sw_format is not None and frame.sw_format.name == "nv12" From 50a71f8a7ecf8e06574fb2e008624d8579b96c86 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 13 Jul 2026 18:27:00 -0400 Subject: [PATCH 829/846] Improve from_dlpack CUDA context ergonomics Passing an explicit cuda_context no longer requires restating primary_ctx/current_ctx; the flags are validated only when given. current_ctx=True without cuda_context now implies primary_ctx=False. Also document CudaContext and add a changelog entry for #2339. --- CHANGELOG.rst | 8 ++++++++ av/video/frame.py | 18 ++++++++++++------ av/video/frame.pyi | 4 ++-- tests/test_dlpack.py | 6 +----- 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0c86d12e4..8dabf3fcb 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -28,6 +28,14 @@ We are operating with `semantic versioning `_. - Bug fixes (PATCH) go here. - $CHANGE by :gh-user:`mikeboers` in (:pr:`1`). +v18.1.0 (Unreleased) +-------------------- + +Features: + +- Support reusing the thread's current CUDA context via a ``current_ctx`` flag on ``CudaContext`` and ``VideoFrame.from_dlpack``, for interop with libraries like PyTorch that initialize CUDA first by :gh-user:`Yozer` (:pr:`2339`). +- ``VideoFrame.from_dlpack`` no longer requires restating ``primary_ctx``/``current_ctx`` when passing an explicit ``cuda_context``; the flags are only validated when explicitly given by :gh-user:`WyattBlue`. + v18.0.0 ------- diff --git a/av/video/frame.py b/av/video/frame.py index 511cb028b..4a85d5b11 100644 --- a/av/video/frame.py +++ b/av/video/frame.py @@ -1487,9 +1487,9 @@ def from_dlpack( height: int = 0, stream=None, device_id: int | None = None, - primary_ctx: bool = True, + primary_ctx: bool | None = None, cuda_context=None, - current_ctx: bool = False, + current_ctx: bool | None = None, ): if not isinstance(planes, (tuple, list)): planes = (planes,) @@ -1633,8 +1633,10 @@ def from_dlpack( if cuda_context is None: ctx = CudaContext( device_id=device_id, - primary_ctx=primary_ctx, - current_ctx=current_ctx, + primary_ctx=( + not current_ctx if primary_ctx is None else primary_ctx + ), + current_ctx=bool(current_ctx), ) else: if not isinstance(cuda_context, CudaContext): @@ -1643,11 +1645,15 @@ def from_dlpack( raise ValueError( "cuda_context.device_id does not match the DLPack tensor device_id" ) - if bool(cuda_context.primary_ctx) != bool(primary_ctx): + if primary_ctx is not None and bool( + cuda_context.primary_ctx + ) != bool(primary_ctx): raise ValueError( "cuda_context.primary_ctx does not match primary_ctx" ) - if bool(cuda_context.current_ctx) != bool(current_ctx): + if current_ctx is not None and bool( + cuda_context.current_ctx + ) != bool(current_ctx): raise ValueError( "cuda_context.current_ctx does not match current_ctx" ) diff --git a/av/video/frame.pyi b/av/video/frame.pyi index 14197f24f..dd6f481d1 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -114,7 +114,7 @@ class VideoFrame(Frame): height: int = 0, stream: int | None = None, device_id: int | None = None, - primary_ctx: bool = True, + primary_ctx: bool | None = None, cuda_context: CudaContext | None = None, - current_ctx: bool = False, + current_ctx: bool | None = None, ) -> VideoFrame: ... diff --git a/tests/test_dlpack.py b/tests/test_dlpack.py index cfff0540c..033316138 100644 --- a/tests/test_dlpack.py +++ b/tests/test_dlpack.py @@ -662,11 +662,7 @@ def test_encode_cuda_frame_with_nvenc_if_available(use_current_ctx: bool) -> Non current_ctx=True, ) frame = VideoFrame.from_dlpack( - (y, uv), - format="nv12", - primary_ctx=False, - cuda_context=current_ctx, - current_ctx=True, + (y, uv), format="nv12", cuda_context=current_ctx ) else: frame = VideoFrame.from_dlpack((y, uv), format="nv12") From 61e4aa875305471fe022cb929d41d77d9d76904c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 15 Jul 2026 13:31:25 -0400 Subject: [PATCH 830/846] Add AVRational type --- av/__init__.py | 1 + av/codec/codec.py | 5 +- av/codec/codec.pyi | 4 +- av/rational.pxd | 11 +++ av/rational.py | 156 +++++++++++++++++++++++++++++++++++++++++ av/rational.pyi | 29 ++++++++ docs/api/time.rst | 21 ++++-- include/avutil.pxd | 4 ++ tests/test_rational.py | 88 +++++++++++++++++++++++ 9 files changed, 309 insertions(+), 10 deletions(-) create mode 100644 av/rational.pxd create mode 100644 av/rational.py create mode 100644 av/rational.pyi create mode 100644 tests/test_rational.py diff --git a/av/__init__.py b/av/__init__.py index 9d8148082..964b93891 100644 --- a/av/__init__.py +++ b/av/__init__.py @@ -21,6 +21,7 @@ from av.device import DeviceInfo, enumerate_input_devices, enumerate_output_devices from av.format import ContainerFormat, formats_available from av.packet import Packet +from av.rational import AVRational from av.error import * # noqa: F403; This is limited to exception types. from av.video.codeccontext import VideoCodecContext from av.video.format import VideoFormat diff --git a/av/codec/codec.py b/av/codec/codec.py index edda0c29d..0e8b958ec 100644 --- a/av/codec/codec.py +++ b/av/codec/codec.py @@ -4,6 +4,7 @@ from cython.cimports import libav as lib from cython.cimports.av.audio.format import get_audio_format from cython.cimports.av.codec.hwaccel import wrap_hwconfig +from cython.cimports.av.rational import from_avrational from cython.cimports.av.utils import avrational_to_fraction from cython.cimports.av.video.format import VideoFormat, get_pix_fmt, get_video_format from cython.cimports.libc.stdlib import free, malloc @@ -193,7 +194,7 @@ def id(self): @property def frame_rates(self): - """A list of supported frame rates (:class:`fractions.Fraction`), or ``None``.""" + """A list of supported frame rates (:class:`av.AVRational`), or ``None``.""" out: cython.pointer[cython.const[cython.void]] = cython.NULL num: cython.int = 0 lib.avcodec_get_supported_config( @@ -207,7 +208,7 @@ def frame_rates(self): if not out: return rates = cython.cast(cython.pointer[lib.AVRational], out) - return [avrational_to_fraction(cython.address(rates[i])) for i in range(num)] + return [from_avrational(rates[i]) for i in range(num)] @property def audio_rates(self): diff --git a/av/codec/codec.pyi b/av/codec/codec.pyi index 8631790b2..be3953dd3 100644 --- a/av/codec/codec.pyi +++ b/av/codec/codec.pyi @@ -1,10 +1,10 @@ from collections.abc import Sequence from enum import Flag, IntEnum, IntFlag -from fractions import Fraction from typing import ClassVar, Literal, cast, overload from av.audio.codeccontext import AudioCodecContext from av.audio.format import AudioFormat +from av.rational import AVRational from av.subtitles.codeccontext import SubtitleCodecContext from av.video.codeccontext import VideoCodecContext from av.video.format import VideoFormat @@ -75,7 +75,7 @@ class Codec: ) -> Literal["video", "audio", "data", "subtitle", "attachment", "unknown"]: ... @property def id(self) -> int: ... - frame_rates: list[Fraction] | None + frame_rates: list[AVRational] | None audio_rates: list[int] | None video_formats: list[VideoFormat] | None audio_formats: list[AudioFormat] | None diff --git a/av/rational.pxd b/av/rational.pxd new file mode 100644 index 000000000..a416fea49 --- /dev/null +++ b/av/rational.pxd @@ -0,0 +1,11 @@ +cimport libav as lib + + +cdef class AVRational: + cdef readonly int num + cdef readonly int den + + cdef lib.AVRational _q(self) + + +cdef AVRational from_avrational(lib.AVRational q) diff --git a/av/rational.py b/av/rational.py new file mode 100644 index 000000000..87ce7ade9 --- /dev/null +++ b/av/rational.py @@ -0,0 +1,156 @@ +# type: ignore +from fractions import Fraction +from numbers import Rational + +import cython +from cython.cimports import libav as lib + +_INT32_MAX: cython.longlong = 2147483647 + + +@cython.cclass +class AVRational: + """ + An exact rational number stored as two int32s, mirroring FFmpeg's + ``AVRational``. + + Values are always reduced to lowest terms with a positive denominator. + Arithmetic between two :class:`AVRational` uses FFmpeg's ``av_mul_q`` + family: intermediates are computed in int64, then reduced back to int32, + **approximating** the result if it does not fit. Arithmetic with other + numeric types promotes to :class:`fractions.Fraction` (exact). + + Following FFmpeg, a zero denominator is allowed: ``1/0``, ``-1/0`` + (infinities) and ``0/0`` (undefined) exist and, like the unset value + ``AVRational(0, 1)``, are falsy — so ``if rate:`` covers every + not-a-real-value case that used to be ``None``. + + Every PyAV setter that accepts a :class:`fractions.Fraction` (e.g. + ``stream.time_base``, ``codec_context.framerate``) also accepts an + :class:`AVRational`. + """ + + def __init__(self, num=0, den=1): + if den == 1 and isinstance(num, Rational): + num, den = num.numerator, num.denominator + n64: cython.longlong = num + d64: cython.longlong = den + n: cython.int + d: cython.int + if not lib.av_reduce( + cython.address(n), cython.address(d), n64, d64, _INT32_MAX + ): + raise OverflowError(f"{num}/{den} cannot be reduced to fit in int32") + self.num = n + self.den = d + + @cython.cfunc + def _q(self) -> lib.AVRational: + q: lib.AVRational + q.num = self.num + q.den = self.den + return q + + @property + def numerator(self): + return self.num + + @property + def denominator(self): + return self.den + + def __repr__(self): + return f"AVRational({self.num}, {self.den})" + + def __str__(self): + return f"{self.num}/{self.den}" + + def __bool__(self): + return self.num != 0 and self.den != 0 + + def __float__(self): + if self.den == 0: + return float("nan") if self.num == 0 else self.num * float("inf") + return self.num / self.den + + def __hash__(self): + if self.den == 0: + return hash((self.num, 0)) + return hash(Fraction(self.num, self.den)) + + def __reduce__(self): + return (AVRational, (self.num, self.den)) + + def __eq__(self, other): + if isinstance(other, AVRational): + o: AVRational = other + return self.num == o.num and self.den == o.den + if self.den == 0: + return False + return Fraction(self.num, self.den) == other + + def __lt__(self, other): + return Fraction(self.num, self.den) < other + + def __le__(self, other): + return Fraction(self.num, self.den) <= other + + def __gt__(self, other): + return Fraction(self.num, self.den) > other + + def __ge__(self, other): + return Fraction(self.num, self.den) >= other + + def __neg__(self): + return AVRational(-self.num, self.den) + + def __mul__(self, other): + if isinstance(other, AVRational): + o: AVRational = other + return from_avrational(lib.av_mul_q(self._q(), o._q())) + return Fraction(self.num, self.den) * other + + def __rmul__(self, other): + return other * Fraction(self.num, self.den) + + def __truediv__(self, other): + if isinstance(other, AVRational): + o: AVRational = other + if o.num == 0: + raise ZeroDivisionError(f"{self} / {other}") + return from_avrational(lib.av_div_q(self._q(), o._q())) + return Fraction(self.num, self.den) / other + + def __rtruediv__(self, other): + return other / Fraction(self.num, self.den) + + def __add__(self, other): + if isinstance(other, AVRational): + o: AVRational = other + return from_avrational(lib.av_add_q(self._q(), o._q())) + return Fraction(self.num, self.den) + other + + def __radd__(self, other): + return other + Fraction(self.num, self.den) + + def __sub__(self, other): + if isinstance(other, AVRational): + o: AVRational = other + return from_avrational(lib.av_sub_q(self._q(), o._q())) + return Fraction(self.num, self.den) - other + + def __rsub__(self, other): + return other - Fraction(self.num, self.den) + + +@cython.cfunc +def from_avrational(q: lib.AVRational) -> AVRational: + obj: AVRational = AVRational.__new__(AVRational) + # FFmpeg does not guarantee reduced form; our invariant requires it. + lib.av_reduce( + cython.address(obj.num), cython.address(obj.den), q.num, q.den, _INT32_MAX + ) + return obj + + +Rational.register(AVRational) diff --git a/av/rational.pyi b/av/rational.pyi new file mode 100644 index 000000000..12716ca04 --- /dev/null +++ b/av/rational.pyi @@ -0,0 +1,29 @@ +from fractions import Fraction +from numbers import Rational +from typing import Any + +class AVRational: + num: int + den: int + def __init__(self, num: int | Rational = 0, den: int = 1) -> None: ... + @property + def numerator(self) -> int: ... + @property + def denominator(self) -> int: ... + def __bool__(self) -> bool: ... + def __float__(self) -> float: ... + def __hash__(self) -> int: ... + def __eq__(self, other: Any) -> bool: ... + def __lt__(self, other: Any) -> bool: ... + def __le__(self, other: Any) -> bool: ... + def __gt__(self, other: Any) -> bool: ... + def __ge__(self, other: Any) -> bool: ... + def __neg__(self) -> AVRational: ... + def __mul__(self, other: Any) -> AVRational | Fraction | float: ... + def __rmul__(self, other: Any) -> Fraction | float: ... + def __truediv__(self, other: Any) -> AVRational | Fraction | float: ... + def __rtruediv__(self, other: Any) -> Fraction | float: ... + def __add__(self, other: Any) -> AVRational | Fraction | float: ... + def __radd__(self, other: Any) -> Fraction | float: ... + def __sub__(self, other: Any) -> AVRational | Fraction | float: ... + def __rsub__(self, other: Any) -> Fraction | float: ... diff --git a/docs/api/time.rst b/docs/api/time.rst index a063e876e..bb0a7dc5b 100644 --- a/docs/api/time.rst +++ b/docs/api/time.rst @@ -12,6 +12,7 @@ Time is expressed as integer multiples of arbitrary units of time called a ``tim .. testsetup:: import av + from fractions import Fraction path = av.datasets.curated('pexels/time-lapse-video-of-night-sky-857195.mp4') def get_nth_packet_and_frame(fh, skip): @@ -26,8 +27,16 @@ Time is expressed as integer multiples of arbitrary units of time called a ``tim >>> fh = av.open(path) >>> video = fh.streams.video[0] - >>> video.time_base - Fraction(1, 25) + >>> video.time_base == Fraction(1, 25) + True + +Rational attributes like ``time_base`` may be unset. Test them by truthiness rather than +``is None`` — an unset value is always falsy, both today (``None``) and as PyAV +transitions these attributes to :class:`av.AVRational` (where unset is the falsy +``AVRational(0, 1)``):: + + if not stream.time_base: + ... # unset; pick a default Attributes that represent time on those objects will be in that object's ``time_base``: @@ -46,13 +55,13 @@ In many cases a stream has a time base of ``1 / frame_rate``, and then its frame >>> p, f = get_nth_packet_and_frame(fh, skip=1) - >>> p.time_base - Fraction(1, 25) + >>> p.time_base == Fraction(1, 25) + True >>> p.dts 1 - >>> f.time_base - Fraction(1, 25) + >>> f.time_base == Fraction(1, 25) + True >>> f.pts 1 diff --git a/include/avutil.pxd b/include/avutil.pxd index 8911adcc0..615b6979d 100644 --- a/include/avutil.pxd +++ b/include/avutil.pxd @@ -366,6 +366,10 @@ cdef extern from "libavutil/pixdesc.h" nogil: cdef extern from "libavutil/rational.h" nogil: cdef int av_reduce(int *dst_num, int *dst_den, int64_t num, int64_t den, int64_t max) + cdef AVRational av_mul_q(AVRational b, AVRational c) + cdef AVRational av_div_q(AVRational b, AVRational c) + cdef AVRational av_add_q(AVRational b, AVRational c) + cdef AVRational av_sub_q(AVRational b, AVRational c) cdef extern from "libavutil/samplefmt.h" nogil: cdef enum AVSampleFormat: diff --git a/tests/test_rational.py b/tests/test_rational.py new file mode 100644 index 000000000..f534e7d68 --- /dev/null +++ b/tests/test_rational.py @@ -0,0 +1,88 @@ +import pickle +from fractions import Fraction + +import pytest + +from av import AVRational + + +def test_construction() -> None: + r = AVRational(2, 4) + assert (r.num, r.den) == (1, 2) + assert (AVRational(1, -2).num, AVRational(1, -2).den) == (-1, 2) + assert AVRational(Fraction(30000, 1001)) == AVRational(30000, 1001) + assert AVRational(10**10, 2 * 10**10) == AVRational(1, 2) + with pytest.raises(OverflowError): + AVRational(2**31, 3) + + +def test_unset_is_falsy() -> None: + assert not AVRational() + assert not AVRational(0, 1) + assert AVRational(1, 25) + + +def test_zero_denominator() -> None: + inf = AVRational(1, 0) + assert not inf and not AVRational(-1, 0) and not AVRational(0, 0) + assert (AVRational(5, 0).num, AVRational(5, 0).den) == (1, 0) + assert (AVRational(-7, 0).num, AVRational(-7, 0).den) == (-1, 0) + assert inf == AVRational(2, 0) + assert inf != AVRational(0, 0) and inf != Fraction(1, 2) and inf != 1 + assert float(inf) == float("inf") + assert float(AVRational(-1, 0)) == float("-inf") + assert str(float(AVRational(0, 0))) == "nan" + assert hash(inf) == hash(AVRational(1, 0)) + assert pickle.loads(pickle.dumps(inf)) == inf + + +def test_fraction_interop() -> None: + r = AVRational(1, 2) + assert r == Fraction(1, 2) + assert Fraction(1, 2) == r + assert hash(r) == hash(Fraction(1, 2)) + assert r < Fraction(2, 3) < AVRational(3, 4) + assert r * Fraction(1, 3) == Fraction(1, 6) + assert Fraction(1, 3) * r == Fraction(1, 6) + assert 2 * r == 1 + assert r + 1 == Fraction(3, 2) + assert 1 - r == Fraction(1, 2) + assert float(r) == 0.5 + + +def test_avrational_arithmetic() -> None: + a = AVRational(1, 25) + b = AVRational(1, 2) + assert a * b == AVRational(1, 50) + assert isinstance(a * b, AVRational) + assert a + b == AVRational(27, 50) + assert b - a == AVRational(23, 50) + assert a / b == AVRational(2, 25) + assert -a == AVRational(-1, 25) + with pytest.raises(ZeroDivisionError): + a / AVRational(0, 1) + huge = AVRational(1, 2**30) * AVRational(1, 2**30) + assert float(huge) == pytest.approx(2.0**-60, rel=1e-6) + + +def test_setters_accept_avrational() -> None: + import av + + cc = av.codec.CodecContext.create("mpeg4", "w") + cc.time_base = AVRational(1001, 30000) # type: ignore[assignment] + assert cc.time_base == Fraction(1001, 30000) + + +def test_codec_frame_rates() -> None: + import av + + rates = av.Codec("mpeg2video", "w").frame_rates + assert rates and all(isinstance(r, AVRational) for r in rates) + assert AVRational(30000, 1001) in rates + + +def test_pickle_and_repr() -> None: + r = AVRational(30000, 1001) + assert pickle.loads(pickle.dumps(r)) == r + assert repr(r) == "AVRational(30000, 1001)" + assert str(r) == "30000/1001" From 49b2e963e75ce9b3e54ddd0ad97274922efbf9a9 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 19 Jul 2026 14:05:31 -0400 Subject: [PATCH 831/846] Expose frame metadata Expose `AVFrame.metadata` as a read-only `Frame.metadata` property and cover metadata-producing audio filters. Closes #2342. --- av/frame.py | 11 ++++++++++- av/frame.pyi | 2 ++ scripts/build-deps | 2 +- tests/test_filters.py | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/av/frame.py b/av/frame.py index 7e43de597..7c54ae2e2 100644 --- a/av/frame.py +++ b/av/frame.py @@ -1,7 +1,11 @@ import cython from cython.cimports.av.error import err_check from cython.cimports.av.opaque import opaque_container -from cython.cimports.av.utils import avrational_to_fraction, to_avrational +from cython.cimports.av.utils import ( + avdict_to_dict, + avrational_to_fraction, + to_avrational, +) from av.sidedata.sidedata import SideDataContainer @@ -178,6 +182,11 @@ def side_data(self): self._side_data = SideDataContainer(self) return self._side_data + @property + def metadata(self): + """Metadata attached to the frame by FFmpeg.""" + return avdict_to_dict(self.ptr.metadata, "utf-8", "strict") + def make_writable(self): """ Ensures that the frame data is writable. Copy the data to new buffer if it is not. diff --git a/av/frame.pyi b/av/frame.pyi index f085fc0f4..caed23482 100644 --- a/av/frame.pyi +++ b/av/frame.pyi @@ -14,6 +14,8 @@ class Frame: side_data: SideData opaque: object @property + def metadata(self) -> dict[str, str]: ... + @property def time(self) -> float | None: ... @property def is_corrupt(self) -> bool: ... diff --git a/scripts/build-deps b/scripts/build-deps index c2602e728..8ff3415c0 100755 --- a/scripts/build-deps +++ b/scripts/build-deps @@ -69,7 +69,7 @@ echo ./configure --disable-bsfs \ --enable-bsf=chomp,extract_extradata,h264_mp4toannexb,setts \ --disable-filters \ - --enable-filter=abuffer,abuffersink,aformat,aresample,atempo,buffer,buffersink,bwdif,color,loudnorm,lutrgb,overlay,palettegen,scale,testsrc,vflip,volume \ + --enable-filter=abuffer,abuffersink,aformat,aresample,atempo,buffer,buffersink,bwdif,color,ebur128,loudnorm,lutrgb,overlay,palettegen,scale,testsrc,vflip,volume \ --enable-sse \ --enable-avx \ --enable-avx2 \ diff --git a/tests/test_filters.py b/tests/test_filters.py index 97038828a..565df3507 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -193,6 +193,40 @@ def test_audio_buffer_volume_filter(self): assert np.allclose(input_data * 0.5, output_data) + def test_audio_frame_metadata(self) -> None: + graph = Graph() + graph.link_nodes( + graph.add_abuffer( + format="fltp", + sample_rate=48000, + layout="stereo", + time_base=Fraction(1, 48000), + ), + graph.add("ebur128", "metadata=1"), + graph.add("abuffersink"), + ).configure() + + frame = AudioFrame.from_ndarray( + np.zeros((2, 4800), dtype=np.float32), + format="fltp", + layout="stereo", + ) + frame.sample_rate = 48000 + frame.time_base = Fraction(1, 48000) + frame.pts = 0 + + graph.push(frame) + graph.push(None) + output = graph.pull() + metadata = output.metadata + + assert "lavfi.r128.I" in metadata + assert "lavfi.r128.LRA" in metadata + + # Frame.metadata returns a copy of the underlying AVDictionary. + metadata.clear() + assert "lavfi.r128.I" in output.metadata + def _test_video_buffer(self, graph): input_container = av.open(format="lavfi", file="color=c=pink:duration=1:r=30") input_video_stream = input_container.streams.video[0] From 97e964e73c5423d203405a74c7ccd08c00f3287f Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 21 Jul 2026 00:44:41 -0400 Subject: [PATCH 832/846] Fix segfaults on streams without a codec context `Stream.codec_context` is cdef-typed, so with Cython's default nonecheck=False, calling into it while None causes a segfault. A stream has no codec context when avcodec_find_decoder comes up empty on demux, or when it was created by `add_mux_stream`. Decoding and encoding such a stream now raise DecoderNotFoundError and EncoderNotFoundError through a shared guard. `BitStreamFilterContext` instead skips the dereference entirely: it only mirrored codecpar into the context, and avcodec_parameters_copy has already given the muxer what it needs, so `add_mux_stream` with h264_mp4toannexb now works rather than crashing. Closes #2344. --- CHANGELOG.rst | 4 ++++ av/audio/stream.py | 5 +++-- av/bitstream.py | 9 ++++++--- av/stream.pxd | 1 + av/stream.py | 8 ++++++++ av/subtitles/stream.py | 1 + av/video/stream.py | 3 ++- tests/test_decode.py | 29 +++++++++++++++++++++++++++++ 8 files changed, 54 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8dabf3fcb..48fe78d4d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -36,6 +36,10 @@ Features: - Support reusing the thread's current CUDA context via a ``current_ctx`` flag on ``CudaContext`` and ``VideoFrame.from_dlpack``, for interop with libraries like PyTorch that initialize CUDA first by :gh-user:`Yozer` (:pr:`2339`). - ``VideoFrame.from_dlpack`` no longer requires restating ``primary_ctx``/``current_ctx`` when passing an explicit ``cuda_context``; the flags are only validated when explicitly given by :gh-user:`WyattBlue`. +Fixes: + +- Fix a crash when using a stream that has no ``CodecContext`` (a demuxed stream with no available decoder, such as one from a truncated file, or a stream created by ``add_mux_stream``); decoding now raises ``DecoderNotFoundError``, encoding now raises ``EncoderNotFoundError``, and ``BitStreamFilterContext`` accepts such a stream as ``out_stream`` by :gh-user:`WyattBlue`, reported by :gh-user:`justinrmiller` (:issue:`2344`). + v18.0.0 ------- diff --git a/av/audio/stream.py b/av/audio/stream.py index 38009db19..e7fc63560 100644 --- a/av/audio/stream.py +++ b/av/audio/stream.py @@ -1,4 +1,5 @@ import cython +from cython.cimports import libav as lib from cython.cimports.av.audio.frame import AudioFrame from cython.cimports.av.packet import Packet @@ -31,7 +32,7 @@ def encode(self, frame: AudioFrame | None = None): .. seealso:: This is mostly a passthrough to :meth:`.CodecContext.encode`. """ - + self._assert_has_codec_context(lib.AVERROR_ENCODER_NOT_FOUND) packets = self.codec_context.encode(frame) packet: Packet for packet in packets: @@ -49,5 +50,5 @@ def decode(self, packet: Packet | None = None): .. seealso:: This is a passthrough to :meth:`.CodecContext.decode`. """ - + self._assert_has_codec_context() return self.codec_context.decode(packet) diff --git a/av/bitstream.py b/av/bitstream.py index 822e710a5..a3bc0088b 100644 --- a/av/bitstream.py +++ b/av/bitstream.py @@ -61,9 +61,12 @@ def __cinit__( out_stream.ptr.codecpar, self.ptr.par_out ) err_check(res) - lib.avcodec_parameters_to_context( - out_stream.codec_context.ptr, out_stream.ptr.codecpar - ) + # codecpar carries everything the muxer needs; a mux-only stream + # (add_mux_stream) has no context to keep in sync. + if out_stream.codec_context is not None: + lib.avcodec_parameters_to_context( + out_stream.codec_context.ptr, out_stream.ptr.codecpar + ) def __dealloc__(self): if self.ptr: diff --git a/av/stream.pxd b/av/stream.pxd index 89d41e559..727e420ea 100644 --- a/av/stream.pxd +++ b/av/stream.pxd @@ -21,6 +21,7 @@ cdef class Stream: # Private API. cdef _init(self, Container, lib.AVStream*, CodecContext) + cdef _assert_has_codec_context(self, int err=*) cdef _finalize_for_output(self) cdef _set_id(self, value) diff --git a/av/stream.py b/av/stream.py index f96030e26..2d087c8ad 100644 --- a/av/stream.py +++ b/av/stream.py @@ -128,6 +128,14 @@ def _init( errors=self.container.metadata_errors, ) + @cython.cfunc + def _assert_has_codec_context( + self, err: cython.int = lib.AVERROR_DECODER_NOT_FOUND + ): + # Calling into a NULL codec_context is a segfault, not an AttributeError. + if self.codec_context is None: + err_check(err) + def __repr__(self): name = getattr(self, "name", None) return ( diff --git a/av/subtitles/stream.py b/av/subtitles/stream.py index 484eff5be..a39f8b0a3 100644 --- a/av/subtitles/stream.py +++ b/av/subtitles/stream.py @@ -18,6 +18,7 @@ def decode(self, packet: Packet | None = None): .. seealso:: This is a passthrough to :meth:`.CodecContext.decode`. """ + self._assert_has_codec_context() if not packet: packet = Packet() diff --git a/av/video/stream.py b/av/video/stream.py index bb8b06fc0..be1fcaf9d 100644 --- a/av/video/stream.py +++ b/av/video/stream.py @@ -40,7 +40,7 @@ def encode(self, frame: VideoFrame | None = None): .. seealso:: This is mostly a passthrough to :meth:`.CodecContext.encode`. """ - + self._assert_has_codec_context(lib.AVERROR_ENCODER_NOT_FOUND) packets = self.codec_context.encode(frame) packet: Packet for packet in packets: @@ -57,6 +57,7 @@ def decode(self, packet: Packet | None = None): .. seealso:: This is a passthrough to :meth:`.CodecContext.decode`. """ + self._assert_has_codec_context() return self.codec_context.decode(packet) @cython.cfunc diff --git a/tests/test_decode.py b/tests/test_decode.py index 814e0fc0e..e0b55e952 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -1,4 +1,5 @@ import functools +import io import os import pathlib from fractions import Fraction @@ -52,6 +53,34 @@ def make_h264_test_video(path: str) -> None: class TestDecode(TestCase): + def test_decode_stream_without_codec_context(self) -> None: + buffer = io.BytesIO() + with av.open(buffer, "w", format="mp4") as output: + stream = output.add_mux_stream("h264", width=16, height=16) + packet = av.Packet(b"invalid") + packet.stream = stream + packet.pts = packet.dts = 0 + packet.time_base = Fraction(1, 1000) + output.mux(packet) + + # Keep the MP4 video stream while making its codec unknown to FFmpeg. + data = buffer.getvalue().replace(b"avc1", b"zzzz") + with av.open(io.BytesIO(data)) as container: + stream = container.streams.video[0] + assert stream.codec_context is None + with pytest.raises(av.DecoderNotFoundError): + list(container.decode(stream)) + + def test_mux_stream_without_codec_context(self) -> None: + with av.open(io.BytesIO(), "w", format="mp4") as output: + stream = output.add_mux_stream("h264", width=16, height=16) + assert stream.codec_context is None + with pytest.raises(av.EncoderNotFoundError): + stream.encode(None) + + # A bitstream filter only needs to update codecpar for a mux stream. + av.BitStreamFilterContext("h264_mp4toannexb", "h264", out_stream=stream) + def test_decoded_video_frame_count(self) -> None: container = av.open(fate_suite("h264/interlaced_crop.mp4")) video_stream = next(s for s in container.streams if s.type == "video") From 9f2b90720e3fc6aabea6ce1453edc00c4ad2278c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 22 Jul 2026 02:50:26 -0400 Subject: [PATCH 833/846] Expose codec level through CodecContext Closes #2348. --- av/codec/context.py | 13 +++++++++++++ av/codec/context.pyi | 1 + include/avcodec.pxd | 1 + tests/test_codec_context.py | 8 ++++++++ 4 files changed, 23 insertions(+) diff --git a/av/codec/context.py b/av/codec/context.py index 6fefb1857..3d3c2ba91 100644 --- a/av/codec/context.py +++ b/av/codec/context.py @@ -661,6 +661,19 @@ def profile(self, value): return i += 1 + @property + def level(self): + """Codec level. + + Wraps :ffmpeg:`AVCodecContext.level`. + + """ + return self.ptr.level + + @level.setter + def level(self, value: cython.int): + self.ptr.level = value + @property def time_base(self): if self.is_decoder: diff --git a/av/codec/context.pyi b/av/codec/context.pyi index 808bb292b..e74502571 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -57,6 +57,7 @@ class CodecContext: type: Literal["video", "audio", "data", "subtitle", "attachment"] options: dict[str, str] profile: str | None + level: int @property def profiles(self) -> list[str]: ... extradata: bytes | None diff --git a/include/avcodec.pxd b/include/avcodec.pxd index 02e9ac9ca..2fec164e4 100644 --- a/include/avcodec.pxd +++ b/include/avcodec.pxd @@ -280,6 +280,7 @@ cdef extern from "libavcodec/avcodec.h" nogil: int thread_type int bits_per_coded_sample int profile + int level AVDiscard skip_frame int subtitle_header_size diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 730b50bbe..d1af537c5 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -75,6 +75,14 @@ def test_global_quality(self): ctx.global_quality = 5 assert ctx.global_quality == 5 + def test_level(self): + with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: + assert container.streams.video[0].codec_context.level == 41 + + ctx = Codec("mpeg4", "w").create() + ctx.level = 5 + assert ctx.level == 5 + def test_skip_frame_default(self): ctx = Codec("png", "w").create() assert ctx.skip_frame == "DEFAULT" From 46a67aae54225cead809515e89952e619f1b3dff Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 22 Jul 2026 17:02:19 -0400 Subject: [PATCH 834/846] Expose `AVCodecContext.field_order` Closes #2347. --- av/video/codeccontext.py | 13 +++++++++++++ av/video/codeccontext.pyi | 1 + include/avcodec.pxd | 1 + tests/test_codec_context.py | 8 ++++++++ 4 files changed, 23 insertions(+) diff --git a/av/video/codeccontext.py b/av/video/codeccontext.py index d3f36514c..3dca9619b 100644 --- a/av/video/codeccontext.py +++ b/av/video/codeccontext.py @@ -434,6 +434,19 @@ def colorspace(self): def colorspace(self, value): self.ptr.colorspace = value + @property + def field_order(self): + """The video field order as FFmpeg's raw integer value. + + Wraps :ffmpeg:`AVCodecContext.field_order`. + + """ + return self.ptr.field_order + + @field_order.setter + def field_order(self, value: cython.int): + self.ptr.field_order = value + @property def max_b_frames(self): """ diff --git a/av/video/codeccontext.pyi b/av/video/codeccontext.pyi index 603dc4dcb..6f11c9aa9 100644 --- a/av/video/codeccontext.pyi +++ b/av/video/codeccontext.pyi @@ -32,6 +32,7 @@ class VideoCodecContext(CodecContext): color_primaries: int color_trc: int colorspace: int + field_order: int qmin: int qmax: int type: Literal["video"] diff --git a/include/avcodec.pxd b/include/avcodec.pxd index 2fec164e4..2255738f6 100644 --- a/include/avcodec.pxd +++ b/include/avcodec.pxd @@ -252,6 +252,7 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVColorTransferCharacteristic color_trc AVColorSpace colorspace AVColorRange color_range + int field_order int has_b_frames AVPixelFormat (*get_format)(AVCodecContext *s, const AVPixelFormat *fmt) diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index d1af537c5..db8c71bb3 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -83,6 +83,14 @@ def test_level(self): ctx.level = 5 assert ctx.level == 5 + def test_field_order(self): + with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: + assert container.streams.video[0].codec_context.field_order == 2 + + ctx = Codec("mpeg4", "w").create() + ctx.field_order = 1 + assert ctx.field_order == 1 + def test_skip_frame_default(self): ctx = Codec("png", "w").create() assert ctx.skip_frame == "DEFAULT" From 2bd7933d021fcb1d4e0fe67049b65b43b4a0be46 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 22 Jul 2026 17:26:30 -0400 Subject: [PATCH 835/846] Allow setting video rate after adding stream Synchronize the stream average frame rate from the codec context before writing the output header. Closes #2301. --- av/video/stream.py | 2 ++ tests/test_encode.py | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/av/video/stream.py b/av/video/stream.py index be1fcaf9d..ed8a79aea 100644 --- a/av/video/stream.py +++ b/av/video/stream.py @@ -63,6 +63,8 @@ def decode(self, packet: Packet | None = None): @cython.cfunc def _finalize_for_output(self): Stream._finalize_for_output(self) + if self.codec_context is not None: + self.ptr.avg_frame_rate = self.codec_context.ptr.framerate # avcodec_parameters_from_context() overwrites codecpar.coded_side_data, # so inject the display matrix after it, before avformat_write_header(). if self.codec_context is not None and self._has_display_matrix: diff --git a/tests/test_encode.py b/tests/test_encode.py index 294cc815b..e6015ae1b 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -162,6 +162,26 @@ def test_encoding_with_pts(self) -> None: assert packet.time_base == Fraction(1, 24) output.mux(packet) + def test_set_rate_after_add_stream(self) -> None: + path = self.sandboxed("deferred_rate.mp4") + + with av.open(path, "w") as output: + stream = output.add_stream("mpeg4") + stream.codec_context.framerate = Fraction(30, 1) + stream.width = 16 + stream.height = 16 + + for i in range(30): + frame = VideoFrame(16, 16, "yuv420p") + frame.pts = i + frame.time_base = Fraction(1, 30) + output.mux(stream.encode(frame)) + + output.mux(stream.encode(None)) + + with av.open(path) as input_: + assert input_.streams.video[0].average_rate == 30 + def test_encoding_with_unicode_filename(self) -> None: path = self.sandboxed("¢∞§¶•ªº.mov") From 611766271411ebf70d49d7cb1e6f6cc2d513743e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 22 Jul 2026 17:48:27 -0400 Subject: [PATCH 836/846] Add Packet.rescale_ts --- CHANGELOG.rst | 1 + av/packet.py | 21 +++++++++++++++++++++ av/packet.pyi | 2 ++ tests/test_packet.py | 40 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 64 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 48fe78d4d..2814b76ab 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -33,6 +33,7 @@ v18.1.0 (Unreleased) Features: +- Add ``Packet.rescale_ts()`` to rescale packet PTS, DTS, and duration to a new ``AVRational`` time base by :gh-user:`WyattBlue`. - Support reusing the thread's current CUDA context via a ``current_ctx`` flag on ``CudaContext`` and ``VideoFrame.from_dlpack``, for interop with libraries like PyTorch that initialize CUDA first by :gh-user:`Yozer` (:pr:`2339`). - ``VideoFrame.from_dlpack`` no longer requires restating ``primary_ctx``/``current_ctx`` when passing an explicit ``cuda_context``; the flags are only validated when explicitly given by :gh-user:`WyattBlue`. diff --git a/av/packet.py b/av/packet.py index 8be300661..3fb7d4f4c 100644 --- a/av/packet.py +++ b/av/packet.py @@ -11,6 +11,8 @@ from cython.cimports.libc.stdint import uint8_t from cython.cimports.libc.string import memcpy +from av.rational import AVRational + # Check https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/packet.h#L41 # for new additions in the future ffmpeg releases # Note: the order must follow that of the AVPacketSideDataType enum def @@ -289,6 +291,25 @@ def _rebase_time(self, dst: lib.AVRational): lib.av_packet_rescale_ts(self.ptr, self.ptr.time_base, dst) self.ptr.time_base = dst + def rescale_ts(self, time_base): + """Rescale the packet timestamps to a new time base. + + This rescales :attr:`pts`, :attr:`dts`, and :attr:`duration`, then updates + :attr:`time_base`. If the current time base is unset, the timestamp values + are unchanged and the new time base is assigned. + + :meth:`~av.container.OutputContainer.mux` already performs this operation + automatically when necessary. + + Wraps :ffmpeg:`av_packet_rescale_ts`. + """ + if not isinstance(time_base, AVRational): + raise TypeError("time_base must be an AVRational") + + dst: lib.AVRational + to_avrational(time_base, cython.address(dst)) + self._rebase_time(dst) + def decode(self): """ Send the packet's data to the decoder and return a list of diff --git a/av/packet.pyi b/av/packet.pyi index 805a3cf97..1f005bf27 100644 --- a/av/packet.pyi +++ b/av/packet.pyi @@ -4,6 +4,7 @@ from typing import Generic, Literal, TypeVar, overload from av.audio.frame import AudioFrame from av.audio.stream import AudioStream +from av.rational import AVRational from av.stream import Stream from av.subtitles.stream import SubtitleStream from av.subtitles.subtitle import AssSubtitle, BitmapSubtitle @@ -92,6 +93,7 @@ class Packet(Buffer, Generic[StreamT]): is_disposable: bool def __init__(self: Packet[Stream], input: int | bytes | None = None) -> None: ... + def rescale_ts(self, time_base: AVRational) -> None: ... # Overloads that return the same type as the stream's decode method @overload diff --git a/tests/test_packet.py b/tests/test_packet.py index c4503caad..e266bbe3c 100644 --- a/tests/test_packet.py +++ b/tests/test_packet.py @@ -6,6 +6,7 @@ from unittest import SkipTest import numpy +import pytest import av @@ -63,6 +64,45 @@ def test_data_packet_bytes(self): class TestProperties: + def test_rescale_ts(self) -> None: + packet = av.Packet() + packet.time_base = fractions.Fraction(1, 1000) + packet.pts = 1000 + packet.dts = 900 + packet.duration = 40 + + packet.rescale_ts(av.AVRational(1, 100)) + + assert packet.time_base == fractions.Fraction(1, 100) + assert packet.pts == 100 + assert packet.dts == 90 + assert packet.duration == 4 + + def test_rescale_ts_without_source_time_base(self) -> None: + packet = av.Packet() + packet.pts = 1000 + packet.dts = 900 + packet.duration = 40 + + packet.rescale_ts(av.AVRational(1, 100)) + + assert packet.time_base == fractions.Fraction(1, 100) + assert packet.pts == 1000 + assert packet.dts == 900 + assert packet.duration == 40 + + def test_rescale_ts_rejects_zero_time_base(self) -> None: + packet = av.Packet() + + with pytest.raises(ValueError, match="Cannot rebase to zero time"): + packet.rescale_ts(av.AVRational(0, 1)) + + def test_rescale_ts_requires_avrational(self) -> None: + packet = av.Packet() + + with pytest.raises(TypeError, match="time_base must be an AVRational"): + packet.rescale_ts(fractions.Fraction(1, 100)) # type: ignore[arg-type] + def test_is_keyframe(self) -> None: with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: stream = container.streams.video[0] From 931543040a4896210344d7772402807229e3475f Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 22 Jul 2026 17:57:17 -0400 Subject: [PATCH 837/846] Update bindings for FFmpeg 8.0 --- av/_core.pxd | 12 +++---- av/video/codeccontext.py | 2 +- include/avcodec.pxd | 78 ++++++++++++++++++++-------------------- include/avdevice.pxd | 6 ++-- include/avfilter.pxd | 22 ++++++------ include/avformat.pxd | 36 +++++++++---------- include/avutil.pxd | 72 +++++++++++++++++++------------------ 7 files changed, 115 insertions(+), 113 deletions(-) diff --git a/av/_core.pxd b/av/_core.pxd index 43d1e716d..2d9e95209 100644 --- a/av/_core.pxd +++ b/av/_core.pxd @@ -1,9 +1,9 @@ cdef extern from "libswscale/swscale.h" nogil: - cdef int swscale_version() - cdef char* swscale_configuration() - cdef char* swscale_license() + cdef unsigned int swscale_version() + cdef const char* swscale_configuration() + cdef const char* swscale_license() cdef extern from "libswresample/swresample.h" nogil: - cdef int swresample_version() - cdef char* swresample_configuration() - cdef char* swresample_license() + cdef unsigned int swresample_version() + cdef const char* swresample_configuration() + cdef const char* swresample_license() diff --git a/av/video/codeccontext.py b/av/video/codeccontext.py index 3dca9619b..2e39a7feb 100644 --- a/av/video/codeccontext.py +++ b/av/video/codeccontext.py @@ -445,7 +445,7 @@ def field_order(self): @field_order.setter def field_order(self, value: cython.int): - self.ptr.field_order = value + self.ptr.field_order = cython.cast(lib.AVFieldOrder, value) @property def max_b_frames(self): diff --git a/include/avcodec.pxd b/include/avcodec.pxd index 2255738f6..0a37cb91c 100644 --- a/include/avcodec.pxd +++ b/include/avcodec.pxd @@ -9,19 +9,19 @@ cdef extern from "libavutil/channel_layout.h" nogil: ctypedef struct AVChannelLayout: int nb_channels - int av_channel_layout_default(AVChannelLayout *ch_layout, int nb_channels) + void av_channel_layout_default(AVChannelLayout *ch_layout, int nb_channels) int av_channel_layout_from_string(AVChannelLayout *channel_layout, const char *str) int av_channel_layout_describe(const AVChannelLayout *channel_layout, char *buf, size_t buf_size) int av_channel_name(char *buf, size_t buf_size, AVChannel channel_id) int av_channel_description(char *buf, size_t buf_size, AVChannel channel_id) - int av_channel_layout_compare(AVChannelLayout *chl, AVChannelLayout *chl1) - AVChannel av_channel_layout_channel_from_index(AVChannelLayout *channel_layout, unsigned int idx) + int av_channel_layout_compare(const AVChannelLayout *chl, const AVChannelLayout *chl1) + AVChannel av_channel_layout_channel_from_index(const AVChannelLayout *channel_layout, unsigned int idx) void av_channel_layout_uninit(AVChannelLayout *channel_layout) cdef extern from "libavcodec/avcodec.h" nogil: - cdef int avcodec_version() - cdef char* avcodec_configuration() - cdef char* avcodec_license() + cdef unsigned int avcodec_version() + cdef const char* avcodec_configuration() + cdef const char* avcodec_license() AVPixelFormat avcodec_find_best_pix_fmt_of_list( const AVPixelFormat *pix_fmt_list, @@ -71,7 +71,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: AV_CODEC_FLAG_4MV AV_CODEC_FLAG_OUTPUT_CORRUPT AV_CODEC_FLAG_QPEL - AV_CODEC_FLAG_DROPCHANGED AV_CODEC_FLAG_RECON_FRAME AV_CODEC_FLAG_COPY_OPAQUE AV_CODEC_FLAG_FRAME_DURATION @@ -168,15 +167,15 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVDISCARD_ALL cdef struct AVCodec: - char *name - char *long_name + const char *name + const char *long_name AVMediaType type AVCodecID id int capabilities - AVClass *priv_class + const AVClass *priv_class - cdef int av_codec_is_encoder(AVCodec*) - cdef int av_codec_is_decoder(AVCodec*) + cdef int av_codec_is_encoder(const AVCodec*) + cdef int av_codec_is_decoder(const AVCodec*) cdef enum AVCodecConfig: AV_CODEC_CONFIG_PIX_FORMAT @@ -195,18 +194,18 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef struct AVProfile: int profile - char *name + const char *name cdef struct AVCodecDescriptor: AVCodecID id AVMediaType type - char *name - char *long_name + const char *name + const char *long_name int props - char **mime_types - AVProfile *profiles + const char *const *mime_types + const AVProfile *profiles - AVCodecDescriptor* avcodec_descriptor_get(AVCodecID) + const AVCodecDescriptor* avcodec_descriptor_get(AVCodecID) cdef enum: AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX @@ -222,11 +221,14 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef struct AVHWAccel: pass + cdef enum AVFieldOrder: + pass + cdef struct AVCodecContext: - AVClass *av_class + const AVClass *av_class AVMediaType codec_type - AVCodec *codec + const AVCodec *codec AVCodecID codec_id unsigned int codec_tag @@ -252,7 +254,7 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVColorTransferCharacteristic color_trc AVColorSpace colorspace AVColorRange color_range - int field_order + AVFieldOrder field_order int has_b_frames AVPixelFormat (*get_format)(AVCodecContext *s, const AVPixelFormat *fmt) @@ -273,7 +275,7 @@ cdef extern from "libavcodec/avcodec.h" nogil: int64_t rc_max_rate int64_t rc_min_rate - AVHWAccel *hwaccel + const AVHWAccel *hwaccel AVBufferRef *hw_device_ctx AVBufferRef *hw_frames_ctx @@ -290,15 +292,15 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef AVCodecContext* avcodec_alloc_context3(const AVCodec *codec) cdef void avcodec_free_context(AVCodecContext **ctx) - cdef AVClass* avcodec_get_class() + cdef const AVClass* avcodec_get_class() cdef const AVCodec* avcodec_find_decoder(AVCodecID id) cdef const AVCodec* avcodec_find_encoder(AVCodecID id) - cdef const AVCodec* avcodec_find_decoder_by_name(char *name) - cdef const AVCodec* avcodec_find_encoder_by_name(char *name) + cdef const AVCodec* avcodec_find_decoder_by_name(const char *name) + cdef const AVCodec* avcodec_find_encoder_by_name(const char *name) cdef const AVCodec* av_codec_iterate(void **opaque) cdef const AVCodecDescriptor* avcodec_descriptor_get(AVCodecID id) - cdef const AVCodecDescriptor* avcodec_descriptor_get_by_name(char *name) - cdef char* avcodec_get_name(AVCodecID id) + cdef const AVCodecDescriptor* avcodec_descriptor_get_by_name(const char *name) + cdef const char* avcodec_get_name(AVCodecID id) cdef int avcodec_open2(AVCodecContext *ctx, const AVCodec *codec, AVDictionary **options) cdef enum AVPacketSideDataType: AV_PKT_DATA_NEW_EXTRADATA @@ -346,8 +348,8 @@ cdef extern from "libavcodec/avcodec.h" nogil: # See: http://ffmpeg.org/doxygen/trunk/structAVFrame.html cdef struct AVFrame: - uint8_t *data[4] - int linesize[4] + uint8_t *data[8] + int linesize[8] uint8_t **extended_data int width int height @@ -378,7 +380,7 @@ cdef extern from "libavcodec/avcodec.h" nogil: int64_t duration cdef struct AVPacket: - void *buf + AVBufferRef *buf int64_t pts int64_t dts uint8_t *data @@ -397,7 +399,7 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVFrame *frame, int nb_channels, AVSampleFormat sample_fmt, - uint8_t *buf, + const uint8_t *buf, int buf_size, int align ) @@ -437,20 +439,20 @@ cdef extern from "libavcodec/avcodec.h" nogil: int64_t pts cdef int avcodec_decode_subtitle2( - AVCodecContext *ctx, AVSubtitle *sub, int *done, AVPacket *pkt, + AVCodecContext *ctx, AVSubtitle *sub, int *done, const AVPacket *pkt, ) cdef int avcodec_encode_subtitle( - AVCodecContext *avctx, uint8_t *buf, int buf_size, AVSubtitle *sub + AVCodecContext *avctx, uint8_t *buf, int buf_size, const AVSubtitle *sub ) cdef void avsubtitle_free(AVSubtitle*) cdef void avcodec_flush_buffers(AVCodecContext *ctx) - cdef int avcodec_send_packet(AVCodecContext *avctx, AVPacket *packet) + cdef int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *packet) cdef int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame) - cdef int avcodec_send_frame(AVCodecContext *avctx, AVFrame *frame) + cdef int avcodec_send_frame(AVCodecContext *avctx, const AVFrame *frame) cdef int avcodec_receive_packet(AVCodecContext *avctx, AVPacket *avpkt) cdef struct AVCodecParser: - int codec_ids[5] + int codec_ids[7] cdef struct AVCodecParserContext: int64_t pts @@ -497,7 +499,7 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef extern from "libavcodec/bsf.h" nogil: cdef struct AVBitStreamFilter: const char *name - AVCodecID *codec_ids + const AVCodecID *codec_ids cdef struct AVCodecParameters: pass @@ -510,7 +512,7 @@ cdef extern from "libavcodec/bsf.h" nogil: cdef int av_bsf_list_parse_str(const char *str, AVBSFContext **bsf) cdef int av_bsf_init(AVBSFContext *ctx) cdef void av_bsf_free(AVBSFContext **ctx) - cdef AVBitStreamFilter* av_bsf_iterate(void **opaque) + cdef const AVBitStreamFilter* av_bsf_iterate(void **opaque) cdef int av_bsf_send_packet(AVBSFContext *ctx, AVPacket *pkt) cdef int av_bsf_receive_packet(AVBSFContext *ctx, AVPacket *pkt) cdef void av_bsf_flush(AVBSFContext *ctx) diff --git a/include/avdevice.pxd b/include/avdevice.pxd index 9631337fa..18a4dfed4 100644 --- a/include/avdevice.pxd +++ b/include/avdevice.pxd @@ -1,7 +1,7 @@ cdef extern from "libavdevice/avdevice.h" nogil: - cdef int avdevice_version() - cdef char* avdevice_configuration() - cdef char* avdevice_license() + cdef unsigned int avdevice_version() + cdef const char* avdevice_configuration() + cdef const char* avdevice_license() void avdevice_register_all() cdef struct AVDeviceInfo: diff --git a/include/avfilter.pxd b/include/avfilter.pxd index afb953fdd..2905a8c88 100644 --- a/include/avfilter.pxd +++ b/include/avfilter.pxd @@ -1,7 +1,7 @@ cdef extern from "libavfilter/avfilter.h" nogil: - cdef int avfilter_version() - cdef char* avfilter_configuration() - cdef char* avfilter_license() + cdef unsigned int avfilter_version() + cdef const char* avfilter_configuration() + cdef const char* avfilter_license() cdef struct AVFilterPad: pass @@ -19,12 +19,12 @@ cdef extern from "libavfilter/avfilter.h" nogil: const AVClass *priv_class int flags - cdef AVFilter* avfilter_get_by_name(const char *name) + cdef const AVFilter* avfilter_get_by_name(const char *name) cdef const AVFilter* av_filter_iterate(void **opaque) cdef struct AVFilterContext: - AVClass *av_class - AVFilter *filter + const AVClass *av_class + const AVFilter *filter char *name @@ -39,24 +39,24 @@ cdef extern from "libavfilter/avfilter.h" nogil: cdef int avfilter_init_str(AVFilterContext *ctx, const char *args) cdef int avfilter_init_dict(AVFilterContext *ctx, AVDictionary **options) cdef void avfilter_free(AVFilterContext*) - cdef AVClass* avfilter_get_class() + cdef const AVClass* avfilter_get_class() cdef struct AVFilterLink: AVFilterContext *src AVFilterPad *srcpad AVFilterContext *dst AVFilterPad *dstpad - AVMediaType Type + AVMediaType type int w int h AVRational sample_aspect_ratio - uint64_t channel_layout + AVChannelLayout ch_layout int sample_rate int format AVRational time_base cdef struct AVFilterGraph: - int nb_filters + unsigned int nb_filters AVFilterContext **filters int nb_threads @@ -75,7 +75,7 @@ cdef extern from "libavfilter/avfilter.h" nogil: ) cdef int avfilter_graph_create_filter( AVFilterContext **filt_ctx, - AVFilter *filt, + const AVFilter *filt, const char *name, const char *args, void *opaque, diff --git a/include/avformat.pxd b/include/avformat.pxd index 54103f723..9a18e5a9b 100644 --- a/include/avformat.pxd +++ b/include/avformat.pxd @@ -2,9 +2,9 @@ from libc.stdint cimport int64_t, uint64_t cdef extern from "libavformat/avformat.h" nogil: - cdef int avformat_version() - cdef char* avformat_configuration() - cdef char* avformat_license() + cdef unsigned int avformat_version() + cdef const char* avformat_configuration() + cdef const char* avformat_license() cdef int AV_TIME_BASE cdef int AVSEEK_FLAG_BACKWARD @@ -91,8 +91,6 @@ cdef extern from "libavformat/avformat.h" nogil: int flags const AVClass *priv_class - int avformat_query_codec(const AVOutputFormat *oformat, AVCodecID codec_id, int std_compliance) - # AVInputFormat.flags and AVOutputFormat.flags cdef enum: AVFMT_NOFILE @@ -134,7 +132,7 @@ cdef extern from "libavformat/avformat.h" nogil: AVMediaType type, int wanted_stream_nb, int related_stream, - AVCodec **decoder_ret, + const AVCodec **decoder_ret, int flags ) @@ -142,6 +140,7 @@ cdef extern from "libavformat/avformat.h" nogil: # http://ffmpeg.org/doxygen/trunk/structAVFormatContext.html cdef struct AVFormatContext: + const AVClass *av_class unsigned int nb_streams AVStream **streams unsigned int nb_chapters @@ -151,7 +150,6 @@ cdef extern from "libavformat/avformat.h" nogil: AVIOContext *pb AVIOInterruptCB interrupt_callback AVDictionary *metadata - char filename int64_t start_time int64_t duration int64_t bit_rate @@ -171,23 +169,23 @@ cdef extern from "libavformat/avformat.h" nogil: cdef AVFormatContext* avformat_alloc_context() cdef int avformat_open_input( AVFormatContext **ctx, - char *filename, + const char *filename, const AVInputFormat *format, AVDictionary **options ) - cdef int avformat_close_input(AVFormatContext **ctx) + cdef void avformat_close_input(AVFormatContext **ctx) cdef int avformat_write_header(AVFormatContext *ctx, AVDictionary **options) cdef int av_write_trailer(AVFormatContext *ctx) cdef int av_interleaved_write_frame(AVFormatContext *ctx, AVPacket *pkt) cdef int av_write_frame(AVFormatContext *ctx, AVPacket *pkt) - cdef int avio_open(AVIOContext **s, char *url, int flags) + cdef int avio_open(AVIOContext **s, const char *url, int flags) cdef int64_t avio_size(AVIOContext *s) cdef const AVOutputFormat* av_guess_format( - char *short_name, char *filename, char *mime_type + const char *short_name, const char *filename, const char *mime_type ) cdef int avformat_query_codec( - AVOutputFormat *ofmt, AVCodecID codec_id, int std_compliance + const AVOutputFormat *ofmt, AVCodecID codec_id, int std_compliance ) cdef void avio_flush(AVIOContext *s) cdef int avio_close(AVIOContext *s) @@ -197,12 +195,12 @@ cdef extern from "libavformat/avformat.h" nogil: cdef int avformat_alloc_output_context2( AVFormatContext **ctx, const AVOutputFormat *oformat, - char *format_name, - char *filename + const char *format_name, + const char *filename ) - cdef int avformat_free_context(AVFormatContext *ctx) - cdef AVClass* avformat_get_class() - cdef void av_dump_format(AVFormatContext *ctx, int index, char *url, int is_output) + cdef void avformat_free_context(AVFormatContext *ctx) + cdef const AVClass* avformat_get_class() + cdef void av_dump_format(AVFormatContext *ctx, int index, const char *url, int is_output) cdef int av_read_frame(AVFormatContext *ctx, AVPacket *packet) cdef int av_seek_frame( AVFormatContext *ctx, int stream_index, int64_t timestamp, int flags @@ -237,6 +235,6 @@ cdef extern from "libavformat/avformat.h" nogil: AVINDEX_KEYFRAME AVINDEX_DISCARD_FRAME - cdef AVIndexEntry *avformat_index_get_entry(AVStream *st, int idx) - cdef int avformat_index_get_entries_count(AVStream *st) + cdef const AVIndexEntry *avformat_index_get_entry(AVStream *st, int idx) + cdef int avformat_index_get_entries_count(const AVStream *st) cdef int av_index_search_timestamp(AVStream *st, int64_t timestamp, int flags) diff --git a/include/avutil.pxd b/include/avutil.pxd index 615b6979d..2cf76c230 100644 --- a/include/avutil.pxd +++ b/include/avutil.pxd @@ -8,15 +8,15 @@ cdef extern from "libavutil/audio_fifo.h" nogil: cdef AVAudioFifo* av_audio_fifo_alloc( AVSampleFormat sample_fmt, int channels, int nb_samples ) - cdef int av_audio_fifo_write(AVAudioFifo *af, void **data, int nb_samples) - cdef int av_audio_fifo_read(AVAudioFifo *af, void **data, int nb_samples) + cdef int av_audio_fifo_write(AVAudioFifo *af, void *const *data, int nb_samples) + cdef int av_audio_fifo_read(AVAudioFifo *af, void *const *data, int nb_samples) cdef int av_audio_fifo_size(AVAudioFifo *af) cdef extern from "libavutil/avutil.h" nogil: cdef const char* av_version_info() - cdef int avutil_version() - cdef char* avutil_configuration() - cdef char* avutil_license() + cdef unsigned int avutil_version() + cdef const char* avutil_configuration() + cdef const char* avutil_license() int FF_QP2LAMBDA @@ -113,18 +113,14 @@ cdef extern from "libavutil/avutil.h" nogil: cdef extern from "libavutil/buffer.h" nogil: AVBufferRef *av_buffer_create(uint8_t *data, size_t size, void (*free)(void *opaque, uint8_t *data), void *opaque, int flags) - AVBufferRef* av_buffer_ref(AVBufferRef *buf) + AVBufferRef* av_buffer_ref(const AVBufferRef *buf) void av_buffer_unref(AVBufferRef **buf) cdef struct AVBuffer: - uint8_t *data - int size - void (*free)(void *opaque, uint8_t *data) - void *opaque - int flags + pass cdef struct AVBufferRef: AVBuffer *buffer uint8_t *data - int size + size_t size cdef extern from "libavutil/dict.h" nogil: # See: http://ffmpeg.org/doxygen/trunk/structAVDictionary.html @@ -137,13 +133,16 @@ cdef extern from "libavutil/dict.h" nogil: cdef int AV_DICT_IGNORE_SUFFIX cdef void av_dict_free(AVDictionary **) cdef AVDictionaryEntry* av_dict_get( - AVDictionary *dict, char *key, AVDictionaryEntry *prev, int flags + const AVDictionary *dict, + const char *key, + const AVDictionaryEntry *prev, + int flags ) cdef int av_dict_set( AVDictionary **pm, const char *key, const char *value, int flags ) - cdef int av_dict_count(AVDictionary *m) - cdef int av_dict_copy(AVDictionary **dst, AVDictionary *src, int flags) + cdef int av_dict_count(const AVDictionary *m) + cdef int av_dict_copy(AVDictionary **dst, const AVDictionary *src, int flags) cdef extern from "libavutil/display.h" nogil: cdef double av_display_rotation_get(const int32_t matrix[9]) @@ -189,9 +188,12 @@ cdef extern from "libavutil/frame.h" nogil: cdef int av_frame_get_buffer(AVFrame *frame, int align) cdef int av_frame_make_writable(AVFrame *frame) cdef int av_frame_copy_props(AVFrame *dst, const AVFrame *src) - cdef AVFrameSideData* av_frame_get_side_data(AVFrame *frame, AVFrameSideDataType type) + cdef AVFrameSideData* av_frame_get_side_data(const AVFrame *frame, AVFrameSideDataType type) cdef extern from "libavutil/hwcontext.h" nogil: + cdef struct AVHWDeviceContext: + pass + enum AVHWDeviceType: AV_HWDEVICE_TYPE_NONE AV_HWDEVICE_TYPE_VDPAU @@ -208,15 +210,15 @@ cdef extern from "libavutil/hwcontext.h" nogil: AV_HWDEVICE_TYPE_D3D12VA ctypedef struct AVHWFramesContext: - const void *av_class + const AVClass *av_class AVBufferRef *device_ref - void *device_ctx + AVHWDeviceContext *device_ctx void *hwctx + int initial_pool_size AVPixelFormat format AVPixelFormat sw_format int width int height - int initial_pool_size cdef int av_hwdevice_ctx_create(AVBufferRef **device_ctx, AVHWDeviceType type, const char *device, AVDictionary *opts, int flags) cdef AVHWDeviceType av_hwdevice_find_type_by_name(const char *name) @@ -304,7 +306,7 @@ cdef extern from "libavutil/opt.h" nogil: AV_OPT_TYPE_CHLAYOUT AV_OPT_TYPE_BOOL - cdef struct AVOption_default_val: + cdef union AVOption_default_val: int64_t i64 double dbl const char *str @@ -334,11 +336,11 @@ cdef extern from "libavutil/opt.h" nogil: cdef extern from "libavutil/pixdesc.h" nogil: # See: http://ffmpeg.org/doxygen/trunk/structAVComponentDescriptor.html cdef struct AVComponentDescriptor: - unsigned int plane - unsigned int step - unsigned int offset - unsigned int shift - unsigned int depth + int plane + int step + int offset + int shift + int depth cdef enum AVPixFmtFlags: AV_PIX_FMT_FLAG_BE @@ -354,13 +356,13 @@ cdef extern from "libavutil/pixdesc.h" nogil: uint8_t nb_components uint8_t log2_chroma_w uint8_t log2_chroma_h - uint8_t flags + uint64_t flags AVComponentDescriptor comp[4] cdef const AVPixFmtDescriptor* av_pix_fmt_desc_get(AVPixelFormat pix_fmt) cdef const AVPixFmtDescriptor* av_pix_fmt_desc_next(const AVPixFmtDescriptor *prev) - cdef char * av_get_pix_fmt_name(AVPixelFormat pix_fmt) - cdef AVPixelFormat av_get_pix_fmt(char* name) + cdef const char *av_get_pix_fmt_name(AVPixelFormat pix_fmt) + cdef AVPixelFormat av_get_pix_fmt(const char *name) int av_get_bits_per_pixel(const AVPixFmtDescriptor *pixdesc) int av_get_padded_bits_per_pixel(const AVPixFmtDescriptor *pixdesc) @@ -379,8 +381,8 @@ cdef extern from "libavutil/samplefmt.h" nogil: AV_SAMPLE_FMT_FLT AV_SAMPLE_FMT_DBL - cdef AVSampleFormat av_get_sample_fmt(char* name) - cdef char *av_get_sample_fmt_name(AVSampleFormat sample_fmt) + cdef AVSampleFormat av_get_sample_fmt(const char *name) + cdef const char *av_get_sample_fmt_name(AVSampleFormat sample_fmt) cdef int av_get_bytes_per_sample(AVSampleFormat sample_fmt) cdef int av_sample_fmt_is_planar(AVSampleFormat sample_fmt) cdef AVSampleFormat av_get_packed_sample_fmt(AVSampleFormat sample_fmt) @@ -401,7 +403,7 @@ cdef extern from "libavutil/video_enc_params.h" nogil: AV_VIDEO_ENC_PARAMS_MPEG2 cdef struct AVVideoEncParams: - uint32_t nb_blocks + unsigned int nb_blocks size_t blocks_offset size_t block_size AVVideoEncParamsType type @@ -409,10 +411,10 @@ cdef extern from "libavutil/video_enc_params.h" nogil: int32_t delta_qp[4][2] cdef struct AVVideoBlockParams: - int32_t src_x - int32_t src_y - int32_t w - int32_t h + int src_x + int src_y + int w + int h int32_t delta_qp cdef extern from "stdarg.h" nogil: From 9df62cc1b479a456e2c8901151dcc41b84884619 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 22 Jul 2026 18:14:38 -0400 Subject: [PATCH 838/846] Fix subtitle decode compiler warnings --- av/codec/context.pxd | 1 + av/codec/context.py | 4 ++++ av/subtitles/codeccontext.pxd | 2 +- av/subtitles/codeccontext.py | 4 ++-- setup.py | 8 -------- 5 files changed, 8 insertions(+), 11 deletions(-) diff --git a/av/codec/context.pxd b/av/codec/context.pxd index 8108ecb1a..48d0bb9bd 100644 --- a/av/codec/context.pxd +++ b/av/codec/context.pxd @@ -28,6 +28,7 @@ cdef class CodecContext: # Wraps both versions of the transcode API, returning lists. cpdef encode(self, Frame frame=?) cpdef decode(self, Packet packet=?) + cdef _decode(self, Packet packet) cpdef flush_buffers(self) # Used by hardware-accelerated decode. diff --git a/av/codec/context.py b/av/codec/context.py index 3d3c2ba91..3b1395590 100644 --- a/av/codec/context.py +++ b/av/codec/context.py @@ -555,6 +555,10 @@ def decode(self, packet: Packet | None = None): from multiple threads, give each thread its own :class:`CodecContext`. """ + return self._decode(packet) + + @cython.cfunc + def _decode(self, packet: Packet | None): if not self.codec.ptr: raise ValueError("cannot decode unknown codec") diff --git a/av/subtitles/codeccontext.pxd b/av/subtitles/codeccontext.pxd index 0be45ed10..fb5748294 100644 --- a/av/subtitles/codeccontext.pxd +++ b/av/subtitles/codeccontext.pxd @@ -4,5 +4,5 @@ from av.packet cimport Packet cdef class SubtitleCodecContext(CodecContext): cdef bint subtitle_header_set - cpdef decode(self, Packet packet=?) + cdef _decode(self, Packet packet) cpdef decode2(self, Packet packet) diff --git a/av/subtitles/codeccontext.py b/av/subtitles/codeccontext.py index 618badfb1..34dc868cf 100644 --- a/av/subtitles/codeccontext.py +++ b/av/subtitles/codeccontext.py @@ -92,8 +92,8 @@ def encode_subtitle(self, subtitle: SubtitleSet) -> Packet: return packet - @cython.ccall - def decode(self, packet: Packet | None = None): + @cython.cfunc + def _decode(self, packet: Packet | None): """Decode a subtitle packet, returning a list of :class:`.Subtitle` objects if a subtitle was decoded, or an empty list otherwise.""" if not self.codec.ptr: diff --git a/setup.py b/setup.py index 2cd23d5a8..872c5f8e8 100644 --- a/setup.py +++ b/setup.py @@ -141,12 +141,6 @@ def parse_cflags(raw_flags): IMPORT_NAME = "av" -# Newer compilers treat incompatible pointer types as an error by default; -# downgrade it back to a warning so FFmpeg API churn doesn't break the build. -extra_compile_args = [] -if platform.system() != "Windows": - extra_compile_args.append("-Wno-error=incompatible-pointer-types") - loudnorm_extension = Extension( f"{IMPORT_NAME}.filter.loudnorm", sources=[ @@ -157,7 +151,6 @@ def parse_cflags(raw_flags): libraries=extension_extra["libraries"], library_dirs=extension_extra["library_dirs"], define_macros=define_macros, - extra_compile_args=extra_compile_args, py_limited_api=py_limited_api, ) @@ -204,7 +197,6 @@ def parse_cflags(raw_flags): library_dirs=extension_extra["library_dirs"], sources=[pyx_path], define_macros=define_macros, - extra_compile_args=extra_compile_args, py_limited_api=py_limited_api, ), compiler_directives=compiler_directives, From 2a349d7695f9639a5fb266cf21a2789b2dd5b70d Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 22 Jul 2026 18:20:04 -0400 Subject: [PATCH 839/846] Expose supported codec options (fixes #2032) --- CHANGELOG.rst | 1 + av/codec/context.py | 166 +++++++++++++++++++++++++++++++++++- av/codec/context.pyi | 63 +++++++++++++- docs/api/codec.rst | 8 +- include/avutil.pxd | 12 +++ tests/test_codec_context.py | 31 +++++++ 6 files changed, 277 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2814b76ab..2ad54e184 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -33,6 +33,7 @@ v18.1.0 (Unreleased) Features: +- Add ``CodecContext.supported_options`` to discover generic and codec-specific options by :gh-user:`WyattBlue` (:issue:`2032`). - Add ``Packet.rescale_ts()`` to rescale packet PTS, DTS, and duration to a new ``AVRational`` time base by :gh-user:`WyattBlue`. - Support reusing the thread's current CUDA context via a ``current_ctx`` flag on ``CudaContext`` and ``VideoFrame.from_dlpack``, for interop with libraries like PyTorch that initialize CUDA first by :gh-user:`Yozer` (:pr:`2339`). - ``VideoFrame.from_dlpack`` no longer requires restating ``primary_ctx``/``current_ctx`` when passing an explicit ``cuda_context``; the flags are only validated when explicitly given by :gh-user:`WyattBlue`. diff --git a/av/codec/context.py b/av/codec/context.py index 3b1395590..65f130dc5 100644 --- a/av/codec/context.py +++ b/av/codec/context.py @@ -1,4 +1,5 @@ -from enum import Flag, IntEnum +from dataclasses import dataclass +from enum import Flag, IntEnum, IntFlag import cython from cython.cimports import libav as lib @@ -10,7 +11,7 @@ from cython.cimports.av.utils import avrational_to_fraction, to_avrational from cython.cimports.libc.errno import EAGAIN from cython.cimports.libc.stdint import uint8_t -from cython.cimports.libc.string import memcpy +from cython.cimports.libc.string import memcpy, strcmp from av.error import InvalidDataError @@ -90,6 +91,142 @@ class Flags2(IntEnum): ro_flush_noop = lib.AV_CODEC_FLAG2_RO_FLUSH_NOOP +class OptionType(IntEnum): + FLAGS = lib.AV_OPT_TYPE_FLAGS + INT = lib.AV_OPT_TYPE_INT + INT64 = lib.AV_OPT_TYPE_INT64 + DOUBLE = lib.AV_OPT_TYPE_DOUBLE + FLOAT = lib.AV_OPT_TYPE_FLOAT + STRING = lib.AV_OPT_TYPE_STRING + RATIONAL = lib.AV_OPT_TYPE_RATIONAL + BINARY = lib.AV_OPT_TYPE_BINARY + DICT = lib.AV_OPT_TYPE_DICT + UINT64 = lib.AV_OPT_TYPE_UINT64 + CONST = lib.AV_OPT_TYPE_CONST + IMAGE_SIZE = lib.AV_OPT_TYPE_IMAGE_SIZE + PIXEL_FMT = lib.AV_OPT_TYPE_PIXEL_FMT + SAMPLE_FMT = lib.AV_OPT_TYPE_SAMPLE_FMT + VIDEO_RATE = lib.AV_OPT_TYPE_VIDEO_RATE + DURATION = lib.AV_OPT_TYPE_DURATION + COLOR = lib.AV_OPT_TYPE_COLOR + CHANNEL_LAYOUT = lib.AV_OPT_TYPE_CHLAYOUT + BOOL = lib.AV_OPT_TYPE_BOOL + UINT = lib.AV_OPT_TYPE_UINT + + +class OptionFlags(IntFlag): + ENCODING_PARAM = lib.AV_OPT_FLAG_ENCODING_PARAM + DECODING_PARAM = lib.AV_OPT_FLAG_DECODING_PARAM + AUDIO_PARAM = lib.AV_OPT_FLAG_AUDIO_PARAM + VIDEO_PARAM = lib.AV_OPT_FLAG_VIDEO_PARAM + SUBTITLE_PARAM = lib.AV_OPT_FLAG_SUBTITLE_PARAM + EXPORT = lib.AV_OPT_FLAG_EXPORT + READONLY = lib.AV_OPT_FLAG_READONLY + BITSTREAM_FILTER_PARAM = lib.AV_OPT_FLAG_BSF_PARAM + RUNTIME_PARAM = lib.AV_OPT_FLAG_RUNTIME_PARAM + FILTERING_PARAM = lib.AV_OPT_FLAG_FILTERING_PARAM + DEPRECATED = lib.AV_OPT_FLAG_DEPRECATED + CHILD_CONSTS = lib.AV_OPT_FLAG_CHILD_CONSTS + + +@dataclass(frozen=True, slots=True) +class CodecOptionChoice: + """A named value accepted by a codec option.""" + + name: str + help: str + + +@dataclass(frozen=True, slots=True) +class CodecOption: + """Description of a generic or codec-specific option.""" + + name: str + help: str + type: OptionType | int + is_array: bool + default: str | None + min: float + max: float + flags: OptionFlags + choices: tuple[CodecOptionChoice, ...] + + +@dataclass(frozen=True, slots=True) +class CodecOptionSet: + """Generic and codec-specific options supported by a codec context.""" + + generic: tuple[CodecOption, ...] + private: tuple[CodecOption, ...] + + +@cython.cfunc +def _get_option_default( + obj: cython.p_void, name: cython.pointer[cython.const[cython.char]] +): + value: cython.pointer[uint8_t] = cython.NULL + if lib.av_opt_get(obj, name, 0, cython.address(value)) < 0: + return None + try: + return cython.cast(cython.p_char, value) if value != cython.NULL else None + finally: + lib.av_free(value) + + +@cython.cfunc +def _get_supported_options(obj: cython.p_void): + options: list = [] + ptr: cython.pointer[cython.const[lib.AVOption]] = lib.av_opt_next(obj, cython.NULL) + choice_ptr: cython.pointer[cython.const[lib.AVOption]] + option_type: object + + while ptr != cython.NULL: + if ptr.type != lib.AV_OPT_TYPE_CONST: + choices: list = [] + if ptr.unit != cython.NULL: + choice_ptr = lib.av_opt_next(obj, cython.NULL) + while choice_ptr != cython.NULL: + if ( + choice_ptr.type == lib.AV_OPT_TYPE_CONST + and choice_ptr.unit != cython.NULL + and strcmp(choice_ptr.unit, ptr.unit) == 0 + ): + choices.append( + CodecOptionChoice( + choice_ptr.name, + choice_ptr.help + if choice_ptr.help != cython.NULL + else "", + ) + ) + choice_ptr = lib.av_opt_next(obj, choice_ptr) + + raw_type = cython.cast(cython.int, ptr.type) + is_array = bool(raw_type & lib.AV_OPT_TYPE_FLAG_ARRAY) + raw_type &= ~lib.AV_OPT_TYPE_FLAG_ARRAY + try: + option_type = OptionType(raw_type) + except ValueError: + option_type = raw_type + + options.append( + CodecOption( + ptr.name, + ptr.help if ptr.help != cython.NULL else "", + option_type, + is_array, + _get_option_default(obj, ptr.name), + ptr.min, + ptr.max, + OptionFlags(ptr.flags), + tuple(choices), + ) + ) + ptr = lib.av_opt_next(obj, ptr) + + return tuple(options) + + @cython.cclass class CodecContext: @staticmethod @@ -108,6 +245,31 @@ def __cinit__(self, sentinel=None, *args, **kwargs): self.stream_index = -1 # This is set by the container immediately. self.is_open = False + @property + def supported_options(self): + """Options supported by this codec context. + + ``generic`` contains options provided by :ffmpeg:`AVCodecContext`, while + ``private`` contains options provided by the selected codec. Values are + descriptors only; set options through :attr:`options`. + """ + ctx: cython.pointer[lib.AVCodecContext] = lib.avcodec_alloc_context3( + self.codec.ptr + ) + child: cython.p_void + private: list = [] + if ctx == cython.NULL: + raise MemoryError("Cannot allocate codec context") + try: + generic = _get_supported_options(ctx) + child = lib.av_opt_child_next(ctx, cython.NULL) + while child != cython.NULL: + private.extend(_get_supported_options(child)) + child = lib.av_opt_child_next(ctx, child) + return CodecOptionSet(generic, tuple(private)) + finally: + lib.avcodec_free_context(cython.address(ctx)) + @cython.cfunc def _init( self, diff --git a/av/codec/context.pyi b/av/codec/context.pyi index e74502571..d500b1761 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -1,4 +1,5 @@ -from enum import Flag, IntEnum +from dataclasses import dataclass +from enum import Flag, IntEnum, IntFlag from fractions import Fraction from typing import ClassVar, Literal, cast, overload @@ -52,10 +53,70 @@ class Flags2(IntEnum): skip_manual = cast(int, ...) ro_flush_noop = cast(int, ...) +class OptionType(IntEnum): + FLAGS = cast(int, ...) + INT = cast(int, ...) + INT64 = cast(int, ...) + DOUBLE = cast(int, ...) + FLOAT = cast(int, ...) + STRING = cast(int, ...) + RATIONAL = cast(int, ...) + BINARY = cast(int, ...) + DICT = cast(int, ...) + UINT64 = cast(int, ...) + CONST = cast(int, ...) + IMAGE_SIZE = cast(int, ...) + PIXEL_FMT = cast(int, ...) + SAMPLE_FMT = cast(int, ...) + VIDEO_RATE = cast(int, ...) + DURATION = cast(int, ...) + COLOR = cast(int, ...) + CHANNEL_LAYOUT = cast(int, ...) + BOOL = cast(int, ...) + UINT = cast(int, ...) + +class OptionFlags(IntFlag): + ENCODING_PARAM = cast(int, ...) + DECODING_PARAM = cast(int, ...) + AUDIO_PARAM = cast(int, ...) + VIDEO_PARAM = cast(int, ...) + SUBTITLE_PARAM = cast(int, ...) + EXPORT = cast(int, ...) + READONLY = cast(int, ...) + BITSTREAM_FILTER_PARAM = cast(int, ...) + RUNTIME_PARAM = cast(int, ...) + FILTERING_PARAM = cast(int, ...) + DEPRECATED = cast(int, ...) + CHILD_CONSTS = cast(int, ...) + +@dataclass(frozen=True, slots=True) +class CodecOptionChoice: + name: str + help: str + +@dataclass(frozen=True, slots=True) +class CodecOption: + name: str + help: str + type: OptionType | int + is_array: bool + default: str | None + min: float + max: float + flags: OptionFlags + choices: tuple[CodecOptionChoice, ...] + +@dataclass(frozen=True, slots=True) +class CodecOptionSet: + generic: tuple[CodecOption, ...] + private: tuple[CodecOption, ...] + class CodecContext: name: str type: Literal["video", "audio", "data", "subtitle", "attachment"] options: dict[str, str] + @property + def supported_options(self) -> CodecOptionSet: ... profile: str | None level: int @property diff --git a/docs/api/codec.rst b/docs/api/codec.rst index a3f9b27a2..a8340bcb2 100644 --- a/docs/api/codec.rst +++ b/docs/api/codec.rst @@ -66,6 +66,13 @@ Contexts .. autoattribute:: CodecContext.codec .. autoattribute:: CodecContext.options +.. autoattribute:: CodecContext.supported_options + +.. autoclass:: CodecOptionSet +.. autoclass:: CodecOption +.. autoclass:: CodecOptionChoice +.. autoclass:: OptionType +.. autoclass:: OptionFlags .. automethod:: CodecContext.create .. automethod:: CodecContext.open @@ -161,4 +168,3 @@ frames passed to ``encode`` are uploaded to the device automatically:: See ``examples/basics/hw_decode.py`` for a complete example, including recommended device types per platform. - diff --git a/include/avutil.pxd b/include/avutil.pxd index 2cf76c230..8d671c98b 100644 --- a/include/avutil.pxd +++ b/include/avutil.pxd @@ -305,6 +305,8 @@ cdef extern from "libavutil/opt.h" nogil: AV_OPT_TYPE_COLOR AV_OPT_TYPE_CHLAYOUT AV_OPT_TYPE_BOOL + AV_OPT_TYPE_UINT + AV_OPT_TYPE_FLAG_ARRAY cdef union AVOption_default_val: int64_t i64 @@ -320,7 +322,11 @@ cdef extern from "libavutil/opt.h" nogil: AV_OPT_FLAG_SUBTITLE_PARAM AV_OPT_FLAG_EXPORT AV_OPT_FLAG_READONLY + AV_OPT_FLAG_BSF_PARAM + AV_OPT_FLAG_RUNTIME_PARAM AV_OPT_FLAG_FILTERING_PARAM + AV_OPT_FLAG_DEPRECATED + AV_OPT_FLAG_CHILD_CONSTS cdef struct AVOption: const char *name @@ -333,6 +339,12 @@ cdef extern from "libavutil/opt.h" nogil: int flags const char *unit + cdef const AVOption *av_opt_next(const void *obj, const AVOption *prev) + cdef void *av_opt_child_next(void *obj, void *prev) + cdef int av_opt_get( + void *obj, const char *name, int search_flags, uint8_t **out_val + ) + cdef extern from "libavutil/pixdesc.h" nogil: # See: http://ffmpeg.org/doxygen/trunk/structAVComponentDescriptor.html cdef struct AVComponentDescriptor: diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index db8c71bb3..272a1d45a 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -20,6 +20,7 @@ VideoFrame, ) from av.codec.codec import UnknownCodecError +from av.codec.context import OptionFlags, OptionType from av.video.frame import PictureType from .common import TestCase, fate_suite @@ -70,6 +71,36 @@ def iter_raw_frames( class TestCodecContext(TestCase): + def test_supported_options(self) -> None: + ctx = Codec("flac", "w").create() + supported = ctx.supported_options + generic = {option.name: option for option in supported.generic} + private = {option.name: option for option in supported.private} + + bit_rate = generic["b"] + assert bit_rate.type is OptionType.INT64 + assert bit_rate.default is not None + assert bit_rate.flags & OptionFlags.ENCODING_PARAM + assert bit_rate.flags & OptionFlags.AUDIO_PARAM + + strict_choices = {choice.name for choice in generic["strict"].choices} + assert "experimental" in strict_choices + + lpc_type = private["lpc_type"] + assert lpc_type.type is OptionType.INT + assert lpc_type.default == "-1" + assert {choice.name for choice in lpc_type.choices} >= { + "none", + "fixed", + "levinson", + } + + # Descriptors report FFmpeg's defaults, not values changed on this context. + ctx.bit_rate = 123456 + changed_supported = ctx.supported_options + changed_generic = {option.name: option for option in changed_supported.generic} + assert changed_generic["b"].default == bit_rate.default + def test_global_quality(self): ctx = Codec("mpeg4", "w").create() ctx.global_quality = 5 From 3ae2d705f698881d3a207c429deffbeebe8cea2f Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 22 Jul 2026 18:34:38 -0400 Subject: [PATCH 840/846] Document MP4 timestamp alignment (fixes #2324) --- examples/numpy/generate_video_with_pts.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/examples/numpy/generate_video_with_pts.py b/examples/numpy/generate_video_with_pts.py index d84b34817..8de95b63f 100644 --- a/examples/numpy/generate_video_with_pts.py +++ b/examples/numpy/generate_video_with_pts.py @@ -2,6 +2,7 @@ import colorsys from fractions import Fraction +from math import lcm import numpy as np @@ -9,9 +10,17 @@ (width, height) = (640, 360) total_frames = 20 -fps = 30 - -container = av.open("generate_video_with_pts.mp4", mode="w") +fps = Fraction(30, 1) + +# MP4 stores a nonzero starting offset in an edit list using the movie timescale, +# which defaults to 1000. Choose a timescale that can represent frame-aligned +# offsets exactly; otherwise, a starting PTS such as 1 at 30 fps is rounded. +movie_timescale = lcm(1000, fps.numerator) +container = av.open( + "generate_video_with_pts.mp4", + mode="w", + container_options={"movie_timescale": str(movie_timescale)}, +) stream = container.add_stream("mpeg4", rate=fps) # alibi frame rate stream.width = width From dabe2ab98e1a6b4cae301fab1bbbd692486e19b2 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 22 Jul 2026 18:59:02 -0400 Subject: [PATCH 841/846] Prevent codec reconfiguration after opening (fixes #2232) --- CHANGELOG.rst | 1 + av/audio/codeccontext.py | 3 +++ av/codec/context.pxd | 1 + av/codec/context.py | 5 +++++ av/container/output.py | 6 ++++++ av/video/codeccontext.py | 6 ++++++ tests/test_encode.py | 25 +++++++++++++++++++++++++ 7 files changed, 47 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2ad54e184..7255af52f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -40,6 +40,7 @@ Features: Fixes: +- Prevent crashes and corrupted output when structural codec properties are changed after an output stream has been opened by :gh-user:`WyattBlue`, reported by :gh-user:`oakaigh` (:issue:`2232`). - Fix a crash when using a stream that has no ``CodecContext`` (a demuxed stream with no available decoder, such as one from a truncated file, or a stream created by ``add_mux_stream``); decoding now raises ``DecoderNotFoundError``, encoding now raises ``EncoderNotFoundError``, and ``BitStreamFilterContext`` accepts such a stream as ``out_stream`` by :gh-user:`WyattBlue`, reported by :gh-user:`justinrmiller` (:issue:`2344`). v18.0.0 diff --git a/av/audio/codeccontext.py b/av/audio/codeccontext.py index 4e7118d34..86ac107cf 100644 --- a/av/audio/codeccontext.py +++ b/av/audio/codeccontext.py @@ -62,6 +62,7 @@ def sample_rate(self): @sample_rate.setter def sample_rate(self, value: cython.int): + self._assert_not_open("sample_rate") self.ptr.sample_rate = value @property @@ -88,6 +89,7 @@ def layout(self): @layout.setter def layout(self, value): + self._assert_not_open("layout") layout: AudioLayout = AudioLayout(value) self.ptr.ch_layout = layout.layout @@ -102,5 +104,6 @@ def format(self): @format.setter def format(self, value): + self._assert_not_open("format") format: AudioFormat = AudioFormat(value) self.ptr.sample_fmt = format.sample_fmt diff --git a/av/codec/context.pxd b/av/codec/context.pxd index 48d0bb9bd..a9cb702a7 100644 --- a/av/codec/context.pxd +++ b/av/codec/context.pxd @@ -17,6 +17,7 @@ cdef class CodecContext: cdef lib.AVCodecParserContext *parser cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec, HWAccel hwaccel) + cdef _assert_not_open(self, name) # Public API. cdef readonly bint is_open diff --git a/av/codec/context.py b/av/codec/context.py index 65f130dc5..7b3fb7cca 100644 --- a/av/codec/context.py +++ b/av/codec/context.py @@ -287,6 +287,11 @@ def _init( self.ptr.thread_count = 0 # use as many threads as there are CPUs. self.ptr.thread_type = 0x02 # thread within a frame. Does not change the API. + @cython.cfunc + def _assert_not_open(self, name): + if self.is_open: + raise RuntimeError(f"Cannot change {name} after codec is open.") + @property def flags(self): """ diff --git a/av/container/output.py b/av/container/output.py index ebf9269e8..a33dadd61 100644 --- a/av/container/output.py +++ b/av/container/output.py @@ -103,6 +103,12 @@ def add_stream( :param \\**kwargs: Set attributes for the stream. :rtype: The new :class:`~av.stream.Stream`. + .. warning:: + + Configure every output stream before muxing the first packet. Writing + the file header opens all stream codec contexts, after which structural + properties such as format, dimensions, layout, and rate cannot change. + """ codec_obj: Codec = Codec(codec_name, "w") diff --git a/av/video/codeccontext.py b/av/video/codeccontext.py index 2e39a7feb..27dbaacb9 100644 --- a/av/video/codeccontext.py +++ b/av/video/codeccontext.py @@ -193,6 +193,7 @@ def format(self): @format.setter def format(self, format: VideoFormat): + self._assert_not_open("format") self.ptr.pix_fmt = format.pix_fmt self.ptr.width = format.width self.ptr.height = format.height @@ -205,6 +206,7 @@ def width(self): @width.setter def width(self, value: cython.uint): + self._assert_not_open("width") self.ptr.width = value @property @@ -215,6 +217,7 @@ def height(self): @height.setter def height(self, value: cython.uint): + self._assert_not_open("height") self.ptr.height = value @property @@ -251,6 +254,7 @@ def pix_fmt(self): @pix_fmt.setter def pix_fmt(self, value): + self._assert_not_open("pix_fmt") self.ptr.pix_fmt = get_pix_fmt(value) @property @@ -277,6 +281,7 @@ def sw_format(self): @sw_format.setter def sw_format(self, value): + self._assert_not_open("sw_format") self.ptr.sw_pix_fmt = get_pix_fmt(value) @property @@ -290,6 +295,7 @@ def framerate(self): @framerate.setter def framerate(self, value): + self._assert_not_open("framerate") to_avrational(value, cython.address(self.ptr.framerate)) @property diff --git a/tests/test_encode.py b/tests/test_encode.py index e6015ae1b..71a337c19 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -4,6 +4,7 @@ import math import os from fractions import Fraction +from typing import cast import numpy as np import pytest @@ -268,6 +269,30 @@ def test_subtitle_muxing(self) -> None: class TestEncodeStreamSemantics(TestCase): + def test_reconfigure_stream_after_mux(self) -> None: + output_bytes = io.BytesIO() + with av.open(output_bytes, "w", format="mp4") as output: + first = cast(VideoStream, output.add_stream("ffv1", rate=30)) + second = cast(VideoStream, output.add_stream("ffv1", rate=30)) + + first.format = av.VideoFormat("bgr0", width=16, height=16) + frame = VideoFrame(16, 16, "bgr0") + frame.pts = 0 + frame.time_base = Fraction(1, 30) + output.mux(first.encode(frame)) + + # Muxing the first packet writes the header and opens every stream. + # Changing the second encoder now used to corrupt FFV1 state and crash. + assert second.codec_context.is_open + with pytest.raises(RuntimeError, match="Cannot change format"): + second.format = av.VideoFormat("bgr0", width=16, height=16) + with pytest.raises(RuntimeError, match="Cannot change width"): + second.width = 16 + with pytest.raises(RuntimeError, match="Cannot change height"): + second.height = 16 + with pytest.raises(RuntimeError, match="Cannot change pix_fmt"): + second.pix_fmt = "bgr0" + def test_stream_index(self) -> None: with av.open(self.sandboxed("output.mov"), "w") as output: vstream = output.add_stream("mpeg4", 24) From f43d6439d3d4eb6aadfd3887bb4cb6389483dd9d Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 22 Jul 2026 19:13:43 -0400 Subject: [PATCH 842/846] Type all FFmpeg encoder names --- CHANGELOG.rst | 1 + av/audio/__init__.pyi | 106 ++++++++++++++++++++++ av/codec/context.pyi | 9 ++ av/container/output.pyi | 9 ++ av/subtitles/__init__.pyi | 18 ++++ av/subtitles/stream.pyi | 3 + av/video/__init__.pyi | 180 ++++++++++++++++++++++++++++++++++++++ tests/test_dlpack.py | 4 +- tests/test_encode.py | 5 +- tests/test_subtitles.py | 9 +- 10 files changed, 333 insertions(+), 11 deletions(-) create mode 100644 av/subtitles/__init__.pyi diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7255af52f..45efcb643 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -33,6 +33,7 @@ v18.1.0 (Unreleased) Features: +- Infer the specific stream and codec context types for all FFmpeg encoder names in type-checked code by :gh-user:`WyattBlue`. - Add ``CodecContext.supported_options`` to discover generic and codec-specific options by :gh-user:`WyattBlue` (:issue:`2032`). - Add ``Packet.rescale_ts()`` to rescale packet PTS, DTS, and duration to a new ``AVRational`` time base by :gh-user:`WyattBlue`. - Support reusing the thread's current CUDA context via a ``current_ctx`` flag on ``CudaContext`` and ``VideoFrame.from_dlpack``, for interop with libraries like PyTorch that initialize CUDA first by :gh-user:`Yozer` (:pr:`2339`). diff --git a/av/audio/__init__.pyi b/av/audio/__init__.pyi index daefab6c9..b6a5c78e0 100644 --- a/av/audio/__init__.pyi +++ b/av/audio/__init__.pyi @@ -3,14 +3,120 @@ from typing import Literal from .frame import AudioFrame from .stream import AudioStream +# FFmpeg 8.1 encoders and the codec descriptor aliases that resolve to them. _AudioCodecName = Literal[ "aac", + "aac_at", + "aac_mf", + "ac3", + "ac3_fixed", + "ac3_mf", + "adpcm_adx", + "adpcm_argo", + "adpcm_g722", + "adpcm_g726", + "adpcm_g726le", + "adpcm_ima_alp", + "adpcm_ima_amv", + "adpcm_ima_apm", + "adpcm_ima_qt", + "adpcm_ima_ssi", + "adpcm_ima_wav", + "adpcm_ima_ws", + "adpcm_ms", + "adpcm_swf", + "adpcm_yamaha", + "alac", + "alac_at", + "amr_nb", + "amr_wb", + "anull", + "aptx", + "aptx_hd", + "codec2", + "comfortnoise", + "dca", + "dfpwm", + "dts", + "eac3", + "flac", + "g722", + "g723_1", + "g726", + "g726le", + "gsm", + "gsm_ms", + "ilbc", + "ilbc_at", + "lc3", + "libcodec2", + "libfdk_aac", + "libgsm", + "libgsm_ms", + "libilbc", + "liblc3", + "libmp3lame", + "libopencore_amrnb", "libopus", + "libshine", + "libspeex", + "libtwolame", + "libvo_amrwbenc", + "libvorbis", + "mlp", "mp2", + "mp2fixed", "mp3", + "mp3_mf", + "nellymoser", + "opus", "pcm_alaw", + "pcm_alaw_at", + "pcm_bluray", + "pcm_dvd", + "pcm_f32be", + "pcm_f32le", + "pcm_f64be", + "pcm_f64le", "pcm_mulaw", + "pcm_mulaw_at", + "pcm_s16be", + "pcm_s16be_planar", "pcm_s16le", + "pcm_s16le_planar", + "pcm_s24be", + "pcm_s24daud", + "pcm_s24le", + "pcm_s24le_planar", + "pcm_s32be", + "pcm_s32le", + "pcm_s32le_planar", + "pcm_s64be", + "pcm_s64le", + "pcm_s8", + "pcm_s8_planar", + "pcm_u16be", + "pcm_u16le", + "pcm_u24be", + "pcm_u24le", + "pcm_u32be", + "pcm_u32le", + "pcm_u8", + "pcm_vidc", + "ra_144", + "real_144", + "roq_dpcm", + "s302m", + "sbc", + "sonic", + "sonicls", + "speex", + "truehd", + "tta", + "vorbis", + "wavpack", + "wmav1", + "wmav2", ] __all__ = ("AudioFrame", "AudioStream") diff --git a/av/codec/context.pyi b/av/codec/context.pyi index d500b1761..3dfb52285 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -6,6 +6,8 @@ from typing import ClassVar, Literal, cast, overload from av.audio import _AudioCodecName from av.audio.codeccontext import AudioCodecContext from av.packet import Packet +from av.subtitles import _SubtitleCodecName +from av.subtitles.codeccontext import SubtitleCodecContext from av.video import _VideoCodecName from av.video.codeccontext import VideoCodecContext @@ -169,6 +171,13 @@ class CodecContext: ) -> VideoCodecContext: ... @overload @staticmethod + def create( + codec: _SubtitleCodecName, + mode: Literal["r", "w"] | None = None, + hwaccel: HWAccel | None = None, + ) -> SubtitleCodecContext: ... + @overload + @staticmethod def create( codec: str | Codec, mode: Literal["r", "w"] | None = None, diff --git a/av/container/output.pyi b/av/container/output.pyi index 5ed78a2b2..961e7ad2d 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -7,6 +7,7 @@ from av.audio.stream import AudioStream from av.codec.hwaccel import HWAccel from av.packet import Packet from av.stream import AttachmentStream, DataStream, Stream +from av.subtitles import _SubtitleCodecName from av.subtitles.stream import SubtitleStream from av.video import _VideoCodecName from av.video.stream import VideoStream @@ -34,6 +35,14 @@ class OutputContainer(Container): **kwargs, ) -> VideoStream: ... @overload + def add_stream( + self, + codec_name: _SubtitleCodecName, + rate: Fraction | int | None = None, + options: dict[str, str] | None = None, + **kwargs, + ) -> SubtitleStream: ... + @overload def add_stream( self, codec_name: str, diff --git a/av/subtitles/__init__.pyi b/av/subtitles/__init__.pyi new file mode 100644 index 000000000..c02a53551 --- /dev/null +++ b/av/subtitles/__init__.pyi @@ -0,0 +1,18 @@ +from typing import Literal + +# FFmpeg 8.1 encoders and the codec descriptor aliases that resolve to them. +_SubtitleCodecName = Literal[ + "ass", + "dvb_subtitle", + "dvbsub", + "dvd_subtitle", + "dvdsub", + "mov_text", + "srt", + "ssa", + "subrip", + "text", + "ttml", + "webvtt", + "xsub", +] diff --git a/av/subtitles/stream.pyi b/av/subtitles/stream.pyi index ac8083802..4c3326f79 100644 --- a/av/subtitles/stream.pyi +++ b/av/subtitles/stream.pyi @@ -2,7 +2,10 @@ from av.packet import Packet from av.stream import Stream from av.subtitles.subtitle import AssSubtitle, BitmapSubtitle, SubtitleSet +from .codeccontext import SubtitleCodecContext + class SubtitleStream(Stream): + codec_context: SubtitleCodecContext def decode( self, packet: Packet | None = None ) -> list[AssSubtitle] | list[BitmapSubtitle]: ... diff --git a/av/video/__init__.pyi b/av/video/__init__.pyi index 58a19a63f..63b5e0501 100644 --- a/av/video/__init__.pyi +++ b/av/video/__init__.pyi @@ -3,15 +3,195 @@ from typing import Literal from .frame import VideoFrame from .stream import VideoStream +# FFmpeg 8.1 encoders and the codec descriptor aliases that resolve to them. _VideoCodecName = Literal[ + "a64_multi", + "a64_multi5", + "a64multi", + "a64multi5", + "alias_pix", + "amv", + "apng", + "apv", + "asv1", + "asv2", + "av1", + "av1_amf", + "av1_d3d12va", + "av1_mediacodec", + "av1_mf", + "av1_nvenc", + "av1_qsv", + "av1_vaapi", + "av1_vulkan", + "avrp", + "avs2", + "avui", + "bitpacked", + "bmp", + "cavs", + "cfhd", + "cinepak", + "cljr", + "dirac", + "dnxhd", + "dpx", + "dvvideo", + "dxv", + "evc", + "exr", + "ffv1", + "ffv1_vulkan", + "ffvhuff", + "fits", + "flashsv", + "flashsv2", + "flv", + "flv1", "gif", + "h261", + "h263", + "h263_v4l2m2m", + "h263p", "h264", + "h264_amf", + "h264_d3d12va", + "h264_mediacodec", + "h264_mf", + "h264_nvenc", + "h264_oh", + "h264_omx", + "h264_qsv", + "h264_rkmpp", + "h264_v4l2m2m", + "h264_vaapi", + "h264_videotoolbox", + "h264_vulkan", + "hap", + "hdr", "hevc", + "hevc_amf", + "hevc_d3d12va", + "hevc_mediacodec", + "hevc_mf", + "hevc_nvenc", + "hevc_oh", + "hevc_qsv", + "hevc_rkmpp", + "hevc_v4l2m2m", + "hevc_vaapi", + "hevc_videotoolbox", + "hevc_vulkan", + "huffyuv", + "jpeg2000", + "jpegls", + "jpegxl", + "jpegxl_anim", + "jpegxs", + "libaom-av1", + "libjxl", + "libjxl_anim", + "libkvazaar", + "liboapv", + "libopenh264", + "libopenjpeg", + "librav1e", + "libsvtav1", + "libsvtjpegxs", + "libtheora", "libvpx", + "libvpx-vp9", + "libvvenc", + "libwebp", + "libwebp_anim", + "libx262", "libx264", + "libx264rgb", + "libx265", + "libxavs", + "libxavs2", + "libxeve", + "libxvid", + "ljpeg", + "magicyuv", + "mjpeg", + "mjpeg_qsv", + "mjpeg_vaapi", + "mpeg1video", + "mpeg2_qsv", + "mpeg2_vaapi", + "mpeg2video", "mpeg4", + "mpeg4_mediacodec", + "mpeg4_omx", + "mpeg4_v4l2m2m", + "msmpeg4", + "msmpeg4v2", + "msmpeg4v3", + "msrle", + "msvideo1", + "pam", + "pbm", + "pcx", + "pfm", + "pgm", + "pgmyuv", + "phm", "png", + "ppm", + "prores", + "prores_aw", + "prores_ks", + "prores_ks_vulkan", + "prores_videotoolbox", + "qoi", "qtrle", + "r10k", + "r210", + "rawvideo", + "roq", + "roqvideo", + "rpza", + "rv10", + "rv20", + "sgi", + "smc", + "snow", + "speedhq", + "sunrast", + "svq1", + "targa", + "theora", + "tiff", + "utvideo", + "v210", + "v308", + "v408", + "v410", + "vbn", + "vc2", + "vnull", + "vp8", + "vp8_mediacodec", + "vp8_v4l2m2m", + "vp8_vaapi", + "vp9", + "vp9_mediacodec", + "vp9_qsv", + "vp9_vaapi", + "vvc", + "wbmp", + "webp", + "wmv1", + "wmv2", + "wrapped_avframe", + "xbm", + "xface", + "xwd", + "y41p", + "yuv4", + "zlib", + "zmbv", ] __all__ = ("VideoFrame", "VideoStream") diff --git a/tests/test_dlpack.py b/tests/test_dlpack.py index 033316138..15a513e6d 100644 --- a/tests/test_dlpack.py +++ b/tests/test_dlpack.py @@ -1,6 +1,5 @@ import gc from fractions import Fraction -from typing import cast import numpy import pytest @@ -8,7 +7,6 @@ import av from av import VideoFrame from av.codec.hwaccel import HWAccel -from av.video.codeccontext import VideoCodecContext from .common import assertNdarraysEqual, fate_png @@ -669,7 +667,7 @@ def test_encode_cuda_frame_with_nvenc_if_available(use_current_ctx: bool) -> Non assert frame.format.name == "cuda" assert frame.sw_format is not None and frame.sw_format.name == "nv12" - cc = cast(VideoCodecContext, av.CodecContext.create("h264_nvenc", "w")) + cc = av.CodecContext.create("h264_nvenc", "w") cc.width = width cc.height = height cc.time_base = Fraction(1, 24) diff --git a/tests/test_encode.py b/tests/test_encode.py index 71a337c19..95942b4b7 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -4,7 +4,6 @@ import math import os from fractions import Fraction -from typing import cast import numpy as np import pytest @@ -272,8 +271,8 @@ class TestEncodeStreamSemantics(TestCase): def test_reconfigure_stream_after_mux(self) -> None: output_bytes = io.BytesIO() with av.open(output_bytes, "w", format="mp4") as output: - first = cast(VideoStream, output.add_stream("ffv1", rate=30)) - second = cast(VideoStream, output.add_stream("ffv1", rate=30)) + first = output.add_stream("ffv1", rate=30) + second = output.add_stream("ffv1", rate=30) first.format = av.VideoFormat("bgr0", width=16, height=16) frame = VideoFrame(16, 16, "bgr0") diff --git a/tests/test_subtitles.py b/tests/test_subtitles.py index c3d462b59..ff3f58027 100644 --- a/tests/test_subtitles.py +++ b/tests/test_subtitles.py @@ -3,7 +3,6 @@ import av from av.codec.context import CodecContext -from av.subtitles.codeccontext import SubtitleCodecContext from av.subtitles.subtitle import AssSubtitle, BitmapSubtitle, Subtitle, SubtitleSet from .common import TestCase, fate_suite @@ -83,13 +82,13 @@ def test_subtitle_header_read(self) -> None: with av.open(path) as container: stream = container.streams.subtitles[0] - ctx = cast(SubtitleCodecContext, stream.codec_context) + ctx = stream.codec_context header = ctx.subtitle_header assert header is None or isinstance(header, bytes) def test_subtitle_header_write(self) -> None: """Test setting subtitle_header on encoder context.""" - ctx = cast(SubtitleCodecContext, CodecContext.create("mov_text", "w")) + ctx = CodecContext.create("mov_text", "w") assert ctx.subtitle_header is None ass_header = b"[Script Info]\nScriptType: v4.00+\n" @@ -155,7 +154,7 @@ def test_subtitle_encode_mp4(self) -> None: video_stream.pix_fmt = "yuv420p" sub_stream = container.add_stream("mov_text") - sub_ctx = cast(SubtitleCodecContext, sub_stream.codec_context) + sub_ctx = sub_stream.codec_context sub_ctx.subtitle_header = ass_header container.start_encoding() @@ -193,7 +192,7 @@ def test_subtitle_encode_mkv_srt(self) -> None: output = io.BytesIO() with av.open(output, "w", format="matroska") as container: sub_stream = container.add_stream("srt") - sub_ctx = cast(SubtitleCodecContext, sub_stream.codec_context) + sub_ctx = sub_stream.codec_context sub_ctx.subtitle_header = minimal_header container.start_encoding() From fada77b1013a67636bf3725d96121b93b59a8440 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 22 Jul 2026 21:50:30 -0400 Subject: [PATCH 843/846] Update subinterpreter compatibility caveat --- docs/overview/caveats.rst | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/docs/overview/caveats.rst b/docs/overview/caveats.rst index 49c97367e..005401318 100644 --- a/docs/overview/caveats.rst +++ b/docs/overview/caveats.rst @@ -20,16 +20,7 @@ Our goal is to provide all of the features that make sense for the contexts that Sub-Interpreters ---------------- -Since we rely upon C callbacks in a few locations, PyAV is not fully compatible with sub-interpreters. Users have experienced lockups in WSGI web applications, for example. - -This is due to the ``PyGILState_Ensure`` calls made by Cython in a C callback from FFmpeg. If this is called in a thread that was not started by Python, it is very likely to break. There is no current instrumentation to detect such events. - -The two main features that can cause lockups are: - -1. Python IO (passing a file-like object to ``av.open``). While this is in theory possible, so far it seems like the callbacks are made in the calling thread, and so are safe. - -2. Logging. If you have logging enabled (disabled by default), those log messages could cause lockups when using threads. - +PyAV can enable Cython's per-interpreter module state as experimental groundwork, but it still cannot be imported by CPython sub-interpreters with their own GIL. Cython extension types continue to share internal vtable state between interpreters; repeated or concurrent interpreter creation can crash the process (`Cython issue #6445 `__). PyAV will not declare compatibility with own-GIL sub-interpreters until Cython supports extension types safely across interpreters. .. _garbage_collection: From 32e9f7de15c3ecdddd78cc17b5ed4cdafdd15149 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 22 Jul 2026 22:05:21 -0400 Subject: [PATCH 844/846] Refresh installation documentation --- README.md | 32 ++++++++++++++++++--------- docs/cookbook/numpy.rst | 3 +-- docs/development/license.rst | 2 +- docs/index.rst | 2 +- docs/overview/installation.rst | 40 ++++++++++++++++------------------ 5 files changed, 44 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 9ca80440d..8e66d98be 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ PyAV ==== -PyAV is a Pythonic binding for the [FFmpeg][ffmpeg] libraries. We aim to provide all of the power and control of the underlying library, but manage the gritty details as much as possible. +PyAV is a Pythonic binding for the [FFmpeg][ffmpeg] libraries. We aim to provide all the power and control of the underlying library, but manage the gritty details as much as possible. --- [![GitHub Test Status][github-tests-badge]][github-tests] [![Documentation][docs-badge]][docs] [![Python Package Index][pypi-badge]][pypi] [![Conda Forge][conda-badge]][conda] -PyAV is for direct and precise access to your media via containers, streams, packets, codecs, and frames. It exposes a few transformations of that data, and helps you get your data to/from other packages (e.g. Numpy and Pillow). +PyAV is for direct and precise access to your media via containers, streams, packets, codecs, and frames. It exposes a few transformations of that data, and helps you get your data to/from other packages (e.g. NumPy and Pillow). This power does come with some responsibility as working with media is horrendously complicated and PyAV can't abstract it away or make all the best decisions for you. If the `ffmpeg` command does the job without you bending over backwards, PyAV is likely going to be more of a hindrance than a help. @@ -17,7 +17,7 @@ But where you can't work without it, PyAV is a critical tool. Installation ------------ -Binary wheels are provided on [PyPI][pypi] for Linux, MacOS and Windows linked against the latest stable version of ffmpeg. You can install these wheels by running: +PyAV requires Python 3.11 or later. Binary wheels are provided on [PyPI][pypi] for Linux, macOS, and Windows with FFmpeg bundled. You can install these wheels by running: ```bash pip install av @@ -29,34 +29,33 @@ Another way of installing PyAV is via [conda-forge][conda-forge]: conda install av -c conda-forge ``` -See the [Conda install][conda-install] docs to get started with (mini)Conda. +See the [Conda install][conda-install] docs to get started with Miniconda. Alternative installation methods -------------------------------- -Due to the complexity of the dependencies, PyAV is not always the easiest Python package to install from source. If you want to use your existing ffmpeg (must be the correct major version), the source version of PyAV is on [PyPI][pypi]: +Due to the complexity of the dependencies, PyAV is not always the easiest Python package to install from source. This release supports FFmpeg 8.x. To build the source distribution against an existing FFmpeg installation on Linux or macOS, run: > [!WARNING] -> You must be in a posix env, and have the correct version of ffmpeg installed on your system. +> FFmpeg's development files and `pkg-config` must be available on your system. ```bash pip install av --no-binary av ``` -Installing From Source +Installing from source ---------------------- -Here's how to build PyAV from source. You must use [MSYS2](https://www.msys2.org/) when using Windows. +On Linux or macOS, build PyAV from a Git checkout with: ```bash git clone https://github.com/PyAV-Org/PyAV.git cd PyAV source scripts/activate.sh -# Build ffmpeg from source. You can skip this step -# if ffmpeg is already installed. +# Build ffmpeg from source. You can skip this step if ffmpeg 8.x is already installed. ./scripts/build-deps # Build PyAV @@ -70,6 +69,19 @@ deactivate pip install . ``` +On Windows, use a Conda environment and the FFmpeg development files maintained by the PyAV project: + +```powershell +git clone https://github.com/PyAV-Org/PyAV.git +cd PyAV +conda create --name pyav-dev --channel conda-forge python=3.11 cython setuptools numpy pillow pytest +conda activate pyav-dev +$ffmpegDir = Join-Path $env:CONDA_PREFIX "Library" +python scripts\fetch-vendor.py --config-file scripts\ffmpeg-latest.json $ffmpegDir +python setup.py build_ext --inplace --ffmpeg-dir=$ffmpegDir +python -m pytest +``` + --- Have fun, [read the docs][docs], [come chat with us][discuss], and good luck! diff --git a/docs/cookbook/numpy.rst b/docs/cookbook/numpy.rst index 6842efefc..8b97e4ecb 100644 --- a/docs/cookbook/numpy.rst +++ b/docs/cookbook/numpy.rst @@ -1,4 +1,4 @@ -Numpy +NumPy ===== @@ -21,4 +21,3 @@ Generating Video .. literalinclude:: ../../examples/numpy/generate_video.py - diff --git a/docs/development/license.rst b/docs/development/license.rst index 57cd288f4..e49e1d435 100644 --- a/docs/development/license.rst +++ b/docs/development/license.rst @@ -6,7 +6,7 @@ License ======= -From `LICENSE.txt `_: +From `LICENSE.txt `_: .. literalinclude:: ../../LICENSE.txt :language: text diff --git a/docs/index.rst b/docs/index.rst index afeaa60d6..5197232b3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,7 +3,7 @@ **PyAV** is a Pythonic binding for FFmpeg_. We aim to provide all of the power and control of the underlying library, but manage the gritty details as much as possible. -PyAV is for direct and precise access to your media via containers, streams, packets, codecs, and frames. It exposes a few transformations of that data, and helps you get your data to/from other packages (e.g. Numpy and Pillow). +PyAV is for direct and precise access to your media via containers, streams, packets, codecs, and frames. It exposes a few transformations of that data, and helps you get your data to/from other packages (e.g. NumPy and Pillow). This power does come with some responsibility as working with media is horrendously complicated and PyAV can't abstract it away or make all the best decisions for you. If the ``ffmpeg`` command does the job without you bending over backwards, PyAV is likely going to be more of a hindrance than a help. diff --git a/docs/overview/installation.rst b/docs/overview/installation.rst index b4399ba48..30d5b0370 100644 --- a/docs/overview/installation.rst +++ b/docs/overview/installation.rst @@ -4,7 +4,7 @@ Installation Binary wheels ------------- -Binary wheels are provided on PyPI for Linux, MacOS, and Windows linked against FFmpeg. The most straight-forward way to install PyAV is to run: +PyAV requires Python 3.11 or later. Binary wheels are provided on PyPI for Linux, macOS, and Windows with FFmpeg bundled. The most straightforward way to install PyAV is to run: .. code-block:: bash @@ -18,13 +18,13 @@ Another way to install PyAV is via `conda-forge ` conda install av -c conda-forge -See the `Conda quick install `_ docs to get started with (mini)Conda. +See the `Conda quick install `_ docs to get started with Miniconda. Bring your own FFmpeg --------------------- -PyAV can also be compiled against your own build of FFmpeg (version ``8.0`` or higher). You can force installing PyAV from source by running: +PyAV can also be compiled against your own build of FFmpeg 8.x. You can force installing PyAV from source by running: .. code-block:: bash @@ -46,26 +46,35 @@ and a few other tools in general: - Python's development headers -MacOS +macOS ^^^^^ -On **MacOS**, Homebrew_ saves the day:: +On **macOS**, install the build dependencies with Homebrew_:: brew install ffmpeg pkg-config -.. _homebrew: http://brew.sh/ +.. _homebrew: https://brew.sh/ Windows ^^^^^^^ -It is possible to build PyAV on Windows without Conda by installing FFmpeg yourself, e.g. from the `shared and dev packages `_. +The Windows build uses FFmpeg development files maintained by the PyAV project. From a PowerShell prompt, create a development environment, fetch the libraries into it, and pass their location to the build: -Unpack them somewhere (like ``C:\ffmpeg``), and then :ref:`tell PyAV where they are located `. +.. code-block:: powershell + conda create --name pyav-dev --channel conda-forge python=3.11 cython setuptools numpy pillow pytest + conda activate pyav-dev + $ffmpegDir = Join-Path $env:CONDA_PREFIX "Library" + python scripts\fetch-vendor.py --config-file scripts\ffmpeg-latest.json $ffmpegDir + python setup.py build_ext --inplace --ffmpeg-dir=$ffmpegDir + python -m pytest -Building from the latest source -------------------------------- +This is the same approach used by PyAV's Windows continuous-integration build. + + +Building from the latest source on Linux or macOS +------------------------------------------------- :: @@ -81,14 +90,3 @@ Building from the latest source # Build PyAV. make - -On **MacOS** you may have issues with regards to Python expecting gcc but finding clang. Try to export the following before installation:: - - export ARCHFLAGS=-Wno-error=unused-command-line-argument-hard-error-in-future - - -.. _build_on_windows: - -On **Windows** you must indicate the location of your FFmpeg, e.g.:: - - python setup.py build --ffmpeg-dir=C:\ffmpeg From 387fca5ec3eadbdd9ba286cac2918b74a5385b42 Mon Sep 17 00:00:00 2001 From: Dominik Baran Date: Wed, 29 Jul 2026 19:54:40 +0200 Subject: [PATCH 845/846] Support explicit CUDA streams --- CHANGELOG.rst | 1 + av/video/frame.pxd | 3 +- av/video/frame.py | 123 ++++++++++++++++++++++++++++++++++++- av/video/frame.pyi | 3 + include/avutil.pxd | 6 +- include/hwcontext_cuda.pxd | 19 ++++++ tests/test_dlpack.py | 72 +++++++++++++++++++++- 7 files changed, 221 insertions(+), 6 deletions(-) create mode 100644 include/hwcontext_cuda.pxd diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 45efcb643..ce37ff4f9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -38,6 +38,7 @@ Features: - Add ``Packet.rescale_ts()`` to rescale packet PTS, DTS, and duration to a new ``AVRational`` time base by :gh-user:`WyattBlue`. - Support reusing the thread's current CUDA context via a ``current_ctx`` flag on ``CudaContext`` and ``VideoFrame.from_dlpack``, for interop with libraries like PyTorch that initialize CUDA first by :gh-user:`Yozer` (:pr:`2339`). - ``VideoFrame.from_dlpack`` no longer requires restating ``primary_ctx``/``current_ctx`` when passing an explicit ``cuda_context``; the flags are only validated when explicitly given by :gh-user:`WyattBlue`. +- Support passing an explicit CUDA stream to FFmpeg CUDA operations, including NVENC input and output, via a ``cuda_stream`` parameter on ``CudaContext``; currently limited to logical CUDA device 0 by :gh-user:`Yozer` (:pr:`2360`). Fixes: diff --git a/av/video/frame.pxd b/av/video/frame.pxd index 0a96311e0..66926058d 100644 --- a/av/video/frame.pxd +++ b/av/video/frame.pxd @@ -1,5 +1,5 @@ cimport libav as lib -from libc.stdint cimport uint8_t +from libc.stdint cimport uint8_t, uintptr_t from av.frame cimport Frame from av.video.format cimport VideoFormat @@ -10,6 +10,7 @@ cdef class CudaContext: cdef readonly int device_id cdef readonly bint primary_ctx cdef readonly bint current_ctx + cdef readonly uintptr_t cuda_stream cdef lib.AVBufferRef* _device_ref cdef dict _frames_cache cdef lib.AVBufferRef* _get_device_ref(self) diff --git a/av/video/frame.py b/av/video/frame.py index 4a85d5b11..c6c2f26a1 100644 --- a/av/video/frame.py +++ b/av/video/frame.py @@ -16,7 +16,62 @@ PyCapsule_SetName, ) from cython.cimports.cpython.ref import Py_DECREF, Py_INCREF -from cython.cimports.libc.stdint import int64_t, uint8_t +from cython.cimports.hwcontext_cuda import AVCUDADeviceContext, CUstream +from cython.cimports.libc.stdint import int64_t, uint8_t, uintptr_t + + +@cython.cfunc +@cython.nogil +@cython.exceptval(check=False) +def _cuda_device_ctx_free( + device_ctx: cython.pointer[lib.AVHWDeviceContext], +) -> cython.void: + owner_ref: cython.pointer[lib.AVBufferRef] = cython.cast( + cython.pointer[lib.AVBufferRef], device_ctx.user_opaque + ) + lib.av_buffer_unref(cython.address(owner_ref)) + device_ctx.user_opaque = cython.NULL + + +@cython.cfunc +def _check_current_ctx_is_device0_primary( + owner_ref: cython.pointer[lib.AVBufferRef], +) -> cython.void: + # The manually initialized device context below cannot populate FFmpeg's + # private cuda_device field, which stays 0. Requiring the wrapped context + # to be the primary context of device 0 keeps that metadata correct. + probe_ref: cython.pointer[lib.AVBufferRef] = cython.NULL + probe_device: cython.p_char = b"0" + options: Dictionary = Dictionary({"primary_ctx": "1"}) + err_check( + lib.av_hwdevice_ctx_create( + cython.address(probe_ref), + lib.AV_HWDEVICE_TYPE_CUDA, + probe_device, + options.ptr, + 0, + ) + ) + owner_device_ctx: cython.pointer[lib.AVHWDeviceContext] = cython.cast( + cython.pointer[lib.AVHWDeviceContext], owner_ref.data + ) + owner_cuda_ctx: cython.pointer[AVCUDADeviceContext] = cython.cast( + cython.pointer[AVCUDADeviceContext], owner_device_ctx.hwctx + ) + probe_device_ctx: cython.pointer[lib.AVHWDeviceContext] = cython.cast( + cython.pointer[lib.AVHWDeviceContext], probe_ref.data + ) + probe_cuda_ctx: cython.pointer[AVCUDADeviceContext] = cython.cast( + cython.pointer[AVCUDADeviceContext], probe_device_ctx.hwctx + ) + matches: cython.bint = probe_cuda_ctx.cuda_ctx == owner_cuda_ctx.cuda_ctx + lib.av_buffer_unref(cython.address(probe_ref)) + if not matches: + raise ValueError( + "cuda_stream with current_ctx requires the calling thread's current " + "CUDA context to be the primary context of CUDA device 0; expose the " + "desired physical GPU as logical device 0 via CUDA_VISIBLE_DEVICES" + ) @cython.final @@ -29,6 +84,14 @@ class CudaContext: :param bool current_ctx: Wrap the CUDA context current on the calling thread when this object is first used. This is mutually exclusive with ``primary_ctx``. + :param int cuda_stream: Optional raw ``CUstream`` pointer for FFmpeg CUDA + operations, including NVENC input and output. The caller owns the + stream and must keep it alive as long as this context can be used. + Nonzero stream pointers are currently supported only for logical CUDA + device 0; with ``current_ctx``, the calling thread's current CUDA + context must be the primary context of device 0 (the context used by + runtime-API libraries such as PyTorch or CuPy). To use another physical + GPU, expose it as logical device 0 via ``CUDA_VISIBLE_DEVICES``. """ def __cinit__( @@ -36,14 +99,27 @@ def __cinit__( device_id: cython.int = 0, primary_ctx: cython.bint = True, current_ctx: cython.bint = False, + cuda_stream: object = None, ): self.device_id = device_id self.primary_ctx = primary_ctx self.current_ctx = current_ctx + self.cuda_stream = 0 self._device_ref = cython.NULL self._frames_cache = {} if primary_ctx and current_ctx: raise ValueError("primary_ctx and current_ctx are mutually exclusive") + if cuda_stream is not None: + if not isinstance(cuda_stream, int): + raise TypeError("cuda_stream must be an integer or None") + if cuda_stream < 0: + raise ValueError("cuda_stream must be non-negative") + self.cuda_stream = cython.cast(uintptr_t, cuda_stream) + if self.cuda_stream and self.device_id != 0: + raise ValueError( + "cuda_stream currently requires device_id 0; expose the desired " + "physical GPU as logical device 0 via CUDA_VISIBLE_DEVICES" + ) def __dealloc__(self): ref: cython.pointer[lib.AVBufferRef] @@ -64,10 +140,11 @@ def __dealloc__(self): @cython.cfunc def _get_device_ref(self) -> cython.pointer[lib.AVBufferRef]: device_ref: cython.pointer[lib.AVBufferRef] = self._device_ref + owner_ref: cython.pointer[lib.AVBufferRef] if device_ref != cython.NULL: return device_ref - device_ref = cython.NULL + owner_ref = cython.NULL device_bytes = f"{self.device_id}".encode() c_device: cython.p_char = device_bytes options_dict = {"primary_ctx": "1" if self.primary_ctx else "0"} @@ -76,13 +153,53 @@ def _get_device_ref(self) -> cython.pointer[lib.AVBufferRef]: options: Dictionary = Dictionary(options_dict) err_check( lib.av_hwdevice_ctx_create( - cython.address(device_ref), + cython.address(owner_ref), lib.AV_HWDEVICE_TYPE_CUDA, c_device, options.ptr, 0, ) ) + if not self.cuda_stream: + self._device_ref = owner_ref + return owner_ref + + if self.current_ctx: + try: + _check_current_ctx_is_device0_primary(owner_ref) + except Exception: + lib.av_buffer_unref(cython.address(owner_ref)) + raise + + device_ref = lib.av_hwdevice_ctx_alloc(lib.AV_HWDEVICE_TYPE_CUDA) + if device_ref == cython.NULL: + lib.av_buffer_unref(cython.address(owner_ref)) + raise MemoryError("av_hwdevice_ctx_alloc() failed") + + try: + owner_device_ctx: cython.pointer[lib.AVHWDeviceContext] = cython.cast( + cython.pointer[lib.AVHWDeviceContext], owner_ref.data + ) + owner_cuda_ctx: cython.pointer[AVCUDADeviceContext] = cython.cast( + cython.pointer[AVCUDADeviceContext], owner_device_ctx.hwctx + ) + device_ctx: cython.pointer[lib.AVHWDeviceContext] = cython.cast( + cython.pointer[lib.AVHWDeviceContext], device_ref.data + ) + cuda_ctx: cython.pointer[AVCUDADeviceContext] = cython.cast( + cython.pointer[AVCUDADeviceContext], device_ctx.hwctx + ) + cuda_ctx.cuda_ctx = owner_cuda_ctx.cuda_ctx + cuda_ctx.stream = cython.cast(CUstream, self.cuda_stream) + device_ctx.free = _cuda_device_ctx_free + device_ctx.user_opaque = cython.cast(cython.p_void, owner_ref) + owner_ref = cython.NULL + err_check(lib.av_hwdevice_ctx_init(device_ref)) + except Exception: + lib.av_buffer_unref(cython.address(device_ref)) + lib.av_buffer_unref(cython.address(owner_ref)) + raise + self._device_ref = device_ref return device_ref diff --git a/av/video/frame.pyi b/av/video/frame.pyi index dd6f481d1..f07a8079f 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -36,11 +36,14 @@ class CudaContext: def primary_ctx(self) -> bool: ... @property def current_ctx(self) -> bool: ... + @property + def cuda_stream(self) -> int: ... def __init__( self, device_id: int = 0, primary_ctx: bool = True, current_ctx: bool = False, + cuda_stream: int | None = None, ) -> None: ... class VideoFrame(Frame): diff --git a/include/avutil.pxd b/include/avutil.pxd index 8d671c98b..8818fdd14 100644 --- a/include/avutil.pxd +++ b/include/avutil.pxd @@ -192,7 +192,9 @@ cdef extern from "libavutil/frame.h" nogil: cdef extern from "libavutil/hwcontext.h" nogil: cdef struct AVHWDeviceContext: - pass + void *hwctx + void (*free)(AVHWDeviceContext *ctx) + void *user_opaque enum AVHWDeviceType: AV_HWDEVICE_TYPE_NONE @@ -220,6 +222,8 @@ cdef extern from "libavutil/hwcontext.h" nogil: int width int height + cdef AVBufferRef *av_hwdevice_ctx_alloc(AVHWDeviceType type) + cdef int av_hwdevice_ctx_init(AVBufferRef *ref) cdef int av_hwdevice_ctx_create(AVBufferRef **device_ctx, AVHWDeviceType type, const char *device, AVDictionary *opts, int flags) cdef AVHWDeviceType av_hwdevice_find_type_by_name(const char *name) cdef const char *av_hwdevice_get_type_name(AVHWDeviceType type) diff --git a/include/hwcontext_cuda.pxd b/include/hwcontext_cuda.pxd new file mode 100644 index 000000000..b97962967 --- /dev/null +++ b/include/hwcontext_cuda.pxd @@ -0,0 +1,19 @@ +cdef extern from *: + """ + #ifndef CUDA_VERSION + #define PYAV_CUDA_TYPES + #define CUDA_VERSION 1 + typedef struct CUctx_st *CUcontext; + typedef struct CUstream_st *CUstream; + #endif + #include + #ifdef PYAV_CUDA_TYPES + #undef CUDA_VERSION + #undef PYAV_CUDA_TYPES + #endif + """ + ctypedef void *CUcontext + ctypedef void *CUstream + ctypedef struct AVCUDADeviceContext: + CUcontext cuda_ctx + CUstream stream diff --git a/tests/test_dlpack.py b/tests/test_dlpack.py index 15a513e6d..b02675525 100644 --- a/tests/test_dlpack.py +++ b/tests/test_dlpack.py @@ -533,13 +533,33 @@ def test_cuda_context_flags() -> None: default_ctx = av.video.frame.CudaContext() assert default_ctx.primary_ctx is True assert default_ctx.current_ctx is False + assert default_ctx.cuda_stream == 0 with pytest.raises(ValueError, match="mutually exclusive"): av.video.frame.CudaContext(primary_ctx=True, current_ctx=True) - ctx = av.video.frame.CudaContext(primary_ctx=False, current_ctx=True) + ctx = av.video.frame.CudaContext( + primary_ctx=False, current_ctx=True, cuda_stream=1234 + ) assert ctx.primary_ctx is False assert ctx.current_ctx is True + assert ctx.cuda_stream == 1234 + + with pytest.raises(TypeError, match="integer or None"): + av.video.frame.CudaContext(cuda_stream="1234") # type: ignore[arg-type] + + with pytest.raises(ValueError, match="non-negative"): + av.video.frame.CudaContext(cuda_stream=-1) + + with pytest.raises(ValueError, match="requires device_id 0.*CUDA_VISIBLE_DEVICES"): + av.video.frame.CudaContext(device_id=1, cuda_stream=1234) + + for default_stream in (None, 0): + nonzero_device_ctx = av.video.frame.CudaContext( + device_id=1, cuda_stream=default_stream + ) + assert nonzero_device_ctx.device_id == 1 + assert nonzero_device_ctx.cuda_stream == 0 def test_video_frame_from_dlpack_cuda_hw_frame_behavior_if_available() -> None: @@ -679,3 +699,53 @@ def test_encode_cuda_frame_with_nvenc_if_available(use_current_ctx: bool) -> Non assert any(p.size for p in packets) except av.FFmpegError as e: pytest.skip(f"nvenc/CUDA not available in this build/runtime: {e}") + + +def test_encode_cuda_frame_with_nvenc_external_stream_if_available() -> None: + try: + import torch # type: ignore + except Exception: + pytest.skip("PyTorch is not available.") + if not torch.cuda.is_available(): + pytest.skip("CUDA is not available.") + + width, height = 256, 256 + with torch.cuda.device(0): + producer_stream = torch.cuda.Stream() + encoder_stream = torch.cuda.Stream() + encoder_stream_ptr = int(encoder_stream.cuda_stream) + + try: + with torch.cuda.device(0), torch.cuda.stream(producer_stream): + y = torch.zeros((height, width), dtype=torch.uint8, device="cuda:0") + uv = torch.zeros( + (height // 2, width // 2, 2), + dtype=torch.uint8, + device="cuda:0", + ) + cuda_context = av.video.frame.CudaContext( + device_id=int(y.__dlpack_device__()[1]), + primary_ctx=False, + current_ctx=True, + cuda_stream=encoder_stream_ptr, + ) + frame = VideoFrame.from_dlpack( + (y, uv), + format="nv12", + stream=encoder_stream_ptr, + cuda_context=cuda_context, + ) + + assert cuda_context.cuda_stream == encoder_stream_ptr + cc = av.CodecContext.create("h264_nvenc", "w") + cc.width = width + cc.height = height + cc.time_base = Fraction(1, 24) + cc.framerate = Fraction(24, 1) + cc.pix_fmt = "cuda" + + packets = cc.encode(frame) + packets += cc.encode(None) + assert any(p.size for p in packets) + except av.FFmpegError as e: + pytest.skip(f"nvenc/CUDA not available in this build/runtime: {e}") From f6f0a5e3d975aab851e12ca881bdadfdd8f9ec74 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 29 Jul 2026 13:59:38 -0400 Subject: [PATCH 846/846] Avoid probing primary context flags for no reason --- av/video/frame.py | 54 ++--------------------------------------------- 1 file changed, 2 insertions(+), 52 deletions(-) diff --git a/av/video/frame.py b/av/video/frame.py index c6c2f26a1..74964107b 100644 --- a/av/video/frame.py +++ b/av/video/frame.py @@ -33,47 +33,6 @@ def _cuda_device_ctx_free( device_ctx.user_opaque = cython.NULL -@cython.cfunc -def _check_current_ctx_is_device0_primary( - owner_ref: cython.pointer[lib.AVBufferRef], -) -> cython.void: - # The manually initialized device context below cannot populate FFmpeg's - # private cuda_device field, which stays 0. Requiring the wrapped context - # to be the primary context of device 0 keeps that metadata correct. - probe_ref: cython.pointer[lib.AVBufferRef] = cython.NULL - probe_device: cython.p_char = b"0" - options: Dictionary = Dictionary({"primary_ctx": "1"}) - err_check( - lib.av_hwdevice_ctx_create( - cython.address(probe_ref), - lib.AV_HWDEVICE_TYPE_CUDA, - probe_device, - options.ptr, - 0, - ) - ) - owner_device_ctx: cython.pointer[lib.AVHWDeviceContext] = cython.cast( - cython.pointer[lib.AVHWDeviceContext], owner_ref.data - ) - owner_cuda_ctx: cython.pointer[AVCUDADeviceContext] = cython.cast( - cython.pointer[AVCUDADeviceContext], owner_device_ctx.hwctx - ) - probe_device_ctx: cython.pointer[lib.AVHWDeviceContext] = cython.cast( - cython.pointer[lib.AVHWDeviceContext], probe_ref.data - ) - probe_cuda_ctx: cython.pointer[AVCUDADeviceContext] = cython.cast( - cython.pointer[AVCUDADeviceContext], probe_device_ctx.hwctx - ) - matches: cython.bint = probe_cuda_ctx.cuda_ctx == owner_cuda_ctx.cuda_ctx - lib.av_buffer_unref(cython.address(probe_ref)) - if not matches: - raise ValueError( - "cuda_stream with current_ctx requires the calling thread's current " - "CUDA context to be the primary context of CUDA device 0; expose the " - "desired physical GPU as logical device 0 via CUDA_VISIBLE_DEVICES" - ) - - @cython.final @cython.cclass class CudaContext: @@ -88,10 +47,8 @@ class CudaContext: operations, including NVENC input and output. The caller owns the stream and must keep it alive as long as this context can be used. Nonzero stream pointers are currently supported only for logical CUDA - device 0; with ``current_ctx``, the calling thread's current CUDA - context must be the primary context of device 0 (the context used by - runtime-API libraries such as PyTorch or CuPy). To use another physical - GPU, expose it as logical device 0 via ``CUDA_VISIBLE_DEVICES``. + device 0. To use another physical GPU, expose it as logical device 0 + via ``CUDA_VISIBLE_DEVICES``. """ def __cinit__( @@ -164,13 +121,6 @@ def _get_device_ref(self) -> cython.pointer[lib.AVBufferRef]: self._device_ref = owner_ref return owner_ref - if self.current_ctx: - try: - _check_current_ctx_is_device0_primary(owner_ref) - except Exception: - lib.av_buffer_unref(cython.address(owner_ref)) - raise - device_ref = lib.av_hwdevice_ctx_alloc(lib.AV_HWDEVICE_TYPE_CUDA) if device_ref == cython.NULL: lib.av_buffer_unref(cython.address(owner_ref))