From d1ee9312869e4517fa20e5970f2c24b1787751b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Thu, 15 Oct 2020 09:58:00 +0200 Subject: [PATCH 001/192] [build] upgrade cibuildwheel to 1.6.3 --- .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 7427250ba..931bae663 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -221,7 +221,7 @@ jobs: 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: CL="/IC:\cibw\vendor\include" LINK="/LIBPATH:C:\cibw\vendor\lib" + CIBW_ENVIRONMENT_WINDOWS: INCLUDE=C:\\cibw\\vendor\\include LIB=C:\\cibw\\vendor\\lib CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: python scripts/inject-dll {wheel} {dest_dir} C:\cibw\vendor\bin CIBW_SKIP: cp27-* pp27-* pp36-win* CIBW_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m unittest discover -t {project} -s tests && mv {project}/av.disabled {project}/av @@ -229,7 +229,7 @@ jobs: CIBW_TEST_COMMAND_MACOS: true CIBW_TEST_REQUIRES: numpy run: | - pip install cibuildwheel==1.4.2 + pip install cibuildwheel cibuildwheel --output-dir dist shell: bash - name: Upload wheels From 0839954c8358746d829be5d6797a7dcd647ca9e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Thu, 15 Oct 2020 10:04:41 +0200 Subject: [PATCH 002/192] Adjust import formatting for isort 5.6.x --- av/audio/fifo.pxd | 1 - av/audio/frame.pxd | 3 +-- av/audio/resampler.pxd | 1 - av/codec/context.pxd | 3 +-- av/container/pyio.pxd | 2 +- av/filter/context.pxd | 1 + av/frame.pxd | 1 + av/packet.pxd | 2 +- av/sidedata/motionvectors.pxd | 4 ++-- av/sidedata/sidedata.pxd | 4 ++-- av/stream.pxd | 1 - av/subtitles/stream.pxd | 1 + av/utils.pxd | 1 - av/video/frame.pxd | 3 +-- 14 files changed, 12 insertions(+), 16 deletions(-) diff --git a/av/audio/fifo.pxd b/av/audio/fifo.pxd index 8b52cc12e..cf3a9dbec 100644 --- a/av/audio/fifo.pxd +++ b/av/audio/fifo.pxd @@ -1,5 +1,4 @@ from libc.stdint cimport int64_t, uint64_t - cimport libav as lib from av.audio.frame cimport AudioFrame diff --git a/av/audio/frame.pxd b/av/audio/frame.pxd index 3192bed1a..a438fe627 100644 --- a/av/audio/frame.pxd +++ b/av/audio/frame.pxd @@ -1,10 +1,9 @@ from libc.stdint cimport uint8_t, uint64_t - cimport libav as lib -from av.frame cimport Frame from av.audio.format cimport AudioFormat from av.audio.layout cimport AudioLayout +from av.frame cimport Frame cdef class AudioFrame(Frame): diff --git a/av/audio/resampler.pxd b/av/audio/resampler.pxd index e1cd8170e..4a2d9ceaf 100644 --- a/av/audio/resampler.pxd +++ b/av/audio/resampler.pxd @@ -1,5 +1,4 @@ from libc.stdint cimport uint64_t - cimport libav as lib from av.audio.format cimport AudioFormat diff --git a/av/codec/context.pxd b/av/codec/context.pxd index 446cef8e9..0f2920191 100644 --- a/av/codec/context.pxd +++ b/av/codec/context.pxd @@ -1,11 +1,10 @@ from libc.stdint cimport int64_t - cimport libav as lib +from av.bytesource cimport ByteSource from av.codec.codec cimport Codec from av.frame cimport Frame from av.packet cimport Packet -from av.bytesource cimport ByteSource cdef class CodecContext(object): diff --git a/av/container/pyio.pxd b/av/container/pyio.pxd index 1b4ca4299..1292d2c71 100644 --- a/av/container/pyio.pxd +++ b/av/container/pyio.pxd @@ -1,4 +1,4 @@ -from libc.stdint cimport uint8_t, int64_t +from libc.stdint cimport int64_t, uint8_t cdef int pyio_read(void *opaque, uint8_t *buf, int buf_size) nogil diff --git a/av/filter/context.pxd b/av/filter/context.pxd index 5a5537fc2..3c69185b2 100644 --- a/av/filter/context.pxd +++ b/av/filter/context.pxd @@ -1,4 +1,5 @@ cimport libav as lib + from av.filter.filter cimport Filter from av.filter.graph cimport Graph diff --git a/av/frame.pxd b/av/frame.pxd index bbb13a5df..e0d5b4280 100644 --- a/av/frame.pxd +++ b/av/frame.pxd @@ -3,6 +3,7 @@ cimport libav as lib from av.packet cimport Packet from av.sidedata.sidedata cimport _SideDataContainer + cdef class Frame(object): cdef lib.AVFrame *ptr diff --git a/av/packet.pxd b/av/packet.pxd index 9f88b8a0b..317443fde 100644 --- a/av/packet.pxd +++ b/av/packet.pxd @@ -1,8 +1,8 @@ cimport libav as lib from av.buffer cimport Buffer -from av.stream cimport Stream from av.bytesource cimport ByteSource +from av.stream cimport Stream cdef class Packet(Buffer): diff --git a/av/sidedata/motionvectors.pxd b/av/sidedata/motionvectors.pxd index 70db62b21..3b7f88bc1 100644 --- a/av/sidedata/motionvectors.pxd +++ b/av/sidedata/motionvectors.pxd @@ -1,8 +1,8 @@ +cimport libav as lib + from av.frame cimport Frame from av.sidedata.sidedata cimport SideData -cimport libav as lib - cdef class _MotionVectors(SideData): diff --git a/av/sidedata/sidedata.pxd b/av/sidedata/sidedata.pxd index 03ef47552..f30d8fef7 100644 --- a/av/sidedata/sidedata.pxd +++ b/av/sidedata/sidedata.pxd @@ -1,10 +1,10 @@ +cimport libav as lib + from av.buffer cimport Buffer from av.dictionary cimport _Dictionary, wrap_dictionary from av.frame cimport Frame -cimport libav as lib - cdef class SideData(Buffer): diff --git a/av/stream.pxd b/av/stream.pxd index 61a65034d..fa0ffb2d3 100644 --- a/av/stream.pxd +++ b/av/stream.pxd @@ -1,5 +1,4 @@ from libc.stdint cimport int64_t - cimport libav as lib from av.codec.context cimport CodecContext diff --git a/av/subtitles/stream.pxd b/av/subtitles/stream.pxd index 3273e1181..e21dceb23 100644 --- a/av/subtitles/stream.pxd +++ b/av/subtitles/stream.pxd @@ -1,4 +1,5 @@ from av.stream cimport Stream + cdef class SubtitleStream(Stream): pass diff --git a/av/utils.pxd b/av/utils.pxd index 790b7179e..1f4a254f6 100644 --- a/av/utils.pxd +++ b/av/utils.pxd @@ -1,5 +1,4 @@ from libc.stdint cimport int64_t, uint8_t, uint64_t - cimport libav as lib diff --git a/av/video/frame.pxd b/av/video/frame.pxd index 69779bd62..a08da1ecc 100644 --- a/av/video/frame.pxd +++ b/av/video/frame.pxd @@ -1,5 +1,4 @@ -from libc.stdint cimport uint8_t, uint16_t, int16_t, int32_t, uint64_t - +from libc.stdint cimport int16_t, int32_t, uint8_t, uint16_t, uint64_t cimport libav as lib from av.frame cimport Frame From 6f9a1561f43e0cedc10c0ee33cd30bded7d34dc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Fri, 16 Oct 2020 09:00:51 +0200 Subject: [PATCH 003/192] [tests] skip unicode filename test when building Windows wheels --- .github/workflows/tests.yml | 2 +- tests/common.py | 1 + tests/test_container.py | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 931bae663..ad4287a3c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -221,7 +221,7 @@ jobs: 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 + 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_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m unittest discover -t {project} -s tests && mv {project}/av.disabled {project}/av diff --git a/tests/common.py b/tests/common.py index ec946fc8b..2bb13d5f7 100644 --- a/tests/common.py +++ b/tests/common.py @@ -19,6 +19,7 @@ is_windows = os.name == 'nt' +skip_tests = frozenset(os.environ.get("PYAV_SKIP_TESTS", "").split(",")) def makedirs(path): diff --git a/tests/test_container.py b/tests/test_container.py index 3ca23460f..fa7052395 100644 --- a/tests/test_container.py +++ b/tests/test_container.py @@ -3,7 +3,7 @@ import av -from .common import TestCase, fate_suite, is_windows +from .common import TestCase, fate_suite, is_windows, skip_tests # On Windows, Python 3.0 - 3.5 have issues handling unicode filenames. @@ -21,7 +21,7 @@ def test_context_manager(self): self.assertEqual(container.format.long_name, 'QuickTime / MOV') self.assertEqual(len(container.streams), 1) - @unittest.skipIf(broken_unicode, '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') From 718eaefdc770d982bd3c3f7a9d11ea18e5006c17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Fri, 22 Jan 2021 16:04:24 +0100 Subject: [PATCH 004/192] [package] mention Python 3.9 compatibility --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index ef7a29100..99392a94d 100644 --- a/setup.py +++ b/setup.py @@ -530,6 +530,7 @@ def run(self): 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Multimedia :: Sound/Audio', 'Topic :: Multimedia :: Sound/Audio :: Conversion', From b23648467fc5beb634c05690b853ea0bf749a593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Fri, 22 Jan 2021 17:16:53 +0100 Subject: [PATCH 005/192] Release v8.0.3 --- CHANGELOG.rst | 5 ++++- VERSION.txt | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9b676529b..cc79ea0f8 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, currently "develop"). -v8.0.3.dev0 +v8.0.3 ------ +Minor: + +- Update FFmpeg to 4.3.1 for the binary wheels. v8.0.2 ------ diff --git a/VERSION.txt b/VERSION.txt index 805d7c489..215aacb45 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -8.0.3.dev0 +8.0.3 From 2b40d57c88ef5722d366c4041b9f80fac8cacce0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Sat, 23 Jan 2021 14:18:26 +0100 Subject: [PATCH 006/192] 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 cc79ea0f8..2fa11f678 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.0.4.dev0 +------ + + v8.0.3 ------ diff --git a/VERSION.txt b/VERSION.txt index 215aacb45..b6c32c94b 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -8.0.3 +8.0.4.dev0 From bf4d90bcc7dd016a129ff2e3d59ea3cba1417691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B5=D0=BC=D1=91=D0=BD=20=D0=9C=D0=B0=D1=80=D1=8C?= =?UTF-8?q?=D1=8F=D1=81=D0=B8=D0=BD?= Date: Thu, 25 Feb 2021 19:54:25 +0300 Subject: [PATCH 007/192] Fix args order in Frame.__repr__ --- av/frame.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/frame.pyx b/av/frame.pyx index 90e616af3..3717ddc81 100644 --- a/av/frame.pyx +++ b/av/frame.pyx @@ -26,8 +26,8 @@ cdef class Frame(object): return 'av.%s #%d pts=%s at 0x%x>' % ( self.__class__.__name__, self.index, - id(self), self.pts, + id(self), ) cdef _copy_internal_attributes(self, Frame source, bint data_layout=True): From b79705db4e91c4f6596d69326b69f962d68eed77 Mon Sep 17 00:00:00 2001 From: Philipp Klaus Date: Tue, 10 Nov 2020 15:26:39 +0100 Subject: [PATCH 008/192] fix docs: loglevel QUIET isn't available --- av/logging.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/logging.pyx b/av/logging.pyx index 8e2294d95..2f76ee4da 100644 --- a/av/logging.pyx +++ b/av/logging.pyx @@ -106,7 +106,7 @@ def set_level(int level): Sets logging threshold when converting from FFmpeg's logging system to Python's. It is recommended to use the constants availible in this - module to set the level: ``QUIET``, ``PANIC``, ``FATAL``, ``ERROR``, + module to set the level: ``PANIC``, ``FATAL``, ``ERROR``, ``WARNING``, ``INFO``, ``VERBOSE``, and ``DEBUG``. While less efficient, it is generally preferable to modify logging From 9c427e775fdb33fb89b0aed52c7a090115ca3e50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Fri, 26 Feb 2021 08:54:47 +0100 Subject: [PATCH 009/192] [typo] replace "availible" by "available" --- av/codec/codec.pyx | 4 ++-- av/codec/context.pyx | 2 +- av/container/core.pyx | 2 +- av/error.pyx | 2 +- av/logging.pyx | 4 ++-- docs/api/error.rst | 4 ++-- setup.py | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/av/codec/codec.pyx b/av/codec/codec.pyx index 39d0484d2..4bbbcf369 100644 --- a/av/codec/codec.pyx +++ b/av/codec/codec.pyx @@ -144,7 +144,7 @@ cdef class Codec(object): :param str name: The codec name. :param str mode: ``'r'`` for decoding or ``'w'`` for encoding. - This object exposes information about an availible codec, and an avenue to + This object exposes information about an available codec, and an avenue to create a :class:`.CodecContext` to encode/decode directly. :: @@ -349,7 +349,7 @@ codec_descriptor = wrap_avclass(lib.avcodec_get_class()) def dump_codecs(): - """Print information about availible codecs.""" + """Print information about available codecs.""" print '''Codecs: D..... = Decoding supported diff --git a/av/codec/context.pyx b/av/codec/context.pyx index ef6129960..45801fa34 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -315,7 +315,7 @@ cdef class CodecContext(object): Anything that can be turned into a :class:`.ByteSource` is fine. ``None`` or empty inputs will flush the parser's buffers. - :return: ``list`` of :class:`.Packet` newly availible. + :return: ``list`` of :class:`.Packet` newly available. """ diff --git a/av/container/core.pyx b/av/container/core.pyx index 2abca6f11..afbe2f6f6 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -25,7 +25,7 @@ ctypedef int64_t (*seek_func_t)(void *opaque, int64_t offset, int whence) nogil cdef object _cinit_sentinel = object() -# We want to use the monotonic clock if it is availible. +# 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: diff --git a/av/error.pyx b/av/error.pyx index fdea6be3a..cde4fec7f 100644 --- a/av/error.pyx +++ b/av/error.pyx @@ -63,7 +63,7 @@ class FFmpegError(Exception): .. attribute:: filename - The filename that was being operated on (if availible). + The filename that was being operated on (if available). .. attribute:: type diff --git a/av/logging.pyx b/av/logging.pyx index 2f76ee4da..399e8159e 100644 --- a/av/logging.pyx +++ b/av/logging.pyx @@ -105,7 +105,7 @@ def set_level(int level): """set_level(level) Sets logging threshold when converting from FFmpeg's logging system - to Python's. It is recommended to use the constants availible in this + 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``. @@ -287,7 +287,7 @@ cdef log_callback_gil(int level, const char *c_name, const char *c_message): log = (level, name, message) # We have to filter it ourselves, but we will still process it in general so - # it is availible to our error handling. + # it is available to our error handling. # Note that FFmpeg's levels are backwards from Python's. cdef bint is_interesting = level <= level_threshold diff --git a/docs/api/error.rst b/docs/api/error.rst index 43e21b574..3f82f4ec2 100644 --- a/docs/api/error.rst +++ b/docs/api/error.rst @@ -26,7 +26,7 @@ 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 availible in +To mimick the stdlib ``errno`` module, all enumeration values are available in the ``av.error`` module, e.g.:: try: @@ -64,7 +64,7 @@ exceptions expose the typical ``errno`` and ``strerror`` attributes (even ``ValueError`` which doesn't typically), as well as some PyAV extensions such as :attr:`FFmpegError.log`. -All of these exceptions are availible on the top-level ``av`` package, e.g.:: +All of these exceptions are available on the top-level ``av`` package, e.g.:: try: do_something() diff --git a/setup.py b/setup.py index 99392a94d..e4135425f 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ import sys try: - # This depends on _winreg, which is not availible on not-Windows. + # This depends on _winreg, which is not available on not-Windows. from distutils.msvc9compiler import MSVCCompiler as MSVC9Compiler except ImportError: MSVC9Compiler = None From d50413d410aa5d87ef6f79b0da98fb696f74bdf5 Mon Sep 17 00:00:00 2001 From: Justin Wong <46082645+uvjustin@users.noreply.github.com> Date: Thu, 25 Feb 2021 14:10:37 +0800 Subject: [PATCH 010/192] Expose codec_context.codec_tag (fixes: #741) --- av/codec/context.pyx | 12 ++++++++++++ tests/test_codec_context.py | 23 ++++++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 45801fa34..7ccda8647 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -540,6 +540,18 @@ cdef class CodecContext(object): def __set__(self, value): 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 diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index b213c0475..20afb68e7 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -43,6 +43,22 @@ def test_skip_frame_default(self): 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') + + # 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.') + + # 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.') + def test_parse(self): # This one parses into a single packet. @@ -149,6 +165,9 @@ def test_encoding_h264(self): def test_encoding_mpeg4(self): self.video_encoding('mpeg4') + def test_encoding_xvid(self): + self.video_encoding('mpeg4', codec_tag='xvid') + def test_encoding_mpeg1video(self): self.video_encoding('mpeg1video') @@ -167,7 +186,7 @@ def test_encoding_dnxhd(self): 'max_frames': 5} self.video_encoding('dnxhd', options) - def video_encoding(self, codec_name, options={}): + def video_encoding(self, codec_name, options={}, codec_tag=None): try: codec = Codec(codec_name, 'w') @@ -190,6 +209,8 @@ def video_encoding(self, codec_name, options={}): ctx.framerate = 1 / ctx.time_base ctx.pix_fmt = pix_fmt ctx.options = options # TODO + if codec_tag: + ctx.codec_tag = codec_tag ctx.open() path = self.sandboxed('encoder.%s' % codec_name) From 81e5cb2e3da2c514d112496b21fa1fb10051f5b6 Mon Sep 17 00:00:00 2001 From: Christoph Rackwitz Date: Wed, 9 Dec 2020 21:32:45 +0100 Subject: [PATCH 011/192] example: encoding with custom PTS --- examples/numpy/generate_video_with_pts.py | 88 +++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 examples/numpy/generate_video_with_pts.py diff --git a/examples/numpy/generate_video_with_pts.py b/examples/numpy/generate_video_with_pts.py new file mode 100644 index 000000000..2ac835f66 --- /dev/null +++ b/examples/numpy/generate_video_with_pts.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 + +from fractions import Fraction +import colorsys + +import numpy as np + +import av + + +(width, height) = (640, 360) +total_frames = 20 +fps = 30 + +container = av.open('generate_video_with_pts.mp4', mode='w') + +stream = container.add_stream('mpeg4', rate=fps) # alibi frame rate +stream.width = width +stream.height = height +stream.pix_fmt = 'yuv420p' + +# ffmpeg time is complicated +# more at https://github.com/PyAV-Org/PyAV/blob/main/docs/api/time.rst +# our situation is the "encoding" one + +# this is independent of the "fps" you give above +# 1/1000 means milliseconds (and you can use that, no problem) +# 1/2 means half a second (would be okay for the delays we use below) +# 1/30 means ~33 milliseconds +# you should use the least fraction that makes sense for you +stream.codec_context.time_base = Fraction(1, fps) + +# this says when to show the next frame +# (increment by how long the current frame will be shown) +my_pts = 0 # [seconds] +# below we'll calculate that into our chosen time base + +# we'll keep this frame around to draw on this persistently +# you can also redraw into a new object every time but you needn't +the_canvas = np.zeros((height, width, 3), dtype=np.uint8) +the_canvas[:, :] = (32, 32, 32) # some dark gray background because why not +block_w2 = int(0.5 * width / total_frames * 0.75) +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) + + # 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 + + 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)) + + # increment by display time to pre-determine next frame's PTS + my_pts += 1.0 if ((frame_i // 3) % 2 == 0) else 0.5 + # yes, the last frame has no "duration" because nothing follows it + # frames don't have duration, only a PTS + + for packet in stream.encode(frame): + container.mux(packet) + +# finish it with a blank frame, so the "last" frame actually gets shown for some time +# 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.pts = int(round(my_pts / stream.codec_context.time_base)) +for packet in stream.encode(frame): + container.mux(packet) + +# the time should now be 15.5 + 1/30 = 15.533 + +# without that last black frame, the real last frame gets shown for 1/30 +# so that video would have been 14.5 + 1/30 = 14.533 seconds long + +# Flush stream +for packet in stream.encode(): + container.mux(packet) + +# Close the file +container.close() From 359dbbdcd2fe8816d1d2eb3f9b67993da44d33cb Mon Sep 17 00:00:00 2001 From: Justin Wong <46082645+uvjustin@users.noreply.github.com> Date: Fri, 26 Feb 2021 17:28:50 +0800 Subject: [PATCH 012/192] Check codec tag against known FATE sample --- tests/test_codec_context.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 20afb68e7..40c52a2ee 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -59,6 +59,9 @@ def test_codec_tag(self): ctx.codec_tag = 123 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') + def test_parse(self): # This one parses into a single packet. From 0bd186ee8857d7b7bf2fa95ac9a2fed0000e3c98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Fri, 26 Feb 2021 15:47:01 +0100 Subject: [PATCH 013/192] Use av_packet_rescale_ts in Packet._rebase_time() (fixes: #737) --- av/packet.pyx | 18 +----------------- include/libavcodec/avcodec.pxd | 1 + 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/av/packet.pyx b/av/packet.pyx index 9fbf3b6d6..2ffbcca62 100644 --- a/av/packet.pyx +++ b/av/packet.pyx @@ -75,23 +75,7 @@ cdef class Packet(Buffer): if self._time_base.num == dst.num and self._time_base.den == dst.den: return - # TODO: Isn't there a function to do this? - - if self.struct.pts != lib.AV_NOPTS_VALUE: - self.struct.pts = lib.av_rescale_q( - self.struct.pts, - self._time_base, dst - ) - if self.struct.dts != lib.AV_NOPTS_VALUE: - self.struct.dts = lib.av_rescale_q( - self.struct.dts, - self._time_base, dst - ) - if self.struct.duration > 0: - self.struct.duration = lib.av_rescale_q( - self.struct.duration, - self._time_base, dst - ) + lib.av_packet_rescale_ts(&self.struct, self._time_base, dst) self._time_base = dst diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 92d860d65..d4dfd3905 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -355,6 +355,7 @@ cdef extern from "libavcodec/avcodec.h" nogil: 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: SUBTITLE_NONE From 116ac733379ff213043ad4d8374a144caaee1c91 Mon Sep 17 00:00:00 2001 From: Pino Toscano Date: Sat, 26 Dec 2020 15:39:06 +0100 Subject: [PATCH 014/192] [tests] do not hardcode errno values Do not assume that 1 == EPERM and 2 == ENOENT (as OSError); rather use the errno module to get/use their values. --- tests/test_errors.py | 3 ++- tests/test_logging.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_errors.py b/tests/test_errors.py index 4df2cfabf..924fdbed0 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -1,3 +1,4 @@ +import errno import traceback import av @@ -47,7 +48,7 @@ def test_filenotfound(self): try: av.open('does not exist') except FileNotFoundError as e: - self.assertEqual(e.errno, 2) + self.assertEqual(e.errno, errno.ENOENT) if is_windows: self.assertTrue(e.strerror in ['Error number -2 occurred', 'No such file or directory']) diff --git a/tests/test_logging.py b/tests/test_logging.py index 8e83c7d3b..1747f40ee 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -1,5 +1,6 @@ from __future__ import division +import errno import logging import threading @@ -76,7 +77,7 @@ def test_error(self): log = (av.logging.ERROR, 'test', 'This is a test.') av.logging.log(*log) try: - av.error.err_check(-1) + av.error.err_check(-errno.EPERM) except OSError as e: self.assertEqual(e.log, log) else: From 60cbc412b7c9fea7bac624ec06b168b58ff785a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Fri, 26 Feb 2021 16:15:56 +0100 Subject: [PATCH 015/192] Use av_guess_format for output container format (fixes: #691) Some formats go by multiple names, so checking for equality of the format name fails. Use the higher-level `av_guess_format` which does the right thing. --- av/format.pyx | 16 +--------------- tests/test_containerformat.py | 23 ++++++++++++++++++++--- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/av/format.pyx b/av/format.pyx index 71e58d47f..05a5402a8 100644 --- a/av/format.pyx +++ b/av/format.pyx @@ -84,7 +84,7 @@ cdef class ContainerFormat(object): self.iptr = lib.av_find_input_format(name) if mode is None or mode == 'w': - self.optr = find_output_format(name) + self.optr = lib.av_guess_format(name, NULL, NULL) if not self.iptr and not self.optr: raise ValueError('no container format %r' % name) @@ -172,20 +172,6 @@ cdef class ContainerFormat(object): seek_to_pts = flags.flag_property('SEEK_TO_PTS') -cdef lib.AVOutputFormat* find_output_format(name): - cdef const lib.AVOutputFormat *ptr - cdef void *opaque = NULL - - while True: - ptr = lib.av_muxer_iterate(&opaque) - if not ptr: - break - if ptr.name == name: - return ptr - - return NULL - - cdef get_output_format_names(): names = set() cdef const lib.AVOutputFormat *ptr diff --git a/tests/test_containerformat.py b/tests/test_containerformat.py index 7403aeb7f..e5e5a9698 100644 --- a/tests/test_containerformat.py +++ b/tests/test_containerformat.py @@ -6,15 +6,12 @@ class TestContainerFormats(TestCase): def test_matroska(self): - 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) def test_mov(self): @@ -23,6 +20,26 @@ def test_mov(self): 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) + + def test_stream_segment(self): + # 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) + + 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) def test_formats_available(self): self.assertTrue(formats_available) From 9ac05d9ac902d71ecb2fe80f04dcae454008378c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Mon, 1 Mar 2021 22:03:06 +0100 Subject: [PATCH 016/192] Fix setting CodecContext.extradata - this member can only be set for decoders - we need to allocate and free the memory --- av/codec/context.pxd | 6 +++--- av/codec/context.pyx | 20 ++++++++++++++++---- include/libavcodec/avcodec.pxd | 1 + include/libavutil/avutil.pxd | 1 + tests/test_codec_context.py | 26 ++++++++++++++++++++++++++ 5 files changed, 47 insertions(+), 7 deletions(-) diff --git a/av/codec/context.pxd b/av/codec/context.pxd index 0f2920191..d9b6906f9 100644 --- a/av/codec/context.pxd +++ b/av/codec/context.pxd @@ -14,6 +14,9 @@ cdef class CodecContext(object): # 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 + # 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. @@ -21,9 +24,6 @@ cdef class CodecContext(object): cdef lib.AVCodecParserContext *parser - # To hold a reference to passed extradata. - cdef ByteSource extradata_source - cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec) cdef readonly Codec codec diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 7ccda8647..d93fb64f5 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -1,6 +1,5 @@ from libc.errno cimport EAGAIN from libc.stdint cimport int64_t, uint8_t -from libc.stdlib cimport free, malloc, realloc from libc.string cimport memcpy cimport libav as lib @@ -230,9 +229,20 @@ cdef class CodecContext(object): return None def __set__(self, data): - self.extradata_source = bytesource(data) - self.ptr.extradata = self.extradata_source.ptr - self.ptr.extradata_size = self.extradata_source.length + 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): @@ -288,6 +298,8 @@ cdef class CodecContext(object): err_check(lib.avcodec_close(self.ptr)) def __dealloc__(self): + if self.ptr and self.extradata_set: + lib.av_freep(&self.ptr.extradata) if self.ptr and self.allocated: lib.avcodec_close(self.ptr) lib.avcodec_free_context(&self.ptr) diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index d4dfd3905..7e921e847 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -15,6 +15,7 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef char* avcodec_configuration() cdef char* avcodec_license() + cdef size_t AV_INPUT_BUFFER_PADDING_SIZE cdef int64_t AV_NOPTS_VALUE # AVCodecDescriptor.props diff --git a/include/libavutil/avutil.pxd b/include/libavutil/avutil.pxd index fa1bdf203..50e6bfffd 100644 --- a/include/libavutil/avutil.pxd +++ b/include/libavutil/avutil.pxd @@ -44,6 +44,7 @@ cdef extern from "libavutil/avutil.h" nogil: cdef void* av_malloc(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_freep(void *ptr) diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 40c52a2ee..9f68785bf 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -62,6 +62,32 @@ def test_codec_tag(self): 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() + self.assertEqual(ctx.extradata, None) + self.assertEqual(ctx.extradata_size, 0) + + ctx.extradata = b"123" + self.assertEqual(ctx.extradata, b"123") + self.assertEqual(ctx.extradata_size, 3) + + ctx.extradata = b"54321" + self.assertEqual(ctx.extradata, b"54321") + self.assertEqual(ctx.extradata_size, 5) + + ctx.extradata = None + self.assertEqual(ctx.extradata, None) + self.assertEqual(ctx.extradata_size, 0) + + def test_encoder_extradata(self): + ctx = av.codec.Codec('h264', 'w').create() + 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.") + def test_parse(self): # This one parses into a single packet. From 1cbd8d1b325d2766cdce02d9e20f4fc5ffd51ad6 Mon Sep 17 00:00:00 2001 From: Santiago Castro Date: Thu, 6 May 2021 22:10:03 -0400 Subject: [PATCH 017/192] 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 018/192] 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 019/192] 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 020/192] [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 021/192] 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 022/192] 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 023/192] 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 024/192] 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 025/192] 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 026/192] 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 027/192] 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 028/192] [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 029/192] [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 030/192] 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 031/192] 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 032/192] [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 033/192] 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 034/192] 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 035/192] 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 036/192] [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 037/192] [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 038/192] [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 039/192] [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 040/192] [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 041/192] [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 042/192] [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 043/192] [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 044/192] 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 045/192] [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 046/192] [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 047/192] [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 048/192] [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 049/192] [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 050/192] 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 051/192] 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 052/192] 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 053/192] [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 054/192] [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 055/192] [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 056/192] [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 057/192] 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 058/192] 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 059/192] [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 060/192] [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 061/192] [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 062/192] [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 063/192] [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 064/192] 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 065/192] [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 066/192] 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 067/192] 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 068/192] [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 069/192] [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 070/192] [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 071/192] 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 072/192] [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 073/192] [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 074/192] [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 075/192] 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 076/192] 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 077/192] [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 078/192] [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 079/192] [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 080/192] 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 081/192] 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 082/192] 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 083/192] [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 084/192] [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 085/192] [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 086/192] [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 087/192] [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 088/192] [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 089/192] [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 090/192] [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 091/192] [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 092/192] 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 093/192] [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 094/192] [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 095/192] 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 096/192] 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 097/192] 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 098/192] 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 099/192] 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 100/192] 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 101/192] 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 102/192] 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 103/192] 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 104/192] 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 105/192] 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 106/192] 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 107/192] 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 108/192] [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 109/192] [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 110/192] 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 111/192] 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 112/192] 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 113/192] 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 114/192] [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 115/192] [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 116/192] 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 117/192] [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 118/192] 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 119/192] 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 120/192] 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 121/192] 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 122/192] =?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 123/192] [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 124/192] [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 125/192] [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 126/192] 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 127/192] 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 128/192] =?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 129/192] 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 130/192] [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 131/192] 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 132/192] 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 133/192] [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 134/192] [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 135/192] [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 136/192] 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 137/192] 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 138/192] 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 139/192] [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 8871e13848cbf78615ba7b429d301adc071841ea Mon Sep 17 00:00:00 2001 From: Mattias Wadman Date: Tue, 21 Mar 2023 17:01:22 +0100 Subject: [PATCH 140/192] Update to ffmpeg 6.0 Removed and renamed constants with entry from API changes https://git.ffmpeg.org/gitweb/ffmpeg.git/blob/HEAD:/doc/APIchanges Remove use of AVCodecContext.sub_text_format 2021-09-20 - 176b8d785bf - lavc 59.9.100 - avcodec.h Deprecate AVCodecContext.sub_text_format and the corresponding AVOptions. It is unused since the last major bump. AV_CODEC_CAP_TRUNCATED removed: 2021-09-20 - dd846bc4a91 - lavc 59.8.100 - avcodec.h codec.h Deprecate AV_CODEC_FLAG_TRUNCATED and AV_CODEC_CAP_TRUNCATED, as they are redundant with parsers. AV_CODEC_CAP_AUTO_THREADS renamed to AV_CODEC_CAP_AUTO_THREADS 2021-03-16 - 7d09579190 - lavc 58.132.100 - codec.h Add AV_CODEC_CAP_OTHER_THREADS as a new name for AV_CODEC_CAP_AUTO_THREADS. AV_CODEC_CAP_AUTO_THREADS is now deprecated. AV_CODEC_CAP_INTRA_ONLY removed (use AV_CODEC_PROP_INTRA_ONLY instead): AV_CODEC_CAP_LOSSLESS removed (use AV_CODEC_PROP_LOESSNES instead): 2020-05-21 - 13b1bbff0b - lavc 58.86.101 - avcodec.h Deprecated AV_CODEC_CAP_INTRA_ONLY and AV_CODEC_CAP_LOSSLESS. AV_CODEC_FLAG_TRUNCATED removed: AV_CODEC_CAP_TRUNCATED removed: 2021-09-20 - dd846bc4a91 - lavc 59.8.100 - avcodec.h codec.h Deprecate AV_CODEC_FLAG_TRUNCATED and AV_CODEC_CAP_TRUNCATED, as they are redundant with parsers. AV_CODEC_FLAG2_DROP_FRAME_TIMECODE removed: Not API changelog but was removed in f843460eb790d37e444e5946628f228421916537: avcodec/avcodec: Remove AV_CODEC_FLAG2_DROP_FRAME_TIMECODE It has been deprecated in 94d68a4 and can't be set via AVOptions. The only codecs that use it (the MPEG-1/2 encoders) have private options for this. So remove it. AVFMT_FLAG_PRIV_OPT removed: 2021-03-03 - 2ff40b98ec - lavf 58.70.100 - avformat.h Deprecate AVFMT_FLAG_PRIV_OPT. It will do nothing as soon as av_demuxer_open() is removed. Related to #1106 --- 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 d21893c43..1c5c75b8f 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 f84887c2ae47129dc9ec1901f58db2a8b7ad0d43 Mon Sep 17 00:00:00 2001 From: Mark Harfouche Date: Sun, 23 Jul 2023 14:11:17 -0400 Subject: [PATCH 141/192] Small updates to enable Cython 3 Mostly it seems that cython wants you to declare things as `noexcept` Closes https://github.com/PyAV-Org/PyAV/issues/1140 I'm going to see if I can report the bug to cython, but it is hard for me to create a minimum reproducible example --- av/container/core.pyx | 12 ++++++------ av/container/pyio.pxd | 6 +++--- av/container/pyio.pyx | 12 ++++++------ av/logging.pyx | 4 ++-- av/video/format.pxd | 2 +- av/video/format.pyx | 8 ++++++-- 6 files changed, 24 insertions(+), 20 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/av/video/format.pyx b/av/video/format.pyx index b96658272..9ad539101 100644 --- a/av/video/format.pyx +++ b/av/video/format.pyx @@ -47,10 +47,14 @@ cdef class VideoFormat(object): self.ptr = lib.av_pix_fmt_desc_get(pix_fmt) self.width = width self.height = height - self.components = tuple( + # hmaarrfk -- 2023/07/23 + # Note on tuple([]) + # Cython 3 seems to have trouble with cdef tuples, so we use a list + # it complains about some const identifier not being able to get assigned + self.components = tuple([ VideoFormatComponent(self, i) for i in range(self.ptr.nb_components) - ) + ]) def __repr__(self): if self.width or self.height: From 4349105a86bf00c7790ce89d6552966a23d7bcb6 Mon Sep 17 00:00:00 2001 From: Mark Harfouche Date: Sun, 23 Jul 2023 14:33:29 -0400 Subject: [PATCH 142/192] Fix removal of function from cython3 --- docs/development/includes.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) 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 bfbbc8dca8ea65b5e98232cfcecfffbd6daf06aa Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 3 Oct 2023 15:34:24 -0400 Subject: [PATCH 143/192] Bump version to 11.0.0 --- av/about.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8d3161e96f14f543d053ed647271ef75da3777b3 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 3 Oct 2023 16:07:23 -0400 Subject: [PATCH 144/192] Fix tests --- .github/workflows/issues.yml | 18 ---------------- .github/workflows/tests.yml | 42 ++++++++---------------------------- 2 files changed, 9 insertions(+), 51 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 d3ea994cf..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@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 - operations-per-run: 60 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ded1052b5..cd917d6d2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,35 +1,25 @@ name: tests - on: [push, pull_request] - 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 - + PYAV_LIBRARY: ffmpeg-6.0 steps: - - uses: actions/checkout@v3 name: Checkout - - name: Python uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.9 - name: Environment run: env | sort @@ -45,30 +35,21 @@ 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: pypy3.9, ffmpeg: "4.4"} - - {os: macos-latest, python: 3.7, ffmpeg: "4.4"} - + - {os: ubuntu-latest, python: 3.9, ffmpeg: "6.0"} + - {os: ubuntu-latest, python: 3.9, ffmpeg: "5.1"} + - {os: macos-latest, python: 3.9, ffmpeg: "6.0"} env: PYAV_PYTHON: python${{ matrix.config.python }} PYAV_LIBRARY: ffmpeg-${{ matrix.config.ffmpeg }} steps: - - uses: actions/checkout@v3 name: Checkout @@ -138,22 +119,17 @@ 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.9, ffmpeg: "6.0"} + - {os: windows-latest, python: 3.9, ffmpeg: "5.1"} steps: - - name: Checkout uses: actions/checkout@v3 @@ -202,7 +178,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.9 - name: Build source package run: | pip install cython @@ -236,7 +212,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.9 - name: Set up QEMU if: matrix.os == 'ubuntu-latest' uses: docker/setup-qemu-action@v2 From cce1e91987e86ae88669c082a7784559886876ec Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 3 Oct 2023 17:17:34 -0400 Subject: [PATCH 145/192] Try using regular ffmpeg releases for ci --- .github/workflows/tests.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cd917d6d2..433af19bd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -149,8 +149,6 @@ jobs: 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 @@ -160,7 +158,7 @@ jobs: run: | . $CONDA/etc/profile.d/conda.sh conda activate pyav - if [[ "${{ matrix.config.ffmpeg }}" != "4.3" ]]; then + if [[ "${{ matrix.config.ffmpeg }}" != "6.0" ]]; then tar -xf ffmpeg.tar.gz -C $CONDA_PREFIX/Library/ fi python setup.py build_ext --inplace --ffmpeg-dir=$CONDA_PREFIX/Library @@ -200,8 +198,8 @@ jobs: arch: arm64 - os: macos-latest arch: x86_64 - - os: ubuntu-latest - arch: aarch64 + # - os: ubuntu-latest + # arch: aarch64 - os: ubuntu-latest arch: i686 - os: ubuntu-latest From 62fb567b8b464b91213fbd0fdd646fa39cd2e68d Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 4 Oct 2023 01:25:25 -0400 Subject: [PATCH 146/192] Remove support for 3.7 and 3.8 --- setup.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 58fc307b1..19e0bbdcd 100644 --- a/setup.py +++ b/setup.py @@ -192,7 +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', + python_requires='>=3.9', zip_safe=False, ext_modules=ext_modules, test_suite="tests", @@ -211,8 +211,6 @@ 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", From b1ee3d84b2f3b2b0c4977483b8077452ae8bf42c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 4 Oct 2023 02:37:03 -0400 Subject: [PATCH 147/192] Modify readme and package details --- .github/workflows/tests.yml | 2 +- README.md | 56 +++++++++---------------------------- setup.py | 11 ++++---- 3 files changed, 19 insertions(+), 50 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 433af19bd..c2793085a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -258,4 +258,4 @@ jobs: uses: pypa/gh-action-pypi-publish@master with: user: __token__ - password: ${{ secrets.PYPI_TOKEN }} + password: ${{ secrets.PYPI_PASSWORD }} diff --git a/README.md b/README.md index 74eecefcb..05b4df560 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,8 @@ -PyAV -==== +# 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] +[![GitHub Test Status][https://github.com/WyattBlue/PyAV/workflows/tests/badge.svg]][https://github.com/WyattBlue/PyAV/actions?workflow=tests] -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][https://ffmpeg.org] 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 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). @@ -14,67 +11,40 @@ This power does come with some responsibility as working with media is horrendou 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: +## Installation +Due to the complexity of the dependencies, PyAV is not always the easiest Python package to install from source. Binary wheels are provided on [PyPI][https://pypi.org/project/pyav] for MacOS, Windows, and Linux linked against a modern FFmpeg. You can install these wheels by running: ```bash -pip install av +pip install pyav ``` -If you want to use your existing FFmpeg, the source version of PyAV is on [PyPI][pypi] too: +If you want to use your existing FFmpeg, the source version is available too: ```bash -pip install av --no-binary av +pip install pyav --no-binary pyav ``` -Alternative installation methods --------------------------------- +## Alternative installation methods -Another way of installing PyAV is via [conda-forge][conda-forge]: +Another way of installing PyAV is via [conda-forge][https://conda-forge.github.io/]: ```bash 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): ```bash -git clone git@github.com:PyAV-Org/PyAV +git clone https://github.com/WyattBlue/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: +pip install -U -r tests/requirements.txt ./scripts/build-deps -# Build PyAV. make ``` --- -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 -[gitter-badge]: https://img.shields.io/gitter/room/nwjs/nw.js.svg?logo=gitter&colorB=cc2b5e -[gitter]: https://gitter.im/PyAV-Org -[pypi-badge]: https://img.shields.io/pypi/v/av.svg?colorB=CCB39A -[pypi]: https://pypi.org/project/av - -[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]: https://github.com/PyAV-Org/PyAV - -[ffmpeg]: http://ffmpeg.org/ -[conda-forge]: https://conda-forge.github.io/ -[conda-install]: https://docs.conda.io/projects/conda/en/latest/user-guide/install/index.html +Have fun, and good luck! diff --git a/setup.py b/setup.py index 19e0bbdcd..9c9549105 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) @@ -184,15 +183,15 @@ def parse_cflags(raw_flags): setup( - name="av", + name="pyav", 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", + author="WyattBlue", + author_email="wyattblue@auto-editor.com", + url="https://github.com/WyattBlue/PyAV", packages=find_packages(exclude=["build*", "examples*", "scratchpad*", "tests*"]), package_data=package_data, - python_requires='>=3.9', + python_requires=">=3.9", zip_safe=False, ext_modules=ext_modules, test_suite="tests", From 4e436aa24b5e30fabc0b5893a9ba2f6068b87822 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 4 Oct 2023 02:46:32 -0400 Subject: [PATCH 148/192] Further improve readme --- .github/workflows/tests.yml | 8 +++++++- README.md | 12 +++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c2793085a..1404fe27e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,5 +1,11 @@ name: tests -on: [push, pull_request] +on: + push: + paths-ignore: + - '**.md' + pull_request: + paths-ignore: + - '**.md' jobs: style: name: "${{ matrix.config.suite }}" diff --git a/README.md b/README.md index 05b4df560..cdad7babf 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ # PyAV -[![GitHub Test Status][https://github.com/WyattBlue/PyAV/workflows/tests/badge.svg]][https://github.com/WyattBlue/PyAV/actions?workflow=tests] +PyAV is a Pythonic binding for the [FFmpeg](https://ffmpeg.org) 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][https://ffmpeg.org] libraries. We aim to provide all of the power and control of the underlying library, but manage the gritty details as much as possible. +--- +[![Actions Status](https://github.com/WyattBlue/PyAV/workflows/tests/badge.svg)](https://github.com/wyattblue/PyAV/actions?workflow=tests) +Code style: black 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). @@ -11,8 +13,8 @@ This power does come with some responsibility as working with media is horrendou 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. Binary wheels are provided on [PyPI][https://pypi.org/project/pyav] for MacOS, Windows, and Linux linked against a modern FFmpeg. You can install these wheels by running: +## Installing +Due to the complexity of the dependencies, PyAV is not always the easiest Python package to install from source. Binary wheels are provided on [PyPI](https://pypi.org/project/pyav) for MacOS, Windows, and Linux linked against a modern FFmpeg. You can install these wheels by running: ```bash pip install pyav @@ -26,7 +28,7 @@ pip install pyav --no-binary pyav ## Alternative installation methods -Another way of installing PyAV is via [conda-forge][https://conda-forge.github.io/]: +Another way of installing PyAV is via [conda-forge](https://conda-forge.github.io/): ```bash conda install av -c conda-forge From fc62d4e8ba3dc2d127b1581746d4b53a7213c6c5 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 4 Oct 2023 03:02:12 -0400 Subject: [PATCH 149/192] Make sure it works without these files --- .editorconfig | 16 ------ .github/workflows/tests.yml | 2 + AUTHORS.py | 100 ------------------------------------ AUTHORS.rst | 46 ----------------- HACKING.rst | 59 --------------------- README.md | 1 + flags.txt | 53 ------------------- 7 files changed, 3 insertions(+), 274 deletions(-) delete mode 100644 .editorconfig delete mode 100644 AUTHORS.py delete mode 100644 AUTHORS.rst delete mode 100644 HACKING.rst delete mode 100644 flags.txt 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 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1404fe27e..908b8980a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,9 +3,11 @@ on: push: paths-ignore: - '**.md' + - '**.rst' pull_request: paths-ignore: - '**.md' + - '**.rst' jobs: style: name: "${{ matrix.config.suite }}" diff --git a/AUTHORS.py b/AUTHORS.py deleted file mode 100644 index eb7ac721c..000000000 --- a/AUTHORS.py +++ /dev/null @@ -1,100 +0,0 @@ -import math -import subprocess - - -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', - - # Junk. - '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', - -} - -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', -} - -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', -} - - -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 bettween 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, )) diff --git a/AUTHORS.rst b/AUTHORS.rst deleted file mode 100644 index 94601edd3..000000000 --- a/AUTHORS.rst +++ /dev/null @@ -1,46 +0,0 @@ -Contributors -============ - -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 - -* 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 `_ - -- rutsh -- 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 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/README.md b/README.md index cdad7babf..ebdbdd2eb 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ pip install -U -r tests/requirements.txt ./scripts/build-deps make +# optional: make test ``` --- 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 6e27b1d73794f9897a0f3e18a0a627a84f9b786a Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 4 Oct 2023 03:53:39 -0400 Subject: [PATCH 150/192] Make release event publish to pypi --- .github/workflows/tests.yml | 4 +++- av/about.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 908b8980a..48e53eb80 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,5 +1,7 @@ name: tests on: + release: + types: [created] push: paths-ignore: - '**.md' @@ -262,7 +264,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' uses: pypa/gh-action-pypi-publish@master with: user: __token__ diff --git a/av/about.py b/av/about.py index 5b461163e..b5d098016 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "11.0.0" +__version__ = "11.0.1" From 6ff0b59272f9ed54cd2cc9f7a2dc7788bebe8a23 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 8 Oct 2023 00:03:07 -0400 Subject: [PATCH 151/192] Update setup.py --- setup.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 9c9549105..5b8818013 100644 --- a/setup.py +++ b/setup.py @@ -81,12 +81,12 @@ def get_config_from_pkg_config(): print("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")) 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) @@ -109,7 +109,7 @@ def parse_cflags(raw_flags): 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) + return config, shlex.join(unknown) # Parse command-line arguments. @@ -213,6 +213,7 @@ def parse_cflags(raw_flags): "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 58742d21415af403f588fd323cf88c29389b2320 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 8 Oct 2023 00:40:09 -0400 Subject: [PATCH 152/192] Remove changelog --- CHANGELOG.rst | 648 -------------------------------------------------- 1 file changed, 648 deletions(-) delete mode 100644 CHANGELOG.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst deleted file mode 100644 index af5416bce..000000000 --- a/CHANGELOG.rst +++ /dev/null @@ -1,648 +0,0 @@ -Changelog -========= - -We are operating with `semantic versioning `_. - -.. - Please try to update this file in the commits that make the changes. - - 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 tracking easier, please add either ``closes #123`` or ``fixes #123`` - to the first line of the commit message. There are more syntaxes at: - . - - 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 ------- - -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 lanaguage level to avoid build warnings. (:issue:`443`) - -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). - - -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 - inconsistant ``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 - 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 writeable. -- ``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. -- Agressively 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 ------- - -- FIRST PUBLIC RELEASE! -- Container/video/audio formats. -- Audio layouts. -- Decoding video/audio/subtitles. -- Encoding video. -- Audio FIFOs and resampling. From 7ab4cfb61ec3253bc98a8b14256d2a40cef659f7 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 8 Oct 2023 01:21:27 -0400 Subject: [PATCH 153/192] Support yuv444p/yuvj444p in `to_ndarray`/`from_ndarray` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ryan Huang 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 3d354f0d6..77a5c215b 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 854af36bceb8d5c5e721b0ef40fbc4e66f0eb54d Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 8 Oct 2023 01:43:45 -0400 Subject: [PATCH 154/192] Update metadata --- setup.cfg | 6 ++---- setup.py | 26 ++++++++++++-------------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/setup.cfg b/setup.cfg index 1de247b85..db31aaa26 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,7 +19,5 @@ 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 + Bug Reports = https://github.com/WyattBlue/pyav/issues + Download = https://pypi.org/project/pyav diff --git a/setup.py b/setup.py index 5b8818013..e4a4bfe51 100644 --- a/setup.py +++ b/setup.py @@ -188,23 +188,23 @@ def parse_cflags(raw_flags): description="Pythonic bindings for FFmpeg's libraries.", author="WyattBlue", author_email="wyattblue@auto-editor.com", - url="https://github.com/WyattBlue/PyAV", + url="https://github.com/WyattBlue/pyav", packages=find_packages(exclude=["build*", "examples*", "scratchpad*", "tests*"]), 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", - ], - }, + python_requires=">=3.9", classifiers=[ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", + "Topic :: Multimedia :: Sound/Audio", + "Topic :: Multimedia :: Sound/Audio :: Conversion", + "Topic :: Multimedia :: Video", + "Topic :: Multimedia :: Video :: Conversion", + "Topic :: Software Development :: Libraries :: Python Modules", "License :: OSI Approved :: BSD License", "Natural Language :: English", + "Intended Audience :: Developers", + "Development Status :: 5 - Production/Stable", "Operating System :: MacOS :: MacOS X", "Operating System :: POSIX", "Operating System :: Unix", @@ -214,10 +214,8 @@ def parse_cflags(raw_flags): "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", - "Topic :: Multimedia :: Video", - "Topic :: Multimedia :: Video :: Conversion", ], + entry_points={ + "console_scripts": ["pyav = av.__main__:main"], + }, ) From 7d7065d78cb3eeb77378ca912477b5616a60e74a Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 8 Oct 2023 02:23:21 -0400 Subject: [PATCH 155/192] Update README --- README.md | 46 ++++++++++++++++++---------------------------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index ebdbdd2eb..3f2a746f4 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,43 @@ -# PyAV - -PyAV is a Pythonic binding for the [FFmpeg](https://ffmpeg.org) 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 +pyav implements bindings for the [ffmpeg](https://ffmpeg.org) libraries. We aim to provide all of the power and control of the underlying library, but manage the gritty details as much as possible. --- [![Actions Status](https://github.com/WyattBlue/PyAV/workflows/tests/badge.svg)](https://github.com/wyattblue/PyAV/actions?workflow=tests) Code style: black -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. - -But where you can't work without it, PyAV is a critical tool. +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 make all the best decisions for you. If the `ffmpeg` cli does the job without you bending over backwards, use it. pyav is much more complicated than the standalone `ffmpeg` program. ## Installing -Due to the complexity of the dependencies, PyAV is not always the easiest Python package to install from source. Binary wheels are provided on [PyPI](https://pypi.org/project/pyav) for MacOS, Windows, and Linux linked against a modern FFmpeg. You can install these wheels by running: - -```bash +Just run: +``` pip install pyav ``` -If you want to use your existing FFmpeg, the source version is available too: +Running the command should install the binary wheel provided. Due to the complexity of the dependencies, pyav is not easy to install from source. If you want to try your luck anyway, you can run: -```bash +``` pip install pyav --no-binary pyav ``` -## Alternative installation methods - -Another way of installing PyAV is via [conda-forge](https://conda-forge.github.io/): +And if you want to build the absolute latest (POSIX only): ```bash -conda install av -c conda-forge -``` +git clone https://github.com/WyattBlue/pyav.git +cd pyav -And if you want to build from the absolute source (for development or testing): - -```bash -git clone https://github.com/WyattBlue/PyAV.git -cd PyAV source scripts/activate.sh - pip install -U -r tests/requirements.txt ./scripts/build-deps - make # optional: make test ``` ---- - -Have fun, and good luck! +## Motivations For a Fork +Unlike [PyAV](https://github.com/PyAV-Org/PyAV) (The original repo), this fork offers the following benefits: + * Wheels for Python 3.12 + * Support for Cython 3 + * Support for FFmpeg 6, and beyond + * Expanded support for different pixel formats + * Being maintained From 54aaeab06c04f89965f0d206086aea72a3a4b1b0 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 15 Oct 2023 01:42:25 -0400 Subject: [PATCH 156/192] Update syntax with pyupgrade --- av/datasets.py | 6 +++--- av/deprecation.py | 6 +++--- docs/api/error_table.py | 5 ++--- docs/conf.py | 15 +++++++-------- docs/development/includes.py | 20 ++++++++++---------- examples/basics/parse.py | 2 +- examples/basics/save_keyframes.py | 2 +- examples/basics/thread_type.py | 4 ++-- scratchpad/audio.py | 2 +- scratchpad/audio_player.py | 2 +- scratchpad/cctx_decode.py | 2 +- scratchpad/decode.py | 8 ++++---- scratchpad/filter_audio.py | 6 +++--- scratchpad/frame_seek_example.py | 14 +++++++------- scratchpad/glproxy.py | 2 +- scratchpad/qtproxy.py | 2 +- scratchpad/resource_use.py | 2 +- scratchpad/second_seek_example.py | 14 +++++++------- scratchpad/seekmany.py | 3 +-- scripts/fetch-vendor.py | 2 +- tests/common.py | 14 +++++++------- tests/test_codec_context.py | 9 +++------ tests/test_deprecation.py | 4 ++-- tests/test_enums.py | 2 +- tests/test_errors.py | 4 ++-- tests/test_python_io.py | 4 ++-- 26 files changed, 75 insertions(+), 81 deletions(-) diff --git a/av/datasets.py b/av/datasets.py index 3324ce0bc..bf610b89b 100644 --- a/av/datasets.py +++ b/av/datasets.py @@ -62,7 +62,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) @@ -72,11 +72,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 1e0cbb317..c89f1a761 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,14 +68,14 @@ 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 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/docs/api/error_table.py b/docs/api/error_table.py index e67b9f40b..2fe029073 100644 --- a/docs/api/error_table.py +++ b/docs/api/error_table.py @@ -1,4 +1,3 @@ - import av @@ -21,8 +20,8 @@ rows.append(( #'{} ({})'.format(enum.tag, code), - '``av.{}``'.format(cls.__name__), - '``av.error.{}``'.format(enum.name), + f'``av.{cls.__name__}``', + f'``av.error.{enum.name}``', enum.strerror, )) diff --git a/docs/conf.py b/docs/conf.py index cc97c4397..bf5fb4b0b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # PyAV documentation build configuration file, created by # sphinx-quickstart on Fri Dec 7 22:13:16 2012. @@ -29,7 +28,7 @@ if sphinx.version_info < (1, 8): - print("Sphinx {} is too old; we require >= 1.8.".format(sphinx.__version__), file=sys.stderr) + print(f"Sphinx {sphinx.__version__} is too old; we require >= 1.8.", file=sys.stderr) exit(1) @@ -72,8 +71,8 @@ master_doc = 'index' # General information about the project. -project = u'PyAV' -copyright = u'2017, Mike Boers' +project = 'PyAV' +copyright = '2017, Mike Boers' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -350,8 +349,8 @@ def makerow(*texts): return row thead += makerow( - '{} Attribute'.format(cls.__name__) if cls else None, - '{} Name'.format(enum.__name__), + f'{cls.__name__} Attribute' if cls else None, + f'{enum.__name__} Name', 'Flag Value', 'Meaning in FFmpeg', ) @@ -371,7 +370,7 @@ def makerow(*texts): continue attr = None - value = '0x{:X}'.format(item.value) + value = f'0x{item.value:X}' doc = item.__doc__ or '-' @@ -420,7 +419,7 @@ def doxylink_create_handler(app, file_name, url_base): parent = parent_map.get(node) 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') if filenode is not None: diff --git a/docs/development/includes.py b/docs/development/includes.py index 8f350a81e..e4cb45e3b 100644 --- a/docs/development/includes.py +++ b/docs/development/includes.py @@ -16,7 +16,7 @@ class Visitor(TreeVisitor): def __init__(self, state=None): - super(Visitor, self).__init__() + super().__init__() self.state = dict(state or {}) self.events = [] @@ -160,14 +160,14 @@ def inspect_member(node, name_prefix=''): anchorfile = node.find('anchorfile').text anchor = node.find('anchor').text - url = '%s/%s#%s' % (doxygen_base, anchorfile, anchor) + url = '{}/{}#{}'.format(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) + sig = '{} {}{}'.format(ret_type, name, arglist) doxygen[name]['sig'] = sig for struct in root.iter('compound'): @@ -190,7 +190,7 @@ def inspect_member(node, name_prefix=''): try: events = extract(path) except Exception as e: - print(" %s in %s" % (e.__class__.__name__, path), file=sys.stderr) + print(" {} in {}".format(e.__class__.__name__, path), file=sys.stderr) print(" %s" % e, file=sys.stderr) continue for event in events: @@ -249,7 +249,7 @@ def inspect_member(node, name_prefix=''): elif event['type'] == 'variable': struct = event.get('struct') if struct: - event['_headline'] = '.. c:member:: %s %s' % (event['vartype'], event['name']) + event['_headline'] = '.. c:member:: {} {}'.format(event['vartype'], event['name']) event['_sort_key'] = 1.1 else: event['_headline'] = '.. c:var:: %s' % event['name'] @@ -258,14 +258,14 @@ def inspect_member(node, name_prefix=''): 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']) + event['_doxygen_url'] = '{}/struct{}.html'.format(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) + name = '{}.{}'.format(event['struct'], name) # Doxygen URLs event.setdefault('_doxygen_url', doxygen.get(name, {}).get('url')) @@ -285,13 +285,13 @@ def inspect_member(node, name_prefix=''): prefix = '.'.join(chunks) + '.' if chunks else '' if ref.get('property'): - ref_pairs.append((ref['property'], ':attr:`%s%s`' % (prefix, ref['property']))) + ref_pairs.append((ref['property'], ':attr:`{}{}`'.format(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('.')))) + ref_pairs.append((name, ':class:`{}{} <{}>`'.format(prefix, name, prefix.rstrip('.')))) else: - ref_pairs.append((name, ':func:`%s%s`' % (prefix, name))) + ref_pairs.append((name, ':func:`{}{}`'.format(prefix, name))) else: continue diff --git a/examples/basics/parse.py b/examples/basics/parse.py index 2d313cb3f..04f4528ef 100644 --- a/examples/basics/parse.py +++ b/examples/basics/parse.py @@ -33,7 +33,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: diff --git a/examples/basics/save_keyframes.py b/examples/basics/save_keyframes.py index 1169c153a..3de25f84a 100644 --- a/examples/basics/save_keyframes.py +++ b/examples/basics/save_keyframes.py @@ -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), + f"night-sky.{frame.pts:04d}.jpg", quality=80, ) diff --git a/examples/basics/thread_type.py b/examples/basics/thread_type.py index 2fa7562c9..51c23a79e 100644 --- a/examples/basics/thread_type.py +++ b/examples/basics/thread_type.py @@ -39,5 +39,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/scratchpad/audio.py b/scratchpad/audio.py index addae0915..4b91a0621 100644 --- a/scratchpad/audio.py +++ b/scratchpad/audio.py @@ -92,7 +92,7 @@ def print_data(frame): try: for frame in frames: ffplay.stdin.write(bytes(frame.planes[0])) - except IOError as e: + except OSError as e: print(e) exit() diff --git a/scratchpad/audio_player.py b/scratchpad/audio_player.py index 8322a3206..32e24be44 100644 --- a/scratchpad/audio_player.py +++ b/scratchpad/audio_player.py @@ -57,7 +57,7 @@ def decode_iter(): 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)) + print('pts: {:.3f}, played: {:.3f}, buffered: {:.3f}'.format(frame.time or 0, us_processed / 1000000.0, us_buffered / 1000000.0)) data = bytes(frame.planes[0]) diff --git a/scratchpad/cctx_decode.py b/scratchpad/cctx_decode.py index bdb6724f4..b1997df0d 100644 --- a/scratchpad/cctx_decode.py +++ b/scratchpad/cctx_decode.py @@ -13,7 +13,7 @@ print(cc) -fh = open('test.mp4', 'r') +fh = open('test.mp4') frame_count = 0 diff --git a/scratchpad/decode.py b/scratchpad/decode.py index edfb413b7..96e5a202a 100644 --- a/scratchpad/decode.py +++ b/scratchpad/decode.py @@ -16,7 +16,7 @@ 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) + return '{:.3f}s ({} or {}/{})'.format(time_base * time, time_base * time, time_base.numerator * time, time_base.denominator) arg_parser = argparse.ArgumentParser() @@ -45,7 +45,7 @@ def format_time(time, time_base): 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('\t\t{}: {!r}'.format(k, v)) print() print(len(container.streams), 'stream(s):') @@ -79,7 +79,7 @@ def format_time(time, time_base): print('\t\tmetadata:') for k, v in sorted(stream.metadata.items()): - print('\t\t\t%s: %r' % (k, v)) + print('\t\t\t{}: {!r}'.format(k, v)) print() @@ -145,7 +145,7 @@ def format_time(time, time_base): proc = subprocess.Popen(cmd, stdin=subprocess.PIPE) try: proc.stdin.write(bytes(frame.planes[0])) - except IOError as e: + except OSError as e: print(e) exit() diff --git a/scratchpad/filter_audio.py b/scratchpad/filter_audio.py index 092aba3ae..5483e62a5 100644 --- a/scratchpad/filter_audio.py +++ b/scratchpad/filter_audio.py @@ -34,7 +34,7 @@ def init_filter_graph(): OUTPUT_SAMPLE_RATE, OUTPUT_CHANNEL_LAYOUT ) - print('Output format: {}'.format(output_format)) + print(f'Output format: {output_format}') # initialize filters filter_chain = [ @@ -52,7 +52,7 @@ def init_filter_graph(): # 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)) + print(f'\t{c} -> {n}') c.link_to(n) # initialize the filter graph @@ -86,7 +86,7 @@ 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())) + print(f'Plane: {i:0d} checksum: {m.hexdigest()}') def main(duration): diff --git a/scratchpad/frame_seek_example.py b/scratchpad/frame_seek_example.py index 25b2d27d0..1f52d456b 100644 --- a/scratchpad/frame_seek_example.py +++ b/scratchpad/frame_seek_example.py @@ -41,7 +41,7 @@ class FrameGrabber(QtCore.QObject): update_frame_range = QtCore.pyqtSignal(object) def __init__(self, parent=None): - super(FrameGrabber, self).__init__(parent) + super().__init__(parent) self.file = None self.stream = None self.frame = None @@ -263,7 +263,7 @@ def set_file(self, path): class DisplayWidget(QtGui.QLabel): def __init__(self, parent=None): - super(DisplayWidget, self).__init__(parent) + super().__init__(parent) #self.setScaledContents(True) self.setMinimumSize(1920 / 10, 1080 / 10) @@ -286,7 +286,7 @@ def setPixmap(self, img, 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)) + super().setPixmap(self.pixmap.scaled(self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) def sizeHint(self): width = self.width() @@ -294,7 +294,7 @@ def sizeHint(self): def resizeEvent(self, event): if self.pixmap: - super(DisplayWidget, self).setPixmap(self.pixmap.scaled(self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) + super().setPixmap(self.pixmap.scaled(self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) def sizeHint(self): return QtCore.QSize(1920 / 2.5, 1080 / 2.5) @@ -307,7 +307,7 @@ class VideoPlayerWidget(QtGui.QWidget): load_file = QtCore.pyqtSignal(object) def __init__(self, parent=None): - super(VideoPlayerWidget, self).__init__(parent) + super().__init__(parent) self.display = DisplayWidget() self.timeline = QtGui.QScrollBar(Qt.Horizontal) self.frame_grabber = FrameGrabber() @@ -378,7 +378,7 @@ def keyPressEvent(self, event): self.timeline.setValue(self.timeline.value() + direction) else: - super(VideoPlayerWidget, self).keyPressEvent(event) + super().keyPressEvent(event) def mousePressEvent(self, event): # clear focus of spinbox @@ -386,7 +386,7 @@ def mousePressEvent(self, event): if focused_widget: focused_widget.clearFocus() - super(VideoPlayerWidget, self).mousePressEvent(event) + super().mousePressEvent(event) def dragEnterEvent(self, event): event.accept() diff --git a/scratchpad/glproxy.py b/scratchpad/glproxy.py index b6054e648..181ff1bac 100644 --- a/scratchpad/glproxy.py +++ b/scratchpad/glproxy.py @@ -14,7 +14,7 @@ '''.strip().split() -class ModuleProxy(object): +class ModuleProxy: def __init__(self, name, module): self.name = name diff --git a/scratchpad/qtproxy.py b/scratchpad/qtproxy.py index a39b76fa2..221c78302 100644 --- a/scratchpad/qtproxy.py +++ b/scratchpad/qtproxy.py @@ -4,7 +4,7 @@ from PyQt4 import QtCore, QtGui, QtOpenGL, QtMultimedia -class QtProxy(object): +class QtProxy: def __init__(self, *modules): self._modules = modules diff --git a/scratchpad/resource_use.py b/scratchpad/resource_use.py index 3387b0d70..930f808f5 100644 --- a/scratchpad/resource_use.py +++ b/scratchpad/resource_use.py @@ -58,4 +58,4 @@ def format_bytes(n): 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))) + print('{} ({})'.format(format_bytes(after.ru_maxrss), format_bytes(after.ru_maxrss - before.ru_maxrss))) diff --git a/scratchpad/second_seek_example.py b/scratchpad/second_seek_example.py index 58b3c3811..4b184c56b 100644 --- a/scratchpad/second_seek_example.py +++ b/scratchpad/second_seek_example.py @@ -41,7 +41,7 @@ class FrameGrabber(QtCore.QObject): update_frame_range = QtCore.pyqtSignal(object, object) def __init__(self, parent=None): - super(FrameGrabber, self).__init__(parent) + super().__init__(parent) self.file = None self.stream = None self.frame = None @@ -325,7 +325,7 @@ def set_file(self, path): class DisplayWidget(QtGui.QLabel): def __init__(self, parent=None): - super(DisplayWidget, self).__init__(parent) + super().__init__(parent) #self.setScaledContents(True) self.setMinimumSize(1920 / 10, 1080 / 10) @@ -348,7 +348,7 @@ def setPixmap(self, img, 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)) + super().setPixmap(self.pixmap.scaled(self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) def sizeHint(self): width = self.width() @@ -356,7 +356,7 @@ def sizeHint(self): def resizeEvent(self, event): if self.pixmap: - super(DisplayWidget, self).setPixmap(self.pixmap.scaled(self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) + super().setPixmap(self.pixmap.scaled(self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) def sizeHint(self): return QtCore.QSize(1920 / 2.5, 1080 / 2.5) @@ -369,7 +369,7 @@ class VideoPlayerWidget(QtGui.QWidget): load_file = QtCore.pyqtSignal(object) def __init__(self, parent=None): - super(VideoPlayerWidget, self).__init__(parent) + super().__init__(parent) self.rate = None @@ -456,7 +456,7 @@ def keyPressEvent(self, event): self.frame_changed(self.frame_control.value() + direction) else: - super(VideoPlayerWidget, self).keyPressEvent(event) + super().keyPressEvent(event) def mousePressEvent(self, event): # clear focus of spinbox @@ -464,7 +464,7 @@ def mousePressEvent(self, event): if focused_widget: focused_widget.clearFocus() - super(VideoPlayerWidget, self).mousePressEvent(event) + super().mousePressEvent(event) def dragEnterEvent(self, event): event.accept() diff --git a/scratchpad/seekmany.py b/scratchpad/seekmany.py index 0b37e5118..a12b88a14 100644 --- a/scratchpad/seekmany.py +++ b/scratchpad/seekmany.py @@ -24,8 +24,7 @@ def iter_frames(): for packet in container.demux(stream): - for frame in packet.decode(): - yield frame + yield from packet.decode() for i in range(steps): diff --git a/scripts/fetch-vendor.py b/scripts/fetch-vendor.py index 3ea3a0c6d..60259ee54 100644 --- a/scripts/fetch-vendor.py +++ b/scripts/fetch-vendor.py @@ -36,7 +36,7 @@ def get_platform(): 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) # create fresh destination directory diff --git a/tests/common.py b/tests/common.py index a49b7bec2..ea3748f2b 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 = [] @@ -142,7 +142,7 @@ def assertNdarraysEqual(self, a, b): msg = "" for equal in it: if not equal: - msg += "- arrays differ at index %s; %s %s\n" % ( + msg += "- arrays differ at index {}; {} {}\n".format( it.multi_index, a[it.multi_index], b[it.multi_index], @@ -173,7 +173,7 @@ def assertIs(self, a, b, msg=None): 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}".format(type(a), id(a), a)) def assertIsNone(self, x, msg=None): if x is not None: @@ -185,16 +185,16 @@ def assertIsNotNone(self, x, msg=None): 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}".format(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}".format(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}".format(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}".format(types, instance)) diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index c94945a54..7a8e4a19b 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -13,8 +13,7 @@ def iter_frames(container, stream): for packet in container.demux(stream): - for frame in packet.decode(): - yield frame + yield from packet.decode() def iter_raw_frames(path, packet_sizes, ctx): @@ -26,15 +25,13 @@ def iter_raw_frames(path, packet_sizes, ctx): assert read_size == size if not read_size: break - for frame in ctx.decode(packet): - yield frame + yield from ctx.decode(packet) while True: try: frames = ctx.decode(None) except EOFError: break - for frame in frames: - yield frame + yield from frames if not frames: break diff --git a/tests/test_deprecation.py b/tests/test_deprecation.py index abdc79f8e..166104c42 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 c22e659fb..6219785d7 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -216,7 +216,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_errors.py b/tests/test_errors.py index 55d969999..56069515e 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -12,7 +12,7 @@ def test_stringify(self): 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(repr(e), f"{cls.__name__}(1, 'foo')") self.assertEqual( traceback.format_exception_only(cls, e)[-1], "{}{}: [Errno 1] foo\n".format( @@ -24,7 +24,7 @@ def test_stringify(self): 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(repr(e), f"{cls.__name__}(1, 'foo', 'bar.txt')") self.assertEqual( traceback.format_exception_only(cls, e)[-1], "{}{}: [Errno 1] foo: 'bar.txt'\n".format( diff --git a/tests/test_python_io.py b/tests/test_python_io.py index 8c341ab2d..8d372a84a 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): @@ -96,7 +96,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)) From a36ab583f1ec163d621800cceb0188d74345ffc7 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 15 Oct 2023 01:50:19 -0400 Subject: [PATCH 157/192] Remove outdated example --- scratchpad/second_seek_example.py | 499 ------------------------------ 1 file changed, 499 deletions(-) delete mode 100644 scratchpad/second_seek_example.py diff --git a/scratchpad/second_seek_example.py b/scratchpad/second_seek_example.py deleted file mode 100644 index 4b184c56b..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().__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().__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().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().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().__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().keyPressEvent(event) - - def mousePressEvent(self, event): - # clear focus of spinbox - focused_widget = QtGui.QApplication.focusWidget() - if focused_widget: - focused_widget.clearFocus() - - super().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_()) From 8e86ab310c158d16d9c2b2fa933cb1f646ced100 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 15 Oct 2023 02:03:24 -0400 Subject: [PATCH 158/192] black tests --- tests/common.py | 8 ++++++-- tests/requirements.txt | 5 +---- 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 | 15 --------------- 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 - 20 files changed, 7 insertions(+), 83 deletions(-) diff --git a/tests/common.py b/tests/common.py index ea3748f2b..e53537471 100644 --- a/tests/common.py +++ b/tests/common.py @@ -173,7 +173,9 @@ def assertIs(self, a, b, msg=None): def assertIsNot(self, a, b, msg=None): if a is b: - self.fail(msg or "both are {!r} at 0x{:x}; {!r}".format(type(a), id(a), a)) + self.fail( + msg or "both are {!r} at 0x{:x}; {!r}".format(type(a), id(a), a) + ) def assertIsNone(self, x, msg=None): if x is not None: @@ -193,7 +195,9 @@ def assertNotIn(self, a, b, msg=None): def assertIsInstance(self, instance, types, msg=None): if not isinstance(instance, types): - self.fail(msg or "not an instance of {!r}; {!r}".format(types, instance)) + self.fail( + msg or "not an instance of {!r}; {!r}".format(types, instance) + ) def assertNotIsInstance(self, instance, types, msg=None): if isinstance(instance, types): diff --git a/tests/requirements.txt b/tests/requirements.txt index 2a321a28d..d3e3ad32c 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,8 +1,5 @@ -autopep8 Cython -editorconfig flake8 isort numpy -Pillow -sphinx < 4.4 +Pillow \ No newline at end of file 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 7a8e4a19b..946f26cc8 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -117,7 +117,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")) @@ -125,7 +124,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): @@ -134,7 +132,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 = [] @@ -159,7 +156,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: @@ -184,7 +180,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) @@ -246,7 +241,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: @@ -277,9 +271,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 @@ -323,7 +315,6 @@ def test_encoding_mp2(self): self.audio_encoding("mp2") def audio_encoding(self, codec_name): - try: codec = Codec(codec_name, "w") except UnknownCodecError: @@ -358,7 +349,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 166104c42..f8857ab73 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: - 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 c2144eab1..07cfdd574 100644 --- a/tests/test_doctests.py +++ b/tests/test_doctests.py @@ -7,9 +7,7 @@ def fix_doctests(suite): - for case in suite._tests: - # Add some more flags. case._dt_optionflags = ( (case._dt_optionflags or 0) @@ -24,14 +22,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 6219785d7..bc8385f5e 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 56069515e..088838625 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..2514693ac 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -15,15 +15,6 @@ 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 - :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 frame.pts = frame_num * frame_size @@ -50,7 +41,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 +52,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 +59,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 +81,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 +92,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 +103,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 8d372a84a..209f7268f 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 77a5c215b..0d36616e1 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -508,7 +508,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 8f9435c5f6a45336d598567004fa5bf3c417fd06 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 15 Oct 2023 02:12:23 -0400 Subject: [PATCH 159/192] Remove unused scripts --- scratchpad/dump_format.py | 10 ---- scripts/build-debug-python | 33 ---------- scripts/clean-branches | 120 ------------------------------------- 3 files changed, 163 deletions(-) delete mode 100644 scratchpad/dump_format.py delete mode 100755 scripts/build-debug-python delete mode 100755 scripts/clean-branches 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/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) - - - From c6468774255ec22ce2a273414403977609f3cbe0 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 15 Oct 2023 02:16:51 -0400 Subject: [PATCH 160/192] Remove docs, (we didn't build these) --- .github/workflows/tests.yml | 12 - .gitignore | 3 - Makefile | 19 +- docs/Makefile | 40 -- docs/_static/custom.css | 14 - docs/_static/examples/numpy/barcode.jpg | Bin 14963 -> 0 bytes docs/_static/favicon.png | Bin 2089 -> 0 bytes docs/_static/logo-250.png | Bin 12552 -> 0 bytes docs/_themes/pyav/layout.html | 12 - docs/_themes/pyav/theme.conf | 2 - docs/api/_globals.rst | 5 - docs/api/audio.rst | 72 ---- docs/api/buffer.rst | 8 - docs/api/codec.rst | 135 ------- docs/api/container.rst | 79 ---- docs/api/enum.rst | 24 -- docs/api/error.rst | 85 ---- docs/api/error_table.py | 36 -- docs/api/filter.rst | 34 -- docs/api/frame.rst | 8 - docs/api/packet.rst | 8 - docs/api/plane.rst | 8 - docs/api/sidedata.rst | 20 - docs/api/stream.rst | 119 ------ docs/api/subtitles.rst | 28 -- docs/api/time.rst | 92 ----- docs/api/utils.rst | 18 - docs/api/video.rst | 119 ------ docs/conf.py | 497 ------------------------ docs/cookbook/basics.rst | 42 -- docs/cookbook/numpy.rst | 24 -- docs/development/changelog.rst | 6 - docs/development/contributors.rst | 3 - docs/development/hacking.rst | 6 - docs/development/includes.py | 365 ----------------- docs/development/includes.rst | 2 - docs/development/license.rst | 12 - docs/generate-tagfile | 54 --- docs/index.rst | 105 ----- docs/overview/caveats.rst | 57 --- docs/overview/installation.rst | 146 ------- 41 files changed, 1 insertion(+), 2318 deletions(-) delete mode 100644 docs/Makefile delete mode 100644 docs/_static/custom.css delete mode 100644 docs/_static/examples/numpy/barcode.jpg delete mode 100644 docs/_static/favicon.png delete mode 100644 docs/_static/logo-250.png delete mode 100644 docs/_themes/pyav/layout.html delete mode 100644 docs/_themes/pyav/theme.conf delete mode 100644 docs/api/_globals.rst delete mode 100644 docs/api/audio.rst delete mode 100644 docs/api/buffer.rst delete mode 100644 docs/api/codec.rst delete mode 100644 docs/api/container.rst delete mode 100644 docs/api/enum.rst delete mode 100644 docs/api/error.rst delete mode 100644 docs/api/error_table.py delete mode 100644 docs/api/filter.rst delete mode 100644 docs/api/frame.rst delete mode 100644 docs/api/packet.rst delete mode 100644 docs/api/plane.rst delete mode 100644 docs/api/sidedata.rst delete mode 100644 docs/api/stream.rst delete mode 100644 docs/api/subtitles.rst delete mode 100644 docs/api/time.rst delete mode 100644 docs/api/utils.rst delete mode 100644 docs/api/video.rst delete mode 100644 docs/conf.py delete mode 100644 docs/cookbook/basics.rst delete mode 100644 docs/cookbook/numpy.rst delete mode 100644 docs/development/changelog.rst delete mode 100644 docs/development/contributors.rst delete mode 100644 docs/development/hacking.rst delete mode 100644 docs/development/includes.py delete mode 100644 docs/development/includes.rst delete mode 100644 docs/development/license.rst delete mode 100755 docs/generate-tagfile delete mode 100644 docs/index.rst delete mode 100644 docs/overview/caveats.rst delete mode 100644 docs/overview/installation.rst diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 48e53eb80..58a177e99 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -104,18 +104,6 @@ jobs: 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 }} - scripts/test doctest - - name: Examples if: matrix.config.extras run: | diff --git a/.gitignore b/.gitignore index bc6c09077..3fb27a301 100644 --- a/.gitignore +++ b/.gitignore @@ -26,14 +26,11 @@ /av/**/*.pyd /build /dist -/docs/_build /ipch /msvc-projects /src # Testing. -*.spyderproject -.idea /.vagrant /sandbox /tests/assets diff --git a/Makefile b/Makefile index 6cb954dea..76b4fee6c 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,6 @@ cythonize: $(PYTHON) setup.py cythonize - wheel: build-mingw32 $(PYTHON) setup.py bdist_wheel @@ -28,7 +27,6 @@ build-mingw32: mv *.pyd av - fate-suite: # Grab ALL of the samples from the ffmpeg site. rsync -vrltLW rsync://fate-suite.ffmpeg.org/fate-suite/ tests/assets/fate-suite/ @@ -41,7 +39,6 @@ test: $(PYTHON) setup.py test - vagrant: vagrant box list | grep -q precise32 || vagrant box add precise32 http://files.vagrantup.com/precise32.box @@ -49,7 +46,6 @@ 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 @@ -61,14 +57,6 @@ tmp/Doxyfile: tmp/ffmpeg-git 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 - - - clean-build: - rm -rf build @@ -81,10 +69,5 @@ clean-sandbox: 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 +clean-all: clean-build clean-sandbox clean-src diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index 65a1e540e..000000000 --- a/docs/Makefile +++ /dev/null @@ -1,40 +0,0 @@ - -SPHINXOPTS = -SPHINXBUILD = sphinx-build -BUILDDIR = _build - -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(SPHINXOPTS) . - -.PHONY: clean html open upload default - -default: html - - -TAGFILE := _build/doxygen/tagfile.xml -$(TAGFILE) : - ./generate-tagfile -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)/* - -html: $(RENDERED) $(TAGFILE) - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - -test: - PYAV_SKIP_DOXYLINK=1 $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - -open: - open _build/html/index.html - -upload: - rsync -avxP --delete _build/html/ pyav.org:/srv/pyav.org/www/httpdocs/docs/develop/ - diff --git a/docs/_static/custom.css b/docs/_static/custom.css deleted file mode 100644 index 1069c8f1d..000000000 --- a/docs/_static/custom.css +++ /dev/null @@ -1,14 +0,0 @@ - -.ffmpeg-quicklink { - float: right; - clear: right; - margin: 0; -} - -.ffmpeg-quicklink:before { - content: "["; -} - -.ffmpeg-quicklink:after { - content: "]"; -} diff --git a/docs/_static/examples/numpy/barcode.jpg b/docs/_static/examples/numpy/barcode.jpg deleted file mode 100644 index 4184c10f76563f9fa97da8fb55c3fc9b8ac4c770..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14963 zcmbWdc|4R|_&+|9^e9PA|s}FP_`jtiz$pXq)e7# zD#?;{j3rsJ4p}B<%rs{E-aXIf`~ALNzrTLJ-*~ytxzD-IIoEa0xvuMdo%;w!g+D-h zt;{UVKq4X_kO=Sx5`G7nfW$<%Z{IE|20X;X#CC}9-YE|Jq;~E4Yqzx2zJ1bC($f16 zC?4DomY0#1mQ|6JS5ksNAo~xh9#U04q^Jy0{?myF&{TYf_@13R_b7v7Aes(Z5ASR6*OMMMR}Vgr7kWfKM@ze}AC=`H5^3-41ZM^Dl{A00C()Xq$+r=r(|V zKmdR`5~v4lmloTnqI-VFe$;hw)gbVRhiNZ%su|aQl(8T9bx6yKO_AkqKC0-pZ~_WzJe8jx!nuq@lf|Hvh>Ee!YR9_|Dir@g5C_?McV{Vy4hI|C924#B1mum2J4U&;R82^RhTOS1nX*#DJl z476KR1Xw&#X%GUm{upuS&zCJ`u+vp`iq;9cGVL41}QM-7z@3p|ie|`9jc_%py;X^OVHh)qAn%Xeq1L zDxPD{UY*n(t$c-*2*Sty2&r`Ufic8)F`pMFeA1oYBktmU)N?JNL4{A*T(}d4~B@=nG%;MmrnxMTI}$XcJNcfeD07E5=Ia0^SM30U`|TMlKZ^K<9a+}9E$be>mk zGcb;>GL>sQ(U61-LDk}#v~Cq97gnihsBSAFWT5qvC&Vs~LSMiGGBI-cPMStyurEep zt{cCX%(GS;Q~u8A0_E zj*7W!V(n$yiI3$PZI|b!de0`Rvk~r!x3jxO&oH3q<92rH;M@YPzr-VmQ@5J_@ zpX_Zp{80!ZAo&I5Q(BL)xCkN0_+({}0tBom#R-9I$=El?zMtYQ|0$kcjMNDhy($Ey zx{c$)fD)OmXCpo}=aaQO|5duojq;KdzpwT=JJPRL#)4KY>&FsvoXh8Px8Du)A*r_( zWCV*PaE&S~`Z>qiaZC0q@XVt@gu@MVKGY2(H!rDzc1-e4ZL^#9s2;R@TOAIkwjfCd zcY#^*A-;BWw%?Bjvuo6}b5Q|^xWwqk`SbF3KKcEwhi7co!`$u@MZ}D*Ez~@cJbk|B zNJb%QugKUlmC70Uv$;-MZ7MP<2{qyFR=eG;v=8WYhqOXSzS;3*B^>LYnhnmqzP*EO zs^%LhM=lhu8bQ?@Bn<2uR_vuARw(H{tCnq)2YObs7SBE4ad?&HBPM=>4^U&o*Hl!& zvB#_Shs)coT8NR*pJwo-@;f)m3LlVQzgMF?nTW1xMHcw)n}ipFON}{>raEsTP^{Zp zosPwt)WQ81l#TILsC^O{p9L8OHZ7T!FD|pBWk5XKi{i-Wt43NEqTNF1e94oma|~># zjFWL)T%agvo5|=Uf)7^g9d<}V+U0_`8PVsa&fwPInCP;J@$L7l3fo;z&&j>dq&2G? ztFN+9gr>oNRV{5{4NW^H4oXNmXIbxivH%Dea&AD3zRwivp) z%&Va21Z=a-NYMp_xDJvVFWC}4P|KPa@=Fv!8nI$di! zJfr0@vr-|8Y9HW^y7cJg1G7j?>HG;vJ7;v*{UI-X^EW9OH(BMUg3N4BD9F9ZHI63s z!Q=}A&d*ag9>k6!yp@N_M%y-oU)I@>;?9M{N7wLUsh{u75FKF$mf8#-$|as#x_I@>S6z^LM$#k!@e5R`@B7nDtzawp?|lbLF+gM~uSr%QH- z`92}Yr^2bDB@9W&Et<82mqcpio(bG*Dg^C&+=AvIw)R$AvfQMjgfk)F6!DcI6^#hTmR$7D%M*Czz?d{Ux`U@O67QX>~!rP{KuBi>;*CBw&gz)Tr26p`cN3)ZT)SbV`&W3zG(<^E z@ihcygFnjNLDZ}^SzZVQ?b-hC1C?Po=bydCam*3qY1ZDP`A?7 zqr8CpR-*Qz_`n;i{P0+$=A}U7a5;2Wt~!d8)lVVSWnVSKxlncMpYxWks>N>MT#B=U z)nKlcb2?uqbB8x>aY@yx{7?KyjNI{NG5h^bbS3Lbe`899Z}JCS4@r-rkvRpR3fr6( zw8Ba{RvzA|Z6H6S>h6cR+)UOQu*@Ne>a0*AsBz-T%|cM-72Tdyj(uJ5EJNU6W%KmK zXwPUXmwLV3yE5)fxrD7lcc-6G!h=r!aIrdajpXuObF-vA*H9kwTIrpPwI%;c$vz=y z-0yYI-vasM;&j53D!o%4y9sOZqPW+7CWi>)uumJfU}mLF=i~QSdF1hcMa(Wse%JM> zcWXoAJx{`)g}~z6Jvr11aQLdqK3r&ir`IH=&xAvfLy~D1Z>1wbqhJ3iJVhInV;h+NAJ+lDioC_g*RpLKW?G?s0RMFkza&?>;nWAg9urz>3ZxOJ31{#clF zcqxQI$@YOvRorwFf_9cjs|ch`KfI$Q@r9t_)IPz~@}*EK4&BOJX;8Ife)LnJBWfOqtm-`6XQw}CQnsqe0Qfh-b?XAY6Y5@ zw%T)Aa42(D%vM6*MyX&`BkfKEs#`uQnO9Kvr8z?gl0kD%4j4)5j8@}4&5!%p1v~@F zVB-)v)%epK3r1i08Z$c3eE{4in9wLdkgZ^i&CprvEJ|Xnb9|oyzQ>v-MVC*-JPA09 z7J}ZmQxad*y7#hS;iZ!=m5e?tS+dlX>#ihxzKUtbp8diukLvmnRQs6W}i=OEAF&@}=RuuSDsCR}(#2ANco<*Bfg{MtE&pMB{uYfz9BVrjkCq? z`}I6b)fN3y%X#&|&rhj7lg}ENh)j{7Ph#Tqgfx#NNQ)LC&q!+CPUKC$;F4J9wl+SU zlEpR|*(m!Zh~Fqy0XBrzwz-FeKr69X;9(la=9TZEqwRhev9GZ!2XXD+3Sl(?w&oT$qWX4+PJIee>kAdE0R=^|Ed_ zsXz87meEu*0U5^owXdYP1}fQtBK_LG^5=^caPOhqi>wRVBQfrVPBqA7`&xID#maFq za@m|D<)r~PUZ2cud&?|8+d8K$j`g(qcn|HcYVu8bEzruguOK?3M}sX1>(RfLG{}l( zox98zQ>U>?a;o(N6{$VJLeSY*cu<7yH{5xpCqV`YW`V5e8E;p`9&QM+r>3-g-DdYw@ z?v2gJ)C8;#h+@J_9KCyjh@Z_JN+s3z54CyywYfWp&_UrB*(;ONDlX{XlvExkkgY;o z-Ra)1#fHnjBQgzFbIEMn`k2=^RnSJ-Bnm#iVZJ0a4~zf8&DO0)zQpw%6@ors5*F~R zV+6LT2J2WU`#@RZs;A%{gu75szg2%vYTm|4TXNr^AiPS0rBnlX=ic?gnvZO5EZxO7 zPXgM`l^WLr^7wP?l_YjYw(-iik?TUx(__}G<;nH>dD9O*Eb!%N^hSyPKl#xDhtthX zeNc^gYt~mhho&=c+GXP^EY@)g${_;V~QU=;#p_I zxzQPzzko#@uFerz)tR@F#aeSv-G*TD>vY7=5e8B4)h$R~;8DFp4q%zU?lHXGzAm`L ziSWU4wLGhujrRSO3#?06uHK zvq%6afl7paa8Hpoe7YoGG1O{lx9;qkpe`~W5LOTQZ>5Gg;Ja~Nr+AE$gONFr2N|%! zZ;|^Mu!sxFJq3>s*$P2_dGC_?y&(jB`$=N@^m1DPET&$SMMD0zMt0+7zYg-7Y9jq1 z`G9#k7|6w*5DSAicXltdQu;M_4rmVj$o0=GP)3?C^J~N?5{jXQ07%rY;4;F$-0y$15tPsI3UfFST{xrajc~@}3WnhEbM_r2B4{W7k zUS-jSCqPiX8?`0KXnsZAgL!ciSDwC6#)|{!PDD8j)&fMsoGfZ6pnxN0LV-h_UsR;QtH&k?TtvLbDxSn#ER{%)h1yaL$0 zzCbE2StS}Ut2KP{-$O60*EFQm(&S;2$wJWm{*cG!o3ToqG!`xc;k$Z@RP*FAA4sPNPIQ6Xpw zM)pX>0_n+-L~D1>Z}+Ze$Br3}*UY;dMdcNy7_PjQ5`w(*<{AP8pK-~dIL|jgADO&z zc$??DJR+}RSTp{-V%XCkcZHyK^WT)cKO$#2QtQ=UR}7-gG`VvZQkS*@BQgOl{GJ{Q zRhjNRdqAA;8D+xr)sE|SR}gRN0FTz*^@o?_k|m?H{TmfqV=Ks^UIdP>AJ~CQ z)&{1;==%m__DUctAZ>CQq2<2Pqws!-gZ`K9eXluoRsdH32+my>f)L_Npxb5K8<-tK zV;;b_&oQ|W9ur9+43Bt3z2rWtc(r;Phg>I$m(qdI^Y@4&tVf)?v}KR;_Eo$SzfrX8 z|EdKb>va`G;0JlWUxD@bBp}RQu^c=L2XRZ+!Gv#)7e zgOCkG2pkSxuq8uHMzrP#L6;EpT8o?a^%3)U!3A*3+jqMB%j`3mCx9-y-P!#`Cw}0F zCV&D-EVU5;wWq}kOu=y@tq-J_I9{Up@Q62}5>7i|GsR$t^O;a{0wCR9+>~*XN#OK= z5TxBI5Q0#1zas>ZSwPMnm<@;{s?CNcZlKt`T_7D#zp5vFac} zBiU;Fa!B>?AIfi~oGl;UV?q$U$a*=-WQ&{=eQXU!dn52Dc8z$3t4?RgyrC^kF}_R* zwqWZ)*HctufJ~%+2fSwC`k_--J)>Ce-It<|5I9R33rDU59KHYL^KF4Qft80?JI~35 zljabWoNTW4IqvEfIx_(G9(TCPv7xSgB7U=kUpOMj23(>qyt;eEZI+IktrURiOv1kz zndd%l+nvcSCMU`4B3?4Y<@{zS3LR8nhx@Uu*zs9t9}na}FqjfxHRdoE`hnHE!7Go% zh!1^Rblv;A6bK)(bod2D?~~6Cd=TjHca9uyYAG3`=PzgqL1y0{?18f>m6OZzX&F&m z?gXBl9KOHoUA}H(&CH`L?hYErXATG)+HQZ6PKmBr#+DGZGf&7h7)C(o5(}oiFMz#| zpLY2?VB--jfn71-Ww^djURiy1aJZ8E=g0%kRN+ItyCzHo5%u%t-$}c1HGv%iUl$Z< zHbBK8UC3kzmq2wo#^MS=oHgq2ySs#-$tX?>ja*ig-hHMw!$d%**;zyp^6yNI9Tb8X z-^ealzf=EZ&YW!>Q@R+>pXz<~Dv8Bg&Xms-;6_d@Bd=I-5zF5JxPs8-npAAyCU{G` z)%b+o#?lNM-ub(MTdm#TuHk zR77MgnF1*-FKLay&EuJIyrnzv>E4vYmDwSjf!M1}_diI!-W}kkH7X>sNeB`|DKLts zwP$H&TvsUv-jZ0w7SN9*4u{`dMrqaS*$Tnko?ZXW z-BkW*zk|PppsQ2)8g`Lg=y=a!?g^j^WZXm;BqwT#zT%%_5Gw@n*ob_CV`khSK8IU1 zHA-zluQ{DHRFZvokCVkH4TtjQdCS>?eqg);8l#A7%oaotdP_&CYY5DqcJ#ak8^EyA zrNFb=d1?}1KbSMXTAt#pOyv+){KAfmnbZz{F2}btM@A+eYH`jM{E8gl`#((qrYb_Y zrR7Ue?#AzKz$Y)RMJ}@TuOa-AwZq=JLeMcW5g?W>Aa&$bPH}!tAcOr?m>V^ zc}yv#vG@`0tIMI>zstFklLedYMm_95)fDTX$ba~ZkWDzrS_(eH>R$4o5Uc z6=Zfsmj2o9J)B%5$$d_+*@PY3digGXSY`C5%e}}>mt3!Ajq)l-rPSrmi=T(d42sp+??}$#X0LcNyXY>x+4q&eJK%kU z(%z?E1z01M+;#yL!Qmarp9rU0*wUK)fa<{g%Iz1_0Y_f&B#Jz3O&i7aIq`9y8f%z+ zCT2QDk}j>`sq5CY6wVe7U~+^9CHM~3%rRRli&ETRHX$ znA04odF0Q5wYJT6UfG2Igdt@87vM%ZOY&9?1rcQahClc_96rs9g?%TprIyTxiYDSm zAR9$Kt6QPCkxl?4<(iQP$$(~a7iwF2zj5O?z{3y8jaw4%sy!?4MxA-H5msD2;8phl z=mR`hwgey|{9_y)FKG7MsUQd^bf9g-McFta#34Qu;+C~hbe*Jx5Jag1fCf1Io?12f z?}d!xW805II14x}q?#Z{kP+#qG#Yuwy(^hW1<(lRcb!snVtrc>ne5u2q78F2+Gy#k-HtfzALvk5^n$)~u2r8N2NCJM-{rR6E zT9;q1B)hLhAULZU{4D^=4)AMz0Hat8U;=l3=8au}TQ{~O++PFWSEnDf=^S=_^Bc=7 zi`7HmZKezOIM$^i7XXeIy#B*p*J}ZQj%@IAnp_RYPapv}iXZX$Ca>mNH#zklpoa*w z^7)MzV1Prw0Omqa!pj#JM*SLo4m#z9V0;!B<10SEo8cW4gZUD~u%!?b1swXzSn}Wf z?=}E)2`>kIg9OC_3)s`yxayiCS$0EL{;gviz$A8Ki?Xr{`7^Kqjk7vgW4e1WVt1z&^>2hFPxNY-AM*im3~bLm@S6mYO7Wb^xv#?I70Wk*t3-#P8BzpwKv<(n;&2CP89j+tH0}~zwH%EAe4@p zV?r`Dbc;RT7D!d;aZfu=qLsww&wWdY*(kre3w4}er=U7FrR|6n!NT-A%&V+ z5$^fWdMvZ!dj6@&bS{imR<%*SsJSy|QIl0PcZN2YVvrEX3ckj;vj5P%MX0`c%!i_} zc$H|_0;^OzcT_Q9a+fmZqvwZqBEnZOPMqZ*fu0%MgPh=2@se+4;-3u`TCgE(S8{D> zZ%k%3;#;$UJrLR8O{Zo&MTLDJa-%+Bj#>5hK_Recg9+o40Qh?ml*CXNUYb zuEG7Hn$qFit#hMMC{E;&o8vBtUaI$iCBGj*y>r!N`_>?=A`4ll6 z^V>nyhQ9I<-=O6-|O(99|37>EB0(Ug!hb=c5f@EH=R6r)9i; z4m)#ufbg#5ZKY2-7-@DV&LEIA|5m(nYxq&GjStI@ zVSDA|DP}q1?P?>(oMr=@U*x35Fsfv_^Q2bhK5#I53SuCdM1yfz$ltuBs;buZ4st$9 zGLZ$IV4JEr;uBULhis1rx2X(@D@tuZ6KA%6oQ9UV+wIxUO916Fhh%4RLx$QX z%H~D}lWvwk_G&^ta?sM6gIPfwL$s?MWV|f?(WO(rJe4zs*Cm9YT8_QkH4J$r!K7rj zRK_K>&*yMq8zqE&+Tx<6M37BLZ97=esx9A$*?G>@N= zjnJ$}oC$azmGN=L5%eP}`hhm?h7Lpr>%Kzl<(V4tHQ(igwSNf#SgQ8!Bizf~CmnH?t0$ZWsJjYRF z-E<8R->mk#fb*t{VcLH=Uy2pfHv!26`d_5zStT?Vlw= zFm5=>U{wH|VAEHt_&=-9Ef~byA+HR`d|I$OxVFVjSb&dBlBg!YBc}L%R5@*PBrBuu@2HR|lKL8c>9%172Aonv>#vHF;^#KlauIA&6vaYspqt0PcsZ95hTXXYzmZV$HUIt8N8C zQ1y{`gQS9F8-pbB1PrK-Bg+A?5{_O0A8+_CGwq7X>mS_n!pPwP-F^$s2otbt%K~)N z^dOV?CWHFnhPzaaO**Ql%uOze{cFoXMX$jhK!CKeKJF0YI~qSF#r@e392vx4=b+&e ze-ej67sL4Xax)I?TxzGzY2;)n(j7Zpur({d0$P+zuRhIRT3U5cw>HNHN~MzDUu`KY zjOa_9r!w4h$qgG17)W=&$MkAI<*NPaI(*mU8EK4TW`Vbn-OWeh`~h4?tEAqFfa8YA zG?#L@x-iLzJ|J+cx@Y9XsFs~V5;k8)dFKW$0P^>7a!G3XDL*FhEI{PoN$oRgWSrkL zeBu-k1@y*|^SE@*Q2CRY(2{ke;+{xM$$GdTt|2B;hn<@n=+(C_PdwpfH+NMrt@7Kb8vssE|S1rL`)@ioHL=2>$M@3jL<5IVV3wzT-dTEuNa}ajeK`P?{<*TG~Ea zbsKj}SqRF9+!BI(hboE< z3gVtG&3N9&#%rlZBaiOZNyW2!{-)+3aGOOr;ztY~YFx#f?KK!V$uFMB+kEb)1AG-2 z=#I(Em&@W;Adlm)jZ>=!1ngytlLvqXd%73zPhoeHbzErVI%E}O?AWsT37MS!zwQ59lYwgX2t`r*5&IPeu&fuB}}FE+!0{e@BcF4@NQFj$)nP88+p< z2oAa5+d}Rh%#6c27{)34Al(=I=gyFf1onpCCt=YGd+a+JVPp(qZsZ%bmSS-GBB|{UVCk#o{e)ku=IKyCspN$h*d3cO`}xI z)!B@g4{nx35}UQQO(DDT-tlJYY@R;u`HHsv>iztv9wpn^hv?#c>p%-tKqdI4k5vY^ zoI2+4`Lb)gMp^0OVxylGC#VUX%a=QIO%H6xFIy=!|2zpzsoR7uJ6q;U8^2L1tvabN z51v6|Xw1SBd8S@-euZ-284CB`hD5W)(^dOb3iay0$|!j5DEY>%6~MmO890XOfm^61!W@~X@kEfZ7Tp@gFk@KHbpj_7Cw$g& zwU6xBvZW1Yb*6Jm(mQ}h{-;lY@VdOdD#MixENTuxJh)48G7=L;aB)|8mwT2FY&Y|2 zO1BwvztwtP3VXJ8$@J5w@HqT&%BpgP=NO<$NZm4q0wqcQxbMOqR9HPxp+{X(78We7 z8E~dF0AAPge&B72w>FB~SF>!!_z8W7(H{f|x!;GASC?&$m`!2N#AHOVHeun(%Qo$e zMWWP!ZpG5Fqm>0DyKpC5fDqI@*smj3F(@`QMP{62_-WBz<<)BB>JC3VzFBr#my&6& zbi{&qUB8nzO}C~+$Ul+@H7e`J;`c|`KW;96AO$Fusr>Hr-jhe~2$q_LqKqn>yEx{{ ze61^SadmYCH&kvcq~B)3cH?pc!ItD@%LXi}0V@$E^ME8lJH{+NsyCMAc7MvM^6?8| ziWF?3otI!16w`COVY5nlcaUc;MgunZcgaDyB^Nby>vS`}W3&AO3sTq@`^(wo_7yLm zQOk9jFC?p8cJnAkwK-kBw`_kXX1H-K#Q5_l$5uM&m|nL65~h}1l-oA`%{ey}9VUep z-OVgN=Ir>z5cmqJ2UUWe(jls(Y`2fr9m|!)g`gkGE?E?dWsPOZg++;T?Ds)0R?`6= zp=Bz0RI>+qqv#H|U-vJgWI5OYWoBNiDEI)Aj_Ps+;^XWpXzq9%Ek+{%r%TE&M1_s)?LtTggb9-p=HV0SA#a^6SSA9Qyq z&XIhnmgG;8+zK0mj|h6ynVT|5@!{we=AGEDg%Lk7B2==AMncb0QiY&*$P6cKelgniLq#|X98lVI@R(c% ztAqo@0S@){|GtMOE^3aKzN&cFymX2Sz_epGBFnnC4)&AGz`{?BKJ!d)HuAhf{Xk28 z_JP|i(MdW)M}{Ut^Uby|2aZDteClf^g%_?Ja2gsB8515xS%}&d>VB!QZfLh2m&Q#jaa5P@eJ->odV$X}f-dU1n z#t`KA7Ui*2m2Y{xZbUb%5G}B^-$!{2y(6xB+XB}~WFSkIFMZBvxuF=dpdH>abm)#? zaY%CWXdw4Z$Flv8ld5QqX0dj5vg0wj<-1A3-<(9(k>TW7>d{m7Z(5$$H#Xn5V^e0&&r^|F zFKxd{m|>}9UzZ(3BL;^m4kadPAJH}d3h0-H8kUhx?3ai47w*#za8tHDdQ+cW@lJs5 z*(n6+xO9I0X7R2mD~wXkiKhIME;B~ zz`gfn^cv(E4Ksc0*3-0_6r|cji>du*Ug2n6Pn{kPWP>0-~(&EEE}Uj zm^I!7c^sS2drBQ&S5HECwXaGS9%9o`U!e?~M6ouKbi$IMag{oB>^#-*nrHGwRd!5~_>-hXp zuvgC+W?8Pity#C$iSQ~gDlh#oMqxO&H_}gae9#h3!pJr5TZ7jnwL|#WM+-3y9^tV^ zF}orLp^}FCMpwV2))p5ZTfJm`&9dZ)Z)eF9OrgSVd6UiJ&f+r_hZBDfBCpm-+qN5F z3Y{BIFWVm48mOO4O~CGJ8;`^gdt|}?NO(OAwml(_##>?9iMqQM>92M2?S$9>yWN<Xv;{dj)&xtUcwGTQ;Jh2-j>Z zQtFYoXYO5_MT)+B&F{g%HTkFULBRd)_m{jV2f!-_)pu+5n2^U(8<~Y4@7(0}OKB8L zl0<@S_ubt>&&zan&Q|G#HJ`hhN6sbfbJaN3=i-05$JlK!j4@n(A+D31aJH-F{Z_)2 z$xXN7@z;mWl>Vjl9Jc9acUawZ-@4spq+glr;Ly8aJN1DyukKOU(W(^Ej_DZVQ!Y7SCa5vYGghoU=*34d?_yQ_Le*#H2GVUOESvHl-waaEOV2f?PK*!M9>Xt0WAe&( zp=(b;(k*QD?yCE~syoH zZOn1ZmnVr2#^rUbEp4{pKRrnRxlz26&sgMriOpjp4z<$j*Y`6XqI}oO14rj?qn4vgvzwZ zZk8rt2TZM@Qer{uSR<=hx`bw#@330^-%92*Jngp9llZqD-lr1VwDZ?d$w$QyJ~U>e}gMVZ1L-?ZUUAXQ*=t!fkuNH1E9odMd}7)Pn5lU+1R{^nt%0J?2bU!u(cs=YbN zb^mY&OpotQ&@%<^JSAaEMV;I6(*~G9Pyo9qtQm~DCP~eUCxI_+3jhZcCTv$In=l1~ sa*AL8oo^3N?42}V3Id}{K`OxVJwTa)owubYXg(L4d6J+hD*X1p0LMMnW&i*H diff --git a/docs/_static/favicon.png b/docs/_static/favicon.png deleted file mode 100644 index 95ab33c6d6d5347c0a69a3fc60e1bff49c4e9520..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2089 zcmV+^2-f$BP)004R= z004l4008;_004mL004C`008P>0026e000+nl3&F}000NXNklQ;N9$*y|p;{hU%(PNA)HE-t8KxpCUQ3@W%B@|ua&^rpGQ<4#4}ah|4yb$nnmNDU%;)=_bKY~_S0V@kFU{EmFbWt9 zDCpSk0xJQNlivgQ2nclX8wntYpKZV)VCTs-Q;>_!7l3tuFHdHHK%A3r8StK?Ln;wy zv?;LLC3q^VZ3=*~Kq+uaDmwN)1svcVV2Z={P0nzItOcV*jol_o@szbJs%-QWC%$T9qGNRdoY1;@1`gKX)tNE1)=IV#N6iMvMA}LmGAiZ8b0*Faxnb zi&!wvcOXCmOa%Vzyz;^-U;LE~u?G^?0k-4&NKp!B>8GfTihgxnOcOX9bMCZ9d!_=JZ`0pSen_Y|6)| zPo~ma>X?Fsz>Id_F9UVJ4d;TpRjWgaWLDPXid z*DAb^elzUYkc45!_h8bEpjiPDp(mgN#Q;8Eeb6S@ioY=81=ud7zBfjOB0*Q-#9xI0Y z5%`@tEzLIhM;UP&QhIhTsG~vUj>7uo%&k+G?Y`Elth?T%E->oPEQ`$wWxqpB4T;#mbzkW}(W<2-YT)`gI$MJE1# zR8%&omhT+*LsE3ax!crfyW(ltuyDuZ_g0^az8U6U?tjUkFPi^SUXP@)hrh+Fo<*bk zW8se#Ad}juY*0~QQGdIA?3Sede?DD6o%UH&^;OAMt;0{v3)c|{P0Wztbszy(Y7WMiaW4F9NsL#nOn6)9OEI%$zkDIhSr++~& zKSNNyL4SAdUoHoH9+$lYi*g|i9?ozp&;vNzkpP7=yrxmf9kbVl)REg02S%N^gxM<; zt$EMVjJGE)J^4yrn16XlzCmwHjJ(oi>4bxeuy`!QqI9)#tvKYOoc!*NozSffE)l3{ zQc`O6O4~Ae+q7Z*j+~~}I}D9^@5+5+M=d@1>K|c#hVXoYzGix4L8q_d_b1Wl{!c8P z@ZiMLYC0eWNO!S#`y&SunM9!Wo;xKqy6x-V+!ZIn``}fRy)tvm$iMv-dn(k|`21Cq zcm2EJMYc8to>_{3j;P6q* z-hI%Re@n?4J!H|*m}9;D4bcTA@A?VhMcU+qeY3I3zot&*3LYws?qAx0rvOXZ1Qb>K zAP9UM;%8*@m>=mMejyjLR|J~cBWtrq4E*r$(4#&5j4u~fXl-M9-qa+=XC}}hThk7_ zx0uqLcHpVNthOElkn%=4dSwHz1YZSpeLl6`J@GZ2P5t@J*A^Ta{&O!s!+^pHt$kRJ z63xbGd!`^rH&Cl~1@8|u1J^o$PkHFVAP52!2ZCMZhDCh_t1JR-^_guaWBSfHIAUM0 zu5xHmrIsPVrJiXs(}#kvi&~8<_+NpGKzTdx^wtU3X5stABCOI$sMVdtCW}B<&F=+K zGZXiZS`*}5HS|W6hS!72RB6-q3`Sya5B{z=z{)%5YrTQ%KryhsbpzZNAX9F}-Vlyp z1F0WMwOZ*RyLapS&1Xsm2ABE%G$TC*g)NojdM|vSDUHUxU~aM72yC^|Bsv1Er&)V3`ek*Ahg_{U=jh$DlqEYu*zM* zlbxzLL)_a>YInukcv~yZ2j+`sK+g_-XcrHs-?@19nfS-u9y4Vdfg`{Lfd35u!V?W> T+`D{800000NkvXXu0mjf?t}NL 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 z3TZ - -{% endblock %} - -{% block relbaritems %} -
  • -{% endblock %} - diff --git a/docs/_themes/pyav/theme.conf b/docs/_themes/pyav/theme.conf deleted file mode 100644 index fa1636ba4..000000000 --- a/docs/_themes/pyav/theme.conf +++ /dev/null @@ -1,2 +0,0 @@ -[theme] -inherit = nature diff --git a/docs/api/_globals.rst b/docs/api/_globals.rst deleted file mode 100644 index ef6ce329d..000000000 --- a/docs/api/_globals.rst +++ /dev/null @@ -1,5 +0,0 @@ - -Globals -======= - -.. autofunction:: av.open diff --git a/docs/api/audio.rst b/docs/api/audio.rst deleted file mode 100644 index bdff5fd4b..000000000 --- a/docs/api/audio.rst +++ /dev/null @@ -1,72 +0,0 @@ - -Audio -===== - -Audio Streams -------------- - -.. automodule:: av.audio.stream - - .. autoclass:: AudioStream - :members: - -Audio Context -------------- - -.. automodule:: av.audio.codeccontext - - .. autoclass:: AudioCodecContext - :members: - :exclude-members: channel_layout, channels - -Audio Formats -------------- - -.. automodule:: av.audio.format - - .. autoclass:: AudioFormat - :members: - -Audio Layouts -------------- - -.. automodule:: av.audio.layout - - .. autoclass:: AudioLayout - :members: - - .. autoclass:: AudioChannel - :members: - -Audio Frames ------------- - -.. automodule:: av.audio.frame - - .. autoclass:: AudioFrame - :members: - :exclude-members: to_nd_array - -Audio FIFOs ------------ - -.. automodule:: av.audio.fifo - - .. autoclass:: AudioFifo - :members: - :exclude-members: write, read, read_many - - .. automethod:: write - .. automethod:: read - .. automethod:: read_many - -Audio Resamplers ----------------- - -.. automodule:: av.audio.resampler - - .. autoclass:: AudioResampler - :members: - :exclude-members: resample - - .. automethod:: resample diff --git a/docs/api/buffer.rst b/docs/api/buffer.rst deleted file mode 100644 index eabe5ba11..000000000 --- a/docs/api/buffer.rst +++ /dev/null @@ -1,8 +0,0 @@ - -Buffers -======= - -.. automodule:: av.buffer - - .. autoclass:: Buffer - :members: diff --git a/docs/api/codec.rst b/docs/api/codec.rst deleted file mode 100644 index ebc147c30..000000000 --- a/docs/api/codec.rst +++ /dev/null @@ -1,135 +0,0 @@ - -Codecs -====== - -Descriptors ------------ - -.. currentmodule:: av.codec -.. automodule:: av.codec - -.. autoclass:: Codec - -.. automethod:: Codec.create - -.. autoattribute:: Codec.is_decoder -.. autoattribute:: Codec.is_encoder - -.. autoattribute:: Codec.descriptor -.. autoattribute:: Codec.name -.. autoattribute:: Codec.long_name -.. autoattribute:: Codec.type -.. autoattribute:: Codec.id - -.. autoattribute:: Codec.frame_rates -.. autoattribute:: Codec.audio_rates -.. autoattribute:: Codec.video_formats -.. autoattribute:: Codec.audio_formats - - -Flags -~~~~~ - -.. autoattribute:: Codec.properties - -.. autoclass:: Properties - - 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 - - -Contexts --------- - -.. currentmodule:: av.codec.context -.. automodule:: av.codec.context - -.. autoclass:: CodecContext - -.. autoattribute:: CodecContext.codec -.. autoattribute:: CodecContext.options - -.. automethod:: CodecContext.create -.. automethod:: CodecContext.open -.. automethod:: CodecContext.close - -Attributes -~~~~~~~~~~ - -.. autoattribute:: CodecContext.is_open -.. autoattribute:: CodecContext.is_encoder -.. autoattribute:: CodecContext.is_decoder - -.. autoattribute:: CodecContext.name -.. autoattribute:: CodecContext.type -.. autoattribute:: CodecContext.profile - -.. autoattribute:: CodecContext.time_base -.. autoattribute:: CodecContext.ticks_per_frame - -.. autoattribute:: CodecContext.bit_rate -.. autoattribute:: CodecContext.bit_rate_tolerance -.. autoattribute:: CodecContext.max_bit_rate - -.. autoattribute:: CodecContext.thread_count -.. autoattribute:: CodecContext.thread_type -.. autoattribute:: CodecContext.skip_frame - -.. autoattribute:: CodecContext.extradata -.. autoattribute:: CodecContext.extradata_size - -Transcoding -~~~~~~~~~~~ -.. automethod:: CodecContext.parse -.. automethod:: CodecContext.encode -.. automethod:: CodecContext.decode - - -Flags -~~~~~ - -.. autoattribute:: CodecContext.flags - -.. autoclass:: av.codec.context.Flags - - .. enumtable:: av.codec.context:Flags - :class: av.codec.context:CodecContext - -.. autoattribute:: CodecContext.flags2 - -.. autoclass:: av.codec.context.Flags2 - - .. enumtable:: av.codec.context:Flags2 - :class: av.codec.context:CodecContext - - -Enums -~~~~~ - -.. autoclass:: av.codec.context.ThreadType - - Which multithreading methods to use. - Use of FF_THREAD_FRAME will increase decoding delay by one frame per thread, - so clients which cannot provide future frames should not use it. - - .. enumtable:: av.codec.context.ThreadType - -.. autoclass:: av.codec.context.SkipType - - .. enumtable:: av.codec.context.SkipType - - diff --git a/docs/api/container.rst b/docs/api/container.rst deleted file mode 100644 index f4c9732c9..000000000 --- a/docs/api/container.rst +++ /dev/null @@ -1,79 +0,0 @@ - -Containers -========== - - -Generic -------- - -.. currentmodule:: av.container - -.. automodule:: av.container - -.. autoclass:: Container - - .. attribute:: options - .. attribute:: container_options - .. attribute:: stream_options - .. attribute:: metadata_encoding - .. attribute:: metadata_errors - .. attribute:: open_timeout - .. attribute:: read_timeout - - -Flags -~~~~~ - -.. attribute:: av.container.Container.flags - -.. class:: av.container.Flags - - Wraps :ffmpeg:`AVFormatContext.flags`. - - .. enumtable:: av.container.core:Flags - :class: av.container.core:Container - - -Input Containers ----------------- - -.. autoclass:: InputContainer - :members: - - -Output Containers ------------------ - -.. autoclass:: OutputContainer - :members: - - -Formats -------- - -.. currentmodule:: av.format - -.. automodule:: av.format - -.. autoclass:: ContainerFormat - -.. autoattribute:: ContainerFormat.name -.. autoattribute:: ContainerFormat.long_name - -.. autoattribute:: ContainerFormat.options -.. autoattribute:: ContainerFormat.input -.. autoattribute:: ContainerFormat.output -.. autoattribute:: ContainerFormat.is_input -.. autoattribute:: ContainerFormat.is_output -.. autoattribute:: ContainerFormat.extensions - -Flags -~~~~~ - -.. autoattribute:: ContainerFormat.flags - -.. autoclass:: av.format.Flags - - .. enumtable:: av.format.Flags - :class: av.format.ContainerFormat - 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 deleted file mode 100644 index 3f82f4ec2..000000000 --- a/docs/api/error.rst +++ /dev/null @@ -1,85 +0,0 @@ -Errors -====== - -.. currentmodule:: av.error - -.. _error_behaviour: - -General Behaviour ------------------ - -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`. - -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 - - -.. _error_classes: - -Error Exception Classes ------------------------ - -PyAV raises the typical builtin exceptions within its own codebase, but things -get a little more complex when it comes to translating FFmpeg errors. - -There are two competing ideas that have influenced the final design: - -1. We want every exception that originates within FFmpeg to inherit from a common - :class:`.FFmpegError` exception; - -2. We want to use the builtin exceptions whenever possible. - -As such, PyAV effectivly shadows as much of the builtin exception heirarchy 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 -can be caught via either :class:`FFmpegError` or ``ValueError``. All of these -exceptions expose the typical ``errno`` and ``strerror`` attributes (even -``ValueError`` which doesn't typically), as well as some PyAV extensions such -as :attr:`FFmpegError.log`. - -All of these exceptions are available on the top-level ``av`` package, e.g.:: - - try: - do_something() - except av.FilterNotFoundError: - handle_error() - - -.. 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 2fe029073..000000000 --- a/docs/api/error_table.py +++ /dev/null @@ -1,36 +0,0 @@ -import av - - -rows = [( - #'Tag (Code)', - '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(( - #'{} ({})'.format(enum.tag, code), - 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/docs/api/filter.rst b/docs/api/filter.rst deleted file mode 100644 index d126fda67..000000000 --- a/docs/api/filter.rst +++ /dev/null @@ -1,34 +0,0 @@ -Filters -======= - -.. automodule:: av.filter.filter - - .. autoclass:: Filter - :members: - - -.. automodule:: av.filter.graph - - .. autoclass:: Graph - :members: - - -.. automodule:: av.filter.context - - .. autoclass:: FilterContext - :members: - - -.. automodule:: av.filter.link - - .. autoclass:: FilterLink - :members: - - -.. automodule:: av.filter.pad - - .. autoclass:: FilterPad - :members: - - .. autoclass:: FilterContextPad - :members: diff --git a/docs/api/frame.rst b/docs/api/frame.rst deleted file mode 100644 index f0e3a7554..000000000 --- a/docs/api/frame.rst +++ /dev/null @@ -1,8 +0,0 @@ - -Frames -====== - -.. automodule:: av.frame - - .. autoclass:: Frame - :members: diff --git a/docs/api/packet.rst b/docs/api/packet.rst deleted file mode 100644 index 315d08345..000000000 --- a/docs/api/packet.rst +++ /dev/null @@ -1,8 +0,0 @@ - -Packets -======= - -.. automodule:: av.packet - - .. autoclass:: Packet - :members: diff --git a/docs/api/plane.rst b/docs/api/plane.rst deleted file mode 100644 index 7310f963f..000000000 --- a/docs/api/plane.rst +++ /dev/null @@ -1,8 +0,0 @@ - -Planes -====== - -.. automodule:: av.plane - - .. autoclass:: Plane - :members: diff --git a/docs/api/sidedata.rst b/docs/api/sidedata.rst deleted file mode 100644 index 48163b09f..000000000 --- a/docs/api/sidedata.rst +++ /dev/null @@ -1,20 +0,0 @@ - -Side Data -========= - -.. automodule:: av.sidedata.sidedata - - .. autoclass:: SideData - :members: - -.. autoclass:: av.sidedata.sidedata.Type -.. enumtable:: av.sidedata.sidedata.Type - - -Motion Vectors --------------- - -.. automodule:: av.sidedata.motionvectors - - .. autoclass:: MotionVectors - :members: diff --git a/docs/api/stream.rst b/docs/api/stream.rst deleted file mode 100644 index 49ff05ac8..000000000 --- a/docs/api/stream.rst +++ /dev/null @@ -1,119 +0,0 @@ - -Streams -======= - - -Stream collections ------------------- - -.. currentmodule:: av.container.streams - -.. autoclass:: StreamContainer - - -Dynamic Slicing -~~~~~~~~~~~~~~~ - -.. automethod:: StreamContainer.get - - -Typed Collections -~~~~~~~~~~~~~~~~~ - -These attributes are preferred for readability if you don't need the -dynamic capabilities of :meth:`.get`: - -.. attribute:: StreamContainer.video - - A tuple of :class:`VideoStream`. - -.. attribute:: StreamContainer.audio - - A tuple of :class:`AudioStream`. - -.. attribute:: StreamContainer.subtitles - - A tuple of :class:`SubtitleStream`. - -.. attribute:: StreamContainer.data - - A tuple of :class:`DataStream`. - -.. attribute:: StreamContainer.other - - A tuple of :class:`Stream` - - -Streams -------- - -.. currentmodule:: av.stream - -.. autoclass:: Stream - - -Basics -~~~~~~ - - -.. autoattribute:: Stream.type - -.. autoattribute:: Stream.codec_context - -.. autoattribute:: Stream.id - -.. autoattribute:: Stream.index - - -Transcoding -~~~~~~~~~~~ - -.. automethod:: Stream.encode - -.. automethod:: Stream.decode - - -Timing -~~~~~~ - -.. seealso:: :ref:`time` for a discussion of time in general. - -.. autoattribute:: Stream.time_base - -.. autoattribute:: Stream.start_time - -.. autoattribute:: Stream.duration - -.. 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 -~~~~~~ - -.. autoattribute:: Stream.profile - -.. autoattribute:: Stream.language - - - diff --git a/docs/api/subtitles.rst b/docs/api/subtitles.rst deleted file mode 100644 index 19d75621c..000000000 --- a/docs/api/subtitles.rst +++ /dev/null @@ -1,28 +0,0 @@ - -Subtitles -=========== - -.. automodule:: av.subtitles.stream - - .. autoclass:: SubtitleStream - :members: - -.. automodule:: av.subtitles.subtitle - - .. autoclass:: SubtitleSet - :members: - - .. autoclass:: Subtitle - :members: - - .. autoclass:: BitmapSubtitle - :members: - - .. autoclass:: BitmapSubtitlePlane - :members: - - .. autoclass:: TextSubtitle - :members: - - .. autoclass:: AssSubtitle - :members: diff --git a/docs/api/time.rst b/docs/api/time.rst deleted file mode 100644 index 35e4cfc85..000000000 --- a/docs/api/time.rst +++ /dev/null @@ -1,92 +0,0 @@ - -.. _time: - -Time -==== - -Overview --------- - -Time is expressed as integer multiples of arbitrary units of time called a ``time_base``. There are different contexts that have different time bases: :class:`.Stream` has :attr:`.Stream.time_base`, :class:`.CodecContext` has :attr:`.CodecContext.time_base`, and :class:`.Container` has :data:`av.TIME_BASE`. - -.. testsetup:: - - import av - path = av.datasets.curated('pexels/time-lapse-video-of-night-sky-857195.mp4') - - def get_nth_packet_and_frame(fh, skip): - for p in fh.demux(): - for f in p.decode(): - if not skip: - return p, f - skip -= 1 - -.. doctest:: - - >>> fh = av.open(path) - >>> video = fh.streams.video[0] - - >>> video.time_base - Fraction(1, 25) - -Attributes that represent time on those objects will be in that object's ``time_base``: - -.. doctest:: - - >>> video.duration - 168 - >>> 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). - -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.. - -.. doctest:: - - >>> p, f = get_nth_packet_and_frame(fh, skip=1) - - >>> p.time_base - Fraction(1, 25) - >>> p.dts - 1 - - >>> f.time_base - Fraction(1, 25) - >>> f.pts - 1 - - -For convenince, :attr:`.Frame.time` is a ``float`` in seconds: - -.. doctest:: - - >>> f.time - 0.04 - - -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. - -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. - -When there is no ``time_base`` (such as on :ffmpeg:`AVFormatContext`), there is an implicit ``time_base`` of ``1/AV_TIME_BASE``. - -Encoding -........ - - -For encoding, you (the PyAV developer / FFmpeg "user") must set :ffmpeg:`AVCodecContext.time_base`, ideally to the inverse of the frame rate (or so the library docs say to do if your frame rate is fixed; we're not sure what to do if it is not fixed), and you may set :ffmpeg:`AVStream.time_base` as a hint to the muxer. After you open all the codecs and call :ffmpeg:`avformat_write_header`, the stream time base may change, and you must respect it. We don't know if the codec time base may change, so we will make the safer assumption that it may and respect it as well. - -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 -........ - -Everything is in :ffmpeg:`AVStream.time_base` because we don't have to rebase it into codec time base (as it generally seems to be the case that :ffmpeg:`AVCodecContext` doesn't really care about your timing; I wish there was a way to assert this without reading every codec). - diff --git a/docs/api/utils.rst b/docs/api/utils.rst deleted file mode 100644 index 820f30f0a..000000000 --- a/docs/api/utils.rst +++ /dev/null @@ -1,18 +0,0 @@ - - -Utilities -========= - -Logging -------- - -.. automodule:: av.logging - :members: - -Other ------ - -.. automodule:: av.utils - :members: - - .. autoclass:: AVError diff --git a/docs/api/video.rst b/docs/api/video.rst deleted file mode 100644 index 5e47b1db8..000000000 --- a/docs/api/video.rst +++ /dev/null @@ -1,119 +0,0 @@ -Video -===== - -Video Streams -------------- - -.. automodule:: av.video.stream - - .. autoclass:: VideoStream - :members: - -Video Codecs -------------- - -.. automodule:: av.video.codeccontext - - .. autoclass:: VideoCodecContext - :members: - -Video Formats -------------- - -.. automodule:: av.video.format - - .. autoclass:: VideoFormat - :members: - - .. autoclass:: VideoFormatComponent - :members: - -Video Frames ------------- - -.. automodule:: av.video.frame - -.. autoclass:: VideoFrame - - A single video frame. - - :param int width: The width of the frame. - :param int height: The height of the frame. - :param format: The format of the frame. - :type format: :class:`VideoFormat` or ``str``. - - >>> frame = VideoFrame(1920, 1080, 'rgb24') - -Structural -~~~~~~~~~~ - -.. autoattribute:: VideoFrame.width -.. autoattribute:: VideoFrame.height -.. attribute:: VideoFrame.format - - The :class:`.VideoFormat` of the frame. - -.. autoattribute:: VideoFrame.planes - -Types -~~~~~ - -.. autoattribute:: VideoFrame.key_frame -.. autoattribute:: VideoFrame.interlaced_frame -.. autoattribute:: VideoFrame.pict_type - -.. autoclass:: av.video.frame.PictureType - - Wraps ``AVPictureType`` (``AV_PICTURE_TYPE_*``). - - .. enumtable:: av.video.frame.PictureType - - -Conversions -~~~~~~~~~~~ - -.. automethod:: VideoFrame.reformat - -.. automethod:: VideoFrame.to_rgb -.. automethod:: VideoFrame.to_image -.. automethod:: VideoFrame.to_ndarray - -.. automethod:: VideoFrame.from_image -.. automethod:: VideoFrame.from_ndarray - - - -Video Planes -------------- - -.. automodule:: av.video.plane - - .. autoclass:: VideoPlane - :members: - - -Video Reformatters ------------------- - -.. automodule:: av.video.reformatter - - .. autoclass:: VideoReformatter - - .. automethod:: reformat - -Enums -~~~~~ - -.. autoclass:: av.video.reformatter.Interpolation - - Wraps the ``SWS_*`` flags. - - .. enumtable:: av.video.reformatter.Interpolation - -.. 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. - - .. enumtable:: av.video.reformatter.Colorspace - diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index bf5fb4b0b..000000000 --- a/docs/conf.py +++ /dev/null @@ -1,497 +0,0 @@ -# -# 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 sphinx import addnodes -from sphinx.util.docutils import SphinxDirective - - -logging.basicConfig() - - -if sphinx.version_info < (1, 8): - print(f"Sphinx {sphinx.__version__} is too old; we require >= 1.8.", 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('..')) - -# -- 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. -] - - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = 'PyAV' -copyright = '2017, Mike Boers' - -# 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] - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# 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 - -# 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 = [] - - -# -- 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 - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -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' - -# 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 - -# 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 = ''' - -import errno -import os - -import av -from av.datasets import fate, fate as fate_suite, curated - -from tests import common -from tests.common import sandboxed as _sandboxed - -def sandboxed(*args, **kwargs): - kwargs['timed'] = True - return _sandboxed('docs', *args, **kwargs) - -_cwd = os.getcwd() -here = sandboxed('__cwd__') -try: - os.makedirs(here) -except OSError as e: - if e.errno != errno.EEXIST: - raise -os.chdir(here) - -video_path = curated('pexels/time-lapse-video-of-night-sky-857195.mp4') - -''' - -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', '@'), -} - -intersphinx_mapping = { - 'https://docs.python.org/3': None, -} - -autodoc_member_order = 'bysource' -autodoc_default_options = { - 'undoc-members': True, - 'show-inheritance': True, -} - - -todo_include_todos = True - - -class PyInclude(SphinxDirective): - - has_content = True - - def run(self): - - - source = '\n'.join(self.content) - output = [] - def write(*content, sep=' ', end='\n'): - output.append(sep.join(map(str, content)) + end) - - namespace = dict(write=write) - exec(compile(source, '', 'exec'), namespace, namespace) - - output = ''.join(output).splitlines() - self.state_machine.insert_input(output, 'blah') - - return [] #[nodes.literal('hello', repr(content))] - - -def load_entrypoint(name): - - parts = name.split(':') - if len(parts) == 1: - parts = name.rsplit('.', 1) - mod_name, attrs = parts - - attrs = attrs.split('.') - try: - obj = __import__(mod_name, fromlist=['.']) - except ImportError as e: - print('Error while importing.', (name, mod_name, attrs, e)) - raise - for attr in attrs: - obj = getattr(obj, attr) - return obj - -class EnumTable(SphinxDirective): - - required_arguments = 1 - option_spec = { - 'class': lambda x: x, - } - - def run(self): - - 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: - for name, value in vars(cls).items(): - if isinstance(value, property): - try: - item = value._enum_item - except AttributeError: - pass - else: - if isinstance(item, enum): - properties[item] = name - - colwidths = [15, 15, 5, 65] if cls else [15, 5, 75] - ncols = len(colwidths) - - table = nodes.table() - - tgroup = nodes.tgroup(cols=ncols) - table += tgroup - - for width in colwidths: - tgroup += nodes.colspec(colwidth=width) - - thead = nodes.thead() - tgroup += thead - - tbody = nodes.tbody() - tgroup += tbody - - def makerow(*texts): - row = nodes.row() - for text in texts: - if text is None: - continue - row += nodes.entry('', nodes.paragraph('', str(text))) - return row - - thead += makerow( - 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()) - - 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] - - - - -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', - ): - try: - return urls[kind][name] - 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: - 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 - ) - - return [node], [] - - return _doxylink_handler - - - - -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/cookbook/basics.rst b/docs/cookbook/basics.rst deleted file mode 100644 index 2896519de..000000000 --- a/docs/cookbook/basics.rst +++ /dev/null @@ -1,42 +0,0 @@ -Basics -====== - -Here are some common things to do without digging too deep into the mechanics. - - -Saving Keyframes ----------------- - -If you just want to look at keyframes, you can set :attr:`.CodecContext.skip_frame` to speed up the process: - -.. literalinclude:: ../../examples/basics/save_keyframes.py - - -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. - -.. literalinclude:: ../../examples/basics/remux.py - - -Parsing -------- - -Sometimes we have a raw stream of data, and we need to split it into packets before working with it. We can use :meth:`.CodecContext.parse` to do this. - -.. literalinclude:: ../../examples/basics/parse.py - - -Threading ---------- - -By default, codec contexts will decode with :data:`~av.codec.context.ThreadType.SLICE` threading. This allows multiple threads to cooperate to decode any given frame. - -This is faster than no threading, but is not as fast as we can go. - -Also enabling :data:`~av.codec.context.ThreadType.FRAME` (or :data:`~av.codec.context.ThreadType.AUTO`) threading allows multiple threads to decode independent frames. This is not enabled by default because it does change the API a bit: you will get a much larger "delay" between starting the decode of a packet and getting it's results. Take a look at the output of this sample to see what we mean: - -.. literalinclude:: ../../examples/basics/thread_type.py - -On the author's machine, the second pass decodes ~5 times faster. diff --git a/docs/cookbook/numpy.rst b/docs/cookbook/numpy.rst deleted file mode 100644 index d4887945c..000000000 --- a/docs/cookbook/numpy.rst +++ /dev/null @@ -1,24 +0,0 @@ -Numpy -===== - - -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: - -.. image:: ../_static/examples/numpy/barcode.jpg - -The code that created this: - -.. literalinclude:: ../../examples/numpy/barcode.py - - -Generating Video ----------------- - -.. literalinclude:: ../../examples/numpy/generate_video.py - - diff --git a/docs/development/changelog.rst b/docs/development/changelog.rst deleted file mode 100644 index 339b4474e..000000000 --- a/docs/development/changelog.rst +++ /dev/null @@ -1,6 +0,0 @@ - -.. It is all in the other file (that we want at the top-level of the repo). - -.. _changelog: - -.. include:: ../../CHANGELOG.rst diff --git a/docs/development/contributors.rst b/docs/development/contributors.rst deleted file mode 100644 index a17b40630..000000000 --- a/docs/development/contributors.rst +++ /dev/null @@ -1,3 +0,0 @@ - - -.. include:: ../../AUTHORS.rst 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 diff --git a/docs/development/includes.py b/docs/development/includes.py deleted file mode 100644 index e4cb45e3b..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().__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 = '{}/{}#{}'.format(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 = '{} {}{}'.format(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(" {} in {}".format(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:: {} {}'.format(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'] = '{}/struct{}.html'.format(doxygen_base, event['name']) - - else: - print('Unknown event type %s' % event['type'], file=sys.stderr) - - name = event['name'] - if event.get('struct'): - name = '{}.{}'.format(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:`{}{}`'.format(prefix, ref['property']))) - elif ref.get('function'): - name = ref['function'] - if name in ('__init__', '__cinit__', '__dealloc__'): - ref_pairs.append((name, ':class:`{}{} <{}>`'.format(prefix, name, prefix.rstrip('.')))) - else: - ref_pairs.append((name, ':func:`{}{}`'.format(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/development/license.rst b/docs/development/license.rst deleted file mode 100644 index 57cd288f4..000000000 --- a/docs/development/license.rst +++ /dev/null @@ -1,12 +0,0 @@ - -.. It is all in the other file (that we want at the top-level of the repo). - -.. _license: - -License -======= - -From `LICENSE.txt `_: - -.. literalinclude:: ../../LICENSE.txt - :language: text diff --git a/docs/generate-tagfile b/docs/generate-tagfile deleted file mode 100755 index f00ed7ad4..000000000 --- a/docs/generate-tagfile +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env python - -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', -))) -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(''' - -#@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()) - - diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index 386e819bd..000000000 --- a/docs/index.rst +++ /dev/null @@ -1,105 +0,0 @@ -**PyAV** Documentation -====================== - -**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). - -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. - -But where you can't work without it, PyAV is a critical tool. - -Currently we provide: - -- ``libavformat``: - :class:`containers <.Container>`, - audio/video/subtitle :class:`streams <.Stream>`, - :class:`packets <.Packet>`; - -- ``libavdevice`` (by specifying a format to containers); - -- ``libavcodec``: - :class:`.Codec`, - :class:`.CodecContext`, - audio/video :class:`frames <.Frame>`, - :class:`data planes <.Plane>`, - :class:`subtitles <.Subtitle>`; - -- ``libavfilter``: - :class:`.Filter`, - :class:`.Graph`; - -- ``libswscale``: - :class:`.VideoReformatter`; - -- ``libswresample``: - :class:`.AudioResampler`; - -- and a few more utilities. - -.. _FFmpeg: https://ffmpeg.org/ - - -Basic Demo ----------- - -.. testsetup:: - - path_to_video = common.fate_png() # We don't need a full QT here. - - -.. testcode:: - - import av - - container = av.open(path_to_video) - - for frame in container.decode(video=0): - frame.to_image().save('frame-%04d.jpg' % frame.index) - - -Overview --------- - -.. toctree:: - :glob: - :maxdepth: 2 - - overview/* - - -Cookbook --------- - -.. toctree:: - :glob: - :maxdepth: 2 - - cookbook/* - - -Reference ---------- - -.. toctree:: - :glob: - :maxdepth: 2 - - api/* - - -Development ------------ - -.. toctree:: - :glob: - :maxdepth: 1 - - development/* - - -Indices and Tables -================== -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` diff --git a/docs/overview/caveats.rst b/docs/overview/caveats.rst deleted file mode 100644 index 5ccac3ffb..000000000 --- a/docs/overview/caveats.rst +++ /dev/null @@ -1,57 +0,0 @@ -Caveats -======= - -.. _authority_of_docs: - -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. - -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. - - -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 ---------------- - -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: - -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`. - - -.. _garbage_collection: - -Garbage Collection ------------------- - -PyAV currently has a number of reference cycles that make it more difficult for the garbage collector than we would like. In some circumstances (usually tight loops involving opening many containers), a :class:`.Container` will not auto-close until many a few thousand have built-up. - -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: - # 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 deleted file mode 100644 index 9ac4dc078..000000000 --- a/docs/overview/installation.rst +++ /dev/null @@ -1,146 +0,0 @@ -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 5.1.2 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 -- libtheora -- libtwolame -- libvorbis -- libvpx -- libx264 -- libx265 -- libxml2 -- libxvid -- lzma -- zlib - -The following additional features are also enabled on Linux: - -- gnutls -- libxcb - - -Conda ------ - -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. - - -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: - -.. 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 - - -Mac OS X -^^^^^^^^ - -On **Mac OS X**, 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 -------------------------------- - -:: - - # Get PyAV from GitHub. - git clone git@github.com:PyAV-Org/PyAV.git - cd PyAV - - # Prep a virtualenv. - source scripts/activate.sh - - # Install basic requirements. - pip install -r tests/requirements.txt - - # Optionally build FFmpeg. - ./scripts/build-deps - - # 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:: - - 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 8f2b18d7347c2a8489571372f79f231307729c2c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 15 Oct 2023 02:33:04 -0400 Subject: [PATCH 161/192] Remove issue template --- .github/ISSUE_TEMPLATE/build-bug-report.md | 74 ------------------- .../ISSUE_TEMPLATE/ffmpeg-feature-request.md | 62 ---------------- .../ISSUE_TEMPLATE/pyav-feature-request.md | 32 -------- .github/ISSUE_TEMPLATE/runtime-bug-report.md | 74 ------------------- .github/ISSUE_TEMPLATE/user-help.md | 52 ------------- 5 files changed, 294 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 delete mode 100644 .github/ISSUE_TEMPLATE/user-help.md diff --git a/.github/ISSUE_TEMPLATE/build-bug-report.md b/.github/ISSUE_TEMPLATE/build-bug-report.md deleted file mode 100644 index 9f327e5de..000000000 --- a/.github/ISSUE_TEMPLATE/build-bug-report.md +++ /dev/null @@ -1,74 +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`. }} -``` -- FFmpeg: -``` -{{ Complete output of `ffmpeg -version` }} -``` - - -## 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://gitter.im/PyAV-Org) -- [ ] ... and waited 72 hours for a response. - - -## 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 115c15b13..000000000 --- a/.github/ISSUE_TEMPLATE/ffmpeg-feature-request.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -name: FFmpeg feature request -about: Request a feature of FFmpeg be exposed or supported by PyAV. -title: "Allow FOO to BAR" -labels: enhancement -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`. }} -``` -- FFmpeg: -``` -{{ Complete output of `ffmpeg -version` }} -``` - - -## 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 7433cbfbe..000000000 --- a/.github/ISSUE_TEMPLATE/pyav-feature-request.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -name: PyAV feature request -about: Request a feature of PyAV that is not provided by FFmpeg. -title: "Allow FOO to BAR" -labels: enhancement -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. }} - - -## 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 b29b11266..000000000 --- a/.github/ISSUE_TEMPLATE/runtime-bug-report.md +++ /dev/null @@ -1,74 +0,0 @@ ---- -name: Runtime bug report -about: Report on an issue while running PyAV. -title: "The FOO does not BAR." -labels: bug -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. }} -``` -- PyAV build: -``` -{{ 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) -- [ ] 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) -- [ ] ... and waited 72 hours for a response. - - -## Additional context - -{{ Add any other context about the problem here. }} diff --git a/.github/ISSUE_TEMPLATE/user-help.md b/.github/ISSUE_TEMPLATE/user-help.md deleted file mode 100644 index 62c3e5c16..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://gitter.im/PyAV-Org) -- [ ] ... and waited 72 hours for a response. - - -## Additional context - -{{ Add any other context about the problem here. }} From 5fe11b322a25b087f397747f641882cb0a876a36 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 15 Oct 2023 02:54:29 -0400 Subject: [PATCH 162/192] Delete manifest --- MANIFEST.in | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 321b65e6d..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 -recursive-include tests *.py From 2ef8febb8eee8be23261db1a463af2be2f69756f Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 15 Oct 2023 03:03:33 -0400 Subject: [PATCH 163/192] Update tests --- .github/workflows/tests.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 58a177e99..f94faa78d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,10 +6,12 @@ on: paths-ignore: - '**.md' - '**.rst' + - '**.txt' pull_request: paths-ignore: - '**.md' - '**.rst' + - '**.txt' jobs: style: name: "${{ matrix.config.suite }}" @@ -37,8 +39,7 @@ jobs: - 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 }} + python -m pip install black flake8 isort - name: "${{ matrix.config.suite }}" run: | From 458b557d81d9524e0e6abfcacae0be969c8ee3bd Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 15 Oct 2023 03:04:31 -0400 Subject: [PATCH 164/192] Bump version to 11.1.0 --- av/about.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/about.py b/av/about.py index b5d098016..f9e29a7cd 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "11.0.1" +__version__ = "11.1.0" From 4416cba6f5e6ec03936c8b6c486ee32121824aa7 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 18 Oct 2023 17:48:11 -0400 Subject: [PATCH 165/192] Simply fetch-vendor script --- .github/workflows/tests.yml | 2 +- scripts/fetch-vendor.json | 3 -- scripts/fetch-vendor.py | 57 +++++++++++++++---------------------- 3 files changed, 24 insertions(+), 38 deletions(-) delete mode 100644 scripts/fetch-vendor.json diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f94faa78d..c4e63fbb3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -84,7 +84,7 @@ jobs: ;; macos-latest) brew update - brew install automake libtool nasm pkg-config shtool texi2html wget + brew install automake libtool nasm pkg-config shtool wget brew install libass libjpeg libpng libvorbis libvpx opus theora x264 ;; esac diff --git a/scripts/fetch-vendor.json b/scripts/fetch-vendor.json deleted file mode 100644 index 2a02f285b..000000000 --- a/scripts/fetch-vendor.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.1.2-1/ffmpeg-{platform}.tar.gz"] -} diff --git a/scripts/fetch-vendor.py b/scripts/fetch-vendor.py index 60259ee54..765201cf1 100644 --- a/scripts/fetch-vendor.py +++ b/scripts/fetch-vendor.py @@ -1,64 +1,53 @@ +from struct import calcsize import argparse -import logging -import json import os import platform import shutil -import struct import subprocess def get_platform(): system = platform.system() machine = platform.machine() + if system == "Linux": return f"manylinux_{machine}" - elif system == "Darwin": + + if 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(f"Unsupported system {system}") + + if system == "Windows": + return "win_amd64" if calcsize("P") * 8 == 64 else "win32" + + 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=os.path.splitext(__file__)[0] + ".json") args = parser.parse_args() -logging.basicConfig(level=logging.INFO) - -# read config file -with open(args.config_file) as fp: - config = json.load(fp) -# create fresh destination directory -logging.info("Creating directory %s" % args.destination_dir) +print(f"Creating directory {args.destination_dir}") if os.path.exists(args.destination_dir): shutil.rmtree(args.destination_dir) os.makedirs(args.destination_dir) -for url_template in config["urls"]: - tarball_url = url_template.replace("{platform}", get_platform()) +tarball_url = f"https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.1.2-1/ffmpeg-{get_platform()}.tar.gz" + +tarball_name = tarball_url.split("/")[-1] +tarball_file = os.path.join(args.cache_dir, tarball_name) - # 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("Downloading %s" % 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] - ) +if not os.path.exists(tarball_file): + print(f"Downloading {tarball_url}") + if not os.path.exists(args.cache_dir): + os.mkdir(args.cache_dir) + subprocess.run( + ["curl", "--location", "--output", tarball_file, "--silent", tarball_url] + ) - # extract tarball - logging.info("Extracting %s" % tarball_name) - subprocess.check_call(["tar", "-C", args.destination_dir, "-xf", tarball_file]) +print(f"Extracting {tarball_name}") +subprocess.run(["tar", "-C", args.destination_dir, "-xf", tarball_file]) From 54e1337c04bb793fbc50a73f64745cc6244f0182 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 18 Oct 2023 18:29:25 -0400 Subject: [PATCH 166/192] more --- .github/workflows/tests.yml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c4e63fbb3..bc4470639 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -78,9 +78,6 @@ jobs: 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-latest) brew update @@ -212,7 +209,7 @@ jobs: python-version: 3.9 - 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: | @@ -228,15 +225,14 @@ 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: "*-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 + CIBW_TEST_SKIP: pp* *_i686 *-macosx_arm64 run: | pip install cibuildwheel delvewheel cibuildwheel --output-dir dist - shell: bash + - name: Upload wheels uses: actions/upload-artifact@v3 with: From a16ec1d22c26b0295b7d827346f074efbf736449 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 18 Oct 2023 21:09:54 -0400 Subject: [PATCH 167/192] Update typehints, docstrings --- av/about.py | 2 +- av/codec/codec.pyx | 56 +++++---------- av/container/core.pyx | 155 +++++++++++++++++++--------------------- av/container/output.pyx | 24 +++---- av/video/frame.pyx | 22 +++--- av/video/plane.pyx | 7 +- 6 files changed, 114 insertions(+), 152 deletions(-) diff --git a/av/about.py b/av/about.py index f9e29a7cd..10db32a35 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "11.1.0" +__version__ = "11.2.0" diff --git a/av/codec/codec.pyx b/av/codec/codec.pyx index 978d42775..24852e94f 100644 --- a/av/codec/codec.pyx +++ b/av/codec/codec.pyx @@ -139,33 +139,27 @@ class UnknownCodecError(ValueError): cdef class Codec(object): - - """Codec(name, mode='r') - - :param str name: The codec name. - :param str mode: ``'r'`` for decoding or ``'w'`` for encoding. + """ + name: str + mode: "r" | "w" This object exposes information about an available codec, and an avenue to - create a :class:`.CodecContext` to encode/decode directly. - - :: - - >>> codec = Codec('mpeg4', 'r') - >>> codec.name - 'mpeg4' - >>> codec.type - 'video' - >>> codec.is_encoder - False - + create a CodecContext to encode/decode directly. + + >>> codec = Codec("mpeg4", "r") + >>> codec.name + 'mpeg4' + >>> codec.type + 'video' + >>> codec.is_encoder + False """ - 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) @@ -223,12 +217,6 @@ cdef class Codec(object): @property def type(self): - """ - The media type of this codec. - - E.g: ``'audio'``, ``'video'``, ``'subtitle'``. - - """ return lib.av_get_media_type_string(self.ptr.type) property id: @@ -292,7 +280,6 @@ cdef class Codec(object): @Properties.property def properties(self): - """Flag property of :class:`.Properties`""" return self.desc.props intra_only = properties.flag_property('INTRA_ONLY') @@ -304,7 +291,6 @@ cdef class Codec(object): @Capabilities.property def capabilities(self): - """Flag property of :class:`.Capabilities`""" return self.ptr.capabilities draw_horiz_band = capabilities.flag_property('DRAW_HORIZ_BAND') @@ -343,16 +329,13 @@ 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()) def dump_codecs(): - """Print information about available codecs.""" - - print '''Codecs: + print """Codecs: D..... = Decoding supported .E.... = Encoding supported ..V... = Video codec @@ -361,17 +344,16 @@ 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 diff --git a/av/container/core.pyx b/av/container/core.pyx index 4b629b36e..b5e7c9c43 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -211,19 +211,13 @@ cdef class Container(object): 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: raise ValueError("Could not determine output format") with nogil: # This does not actually open the file. - res = lib.avformat_alloc_output_context2( - &self.ptr, - ofmt, - NULL, - name, - ) + res = lib.avformat_alloc_output_context2(&self.ptr, ofmt, NULL, name) self.err_check(res) else: @@ -252,20 +246,14 @@ cdef class Container(object): 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 @@ -284,7 +272,7 @@ cdef class Container(object): 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) @@ -292,7 +280,7 @@ cdef class Container(object): def dumps_format(self): 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: @@ -309,11 +297,7 @@ cdef class Container(object): def _set_flags(self, value): self.ptr.flags = value - flags = Flags.property( - _get_flags, - _set_flags, - """Flags property of :class:`.Flags`""" - ) + flags = Flags.property(_get_flags, _set_flags, "Flags property of :class:`.Flags`") gen_pts = flags.flag_property('GENPTS') ign_idx = flags.flag_property('IGNIDX') @@ -332,60 +316,66 @@ cdef class Container(object): 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): - """open(file, mode='r', **kwargs) - - Main entrypoint to opening files/streams. - - :param str file: The file to open, which can be either a string or a file-like object. - :param str mode: ``"r"`` for reading and ``"w"`` for writing. - :param str format: Specific format to use. Defaults to autodect. - :param dict options: Options to pass to the container and all streams. - :param dict container_options: Options to pass to the container. - :param list stream_options: Options to pass to each stream. - :param str metadata_encoding: Encoding to use when reading or writing file metadata. - Defaults to ``"utf-8"``. - :param str metadata_errors: Specifies how to handle encoding errors; behaves like - ``str.encode`` parameter. Defaults to ``"strict"``. - :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. - :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.:: - - >>> # 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 - `FFmpeg website `_. - """ +"""Main entrypoint to opening files/streams. + +open(file) -> InputContainer | OutputContainer +open(file, mode="r") -> InputContainer +open(file, mode="w") -> OutputContainer + +file :: The file to open, which can be either a string or a file-like object. +mode: "r" | "w" | None +format: str | None :: Specific format to use. Defaults to autodect. +options: dict :: Options to pass to the container and all streams. +container_options: dict :: Options to pass to the container. +stream_options: list :: Options to pass to each stream. +metadata_encoding: str :: Encoding to use when reading or writing file metadata. +metadata_errors: str :: Specifies how to handle encoding errors +buffer_size: int :: Size of buffer for Python input/output operations in bytes. + Honored only when `file` is a file-like object. +timeout: float | None | tuple[open timeout, read timeout] + :: How many seconds to wait for data before giving up +io_open: callable | None + :: 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. + >>> # Open webcam on MacOS. + >>> av.open(format="avfoundation", file="0") + +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) +""" + +def open( + file, + mode: str | None = None, + format: str | None = None, + options: dict | None = None, + container_options: dict | None = None, # dict[str, str]?? + stream_options: list | None = None, + metadata_encoding: str = "utf-8", + metadata_errors: str = "strict", + buffer_size: int = 32768, + timeout = None, # Real | None | tuple[Real | None, Real | None] + io_open = None, +): + if not (mode is None or (type(mode) is str and (mode == "r" or mode == "w"))): + raise ValueError('mode must be "r" or "w" or None') if mode is None: - mode = getattr(file, 'mode', None) + mode = getattr(file, "mode", None) if mode is None: - mode = 'r' + mode = "r" if isinstance(timeout, tuple): open_timeout = timeout[0] @@ -394,15 +384,7 @@ def open(file, mode=None, format=None, options=None, 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('w'): + if mode.startswith("w"): if stream_options: raise ValueError("Provide stream options via Container.add_stream(..., options={}).") return OutputContainer( @@ -412,4 +394,11 @@ def open(file, mode=None, format=None, options=None, buffer_size, open_timeout, read_timeout, io_open ) - raise ValueError("mode must be 'r' or 'w'; got %r" % mode) + + return InputContainer( + _cinit_sentinel, file, format, options, + container_options, stream_options, + metadata_encoding, metadata_errors, + buffer_size, open_timeout, read_timeout, + io_open + ) diff --git a/av/container/output.pyx b/av/container/output.pyx index c4a7b5a33..e778420f3 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -44,40 +44,38 @@ cdef class OutputContainer(Container): 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) + """add_stream(codec_name, rate=None) -> av.stream.Stream Create a new stream, and return 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``, ``23.976``, and - ``Fraction(30000,1001)``. Examples for audio include ``48000`` - and ``44100``. - :returns: The new :class:`~av.stream.Stream`. - + codec_name: str | Codec | None :: The name of a codec. + rate: Real | None :: 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`. """ 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") codec_obj = template.codec_context.codec 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)) + raise ValueError( + f"{self.format.name} format does not support {codec_name} codec" + ) # Create new stream in the AVFormatContext, set AVCodecContext values. lib.avformat_new_stream(self.ptr, codec) @@ -190,7 +188,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(f"Some options were not used: {unused_options}") self._started = True diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 972abb779..dfc46861c 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -120,7 +120,7 @@ cdef class VideoFrame(Frame): lib.av_freep(&self._buffer) def __repr__(self): - return '' % ( + return "" % ( self.__class__.__name__, self.index, self.pts, @@ -133,7 +133,7 @@ cdef class VideoFrame(Frame): @property def planes(self): """ - A tuple of :class:`.VideoPlane` objects. + planes(self) -> tuple[VideoPlane ...] """ # We need to detect which planes actually exist, but also contrain # ourselves to the maximum plane count (as determined only by VideoFrames @@ -144,7 +144,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 @@ -154,26 +154,22 @@ cdef class VideoFrame(Frame): return tuple([VideoPlane(self, i) for i in range(plane_count)]) property width: - """Width of the image, in pixels.""" def __get__(self): return self.ptr.width property height: - """Height of the image, in pixels.""" def __get__(self): return self.ptr.height property key_frame: - """Is this frame a key frame? - - Wraps :ffmpeg:`AVFrame.key_frame`. - + """ + -> bool + Wraps AVFrame.key_frame """ def __get__(self): return self.ptr.key_frame property interlaced_frame: - """Is this frame an interlaced or progressive? - - Wraps :ffmpeg:`AVFrame.interlaced_frame`. - + """ + -> bool + Wraps AVFrame.interlaced_frame """ def __get__(self): return self.ptr.interlaced_frame diff --git a/av/video/plane.pyx b/av/video/plane.pyx index 6f1286ca3..14a5dc1e0 100644 --- a/av/video/plane.pyx +++ b/av/video/plane.pyx @@ -2,9 +2,7 @@ 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: self.width = 256 @@ -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(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 @@ -31,9 +29,8 @@ cdef class VideoPlane(Plane): property line_size: """ + -> int Bytes per horizontal line in this plane. - - :type: int """ def __get__(self): return self.frame.ptr.linesize[self.index] From ded8cab783a6201fa711dd4360a63ffe4af74e0e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 19 Oct 2023 13:01:58 -0400 Subject: [PATCH 168/192] Add stream side data (#6) Co-authored-by: hyenal Co-authored-by: Sebastien Ehrhardt --- av/stream.pxd | 4 +++- av/stream.pyx | 23 +++++++++++++++++- include/libavcodec/avcodec.pxd | 40 ++++++++++++++++++++++++++++++++ include/libavformat/avformat.pxd | 4 ++++ include/libavutil/avutil.pxd | 5 +++- 5 files changed, 73 insertions(+), 3 deletions(-) diff --git a/av/stream.pxd b/av/stream.pxd index 5ad3b965e..dcb0615b0 100644 --- a/av/stream.pxd +++ b/av/stream.pxd @@ -1,4 +1,4 @@ -from libc.stdint cimport int64_t +from libc.stdint cimport int32_t, int64_t cimport libav as lib from av.codec.context cimport CodecContext @@ -13,6 +13,8 @@ cdef class Stream(object): # 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.pyx b/av/stream.pyx index 971eaded1..a9742aec4 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -1,11 +1,12 @@ import warnings from cpython cimport PyWeakref_NewRef -from libc.stdint cimport int64_t, uint8_t +from libc.stdint cimport int32_t, int64_t, uint8_t from libc.string cimport memcpy cimport libav as lib from av.codec.context cimport wrap_codec_context +from av.enum cimport define_enum from av.error cimport err_check from av.packet cimport Packet from av.utils cimport ( @@ -20,6 +21,11 @@ from av.deprecation import AVDeprecationWarning cdef object _cinit_bypass_sentinel = object() +SideData = define_enum('SideData', __name__, ( + ('DISPLAYMATRIX', lib.AV_PKT_DATA_DISPLAYMATRIX , + """Display Matrix"""), + # TODO: put all others from here https://ffmpeg.org/doxygen/trunk/group__lavc__packet.html#ga9a80bfcacc586b483a973272800edb97 +)) cdef Stream wrap_stream(Container container, lib.AVStream *c_stream, CodecContext codec_context): """Build an av.Stream for an existing AVStream. @@ -83,6 +89,17 @@ cdef class Stream(object): if self.codec_context: self.codec_context.stream_index = stream.index + self.nb_side_data = stream.nb_side_data + if self.nb_side_data: + self.side_data = {} + for i in range(self.nb_side_data): + # Get side_data that we know how to get + if SideData.get(stream.side_data[i].type): + # Use dumpsidedata maybe here I guess : https://www.ffmpeg.org/doxygen/trunk/dump_8c_source.html#l00430 + self.side_data[SideData.get(stream.side_data[i].type)] = lib.av_display_rotation_get(stream.side_data[i].data) + else: + self.side_data = None + self.metadata = avdict_to_dict( stream.metadata, encoding=self.container.metadata_encoding, @@ -107,6 +124,10 @@ cdef class Stream(object): AVDeprecationWarning ) + + if name == 'side_data': + return self.side_data + # Convenience getter for codec context properties. if self.codec_context is not None: return getattr(self.codec_context, name) diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 0334b18e4..ac0dedd8b 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -264,6 +264,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 06029d9f9..fcc0e3291 100644 --- a/include/libavformat/avformat.pxd +++ b/include/libavformat/avformat.pxd @@ -48,6 +48,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 50e6bfffd..c9e51723f 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) From a1acb42f188b4386dad2cf0bf2e37d871f74e664 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 19 Oct 2023 14:11:11 -0400 Subject: [PATCH 169/192] Update GA --- .github/workflows/tests.yml | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bc4470639..53dc0e6d7 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,7 +1,5 @@ name: tests on: - release: - types: [created] push: paths-ignore: - '**.md' @@ -102,18 +100,6 @@ jobs: python -m av --version # Assert it can import. scripts/test - - name: Examples - if: matrix.config.extras - run: | - . 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 }} @@ -249,7 +235,7 @@ jobs: name: dist path: dist/ - name: Publish to PyPI - if: github.event_name == 'release' + if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/') uses: pypa/gh-action-pypi-publish@master with: user: __token__ From 34de59f7be30ad4dd8f60f9ea5d7029c37cc168d Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 19 Oct 2023 21:31:08 -0400 Subject: [PATCH 170/192] Remove unused vagrant tests --- .gitignore | 1 - Makefile | 18 +----------------- scripts/activate.sh | 8 -------- scripts/test | 6 ------ scripts/vagrant-test | 7 ------- 5 files changed, 1 insertion(+), 39 deletions(-) delete mode 100755 scripts/vagrant-test diff --git a/.gitignore b/.gitignore index 3fb27a301..908942ba1 100644 --- a/.gitignore +++ b/.gitignore @@ -31,7 +31,6 @@ /src # Testing. -/.vagrant /sandbox /tests/assets /tests/samples diff --git a/Makefile b/Makefile index 76b4fee6c..eec4c7ee2 100644 --- a/Makefile +++ b/Makefile @@ -38,33 +38,17 @@ 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 -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 - - clean-build: - rm -rf build - find av -name '*.so' -delete clean-sandbox: - rm -rf sandbox/201* - - rm sandbox/last + - rm -f sandbox/last clean-src: - rm -rf src diff --git a/scripts/activate.sh b/scripts/activate.sh index 167266549..6e97dda24 100755 --- a/scripts/activate.sh +++ b/scripts/activate.sh @@ -9,8 +9,6 @@ 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" else @@ -65,15 +63,9 @@ 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 -if [[ ! "$PYAV_LIBRARY_BUILD_ROOT" && -d /vagrant ]]; then - # On Vagrant, building the library in the shared directory causes some - # problems, so we move it to the user's home. - PYAV_LIBRARY_ROOT="/home/vagrant/vendor" -fi export PYAV_LIBRARY_ROOT="${PYAV_LIBRARY_ROOT-$PYAV_ROOT/vendor}" export PYAV_LIBRARY_BUILD="${PYAV_LIBRARY_BUILD-$PYAV_LIBRARY_ROOT/build}" export PYAV_LIBRARY_PREFIX="$PYAV_LIBRARY_BUILD/$PYAV_LIBRARY" diff --git a/scripts/test b/scripts/test index 0ed7eb862..9eb8fa9ed 100755 --- a/scripts/test +++ b/scripts/test @@ -23,12 +23,10 @@ if istest black; then 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 @@ -41,10 +39,6 @@ if istest sdist; then $PYAV_PYTHON setup.py sdist fi -if istest doctest; then - make -C docs test -fi - if istest examples; then for name in $(find examples -name '*.py'); do echo 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 77136a69216ad191a07be21ea0d4100d49b53009 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 21 Oct 2023 05:01:34 -0400 Subject: [PATCH 171/192] Try using ffmpeg 6.0 wheels --- .github/workflows/tests.yml | 16 +++++----------- scripts/fetch-vendor.py | 15 +++++++++------ 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 53dc0e6d7..75ed7c4a2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,14 +12,8 @@ on: - '**.txt' jobs: style: - name: "${{ matrix.config.suite }}" + name: "Style" runs-on: ubuntu-latest - strategy: - matrix: - config: - - {suite: black} - - {suite: flake8} - - {suite: isort} env: PYAV_PYTHON: python3 PYAV_LIBRARY: ffmpeg-6.0 @@ -39,10 +33,12 @@ jobs: . scripts/activate.sh python -m pip install black flake8 isort - - name: "${{ matrix.config.suite }}" + - name: "Test Style" run: | . scripts/activate.sh - ./scripts/test ${{ matrix.config.suite }} + ./scripts/test black + ./scripts/test flake8 + ./scripts/test isort nix: name: "py-${{ matrix.config.python }} lib-${{ matrix.config.ffmpeg }} ${{matrix.config.os}}" @@ -129,8 +125,6 @@ jobs: 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 else conda install -q -n pyav ffmpeg=${{ matrix.config.ffmpeg }} fi diff --git a/scripts/fetch-vendor.py b/scripts/fetch-vendor.py index 765201cf1..6eea55f49 100644 --- a/scripts/fetch-vendor.py +++ b/scripts/fetch-vendor.py @@ -6,22 +6,26 @@ import subprocess -def get_platform(): +def get_url(): system = platform.system() machine = platform.machine() if system == "Linux": - return f"manylinux_{machine}" + plat = f"manylinux_{machine}" if 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}" + plat = f"macosx_{machine}" + + if system == "Linux" or system == "Darwin": + return f"https://github.com/WyattBlue/pyav-ffmpeg/releases/download/6.0-1/ffmpeg-{plat}.tar.gz" if system == "Windows": - return "win_amd64" if calcsize("P") * 8 == 64 else "win32" + plat = "win_amd64" if calcsize("P") * 8 == 64 else "win32" + return f"https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.1.2-1/ffmpeg-{plat}.tar.gz" raise Exception(f"Unsupported system {system}") @@ -36,8 +40,7 @@ def get_platform(): shutil.rmtree(args.destination_dir) os.makedirs(args.destination_dir) -tarball_url = f"https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.1.2-1/ffmpeg-{get_platform()}.tar.gz" - +tarball_url = get_url() tarball_name = tarball_url.split("/")[-1] tarball_file = os.path.join(args.cache_dir, tarball_name) From 0d2992b482aeac9cd85376007b727eb1dce4d2a4 Mon Sep 17 00:00:00 2001 From: Roland van Laar Date: Tue, 24 Oct 2023 17:46:08 +0200 Subject: [PATCH 172/192] Expose bits_per_coded_sample on VideoCodecContext `bits_per_coded_sample` needs to be set when decoding qtrle frames. --- av/video/codeccontext.pyx | 8 ++++++++ include/libavcodec/avcodec.pxd | 2 ++ 2 files changed, 10 insertions(+) diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.pyx index 8dac3b3fe..b9966da9d 100644 --- a/av/video/codeccontext.pyx +++ b/av/video/codeccontext.pyx @@ -93,6 +93,14 @@ cdef class VideoCodecContext(CodecContext): self.ptr.height = value self._build_format() + property bits_per_coded_sample: + def __get__(self): + return self.ptr.bits_per_coded_sample + + def __set__(self, unsigned int value): + self.ptr.bits_per_coded_sample = value + self._build_format() + property pix_fmt: """ The pixel format's name. diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index ac0dedd8b..b48e746b4 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -175,6 +175,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 From a5b49d952fedec881d593e2b6be2ffc9bbd9a01a Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 24 Oct 2023 14:53:40 -0400 Subject: [PATCH 173/192] Improve tests --- av/__main__.py | 2 - av/datasets.py | 2 - tests/common.py | 86 +++++++++++++++++++----------------------- tests/test_doctests.py | 59 ----------------------------- 4 files changed, 38 insertions(+), 111 deletions(-) delete mode 100644 tests/test_doctests.py 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 bf610b89b..5c189365a 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/tests/common.py b/tests/common.py index e53537471..38ee0a0fb 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 @@ -147,7 +146,7 @@ def assertNdarraysEqual(self, a, b): a[it.multi_index], b[it.multi_index], ) - self.fail("ndarrays contents differ\n%s" % msg) + self.fail(f"ndarrays contents differ\n{msg}") def assertImagesAlmostEqual(self, a, b, epsilon=0.1, *args): self.assertEqual(a.size, b.size, "sizes dont match") @@ -156,49 +155,40 @@ def assertImagesAlmostEqual(self, a, b, epsilon=0.1, *args): 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) - ) - - # 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}".format(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}".format(a, b)) - - def assertNotIn(self, a, b, msg=None): - if a in b: - self.fail(msg or "{!r} in {!r}".format(a, b)) - - def assertIsInstance(self, instance, types, msg=None): - if not isinstance(instance, types): - self.fail( - msg or "not an instance of {!r}; {!r}".format(types, instance) - ) - - def assertNotIsInstance(self, instance, types, msg=None): - if isinstance(instance, types): - self.fail(msg or "is an instance of {!r}; {!r}".format(types, instance)) + self.fail(f"images differed by {diff} at index {i}; {ax} {bx}") + + 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 f"both are {type(a)!r} at 0x{id(a):x}; {a!r}") + + def assertIsNone(self, x, msg=None): + if x is not None: + self.fail(msg or f"is not None; {x!r}") + + def assertIsNotNone(self, x, msg=None): + if x is None: + self.fail(msg or f"is None; {x!r}") + + def assertIn(self, a, b, msg=None): + if a not in b: + self.fail(msg or f"{a!r} not in {b!r}") + + def assertNotIn(self, a, b, msg=None): + if a in b: + self.fail(msg or f"{a!r} in {b!r}") + + def assertIsInstance(self, instance, types, msg=None): + if not isinstance(instance, types): + self.fail(msg or f"not an instance of {types!r}; {instance!r}") + + def assertNotIsInstance(self, instance, types, msg=None): + if isinstance(instance, types): + self.fail(msg or f"is an instance of {types!r}; {instance!r}") diff --git a/tests/test_doctests.py b/tests/test_doctests.py deleted file mode 100644 index 07cfdd574..000000000 --- a/tests/test_doctests.py +++ /dev/null @@ -1,59 +0,0 @@ -from unittest import TestCase -import doctest -import pkgutil -import re - -import av - - -def fix_doctests(suite): - for case in suite._tests: - # Add some more flags. - case._dt_optionflags = ( - (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" - ) - - 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=[""]) - - try: - suite = doctest.DocTestSuite(mod) - except ValueError: - return - - fix_doctests(suite) - - 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("_")) - func.__name__ = name - setattr(cls, name, func) - - globals()[cls_name] = cls - - -for importer, mod_name, ispkg in pkgutil.walk_packages( - path=av.__path__, prefix=av.__name__ + ".", onerror=lambda x: None -): - register_doctests(mod_name) From 3c4e9327ccb29399260f426d2bfb6dd7d9687852 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 24 Oct 2023 15:54:16 -0400 Subject: [PATCH 174/192] readme, build-deps --- README.md | 3 ++- av/about.py | 2 +- scripts/build-deps | 1 + scripts/fetch-vendor.py | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3f2a746f4..38f0d315c 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,8 @@ source scripts/activate.sh pip install -U -r tests/requirements.txt ./scripts/build-deps make -# optional: make test +deactivate +pip install . ``` ## Motivations For a Fork diff --git a/av/about.py b/av/about.py index 10db32a35..80314c5f3 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "11.2.0" +__version__ = "11.3.0" 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/fetch-vendor.py b/scripts/fetch-vendor.py index 6eea55f49..bdd9e37c7 100644 --- a/scripts/fetch-vendor.py +++ b/scripts/fetch-vendor.py @@ -21,7 +21,7 @@ def get_url(): plat = f"macosx_{machine}" if system == "Linux" or system == "Darwin": - return f"https://github.com/WyattBlue/pyav-ffmpeg/releases/download/6.0-1/ffmpeg-{plat}.tar.gz" + return f"https://github.com/WyattBlue/pyav-ffmpeg/releases/download/6.0-2/ffmpeg-{plat}.tar.gz" if system == "Windows": plat = "win_amd64" if calcsize("P") * 8 == 64 else "win32" From 60df52b00d52950862f1eb1979d9a17d02e1567b Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Wed, 25 Oct 2023 11:07:37 -0600 Subject: [PATCH 175/192] Specify version for pypa/gh-action-pypi-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 75ed7c4a2..2cc85d062 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -230,7 +230,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@v1 with: user: __token__ password: ${{ secrets.PYPI_PASSWORD }} From 8fc8c1e6940a4cbbc9e4481938dae5f6901ea362 Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Wed, 25 Oct 2023 11:07:50 -0600 Subject: [PATCH 176/192] Upgrade actions/checkout --- .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 2cc85d062..7ecb43125 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,7 +18,7 @@ jobs: PYAV_PYTHON: python3 PYAV_LIBRARY: ffmpeg-6.0 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 name: Checkout - name: Python uses: actions/setup-python@v4 @@ -55,7 +55,7 @@ jobs: PYAV_LIBRARY: ffmpeg-${{ matrix.config.ffmpeg }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 name: Checkout - name: Python ${{ matrix.config.python }} @@ -109,7 +109,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Conda shell: bash @@ -149,7 +149,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.9 @@ -183,7 +183,7 @@ jobs: - os: windows-latest arch: AMD64 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: 3.9 @@ -223,7 +223,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 6a6bfd87dcc8eb7c341db99419c7ac1418d2070c Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Wed, 25 Oct 2023 11:45:53 -0600 Subject: [PATCH 177/192] Allow setting is_keyframe, is_corrupt and * Add __set__ for is_keyframe and is_corrupt * Add __set__ for dts in AVFrame --- av/frame.pyx | 6 ++++++ av/packet.pyx | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/av/frame.pyx b/av/frame.pyx index 3717ddc81..7f5599bba 100644 --- a/av/frame.pyx +++ b/av/frame.pyx @@ -77,6 +77,12 @@ cdef class Frame(object): 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. 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 0d99ae549bd9f1ad0c5e3abfe2dbac183fc234ce Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Wed, 25 Oct 2023 11:48:57 -0600 Subject: [PATCH 178/192] pypa/gh-action-pypi-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 7ecb43125..993343d3d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -230,7 +230,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@v1 + uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.PYPI_PASSWORD }} From 6d5b37535add7882fcea54760af198914eeff541 Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Wed, 25 Oct 2023 14:37:15 -0600 Subject: [PATCH 179/192] Set time_base for AudioResampler (#11) --- av/audio/resampler.pyx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/av/audio/resampler.pyx b/av/audio/resampler.pyx index b1c6c0aad..359a4a5bc 100644 --- a/av/audio/resampler.pyx +++ b/av/audio/resampler.pyx @@ -72,10 +72,14 @@ cdef class AudioResampler(object): # 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, From 672944a9aba5f993dd88a175570ba289fd606f6c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 25 Oct 2023 16:54:49 -0400 Subject: [PATCH 180/192] See if we can not inherit from object --- av/format.pyx | 49 +++++++++++++++---------------------------------- av/frame.pyx | 8 +------- 2 files changed, 16 insertions(+), 41 deletions(-) diff --git a/av/format.pyx b/av/format.pyx index 05a5402a8..2dece5325 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 @@ -18,31 +18,24 @@ cdef ContainerFormat build_container_format(lib.AVInputFormat* iptr, lib.AVOutpu 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."""), + ('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."""), + "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"""), + ('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"""), + "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"""), + "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"""), + "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."""), @@ -54,23 +47,12 @@ Flags = define_enum('Flags', __name__, ( 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) -cdef class ContainerFormat(object): - - """Descriptor of a container format. - - :param str name: The name of the format. - :param str mode: ``'r'`` or ``'w'`` for input and output formats; defaults - to None which will grab either. - - """ - +cdef class ContainerFormat: def __cinit__(self, name, mode=None): - if name is _cinit_bypass_sentinel: return @@ -90,7 +72,7 @@ cdef class ContainerFormat(object): raise ValueError('no container format %r' % name) def __repr__(self): - return '' % (self.__class__.__name__, self.name) + return f"" property descriptor: def __get__(self): @@ -199,5 +181,4 @@ cdef get_input_format_names(): 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/frame.pyx b/av/frame.pyx index 7f5599bba..417e5edf3 100644 --- a/av/frame.pyx +++ b/av/frame.pyx @@ -5,13 +5,7 @@ from fractions import Fraction from av.sidedata.sidedata import SideDataContainer -cdef class Frame(object): - """ - Base class for audio and video frames. - - See also :class:`~av.audio.frame.AudioFrame` and :class:`~av.video.frame.VideoFrame`. - """ - +cdef class Frame: def __cinit__(self, *args, **kwargs): with nogil: self.ptr = lib.av_frame_alloc() From 660597552b60b5d20dfc90769d83b3fbbeb1c2e4 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 25 Oct 2023 17:30:35 -0400 Subject: [PATCH 181/192] Remove all pointless object inherits --- av/__init__.py | 2 +- av/audio/format.pxd | 2 +- av/audio/format.pyx | 9 +++------ av/audio/layout.pxd | 5 ++--- av/audio/layout.pyx | 7 ++----- av/audio/resampler.pxd | 3 +-- av/audio/resampler.pyx | 3 +-- av/buffer.pxd | 3 +-- av/buffer.pyx | 9 +-------- av/bytesource.pxd | 3 +-- av/bytesource.pyx | 3 +-- av/codec/codec.pxd | 2 +- av/codec/codec.pyx | 4 +--- av/codec/context.pxd | 2 +- av/codec/context.pyx | 3 +-- av/container/core.pxd | 2 +- av/container/core.pyx | 3 +-- av/container/pyio.pxd | 2 +- av/container/pyio.pyx | 4 +--- av/container/streams.pxd | 2 +- av/container/streams.pyx | 3 +-- av/datasets.py | 24 ++++++------------------ av/descriptor.pxd | 2 +- av/descriptor.pyx | 3 +-- av/dictionary.pxd | 3 +-- av/dictionary.pyx | 3 +-- av/enum.pyx | 7 ++----- av/error.pyx | 26 -------------------------- av/filter/context.pxd | 2 +- av/filter/context.pyx | 4 +--- av/filter/filter.pxd | 3 +-- av/filter/filter.pyx | 5 ++--- av/filter/graph.pxd | 3 +-- av/filter/graph.pyx | 35 +---------------------------------- av/filter/link.pxd | 2 +- av/filter/link.pyx | 3 +-- av/filter/pad.pxd | 4 +--- av/filter/pad.pyx | 6 +----- av/format.pxd | 2 +- av/frame.pxd | 3 +-- av/logging.pyx | 15 +-------------- av/option.pxd | 5 +---- av/option.pyx | 4 +--- av/packet.pyx | 2 -- av/plane.pxd | 1 - av/sidedata/motionvectors.pxd | 3 +-- av/sidedata/motionvectors.pyx | 7 ++----- av/sidedata/sidedata.pxd | 3 +-- av/sidedata/sidedata.pyx | 10 +--------- av/stream.pxd | 2 +- av/stream.pyx | 2 +- av/subtitles/subtitle.pxd | 12 ++++-------- av/subtitles/subtitle.pyx | 16 ++++------------ av/utils.pyx | 1 - av/video/format.pxd | 4 ++-- av/video/format.pyx | 14 ++------------ av/video/reformatter.pxd | 3 +-- av/video/reformatter.pyx | 2 +- 58 files changed, 74 insertions(+), 248 deletions(-) diff --git a/av/__init__.py b/av/__init__.py index 8c87e17cc..e514682da 100644 --- a/av/__init__.py +++ b/av/__init__.py @@ -6,7 +6,7 @@ # 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 sys.version_info[:2] == (3, 9) and os.path.exists(os.path.join(sys.base_prefix, "conda-meta")) ): os.environ["PATH"] = ( 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..a6487b520 100644 --- a/av/audio/format.pyx +++ b/av/audio/format.pyx @@ -17,12 +17,10 @@ cdef AudioFormat get_audio_format(lib.AVSampleFormat c_format): return format -cdef class AudioFormat(object): - +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(object): 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(object): 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(object): """ def __get__(self): - if self.is_planar: raise ValueError('no planar container formats') diff --git a/av/audio/layout.pxd b/av/audio/layout.pxd index 60c8c953d..b62db4e28 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,8 +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..183ce69b8 100644 --- a/av/audio/layout.pyx +++ b/av/audio/layout.pyx @@ -64,10 +64,8 @@ cdef dict channel_descriptions = { } -cdef class AudioLayout(object): - +cdef class AudioLayout: def __init__(self, layout): - if layout is _cinit_bypass_sentinel: return @@ -105,8 +103,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..aeeab11ea 100644 --- a/av/audio/resampler.pxd +++ b/av/audio/resampler.pxd @@ -4,8 +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 cdef AudioFrame template diff --git a/av/audio/resampler.pyx b/av/audio/resampler.pyx index 359a4a5bc..0859e6df2 100644 --- a/av/audio/resampler.pyx +++ b/av/audio/resampler.pyx @@ -7,8 +7,7 @@ import errno import av.filter -cdef class AudioResampler(object): - +cdef class AudioResampler: """AudioResampler(format=None, layout=None, rate=None) :param AudioFormat format: The target format, or string that parses to one diff --git a/av/buffer.pxd b/av/buffer.pxd index 199d2cc8b..40dd2eabc 100644 --- a/av/buffer.pxd +++ b/av/buffer.pxd @@ -1,6 +1,5 @@ -cdef class Buffer(object): - +cdef class Buffer: cdef size_t _buffer_size(self) cdef void* _buffer_ptr(self) cdef bint _buffer_writable(self) diff --git a/av/buffer.pyx b/av/buffer.pyx index 8176e1565..19912815c 100644 --- a/av/buffer.pyx +++ b/av/buffer.pyx @@ -5,13 +5,7 @@ from av import deprecation from av.bytesource cimport ByteSource, bytesource -cdef class Buffer(object): - - """A base class for PyAV objects which support the buffer protocol, such - as :class:`.Packet` and :class:`.Plane`. - - """ - +cdef class Buffer: cdef size_t _buffer_size(self): return 0 @@ -28,7 +22,6 @@ cdef class Buffer(object): @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 index 68a6cca0f..ddf348394 100644 --- a/av/bytesource.pxd +++ b/av/bytesource.pxd @@ -1,8 +1,7 @@ from cpython.buffer cimport Py_buffer -cdef class ByteSource(object): - +cdef class ByteSource: cdef object owner cdef bint has_view diff --git a/av/bytesource.pyx b/av/bytesource.pyx index fd29bcf06..bb826c36b 100644 --- a/av/bytesource.pyx +++ b/av/bytesource.pyx @@ -6,8 +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 24852e94f..6672e412e 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: """ name: str mode: "r" | "w" @@ -183,7 +183,6 @@ cdef class Codec(object): raise RuntimeError("Found codec does not match mode.", name, mode) cdef _init(self, name=None): - if not self.ptr: raise UnknownCodecError(name) @@ -199,7 +198,6 @@ cdef class Codec(object): raise RuntimeError('%s is both encoder and decoder.') def create(self): - """Create a :class:`.CodecContext` for this codec.""" from .context import CodecContext return CodecContext.create(self) 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..597c30b31 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): @@ -389,7 +389,6 @@ cdef class CodecContext(object): return packets cdef _send_frame_and_recv(self, Frame frame): - cdef Packet packet cdef int res 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 b5e7c9c43..f416a4e44 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -166,8 +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, metadata_encoding, metadata_errors, 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..7b550737c 100644 --- a/av/container/pyio.pyx +++ b/av/container/pyio.pyx @@ -7,10 +7,8 @@ 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): - self.file = file cdef seek_func_t seek_func = NULL 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..d98686642 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: """ @@ -95,7 +95,6 @@ cdef class StreamContainer(object): selection = [] for x in _flatten((args, kwargs)): - if x is None: pass diff --git a/av/datasets.py b/av/datasets.py index 5c189365a..94539db11 100644 --- a/av/datasets.py +++ b/av/datasets.py @@ -8,7 +8,7 @@ log = logging.getLogger(__name__) -def iter_data_dirs(check_writable=False): +def iter_data_dirs(check_writable: bool = False): try: yield os.environ["PYAV_TESTDATA_DIR"] except KeyError: @@ -43,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 @@ -97,27 +97,15 @@ def cached_download(url, name): return path -def fate(name): - """Download and return a path to a sample from the FFmpeg test suite. - - Data is handled by :func:`cached_download`. - - See the `FFmpeg Automated Test Environment `_ - - """ +def fate(name: str) -> str: return cached_download( - "http://fate.ffmpeg.org/fate-suite/" + name, + f"http://fate.ffmpeg.org/fate-suite/{name}", os.path.join("fate-suite", name.replace("/", os.path.sep)), ) -def curated(name): - """Download and return a path to a sample that is curated by the PyAV developers. - - Data is handled by :func:`cached_download`. - - """ +def curated(name: str) -> str: return cached_download( - "https://pyav.org/datasets/" + name, + f"https://pyav.org/datasets/{name}", os.path.join("pyav-curated", name.replace("/", os.path.sep)), ) 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..5c1ac3cf4 100644 --- a/av/descriptor.pyx +++ b/av/descriptor.pyx @@ -13,8 +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: raise RuntimeError('Cannot construct av.Descriptor') diff --git a/av/dictionary.pxd b/av/dictionary.pxd index 84cb24068..be6b2d286 100644 --- a/av/dictionary.pxd +++ b/av/dictionary.pxd @@ -1,8 +1,7 @@ cimport libav as lib -cdef class _Dictionary(object): - +cdef class _Dictionary: cdef lib.AVDictionary *ptr cpdef _Dictionary copy(self) diff --git a/av/dictionary.pyx b/av/dictionary.pyx index d88ccebcd..892eb6fc7 100644 --- a/av/dictionary.pyx +++ b/av/dictionary.pyx @@ -3,8 +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: self.update(arg) diff --git a/av/enum.pyx b/av/enum.pyx index 85f1b0748..fdfd5946f 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 @@ -251,7 +251,6 @@ cdef class EnumItem(object): 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. @@ -322,8 +321,7 @@ cdef class EnumFlag(EnumItem): return bool(self.value) -cdef class EnumProperty(object): - +cdef class EnumProperty: cdef object enum cdef object fget cdef object fset @@ -378,7 +376,6 @@ cdef class EnumProperty(object): cpdef define_enum(name, module, items, bint is_flags=False): - if is_flags: base_cls = EnumFlag else: diff --git a/av/error.pyx b/av/error.pyx index cde4fec7f..16f98474e 100644 --- a/av/error.pyx +++ b/av/error.pyx @@ -50,31 +50,6 @@ cpdef tag_to_code(bytes tag): class FFmpegError(Exception): - - """Exception class for errors from within FFmpeg. - - .. attribute:: errno - - FFmpeg's integer error code. - - .. attribute:: strerror - - FFmpeg's error message. - - .. attribute:: filename - - 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``. - - """ - def __init__(self, code, message, filename=None, log=None): args = [code, message] if filename or log: @@ -108,7 +83,6 @@ class FFmpegError(Exception): pass def __str__(self): - msg = f'[Errno {self.errno}] {self.strerror}' if self.filename: 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..7dea198f7 100644 --- a/av/filter/context.pyx +++ b/av/filter/context.pyx @@ -21,8 +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: raise RuntimeError('cannot construct FilterContext') @@ -52,7 +51,6 @@ cdef class FilterContext(object): return self._outputs def init(self, args=None, **kwargs): - if self.inited: raise ValueError('already inited') if args and kwargs: diff --git a/av/filter/filter.pxd b/av/filter/filter.pxd index e3d937e7c..d09153090 100644 --- a/av/filter/filter.pxd +++ b/av/filter/filter.pxd @@ -3,8 +3,7 @@ cimport libav as lib from av.descriptor cimport Descriptor -cdef class Filter(object): - +cdef class Filter: cdef const lib.AVFilter *ptr cdef object _inputs diff --git a/av/filter/filter.pyx b/av/filter/filter.pyx index f5b5f1eee..57ea52542 100644 --- a/av/filter/filter.pyx +++ b/av/filter/filter.pyx @@ -21,8 +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: return @@ -30,7 +29,7 @@ cdef class Filter(object): 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.pxd b/av/filter/graph.pxd index e01536527..b3bf352a3 100644 --- a/av/filter/graph.pxd +++ b/av/filter/graph.pxd @@ -3,8 +3,7 @@ cimport libav as lib from av.filter.context cimport FilterContext -cdef class Graph(object): - +cdef class Graph: cdef lib.AVFilterGraph *ptr cdef readonly bint configured diff --git a/av/filter/graph.pyx b/av/filter/graph.pyx index bcb49f788..c107e34ed 100644 --- a/av/filter/graph.pyx +++ b/av/filter/graph.pyx @@ -11,10 +11,8 @@ from av.video.format cimport VideoFormat from av.video.frame cimport VideoFrame -cdef class Graph(object): - +cdef class Graph: def __cinit__(self): - self.ptr = lib.avfilter_graph_alloc() self.configured = False self._name_counts = {} @@ -41,41 +39,13 @@ cdef class Graph(object): 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 +94,6 @@ cdef class Graph(object): 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 +164,6 @@ cdef class Graph(object): 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 +179,6 @@ cdef class Graph(object): contexts[0].push(frame) def pull(self): - vsinks = self._context_by_type.get('buffersink', []) asinks = self._context_by_type.get('abuffersink', []) 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..362235e9b 100644 --- a/av/filter/link.pyx +++ b/av/filter/link.pyx @@ -6,8 +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: raise RuntimeError('cannot instantiate FilterLink') diff --git a/av/filter/pad.pxd b/av/filter/pad.pxd index 088c0b0c0..5a1c6586c 100644 --- a/av/filter/pad.pxd +++ b/av/filter/pad.pxd @@ -5,8 +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 cdef readonly bint is_input @@ -16,7 +15,6 @@ cdef class FilterPad(object): cdef class FilterContextPad(FilterPad): - cdef FilterLink _link diff --git a/av/filter/pad.pyx b/av/filter/pad.pyx index 64ec9a6b1..86600d733 100644 --- a/av/filter/pad.pyx +++ b/av/filter/pad.pyx @@ -4,8 +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: raise RuntimeError('cannot construct FilterPad') @@ -40,9 +39,7 @@ cdef class FilterPad(object): cdef class FilterContextPad(FilterPad): - def __repr__(self): - return '' % ( self.filter.name, 'inputs' if self.is_input else 'outputs', @@ -71,7 +68,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.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/frame.pxd b/av/frame.pxd index e0d5b4280..e3d8269f3 100644 --- a/av/frame.pxd +++ b/av/frame.pxd @@ -4,8 +4,7 @@ from av.packet cimport Packet from av.sidedata.sidedata cimport _SideDataContainer -cdef class Frame(object): - +cdef class Frame: cdef lib.AVFrame *ptr # We define our own time. diff --git a/av/logging.pyx b/av/logging.pyx index 2253560ad..2237b116f 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. @@ -188,7 +188,6 @@ cdef class Capture(object): cdef list captures def __init__(self, bint local=True): - self.logs = [] if local: @@ -216,12 +215,6 @@ cdef lib.AVClass log_class log_class.item_name = log_context_name cpdef log(int level, str name, str message): - """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.name = name @@ -230,7 +223,6 @@ cpdef log(int level, str name, str message): 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 @@ -259,7 +251,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 +263,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 +284,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 +317,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.pxd b/av/option.pxd index e455bff03..5f5a4c025 100644 --- a/av/option.pxd +++ b/av/option.pxd @@ -1,18 +1,15 @@ cimport libav as lib -cdef class BaseOption(object): - +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 diff --git a/av/option.pyx b/av/option.pyx index 24f945bf2..2030d8d3e 100644 --- a/av/option.pyx +++ b/av/option.pyx @@ -59,8 +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: raise RuntimeError('Cannot construct av.%s' % self.__class__.__name__) @@ -105,7 +104,6 @@ cdef class BaseOption(object): cdef class Option(BaseOption): - property type: def __get__(self): return OptionType._get(self.ptr.type, create=True) diff --git a/av/packet.pyx b/av/packet.pyx index af77e2c10..a7b450a7d 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 @@ -64,7 +63,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.pxd b/av/plane.pxd index df3847d7b..0cbdb3e1f 100644 --- a/av/plane.pxd +++ b/av/plane.pxd @@ -3,7 +3,6 @@ from av.frame cimport Frame cdef class Plane(Buffer): - cdef Frame frame cdef int index diff --git a/av/sidedata/motionvectors.pxd b/av/sidedata/motionvectors.pxd index 3b7f88bc1..96fd85746 100644 --- a/av/sidedata/motionvectors.pxd +++ b/av/sidedata/motionvectors.pxd @@ -10,7 +10,6 @@ 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..80c9cfca1 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 = {} @@ -17,7 +16,6 @@ cdef class _MotionVectors(SideData): return f'self.ptr.data:0x}' def __getitem__(self, int index): - try: return self._vectors[index] except KeyError: @@ -53,8 +51,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: raise RuntimeError('cannot manually instatiate MotionVector') @@ -63,7 +60,7 @@ cdef class MotionVector(object): self.ptr = base + index def __repr__(self): - return f'' + return f"" @property def source(self): diff --git a/av/sidedata/sidedata.pxd b/av/sidedata/sidedata.pxd index f30d8fef7..c5baddfa6 100644 --- a/av/sidedata/sidedata.pxd +++ b/av/sidedata/sidedata.pxd @@ -15,8 +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 cdef list _by_index diff --git a/av/sidedata/sidedata.pyx b/av/sidedata/sidedata.pyx index ec7de5997..def5d6791 100644 --- a/av/sidedata/sidedata.pyx +++ b/av/sidedata/sidedata.pyx @@ -27,11 +27,6 @@ Type = define_enum('Type', __name__, ( ('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), - )) @@ -45,7 +40,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') @@ -70,10 +64,8 @@ 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): - self.frame = frame self._by_index = [] self._by_type = {} diff --git a/av/stream.pxd b/av/stream.pxd index dcb0615b0..1ae5463f4 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 a9742aec4..0d93bff51 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -59,7 +59,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..b373a6a11 100644 --- a/av/subtitles/subtitle.pxd +++ b/av/subtitles/subtitle.pxd @@ -3,20 +3,17 @@ 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 cdef readonly bytes type @@ -31,8 +28,7 @@ cdef class BitmapSubtitle(Subtitle): cdef readonly planes -cdef class BitmapSubtitlePlane(object): - +cdef class BitmapSubtitlePlane: cdef readonly BitmapSubtitle subtitle cdef readonly int index cdef readonly long buffer_size diff --git a/av/subtitles/subtitle.pyx b/av/subtitles/subtitle.pyx index 2f22bb0be..60224fd09 100644 --- a/av/subtitles/subtitle.pyx +++ b/av/subtitles/subtitle.pyx @@ -1,13 +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 cdef int i @@ -63,8 +62,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: raise ValueError('subtitle rect index out of range') @@ -91,7 +89,6 @@ cdef class Subtitle(object): cdef class BitmapSubtitle(Subtitle): - def __cinit__(self, SubtitleSet subtitle, int index): self.planes = tuple( BitmapSubtitlePlane(self, i) @@ -131,10 +128,8 @@ cdef class BitmapSubtitle(Subtitle): return self.planes[i] -cdef class BitmapSubtitlePlane(object): - +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,13 +141,11 @@ cdef class BitmapSubtitlePlane(object): 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__, @@ -166,7 +159,6 @@ cdef class TextSubtitle(Subtitle): cdef class AssSubtitle(Subtitle): - def __repr__(self): return '<%s.%s %r at 0x%x>' % ( self.__class__.__module__, diff --git a/av/utils.pyx b/av/utils.pyx index 1894ce7ab..5964765a2 100644 --- a/av/utils.pyx +++ b/av/utils.pyx @@ -43,7 +43,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/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 9ad539101..556d9d3b4 100644 --- a/av/video/format.pyx +++ b/av/video/format.pyx @@ -19,17 +19,8 @@ cdef lib.AVPixelFormat get_pix_fmt(const char *name) except lib.AV_PIX_FMT_NONE: return pix_fmt -cdef class VideoFormat(object): - """ - - >>> format = VideoFormat('rgb24') - >>> format.name - 'rgb24' - - """ - +cdef class VideoFormat: def __cinit__(self, name, width=0, height=0): - if name is _cinit_bypass_sentinel: return @@ -122,8 +113,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 self.index = index diff --git a/av/video/reformatter.pxd b/av/video/reformatter.pxd index 25135c27a..579402046 100644 --- a/av/video/reformatter.pxd +++ b/av/video/reformatter.pxd @@ -3,8 +3,7 @@ cimport libav as lib from av.video.frame cimport VideoFrame -cdef class VideoReformatter(object): - +cdef class VideoReformatter: cdef lib.SwsContext *ptr cdef _reformat(self, VideoFrame frame, int width, int height, 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`. From 4bea10247262ef04b6aa091baec15b7f866c8b35 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 25 Oct 2023 17:44:53 -0400 Subject: [PATCH 182/192] Remove all deprecated methods and some deprecated attributes --- av/buffer.pyx | 15 --------------- av/packet.pyx | 15 --------------- av/stream.pyx | 11 ----------- tests/test_file_probing.py | 18 ------------------ 4 files changed, 59 deletions(-) diff --git a/av/buffer.pyx b/av/buffer.pyx index 19912815c..e8b94df8d 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 @@ -29,20 +28,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. diff --git a/av/packet.pyx b/av/packet.pyx index a7b450a7d..f5286d221 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): @@ -84,19 +82,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 stream_index: def __get__(self): return self.ptr.stream_index diff --git a/av/stream.pyx b/av/stream.pyx index 0d93bff51..7fd1962b1 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -16,8 +16,6 @@ from av.utils cimport ( to_avrational ) -from av.deprecation import AVDeprecationWarning - cdef object _cinit_bypass_sentinel = object() @@ -116,15 +114,6 @@ 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 - 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 - ) - - if name == 'side_data': return self.side_data diff --git a/tests/test_file_probing.py b/tests/test_file_probing.py index c67f7fb8e..2d6c87257 100644 --- a/tests/test_file_probing.py +++ b/tests/test_file_probing.py @@ -1,5 +1,4 @@ from fractions import Fraction -import warnings import av @@ -26,7 +25,6 @@ def test_container_probing(self): def test_stream_probing(self): stream = self.file.streams[0] - # check __repr__ self.assertTrue( str(stream).startswith( "= 4.2. ( "operational_pattern_ul", "060e2b34.04010102.0d010201.10030000", @@ -320,20 +316,6 @@ 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 a62d81f732401770939faff23df58fa93f44979b Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Thu, 26 Oct 2023 16:20:51 -0600 Subject: [PATCH 183/192] Add codec parameters (#15) --- include/libav.pxd | 2 + include/libavcodec/avcodec.pxd | 31 +++- include/libavcodec/defs.pxd | 18 ++ include/libavutil/avutil.pxd | 7 - include/libavutil/pixfmt.pxd | 314 +++++++++++++++++++++++++++++++++ 5 files changed, 356 insertions(+), 16 deletions(-) create mode 100644 include/libavcodec/defs.pxd create mode 100644 include/libavutil/pixfmt.pxd diff --git a/include/libav.pxd b/include/libav.pxd index b9bfe3943..9a5720cfd 100644 --- a/include/libav.pxd +++ b/include/libav.pxd @@ -3,9 +3,11 @@ include "libavutil/channel_layout.pxd" include "libavutil/dict.pxd" include "libavutil/error.pxd" include "libavutil/frame.pxd" +include "libavutil/pixfmt.pxd" include "libavutil/samplefmt.pxd" include "libavutil/motion_vector.pxd" +include "libavcodec/defs.pxd" include "libavcodec/avcodec.pxd" include "libavdevice/avdevice.pxd" include "libavformat/avformat.pxd" diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index b48e746b4..0fc250417 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -111,15 +111,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: AV_CODEC_ID_MPEG2VIDEO AV_CODEC_ID_MPEG1VIDEO - cdef enum AVDiscard: - AVDISCARD_NONE - AVDISCARD_DEFAULT - AVDISCARD_NONREF - AVDISCARD_BIDIR - AVDISCARD_NONINTRA - AVDISCARD_NONKEY - AVDISCARD_ALL - cdef struct AVCodec: char *name @@ -491,6 +482,28 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef struct AVCodecParameters: AVMediaType codec_type AVCodecID codec_id + uint32_t codec_tag + uint8_t *extradata + int extradata_size + int format + int64_t bit_rate + int bits_per_coded_sample + int bits_per_raw_sample + int profile + int level + int width + int height + AVRational sample_aspect_ratio + AVFieldOrder field_order + AVColorRange color_range + AVColorPrimaries color_primaries + AVColorTransferCharacteristic color_trc + AVColorSpace color_space + AVChromaLocation chroma_location + int video_delay + AVRational framerate + AVPacketSideData *coded_side_data + int nb_coded_side_data cdef int avcodec_parameters_copy( AVCodecParameters *dst, diff --git a/include/libavcodec/defs.pxd b/include/libavcodec/defs.pxd new file mode 100644 index 000000000..f2edb5844 --- /dev/null +++ b/include/libavcodec/defs.pxd @@ -0,0 +1,18 @@ +cdef extern from "libavcodec/defs.h" nogil: + + cdef enum AVFieldOrder: + AV_FIELD_UNKNOWN + AV_FIELD_PROGRESSIVE + AV_FIELD_TT + AV_FIELD_BB + AV_FIELD_TB + AV_FIELD_BT + + cdef enum AVDiscard: + AVDISCARD_NONE + AVDISCARD_DEFAULT + AVDISCARD_NONREF + AVDISCARD_BIDIR + AVDISCARD_NONINTRA + AVDISCARD_NONKEY + AVDISCARD_ALL diff --git a/include/libavutil/avutil.pxd b/include/libavutil/avutil.pxd index c9e51723f..245f57320 100644 --- a/include/libavutil/avutil.pxd +++ b/include/libavutil/avutil.pxd @@ -26,13 +26,6 @@ cdef extern from "libavutil/avutil.h" nogil: AV_PICTURE_TYPE_SP AV_PICTURE_TYPE_BI - cdef enum AVPixelFormat: - AV_PIX_FMT_NONE - AV_PIX_FMT_YUV420P - AV_PIX_FMT_RGB24 - PIX_FMT_RGB24 - PIX_FMT_RGBA - cdef enum AVRounding: AV_ROUND_ZERO AV_ROUND_INF diff --git a/include/libavutil/pixfmt.pxd b/include/libavutil/pixfmt.pxd new file mode 100644 index 000000000..877f5ea03 --- /dev/null +++ b/include/libavutil/pixfmt.pxd @@ -0,0 +1,314 @@ +cdef extern from "libavutil/pixfmt.h" nogil: + + cdef enum AVPixelFormat: + AV_PIX_FMT_NONE = -1 + AV_PIX_FMT_YUV420P + AV_PIX_FMT_YUYV422 + AV_PIX_FMT_RGB24 + AV_PIX_FMT_BGR24 + AV_PIX_FMT_YUV422P + AV_PIX_FMT_YUV444P + AV_PIX_FMT_YUV410P + AV_PIX_FMT_YUV411P + AV_PIX_FMT_GRAY8 + AV_PIX_FMT_MONOWHITE + AV_PIX_FMT_MONOBLACK + AV_PIX_FMT_PAL8 + AV_PIX_FMT_YUVJ420P + AV_PIX_FMT_YUVJ422P + AV_PIX_FMT_YUVJ444P + AV_PIX_FMT_UYVY422 + AV_PIX_FMT_UYYVYY411 + AV_PIX_FMT_BGR8 + AV_PIX_FMT_BGR4 + AV_PIX_FMT_BGR4_BYTE + AV_PIX_FMT_RGB8 + AV_PIX_FMT_RGB4 + AV_PIX_FMT_RGB4_BYTE + AV_PIX_FMT_NV12 + AV_PIX_FMT_NV21 + AV_PIX_FMT_ARGB + AV_PIX_FMT_RGBA + AV_PIX_FMT_ABGR + AV_PIX_FMT_BGRA + AV_PIX_FMT_GRAY16BE + AV_PIX_FMT_GRAY16LE + AV_PIX_FMT_YUV440P + AV_PIX_FMT_YUVJ440P + AV_PIX_FMT_YUVA420P + AV_PIX_FMT_RGB48BE + AV_PIX_FMT_RGB48LE + AV_PIX_FMT_RGB565BE + AV_PIX_FMT_RGB565LE + AV_PIX_FMT_RGB555BE + AV_PIX_FMT_RGB555LE + AV_PIX_FMT_BGR565BE + AV_PIX_FMT_BGR565LE + AV_PIX_FMT_BGR555BE + AV_PIX_FMT_BGR555LE + AV_PIX_FMT_VAAPI + AV_PIX_FMT_YUV420P16LE + AV_PIX_FMT_YUV420P16BE + AV_PIX_FMT_YUV422P16LE + AV_PIX_FMT_YUV422P16BE + AV_PIX_FMT_YUV444P16LE + AV_PIX_FMT_YUV444P16BE + AV_PIX_FMT_DXVA2_VLD + AV_PIX_FMT_RGB444LE + AV_PIX_FMT_RGB444BE + AV_PIX_FMT_BGR444LE + AV_PIX_FMT_BGR444BE + AV_PIX_FMT_YA8 + AV_PIX_FMT_Y400A = AV_PIX_FMT_YA8 + AV_PIX_FMT_GRAY8A= AV_PIX_FMT_YA8 + AV_PIX_FMT_BGR48BE + AV_PIX_FMT_BGR48LE + AV_PIX_FMT_YUV420P9BE + AV_PIX_FMT_YUV420P9LE + AV_PIX_FMT_YUV420P10BE + AV_PIX_FMT_YUV420P10LE + AV_PIX_FMT_YUV422P10BE + AV_PIX_FMT_YUV422P10LE + AV_PIX_FMT_YUV444P9BE + AV_PIX_FMT_YUV444P9LE + AV_PIX_FMT_YUV444P10BE + AV_PIX_FMT_YUV444P10LE + AV_PIX_FMT_YUV422P9BE + AV_PIX_FMT_YUV422P9LE + AV_PIX_FMT_GBRP + AV_PIX_FMT_GBR24P = AV_PIX_FMT_GBRP + AV_PIX_FMT_GBRP9BE + AV_PIX_FMT_GBRP9LE + AV_PIX_FMT_GBRP10BE + AV_PIX_FMT_GBRP10LE + AV_PIX_FMT_GBRP16BE + AV_PIX_FMT_GBRP16LE + AV_PIX_FMT_YUVA422P + AV_PIX_FMT_YUVA444P + AV_PIX_FMT_YUVA420P9BE + AV_PIX_FMT_YUVA420P9LE + AV_PIX_FMT_YUVA422P9BE + AV_PIX_FMT_YUVA422P9LE + AV_PIX_FMT_YUVA444P9BE + AV_PIX_FMT_YUVA444P9LE + AV_PIX_FMT_YUVA420P10BE + AV_PIX_FMT_YUVA420P10LE + AV_PIX_FMT_YUVA422P10BE + AV_PIX_FMT_YUVA422P10LE + AV_PIX_FMT_YUVA444P10BE + AV_PIX_FMT_YUVA444P10LE + AV_PIX_FMT_YUVA420P16BE + AV_PIX_FMT_YUVA420P16LE + AV_PIX_FMT_YUVA422P16BE + AV_PIX_FMT_YUVA422P16LE + AV_PIX_FMT_YUVA444P16BE + AV_PIX_FMT_YUVA444P16LE + AV_PIX_FMT_VDPAU + AV_PIX_FMT_XYZ12LE + AV_PIX_FMT_XYZ12BE + AV_PIX_FMT_NV16 + AV_PIX_FMT_NV20LE + AV_PIX_FMT_NV20BE + AV_PIX_FMT_RGBA64BE + AV_PIX_FMT_RGBA64LE + AV_PIX_FMT_BGRA64BE + AV_PIX_FMT_BGRA64LE + AV_PIX_FMT_YVYU422 + AV_PIX_FMT_YA16BE + AV_PIX_FMT_YA16LE + AV_PIX_FMT_GBRAP + AV_PIX_FMT_GBRAP16BE + AV_PIX_FMT_GBRAP16LE + AV_PIX_FMT_QSV + AV_PIX_FMT_MMAL + AV_PIX_FMT_D3D11VA_VLD + AV_PIX_FMT_CUDA + AV_PIX_FMT_0RGB + AV_PIX_FMT_RGB0 + AV_PIX_FMT_0BGR + AV_PIX_FMT_BGR0 + AV_PIX_FMT_YUV420P12BE + AV_PIX_FMT_YUV420P12LE + AV_PIX_FMT_YUV420P14BE + AV_PIX_FMT_YUV420P14LE + AV_PIX_FMT_YUV422P12BE + AV_PIX_FMT_YUV422P12LE + AV_PIX_FMT_YUV422P14BE + AV_PIX_FMT_YUV422P14LE + AV_PIX_FMT_YUV444P12BE + AV_PIX_FMT_YUV444P12LE + AV_PIX_FMT_YUV444P14BE + AV_PIX_FMT_YUV444P14LE + AV_PIX_FMT_GBRP12BE + AV_PIX_FMT_GBRP12LE + AV_PIX_FMT_GBRP14BE + AV_PIX_FMT_GBRP14LE + AV_PIX_FMT_YUVJ411P + AV_PIX_FMT_BAYER_BGGR8 + AV_PIX_FMT_BAYER_RGGB8 + AV_PIX_FMT_BAYER_GBRG8 + AV_PIX_FMT_BAYER_GRBG8 + AV_PIX_FMT_BAYER_BGGR16LE + AV_PIX_FMT_BAYER_BGGR16BE + AV_PIX_FMT_BAYER_RGGB16LE + AV_PIX_FMT_BAYER_RGGB16BE + AV_PIX_FMT_BAYER_GBRG16LE + AV_PIX_FMT_BAYER_GBRG16BE + AV_PIX_FMT_BAYER_GRBG16LE + AV_PIX_FMT_BAYER_GRBG16BE + AV_PIX_FMT_XVMC + AV_PIX_FMT_YUV440P10LE + AV_PIX_FMT_YUV440P10BE + AV_PIX_FMT_YUV440P12LE + AV_PIX_FMT_YUV440P12BE + AV_PIX_FMT_AYUV64LE + AV_PIX_FMT_AYUV64BE + AV_PIX_FMT_VIDEOTOOLBOX + AV_PIX_FMT_P010LE + AV_PIX_FMT_P010BE + AV_PIX_FMT_GBRAP12BE + AV_PIX_FMT_GBRAP12LE + AV_PIX_FMT_GBRAP10BE + AV_PIX_FMT_GBRAP10LE + AV_PIX_FMT_MEDIACODEC + AV_PIX_FMT_GRAY12BE + AV_PIX_FMT_GRAY12LE + AV_PIX_FMT_GRAY10BE + AV_PIX_FMT_GRAY10LE + AV_PIX_FMT_P016LE + AV_PIX_FMT_P016BE + AV_PIX_FMT_D3D11 + AV_PIX_FMT_GRAY9BE + AV_PIX_FMT_GRAY9LE + AV_PIX_FMT_GBRPF32BE + AV_PIX_FMT_GBRPF32LE + AV_PIX_FMT_GBRAPF32BE + AV_PIX_FMT_GBRAPF32LE + AV_PIX_FMT_DRM_PRIME + AV_PIX_FMT_OPENCL + AV_PIX_FMT_GRAY14BE + AV_PIX_FMT_GRAY14LE + AV_PIX_FMT_GRAYF32BE + AV_PIX_FMT_GRAYF32LE + AV_PIX_FMT_YUVA422P12BE + AV_PIX_FMT_YUVA422P12LE + AV_PIX_FMT_YUVA444P12BE + AV_PIX_FMT_YUVA444P12LE + AV_PIX_FMT_NV24 + AV_PIX_FMT_NV42 + AV_PIX_FMT_VULKAN + AV_PIX_FMT_Y210BE + AV_PIX_FMT_Y210LE + AV_PIX_FMT_X2RGB10LE + AV_PIX_FMT_X2RGB10BE + AV_PIX_FMT_X2BGR10LE + AV_PIX_FMT_X2BGR10BE + AV_PIX_FMT_P210BE + AV_PIX_FMT_P210LE + AV_PIX_FMT_P410BE + AV_PIX_FMT_P410LE + AV_PIX_FMT_P216BE + AV_PIX_FMT_P216LE + AV_PIX_FMT_P416BE + AV_PIX_FMT_P416LE + AV_PIX_FMT_VUYA + AV_PIX_FMT_RGBAF16BE + AV_PIX_FMT_RGBAF16LE + AV_PIX_FMT_VUYX + AV_PIX_FMT_P012LE + AV_PIX_FMT_P012BE + AV_PIX_FMT_Y212BE + AV_PIX_FMT_Y212LE + AV_PIX_FMT_XV30BE + AV_PIX_FMT_XV30LE + AV_PIX_FMT_XV36BE + AV_PIX_FMT_XV36LE + AV_PIX_FMT_RGBF32BE + AV_PIX_FMT_RGBF32LE + AV_PIX_FMT_RGBAF32BE + AV_PIX_FMT_RGBAF32LE + AV_PIX_FMT_P212BE + AV_PIX_FMT_P212LE + AV_PIX_FMT_P412BE + AV_PIX_FMT_P412LE + AV_PIX_FMT_GBRAP14BE + AV_PIX_FMT_GBRAP14LE + AV_PIX_FMT_NB + + cdef enum AVColorPrimaries: + AVCOL_PRI_RESERVED0 = 0 + AVCOL_PRI_BT709 = 1 + AVCOL_PRI_UNSPECIFIED = 2 + AVCOL_PRI_RESERVED = 3 + AVCOL_PRI_BT470M = 4 + AVCOL_PRI_BT470BG = 5 + AVCOL_PRI_SMPTE170M = 6 + AVCOL_PRI_SMPTE240M = 7 + AVCOL_PRI_FILM = 8 + AVCOL_PRI_BT2020 = 9 + AVCOL_PRI_SMPTE428 = 10 + AVCOL_PRI_SMPTEST428_1 = AVCOL_PRI_SMPTE428 + AVCOL_PRI_SMPTE431 = 11 + AVCOL_PRI_SMPTE432 = 12 + AVCOL_PRI_EBU3213 = 22 + AVCOL_PRI_JEDEC_P22 = AVCOL_PRI_EBU3213 + AVCOL_PRI_NB + + cdef enum AVColorTransferCharacteristic: + AVCOL_TRC_RESERVED0 = 0 + AVCOL_TRC_BT709 = 1 + AVCOL_TRC_UNSPECIFIED = 2 + AVCOL_TRC_RESERVED = 3 + AVCOL_TRC_GAMMA22 = 4 + AVCOL_TRC_GAMMA28 = 5 + AVCOL_TRC_SMPTE170M = 6 + AVCOL_TRC_SMPTE240M = 7 + AVCOL_TRC_LINEAR = 8 + AVCOL_TRC_LOG = 9 + AVCOL_TRC_LOG_SQRT = 10 + AVCOL_TRC_IEC61966_2_4 = 11 + AVCOL_TRC_BT1361_ECG = 12 + AVCOL_TRC_IEC61966_2_1 = 13 + AVCOL_TRC_BT2020_10 = 14 + AVCOL_TRC_BT2020_12 = 15 + AVCOL_TRC_SMPTE2084 = 16 + AVCOL_TRC_SMPTEST2084 = AVCOL_TRC_SMPTE2084 + AVCOL_TRC_SMPTE428 = 17 + AVCOL_TRC_SMPTEST428_1 = AVCOL_TRC_SMPTE428 + AVCOL_TRC_ARIB_STD_B67 = 18 + AVCOL_TRC_NB + + cdef enum AVColorSpace: + AVCOL_SPC_RGB = 0 + AVCOL_SPC_BT709 = 1 + AVCOL_SPC_UNSPECIFIED = 2 + AVCOL_SPC_RESERVED = 3 + AVCOL_SPC_FCC = 4 + AVCOL_SPC_BT470BG = 5 + AVCOL_SPC_SMPTE170M = 6 + AVCOL_SPC_SMPTE240M = 7 + AVCOL_SPC_YCGCO = 8 + AVCOL_SPC_YCOCG = AVCOL_SPC_YCGCO + AVCOL_SPC_BT2020_NCL = 9 + AVCOL_SPC_BT2020_CL = 10 + AVCOL_SPC_SMPTE2085 = 11 + AVCOL_SPC_CHROMA_DERIVED_NCL = 12 + AVCOL_SPC_CHROMA_DERIVED_CL = 13 + AVCOL_SPC_ICTCP = 14 + AVCOL_SPC_NB + + cdef enum AVColorRange: + AVCOL_RANGE_UNSPECIFIED = 0 + AVCOL_RANGE_MPEG = 1 + AVCOL_RANGE_JPEG = 2 + AVCOL_RANGE_NB + + cdef enum AVChromaLocation: + AVCHROMA_LOC_UNSPECIFIED = 0 + AVCHROMA_LOC_LEFT = 1 + AVCHROMA_LOC_CENTER = 2 + AVCHROMA_LOC_TOPLEFT = 3 + AVCHROMA_LOC_TOP = 4 + AVCHROMA_LOC_BOTTOMLEFT = 5 + AVCHROMA_LOC_BOTTOM = 6 + AVCHROMA_LOC_NB From 29e969f7f7067bf0588e43d65f8ac8bdb4f6352d Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Thu, 26 Oct 2023 18:50:19 -0600 Subject: [PATCH 184/192] Cleanup (#16) --- .gitignore | 3 + av/audio/codeccontext.pyx | 1 - av/audio/fifo.pyx | 2 - av/audio/frame.pyx | 1 - av/audio/resampler.pyx | 2 +- av/codec/codec.pyx | 2 +- av/codec/context.pyx | 2 +- av/container/core.pyx | 80 ++++++++++--------- av/container/input.pyx | 2 +- av/container/output.pyx | 1 - av/enum.pyx | 2 - av/error.pyx | 29 ++++--- av/filter/context.pyx | 6 +- av/frame.pyx | 2 - av/logging.pyx | 2 +- av/stream.pyx | 11 +-- av/subtitles/codeccontext.pyx | 1 - av/utils.pyx | 8 +- av/video/codeccontext.pyx | 1 - av/video/frame.pyx | 2 +- include/libavcodec/avcodec.pxd | 107 ++++++++++++-------------- include/libavfilter/avfilter.pxd | 2 +- include/libavfilter/avfiltergraph.pxd | 1 - include/libavformat/avformat.pxd | 12 ++- include/libavutil/avutil.pxd | 14 ++-- include/libavutil/error.pxd | 1 - include/libavutil/pixfmt.pxd | 104 ++++++++++++------------- include/libavutil/samplefmt.pxd | 3 +- include/libswresample/swresample.pxd | 3 +- scratchpad/audio.py | 4 - scratchpad/audio_player.py | 6 +- scratchpad/average.py | 3 - scratchpad/cctx_decode.py | 8 +- scratchpad/decode.py | 10 +-- scratchpad/encode.py | 3 +- scratchpad/encode_frames.py | 1 - scratchpad/frame_seek_example.py | 9 +-- scratchpad/player.py | 15 ++-- scratchpad/qtproxy.py | 3 - scratchpad/resource_use.py | 2 +- scratchpad/save_subtitles.py | 1 - scratchpad/show_frames_opencv.py | 1 - tests/test_filters.py | 2 +- 43 files changed, 203 insertions(+), 272 deletions(-) diff --git a/.gitignore b/.gitignore index 908942ba1..90d89a890 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ /venv /venvs +# IDE +/.idea + # Build products *.dll *.egg-info 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/resampler.pyx b/av/audio/resampler.pyx index 0859e6df2..8159661fb 100644 --- a/av/audio/resampler.pyx +++ b/av/audio/resampler.pyx @@ -109,7 +109,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/codec/codec.pyx b/av/codec/codec.pyx index 6672e412e..52ed4d998 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 597c30b31..163032aba 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 f416a4e44..8b76f364c 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 @@ -206,7 +205,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: @@ -315,45 +313,45 @@ cdef class Container: auto_bsf = flags.flag_property('AUTO_BSF') -"""Main entrypoint to opening files/streams. - -open(file) -> InputContainer | OutputContainer -open(file, mode="r") -> InputContainer -open(file, mode="w") -> OutputContainer - -file :: The file to open, which can be either a string or a file-like object. -mode: "r" | "w" | None -format: str | None :: Specific format to use. Defaults to autodect. -options: dict :: Options to pass to the container and all streams. -container_options: dict :: Options to pass to the container. -stream_options: list :: Options to pass to each stream. -metadata_encoding: str :: Encoding to use when reading or writing file metadata. -metadata_errors: str :: Specifies how to handle encoding errors -buffer_size: int :: Size of buffer for Python input/output operations in bytes. - Honored only when `file` is a file-like object. -timeout: float | None | tuple[open timeout, read timeout] - :: How many seconds to wait for data before giving up -io_open: callable | None - :: 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. - >>> # Open webcam on MacOS. - >>> av.open(format="avfoundation", file="0") - -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) -""" +# Main entrypoint to opening files/streams. +# +# open(file) -> InputContainer | OutputContainer +# open(file, mode="r") -> InputContainer +# open(file, mode="w") -> OutputContainer +# +# file :: The file to open, which can be either a string or a file-like object. +# mode: "r" | "w" | None +# format: str | None :: Specific format to use. Defaults to autodect. +# options: dict :: Options to pass to the container and all streams. +# container_options: dict :: Options to pass to the container. +# stream_options: list :: Options to pass to each stream. +# metadata_encoding: str :: Encoding to use when reading or writing file metadata. +# metadata_errors: str :: Specifies how to handle encoding errors +# buffer_size: int :: Size of buffer for Python input/output operations in bytes. +# Honored only when `file` is a file-like object. +# timeout: float | None | tuple[open timeout, read timeout] +# :: How many seconds to wait for data before giving up +# io_open: callable | None +# :: 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. +# >>> # Open webcam on MacOS. +# >>> av.open(format="avfoundation", file="0") +# +# 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) + def open( file, 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 e778420f3..3200c901e 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 fdfd5946f..c839c53c0 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/error.pyx b/av/error.pyx index 16f98474e..3f70dbbc6 100644 --- a/av/error.pyx +++ b/av/error.pyx @@ -174,23 +174,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/av/filter/context.pyx b/av/filter/context.pyx index 7dea198f7..adacc757f 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 417e5edf3..c639cc92f 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 2237b116f..6e3626ab3 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.pyx b/av/stream.pyx index 7fd1962b1..f2e4b511e 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -1,11 +1,6 @@ -import warnings - -from cpython cimport PyWeakref_NewRef -from libc.stdint cimport int32_t, int64_t, uint8_t -from libc.string cimport memcpy +from libc.stdint cimport int32_t cimport libav as lib -from av.codec.context cimport wrap_codec_context from av.enum cimport define_enum from av.error cimport err_check from av.packet cimport Packet @@ -95,9 +90,9 @@ cdef class Stream: if SideData.get(stream.side_data[i].type): # Use dumpsidedata maybe here I guess : https://www.ffmpeg.org/doxygen/trunk/dump_8c_source.html#l00430 self.side_data[SideData.get(stream.side_data[i].type)] = lib.av_display_rotation_get(stream.side_data[i].data) - else: + else: self.side_data = None - + self.metadata = avdict_to_dict( stream.metadata, encoding=self.container.metadata_encoding, 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.pyx b/av/utils.pyx index 5964765a2..c453ea955 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 @@ -84,9 +84,3 @@ cdef flag_in_bitfield(uint64_t bitfield, uint64_t flag): if not flag: return None return bool(bitfield & flag) - - -# === BACKWARDS COMPAT === - -from .error import FFmpegError as AVError -from .error import err_check diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.pyx index b9966da9d..891f5ccaf 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.pyx b/av/video/frame.pyx index dfc46861c..3e058961c 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 0fc250417..c63a7cc4b 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -35,7 +35,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 @@ -127,7 +127,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*) @@ -140,7 +139,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVCodecDescriptor* avcodec_descriptor_get(AVCodecID) - cdef struct AVCodecContext: AVClass *av_class @@ -201,7 +199,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 @@ -252,50 +250,50 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVDictionary **options, ) - cdef int avcodec_is_open(AVCodecContext *ctx ) + cdef int avcodec_is_open(AVCodecContext *ctx) cdef int avcodec_close(AVCodecContext *ctx) 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 + 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; + uint8_t *data + size_t size + AVPacketSideDataType type cdef enum AVFrameSideDataType: AV_FRAME_DATA_PANSCAN @@ -326,15 +324,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 @@ -342,10 +340,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 @@ -358,7 +356,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: int flags int decode_error_flags - cdef AVFrame* avcodec_alloc_frame() cdef struct AVPacket: @@ -375,7 +372,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: int64_t pos - cdef int avcodec_fill_audio_frame( AVFrame *frame, int nb_channels, @@ -405,8 +401,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 @@ -440,7 +436,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) @@ -478,7 +474,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 fcc0e3291..2e787a2a5 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: @@ -51,7 +50,6 @@ cdef extern from "libavformat/avformat.h" nogil: int nb_side_data AVPacketSideData *side_data - # http://ffmpeg.org/doxygen/trunk/structAVIOContext.html cdef struct AVIOContext: unsigned char* buffer @@ -211,10 +209,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) @@ -228,7 +226,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) @@ -273,7 +271,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 245f57320..27ade47b1 100644 --- a/include/libavutil/avutil.pxd +++ b/include/libavutil/avutil.pxd @@ -35,7 +35,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) @@ -63,9 +62,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. @@ -74,14 +73,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( @@ -142,7 +141,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. @@ -162,8 +160,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/pixfmt.pxd b/include/libavutil/pixfmt.pxd index 877f5ea03..c45fc154d 100644 --- a/include/libavutil/pixfmt.pxd +++ b/include/libavutil/pixfmt.pxd @@ -236,79 +236,79 @@ cdef extern from "libavutil/pixfmt.h" nogil: AV_PIX_FMT_NB cdef enum AVColorPrimaries: - AVCOL_PRI_RESERVED0 = 0 - AVCOL_PRI_BT709 = 1 + AVCOL_PRI_RESERVED0 = 0 + AVCOL_PRI_BT709 = 1 AVCOL_PRI_UNSPECIFIED = 2 - AVCOL_PRI_RESERVED = 3 - AVCOL_PRI_BT470M = 4 - AVCOL_PRI_BT470BG = 5 - AVCOL_PRI_SMPTE170M = 6 - AVCOL_PRI_SMPTE240M = 7 - AVCOL_PRI_FILM = 8 - AVCOL_PRI_BT2020 = 9 - AVCOL_PRI_SMPTE428 = 10 + AVCOL_PRI_RESERVED = 3 + AVCOL_PRI_BT470M = 4 + AVCOL_PRI_BT470BG = 5 + AVCOL_PRI_SMPTE170M = 6 + AVCOL_PRI_SMPTE240M = 7 + AVCOL_PRI_FILM = 8 + AVCOL_PRI_BT2020 = 9 + AVCOL_PRI_SMPTE428 = 10 AVCOL_PRI_SMPTEST428_1 = AVCOL_PRI_SMPTE428 - AVCOL_PRI_SMPTE431 = 11 - AVCOL_PRI_SMPTE432 = 12 - AVCOL_PRI_EBU3213 = 22 - AVCOL_PRI_JEDEC_P22 = AVCOL_PRI_EBU3213 + AVCOL_PRI_SMPTE431 = 11 + AVCOL_PRI_SMPTE432 = 12 + AVCOL_PRI_EBU3213 = 22 + AVCOL_PRI_JEDEC_P22 = AVCOL_PRI_EBU3213 AVCOL_PRI_NB cdef enum AVColorTransferCharacteristic: - AVCOL_TRC_RESERVED0 = 0 - AVCOL_TRC_BT709 = 1 - AVCOL_TRC_UNSPECIFIED = 2 - AVCOL_TRC_RESERVED = 3 - AVCOL_TRC_GAMMA22 = 4 - AVCOL_TRC_GAMMA28 = 5 - AVCOL_TRC_SMPTE170M = 6 - AVCOL_TRC_SMPTE240M = 7 - AVCOL_TRC_LINEAR = 8 - AVCOL_TRC_LOG = 9 - AVCOL_TRC_LOG_SQRT = 10 + AVCOL_TRC_RESERVED0 = 0 + AVCOL_TRC_BT709 = 1 + AVCOL_TRC_UNSPECIFIED = 2 + AVCOL_TRC_RESERVED = 3 + AVCOL_TRC_GAMMA22 = 4 + AVCOL_TRC_GAMMA28 = 5 + AVCOL_TRC_SMPTE170M = 6 + AVCOL_TRC_SMPTE240M = 7 + AVCOL_TRC_LINEAR = 8 + AVCOL_TRC_LOG = 9 + AVCOL_TRC_LOG_SQRT = 10 AVCOL_TRC_IEC61966_2_4 = 11 - AVCOL_TRC_BT1361_ECG = 12 + AVCOL_TRC_BT1361_ECG = 12 AVCOL_TRC_IEC61966_2_1 = 13 - AVCOL_TRC_BT2020_10 = 14 - AVCOL_TRC_BT2020_12 = 15 - AVCOL_TRC_SMPTE2084 = 16 - AVCOL_TRC_SMPTEST2084 = AVCOL_TRC_SMPTE2084 - AVCOL_TRC_SMPTE428 = 17 + AVCOL_TRC_BT2020_10 = 14 + AVCOL_TRC_BT2020_12 = 15 + AVCOL_TRC_SMPTE2084 = 16 + AVCOL_TRC_SMPTEST2084 = AVCOL_TRC_SMPTE2084 + AVCOL_TRC_SMPTE428 = 17 AVCOL_TRC_SMPTEST428_1 = AVCOL_TRC_SMPTE428 AVCOL_TRC_ARIB_STD_B67 = 18 AVCOL_TRC_NB cdef enum AVColorSpace: - AVCOL_SPC_RGB = 0 - AVCOL_SPC_BT709 = 1 + AVCOL_SPC_RGB = 0 + AVCOL_SPC_BT709 = 1 AVCOL_SPC_UNSPECIFIED = 2 - AVCOL_SPC_RESERVED = 3 - AVCOL_SPC_FCC = 4 - AVCOL_SPC_BT470BG = 5 - AVCOL_SPC_SMPTE170M = 6 - AVCOL_SPC_SMPTE240M = 7 - AVCOL_SPC_YCGCO = 8 - AVCOL_SPC_YCOCG = AVCOL_SPC_YCGCO - AVCOL_SPC_BT2020_NCL = 9 - AVCOL_SPC_BT2020_CL = 10 - AVCOL_SPC_SMPTE2085 = 11 + AVCOL_SPC_RESERVED = 3 + AVCOL_SPC_FCC = 4 + AVCOL_SPC_BT470BG = 5 + AVCOL_SPC_SMPTE170M = 6 + AVCOL_SPC_SMPTE240M = 7 + AVCOL_SPC_YCGCO = 8 + AVCOL_SPC_YCOCG = AVCOL_SPC_YCGCO + AVCOL_SPC_BT2020_NCL = 9 + AVCOL_SPC_BT2020_CL = 10 + AVCOL_SPC_SMPTE2085 = 11 AVCOL_SPC_CHROMA_DERIVED_NCL = 12 AVCOL_SPC_CHROMA_DERIVED_CL = 13 - AVCOL_SPC_ICTCP = 14 + AVCOL_SPC_ICTCP = 14 AVCOL_SPC_NB cdef enum AVColorRange: AVCOL_RANGE_UNSPECIFIED = 0 - AVCOL_RANGE_MPEG = 1 - AVCOL_RANGE_JPEG = 2 + AVCOL_RANGE_MPEG = 1 + AVCOL_RANGE_JPEG = 2 AVCOL_RANGE_NB cdef enum AVChromaLocation: AVCHROMA_LOC_UNSPECIFIED = 0 - AVCHROMA_LOC_LEFT = 1 - AVCHROMA_LOC_CENTER = 2 - AVCHROMA_LOC_TOPLEFT = 3 - AVCHROMA_LOC_TOP = 4 - AVCHROMA_LOC_BOTTOMLEFT = 5 - AVCHROMA_LOC_BOTTOM = 6 + AVCHROMA_LOC_LEFT = 1 + AVCHROMA_LOC_CENTER = 2 + AVCHROMA_LOC_TOPLEFT = 3 + AVCHROMA_LOC_TOP = 4 + AVCHROMA_LOC_BOTTOMLEFT = 5 + AVCHROMA_LOC_BOTTOM = 6 AVCHROMA_LOC_NB 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 703310139..d4ccbf257 100644 --- a/include/libswresample/swresample.pxd +++ b/include/libswresample/swresample.pxd @@ -6,7 +6,6 @@ cdef extern from "libswresample/swresample.h" nogil: cdef char* swresample_configuration() cdef char* swresample_license() - cdef struct SwrContext: pass @@ -19,7 +18,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/scratchpad/audio.py b/scratchpad/audio.py index 4b91a0621..fa28d5bf1 100644 --- a/scratchpad/audio.py +++ b/scratchpad/audio.py @@ -1,10 +1,6 @@ -import array import argparse -import sys -import pprint import subprocess -from PIL import Image import av diff --git a/scratchpad/audio_player.py b/scratchpad/audio_player.py index 32e24be44..a7cf6b067 100644 --- a/scratchpad/audio_player.py +++ b/scratchpad/audio_player.py @@ -1,8 +1,4 @@ -import array import argparse -import sys -import pprint -import subprocess import time from qtproxy import Q @@ -57,7 +53,7 @@ def decode_iter(): 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}'.format(frame.time or 0, us_processed / 1000000.0, us_buffered / 1000000.0)) + print(f'pts: {frame.time or 0:.3f}, played: {us_processed / 1000000.0:.3f}, buffered: {us_buffered / 1000000.0:.3f}') data = bytes(frame.planes[0]) diff --git a/scratchpad/average.py b/scratchpad/average.py index f72297f08..ec8cdf899 100644 --- a/scratchpad/average.py +++ b/scratchpad/average.py @@ -1,8 +1,5 @@ import argparse import os -import sys -import pprint -import itertools import cv2 diff --git a/scratchpad/cctx_decode.py b/scratchpad/cctx_decode.py index b1997df0d..2a4c727cf 100644 --- a/scratchpad/cctx_decode.py +++ b/scratchpad/cctx_decode.py @@ -1,12 +1,8 @@ import logging -logging.basicConfig() - +from av.codec import CodecContext -import av -from av.codec import CodecContext, CodecParser -from av.video import VideoFrame -from av.packet import Packet +logging.basicConfig() cc = CodecContext.create('mpeg4', 'r') diff --git a/scratchpad/decode.py b/scratchpad/decode.py index 96e5a202a..23ed0cb53 100644 --- a/scratchpad/decode.py +++ b/scratchpad/decode.py @@ -1,11 +1,7 @@ -import array import argparse import logging -import sys -import pprint import subprocess -from PIL import Image from av import open, time_base @@ -16,7 +12,7 @@ def format_time(time, time_base): if time is None: return 'None' - return '{:.3f}s ({} or {}/{})'.format(time_base * time, time_base * time, time_base.numerator * time, time_base.denominator) + return f'{time_base * time:.3f}s ({time_base * time} or {time_base.numerator * time}/{time_base.denominator})' arg_parser = argparse.ArgumentParser() @@ -45,7 +41,7 @@ def format_time(time, time_base): print('\tduration:', float(container.duration) / time_base) print('\tmetadata:') for k, v in sorted(container.metadata.items()): - print('\t\t{}: {!r}'.format(k, v)) + print(f'\t\t{k}: {v!r}') print() print(len(container.streams), 'stream(s):') @@ -79,7 +75,7 @@ def format_time(time, time_base): print('\t\tmetadata:') for k, v in sorted(stream.metadata.items()): - print('\t\t\t{}: {!r}'.format(k, v)) + print(f'\t\t\t{k}: {v!r}') print() diff --git a/scratchpad/encode.py b/scratchpad/encode.py index 099ac4a14..390415dcf 100644 --- a/scratchpad/encode.py +++ b/scratchpad/encode.py @@ -1,10 +1,9 @@ import argparse -import logging import os import sys import av -from tests.common import asset, sandboxed +from tests.common import sandboxed arg_parser = argparse.ArgumentParser() diff --git a/scratchpad/encode_frames.py b/scratchpad/encode_frames.py index 642a1c2c6..8916fee83 100644 --- a/scratchpad/encode_frames.py +++ b/scratchpad/encode_frames.py @@ -1,6 +1,5 @@ import argparse import os -import sys import av import cv2 diff --git a/scratchpad/frame_seek_example.py b/scratchpad/frame_seek_example.py index 1f52d456b..5385cd1bb 100644 --- a/scratchpad/frame_seek_example.py +++ b/scratchpad/frame_seek_example.py @@ -76,10 +76,10 @@ def next_frame(self): else: pts = frame.dts - if not pts is None: + if pts is not None: frame_index = pts_to_frame(pts, time_base, rate, self.start_time) - elif not frame_index is None: + elif frame_index is not None: frame_index += 1 @@ -218,7 +218,7 @@ def get_frame_count(self): print(frame_index, frame) continue - if not frame_index is None: + if frame_index is not None: break else: seek_frame -= 1 @@ -296,9 +296,6 @@ def resizeEvent(self, event): if self.pixmap: super().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): diff --git a/scratchpad/player.py b/scratchpad/player.py index e3a7898dc..c986b9904 100644 --- a/scratchpad/player.py +++ b/scratchpad/player.py @@ -1,8 +1,5 @@ import argparse import ctypes -import os -import sys -import pprint import time from qtproxy import Q @@ -45,10 +42,14 @@ 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) + 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) diff --git a/scratchpad/qtproxy.py b/scratchpad/qtproxy.py index 221c78302..3ffb07955 100644 --- a/scratchpad/qtproxy.py +++ b/scratchpad/qtproxy.py @@ -1,6 +1,3 @@ -import sys - -sys.path.append('/usr/local/lib/python2.7/site-packages') from PyQt4 import QtCore, QtGui, QtOpenGL, QtMultimedia diff --git a/scratchpad/resource_use.py b/scratchpad/resource_use.py index 930f808f5..aba327a41 100644 --- a/scratchpad/resource_use.py +++ b/scratchpad/resource_use.py @@ -58,4 +58,4 @@ def format_bytes(n): for i in range(len(usage) - 1): before = usage[i] after = usage[i + 1] - print('{} ({})'.format(format_bytes(after.ru_maxrss), format_bytes(after.ru_maxrss - before.ru_maxrss))) + print(f'{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 index 8666501d8..d937e8152 100644 --- a/scratchpad/save_subtitles.py +++ b/scratchpad/save_subtitles.py @@ -6,7 +6,6 @@ import os import sys -import pprint from PIL import Image diff --git a/scratchpad/show_frames_opencv.py b/scratchpad/show_frames_opencv.py index c618afaac..dc7d73f38 100644 --- a/scratchpad/show_frames_opencv.py +++ b/scratchpad/show_frames_opencv.py @@ -1,4 +1,3 @@ -import os import sys import cv2 diff --git a/tests/test_filters.py b/tests/test_filters.py index 2514693ac..f6f727866 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -33,7 +33,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 767b51e0d468d380b5590427351680b068abf6b0 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 27 Oct 2023 00:04:50 -0400 Subject: [PATCH 185/192] Remove unused classes --- av/codec/context.pyx | 6 ---- av/deprecation.py | 69 --------------------------------------- tests/test_deprecation.py | 49 --------------------------- 3 files changed, 124 deletions(-) delete mode 100644 tests/test_deprecation.py diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 163032aba..bdc7fd288 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -344,7 +344,6 @@ cdef class CodecContext: packets = [] while True: - with nogil: consumed = lib.av_parser_parse2( self.parser, @@ -357,7 +356,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. @@ -406,7 +404,6 @@ cdef class CodecContext: return out cdef _send_packet_and_recv(self, Packet packet): - cdef Frame frame cdef int res @@ -430,7 +427,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 @@ -448,7 +444,6 @@ cdef class CodecContext: return frame cdef _recv_packet(self): - cdef Packet packet = Packet() cdef int res @@ -516,7 +511,6 @@ cdef class CodecContext: return res cdef _setup_decoded_frame(self, Frame frame, Packet packet): - # Propagate our manual times. # While decoding, frame times are in stream time_base, which PyAV # is carrying around. diff --git a/av/deprecation.py b/av/deprecation.py index c89f1a761..3e01435f4 100644 --- a/av/deprecation.py +++ b/av/deprecation.py @@ -1,4 +1,3 @@ -import functools import warnings @@ -6,76 +5,8 @@ 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 f8857ab73..000000000 --- a/tests/test_deprecation.py +++ /dev/null @@ -1,49 +0,0 @@ -import warnings - -from av import deprecation - -from .common import TestCase - - -class TestDeprecations(TestCase): - def test_method(self): - class Example: - def __init__(self, x=100): - self.x = x - - @deprecation.method - def foo(self, a, b): - return self.x + a + b - - obj = Example() - - 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 new_func(self, a, b): - return a + b - - 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] - ) - - 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]) From ba09e8950764b57989b548bff8a8c5058c529ac7 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 27 Oct 2023 04:07:28 -0400 Subject: [PATCH 186/192] Change readme since jlaine is now active --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 38f0d315c..c7fbdcfbc 100644 --- a/README.md +++ b/README.md @@ -35,10 +35,10 @@ deactivate pip install . ``` -## Motivations For a Fork -Unlike [PyAV](https://github.com/PyAV-Org/PyAV) (The original repo), this fork offers the following benefits: +## Features +pyav is forked from [PyAV](https://github.com/PyAV-Org/PyAV), and currently has the following benefits: * Wheels for Python 3.12 * Support for Cython 3 * Support for FFmpeg 6, and beyond * Expanded support for different pixel formats - * Being maintained + From c102de8c46c2670b0f3cff6e1946bfb04b45f430 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 27 Oct 2023 13:41:28 -0400 Subject: [PATCH 187/192] Remove unused lib --- av/audio/plane.pyx | 3 --- 1 file changed, 3 deletions(-) diff --git a/av/audio/plane.pyx b/av/audio/plane.pyx index 50fe0aa59..86bb363db 100644 --- a/av/audio/plane.pyx +++ b/av/audio/plane.pyx @@ -1,10 +1,7 @@ -cimport libav as lib - 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] From 50dad23e0bac7ae6d618dbc3d1d4248892247620 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 27 Oct 2023 13:42:52 -0400 Subject: [PATCH 188/192] This is fixed in Cython 3.0.1 --- av/video/format.pyx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/av/video/format.pyx b/av/video/format.pyx index 556d9d3b4..377310bbd 100644 --- a/av/video/format.pyx +++ b/av/video/format.pyx @@ -38,14 +38,10 @@ cdef class VideoFormat: self.ptr = lib.av_pix_fmt_desc_get(pix_fmt) self.width = width self.height = height - # hmaarrfk -- 2023/07/23 - # Note on tuple([]) - # Cython 3 seems to have trouble with cdef tuples, so we use a list - # it complains about some const identifier not being able to get assigned - self.components = tuple([ + self.components = tuple( VideoFormatComponent(self, i) for i in range(self.ptr.nb_components) - ]) + ) def __repr__(self): if self.width or self.height: From 938f180b120d8fc0432d43e1660b082b8df5f03f Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 27 Oct 2023 13:58:16 -0400 Subject: [PATCH 189/192] Use newer ffmpeg build --- 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 bdd9e37c7..9a12c0506 100644 --- a/scripts/fetch-vendor.py +++ b/scripts/fetch-vendor.py @@ -21,7 +21,7 @@ def get_url(): plat = f"macosx_{machine}" if system == "Linux" or system == "Darwin": - return f"https://github.com/WyattBlue/pyav-ffmpeg/releases/download/6.0-2/ffmpeg-{plat}.tar.gz" + return f"https://github.com/WyattBlue/pyav-ffmpeg/releases/download/6.0-3/ffmpeg-{plat}.tar.gz" if system == "Windows": plat = "win_amd64" if calcsize("P") * 8 == 64 else "win32" From 68270599dd3db9248709508dbd2632dd081f1af8 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 27 Oct 2023 23:45:36 -0400 Subject: [PATCH 190/192] Bump version --- av/about.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/about.py b/av/about.py index 80314c5f3..faa2340ba 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "11.3.0" +__version__ = "11.4.0" From 450f14903b49f20e87e84f20aa8668c329cf87e6 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 31 Oct 2023 13:56:24 -0400 Subject: [PATCH 191/192] Catch AttributeError when calling uninit repr(fifo) --- av/audio/fifo.pyx | 26 ++++++++++++++++---------- tests/test_audiofifo.py | 14 ++++++++++++++ 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/av/audio/fifo.pyx b/av/audio/fifo.pyx index 9b95d5770..afbaa197d 100644 --- a/av/audio/fifo.pyx +++ b/av/audio/fifo.pyx @@ -3,24 +3,30 @@ from av.error cimport err_check 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: lib.av_audio_fifo_free(self.ptr) - cpdef write(self, AudioFrame frame): + cpdef write(self, frame: AudioFrame): """write(frame) Push a frame of samples into the queue. 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 6adf32a8913f787381a69c4ab9a5fedc7bf023a4 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 31 Oct 2023 15:53:00 -0400 Subject: [PATCH 192/192] Bump version to 11.4.1 --- av/about.py | 2 +- scripts/fetch-vendor.py | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/av/about.py b/av/about.py index faa2340ba..043983b45 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "11.4.0" +__version__ = "11.4.1" diff --git a/scripts/fetch-vendor.py b/scripts/fetch-vendor.py index 9a12c0506..016f394d9 100644 --- a/scripts/fetch-vendor.py +++ b/scripts/fetch-vendor.py @@ -13,21 +13,19 @@ def get_url(): if system == "Linux": plat = f"manylinux_{machine}" - if system == "Darwin": + 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] plat = f"macosx_{machine}" - if system == "Linux" or system == "Darwin": - return f"https://github.com/WyattBlue/pyav-ffmpeg/releases/download/6.0-3/ffmpeg-{plat}.tar.gz" - - if system == "Windows": + elif system == "Windows": plat = "win_amd64" if calcsize("P") * 8 == 64 else "win32" - return f"https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.1.2-1/ffmpeg-{plat}.tar.gz" + else: + raise Exception(f"Unsupported system {system}") - raise Exception(f"Unsupported system {system}") + return f"https://github.com/WyattBlue/pyav-ffmpeg/releases/download/6.0-4/ffmpeg-{plat}.tar.gz" parser = argparse.ArgumentParser(description="Fetch and extract tarballs")