From f77e3d4a9271402947ca9d9d67c45dddd594c60f Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 24 Apr 2024 17:14:42 -0400 Subject: [PATCH 001/365] Disable logs by default --- av/logging.pyi | 7 +- av/logging.pyx | 168 +++++++++++++++++---------------------- docs/index.rst | 1 + tests/test_colorspace.py | 1 + tests/test_logging.py | 14 ++++ tests/test_python_io.py | 4 + 6 files changed, 93 insertions(+), 102 deletions(-) diff --git a/av/logging.pyi b/av/logging.pyi index a85eba2df..64ca9df3c 100644 --- a/av/logging.pyi +++ b/av/logging.pyi @@ -13,11 +13,9 @@ TRACE: int CRITICAL: int def adapt_level(level: int) -> int: ... -def get_level() -> int: ... -def set_level(level: int) -> None: ... +def get_level() -> int | None: ... +def set_level(level: int | None) -> None: ... def restore_default_callback() -> None: ... -def get_print_after_shutdown() -> bool: ... -def set_print_after_shutdown(v: bool) -> None: ... def get_skip_repeated() -> bool: ... def set_skip_repeated(v: bool) -> None: ... def get_last_error() -> tuple[int, tuple[int, str, str] | None]: ... @@ -36,7 +34,6 @@ class Capture: ) -> None: ... level_threshold: int -print_after_shutdown: bool skip_repeated: bool skip_lock: Lock last_log: tuple[int, str, str] | None diff --git a/av/logging.pyx b/av/logging.pyx index 8940c3139..d3c3bfce5 100644 --- a/av/logging.pyx +++ b/av/logging.pyx @@ -1,6 +1,21 @@ """ -FFmpeg has a logging system that it uses extensively. PyAV hooks into that system -to translate FFmpeg logs into Python's +FFmpeg has a logging system that it uses extensively. It's very noisy so PyAV turns it +off by default. This, unfortunately has the effect of making raised errors having less +detailed messages. It's therefore recommended to use VERBOSE when developing. + +.. _enable_logging: + +Enabling Logging +~~~~~~~~~~~~~~~~~ + +You can hook the logging system with Python by setting the log level:: + + import av + + av.logging.set_level(av.logging.VERBOSE) + + +PyAV hooks into that system to translate FFmpeg logs into Python's `logging system `_. If you are not already using Python's logging system, you can initialize it @@ -10,19 +25,12 @@ quickly with:: logging.basicConfig() -.. _disable_logging: - -Disabling Logging -~~~~~~~~~~~~~~~~~ - -You can disable hooking the logging system with an environment variable:: - - export PYAV_LOGGING=off - -or at runtime with :func:`restore_default_callback`. +Note that handling logs with Python sometimes doesn't play nice multi-threads workflows. +An alternative is :func:`restore_default_callback`. -This will leave (or restore) the FFmpeg logging system, which prints to the terminal. -This may also result in raised errors having less detailed messages. +This will restores FFmpeg's logging default system, which prints to the terminal. +Like with setting the log level to ``None``, this may also result in raised errors +having less detailed messages. API Reference @@ -37,7 +45,6 @@ from libc.stdio cimport fprintf, stderr from libc.stdlib cimport free, malloc import logging -import os import sys from threading import Lock, get_ident @@ -72,13 +79,10 @@ cpdef adapt_level(int level): elif level <= lib.AV_LOG_DEBUG: return 5 # Lower than any logging constant. else: # lib.AV_LOG_TRACE - return 1 # ... yeah. + return 1 -# While we start with the level quite low, Python defaults to INFO, and so -# they will not show. The logging system can add significant overhead, so -# be wary of dropping this lower. -cdef int level_threshold = lib.AV_LOG_VERBOSE +cdef object level_threshold = None # ... but lets limit ourselves to WARNING (assuming nobody already did this). if "libav" not in logging.Logger.manager.loggerDict: @@ -86,33 +90,30 @@ if "libav" not in logging.Logger.manager.loggerDict: def get_level(): - """Return current FFmpeg logging threshold. See :func:`set_level`.""" + """Returns the current log level. See :func:`set_level`.""" return level_threshold -def set_level(int level): +def set_level(level): """set_level(level) - Sets logging threshold when converting from FFmpeg's logging system - to Python's. It is recommended to use the constants available in this - module to set the level: ``PANIC``, ``FATAL``, ``ERROR``, - ``WARNING``, ``INFO``, ``VERBOSE``, and ``DEBUG``. - - While less efficient, it is generally preferable to modify logging - with Python's :mod:`logging`, e.g.:: + Sets PyAV's log level. It can be set to constants available in this + module: ``PANIC``, ``FATAL``, ``ERROR``, ``WARNING``, ``INFO``, + ``VERBOSE``, ``DEBUG``, or ``None`` (the default). - logging.getLogger('libav').setLevel(logging.ERROR) + PyAV defaults to totally ignoring all ffmpeg logs. This has the side effect of + making certain Exceptions have no messages. It's therefore recommended to use: - PyAV defaults to translating everything except ``AV_LOG_DEBUG``, so this - function is only nessesary to use if you want to see those messages as well. - ``AV_LOG_DEBUG`` will be translated to a level 5 message, which is lower - than any builtin Python logging level, so you must lower that as well:: - - logging.getLogger().setLevel(5) + av.logging.set_level(av.logging.VERBOSE) + When developing your application. """ global level_threshold - level_threshold = level + + if level is None or type(level) is int: + level_threshold = level + else: + raise ValueError("level must be: int | None") def restore_default_callback(): @@ -120,20 +121,6 @@ def restore_default_callback(): lib.av_log_set_callback(lib.av_log_default_callback) -cdef bint print_after_shutdown = False - - -def get_print_after_shutdown(): - """Will logging continue to ``stderr`` after Python shutdown?""" - return print_after_shutdown - - -def set_print_after_shutdown(v): - """Set if logging should continue to ``stderr`` after Python shutdown.""" - global print_after_shutdown - print_after_shutdown = bool(v) - - cdef bint skip_repeated = True cdef skip_lock = Lock() cdef object last_log = None @@ -226,47 +213,6 @@ cpdef log(int level, str name, str message): free(obj) -cdef void log_callback(void *ptr, int level, const char *format, lib.va_list args) noexcept nogil: - - cdef bint inited = lib.Py_IsInitialized() - if not inited and not print_after_shutdown: - return - - # Fast path: avoid logging overhead. This should match the - # log_callback_gil() checks that result in ignoring the message. - with gil: - if level > level_threshold and level != lib.AV_LOG_ERROR: - return - - # Format the message. - cdef char message[1024] - lib.vsnprintf(message, 1023, format, args) - - # Get the name. - cdef const char *name = NULL - cdef lib.AVClass *cls = (ptr)[0] if ptr else NULL - if cls and cls.item_name: - # I'm not 100% on this, but this should be static, and so - # it doesn't matter if the AVClass that returned it vanishes or not. - name = cls.item_name(ptr) - - if not inited: - fprintf(stderr, "av.logging (after shutdown): %s[%d]: %s\n", - name, level, message) - return - - with gil: - try: - log_callback_gil(level, name, message) - - except Exception as e: - fprintf(stderr, "av.logging: exception while handling %s[%d]: %s\n", - name, level, message) - # For some reason lib.PyErr_PrintEx(0) won't work. - exc, type_, tb = sys.exc_info() - lib.PyErr_Display(exc, type_, tb) - - cdef log_callback_gil(int level, const char *c_name, const char *c_message): global error_count global skip_count @@ -336,8 +282,36 @@ cdef log_callback_emit(log): logger.log(py_level, message.strip()) -# Start the magic! -# We allow the user to fully disable the logging system as it will not play -# nicely with subinterpreters due to FFmpeg-created threads. -if os.environ.get("PYAV_LOGGING") != "off": - lib.av_log_set_callback(log_callback) +cdef void log_callback(void *ptr, int level, const char *format, lib.va_list args) noexcept nogil: + cdef bint inited = lib.Py_IsInitialized() + if not inited: + return + + with gil: + if level_threshold is None: + return + if level > level_threshold and level != lib.AV_LOG_ERROR: + return + + # Format the message. + cdef char message[1024] + lib.vsnprintf(message, 1023, format, args) + + # Get the name. + cdef const char *name = NULL + cdef lib.AVClass *cls = (ptr)[0] if ptr else NULL + if cls and cls.item_name: + name = cls.item_name(ptr) + + with gil: + try: + log_callback_gil(level, name, message) + except Exception as e: + fprintf(stderr, "av.logging: exception while handling %s[%d]: %s\n", + name, level, message) + # For some reason lib.PyErr_PrintEx(0) won't work. + exc, type_, tb = sys.exc_info() + lib.PyErr_Display(exc, type_, tb) + + +lib.av_log_set_callback(log_callback) diff --git a/docs/index.rst b/docs/index.rst index 66c8ffbba..afeaa60d6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -53,6 +53,7 @@ Basic Demo import av + av.logging.set_level(av.logging.VERBOSE) container = av.open(path_to_video) for index, frame in enumerate(container.decode(video=0)): diff --git a/tests/test_colorspace.py b/tests/test_colorspace.py index 571e7bd2b..afc16846f 100644 --- a/tests/test_colorspace.py +++ b/tests/test_colorspace.py @@ -22,6 +22,7 @@ def test_penguin_joke(self) -> None: for packet in container.demux(stream): for frame in packet.decode(): + assert isinstance(frame, av.VideoFrame) self.assertEqual(frame.color_range, ColorRange.JPEG) # a.k.a "pc" self.assertEqual(frame.colorspace, Colorspace.ITU601) return diff --git a/tests/test_logging.py b/tests/test_logging.py index 2e35879e1..2a09a9c1a 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -22,6 +22,8 @@ def test_adapt_level(self): ) def test_threaded_captures(self): + av.logging.set_level(av.logging.VERBOSE) + with av.logging.Capture(local=True) as logs: do_log("main") thread = threading.Thread(target=do_log, args=("thread",)) @@ -29,8 +31,11 @@ def test_threaded_captures(self): thread.join() self.assertIn((av.logging.INFO, "test", "main"), logs) + av.logging.set_level(None) def test_global_captures(self): + av.logging.set_level(av.logging.VERBOSE) + with av.logging.Capture(local=False) as logs: do_log("main") thread = threading.Thread(target=do_log, args=("thread",)) @@ -39,8 +44,11 @@ def test_global_captures(self): self.assertIn((av.logging.INFO, "test", "main"), logs) self.assertIn((av.logging.INFO, "test", "thread"), logs) + av.logging.set_level(None) def test_repeats(self): + av.logging.set_level(av.logging.VERBOSE) + with av.logging.Capture() as logs: do_log("foo") do_log("foo") @@ -62,7 +70,11 @@ def test_repeats(self): ], ) + av.logging.set_level(None) + def test_error(self): + av.logging.set_level(av.logging.VERBOSE) + log = (av.logging.ERROR, "test", "This is a test.") av.logging.log(*log) try: @@ -71,3 +83,5 @@ def test_error(self): self.assertEqual(e.log, log) else: self.fail() + + av.logging.set_level(None) diff --git a/tests/test_python_io.py b/tests/test_python_io.py index 42b8dd132..58ae4fd0d 100644 --- a/tests/test_python_io.py +++ b/tests/test_python_io.py @@ -292,6 +292,8 @@ def test_writing_to_pipe_readonly(self) -> None: ) def test_writing_to_pipe_writeonly(self): + av.logging.set_level(av.logging.VERBOSE) + buf = WriteOnlyPipe() with self.assertRaises(ValueError) as cm: self.write(buf) @@ -300,6 +302,8 @@ def test_writing_to_pipe_writeonly(self): str(cm.exception), ) + av.logging.set_level(None) + def read(self, fh, seekable: bool = True) -> None: wrapped = MethodLogger(fh) From fb880d21e833da6be4bde390675a28e549f32b4f Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 24 Apr 2024 18:43:10 -0400 Subject: [PATCH 002/365] Default callback no longer needs gil --- av/logging.pyi | 11 ----------- av/logging.pyx | 14 +++++++++----- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/av/logging.pyi b/av/logging.pyi index 64ca9df3c..1db5e4a3b 100644 --- a/av/logging.pyi +++ b/av/logging.pyi @@ -1,5 +1,3 @@ -import logging -from threading import Lock from typing import Any, Callable PANIC: int @@ -32,12 +30,3 @@ class Capture: value: Exception | None, traceback: Callable[..., Any] | None, ) -> None: ... - -level_threshold: int -skip_repeated: bool -skip_lock: Lock -last_log: tuple[int, str, str] | None -skip_count: int -last_error: tuple[int, str, str] | None -global_captures: list[list[tuple[int, str, str]]] -thread_captures: dict[int, list[tuple[int, str, str]]] diff --git a/av/logging.pyx b/av/logging.pyx index d3c3bfce5..1006d094c 100644 --- a/av/logging.pyx +++ b/av/logging.pyx @@ -49,7 +49,6 @@ import sys from threading import Lock, get_ident # Library levels. -# QUIET = lib.AV_LOG_QUIET # -8; not really a level. PANIC = lib.AV_LOG_PANIC # 0 FATAL = lib.AV_LOG_FATAL # 8 ERROR = lib.AV_LOG_ERROR @@ -110,8 +109,12 @@ def set_level(level): """ global level_threshold - if level is None or type(level) is int: + if level is None: level_threshold = level + lib.av_log_set_callback(nolog_callback) + elif type(level) is int: + level_threshold = level + lib.av_log_set_callback(log_callback) else: raise ValueError("level must be: int | None") @@ -288,8 +291,6 @@ cdef void log_callback(void *ptr, int level, const char *format, lib.va_list arg return with gil: - if level_threshold is None: - return if level > level_threshold and level != lib.AV_LOG_ERROR: return @@ -314,4 +315,7 @@ cdef void log_callback(void *ptr, int level, const char *format, lib.va_list arg lib.PyErr_Display(exc, type_, tb) -lib.av_log_set_callback(log_callback) +cdef void nolog_callback(void *ptr, int level, const char *format, lib.va_list args) noexcept nogil: + pass + +lib.av_log_set_callback(nolog_callback) From c983f75852df22f76c7f74290fe65861109331f9 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 24 Apr 2024 23:47:00 -0400 Subject: [PATCH 003/365] Workaround bug in Actions --- .github/workflows/tests.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0655951b1..8912dd02e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -42,7 +42,7 @@ jobs: - {os: ubuntu-latest, python: 3.9, ffmpeg: "6.1", extras: true} - {os: ubuntu-latest, python: 3.9, ffmpeg: "6.0"} - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1"} - - {os: macos-latest, python: 3.9, ffmpeg: "6.1"} + # - {os: macos-latest, python: 3.9, ffmpeg: "6.1"} env: PYAV_PYTHON: python${{ matrix.config.python }} @@ -70,7 +70,7 @@ jobs: sudo apt-get install doxygen fi ;; - macos-latest) + macos-13) brew install automake libtool nasm pkg-config shtool texi2html wget brew install libass libjpeg libpng libvorbis libvpx opus theora x264 ;; @@ -177,9 +177,9 @@ jobs: fail-fast: false matrix: include: - - os: macos-latest + - os: macos-13 arch: arm64 - - os: macos-latest + - os: macos-13 arch: x86_64 - os: ubuntu-latest arch: aarch64 @@ -198,7 +198,7 @@ jobs: if: matrix.os == 'ubuntu-latest' uses: docker/setup-qemu-action@v3 - name: Install packages - if: matrix.os == 'macos-latest' + if: matrix.os == 'macos-13' run: | brew update brew install pkg-config From 19afbf277150b99e0770d55e29ae0611becb04a8 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 25 Apr 2024 01:15:58 -0400 Subject: [PATCH 004/365] Update doc site --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index cefe7ca6a..0b3ce00f7 100644 --- a/setup.py +++ b/setup.py @@ -194,8 +194,7 @@ def parse_cflags(raw_flags): license="BSD", project_urls={ "Bug Reports": "https://github.com/PyAV-Org/PyAV/issues", - "Documentation": "https://pyav.org/docs", - "Feedstock": "https://github.com/conda-forge/av-feedstock", + "Documentation": "https://pyav.basswood-io.com", "Download": "https://pypi.org/project/av", }, author="Mike Boers", From 7f357af753f18840d7d802c91a6e85d239e8c78a Mon Sep 17 00:00:00 2001 From: Santtu Keskinen Date: Wed, 24 Apr 2024 22:56:06 +0300 Subject: [PATCH 005/365] Expose CodecContext flush_buffers --- av/codec/context.pxd | 2 ++ av/codec/context.pyi | 4 ++++ av/codec/context.pyx | 11 +++++++++++ av/container/input.pyx | 5 ++--- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/av/codec/context.pxd b/av/codec/context.pxd index f247655ff..58bc6a07d 100644 --- a/av/codec/context.pxd +++ b/av/codec/context.pxd @@ -37,6 +37,8 @@ cdef class CodecContext: cpdef encode(self, Frame frame=?) cpdef decode(self, Packet packet=?) + cpdef flush_buffers(self) + # Used by both transcode APIs to setup user-land objects. # TODO: Remove the `Packet` from `_setup_decoded_frame` (because flushing # packets are bogus). It should take all info it needs from the context and/or stream. diff --git a/av/codec/context.pyi b/av/codec/context.pyi index ee81daae5..0477aeb93 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -1,6 +1,7 @@ from typing import Any, Literal from av.enum import EnumFlag, EnumItem +from av.frame import Frame from av.packet import Packet from .codec import Codec @@ -78,3 +79,6 @@ class CodecContext: def parse( self, raw_input: bytes | bytearray | memoryview | None = None ) -> list[Packet]: ... + def encode(self, frame: Frame | None) -> list[Packet]: ... + def decode(self, packet: Packet | None) -> list[Frame]: ... + def flush_buffers(self) -> None: ... diff --git a/av/codec/context.pyx b/av/codec/context.pyx index e2557e702..d94a08015 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -510,6 +510,17 @@ cdef class CodecContext: res.append(frame) return res + cpdef flush_buffers(self): + """Reset the internal codec state and discard all internal buffers. + + Should be called before you start decoding from a new position e.g. + when seeking or when switching to a different stream. + + """ + if self.is_open: + with nogil: + lib.avcodec_flush_buffers(self.ptr) + cdef _setup_decoded_frame(self, Frame frame, Packet packet): # Propagate our manual times. diff --git a/av/container/input.pyx b/av/container/input.pyx index acf02fbab..88cc95ee0 100644 --- a/av/container/input.pyx +++ b/av/container/input.pyx @@ -278,6 +278,5 @@ cdef class InputContainer(Container): for stream in self.streams: codec_context = stream.codec_context - if codec_context and codec_context.is_open: - with nogil: - lib.avcodec_flush_buffers(codec_context.ptr) + if codec_context: + codec_context.flush_buffers() From ba7a2c9f716af506838d399e6ed27ed6d64d2435 Mon Sep 17 00:00:00 2001 From: Artturin Date: Sat, 27 Apr 2024 01:56:57 +0300 Subject: [PATCH 006/365] setup.py: use PKG_CONFIG env var to get the pkg-config to use pkg-config may be prefixed when cross-compiling --- setup.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 0b3ce00f7..9f43f5f27 100644 --- a/setup.py +++ b/setup.py @@ -72,16 +72,17 @@ def get_config_from_pkg_config(): """ Get distutils-compatible extension arguments using pkg-config. """ + pkg_config = os.environ.get('PKG_CONFIG', 'pkg-config') try: raw_cflags = subprocess.check_output( - ["pkg-config", "--cflags", "--libs"] + [pkg_config, "--cflags", "--libs"] + ["lib" + name for name in FFMPEG_LIBRARIES] ) except FileNotFoundError: - print("pkg-config is required for building PyAV") + print(f"{pkg_config} is required for building PyAV") exit(1) except subprocess.CalledProcessError: - print("pkg-config could not find libraries {}".format(FFMPEG_LIBRARIES)) + print(f"{pkg_config} could not find libraries {FFMPEG_LIBRARIES}") exit(1) known, unknown = parse_cflags(raw_cflags.decode("utf-8")) From 73c59eaf4e9e9fa8e319fe383d66e1d390165b19 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 27 Apr 2024 03:43:57 -0400 Subject: [PATCH 007/365] Use mypy in lint --- .github/workflows/tests.yml | 2 +- Makefile | 3 ++- av/codec/context.pyi | 2 -- setup.py | 6 ++++-- tests/requirements.txt | 1 + 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8912dd02e..4dd70a1ad 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -24,7 +24,7 @@ jobs: - name: Python uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: 3.12 - name: Packages run: pip install -r tests/requirements.txt diff --git a/Makefile b/Makefile index 9a0b0a95f..968494e2e 100644 --- a/Makefile +++ b/Makefile @@ -25,9 +25,10 @@ fate-suite: rsync -vrltLW rsync://fate-suite.ffmpeg.org/fate-suite/ tests/assets/fate-suite/ lint: - black --check av examples tests + black --check av examples tests setup.py flake8 av examples tests isort --check-only --diff av examples tests + mypy av tests test: $(PYTHON) setup.py test diff --git a/av/codec/context.pyi b/av/codec/context.pyi index 0477aeb93..4f7f3d2dc 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -79,6 +79,4 @@ class CodecContext: def parse( self, raw_input: bytes | bytearray | memoryview | None = None ) -> list[Packet]: ... - def encode(self, frame: Frame | None) -> list[Packet]: ... - def decode(self, packet: Packet | None) -> list[Frame]: ... def flush_buffers(self) -> None: ... diff --git a/setup.py b/setup.py index 9f43f5f27..8cfcbaa58 100644 --- a/setup.py +++ b/setup.py @@ -72,7 +72,7 @@ def get_config_from_pkg_config(): """ Get distutils-compatible extension arguments using pkg-config. """ - pkg_config = os.environ.get('PKG_CONFIG', 'pkg-config') + pkg_config = os.environ.get("PKG_CONFIG", "pkg-config") try: raw_cflags = subprocess.check_output( [pkg_config, "--cflags", "--libs"] @@ -180,7 +180,9 @@ def parse_cflags(raw_flags): exec(fp.read(), about) package_folders = pathlib.Path("av").glob("**/") -package_data = {".".join(pckg.parts): ["*.pxd", "*.pyi", "*.typed"] for pckg in package_folders} +package_data = { + ".".join(pckg.parts): ["*.pxd", "*.pyi", "*.typed"] for pckg in package_folders +} with open("README.md") as f: diff --git a/tests/requirements.txt b/tests/requirements.txt index fd44ed1b8..f9f354439 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -6,3 +6,4 @@ isort flake8 flake8-pyproject sphinx==5.1.0 +mypy==1.10.0 From fa49068b6bfa20b6e9a0ff22cb3612a66ffff8e2 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 27 Apr 2024 04:14:06 -0400 Subject: [PATCH 008/365] Separate smoke tests from building --- .github/workflows/smoke.yml | 152 ++++++++++++++++++++++++++++++++++++ .github/workflows/tests.yml | 151 +---------------------------------- 2 files changed, 153 insertions(+), 150 deletions(-) create mode 100644 .github/workflows/smoke.yml diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml new file mode 100644 index 000000000..69d5db44e --- /dev/null +++ b/.github/workflows/smoke.yml @@ -0,0 +1,152 @@ +name: smoke +on: + push: + branches: main + paths-ignore: + - '**.md' + - '**.rst' + - '**.txt' + pull_request: + branches: main + paths-ignore: + - '**.md' + - '**.rst' + - '**.txt' +jobs: + style: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Python + uses: actions/setup-python@v5 + with: + python-version: 3.12 + + - name: Packages + run: pip install -r tests/requirements.txt + + - name: Linters + run: make lint + + nix: + name: "py-${{ matrix.config.python }} lib-${{ matrix.config.ffmpeg }} ${{matrix.config.os}}" + runs-on: ${{ matrix.config.os }} + strategy: + fail-fast: false + matrix: + config: + - {os: ubuntu-latest, python: 3.9, ffmpeg: "6.1", extras: true} + - {os: ubuntu-latest, python: 3.9, ffmpeg: "6.0"} + - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1"} + #- {os: macos-13, python: 3.12, ffmpeg: "6.1"} + + env: + PYAV_PYTHON: python${{ matrix.config.python }} + PYAV_LIBRARY: ffmpeg-${{ matrix.config.ffmpeg }} + + steps: + - uses: actions/checkout@v4 + name: Checkout + + - name: Python ${{ matrix.config.python }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.config.python }} + + - name: OS Packages + run: | + case ${{ matrix.config.os }} in + ubuntu-latest) + sudo apt-get update + sudo apt-get install autoconf automake build-essential cmake \ + libtool mercurial pkg-config texinfo wget yasm zlib1g-dev + sudo apt-get install libass-dev libfreetype6-dev libjpeg-dev \ + libtheora-dev libvorbis-dev libx264-dev + if [[ "${{ matrix.config.extras }}" ]]; then + sudo apt-get install doxygen + fi + ;; + macos-13) + brew install automake libtool nasm pkg-config shtool texi2html wget + brew install libass libjpeg libpng libvorbis libvpx opus theora x264 + ;; + esac + + - name: Pip and FFmpeg + run: | + . scripts/activate.sh ffmpeg-${{ matrix.config.ffmpeg }} + scripts/build-deps + + - name: Build + run: | + . scripts/activate.sh ffmpeg-${{ matrix.config.ffmpeg }} + scripts/build + + - name: Test + run: | + . scripts/activate.sh ffmpeg-${{ matrix.config.ffmpeg }} + python -m av --version # Assert it can import. + scripts/test + + - name: Docs + if: matrix.config.extras + run: | + . scripts/activate.sh ffmpeg-${{ matrix.config.ffmpeg }} + make -C docs html + + - name: Doctest + if: matrix.config.extras + run: | + . scripts/activate.sh ffmpeg-${{ matrix.config.ffmpeg }} + make -C docs test + + - name: Examples + if: matrix.config.extras + run: | + . scripts/activate.sh ffmpeg-${{ matrix.config.ffmpeg }} + scripts/test examples + + windows: + name: "py-${{ matrix.config.python }} lib-${{ matrix.config.ffmpeg }} ${{matrix.config.os}}" + runs-on: ${{ matrix.config.os }} + + strategy: + fail-fast: false + matrix: + config: + - {os: windows-latest, python: 3.9, ffmpeg: "6.1"} + - {os: windows-latest, python: 3.9, ffmpeg: "6.0"} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Conda + shell: bash + run: | + . $CONDA/etc/profile.d/conda.sh + conda config --set always_yes true + conda config --add channels conda-forge + conda create -q -n pyav \ + cython \ + numpy \ + pillow \ + python=${{ matrix.config.python }} \ + setuptools + + - name: Build + shell: bash + run: | + . $CONDA/etc/profile.d/conda.sh + conda activate pyav + python scripts\\fetch-vendor.py --config-file scripts\\ffmpeg-${{ matrix.config.ffmpeg }}.json $CONDA_PREFIX\\Library + python setup.py build_ext --inplace --ffmpeg-dir=$CONDA_PREFIX\\Library + + - name: Test + shell: bash + run: | + . $CONDA/etc/profile.d/conda.sh + conda activate pyav + python setup.py test diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4dd70a1ad..d1c57372b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,157 +2,8 @@ name: tests on: release: types: [published] - push: - branches: main - paths-ignore: - - '**.md' - - '**.rst' - - '**.txt' - pull_request: - branches: main - paths-ignore: - - '**.md' - - '**.rst' - - '**.txt' + workflow_dispatch: jobs: - style: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Python - uses: actions/setup-python@v5 - with: - python-version: 3.12 - - - name: Packages - run: pip install -r tests/requirements.txt - - - name: Linters - run: make lint - - nix: - name: "py-${{ matrix.config.python }} lib-${{ matrix.config.ffmpeg }} ${{matrix.config.os}}" - runs-on: ${{ matrix.config.os }} - strategy: - fail-fast: false - matrix: - config: - - {os: ubuntu-latest, python: 3.9, ffmpeg: "6.1", extras: true} - - {os: ubuntu-latest, python: 3.9, ffmpeg: "6.0"} - - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1"} - # - {os: macos-latest, python: 3.9, ffmpeg: "6.1"} - - env: - PYAV_PYTHON: python${{ matrix.config.python }} - PYAV_LIBRARY: ffmpeg-${{ matrix.config.ffmpeg }} - - steps: - - uses: actions/checkout@v4 - name: Checkout - - - name: Python ${{ matrix.config.python }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.config.python }} - - - name: OS Packages - run: | - case ${{ matrix.config.os }} in - ubuntu-latest) - sudo apt-get update - sudo apt-get install autoconf automake build-essential cmake \ - libtool mercurial pkg-config texinfo wget yasm zlib1g-dev - sudo apt-get install libass-dev libfreetype6-dev libjpeg-dev \ - libtheora-dev libvorbis-dev libx264-dev - if [[ "${{ matrix.config.extras }}" ]]; then - sudo apt-get install doxygen - fi - ;; - macos-13) - brew install automake libtool nasm pkg-config shtool texi2html wget - brew install libass libjpeg libpng libvorbis libvpx opus theora x264 - ;; - esac - - - name: Pip and FFmpeg - run: | - . scripts/activate.sh ffmpeg-${{ matrix.config.ffmpeg }} - scripts/build-deps - - - name: Build - run: | - . scripts/activate.sh ffmpeg-${{ matrix.config.ffmpeg }} - scripts/build - - - name: Test - run: | - . scripts/activate.sh ffmpeg-${{ matrix.config.ffmpeg }} - python -m av --version # Assert it can import. - scripts/test - - - name: Docs - if: matrix.config.extras - run: | - . scripts/activate.sh ffmpeg-${{ matrix.config.ffmpeg }} - make -C docs html - - - name: Doctest - if: matrix.config.extras - run: | - . scripts/activate.sh ffmpeg-${{ matrix.config.ffmpeg }} - make -C docs test - - - name: Examples - if: matrix.config.extras - run: | - . scripts/activate.sh ffmpeg-${{ matrix.config.ffmpeg }} - scripts/test examples - - windows: - name: "py-${{ matrix.config.python }} lib-${{ matrix.config.ffmpeg }} ${{matrix.config.os}}" - runs-on: ${{ matrix.config.os }} - - strategy: - fail-fast: false - matrix: - config: - - {os: windows-latest, python: 3.9, ffmpeg: "6.1"} - - {os: windows-latest, python: 3.9, ffmpeg: "6.0"} - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Conda - shell: bash - run: | - . $CONDA/etc/profile.d/conda.sh - conda config --set always_yes true - conda config --add channels conda-forge - conda create -q -n pyav \ - cython \ - numpy \ - pillow \ - python=${{ matrix.config.python }} \ - setuptools - - - name: Build - shell: bash - run: | - . $CONDA/etc/profile.d/conda.sh - conda activate pyav - python scripts\\fetch-vendor.py --config-file scripts\\ffmpeg-${{ matrix.config.ffmpeg }}.json $CONDA_PREFIX\\Library - python setup.py build_ext --inplace --ffmpeg-dir=$CONDA_PREFIX\\Library - - - name: Test - shell: bash - run: | - . $CONDA/etc/profile.d/conda.sh - conda activate pyav - python setup.py test - package-source: runs-on: ubuntu-latest steps: From ea6c6d84a65925666b40874f4278e69535f28dac Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 27 Apr 2024 04:40:25 -0400 Subject: [PATCH 009/365] Add macos to smoke tests --- .github/workflows/smoke.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 69d5db44e..a0fa94fe5 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -40,7 +40,7 @@ jobs: - {os: ubuntu-latest, python: 3.9, ffmpeg: "6.1", extras: true} - {os: ubuntu-latest, python: 3.9, ffmpeg: "6.0"} - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1"} - #- {os: macos-13, python: 3.12, ffmpeg: "6.1"} + - {os: macos-12, python: 3.9, ffmpeg: "6.1"} env: PYAV_PYTHON: python${{ matrix.config.python }} @@ -68,7 +68,7 @@ jobs: sudo apt-get install doxygen fi ;; - macos-13) + macos-12) brew install automake libtool nasm pkg-config shtool texi2html wget brew install libass libjpeg libpng libvorbis libvpx opus theora x264 ;; From 683ce79a9245535b3c42339032f12171b361a975 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 5 May 2024 03:43:54 -0400 Subject: [PATCH 010/365] Build with Arm for PyPy now --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d1c57372b..17c9a687b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -28,7 +28,7 @@ jobs: fail-fast: false matrix: include: - - os: macos-13 + - os: macos-latest arch: arm64 - os: macos-13 arch: x86_64 @@ -63,7 +63,7 @@ jobs: CIBW_ENVIRONMENT_MACOS: PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig LDFLAGS=-headerpad_max_install_names CIBW_ENVIRONMENT_WINDOWS: INCLUDE=C:\\cibw\\vendor\\include LIB=C:\\cibw\\vendor\\lib PYAV_SKIP_TESTS=unicode_filename CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: delvewheel repair --add-path C:\cibw\vendor\bin -w {dest_dir} {wheel} - CIBW_SKIP: pp38-win* *-musllinux* + CIBW_SKIP: "*-musllinux*" CIBW_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m unittest discover -t {project} -s tests && mv {project}/av.disabled {project}/av CIBW_TEST_REQUIRES: numpy # skip tests when there are no binary wheels of numpy From dcee25a3146b50839dabe7c249633a718db5a6d5 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 7 May 2024 18:37:58 -0400 Subject: [PATCH 011/365] Set version to 12.1.0 --- av/about.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/about.py b/av/about.py index 79a759ae1..5170f5963 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "13.0.0rc1" +__version__ = "12.1.0rc1" From 7ee9aa0710f087b2d72a7b18a5ad7a8212c1dfb0 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 7 May 2024 20:32:53 -0400 Subject: [PATCH 012/365] Prevent being YAML'd in the future --- .github/workflows/smoke.yml | 8 ++++---- .github/workflows/tests.yml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index a0fa94fe5..2c360ce55 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -22,7 +22,7 @@ jobs: - name: Python uses: actions/setup-python@v5 with: - python-version: 3.12 + python-version: "3.12" - name: Packages run: pip install -r tests/requirements.txt @@ -37,10 +37,10 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-latest, python: 3.9, ffmpeg: "6.1", extras: true} - - {os: ubuntu-latest, python: 3.9, ffmpeg: "6.0"} + - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.1", extras: true} + - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.0"} - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1"} - - {os: macos-12, python: 3.9, ffmpeg: "6.1"} + - {os: macos-12, python: "3.9", ffmpeg: "6.1"} env: PYAV_PYTHON: python${{ matrix.config.python }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 17c9a687b..564a1c922 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: "3.9" - name: Build source package run: | pip install cython @@ -44,7 +44,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: "3.9" - name: Set up QEMU if: matrix.os == 'ubuntu-latest' uses: docker/setup-qemu-action@v3 From 471fc13dc5ed8c6a42ed8aa074144e53749282ca Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 9 May 2024 01:47:23 -0400 Subject: [PATCH 013/365] Subtitles: make text bytes, type hints --- av/packet.pyi | 3 +-- av/subtitles/codeccontext.pyx | 13 +++++++------ av/subtitles/subtitle.pxd | 5 ----- av/subtitles/subtitle.pyi | 3 ++- av/subtitles/subtitle.pyx | 4 +++- 5 files changed, 13 insertions(+), 15 deletions(-) diff --git a/av/packet.pyi b/av/packet.pyi index 14b4b0bce..24cb9f5a0 100644 --- a/av/packet.pyi +++ b/av/packet.pyi @@ -1,6 +1,5 @@ from collections.abc import Buffer from fractions import Fraction -from typing import Iterator from av.subtitles.subtitle import SubtitleSet @@ -22,5 +21,5 @@ class Packet(Buffer): is_disposable: bool def __init__(self, input: int | bytes | None = None) -> None: ... - def decode(self) -> Iterator[SubtitleSet]: ... + def decode(self) -> list[SubtitleSet]: ... def __buffer__(self, arg1) -> memoryview: ... diff --git a/av/subtitles/codeccontext.pyx b/av/subtitles/codeccontext.pyx index 227add919..10e8a6009 100644 --- a/av/subtitles/codeccontext.pyx +++ b/av/subtitles/codeccontext.pyx @@ -8,13 +8,14 @@ from av.subtitles.subtitle cimport SubtitleProxy, SubtitleSet cdef class SubtitleCodecContext(CodecContext): cdef _send_packet_and_recv(self, Packet packet): cdef SubtitleProxy proxy = SubtitleProxy() - cdef int got_frame = 0 - err_check(lib.avcodec_decode_subtitle2( - self.ptr, - &proxy.struct, - &got_frame, - packet.ptr if packet else NULL)) + + err_check( + lib.avcodec_decode_subtitle2( + self.ptr, &proxy.struct, &got_frame, packet.ptr if packet else NULL + ) + ) + if got_frame: return [SubtitleSet(proxy)] else: diff --git a/av/subtitles/subtitle.pxd b/av/subtitles/subtitle.pxd index e9003ab9b..29ddc4220 100644 --- a/av/subtitles/subtitle.pxd +++ b/av/subtitles/subtitle.pxd @@ -4,19 +4,16 @@ from av.packet cimport Packet cdef class SubtitleProxy: - cdef lib.AVSubtitle struct cdef class SubtitleSet: - cdef readonly Packet packet cdef SubtitleProxy proxy cdef readonly tuple rects cdef class Subtitle: - cdef SubtitleProxy proxy cdef lib.AVSubtitleRect *ptr cdef readonly bytes type @@ -28,11 +25,9 @@ cdef class ASSSubtitle(Subtitle): pass cdef class BitmapSubtitle(Subtitle): - cdef readonly planes cdef class BitmapSubtitlePlane: - cdef readonly BitmapSubtitle subtitle cdef readonly int index cdef readonly long buffer_size diff --git a/av/subtitles/subtitle.pyi b/av/subtitles/subtitle.pyi index 6dcd16e8d..cdbe7fb31 100644 --- a/av/subtitles/subtitle.pyi +++ b/av/subtitles/subtitle.pyi @@ -5,6 +5,7 @@ class SubtitleSet: start_display_time: int end_display_time: int pts: int + rects: tuple[Subtitle] def __len__(self) -> int: ... def __iter__(self) -> Iterator[Subtitle]: ... @@ -29,7 +30,7 @@ class BitmapSubtitlePlane: class TextSubtitle(Subtitle): type: Literal[b"text"] - text: str + text: bytes class AssSubtitle(Subtitle): type: Literal[b"ass"] diff --git a/av/subtitles/subtitle.pyx b/av/subtitles/subtitle.pyx index d8d45e3fc..e1deca7b7 100644 --- a/av/subtitles/subtitle.pyx +++ b/av/subtitles/subtitle.pyx @@ -145,7 +145,9 @@ cdef class TextSubtitle(Subtitle): @property def text(self): - return self.ptr.text + if self.ptr.text is not NULL: + return PyBytes_FromString(self.ptr.text) + return b"" cdef class AssSubtitle(Subtitle): From f42cc8852a67cbdbae3f7775f94fb55a3687cca9 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 9 May 2024 04:22:40 -0400 Subject: [PATCH 014/365] Allow packet.duration to be writable --- av/packet.pyx | 7 +++++++ tests/test_packet.py | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/av/packet.pyx b/av/packet.pyx index 24fd5581b..112421090 100644 --- a/av/packet.pyx +++ b/av/packet.pyx @@ -177,6 +177,13 @@ cdef class Packet(Buffer): if self.ptr.duration != lib.AV_NOPTS_VALUE: return self.ptr.duration + @duration.setter + def duration(self, v): + if v is None: + self.ptr.duration = lib.AV_NOPTS_VALUE + else: + self.ptr.duration = v + @property def is_keyframe(self): return bool(self.ptr.flags & lib.AV_PKT_FLAG_KEY) diff --git a/tests/test_packet.py b/tests/test_packet.py index 8a2b6266c..cb0909116 100644 --- a/tests/test_packet.py +++ b/tests/test_packet.py @@ -39,3 +39,11 @@ def test_is_disposable(self): self.assertTrue(packet.is_disposable) else: self.assertFalse(packet.is_disposable) + + def test_set_duration(self): + with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: + for packet in container.demux(): + old_duration = packet.duration + packet.duration += 10 + + self.assertEqual(packet.duration, old_duration + 10) From b8dc8b98912f3a29a00fa4c00ab23a6a0cdaf624 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 14 May 2024 02:34:21 -0400 Subject: [PATCH 015/365] Fix #1378 Use 0 (unknown color range) if the given a None value instead of copying the input frame. --- av/video/reformatter.pyx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/av/video/reformatter.pyx b/av/video/reformatter.pyx index f41094eda..11ee1bf74 100644 --- a/av/video/reformatter.pyx +++ b/av/video/reformatter.pyx @@ -87,8 +87,8 @@ cdef class VideoReformatter: cdef int c_src_colorspace = (Colorspace[src_colorspace].value if src_colorspace is not None else frame.colorspace) cdef int c_dst_colorspace = (Colorspace[dst_colorspace].value if dst_colorspace is not None else frame.colorspace) cdef int c_interpolation = (Interpolation[interpolation] if interpolation is not None else Interpolation.BILINEAR).value - cdef int c_src_color_range = (ColorRange[src_color_range].value if src_color_range is not None else frame.color_range) - cdef int c_dst_color_range = (ColorRange[dst_color_range].value if dst_color_range is not None else frame.color_range) + cdef int c_src_color_range = (ColorRange[src_color_range].value if src_color_range is not None else 0) + cdef int c_dst_color_range = (ColorRange[dst_color_range].value if dst_color_range is not None else 0) return self._reformat( frame, From ace1bfa7e513d1cefc5f1d4ca5ac69a22675ca2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Tue, 28 May 2024 07:48:33 +0200 Subject: [PATCH 016/365] Update binary FFmpeg build This adds support for `webp` and removes X11 libraries which were dragged into the macOs builds. --- scripts/ffmpeg-6.1.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ffmpeg-6.1.json b/scripts/ffmpeg-6.1.json index d5b84be19..49ce58d17 100644 --- a/scripts/ffmpeg-6.1.json +++ b/scripts/ffmpeg-6.1.json @@ -1,3 +1,3 @@ { - "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/6.1.1-1/ffmpeg-{platform}.tar.gz"] + "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/6.1.1-3/ffmpeg-{platform}.tar.gz"] } From 3d0299f7022a032ae094322db431fb4bcc94244d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Tue, 28 May 2024 07:52:13 +0200 Subject: [PATCH 017/365] Restore support for Python 3.8 This reverts 5707fb605cbd132e03c60e1a9e9463fd747ea93c. --- .github/workflows/smoke.yml | 10 +++++----- .github/workflows/tests.yml | 4 ++-- setup.py | 3 ++- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 2c360ce55..39cf096cd 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -37,10 +37,10 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.1", extras: true} - - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.0"} + - {os: ubuntu-latest, python: "3.8", ffmpeg: "6.1", extras: true} + - {os: ubuntu-latest, python: "3.8", ffmpeg: "6.0"} - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1"} - - {os: macos-12, python: "3.9", ffmpeg: "6.1"} + - {os: macos-12, python: "3.8", ffmpeg: "6.1"} env: PYAV_PYTHON: python${{ matrix.config.python }} @@ -116,8 +116,8 @@ jobs: fail-fast: false matrix: config: - - {os: windows-latest, python: 3.9, ffmpeg: "6.1"} - - {os: windows-latest, python: 3.9, ffmpeg: "6.0"} + - {os: windows-latest, python: "3.8", ffmpeg: "6.1"} + - {os: windows-latest, python: "3.8", ffmpeg: "6.0"} steps: - name: Checkout diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 564a1c922..fcafb8d82 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.9" + python-version: "3.8" - name: Build source package run: | pip install cython @@ -44,7 +44,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.9" + python-version: "3.8" - name: Set up QEMU if: matrix.os == 'ubuntu-latest' uses: docker/setup-qemu-action@v3 diff --git a/setup.py b/setup.py index 8cfcbaa58..cdedbcfb6 100644 --- a/setup.py +++ b/setup.py @@ -205,7 +205,7 @@ def parse_cflags(raw_flags): url="https://github.com/PyAV-Org/PyAV", packages=find_packages(exclude=["build*", "examples*", "scratchpad*", "tests*"]), package_data=package_data, - python_requires=">=3.9", + python_requires=">=3.8", zip_safe=False, ext_modules=ext_modules, test_suite="tests", @@ -222,6 +222,7 @@ def parse_cflags(raw_flags): "Operating System :: Unix", "Operating System :: Microsoft :: Windows", "Programming Language :: Cython", + "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", From 5e23c2adc445b2b22c8c2137bcb1a55cabf5bf56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Mon, 27 May 2024 23:48:15 +0200 Subject: [PATCH 018/365] Update GitHub actions to their latest version The behaviour of `actions/upload-artifact` have changed, we now need distinct names for each artifact. This also means adjusting our use of `actions/download-artifact` to collect all artifacts. --- .github/workflows/tests.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fcafb8d82..14bc87525 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,9 +17,9 @@ jobs: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-6.1.json /tmp/vendor PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig python setup.py sdist - name: Upload source package - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: dist + name: dist-source path: dist/ package-wheel: @@ -73,9 +73,9 @@ jobs: cibuildwheel --output-dir dist shell: bash - name: Upload wheels - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: dist + name: dist-${{ matrix.os }}-${{ matrix.arch }} path: dist/ publish: @@ -83,9 +83,9 @@ jobs: needs: [package-source, package-wheel] steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: - name: dist + merge-multiple: true path: dist/ - name: Publish to PyPI if: github.event_name == 'release' && github.event.action == 'published' From e4a25fdac8c5cc155a3ffe3df75585c9deb70dd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Tue, 28 May 2024 07:57:52 +0200 Subject: [PATCH 019/365] Pin macOs to version 14 for arm64 Using `macos-latest` has bitten us in the past, so pin the version of macOs used to build wheels. --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 14bc87525..b66ef500b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -28,7 +28,7 @@ jobs: fail-fast: false matrix: include: - - os: macos-latest + - os: macos-14 arch: arm64 - os: macos-13 arch: x86_64 From f2d1f37991067aedd90bc979ebcca83f136cb3a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Wed, 29 May 2024 23:58:58 +0200 Subject: [PATCH 020/365] Restore tests against FFmpeg 5.x This reverts e884e6cd1dac2646fd002b441ca4fdfb2ef818c5. So far we have not made any changes which justify dropping FFmpeg 5.x, and we do not change the supported platforms in a minor release. We will drop compatibility for FFmpeg 5.x once we switch to the new audio channel API. --- .github/workflows/smoke.yml | 4 ++++ docs/overview/installation.rst | 2 +- scripts/ffmpeg-5.0.json | 3 +++ scripts/ffmpeg-5.1.json | 3 +++ 4 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 scripts/ffmpeg-5.0.json create mode 100644 scripts/ffmpeg-5.1.json diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 39cf096cd..7694fbfdb 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -39,6 +39,8 @@ jobs: config: - {os: ubuntu-latest, python: "3.8", ffmpeg: "6.1", extras: true} - {os: ubuntu-latest, python: "3.8", ffmpeg: "6.0"} + - {os: ubuntu-latest, python: "3.8", ffmpeg: "5.1"} + - {os: ubuntu-latest, python: "3.8", ffmpeg: "5.0"} - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1"} - {os: macos-12, python: "3.8", ffmpeg: "6.1"} @@ -118,6 +120,8 @@ jobs: config: - {os: windows-latest, python: "3.8", ffmpeg: "6.1"} - {os: windows-latest, python: "3.8", ffmpeg: "6.0"} + - {os: windows-latest, python: "3.8", ffmpeg: "5.1"} + - {os: windows-latest, python: "3.8", ffmpeg: "5.0"} steps: - name: Checkout diff --git a/docs/overview/installation.rst b/docs/overview/installation.rst index 4fdeb38a5..c43504d7a 100644 --- a/docs/overview/installation.rst +++ b/docs/overview/installation.rst @@ -55,7 +55,7 @@ See the `Conda quick install `_ docs t Bring your own FFmpeg --------------------- -PyAV can also be compiled against your own build of FFmpeg ((version ``6.0`` or higher). You can force installing PyAV from source by running: +PyAV can also be compiled against your own build of FFmpeg ((version ``5.0`` or higher). You can force installing PyAV from source by running: .. code-block:: bash diff --git a/scripts/ffmpeg-5.0.json b/scripts/ffmpeg-5.0.json new file mode 100644 index 000000000..41969df2c --- /dev/null +++ b/scripts/ffmpeg-5.0.json @@ -0,0 +1,3 @@ +{ + "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.0.1-1/ffmpeg-{platform}.tar.gz"] +} diff --git a/scripts/ffmpeg-5.1.json b/scripts/ffmpeg-5.1.json new file mode 100644 index 000000000..75e4b8eca --- /dev/null +++ b/scripts/ffmpeg-5.1.json @@ -0,0 +1,3 @@ +{ + "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.1.3-1/ffmpeg-{platform}.tar.gz"] +} From 873f17975834aef62c682c955f150650f0237071 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 29 May 2024 20:39:51 -0400 Subject: [PATCH 021/365] Release v12.1.0 --- AUTHORS.py | 3 + AUTHORS.rst | 158 +++++++++++++++++++++++++------------------------- CHANGELOG.rst | 22 +++++++ av/about.py | 2 +- 4 files changed, 105 insertions(+), 80 deletions(-) diff --git a/AUTHORS.py b/AUTHORS.py index eb7ac721c..fd47a713d 100644 --- a/AUTHORS.py +++ b/AUTHORS.py @@ -26,6 +26,7 @@ 'tcaswell@bnl.gov': 'tcaswell@gmail.com', 'xxr3376@gmail.com': 'xxr@megvii.com', 'dallan@pha.jhu.edu': 'daniel.b.allan@gmail.com', + '61652821+laggykiller@users.noreply.github.com': 'chaudominic2@gmail.com', } @@ -55,6 +56,8 @@ 'vidartf@gmail.com': 'vidartf', 'willpatera@gmail.com': 'willpatera', 'xxr@megvii.com': 'xxr3376', + 'chaudominic2@gmail.com': 'laggykiller', + 'wyattblue@auto-editor.com': 'WyattBlue', } diff --git a/AUTHORS.rst b/AUTHORS.rst index 87f2492ad..68595a7da 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -7,86 +7,86 @@ All contributors (by number of commits): * Jeremy Lainé ; `@jlaine `_ +- WyattBlue ; `@WyattBlue `_ - Mark Reid ; `@markreidvfx `_ -* WyattBlue +* Vidar Tonaas Fauske ; `@vidartf `_ +* laggykiller ; `@laggykiller `_ +* Billy Shambrook ; `@billyshambrook `_ +* Casper van der Wel +* Philip de Nier +* Tadas Dailyda +* JoeUgly <41972063+JoeUgly@users.noreply.github.com> +* Justin Wong <46082645+uvjustin@users.noreply.github.com> -- Vidar Tonaas Fauske ; `@vidartf `_ -- Billy Shambrook ; `@billyshambrook `_ -- Casper van der Wel -- Philip de Nier -- Tadas Dailyda -- laggykiller <61652821+laggykiller@users.noreply.github.com> -- JoeUgly <41972063+JoeUgly@users.noreply.github.com> -- Justin Wong <46082645+uvjustin@users.noreply.github.com> +- Alba Mendez +- Mark Harfouche +- Xinran Xu ; `@xxr3376 `_ +- Dan Allan ; `@danielballan `_ +- Dave Johansen +- Christoph Rackwitz +- Alireza Davoudi ; `@adavoudi `_ +- Jonathan Drolet +- Moritz Kassner ; `@mkassner `_ +- Santtu Keskinen +- Thomas A Caswell ; `@tacaswell `_ +- Ulrik Mikaelsson ; `@rawler `_ +- Wel C. van der +- Will Patera ; `@willpatera `_ -* Alba Mendez -* Mark Harfouche -* Xinran Xu ; `@xxr3376 `_ -* laggykiller -* Dan Allan ; `@danielballan `_ -* Dave Johansen -* Christoph Rackwitz -* Alireza Davoudi ; `@adavoudi `_ -* Jonathan Drolet -* Moritz Kassner ; `@mkassner `_ -* Thomas A Caswell ; `@tacaswell `_ -* Ulrik Mikaelsson ; `@rawler `_ -* Wel C. van der -* Will Patera ; `@willpatera `_ - -- rutsh -- Felix Vollmer -- Santiago Castro -- Christian Clauss -- Ihor Liubymov -- Johannes Erdfelt -- Karl Litterfeldt ; `@litterfeldt `_ -- Martin Larralde -- Simon-Martin Schröder -- mephi42 -- Miles Kaufmann -- Pablo Prietz -- Radek Senfeld ; `@radek-senfeld `_ -- Benjamin Chrétien <2742231+bchretien@users.noreply.github.com> -- Marc Mueller <30130371+cdce8p@users.noreply.github.com> -- zzjjbb <31069326+zzjjbb@users.noreply.github.com> -- Hanz <40712686+HanzCEO@users.noreply.github.com> -- Joe Schiff <41972063+JoeSchiff@users.noreply.github.com> -- Ian Lee -- Ryan Huang -- Arthur Barros -- Carlos Ruiz -- David Plowman -- Maxime Desroches -- egao1980 -- Eric Kalosa-Kenyon -- Gemfield -- Jonathan Martin -- Johan Jeppsson Karlin -- Philipp Klaus -- Mattias Wadman -- Manuel Goacolou -- Julian Schweizer -- Ömer Sezgin Uğurlu -- Orivej Desh -- Philipp Krähenbühl -- ramoncaldeira -- Roland van Laar -- Santiago Castro -- Kengo Sawatsu -- FirefoxMetzger -- hyenal -- Brendan Long ; `@brendanlong `_ -- Семён Марьясин -- Stephen.Y -- Tom Flanagan -- Tim O'Shea -- Tim Ahpee -- Jonas Tingeborn -- Pino Toscano -- Ulrik Mikaelsson -- Vasiliy Kotov -- Koichi Akabe -- David Joy -- Sviatoslav Sydorenko (Святослав Сидоренко) +* rutsh +* Felix Vollmer +* Santiago Castro +* Christian Clauss +* Ihor Liubymov +* Johannes Erdfelt +* Karl Litterfeldt ; `@litterfeldt `_ +* Martin Larralde +* Simon-Martin Schröder +* mephi42 +* Miles Kaufmann +* Pablo Prietz +* Radek Senfeld ; `@radek-senfeld `_ +* Benjamin Chrétien <2742231+bchretien@users.noreply.github.com> +* Marc Mueller <30130371+cdce8p@users.noreply.github.com> +* zzjjbb <31069326+zzjjbb@users.noreply.github.com> +* Hanz <40712686+HanzCEO@users.noreply.github.com> +* Joe Schiff <41972063+JoeSchiff@users.noreply.github.com> +* Artturin +* Ian Lee +* Ryan Huang +* Arthur Barros +* Carlos Ruiz +* David Plowman +* Maxime Desroches +* egao1980 +* Eric Kalosa-Kenyon +* Gemfield +* Jonathan Martin +* Johan Jeppsson Karlin +* Philipp Klaus +* Mattias Wadman +* Manuel Goacolou +* Julian Schweizer +* Ömer Sezgin Uğurlu +* Orivej Desh +* Philipp Krähenbühl +* ramoncaldeira +* Roland van Laar +* Santiago Castro +* Kengo Sawatsu +* FirefoxMetzger +* hyenal +* Brendan Long ; `@brendanlong `_ +* Семён Марьясин +* Stephen.Y +* Tom Flanagan +* Tim O'Shea +* Tim Ahpee +* Jonas Tingeborn +* Pino Toscano +* Ulrik Mikaelsson +* Vasiliy Kotov +* Koichi Akabe +* David Joy +* Sviatoslav Sydorenko (Святослав Сидоренко) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 72aa0897b..526ce98a6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,28 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. + +v12.1.0 +------- + +Features: + +- Build binary wheels with webp support. +- Allow disabling logs, disable logs by default. +- Add bitstream filters by @skeskinen in (:issue:`1375`) (:issue:`1379`). +- Expose CodecContext flush_buffers by @skeskinen in (:issue:`1382`). + +Fixes: + +- Fix type stubs, add missing type stubs. +- Add S12M_TIMECODE by @WyattBlue in (:issue:`1381`). +- Subtitle.text now returns bytes by @WyattBlue in (:issue:`13981). +- Allow packet.duration to be writable by @WyattBlue in (:issue:`1399`). +- Remove deprecated `VideoStream.frame_rate` by @WyattBlue in (:issue:`1351`). +- Build with Arm for PyPy now by @WyattBlue in (:issue:`1395`). +- Fix #1378 by @WyattBlue in (:issue:`1400`). +- setup.py: use PKG_CONFIG env var to get the pkg-config to use by @Artturin in (:issue:`1387`). + v12.0.0 ------- diff --git a/av/about.py b/av/about.py index 5170f5963..13b4a72f4 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "12.1.0rc1" +__version__ = "12.1.0" From c9e4f3eda5b25bb620e526cfb30139994fac6904 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 30 May 2024 01:56:04 -0400 Subject: [PATCH 022/365] Bump to next dev version --- av/about.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/about.py b/av/about.py index 13b4a72f4..79a759ae1 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "12.1.0" +__version__ = "13.0.0rc1" From 80f1dd7103eaf5afa00b6abff1d003b845779b5e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 30 May 2024 02:09:41 -0400 Subject: [PATCH 023/365] Drop support for python3.8 --- .github/workflows/smoke.yml | 18 +++++++++--------- .github/workflows/tests.yml | 4 ++-- setup.py | 3 +-- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 7694fbfdb..5a23ce156 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -37,12 +37,12 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-latest, python: "3.8", ffmpeg: "6.1", extras: true} - - {os: ubuntu-latest, python: "3.8", ffmpeg: "6.0"} - - {os: ubuntu-latest, python: "3.8", ffmpeg: "5.1"} - - {os: ubuntu-latest, python: "3.8", ffmpeg: "5.0"} + - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.1", extras: true} + - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.0"} + - {os: ubuntu-latest, python: "3.9", ffmpeg: "5.1"} + - {os: ubuntu-latest, python: "3.9", ffmpeg: "5.0"} - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1"} - - {os: macos-12, python: "3.8", ffmpeg: "6.1"} + - {os: macos-12, python: "3.9", ffmpeg: "6.1"} env: PYAV_PYTHON: python${{ matrix.config.python }} @@ -118,10 +118,10 @@ jobs: fail-fast: false matrix: config: - - {os: windows-latest, python: "3.8", ffmpeg: "6.1"} - - {os: windows-latest, python: "3.8", ffmpeg: "6.0"} - - {os: windows-latest, python: "3.8", ffmpeg: "5.1"} - - {os: windows-latest, python: "3.8", ffmpeg: "5.0"} + - {os: windows-latest, python: "3.9", ffmpeg: "6.1"} + - {os: windows-latest, python: "3.9", ffmpeg: "6.0"} + - {os: windows-latest, python: "3.9", ffmpeg: "5.1"} + - {os: windows-latest, python: "3.9", ffmpeg: "5.0"} steps: - name: Checkout diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b66ef500b..550065a84 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.8" + python-version: "3.9" - name: Build source package run: | pip install cython @@ -44,7 +44,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.8" + python-version: "3.9" - name: Set up QEMU if: matrix.os == 'ubuntu-latest' uses: docker/setup-qemu-action@v3 diff --git a/setup.py b/setup.py index cdedbcfb6..8cfcbaa58 100644 --- a/setup.py +++ b/setup.py @@ -205,7 +205,7 @@ def parse_cflags(raw_flags): url="https://github.com/PyAV-Org/PyAV", packages=find_packages(exclude=["build*", "examples*", "scratchpad*", "tests*"]), package_data=package_data, - python_requires=">=3.8", + python_requires=">=3.9", zip_safe=False, ext_modules=ext_modules, test_suite="tests", @@ -222,7 +222,6 @@ def parse_cflags(raw_flags): "Operating System :: Unix", "Operating System :: Microsoft :: Windows", "Programming Language :: Cython", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", From 77e86c2c666b61b70165ce306077d9c36c9821e3 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 31 May 2024 05:32:58 -0400 Subject: [PATCH 024/365] Fix for arch/gcc14 --- av/bitstream.pxd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/bitstream.pxd b/av/bitstream.pxd index 620fa0366..dbb89c984 100644 --- a/av/bitstream.pxd +++ b/av/bitstream.pxd @@ -5,7 +5,7 @@ from av.packet cimport Packet cdef class BitStreamFilterContext: - cdef const lib.AVBSFContext *ptr + cdef lib.AVBSFContext *ptr cpdef filter(self, Packet packet=?) cpdef flush(self) From deb1cfc31f86ef1cd62f514b067d1889a458f7df Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 3 Jun 2024 02:55:12 -0400 Subject: [PATCH 025/365] Drop ffmpeg 5 --- .github/workflows/smoke.yml | 4 ---- docs/overview/installation.rst | 2 +- scripts/ffmpeg-5.0.json | 3 --- scripts/ffmpeg-5.1.json | 3 --- 4 files changed, 1 insertion(+), 11 deletions(-) delete mode 100644 scripts/ffmpeg-5.0.json delete mode 100644 scripts/ffmpeg-5.1.json diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 5a23ce156..1eda45934 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -39,8 +39,6 @@ jobs: config: - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.1", extras: true} - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.0"} - - {os: ubuntu-latest, python: "3.9", ffmpeg: "5.1"} - - {os: ubuntu-latest, python: "3.9", ffmpeg: "5.0"} - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1"} - {os: macos-12, python: "3.9", ffmpeg: "6.1"} @@ -120,8 +118,6 @@ jobs: config: - {os: windows-latest, python: "3.9", ffmpeg: "6.1"} - {os: windows-latest, python: "3.9", ffmpeg: "6.0"} - - {os: windows-latest, python: "3.9", ffmpeg: "5.1"} - - {os: windows-latest, python: "3.9", ffmpeg: "5.0"} steps: - name: Checkout diff --git a/docs/overview/installation.rst b/docs/overview/installation.rst index c43504d7a..1e57c8bef 100644 --- a/docs/overview/installation.rst +++ b/docs/overview/installation.rst @@ -55,7 +55,7 @@ See the `Conda quick install `_ docs t Bring your own FFmpeg --------------------- -PyAV can also be compiled against your own build of FFmpeg ((version ``5.0`` or higher). You can force installing PyAV from source by running: +PyAV can also be compiled against your own build of FFmpeg (version ``6.0`` or higher). You can force installing PyAV from source by running: .. code-block:: bash diff --git a/scripts/ffmpeg-5.0.json b/scripts/ffmpeg-5.0.json deleted file mode 100644 index 41969df2c..000000000 --- a/scripts/ffmpeg-5.0.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.0.1-1/ffmpeg-{platform}.tar.gz"] -} diff --git a/scripts/ffmpeg-5.1.json b/scripts/ffmpeg-5.1.json deleted file mode 100644 index 75e4b8eca..000000000 --- a/scripts/ffmpeg-5.1.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/5.1.3-1/ffmpeg-{platform}.tar.gz"] -} From 723dd775c98d26a957d43e225aae09214b73a719 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 4 Jun 2024 03:00:10 -0400 Subject: [PATCH 026/365] long doesn't exist anymore --- av/packet.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/packet.pyx b/av/packet.pyx index 112421090..63b0b50ab 100644 --- a/av/packet.pyx +++ b/av/packet.pyx @@ -25,7 +25,7 @@ cdef class Packet(Buffer): if input is None: return - if isinstance(input, (int, long)): + if isinstance(input, int): size = input else: source = bytesource(input) From e09067bb4f92f3e71a7d58ef2a1ec32535b4ee76 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 4 Jun 2024 03:02:34 -0400 Subject: [PATCH 027/365] Clean up time docs --- docs/api/time.rst | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/api/time.rst b/docs/api/time.rst index fd65de1a2..a063e876e 100644 --- a/docs/api/time.rst +++ b/docs/api/time.rst @@ -57,7 +57,7 @@ In many cases a stream has a time base of ``1 / frame_rate``, and then its frame 1 -For convenince, :attr:`.Frame.time` is a ``float`` in seconds: +For convenience, :attr:`.Frame.time` is a ``float`` in seconds: .. doctest:: @@ -65,10 +65,10 @@ For convenince, :attr:`.Frame.time` is a ``float`` in seconds: 0.04 -FFMpeg Internals +FFmpeg Internals ---------------- -.. note:: Time in FFmpeg is not 100% clear to us (see :ref:`authority_of_docs`). At times the FFmpeg documentation and canonical seeming posts in the forums appear contradictory. We've experiemented with it, and what follows is the picture that we are operating under. +.. note:: Time in FFmpeg is not 100% clear to us (see :ref:`authority_of_docs`). At times the FFmpeg documentation and canonical seeming posts in the forums appear contradictory. We've experimented with it, and what follows is the picture that we are operating under. Both :ffmpeg:`AVStream` and :ffmpeg:`AVCodecContext` have a ``time_base`` member. However, they are used for different purposes, and (this author finds) it is too easy to abstract the concept too far. @@ -82,9 +82,6 @@ For encoding, you (the PyAV developer / FFmpeg "user") must set :ffmpeg:`AVCodec You then prepare :ffmpeg:`AVFrame.pts` in :ffmpeg:`AVCodecContext.time_base`. The encoded :ffmpeg:`AVPacket.pts` is simply copied from the frame by the library, and so is still in the codec's time base. You must rescale it to :ffmpeg:`AVStream.time_base` before muxing (as all stream operations assume the packet time is in stream time base). -For fixed-fps content your frames' ``pts`` would be the frame or sample index (for video and audio, respectively). PyAV should attempt to do this. - - Decoding ........ From b474fcacbaaf3c4e8bf28e8aea56893f78e13eb6 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 4 Jun 2024 03:15:34 -0400 Subject: [PATCH 028/365] Change notice about logging --- docs/overview/caveats.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/overview/caveats.rst b/docs/overview/caveats.rst index 43b764e48..9e1f69810 100644 --- a/docs/overview/caveats.rst +++ b/docs/overview/caveats.rst @@ -30,7 +30,7 @@ The two main features that are able to cause lockups are: 1. Python IO (passing a file-like object to ``av.open``). While this is in theory possible, so far it seems like the callbacks are made in the calling thread, and so are safe. -2. Logging. As soon as you en/decode with threads you are highly likely to get log messages issues from threads started by FFmpeg, and you will get lockups. See :ref:`disable_logging`. +2. Logging. If you have logging enabled (disabled by default), those log messages could cause lockups when using threads. .. _garbage_collection: From 21d09212c9a2481d64663f865fb7e0a7c2f05ae9 Mon Sep 17 00:00:00 2001 From: Santtu Keskinen Date: Thu, 6 Jun 2024 23:56:41 +0300 Subject: [PATCH 029/365] Add flush_buffers to CodecContext docs --- docs/api/codec.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/api/codec.rst b/docs/api/codec.rst index ebc147c30..a3cc90e86 100644 --- a/docs/api/codec.rst +++ b/docs/api/codec.rst @@ -97,6 +97,7 @@ Transcoding .. automethod:: CodecContext.parse .. automethod:: CodecContext.encode .. automethod:: CodecContext.decode +.. automethod:: CodecContext.flush_buffers Flags From 5ab63db8c557cdd0bb48fd9a70e3ba6475954b70 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 12 Jun 2024 15:29:50 -0400 Subject: [PATCH 030/365] Format docs script --- docs/api/error_table.py | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/docs/api/error_table.py b/docs/api/error_table.py index e67b9f40b..3a1b5f219 100644 --- a/docs/api/error_table.py +++ b/docs/api/error_table.py @@ -1,37 +1,25 @@ - import av -rows = [( - #'Tag (Code)', - 'Exception Class', - 'Code/Enum Name', - 'FFmpeg Error Message', -)] +rows = [("Exception Class", "Code/Enum Name", "FFmpeg Error Message")] for code, cls in av.error.classes.items(): - enum = av.error.ErrorType.get(code) - + if not enum: continue - if enum.tag == b'PyAV': + if enum.tag == b"PyAV": continue - rows.append(( - #'{} ({})'.format(enum.tag, code), - '``av.{}``'.format(cls.__name__), - '``av.error.{}``'.format(enum.name), - enum.strerror, - )) + rows.append((f"``av.{cls.__name__}``", f"``av.error.{enum.name}``", enum.strerror)) lens = [max(len(row[i]) for row in rows) for i in range(len(rows[0]))] -header = tuple('=' * x for x in lens) +header = tuple("=" * x for x in lens) rows.insert(0, header) rows.insert(2, header) rows.append(header) for row in rows: - print(' '.join('{:{}s}'.format(cell, len_) for cell, len_ in zip(row, lens))) + print(" ".join("{:{}s}".format(cell, len_) for cell, len_ in zip(row, lens))) From 3646f8d39206cc8b72ca54cc841077b66bde8c5a Mon Sep 17 00:00:00 2001 From: Moonsik Park Date: Mon, 17 Jun 2024 18:03:22 +0900 Subject: [PATCH 031/365] Use `av_guess_sample_aspect_ratio` when reporting stream SAR, DAR --- av/video/stream.pyi | 4 ++-- av/video/stream.pyx | 29 +++++++++++++++++++++++++++++ include/libavformat/avformat.pxd | 6 ++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/av/video/stream.pyi b/av/video/stream.pyi index 4b78efd06..929847ed4 100644 --- a/av/video/stream.pyi +++ b/av/video/stream.pyi @@ -14,6 +14,8 @@ class VideoStream(Stream): bit_rate_tolerance: int thread_count: int thread_type: Any + sample_aspect_ratio: Fraction | None + display_aspect_ratio: Fraction | None codec_context: VideoCodecContext # from codec context format: VideoFormat @@ -24,8 +26,6 @@ class VideoStream(Stream): framerate: Fraction rate: Fraction gop_size: int - sample_aspect_ratio: Fraction | None - display_aspect_ratio: Fraction | None has_b_frames: bool coded_width: int coded_height: int diff --git a/av/video/stream.pyx b/av/video/stream.pyx index ae57ed136..6ab685167 100644 --- a/av/video/stream.pyx +++ b/av/video/stream.pyx @@ -86,3 +86,32 @@ cdef class VideoStream(Stream): # The two NULL arguments aren't used in FFmpeg >= 4.0 cdef lib.AVRational val = lib.av_guess_frame_rate(NULL, self.ptr, NULL) return avrational_to_fraction(&val) + + @property + def sample_aspect_ratio(self): + """The guessed sample aspect ratio (SAR) of this stream. + + This is a wrapper around :ffmpeg:`av_guess_sample_aspect_ratio`, and uses multiple + heuristics to decide what is "the" sample aspect ratio. + + :type: :class:`~fractions.Fraction` or ``None`` + """ + cdef lib.AVRational sar = lib.av_guess_sample_aspect_ratio(self.container.ptr, self.ptr, NULL) + return avrational_to_fraction(&sar) + + @property + def display_aspect_ratio(self): + """The guessed display aspect ratio (DAR) of this stream. + + This is calculated from :meth:`.VideoStream.guessed_sample_aspect_ratio`. + + :type: :class:`~fractions.Fraction` or ``None`` + """ + cdef lib.AVRational dar + + lib.av_reduce( + &dar.num, &dar.den, + self.format.width * self.sample_aspect_ratio.num, + self.format.height * self.sample_aspect_ratio.den, 1024*1024) + + return avrational_to_fraction(&dar) diff --git a/include/libavformat/avformat.pxd b/include/libavformat/avformat.pxd index 224e76b4e..9d9061cc2 100644 --- a/include/libavformat/avformat.pxd +++ b/include/libavformat/avformat.pxd @@ -325,6 +325,12 @@ cdef extern from "libavformat/avformat.h" nogil: AVFrame *frame ) + cdef AVRational av_guess_sample_aspect_ratio( + AVFormatContext *ctx, + AVStream *stream, + AVFrame *frame + ) + cdef const AVInputFormat* av_demuxer_iterate(void **opaque) cdef const AVOutputFormat* av_muxer_iterate(void **opaque) From 66d94a4ab86f0467cc94c4bb750e9211c67bb37b Mon Sep 17 00:00:00 2001 From: Moonsik Park Date: Tue, 18 Jun 2024 12:40:34 +0900 Subject: [PATCH 032/365] Expose `av_frame_make_writable()` for `av.Frame` --- av/frame.pyi | 2 ++ av/frame.pyx | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/av/frame.pyi b/av/frame.pyi index 8e81a8198..47ed9f0cc 100644 --- a/av/frame.pyi +++ b/av/frame.pyi @@ -7,3 +7,5 @@ class Frame: time_base: Fraction is_corrupt: bool side_data: dict[str, str] + + def make_writable(self) -> None: ... diff --git a/av/frame.pyx b/av/frame.pyx index 1c8fb357a..489eef540 100644 --- a/av/frame.pyx +++ b/av/frame.pyx @@ -1,3 +1,4 @@ +from av.error cimport err_check from av.utils cimport avrational_to_fraction, to_avrational from av.sidedata.sidedata import SideDataContainer @@ -137,3 +138,13 @@ cdef class Frame: if self._side_data is None: self._side_data = SideDataContainer(self) return self._side_data + + def make_writable(self): + """ + Ensures that the frame data is writable. Copy the data to new buffer if it is not. + This is a wrapper around :ffmpeg:`av_frame_make_writable`. + """ + cdef int ret + + ret = lib.av_frame_make_writable(self.ptr) + err_check(ret) From 080d675dad4cd41de91bdb11aadce6823a44f368 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 18 Jun 2024 15:32:33 -0400 Subject: [PATCH 033/365] Type stub all error exceptions --- av/_core.pyi | 2 ++ av/error.pyi | 48 +++++++++++++++++++++++++++++++++++++++++++++++- av/error.pyx | 2 -- 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/av/_core.pyi b/av/_core.pyi index 4fbfe8525..26b3bc0c8 100644 --- a/av/_core.pyi +++ b/av/_core.pyi @@ -7,3 +7,5 @@ class _Meta(TypedDict): library_meta: dict[str, _Meta] library_versions: dict[str, tuple[int, int, int]] + +time_base: int diff --git a/av/error.pyi b/av/error.pyi index db6581238..527d8c00d 100644 --- a/av/error.pyi +++ b/av/error.pyi @@ -1,3 +1,5 @@ +import builtins + from .enum import EnumItem classes: dict[int, Exception] @@ -58,4 +60,48 @@ class LookupError(FFmpegError): ... class HTTPError(FFmpegError): ... class HTTPClientError(FFmpegError): ... class UndefinedError(FFmpegError): ... -class InvalidDataError(ValueError): ... +class InvalidDataError(FFmpegError, builtins.ValueError): ... +class BugError(FFmpegError, builtins.RuntimeError): ... +class BufferTooSmallError(FFmpegError, builtins.ValueError): ... +class BSFNotFoundError(LookupError): ... +class DecoderNotFoundError(LookupError): ... +class DemuxerNotFoundError(LookupError): ... +class EncoderNotFoundError(LookupError): ... +class ExitError(FFmpegError): ... +class ExternalError(FFmpegError): ... +class FilterNotFoundError(LookupError): ... +class MuxerNotFoundError(LookupError): ... +class OptionNotFoundError(LookupError): ... +class PatchWelcomeError(FFmpegError): ... +class ProtocolNotFoundError(LookupError): ... +class UnknownError(FFmpegError): ... +class ExperimentalError(FFmpegError): ... +class InputChangedError(FFmpegError): ... +class OutputChangedError(FFmpegError): ... +class HTTPBadRequestError(HTTPClientError): ... +class HTTPUnauthorizedError(HTTPClientError): ... +class HTTPForbiddenError(HTTPClientError): ... +class HTTPNotFoundError(HTTPClientError): ... +class HTTPOtherClientError(HTTPClientError): ... +class HTTPServerError(HTTPError): ... +class PyAVCallbackError(FFmpegError, builtins.RuntimeError): ... +class BrokenPipeError(FFmpegError, builtins.BrokenPipeError): ... +class ChildProcessError(FFmpegError, builtins.ChildProcessError): ... +class ConnectionAbortedError(FFmpegError, builtins.ConnectionAbortedError): ... +class ConnectionRefusedError(FFmpegError, builtins.ConnectionRefusedError): ... +class ConnectionResetError(FFmpegError, builtins.ConnectionResetError): ... +class BlockingIOError(FFmpegError, builtins.BlockingIOError): ... +class EOFError(FFmpegError, builtins.EOFError): ... +class FileExistsError(FFmpegError, builtins.FileExistsError): ... +class FileNotFoundError(FFmpegError, builtins.FileNotFoundError): ... +class InterruptedError(FFmpegError, builtins.InterruptedError): ... +class IsADirectoryError(FFmpegError, builtins.IsADirectoryError): ... +class MemoryError(FFmpegError, builtins.MemoryError): ... +class NotADirectoryError(FFmpegError, builtins.NotADirectoryError): ... +class NotImplementedError(FFmpegError, builtins.NotImplementedError): ... +class OverflowError(FFmpegError, builtins.OverflowError): ... +class OSError(FFmpegError, builtins.OSError): ... +class PermissionError(FFmpegError, builtins.PermissionError): ... +class ProcessLookupError(FFmpegError, builtins.ProcessLookupError): ... +class TimeoutError(FFmpegError, builtins.TimeoutError): ... +class ValueError(FFmpegError, builtins.ValueError): ... diff --git a/av/error.pyx b/av/error.pyx index 64cd70594..97fab0e41 100644 --- a/av/error.pyx +++ b/av/error.pyx @@ -250,8 +250,6 @@ _extend_builtin("ValueError", (errno.EINVAL, )) _extend_builtin("MemoryError", (errno.ENOMEM, )) _extend_builtin("NotImplementedError", (errno.ENOSYS, )) _extend_builtin("OverflowError", (errno.ERANGE, )) -if IOError is not OSError: - _extend_builtin("IOError", (errno.EIO, )) # The rest of them (for now) _extend_builtin("OSError", [code for code in errno.errorcode if code not in classes]) From 84d758dcbfa3a19c11463eb6528bac9f6458dbdb Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 21 Jun 2024 02:56:19 -0400 Subject: [PATCH 034/365] Add fifo stub, remove `__init__.pyi` --- av/__init__.pyi | 18 ------------------ av/audio/fifo.pyi | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 18 deletions(-) delete mode 100644 av/__init__.pyi create mode 100644 av/audio/fifo.pyi diff --git a/av/__init__.pyi b/av/__init__.pyi deleted file mode 100644 index 493c62012..000000000 --- a/av/__init__.pyi +++ /dev/null @@ -1,18 +0,0 @@ -from av import error, logging - -from .audio.format import * -from .audio.frame import * -from .audio.layout import * -from .audio.resampler import * -from .codec.codec import * -from .container.core import * -from .container.input import * -from .container.output import * -from .error import * -from .filter import * -from .format import * -from .packet import * -from .video.format import * -from .video.frame import * - -__version__: str diff --git a/av/audio/fifo.pyi b/av/audio/fifo.pyi new file mode 100644 index 000000000..aece9d381 --- /dev/null +++ b/av/audio/fifo.pyi @@ -0,0 +1,16 @@ +from .format import AudioFormat +from .frame import AudioFrame +from .layout import AudioLayout + +class AudioFifo: + def write(self, frame: AudioFrame) -> None: ... + def read(self, samples: int = 0, partial: bool = False) -> AudioFrame | None: ... + def read_many(self, samples: int, partial: bool = False) -> list[AudioFrame]: ... + @property + def format(self) -> AudioFormat: ... + @property + def layout(self) -> AudioLayout: ... + @property + def sample_rate(self) -> int: ... + @property + def samples(self) -> int: ... From 2c8135535c57154c3227c0bba783bf7bdec86a06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Fri, 21 Jun 2024 10:33:29 +0200 Subject: [PATCH 035/365] Fix `AudioLayout`'s "copy" constructor (fixes: #1434) There are several places where `AudioLayout` instances can be created from another instance, for example the `AudioCodecContext.layout` setter, but this variant of the constructor is broken. --- av/audio/layout.pyx | 2 +- tests/test_audiolayout.py | 9 +++++++-- tests/test_codec_context.py | 11 +++++++---- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/av/audio/layout.pyx b/av/audio/layout.pyx index 79b4f3388..76edd177d 100644 --- a/av/audio/layout.pyx +++ b/av/audio/layout.pyx @@ -76,7 +76,7 @@ cdef class AudioLayout: elif isinstance(layout, str): c_layout = lib.av_get_channel_layout(layout) elif isinstance(layout, AudioLayout): - c_layout = layout.layout + c_layout = (layout).layout else: raise TypeError("layout must be str or int") diff --git a/tests/test_audiolayout.py b/tests/test_audiolayout.py index 9cecac19a..2a163186b 100644 --- a/tests/test_audiolayout.py +++ b/tests/test_audiolayout.py @@ -4,14 +4,19 @@ class TestAudioLayout(TestCase): - def test_stereo_properties(self): + def test_stereo_from_str(self): layout = AudioLayout("stereo") self._test_stereo(layout) - def test_2channel_properties(self) -> None: + def test_stereo_from_int(self): layout = AudioLayout(2) self._test_stereo(layout) + def test_stereo_from_layout(self): + layout = AudioLayout("stereo") + layout2 = AudioLayout(layout) + self._test_stereo(layout2) + def test_channel_counts(self): self.assertRaises(ValueError, AudioLayout, -1) self.assertRaises(ValueError, AudioLayout, 9) diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 1a2b189cc..d30401f4e 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -4,7 +4,7 @@ from unittest import SkipTest import av -from av import AudioResampler, Codec, Packet +from av import AudioLayout, AudioResampler, Codec, Packet from av.codec.codec import UnknownCodecError from av.video.frame import PictureType @@ -393,6 +393,12 @@ def test_encoding_mp2(self): maxDiff = None def audio_encoding(self, codec_name): + self._audio_encoding(codec_name=codec_name, channel_layout="stereo") + self._audio_encoding( + codec_name=codec_name, channel_layout=AudioLayout("stereo") + ) + + def _audio_encoding(self, *, codec_name, channel_layout): try: codec = Codec(codec_name, "w") except UnknownCodecError: @@ -404,14 +410,12 @@ def audio_encoding(self, codec_name): sample_fmt = ctx.codec.audio_formats[-1].name sample_rate = 48000 - channel_layout = "stereo" channels = 2 ctx.time_base = Fraction(1) / sample_rate ctx.sample_rate = sample_rate ctx.format = sample_fmt ctx.layout = channel_layout - ctx.channels = channels ctx.open() @@ -549,7 +553,6 @@ def audio_encoding(self, codec_name): ctx.sample_rate = sample_rate ctx.format = sample_fmt ctx.layout = channel_layout - ctx.channels = channels ctx.open() result_samples = 0 From a1400ad2b09f72ee2a5286da77a97a2667686335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Fri, 21 Jun 2024 09:05:52 +0200 Subject: [PATCH 036/365] Add type definitions for `AudioFormat` constructor, fix docstrings The docstrings contained examples referring to erroneous class names instead of `AudioFormat`. --- av/audio/format.pyi | 2 ++ av/audio/format.pyx | 11 +++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/av/audio/format.pyi b/av/audio/format.pyi index bb4a1e603..5f7e322ed 100644 --- a/av/audio/format.pyi +++ b/av/audio/format.pyi @@ -7,3 +7,5 @@ class AudioFormat: planar: AudioFormat packed: AudioFormat container_name: str + + def __init__(self, name: str | AudioFormat) -> None: ... diff --git a/av/audio/format.pyx b/av/audio/format.pyx index 608610781..f86e7b604 100644 --- a/av/audio/format.pyx +++ b/av/audio/format.pyx @@ -45,7 +45,7 @@ cdef class AudioFormat: def name(self): """Canonical name of the sample format. - >>> SampleFormat('s16p').name + >>> AudioFormat('s16p').name 's16p' """ @@ -55,7 +55,7 @@ cdef class AudioFormat: def bytes(self): """Number of bytes per sample. - >>> SampleFormat('s16p').bytes + >>> AudioFormat('s16p').bytes 2 """ @@ -65,7 +65,7 @@ cdef class AudioFormat: def bits(self): """Number of bits per sample. - >>> SampleFormat('s16p').bits + >>> AudioFormat('s16p').bits 16 """ @@ -95,8 +95,7 @@ cdef class AudioFormat: Is itself when planar: - >>> from av import AudioFormat as Format - >>> fmt = Format('s16p') + >>> fmt = AudioFormat('s16p') >>> fmt.planar is fmt True @@ -111,7 +110,7 @@ cdef class AudioFormat: Is itself when packed: - >>> fmt = Format('s16') + >>> fmt = AudioFormat('s16') >>> fmt.packed is fmt True From 74fe3fd8feb17e3f4e8147b575547d07fdead7b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Fri, 21 Jun 2024 09:45:24 +0200 Subject: [PATCH 037/365] Add type definitions for `AudioCodecContext` encoding / decoding The `encode`, `encode_lazy` and `decode` methods were missing type definitions. --- av/audio/codeccontext.pyi | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/av/audio/codeccontext.pyi b/av/audio/codeccontext.pyi index ed478a906..ada12efcb 100644 --- a/av/audio/codeccontext.pyi +++ b/av/audio/codeccontext.pyi @@ -1,8 +1,10 @@ -from typing import Literal +from typing import Iterator, Literal from av.codec.context import CodecContext +from av.packet import Packet from .format import AudioFormat +from .frame import AudioFrame from .layout import AudioLayout class AudioCodecContext(CodecContext): @@ -14,3 +16,7 @@ class AudioCodecContext(CodecContext): layout: AudioLayout format: AudioFormat type: Literal["audio"] + + def encode(self, frame: AudioFrame | None = None) -> list[Packet]: ... + def encode_lazy(self, frame: AudioFrame | None = None) -> Iterator[Packet]: ... + def decode(self, packet: Packet | None = None) -> list[AudioFrame]: ... From 5c5e31bfbe408b4040524e339530ae43c59cde79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Fri, 21 Jun 2024 10:52:15 +0200 Subject: [PATCH 038/365] Fix `Buffer` subclass type definitions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It does not make sense to special-case the `Packet` class concerning buffer protocol support, we have multiple classes inhering our `Buffer` class: - `Packet` - `Plane` and its subclasses `AudioPlane` and `VideoPlane` - `SideData` We cannot use `collection.abc.Buffer`, yet as this is only available in Python 3.12 and up. We do however want to declare that `bytes(foo)` is available, so declare a `__bytes__` method in `Buffer`. --- av/buffer.pyi | 5 +++++ av/packet.pyi | 3 +-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/av/buffer.pyi b/av/buffer.pyi index 41db73e03..bc1090d1d 100644 --- a/av/buffer.pyi +++ b/av/buffer.pyi @@ -1,4 +1,9 @@ +# When Python 3.12 becomes our lowest supported version, we could make this +# class inherit `collections.abc.Buffer`. + class Buffer: buffer_size: int buffer_ptr: int def update(self, input: bytes) -> None: ... + def __buffer__(self, flags: int) -> memoryview: ... + def __bytes__(self) -> bytes: ... diff --git a/av/packet.pyi b/av/packet.pyi index 24cb9f5a0..f122c2886 100644 --- a/av/packet.pyi +++ b/av/packet.pyi @@ -1,8 +1,8 @@ -from collections.abc import Buffer from fractions import Fraction from av.subtitles.subtitle import SubtitleSet +from .buffer import Buffer from .stream import Stream class Packet(Buffer): @@ -22,4 +22,3 @@ class Packet(Buffer): def __init__(self, input: int | bytes | None = None) -> None: ... def decode(self) -> list[SubtitleSet]: ... - def __buffer__(self, arg1) -> memoryview: ... From 6369e37a1aab4e9e6671b06e909ed4f14d4a42ba Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 21 Jun 2024 18:00:36 -0400 Subject: [PATCH 039/365] Support Python 3.8 Do not remove until EOL date has passed --- .github/workflows/smoke.yml | 10 +++++----- .github/workflows/tests.yml | 4 ++-- setup.py | 3 ++- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 1eda45934..39cf096cd 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -37,10 +37,10 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.1", extras: true} - - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.0"} + - {os: ubuntu-latest, python: "3.8", ffmpeg: "6.1", extras: true} + - {os: ubuntu-latest, python: "3.8", ffmpeg: "6.0"} - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1"} - - {os: macos-12, python: "3.9", ffmpeg: "6.1"} + - {os: macos-12, python: "3.8", ffmpeg: "6.1"} env: PYAV_PYTHON: python${{ matrix.config.python }} @@ -116,8 +116,8 @@ jobs: fail-fast: false matrix: config: - - {os: windows-latest, python: "3.9", ffmpeg: "6.1"} - - {os: windows-latest, python: "3.9", ffmpeg: "6.0"} + - {os: windows-latest, python: "3.8", ffmpeg: "6.1"} + - {os: windows-latest, python: "3.8", ffmpeg: "6.0"} steps: - name: Checkout diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 550065a84..b66ef500b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.9" + python-version: "3.8" - name: Build source package run: | pip install cython @@ -44,7 +44,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.9" + python-version: "3.8" - name: Set up QEMU if: matrix.os == 'ubuntu-latest' uses: docker/setup-qemu-action@v3 diff --git a/setup.py b/setup.py index 8cfcbaa58..cdedbcfb6 100644 --- a/setup.py +++ b/setup.py @@ -205,7 +205,7 @@ def parse_cflags(raw_flags): url="https://github.com/PyAV-Org/PyAV", packages=find_packages(exclude=["build*", "examples*", "scratchpad*", "tests*"]), package_data=package_data, - python_requires=">=3.9", + python_requires=">=3.8", zip_safe=False, ext_modules=ext_modules, test_suite="tests", @@ -222,6 +222,7 @@ def parse_cflags(raw_flags): "Operating System :: Unix", "Operating System :: Microsoft :: Windows", "Programming Language :: Cython", + "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", From 96a05270b474f515531567f70d731a48e0437e2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Sat, 22 Jun 2024 00:20:48 +0200 Subject: [PATCH 040/365] Add type definitions for `CodecContext` properties Add type definitions for `options` and `time_base`. --- av/codec/context.pyi | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/av/codec/context.pyi b/av/codec/context.pyi index 4f7f3d2dc..d84b41de1 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -1,7 +1,7 @@ +from fractions import Fraction from typing import Any, Literal from av.enum import EnumFlag, EnumItem -from av.frame import Frame from av.packet import Packet from .codec import Codec @@ -60,8 +60,10 @@ class CodecContext: is_encoder: bool is_decoder: bool name: str + options: dict[str, str] type: Literal["video", "audio", "data", "subtitle", "attachment"] profile: str | None + time_base: Fraction codec_tag: str bit_rate: int | None max_bit_rate: int | None From 777726112046d5a7b024c005f2fbf4b4e7109806 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 25 Jun 2024 14:18:14 -0400 Subject: [PATCH 041/365] Remove SubtitleSet.packet Always returned `None`. Appeared to be not implemented correctly. Undocumented in type stubs. --- av/subtitles/subtitle.pxd | 3 --- 1 file changed, 3 deletions(-) diff --git a/av/subtitles/subtitle.pxd b/av/subtitles/subtitle.pxd index 29ddc4220..508eb9034 100644 --- a/av/subtitles/subtitle.pxd +++ b/av/subtitles/subtitle.pxd @@ -1,14 +1,11 @@ cimport libav as lib -from av.packet cimport Packet - cdef class SubtitleProxy: cdef lib.AVSubtitle struct cdef class SubtitleSet: - cdef readonly Packet packet cdef SubtitleProxy proxy cdef readonly tuple rects From 69cab4956413985050538614947c29630b089261 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 25 Jun 2024 15:36:06 -0400 Subject: [PATCH 042/365] Make Subtitle an abstract base class --- av/codec/context.pyx | 2 +- av/subtitles/stream.pyx | 3 +++ av/subtitles/subtitle.pyi | 3 +-- av/subtitles/subtitle.pyx | 11 ++++++++--- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/av/codec/context.pyx b/av/codec/context.pyx index d94a08015..f8c1263c6 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -638,7 +638,7 @@ cdef class CodecContext: def skip_frame(self): """One of :class:`.SkipType`. - Wraps ffmpeg:`AVCodecContext.skip_frame`. + Wraps :ffmpeg:`AVCodecContext.skip_frame`. """ return SkipType._get(self.ptr.skip_frame, create=True) diff --git a/av/subtitles/stream.pyx b/av/subtitles/stream.pyx index 6cf0c2d88..1deed5867 100644 --- a/av/subtitles/stream.pyx +++ b/av/subtitles/stream.pyx @@ -1,3 +1,6 @@ cdef class SubtitleStream(Stream): + """ + A :class:`SubtitleStream` can contain many :class:`SubtitleSet` objects accessible via decoding. + """ def __getattr__(self, name): return getattr(self.codec_context, name) diff --git a/av/subtitles/subtitle.pyi b/av/subtitles/subtitle.pyi index cdbe7fb31..2ac9195ec 100644 --- a/av/subtitles/subtitle.pyi +++ b/av/subtitles/subtitle.pyi @@ -11,8 +11,7 @@ class SubtitleSet: def __iter__(self) -> Iterator[Subtitle]: ... def __getitem__(self, i: int) -> Subtitle: ... -class Subtitle: - type: Literal[b"none", b"bitmap", b"text", b"ass"] +class Subtitle: ... class BitmapSubtitle(Subtitle): type: Literal[b"bitmap"] diff --git a/av/subtitles/subtitle.pyx b/av/subtitles/subtitle.pyx index e1deca7b7..7ced655f6 100644 --- a/av/subtitles/subtitle.pyx +++ b/av/subtitles/subtitle.pyx @@ -11,6 +11,9 @@ cdef class SubtitleProxy: cdef class SubtitleSet: + """ + A :class:`SubtitleSet` can contain many :class:`Subtitle` objects. + """ def __cinit__(self, SubtitleProxy proxy): self.proxy = proxy cdef int i @@ -50,9 +53,7 @@ cdef Subtitle build_subtitle(SubtitleSet subtitle, int index): raise ValueError("subtitle rect index out of range") cdef lib.AVSubtitleRect *ptr = subtitle.proxy.struct.rects[index] - if ptr.type == lib.SUBTITLE_NONE: - return Subtitle(subtitle, index) - elif ptr.type == lib.SUBTITLE_BITMAP: + if ptr.type == lib.SUBTITLE_BITMAP: return BitmapSubtitle(subtitle, index) elif ptr.type == lib.SUBTITLE_TEXT: return TextSubtitle(subtitle, index) @@ -63,6 +64,10 @@ cdef Subtitle build_subtitle(SubtitleSet subtitle, int index): cdef class Subtitle: + """ + An abstract base class for each concrete type of subtitle. + Wraps :ffmpeg:`AVSubtitleRect` + """ def __cinit__(self, SubtitleSet subtitle, int index): if index < 0 or index >= subtitle.proxy.struct.num_rects: raise ValueError("subtitle rect index out of range") From 268f9daafad86f584158fbadcfdde1324c48f321 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 25 Jun 2024 17:56:56 -0400 Subject: [PATCH 043/365] Add Graph.vpush() and Graph.vpull() --- av/container/input.pyi | 6 ++++++ av/filter/graph.pxd | 1 - av/filter/graph.pyi | 2 ++ av/filter/graph.pyx | 18 +++++++++++++++--- tests/test_filters.py | 10 +++++----- 5 files changed, 28 insertions(+), 9 deletions(-) diff --git a/av/container/input.pyi b/av/container/input.pyi index f85eaec46..90154c331 100644 --- a/av/container/input.pyi +++ b/av/container/input.pyi @@ -21,6 +21,12 @@ class InputContainer(Container): def close(self) -> None: ... def demux(self, *args: Any, **kwargs: Any) -> Iterator[Packet]: ... @overload + def decode(self, video: int) -> Iterator[VideoFrame]: ... + @overload + def decode(self, audio: int) -> Iterator[AudioFrame]: ... + @overload + def decode(self, subtitles: int) -> Iterator[SubtitleSet]: ... + @overload def decode(self, *args: VideoStream) -> Iterator[VideoFrame]: ... @overload def decode(self, *args: AudioStream) -> Iterator[AudioFrame]: ... diff --git a/av/filter/graph.pxd b/av/filter/graph.pxd index c9226749c..b3bf352a3 100644 --- a/av/filter/graph.pxd +++ b/av/filter/graph.pxd @@ -4,7 +4,6 @@ from av.filter.context cimport FilterContext cdef class Graph: - cdef lib.AVFilterGraph *ptr cdef readonly bint configured diff --git a/av/filter/graph.pyi b/av/filter/graph.pyi index 75930d08a..625364f35 100644 --- a/av/filter/graph.pyi +++ b/av/filter/graph.pyi @@ -41,3 +41,5 @@ class Graph: ) -> FilterContext: ... def push(self, frame: None | AudioFrame | VideoFrame) -> None: ... def pull(self) -> VideoFrame | AudioFrame: ... + def vpush(self, frame: VideoFrame | None) -> None: ... + def vpull(self) -> VideoFrame: ... diff --git a/av/filter/graph.pyx b/av/filter/graph.pyx index f6376b3a3..53689c432 100644 --- a/av/filter/graph.pyx +++ b/av/filter/graph.pyx @@ -174,11 +174,15 @@ cdef class Graph: else: raise ValueError(f"can only AudioFrame, VideoFrame or None; got {type(frame)}") - if len(contexts) != 1: - raise ValueError(f"can only auto-push with single buffer; found {len(contexts)}") + for ctx in contexts: + ctx.push(frame) - contexts[0].push(frame) + def vpush(self, VideoFrame frame): + for ctx in self._context_by_type.get("buffer", []): + ctx.push(frame) + + # TODO: Test complex filter graphs, add `at: int = 0` arg to pull() and vpull(). def pull(self): vsinks = self._context_by_type.get("buffersink", []) asinks = self._context_by_type.get("abuffersink", []) @@ -188,3 +192,11 @@ cdef class Graph: raise ValueError(f"can only auto-pull with single sink; found {nsinks}") return (vsinks or asinks)[0].pull() + + def vpull(self): + vsinks = self._context_by_type.get("buffersink", []) + nsinks = len(vsinks) + if nsinks != 1: + raise ValueError(f"can only auto-pull with single sink; found {nsinks}") + + return vsinks[0].pull() diff --git a/tests/test_filters.py b/tests/test_filters.py index 326ef7fb9..3d9e6e9d0 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -204,7 +204,7 @@ def test_video_buffer(self): for frame in input_container.decode(): self.assertEqual(frame.time_base, Fraction(1, 30)) - graph.push(frame) + graph.vpush(frame) filtered_frames = pull_until_blocked(graph) if frame.pts == 0: @@ -220,7 +220,7 @@ def test_video_buffer(self): self.assertEqual(filtered_frames[1].pts, (frame.pts - 1) * 2 + 1) self.assertEqual(filtered_frames[1].time_base, Fraction(1, 60)) - def test_EOF(self): + def test_EOF(self) -> None: input_container = av.open(format="lavfi", file="color=c=pink:duration=1:r=30") video_stream = input_container.streams.video[0] @@ -233,12 +233,12 @@ def test_EOF(self): graph.configure() for frame in input_container.decode(video=0): - graph.push(frame) + graph.vpush(frame) - graph.push(None) + graph.vpush(None) # if we do not push None, we get a BlockingIOError - palette_frame = graph.pull() + palette_frame = graph.vpull() self.assertIsInstance(palette_frame, av.VideoFrame) self.assertEqual(palette_frame.width, 16) From ff65d9ece969fafc596db0498bec706b20caebee Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 25 Jun 2024 19:35:58 -0400 Subject: [PATCH 044/365] Set version to 12.2.0 --- av/about.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/about.py b/av/about.py index 79a759ae1..239627e93 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "13.0.0rc1" +__version__ = "12.2.0rc1" From c8906362b4a64fa59a5a31d170970d9ad9a81a6d Mon Sep 17 00:00:00 2001 From: Moonsik Park Date: Wed, 26 Jun 2024 11:45:11 +0900 Subject: [PATCH 045/365] Remove circular reference between `Graph` and `FilterContext` --- av/filter/context.pxd | 2 +- av/filter/context.pyi | 3 +++ av/filter/context.pyx | 11 ++++++++++- av/filter/graph.pxd | 2 ++ 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/av/filter/context.pxd b/av/filter/context.pxd index 18954fbdd..ae9f27c99 100644 --- a/av/filter/context.pxd +++ b/av/filter/context.pxd @@ -7,7 +7,7 @@ from av.filter.graph cimport Graph cdef class FilterContext: cdef lib.AVFilterContext *ptr - cdef readonly Graph graph + cdef readonly object _graph cdef readonly Filter filter cdef object _inputs diff --git a/av/filter/context.pyi b/av/filter/context.pyi index 0d70c0095..7c00087a9 100644 --- a/av/filter/context.pyi +++ b/av/filter/context.pyi @@ -1,3 +1,4 @@ +from av.filter import Graph from av.frame import Frame from .pad import FilterContextPad @@ -11,5 +12,7 @@ class FilterContext: def link_to( self, input_: FilterContext, output_idx: int = 0, input_idx: int = 0 ) -> None: ... + @property + def graph(self) -> Graph: ... def push(self, frame: Frame) -> None: ... def pull(self) -> Frame: ... diff --git a/av/filter/context.pyx b/av/filter/context.pyx index 54ed710ab..b820d3d18 100644 --- a/av/filter/context.pyx +++ b/av/filter/context.pyx @@ -1,3 +1,5 @@ +import weakref + from av.audio.frame cimport alloc_audio_frame from av.dictionary cimport _Dictionary from av.dictionary import Dictionary @@ -13,7 +15,7 @@ cdef object _cinit_sentinel = object() cdef FilterContext wrap_filter_context(Graph graph, Filter filter, lib.AVFilterContext *ptr): cdef FilterContext self = FilterContext(_cinit_sentinel) - self.graph = graph + self._graph = weakref.ref(graph) self.filter = filter self.ptr = ptr return self @@ -72,6 +74,13 @@ cdef class FilterContext: def link_to(self, FilterContext input_, int output_idx=0, int input_idx=0): err_check(lib.avfilter_link(self.ptr, output_idx, input_.ptr, input_idx)) + + @property + def graph(self): + if (graph := self._graph()): + return graph + else: + raise RuntimeError("graph is unallocated") def push(self, Frame frame): cdef int res diff --git a/av/filter/graph.pxd b/av/filter/graph.pxd index b3bf352a3..2e52bd6ec 100644 --- a/av/filter/graph.pxd +++ b/av/filter/graph.pxd @@ -4,6 +4,8 @@ from av.filter.context cimport FilterContext cdef class Graph: + cdef object __weakref__ + cdef lib.AVFilterGraph *ptr cdef readonly bint configured From 29fb3fd06b6ce216b10f3d4e20d04cb5b9b33611 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 27 Jun 2024 12:27:07 -0400 Subject: [PATCH 046/365] Add docstrings for vpush and vpull --- av/filter/graph.pyx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/av/filter/graph.pyx b/av/filter/graph.pyx index 53689c432..e2d95e4cd 100644 --- a/av/filter/graph.pyx +++ b/av/filter/graph.pyx @@ -178,6 +178,7 @@ cdef class Graph: ctx.push(frame) def vpush(self, VideoFrame frame): + """Like `push`, but only for VideoFrames.""" for ctx in self._context_by_type.get("buffer", []): ctx.push(frame) @@ -194,6 +195,7 @@ cdef class Graph: return (vsinks or asinks)[0].pull() def vpull(self): + """Like `pull`, but only for VideoFrames.""" vsinks = self._context_by_type.get("buffersink", []) nsinks = len(vsinks) if nsinks != 1: From fb6c064e80fe63f414a621eb8f2b6a44a22c788b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Fri, 28 Jun 2024 08:41:55 +0200 Subject: [PATCH 047/365] Release v12.2.0 --- CHANGELOG.rst | 13 +++++++++++++ av/about.py | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 526ce98a6..3f29ef131 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,19 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. +v12.2.0 +------- + +Features: + +- Add a `make_writable` method to `Frame` instances (:issue:`1414`). +- Use `av_guess_sample_aspect_ratio` to report sample and display aspect ratios. + +Fixes: + +- Fix a crash when assigning an `AudioLayout` to `AudioCodecContext.layout` (:issue:`1434`). +- Remove a circular reference which caused `AudioSampler` to occupy memory until garbage collected (:issue:`1429`). +- Fix more type stubs, remove incorrect `__init__.pyi`. v12.1.0 ------- diff --git a/av/about.py b/av/about.py index 239627e93..59c3246f6 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "12.2.0rc1" +__version__ = "12.2.0" From cd3f4c263ac3d84c7633954ad6d3228be1e0b421 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 1 Jul 2024 04:11:53 -0400 Subject: [PATCH 048/365] Resampler: format, layout accepts `str` `int` too --- av/audio/resampler.pyi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/av/audio/resampler.pyi b/av/audio/resampler.pyi index 32861258e..cbf2134aa 100644 --- a/av/audio/resampler.pyi +++ b/av/audio/resampler.pyi @@ -12,8 +12,8 @@ class AudioResampler: def __init__( self, - format: AudioFormat | None = None, - layout: AudioLayout | None = None, + format: str | int | AudioFormat | None = None, + layout: str | int | AudioLayout | None = None, rate: int | None = None, frame_size: int | None = None, ) -> None: ... From b98a12edb2918f22c8e747f0714edf48e19b9f59 Mon Sep 17 00:00:00 2001 From: Matteo Destro Date: Tue, 2 Jul 2024 07:57:35 +0200 Subject: [PATCH 049/365] Support libav's `av_log_set_level` --- av/logging.pyi | 1 + av/logging.pyx | 11 +++++++++++ include/libavutil/avutil.pxd | 1 + 3 files changed, 13 insertions(+) diff --git a/av/logging.pyi b/av/logging.pyi index 1db5e4a3b..8c32de77d 100644 --- a/av/logging.pyi +++ b/av/logging.pyi @@ -13,6 +13,7 @@ CRITICAL: int def adapt_level(level: int) -> int: ... def get_level() -> int | None: ... def set_level(level: int | None) -> None: ... +def set_libav_level(level: int) -> None: ... def restore_default_callback() -> None: ... def get_skip_repeated() -> bool: ... def set_skip_repeated(v: bool) -> None: ... diff --git a/av/logging.pyx b/av/logging.pyx index 1006d094c..6b6858db6 100644 --- a/av/logging.pyx +++ b/av/logging.pyx @@ -119,6 +119,17 @@ def set_level(level): raise ValueError("level must be: int | None") +def set_libav_level(level): + """Set libav's log level. It can be set to constants available in this + module: ``PANIC``, ``FATAL``, ``ERROR``, ``WARNING``, ``INFO``, + ``VERBOSE``, ``DEBUG``. + + When PyAV logging is disabled, setting this will change the level of + the logs printed to the terminal. + """ + lib.av_log_set_level(level) + + def restore_default_callback(): """Revert back to FFmpeg's log callback, which prints to the terminal.""" lib.av_log_set_callback(lib.av_log_default_callback) diff --git a/include/libavutil/avutil.pxd b/include/libavutil/avutil.pxd index f9af7a7b0..b4184d0de 100644 --- a/include/libavutil/avutil.pxd +++ b/include/libavutil/avutil.pxd @@ -399,3 +399,4 @@ cdef extern from "libavutil/log.h" nogil: ctypedef void(*av_log_callback)(void *, int, const char *, va_list) void av_log_default_callback(void *, int, const char *, va_list) void av_log_set_callback (av_log_callback callback) + void av_log_set_level(int level) From 42f3e1e6ab0af53c9bee5f9000fa10ad730b2d94 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 9 Jul 2024 16:31:05 -0400 Subject: [PATCH 050/365] Add Graph.link_nodes --- av/filter/graph.pyi | 1 + av/filter/graph.pyx | 9 ++++++++- tests/test_filters.py | 15 ++++----------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/av/filter/graph.pyi b/av/filter/graph.pyi index 625364f35..337be2bee 100644 --- a/av/filter/graph.pyi +++ b/av/filter/graph.pyi @@ -17,6 +17,7 @@ class Graph: def __init__(self) -> None: ... def configure(self, auto_buffer: bool = True, force: bool = False) -> None: ... + def link_nodes(self, *nodes: FilterContext) -> Graph: ... def add( self, filter: str | Filter, args: Any = None, **kwargs: str ) -> FilterContext: ... diff --git a/av/filter/graph.pyx b/av/filter/graph.pyx index e2d95e4cd..e0effd12f 100644 --- a/av/filter/graph.pyx +++ b/av/filter/graph.pyx @@ -45,6 +45,13 @@ cdef class Graph: # We get auto-inserted stuff here. self._auto_register() + def link_nodes(self, *nodes): + """ + Links nodes together for simple filter graphs. + """ + for c, n in zip(nodes, nodes[1:]): + c.link_to(n) + return self def add(self, filter, args=None, **kwargs): cdef Filter cy_filter @@ -68,7 +75,7 @@ cdef class Graph: # There might have been automatic contexts added (e.g. resamplers, # fifos, and scalers). It is more likely to see them after the graph - # is configured, but we wan't to be safe. + # is configured, but we want to be safe. self._auto_register() return ctx diff --git a/tests/test_filters.py b/tests/test_filters.py index 3d9e6e9d0..1f3b8ae99 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -128,14 +128,9 @@ def test_audio_buffer_sink(self): if e.errno != errno.EAGAIN: raise - @staticmethod - def link_nodes(*nodes): - for c, n in zip(nodes, nodes[1:]): - c.link_to(n) - def test_audio_buffer_resample(self): graph = Graph() - self.link_nodes( + graph.link_nodes( graph.add_abuffer( format="fltp", sample_rate=48000, @@ -146,8 +141,7 @@ def test_audio_buffer_resample(self): "aformat", "sample_fmts=s16:sample_rates=44100:channel_layouts=stereo" ), graph.add("abuffersink"), - ) - graph.configure() + ).configure() graph.push( generate_audio_frame( @@ -161,7 +155,7 @@ def test_audio_buffer_resample(self): def test_audio_buffer_volume_filter(self): graph = Graph() - self.link_nodes( + graph.link_nodes( graph.add_abuffer( format="fltp", sample_rate=48000, @@ -170,8 +164,7 @@ def test_audio_buffer_volume_filter(self): ), graph.add("volume", volume="0.5"), graph.add("abuffersink"), - ) - graph.configure() + ).configure() input_frame = generate_audio_frame( 0, input_format="fltp", layout="stereo", sample_rate=48000 From a493150c98b1241ec828524059fd124bf7cfb33f Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 12 Jul 2024 02:41:22 -0400 Subject: [PATCH 051/365] Add default codec properties --- av/container/output.pyi | 6 ++++++ av/container/output.pyx | 23 +++++++++++++++++++++++ av/format.pyi | 4 ++++ tests/test_containerformat.py | 26 +++++++++++++++++++++----- 4 files changed, 54 insertions(+), 5 deletions(-) diff --git a/av/container/output.pyi b/av/container/output.pyi index 6df32c897..882592667 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -19,3 +19,9 @@ class OutputContainer(Container): def close(self) -> None: ... def mux(self, packets: Packet | Sequence[Packet]) -> None: ... def mux_one(self, packet: Packet) -> None: ... + @property + def default_video_codec(self) -> str: ... + @property + def default_audio_codec(self) -> str: ... + @property + def default_subtitle_codec(self) -> str: ... diff --git a/av/container/output.pyx b/av/container/output.pyx index 55e8b5006..184a30b2d 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -1,6 +1,8 @@ import logging import os +cimport libav as lib + from av.codec.codec cimport Codec from av.codec.context cimport CodecContext, wrap_codec_context from av.container.streams cimport StreamContainer @@ -192,6 +194,27 @@ cdef class OutputContainer(Container): self._started = True + @property + def default_video_codec(self): + """ + Returns the default video codec this container recommends. + """ + return lib.avcodec_get_name(self.format.optr.video_codec) + + @property + def default_audio_codec(self): + """ + Returns the default audio codec this container recommends. + """ + return lib.avcodec_get_name(self.format.optr.audio_codec) + + @property + def default_subtitle_codec(self): + """ + Returns the default subtitle codec this container recommends. + """ + return lib.avcodec_get_name(self.format.optr.subtitle_codec) + def close(self): for stream in self.streams: if stream.codec_context: diff --git a/av/format.pyi b/av/format.pyi index d2aef4764..c3506ed51 100644 --- a/av/format.pyi +++ b/av/format.pyi @@ -1,5 +1,7 @@ __all__ = ("ContainerFormat", "formats_available") +from typing import Literal + from .enum import EnumFlag class Flags(EnumFlag): @@ -22,10 +24,12 @@ class Flags(EnumFlag): SEEK_TO_PTS: int class ContainerFormat: + def __init__(self, name: str, mode: Literal["r", "w"] | None = None) -> None: ... name: str long_name: str is_input: bool is_output: bool + extensions: set[str] # flags no_file: int diff --git a/tests/test_containerformat.py b/tests/test_containerformat.py index dea3d29dc..f0460b131 100644 --- a/tests/test_containerformat.py +++ b/tests/test_containerformat.py @@ -1,10 +1,15 @@ -from av import ContainerFormat, formats_available +from av import ContainerFormat, formats_available, open from .common import TestCase class TestContainerFormats(TestCase): - def test_matroska(self): + def test_matroska(self) -> None: + with open("test.mkv", "w") as container: + self.assertNotEqual(container.default_video_codec, "none") + self.assertNotEqual(container.default_audio_codec, "none") + self.assertEqual(container.default_subtitle_codec, "ass") + fmt = ContainerFormat("matroska") self.assertTrue(fmt.is_input) self.assertTrue(fmt.is_output) @@ -13,7 +18,12 @@ def test_matroska(self): self.assertIn("mkv", fmt.extensions) self.assertFalse(fmt.no_file) - def test_mov(self): + def test_mov(self) -> None: + with open("test.mov", "w") as container: + self.assertNotEqual(container.default_video_codec, "none") + self.assertNotEqual(container.default_audio_codec, "none") + self.assertEqual(container.default_subtitle_codec, "none") + fmt = ContainerFormat("mov") self.assertTrue(fmt.is_input) self.assertTrue(fmt.is_output) @@ -22,7 +32,13 @@ def test_mov(self): self.assertIn("mov", fmt.extensions) self.assertFalse(fmt.no_file) - def test_stream_segment(self): + def test_gif(self) -> None: + with open("test.gif", "w") as container: + self.assertEqual(container.default_video_codec, "gif") + self.assertEqual(container.default_audio_codec, "none") + self.assertEqual(container.default_subtitle_codec, "none") + + def test_stream_segment(self) -> None: # This format goes by two names, check both. fmt = ContainerFormat("stream_segment") self.assertFalse(fmt.is_input) @@ -40,5 +56,5 @@ def test_stream_segment(self): self.assertEqual(fmt.extensions, set()) self.assertTrue(fmt.no_file) - def test_formats_available(self): + def test_formats_available(self) -> None: self.assertTrue(formats_available) From 996715a0bb33a7725ee131d28ea73cdabc5dc566 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 12 Jul 2024 03:24:48 -0400 Subject: [PATCH 052/365] Bump to next dev version --- av/about.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/about.py b/av/about.py index 59c3246f6..b0de45783 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "12.2.0" +__version__ = "12.3.0rc1" From 5e758b4539455f6482ab9d87535b752546add917 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 13 Jul 2024 16:01:21 -0400 Subject: [PATCH 053/365] Use ffmpeg 6.1.1-4 --- .github/workflows/tests.yml | 3 +++ scripts/ffmpeg-6.1.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b66ef500b..2e4c221d9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -53,6 +53,9 @@ jobs: run: | brew update brew install pkg-config + - name: Set deployment target + if: matrix.os == 'macos-13' || matrix.os == 'macos-14' + run: echo "MACOSX_DEPLOYMENT_TARGET=10.13" >> $GITHUB_ENV - name: Build wheels env: CIBW_ARCHS: ${{ matrix.arch }} diff --git a/scripts/ffmpeg-6.1.json b/scripts/ffmpeg-6.1.json index 49ce58d17..d2b41f4c1 100644 --- a/scripts/ffmpeg-6.1.json +++ b/scripts/ffmpeg-6.1.json @@ -1,3 +1,3 @@ { - "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/6.1.1-3/ffmpeg-{platform}.tar.gz"] + "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/6.1.1-4/ffmpeg-{platform}.tar.gz"] } From 6488603a5edece1fb96b3643c465dcc8fe177c6a Mon Sep 17 00:00:00 2001 From: Matteo Destro Date: Sun, 14 Jul 2024 09:54:15 +0200 Subject: [PATCH 054/365] Include libav headers in final distribution --- av/__init__.py | 12 ++++++++++++ setup.py | 13 ++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/av/__init__.py b/av/__init__.py index b4705dc69..68b9fed09 100644 --- a/av/__init__.py +++ b/av/__init__.py @@ -41,3 +41,15 @@ # Backwards compatibility AVError = FFmpegError # noqa: F405 + + +def get_include() -> str: + """ + Returns the path to the `include` folder to be used when building extensions to av. + """ + # Installed package + include_path = os.path.join(os.path.dirname(__file__), "include") + if os.path.exists(include_path): + return include_path + # Running from source directory + return os.path.join(os.path.dirname(__file__), os.pardir, "include") diff --git a/setup.py b/setup.py index cdedbcfb6..2a88c8967 100644 --- a/setup.py +++ b/setup.py @@ -184,6 +184,13 @@ def parse_cflags(raw_flags): ".".join(pckg.parts): ["*.pxd", "*.pyi", "*.typed"] for pckg in package_folders } +# Add include/ headers to av.include +package_dir = { + ".".join(["av", *pckg.parts]): str(pckg) + for pckg in pathlib.Path("include").glob("**/") +} +package_data.update({pckg: ["*.pxd"] for pckg in package_dir}) + with open("README.md") as f: long_description = f.read() @@ -203,7 +210,11 @@ def parse_cflags(raw_flags): author="Mike Boers", author_email="pyav@mikeboers.com", url="https://github.com/PyAV-Org/PyAV", - packages=find_packages(exclude=["build*", "examples*", "scratchpad*", "tests*"]), + packages=find_packages( + exclude=["build*", "examples*", "scratchpad*", "tests*", "include*"] + ) + + list(package_dir.keys()), + package_dir=package_dir, package_data=package_data, python_requires=">=3.8", zip_safe=False, From 5730ef0430cdc9803df04d042f76f13467fee2d7 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 15 Jul 2024 14:09:51 -0400 Subject: [PATCH 055/365] Remove TextSubtitle, add dialogue property AssSubtitle is used even for non ASS/SSA formats. These changes make that more obvious to users. FFmpeg does not export ass_split.h, so the dialogue getter is implemented directly in Cython. --- av/subtitles/subtitle.pyi | 13 ++++---- av/subtitles/subtitle.pyx | 69 +++++++++++++++++++++++++++++---------- tests/test_subtitles.py | 6 +++- 3 files changed, 64 insertions(+), 24 deletions(-) diff --git a/av/subtitles/subtitle.pyi b/av/subtitles/subtitle.pyi index 2ac9195ec..2a35d0a55 100644 --- a/av/subtitles/subtitle.pyi +++ b/av/subtitles/subtitle.pyi @@ -27,10 +27,11 @@ class BitmapSubtitlePlane: index: int buffer_size: int -class TextSubtitle(Subtitle): - type: Literal[b"text"] - text: bytes - class AssSubtitle(Subtitle): - type: Literal[b"ass"] - ass: bytes + type: Literal[b"ass", b"text"] + @property + def ass(self) -> bytes: ... + @property + def dialogue(self) -> bytes: ... + @property + def text(self) -> bytes: ... diff --git a/av/subtitles/subtitle.pyx b/av/subtitles/subtitle.pyx index 7ced655f6..373bb529b 100644 --- a/av/subtitles/subtitle.pyx +++ b/av/subtitles/subtitle.pyx @@ -55,9 +55,7 @@ cdef Subtitle build_subtitle(SubtitleSet subtitle, int index): if ptr.type == lib.SUBTITLE_BITMAP: return BitmapSubtitle(subtitle, index) - elif ptr.type == lib.SUBTITLE_TEXT: - return TextSubtitle(subtitle, index) - elif ptr.type == lib.SUBTITLE_ASS: + elif ptr.type == lib.SUBTITLE_ASS or ptr.type == lib.SUBTITLE_TEXT: return AssSubtitle(subtitle, index) else: raise ValueError("unknown subtitle type %r" % ptr.type) @@ -141,7 +139,10 @@ cdef class BitmapSubtitlePlane: PyBuffer_FillInfo(view, self, self._buffer, self.buffer_size, 0, flags) -cdef class TextSubtitle(Subtitle): +cdef class AssSubtitle(Subtitle): + """ + Represents an ASS/Text subtitle format, as opposed to a bitmap Subtitle format. + """ def __repr__(self): return ( f"<{self.__class__.__module__}.{self.__class__.__name__} " @@ -149,21 +150,55 @@ cdef class TextSubtitle(Subtitle): ) @property - def text(self): - if self.ptr.text is not NULL: - return PyBytes_FromString(self.ptr.text) + def ass(self): + """ + Returns the subtitle in the ASS/SSA format. Used by the vast majority of subtitle formats. + """ + if self.ptr.ass is not NULL: + return PyBytes_FromString(self.ptr.ass) return b"" - -cdef class AssSubtitle(Subtitle): - def __repr__(self): - return ( - f"<{self.__class__.__module__}.{self.__class__.__name__} " - f"{self.ass!r} at 0x{id(self):x}>" - ) + @property + def dialogue(self): + """ + Extract the dialogue from the ass format. Strip comments. + """ + comma_count = 0 + i = 0 + cdef bytes ass_text = self.ass + cdef bytes result = b"" + + while comma_count < 8 and i < len(ass_text): + if bytes([ass_text[i]]) == b",": + comma_count += 1 + i += 1 + + state = False + while i < len(ass_text): + char = bytes([ass_text[i]]) + next_char = b"" if i + 1 >= len(ass_text) else bytes([ass_text[i + 1]]) + + if char == b"\\" and next_char == b"N": + result += b"\n" + i += 2 + continue + + if not state: + if char == b"{" and next_char != b"\\": + state = True + else: + result += char + elif char == b"}": + state = False + i += 1 + + return result @property - def ass(self): - if self.ptr.ass is not NULL: - return PyBytes_FromString(self.ptr.ass) + def text(self): + """ + Rarely used attribute. You're probably looking for dialogue. + """ + if self.ptr.text is not NULL: + return PyBytes_FromString(self.ptr.text) return b"" diff --git a/tests/test_subtitles.py b/tests/test_subtitles.py index fe4f2ee3d..cc1409f2b 100644 --- a/tests/test_subtitles.py +++ b/tests/test_subtitles.py @@ -5,7 +5,7 @@ class TestSubtitle(TestCase): - def test_movtext(self): + def test_movtext(self) -> None: path = fate_suite("sub/MovText_capability_tester.mp4") subs = [] @@ -23,8 +23,12 @@ def test_movtext(self): sub = subset[0] self.assertIsInstance(sub, AssSubtitle) + assert isinstance(sub, AssSubtitle) + self.assertEqual(sub.type, b"ass") + self.assertEqual(sub.text, b"") self.assertEqual(sub.ass, b"0,0,Default,,0,0,0,,- Test 1.\\N- Test 2.") + self.assertEqual(sub.dialogue, b"- Test 1.\n- Test 2.") def test_vobsub(self): path = fate_suite("sub/vobsub.sub") From 4bd4984daaf1c81731185e3bcada1e017ca1720c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 16 Jul 2024 02:01:21 -0400 Subject: [PATCH 056/365] Remove TextSubtitle in docs --- av/container/core.pyx | 2 +- docs/api/subtitles.rst | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/av/container/core.pyx b/av/container/core.pyx index 548fe4fc2..df1b25939 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -365,7 +365,7 @@ def open( :param int buffer_size: Size of buffer for Python input/output operations in bytes. Honored only when ``file`` is a file-like object. Defaults to 32768 (32k). :param timeout: How many seconds to wait for data before giving up, as a float, or a - :ref:`(open timeout, read timeout) ` tuple. + :ref:`(open timeout, read timeout)` tuple. :param callable io_open: Custom I/O callable for opening files/streams. This option is intended for formats that need to open additional file-like objects to ``file`` using custom I/O. diff --git a/docs/api/subtitles.rst b/docs/api/subtitles.rst index 19d75621c..949896d6d 100644 --- a/docs/api/subtitles.rst +++ b/docs/api/subtitles.rst @@ -15,14 +15,11 @@ Subtitles .. autoclass:: Subtitle :members: - .. autoclass:: BitmapSubtitle - :members: - - .. autoclass:: BitmapSubtitlePlane + .. autoclass:: AssSubtitle :members: - .. autoclass:: TextSubtitle + .. autoclass:: BitmapSubtitle :members: - .. autoclass:: AssSubtitle + .. autoclass:: BitmapSubtitlePlane :members: From 4edb00f56bb540a43120f278b0646cd9c34a56ec Mon Sep 17 00:00:00 2001 From: elxy Date: Wed, 17 Jul 2024 10:40:32 +0800 Subject: [PATCH 057/365] Fix values of dstRange/srcRange when calling sws_setColorspaceDetails() In sws_setColorspaceDetails(), dstRange/srcRange is 1 for JPEG range and 0 for MPEG range. --- av/video/reformatter.pyx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/av/video/reformatter.pyx b/av/video/reformatter.pyx index 11ee1bf74..624454f0c 100644 --- a/av/video/reformatter.pyx +++ b/av/video/reformatter.pyx @@ -76,9 +76,9 @@ cdef class VideoReformatter: :type dst_colorspace: :class:`Colorspace` or ``str`` :param interpolation: The interpolation method to use, or ``None`` for ``BILINEAR``. :type interpolation: :class:`Interpolation` or ``str`` - :param src_color_range: Current color range, or ``None`` for the frame color range. + :param src_color_range: Current color range, or ``None`` for the ``UNSPECIFIED``. :type src_color_range: :class:`color range` or ``str`` - :param dst_color_range: Desired color range, or ``None`` for the frame color range. + :param dst_color_range: Desired color range, or ``None`` for the ``UNSPECIFIED``. :type dst_color_range: :class:`color range` or ``str`` """ @@ -110,6 +110,10 @@ cdef class VideoReformatter: if frame.ptr.format < 0: raise ValueError("Frame does not have format set.") + # The definition of color range in pixfmt.h and swscale.h is different. + src_color_range = 1 if src_color_range == ColorRange.JPEG.value else 0 + dst_color_range = 1 if dst_color_range == ColorRange.JPEG.value else 0 + cdef lib.AVPixelFormat src_format = frame.ptr.format # Shortcut! From be562241e31668bd92bd710878155ab5539d035f Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 16 Jul 2024 22:23:10 -0400 Subject: [PATCH 058/365] Cleanup codec/context.pyx --- av/codec/context.pyx | 106 +++++++++++++------------------------------ 1 file changed, 32 insertions(+), 74 deletions(-) diff --git a/av/codec/context.pyx b/av/codec/context.pyx index f8c1263c6..1d96899e2 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -25,7 +25,6 @@ cdef CodecContext wrap_codec_context(lib.AVCodecContext *c_ctx, const lib.AVCode cdef CodecContext py_ctx - # TODO: This. if c_ctx.codec_type == lib.AVMEDIA_TYPE_VIDEO: from av.video.codeccontext import VideoCodecContext py_ctx = VideoCodecContext(_cinit_sentinel) @@ -45,69 +44,46 @@ cdef CodecContext wrap_codec_context(lib.AVCodecContext *c_ctx, const lib.AVCode ThreadType = define_enum("ThreadType", __name__, ( ("NONE", 0), - ("FRAME", lib.FF_THREAD_FRAME, - """Decode more than one frame at once"""), - ("SLICE", lib.FF_THREAD_SLICE, - """Decode more than one part of a single frame at once"""), - ("AUTO", lib.FF_THREAD_SLICE | lib.FF_THREAD_FRAME, - """Decode using both FRAME and SLICE methods."""), + ("FRAME", lib.FF_THREAD_FRAME, "Decode more than one frame at once"), + ("SLICE", lib.FF_THREAD_SLICE, "Decode more than one part of a single frame at once"), + ("AUTO", lib.FF_THREAD_SLICE | lib.FF_THREAD_FRAME, "Decode using both FRAME and SLICE methods."), ), is_flags=True) SkipType = define_enum("SkipType", __name__, ( - ("NONE", lib.AVDISCARD_NONE, - """Discard nothing"""), - ("DEFAULT", lib.AVDISCARD_DEFAULT, - """Discard useless packets like 0 size packets in AVI"""), - ("NONREF", lib.AVDISCARD_NONREF, - """Discard all non reference"""), - ("BIDIR", lib.AVDISCARD_BIDIR, - """Discard all bidirectional frames"""), - ("NONINTRA", lib.AVDISCARD_NONINTRA, - """Discard all non intra frames"""), - ("NONKEY", lib.AVDISCARD_NONKEY, - """Discard all frames except keyframes"""), - ("ALL", lib.AVDISCARD_ALL, - """Discard all"""), + ("NONE", lib.AVDISCARD_NONE, "Discard nothing"), + ("DEFAULT", lib.AVDISCARD_DEFAULT, "Discard useless packets like 0 size packets in AVI"), + ("NONREF", lib.AVDISCARD_NONREF, "Discard all non reference"), + ("BIDIR", lib.AVDISCARD_BIDIR, "Discard all bidirectional frames"), + ("NONINTRA", lib.AVDISCARD_NONINTRA, "Discard all non intra frames"), + ("NONKEY", lib.AVDISCARD_NONKEY, "Discard all frames except keyframes"), + ("ALL", lib.AVDISCARD_ALL, "Discard all"), )) Flags = define_enum("Flags", __name__, ( ("NONE", 0), ("UNALIGNED", lib.AV_CODEC_FLAG_UNALIGNED, - """Allow decoders to produce frames with data planes that are not aligned - to CPU requirements (e.g. due to cropping)."""), - ("QSCALE", lib.AV_CODEC_FLAG_QSCALE, - """Use fixed qscale."""), - ("4MV", lib.AV_CODEC_FLAG_4MV, - """4 MV per MB allowed / advanced prediction for H.263."""), - ("OUTPUT_CORRUPT", lib.AV_CODEC_FLAG_OUTPUT_CORRUPT, - """Output even those frames that might be corrupted."""), - ("QPEL", lib.AV_CODEC_FLAG_QPEL, - """Use qpel MC."""), + "Allow decoders to produce frames with data planes that are not aligned to CPU requirements (e.g. due to cropping)." + ), + ("QSCALE", lib.AV_CODEC_FLAG_QSCALE, "Use fixed qscale."), + ("4MV", lib.AV_CODEC_FLAG_4MV, "4 MV per MB allowed / advanced prediction for H.263."), + ("OUTPUT_CORRUPT", lib.AV_CODEC_FLAG_OUTPUT_CORRUPT, "Output even those frames that might be corrupted."), + ("QPEL", lib.AV_CODEC_FLAG_QPEL, "Use qpel MC."), ("DROPCHANGED", 1 << 5, - """Don't output frames whose parameters differ from first - decoded frame in stream."""), - ("PASS1", lib.AV_CODEC_FLAG_PASS1, - """Use internal 2pass ratecontrol in first pass mode."""), - ("PASS2", lib.AV_CODEC_FLAG_PASS2, - """Use internal 2pass ratecontrol in second pass mode."""), - ("LOOP_FILTER", lib.AV_CODEC_FLAG_LOOP_FILTER, - """loop filter."""), - ("GRAY", lib.AV_CODEC_FLAG_GRAY, - """Only decode/encode grayscale."""), - ("PSNR", lib.AV_CODEC_FLAG_PSNR, - """error[?] variables will be set during encoding."""), - ("INTERLACED_DCT", lib.AV_CODEC_FLAG_INTERLACED_DCT, - """Use interlaced DCT."""), - ("LOW_DELAY", lib.AV_CODEC_FLAG_LOW_DELAY, - """Force low delay."""), + "Don't output frames whose parameters differ from first decoded frame in stream." + ), + ("PASS1", lib.AV_CODEC_FLAG_PASS1, "Use internal 2pass ratecontrol in first pass mode."), + ("PASS2", lib.AV_CODEC_FLAG_PASS2, "Use internal 2pass ratecontrol in second pass mode."), + ("LOOP_FILTER", lib.AV_CODEC_FLAG_LOOP_FILTER, "loop filter."), + ("GRAY", lib.AV_CODEC_FLAG_GRAY, "Only decode/encode grayscale."), + ("PSNR", lib.AV_CODEC_FLAG_PSNR, "error[?] variables will be set during encoding."), + ("INTERLACED_DCT", lib.AV_CODEC_FLAG_INTERLACED_DCT, "Use interlaced DCT."), + ("LOW_DELAY", lib.AV_CODEC_FLAG_LOW_DELAY, "Force low delay."), ("GLOBAL_HEADER", lib.AV_CODEC_FLAG_GLOBAL_HEADER, - """Place global headers in extradata instead of every keyframe."""), - ("BITEXACT", lib.AV_CODEC_FLAG_BITEXACT, - """Use only bitexact stuff (except (I)DCT)."""), - ("AC_PRED", lib.AV_CODEC_FLAG_AC_PRED, - """H.263 advanced intra coding / MPEG-4 AC prediction"""), - ("INTERLACED_ME", lib.AV_CODEC_FLAG_INTERLACED_ME, - """Interlaced motion estimation"""), + "Place global headers in extradata instead of every keyframe." + ), + ("BITEXACT", lib.AV_CODEC_FLAG_BITEXACT, "Use only bitexact stuff (except (I)DCT)."), + ("AC_PRED", lib.AV_CODEC_FLAG_AC_PRED, "H.263 advanced intra coding / MPEG-4 AC prediction"), + ("INTERLACED_ME", lib.AV_CODEC_FLAG_INTERLACED_ME, "Interlaced motion estimation"), ("CLOSED_GOP", lib.AV_CODEC_FLAG_CLOSED_GOP), ), is_flags=True) @@ -168,11 +144,7 @@ cdef class CodecContext: def _set_flags(self, value): self.ptr.flags = value - flags = Flags.property( - _get_flags, - _set_flags, - """Flag property of :class:`.Flags`.""" - ) + flags = Flags.property(_get_flags, _set_flags, "Flag property of :class:`.Flags`.") unaligned = flags.flag_property("UNALIGNED") qscale = flags.flag_property("QSCALE") @@ -199,12 +171,7 @@ cdef class CodecContext: def _set_flags2(self, value): self.ptr.flags2 = value - flags2 = Flags2.property( - _get_flags2, - _set_flags2, - """Flag property of :class:`.Flags2`.""" - ) - + flags2 = Flags2.property(_get_flags2, _set_flags2, "Flag property of :class:`.Flags2`.") fast = flags2.flag_property("FAST") no_output = flags2.flag_property("NO_OUTPUT") local_header = flags2.flag_property("LOCAL_HEADER") @@ -261,14 +228,6 @@ cdef class CodecContext: raise ValueError("CodecContext is already open.") return - # We might pass partial frames. - # TODO: What is this for?! This is causing problems with raw decoding - # as the internal parser doesn't seem to see a frame until it sees - # the next one. - # if self.codec.ptr.capabilities & lib.CODEC_CAP_TRUNCATED: - # self.ptr.flags |= lib.CODEC_FLAG_TRUNCATED - - # TODO: Do this better. cdef _Dictionary options = Dictionary() options.update(self.options or {}) @@ -377,7 +336,6 @@ cdef class CodecContext: in_size -= consumed if not in_size: - # Aaaand now we're done. break return packets From a6627f8ac24ac447801a383805186b61944518b3 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 16 Jul 2024 22:56:17 -0400 Subject: [PATCH 059/365] Remove frame.index --- av/audio/frame.pyx | 9 +-------- av/codec/context.pyx | 3 --- av/frame.pxd | 7 ------- av/frame.pyx | 3 +-- av/video/frame.pyx | 4 ++-- include/libavcodec/avcodec.pxd | 2 -- tests/test_codec_context.py | 11 ----------- 7 files changed, 4 insertions(+), 35 deletions(-) diff --git a/av/audio/frame.pyx b/av/audio/frame.pyx index ac3638230..1225f8de2 100644 --- a/av/audio/frame.pyx +++ b/av/audio/frame.pyx @@ -94,7 +94,7 @@ cdef class AudioFrame(Frame): def __repr__(self): return ( - f"" + return f"av.{self.__class__.__name__} pts={self.pts} at 0x{id(self):x}>" cdef _copy_internal_attributes(self, Frame source, bint data_layout=True): """Mimic another frame.""" - self.index = source.index self._time_base = source._time_base lib.av_frame_copy_props(self.ptr, source.ptr) if data_layout: diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 6ff982491..e64eb8573 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -124,8 +124,8 @@ cdef class VideoFrame(Frame): def __repr__(self): return ( - f"" + f"" ) @property diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 16d88a473..cc11a5e9b 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -174,8 +174,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: int global_quality int compression_level - int frame_number - int qmin int qmax int rc_max_rate diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index d30401f4e..59a10a297 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -96,17 +96,6 @@ def test_decoder_gop_size(self): "Using VideoCodecContext.gop_size for decoders is deprecated.", ) - def test_frame_index(self): - container = av.open(fate_suite("h264/interlaced_crop.mp4")) - stream = container.streams[0] - for frame in container.decode(stream): - with warnings.catch_warnings(record=True) as captured: - self.assertIsInstance(frame.index, int) - self.assertEqual( - captured[0].message.args[0], - "Using `frame.index` is deprecated.", - ) - def test_decoder_timebase(self): ctx = av.codec.Codec("h264", "r").create() From 53bb5ca8a4c2d3ea83ea5e463bd46771718b94e4 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 17 Jul 2024 13:11:43 -0400 Subject: [PATCH 060/365] Fix segfault when calling subtitle_stream.decode() --- av/subtitles/codeccontext.pyx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/av/subtitles/codeccontext.pyx b/av/subtitles/codeccontext.pyx index 10e8a6009..c0712c92c 100644 --- a/av/subtitles/codeccontext.pyx +++ b/av/subtitles/codeccontext.pyx @@ -7,13 +7,14 @@ from av.subtitles.subtitle cimport SubtitleProxy, SubtitleSet cdef class SubtitleCodecContext(CodecContext): cdef _send_packet_and_recv(self, Packet packet): + if packet is None: + raise RuntimeError("packet cannot be None") + cdef SubtitleProxy proxy = SubtitleProxy() cdef int got_frame = 0 err_check( - lib.avcodec_decode_subtitle2( - self.ptr, &proxy.struct, &got_frame, packet.ptr if packet else NULL - ) + lib.avcodec_decode_subtitle2(self.ptr, &proxy.struct, &got_frame, packet.ptr) ) if got_frame: From d21b8a2a3340e94b8afdd1b433679ae8e19d6434 Mon Sep 17 00:00:00 2001 From: Moonsik Park Date: Thu, 18 Jul 2024 15:07:24 +0900 Subject: [PATCH 061/365] Flushing subtitle decoder requires a new uninitialized packet --- av/subtitles/stream.pxd | 3 ++- av/subtitles/stream.pyi | 5 ++++- av/subtitles/stream.pyx | 17 +++++++++++++++++ tests/test_subtitles.py | 12 ++++++++++++ 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/av/subtitles/stream.pxd b/av/subtitles/stream.pxd index e21dceb23..745032af9 100644 --- a/av/subtitles/stream.pxd +++ b/av/subtitles/stream.pxd @@ -1,5 +1,6 @@ +from av.packet cimport Packet from av.stream cimport Stream cdef class SubtitleStream(Stream): - pass + cpdef decode(self, Packet packet=?) diff --git a/av/subtitles/stream.pyi b/av/subtitles/stream.pyi index 38f4ae6cb..cb1ac34a2 100644 --- a/av/subtitles/stream.pyi +++ b/av/subtitles/stream.pyi @@ -1,3 +1,6 @@ +from av.packet import Packet from av.stream import Stream +from av.subtitles.subtitle import SubtitleSet -class SubtitleStream(Stream): ... +class SubtitleStream(Stream): + def decode(self, packet: Packet | None = None) -> list[SubtitleSet]: ... diff --git a/av/subtitles/stream.pyx b/av/subtitles/stream.pyx index 1deed5867..9f90b9871 100644 --- a/av/subtitles/stream.pyx +++ b/av/subtitles/stream.pyx @@ -1,6 +1,23 @@ +from av.packet cimport Packet +from av.stream cimport Stream + + cdef class SubtitleStream(Stream): """ A :class:`SubtitleStream` can contain many :class:`SubtitleSet` objects accessible via decoding. """ def __getattr__(self, name): return getattr(self.codec_context, name) + + cpdef decode(self, Packet packet=None): + """ + Decode a :class:`.Packet` and return a list of :class:`.SubtitleSet`. + + :rtype: list[SubtitleSet] + + .. seealso:: This is a passthrough to :meth:`.CodecContext.decode`. + """ + if not packet: + packet = Packet() + + return self.codec_context.decode(packet) diff --git a/tests/test_subtitles.py b/tests/test_subtitles.py index cc1409f2b..9a5e9ceb1 100644 --- a/tests/test_subtitles.py +++ b/tests/test_subtitles.py @@ -58,3 +58,15 @@ def test_vobsub(self): bms = sub.planes self.assertEqual(len(bms), 1) self.assertEqual(len(memoryview(bms[0])), 4800) + + def test_subtitle_flush(self) -> None: + path = fate_suite("sub/MovText_capability_tester.mp4") + + subs = [] + with av.open(path) as container: + stream = container.streams.subtitles[0] + for packet in container.demux(stream): + subs.extend(stream.decode(packet)) + subs.extend(stream.decode()) + + self.assertEqual(len(subs), 3) From 8720cd5b60e2aca962121d59427a691d832a6351 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 18 Jul 2024 23:03:46 -0400 Subject: [PATCH 062/365] Use ffmpeg 6.1.1-5 --- scripts/ffmpeg-6.1.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ffmpeg-6.1.json b/scripts/ffmpeg-6.1.json index d2b41f4c1..9bae82dd3 100644 --- a/scripts/ffmpeg-6.1.json +++ b/scripts/ffmpeg-6.1.json @@ -1,3 +1,3 @@ { - "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/6.1.1-4/ffmpeg-{platform}.tar.gz"] + "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/6.1.1-5/ffmpeg-{platform}.tar.gz"] } From 68ac522517f00696928221f13ce7c830af47ada6 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 20 Jul 2024 12:35:58 -0400 Subject: [PATCH 063/365] Update authors and changelog --- AUTHORS.rst | 5 ++++- CHANGELOG.rst | 22 +++++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 68595a7da..af7ea08cc 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -24,11 +24,12 @@ All contributors (by number of commits): - Xinran Xu ; `@xxr3376 `_ - Dan Allan ; `@danielballan `_ - Dave Johansen +- Moonsik Park +- Santtu Keskinen - Christoph Rackwitz - Alireza Davoudi ; `@adavoudi `_ - Jonathan Drolet - Moritz Kassner ; `@mkassner `_ -- Santtu Keskinen - Thomas A Caswell ; `@tacaswell `_ - Ulrik Mikaelsson ; `@rawler `_ - Wel C. van der @@ -43,6 +44,7 @@ All contributors (by number of commits): * Karl Litterfeldt ; `@litterfeldt `_ * Martin Larralde * Simon-Martin Schröder +* Matteo Destro * mephi42 * Miles Kaufmann * Pablo Prietz @@ -61,6 +63,7 @@ All contributors (by number of commits): * Maxime Desroches * egao1980 * Eric Kalosa-Kenyon +* elxy * Gemfield * Jonathan Martin * Johan Jeppsson Karlin diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3f29ef131..dad978552 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,26 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. + +v12.3.0 +------- + +Features: + +- Support libav's `av_log_set_level` by @materight in (:issue:`1448`). +- Add Graph.link_nodes by @WyattBlue in (:issue:`1449`). +- Add default codec properties by @WyattBlue in (:issue:`1450`). +- Use ffmpeg 6.1.1-5 by @WyattBlue in (:issue:`1462`). +- Add text and dialogue property to AssSubtitle, remove TextSubtitle by @WyattBlue in (:issue:`1456`). + +Fixes: + +- Include libav headers in final distribution by @materight in (:issue:`1455`). +- Fix segfault when calling subtitle_stream.decode() by @WyattBlue in (:issue:`1460`). +- flushing subtitle decoder requires a new uninitialized packet by @moonsikpark in (:issue:`1461`). +- Set default color range for VideoReformatter.format() by @elxy in (:issue:`1458`). +- Resampler: format, layout accepts `str` `int` too by @WyattBlue in (:issue:`1446`). + v12.2.0 ------- @@ -44,7 +64,7 @@ Fixes: - Fix type stubs, add missing type stubs. - Add S12M_TIMECODE by @WyattBlue in (:issue:`1381`). -- Subtitle.text now returns bytes by @WyattBlue in (:issue:`13981). +- Subtitle.text now returns bytes by @WyattBlue in (:issue:`1398`). - Allow packet.duration to be writable by @WyattBlue in (:issue:`1399`). - Remove deprecated `VideoStream.frame_rate` by @WyattBlue in (:issue:`1351`). - Build with Arm for PyPy now by @WyattBlue in (:issue:`1395`). From b1ba1e7d13620dc3c7c8e861ab1b332fdc1f3747 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 20 Jul 2024 19:54:10 -0400 Subject: [PATCH 064/365] Add container.supported_codecs --- av/container/output.pyi | 2 ++ av/container/output.pyx | 20 ++++++++++++++++++++ include/libavcodec/avcodec.pxd | 15 +++++++-------- include/libavformat/avformat.pxd | 2 ++ tests/test_containerformat.py | 3 +++ 5 files changed, 34 insertions(+), 8 deletions(-) diff --git a/av/container/output.pyi b/av/container/output.pyi index 882592667..9e46b413a 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -25,3 +25,5 @@ class OutputContainer(Container): def default_audio_codec(self) -> str: ... @property def default_subtitle_codec(self) -> str: ... + @property + def supported_codecs(self) -> set[str]: ... diff --git a/av/container/output.pyx b/av/container/output.pyx index 184a30b2d..c4e39263d 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -194,6 +194,26 @@ cdef class OutputContainer(Container): self._started = True + @property + def supported_codecs(self): + """ + Returns a set of all codecs this format supports. + """ + result = set() + cdef const lib.AVCodec *codec = NULL + cdef void *opaque = NULL + + while True: + codec = lib.av_codec_iterate(&opaque) + if codec == NULL: + break + + if lib.avformat_query_codec(self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL) == 1: + result.add(codec.name) + + return result + + @property def default_video_codec(self): """ diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index cc11a5e9b..d4335b2a9 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -1,15 +1,14 @@ from libc.stdint cimport int8_t, int64_t, uint16_t, uint32_t +cdef extern from "libavcodec/codec.h": + struct AVCodecTag: + pass + +cdef extern from "libavcodec/codec_id.h": + AVCodecID av_codec_get_id(const AVCodecTag *const *tags, uint32_t tag) + cdef extern from "libavcodec/avcodec.h" nogil: - """ - // AV_FRAME_DATA_SEI_UNREGISTERED available since version 56.54.100 of libavutil (FFmpeg >= 4.4) - #define HAS_AV_FRAME_DATA_SEI_UNREGISTERED (LIBAVUTIL_VERSION_INT >= 3683940) - - #if !HAS_AV_FRAME_DATA_SEI_UNREGISTERED - #define AV_FRAME_DATA_SEI_UNREGISTERED -1 - #endif - """ cdef set pyav_get_available_codecs() cdef int avcodec_version() diff --git a/include/libavformat/avformat.pxd b/include/libavformat/avformat.pxd index 9d9061cc2..f51ba269b 100644 --- a/include/libavformat/avformat.pxd +++ b/include/libavformat/avformat.pxd @@ -115,6 +115,8 @@ cdef extern from "libavformat/avformat.h" nogil: # const AVCodecTag* const *codec_tag const AVClass *priv_class + int avformat_query_codec(const AVOutputFormat *oformat, AVCodecID codec_id, int std_compliance) + # AVInputFormat.flags and AVOutputFormat.flags cdef enum: AVFMT_NOFILE diff --git a/tests/test_containerformat.py b/tests/test_containerformat.py index f0460b131..5b6d31c35 100644 --- a/tests/test_containerformat.py +++ b/tests/test_containerformat.py @@ -9,6 +9,7 @@ def test_matroska(self) -> None: self.assertNotEqual(container.default_video_codec, "none") self.assertNotEqual(container.default_audio_codec, "none") self.assertEqual(container.default_subtitle_codec, "ass") + self.assertIn("ass", container.supported_codecs) fmt = ContainerFormat("matroska") self.assertTrue(fmt.is_input) @@ -23,6 +24,7 @@ def test_mov(self) -> None: self.assertNotEqual(container.default_video_codec, "none") self.assertNotEqual(container.default_audio_codec, "none") self.assertEqual(container.default_subtitle_codec, "none") + self.assertIn("h264", container.supported_codecs) fmt = ContainerFormat("mov") self.assertTrue(fmt.is_input) @@ -37,6 +39,7 @@ def test_gif(self) -> None: self.assertEqual(container.default_video_codec, "gif") self.assertEqual(container.default_audio_codec, "none") self.assertEqual(container.default_subtitle_codec, "none") + self.assertIn("gif", container.supported_codecs) def test_stream_segment(self) -> None: # This format goes by two names, check both. From 4baf011d9d76cb99e1d7deb96c819632cc4a8fff Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 20 Jul 2024 20:27:56 -0400 Subject: [PATCH 065/365] Update changelog --- CHANGELOG.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index dad978552..2b7de678b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -25,7 +25,8 @@ Features: - Support libav's `av_log_set_level` by @materight in (:issue:`1448`). - Add Graph.link_nodes by @WyattBlue in (:issue:`1449`). - Add default codec properties by @WyattBlue in (:issue:`1450`). -- Use ffmpeg 6.1.1-5 by @WyattBlue in (:issue:`1462`). +- Remove the xvid and ass packages in ffmpeg binaries because they were unused by @WyattBlue in (:issue:`1462`). +- Add supported_codecs property to OutputContainer by @WyattBlue in (:issue:`1465`). - Add text and dialogue property to AssSubtitle, remove TextSubtitle by @WyattBlue in (:issue:`1456`). Fixes: From 0b5e4dfeef351042ed94aff5b7b7c4510ba1aae9 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 20 Jul 2024 20:28:33 -0400 Subject: [PATCH 066/365] Release 12.3.0 --- av/about.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/about.py b/av/about.py index b0de45783..e4400ae60 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "12.3.0rc1" +__version__ = "12.3.0" From 55953089f078d5691d5009772dc2f7710aca74aa Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 20 Jul 2024 22:28:50 -0400 Subject: [PATCH 067/365] Bump to next dev version --- av/about.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/about.py b/av/about.py index e4400ae60..79a759ae1 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "12.3.0" +__version__ = "13.0.0rc1" From b2193fd120bdc1f8b6b5b836e72aa895808729a4 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 25 Jul 2024 21:08:05 -0400 Subject: [PATCH 068/365] Build with ffmpeg 7 --- .github/workflows/smoke.yml | 3 + av/audio/codeccontext.pxd | 2 - av/audio/codeccontext.pyi | 4 +- av/audio/codeccontext.pyx | 24 +------ av/audio/fifo.pyx | 6 +- av/audio/frame.pxd | 3 +- av/audio/frame.pyx | 24 +++---- av/audio/layout.pxd | 26 ++------ av/audio/layout.pyi | 10 +-- av/audio/layout.pyx | 108 ++++++------------------------ av/audio/resampler.pyx | 31 +++++---- av/codec/codec.pyx | 7 +- av/codec/context.pyx | 4 -- av/container/core.pyx | 18 ++--- av/container/output.pyx | 4 +- av/container/pyio.pxd | 12 +--- av/container/pyio.pyx | 17 +++-- av/frame.pyx | 3 +- av/option.pyx | 6 +- include/libavcodec/avcodec.pxd | 48 +++++++++++--- include/libavformat/avformat.pxd | 2 +- include/libavutil/avutil.pxd | 14 +--- scripts/activate.sh | 2 +- scripts/build-deps | 1 + scripts/ffmpeg-7.0.json | 3 + tests/test_audiolayout.py | 47 ++++--------- tests/test_audioresampler.py | 91 +++++++++++++------------- tests/test_codec.py | 18 ++--- tests/test_codec_context.py | 109 +------------------------------ tests/test_encode.py | 8 +-- tests/test_filters.py | 2 +- 31 files changed, 207 insertions(+), 450 deletions(-) create mode 100644 scripts/ffmpeg-7.0.json diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 39cf096cd..22d9235b7 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -38,8 +38,10 @@ jobs: matrix: config: - {os: ubuntu-latest, python: "3.8", ffmpeg: "6.1", extras: true} + # TODO: - {os: ubuntu-latest, python: "3.8", ffmpeg: "7.0"} - {os: ubuntu-latest, python: "3.8", ffmpeg: "6.0"} - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1"} + - {os: macos-12, python: "3.8", ffmpeg: "7.0"} - {os: macos-12, python: "3.8", ffmpeg: "6.1"} env: @@ -116,6 +118,7 @@ jobs: fail-fast: false matrix: config: + - {os: windows-latest, python: "3.8", ffmpeg: "7.0"} - {os: windows-latest, python: "3.8", ffmpeg: "6.1"} - {os: windows-latest, python: "3.8", ffmpeg: "6.0"} diff --git a/av/audio/codeccontext.pxd b/av/audio/codeccontext.pxd index 277d47780..55ad15e9f 100644 --- a/av/audio/codeccontext.pxd +++ b/av/audio/codeccontext.pxd @@ -5,9 +5,7 @@ from av.codec.context cimport CodecContext cdef class AudioCodecContext(CodecContext): - # Hold onto the frames that we will decode until we have a full one. cdef AudioFrame next_frame - # For encoding. cdef AudioResampler resampler diff --git a/av/audio/codeccontext.pyi b/av/audio/codeccontext.pyi index ada12efcb..e11a42cdc 100644 --- a/av/audio/codeccontext.pyi +++ b/av/audio/codeccontext.pyi @@ -11,12 +11,12 @@ class AudioCodecContext(CodecContext): frame_size: int sample_rate: int rate: int - channels: int - channel_layout: int layout: AudioLayout format: AudioFormat type: Literal["audio"] + @property + def channels(self) -> int: ... def encode(self, frame: AudioFrame | None = None) -> list[Packet]: ... def encode_lazy(self, frame: AudioFrame | None = None) -> Iterator[Packet]: ... def decode(self, packet: Packet | None = None) -> list[AudioFrame]: ... diff --git a/av/audio/codeccontext.pyx b/av/audio/codeccontext.pyx index 1978759ae..1723cbac7 100644 --- a/av/audio/codeccontext.pyx +++ b/av/audio/codeccontext.pyx @@ -11,13 +11,6 @@ cdef class AudioCodecContext(CodecContext): cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec): CodecContext._init(self, ptr, codec) - # Sometimes there isn't a layout set, but there are a number of - # channels. Assume it is the default layout. - # TODO: Put this behind `not bare_metal`. - # TODO: Do this more efficiently. - if self.ptr.channels and not self.ptr.channel_layout: - self.ptr.channel_layout = get_audio_layout(self.ptr.channels, 0).layout - cdef _set_default_time_base(self): self.ptr.time_base.num = 1 self.ptr.time_base.den = self.ptr.sample_rate @@ -62,7 +55,6 @@ cdef class AudioCodecContext(CodecContext): """ return self.ptr.frame_size - @property def sample_rate(self): """ @@ -85,18 +77,9 @@ cdef class AudioCodecContext(CodecContext): def rate(self, value): self.sample_rate = value - # TODO: Integrate into AudioLayout. @property def channels(self): - return self.ptr.channels - - @channels.setter - def channels(self, value): - self.ptr.channels = value - self.ptr.channel_layout = lib.av_get_default_channel_layout(value) - @property - def channel_layout(self): - return self.ptr.channel_layout + return self.layout.nb_channels @property def layout(self): @@ -105,13 +88,12 @@ cdef class AudioCodecContext(CodecContext): :type: AudioLayout """ - return get_audio_layout(self.ptr.channels, self.ptr.channel_layout) + return get_audio_layout(self.ptr.ch_layout) @layout.setter def layout(self, value): cdef AudioLayout layout = AudioLayout(value) - self.ptr.channel_layout = layout.layout - self.ptr.channels = layout.nb_channels + self.ptr.ch_layout = layout.layout @property def format(self): diff --git a/av/audio/fifo.pyx b/av/audio/fifo.pyx index 83d9cc71d..9b1380270 100644 --- a/av/audio/fifo.pyx +++ b/av/audio/fifo.pyx @@ -61,7 +61,7 @@ cdef class AudioFifo: self.ptr = lib.av_audio_fifo_alloc( frame.ptr.format, - len(frame.layout.channels), # TODO: Can we safely use frame.ptr.nb_channels? + frame.layout.nb_channels, frame.ptr.nb_samples * 2, # Just a default number of samples; it will adjust. ) @@ -71,7 +71,7 @@ cdef class AudioFifo: # Make sure nothing changed. elif ( frame.ptr.format != self.template.ptr.format or - frame.ptr.channel_layout != self.template.ptr.channel_layout or + # TODO: frame.ptr.ch_layout != self.template.ptr.ch_layout or frame.ptr.sample_rate != self.template.ptr.sample_rate or (frame._time_base.num and self.template._time_base.num and ( frame._time_base.num != self.template._time_base.num or @@ -131,7 +131,7 @@ cdef class AudioFifo: frame._copy_internal_attributes(self.template) frame._init( self.template.ptr.format, - self.template.ptr.channel_layout, + self.template.ptr.ch_layout, samples, 1, # Align? ) diff --git a/av/audio/frame.pxd b/av/audio/frame.pxd index e7ee88591..398d76d33 100644 --- a/av/audio/frame.pxd +++ b/av/audio/frame.pxd @@ -7,7 +7,6 @@ from av.frame cimport Frame cdef class AudioFrame(Frame): - # For raw storage of the frame's data; don't ever touch this. cdef uint8_t *_buffer cdef size_t _buffer_size @@ -26,7 +25,7 @@ cdef class AudioFrame(Frame): :type: AudioFormat """ - cdef _init(self, lib.AVSampleFormat format, uint64_t layout, unsigned int nb_samples, unsigned int align) + cdef _init(self, lib.AVSampleFormat format, lib.AVChannelLayout layout, unsigned int nb_samples, unsigned int align) cdef _init_user_attributes(self) cdef AudioFrame alloc_audio_frame() diff --git a/av/audio/frame.pyx b/av/audio/frame.pyx index 1225f8de2..1e45842ea 100644 --- a/av/audio/frame.pyx +++ b/av/audio/frame.pyx @@ -4,10 +4,6 @@ from av.audio.plane cimport AudioPlane from av.error cimport err_check from av.utils cimport check_ndarray, check_ndarray_shape -import warnings - -from av.deprecation import AVDeprecationWarning - cdef object _cinit_bypass_sentinel @@ -47,26 +43,22 @@ cdef class AudioFrame(Frame): cdef AudioLayout cy_layout = AudioLayout(layout) self._init(cy_format.sample_fmt, cy_layout.layout, samples, align) - cdef _init(self, lib.AVSampleFormat format, uint64_t layout, unsigned int nb_samples, unsigned int align): - + cdef _init(self, lib.AVSampleFormat format, lib.AVChannelLayout layout, unsigned int nb_samples, unsigned int align): self.ptr.nb_samples = nb_samples self.ptr.format = format - self.ptr.channel_layout = layout + self.ptr.ch_layout = layout # Sometimes this is called twice. Oh well. self._init_user_attributes() - # Audio filters need AVFrame.channels to match number of channels from layout. - self.ptr.channels = self.layout.nb_channels - - if self.layout.channels and nb_samples: + if self.layout.nb_channels != 0 and nb_samples: # Cleanup the old buffer. lib.av_freep(&self._buffer) # Get a new one. self._buffer_size = err_check(lib.av_samples_get_buffer_size( NULL, - len(self.layout.channels), + self.layout.nb_channels, nb_samples, format, align @@ -78,7 +70,7 @@ cdef class AudioFrame(Frame): # Connect the data pointers to the buffer. err_check(lib.avcodec_fill_audio_frame( self.ptr, - len(self.layout.channels), + self.layout.nb_channels, self.ptr.format, self._buffer, self._buffer_size, @@ -89,7 +81,7 @@ cdef class AudioFrame(Frame): lib.av_freep(&self._buffer) cdef _init_user_attributes(self): - self.layout = get_audio_layout(0, self.ptr.channel_layout) + self.layout = get_audio_layout(self.ptr.ch_layout) self.format = get_audio_format(self.ptr.format) def __repr__(self): @@ -114,7 +106,7 @@ cdef class AudioFrame(Frame): ) # check input format - nb_channels = len(AudioLayout(layout).channels) + nb_channels = AudioLayout(layout).nb_channels check_ndarray(array, dtype, 2) if AudioFormat(format).is_planar: check_ndarray_shape(array, array.shape[0] == nb_channels) @@ -188,6 +180,6 @@ cdef class AudioFrame(Frame): if self.format.is_planar: count = self.samples else: - count = self.samples * len(self.layout.channels) + count = self.samples * self.layout.nb_channels return np.vstack([np.frombuffer(x, dtype=dtype, count=count) for x in self.planes]) diff --git a/av/audio/layout.pxd b/av/audio/layout.pxd index c46b9c741..c7a2368f1 100644 --- a/av/audio/layout.pxd +++ b/av/audio/layout.pxd @@ -1,26 +1,8 @@ -from libc.stdint cimport uint64_t +cimport libav as lib cdef class AudioLayout: + cdef lib.AVChannelLayout layout + cdef _init(self, lib.AVChannelLayout layout) - # The layout for FFMpeg; this is essentially a bitmask of channels. - cdef uint64_t layout - cdef int nb_channels - - cdef readonly tuple channels - """ - A tuple of :class:`AudioChannel` objects. - - :type: tuple - """ - - cdef _init(self, uint64_t layout) - - -cdef class AudioChannel: - - # The channel for FFmpeg. - cdef uint64_t channel - - -cdef AudioLayout get_audio_layout(int channels, uint64_t c_layout) +cdef AudioLayout get_audio_layout(lib.AVChannelLayout c_layout) diff --git a/av/audio/layout.pyi b/av/audio/layout.pyi index a59398fcc..073cd1723 100644 --- a/av/audio/layout.pyi +++ b/av/audio/layout.pyi @@ -1,12 +1,4 @@ -channel_descriptions: dict[str, str] - class AudioLayout: name: str - layout: int nb_channels: int - channels: tuple[AudioChannel, ...] - def __init__(self, layout: int | str | AudioLayout): ... - -class AudioChannel: - name: str - description: str + def __init__(self, layout: str | AudioLayout): ... diff --git a/av/audio/layout.pyx b/av/audio/layout.pyx index 76edd177d..59753138e 100644 --- a/av/audio/layout.pyx +++ b/av/audio/layout.pyx @@ -3,118 +3,50 @@ cimport libav as lib cdef object _cinit_bypass_sentinel -cdef AudioLayout get_audio_layout(int channels, uint64_t c_layout): +cdef AudioLayout get_audio_layout(lib.AVChannelLayout c_layout): """Get an AudioLayout from Cython land.""" cdef AudioLayout layout = AudioLayout.__new__(AudioLayout, _cinit_bypass_sentinel) - if channels and not c_layout: - c_layout = default_layouts[channels] layout._init(c_layout) return layout -# TODO: What about av_get_default_channel_layout(...)? -cdef uint64_t default_layouts[17] -default_layouts[0] = 0 -default_layouts[1] = lib.AV_CH_LAYOUT_MONO -default_layouts[2] = lib.AV_CH_LAYOUT_STEREO -default_layouts[3] = lib.AV_CH_LAYOUT_2POINT1 -default_layouts[4] = lib.AV_CH_LAYOUT_4POINT0 -default_layouts[5] = lib.AV_CH_LAYOUT_5POINT0_BACK -default_layouts[6] = lib.AV_CH_LAYOUT_5POINT1_BACK -default_layouts[7] = lib.AV_CH_LAYOUT_6POINT1 -default_layouts[8] = lib.AV_CH_LAYOUT_7POINT1 -default_layouts[9] = 0x01FF -default_layouts[10] = 0x03FF -default_layouts[11] = 0x07FF -default_layouts[12] = 0x0FFF -default_layouts[13] = 0x1FFF -default_layouts[14] = 0x3FFF -default_layouts[15] = 0x7FFF -default_layouts[16] = 0xFFFF - - -cdef dict channel_descriptions = { - "FL": "front left", - "FR": "front right", - "FC": "front center", - "LFE": "low frequency", - "BL": "back left", - "BR": "back right", - "FLC": "front left-of-center", - "FRC": "front right-of-center", - "BC": "back center", - "SL": "side left", - "SR": "side right", - "TC": "top center", - "TFL": "top front left", - "TFC": "top front center", - "TFR": "top front right", - "TBL": "top back left", - "TBC": "top back center", - "TBR": "top back right", - "DL": "downmix left", - "DR": "downmix right", - "WL": "wide left", - "WR": "wide right", - "SDL": "surround direct left", - "SDR": "surround direct right", - "LFE2": "low frequency 2", -} - - cdef class AudioLayout: def __init__(self, layout): if layout is _cinit_bypass_sentinel: return - cdef uint64_t c_layout - if isinstance(layout, int): - if layout < 0 or layout > 8: - raise ValueError(f"no layout with {layout} channels") - - c_layout = default_layouts[layout] - elif isinstance(layout, str): - c_layout = lib.av_get_channel_layout(layout) + if type(layout) is str: + ret = lib.av_channel_layout_from_string(&c_layout, layout) + if ret != 0: + raise ValueError(f"Invalid layout: {layout}") elif isinstance(layout, AudioLayout): c_layout = (layout).layout else: - raise TypeError("layout must be str or int") - - if not c_layout: - raise ValueError(f"invalid channel layout: {layout}") + raise TypeError(f"layout must be of type: string | av.AudioLayout, got {type(layout)}") self._init(c_layout) - cdef _init(self, uint64_t layout): + cdef _init(self, lib.AVChannelLayout layout): self.layout = layout - self.nb_channels = lib.av_get_channel_layout_nb_channels(layout) # This just counts bits. - self.channels = tuple(AudioChannel(self, i) for i in range(self.nb_channels)) def __repr__(self): return f"" - @property - def name(self): - """The canonical name of the audio layout.""" - cdef char out[32] - # Passing 0 as number of channels... fix this later? - lib.av_get_channel_layout_string(out, 32, 0, self.layout) - return out - + def __eq__(self, other): + return isinstance(other, AudioLayout) and self.name == other.name and self.nb_channels == other.nb_channels -cdef class AudioChannel: - def __cinit__(self, AudioLayout layout, int index): - self.channel = lib.av_channel_layout_extract_channel(layout.layout, index) - - def __repr__(self): - return f"" + @property + def nb_channels(self): + return self.layout.nb_channels @property def name(self): - """The canonical name of the audio channel.""" - return lib.av_get_channel_name(self.channel) + """The canonical name of the audio layout.""" + cdef char layout_name[128] # Adjust buffer size as needed + cdef int ret - @property - def description(self): - """A human description of the audio channel.""" - return channel_descriptions.get(self.name) + ret = lib.av_channel_layout_describe(&self.layout, layout_name, sizeof(layout_name)) + if ret < 0: + raise RuntimeError(f"Failed to get layout name: {ret}") + + return layout_name diff --git a/av/audio/resampler.pyx b/av/audio/resampler.pyx index 89a5428e1..59cddec8c 100644 --- a/av/audio/resampler.pyx +++ b/av/audio/resampler.pyx @@ -23,8 +23,9 @@ cdef class AudioResampler: def __cinit__(self, format=None, layout=None, rate=None, frame_size=None): if format is not None: self.format = format if isinstance(format, AudioFormat) else AudioFormat(format) + if layout is not None: - self.layout = layout if isinstance(layout, AudioLayout) else AudioLayout(layout) + self.layout = AudioLayout(layout) self.rate = int(rate) if rate else 0 self.frame_size = int(frame_size) if frame_size else 0 @@ -62,7 +63,7 @@ cdef class AudioResampler: # Check if we can passthrough or if there is actually work to do. if ( frame.format.sample_fmt == self.format.sample_fmt and - frame.layout.layout == self.layout.layout and + frame.layout == self.layout and frame.sample_rate == self.rate and self.frame_size == 0 ): @@ -75,15 +76,19 @@ cdef class AudioResampler: extra_args = {} if frame.time_base is not None: extra_args["time_base"] = str(frame.time_base) - abuffer = self.graph.add("abuffer", - sample_rate=str(frame.sample_rate), - sample_fmt=AudioFormat(frame.format).name, - channel_layout=frame.layout.name, - **extra_args) - aformat = self.graph.add("aformat", - sample_rates=str(self.rate), - sample_fmts=self.format.name, - channel_layouts=str(self.layout.layout)) + abuffer = self.graph.add( + "abuffer", + sample_rate=str(frame.sample_rate), + sample_fmt=AudioFormat(frame.format).name, + channel_layout=frame.layout.name, + **extra_args, + ) + aformat = self.graph.add( + "aformat", + sample_rates=str(self.rate), + sample_fmts=self.format.name, + channel_layouts=self.layout.name, + ) abuffersink = self.graph.add("abuffersink") abuffer.link_to(aformat) aformat.link_to(abuffersink) @@ -93,11 +98,9 @@ cdef class AudioResampler: lib.av_buffersink_set_frame_size((abuffersink).ptr, self.frame_size) elif frame is not None: - - # Assert the settings are the same on consecutive frames. if ( frame.format.sample_fmt != self.template.format.sample_fmt or - frame.layout.layout != self.template.layout.layout or + frame.layout != self.template.layout or frame.sample_rate != self.template.rate ): raise ValueError("Frame does not match AudioResampler setup.") diff --git a/av/codec/codec.pyx b/av/codec/codec.pyx index b350a8b4f..9c4b7ed94 100644 --- a/av/codec/codec.pyx +++ b/av/codec/codec.pyx @@ -139,7 +139,6 @@ class UnknownCodecError(ValueError): cdef class Codec: - """Codec(name, mode='r') :param str name: The codec name. @@ -161,7 +160,6 @@ cdef class Codec: """ def __cinit__(self, name, mode="r"): - if name is _cinit_sentinel: return @@ -189,7 +187,6 @@ cdef class Codec: raise RuntimeError("Found codec does not match mode.", name, mode) cdef _init(self, name=None): - if not self.ptr: raise UnknownCodecError(name) @@ -218,6 +215,7 @@ cdef class Codec: @property def name(self): return self.ptr.name or "" + @property def long_name(self): return self.ptr.long_name or "" @@ -343,9 +341,8 @@ cdef get_codec_names(): break return names -codecs_available = get_codec_names() - +codecs_available = get_codec_names() codec_descriptor = wrap_avclass(lib.avcodec_get_class()) diff --git a/av/codec/context.pyx b/av/codec/context.pyx index dc71bb2a5..70100e65e 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -532,10 +532,6 @@ cdef class CodecContext: else: raise ValueError("Codec tag should be a 4 character string.") - @property - def ticks_per_frame(self): - return self.ptr.ticks_per_frame - @property def bit_rate(self): return self.ptr.bit_rate if self.ptr.bit_rate > 0 else None diff --git a/av/container/core.pyx b/av/container/core.pyx index df1b25939..683a3f65d 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -100,29 +100,29 @@ cdef int pyav_io_open_gil(lib.AVFormatContext *s, return stash_exception() -cdef void pyav_io_close(lib.AVFormatContext *s, - lib.AVIOContext *pb) noexcept nogil: +cdef int pyav_io_close(lib.AVFormatContext *s, lib.AVIOContext *pb) noexcept nogil: with gil: - pyav_io_close_gil(s, pb) + return pyav_io_close_gil(s, pb) - -cdef void pyav_io_close_gil(lib.AVFormatContext *s, - lib.AVIOContext *pb) noexcept: +cdef int pyav_io_close_gil(lib.AVFormatContext *s, lib.AVIOContext *pb) noexcept: cdef Container container + cdef int result = 0 try: container = dereference(s).opaque if container.open_files is not None and pb.opaque in container.open_files: - pyio_close_custom_gil(pb) + result = pyio_close_custom_gil(pb) # Remove it from the container so that it can be deallocated del container.open_files[pb.opaque] else: - pyio_close_gil(pb) + result = pyio_close_gil(pb) except Exception as e: stash_exception() + result = lib.AVERROR_UNKNOWN # Or another appropriate error code + return result Flags = define_enum("Flags", __name__, ( ("GENPTS", lib.AVFMT_FLAG_GENPTS, @@ -242,7 +242,7 @@ cdef class Container: if io_open is not None: self.ptr.io_open = pyav_io_open - self.ptr.io_close = pyav_io_close + self.ptr.io_close2 = pyav_io_close self.ptr.flags |= lib.AVFMT_FLAG_CUSTOM_IO cdef lib.AVInputFormat *ifmt diff --git a/av/container/output.pyx b/av/container/output.pyx index c4e39263d..00f5d55b3 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -97,7 +97,6 @@ cdef class OutputContainer(Container): codec_context.height = 480 codec_context.bit_rate = 1024000 codec_context.bit_rate_tolerance = 128000 - codec_context.ticks_per_frame = 1 to_avrational(rate or 24, &codec_context.framerate) stream.avg_frame_rate = codec_context.framerate @@ -109,8 +108,7 @@ cdef class OutputContainer(Container): codec_context.bit_rate = 128000 codec_context.bit_rate_tolerance = 32000 codec_context.sample_rate = rate or 48000 - codec_context.channels = 2 - codec_context.channel_layout = lib.AV_CH_LAYOUT_STEREO + lib.av_channel_layout_default(&codec_context.ch_layout, 2) # Some formats want stream headers to be separate if self.ptr.oformat.flags & lib.AVFMT_GLOBALHEADER: diff --git a/av/container/pyio.pxd b/av/container/pyio.pxd index b3ec04087..80edc8a6b 100644 --- a/av/container/pyio.pxd +++ b/av/container/pyio.pxd @@ -3,18 +3,12 @@ from libc.stdint cimport int64_t, uint8_t cdef int pyio_read(void *opaque, uint8_t *buf, int buf_size) noexcept nogil - -cdef int pyio_write(void *opaque, uint8_t *buf, int buf_size) noexcept nogil - +cdef int pyio_write(void *opaque, const uint8_t *buf, int buf_size) noexcept nogil cdef int64_t pyio_seek(void *opaque, int64_t offset, int whence) noexcept nogil - -cdef void pyio_close_gil(lib.AVIOContext *pb) - -cdef void pyio_close_custom_gil(lib.AVIOContext *pb) - +cdef int pyio_close_gil(lib.AVIOContext *pb) +cdef int pyio_close_custom_gil(lib.AVIOContext *pb) cdef class PyIOFile: - # File-like source. cdef readonly object file cdef object fread diff --git a/av/container/pyio.pyx b/av/container/pyio.pyx index ab29cee11..821ae9c0c 100644 --- a/av/container/pyio.pyx +++ b/av/container/pyio.pyx @@ -47,7 +47,8 @@ cdef class PyIOFile: self.buffer = lib.av_malloc(buffer_size) self.iocontext = lib.avio_alloc_context( - self.buffer, buffer_size, + self.buffer, + buffer_size, writeable, self, # User data. pyio_read, @@ -92,11 +93,11 @@ cdef int pyio_read_gil(void *opaque, uint8_t *buf, int buf_size) noexcept: return stash_exception() -cdef int pyio_write(void *opaque, uint8_t *buf, int buf_size) noexcept nogil: +cdef int pyio_write(void *opaque, const uint8_t *buf, int buf_size) noexcept nogil: with gil: return pyio_write_gil(opaque, buf, buf_size) -cdef int pyio_write_gil(void *opaque, uint8_t *buf, int buf_size) noexcept: +cdef int pyio_write_gil(void *opaque, const uint8_t *buf, int buf_size) noexcept: cdef PyIOFile self cdef bytes bytes_to_write cdef int bytes_written @@ -143,24 +144,26 @@ cdef int64_t pyio_seek_gil(void *opaque, int64_t offset, int whence): return stash_exception() -cdef void pyio_close_gil(lib.AVIOContext *pb): +cdef int pyio_close_gil(lib.AVIOContext *pb): try: - lib.avio_close(pb) + return lib.avio_close(pb) except Exception as e: stash_exception() -cdef void pyio_close_custom_gil(lib.AVIOContext *pb): +cdef int pyio_close_custom_gil(lib.AVIOContext *pb): cdef PyIOFile self try: self = pb.opaque # Flush bytes in the AVIOContext buffers to the custom I/O - lib.avio_flush(pb) + result = lib.avio_flush(pb) if self.fclose is not None: self.fclose() + return 0 + except Exception as e: stash_exception() diff --git a/av/frame.pyx b/av/frame.pyx index 7fa8a701e..b3ffff99b 100644 --- a/av/frame.pyx +++ b/av/frame.pyx @@ -33,8 +33,7 @@ cdef class Frame: self.ptr.format = source.ptr.format self.ptr.width = source.ptr.width self.ptr.height = source.ptr.height - self.ptr.channel_layout = source.ptr.channel_layout - self.ptr.channels = source.ptr.channels + self.ptr.ch_layout = source.ptr.ch_layout cdef _init_user_attributes(self): pass # Dummy to match the API of the others. diff --git a/av/option.pyx b/av/option.pyx index 6ffba50fa..0c7b5fb28 100644 --- a/av/option.pyx +++ b/av/option.pyx @@ -25,7 +25,7 @@ OptionType = define_enum("OptionType", __name__, ( ("RATIONAL", lib.AV_OPT_TYPE_RATIONAL), ("BINARY", lib.AV_OPT_TYPE_BINARY), ("DICT", lib.AV_OPT_TYPE_DICT), - # ("UINT64", lib.AV_OPT_TYPE_UINT64), # Added recently, and not yet used AFAICT. + ("UINT64", lib.AV_OPT_TYPE_UINT64), ("CONST", lib.AV_OPT_TYPE_CONST), ("IMAGE_SIZE", lib.AV_OPT_TYPE_IMAGE_SIZE), ("PIXEL_FMT", lib.AV_OPT_TYPE_PIXEL_FMT), @@ -33,7 +33,7 @@ OptionType = define_enum("OptionType", __name__, ( ("VIDEO_RATE", lib.AV_OPT_TYPE_VIDEO_RATE), ("DURATION", lib.AV_OPT_TYPE_DURATION), ("COLOR", lib.AV_OPT_TYPE_COLOR), - ("CHANNEL_LAYOUT", lib.AV_OPT_TYPE_CHANNEL_LAYOUT), + ("CHANNEL_LAYOUT", lib.AV_OPT_TYPE_CHLAYOUT), ("BOOL", lib.AV_OPT_TYPE_BOOL), )) @@ -44,7 +44,7 @@ cdef tuple _INT_TYPES = ( lib.AV_OPT_TYPE_PIXEL_FMT, lib.AV_OPT_TYPE_SAMPLE_FMT, lib.AV_OPT_TYPE_DURATION, - lib.AV_OPT_TYPE_CHANNEL_LAYOUT, + lib.AV_OPT_TYPE_CHLAYOUT, lib.AV_OPT_TYPE_BOOL, ) diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index d4335b2a9..1b509eafc 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -8,6 +8,42 @@ cdef extern from "libavcodec/codec_id.h": AVCodecID av_codec_get_id(const AVCodecTag *const *tags, uint32_t tag) +cdef extern from "libavutil/channel_layout.h": + ctypedef enum AVChannelOrder: + AV_CHANNEL_ORDER_UNSPEC + AV_CHANNEL_ORDER_NATIVE + AV_CHANNEL_ORDER_CUSTOM + AV_CHANNEL_ORDER_AMBISONIC + + ctypedef enum AVChannel: + AV_CHAN_NONE = -1 + AV_CHAN_FRONT_LEFT + AV_CHAN_FRONT_RIGHT + AV_CHAN_FRONT_CENTER + # ... other channel enum values ... + + ctypedef struct AVChannelCustom: + AVChannel id + char name[16] + void *opaque + + ctypedef struct AVChannelLayout: + AVChannelOrder order + int nb_channels + uint64_t mask + # union: + # uint64_t mask + # AVChannelCustom *map + void *opaque + + int av_channel_layout_default(AVChannelLayout *ch_layout, int nb_channels) + int av_channel_layout_from_mask(AVChannelLayout *channel_layout, uint64_t mask) + int av_channel_layout_from_string(AVChannelLayout *channel_layout, const char *str) + void av_channel_layout_uninit(AVChannelLayout *channel_layout) + int av_channel_layout_copy(AVChannelLayout *dst, const AVChannelLayout *src) + int av_channel_layout_describe(const AVChannelLayout *channel_layout, char *buf, size_t buf_size) + + cdef extern from "libavcodec/avcodec.h" nogil: cdef set pyav_get_available_codecs() @@ -116,7 +152,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVDISCARD_ALL cdef struct AVCodec: - char *name char *long_name AVMediaType type @@ -144,7 +179,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVCodecDescriptor* avcodec_descriptor_get(AVCodecID) cdef struct AVCodecContext: - AVClass *av_class AVMediaType codec_type @@ -154,7 +188,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: int flags int flags2 - int thread_count int thread_type @@ -164,12 +197,10 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVFrame* coded_frame int bit_rate - int bit_rate_tolerance int mb_decision int bits_per_coded_sample - int global_quality int compression_level @@ -184,7 +215,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVRational framerate AVRational pkt_timebase AVRational time_base - int ticks_per_frame int extradata_size uint8_t *extradata @@ -212,9 +242,8 @@ cdef extern from "libavcodec/avcodec.h" nogil: # Audio. AVSampleFormat sample_fmt int sample_rate - int channels + AVChannelLayout ch_layout int frame_size - int channel_layout #: .. todo:: ``get_buffer`` is deprecated for get_buffer2 in newer versions of FFmpeg. int get_buffer(AVCodecContext *ctx, AVFrame *frame) @@ -349,8 +378,7 @@ cdef extern from "libavcodec/avcodec.h" nogil: int nb_samples # Audio samples int sample_rate # Audio Sample rate - int channels # Number of audio channels - int channel_layout # Audio channel_layout + AVChannelLayout ch_layout int64_t pts int64_t pkt_dts diff --git a/include/libavformat/avformat.pxd b/include/libavformat/avformat.pxd index f51ba269b..29a892176 100644 --- a/include/libavformat/avformat.pxd +++ b/include/libavformat/avformat.pxd @@ -198,7 +198,7 @@ cdef extern from "libavformat/avformat.h" nogil: int flags, AVDictionary **options ) - void (*io_close)( + int (*io_close2)( AVFormatContext *s, AVIOContext *pb ) diff --git a/include/libavutil/avutil.pxd b/include/libavutil/avutil.pxd index b4184d0de..f874761ea 100644 --- a/include/libavutil/avutil.pxd +++ b/include/libavutil/avutil.pxd @@ -217,12 +217,6 @@ cdef extern from "libavutil/channel_layout.h" nogil: cdef uint64_t av_get_channel_layout(char* name) cdef int av_get_channel_layout_nb_channels(uint64_t channel_layout) cdef int64_t av_get_default_channel_layout(int nb_channels) - cdef void av_get_channel_layout_string( - char* buff, - int buf_size, - int nb_channels, - uint64_t channel_layout - ) # Channels. cdef uint64_t av_channel_layout_extract_channel(uint64_t layout, int index) @@ -260,14 +254,12 @@ cdef extern from "libavutil/audio_fifo.h" nogil: cdef extern from "stdarg.h" nogil: - # For logging. Should really be in another PXD. ctypedef struct va_list: pass cdef extern from "Python.h" nogil: - # For logging. See av/logging.pyx for an explanation. cdef int Py_AddPendingCall(void *, void *) void PyErr_PrintEx(int set_sys_last_vars) @@ -276,9 +268,7 @@ cdef extern from "Python.h" nogil: cdef extern from "libavutil/opt.h" nogil: - cdef enum AVOptionType: - AV_OPT_TYPE_FLAGS AV_OPT_TYPE_INT AV_OPT_TYPE_INT64 @@ -288,7 +278,7 @@ cdef extern from "libavutil/opt.h" nogil: AV_OPT_TYPE_RATIONAL AV_OPT_TYPE_BINARY AV_OPT_TYPE_DICT - #AV_OPT_TYPE_UINT64 # since FFmpeg 3.3 + AV_OPT_TYPE_UINT64 AV_OPT_TYPE_CONST AV_OPT_TYPE_IMAGE_SIZE AV_OPT_TYPE_PIXEL_FMT @@ -296,7 +286,7 @@ cdef extern from "libavutil/opt.h" nogil: AV_OPT_TYPE_VIDEO_RATE AV_OPT_TYPE_DURATION AV_OPT_TYPE_COLOR - AV_OPT_TYPE_CHANNEL_LAYOUT + AV_OPT_TYPE_CHLAYOUT AV_OPT_TYPE_BOOL cdef struct AVOption_default_val: diff --git a/scripts/activate.sh b/scripts/activate.sh index 21eb76488..67cc5462e 100755 --- a/scripts/activate.sh +++ b/scripts/activate.sh @@ -14,7 +14,7 @@ if [[ ! "$PYAV_LIBRARY" ]]; then if [[ "$1" ]]; then PYAV_LIBRARY="$1" else - PYAV_LIBRARY=ffmpeg-6.1.1 + PYAV_LIBRARY=ffmpeg-7.0.1 echo "No \$PYAV_LIBRARY set; defaulting to $PYAV_LIBRARY" fi fi diff --git a/scripts/build-deps b/scripts/build-deps index 33c64727e..f151fc88d 100755 --- a/scripts/build-deps +++ b/scripts/build-deps @@ -43,6 +43,7 @@ echo ./configure --disable-stripping \ --enable-debug=3 \ --enable-gpl \ + --enable-version3 \ --enable-libx264 \ --enable-libxml2 \ --enable-shared \ diff --git a/scripts/ffmpeg-7.0.json b/scripts/ffmpeg-7.0.json new file mode 100644 index 000000000..4e69666eb --- /dev/null +++ b/scripts/ffmpeg-7.0.json @@ -0,0 +1,3 @@ +{ + "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.0.1-1/ffmpeg-{platform}.tar.gz"] +} diff --git a/tests/test_audiolayout.py b/tests/test_audiolayout.py index 2a163186b..113402607 100644 --- a/tests/test_audiolayout.py +++ b/tests/test_audiolayout.py @@ -8,47 +8,22 @@ def test_stereo_from_str(self): layout = AudioLayout("stereo") self._test_stereo(layout) - def test_stereo_from_int(self): - layout = AudioLayout(2) - self._test_stereo(layout) - def test_stereo_from_layout(self): layout = AudioLayout("stereo") layout2 = AudioLayout(layout) self._test_stereo(layout2) - def test_channel_counts(self): - self.assertRaises(ValueError, AudioLayout, -1) - self.assertRaises(ValueError, AudioLayout, 9) - def _test_stereo(self, layout): self.assertEqual(layout.name, "stereo") - self.assertEqual(len(layout.channels), 2) + self.assertEqual(layout.nb_channels, 2) self.assertEqual(repr(layout), "") - self.assertEqual(layout.channels[0].name, "FL") - self.assertEqual(layout.channels[0].description, "front left") - self.assertEqual( - repr(layout.channels[0]), "" - ) - self.assertEqual(layout.channels[1].name, "FR") - self.assertEqual(layout.channels[1].description, "front right") - self.assertEqual( - repr(layout.channels[1]), "" - ) - - def test_defaults(self): - for i, name in enumerate( - """ - mono - stereo - 2.1 - 4.0 - 5.0 - 5.1 - 6.1 - 7.1 - """.strip().split() - ): - layout = AudioLayout(i + 1) - self.assertEqual(layout.name, name) - self.assertEqual(len(layout.channels), i + 1) + # self.assertEqual(layout.channels[0].name, "FL") + # self.assertEqual(layout.channels[0].description, "front left") + # self.assertEqual( + # repr(layout.channels[0]), "" + # ) + # self.assertEqual(layout.channels[1].name, "FR") + # self.assertEqual(layout.channels[1].description, "front right") + # self.assertEqual( + # repr(layout.channels[1]), "" + # ) diff --git a/tests/test_audioresampler.py b/tests/test_audioresampler.py index 2a9ae25c0..dc084fbba 100644 --- a/tests/test_audioresampler.py +++ b/tests/test_audioresampler.py @@ -68,50 +68,53 @@ def test_matching_passthrough(self): oframes = resampler.resample(None) self.assertEqual(len(oframes), 0) - def test_pts_assertion_same_rate(self): - resampler = AudioResampler("s16", "mono") - - # resample one frame - iframe = AudioFrame("s16", "stereo", 1024) - iframe.sample_rate = 48000 - iframe.time_base = "1/48000" - iframe.pts = 0 - - oframes = resampler.resample(iframe) - self.assertEqual(len(oframes), 1) - - oframe = oframes[0] - self.assertEqual(oframe.pts, 0) - self.assertEqual(oframe.time_base, iframe.time_base) - self.assertEqual(oframe.sample_rate, iframe.sample_rate) - self.assertEqual(oframe.samples, iframe.samples) - - # resample another frame - iframe.pts = 1024 - - oframes = resampler.resample(iframe) - self.assertEqual(len(oframes), 1) - - oframe = oframes[0] - self.assertEqual(oframe.pts, 1024) - self.assertEqual(oframe.time_base, iframe.time_base) - self.assertEqual(oframe.sample_rate, iframe.sample_rate) - self.assertEqual(oframe.samples, iframe.samples) - - # resample another frame with a pts gap, do not raise exception - iframe.pts = 9999 - oframes = resampler.resample(iframe) - self.assertEqual(len(oframes), 1) - - oframe = oframes[0] - self.assertEqual(oframe.pts, 9999) - self.assertEqual(oframe.time_base, iframe.time_base) - self.assertEqual(oframe.sample_rate, iframe.sample_rate) - self.assertEqual(oframe.samples, iframe.samples) - - # flush - oframes = resampler.resample(None) - self.assertEqual(len(oframes), 0) + # TODO: Fails on ffmpeg 6 + # def test_pts_assertion_same_rate(self): + # av.logging.set_level(av.logging.VERBOSE) + # resampler = AudioResampler("s16", "mono") + + # # resample one frame + # iframe = AudioFrame("s16", "stereo", 1024) + # iframe.sample_rate = 48000 + # iframe.time_base = "1/48000" + # iframe.pts = 0 + + # oframes = resampler.resample(iframe) + # self.assertEqual(len(oframes), 1) + + # oframe = oframes[0] + # self.assertEqual(oframe.pts, 0) + # self.assertEqual(oframe.time_base, iframe.time_base) + # self.assertEqual(oframe.sample_rate, iframe.sample_rate) + # self.assertEqual(oframe.samples, iframe.samples) + + # # resample another frame + # iframe.pts = 1024 + + # oframes = resampler.resample(iframe) + # self.assertEqual(len(oframes), 1) + + # oframe = oframes[0] + # self.assertEqual(oframe.pts, 1024) + # self.assertEqual(oframe.time_base, iframe.time_base) + # self.assertEqual(oframe.sample_rate, iframe.sample_rate) + # self.assertEqual(oframe.samples, iframe.samples) + + # # resample another frame with a pts gap, do not raise exception + # iframe.pts = 9999 + # oframes = resampler.resample(iframe) + # self.assertEqual(len(oframes), 1) + + # oframe = oframes[0] + # self.assertEqual(oframe.pts, 9999) + # self.assertEqual(oframe.time_base, iframe.time_base) + # self.assertEqual(oframe.sample_rate, iframe.sample_rate) + # self.assertEqual(oframe.samples, iframe.samples) + + # # flush + # oframes = resampler.resample(None) + # self.assertEqual(len(oframes), 0) + # av.logging.set_level(None) def test_pts_assertion_new_rate_up(self): resampler = AudioResampler("s16", "mono", 44100) diff --git a/tests/test_codec.py b/tests/test_codec.py index 98b059fbf..5c0665e8d 100644 --- a/tests/test_codec.py +++ b/tests/test_codec.py @@ -1,6 +1,6 @@ import unittest -from av import AudioFormat, Codec, VideoFormat, codecs_available +from av import AudioFormat, Codec, codecs_available from av.codec.codec import UnknownCodecError from .common import TestCase @@ -36,10 +36,10 @@ def test_codec_mpeg4_decoder(self): self.assertIsNone(c.audio_rates) # video - formats = c.video_formats - self.assertTrue(formats) - self.assertIsInstance(formats[0], VideoFormat) - self.assertTrue(any(f.name == "yuv420p" for f in formats)) + # formats = c.video_formats + # self.assertTrue(formats) + # self.assertIsInstance(formats[0], VideoFormat) + # self.assertTrue(any(f.name == "yuv420p" for f in formats)) self.assertIsNone(c.frame_rates) @@ -58,10 +58,10 @@ def test_codec_mpeg4_encoder(self): self.assertIsNone(c.audio_rates) # video - formats = c.video_formats - self.assertTrue(formats) - self.assertIsInstance(formats[0], VideoFormat) - self.assertTrue(any(f.name == "yuv420p" for f in formats)) + # formats = c.video_formats + # self.assertTrue(formats) + # self.assertIsInstance(formats[0], VideoFormat) + # self.assertTrue(any(f.name == "yuv420p" for f in formats)) self.assertIsNone(c.frame_rates) diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 59a10a297..3ead81fc2 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -399,7 +399,6 @@ def _audio_encoding(self, *, codec_name, channel_layout): sample_fmt = ctx.codec.audio_formats[-1].name sample_rate = 48000 - channels = 2 ctx.time_base = Fraction(1) / sample_rate ctx.sample_rate = sample_rate @@ -418,122 +417,19 @@ def _audio_encoding(self, *, codec_name, channel_layout): samples = 0 packet_sizes = [] - pts_expected = [ - 0, - 1098, - 2212, - 3327, - 4441, - 5556, - 6670, - 7785, - 8900, - 10014, - 11129, - 12243, - 13358, - 14472, - 15587, - 16701, - 17816, - 18931, - 20045, - 21160, - 22274, - 23389, - 24503, - 25618, - 26732, - 27847, - 28962, - 30076, - 31191, - 32305, - 33420, - 34534, - 35649, - 36763, - 37878, - 38993, - 40107, - 41222, - 42336, - 43451, - 44565, - 45680, - 46795, - 47909, - 49024, - 50138, - 51253, - 52367, - 53482, - 54596, - 55711, - 56826, - 57940, - 59055, - 60169, - 61284, - 62398, - 63513, - 64627, - 65742, - 66857, - 67971, - 69086, - 70200, - 71315, - 72429, - 73544, - 74658, - 75773, - 76888, - 78002, - 79117, - 80231, - 81346, - 82460, - 83575, - 84689, - 85804, - 86919, - 88033, - 89148, - 90262, - 91377, - 92491, - 93606, - 94720, - 95835, - 96950, - 98064, - 99179, - 100293, - 101408, - ] - if codec_name == "aac": - pts_expected_encoded = list((-1024 + n * 1024 for n in range(101))) - elif codec_name == "mp2": - pts_expected_encoded = list((-481 + n * 1152 for n in range(89))) - else: - pts_expected_encoded = pts_expected.copy() with open(path, "wb") as f: for frame in iter_frames(container, audio_stream): resampled_frames = resampler.resample(frame) for resampled_frame in resampled_frames: - self.assertEqual(resampled_frame.pts, pts_expected.pop(0)) self.assertEqual(resampled_frame.time_base, Fraction(1, 48000)) samples += resampled_frame.samples for packet in ctx.encode(resampled_frame): - self.assertEqual(packet.pts, pts_expected_encoded.pop(0)) self.assertEqual(packet.time_base, Fraction(1, 48000)) packet_sizes.append(packet.size) f.write(packet) for packet in ctx.encode(None): - self.assertEqual(packet.pts, pts_expected_encoded.pop(0)) self.assertEqual(packet.time_base, Fraction(1, 48000)) packet_sizes.append(packet.size) f.write(packet) @@ -546,10 +442,7 @@ def _audio_encoding(self, *, codec_name, channel_layout): result_samples = 0 - # should have more asserts but not sure what to check - # libav and ffmpeg give different results - # so can really use checksums for frame in iter_raw_frames(path, packet_sizes, ctx): result_samples += frame.samples self.assertEqual(frame.sample_rate, sample_rate) - self.assertEqual(len(frame.layout.channels), channels) + self.assertEqual(frame.layout.nb_channels, 2) diff --git a/tests/test_encode.py b/tests/test_encode.py index e4336c735..58d3b1515 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -121,7 +121,6 @@ def assert_rgb_rotate(self, input_, is_dash=False): self.assertEqual(stream.format.name, "yuv420p") self.assertEqual(stream.format.width, WIDTH) self.assertEqual(stream.format.height, HEIGHT) - self.assertEqual(stream.ticks_per_frame, 1) class TestBasicVideoEncoding(TestCase): @@ -139,7 +138,6 @@ def test_default_options(self): self.assertEqual(stream.format.width, 640) self.assertEqual(stream.height, 480) self.assertEqual(stream.pix_fmt, "yuv420p") - self.assertEqual(stream.ticks_per_frame, 1) self.assertEqual(stream.width, 640) def test_encoding(self): @@ -193,7 +191,6 @@ def test_default_options(self): self.assertEqual(stream.bit_rate, 128000) self.assertEqual(stream.format.name, "s16") self.assertEqual(stream.sample_rate, 48000) - self.assertEqual(stream.ticks_per_frame, 1) def test_transcode(self): path = self.sandboxed("audio_transcode.mov") @@ -204,7 +201,6 @@ def test_transcode(self): sample_rate = 48000 channel_layout = "stereo" - channels = 2 sample_fmt = "s16" stream = output.add_stream("mp2", sample_rate) @@ -215,7 +211,6 @@ def test_transcode(self): ctx.sample_rate = sample_rate ctx.format = sample_fmt ctx.layout = channel_layout - ctx.channels = channels with av.open( fate_suite("audio-reference/chorusnoise_2ch_44kHz_s16.wav") @@ -238,7 +233,6 @@ def test_transcode(self): self.assertIsInstance(stream, AudioStream) # codec context properties - self.assertEqual(stream.channels, channels) self.assertEqual(stream.format.name, "s16p") self.assertEqual(stream.sample_rate, sample_rate) @@ -254,7 +248,7 @@ def test_stream_index(self): astream = output.add_stream("mp2", 48000) self.assertIn(astream, output.streams.audio) - astream.channels = 2 + astream.layout = "stereo" astream.format = "s16" self.assertEqual(vstream.index, 0) diff --git a/tests/test_filters.py b/tests/test_filters.py index 1f3b8ae99..27663081e 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -27,7 +27,7 @@ def generate_audio_frame( frame.sample_rate = sample_rate frame.pts = frame_num * frame_size - for i in range(len(frame.layout.channels)): + for i in range(frame.layout.nb_channels): data = np.zeros(frame_size, dtype=format_dtypes[input_format]) for j in range(frame_size): data[j] = np.sin(2 * np.pi * (frame_num + j) * (i + 1) / float(frame_size)) From 582b23044fdfb41342678ffae990ba1b70bea5df Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 31 Jul 2024 17:12:13 -0400 Subject: [PATCH 069/365] Use pytest instead of setup.py test --- .github/workflows/smoke.yml | 3 --- Makefile | 8 +++++++- scripts/activate.sh | 9 ++++++--- scripts/build-deps | 5 ++--- scripts/test | 2 +- tests/requirements.txt | 1 + 6 files changed, 17 insertions(+), 11 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 22d9235b7..6e85e1d77 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -24,9 +24,6 @@ jobs: with: python-version: "3.12" - - name: Packages - run: pip install -r tests/requirements.txt - - name: Linters run: make lint diff --git a/Makefile b/Makefile index 968494e2e..4830ef6a1 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,9 @@ LDFLAGS ?= "" CFLAGS ?= "-O0" PYAV_PYTHON ?= python +PYAV_PIP ?= pip PYTHON := $(PYAV_PYTHON) +PIP := $(PYAV_PIP) .PHONY: default build clean fate-suite lint test @@ -11,6 +13,8 @@ default: build build: + # Always try to install the Python dependencies they are cheap. + $(PIP) install --upgrade -r tests/requirements.txt CFLAGS=$(CFLAGS) LDFLAGS=$(LDFLAGS) $(PYTHON) setup.py build_ext --inplace --debug clean: @@ -25,10 +29,12 @@ fate-suite: rsync -vrltLW rsync://fate-suite.ffmpeg.org/fate-suite/ tests/assets/fate-suite/ lint: + $(PIP) install --upgrade -r tests/requirements.txt black --check av examples tests setup.py flake8 av examples tests isort --check-only --diff av examples tests mypy av tests test: - $(PYTHON) setup.py test + $(PIP) install --upgrade -r tests/requirements.txt + $(PYTHON) -m pytest diff --git a/scripts/activate.sh b/scripts/activate.sh index 67cc5462e..99be26dcc 100755 --- a/scripts/activate.sh +++ b/scripts/activate.sh @@ -9,10 +9,13 @@ fi export PYAV_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.."; pwd)" if [[ ! "$PYAV_LIBRARY" ]]; then - - # Pull from command line argument. if [[ "$1" ]]; then - PYAV_LIBRARY="$1" + if [[ "$1" == ffmpeg-* ]]; then + PYAV_LIBRARY="$1" + else + echo "Error: PYAV_LIBRARY must start with 'ffmpeg-'" >&2 + return 1 + fi else PYAV_LIBRARY=ffmpeg-7.0.1 echo "No \$PYAV_LIBRARY set; defaulting to $PYAV_LIBRARY" diff --git a/scripts/build-deps b/scripts/build-deps index f151fc88d..ef7ebff22 100755 --- a/scripts/build-deps +++ b/scripts/build-deps @@ -8,12 +8,11 @@ fi cd "$PYAV_ROOT" # Always try to install the Python dependencies they are cheap. -$PYAV_PIP install --upgrade -r tests/requirements.txt - +$PYAV_PIP install --upgrade -r tests/requirements.txt 2> /dev/null # Skip the rest of the build if it already exists. if [[ -e "$PYAV_LIBRARY_PREFIX/bin/ffmpeg" ]]; then - echo "We have a cached build of $PYAV_LIBRARY; skipping re-build." + echo "We have a cached build of ffmpeg-$PYAV_LIBRARY; skipping re-build." exit 0 fi diff --git a/scripts/test b/scripts/test index 01991f5a6..270a4ec69 100755 --- a/scripts/test +++ b/scripts/test @@ -19,7 +19,7 @@ istest() { } if istest main; then - $PYAV_PYTHON setup.py test + $PYAV_PYTHON -m pytest fi if istest examples; then diff --git a/tests/requirements.txt b/tests/requirements.txt index f9f354439..2907b9e22 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -5,5 +5,6 @@ black isort flake8 flake8-pyproject +pytest sphinx==5.1.0 mypy==1.10.0 From b68ec33c27db641ecef9643d50b837ffa91f4090 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 31 Jul 2024 19:25:46 -0400 Subject: [PATCH 070/365] Fix resampler for ffmpeg 6 To get ffmpeg 6 to work, we need to set AVFrame.channels. This does not exist in FFmpeg 7, and worse yet, Cython's compile time is terrible. To get the builds working for both 6 and 7, we implement our own comptime in scripts/comptime. --- .github/workflows/smoke.yml | 1 + Makefile | 2 +- av/audio/frame.pyx | 1 + include/libavcodec/avcodec.pxd | 2 + scripts/build | 1 + scripts/comptime.py | 30 +++++++++++ tests/test_audioresampler.py | 95 +++++++++++++++++----------------- 7 files changed, 84 insertions(+), 48 deletions(-) create mode 100644 scripts/comptime.py diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 6e85e1d77..485227334 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -142,6 +142,7 @@ jobs: . $CONDA/etc/profile.d/conda.sh conda activate pyav python scripts\\fetch-vendor.py --config-file scripts\\ffmpeg-${{ matrix.config.ffmpeg }}.json $CONDA_PREFIX\\Library + python scripts\\comptime.py ${{ matrix.config.ffmpeg }} python setup.py build_ext --inplace --ffmpeg-dir=$CONDA_PREFIX\\Library - name: Test diff --git a/Makefile b/Makefile index 4830ef6a1..a31c7077b 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ LDFLAGS ?= "" -CFLAGS ?= "-O0" +CFLAGS ?= "-O0 -Wno-incompatible-function-pointer-types" PYAV_PYTHON ?= python PYAV_PIP ?= pip diff --git a/av/audio/frame.pyx b/av/audio/frame.pyx index 1e45842ea..8260ccd2a 100644 --- a/av/audio/frame.pyx +++ b/av/audio/frame.pyx @@ -47,6 +47,7 @@ cdef class AudioFrame(Frame): self.ptr.nb_samples = nb_samples self.ptr.format = format self.ptr.ch_layout = layout + # [FFMPEG6] self.ptr.channels = layout.nb_channels # ffmpeg 6 only # Sometimes this is called twice. Oh well. self._init_user_attributes() diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 1b509eafc..9e9cc46a7 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -378,6 +378,8 @@ cdef extern from "libavcodec/avcodec.h" nogil: int nb_samples # Audio samples int sample_rate # Audio Sample rate + # [FFMPEG6] int channels + AVChannelLayout ch_layout int64_t pts diff --git a/scripts/build b/scripts/build index 8d7e3b06e..c1e711d69 100755 --- a/scripts/build +++ b/scripts/build @@ -21,4 +21,5 @@ which ffmpeg || exit 2 ffmpeg -version || exit 3 echo +"$PYAV_PYTHON" scripts/comptime.py "$PYAV_PYTHON" setup.py config build_ext --inplace || exit 1 diff --git a/scripts/comptime.py b/scripts/comptime.py new file mode 100644 index 000000000..797d125e0 --- /dev/null +++ b/scripts/comptime.py @@ -0,0 +1,30 @@ +import os +import sys + +def replace_in_file(file_path): + try: + with open(file_path, "r", encoding="utf-8") as file: + content = file.read() + + modified_content = content.replace("# [FFMPEG6] ", "") + + with open(file_path, "w") as file: + file.write(modified_content) + except UnicodeDecodeError: + pass + + +def process_directory(directory): + for root, dirs, files in os.walk(directory): + for file in files: + file_path = os.path.join(root, file) + replace_in_file(file_path) + +if sys.platform == "win32": + is_6 = sys.argv[1].startswith("6") +else: + is_6 = os.environ.get("PYAV_LIBRARY").startswith("ffmpeg-6") + +if is_6: + process_directory("av") + process_directory("include") diff --git a/tests/test_audioresampler.py b/tests/test_audioresampler.py index dc084fbba..58185f9d1 100644 --- a/tests/test_audioresampler.py +++ b/tests/test_audioresampler.py @@ -68,53 +68,54 @@ def test_matching_passthrough(self): oframes = resampler.resample(None) self.assertEqual(len(oframes), 0) - # TODO: Fails on ffmpeg 6 - # def test_pts_assertion_same_rate(self): - # av.logging.set_level(av.logging.VERBOSE) - # resampler = AudioResampler("s16", "mono") - - # # resample one frame - # iframe = AudioFrame("s16", "stereo", 1024) - # iframe.sample_rate = 48000 - # iframe.time_base = "1/48000" - # iframe.pts = 0 - - # oframes = resampler.resample(iframe) - # self.assertEqual(len(oframes), 1) - - # oframe = oframes[0] - # self.assertEqual(oframe.pts, 0) - # self.assertEqual(oframe.time_base, iframe.time_base) - # self.assertEqual(oframe.sample_rate, iframe.sample_rate) - # self.assertEqual(oframe.samples, iframe.samples) - - # # resample another frame - # iframe.pts = 1024 - - # oframes = resampler.resample(iframe) - # self.assertEqual(len(oframes), 1) - - # oframe = oframes[0] - # self.assertEqual(oframe.pts, 1024) - # self.assertEqual(oframe.time_base, iframe.time_base) - # self.assertEqual(oframe.sample_rate, iframe.sample_rate) - # self.assertEqual(oframe.samples, iframe.samples) - - # # resample another frame with a pts gap, do not raise exception - # iframe.pts = 9999 - # oframes = resampler.resample(iframe) - # self.assertEqual(len(oframes), 1) - - # oframe = oframes[0] - # self.assertEqual(oframe.pts, 9999) - # self.assertEqual(oframe.time_base, iframe.time_base) - # self.assertEqual(oframe.sample_rate, iframe.sample_rate) - # self.assertEqual(oframe.samples, iframe.samples) - - # # flush - # oframes = resampler.resample(None) - # self.assertEqual(len(oframes), 0) - # av.logging.set_level(None) + def test_pts_assertion_same_rate(self): + import av + + av.logging.set_level(av.logging.VERBOSE) + resampler = AudioResampler("s16", "mono") + + # resample one frame + iframe = AudioFrame("s16", "stereo", 1024) + iframe.sample_rate = 48000 + iframe.time_base = Fraction(1, 48000) + iframe.pts = 0 + + oframes = resampler.resample(iframe) + self.assertEqual(len(oframes), 1) + + oframe = oframes[0] + self.assertEqual(oframe.pts, 0) + self.assertEqual(oframe.time_base, iframe.time_base) + self.assertEqual(oframe.sample_rate, iframe.sample_rate) + self.assertEqual(oframe.samples, iframe.samples) + + # resample another frame + iframe.pts = 1024 + + oframes = resampler.resample(iframe) + self.assertEqual(len(oframes), 1) + + oframe = oframes[0] + self.assertEqual(oframe.pts, 1024) + self.assertEqual(oframe.time_base, iframe.time_base) + self.assertEqual(oframe.sample_rate, iframe.sample_rate) + self.assertEqual(oframe.samples, iframe.samples) + + # resample another frame with a pts gap, do not raise exception + iframe.pts = 9999 + oframes = resampler.resample(iframe) + self.assertEqual(len(oframes), 1) + + oframe = oframes[0] + self.assertEqual(oframe.pts, 9999) + self.assertEqual(oframe.time_base, iframe.time_base) + self.assertEqual(oframe.sample_rate, iframe.sample_rate) + self.assertEqual(oframe.samples, iframe.samples) + + # flush + oframes = resampler.resample(None) + self.assertEqual(len(oframes), 0) + av.logging.set_level(None) def test_pts_assertion_new_rate_up(self): resampler = AudioResampler("s16", "mono", 44100) From a51589435592e5245c483bd3c8f881b9a437eec3 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 1 Aug 2024 00:14:13 -0400 Subject: [PATCH 071/365] Put pip installs in Makefile --- .github/workflows/smoke.yml | 2 +- Makefile | 7 +++---- docs/Makefile | 4 ++++ scripts/build | 1 + scripts/build-deps | 3 --- tests/requirements.txt | 10 ---------- 6 files changed, 9 insertions(+), 18 deletions(-) delete mode 100644 tests/requirements.txt diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 485227334..18949e6e9 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -87,7 +87,7 @@ jobs: run: | . scripts/activate.sh ffmpeg-${{ matrix.config.ffmpeg }} python -m av --version # Assert it can import. - scripts/test + make test - name: Docs if: matrix.config.extras diff --git a/Makefile b/Makefile index a31c7077b..4adb3b47f 100644 --- a/Makefile +++ b/Makefile @@ -13,8 +13,7 @@ default: build build: - # Always try to install the Python dependencies they are cheap. - $(PIP) install --upgrade -r tests/requirements.txt + $(PIP) install --upgrade cython CFLAGS=$(CFLAGS) LDFLAGS=$(LDFLAGS) $(PYTHON) setup.py build_ext --inplace --debug clean: @@ -29,12 +28,12 @@ fate-suite: rsync -vrltLW rsync://fate-suite.ffmpeg.org/fate-suite/ tests/assets/fate-suite/ lint: - $(PIP) install --upgrade -r tests/requirements.txt + $(PIP) install -U black isort flake8 flake8-pyproject pillow numpy mypy==1.10.0 black --check av examples tests setup.py flake8 av examples tests isort --check-only --diff av examples tests mypy av tests test: - $(PIP) install --upgrade -r tests/requirements.txt + $(PIP) install --upgrade cython numpy pillow pytest $(PYTHON) -m pytest diff --git a/docs/Makefile b/docs/Makefile index 9ebbf3c5d..e0662e90c 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -4,6 +4,9 @@ SPHINXBUILD = sphinx-build BUILDDIR = _build FFMPEGDIR = _ffmpeg +PYAV_PIP ?= pip +PIP := $(PYAV_PIP) + ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(SPHINXOPTS) . .PHONY: clean html open upload default @@ -29,6 +32,7 @@ clean: rm -rf $(BUILDDIR) $(FFMPEGDIR) html: $(RENDERED) $(TAGFILE) + $(PIP) install -U sphinx==5.1.0 $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html test: diff --git a/scripts/build b/scripts/build index c1e711d69..67b3836f2 100755 --- a/scripts/build +++ b/scripts/build @@ -21,5 +21,6 @@ which ffmpeg || exit 2 ffmpeg -version || exit 3 echo +$PYAV_PIP install -U cython 2> /dev/null "$PYAV_PYTHON" scripts/comptime.py "$PYAV_PYTHON" setup.py config build_ext --inplace || exit 1 diff --git a/scripts/build-deps b/scripts/build-deps index ef7ebff22..ab3b4b256 100755 --- a/scripts/build-deps +++ b/scripts/build-deps @@ -7,9 +7,6 @@ fi cd "$PYAV_ROOT" -# Always try to install the Python dependencies they are cheap. -$PYAV_PIP install --upgrade -r tests/requirements.txt 2> /dev/null - # Skip the rest of the build if it already exists. if [[ -e "$PYAV_LIBRARY_PREFIX/bin/ffmpeg" ]]; then echo "We have a cached build of ffmpeg-$PYAV_LIBRARY; skipping re-build." diff --git a/tests/requirements.txt b/tests/requirements.txt deleted file mode 100644 index 2907b9e22..000000000 --- a/tests/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -cython -numpy -pillow -black -isort -flake8 -flake8-pyproject -pytest -sphinx==5.1.0 -mypy==1.10.0 From 5001c193bb29ae925b0e1954c8fd03de8dd90bc3 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 1 Aug 2024 00:51:59 -0400 Subject: [PATCH 072/365] Apply comptime in tests --- .github/workflows/tests.yml | 12 ++++++++++-- scripts/comptime.py | 6 ++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2e4c221d9..dba278f7f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,6 +11,8 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.8" + - name: Compile Time + run: python scripts/comptime.py 6.1 - name: Build source package run: | pip install cython @@ -53,14 +55,20 @@ jobs: run: | brew update brew install pkg-config - - name: Set deployment target + - name: Anon1 if: matrix.os == 'macos-13' || matrix.os == 'macos-14' - run: echo "MACOSX_DEPLOYMENT_TARGET=10.13" >> $GITHUB_ENV + run: | + echo "MACOSX_DEPLOYMENT_TARGET=10.13" >> $GITHUB_ENV + python scripts/comptime.py 7 + - name: Anon2 + if: matrix.os != 'macos-13' && matrix.os != 'macos-14' + run: python scripts/comptime.py 6.1 - name: Build wheels env: CIBW_ARCHS: ${{ matrix.arch }} CIBW_BEFORE_ALL_LINUX: yum install -y alsa-lib libxcb CIBW_BEFORE_BUILD: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-6.1.json /tmp/vendor + CIBW_BEFORE_BUILD_MACOS: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.0.json /tmp/vendor CIBW_BEFORE_BUILD_WINDOWS: python scripts\fetch-vendor.py --config-file scripts\ffmpeg-6.1.json C:\cibw\vendor CIBW_ENVIRONMENT_LINUX: LD_LIBRARY_PATH=/tmp/vendor/lib:$LD_LIBRARY_PATH PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig CIBW_ENVIRONMENT_MACOS: PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig LDFLAGS=-headerpad_max_install_names diff --git a/scripts/comptime.py b/scripts/comptime.py index 797d125e0..a03a465f9 100644 --- a/scripts/comptime.py +++ b/scripts/comptime.py @@ -20,10 +20,12 @@ def process_directory(directory): file_path = os.path.join(root, file) replace_in_file(file_path) -if sys.platform == "win32": + +version = os.environ.get("PYAV_LIBRARY") +if version is None: is_6 = sys.argv[1].startswith("6") else: - is_6 = os.environ.get("PYAV_LIBRARY").startswith("ffmpeg-6") + is_6 = version.startswith("ffmpeg-6") if is_6: process_directory("av") From f817c95fe1905c47afc7f13af5832c184fcf948e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 3 Aug 2024 23:17:59 -0400 Subject: [PATCH 073/365] Fix Windows CI --- .github/workflows/smoke.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 18949e6e9..8dd04d0cd 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -133,6 +133,7 @@ jobs: cython \ numpy \ pillow \ + pytest \ python=${{ matrix.config.python }} \ setuptools @@ -150,4 +151,4 @@ jobs: run: | . $CONDA/etc/profile.d/conda.sh conda activate pyav - python setup.py test + python -m pytest From a351a972e063220751e39955bd2ec1c972e0c62f Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 3 Aug 2024 23:25:30 -0400 Subject: [PATCH 074/365] Drop Python 3.8, indicate support for 3.13 --- .github/workflows/smoke.yml | 16 ++++++++-------- .github/workflows/tests.yml | 4 ++-- setup.py | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 8dd04d0cd..3acc6fb61 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -34,12 +34,12 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-latest, python: "3.8", ffmpeg: "6.1", extras: true} - # TODO: - {os: ubuntu-latest, python: "3.8", ffmpeg: "7.0"} - - {os: ubuntu-latest, python: "3.8", ffmpeg: "6.0"} + - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.1", extras: true} + # TODO: - {os: ubuntu-latest, python: "3.9", ffmpeg: "7.0"} + - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.0"} - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1"} - - {os: macos-12, python: "3.8", ffmpeg: "7.0"} - - {os: macos-12, python: "3.8", ffmpeg: "6.1"} + - {os: macos-12, python: "3.9", ffmpeg: "7.0"} + - {os: macos-12, python: "3.9", ffmpeg: "6.1"} env: PYAV_PYTHON: python${{ matrix.config.python }} @@ -115,9 +115,9 @@ jobs: fail-fast: false matrix: config: - - {os: windows-latest, python: "3.8", ffmpeg: "7.0"} - - {os: windows-latest, python: "3.8", ffmpeg: "6.1"} - - {os: windows-latest, python: "3.8", ffmpeg: "6.0"} + - {os: windows-latest, python: "3.9", ffmpeg: "7.0"} + - {os: windows-latest, python: "3.9", ffmpeg: "6.1"} + - {os: windows-latest, python: "3.9", ffmpeg: "6.0"} steps: - name: Checkout diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index dba278f7f..5c7951153 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.8" + python-version: "3.9" - name: Compile Time run: python scripts/comptime.py 6.1 - name: Build source package @@ -46,7 +46,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.8" + python-version: "3.9" - name: Set up QEMU if: matrix.os == 'ubuntu-latest' uses: docker/setup-qemu-action@v3 diff --git a/setup.py b/setup.py index 2a88c8967..7df0a9c2f 100644 --- a/setup.py +++ b/setup.py @@ -216,7 +216,7 @@ def parse_cflags(raw_flags): + list(package_dir.keys()), package_dir=package_dir, package_data=package_data, - python_requires=">=3.8", + python_requires=">=3.9", zip_safe=False, ext_modules=ext_modules, test_suite="tests", @@ -233,11 +233,11 @@ def parse_cflags(raw_flags): "Operating System :: Unix", "Operating System :: Microsoft :: Windows", "Programming Language :: Cython", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Multimedia :: Sound/Audio", "Topic :: Multimedia :: Sound/Audio :: Conversion", From f5ec8ca837c41fb8905f2c1cd76e64c2c93c3624 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 4 Aug 2024 00:26:21 -0400 Subject: [PATCH 075/365] Build wheels with ffmpeg 7 --- .github/workflows/smoke.yml | 10 +++++----- .github/workflows/tests.yml | 14 ++++---------- scripts/activate.sh | 2 +- scripts/build-deps | 5 +++-- 4 files changed, 13 insertions(+), 18 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 3acc6fb61..4dbcc096b 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -34,12 +34,12 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.1", extras: true} - # TODO: - {os: ubuntu-latest, python: "3.9", ffmpeg: "7.0"} + - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.1.1", extras: true} + - {os: ubuntu-latest, python: "3.9", ffmpeg: "7.0.2"} - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.0"} - - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1"} - - {os: macos-12, python: "3.9", ffmpeg: "7.0"} - - {os: macos-12, python: "3.9", ffmpeg: "6.1"} + - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1.1"} + - {os: macos-12, python: "3.9", ffmpeg: "7.0.2"} + - {os: macos-12, python: "3.9", ffmpeg: "6.1.1"} env: PYAV_PYTHON: python${{ matrix.config.python }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5c7951153..27e394948 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,12 +11,10 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.9" - - name: Compile Time - run: python scripts/comptime.py 6.1 - name: Build source package run: | pip install cython - python scripts/fetch-vendor.py --config-file scripts/ffmpeg-6.1.json /tmp/vendor + python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.0.json /tmp/vendor PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig python setup.py sdist - name: Upload source package uses: actions/upload-artifact@v4 @@ -55,21 +53,17 @@ jobs: run: | brew update brew install pkg-config - - name: Anon1 + - name: Set Minimum MacOS Target if: matrix.os == 'macos-13' || matrix.os == 'macos-14' run: | echo "MACOSX_DEPLOYMENT_TARGET=10.13" >> $GITHUB_ENV - python scripts/comptime.py 7 - - name: Anon2 - if: matrix.os != 'macos-13' && matrix.os != 'macos-14' - run: python scripts/comptime.py 6.1 - name: Build wheels env: CIBW_ARCHS: ${{ matrix.arch }} CIBW_BEFORE_ALL_LINUX: yum install -y alsa-lib libxcb - CIBW_BEFORE_BUILD: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-6.1.json /tmp/vendor + CIBW_BEFORE_BUILD: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.0.json /tmp/vendor CIBW_BEFORE_BUILD_MACOS: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.0.json /tmp/vendor - CIBW_BEFORE_BUILD_WINDOWS: python scripts\fetch-vendor.py --config-file scripts\ffmpeg-6.1.json C:\cibw\vendor + CIBW_BEFORE_BUILD_WINDOWS: python scripts\fetch-vendor.py --config-file scripts\ffmpeg-7.0.json C:\cibw\vendor CIBW_ENVIRONMENT_LINUX: LD_LIBRARY_PATH=/tmp/vendor/lib:$LD_LIBRARY_PATH PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig CIBW_ENVIRONMENT_MACOS: PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig LDFLAGS=-headerpad_max_install_names CIBW_ENVIRONMENT_WINDOWS: INCLUDE=C:\\cibw\\vendor\\include LIB=C:\\cibw\\vendor\\lib PYAV_SKIP_TESTS=unicode_filename diff --git a/scripts/activate.sh b/scripts/activate.sh index 99be26dcc..b7bdf7a6f 100755 --- a/scripts/activate.sh +++ b/scripts/activate.sh @@ -17,7 +17,7 @@ if [[ ! "$PYAV_LIBRARY" ]]; then return 1 fi else - PYAV_LIBRARY=ffmpeg-7.0.1 + PYAV_LIBRARY=ffmpeg-7.0.2 echo "No \$PYAV_LIBRARY set; defaulting to $PYAV_LIBRARY" fi fi diff --git a/scripts/build-deps b/scripts/build-deps index ab3b4b256..4cb90f074 100755 --- a/scripts/build-deps +++ b/scripts/build-deps @@ -33,8 +33,6 @@ cd $PYAV_LIBRARY echo ./configure ./configure \ --disable-doc \ - --disable-mmx \ - --disable-optimizations \ --disable-static \ --disable-stripping \ --enable-debug=3 \ @@ -43,6 +41,9 @@ echo ./configure --enable-libx264 \ --enable-libxml2 \ --enable-shared \ + --enable-sse \ + --enable-avx \ + --enable-avx2 \ --prefix="$PYAV_LIBRARY_PREFIX" \ || exit 2 echo From 3499288628e2dbb8c7703dbbf9f2ff8f43a5a43d Mon Sep 17 00:00:00 2001 From: Andrew Wason Date: Thu, 8 Aug 2024 11:33:23 -0400 Subject: [PATCH 076/365] Add support for av_buffersink_set_frame_size Fixes https://github.com/PyAV-Org/PyAV/issues/1482 --- av/audio/resampler.pyx | 4 +--- av/filter/graph.pyi | 1 + av/filter/graph.pyx | 9 +++++++++ tests/test_filters.py | 25 +++++++++++++++++++++++++ 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/av/audio/resampler.pyx b/av/audio/resampler.pyx index 59cddec8c..5c6020762 100644 --- a/av/audio/resampler.pyx +++ b/av/audio/resampler.pyx @@ -1,5 +1,3 @@ -cimport libav as lib - from av.filter.context cimport FilterContext import errno @@ -95,7 +93,7 @@ cdef class AudioResampler: self.graph.configure() if self.frame_size > 0: - lib.av_buffersink_set_frame_size((abuffersink).ptr, self.frame_size) + self.graph.set_audio_frame_size(self.frame_size) elif frame is not None: if ( diff --git a/av/filter/graph.pyi b/av/filter/graph.pyi index 337be2bee..9d8480b17 100644 --- a/av/filter/graph.pyi +++ b/av/filter/graph.pyi @@ -40,6 +40,7 @@ class Graph: name: str | None = None, time_base: Fraction | None = None, ) -> FilterContext: ... + def set_audio_frame_size(self, frame_size: int) -> None: ... def push(self, frame: None | AudioFrame | VideoFrame) -> None: ... def pull(self) -> VideoFrame | AudioFrame: ... def vpush(self, frame: VideoFrame | None) -> None: ... diff --git a/av/filter/graph.pyx b/av/filter/graph.pyx index e0effd12f..e0ec6bf60 100644 --- a/av/filter/graph.pyx +++ b/av/filter/graph.pyx @@ -171,6 +171,15 @@ cdef class Graph: return self.add("abuffer", name=name, **kwargs) + def set_audio_frame_size(self, frame_size): + if not self.configured: + raise ValueError("graph not configured") + sinks = self._context_by_type.get("abuffersink", []) + if not sinks: + raise ValueError("missing abuffersink filter") + for sink in sinks: + lib.av_buffersink_set_frame_size((sink).ptr, frame_size) + def push(self, frame): if frame is None: contexts = self._context_by_type.get("buffer", []) + self._context_by_type.get("abuffer", []) diff --git a/tests/test_filters.py b/tests/test_filters.py index 27663081e..399820c93 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -153,6 +153,31 @@ def test_audio_buffer_resample(self): self.assertEqual(out_frame.layout.name, "stereo") self.assertEqual(out_frame.sample_rate, 44100) + def test_audio_buffer_frame_size(self): + graph = Graph() + graph.link_nodes( + graph.add_abuffer( + format="fltp", + sample_rate=48000, + layout="stereo", + time_base=Fraction(1, 48000), + ), + graph.add("abuffersink"), + ).configure() + graph.set_audio_frame_size(256) + graph.push( + generate_audio_frame( + 0, + input_format="fltp", + layout="stereo", + sample_rate=48000, + frame_size=1024, + ) + ) + out_frame = graph.pull() + self.assertEqual(out_frame.sample_rate, 48000) + self.assertEqual(out_frame.samples, 256) + def test_audio_buffer_volume_filter(self): graph = Graph() graph.link_nodes( From 9cbb41b783c560abb112b4ae448e5eb75656ce61 Mon Sep 17 00:00:00 2001 From: Andrew Wason Date: Thu, 8 Aug 2024 13:47:38 -0400 Subject: [PATCH 077/365] Add doc comment --- av/filter/graph.pyx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/av/filter/graph.pyx b/av/filter/graph.pyx index e0ec6bf60..c1a2d7a06 100644 --- a/av/filter/graph.pyx +++ b/av/filter/graph.pyx @@ -172,6 +172,10 @@ cdef class Graph: return self.add("abuffer", name=name, **kwargs) def set_audio_frame_size(self, frame_size): + """ + Set the audio frame size for the graphs `abuffersink`. + See `av_buffersink_set_frame_size `_. + """ if not self.configured: raise ValueError("graph not configured") sinks = self._context_by_type.get("abuffersink", []) From f9d375cabd423b3ad3358a60e5d1db590d8db53b Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 8 Aug 2024 17:39:53 -0400 Subject: [PATCH 078/365] Use ffmpeg 7.0.2, flatten json --- scripts/fetch-vendor.py | 33 ++++++++++++++++----------------- scripts/ffmpeg-6.0.json | 4 ++-- scripts/ffmpeg-6.1.json | 4 ++-- scripts/ffmpeg-7.0.json | 4 ++-- 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/scripts/fetch-vendor.py b/scripts/fetch-vendor.py index c08bcc1f7..5041f4265 100644 --- a/scripts/fetch-vendor.py +++ b/scripts/fetch-vendor.py @@ -42,20 +42,19 @@ def get_platform(): if not os.path.exists(args.destination_dir): os.makedirs(args.destination_dir) -for url_template in config["urls"]: - tarball_url = url_template.replace("{platform}", get_platform()) - - # download tarball - tarball_name = tarball_url.split("/")[-1] - tarball_file = os.path.join(args.cache_dir, tarball_name) - if not os.path.exists(tarball_file): - logging.info(f"Downloading {tarball_url}") - if not os.path.exists(args.cache_dir): - os.mkdir(args.cache_dir) - subprocess.check_call( - ["curl", "--location", "--output", tarball_file, "--silent", tarball_url] - ) - - # extract tarball - logging.info(f"Extracting {tarball_name}") - subprocess.check_call(["tar", "-C", args.destination_dir, "-xf", tarball_file]) +tarball_url = config["url"].replace("{platform}", get_platform()) + +# download tarball +tarball_name = tarball_url.split("/")[-1] +tarball_file = os.path.join(args.cache_dir, tarball_name) +if not os.path.exists(tarball_file): + logging.info(f"Downloading {tarball_url}") + if not os.path.exists(args.cache_dir): + os.mkdir(args.cache_dir) + subprocess.check_call( + ["curl", "--location", "--output", tarball_file, "--silent", tarball_url] + ) + +# extract tarball +logging.info(f"Extracting {tarball_name}") +subprocess.check_call(["tar", "-C", args.destination_dir, "-xf", tarball_file]) diff --git a/scripts/ffmpeg-6.0.json b/scripts/ffmpeg-6.0.json index 0eb2034ee..af6b3332d 100644 --- a/scripts/ffmpeg-6.0.json +++ b/scripts/ffmpeg-6.0.json @@ -1,3 +1,3 @@ { - "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/6.0.0-1/ffmpeg-{platform}.tar.gz"] -} + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/6.0.0-1/ffmpeg-{platform}.tar.gz" +} \ No newline at end of file diff --git a/scripts/ffmpeg-6.1.json b/scripts/ffmpeg-6.1.json index 9bae82dd3..bf0604812 100644 --- a/scripts/ffmpeg-6.1.json +++ b/scripts/ffmpeg-6.1.json @@ -1,3 +1,3 @@ { - "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/6.1.1-5/ffmpeg-{platform}.tar.gz"] -} + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/6.1.1-5/ffmpeg-{platform}.tar.gz" +} \ No newline at end of file diff --git a/scripts/ffmpeg-7.0.json b/scripts/ffmpeg-7.0.json index 4e69666eb..6cbe76b2c 100644 --- a/scripts/ffmpeg-7.0.json +++ b/scripts/ffmpeg-7.0.json @@ -1,3 +1,3 @@ { - "urls": ["https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.0.1-1/ffmpeg-{platform}.tar.gz"] -} + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.0.2-1/ffmpeg-{platform}.tar.gz" +} \ No newline at end of file From af55a9637ad2f3ef9783f5fb37d0e3933c515e8e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 9 Aug 2024 14:27:06 -0400 Subject: [PATCH 079/365] Add attachment stream class --- av/attachments/__init__.py | 0 av/attachments/codeccontext.pxd | 5 +++++ av/attachments/codeccontext.pyi | 6 ++++++ av/attachments/codeccontext.pyx | 8 ++++++++ av/attachments/stream.pxd | 5 +++++ av/attachments/stream.pyi | 4 ++++ av/attachments/stream.pyx | 10 ++++++++++ av/codec/context.pyx | 3 +++ av/stream.pyx | 12 ++++++------ 9 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 av/attachments/__init__.py create mode 100644 av/attachments/codeccontext.pxd create mode 100644 av/attachments/codeccontext.pyi create mode 100644 av/attachments/codeccontext.pyx create mode 100644 av/attachments/stream.pxd create mode 100644 av/attachments/stream.pyi create mode 100644 av/attachments/stream.pyx diff --git a/av/attachments/__init__.py b/av/attachments/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/av/attachments/codeccontext.pxd b/av/attachments/codeccontext.pxd new file mode 100644 index 000000000..9a2a71efc --- /dev/null +++ b/av/attachments/codeccontext.pxd @@ -0,0 +1,5 @@ +from av.codec.context cimport CodecContext + + +cdef class AttachmentCodecContext(CodecContext): + pass diff --git a/av/attachments/codeccontext.pyi b/av/attachments/codeccontext.pyi new file mode 100644 index 000000000..2bc21cc59 --- /dev/null +++ b/av/attachments/codeccontext.pyi @@ -0,0 +1,6 @@ +from typing import Literal + +from av.codec.context import CodecContext + +class AttachmentCodecContext(CodecContext): + type: Literal["attachment"] diff --git a/av/attachments/codeccontext.pyx b/av/attachments/codeccontext.pyx new file mode 100644 index 000000000..f83972cc2 --- /dev/null +++ b/av/attachments/codeccontext.pyx @@ -0,0 +1,8 @@ +cimport libav as lib + +from av.error cimport err_check +from av.packet cimport Packet + + +cdef class AttachmentCodecContext(CodecContext): + pass \ No newline at end of file diff --git a/av/attachments/stream.pxd b/av/attachments/stream.pxd new file mode 100644 index 000000000..81f788b77 --- /dev/null +++ b/av/attachments/stream.pxd @@ -0,0 +1,5 @@ +from av.stream cimport Stream + + +cdef class AttachmentStream(Stream): + pass diff --git a/av/attachments/stream.pyi b/av/attachments/stream.pyi new file mode 100644 index 000000000..b237be771 --- /dev/null +++ b/av/attachments/stream.pyi @@ -0,0 +1,4 @@ +from av.stream import Stream + +class AttachmentStream(Stream): + pass diff --git a/av/attachments/stream.pyx b/av/attachments/stream.pyx new file mode 100644 index 000000000..97b8b8da3 --- /dev/null +++ b/av/attachments/stream.pyx @@ -0,0 +1,10 @@ +from av.packet cimport Packet +from av.stream cimport Stream + + +cdef class AttachmentStream(Stream): + """ + An :class:`AttachmentStream`. + """ + def __getattr__(self, name): + return getattr(self.codec_context, name) diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 70100e65e..643f56160 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -34,6 +34,9 @@ cdef CodecContext wrap_codec_context(lib.AVCodecContext *c_ctx, const lib.AVCode elif c_ctx.codec_type == lib.AVMEDIA_TYPE_SUBTITLE: from av.subtitles.codeccontext import SubtitleCodecContext py_ctx = SubtitleCodecContext(_cinit_sentinel) + elif c_ctx.codec_type == lib.AVMEDIA_TYPE_ATTACHMENT: + from av.attachments.codeccontext import AttachmentCodecContext + py_ctx = AttachmentCodecContext(_cinit_sentinel) else: py_ctx = CodecContext(_cinit_sentinel) diff --git a/av/stream.pyx b/av/stream.pyx index 0cf3a7011..44354b651 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -1,5 +1,3 @@ -import warnings - cimport libav as lib from libc.stdint cimport int32_t @@ -13,8 +11,6 @@ from av.utils cimport ( to_avrational, ) -from av.deprecation import AVDeprecationWarning - cdef object _cinit_bypass_sentinel = object() @@ -47,6 +43,9 @@ cdef Stream wrap_stream(Container container, lib.AVStream *c_stream, CodecContex elif c_stream.codecpar.codec_type == lib.AVMEDIA_TYPE_SUBTITLE: from av.subtitles.stream import SubtitleStream py_stream = SubtitleStream.__new__(SubtitleStream, _cinit_bypass_sentinel) + elif c_stream.codecpar.codec_type == lib.AVMEDIA_TYPE_ATTACHMENT: + from av.attachments.stream import AttachmentStream + py_stream = AttachmentStream.__new__(AttachmentStream, _cinit_bypass_sentinel) elif c_stream.codecpar.codec_type == lib.AVMEDIA_TYPE_DATA: from av.data.stream import DataStream py_stream = DataStream.__new__(DataStream, _cinit_bypass_sentinel) @@ -96,9 +95,10 @@ cdef class Stream: ) def __repr__(self): + name = getattr(self, "name", None) return ( f"'}/" - f"{self.name or ''} at 0x{id(self):x}>" + f"{name or ''} at 0x{id(self):x}>" ) def __setattr__(self, name, value): @@ -239,7 +239,7 @@ cdef class Stream: :type: :class:`str` or ``None`` """ - return self.metadata.get('language') + return self.metadata.get("language") @property def type(self): From b7b1b156e7877071f3168522cc6ab93ffd832fc2 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 10 Aug 2024 03:48:21 -0400 Subject: [PATCH 080/365] Add attachments to StreamContainer --- av/attachments/codeccontext.pxd | 5 ----- av/attachments/codeccontext.pyi | 6 ------ av/attachments/codeccontext.pyx | 8 -------- av/attachments/stream.pyi | 6 +++++- av/attachments/stream.pyx | 11 ++++++++--- av/codec/context.pyx | 3 --- av/container/streams.pxd | 1 + av/container/streams.pyi | 2 ++ av/container/streams.pyx | 4 +++- 9 files changed, 19 insertions(+), 27 deletions(-) delete mode 100644 av/attachments/codeccontext.pxd delete mode 100644 av/attachments/codeccontext.pyi delete mode 100644 av/attachments/codeccontext.pyx diff --git a/av/attachments/codeccontext.pxd b/av/attachments/codeccontext.pxd deleted file mode 100644 index 9a2a71efc..000000000 --- a/av/attachments/codeccontext.pxd +++ /dev/null @@ -1,5 +0,0 @@ -from av.codec.context cimport CodecContext - - -cdef class AttachmentCodecContext(CodecContext): - pass diff --git a/av/attachments/codeccontext.pyi b/av/attachments/codeccontext.pyi deleted file mode 100644 index 2bc21cc59..000000000 --- a/av/attachments/codeccontext.pyi +++ /dev/null @@ -1,6 +0,0 @@ -from typing import Literal - -from av.codec.context import CodecContext - -class AttachmentCodecContext(CodecContext): - type: Literal["attachment"] diff --git a/av/attachments/codeccontext.pyx b/av/attachments/codeccontext.pyx deleted file mode 100644 index f83972cc2..000000000 --- a/av/attachments/codeccontext.pyx +++ /dev/null @@ -1,8 +0,0 @@ -cimport libav as lib - -from av.error cimport err_check -from av.packet cimport Packet - - -cdef class AttachmentCodecContext(CodecContext): - pass \ No newline at end of file diff --git a/av/attachments/stream.pyi b/av/attachments/stream.pyi index b237be771..3d660e4a0 100644 --- a/av/attachments/stream.pyi +++ b/av/attachments/stream.pyi @@ -1,4 +1,8 @@ +from typing import Literal + from av.stream import Stream class AttachmentStream(Stream): - pass + type: Literal["attachment"] + @property + def mimetype(self) -> str | None: ... diff --git a/av/attachments/stream.pyx b/av/attachments/stream.pyx index 97b8b8da3..b5b55ac1a 100644 --- a/av/attachments/stream.pyx +++ b/av/attachments/stream.pyx @@ -1,4 +1,3 @@ -from av.packet cimport Packet from av.stream cimport Stream @@ -6,5 +5,11 @@ cdef class AttachmentStream(Stream): """ An :class:`AttachmentStream`. """ - def __getattr__(self, name): - return getattr(self.codec_context, name) + + @property + def name(self): + return self.metadata.get("filename") + + @property + def mimetype(self): + return self.metadata.get("mimetype") diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 643f56160..70100e65e 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -34,9 +34,6 @@ cdef CodecContext wrap_codec_context(lib.AVCodecContext *c_ctx, const lib.AVCode elif c_ctx.codec_type == lib.AVMEDIA_TYPE_SUBTITLE: from av.subtitles.codeccontext import SubtitleCodecContext py_ctx = SubtitleCodecContext(_cinit_sentinel) - elif c_ctx.codec_type == lib.AVMEDIA_TYPE_ATTACHMENT: - from av.attachments.codeccontext import AttachmentCodecContext - py_ctx = AttachmentCodecContext(_cinit_sentinel) else: py_ctx = CodecContext(_cinit_sentinel) diff --git a/av/container/streams.pxd b/av/container/streams.pxd index bf217d7c6..43f7f3e4f 100644 --- a/av/container/streams.pxd +++ b/av/container/streams.pxd @@ -9,6 +9,7 @@ cdef class StreamContainer: cdef readonly tuple video cdef readonly tuple audio cdef readonly tuple subtitles + cdef readonly tuple attachments cdef readonly tuple data cdef readonly tuple other diff --git a/av/container/streams.pyi b/av/container/streams.pyi index cb658d35f..9f67e7981 100644 --- a/av/container/streams.pyi +++ b/av/container/streams.pyi @@ -1,5 +1,6 @@ from typing import Iterator, overload +from av.attachments.stream import AttachmentStream from av.audio.stream import AudioStream from av.data.stream import DataStream from av.stream import Stream @@ -10,6 +11,7 @@ class StreamContainer: video: tuple[VideoStream, ...] audio: tuple[AudioStream, ...] subtitles: tuple[SubtitleStream, ...] + attachments: tuple[AttachmentStream, ...] data: tuple[DataStream, ...] other: tuple[Stream, ...] diff --git a/av/container/streams.pyx b/av/container/streams.pyx index 921bd40b2..6071bc811 100644 --- a/av/container/streams.pyx +++ b/av/container/streams.pyx @@ -12,7 +12,6 @@ def _flatten(input_): cdef class StreamContainer: - """ A tuple-like container of :class:`Stream`. @@ -33,6 +32,7 @@ cdef class StreamContainer: self.audio = () self.subtitles = () self.data = () + self.attachments = () self.other = () cdef add_stream(self, Stream stream): @@ -46,6 +46,8 @@ cdef class StreamContainer: self.audio = self.audio + (stream, ) elif stream.ptr.codecpar.codec_type == lib.AVMEDIA_TYPE_SUBTITLE: self.subtitles = self.subtitles + (stream, ) + elif stream.ptr.codecpar.codec_type == lib.AVMEDIA_TYPE_ATTACHMENT: + self.attachments = self.attachments + (stream, ) elif stream.ptr.codecpar.codec_type == lib.AVMEDIA_TYPE_DATA: self.data = self.data + (stream, ) else: From 622456948f83890ca8512cd107dcd4dcbc07636a Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 11 Aug 2024 02:46:04 -0400 Subject: [PATCH 081/365] Fix Video CC (`self._format` can be None) This fixes wyattblue/auto-editor #489. --- av/video/codeccontext.pyi | 2 +- av/video/codeccontext.pyx | 13 +++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/av/video/codeccontext.pyi b/av/video/codeccontext.pyi index 4576f5155..7bdda89e0 100644 --- a/av/video/codeccontext.pyi +++ b/av/video/codeccontext.pyi @@ -8,7 +8,7 @@ from .format import VideoFormat from .frame import VideoFrame class VideoCodecContext(CodecContext): - format: VideoFormat + format: VideoFormat | None width: int height: int bits_per_codec_sample: int diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.pyx index 5ef49ee4c..32c74f867 100644 --- a/av/video/codeccontext.pyx +++ b/av/video/codeccontext.pyx @@ -34,6 +34,9 @@ cdef class VideoCodecContext(CodecContext): cdef VideoFrame vframe = input + if self._format is None: + raise ValueError("self._format is None, cannot encode") + # Reformat if it doesn't match. if ( vframe.format.pix_fmt != self._format.pix_fmt or @@ -42,11 +45,9 @@ cdef class VideoCodecContext(CodecContext): ): if not self.reformatter: self.reformatter = VideoReformatter() + vframe = self.reformatter.reformat( - vframe, - self.ptr.width, - self.ptr.height, - self._format, + vframe, self.ptr.width, self.ptr.height, self._format ) # There is no pts, so create one. @@ -121,9 +122,9 @@ cdef class VideoCodecContext(CodecContext): """ The pixel format's name. - :type: str + :type: str | None """ - return self._format.name + return getattr(self._format, "name") @pix_fmt.setter def pix_fmt(self, value): From ba1ace7d0f5025ad10ed92e3f6dc85dca6374fa5 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 11 Aug 2024 03:15:22 -0400 Subject: [PATCH 082/365] Disallow accessing gop_size, timebase as a decoder --- av/codec/context.pyx | 12 ++---------- av/video/codeccontext.pyx | 14 ++------------ av/video/frame.pyx | 11 ----------- tests/test_codec_context.py | 25 ++++++------------------- 4 files changed, 10 insertions(+), 52 deletions(-) diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 70100e65e..66b21f9a3 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -1,5 +1,3 @@ -import warnings - cimport libav as lib from libc.errno cimport EAGAIN from libc.stdint cimport uint8_t @@ -504,19 +502,13 @@ cdef class CodecContext: @property def time_base(self): if self.is_decoder: - warnings.warn( - "Using CodecContext.time_base for decoders is deprecated.", - AVDeprecationWarning - ) + raise RuntimeError("Cannot access 'time_base' as a decoder") return avrational_to_fraction(&self.ptr.time_base) @time_base.setter def time_base(self, value): if self.is_decoder: - warnings.warn( - "Using CodecContext.time_base for decoders is deprecated.", - AVDeprecationWarning - ) + raise RuntimeError("Cannot access 'time_base' as a decoder") to_avrational(value, &self.ptr.time_base) @property diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.pyx index 32c74f867..c37223784 100644 --- a/av/video/codeccontext.pyx +++ b/av/video/codeccontext.pyx @@ -1,5 +1,3 @@ -import warnings - cimport libav as lib from libc.stdint cimport int64_t @@ -11,8 +9,6 @@ from av.video.format cimport VideoFormat, get_pix_fmt, get_video_format from av.video.frame cimport VideoFrame, alloc_video_frame from av.video.reformatter cimport VideoReformatter -from av.deprecation import AVDeprecationWarning - cdef class VideoCodecContext(CodecContext): def __cinit__(self, *args, **kwargs): @@ -161,19 +157,13 @@ cdef class VideoCodecContext(CodecContext): :type: int """ if self.is_decoder: - warnings.warn( - "Using VideoCodecContext.gop_size for decoders is deprecated.", - AVDeprecationWarning - ) + raise RuntimeError("Cannnot access 'gop_size' as a decoder") return self.ptr.gop_size @gop_size.setter def gop_size(self, int value): if self.is_decoder: - warnings.warn( - "Using VideoCodecContext.gop_size for decoders is deprecated.", - AVDeprecationWarning - ) + raise RuntimeError("Cannnot access 'gop_size' as a decoder") self.ptr.gop_size = value @property diff --git a/av/video/frame.pyx b/av/video/frame.pyx index e64eb8573..c27ead7b4 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -8,10 +8,6 @@ from av.utils cimport check_ndarray, check_ndarray_shape from av.video.format cimport get_pix_fmt, get_video_format from av.video.plane cimport VideoPlane -import warnings - -from av.deprecation import AVDeprecationWarning - cdef object _cinit_bypass_sentinel @@ -574,10 +570,3 @@ cdef class VideoFrame(Frame): copy_array_to_plane(array, frame.planes[0], 1 if array.ndim == 2 else array.shape[2]) return frame - - def __getattribute__(self, attribute): - # This method should be deleted when `frame.index` is removed - if attribute == "index": - warnings.warn("Using `frame.index` is deprecated.", AVDeprecationWarning) - - return Frame.__getattribute__(self, attribute) diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 3ead81fc2..7ada318d9 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -1,5 +1,4 @@ import os -import warnings from fractions import Fraction from unittest import SkipTest @@ -89,29 +88,17 @@ def test_decoder_extradata(self): def test_decoder_gop_size(self): ctx = av.codec.Codec("h264", "r").create() - with warnings.catch_warnings(record=True) as captured: - self.assertIsInstance(ctx.gop_size, int) - self.assertEqual( - captured[0].message.args[0], - "Using VideoCodecContext.gop_size for decoders is deprecated.", - ) + with self.assertRaises(RuntimeError): + ctx.gop_size - def test_decoder_timebase(self): + def test_decoder_timebase(self) -> None: ctx = av.codec.Codec("h264", "r").create() - with warnings.catch_warnings(record=True) as captured: - self.assertIsNone(ctx.time_base) - self.assertEqual( - captured[0].message.args[0], - "Using CodecContext.time_base for decoders is deprecated.", - ) + with self.assertRaises(RuntimeError): + ctx.time_base - with warnings.catch_warnings(record=True) as captured: + with self.assertRaises(RuntimeError): ctx.time_base = Fraction(1, 25) - self.assertEqual( - captured[0].message.args[0], - "Using CodecContext.time_base for decoders is deprecated.", - ) def test_encoder_extradata(self): ctx = av.codec.Codec("h264", "w").create() From 2941cc33635b91a99afc73ffd43fc2b01660d2a8 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 11 Aug 2024 04:31:17 -0400 Subject: [PATCH 083/365] Add `best()` to StreamContainer Closes #1481 --- av/container/streams.pxd | 7 ++++- av/container/streams.pyi | 8 +++++- av/container/streams.pyx | 47 +++++++++++++++++++++++++++++++- docs/api/attachments.rst | 8 ++++++ docs/api/stream.rst | 6 ++++ include/libavformat/avformat.pxd | 10 ++++++- tests/test_streams.py | 20 ++++++++++---- 7 files changed, 97 insertions(+), 9 deletions(-) create mode 100644 docs/api/attachments.rst diff --git a/av/container/streams.pxd b/av/container/streams.pxd index 43f7f3e4f..097176e10 100644 --- a/av/container/streams.pxd +++ b/av/container/streams.pxd @@ -1,8 +1,11 @@ +cimport libav as lib + from av.stream cimport Stream +from .core cimport Container -cdef class StreamContainer: +cdef class StreamContainer: cdef list _streams # For the different types. @@ -14,3 +17,5 @@ cdef class StreamContainer: cdef readonly tuple other cdef add_stream(self, Stream stream) + cdef int _get_best_stream_index(self, Container container, lib.AVMediaType type_enum, Stream related) noexcept + diff --git a/av/container/streams.pyi b/av/container/streams.pyi index 9f67e7981..5d8647afe 100644 --- a/av/container/streams.pyi +++ b/av/container/streams.pyi @@ -1,4 +1,4 @@ -from typing import Iterator, overload +from typing import Iterator, Literal, overload from av.attachments.stream import AttachmentStream from av.audio.stream import AudioStream @@ -30,3 +30,9 @@ class StreamContainer: *args: int | Stream | dict[str, int | tuple[int, ...]], **kwargs: int | tuple[int, ...], ) -> list[Stream]: ... + def best( + self, + type: Literal["video", "audio", "subtitle", "data", "attachment"], + /, + related: Stream | None = None, + ) -> Stream | None: ... diff --git a/av/container/streams.pyx b/av/container/streams.pyx index 6071bc811..48830278c 100644 --- a/av/container/streams.pyx +++ b/av/container/streams.pyx @@ -1,4 +1,3 @@ - cimport libav as lib @@ -10,6 +9,19 @@ def _flatten(input_): else: yield x +cdef lib.AVMediaType _get_media_type_enum(str type): + if type == "video": + return lib.AVMEDIA_TYPE_VIDEO + elif type == "audio": + return lib.AVMEDIA_TYPE_AUDIO + elif type == "subtitle": + return lib.AVMEDIA_TYPE_SUBTITLE + elif type == "attachment": + return lib.AVMEDIA_TYPE_ATTACHMENT + elif type == "data": + return lib.AVMEDIA_TYPE_DATA + else: + raise ValueError(f"Invalid stream type: {type}") cdef class StreamContainer: """ @@ -121,3 +133,36 @@ cdef class StreamContainer: raise TypeError("Argument must be Stream or int.", type(x)) return selection or self._streams[:] + + cdef int _get_best_stream_index(self, Container container, lib.AVMediaType type_enum, Stream related) noexcept: + cdef int stream_index + + if related is None: + stream_index = lib.av_find_best_stream(container.ptr, type_enum, -1, -1, NULL, 0) + else: + stream_index = lib.av_find_best_stream(container.ptr, type_enum, -1, related.ptr.index, NULL, 0) + + return stream_index + + def best(self, str type, /, Stream related = None): + """best(type: Literal["video", "audio", "subtitle", "attachment", "data"], /, related: Stream | None) + Finds the "best" stream in the file. Wraps :ffmpeg:`av_find_best_stream` + + :param type: The type of stream to find + :param related: A related stream to use as a reference (optional) + :return: The best stream of the specified type + :rtype: Stream | None + """ + cdef type_enum = _get_media_type_enum(type) + + if len(self._streams) == 0: + return None + + cdef container = self._streams[0].container + + cdef int stream_index = self._get_best_stream_index(container, type_enum, related) + + if stream_index < 0: + return None + + return self._streams[stream_index] diff --git a/docs/api/attachments.rst b/docs/api/attachments.rst new file mode 100644 index 000000000..6e59a202b --- /dev/null +++ b/docs/api/attachments.rst @@ -0,0 +1,8 @@ + +Attachments +=========== + +.. automodule:: av.attachments.stream + + .. autoclass:: AttachmentStream + :members: diff --git a/docs/api/stream.rst b/docs/api/stream.rst index a6bc1fc8b..99a30b136 100644 --- a/docs/api/stream.rst +++ b/docs/api/stream.rst @@ -16,6 +16,8 @@ Dynamic Slicing .. automethod:: StreamContainer.get +.. automethod:: StreamContainer.best + Typed Collections ~~~~~~~~~~~~~~~~~ @@ -35,6 +37,10 @@ dynamic capabilities of :meth:`.get`: A tuple of :class:`SubtitleStream`. +.. attribute:: StreamContainer.attachments + + A tuple of :class:`AttachmentStream`. + .. attribute:: StreamContainer.data A tuple of :class:`DataStream`. diff --git a/include/libavformat/avformat.pxd b/include/libavformat/avformat.pxd index 29a892176..195134970 100644 --- a/include/libavformat/avformat.pxd +++ b/include/libavformat/avformat.pxd @@ -28,7 +28,6 @@ cdef extern from "libavformat/avformat.h" nogil: AVMEDIA_TYPE_NB cdef struct AVStream: - int index int id @@ -164,6 +163,15 @@ cdef extern from "libavformat/avformat.h" nogil: unsigned int max_probe_size ) + cdef int av_find_best_stream( + AVFormatContext *ic, + AVMediaType type, + int wanted_stream_nb, + int related_stream, + AVCodec **decoder_ret, + int flags + ) + cdef AVInputFormat* av_find_input_format(const char *name) # http://ffmpeg.org/doxygen/trunk/structAVFormatContext.html diff --git a/tests/test_streams.py b/tests/test_streams.py index a4e3eae5e..c146abfb1 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -14,16 +14,26 @@ def test_stream_tuples(self): audio_streams = tuple([s for s in container.streams if s.type == "audio"]) self.assertEqual(audio_streams, container.streams.audio) - def test_selection(self): - container = av.open(fate_suite("h264/interlaced_crop.mp4")) + def test_selection(self) -> None: + container = av.open( + fate_suite("amv/MTV_high_res_320x240_sample_Penguin_Joke_MTV_from_WMV.amv") + ) video = container.streams.video[0] - # audio_stream = container.streams.audio[0] - # audio_streams = list(container.streams.audio[0:2]) + audio = container.streams.audio[0] self.assertEqual([video], container.streams.get(video=0)) self.assertEqual([video], container.streams.get(video=(0,))) - # TODO: Find something in the fate suite with video, audio, and subtitles. + self.assertEqual(video, container.streams.best("video")) + self.assertEqual(audio, container.streams.best("audio")) + + container = av.open(fate_suite("sub/MovText_capability_tester.mp4")) + subtitle = container.streams.subtitles[0] + self.assertEqual(subtitle, container.streams.best("subtitle")) + + container = av.open(fate_suite("mxf/track_01_v02.mxf")) + data = container.streams.data[0] + self.assertEqual(data, container.streams.best("data")) def test_noside_data(self): container = av.open(fate_suite("h264/interlaced_crop.mp4")) From 5753e259fbcd4794aacb2c439d0dee72a173ea1a Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 11 Aug 2024 05:17:18 -0400 Subject: [PATCH 084/365] Add docstrings for best, AttachmentStream --- av/attachments/stream.pyx | 13 ++++++++++++- av/container/streams.pyx | 4 +++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/av/attachments/stream.pyx b/av/attachments/stream.pyx index b5b55ac1a..de7d10119 100644 --- a/av/attachments/stream.pyx +++ b/av/attachments/stream.pyx @@ -3,13 +3,24 @@ from av.stream cimport Stream cdef class AttachmentStream(Stream): """ - An :class:`AttachmentStream`. + An :class:`AttachmentStream` represents a stream of attachment data within a media container. + Typically used to attach font files that are referenced in ASS/SSA Subtitle Streams. """ @property def name(self): + """ + Returns the file name of the attachment. + + :rtype: str | None + """ return self.metadata.get("filename") @property def mimetype(self): + """ + Returns the MIME type of the attachment. + + :rtype: str | None + """ return self.metadata.get("mimetype") diff --git a/av/container/streams.pyx b/av/container/streams.pyx index 48830278c..17e4992d8 100644 --- a/av/container/streams.pyx +++ b/av/container/streams.pyx @@ -146,7 +146,9 @@ cdef class StreamContainer: def best(self, str type, /, Stream related = None): """best(type: Literal["video", "audio", "subtitle", "attachment", "data"], /, related: Stream | None) - Finds the "best" stream in the file. Wraps :ffmpeg:`av_find_best_stream` + Finds the "best" stream in the file. Wraps :ffmpeg:`av_find_best_stream`. Example:: + + stream = container.streams.best("video") :param type: The type of stream to find :param related: A related stream to use as a reference (optional) From cd56a75eac5b81f21166f29f649cadd91a17e5e9 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 11 Aug 2024 12:43:32 -0400 Subject: [PATCH 085/365] Return None instead of erroring --- av/video/codeccontext.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.pyx index c37223784..5764c400b 100644 --- a/av/video/codeccontext.pyx +++ b/av/video/codeccontext.pyx @@ -120,7 +120,7 @@ cdef class VideoCodecContext(CodecContext): :type: str | None """ - return getattr(self._format, "name") + return getattr(self._format, "name", None) @pix_fmt.setter def pix_fmt(self, value): From 8d76aac36788b5ce3811e151053f11a2690513bb Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 15 Aug 2024 17:16:44 -0400 Subject: [PATCH 086/365] Edit readme to reflect changes in build step --- README.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 70023dbf4..27e5182d6 100644 --- a/README.md +++ b/README.md @@ -49,13 +49,18 @@ git clone https://github.com/PyAV-Org/PyAV.git cd PyAV source scripts/activate.sh -# Either install the testing dependencies: -pip install --upgrade -r tests/requirements.txt -# or have it all, including FFmpeg, built/installed for you: +# Build ffmpeg from source. You can skip this step +# if ffmpeg is already installed. ./scripts/build-deps -# Build PyAV. +# Build PyAV make + +# Testing +make test + +# Install globally +deactivate pip install . ``` From 4f14a7cece991c3764416f6fdc8ff34092582eea Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 16 Aug 2024 20:17:58 -0400 Subject: [PATCH 087/365] Skip 3.13 Windows (broken numpy) --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 27e394948..91496725c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -72,7 +72,7 @@ jobs: CIBW_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m unittest discover -t {project} -s tests && mv {project}/av.disabled {project}/av CIBW_TEST_REQUIRES: numpy # skip tests when there are no binary wheels of numpy - CIBW_TEST_SKIP: pp* *_i686 + CIBW_TEST_SKIP: pp* *_i686 cp313-win_amd64 run: | pip install cibuildwheel delvewheel cibuildwheel --output-dir dist From 9b2576293aaa5fbb518a85e93400864a0c0d1b16 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 16 Aug 2024 23:07:55 -0400 Subject: [PATCH 088/365] We don't use setup.py to do tests anymore --- setup.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 7df0a9c2f..f3a3f6447 100644 --- a/setup.py +++ b/setup.py @@ -210,16 +210,13 @@ def parse_cflags(raw_flags): author="Mike Boers", author_email="pyav@mikeboers.com", url="https://github.com/PyAV-Org/PyAV", - packages=find_packages( - exclude=["build*", "examples*", "scratchpad*", "tests*", "include*"] - ) + packages=find_packages(exclude=["build*", "examples*", "tests*", "include*"]) + list(package_dir.keys()), package_dir=package_dir, package_data=package_data, python_requires=">=3.9", zip_safe=False, ext_modules=ext_modules, - test_suite="tests", entry_points={ "console_scripts": ["pyav = av.__main__:main"], }, From 8042cf8240d2bbd0a06d02d8218324aff1c4956a Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 18 Aug 2024 18:52:16 -0400 Subject: [PATCH 089/365] Try 3.13 Numpy --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 91496725c..27e394948 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -72,7 +72,7 @@ jobs: CIBW_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m unittest discover -t {project} -s tests && mv {project}/av.disabled {project}/av CIBW_TEST_REQUIRES: numpy # skip tests when there are no binary wheels of numpy - CIBW_TEST_SKIP: pp* *_i686 cp313-win_amd64 + CIBW_TEST_SKIP: pp* *_i686 run: | pip install cibuildwheel delvewheel cibuildwheel --output-dir dist From 64612012be2e0d6005d4f2d398d3ba1384fc7d4c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 19 Aug 2024 04:58:48 -0400 Subject: [PATCH 090/365] Add back channels property to AudioLayout --- av/audio/layout.pyi | 8 +++++++ av/audio/layout.pyx | 38 +++++++++++++++++++++++++++++++--- include/libavcodec/avcodec.pxd | 3 +++ setup.py | 18 ++++++++++++++++ tests/test_audiolayout.py | 5 ++++- 5 files changed, 68 insertions(+), 4 deletions(-) diff --git a/av/audio/layout.pyi b/av/audio/layout.pyi index 073cd1723..9fdf0ac15 100644 --- a/av/audio/layout.pyi +++ b/av/audio/layout.pyi @@ -1,4 +1,12 @@ +from dataclasses import dataclass + class AudioLayout: name: str nb_channels: int + channels: tuple[AudioChannel, ...] def __init__(self, layout: str | AudioLayout): ... + +@dataclass +class AudioChannel: + name: str + description: str diff --git a/av/audio/layout.pyx b/av/audio/layout.pyx index 59753138e..ea259d0fd 100644 --- a/av/audio/layout.pyx +++ b/av/audio/layout.pyx @@ -1,5 +1,16 @@ cimport libav as lib +from cpython.bytes cimport PyBytes_FromStringAndSize +from dataclasses import dataclass + + +@dataclass +class AudioChannel: + name: str + description: str + + def __repr__(self): + return f"" cdef object _cinit_bypass_sentinel @@ -40,13 +51,34 @@ cdef class AudioLayout: return self.layout.nb_channels @property - def name(self): + def channels(self): + cdef lib.AVChannel channel + cdef char buf[16] + cdef char buf2[128] + + results = [] + + for index in range(self.layout.nb_channels): + channel = lib.av_channel_layout_channel_from_index(&self.layout, index); + size = lib.av_channel_name(buf, sizeof(buf), channel) - 1 + size2 = lib.av_channel_description(buf2, sizeof(buf2), channel) - 1 + results.append( + AudioChannel( + PyBytes_FromStringAndSize(buf, size).decode("utf-8"), + PyBytes_FromStringAndSize(buf2, size2).decode("utf-8"), + ) + ) + + return tuple(results) + + @property + def name(self) -> str: """The canonical name of the audio layout.""" - cdef char layout_name[128] # Adjust buffer size as needed + cdef char layout_name[128] cdef int ret ret = lib.av_channel_layout_describe(&self.layout, layout_name, sizeof(layout_name)) if ret < 0: raise RuntimeError(f"Failed to get layout name: {ret}") - return layout_name + return layout_name \ No newline at end of file diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 9e9cc46a7..9add5ae2d 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -42,6 +42,9 @@ cdef extern from "libavutil/channel_layout.h": void av_channel_layout_uninit(AVChannelLayout *channel_layout) int av_channel_layout_copy(AVChannelLayout *dst, const AVChannelLayout *src) int av_channel_layout_describe(const AVChannelLayout *channel_layout, char *buf, size_t buf_size) + int av_channel_name(char *buf, size_t buf_size, AVChannel channel_id) + int av_channel_description(char *buf, size_t buf_size, AVChannel channel_id) + AVChannel av_channel_layout_channel_from_index(AVChannelLayout *channel_layout, unsigned int idx) cdef extern from "libavcodec/avcodec.h" nogil: diff --git a/setup.py b/setup.py index f3a3f6447..5329223b3 100644 --- a/setup.py +++ b/setup.py @@ -27,6 +27,21 @@ old_embed_signature = EmbedSignature._embed_signature +def insert_enum_in_generated_files(source): + # Work around Cython failing to add `enum` to `AVChannel` type. + # TODO: Make Cython bug report + if source.endswith(".c"): + with open(source, "r") as file: + content = file.read() + + # Replace "AVChannel __pyx_v_channel;" with "enum AVChannel __pyx_v_channel;" + modified_content = re.sub( + r"\b(? None: self.assertEqual(layout.name, "stereo") self.assertEqual(layout.nb_channels, 2) self.assertEqual(repr(layout), "") + + # Re-enable when FFmpeg 6.0 is dropped. + # self.assertEqual(layout.channels[0].name, "FL") # self.assertEqual(layout.channels[0].description, "front left") # self.assertEqual( From 1411bf77eec86bb47a2dc79c5a6aaeff7d7ca706 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 1 Sep 2024 03:14:04 -0400 Subject: [PATCH 091/365] Write release notes --- CHANGELOG.rst | 23 +++++++++++++++++++++++ av/about.py | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2b7de678b..45cbf280b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,29 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. +v13.0.0 +------- + +Major: + +- Drop FFmpeg 5, Support FFmpeg 7. +- Drop Python 3.8, Support Python 3.13. +- Update FFmpeg to 7.0.2 for the binary wheels. +- Disallow initializing an AudioLayout object with an int. +- Disallow accessing gop_size, timebase as a decoder (Raised deprecation warning before). +- Remove `ticks_per_frame` property because it was deprecated in FFmpeg. + +Features: + +- Add AttachmentStream class. +- Add `best()` method to StreamContainer. +- Add `set_audio_frame_size()` method to Graph object. +- Add `nb_channels` property to AudioLayout object. + +Fixes: + +- Fix VideoCC's repl breaking when `self._format` is None. +- Fix getting `pix_fmt` property when VideoCC's `self._format` is None. v12.3.0 ------- diff --git a/av/about.py b/av/about.py index 79a759ae1..1311252a5 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "13.0.0rc1" +__version__ = "13.0.0" From 5dc17e8a49d929bda845096816390479e5b37bf9 Mon Sep 17 00:00:00 2001 From: Dexer <73297572+DexerBR@users.noreply.github.com> Date: Mon, 2 Sep 2024 13:10:43 -0300 Subject: [PATCH 092/365] add from_bytes --- av/video/frame.pyi | 9 +++++++++ av/video/frame.pyx | 40 ++++++++++++++++++++++++++++++++++------ 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/av/video/frame.pyi b/av/video/frame.pyi index 17cacf05a..9c04c9bac 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -63,3 +63,12 @@ class VideoFrame(Frame): ) -> VideoFrame: ... @staticmethod def from_ndarray(array: _SupportedNDarray, format: str = "rgb24") -> VideoFrame: ... + @staticmethod + def from_bytes( + data: bytes, + width: int, + height: int, + format="rgba", + flip_horizontal=False, + flip_vertical=False, + ) -> VideoFrame: ... diff --git a/av/video/frame.pyx b/av/video/frame.pyx index c27ead7b4..ee060e16a 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -40,9 +40,8 @@ cdef byteswap_array(array, bint big_endian): return array -cdef copy_array_to_plane(array, VideoPlane plane, unsigned int bytes_per_pixel): - cdef bytes imgbytes = array.tobytes() - cdef const uint8_t[:] i_buf = imgbytes +cdef copy_bytes_to_plane(img_bytes, VideoPlane plane, unsigned int bytes_per_pixel, bint flip_horizontal, bint flip_vertical): + cdef const uint8_t[:] i_buf = img_bytes cdef size_t i_pos = 0 cdef size_t i_stride = plane.width * bytes_per_pixel cdef size_t i_size = plane.height * i_stride @@ -51,12 +50,33 @@ cdef copy_array_to_plane(array, VideoPlane plane, unsigned int bytes_per_pixel): cdef size_t o_pos = 0 cdef size_t o_stride = abs(plane.line_size) - while i_pos < i_size: - o_buf[o_pos:o_pos + i_stride] = i_buf[i_pos:i_pos + i_stride] - i_pos += i_stride + cdef int start_row, end_row, step + if flip_vertical: + start_row = plane.height - 1 + end_row = -1 + step = -1 + else: + start_row = 0 + end_row = plane.height + step = 1 + + cdef int i, j + for row in range(start_row, end_row, step): + i_pos = row * i_stride + if flip_horizontal: + for i in range(0, i_stride, bytes_per_pixel): + for j in range(bytes_per_pixel): + o_buf[o_pos + i + j] = i_buf[i_pos + i_stride - i - bytes_per_pixel + j] + else: + o_buf[o_pos:o_pos + i_stride] = i_buf[i_pos:i_pos + i_stride] o_pos += o_stride +cdef copy_array_to_plane(array, VideoPlane plane, unsigned int bytes_per_pixel): + cdef bytes imgbytes = array.tobytes() + copy_bytes_to_plane(imgbytes, plane, bytes_per_pixel, False, False) + + cdef useful_array(VideoPlane plane, unsigned int bytes_per_pixel=1, str dtype="uint8"): """ Return the useful part of the VideoPlane as a single dimensional array. @@ -570,3 +590,11 @@ cdef class VideoFrame(Frame): copy_array_to_plane(array, frame.planes[0], 1 if array.ndim == 2 else array.shape[2]) return frame + + def from_bytes(img_bytes: bytes, width: int, height: int, format="rgba", flip_horizontal=False, flip_vertical=False): + frame = VideoFrame(width, height, format) + if format == "rgba": + copy_bytes_to_plane(img_bytes, frame.planes[0], 4, flip_horizontal, flip_vertical) + else: + raise NotImplementedError(f"Format '{format}' is not supported.") + return frame From 0e4b711ab7f5b67f6490c7a0f23b555e87d89d6c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 3 Sep 2024 02:22:21 -0400 Subject: [PATCH 093/365] Update type stubs for `from_bytes()` --- AUTHORS.rst | 2 ++ CHANGELOG.rst | 1 + av/video/frame.pyi | 6 +++--- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index af7ea08cc..bf3a6f49d 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -48,12 +48,14 @@ All contributors (by number of commits): * mephi42 * Miles Kaufmann * Pablo Prietz +* Andrew Wason * Radek Senfeld ; `@radek-senfeld `_ * Benjamin Chrétien <2742231+bchretien@users.noreply.github.com> * Marc Mueller <30130371+cdce8p@users.noreply.github.com> * zzjjbb <31069326+zzjjbb@users.noreply.github.com> * Hanz <40712686+HanzCEO@users.noreply.github.com> * Joe Schiff <41972063+JoeSchiff@users.noreply.github.com> +* Dexer <73297572+DexerBR@users.noreply.github.com> * Artturin * Ian Lee * Ryan Huang diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 45cbf280b..75bd7adff 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -34,6 +34,7 @@ Features: - Add `best()` method to StreamContainer. - Add `set_audio_frame_size()` method to Graph object. - Add `nb_channels` property to AudioLayout object. +- Add `from_bytes()` method to VideoFrame object. Fixes: diff --git a/av/video/frame.pyi b/av/video/frame.pyi index 9c04c9bac..de84faaa0 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -68,7 +68,7 @@ class VideoFrame(Frame): data: bytes, width: int, height: int, - format="rgba", - flip_horizontal=False, - flip_vertical=False, + format: str = "rgba", + flip_horizontal: bool = False, + flip_vertical: bool = False, ) -> VideoFrame: ... From 75c0e0bb1cee28c20564e75ed4b42d08499ea974 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 3 Sep 2024 17:42:52 -0400 Subject: [PATCH 094/365] Remove unneeded libraries --- .github/workflows/smoke.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 4dbcc096b..f5fabe342 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -60,16 +60,13 @@ jobs: ubuntu-latest) sudo apt-get update sudo apt-get install autoconf automake build-essential cmake \ - libtool mercurial pkg-config texinfo wget yasm zlib1g-dev - sudo apt-get install libass-dev libfreetype6-dev libjpeg-dev \ - libtheora-dev libvorbis-dev libx264-dev + libtool pkg-config yasm zlib1g-dev libvorbis-dev libx264-dev if [[ "${{ matrix.config.extras }}" ]]; then - sudo apt-get install doxygen + sudo apt-get install doxygen wget fi ;; macos-12) - brew install automake libtool nasm pkg-config shtool texi2html wget - brew install libass libjpeg libpng libvorbis libvpx opus theora x264 + brew install automake libtool nasm pkg-config libpng libvorbis libvpx opus x264 ;; esac From 6b20f97fec0735034fe243fddaec7bd5a8cb945d Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 4 Sep 2024 00:03:44 -0400 Subject: [PATCH 095/365] Use GitHub Discussions over Gitter GH Discussions support upvoting threads and upvoting. Features lacking in Gitter. Gitter is also less activate. --- README.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 27e5182d6..10cd7709d 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ PyAV ==== -[![GitHub Test Status][github-tests-badge]][github-tests] -[![Gitter Chat][gitter-badge]][gitter] [![Documentation][docs-badge]][docs] \ -[![Python Package Index][pypi-badge]][pypi] [![Conda Forge][conda-badge]][conda] - PyAV is a Pythonic binding for the [FFmpeg][ffmpeg] libraries. We aim to provide all of the power and control of the underlying library, but manage the gritty details as much as possible. +--- + +[![GitHub Test Status][github-tests-badge]][github-tests] [![Documentation][docs-badge]][docs] [![Python Package Index][pypi-badge]][pypi] [![Conda Forge][conda-badge]][conda] + PyAV is for direct and precise access to your media via containers, streams, packets, codecs, and frames. It exposes a few transformations of that data, and helps you get your data to/from other packages (e.g. Numpy and Pillow). This power does come with some responsibility as working with media is horrendously complicated and PyAV can't abstract it away or make all the best decisions for you. If the `ffmpeg` command does the job without you bending over backwards, PyAV is likely going to be more of a hindrance than a help. @@ -66,7 +66,7 @@ pip install . --- -Have fun, [read the docs][docs], [come chat with us][gitter], and good luck! +Have fun, [read the docs][docs], [come chat with us][discuss], and good luck! @@ -74,10 +74,9 @@ Have fun, [read the docs][docs], [come chat with us][gitter], and good luck! [conda]: https://anaconda.org/conda-forge/av [docs-badge]: https://img.shields.io/badge/docs-on%20pyav.basswood--io.com-blue.svg [docs]: https://pyav.basswood-io.com -[gitter-badge]: https://img.shields.io/gitter/room/nwjs/nw.js.svg?logo=gitter&colorB=cc2b5e -[gitter]: https://app.gitter.im/#/room/#PyAV-Org_User-Help:gitter.im [pypi-badge]: https://img.shields.io/pypi/v/av.svg?colorB=CCB39A [pypi]: https://pypi.org/project/av +[discuss]: https://github.com/PyAV-Org/PyAV/discussions [github-tests-badge]: https://github.com/PyAV-Org/PyAV/workflows/tests/badge.svg [github-tests]: https://github.com/PyAV-Org/PyAV/actions?workflow=tests From 71ad5c3ecda2500c5b48bf9beeb854e10927fbee Mon Sep 17 00:00:00 2001 From: Mark Harfouche Date: Wed, 4 Sep 2024 19:15:30 -0400 Subject: [PATCH 096/365] Declare `write_packet` C function as const Otherwise llvm 17 complains --- include/libavformat/avformat.pxd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/libavformat/avformat.pxd b/include/libavformat/avformat.pxd index 195134970..1e16378bf 100644 --- a/include/libavformat/avformat.pxd +++ b/include/libavformat/avformat.pxd @@ -79,7 +79,7 @@ cdef extern from "libavformat/avformat.h" nogil: int write_flag, void *opaque, int(*read_packet)(void *opaque, uint8_t *buf, int buf_size), - int(*write_packet)(void *opaque, uint8_t *buf, int buf_size), + int(*write_packet)(void *opaque, const uint8_t *buf, int buf_size), int64_t(*seek)(void *opaque, int64_t offset, int whence) ) From 880903280d54309d800aefeabc4468cf7cbdbcb3 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 11 Sep 2024 17:01:48 -0400 Subject: [PATCH 097/365] Update issue templates --- .github/ISSUE_TEMPLATE/ffmpeg-feature-request.md | 6 ------ .github/ISSUE_TEMPLATE/pyav-feature-request.md | 6 ------ .github/ISSUE_TEMPLATE/runtime-bug-report.md | 7 ++----- 3 files changed, 2 insertions(+), 17 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/ffmpeg-feature-request.md b/.github/ISSUE_TEMPLATE/ffmpeg-feature-request.md index 115c15b13..d47b011e1 100644 --- a/.github/ISSUE_TEMPLATE/ffmpeg-feature-request.md +++ b/.github/ISSUE_TEMPLATE/ffmpeg-feature-request.md @@ -2,7 +2,6 @@ name: FFmpeg feature request about: Request a feature of FFmpeg be exposed or supported by PyAV. title: "Allow FOO to BAR" -labels: enhancement assignees: '' --- @@ -51,11 +50,6 @@ Example: ``` {{ Complete output of `python setup.py config --verbose`. }} ``` -- FFmpeg: -``` -{{ Complete output of `ffmpeg -version` }} -``` - ## Additional context diff --git a/.github/ISSUE_TEMPLATE/pyav-feature-request.md b/.github/ISSUE_TEMPLATE/pyav-feature-request.md index 7433cbfbe..cd024d61d 100644 --- a/.github/ISSUE_TEMPLATE/pyav-feature-request.md +++ b/.github/ISSUE_TEMPLATE/pyav-feature-request.md @@ -2,7 +2,6 @@ name: PyAV feature request about: Request a feature of PyAV that is not provided by FFmpeg. title: "Allow FOO to BAR" -labels: enhancement assignees: '' --- @@ -10,11 +9,6 @@ assignees: '' **IMPORTANT:** Be sure to replace all template sections {{ like this }} or your issue may be discarded. -## Overview - -{{ A clear and concise description of what the feature is. }} - - ## Desired Behavior {{ A description of how you think PyAV should behave. }} diff --git a/.github/ISSUE_TEMPLATE/runtime-bug-report.md b/.github/ISSUE_TEMPLATE/runtime-bug-report.md index 267598841..9c8632a7d 100644 --- a/.github/ISSUE_TEMPLATE/runtime-bug-report.md +++ b/.github/ISSUE_TEMPLATE/runtime-bug-report.md @@ -2,7 +2,6 @@ name: Runtime bug report about: Report on an issue while running PyAV. title: "The FOO does not BAR." -labels: bug assignees: '' --- @@ -47,10 +46,8 @@ Traceback: ``` {{ Complete output of `python -m av --version`. If this command won't run, you are likely dealing with the build issue and should use the appropriate template. }} ``` -- PyAV build: -``` -{{ Complete output of `python setup.py config --verbose`. }} -``` +- [ ] I am/tried using the binary wheels +- [ ] I compiled from source ## Research From ee5c40e41ca8badf4a14c38794cb19647084f656 Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Wed, 11 Sep 2024 18:58:55 -0600 Subject: [PATCH 098/365] Allow `extradata` to be set for encoders --- av/codec/context.pyx | 3 --- av/container/output.pyx | 21 +++++++++++++++------ tests/test_codec_context.py | 6 +++--- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 66b21f9a3..e2e3632ee 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -189,9 +189,6 @@ cdef class CodecContext: @extradata.setter def extradata(self, data): - if not self.is_decoder: - raise ValueError("Can only set extradata for decoders.") - if data is None: lib.av_freep(&self.ptr.extradata) self.ptr.extradata_size = 0 diff --git a/av/container/output.pyx b/av/container/output.pyx index 00f5d55b3..717df8fb3 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -93,10 +93,14 @@ cdef class OutputContainer(Container): # Now lets set some more sane video defaults elif codec.type == lib.AVMEDIA_TYPE_VIDEO: codec_context.pix_fmt = lib.AV_PIX_FMT_YUV420P - codec_context.width = 640 - codec_context.height = 480 - codec_context.bit_rate = 1024000 - codec_context.bit_rate_tolerance = 128000 + codec_context.width = kwargs.pop("width", 640) + codec_context.height = kwargs.pop("height", 480) + codec_context.bit_rate = kwargs.pop("bit_rate", 1024000) + codec_context.bit_rate_tolerance = kwargs.pop("bit_rate_tolerance", 128000) + try: + to_avrational(kwargs.pop("time_base"), &codec_context.time_base) + except KeyError: + pass to_avrational(rate or 24, &codec_context.framerate) stream.avg_frame_rate = codec_context.framerate @@ -105,9 +109,14 @@ cdef class OutputContainer(Container): # Some sane audio defaults elif codec.type == lib.AVMEDIA_TYPE_AUDIO: codec_context.sample_fmt = codec.sample_fmts[0] - codec_context.bit_rate = 128000 - codec_context.bit_rate_tolerance = 32000 + codec_context.bit_rate = kwargs.pop("bit_rate", 128000) + codec_context.bit_rate_tolerance = kwargs.pop("bit_rate_tolerance", 32000) + try: + to_avrational(kwargs.pop("time_base"), &codec_context.time_base) + except KeyError: + pass codec_context.sample_rate = rate or 48000 + stream.time_base = codec_context.time_base lib.av_channel_layout_default(&codec_context.ch_layout, 2) # Some formats want stream headers to be separate diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 7ada318d9..634fe0280 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -105,9 +105,9 @@ def test_encoder_extradata(self): self.assertEqual(ctx.extradata, None) self.assertEqual(ctx.extradata_size, 0) - with self.assertRaises(ValueError) as cm: - ctx.extradata = b"123" - self.assertEqual(str(cm.exception), "Can only set extradata for decoders.") + ctx.extradata = b"123" + self.assertEqual(ctx.extradata, b"123") + self.assertEqual(ctx.extradata_size, 3) def test_encoder_pix_fmt(self): ctx = av.codec.Codec("h264", "w").create() From 00b914e5d38a5a8846696dd0959248ac77129d62 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 18 Sep 2024 00:27:42 -0400 Subject: [PATCH 099/365] Add audio atempo example --- examples/audio_atempo.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 examples/audio_atempo.py diff --git a/examples/audio_atempo.py b/examples/audio_atempo.py new file mode 100644 index 000000000..62a126705 --- /dev/null +++ b/examples/audio_atempo.py @@ -0,0 +1,36 @@ +import av + +av.logging.set_level(av.logging.VERBOSE) + +input_file = av.open("input.wav") +output_file = av.open("output.wav", mode="w") + +input_stream = input_file.streams.audio[0] + +output_stream = output_file.add_stream("pcm_s16le", rate=input_stream.rate) +assert isinstance(output_stream, av.audio.AudioStream) + +graph = av.filter.Graph() + +graph.link_nodes( + graph.add_abuffer(template=input_stream), + graph.add("atempo", "2.0"), + graph.add("abuffersink"), +).configure() + +for frame in input_file.decode(input_stream): + graph.push(frame) + while True: + try: + for packet in output_stream.encode(graph.pull()): + output_file.mux(packet) + except (av.BlockingIOError, av.EOFError): + break + +# Flush the stream +for packet in output_stream.encode(None): + output_file.mux(packet) + +# Close the files +input_file.close() +output_file.close() From d8819800d14255199915315fb4ce0f01ee8b94eb Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 18 Sep 2024 12:28:25 -0400 Subject: [PATCH 100/365] Add smart overloading for `add_stream()` --- av/container/output.pyi | 21 ++++++++++++++++++++- av/stream.pyi | 3 ++- av/video/format.pyi | 2 ++ tests/test_encode.py | 26 +++++++++----------------- 4 files changed, 33 insertions(+), 19 deletions(-) diff --git a/av/container/output.pyi b/av/container/output.pyi index 9e46b413a..8cbd5543b 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -1,13 +1,32 @@ from fractions import Fraction -from typing import Sequence +from typing import Literal, Sequence, overload +from av.audio.stream import AudioStream from av.packet import Packet from av.stream import Stream +from av.video.stream import VideoStream from .core import Container class OutputContainer(Container): def __enter__(self) -> OutputContainer: ... + @overload + def add_stream( + self, + codec_name: Literal["pcm_s16le", "aac", "mp3", "mp2"], + rate: Fraction | int | float | None = None, + template: None = None, + options: dict[str, str] | None = None, + ) -> AudioStream: ... + @overload + def add_stream( + self, + codec_name: Literal["h264", "mpeg4", "png"], + rate: Fraction | int | float | None = None, + template: None = None, + options: dict[str, str] | None = None, + ) -> VideoStream: ... + @overload def add_stream( self, codec_name: str | None = None, diff --git a/av/stream.pyi b/av/stream.pyi index 4c4c05030..212c721a9 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -1,7 +1,7 @@ from fractions import Fraction from typing import Literal -from .codec import CodecContext +from .codec import Codec, CodecContext from .container import Container from .enum import EnumItem from .frame import Frame @@ -15,6 +15,7 @@ class Stream: thread_type: Literal["NONE", "FRAME", "SLICE", "AUTO"] container: Container + codec: Codec codec_context: CodecContext metadata: dict[str, str] id: int diff --git a/av/video/format.pyi b/av/video/format.pyi index 16739a0e3..17c66de82 100644 --- a/av/video/format.pyi +++ b/av/video/format.pyi @@ -7,6 +7,8 @@ class VideoFormat: is_bit_stream: bool is_planar: bool is_rgb: bool + width: int + height: int def __init__(self, name: str, width: int = 0, height: int = 0) -> None: ... def chroma_width(self, luma_width: int = 0) -> int: ... diff --git a/tests/test_encode.py b/tests/test_encode.py index 58d3b1515..e7c3c02f3 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -1,7 +1,6 @@ import io import math from fractions import Fraction -from typing import cast from unittest import SkipTest import numpy as np @@ -27,7 +26,7 @@ def write_rgb_rotate(output: av.container.OutputContainer) -> None: output.metadata["title"] = "container" output.metadata["key"] = "value" - stream = cast(VideoStream, output.add_stream("mpeg4", 24)) + stream = output.add_stream("mpeg4", 24) stream.width = WIDTH stream.height = HEIGHT stream.pix_fmt = "yuv420p" @@ -66,15 +65,13 @@ def write_rgb_rotate(output: av.container.OutputContainer) -> None: output.mux(packet) -def assert_rgb_rotate(self, input_, is_dash=False): +def assert_rgb_rotate( + self, input_: av.container.InputContainer, is_dash: bool = False +) -> None: # Now inspect it a little. self.assertEqual(len(input_.streams), 1) if is_dash: - # FFmpeg 4.2 added parsing of the programme information and it is named "Title" - if av.library_versions["libavformat"] >= (58, 28): - self.assertTrue( - input_.metadata.get("Title") == "container", input_.metadata - ) + self.assertTrue(input_.metadata.get("Title") == "container", input_.metadata) else: self.assertEqual(input_.metadata.get("title"), "container", input_.metadata) self.assertEqual(input_.metadata.get("key"), None) @@ -90,19 +87,14 @@ def assert_rgb_rotate(self, input_, is_dash=False): expected_frames = 0 expected_id = 0 else: - if av.library_versions["libavformat"] < (58, 76): - # FFmpeg < 4.4 - expected_average_rate = Fraction(1152, 47) - expected_duration = 24064 - else: - # FFmpeg >= 4.4 - expected_average_rate = 24 - expected_duration = 24576 + expected_average_rate = 24 + expected_duration = 24576 expected_frames = 48 expected_id = 1 # actual stream properties self.assertIsInstance(stream, VideoStream) + assert isinstance(stream, VideoStream) self.assertEqual(stream.average_rate, expected_average_rate) self.assertEqual(stream.base_rate, 24) self.assertEqual(stream.duration, expected_duration) @@ -124,7 +116,7 @@ def assert_rgb_rotate(self, input_, is_dash=False): class TestBasicVideoEncoding(TestCase): - def test_default_options(self): + def test_default_options(self) -> None: with av.open(self.sandboxed("output.mov"), "w") as output: stream = output.add_stream("mpeg4") self.assertIn(stream, output.streams.video) From 2fd2210b157aebf248a2192b14cfeb74ee5c6be5 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 19 Sep 2024 21:54:10 -0400 Subject: [PATCH 101/365] Add type stubs for test_encode.py --- Makefile | 2 +- av/audio/codeccontext.pyi | 10 +++++++-- av/audio/stream.pyi | 12 +++++++--- av/container/output.pyi | 4 ++++ av/video/codeccontext.pyi | 1 + av/video/stream.pyi | 1 + tests/test_encode.py | 46 +++++++++++++++++++-------------------- 7 files changed, 46 insertions(+), 30 deletions(-) diff --git a/Makefile b/Makefile index 4adb3b47f..2dc617edf 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ fate-suite: rsync -vrltLW rsync://fate-suite.ffmpeg.org/fate-suite/ tests/assets/fate-suite/ lint: - $(PIP) install -U black isort flake8 flake8-pyproject pillow numpy mypy==1.10.0 + $(PIP) install -U black isort flake8 flake8-pyproject pillow numpy mypy==1.11.2 black --check av examples tests setup.py flake8 av examples tests isort --check-only --diff av examples tests diff --git a/av/audio/codeccontext.pyi b/av/audio/codeccontext.pyi index e11a42cdc..afd28ec34 100644 --- a/av/audio/codeccontext.pyi +++ b/av/audio/codeccontext.pyi @@ -11,10 +11,16 @@ class AudioCodecContext(CodecContext): frame_size: int sample_rate: int rate: int - layout: AudioLayout - format: AudioFormat type: Literal["audio"] + @property + def format(self) -> AudioFormat: ... + @format.setter + def format(self, value: AudioFormat | str) -> None: ... + @property + def layout(self) -> AudioLayout: ... + @layout.setter + def layout(self, value: AudioLayout | str) -> None: ... @property def channels(self) -> int: ... def encode(self, frame: AudioFrame | None = None) -> list[Packet]: ... diff --git a/av/audio/stream.pyi b/av/audio/stream.pyi index 9e972c56d..48a9d65c6 100644 --- a/av/audio/stream.pyi +++ b/av/audio/stream.pyi @@ -13,12 +13,18 @@ class AudioStream(Stream): # From codec context frame_size: int sample_rate: int + bit_rate: int rate: int channels: int - channel_layout: int - layout: AudioLayout - format: AudioFormat type: Literal["audio"] + @property + def format(self) -> AudioFormat: ... + @format.setter + def format(self, value: AudioFormat | str) -> None: ... + @property + def layout(self) -> AudioLayout: ... + @layout.setter + def layout(self, value: AudioLayout | str) -> None: ... def encode(self, frame: AudioFrame | None = None) -> list[Packet]: ... def decode(self, packet: Packet | None = None) -> list[AudioFrame]: ... diff --git a/av/container/output.pyi b/av/container/output.pyi index 8cbd5543b..a3218ae34 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -1,6 +1,7 @@ from fractions import Fraction from typing import Literal, Sequence, overload +from av.audio.layout import AudioLayout from av.audio.stream import AudioStream from av.packet import Packet from av.stream import Stream @@ -17,6 +18,7 @@ class OutputContainer(Container): rate: Fraction | int | float | None = None, template: None = None, options: dict[str, str] | None = None, + **kwargs, ) -> AudioStream: ... @overload def add_stream( @@ -25,6 +27,7 @@ class OutputContainer(Container): rate: Fraction | int | float | None = None, template: None = None, options: dict[str, str] | None = None, + **kwargs, ) -> VideoStream: ... @overload def add_stream( @@ -33,6 +36,7 @@ class OutputContainer(Container): rate: Fraction | int | float | None = None, template: Stream | None = None, options: dict[str, str] | None = None, + **kwargs, ) -> Stream: ... def start_encoding(self) -> None: ... def close(self) -> None: ... diff --git a/av/video/codeccontext.pyi b/av/video/codeccontext.pyi index 7bdda89e0..ebef8b275 100644 --- a/av/video/codeccontext.pyi +++ b/av/video/codeccontext.pyi @@ -19,6 +19,7 @@ class VideoCodecContext(CodecContext): sample_aspect_ratio: Fraction | None display_aspect_ratio: Fraction | None has_b_frames: bool + max_b_frames: int coded_width: int coded_height: int color_range: int diff --git a/av/video/stream.pyi b/av/video/stream.pyi index 929847ed4..94ffccd64 100644 --- a/av/video/stream.pyi +++ b/av/video/stream.pyi @@ -27,6 +27,7 @@ class VideoStream(Stream): rate: Fraction gop_size: int has_b_frames: bool + max_b_frames: int coded_width: int coded_height: int color_range: int diff --git a/tests/test_encode.py b/tests/test_encode.py index e7c3c02f3..f19ad709d 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -132,7 +132,7 @@ def test_default_options(self) -> None: self.assertEqual(stream.pix_fmt, "yuv420p") self.assertEqual(stream.width, 640) - def test_encoding(self): + def test_encoding(self) -> None: path = self.sandboxed("rgb_rotate.mov") with av.open(path, "w") as output: @@ -140,7 +140,7 @@ def test_encoding(self): with av.open(path) as input: assert_rgb_rotate(self, input) - def test_encoding_with_pts(self): + def test_encoding_with_pts(self) -> None: path = self.sandboxed("video_with_pts.mov") with av.open(path, "w") as output: @@ -163,7 +163,7 @@ def test_encoding_with_pts(self): self.assertEqual(packet.time_base, Fraction(1, 24)) output.mux(packet) - def test_encoding_with_unicode_filename(self): + def test_encoding_with_unicode_filename(self) -> None: path = self.sandboxed("¢∞§¶•ªº.mov") with av.open(path, "w") as output: @@ -173,7 +173,7 @@ def test_encoding_with_unicode_filename(self): class TestBasicAudioEncoding(TestCase): - def test_default_options(self): + def test_default_options(self) -> None: with av.open(self.sandboxed("output.mov"), "w") as output: stream = output.add_stream("mp2") self.assertIn(stream, output.streams.audio) @@ -184,7 +184,7 @@ def test_default_options(self): self.assertEqual(stream.format.name, "s16") self.assertEqual(stream.sample_rate, 48000) - def test_transcode(self): + def test_transcode(self) -> None: path = self.sandboxed("audio_transcode.mov") with av.open(path, "w") as output: @@ -199,10 +199,9 @@ def test_transcode(self): self.assertIn(stream, output.streams.audio) ctx = stream.codec_context - ctx.time_base = sample_rate ctx.sample_rate = sample_rate - ctx.format = sample_fmt - ctx.layout = channel_layout + stream.format = sample_fmt # type: ignore + ctx.layout = channel_layout # type: ignore with av.open( fate_suite("audio-reference/chorusnoise_2ch_44kHz_s16.wav") @@ -230,7 +229,7 @@ def test_transcode(self): class TestEncodeStreamSemantics(TestCase): - def test_stream_index(self): + def test_stream_index(self) -> None: with av.open(self.sandboxed("output.mov"), "w") as output: vstream = output.add_stream("mpeg4", 24) self.assertIn(vstream, output.streams.video) @@ -240,8 +239,8 @@ def test_stream_index(self): astream = output.add_stream("mp2", 48000) self.assertIn(astream, output.streams.audio) - astream.layout = "stereo" - astream.format = "s16" + astream.layout = "stereo" # type: ignore + astream.format = "s16" # type: ignore self.assertEqual(vstream.index, 0) self.assertEqual(astream.index, 1) @@ -268,7 +267,7 @@ def test_stream_index(self): self.assertIs(apacket.stream, astream) self.assertEqual(apacket.stream_index, 1) - def test_stream_audio_resample(self): + def test_stream_audio_resample(self) -> None: with av.open(self.sandboxed("output.mov"), "w") as output: vstream = output.add_stream("mpeg4", 24) vstream.pix_fmt = "yuv420p" @@ -299,7 +298,7 @@ def test_stream_audio_resample(self): self.assertEqual(apacket.pts, pts_expected.pop(0)) self.assertEqual(apacket.time_base, Fraction(1, 8000)) - def test_set_id_and_time_base(self): + def test_set_id_and_time_base(self) -> None: with av.open(self.sandboxed("output.mov"), "w") as output: stream = output.add_stream("mp2") self.assertIn(stream, output.streams.audio) @@ -315,7 +314,7 @@ def test_set_id_and_time_base(self): self.assertEqual(stream.time_base, Fraction(1, 48000)) -def encode_file_with_max_b_frames(max_b_frames): +def encode_file_with_max_b_frames(max_b_frames: int) -> io.BytesIO: """ Create an encoded video file (or file-like object) with the given maximum run of B frames. @@ -354,7 +353,7 @@ def encode_file_with_max_b_frames(max_b_frames): return file -def max_b_frame_run_in_file(file): +def max_b_frame_run_in_file(file: io.BytesIO) -> int: """ Count the maximum run of B frames in a file (or file-like object). @@ -363,18 +362,17 @@ def max_b_frame_run_in_file(file): Returns: non-negative integer which is the maximum B frame run length. """ - container = av.open(file) + container = av.open(file, "r") stream = container.streams.video[0] max_b_frame_run = 0 b_frame_run = 0 - for packet in container.demux(stream): - for frame in packet.decode(): - if frame.pict_type == av.video.frame.PictureType.B: - b_frame_run += 1 - else: - max_b_frame_run = max(max_b_frame_run, b_frame_run) - b_frame_run = 0 + for frame in container.decode(stream): + if frame.pict_type == av.video.frame.PictureType.B: + b_frame_run += 1 + else: + max_b_frame_run = max(max_b_frame_run, b_frame_run) + b_frame_run = 0 # Outside chance that the longest run was at the end of the file. max_b_frame_run = max(max_b_frame_run, b_frame_run) @@ -385,7 +383,7 @@ def max_b_frame_run_in_file(file): class TestMaxBFrameEncoding(TestCase): - def test_max_b_frames(self): + def test_max_b_frames(self) -> None: """ Test that we never get longer runs of B frames than we asked for with the max_b_frames property. From b074fe6faac5ecc56ea79fc8902511b6c320536b Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 19 Sep 2024 23:04:29 -0400 Subject: [PATCH 102/365] Add type stubs for test_audiofifo.py --- av/audio/fifo.pyi | 6 +++++ tests/common.py | 8 +++---- tests/test_audiofifo.py | 52 +++++++++++++++++++++-------------------- tests/test_encode.py | 2 +- 4 files changed, 38 insertions(+), 30 deletions(-) diff --git a/av/audio/fifo.pyi b/av/audio/fifo.pyi index aece9d381..085ed4bba 100644 --- a/av/audio/fifo.pyi +++ b/av/audio/fifo.pyi @@ -14,3 +14,9 @@ class AudioFifo: def sample_rate(self) -> int: ... @property def samples(self) -> int: ... + @property + def samples_written(self) -> int: ... + @property + def samples_read(self) -> int: ... + @property + def pts_per_sample(self) -> float: ... diff --git a/tests/common.py b/tests/common.py index 6c2b2d2e1..8c0db1129 100644 --- a/tests/common.py +++ b/tests/common.py @@ -57,7 +57,7 @@ def fate_png(): return fate_suite("png1/55c99e750a5fd6_50314226.png") -def sandboxed(*args, **kwargs): +def sandboxed(*args, **kwargs) -> str: do_makedirs = kwargs.pop("makedirs", True) base = kwargs.pop("sandbox", None) timed = kwargs.pop("timed", False) @@ -114,16 +114,16 @@ def _filter(self, type_): class TestCase(_Base): @classmethod - def _sandbox(cls, timed=True): + def _sandbox(cls, timed: bool = True) -> str: path = os.path.join(_sandbox(timed=timed), cls.__name__) makedirs(path) return path @property - def sandbox(self): + def sandbox(self) -> str: return self._sandbox(timed=True) - def sandboxed(self, *args, **kwargs): + def sandboxed(self, *args, **kwargs) -> str: kwargs.setdefault("sandbox", self.sandbox) kwargs.setdefault("timed", True) return sandboxed(*args, **kwargs) diff --git a/tests/test_audiofifo.py b/tests/test_audiofifo.py index 0cbb4acc4..2874c7145 100644 --- a/tests/test_audiofifo.py +++ b/tests/test_audiofifo.py @@ -1,10 +1,12 @@ +from fractions import Fraction + import av from .common import TestCase, fate_suite class TestAudioFifo(TestCase): - def test_data(self): + def test_data(self) -> None: container = av.open(fate_suite("audio-reference/chorusnoise_2ch_44kHz_s16.wav")) stream = container.streams.audio[0] @@ -13,23 +15,22 @@ def test_data(self): input_ = [] output = [] - for i, packet in enumerate(container.demux(stream)): - for frame in packet.decode(): - input_.append(bytes(frame.planes[0])) - fifo.write(frame) - for frame in fifo.read_many(512, partial=i == 10): - output.append(bytes(frame.planes[0])) + for i, frame in enumerate(container.decode(stream)): + input_.append(bytes(frame.planes[0])) + fifo.write(frame) + for frame in fifo.read_many(512, partial=i == 10): + output.append(bytes(frame.planes[0])) if i == 10: break - input_ = b"".join(input_) - output = b"".join(output) - min_len = min(len(input_), len(output)) + input_bytes = b"".join(input_) + output_bytes = b"".join(output) + min_len = min(len(input_bytes), len(output_bytes)) - self.assertTrue(min_len > 10 * 512 * 2 * 2) - self.assertTrue(input_[:min_len] == output[:min_len]) + assert min_len > 10 * 512 * 2 * 2 + assert input_bytes[:min_len] == output_bytes[:min_len] - def test_pts_simple(self): + def test_pts_simple(self) -> None: fifo = av.AudioFifo() # ensure __repr__ does not crash @@ -42,7 +43,7 @@ def test_pts_simple(self): iframe = av.AudioFrame(samples=1024) iframe.pts = 0 iframe.sample_rate = 48000 - iframe.time_base = "1/48000" + iframe.time_base = Fraction("1/48000") fifo.write(iframe) @@ -54,9 +55,9 @@ def test_pts_simple(self): ) oframe = fifo.read(512) - self.assertTrue(oframe is not None) - self.assertEqual(oframe.pts, 0) - self.assertEqual(oframe.time_base, iframe.time_base) + assert oframe is not None + assert oframe.pts == 0 + assert oframe.time_base == iframe.time_base self.assertEqual(fifo.samples_written, 1024) self.assertEqual(fifo.samples_read, 512) @@ -65,20 +66,21 @@ def test_pts_simple(self): iframe.pts = 1024 fifo.write(iframe) oframe = fifo.read(512) - self.assertTrue(oframe is not None) + assert oframe is not None + self.assertEqual(oframe.pts, 512) self.assertEqual(oframe.time_base, iframe.time_base) iframe.pts = 9999 # Wrong! self.assertRaises(ValueError, fifo.write, iframe) - def test_pts_complex(self): + def test_pts_complex(self) -> None: fifo = av.AudioFifo() iframe = av.AudioFrame(samples=1024) iframe.pts = 0 iframe.sample_rate = 48000 - iframe.time_base = "1/96000" + iframe.time_base = Fraction("1/96000") fifo.write(iframe) iframe.pts = 2048 @@ -89,23 +91,23 @@ def test_pts_complex(self): self.assertEqual(oframe.pts, 2048) self.assertEqual(fifo.pts_per_sample, 2.0) - def test_missing_sample_rate(self): + def test_missing_sample_rate(self) -> None: fifo = av.AudioFifo() iframe = av.AudioFrame(samples=1024) iframe.pts = 0 - iframe.time_base = "1/48000" + iframe.time_base = Fraction("1/48000") fifo.write(iframe) oframe = fifo.read(512) - self.assertTrue(oframe is not None) + assert oframe is not None self.assertIsNone(oframe.pts) self.assertEqual(oframe.sample_rate, 0) self.assertEqual(oframe.time_base, iframe.time_base) - def test_missing_time_base(self): + def test_missing_time_base(self) -> None: fifo = av.AudioFifo() iframe = av.AudioFrame(samples=1024) @@ -116,7 +118,7 @@ def test_missing_time_base(self): oframe = fifo.read(512) - self.assertTrue(oframe is not None) + assert oframe is not None self.assertIsNone(oframe.pts) self.assertIsNone(oframe.time_base) self.assertEqual(oframe.sample_rate, iframe.sample_rate) diff --git a/tests/test_encode.py b/tests/test_encode.py index f19ad709d..c77593c8a 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -220,8 +220,8 @@ def test_transcode(self) -> None: ) self.assertEqual(container.metadata.get("key"), None) + assert isinstance(container.streams[0], AudioStream) stream = container.streams[0] - self.assertIsInstance(stream, AudioStream) # codec context properties self.assertEqual(stream.format.name, "s16p") From 113ba78bb06b98ce8c5b16207069c29788d012dd Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 19 Sep 2024 23:47:17 -0400 Subject: [PATCH 103/365] Add type stubs for test_audioformat, audioframe --- tests/test_audioformat.py | 26 ++++---- tests/test_audioframe.py | 136 +++++++++++++++++++------------------- 2 files changed, 80 insertions(+), 82 deletions(-) diff --git a/tests/test_audioformat.py b/tests/test_audioformat.py index 5334b37d6..733761f57 100644 --- a/tests/test_audioformat.py +++ b/tests/test_audioformat.py @@ -8,20 +8,20 @@ class TestAudioFormats(TestCase): - def test_s16_inspection(self): + def test_s16_inspection(self) -> None: fmt = AudioFormat("s16") - self.assertEqual(fmt.name, "s16") - self.assertFalse(fmt.is_planar) - self.assertEqual(fmt.bits, 16) - self.assertEqual(fmt.bytes, 2) - self.assertEqual(fmt.container_name, "s16" + postfix) - self.assertEqual(fmt.planar.name, "s16p") - self.assertIs(fmt.packed, fmt) + assert fmt.name == "s16" + assert not fmt.is_planar + assert fmt.bits == 16 + assert fmt.bytes == 2 + assert fmt.container_name == "s16" + postfix + assert fmt.planar.name == "s16p" + assert fmt.packed is fmt - def test_s32p_inspection(self): + def test_s32p_inspection(self) -> None: fmt = AudioFormat("s32p") - self.assertEqual(fmt.name, "s32p") - self.assertTrue(fmt.is_planar) - self.assertEqual(fmt.bits, 32) - self.assertEqual(fmt.bytes, 4) + assert fmt.name == "s32p" + assert fmt.is_planar + assert fmt.bits == 32 + assert fmt.bytes == 4 self.assertRaises(ValueError, lambda: fmt.container_name) diff --git a/tests/test_audioframe.py b/tests/test_audioframe.py index 378665b78..4a382d18c 100644 --- a/tests/test_audioframe.py +++ b/tests/test_audioframe.py @@ -1,4 +1,4 @@ -import numpy +import numpy as np from av import AudioFrame @@ -76,114 +76,112 @@ class TestAudioFrameConveniences(TestCase): def test_basic_to_ndarray(self) -> None: frame = AudioFrame(format="s16p", layout="stereo", samples=160) array = frame.to_ndarray() - self.assertEqual(array.dtype, "i2") - self.assertEqual(array.shape, (2, 160)) + assert array.dtype == "i2" + assert array.shape == (2, 160) - def test_ndarray_dbl(self): + def test_ndarray_dbl(self) -> None: layouts = [ - ("dbl", "mono", "f8", (1, 160)), - ("dbl", "stereo", "f8", (1, 320)), - ("dblp", "mono", "f8", (1, 160)), - ("dblp", "stereo", "f8", (2, 160)), + ("dbl", "mono", (1, 160)), + ("dbl", "stereo", (1, 320)), + ("dblp", "mono", (1, 160)), + ("dblp", "stereo", (2, 160)), ] - for format, layout, dtype, size in layouts: - array = numpy.ndarray(shape=size, dtype=dtype) + for format, layout, size in layouts: + array = np.zeros(shape=size, dtype="f8") for i in range(size[0]): - array[i][:] = numpy.random.rand(size[1]) + array[i][:] = np.random.rand(size[1]) frame = AudioFrame.from_ndarray(array, format=format, layout=layout) - self.assertEqual(frame.format.name, format) - self.assertEqual(frame.layout.name, layout) - self.assertEqual(frame.samples, 160) + assert frame.format.name == format + assert frame.layout.name == layout + assert frame.samples == 160 self.assertNdarraysEqual(frame.to_ndarray(), array) - def test_from_ndarray_value_error(self): + def test_from_ndarray_value_error(self) -> None: # incorrect dtype - array = numpy.ndarray(shape=(1, 160), dtype="f2") + array = np.zeros(shape=(1, 160), dtype="f2") with self.assertRaises(ValueError) as cm: AudioFrame.from_ndarray(array, format="flt", layout="mono") - self.assertEqual( - str(cm.exception), - "Expected numpy array with dtype `float32` but got `float16`", + assert ( + str(cm.exception) + == "Expected numpy array with dtype `float32` but got `float16`" ) # incorrect number of dimensions - array = numpy.ndarray(shape=(1, 160, 2), dtype="f4") + array = np.zeros(shape=(1, 160, 2), dtype="f4") with self.assertRaises(ValueError) as cm: AudioFrame.from_ndarray(array, format="flt", layout="mono") - self.assertEqual( - str(cm.exception), "Expected numpy array with ndim `2` but got `3`" - ) + assert str(cm.exception) == "Expected numpy array with ndim `2` but got `3`" # incorrect shape - array = numpy.ndarray(shape=(2, 160), dtype="f4") + array = np.zeros(shape=(2, 160), dtype="f4") with self.assertRaises(ValueError) as cm: AudioFrame.from_ndarray(array, format="flt", layout="mono") - self.assertEqual(str(cm.exception), "Unexpected numpy array shape `(2, 160)`") + assert str(cm.exception) == "Unexpected numpy array shape `(2, 160)`" - def test_ndarray_flt(self): + def test_ndarray_flt(self) -> None: layouts = [ - ("flt", "mono", "f4", (1, 160)), - ("flt", "stereo", "f4", (1, 320)), - ("fltp", "mono", "f4", (1, 160)), - ("fltp", "stereo", "f4", (2, 160)), + ("flt", "mono", (1, 160)), + ("flt", "stereo", (1, 320)), + ("fltp", "mono", (1, 160)), + ("fltp", "stereo", (2, 160)), ] - for format, layout, dtype, size in layouts: - array = numpy.ndarray(shape=size, dtype=dtype) + for format, layout, size in layouts: + array: np.ndarray = np.zeros(shape=size, dtype="f4") for i in range(size[0]): - array[i][:] = numpy.random.rand(size[1]) + array[i][:] = np.random.rand(size[1]) frame = AudioFrame.from_ndarray(array, format=format, layout=layout) - self.assertEqual(frame.format.name, format) - self.assertEqual(frame.layout.name, layout) - self.assertEqual(frame.samples, 160) + assert frame.format.name == format + assert frame.layout.name == layout + assert frame.samples == 160 self.assertNdarraysEqual(frame.to_ndarray(), array) - def test_ndarray_s16(self): + def test_ndarray_s16(self) -> None: layouts = [ - ("s16", "mono", "i2", (1, 160)), - ("s16", "stereo", "i2", (1, 320)), - ("s16p", "mono", "i2", (1, 160)), - ("s16p", "stereo", "i2", (2, 160)), + ("s16", "mono", (1, 160)), + ("s16", "stereo", (1, 320)), + ("s16p", "mono", (1, 160)), + ("s16p", "stereo", (2, 160)), ] - for format, layout, dtype, size in layouts: - array = numpy.random.randint(0, 256, size=size, dtype=dtype) + for format, layout, size in layouts: + array = np.random.randint(0, 256, size=size, dtype="i2") frame = AudioFrame.from_ndarray(array, format=format, layout=layout) - self.assertEqual(frame.format.name, format) - self.assertEqual(frame.layout.name, layout) - self.assertEqual(frame.samples, 160) + assert frame.format.name == format + assert frame.layout.name == layout + assert frame.samples == 160 self.assertNdarraysEqual(frame.to_ndarray(), array) - def test_ndarray_s16p_align_8(self): + def test_ndarray_s16p_align_8(self) -> None: frame = AudioFrame(format="s16p", layout="stereo", samples=159, align=8) array = frame.to_ndarray() - self.assertEqual(array.dtype, "i2") - self.assertEqual(array.shape, (2, 159)) + assert array.dtype == "i2" + assert array.shape == (2, 159) - def test_ndarray_s32(self): + def test_ndarray_s32(self) -> None: layouts = [ - ("s32", "mono", "i4", (1, 160)), - ("s32", "stereo", "i4", (1, 320)), - ("s32p", "mono", "i4", (1, 160)), - ("s32p", "stereo", "i4", (2, 160)), + ("s32", "mono", (1, 160)), + ("s32", "stereo", (1, 320)), + ("s32p", "mono", (1, 160)), + ("s32p", "stereo", (2, 160)), ] - for format, layout, dtype, size in layouts: - array = numpy.random.randint(0, 256, size=size, dtype=dtype) + for format, layout, size in layouts: + array = np.random.randint(0, 256, size=size, dtype="i4") frame = AudioFrame.from_ndarray(array, format=format, layout=layout) - self.assertEqual(frame.format.name, format) - self.assertEqual(frame.layout.name, layout) - self.assertEqual(frame.samples, 160) + assert frame.format.name == format + assert frame.layout.name == layout + assert frame.samples == 160 self.assertNdarraysEqual(frame.to_ndarray(), array) - def test_ndarray_u8(self): + def test_ndarray_u8(self) -> None: layouts = [ - ("u8", "mono", "u1", (1, 160)), - ("u8", "stereo", "u1", (1, 320)), - ("u8p", "mono", "u1", (1, 160)), - ("u8p", "stereo", "u1", (2, 160)), + ("u8", "mono", (1, 160)), + ("u8", "stereo", (1, 320)), + ("u8p", "mono", (1, 160)), + ("u8p", "stereo", (2, 160)), ] - for format, layout, dtype, size in layouts: - array = numpy.random.randint(0, 256, size=size, dtype=dtype) + for format, layout, size in layouts: + array = np.random.randint(0, 256, size=size, dtype="u1") frame = AudioFrame.from_ndarray(array, format=format, layout=layout) - self.assertEqual(frame.format.name, format) - self.assertEqual(frame.layout.name, layout) - self.assertEqual(frame.samples, 160) + assert frame.format.name == format + assert frame.layout.name == layout + assert frame.samples == 160 self.assertNdarraysEqual(frame.to_ndarray(), array) From 07715a1b2e487ece4dbb7e93fc28fc35e9639c22 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 20 Sep 2024 00:19:41 -0400 Subject: [PATCH 104/365] Add type stubs, fixes for test_codec --- av/codec/codec.pyi | 4 +- tests/test_codec.py | 123 ++++++++++++++++++++------------------------ 2 files changed, 59 insertions(+), 68 deletions(-) diff --git a/av/codec/codec.pyi b/av/codec/codec.pyi index 572fa3149..f6f5c2e65 100644 --- a/av/codec/codec.pyi +++ b/av/codec/codec.pyi @@ -43,6 +43,7 @@ class Capabilities(EnumFlag): class UnknownCodecError(ValueError): ... class Codec: + is_encoder: bool is_decoder: bool descriptor: Descriptor name: str @@ -55,8 +56,9 @@ class Codec: audio_formats: list[AudioFormat] | None properties: Properties capabilities: Capabilities + delay: bool - def __init__(self, name: str, mode: Literal["r", "w"]) -> None: ... + def __init__(self, name: str, mode: Literal["r", "w"] = "r") -> None: ... def create(self) -> CodecContext: ... class codec_descriptor: diff --git a/tests/test_codec.py b/tests/test_codec.py index 5c0665e8d..357509b2d 100644 --- a/tests/test_codec.py +++ b/tests/test_codec.py @@ -1,110 +1,99 @@ -import unittest - -from av import AudioFormat, Codec, codecs_available +from av import AudioFormat, Codec, VideoFormat, codecs_available from av.codec.codec import UnknownCodecError from .common import TestCase -# some older ffmpeg versions have no native opus encoder -try: - opus_c = Codec("opus", "w") - opus_encoder_missing = False -except UnknownCodecError: - opus_encoder_missing = True - class TestCodecs(TestCase): - def test_codec_bogus(self): + def test_codec_bogus(self) -> None: with self.assertRaises(UnknownCodecError): Codec("bogus123") with self.assertRaises(UnknownCodecError): Codec("bogus123", "w") - def test_codec_mpeg4_decoder(self): + def test_codec_mpeg4_decoder(self) -> None: c = Codec("mpeg4") - self.assertEqual(c.name, "mpeg4") - self.assertEqual(c.long_name, "MPEG-4 part 2") - self.assertEqual(c.type, "video") - self.assertIn(c.id, (12, 13)) - self.assertTrue(c.is_decoder) - self.assertFalse(c.is_encoder) - self.assertTrue(c.delay) + assert c.name == "mpeg4" + assert c.long_name == "MPEG-4 part 2" + assert c.type == "video" + assert c.id in (12, 13) + assert c.is_decoder + assert not c.is_encoder + assert c.delay # audio - self.assertIsNone(c.audio_formats) - self.assertIsNone(c.audio_rates) + assert c.audio_formats is None + assert c.audio_rates is None # video # formats = c.video_formats - # self.assertTrue(formats) - # self.assertIsInstance(formats[0], VideoFormat) - # self.assertTrue(any(f.name == "yuv420p" for f in formats)) + # assert formats + # assert isinstance(formats[0], VideoFormat) + # assert any(f.name == "yuv420p" for f in formats) - self.assertIsNone(c.frame_rates) + assert c.frame_rates is None - def test_codec_mpeg4_encoder(self): + def test_codec_mpeg4_encoder(self) -> None: c = Codec("mpeg4", "w") - self.assertEqual(c.name, "mpeg4") - self.assertEqual(c.long_name, "MPEG-4 part 2") - self.assertEqual(c.type, "video") - self.assertIn(c.id, (12, 13)) - self.assertTrue(c.is_encoder) - self.assertFalse(c.is_decoder) - self.assertTrue(c.delay) + assert c.name == "mpeg4" + assert c.long_name == "MPEG-4 part 2" + assert c.type == "video" + assert c.id in (12, 13) + assert c.is_encoder + assert not c.is_decoder + assert c.delay # audio - self.assertIsNone(c.audio_formats) - self.assertIsNone(c.audio_rates) + assert c.audio_formats is None + assert c.audio_rates is None # video - # formats = c.video_formats - # self.assertTrue(formats) - # self.assertIsInstance(formats[0], VideoFormat) - # self.assertTrue(any(f.name == "yuv420p" for f in formats)) - - self.assertIsNone(c.frame_rates) + formats = c.video_formats + assert formats + assert isinstance(formats[0], VideoFormat) + assert any(f.name == "yuv420p" for f in formats) + assert c.frame_rates is None - def test_codec_opus_decoder(self): + def test_codec_opus_decoder(self) -> None: c = Codec("opus") self.assertEqual(c.name, "opus") self.assertEqual(c.long_name, "Opus") - self.assertEqual(c.type, "audio") - self.assertTrue(c.is_decoder) - self.assertFalse(c.is_encoder) - self.assertTrue(c.delay) + assert c.type == "audio" + assert c.is_decoder + assert not c.is_encoder + assert c.delay # audio - self.assertIsNone(c.audio_formats) - self.assertIsNone(c.audio_rates) + assert c.audio_formats is None + assert c.audio_rates is None # video - self.assertIsNone(c.video_formats) - self.assertIsNone(c.frame_rates) + assert c.video_formats is None + assert c.frame_rates is None - @unittest.skipIf(opus_encoder_missing, "Opus encoder is not available") - def test_codec_opus_encoder(self): + def test_codec_opus_encoder(self) -> None: c = Codec("opus", "w") - self.assertIn(c.name, ("opus", "libopus")) - self.assertIn(c.long_name, ("Opus", "libopus Opus")) - self.assertEqual(c.type, "audio") - self.assertTrue(c.is_encoder) - self.assertFalse(c.is_decoder) - self.assertTrue(c.delay) + assert c.name in ("opus", "libopus") + assert c.long_name in ("Opus", "libopus Opus") + assert c.type == "audio" + assert c.is_encoder + assert not c.is_decoder + assert c.delay # audio formats = c.audio_formats - self.assertTrue(formats) - self.assertIsInstance(formats[0], AudioFormat) - self.assertTrue(any(f.name in ["flt", "fltp"] for f in formats)) + assert formats + assert isinstance(formats[0], AudioFormat) + assert any(f.name in ("flt", "fltp") for f in formats) - self.assertIsNotNone(c.audio_rates) - self.assertIn(48000, c.audio_rates) + assert c.audio_rates is not None + assert 48000 in c.audio_rates # video - self.assertIsNone(c.video_formats) - self.assertIsNone(c.frame_rates) + assert c.video_formats is None + assert c.frame_rates is None - def test_codecs_available(self): - self.assertTrue(codecs_available) + def test_codecs_available(self) -> None: + assert codecs_available From 030632e2e24902eb4e61eb869999f4beaff9ae5e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 20 Sep 2024 01:58:33 -0400 Subject: [PATCH 105/365] Add boolean flags to Codec type stub --- av/codec/codec.pyi | 45 +++++++++++++++++++++- av/codec/codec.pyx | 7 +++- av/codec/context.pyi | 1 + tests/test_codec_context.py | 76 +++++++++++++++++++------------------ 4 files changed, 88 insertions(+), 41 deletions(-) diff --git a/av/codec/codec.pyi b/av/codec/codec.pyi index f6f5c2e65..6cb421430 100644 --- a/av/codec/codec.pyi +++ b/av/codec/codec.pyi @@ -1,9 +1,12 @@ from fractions import Fraction -from typing import Literal +from typing import Literal, overload +from av.audio.codeccontext import AudioCodecContext from av.audio.format import AudioFormat from av.descriptor import Descriptor from av.enum import EnumFlag +from av.subtitles.codeccontext import SubtitleCodecContext +from av.video.codeccontext import VideoCodecContext from av.video.format import VideoFormat from .context import CodecContext @@ -56,10 +59,48 @@ class Codec: audio_formats: list[AudioFormat] | None properties: Properties capabilities: Capabilities + intra_only: bool + lossy: bool + lossless: bool + reorder: bool + bitmap_sub: bool + text_sub: bool + draw_horiz_band: bool + dr1: bool + hwaccel: bool delay: bool + small_last_frame: bool + hwaccel_vdpau: bool + subframes: bool + experimental: bool + channel_conf: bool + neg_linesizes: bool + frame_threads: bool + slice_threads: bool + param_change: bool + auto_threads: bool + variable_frame_size: bool + avoid_probing: bool + hardware: bool + hybrid: bool + encoder_reordered_opaque: bool + encoder_flush: bool def __init__(self, name: str, mode: Literal["r", "w"] = "r") -> None: ... - def create(self) -> CodecContext: ... + @overload + def create(self, kind: Literal["video"]) -> VideoCodecContext: ... + @overload + def create(self, kind: Literal["audio"]) -> AudioCodecContext: ... + @overload + def create(self, kind: Literal["subtitle"]) -> SubtitleCodecContext: ... + @overload + def create(self, kind: None = None) -> CodecContext: ... + @overload + def create( + self, kind: Literal["video", "audio", "subtitle"] | None = None + ) -> ( + VideoCodecContext | AudioCodecContext | SubtitleCodecContext | CodecContext + ): ... class codec_descriptor: name: str diff --git a/av/codec/codec.pyx b/av/codec/codec.pyx index 9c4b7ed94..da66d7184 100644 --- a/av/codec/codec.pyx +++ b/av/codec/codec.pyx @@ -201,8 +201,11 @@ cdef class Codec: if self.is_encoder and lib.av_codec_is_decoder(self.ptr): raise RuntimeError("%s is both encoder and decoder.") - def create(self): - """Create a :class:`.CodecContext` for this codec.""" + def create(self, str kind = None): + """Create a :class:`.CodecContext` for this codec. + + :param str kind: Gives a hint to static type checkers for what exact CodecContext is used. + """ from .context import CodecContext return CodecContext.create(self) diff --git a/av/codec/context.pyi b/av/codec/context.pyi index d84b41de1..f193487a9 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -60,6 +60,7 @@ class CodecContext: is_encoder: bool is_decoder: bool name: str + codec: Codec options: dict[str, str] type: Literal["video", "audio", "data", "subtitle", "attachment"] profile: str | None diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 634fe0280..ad6247f2c 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os from fractions import Fraction from unittest import SkipTest @@ -10,12 +12,6 @@ from .common import TestCase, fate_suite -def iter_frames(container, stream): - for packet in container.demux(stream): - for frame in packet.decode(): - yield frame - - def iter_raw_frames(path, packet_sizes, ctx): with open(path, "rb") as f: for i, size in enumerate(packet_sizes): @@ -27,6 +23,7 @@ def iter_raw_frames(path, packet_sizes, ctx): break for frame in ctx.decode(packet): yield frame + while True: try: frames = ctx.decode(None) @@ -177,20 +174,20 @@ def _assert_parse(self, codec_name, path): class TestEncoding(TestCase): - def test_encoding_png(self): + def test_encoding_png(self) -> None: self.image_sequence_encode("png") - def test_encoding_mjpeg(self): + def test_encoding_mjpeg(self) -> None: self.image_sequence_encode("mjpeg") - def test_encoding_tiff(self): + def test_encoding_tiff(self) -> None: self.image_sequence_encode("tiff") - def image_sequence_encode(self, codec_name): + def image_sequence_encode(self, codec_name: str) -> None: try: codec = Codec(codec_name, "w") except UnknownCodecError: - raise SkipTest() + raise SkipTest(f"Unknown codec: {codec_name}") container = av.open(fate_suite("h264/interlaced_crop.mp4")) video_stream = container.streams.video[0] @@ -198,19 +195,22 @@ def image_sequence_encode(self, codec_name): width = 640 height = 480 - ctx = codec.create() + ctx = codec.create("video") + assert ctx.codec.video_formats pix_fmt = ctx.codec.video_formats[0].name ctx.width = width ctx.height = height + + assert video_stream.time_base is not None ctx.time_base = video_stream.time_base ctx.pix_fmt = pix_fmt ctx.open() frame_count = 1 path_list = [] - for frame in iter_frames(container, video_stream): + for frame in container.decode(video_stream): new_frame = frame.reformat(width, height, pix_fmt) new_packets = ctx.encode(new_frame) @@ -232,7 +232,7 @@ def image_sequence_encode(self, codec_name): if frame_count > 5: break - ctx = av.Codec(codec_name, "r").create() + ctx = av.Codec(codec_name, "r").create("video") for path in path_list: with open(path, "rb") as f: @@ -275,7 +275,7 @@ def video_encoding(self, codec_name, options={}, codec_tag=None): try: codec = Codec(codec_name, "w") except UnknownCodecError: - raise SkipTest() + raise SkipTest(f"Unknown codec: {codec_name}") container = av.open(fate_suite("h264/interlaced_crop.mp4")) video_stream = container.streams.video[0] @@ -299,12 +299,12 @@ def video_encoding(self, codec_name, options={}, codec_tag=None): ctx.codec_tag = codec_tag ctx.open() - path = self.sandboxed("encoder.%s" % codec_name) + path = self.sandboxed(f"encoder.{codec_name}") packet_sizes = [] frame_count = 0 with open(path, "wb") as f: - for frame in iter_frames(container, video_stream): + for frame in container.decode(video_stream): new_frame = frame.reformat(width, height, pix_fmt) # reset the picture type @@ -357,40 +357,42 @@ def video_encoding(self, codec_name, options={}, codec_tag=None): final_gop_size = decoded_frame_count - max(keyframe_indices) self.assertLessEqual(final_gop_size, gop_size) - def test_encoding_pcm_s24le(self): + def test_encoding_pcm_s24le(self) -> None: self.audio_encoding("pcm_s24le") - def test_encoding_aac(self): + def test_encoding_aac(self) -> None: self.audio_encoding("aac") - def test_encoding_mp2(self): + def test_encoding_mp2(self) -> None: self.audio_encoding("mp2") - maxDiff = None - - def audio_encoding(self, codec_name): + def audio_encoding(self, codec_name: str) -> None: self._audio_encoding(codec_name=codec_name, channel_layout="stereo") self._audio_encoding( codec_name=codec_name, channel_layout=AudioLayout("stereo") ) - def _audio_encoding(self, *, codec_name, channel_layout): + def _audio_encoding( + self, *, codec_name: str, channel_layout: str | AudioLayout + ) -> None: try: codec = Codec(codec_name, "w") except UnknownCodecError: - raise SkipTest() + raise SkipTest(f"Unknown codec: {codec_name}") + + ctx = codec.create(kind="audio") - ctx = codec.create() if ctx.codec.experimental: - raise SkipTest() + raise SkipTest(f"Experimental codec: {codec_name}") + assert ctx.codec.audio_formats sample_fmt = ctx.codec.audio_formats[-1].name sample_rate = 48000 ctx.time_base = Fraction(1) / sample_rate ctx.sample_rate = sample_rate - ctx.format = sample_fmt - ctx.layout = channel_layout + ctx.format = sample_fmt # type: ignore + ctx.layout = channel_layout # type: ignore ctx.open() @@ -399,32 +401,32 @@ def _audio_encoding(self, *, codec_name, channel_layout): container = av.open(fate_suite("audio-reference/chorusnoise_2ch_44kHz_s16.wav")) audio_stream = container.streams.audio[0] - path = self.sandboxed("encoder.%s" % codec_name) + path = self.sandboxed(f"encoder.{codec_name}") samples = 0 packet_sizes = [] with open(path, "wb") as f: - for frame in iter_frames(container, audio_stream): + for frame in container.decode(audio_stream): resampled_frames = resampler.resample(frame) for resampled_frame in resampled_frames: - self.assertEqual(resampled_frame.time_base, Fraction(1, 48000)) + assert resampled_frame.time_base == Fraction(1, 48000) samples += resampled_frame.samples for packet in ctx.encode(resampled_frame): - self.assertEqual(packet.time_base, Fraction(1, 48000)) + assert packet.time_base == Fraction(1, 48000) packet_sizes.append(packet.size) f.write(packet) for packet in ctx.encode(None): - self.assertEqual(packet.time_base, Fraction(1, 48000)) + assert packet.time_base == Fraction(1, 48000) packet_sizes.append(packet.size) f.write(packet) - ctx = Codec(codec_name, "r").create() + ctx = Codec(codec_name, "r").create("audio") ctx.sample_rate = sample_rate - ctx.format = sample_fmt - ctx.layout = channel_layout + ctx.format = sample_fmt # type: ignore + ctx.layout = channel_layout # type: ignore ctx.open() result_samples = 0 From d4745bd08e01461c9f9b705e7b30af69661969cf Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 23 Sep 2024 05:45:58 -0400 Subject: [PATCH 106/365] Disallow setting frame.time_base with string --- av/utils.pxd | 2 +- av/utils.pyx | 12 +---------- tests/test_audioresampler.py | 40 +++++++++++++++++------------------- tests/test_codec_context.py | 2 +- tests/test_videoframe.py | 3 ++- 5 files changed, 24 insertions(+), 35 deletions(-) diff --git a/av/utils.pxd b/av/utils.pxd index 125272c5c..7f342c05b 100644 --- a/av/utils.pxd +++ b/av/utils.pxd @@ -7,7 +7,7 @@ cdef dict_to_avdict(lib.AVDictionary **dst, dict src, str encoding, str errors) cdef object avrational_to_fraction(const lib.AVRational *input) -cdef object to_avrational(object value, lib.AVRational *input) +cdef void to_avrational(object frac, lib.AVRational *input) cdef check_ndarray(object array, object dtype, int ndim) diff --git a/av/utils.pyx b/av/utils.pyx index cc01925de..afc82d24d 100644 --- a/av/utils.pyx +++ b/av/utils.pyx @@ -47,17 +47,7 @@ cdef object avrational_to_fraction(const lib.AVRational *input): return Fraction(input.num, input.den) -cdef object to_avrational(object value, lib.AVRational *input): - if value is None: - input.num = 0 - input.den = 1 - return - - if isinstance(value, Fraction): - frac = value - else: - frac = Fraction(value) - +cdef void to_avrational(object frac, lib.AVRational *input): input.num = frac.numerator input.den = frac.denominator diff --git a/tests/test_audioresampler.py b/tests/test_audioresampler.py index 58185f9d1..47dd1ddc3 100644 --- a/tests/test_audioresampler.py +++ b/tests/test_audioresampler.py @@ -1,12 +1,13 @@ from fractions import Fraction +import av from av import AudioFrame, AudioResampler from .common import TestCase class TestAudioResampler(TestCase): - def test_flush_immediately(self): + def test_flush_immediately(self) -> None: """ If we flush the resampler before passing any input, it returns a `None` frame without setting up the graph. @@ -18,7 +19,7 @@ def test_flush_immediately(self): oframes = resampler.resample(None) self.assertEqual(len(oframes), 0) - def test_identity_passthrough(self): + def test_identity_passthrough(self) -> None: """ If we don't ask it to do anything, it won't. """ @@ -37,13 +38,13 @@ def test_identity_passthrough(self): oframes = resampler.resample(iframe) self.assertEqual(len(oframes), 1) - self.assertIs(iframe, oframes[0]) + assert iframe is oframes[0] # flush oframes = resampler.resample(None) self.assertEqual(len(oframes), 0) - def test_matching_passthrough(self): + def test_matching_passthrough(self) -> None: """ If the frames match, it won't do anything. """ @@ -68,9 +69,7 @@ def test_matching_passthrough(self): oframes = resampler.resample(None) self.assertEqual(len(oframes), 0) - def test_pts_assertion_same_rate(self): - import av - + def test_pts_assertion_same_rate(self) -> None: av.logging.set_level(av.logging.VERBOSE) resampler = AudioResampler("s16", "mono") @@ -117,13 +116,13 @@ def test_pts_assertion_same_rate(self): self.assertEqual(len(oframes), 0) av.logging.set_level(None) - def test_pts_assertion_new_rate_up(self): + def test_pts_assertion_new_rate_up(self) -> None: resampler = AudioResampler("s16", "mono", 44100) # resample one frame iframe = AudioFrame("s16", "stereo", 1024) iframe.sample_rate = 48000 - iframe.time_base = "1/48000" + iframe.time_base = Fraction(1, 48000) iframe.pts = 0 oframes = resampler.resample(iframe) @@ -137,7 +136,7 @@ def test_pts_assertion_new_rate_up(self): iframe = AudioFrame("s16", "stereo", 1024) iframe.sample_rate = 48000 - iframe.time_base = "1/48000" + iframe.time_base = Fraction(1, 48000) iframe.pts = 1024 oframes = resampler.resample(iframe) @@ -159,13 +158,13 @@ def test_pts_assertion_new_rate_up(self): self.assertEqual(oframe.sample_rate, 44100) self.assertEqual(oframe.samples, 15) - def test_pts_assertion_new_rate_down(self): + def test_pts_assertion_new_rate_down(self) -> None: resampler = AudioResampler("s16", "mono", 48000) # resample one frame iframe = AudioFrame("s16", "stereo", 1024) iframe.sample_rate = 44100 - iframe.time_base = "1/44100" + iframe.time_base = Fraction(1, 44100) iframe.pts = 0 oframes = resampler.resample(iframe) @@ -179,7 +178,7 @@ def test_pts_assertion_new_rate_down(self): iframe = AudioFrame("s16", "stereo", 1024) iframe.sample_rate = 44100 - iframe.time_base = "1/44100" + iframe.time_base = Fraction(1, 44100) iframe.pts = 1024 oframes = resampler.resample(iframe) @@ -201,13 +200,13 @@ def test_pts_assertion_new_rate_down(self): self.assertEqual(oframe.sample_rate, 48000) self.assertEqual(oframe.samples, 18) - def test_pts_assertion_new_rate_fltp(self): + def test_pts_assertion_new_rate_fltp(self) -> None: resampler = AudioResampler("fltp", "mono", 8000, 1024) # resample one frame iframe = AudioFrame("s16", "mono", 1024) iframe.sample_rate = 8000 - iframe.time_base = "1/1000" + iframe.time_base = Fraction(1, 1000) iframe.pts = 0 oframes = resampler.resample(iframe) @@ -221,7 +220,7 @@ def test_pts_assertion_new_rate_fltp(self): iframe = AudioFrame("s16", "mono", 1024) iframe.sample_rate = 8000 - iframe.time_base = "1/1000" + iframe.time_base = Fraction(1, 1000) iframe.pts = 8192 oframes = resampler.resample(iframe) @@ -237,7 +236,7 @@ def test_pts_assertion_new_rate_fltp(self): oframes = resampler.resample(None) self.assertEqual(len(oframes), 0) - def test_pts_missing_time_base(self): + def test_pts_missing_time_base(self) -> None: resampler = AudioResampler("s16", "mono", 44100) # resample one frame @@ -263,7 +262,7 @@ def test_pts_missing_time_base(self): self.assertEqual(oframe.sample_rate, 44100) self.assertEqual(oframe.samples, 16) - def test_mismatched_input(self): + def test_mismatched_input(self) -> None: """ Consecutive frames must have the same layout, sample format and sample rate. """ @@ -279,6 +278,5 @@ def test_mismatched_input(self): iframe.sample_rate = 48000 with self.assertRaises(ValueError) as cm: resampler.resample(iframe) - self.assertEqual( - str(cm.exception), "Frame does not match AudioResampler setup." - ) + + assert str(cm.exception) == "Frame does not match AudioResampler setup." diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index ad6247f2c..12997d76a 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -266,7 +266,7 @@ def test_encoding_dnxhd(self): "pix_fmt": "yuv422p", "width": 1920, "height": 1080, - "time_base": "1001/30000", + "time_base": Fraction(1001, 30_000), "max_frames": 5, } self.video_encoding("dnxhd", options) diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 28540ccc3..9dff66d7f 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -1,3 +1,4 @@ +from fractions import Fraction from unittest import SkipTest import numpy @@ -595,7 +596,7 @@ class TestVideoFrameTiming(TestCase): def test_reformat_pts(self): frame = VideoFrame(640, 480, "rgb24") frame.pts = 123 - frame.time_base = "456/1" # Just to be different. + frame.time_base = Fraction("456/1") frame = frame.reformat(320, 240) self.assertEqual(frame.pts, 123) self.assertEqual(frame.time_base, 456) From 68d6a992b8446881ade7f3be9c47a1b1f4542a90 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 23 Sep 2024 06:16:22 -0400 Subject: [PATCH 107/365] Prefer asserts because mypy understands them --- tests/test_encode.py | 98 +++++------ tests/test_file_probing.py | 320 ++++++++++++++++++------------------ tests/test_filters.py | 62 +++---- tests/test_logging.py | 6 +- tests/test_options.py | 14 +- tests/test_packet.py | 33 ++-- tests/test_python_io.py | 43 +++-- tests/test_seek.py | 12 +- tests/test_subtitles.py | 56 ++++--- tests/test_videoframe.py | 324 ++++++++++++++++++------------------- 10 files changed, 478 insertions(+), 490 deletions(-) diff --git a/tests/test_encode.py b/tests/test_encode.py index c77593c8a..79c8423bf 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -69,7 +69,7 @@ def assert_rgb_rotate( self, input_: av.container.InputContainer, is_dash: bool = False ) -> None: # Now inspect it a little. - self.assertEqual(len(input_.streams), 1) + assert len(input_.streams) == 1 if is_dash: self.assertTrue(input_.metadata.get("Title") == "container", input_.metadata) else: @@ -81,7 +81,7 @@ def assert_rgb_rotate( if is_dash: # The DASH format doesn't provide a duration for the stream # and so the container duration (micro seconds) is checked instead - self.assertEqual(input_.duration, 2000000) + assert input_.duration == 2000000 expected_average_rate = 24 expected_duration = None expected_frames = 0 @@ -95,24 +95,24 @@ def assert_rgb_rotate( # actual stream properties self.assertIsInstance(stream, VideoStream) assert isinstance(stream, VideoStream) - self.assertEqual(stream.average_rate, expected_average_rate) - self.assertEqual(stream.base_rate, 24) - self.assertEqual(stream.duration, expected_duration) - self.assertEqual(stream.guessed_rate, 24) - self.assertEqual(stream.frames, expected_frames) - self.assertEqual(stream.id, expected_id) - self.assertEqual(stream.index, 0) - self.assertEqual(stream.profile, "Simple Profile") - self.assertEqual(stream.start_time, 0) - self.assertEqual(stream.time_base, Fraction(1, 12288)) - self.assertEqual(stream.type, "video") + assert stream.average_rate == expected_average_rate + assert stream.base_rate == 24 + assert stream.duration == expected_duration + assert stream.guessed_rate == 24 + assert stream.frames == expected_frames + assert stream.id == expected_id + assert stream.index == 0 + assert stream.profile == "Simple Profile" + assert stream.start_time == 0 + assert stream.time_base == Fraction(1, 12288) + assert stream.type == "video" # codec context properties - self.assertEqual(stream.codec.name, "mpeg4") - self.assertEqual(stream.codec.long_name, "MPEG-4 part 2") - self.assertEqual(stream.format.name, "yuv420p") - self.assertEqual(stream.format.width, WIDTH) - self.assertEqual(stream.format.height, HEIGHT) + assert stream.codec.name == "mpeg4" + assert stream.codec.long_name == "MPEG-4 part 2" + assert stream.format.name == "yuv420p" + assert stream.format.width == WIDTH + assert stream.format.height == HEIGHT class TestBasicVideoEncoding(TestCase): @@ -120,17 +120,17 @@ def test_default_options(self) -> None: with av.open(self.sandboxed("output.mov"), "w") as output: stream = output.add_stream("mpeg4") self.assertIn(stream, output.streams.video) - self.assertEqual(stream.average_rate, Fraction(24, 1)) - self.assertEqual(stream.time_base, None) + assert stream.average_rate == Fraction(24, 1) + assert stream.time_base is None # codec context properties - self.assertEqual(stream.bit_rate, 1024000) - self.assertEqual(stream.format.height, 480) - self.assertEqual(stream.format.name, "yuv420p") - self.assertEqual(stream.format.width, 640) - self.assertEqual(stream.height, 480) - self.assertEqual(stream.pix_fmt, "yuv420p") - self.assertEqual(stream.width, 640) + assert stream.bit_rate == 1024000 + assert stream.format.height == 480 + assert stream.format.name == "yuv420p" + assert stream.format.width == 640 + assert stream.height == 480 + assert stream.pix_fmt == "yuv420p" + assert stream.width == 640 def test_encoding(self) -> None: path = self.sandboxed("rgb_rotate.mov") @@ -156,11 +156,11 @@ def test_encoding_with_pts(self) -> None: frame.time_base = Fraction(1, 48000) for packet in stream.encode(frame): - self.assertEqual(packet.time_base, Fraction(1, 24)) + assert packet.time_base == Fraction(1, 24) output.mux(packet) for packet in stream.encode(None): - self.assertEqual(packet.time_base, Fraction(1, 24)) + assert packet.time_base == Fraction(1, 24) output.mux(packet) def test_encoding_with_unicode_filename(self) -> None: @@ -177,12 +177,12 @@ def test_default_options(self) -> None: with av.open(self.sandboxed("output.mov"), "w") as output: stream = output.add_stream("mp2") self.assertIn(stream, output.streams.audio) - self.assertEqual(stream.time_base, None) + assert stream.time_base is None # codec context properties - self.assertEqual(stream.bit_rate, 128000) - self.assertEqual(stream.format.name, "s16") - self.assertEqual(stream.sample_rate, 48000) + assert stream.bit_rate == 128000 + assert stream.format.name == "s16" + assert stream.sample_rate == 48000 def test_transcode(self) -> None: path = self.sandboxed("audio_transcode.mov") @@ -214,18 +214,18 @@ def test_transcode(self) -> None: output.mux(packet) with av.open(path) as container: - self.assertEqual(len(container.streams), 1) + assert len(container.streams) == 1 self.assertEqual( container.metadata.get("title"), "container", container.metadata ) - self.assertEqual(container.metadata.get("key"), None) + assert container.metadata.get("key") is None assert isinstance(container.streams[0], AudioStream) stream = container.streams[0] # codec context properties - self.assertEqual(stream.format.name, "s16p") - self.assertEqual(stream.sample_rate, sample_rate) + assert stream.format.name == "s16p" + assert stream.sample_rate == sample_rate class TestEncodeStreamSemantics(TestCase): @@ -242,14 +242,14 @@ def test_stream_index(self) -> None: astream.layout = "stereo" # type: ignore astream.format = "s16" # type: ignore - self.assertEqual(vstream.index, 0) - self.assertEqual(astream.index, 1) + assert vstream.index == 0 + assert astream.index == 1 vframe = VideoFrame(320, 240, "yuv420p") vpacket = vstream.encode(vframe)[0] self.assertIs(vpacket.stream, vstream) - self.assertEqual(vpacket.stream_index, 0) + assert vpacket.stream_index == 0 for i in range(10): if astream.frame_size != 0: @@ -265,7 +265,7 @@ def test_stream_index(self) -> None: break self.assertIs(apacket.stream, astream) - self.assertEqual(apacket.stream_index, 1) + assert apacket.stream_index == 1 def test_stream_audio_resample(self) -> None: with av.open(self.sandboxed("output.mov"), "w") as output: @@ -289,14 +289,14 @@ def test_stream_audio_resample(self) -> None: apackets = astream.encode(aframe) if apackets: apacket = apackets[0] - self.assertEqual(apacket.pts, pts_expected.pop(0)) - self.assertEqual(apacket.time_base, Fraction(1, 8000)) + assert apacket.pts == pts_expected.pop(0) + assert apacket.time_base == Fraction(1, 8000) apackets = astream.encode(None) if apackets: apacket = apackets[0] - self.assertEqual(apacket.pts, pts_expected.pop(0)) - self.assertEqual(apacket.time_base, Fraction(1, 8000)) + assert apacket.pts == pts_expected.pop(0) + assert apacket.time_base == Fraction(1, 8000) def test_set_id_and_time_base(self) -> None: with av.open(self.sandboxed("output.mov"), "w") as output: @@ -304,14 +304,14 @@ def test_set_id_and_time_base(self) -> None: self.assertIn(stream, output.streams.audio) # set id - self.assertEqual(stream.id, 0) + assert stream.id == 0 stream.id = 1 - self.assertEqual(stream.id, 1) + assert stream.id == 1 # set time_base - self.assertEqual(stream.time_base, None) + assert stream.time_base is None stream.time_base = Fraction(1, 48000) - self.assertEqual(stream.time_base, Fraction(1, 48000)) + assert stream.time_base == Fraction(1, 48000) def encode_file_with_max_b_frames(max_b_frames: int) -> io.BytesIO: diff --git a/tests/test_file_probing.py b/tests/test_file_probing.py index a4e35a1e1..aab232804 100644 --- a/tests/test_file_probing.py +++ b/tests/test_file_probing.py @@ -10,17 +10,17 @@ def setUp(self): self.file = av.open(fate_suite("aac/latm_stereo_to_51.ts")) def test_container_probing(self): - self.assertEqual(self.file.bit_rate, 269558) - self.assertEqual(self.file.duration, 6165333) - self.assertEqual(str(self.file.format), "") - self.assertEqual(self.file.format.name, "mpegts") + assert self.file.bit_rate == 269558 + assert self.file.duration == 6165333 + assert str(self.file.format) == "" + assert self.file.format.name == "mpegts" self.assertEqual( self.file.format.long_name, "MPEG-TS (MPEG-2 Transport Stream)" ) - self.assertEqual(self.file.metadata, {}) - self.assertEqual(self.file.size, 207740) - self.assertEqual(self.file.start_time, 1400000) - self.assertEqual(len(self.file.streams), 1) + assert self.file.metadata == {} + assert self.file.size == 207740 + assert self.file.start_time == 1400000 + assert len(self.file.streams) == 1 def test_stream_probing(self): stream = self.file.streams[0] @@ -33,29 +33,29 @@ def test_stream_probing(self): ) # actual stream properties - self.assertEqual(stream.duration, 554880) - self.assertEqual(stream.frames, 0) - self.assertEqual(stream.id, 256) - self.assertEqual(stream.index, 0) - self.assertEqual(stream.language, "eng") - self.assertEqual(stream.metadata, {"language": "eng"}) - self.assertEqual(stream.profile, "LC") - self.assertEqual(stream.start_time, 126000) - self.assertEqual(stream.time_base, Fraction(1, 90000)) - self.assertEqual(stream.type, "audio") + assert stream.duration == 554880 + assert stream.frames == 0 + assert stream.id == 256 + assert stream.index == 0 + assert stream.language == "eng" + assert stream.metadata == {"language": "eng"} + assert stream.profile == "LC" + assert stream.start_time == 126000 + assert stream.time_base == Fraction(1, 90000) + assert stream.type == "audio" # codec context properties - self.assertEqual(stream.bit_rate, None) - self.assertEqual(stream.channels, 2) - self.assertEqual(stream.codec.name, "aac_latm") + assert stream.bit_rate is None + assert stream.channels == 2 + assert stream.codec.name == "aac_latm" self.assertEqual( stream.codec.long_name, "AAC LATM (Advanced Audio Coding LATM syntax)" ) - self.assertEqual(stream.format.bits, 32) - self.assertEqual(stream.format.name, "fltp") - self.assertEqual(stream.layout.name, "stereo") - self.assertEqual(stream.max_bit_rate, None) - self.assertEqual(stream.sample_rate, 48000) + assert stream.format.bits == 32 + assert stream.format.name == "fltp" + assert stream.layout.name == "stereo" + assert stream.max_bit_rate is None + assert stream.sample_rate == 48000 class TestAudioProbeCorrupt(TestCase): @@ -68,15 +68,15 @@ def setUp(self): self.file = av.open(path) def test_container_probing(self): - self.assertEqual(self.file.bit_rate, 0) - self.assertEqual(self.file.duration, None) - self.assertEqual(str(self.file.format), "") - self.assertEqual(self.file.format.name, "flac") - self.assertEqual(self.file.format.long_name, "raw FLAC") - self.assertEqual(self.file.metadata, {}) - self.assertEqual(self.file.size, 0) - self.assertEqual(self.file.start_time, None) - self.assertEqual(len(self.file.streams), 1) + assert self.file.bit_rate == 0 + assert self.file.duration is None + assert str(self.file.format) == "" + assert self.file.format.name == "flac" + assert self.file.format.long_name == "raw FLAC" + assert self.file.metadata == {} + assert self.file.size == 0 + assert self.file.start_time is None + assert len(self.file.streams) == 1 def test_stream_probing(self): stream = self.file.streams[0] @@ -89,26 +89,26 @@ def test_stream_probing(self): ) # actual stream properties - self.assertEqual(stream.duration, None) - self.assertEqual(stream.frames, 0) - self.assertEqual(stream.id, 0) - self.assertEqual(stream.index, 0) - self.assertEqual(stream.language, None) - self.assertEqual(stream.metadata, {}) - self.assertEqual(stream.profile, None) - self.assertEqual(stream.start_time, None) - self.assertEqual(stream.time_base, Fraction(1, 90000)) - self.assertEqual(stream.type, "audio") + assert stream.duration is None + assert stream.frames == 0 + assert stream.id == 0 + assert stream.index == 0 + assert stream.language is None + assert stream.metadata == {} + assert stream.profile is None + assert stream.start_time is None + assert stream.time_base == Fraction(1, 90000) + assert stream.type == "audio" # codec context properties - self.assertEqual(stream.bit_rate, None) - self.assertEqual(stream.codec.name, "flac") - self.assertEqual(stream.codec.long_name, "FLAC (Free Lossless Audio Codec)") - self.assertEqual(stream.channels, 0) - self.assertEqual(stream.format, None) - self.assertEqual(stream.layout.name, "0 channels") - self.assertEqual(stream.max_bit_rate, None) - self.assertEqual(stream.sample_rate, 0) + assert stream.bit_rate is None + assert stream.codec.name == "flac" + assert stream.codec.long_name == "FLAC (Free Lossless Audio Codec)" + assert stream.channels == 0 + assert stream.format is None + assert stream.layout.name == "0 channels" + assert stream.max_bit_rate is None + assert stream.sample_rate == 0 class TestDataProbe(TestCase): @@ -116,14 +116,14 @@ def setUp(self): self.file = av.open(fate_suite("mxf/track_01_v02.mxf")) def test_container_probing(self): - self.assertEqual(self.file.bit_rate, 27872687) - self.assertEqual(self.file.duration, 417083) - self.assertEqual(str(self.file.format), "") - self.assertEqual(self.file.format.name, "mxf") - self.assertEqual(self.file.format.long_name, "MXF (Material eXchange Format)") - self.assertEqual(self.file.size, 1453153) - self.assertEqual(self.file.start_time, 0) - self.assertEqual(len(self.file.streams), 4) + assert self.file.bit_rate == 27872687 + assert self.file.duration == 417083 + assert str(self.file.format) == "" + assert self.file.format.name == "mxf" + assert self.file.format.long_name == "MXF (Material eXchange Format)" + assert self.file.size == 1453153 + assert self.file.start_time == 0 + assert len(self.file.streams) == 4 for key, value, min_version in ( ("application_platform", "AAFSDK (MacOS X)", None), @@ -159,20 +159,18 @@ def test_container_probing(self): for name, version in min_version.items() ): continue - self.assertEqual(self.file.metadata.get(key), value) + assert self.file.metadata.get(key) == value def test_stream_probing(self): stream = self.file.streams[0] - # check __repr__ - self.assertTrue(str(stream).startswith(" at ")) + assert str(stream).startswith(" at ") - # actual stream properties - self.assertEqual(stream.duration, 37537) - self.assertEqual(stream.frames, 0) - self.assertEqual(stream.id, 1) - self.assertEqual(stream.index, 0) - self.assertEqual(stream.language, None) + assert stream.duration == 37537 + assert stream.frames == 0 + assert stream.id == 1 + assert stream.index == 0 + assert stream.language is None self.assertEqual( stream.metadata, { @@ -181,10 +179,10 @@ def test_stream_probing(self): "track_name": "Base", }, ) - self.assertEqual(stream.profile, None) - self.assertEqual(stream.start_time, 0) - self.assertEqual(stream.time_base, Fraction(1, 90000)) - self.assertEqual(stream.type, "data") + assert stream.profile is None + assert stream.start_time == 0 + assert stream.time_base == Fraction(1, 90000) + assert stream.type == "data" self.assertEqual(hasattr(stream, "codec"), False) @@ -193,13 +191,13 @@ def setUp(self): self.file = av.open(fate_suite("sub/MovText_capability_tester.mp4")) def test_container_probing(self): - self.assertEqual(self.file.bit_rate, 810) - self.assertEqual(self.file.duration, 8140000) + assert self.file.bit_rate == 810 + assert self.file.duration == 8140000 self.assertEqual( str(self.file.format), "" ) - self.assertEqual(self.file.format.name, "mov,mp4,m4a,3gp,3g2,mj2") - self.assertEqual(self.file.format.long_name, "QuickTime / MOV") + assert self.file.format.name == "mov,mp4,m4a,3gp,3g2,mj2" + assert self.file.format.long_name == "QuickTime / MOV" self.assertEqual( self.file.metadata, { @@ -209,9 +207,9 @@ def test_container_probing(self): "minor_version": "1", }, ) - self.assertEqual(self.file.size, 825) - self.assertEqual(self.file.start_time, None) - self.assertEqual(len(self.file.streams), 1) + assert self.file.size == 825 + assert self.file.start_time is None + assert len(self.file.streams) == 1 def test_stream_probing(self): stream = self.file.streams[0] @@ -222,11 +220,11 @@ def test_stream_probing(self): ) # actual stream properties - self.assertEqual(stream.duration, 8140) - self.assertEqual(stream.frames, 6) - self.assertEqual(stream.id, 1) - self.assertEqual(stream.index, 0) - self.assertEqual(stream.language, "und") + assert stream.duration == 8140 + assert stream.frames == 6 + assert stream.id == 1 + assert stream.index == 0 + assert stream.language == "und" self.assertEqual( stream.metadata, { @@ -235,14 +233,14 @@ def test_stream_probing(self): "language": "und", }, ) - self.assertEqual(stream.profile, None) - self.assertEqual(stream.start_time, None) - self.assertEqual(stream.time_base, Fraction(1, 1000)) - self.assertEqual(stream.type, "subtitle") + assert stream.profile is None + assert stream.start_time is None + assert stream.time_base == Fraction(1, 1000) + assert stream.type == "subtitle" # codec context properties - self.assertEqual(stream.codec.name, "mov_text") - self.assertEqual(stream.codec.long_name, "3GPP Timed Text subtitle") + assert stream.codec.name == "mov_text" + assert stream.codec.long_name == "3GPP Timed Text subtitle" class TestVideoProbe(TestCase): @@ -250,17 +248,15 @@ def setUp(self): self.file = av.open(fate_suite("mpeg2/mpeg2_field_encoding.ts")) def test_container_probing(self): - self.assertEqual(self.file.bit_rate, 3950617) - self.assertEqual(self.file.duration, 1620000) - self.assertEqual(str(self.file.format), "") - self.assertEqual(self.file.format.name, "mpegts") - self.assertEqual( - self.file.format.long_name, "MPEG-TS (MPEG-2 Transport Stream)" - ) - self.assertEqual(self.file.metadata, {}) - self.assertEqual(self.file.size, 800000) - self.assertEqual(self.file.start_time, 22953408322) - self.assertEqual(len(self.file.streams), 1) + assert self.file.bit_rate == 3950617 + assert self.file.duration == 1620000 + assert str(self.file.format) == "" + assert self.file.format.name == "mpegts" + assert self.file.format.long_name == "MPEG-TS (MPEG-2 Transport Stream)" + assert self.file.metadata == {} + assert self.file.size == 800000 + assert self.file.start_time == 22953408322 + assert len(self.file.streams) == 1 def test_stream_probing(self): stream = self.file.streams[0] @@ -271,38 +267,38 @@ def test_stream_probing(self): ) # actual stream properties - self.assertEqual(stream.average_rate, Fraction(25, 1)) - self.assertEqual(stream.duration, 145800) - self.assertEqual(stream.frames, 0) - self.assertEqual(stream.id, 4131) - self.assertEqual(stream.index, 0) - self.assertEqual(stream.language, None) - self.assertEqual(stream.metadata, {}) - self.assertEqual(stream.profile, "Simple") - self.assertEqual(stream.start_time, 2065806749) - self.assertEqual(stream.time_base, Fraction(1, 90000)) - self.assertEqual(stream.type, "video") + assert stream.average_rate == Fraction(25, 1) + assert stream.duration == 145800 + assert stream.frames == 0 + assert stream.id == 4131 + assert stream.index == 0 + assert stream.language is None + assert stream.metadata == {} + assert stream.profile == "Simple" + assert stream.start_time == 2065806749 + assert stream.time_base == Fraction(1, 90000) + assert stream.type == "video" # codec context properties - self.assertEqual(stream.bit_rate, 3364800) - self.assertEqual(stream.codec.long_name, "MPEG-2 video") - self.assertEqual(stream.codec.name, "mpeg2video") - self.assertEqual(stream.display_aspect_ratio, Fraction(4, 3)) - self.assertEqual(stream.format.name, "yuv420p") - self.assertFalse(stream.has_b_frames) - self.assertEqual(stream.height, 576) - self.assertEqual(stream.max_bit_rate, None) - self.assertEqual(stream.sample_aspect_ratio, Fraction(16, 15)) - self.assertEqual(stream.width, 720) + assert stream.bit_rate == 3364800 + assert stream.codec.long_name == "MPEG-2 video" + assert stream.codec.name == "mpeg2video" + assert stream.display_aspect_ratio == Fraction(4, 3) + assert stream.format.name == "yuv420p" + assert not stream.has_b_frames + assert stream.height == 576 + assert stream.max_bit_rate is None + assert stream.sample_aspect_ratio == Fraction(16, 15) + assert stream.width == 720 # For some reason, these behave differently on OS X (@mikeboers) and # Ubuntu (Travis). We think it is FFmpeg, but haven't been able to # confirm. - self.assertIn(stream.coded_width, (720, 0)) - self.assertIn(stream.coded_height, (576, 0)) + assert stream.coded_width in (720, 0) + assert stream.coded_height in (576, 0) - self.assertEqual(hasattr(stream, "framerate"), False) - self.assertEqual(hasattr(stream, "rate"), False) + assert not hasattr(stream, "framerate") + assert not hasattr(stream, "rate") class TestVideoProbeCorrupt(TestCase): @@ -314,48 +310,46 @@ def setUp(self): self.file = av.open(path) def test_container_probing(self): - self.assertEqual(str(self.file.format), "") - self.assertEqual(self.file.format.name, "h264") - self.assertEqual(self.file.format.long_name, "raw H.264 video") - self.assertEqual(self.file.size, 0) - self.assertEqual(self.file.bit_rate, 0) - self.assertEqual(self.file.duration, None) + assert str(self.file.format) == "" + assert self.file.format.name == "h264" + assert self.file.format.long_name == "raw H.264 video" + assert self.file.size == 0 + assert self.file.bit_rate == 0 + assert self.file.duration is None - self.assertEqual(len(self.file.streams), 1) - self.assertEqual(self.file.start_time, None) - self.assertEqual(self.file.metadata, {}) + assert len(self.file.streams) == 1 + assert self.file.start_time is None + assert self.file.metadata == {} def test_stream_probing(self): stream = self.file.streams[0] # ensure __repr__ does not crash - self.assertTrue(str(stream).startswith(" None: input_container = av.open(format="lavfi", file="color=c=pink:duration=1:r=30") @@ -258,6 +258,6 @@ def test_EOF(self) -> None: # if we do not push None, we get a BlockingIOError palette_frame = graph.vpull() - self.assertIsInstance(palette_frame, av.VideoFrame) - self.assertEqual(palette_frame.width, 16) - self.assertEqual(palette_frame.height, 16) + assert isinstance(palette_frame, av.VideoFrame) + assert palette_frame.width == 16 + assert palette_frame.height == 16 diff --git a/tests/test_logging.py b/tests/test_logging.py index 2a09a9c1a..5602ce751 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -14,8 +14,8 @@ def do_log(message): class TestLogging(TestCase): def test_adapt_level(self): - self.assertEqual(av.logging.adapt_level(av.logging.ERROR), logging.ERROR) - self.assertEqual(av.logging.adapt_level(av.logging.WARNING), logging.WARNING) + assert av.logging.adapt_level(av.logging.ERROR) == logging.ERROR + assert av.logging.adapt_level(av.logging.WARNING) == logging.WARNING self.assertEqual( av.logging.adapt_level((av.logging.WARNING + av.logging.ERROR) // 2), logging.WARNING, @@ -80,7 +80,7 @@ def test_error(self): try: av.error.err_check(-errno.EPERM) except OSError as e: - self.assertEqual(e.log, log) + assert e.log == log else: self.fail() diff --git a/tests/test_options.py b/tests/test_options.py index 790780b20..f5593a77d 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -1,19 +1,17 @@ from av import ContainerFormat from av.option import Option, OptionType -from .common import TestCase - -class TestOptions(TestCase): - def test_mov_options(self): +class TestOptions: + def test_mov_options(self) -> None: mov = ContainerFormat("mov") - options = mov.descriptor.options + options = mov.descriptor.options # type: ignore by_name = {opt.name: opt for opt in options} opt = by_name.get("use_absolute_path") - self.assertIsInstance(opt, Option) - self.assertEqual(opt.name, "use_absolute_path") + assert isinstance(opt, Option) + assert opt.name == "use_absolute_path" # This was not a good option to actually test. - self.assertIn(opt.type, (OptionType.BOOL, OptionType.INT)) + assert opt.type in (OptionType.BOOL, OptionType.INT) diff --git a/tests/test_packet.py b/tests/test_packet.py index cb0909116..423396f71 100644 --- a/tests/test_packet.py +++ b/tests/test_packet.py @@ -1,49 +1,50 @@ import av -from .common import TestCase, fate_suite +from .common import fate_suite -class TestProperties(TestCase): - def test_is_keyframe(self): +class TestProperties: + def test_is_keyframe(self) -> None: with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: stream = container.streams.video[0] for i, packet in enumerate(container.demux(stream)): if i in (0, 21, 45, 69, 93, 117): - self.assertTrue(packet.is_keyframe) + assert packet.is_keyframe else: - self.assertFalse(packet.is_keyframe) + assert not packet.is_keyframe - def test_is_corrupt(self): + def test_is_corrupt(self) -> None: with av.open(fate_suite("mov/white_zombie_scrunch-part.mov")) as container: stream = container.streams.video[0] for i, packet in enumerate(container.demux(stream)): if i == 65: - self.assertTrue(packet.is_corrupt) + assert packet.is_corrupt else: - self.assertFalse(packet.is_corrupt) + assert not packet.is_corrupt - def test_is_discard(self): + def test_is_discard(self) -> None: with av.open(fate_suite("mov/mov-1elist-ends-last-bframe.mov")) as container: stream = container.streams.video[0] for i, packet in enumerate(container.demux(stream)): if i == 46: - self.assertTrue(packet.is_discard) + assert packet.is_discard else: - self.assertFalse(packet.is_discard) + assert not packet.is_discard - def test_is_disposable(self): + def test_is_disposable(self) -> None: with av.open(fate_suite("hap/HAPQA_NoSnappy_127x1.mov")) as container: stream = container.streams.video[0] for i, packet in enumerate(container.demux(stream)): if i == 0: - self.assertTrue(packet.is_disposable) + assert packet.is_disposable else: - self.assertFalse(packet.is_disposable) + assert not packet.is_disposable - def test_set_duration(self): + def test_set_duration(self) -> None: with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: for packet in container.demux(): + assert packet.duration is not None old_duration = packet.duration packet.duration += 10 - self.assertEqual(packet.duration, old_duration + 10) + assert packet.duration == old_duration + 10 diff --git a/tests/test_python_io.py b/tests/test_python_io.py index 58ae4fd0d..9f5e55999 100644 --- a/tests/test_python_io.py +++ b/tests/test_python_io.py @@ -243,34 +243,34 @@ def test_writing_to_custom_io_image2(self): output.mux(packet) # Check that "frame_count" files were opened using the custom IO - self.assertEqual(len(wrapped_custom_io._log), frame_count) - self.assertEqual(len(wrapped_custom_io._method_log), frame_count) + assert len(wrapped_custom_io._log) == frame_count + assert len(wrapped_custom_io._method_log) == frame_count # Check that all files were written to all_write = all( method_log._filter("write") for method_log in wrapped_custom_io._method_log ) - self.assertTrue(all_write) + assert all_write # Check that all files were closed all_closed = all( method_log._filter("close") for method_log in wrapped_custom_io._method_log ) - self.assertTrue(all_closed) + assert all_closed # Check contents. with av.open(sequence_filename, "r", "image2") as container: - self.assertEqual(len(container.streams), 1) + assert len(container.streams) == 1 stream = container.streams[0] - self.assertIsInstance(stream, av.video.stream.VideoStream) - self.assertEqual(stream.duration, frame_count) - self.assertEqual(stream.type, "video") + assert isinstance(stream, av.video.stream.VideoStream) + assert stream.duration == frame_count + assert stream.type == "video" # codec context properties - self.assertEqual(stream.codec.name, "png") - self.assertEqual(stream.format.name, "rgb24") - self.assertEqual(stream.format.width, width) - self.assertEqual(stream.format.height, height) + assert stream.codec.name == "png" + assert stream.format.name == "rgb24" + assert stream.format.width == width + assert stream.format.height == height def test_writing_to_file(self) -> None: path = self.sandboxed("writing.mp4") @@ -286,9 +286,9 @@ def test_writing_to_pipe_readonly(self) -> None: buf = ReadOnlyPipe() with self.assertRaises(ValueError) as cm: self.write(buf) - self.assertEqual( - str(cm.exception), - "File object has no write() method, or writable() returned False.", + assert ( + str(cm.exception) + == "File object has no write() method, or writable() returned False." ) def test_writing_to_pipe_writeonly(self): @@ -297,10 +297,7 @@ def test_writing_to_pipe_writeonly(self): buf = WriteOnlyPipe() with self.assertRaises(ValueError) as cm: self.write(buf) - self.assertIn( - "[mp4] muxer does not support non seekable output", - str(cm.exception), - ) + assert "[mp4] muxer does not support non seekable output" in str(cm.exception) av.logging.set_level(None) @@ -308,14 +305,14 @@ def read(self, fh, seekable: bool = True) -> None: wrapped = MethodLogger(fh) with av.open(wrapped, "r") as container: - self.assertEqual(container.format.name, "mpegts") + assert container.format.name == "mpegts" self.assertEqual( container.format.long_name, "MPEG-TS (MPEG-2 Transport Stream)" ) - self.assertEqual(len(container.streams), 1) + assert len(container.streams) == 1 if seekable: - self.assertEqual(container.size, 800000) - self.assertEqual(container.metadata, {}) + assert container.size == 800000 + assert container.metadata == {} # Check method calls. self.assertTrue(wrapped._filter("read")) diff --git a/tests/test_seek.py b/tests/test_seek.py index c29b3c9d6..ee6f84536 100644 --- a/tests/test_seek.py +++ b/tests/test_seek.py @@ -47,7 +47,7 @@ def test_seek_start(self): for packet in container.demux(): seek_packet_count += 1 - self.assertEqual(total_packet_count, seek_packet_count) + assert total_packet_count == seek_packet_count def test_seek_middle(self): container = av.open(fate_suite("h264/interlaced_crop.mp4")) @@ -84,8 +84,8 @@ def test_seek_end(self): seek_packet_count += 1 # there should be some packet because we're seeking to the last keyframe - self.assertTrue(seek_packet_count > 0) - self.assertTrue(seek_packet_count < middle_packet_count) + assert seek_packet_count > 0 + assert seek_packet_count < middle_packet_count def test_decode_half(self): container = av.open(fate_suite("h264/interlaced_crop.mp4")) @@ -98,7 +98,7 @@ def test_decode_half(self): for frame in packet.decode(): total_frame_count += 1 - self.assertEqual(video_stream.frames, total_frame_count) + assert video_stream.frames == total_frame_count # set target frame to middle frame target_frame = int(total_frame_count / 2.0) @@ -123,7 +123,7 @@ def test_decode_half(self): if current_frame is not None and current_frame >= target_frame: frame_count += 1 - self.assertEqual(frame_count, total_frame_count - target_frame) + assert frame_count == total_frame_count - target_frame def test_stream_seek(self): container = av.open(fate_suite("h264/interlaced_crop.mp4")) @@ -160,7 +160,7 @@ def test_stream_seek(self): if current_frame is not None and current_frame >= target_frame: frame_count += 1 - self.assertEqual(frame_count, total_frame_count - target_frame) + assert frame_count == total_frame_count - target_frame if __name__ == "__main__": diff --git a/tests/test_subtitles.py b/tests/test_subtitles.py index 9a5e9ceb1..e2b5ab512 100644 --- a/tests/test_subtitles.py +++ b/tests/test_subtitles.py @@ -1,10 +1,10 @@ import av from av.subtitles.subtitle import AssSubtitle, BitmapSubtitle -from .common import TestCase, fate_suite +from .common import fate_suite -class TestSubtitle(TestCase): +class TestSubtitle: def test_movtext(self) -> None: path = fate_suite("sub/MovText_capability_tester.mp4") @@ -13,24 +13,22 @@ def test_movtext(self) -> None: for packet in container.demux(): subs.extend(packet.decode()) - self.assertEqual(len(subs), 3) + assert len(subs) == 3 subset = subs[0] - self.assertEqual(subset.format, 1) - self.assertEqual(subset.pts, 970000) - self.assertEqual(subset.start_display_time, 0) - self.assertEqual(subset.end_display_time, 1570) + assert subset.format == 1 + assert subset.pts == 970000 + assert subset.start_display_time == 0 + assert subset.end_display_time == 1570 sub = subset[0] - self.assertIsInstance(sub, AssSubtitle) assert isinstance(sub, AssSubtitle) + assert sub.type == b"ass" + assert sub.text == b"" + assert sub.ass == b"0,0,Default,,0,0,0,,- Test 1.\\N- Test 2." + assert sub.dialogue == b"- Test 1.\n- Test 2." - self.assertEqual(sub.type, b"ass") - self.assertEqual(sub.text, b"") - self.assertEqual(sub.ass, b"0,0,Default,,0,0,0,,- Test 1.\\N- Test 2.") - self.assertEqual(sub.dialogue, b"- Test 1.\n- Test 2.") - - def test_vobsub(self): + def test_vobsub(self) -> None: path = fate_suite("sub/vobsub.sub") subs = [] @@ -38,26 +36,26 @@ def test_vobsub(self): for packet in container.demux(): subs.extend(packet.decode()) - self.assertEqual(len(subs), 43) + assert len(subs) == 43 subset = subs[0] - self.assertEqual(subset.format, 0) - self.assertEqual(subset.pts, 132499044) - self.assertEqual(subset.start_display_time, 0) - self.assertEqual(subset.end_display_time, 4960) + assert subset.format == 0 + assert subset.pts == 132499044 + assert subset.start_display_time == 0 + assert subset.end_display_time == 4960 sub = subset[0] - self.assertIsInstance(sub, BitmapSubtitle) - self.assertEqual(sub.type, b"bitmap") - self.assertEqual(sub.x, 259) - self.assertEqual(sub.y, 379) - self.assertEqual(sub.width, 200) - self.assertEqual(sub.height, 24) - self.assertEqual(sub.nb_colors, 4) + assert isinstance(sub, BitmapSubtitle) + assert sub.type == b"bitmap" + assert sub.x == 259 + assert sub.y == 379 + assert sub.width == 200 + assert sub.height == 24 + assert sub.nb_colors == 4 bms = sub.planes - self.assertEqual(len(bms), 1) - self.assertEqual(len(memoryview(bms[0])), 4800) + assert len(bms) == 1 + assert len(memoryview(bms[0])) == 4800 # type: ignore def test_subtitle_flush(self) -> None: path = fate_suite("sub/MovText_capability_tester.mp4") @@ -69,4 +67,4 @@ def test_subtitle_flush(self) -> None: subs.extend(stream.decode(packet)) subs.extend(stream.decode()) - self.assertEqual(len(subs), 3) + assert len(subs) == 3 diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 9dff66d7f..a7b753de7 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -12,67 +12,67 @@ class TestVideoFrameConstructors(TestCase): def test_invalid_pixel_format(self): with self.assertRaises(ValueError) as cm: VideoFrame(640, 480, "__unknown_pix_fmt") - self.assertEqual(str(cm.exception), "not a pixel format: '__unknown_pix_fmt'") + assert str(cm.exception) == "not a pixel format: '__unknown_pix_fmt'" def test_null_constructor(self): frame = VideoFrame() - self.assertEqual(frame.width, 0) - self.assertEqual(frame.height, 0) - self.assertEqual(frame.format.name, "yuv420p") + assert frame.width == 0 + assert frame.height == 0 + assert frame.format.name == "yuv420p" def test_manual_yuv_constructor(self): frame = VideoFrame(640, 480, "yuv420p") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "yuv420p") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "yuv420p" def test_manual_rgb_constructor(self): frame = VideoFrame(640, 480, "rgb24") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "rgb24") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "rgb24" class TestVideoFramePlanes(TestCase): def test_null_planes(self): frame = VideoFrame() # yuv420p - self.assertEqual(len(frame.planes), 0) + assert len(frame.planes) == 0 def test_yuv420p_planes(self): frame = VideoFrame(640, 480, "yuv420p") - self.assertEqual(len(frame.planes), 3) - self.assertEqual(frame.planes[0].width, 640) - self.assertEqual(frame.planes[0].height, 480) - self.assertEqual(frame.planes[0].line_size, 640) - self.assertEqual(frame.planes[0].buffer_size, 640 * 480) + assert len(frame.planes) == 3 + assert frame.planes[0].width == 640 + assert frame.planes[0].height == 480 + assert frame.planes[0].line_size == 640 + assert frame.planes[0].buffer_size == 640 * 480 for i in range(1, 3): - self.assertEqual(frame.planes[i].width, 320) - self.assertEqual(frame.planes[i].height, 240) - self.assertEqual(frame.planes[i].line_size, 320) - self.assertEqual(frame.planes[i].buffer_size, 320 * 240) + assert frame.planes[i].width == 320 + assert frame.planes[i].height == 240 + assert frame.planes[i].line_size == 320 + assert frame.planes[i].buffer_size == 320 * 240 def test_yuv420p_planes_align(self): # If we request 8-byte alignment for a width which is not a multiple of 8, # the line sizes are larger than the plane width. frame = VideoFrame(318, 238, "yuv420p") - self.assertEqual(len(frame.planes), 3) - self.assertEqual(frame.planes[0].width, 318) - self.assertEqual(frame.planes[0].height, 238) - self.assertEqual(frame.planes[0].line_size, 320) - self.assertEqual(frame.planes[0].buffer_size, 320 * 238) + assert len(frame.planes) == 3 + assert frame.planes[0].width == 318 + assert frame.planes[0].height == 238 + assert frame.planes[0].line_size == 320 + assert frame.planes[0].buffer_size == 320 * 238 for i in range(1, 3): - self.assertEqual(frame.planes[i].width, 159) - self.assertEqual(frame.planes[i].height, 119) - self.assertEqual(frame.planes[i].line_size, 160) - self.assertEqual(frame.planes[i].buffer_size, 160 * 119) + assert frame.planes[i].width == 159 + assert frame.planes[i].height == 119 + assert frame.planes[i].line_size == 160 + assert frame.planes[i].buffer_size == 160 * 119 def test_rgb24_planes(self): frame = VideoFrame(640, 480, "rgb24") - self.assertEqual(len(frame.planes), 1) - self.assertEqual(frame.planes[0].width, 640) - self.assertEqual(frame.planes[0].height, 480) - self.assertEqual(frame.planes[0].line_size, 640 * 3) - self.assertEqual(frame.planes[0].buffer_size, 640 * 480 * 3) + assert len(frame.planes) == 1 + assert frame.planes[0].width == 640 + assert frame.planes[0].height == 480 + assert frame.planes[0].line_size == 640 * 3 + assert frame.planes[0].buffer_size == 640 * 480 * 3 class TestVideoFrameBuffers(TestCase): @@ -80,13 +80,13 @@ def test_memoryview_read(self): frame = VideoFrame(640, 480, "rgb24") frame.planes[0].update(b"01234" + (b"x" * (640 * 480 * 3 - 5))) mem = memoryview(frame.planes[0]) - self.assertEqual(mem.ndim, 1) - self.assertEqual(mem.shape, (640 * 480 * 3,)) + assert mem.ndim == 1 + assert mem.shape == (640 * 480 * 3,) self.assertFalse(mem.readonly) - self.assertEqual(mem[1], 49) - self.assertEqual(mem[:7], b"01234xx") + assert mem[1] == 49 + assert mem[:7] == b"01234xx" mem[1] = 46 - self.assertEqual(mem[:7], b"0.234xx") + assert mem[:7] == b"0.234xx" class TestVideoFrameImage(TestCase): @@ -131,253 +131,253 @@ def test_to_image_rgb24(self): pos += 1 img = frame.to_image() - self.assertEqual(img.size, (width, height)) - self.assertEqual(img.tobytes(), expected) + assert img.size == (width, height) + assert img.tobytes() == expected def test_to_image_with_dimensions(self): frame = VideoFrame(640, 480, format="rgb24") img = frame.to_image(width=320, height=240) - self.assertEqual(img.size, (320, 240)) + assert img.size == (320, 240) class TestVideoFrameNdarray(TestCase): def assertPixelValue16(self, plane, expected, byteorder: str): view = memoryview(plane) if byteorder == "big": - self.assertEqual(view[0], (expected >> 8) & 0xFF) - self.assertEqual(view[1], expected & 0xFF) + assert view[0] == (expected >> 8 & 0xFF) + assert view[1] == expected & 0xFF else: - self.assertEqual(view[0], expected & 0xFF) - self.assertEqual(view[1], (expected >> 8) & 0xFF) + assert view[0] == expected & 0xFF + assert view[1] == (expected >> 8 & 0xFF) def test_basic_to_ndarray(self): frame = VideoFrame(640, 480, "rgb24") array = frame.to_ndarray() - self.assertEqual(array.shape, (480, 640, 3)) + assert array.shape == (480, 640, 3) def test_ndarray_gray(self): array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) for format in ["gray", "gray8"]: frame = VideoFrame.from_ndarray(array, format=format) - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "gray") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "gray" self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_gray_align(self): array = numpy.random.randint(0, 256, size=(238, 318), dtype=numpy.uint8) for format in ["gray", "gray8"]: frame = VideoFrame.from_ndarray(array, format=format) - self.assertEqual(frame.width, 318) - self.assertEqual(frame.height, 238) - self.assertEqual(frame.format.name, "gray") + assert frame.width == 318 + assert frame.height == 238 + assert frame.format.name == "gray" self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_rgb(self): array = numpy.random.randint(0, 256, size=(480, 640, 3), dtype=numpy.uint8) for format in ["rgb24", "bgr24"]: frame = VideoFrame.from_ndarray(array, format=format) - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, format) + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == format self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_rgb_align(self): array = numpy.random.randint(0, 256, size=(238, 318, 3), dtype=numpy.uint8) for format in ["rgb24", "bgr24"]: frame = VideoFrame.from_ndarray(array, format=format) - self.assertEqual(frame.width, 318) - self.assertEqual(frame.height, 238) - self.assertEqual(frame.format.name, format) + assert frame.width == 318 + assert frame.height == 238 + assert frame.format.name == format self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_rgba(self): array = numpy.random.randint(0, 256, size=(480, 640, 4), dtype=numpy.uint8) for format in ["argb", "rgba", "abgr", "bgra"]: frame = VideoFrame.from_ndarray(array, format=format) - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, format) + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == format self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_rgba_align(self): array = numpy.random.randint(0, 256, size=(238, 318, 4), dtype=numpy.uint8) for format in ["argb", "rgba", "abgr", "bgra"]: frame = VideoFrame.from_ndarray(array, format=format) - self.assertEqual(frame.width, 318) - self.assertEqual(frame.height, 238) - self.assertEqual(frame.format.name, format) + assert frame.width == 318 + assert frame.height == 238 + assert frame.format.name == format self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_gbrp(self): array = numpy.random.randint(0, 256, size=(480, 640, 3), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="gbrp") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "gbrp") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "gbrp" self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_gbrp_align(self): array = numpy.random.randint(0, 256, size=(238, 318, 3), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="gbrp") - self.assertEqual(frame.width, 318) - self.assertEqual(frame.height, 238) - self.assertEqual(frame.format.name, "gbrp") + assert frame.width == 318 + assert frame.height == 238 + assert frame.format.name == "gbrp" self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_gbrp10(self): array = numpy.random.randint(0, 1024, size=(480, 640, 3), dtype=numpy.uint16) for format in ["gbrp10be", "gbrp10le"]: frame = VideoFrame.from_ndarray(array, format=format) - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, format) + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == format self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_gbrp10_align(self): array = numpy.random.randint(0, 1024, size=(238, 318, 3), dtype=numpy.uint16) for format in ["gbrp10be", "gbrp10le"]: frame = VideoFrame.from_ndarray(array, format=format) - self.assertEqual(frame.width, 318) - self.assertEqual(frame.height, 238) - self.assertEqual(frame.format.name, format) + assert frame.width == 318 + assert frame.height == 238 + assert frame.format.name == format self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_gbrp12(self): array = numpy.random.randint(0, 4096, size=(480, 640, 3), dtype=numpy.uint16) for format in ["gbrp12be", "gbrp12le"]: frame = VideoFrame.from_ndarray(array, format=format) - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, format) + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == format self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_gbrp12_align(self): array = numpy.random.randint(0, 4096, size=(238, 318, 3), dtype=numpy.uint16) for format in ["gbrp12be", "gbrp12le"]: frame = VideoFrame.from_ndarray(array, format=format) - self.assertEqual(frame.width, 318) - self.assertEqual(frame.height, 238) - self.assertEqual(frame.format.name, format) + assert frame.width == 318 + assert frame.height == 238 + assert frame.format.name == format self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_gbrp14(self): array = numpy.random.randint(0, 16384, size=(480, 640, 3), dtype=numpy.uint16) for format in ["gbrp14be", "gbrp14le"]: frame = VideoFrame.from_ndarray(array, format=format) - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, format) + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == format self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_gbrp14_align(self): array = numpy.random.randint(0, 16384, size=(238, 318, 3), dtype=numpy.uint16) for format in ["gbrp14be", "gbrp14le"]: frame = VideoFrame.from_ndarray(array, format=format) - self.assertEqual(frame.width, 318) - self.assertEqual(frame.height, 238) - self.assertEqual(frame.format.name, format) + assert frame.width == 318 + assert frame.height == 238 + assert frame.format.name == format self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_gbrp16(self): array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) for format in ["gbrp16be", "gbrp16le"]: frame = VideoFrame.from_ndarray(array, format=format) - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, format) + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == format self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_gbrp16_align(self): array = numpy.random.randint(0, 65536, size=(238, 318, 3), dtype=numpy.uint16) for format in ["gbrp16be", "gbrp16le"]: frame = VideoFrame.from_ndarray(array, format=format) - self.assertEqual(frame.width, 318) - self.assertEqual(frame.height, 238) - self.assertEqual(frame.format.name, format) + assert frame.width == 318 + assert frame.height == 238 + assert frame.format.name == format self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_gbrpf32(self): array = numpy.random.random_sample(size=(480, 640, 3)).astype(numpy.float32) for format in ["gbrpf32be", "gbrpf32le"]: frame = VideoFrame.from_ndarray(array, format=format) - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, format) + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == format self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_gbrpf32_align(self): array = numpy.random.random_sample(size=(238, 318, 3)).astype(numpy.float32) for format in ["gbrpf32be", "gbrpf32le"]: frame = VideoFrame.from_ndarray(array, format=format) - self.assertEqual(frame.width, 318) - self.assertEqual(frame.height, 238) - self.assertEqual(frame.format.name, format) + assert frame.width == 318 + assert frame.height == 238 + assert frame.format.name == format self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_yuv420p(self): array = numpy.random.randint(0, 256, size=(720, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuv420p") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "yuv420p") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "yuv420p" self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_yuv420p_align(self): array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuv420p") - self.assertEqual(frame.width, 318) - self.assertEqual(frame.height, 238) - self.assertEqual(frame.format.name, "yuv420p") + assert frame.width == 318 + assert frame.height == 238 + assert frame.format.name == "yuv420p" self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_yuvj420p(self): array = numpy.random.randint(0, 256, size=(720, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuvj420p") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "yuvj420p") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "yuvj420p" self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_yuyv422(self): array = numpy.random.randint(0, 256, size=(480, 640, 2), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuyv422") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "yuyv422") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "yuyv422" self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_yuv444p(self): array = numpy.random.randint(0, 256, size=(3, 480, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuv444p") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "yuv444p") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "yuv444p" self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_yuvj444p(self): array = numpy.random.randint(0, 256, size=(3, 480, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuvj444p") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "yuvj444p") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "yuvj444p" self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_yuyv422_align(self): array = numpy.random.randint(0, 256, size=(238, 318, 2), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuyv422") - self.assertEqual(frame.width, 318) - self.assertEqual(frame.height, 238) - self.assertEqual(frame.format.name, "yuyv422") + assert frame.width == 318 + assert frame.height == 238 + assert frame.format.name == "yuyv422" self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_gray16be(self): array = numpy.random.randint(0, 65536, size=(480, 640), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="gray16be") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "gray16be") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "gray16be" self.assertNdarraysEqual(frame.to_ndarray(), array) # check endianness by examining value of first pixel @@ -386,9 +386,9 @@ def test_ndarray_gray16be(self): def test_ndarray_gray16le(self): array = numpy.random.randint(0, 65536, size=(480, 640), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="gray16le") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "gray16le") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "gray16le" self.assertNdarraysEqual(frame.to_ndarray(), array) # check endianness by examining value of first pixel @@ -397,9 +397,9 @@ def test_ndarray_gray16le(self): def test_ndarray_rgb48be(self): array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="rgb48be") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "rgb48be") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "rgb48be" self.assertNdarraysEqual(frame.to_ndarray(), array) # check endianness by examining red value of first pixel @@ -408,9 +408,9 @@ def test_ndarray_rgb48be(self): def test_ndarray_rgb48le(self): array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="rgb48le") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "rgb48le") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "rgb48le" self.assertNdarraysEqual(frame.to_ndarray(), array) # check endianness by examining red value of first pixel @@ -419,9 +419,9 @@ def test_ndarray_rgb48le(self): def test_ndarray_rgb48le_align(self): array = numpy.random.randint(0, 65536, size=(238, 318, 3), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="rgb48le") - self.assertEqual(frame.width, 318) - self.assertEqual(frame.height, 238) - self.assertEqual(frame.format.name, "rgb48le") + assert frame.width == 318 + assert frame.height == 238 + assert frame.format.name == "rgb48le" self.assertNdarraysEqual(frame.to_ndarray(), array) # check endianness by examining red value of first pixel @@ -430,9 +430,9 @@ def test_ndarray_rgb48le_align(self): def test_ndarray_rgba64be(self): array = numpy.random.randint(0, 65536, size=(480, 640, 4), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="rgba64be") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "rgba64be") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "rgba64be" self.assertNdarraysEqual(frame.to_ndarray(), array) # check endianness by examining red value of first pixel @@ -441,9 +441,9 @@ def test_ndarray_rgba64be(self): def test_ndarray_rgba64le(self): array = numpy.random.randint(0, 65536, size=(480, 640, 4), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="rgba64le") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "rgba64le") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "rgba64le" self.assertNdarraysEqual(frame.to_ndarray(), array) # check endianness by examining red value of first pixel @@ -452,26 +452,26 @@ def test_ndarray_rgba64le(self): def test_ndarray_rgb8(self): array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="rgb8") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "rgb8") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "rgb8" self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_bgr8(self): array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="bgr8") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "bgr8") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "bgr8" self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_pal8(self): array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) palette = numpy.random.randint(0, 256, size=(256, 4), dtype=numpy.uint8) frame = VideoFrame.from_ndarray((array, palette), format="pal8") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "pal8") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "pal8" returned = frame.to_ndarray() self.assertTrue((type(returned) is tuple) and len(returned) == 2) self.assertNdarraysEqual(returned[0], array) @@ -480,17 +480,17 @@ def test_ndarray_pal8(self): def test_ndarray_nv12(self): array = numpy.random.randint(0, 256, size=(720, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="nv12") - self.assertEqual(frame.width, 640) - self.assertEqual(frame.height, 480) - self.assertEqual(frame.format.name, "nv12") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "nv12" self.assertNdarraysEqual(frame.to_ndarray(), array) def test_ndarray_nv12_align(self): array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="nv12") - self.assertEqual(frame.width, 318) - self.assertEqual(frame.height, 238) - self.assertEqual(frame.format.name, "nv12") + assert frame.width == 318 + assert frame.height == 238 + assert frame.format.name == "nv12" self.assertNdarraysEqual(frame.to_ndarray(), array) @@ -593,20 +593,20 @@ def test_shares_memory_bgr24(self): class TestVideoFrameTiming(TestCase): - def test_reformat_pts(self): + def test_reformat_pts(self) -> None: frame = VideoFrame(640, 480, "rgb24") frame.pts = 123 frame.time_base = Fraction("456/1") frame = frame.reformat(320, 240) - self.assertEqual(frame.pts, 123) - self.assertEqual(frame.time_base, 456) + assert frame.pts == 123 + assert frame.time_base == 456 class TestVideoFrameReformat(TestCase): def test_reformat_identity(self): frame1 = VideoFrame(640, 480, "rgb24") frame2 = frame1.reformat(640, 480, "rgb24") - self.assertIs(frame1, frame2) + assert frame1 is frame2 def test_reformat_colourspace(self): # This is allowed. From 985f9bf9d9232d5e9d77b603a24bfed65cfb9a1e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 23 Sep 2024 18:39:40 -0400 Subject: [PATCH 108/365] Replace self.assertEqual with `assert` statement Works better with mypy. `assert`s work equally well since we're using pytest now. --- tests/test_audiofifo.py | 25 ++++++------ tests/test_bitstream.py | 26 ++++++------ tests/test_codec_context.py | 62 ++++++++++++++--------------- tests/test_colorspace.py | 36 ++++++++--------- tests/test_containerformat.py | 74 ++++++++++++++++------------------- tests/test_decode.py | 44 ++++++++++----------- tests/test_encode.py | 8 ++-- tests/test_enums.py | 42 ++++++++++---------- tests/test_seek.py | 7 ---- tests/test_streams.py | 32 +++++++-------- tests/test_videoformat.py | 70 ++++++++++++++++----------------- 11 files changed, 205 insertions(+), 221 deletions(-) diff --git a/tests/test_audiofifo.py b/tests/test_audiofifo.py index 2874c7145..9eee420a8 100644 --- a/tests/test_audiofifo.py +++ b/tests/test_audiofifo.py @@ -59,17 +59,17 @@ def test_pts_simple(self) -> None: assert oframe.pts == 0 assert oframe.time_base == iframe.time_base - self.assertEqual(fifo.samples_written, 1024) - self.assertEqual(fifo.samples_read, 512) - self.assertEqual(fifo.pts_per_sample, 1.0) + assert fifo.samples_written == 1024 + assert fifo.samples_read == 512 + assert fifo.pts_per_sample == 1.0 iframe.pts = 1024 fifo.write(iframe) oframe = fifo.read(512) assert oframe is not None - self.assertEqual(oframe.pts, 512) - self.assertEqual(oframe.time_base, iframe.time_base) + assert oframe.pts == 512 + assert oframe.time_base == iframe.time_base iframe.pts = 9999 # Wrong! self.assertRaises(ValueError, fifo.write, iframe) @@ -88,8 +88,8 @@ def test_pts_complex(self) -> None: oframe = fifo.read_many(1024)[-1] - self.assertEqual(oframe.pts, 2048) - self.assertEqual(fifo.pts_per_sample, 2.0) + assert oframe.pts == 2048 + assert fifo.pts_per_sample == 2.0 def test_missing_sample_rate(self) -> None: fifo = av.AudioFifo() @@ -103,9 +103,9 @@ def test_missing_sample_rate(self) -> None: oframe = fifo.read(512) assert oframe is not None - self.assertIsNone(oframe.pts) - self.assertEqual(oframe.sample_rate, 0) - self.assertEqual(oframe.time_base, iframe.time_base) + assert oframe.pts is None + assert oframe.sample_rate == 0 + assert oframe.time_base == iframe.time_base def test_missing_time_base(self) -> None: fifo = av.AudioFifo() @@ -119,6 +119,5 @@ def test_missing_time_base(self) -> None: oframe = fifo.read(512) assert oframe is not None - self.assertIsNone(oframe.pts) - self.assertIsNone(oframe.time_base) - self.assertEqual(oframe.sample_rate, iframe.sample_rate) + assert oframe.pts is None and oframe.time_base is None + assert oframe.sample_rate == iframe.sample_rate diff --git a/tests/test_bitstream.py b/tests/test_bitstream.py index 8538594a7..335abac1c 100644 --- a/tests/test_bitstream.py +++ b/tests/test_bitstream.py @@ -23,14 +23,14 @@ def test_filter_chomp(self) -> None: ctx = BitStreamFilterContext("chomp") src_packets: tuple[Packet, None] = (Packet(b"\x0012345\0\0\0"), None) - self.assertEqual(bytes(src_packets[0]), b"\x0012345\0\0\0") + assert bytes(src_packets[0]) == b"\x0012345\0\0\0" result_packets = [] for p in src_packets: result_packets.extend(ctx.filter(p)) - self.assertEqual(len(result_packets), 1) - self.assertEqual(bytes(result_packets[0]), b"\x0012345") + assert len(result_packets) == 1 + assert bytes(result_packets[0]) == b"\x0012345" def test_filter_setts(self) -> None: ctx = BitStreamFilterContext("setts=pts=N") @@ -48,9 +48,9 @@ def test_filter_setts(self) -> None: for p in src_packets: result_packets.extend(ctx.filter(p)) - self.assertEqual(len(result_packets), 2) - self.assertEqual(result_packets[0].pts, 0) - self.assertEqual(result_packets[1].pts, 1) + assert len(result_packets) == 2 + assert result_packets[0].pts == 0 + assert result_packets[1].pts == 1 def test_filter_h264_mp4toannexb(self) -> None: with av.open(fate_suite("h264/interlaced_crop.mp4"), "r") as container: @@ -62,22 +62,22 @@ def test_filter_h264_mp4toannexb(self) -> None: self.assertFalse(is_annexb(p)) res_packets.extend(ctx.filter(p)) - self.assertEqual(len(res_packets), stream.frames) + assert len(res_packets) == stream.frames for p in res_packets: - self.assertTrue(is_annexb(p)) + assert is_annexb(p) def test_filter_output_parameters(self) -> None: with av.open(fate_suite("h264/interlaced_crop.mp4"), "r") as container: stream = container.streams.video[0] - self.assertFalse(is_annexb(stream.codec_context.extradata)) + assert not is_annexb(stream.codec_context.extradata) ctx = BitStreamFilterContext("h264_mp4toannexb", stream) - self.assertFalse(is_annexb(stream.codec_context.extradata)) + assert not is_annexb(stream.codec_context.extradata) del ctx _ = BitStreamFilterContext("h264_mp4toannexb", stream, out_stream=stream) - self.assertTrue(is_annexb(stream.codec_context.extradata)) + assert is_annexb(stream.codec_context.extradata) def test_filter_flush(self) -> None: with av.open(fate_suite("h264/interlaced_crop.mp4"), "r") as container: @@ -87,7 +87,7 @@ def test_filter_flush(self) -> None: res_packets = [] for p in container.demux(stream): res_packets.extend(ctx.filter(p)) - self.assertEqual(len(res_packets), stream.frames) + assert len(res_packets) == stream.frames container.seek(0) # Without flushing, we expect to get an error: "A non-NULL packet sent after an EOF." @@ -100,4 +100,4 @@ def test_filter_flush(self) -> None: for p in container.demux(stream): res_packets.extend(ctx.filter(p)) - self.assertEqual(len(res_packets), stream.frames * 2) + assert len(res_packets) == stream.frames * 2 diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 12997d76a..cb89b74d0 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -38,52 +38,52 @@ def iter_raw_frames(path, packet_sizes, ctx): class TestCodecContext(TestCase): def test_skip_frame_default(self): ctx = Codec("png", "w").create() - self.assertEqual(ctx.skip_frame.name, "DEFAULT") + assert ctx.skip_frame.name == "DEFAULT" def test_codec_delay(self): with av.open(fate_suite("mkv/codec_delay_opus.mkv")) as container: - self.assertEqual(container.streams.audio[0].codec_context.delay, 312) + assert container.streams.audio[0].codec_context.delay == 312 with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: - self.assertEqual(container.streams.video[0].codec_context.delay, 0) + assert container.streams.video[0].codec_context.delay == 0 def test_codec_tag(self): ctx = Codec("mpeg4", "w").create() - self.assertEqual(ctx.codec_tag, "\x00\x00\x00\x00") + assert ctx.codec_tag == "\x00\x00\x00\x00" ctx.codec_tag = "xvid" - self.assertEqual(ctx.codec_tag, "xvid") + assert ctx.codec_tag == "xvid" # wrong length with self.assertRaises(ValueError) as cm: ctx.codec_tag = "bob" - self.assertEqual(str(cm.exception), "Codec tag should be a 4 character string.") + assert str(cm.exception) == "Codec tag should be a 4 character string." # wrong type with self.assertRaises(ValueError) as cm: ctx.codec_tag = 123 - self.assertEqual(str(cm.exception), "Codec tag should be a 4 character string.") + assert str(cm.exception) == "Codec tag should be a 4 character string." with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: - self.assertEqual(container.streams[0].codec_tag, "avc1") + assert container.streams[0].codec_tag == "avc1" def test_decoder_extradata(self): ctx = av.codec.Codec("h264", "r").create() - self.assertEqual(ctx.extradata, None) - self.assertEqual(ctx.extradata_size, 0) + assert ctx.extradata is None + assert ctx.extradata_size == 0 ctx.extradata = b"123" - self.assertEqual(ctx.extradata, b"123") - self.assertEqual(ctx.extradata_size, 3) + assert ctx.extradata == b"123" + assert ctx.extradata_size == 3 ctx.extradata = b"54321" - self.assertEqual(ctx.extradata, b"54321") - self.assertEqual(ctx.extradata_size, 5) + assert ctx.extradata == b"54321" + assert ctx.extradata_size == 5 ctx.extradata = None - self.assertEqual(ctx.extradata, None) - self.assertEqual(ctx.extradata_size, 0) + assert ctx.extradata is None + assert ctx.extradata_size == 0 - def test_decoder_gop_size(self): - ctx = av.codec.Codec("h264", "r").create() + def test_decoder_gop_size(self) -> None: + ctx = av.codec.Codec("h264", "r").create("video") with self.assertRaises(RuntimeError): ctx.gop_size @@ -99,7 +99,7 @@ def test_decoder_timebase(self) -> None: def test_encoder_extradata(self): ctx = av.codec.Codec("h264", "w").create() - self.assertEqual(ctx.extradata, None) + assert ctx.extradata is None self.assertEqual(ctx.extradata_size, 0) ctx.extradata = b"123" @@ -170,7 +170,7 @@ def _assert_parse(self, codec_name, path): parsed_source = b"".join(bytes(p) for p in packets) self.assertEqual(len(parsed_source), len(full_source)) - self.assertEqual(full_source, parsed_source) + assert full_source == parsed_source class TestEncoding(TestCase): @@ -214,7 +214,7 @@ def image_sequence_encode(self, codec_name: str) -> None: new_frame = frame.reformat(width, height, pix_fmt) new_packets = ctx.encode(new_frame) - self.assertEqual(len(new_packets), 1) + assert len(new_packets) == 1 new_packet = new_packets[0] path = self.sandboxed( @@ -240,9 +240,9 @@ def image_sequence_encode(self, codec_name: str) -> None: packet = Packet(size) size = f.readinto(packet) frame = ctx.decode(packet)[0] - self.assertEqual(frame.width, width) - self.assertEqual(frame.height, height) - self.assertEqual(frame.format.name, pix_fmt) + assert frame.width == width + assert frame.height == height + assert frame.format.name == pix_fmt def test_encoding_h264(self): self.video_encoding("h264", {"crf": "19"}) @@ -333,13 +333,13 @@ def video_encoding(self, codec_name, options={}, codec_tag=None): decoded_frame_count = 0 for frame in iter_raw_frames(path, packet_sizes, ctx): decoded_frame_count += 1 - self.assertEqual(frame.width, width) - self.assertEqual(frame.height, height) - self.assertEqual(frame.format.name, pix_fmt) + assert frame.width == width + assert frame.height == height + assert frame.format.name == pix_fmt if frame.key_frame: keyframe_indices.append(decoded_frame_count) - self.assertEqual(frame_count, decoded_frame_count) + assert frame_count == decoded_frame_count self.assertIsInstance( all(keyframe_index for keyframe_index in keyframe_indices), int @@ -352,7 +352,7 @@ def video_encoding(self, codec_name, options={}, codec_tag=None): ): raise SkipTest() for i in decoded_gop_sizes: - self.assertEqual(i, gop_size) + assert i == gop_size final_gop_size = decoded_frame_count - max(keyframe_indices) self.assertLessEqual(final_gop_size, gop_size) @@ -433,5 +433,5 @@ def _audio_encoding( for frame in iter_raw_frames(path, packet_sizes, ctx): result_samples += frame.samples - self.assertEqual(frame.sample_rate, sample_rate) - self.assertEqual(frame.layout.nb_channels, 2) + assert frame.sample_rate == sample_rate + assert frame.layout.nb_channels == 2 diff --git a/tests/test_colorspace.py b/tests/test_colorspace.py index afc16846f..b54229643 100644 --- a/tests/test_colorspace.py +++ b/tests/test_colorspace.py @@ -1,31 +1,29 @@ import av from av.video.reformatter import ColorRange, Colorspace -from .common import TestCase, fate_suite +from .common import fate_suite -class TestColorSpace(TestCase): +class TestColorSpace: def test_penguin_joke(self) -> None: container = av.open( fate_suite("amv/MTV_high_res_320x240_sample_Penguin_Joke_MTV_from_WMV.amv") ) stream = container.streams.video[0] - self.assertEqual(stream.codec_context.color_range, 2) - self.assertEqual(stream.codec_context.color_range, ColorRange.JPEG) + assert stream.codec_context.color_range == 2 + assert stream.codec_context.color_range == ColorRange.JPEG - self.assertEqual(stream.codec_context.color_primaries, 2) - self.assertEqual(stream.codec_context.color_trc, 2) + assert stream.codec_context.color_primaries == 2 + assert stream.codec_context.color_trc == 2 - self.assertEqual(stream.codec_context.colorspace, 5) - self.assertEqual(stream.codec_context.colorspace, Colorspace.ITU601) + assert stream.codec_context.colorspace == 5 + assert stream.codec_context.colorspace == Colorspace.ITU601 - for packet in container.demux(stream): - for frame in packet.decode(): - assert isinstance(frame, av.VideoFrame) - self.assertEqual(frame.color_range, ColorRange.JPEG) # a.k.a "pc" - self.assertEqual(frame.colorspace, Colorspace.ITU601) - return + for frame in container.decode(stream): + assert frame.color_range == ColorRange.JPEG # a.k.a "pc" + assert frame.colorspace == Colorspace.ITU601 + return def test_sky_timelapse(self) -> None: container = av.open( @@ -33,8 +31,8 @@ def test_sky_timelapse(self) -> None: ) stream = container.streams.video[0] - self.assertEqual(stream.codec_context.color_range, 1) - self.assertEqual(stream.codec_context.color_range, ColorRange.MPEG) - self.assertEqual(stream.codec_context.color_primaries, 1) - self.assertEqual(stream.codec_context.color_trc, 1) - self.assertEqual(stream.codec_context.colorspace, 1) + assert stream.codec_context.color_range == 1 + assert stream.codec_context.color_range == ColorRange.MPEG + assert stream.codec_context.color_primaries == 1 + assert stream.codec_context.color_trc == 1 + assert stream.codec_context.colorspace == 1 diff --git a/tests/test_containerformat.py b/tests/test_containerformat.py index 5b6d31c35..6475cf9cd 100644 --- a/tests/test_containerformat.py +++ b/tests/test_containerformat.py @@ -1,63 +1,57 @@ from av import ContainerFormat, formats_available, open -from .common import TestCase - -class TestContainerFormats(TestCase): +class TestContainerFormats: def test_matroska(self) -> None: with open("test.mkv", "w") as container: - self.assertNotEqual(container.default_video_codec, "none") - self.assertNotEqual(container.default_audio_codec, "none") - self.assertEqual(container.default_subtitle_codec, "ass") - self.assertIn("ass", container.supported_codecs) + assert container.default_video_codec != "none" + assert container.default_audio_codec != "none" + assert container.default_subtitle_codec == "ass" + assert "ass" in container.supported_codecs fmt = ContainerFormat("matroska") - self.assertTrue(fmt.is_input) - self.assertTrue(fmt.is_output) - self.assertEqual(fmt.name, "matroska") - self.assertEqual(fmt.long_name, "Matroska") - self.assertIn("mkv", fmt.extensions) - self.assertFalse(fmt.no_file) + assert fmt.is_input and fmt.is_output + assert fmt.name == "matroska" + assert fmt.long_name == "Matroska" + assert "mkv" in fmt.extensions + assert not fmt.no_file def test_mov(self) -> None: with open("test.mov", "w") as container: - self.assertNotEqual(container.default_video_codec, "none") - self.assertNotEqual(container.default_audio_codec, "none") - self.assertEqual(container.default_subtitle_codec, "none") - self.assertIn("h264", container.supported_codecs) + assert container.default_video_codec != "none" + assert container.default_audio_codec != "none" + assert container.default_subtitle_codec == "none" + assert "h264" in container.supported_codecs fmt = ContainerFormat("mov") - self.assertTrue(fmt.is_input) - self.assertTrue(fmt.is_output) - self.assertEqual(fmt.name, "mov") - self.assertEqual(fmt.long_name, "QuickTime / MOV") - self.assertIn("mov", fmt.extensions) - self.assertFalse(fmt.no_file) + assert fmt.is_input and fmt.is_output + assert fmt.name == "mov" + assert fmt.long_name == "QuickTime / MOV" + assert "mov" in fmt.extensions + assert not fmt.no_file def test_gif(self) -> None: with open("test.gif", "w") as container: - self.assertEqual(container.default_video_codec, "gif") - self.assertEqual(container.default_audio_codec, "none") - self.assertEqual(container.default_subtitle_codec, "none") - self.assertIn("gif", container.supported_codecs) + assert container.default_video_codec == "gif" + assert container.default_audio_codec == "none" + assert container.default_subtitle_codec == "none" + assert "gif" in container.supported_codecs def test_stream_segment(self) -> None: # This format goes by two names, check both. fmt = ContainerFormat("stream_segment") - self.assertFalse(fmt.is_input) - self.assertTrue(fmt.is_output) - self.assertEqual(fmt.name, "stream_segment") - self.assertEqual(fmt.long_name, "streaming segment muxer") - self.assertEqual(fmt.extensions, set()) - self.assertTrue(fmt.no_file) + assert not fmt.is_input and fmt.is_output + assert fmt.name == "stream_segment" + assert fmt.long_name == "streaming segment muxer" + assert fmt.extensions == set() + assert fmt.no_file fmt = ContainerFormat("ssegment") - self.assertFalse(fmt.is_input) - self.assertTrue(fmt.is_output) - self.assertEqual(fmt.name, "ssegment") - self.assertEqual(fmt.long_name, "streaming segment muxer") - self.assertEqual(fmt.extensions, set()) - self.assertTrue(fmt.no_file) + assert not fmt.is_input and fmt.is_output + assert fmt.name == "ssegment" + assert fmt.long_name == "streaming segment muxer" + assert fmt.extensions == set() + assert fmt.no_file def test_formats_available(self) -> None: - self.assertTrue(formats_available) + assert formats_available diff --git a/tests/test_decode.py b/tests/test_decode.py index 87a84ba12..82e8461e2 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -6,21 +6,19 @@ class TestDecode(TestCase): - def test_decoded_video_frame_count(self): + def test_decoded_video_frame_count(self) -> None: container = av.open(fate_suite("h264/interlaced_crop.mp4")) video_stream = next(s for s in container.streams if s.type == "video") - self.assertIs(video_stream, container.streams.video[0]) + assert video_stream is container.streams.video[0] frame_count = 0 + for frame in container.decode(video_stream): + frame_count += 1 - for packet in container.demux(video_stream): - for frame in packet.decode(): - frame_count += 1 + assert frame_count == video_stream.frames - self.assertEqual(frame_count, video_stream.frames) - - def test_decode_audio_corrupt(self): + def test_decode_audio_corrupt(self) -> None: # write an empty file path = self.sandboxed("empty.flac") with open(path, "wb"): @@ -35,36 +33,38 @@ def test_decode_audio_corrupt(self): frame_count += 1 packet_count += 1 - self.assertEqual(packet_count, 1) - self.assertEqual(frame_count, 0) + assert packet_count == 1 + assert frame_count == 0 - def test_decode_audio_sample_count(self): + def test_decode_audio_sample_count(self) -> None: container = av.open(fate_suite("audio-reference/chorusnoise_2ch_44kHz_s16.wav")) audio_stream = next(s for s in container.streams if s.type == "audio") - self.assertIs(audio_stream, container.streams.audio[0]) + assert audio_stream is container.streams.audio[0] + assert isinstance(audio_stream, av.audio.AudioStream) sample_count = 0 - for packet in container.demux(audio_stream): - for frame in packet.decode(): - sample_count += frame.samples + for frame in container.decode(audio_stream): + sample_count += frame.samples + assert audio_stream.duration is not None + assert audio_stream.time_base is not None total_samples = ( audio_stream.duration * audio_stream.sample_rate.numerator ) / audio_stream.time_base.denominator - self.assertEqual(sample_count, total_samples) + assert sample_count == total_samples def test_decoded_time_base(self): container = av.open(fate_suite("h264/interlaced_crop.mp4")) stream = container.streams.video[0] - self.assertEqual(stream.time_base, Fraction(1, 25)) + assert stream.time_base == Fraction(1, 25) for packet in container.demux(stream): for frame in packet.decode(): - self.assertEqual(packet.time_base, frame.time_base) - self.assertEqual(stream.time_base, frame.time_base) + assert packet.time_base == frame.time_base + assert stream.time_base == frame.time_base return def test_decoded_motion_vectors(self): @@ -109,8 +109,8 @@ def test_decode_video_corrupt(self): frame_count += 1 packet_count += 1 - self.assertEqual(packet_count, 1) - self.assertEqual(frame_count, 0) + assert packet_count == 1 + assert frame_count == 0 def test_decode_close_then_use(self): container = av.open(fate_suite("h264/interlaced_crop.mp4")) @@ -155,4 +155,4 @@ def test_flush_decoded_video_frame_count(self): self.assertIsNone(frame.time_base) output_count += 1 - self.assertEqual(output_count, input_count) + assert output_count == input_count diff --git a/tests/test_encode.py b/tests/test_encode.py index 79c8423bf..40b17cd73 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -232,13 +232,13 @@ class TestEncodeStreamSemantics(TestCase): def test_stream_index(self) -> None: with av.open(self.sandboxed("output.mov"), "w") as output: vstream = output.add_stream("mpeg4", 24) - self.assertIn(vstream, output.streams.video) + assert vstream in output.streams.video vstream.pix_fmt = "yuv420p" vstream.width = 320 vstream.height = 240 astream = output.add_stream("mp2", 48000) - self.assertIn(astream, output.streams.audio) + assert astream in output.streams.audio astream.layout = "stereo" # type: ignore astream.format = "s16" # type: ignore @@ -248,7 +248,7 @@ def test_stream_index(self) -> None: vframe = VideoFrame(320, 240, "yuv420p") vpacket = vstream.encode(vframe)[0] - self.assertIs(vpacket.stream, vstream) + assert vpacket.stream is vstream assert vpacket.stream_index == 0 for i in range(10): @@ -264,7 +264,7 @@ def test_stream_index(self) -> None: apacket = apackets[0] break - self.assertIs(apacket.stream, astream) + assert apacket.stream is astream assert apacket.stream_index == 1 def test_stream_audio_resample(self) -> None: diff --git a/tests/test_enums.py b/tests/test_enums.py index 8e4839a77..30eb125f1 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -31,9 +31,9 @@ def test_access(self): foo2 = cls["FOO"] foo3 = cls[1] foo4 = cls[foo1] - self.assertIs(foo1, foo2) - self.assertIs(foo1, foo3) - self.assertIs(foo1, foo4) + assert foo1 is foo2 + assert foo1 is foo3 + assert foo1 is foo4 self.assertIn(foo1, cls) self.assertIn("FOO", cls) @@ -44,7 +44,7 @@ def test_access(self): self.assertRaises(TypeError, lambda: cls[()]) self.assertEqual(cls.get("FOO"), foo1) - self.assertIs(cls.get("not a foo"), None) + assert cls.get("not a foo") is None def test_casting(self): cls = self.define_foobar() @@ -86,8 +86,8 @@ def test_as_key(self): d = {foo: "value"} self.assertEqual(d[foo], "value") - self.assertIs(d.get("FOO"), None) - self.assertIs(d.get(1), None) + assert d.get("FOO") is None + assert d.get(1) is None def test_pickleable(self): cls = PickleableFooBar @@ -97,7 +97,7 @@ def test_pickleable(self): foo2 = pickle.loads(enc) - self.assertIs(foo, foo2) + assert foo is foo2 def test_create_unknown(self): cls = self.define_foobar() @@ -118,7 +118,7 @@ def test_multiple_names(self): ), ) - self.assertIs(cls.F, cls.FOO) + assert cls.F is cls.FOO self.assertEqual(cls.F.name, "FOO") self.assertNotEqual(cls.F.name, "F") # This is actually the string. @@ -140,24 +140,24 @@ def test_flag_basics(self): bar = cls.BAR foobar = foo | bar - self.assertIs(foobar, cls.FOOBAR) + assert foobar is cls.FOOBAR foo2 = foobar & foo - self.assertIs(foo2, foo) + assert foo2 is foo bar2 = foobar ^ foo - self.assertIs(bar2, bar) + assert bar2 is bar bar3 = foobar & ~foo - self.assertIs(bar3, bar) + assert bar3 is bar x = cls.FOO x |= cls.BAR - self.assertIs(x, cls.FOOBAR) + assert x is cls.FOOBAR x = cls.FOOBAR x &= cls.FOO - self.assertIs(x, cls.FOO) + assert x is cls.FOO def test_multi_flags_basics(self): cls = self.define_foobar(is_flags=True) @@ -173,9 +173,9 @@ def test_multi_flags_basics(self): foobar3 = cls[3] foobar4 = cls[foobar] - self.assertIs(foobar, foobar2) - self.assertIs(foobar, foobar3) - self.assertIs(foobar, foobar4) + assert foobar is foobar2 + assert foobar is foobar3 + assert foobar is foobar4 self.assertRaises(KeyError, lambda: cls["FOO|BAR"]) @@ -186,7 +186,7 @@ def test_multi_flags_create_missing(self): cls = self.define_foobar(is_flags=True) foobar = cls[3] - self.assertIs(foobar, cls.FOO | cls.BAR) + assert foobar is cls.FOO | cls.BAR self.assertRaises(KeyError, lambda: cls[4]) # Not FOO or BAR self.assertRaises(KeyError, lambda: cls[7]) # FOO and BAR and missing flag. @@ -212,16 +212,16 @@ def flags(self, value): obj = Class("FOO") - self.assertIs(obj.flags, Flags.FOO) + assert obj.flags is Flags.FOO self.assertTrue(obj.foo) self.assertFalse(obj.bar) obj.bar = True - self.assertIs(obj.flags, foobar) + assert obj.flags is foobar self.assertTrue(obj.foo) self.assertTrue(obj.bar) obj.foo = False - self.assertIs(obj.flags, Flags.BAR) + assert obj.flags is Flags.BAR self.assertFalse(obj.foo) self.assertTrue(obj.bar) diff --git a/tests/test_seek.py b/tests/test_seek.py index ee6f84536..1ba71cc34 100644 --- a/tests/test_seek.py +++ b/tests/test_seek.py @@ -13,13 +13,6 @@ def timestamp_to_frame(timestamp, stream): return frame -def step_forward(container, stream): - for packet in container.demux(stream): - for frame in packet.decode(): - if frame: - return frame - - class TestSeek(TestCase): def test_seek_float(self): container = av.open(fate_suite("h264/interlaced_crop.mp4")) diff --git a/tests/test_streams.py b/tests/test_streams.py index c146abfb1..64bb63c36 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -1,18 +1,18 @@ import av -from .common import TestCase, fate_suite +from .common import fate_suite -class TestStreams(TestCase): - def test_stream_tuples(self): +class TestStreams: + def test_stream_tuples(self) -> None: for fate_name in ("h264/interlaced_crop.mp4",): container = av.open(fate_suite(fate_name)) video_streams = tuple([s for s in container.streams if s.type == "video"]) - self.assertEqual(video_streams, container.streams.video) + assert video_streams == container.streams.video audio_streams = tuple([s for s in container.streams if s.type == "audio"]) - self.assertEqual(audio_streams, container.streams.audio) + assert audio_streams == container.streams.audio def test_selection(self) -> None: container = av.open( @@ -21,29 +21,29 @@ def test_selection(self) -> None: video = container.streams.video[0] audio = container.streams.audio[0] - self.assertEqual([video], container.streams.get(video=0)) - self.assertEqual([video], container.streams.get(video=(0,))) + assert [video] == container.streams.get(video=0) + assert [video] == container.streams.get(video=(0,)) - self.assertEqual(video, container.streams.best("video")) - self.assertEqual(audio, container.streams.best("audio")) + assert video == container.streams.best("video") + assert audio == container.streams.best("audio") container = av.open(fate_suite("sub/MovText_capability_tester.mp4")) subtitle = container.streams.subtitles[0] - self.assertEqual(subtitle, container.streams.best("subtitle")) + assert subtitle == container.streams.best("subtitle") container = av.open(fate_suite("mxf/track_01_v02.mxf")) data = container.streams.data[0] - self.assertEqual(data, container.streams.best("data")) + assert data == container.streams.best("data") - def test_noside_data(self): + def test_noside_data(self) -> None: container = av.open(fate_suite("h264/interlaced_crop.mp4")) video = container.streams.video[0] - self.assertEqual(video.nb_side_data, 0) + assert video.nb_side_data == 0 - def test_side_data(self): + def test_side_data(self) -> None: container = av.open(fate_suite("mov/displaymatrix.mov")) video = container.streams.video[0] - self.assertEqual(video.nb_side_data, 1) - self.assertEqual(video.side_data["DISPLAYMATRIX"], -90.0) + assert video.nb_side_data == 1 + assert video.side_data["DISPLAYMATRIX"] == -90.0 diff --git a/tests/test_videoformat.py b/tests/test_videoformat.py index 61b9ca0fc..4670dc217 100644 --- a/tests/test_videoformat.py +++ b/tests/test_videoformat.py @@ -7,47 +7,47 @@ class TestVideoFormats(TestCase): def test_invalid_pixel_format(self): with self.assertRaises(ValueError) as cm: VideoFormat("__unknown_pix_fmt", 640, 480) - self.assertEqual(str(cm.exception), "not a pixel format: '__unknown_pix_fmt'") + assert str(cm.exception) == "not a pixel format: '__unknown_pix_fmt'" def test_rgb24_inspection(self): fmt = VideoFormat("rgb24", 640, 480) - self.assertEqual(fmt.name, "rgb24") - self.assertEqual(len(fmt.components), 3) + assert fmt.name == "rgb24" + assert len(fmt.components) == 3 self.assertFalse(fmt.is_planar) self.assertFalse(fmt.has_palette) self.assertTrue(fmt.is_rgb) - self.assertEqual(fmt.chroma_width(), 640) - self.assertEqual(fmt.chroma_height(), 480) - self.assertEqual(fmt.chroma_width(1024), 1024) - self.assertEqual(fmt.chroma_height(1024), 1024) + assert fmt.chroma_width() == 640 + assert fmt.chroma_height() == 480 + assert fmt.chroma_width(1024) == 1024 + assert fmt.chroma_height(1024) == 1024 for i in range(3): comp = fmt.components[i] - self.assertEqual(comp.plane, 0) - self.assertEqual(comp.bits, 8) + assert comp.plane == 0 + assert comp.bits == 8 self.assertFalse(comp.is_luma) self.assertFalse(comp.is_chroma) self.assertFalse(comp.is_alpha) - self.assertEqual(comp.width, 640) - self.assertEqual(comp.height, 480) + assert comp.width == 640 + assert comp.height == 480 def test_yuv420p_inspection(self): fmt = VideoFormat("yuv420p", 640, 480) - self.assertEqual(fmt.name, "yuv420p") - self.assertEqual(len(fmt.components), 3) + assert fmt.name == "yuv420p" + assert len(fmt.components) == 3 self._test_yuv420(fmt) def _test_yuv420(self, fmt): self.assertTrue(fmt.is_planar) self.assertFalse(fmt.has_palette) self.assertFalse(fmt.is_rgb) - self.assertEqual(fmt.chroma_width(), 320) - self.assertEqual(fmt.chroma_height(), 240) - self.assertEqual(fmt.chroma_width(1024), 512) - self.assertEqual(fmt.chroma_height(1024), 512) + assert fmt.chroma_width() == 320 + assert fmt.chroma_height() == 240 + assert fmt.chroma_width(1024) == 512 + assert fmt.chroma_height(1024) == 512 for i, comp in enumerate(fmt.components): comp = fmt.components[i] - self.assertEqual(comp.plane, i) - self.assertEqual(comp.bits, 8) + assert comp.plane == i + assert comp.bits == 8 self.assertFalse(fmt.components[0].is_chroma) self.assertTrue(fmt.components[1].is_chroma) self.assertTrue(fmt.components[2].is_chroma) @@ -57,38 +57,38 @@ def _test_yuv420(self, fmt): self.assertFalse(fmt.components[0].is_alpha) self.assertFalse(fmt.components[1].is_alpha) self.assertFalse(fmt.components[2].is_alpha) - self.assertEqual(fmt.components[0].width, 640) - self.assertEqual(fmt.components[1].width, 320) - self.assertEqual(fmt.components[2].width, 320) + assert fmt.components[0].width == 640 + assert fmt.components[1].width == 320 + assert fmt.components[2].width == 320 def test_yuva420p_inspection(self): fmt = VideoFormat("yuva420p", 640, 480) - self.assertEqual(len(fmt.components), 4) + assert len(fmt.components) == 4 self._test_yuv420(fmt) self.assertFalse(fmt.components[3].is_chroma) - self.assertEqual(fmt.components[3].width, 640) + assert fmt.components[3].width == 640 def test_gray16be_inspection(self): fmt = VideoFormat("gray16be", 640, 480) - self.assertEqual(fmt.name, "gray16be") - self.assertEqual(len(fmt.components), 1) + assert fmt.name == "gray16be" + assert len(fmt.components) == 1 self.assertFalse(fmt.is_planar) self.assertFalse(fmt.has_palette) self.assertFalse(fmt.is_rgb) - self.assertEqual(fmt.chroma_width(), 640) - self.assertEqual(fmt.chroma_height(), 480) - self.assertEqual(fmt.chroma_width(1024), 1024) - self.assertEqual(fmt.chroma_height(1024), 1024) + assert fmt.chroma_width() == 640 + assert fmt.chroma_height() == 480 + assert fmt.chroma_width(1024) == 1024 + assert fmt.chroma_height(1024) == 1024 comp = fmt.components[0] - self.assertEqual(comp.plane, 0) - self.assertEqual(comp.bits, 16) + assert comp.plane == 0 + assert comp.bits == 16 self.assertTrue(comp.is_luma) self.assertFalse(comp.is_chroma) - self.assertEqual(comp.width, 640) - self.assertEqual(comp.height, 480) + assert comp.width == 640 + assert comp.height == 480 self.assertFalse(comp.is_alpha) def test_pal8_inspection(self): fmt = VideoFormat("pal8", 640, 480) - self.assertEqual(len(fmt.components), 1) + assert len(fmt.components) == 1 self.assertTrue(fmt.has_palette) From b824d26857de58529b7f3308565ad2b01bb1c979 Mon Sep 17 00:00:00 2001 From: Carlos Ruiz Date: Thu, 19 Sep 2024 15:43:24 +0200 Subject: [PATCH 109/365] feat: expose opaque and opaque_ref Allows tagging AVPackets and AVFrames with any Python object and, when AV_CODEC_FLAG_COPY_OPAQUE is enabled, ffmpeg will take care of propagating the references to those objects from packets -> frames (decoders) and frames -> packets (encoders). --- av/codec/context.pyx | 11 +++++++++++ av/frame.pyx | 14 ++++++++++++++ av/opaque.pxd | 12 ++++++++++++ av/opaque.pyx | 32 ++++++++++++++++++++++++++++++++ av/packet.pyx | 16 +++++++++++++++- include/libav.pxd | 1 + include/libavcodec/avcodec.pxd | 8 ++++++++ include/libavutil/buffer.pxd | 9 +++++++++ 8 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 av/opaque.pxd create mode 100644 av/opaque.pyx create mode 100644 include/libavutil/buffer.pxd diff --git a/av/codec/context.pyx b/av/codec/context.pyx index e2e3632ee..bfaa9544d 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -69,6 +69,14 @@ Flags = define_enum("Flags", __name__, ( ("DROPCHANGED", 1 << 5, "Don't output frames whose parameters differ from first decoded frame in stream." ), + ("RECON_FRAME", lib.AV_CODEC_FLAG_RECON_FRAME, "Request the encoder to output reconstructed frames, i.e. frames that would be produced by decoding the encoded bistream."), + ("COPY_OPAQUE", lib.AV_CODEC_FLAG_COPY_OPAQUE, + """Request the decoder to propagate each packet's AVPacket.opaque and AVPacket.opaque_ref + to its corresponding output AVFrame. Request the encoder to propagate each frame's + AVFrame.opaque and AVFrame.opaque_ref values to its corresponding output AVPacket."""), + ("FRAME_DURATION", lib.AV_CODEC_FLAG_FRAME_DURATION, + """Signal to the encoder that the values of AVFrame.duration are valid and should be + used (typically for transferring them to output packets)."""), ("PASS1", lib.AV_CODEC_FLAG_PASS1, "Use internal 2pass ratecontrol in first pass mode."), ("PASS2", lib.AV_CODEC_FLAG_PASS2, "Use internal 2pass ratecontrol in second pass mode."), ("LOOP_FILTER", lib.AV_CODEC_FLAG_LOOP_FILTER, "loop filter."), @@ -150,6 +158,9 @@ cdef class CodecContext: output_corrupt = flags.flag_property("OUTPUT_CORRUPT") qpel = flags.flag_property("QPEL") drop_changed = flags.flag_property("DROPCHANGED") + recon_frame = flags.flag_property("RECON_FRAME") + copy_opaque = flags.flag_property("COPY_OPAQUE") + frame_duration = flags.flag_property("FRAME_DURATION") pass1 = flags.flag_property("PASS1") pass2 = flags.flag_property("PASS2") loop_filter = flags.flag_property("LOOP_FILTER") diff --git a/av/frame.pyx b/av/frame.pyx index b3ffff99b..a8871fd8f 100644 --- a/av/frame.pyx +++ b/av/frame.pyx @@ -1,4 +1,5 @@ from av.error cimport err_check +from av.opaque cimport opaque_container from av.utils cimport avrational_to_fraction, to_avrational from av.sidedata.sidedata import SideDataContainer @@ -146,3 +147,16 @@ cdef class Frame: ret = lib.av_frame_make_writable(self.ptr) err_check(ret) + + @property + def opaque(self): + if self.ptr.opaque_ref is not NULL: + return opaque_container.get( self.ptr.opaque_ref.data) + + @opaque.setter + def opaque(self, v): + lib.av_buffer_unref(&self.ptr.opaque_ref) + + if v is None: + return + self.ptr.opaque_ref = opaque_container.add(v) diff --git a/av/opaque.pxd b/av/opaque.pxd new file mode 100644 index 000000000..f5c38d7fa --- /dev/null +++ b/av/opaque.pxd @@ -0,0 +1,12 @@ +cimport libav as lib + + +cdef class OpaqueContainer: + cdef dict _by_name + + cdef lib.AVBufferRef *add(self, object v) + cdef object get(self, bytes name) + cdef object pop(self, bytes name) + + +cdef OpaqueContainer opaque_container diff --git a/av/opaque.pyx b/av/opaque.pyx new file mode 100644 index 000000000..1e6769898 --- /dev/null +++ b/av/opaque.pyx @@ -0,0 +1,32 @@ +cimport libav as lib +from libc.stdint cimport uint8_t + +from uuid import uuid4 + + +cdef void key_free(void *opaque, uint8_t *data) noexcept nogil: + cdef char *name = data + with gil: + opaque_container.pop(name) + + +cdef class OpaqueContainer: + """A container that holds references to Python objects, indexed by uuid""" + + def __cinit__(self): + self._by_name = {} + + cdef lib.AVBufferRef *add(self, v): + cdef bytes uuid = str(uuid4()).encode("utf-8") + cdef lib.AVBufferRef *ref = lib.av_buffer_create(uuid, len(uuid), &key_free, NULL, 0) + self._by_name[uuid] = v + return ref + + cdef object get(self, bytes name): + return self._by_name.get(name) + + cdef object pop(self, bytes name): + return self._by_name.pop(name) + + +cdef opaque_container = OpaqueContainer() diff --git a/av/packet.pyx b/av/packet.pyx index 63b0b50ab..b5c9251eb 100644 --- a/av/packet.pyx +++ b/av/packet.pyx @@ -2,6 +2,7 @@ cimport libav as lib from av.bytesource cimport bytesource from av.error cimport err_check +from av.opaque cimport opaque_container from av.utils cimport avrational_to_fraction, to_avrational @@ -207,7 +208,7 @@ cdef class Packet(Buffer): self.ptr.flags &= ~(lib.AV_PKT_FLAG_CORRUPT) @property - def is_discard(self): + def is_discard(self): return bool(self.ptr.flags & lib.AV_PKT_FLAG_DISCARD) @property @@ -218,3 +219,16 @@ cdef class Packet(Buffer): def is_disposable(self): return bool(self.ptr.flags & lib.AV_PKT_FLAG_DISPOSABLE) + @property + def opaque(self): + if self.ptr.opaque_ref is not NULL: + return opaque_container.get( self.ptr.opaque_ref.data) + + @opaque.setter + def opaque(self, v): + lib.av_buffer_unref(&self.ptr.opaque_ref) + + if v is None: + return + self.ptr.opaque_ref = opaque_container.add(v) + diff --git a/include/libav.pxd b/include/libav.pxd index 4312be2e8..c793b9988 100644 --- a/include/libav.pxd +++ b/include/libav.pxd @@ -1,4 +1,5 @@ include "libavutil/avutil.pxd" +include "libavutil/buffer.pxd" include "libavutil/channel_layout.pxd" include "libavutil/dict.pxd" include "libavutil/error.pxd" diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 9add5ae2d..805bf50cb 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -99,6 +99,9 @@ cdef extern from "libavcodec/avcodec.h" nogil: AV_CODEC_FLAG_OUTPUT_CORRUPT AV_CODEC_FLAG_QPEL AV_CODEC_FLAG_DROPCHANGED + AV_CODEC_FLAG_RECON_FRAME + AV_CODEC_FLAG_COPY_OPAQUE + AV_CODEC_FLAG_FRAME_DURATION AV_CODEC_FLAG_PASS1 AV_CODEC_FLAG_PASS2 AV_CODEC_FLAG_LOOP_FILTER @@ -392,6 +395,7 @@ cdef extern from "libavcodec/avcodec.h" nogil: uint8_t **base void *opaque + AVBufferRef *opaque_ref AVDictionary *metadata int flags int decode_error_flags @@ -416,6 +420,10 @@ cdef extern from "libavcodec/avcodec.h" nogil: int64_t pos + void *opaque + AVBufferRef *opaque_ref + + cdef int avcodec_fill_audio_frame( AVFrame *frame, int nb_channels, diff --git a/include/libavutil/buffer.pxd b/include/libavutil/buffer.pxd new file mode 100644 index 000000000..daf86105b --- /dev/null +++ b/include/libavutil/buffer.pxd @@ -0,0 +1,9 @@ +from libc.stdint cimport uint8_t + +cdef extern from "libavutil/buffer.h" nogil: + + AVBufferRef *av_buffer_create(uint8_t *data, size_t size, void (*free)(void *opaque, uint8_t *data), void *opaque, int flags) + void av_buffer_unref(AVBufferRef **buf) + + cdef struct AVBufferRef: + uint8_t *data From 170577208a5f73ef2ee5987b697ff9776b71e4a3 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 24 Sep 2024 01:21:19 -0400 Subject: [PATCH 110/365] Test opaque --- av/codec/context.pyi | 23 +++++++++++++++++++++++ av/frame.pyi | 1 + av/packet.pyi | 1 + tests/test_videoframe.py | 18 ++++++++++++++++-- 4 files changed, 41 insertions(+), 2 deletions(-) diff --git a/av/codec/context.pyi b/av/codec/context.pyi index f193487a9..805b06faf 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -73,6 +73,29 @@ class CodecContext: thread_type: Any skip_frame: Any + # flgas + unaligned: bool + qscale: bool + four_mv: bool + output_corrupt: bool + qpel: bool + drop_changed: bool + recon_frame: bool + copy_opaque: bool + frame_duration: bool + pass1: bool + pass2: bool + loop_filter: bool + gray: bool + psnr: bool + interlaced_dct: bool + low_delay: bool + global_header: bool + bitexact: bool + ac_pred: bool + interlaced_me: bool + closed_gop: bool + def open(self, strict: bool = True) -> None: ... def close(self, strict: bool = True) -> None: ... @staticmethod diff --git a/av/frame.pyi b/av/frame.pyi index 47ed9f0cc..6e5348119 100644 --- a/av/frame.pyi +++ b/av/frame.pyi @@ -7,5 +7,6 @@ class Frame: time_base: Fraction is_corrupt: bool side_data: dict[str, str] + opaque: object def make_writable(self) -> None: ... diff --git a/av/packet.pyi b/av/packet.pyi index f122c2886..9bdbb8c62 100644 --- a/av/packet.pyi +++ b/av/packet.pyi @@ -14,6 +14,7 @@ class Packet(Buffer): pos: int | None size: int duration: int | None + opaque: object is_keyframe: bool is_corrupt: bool is_discard: bool diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index a7b753de7..b0ea0bcc5 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -1,11 +1,25 @@ +import time from fractions import Fraction from unittest import SkipTest import numpy +import av from av import VideoFrame -from .common import TestCase, fate_png, has_pillow +from .common import TestCase, fate_png, fate_suite, has_pillow + + +class TestOpaque: + def test_opaque(self) -> None: + with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: + video_stream = container.streams.video[0] + video_stream.codec_context.copy_opaque = True + for packet_idx, packet in enumerate(container.demux()): + packet.opaque = (time.time(), packet_idx) + for frame in packet.decode(): + assert isinstance(frame, av.frame.Frame) + assert type(frame.opaque) is tuple and len(frame.opaque) == 2 class TestVideoFrameConstructors(TestCase): @@ -33,7 +47,7 @@ def test_manual_rgb_constructor(self): assert frame.format.name == "rgb24" -class TestVideoFramePlanes(TestCase): +class TestVideoFramePlanes: def test_null_planes(self): frame = VideoFrame() # yuv420p assert len(frame.planes) == 0 From 3c0f4966f719c2b5fe9d52b9605b3b133560df22 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 24 Sep 2024 01:38:14 -0400 Subject: [PATCH 111/365] Continue test refactor --- Makefile | 2 +- av/__init__.py | 33 ++++++++- av/container/core.pyi | 4 +- tests/common.py | 2 + tests/test_codec_context.py | 70 ++++++++++++++----- tests/test_decode.py | 16 ++--- tests/test_enums.py | 24 +++---- tests/test_errors.py | 48 ++++++------- tests/test_file_probing.py | 135 ++++++++++++++---------------------- tests/test_seek.py | 97 ++++++++++++-------------- tests/test_timeout.py | 32 ++++----- tests/test_videoframe.py | 14 ++-- 12 files changed, 251 insertions(+), 226 deletions(-) diff --git a/Makefile b/Makefile index 2dc617edf..43e589717 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,7 @@ fate-suite: lint: $(PIP) install -U black isort flake8 flake8-pyproject pillow numpy mypy==1.11.2 black --check av examples tests setup.py - flake8 av examples tests + flake8 av isort --check-only --diff av examples tests mypy av tests diff --git a/av/__init__.py b/av/__init__.py index 68b9fed09..266aec3e1 100644 --- a/av/__init__.py +++ b/av/__init__.py @@ -15,20 +15,22 @@ + os.environ["PATH"] ) -# MUST import the core before anything else in order to initalize the underlying +# MUST import the core before anything else in order to initialize the underlying # library that is being wrapped. from av._core import time_base, library_versions # Capture logging (by importing it). from av import logging -# For convenience, IMPORT ALL OF THE THINGS (that are constructable by the user). +# For convenience, import all common attributes. from av.about import __version__ +from av.audio.codeccontext import AudioCodecContext from av.audio.fifo import AudioFifo from av.audio.format import AudioFormat from av.audio.frame import AudioFrame from av.audio.layout import AudioLayout from av.audio.resampler import AudioResampler +from av.audio.stream import AudioStream from av.bitstream import BitStreamFilterContext, bitstream_filters_available from av.codec.codec import Codec, codecs_available from av.codec.context import CodecContext @@ -36,12 +38,39 @@ from av.format import ContainerFormat, formats_available from av.packet import Packet from av.error import * # noqa: F403; This is limited to exception types. +from av.video.codeccontext import VideoCodecContext from av.video.format import VideoFormat from av.video.frame import VideoFrame +from av.video.stream import VideoStream # Backwards compatibility AVError = FFmpegError # noqa: F405 +__all__ = ( + "time_base", + "library_versions", + "AudioCodecContext", + "AudioFifo", + "AudioFormat", + "AudioFrame", + "AudioLayout", + "AudioResampler", + "AudioStream", + "BitStreamFilterContext", + "bitstream_filters_available", + "Codec", + "codecs_available", + "CodecContext", + "open", + "ContainerFormat", + "formats_available", + "Packet", + "VideoCodecContext", + "VideoFormat", + "VideoFrame", + "VideoStream", +) + def get_include() -> str: """ diff --git a/av/container/core.pyi b/av/container/core.pyi index 862f0d326..c1d43ca4c 100644 --- a/av/container/core.pyi +++ b/av/container/core.pyi @@ -1,4 +1,4 @@ -from numbers import Real +from fractions import Fraction from pathlib import Path from types import TracebackType from typing import Any, Callable, Literal, Type, overload @@ -10,6 +10,8 @@ from .input import InputContainer from .output import OutputContainer from .streams import StreamContainer +Real = int | float | Fraction + class Flags(EnumFlag): GENPTS: int IGNIDX: int diff --git a/tests/common.py b/tests/common.py index 8c0db1129..ece6ac5e1 100644 --- a/tests/common.py +++ b/tests/common.py @@ -14,6 +14,8 @@ except ImportError: has_pillow = False +__all__ = ("fate_suite",) + is_windows = os.name == "nt" skip_tests = frozenset(os.environ.get("PYAV_SKIP_TESTS", "").split(",")) diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index cb89b74d0..2b28730a4 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -2,17 +2,48 @@ import os from fractions import Fraction +from typing import Iterator, TypedDict, overload from unittest import SkipTest import av -from av import AudioLayout, AudioResampler, Codec, Packet +from av import ( + AudioCodecContext, + AudioFrame, + AudioLayout, + AudioResampler, + Codec, + Packet, + VideoCodecContext, + VideoFrame, +) from av.codec.codec import UnknownCodecError from av.video.frame import PictureType from .common import TestCase, fate_suite -def iter_raw_frames(path, packet_sizes, ctx): +class Options(TypedDict, total=False): + b: str + crf: str + pix_fmt: str + width: int + height: int + max_frames: int + time_base: Fraction + gop_size: int + + +@overload +def iter_raw_frames( + path: str, packet_sizes: list, ctx: VideoCodecContext +) -> Iterator[VideoFrame]: ... +@overload +def iter_raw_frames( + path: str, packet_sizes: list, ctx: AudioCodecContext +) -> Iterator[AudioFrame]: ... +def iter_raw_frames( + path: str, packet_sizes: list, ctx: VideoCodecContext | AudioCodecContext +) -> Iterator[VideoFrame | AudioFrame]: with open(path, "rb") as f: for i, size in enumerate(packet_sizes): packet = Packet(size) @@ -244,24 +275,24 @@ def image_sequence_encode(self, codec_name: str) -> None: assert frame.height == height assert frame.format.name == pix_fmt - def test_encoding_h264(self): + def test_encoding_h264(self) -> None: self.video_encoding("h264", {"crf": "19"}) - def test_encoding_mpeg4(self): + def test_encoding_mpeg4(self) -> None: self.video_encoding("mpeg4") - def test_encoding_xvid(self): + def test_encoding_xvid(self) -> None: self.video_encoding("mpeg4", codec_tag="xvid") - def test_encoding_mpeg1video(self): + def test_encoding_mpeg1video(self) -> None: self.video_encoding("mpeg1video") - def test_encoding_dvvideo(self): - options = {"pix_fmt": "yuv411p", "width": 720, "height": 480} + def test_encoding_dvvideo(self) -> None: + options: Options = {"pix_fmt": "yuv411p", "width": 720, "height": 480} self.video_encoding("dvvideo", options) - def test_encoding_dnxhd(self): - options = { + def test_encoding_dnxhd(self) -> None: + options: Options = { "b": "90M", # bitrate "pix_fmt": "yuv422p", "width": 1920, @@ -271,7 +302,12 @@ def test_encoding_dnxhd(self): } self.video_encoding("dnxhd", options) - def video_encoding(self, codec_name, options={}, codec_tag=None): + def video_encoding( + self, + codec_name: str, + options: Options = {}, + codec_tag: str | None = None, + ) -> None: try: codec = Codec(codec_name, "w") except UnknownCodecError: @@ -280,6 +316,8 @@ def video_encoding(self, codec_name, options={}, codec_tag=None): container = av.open(fate_suite("h264/interlaced_crop.mp4")) video_stream = container.streams.video[0] + assert video_stream.time_base is not None + pix_fmt = options.pop("pix_fmt", "yuv420p") width = options.pop("width", 640) height = options.pop("height", 480) @@ -287,14 +325,14 @@ def video_encoding(self, codec_name, options={}, codec_tag=None): time_base = options.pop("time_base", video_stream.time_base) gop_size = options.pop("gop_size", 20) - ctx = codec.create() + ctx = codec.create("video") ctx.width = width ctx.height = height ctx.time_base = time_base ctx.framerate = 1 / ctx.time_base ctx.pix_fmt = pix_fmt ctx.gop_size = gop_size - ctx.options = options # TODO + ctx.options = options # type: ignore if codec_tag: ctx.codec_tag = codec_tag ctx.open() @@ -326,7 +364,7 @@ def video_encoding(self, codec_name, options={}, codec_tag=None): if codec_name == "libx264": dec_codec_name = "h264" - ctx = av.Codec(dec_codec_name, "r").create() + ctx = av.Codec(dec_codec_name, "r").create("video") ctx.open() keyframe_indices = [] @@ -341,7 +379,7 @@ def video_encoding(self, codec_name, options={}, codec_tag=None): assert frame_count == decoded_frame_count - self.assertIsInstance( + assert isinstance( all(keyframe_index for keyframe_index in keyframe_indices), int ) decoded_gop_sizes = [ @@ -350,7 +388,7 @@ def video_encoding(self, codec_name, options={}, codec_tag=None): if codec_name in ("dvvideo", "dnxhd") and all( i == 1 for i in decoded_gop_sizes ): - raise SkipTest() + raise SkipTest("I'm not sure why we skip this actually.") for i in decoded_gop_sizes: assert i == gop_size diff --git a/tests/test_decode.py b/tests/test_decode.py index 82e8461e2..aafea3b22 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -51,8 +51,10 @@ def test_decode_audio_sample_count(self) -> None: assert audio_stream.duration is not None assert audio_stream.time_base is not None total_samples = ( - audio_stream.duration * audio_stream.sample_rate.numerator - ) / audio_stream.time_base.denominator + audio_stream.duration + * audio_stream.sample_rate.numerator + / audio_stream.time_base.denominator + ) assert sample_count == total_samples def test_decoded_time_base(self): @@ -125,11 +127,9 @@ def test_decode_close_then_use(self): except AssertionError: pass - def test_flush_decoded_video_frame_count(self): + def test_flush_decoded_video_frame_count(self) -> None: container = av.open(fate_suite("h264/interlaced_crop.mp4")) - video_stream = next(s for s in container.streams if s.type == "video") - - self.assertIs(video_stream, container.streams.video[0]) + video_stream = container.streams.video[0] # Decode the first GOP, which requires a flush to get all frames have_keyframe = False @@ -148,11 +148,11 @@ def test_flush_decoded_video_frame_count(self): output_count += 1 # Check the test works as expected and requires a flush - self.assertLess(output_count, input_count) + assert output_count < input_count for frame in video_stream.decode(None): # The Frame._time_base is not set by PyAV - self.assertIsNone(frame.time_base) + assert frame.time_base is None output_count += 1 assert output_count == input_count diff --git a/tests/test_enums.py b/tests/test_enums.py index 30eb125f1..e96956eca 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -15,15 +15,13 @@ def define_foobar(self, **kwargs): def test_basics(self): cls = self.define_foobar() - self.assertIsInstance(cls, EnumType) + assert isinstance(cls, EnumType) foo = cls.FOO - self.assertIsInstance(foo, cls) - self.assertEqual(foo.name, "FOO") - self.assertEqual(foo.value, 1) - - self.assertNotIsInstance(foo, PickleableFooBar) + assert isinstance(foo, cls) + assert foo.name == "FOO" and foo.value == 1 + assert not isinstance(foo, PickleableFooBar) def test_access(self): cls = self.define_foobar() @@ -35,9 +33,9 @@ def test_access(self): assert foo1 is foo3 assert foo1 is foo4 - self.assertIn(foo1, cls) - self.assertIn("FOO", cls) - self.assertIn(1, cls) + assert foo1 in cls + assert "FOO" in cls + assert 1 in cls self.assertRaises(KeyError, lambda: cls["not a foo"]) self.assertRaises(KeyError, lambda: cls[10]) @@ -53,12 +51,12 @@ def test_casting(self): self.assertEqual(repr(foo), "") str_foo = str(foo) - self.assertIsInstance(str_foo, str) - self.assertEqual(str_foo, "FOO") + assert isinstance(str_foo, str) + assert str_foo == "FOO" int_foo = int(foo) - self.assertIsInstance(int_foo, int) - self.assertEqual(int_foo, 1) + assert isinstance(int_foo, int) + assert int_foo == 1 def test_iteration(self): cls = self.define_foobar() diff --git a/tests/test_errors.py b/tests/test_errors.py index c7dc1729e..fcd08845e 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -7,53 +7,47 @@ class TestErrorBasics(TestCase): - def test_stringify(self): + def test_stringify(self) -> None: for cls in (av.ValueError, av.FileNotFoundError, av.DecoderNotFoundError): e = cls(1, "foo") - self.assertEqual(str(e), "[Errno 1] foo") - self.assertEqual(repr(e), "{}(1, 'foo')".format(cls.__name__)) - self.assertEqual( - traceback.format_exception_only(cls, e)[-1], - "{}{}: [Errno 1] foo\n".format( - "av.error.", - cls.__name__, - ), + assert f"{e}" == "[Errno 1] foo" + assert f"{e!r}" == f"{cls.__name__}(1, 'foo')" + assert ( + traceback.format_exception_only(cls, e)[-1] + == f"av.error.{cls.__name__}: [Errno 1] foo\n" ) for cls in (av.ValueError, av.FileNotFoundError, av.DecoderNotFoundError): e = cls(1, "foo", "bar.txt") - self.assertEqual(str(e), "[Errno 1] foo: 'bar.txt'") - self.assertEqual(repr(e), "{}(1, 'foo', 'bar.txt')".format(cls.__name__)) - self.assertEqual( - traceback.format_exception_only(cls, e)[-1], - "{}{}: [Errno 1] foo: 'bar.txt'\n".format( - "av.error.", - cls.__name__, - ), + assert f"{e}" == "[Errno 1] foo: 'bar.txt'" + assert f"{e!r}" == f"{cls.__name__}(1, 'foo', 'bar.txt')" + assert ( + traceback.format_exception_only(cls, e)[-1] + == f"av.error.{cls.__name__}: [Errno 1] foo: 'bar.txt'\n" ) - def test_bases(self): - self.assertTrue(issubclass(av.ValueError, ValueError)) - self.assertTrue(issubclass(av.ValueError, av.FFmpegError)) + def test_bases(self) -> None: + assert issubclass(av.ValueError, ValueError) + assert issubclass(av.ValueError, av.FFmpegError) - self.assertTrue(issubclass(av.FileNotFoundError, FileNotFoundError)) - self.assertTrue(issubclass(av.FileNotFoundError, OSError)) - self.assertTrue(issubclass(av.FileNotFoundError, av.FFmpegError)) + assert issubclass(av.FileNotFoundError, FileNotFoundError) + assert issubclass(av.FileNotFoundError, OSError) + assert issubclass(av.FileNotFoundError, av.FFmpegError) def test_filenotfound(self): """Catch using builtin class on Python 3.3""" try: av.open("does not exist") except FileNotFoundError as e: - self.assertEqual(e.errno, errno.ENOENT) + assert e.errno == errno.ENOENT if is_windows: self.assertTrue( e.strerror in ["Error number -2 occurred", "No such file or directory"] ) else: - self.assertEqual(e.strerror, "No such file or directory") - self.assertEqual(e.filename, "does not exist") + assert e.strerror == "No such file or directory" + assert e.filename == "does not exist" else: self.fail("no exception raised") @@ -62,6 +56,6 @@ def test_buffertoosmall(self): try: av.error.err_check(-av.error.BUFFER_TOO_SMALL.value) except av.BufferTooSmallError as e: - self.assertEqual(e.errno, av.error.BUFFER_TOO_SMALL.value) + assert e.errno == av.error.BUFFER_TOO_SMALL.value else: self.fail("no exception raised") diff --git a/tests/test_file_probing.py b/tests/test_file_probing.py index aab232804..d67a69195 100644 --- a/tests/test_file_probing.py +++ b/tests/test_file_probing.py @@ -112,10 +112,10 @@ def test_stream_probing(self): class TestDataProbe(TestCase): - def setUp(self): + def setUp(self) -> None: self.file = av.open(fate_suite("mxf/track_01_v02.mxf")) - def test_container_probing(self): + def test_container_probing(self) -> None: assert self.file.bit_rate == 27872687 assert self.file.duration == 417083 assert str(self.file.format) == "" @@ -125,43 +125,28 @@ def test_container_probing(self): assert self.file.start_time == 0 assert len(self.file.streams) == 4 - for key, value, min_version in ( - ("application_platform", "AAFSDK (MacOS X)", None), - ("comment_Comments", "example comment", None), - ( - "comment_UNC Path", - "/Users/mark/Desktop/dnxhr_tracknames_export.aaf", - None, - ), - ("company_name", "Avid Technology, Inc.", None), - ("generation_uid", "b6bcfcab-70ff-7331-c592-233869de11d2", None), - ("material_package_name", "Example.new.04", None), + for key, value in ( + ("application_platform", "AAFSDK (MacOS X)"), + ("comment_Comments", "example comment"), + ("comment_UNC Path", "/Users/mark/Desktop/dnxhr_tracknames_export.aaf"), + ("company_name", "Avid Technology, Inc."), + ("generation_uid", "b6bcfcab-70ff-7331-c592-233869de11d2"), + ("material_package_name", "Example.new.04"), ( "material_package_umid", "0x060A2B340101010101010F001300000057E19D16BA8202DB060E2B347F7F2A80", - None, ), - ("modification_date", "2016-09-20T20:33:26.000000Z", None), - # Next one is FFmpeg >= 4.2. - ( - "operational_pattern_ul", - "060e2b34.04010102.0d010201.10030000", - {"libavformat": (58, 29)}, - ), - ("product_name", "Avid Media Composer 8.6.3.43955", None), - ("product_uid", "acfbf03a-4f42-a231-d0b7-c06ecd3d4ad7", None), - ("product_version", "Unknown version", None), - ("project_name", "UHD", None), - ("uid", "4482d537-4203-ea40-9e4e-08a22900dd39", None), + ("modification_date", "2016-09-20T20:33:26.000000Z"), + ("operational_pattern_ul", "060e2b34.04010102.0d010201.10030000"), + ("product_name", "Avid Media Composer 8.6.3.43955"), + ("product_uid", "acfbf03a-4f42-a231-d0b7-c06ecd3d4ad7"), + ("product_version", "Unknown version"), + ("project_name", "UHD"), + ("uid", "4482d537-4203-ea40-9e4e-08a22900dd39"), ): - if min_version and any( - av.library_versions[name] < version - for name, version in min_version.items() - ): - continue assert self.file.metadata.get(key) == value - def test_stream_probing(self): + def test_stream_probing(self) -> None: stream = self.file.streams[0] assert str(stream).startswith(" at ") @@ -171,53 +156,43 @@ def test_stream_probing(self): assert stream.id == 1 assert stream.index == 0 assert stream.language is None - self.assertEqual( - stream.metadata, - { - "data_type": "video", - "file_package_umid": "0x060A2B340101010101010F001300000057E19D16BA8302DB060E2B347F7F2A80", - "track_name": "Base", - }, - ) + assert stream.metadata == { + "data_type": "video", + "file_package_umid": "0x060A2B340101010101010F001300000057E19D16BA8302DB060E2B347F7F2A80", + "track_name": "Base", + } assert stream.profile is None assert stream.start_time == 0 assert stream.time_base == Fraction(1, 90000) assert stream.type == "data" - self.assertEqual(hasattr(stream, "codec"), False) + assert not hasattr(stream, "codec") class TestSubtitleProbe(TestCase): - def setUp(self): + def setUp(self) -> None: self.file = av.open(fate_suite("sub/MovText_capability_tester.mp4")) - def test_container_probing(self): + def test_container_probing(self) -> None: assert self.file.bit_rate == 810 assert self.file.duration == 8140000 - self.assertEqual( - str(self.file.format), "" - ) + assert str(self.file.format) == "" assert self.file.format.name == "mov,mp4,m4a,3gp,3g2,mj2" assert self.file.format.long_name == "QuickTime / MOV" - self.assertEqual( - self.file.metadata, - { - "compatible_brands": "isom", - "creation_time": "2012-07-04T05:10:41.000000Z", - "major_brand": "isom", - "minor_version": "1", - }, - ) + assert self.file.metadata == { + "compatible_brands": "isom", + "creation_time": "2012-07-04T05:10:41.000000Z", + "major_brand": "isom", + "minor_version": "1", + } assert self.file.size == 825 assert self.file.start_time is None assert len(self.file.streams) == 1 - def test_stream_probing(self): + def test_stream_probing(self) -> None: stream = self.file.streams[0] # check __repr__ - self.assertTrue( - str(stream).startswith(" None: self.file = av.open(fate_suite("mpeg2/mpeg2_field_encoding.ts")) - def test_container_probing(self): + def test_container_probing(self) -> None: assert self.file.bit_rate == 3950617 assert self.file.duration == 1620000 assert str(self.file.format) == "" @@ -258,12 +230,12 @@ def test_container_probing(self): assert self.file.start_time == 22953408322 assert len(self.file.streams) == 1 - def test_stream_probing(self): + def test_stream_probing(self) -> None: stream = self.file.streams[0] - # check __repr__ - self.assertTrue( - str(stream).startswith(" None: path = self.sandboxed("empty.h264") with open(path, "wb"): pass self.file = av.open(path) - def test_container_probing(self): + def test_container_probing(self) -> None: assert str(self.file.format) == "" assert self.file.format.name == "h264" assert self.file.format.long_name == "raw H.264 video" @@ -321,13 +289,13 @@ def test_container_probing(self): assert self.file.start_time is None assert self.file.metadata == {} - def test_stream_probing(self): + def test_stream_probing(self) -> None: stream = self.file.streams[0] - # ensure __repr__ does not crash assert str(stream).startswith(" float: fps = stream.average_rate time_base = stream.time_base start_time = stream.start_time - frame = (timestamp - start_time) * float(time_base) * float(fps) - return frame + assert time_base is not None and start_time is not None and fps is not None + return (timestamp - start_time) * float(time_base) * float(fps) class TestSeek(TestCase): - def test_seek_float(self): + def test_seek_float(self) -> None: container = av.open(fate_suite("h264/interlaced_crop.mp4")) self.assertRaises(TypeError, container.seek, 1.0) - def test_seek_int64(self): + def test_seek_int64(self) -> None: # Assert that it accepts large values. # Issue 251 pointed this out. container = av.open(fate_suite("h264/interlaced_crop.mp4")) container.seek(2**32) - def test_seek_start(self): + def test_seek_start(self) -> None: container = av.open(fate_suite("h264/interlaced_crop.mp4")) # count all the packets @@ -42,8 +40,9 @@ def test_seek_start(self): assert total_packet_count == seek_packet_count - def test_seek_middle(self): + def test_seek_middle(self) -> None: container = av.open(fate_suite("h264/interlaced_crop.mp4")) + assert container.duration is not None # count all the packets total_packet_count = 0 @@ -57,10 +56,11 @@ def test_seek_middle(self): for packet in container.demux(): seek_packet_count += 1 - self.assertTrue(seek_packet_count < total_packet_count) + assert seek_packet_count < total_packet_count - def test_seek_end(self): + def test_seek_end(self) -> None: container = av.open(fate_suite("h264/interlaced_crop.mp4")) + assert container.duration is not None # seek to middle container.seek(container.duration // 2) @@ -80,21 +80,19 @@ def test_seek_end(self): assert seek_packet_count > 0 assert seek_packet_count < middle_packet_count - def test_decode_half(self): + def test_decode_half(self) -> None: container = av.open(fate_suite("h264/interlaced_crop.mp4")) + video_stream = container.streams.video[0] - video_stream = next(s for s in container.streams if s.type == "video") total_frame_count = 0 - - # Count number of frames in video - for packet in container.demux(video_stream): - for frame in packet.decode(): - total_frame_count += 1 + for frame in container.decode(video_stream): + total_frame_count += 1 assert video_stream.frames == total_frame_count + assert video_stream.average_rate is not None # set target frame to middle frame - target_frame = int(total_frame_count / 2.0) + target_frame = total_frame_count // 2 target_timestamp = int( (target_frame * av.time_base) / video_stream.average_rate ) @@ -105,35 +103,33 @@ def test_decode_half(self): current_frame = None frame_count = 0 - for packet in container.demux(video_stream): - for frame in packet.decode(): - if current_frame is None: - current_frame = timestamp_to_frame(frame.pts, video_stream) - else: - current_frame += 1 + for frame in container.decode(video_stream): + if current_frame is None: + current_frame = timestamp_to_frame(frame.pts, video_stream) + else: + current_frame += 1 - # start counting once we reach the target frame - if current_frame is not None and current_frame >= target_frame: - frame_count += 1 + # start counting once we reach the target frame + if current_frame is not None and current_frame >= target_frame: + frame_count += 1 assert frame_count == total_frame_count - target_frame - def test_stream_seek(self): + def test_stream_seek(self) -> None: container = av.open(fate_suite("h264/interlaced_crop.mp4")) + video_stream = container.streams.video[0] - video_stream = next(s for s in container.streams if s.type == "video") - total_frame_count = 0 + assert video_stream.time_base is not None + assert video_stream.start_time is not None + assert video_stream.average_rate is not None - # Count number of frames in video - for packet in container.demux(video_stream): - for frame in packet.decode(): - total_frame_count += 1 + total_frame_count = 0 + for frame in container.decode(video_stream): + total_frame_count += 1 - target_frame = int(total_frame_count / 2.0) + target_frame = total_frame_count // 2 time_base = float(video_stream.time_base) - - rate = float(video_stream.average_rate) - target_sec = target_frame * 1 / rate + target_sec = target_frame * 1 / float(video_stream.average_rate) target_timestamp = int(target_sec / time_base) + video_stream.start_time container.seek(target_timestamp, stream=video_stream) @@ -141,20 +137,15 @@ def test_stream_seek(self): current_frame = None frame_count = 0 - for packet in container.demux(video_stream): - for frame in packet.decode(): - if current_frame is None: - current_frame = timestamp_to_frame(frame.pts, video_stream) - - else: - current_frame += 1 + for frame in container.decode(video_stream): + if current_frame is None: + assert frame.pts is not None + current_frame = timestamp_to_frame(frame.pts, video_stream) + else: + current_frame += 1 - # start counting once we reach the target frame - if current_frame is not None and current_frame >= target_frame: - frame_count += 1 + # start counting once we reach the target frame + if current_frame is not None and current_frame >= target_frame: + frame_count += 1 assert frame_count == total_frame_count - target_frame - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_timeout.py b/tests/test_timeout.py index cf6e7f0f8..0640237f9 100644 --- a/tests/test_timeout.py +++ b/tests/test_timeout.py @@ -17,54 +17,54 @@ class HttpServer(TCPServer): allow_reuse_address = True - def handle_error(self, request, client_address): + def handle_error(self, request: object, client_address: object) -> None: pass class SlowRequestHandler(BaseHTTPRequestHandler): - def do_GET(self): + def do_GET(self) -> None: time.sleep(DELAY) self.send_response(200) self.send_header("Content-Length", str(len(CONTENT))) self.end_headers() self.wfile.write(CONTENT) - def log_message(self, format, *args): + def log_message(self, format: object, *args: object) -> None: pass class TestTimeout(TestCase): - def setUp(cls): + def setUp(cls) -> None: cls._server = HttpServer(("", PORT), SlowRequestHandler) cls._thread = threading.Thread(target=cls._server.handle_request) cls._thread.daemon = True # Make sure the tests will exit. cls._thread.start() - def tearDown(cls): + def tearDown(cls) -> None: cls._thread.join(1) # Can't wait forever or the tests will never exit. cls._server.server_close() - def test_no_timeout(self): + def test_no_timeout(self) -> None: start = time.time() - av.open("http://localhost:%d/mpeg2_field_encoding.ts" % PORT) + av.open(f"http://localhost:{PORT}/mpeg2_field_encoding.ts") duration = time.time() - start - self.assertGreater(duration, DELAY) + assert duration > DELAY - def test_open_timeout(self): + def test_open_timeout(self) -> None: with self.assertRaises(av.ExitError): start = time.time() - av.open( - "http://localhost:%d/mpeg2_field_encoding.ts" % PORT, timeout=TIMEOUT - ) + av.open(f"http://localhost:{PORT}/mpeg2_field_encoding.ts", timeout=TIMEOUT) + duration = time.time() - start - self.assertLess(duration, DELAY) + assert duration < DELAY - def test_open_timeout_2(self): + def test_open_timeout_2(self) -> None: with self.assertRaises(av.ExitError): start = time.time() av.open( - "http://localhost:%d/mpeg2_field_encoding.ts" % PORT, + f"http://localhost:{PORT}/mpeg2_field_encoding.ts", timeout=(TIMEOUT, None), ) + duration = time.time() - start - self.assertLess(duration, DELAY) + assert duration < DELAY diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index b0ea0bcc5..60cf78035 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -19,6 +19,10 @@ def test_opaque(self) -> None: packet.opaque = (time.time(), packet_idx) for frame in packet.decode(): assert isinstance(frame, av.frame.Frame) + + if frame.opaque is None: + continue + assert type(frame.opaque) is tuple and len(frame.opaque) == 2 @@ -28,19 +32,19 @@ def test_invalid_pixel_format(self): VideoFrame(640, 480, "__unknown_pix_fmt") assert str(cm.exception) == "not a pixel format: '__unknown_pix_fmt'" - def test_null_constructor(self): + def test_null_constructor(self) -> None: frame = VideoFrame() assert frame.width == 0 assert frame.height == 0 assert frame.format.name == "yuv420p" - def test_manual_yuv_constructor(self): + def test_manual_yuv_constructor(self) -> None: frame = VideoFrame(640, 480, "yuv420p") assert frame.width == 640 assert frame.height == 480 assert frame.format.name == "yuv420p" - def test_manual_rgb_constructor(self): + def test_manual_rgb_constructor(self) -> None: frame = VideoFrame(640, 480, "rgb24") assert frame.width == 640 assert frame.height == 480 @@ -48,11 +52,11 @@ def test_manual_rgb_constructor(self): class TestVideoFramePlanes: - def test_null_planes(self): + def test_null_planes(self) -> None: frame = VideoFrame() # yuv420p assert len(frame.planes) == 0 - def test_yuv420p_planes(self): + def test_yuv420p_planes(self) -> None: frame = VideoFrame(640, 480, "yuv420p") assert len(frame.planes) == 3 assert frame.planes[0].width == 640 From d1a21b52d07754efa104af6ee60e470d7ecbd44d Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 24 Sep 2024 16:21:39 -0400 Subject: [PATCH 112/365] Make tests more idiomatic for pytest We don't need to put tests in a class anymore. I also sprinkled some type hints here and there. Left a few because the commit would be too big otherwise. --- Makefile | 2 +- tests/common.py | 82 +-- tests/test_audioformat.py | 47 +- tests/test_audioframe.py | 381 ++++++----- tests/test_audiolayout.py | 54 +- tests/test_audioresampler.py | 517 +++++++------- tests/test_bitstream.py | 134 ++-- tests/test_codec.py | 183 +++-- tests/test_codec_context.py | 44 +- tests/test_colorspace.py | 64 +- tests/test_containerformat.py | 111 +-- tests/test_deprecation.py | 60 +- tests/test_dictionary.py | 25 +- tests/test_encode.py | 22 +- tests/test_enums.py | 342 +++++----- tests/test_errors.py | 112 +-- tests/test_python_io.py | 53 +- tests/test_videoframe.py | 1209 +++++++++++++++++---------------- 18 files changed, 1723 insertions(+), 1719 deletions(-) diff --git a/Makefile b/Makefile index 43e589717..77178b9d1 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ fate-suite: rsync -vrltLW rsync://fate-suite.ffmpeg.org/fate-suite/ tests/assets/fate-suite/ lint: - $(PIP) install -U black isort flake8 flake8-pyproject pillow numpy mypy==1.11.2 + $(PIP) install -U black isort flake8 flake8-pyproject pillow numpy mypy==1.11.2 pytest black --check av examples tests setup.py flake8 av isort --check-only --diff av examples tests diff --git a/tests/common.py b/tests/common.py index ece6ac5e1..33b307c80 100644 --- a/tests/common.py +++ b/tests/common.py @@ -5,6 +5,8 @@ import types from unittest import TestCase as _Base +import numpy as np + from av.datasets import fate as fate_suite try: @@ -55,7 +57,7 @@ def asset(*args: str) -> str: os.environ["PYAV_TESTDATA_DIR"] = asset() -def fate_png(): +def fate_png() -> str: return fate_suite("png1/55c99e750a5fd6_50314226.png") @@ -85,33 +87,32 @@ def _inner(self, *args, **kwargs): return _inner -class MethodLogger: - def __init__(self, obj): - self._obj = obj - self._log = [] +def assertNdarraysEqual(a: np.ndarray, b: np.ndarray) -> None: + assert a.shape == b.shape + + comparison = a == b + if not comparison.all(): + it = np.nditer(comparison, flags=["multi_index"]) + msg = "" + for equal in it: + if not equal: + msg += "- arrays differ at index {}; {} {}\n".format( + it.multi_index, + a[it.multi_index], + b[it.multi_index], + ) + assert False, f"ndarrays contents differ\n{msg}" - def __getattr__(self, name): - value = getattr(self._obj, name) - if isinstance( - value, - ( - types.MethodType, - types.FunctionType, - types.BuiltinFunctionType, - types.BuiltinMethodType, - ), - ): - return functools.partial(self._method, name, value) - else: - self._log.append(("__getattr__", (name,), {})) - return value - def _method(self, name, meth, *args, **kwargs): - self._log.append((name, args, kwargs)) - return meth(*args, **kwargs) +def assertImagesAlmostEqual(a, b, epsilon=0.1): + import PIL.ImageFilter as ImageFilter - def _filter(self, type_): - return [log for log in self._log if log[0] == type_] + assert a.size == b.size + a = a.filter(ImageFilter.BLUR).getdata() + b = b.filter(ImageFilter.BLUR).getdata() + for i, ax, bx in zip(range(len(a)), a, b): + diff = sum(abs(ac / 256 - bc / 256) for ac, bc in zip(ax, bx)) / 3 + assert diff < epsilon, f"images differed by {diff} at index {i}; {ax} {bx}" class TestCase(_Base): @@ -129,34 +130,3 @@ def sandboxed(self, *args, **kwargs) -> str: kwargs.setdefault("sandbox", self.sandbox) kwargs.setdefault("timed", True) return sandboxed(*args, **kwargs) - - def assertNdarraysEqual(self, a, b): - import numpy - - self.assertEqual(a.shape, b.shape) - - comparison = a == b - if not comparison.all(): - it = numpy.nditer(comparison, flags=["multi_index"]) - msg = "" - for equal in it: - if not equal: - msg += "- arrays differ at index %s; %s %s\n" % ( - it.multi_index, - a[it.multi_index], - b[it.multi_index], - ) - self.fail("ndarrays contents differ\n%s" % msg) - - def assertImagesAlmostEqual(self, a, b, epsilon=0.1, *args): - import PIL.ImageFilter as ImageFilter - - self.assertEqual(a.size, b.size, "sizes dont match") - a = a.filter(ImageFilter.BLUR).getdata() - b = b.filter(ImageFilter.BLUR).getdata() - for i, ax, bx in zip(range(len(a)), a, b): - diff = sum(abs(ac / 256 - bc / 256) for ac, bc in zip(ax, bx)) / 3 - if diff > epsilon: - self.fail( - "images differed by %s at index %d; %s %s" % (diff, i, ax, bx) - ) diff --git a/tests/test_audioformat.py b/tests/test_audioformat.py index 733761f57..244d3ad7d 100644 --- a/tests/test_audioformat.py +++ b/tests/test_audioformat.py @@ -1,27 +1,28 @@ import sys +import pytest + from av import AudioFormat -from .common import TestCase - -postfix = "le" if sys.byteorder == "little" else "be" - - -class TestAudioFormats(TestCase): - def test_s16_inspection(self) -> None: - fmt = AudioFormat("s16") - assert fmt.name == "s16" - assert not fmt.is_planar - assert fmt.bits == 16 - assert fmt.bytes == 2 - assert fmt.container_name == "s16" + postfix - assert fmt.planar.name == "s16p" - assert fmt.packed is fmt - - def test_s32p_inspection(self) -> None: - fmt = AudioFormat("s32p") - assert fmt.name == "s32p" - assert fmt.is_planar - assert fmt.bits == 32 - assert fmt.bytes == 4 - self.assertRaises(ValueError, lambda: fmt.container_name) + +def test_s16_inspection() -> None: + fmt = AudioFormat("s16") + postfix = "le" if sys.byteorder == "little" else "be" + + assert fmt.name == "s16" + assert not fmt.is_planar + assert fmt.bits == 16 + assert fmt.bytes == 2 + assert fmt.container_name == "s16" + postfix + assert fmt.planar.name == "s16p" + assert fmt.packed is fmt + + +def test_s32p_inspection() -> None: + fmt = AudioFormat("s32p") + assert fmt.name == "s32p" + assert fmt.is_planar + assert fmt.bits == 32 + assert fmt.bytes == 4 + + pytest.raises(ValueError, lambda: fmt.container_name) diff --git a/tests/test_audioframe.py b/tests/test_audioframe.py index 4a382d18c..7211ad023 100644 --- a/tests/test_audioframe.py +++ b/tests/test_audioframe.py @@ -1,187 +1,202 @@ +from re import escape + import numpy as np +import pytest from av import AudioFrame -from .common import TestCase - - -class TestAudioFrameConstructors(TestCase): - def test_null_constructor(self) -> None: - frame = AudioFrame() - self.assertEqual(frame.format.name, "s16") - self.assertEqual(frame.layout.name, "stereo") - self.assertEqual(len(frame.planes), 0) - self.assertEqual(frame.samples, 0) - - def test_manual_flt_mono_constructor(self) -> None: - frame = AudioFrame(format="flt", layout="mono", samples=160) - self.assertEqual(frame.format.name, "flt") - self.assertEqual(frame.layout.name, "mono") - self.assertEqual(len(frame.planes), 1) - self.assertEqual(frame.planes[0].buffer_size, 640) - self.assertEqual(frame.samples, 160) - - def test_manual_flt_stereo_constructor(self) -> None: - frame = AudioFrame(format="flt", layout="stereo", samples=160) - self.assertEqual(frame.format.name, "flt") - self.assertEqual(frame.layout.name, "stereo") - self.assertEqual(len(frame.planes), 1) - self.assertEqual(frame.planes[0].buffer_size, 1280) - self.assertEqual(frame.samples, 160) - - def test_manual_fltp_stereo_constructor(self) -> None: - frame = AudioFrame(format="fltp", layout="stereo", samples=160) - self.assertEqual(frame.format.name, "fltp") - self.assertEqual(frame.layout.name, "stereo") - self.assertEqual(len(frame.planes), 2) - self.assertEqual(frame.planes[0].buffer_size, 640) - self.assertEqual(frame.planes[1].buffer_size, 640) - self.assertEqual(frame.samples, 160) - - def test_manual_s16_mono_constructor(self) -> None: - frame = AudioFrame(format="s16", layout="mono", samples=160) - self.assertEqual(frame.format.name, "s16") - self.assertEqual(frame.layout.name, "mono") - self.assertEqual(len(frame.planes), 1) - self.assertEqual(frame.planes[0].buffer_size, 320) - self.assertEqual(frame.samples, 160) - - def test_manual_s16_mono_constructor_align_8(self) -> None: - frame = AudioFrame(format="s16", layout="mono", samples=159, align=8) - self.assertEqual(frame.format.name, "s16") - self.assertEqual(frame.layout.name, "mono") - self.assertEqual(len(frame.planes), 1) - self.assertEqual(frame.planes[0].buffer_size, 320) - self.assertEqual(frame.samples, 159) - - def test_manual_s16_stereo_constructor(self) -> None: - frame = AudioFrame(format="s16", layout="stereo", samples=160) - self.assertEqual(frame.format.name, "s16") - self.assertEqual(frame.layout.name, "stereo") - self.assertEqual(len(frame.planes), 1) - self.assertEqual(frame.planes[0].buffer_size, 640) - self.assertEqual(frame.samples, 160) - - def test_manual_s16p_stereo_constructor(self) -> None: - frame = AudioFrame(format="s16p", layout="stereo", samples=160) - self.assertEqual(frame.format.name, "s16p") - self.assertEqual(frame.layout.name, "stereo") - self.assertEqual(len(frame.planes), 2) - self.assertEqual(frame.planes[0].buffer_size, 320) - self.assertEqual(frame.planes[1].buffer_size, 320) - self.assertEqual(frame.samples, 160) - - -class TestAudioFrameConveniences(TestCase): - def test_basic_to_ndarray(self) -> None: - frame = AudioFrame(format="s16p", layout="stereo", samples=160) - array = frame.to_ndarray() - assert array.dtype == "i2" - assert array.shape == (2, 160) - - def test_ndarray_dbl(self) -> None: - layouts = [ - ("dbl", "mono", (1, 160)), - ("dbl", "stereo", (1, 320)), - ("dblp", "mono", (1, 160)), - ("dblp", "stereo", (2, 160)), - ] - for format, layout, size in layouts: - array = np.zeros(shape=size, dtype="f8") - for i in range(size[0]): - array[i][:] = np.random.rand(size[1]) - frame = AudioFrame.from_ndarray(array, format=format, layout=layout) - assert frame.format.name == format - assert frame.layout.name == layout - assert frame.samples == 160 - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_from_ndarray_value_error(self) -> None: - # incorrect dtype - array = np.zeros(shape=(1, 160), dtype="f2") - with self.assertRaises(ValueError) as cm: - AudioFrame.from_ndarray(array, format="flt", layout="mono") - assert ( - str(cm.exception) - == "Expected numpy array with dtype `float32` but got `float16`" - ) - - # incorrect number of dimensions - array = np.zeros(shape=(1, 160, 2), dtype="f4") - with self.assertRaises(ValueError) as cm: - AudioFrame.from_ndarray(array, format="flt", layout="mono") - assert str(cm.exception) == "Expected numpy array with ndim `2` but got `3`" - - # incorrect shape - array = np.zeros(shape=(2, 160), dtype="f4") - with self.assertRaises(ValueError) as cm: - AudioFrame.from_ndarray(array, format="flt", layout="mono") - assert str(cm.exception) == "Unexpected numpy array shape `(2, 160)`" - - def test_ndarray_flt(self) -> None: - layouts = [ - ("flt", "mono", (1, 160)), - ("flt", "stereo", (1, 320)), - ("fltp", "mono", (1, 160)), - ("fltp", "stereo", (2, 160)), - ] - for format, layout, size in layouts: - array: np.ndarray = np.zeros(shape=size, dtype="f4") - for i in range(size[0]): - array[i][:] = np.random.rand(size[1]) - frame = AudioFrame.from_ndarray(array, format=format, layout=layout) - assert frame.format.name == format - assert frame.layout.name == layout - assert frame.samples == 160 - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_s16(self) -> None: - layouts = [ - ("s16", "mono", (1, 160)), - ("s16", "stereo", (1, 320)), - ("s16p", "mono", (1, 160)), - ("s16p", "stereo", (2, 160)), - ] - for format, layout, size in layouts: - array = np.random.randint(0, 256, size=size, dtype="i2") - frame = AudioFrame.from_ndarray(array, format=format, layout=layout) - assert frame.format.name == format - assert frame.layout.name == layout - assert frame.samples == 160 - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_s16p_align_8(self) -> None: - frame = AudioFrame(format="s16p", layout="stereo", samples=159, align=8) - array = frame.to_ndarray() - assert array.dtype == "i2" - assert array.shape == (2, 159) - - def test_ndarray_s32(self) -> None: - layouts = [ - ("s32", "mono", (1, 160)), - ("s32", "stereo", (1, 320)), - ("s32p", "mono", (1, 160)), - ("s32p", "stereo", (2, 160)), - ] - for format, layout, size in layouts: - array = np.random.randint(0, 256, size=size, dtype="i4") - frame = AudioFrame.from_ndarray(array, format=format, layout=layout) - assert frame.format.name == format - assert frame.layout.name == layout - assert frame.samples == 160 - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_u8(self) -> None: - layouts = [ - ("u8", "mono", (1, 160)), - ("u8", "stereo", (1, 320)), - ("u8p", "mono", (1, 160)), - ("u8p", "stereo", (2, 160)), - ] - for format, layout, size in layouts: - array = np.random.randint(0, 256, size=size, dtype="u1") - frame = AudioFrame.from_ndarray(array, format=format, layout=layout) - assert frame.format.name == format - assert frame.layout.name == layout - assert frame.samples == 160 - self.assertNdarraysEqual(frame.to_ndarray(), array) +from .common import assertNdarraysEqual + + +def test_null_constructor() -> None: + frame = AudioFrame() + assert frame.format.name == "s16" + assert frame.layout.name == "stereo" + assert len(frame.planes) == 0 + assert frame.samples == 0 + + +def test_manual_flt_mono_constructor() -> None: + frame = AudioFrame(format="flt", layout="mono", samples=160) + assert frame.format.name == "flt" + assert frame.layout.name == "mono" + assert len(frame.planes) == 1 + assert frame.planes[0].buffer_size == 640 + assert frame.samples == 160 + + +def test_manual_flt_stereo_constructor() -> None: + frame = AudioFrame(format="flt", layout="stereo", samples=160) + assert frame.format.name == "flt" + assert frame.layout.name == "stereo" + assert len(frame.planes) == 1 + assert frame.planes[0].buffer_size == 1280 + assert frame.samples == 160 + + +def test_manual_fltp_stereo_constructor() -> None: + frame = AudioFrame(format="fltp", layout="stereo", samples=160) + assert frame.format.name == "fltp" + assert frame.layout.name == "stereo" + assert len(frame.planes) == 2 + assert frame.planes[0].buffer_size == 640 + assert frame.planes[1].buffer_size == 640 + assert frame.samples == 160 + + +def test_manual_s16_mono_constructor() -> None: + frame = AudioFrame(format="s16", layout="mono", samples=160) + assert frame.format.name == "s16" + assert frame.layout.name == "mono" + assert len(frame.planes) == 1 + assert frame.planes[0].buffer_size == 320 + assert frame.samples == 160 + + +def test_manual_s16_mono_constructor_align_8() -> None: + frame = AudioFrame(format="s16", layout="mono", samples=159, align=8) + assert frame.format.name == "s16" + assert frame.layout.name == "mono" + assert len(frame.planes) == 1 + assert frame.planes[0].buffer_size == 320 + assert frame.samples == 159 + + +def test_manual_s16_stereo_constructor() -> None: + frame = AudioFrame(format="s16", layout="stereo", samples=160) + assert frame.format.name == "s16" + assert frame.layout.name == "stereo" + assert len(frame.planes) == 1 + assert frame.planes[0].buffer_size == 640 + assert frame.samples == 160 + + +def test_manual_s16p_stereo_constructor() -> None: + frame = AudioFrame(format="s16p", layout="stereo", samples=160) + assert frame.format.name == "s16p" + assert frame.layout.name == "stereo" + assert len(frame.planes) == 2 + assert frame.planes[0].buffer_size == 320 + assert frame.planes[1].buffer_size == 320 + assert frame.samples == 160 + + +def test_basic_to_ndarray() -> None: + frame = AudioFrame(format="s16p", layout="stereo", samples=160) + array = frame.to_ndarray() + assert array.dtype == "i2" + assert array.shape == (2, 160) + + +def test_ndarray_dbl() -> None: + layouts = [ + ("dbl", "mono", (1, 160)), + ("dbl", "stereo", (1, 320)), + ("dblp", "mono", (1, 160)), + ("dblp", "stereo", (2, 160)), + ] + for format, layout, size in layouts: + array = np.zeros(shape=size, dtype="f8") + for i in range(size[0]): + array[i][:] = np.random.rand(size[1]) + frame = AudioFrame.from_ndarray(array, format=format, layout=layout) + assert frame.format.name == format + assert frame.layout.name == layout + assert frame.samples == 160 + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_from_ndarray_value_error() -> None: + # incorrect dtype + array = np.zeros(shape=(1, 160), dtype="f2") + with pytest.raises( + ValueError, match="Expected numpy array with dtype `float32` but got `float16`" + ) as cm: + AudioFrame.from_ndarray(array, format="flt", layout="mono") + + # incorrect number of dimensions + array = np.zeros(shape=(1, 160, 2), dtype="f4") + with pytest.raises( + ValueError, match="Expected numpy array with ndim `2` but got `3`" + ) as cm: + AudioFrame.from_ndarray(array, format="flt", layout="mono") + + # incorrect shape + array = np.zeros(shape=(2, 160), dtype="f4") + with pytest.raises( + ValueError, match=escape("Unexpected numpy array shape `(2, 160)`") + ) as cm: + AudioFrame.from_ndarray(array, format="flt", layout="mono") + + +def test_ndarray_flt() -> None: + layouts = [ + ("flt", "mono", (1, 160)), + ("flt", "stereo", (1, 320)), + ("fltp", "mono", (1, 160)), + ("fltp", "stereo", (2, 160)), + ] + for format, layout, size in layouts: + array: np.ndarray = np.zeros(shape=size, dtype="f4") + for i in range(size[0]): + array[i][:] = np.random.rand(size[1]) + frame = AudioFrame.from_ndarray(array, format=format, layout=layout) + assert frame.format.name == format + assert frame.layout.name == layout + assert frame.samples == 160 + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_s16() -> None: + layouts = [ + ("s16", "mono", (1, 160)), + ("s16", "stereo", (1, 320)), + ("s16p", "mono", (1, 160)), + ("s16p", "stereo", (2, 160)), + ] + for format, layout, size in layouts: + array = np.random.randint(0, 256, size=size, dtype="i2") + frame = AudioFrame.from_ndarray(array, format=format, layout=layout) + assert frame.format.name == format + assert frame.layout.name == layout + assert frame.samples == 160 + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_s16p_align_8() -> None: + frame = AudioFrame(format="s16p", layout="stereo", samples=159, align=8) + array = frame.to_ndarray() + assert array.dtype == "i2" + assert array.shape == (2, 159) + + +def test_ndarray_s32() -> None: + layouts = [ + ("s32", "mono", (1, 160)), + ("s32", "stereo", (1, 320)), + ("s32p", "mono", (1, 160)), + ("s32p", "stereo", (2, 160)), + ] + for format, layout, size in layouts: + array = np.random.randint(0, 256, size=size, dtype="i4") + frame = AudioFrame.from_ndarray(array, format=format, layout=layout) + assert frame.format.name == format + assert frame.layout.name == layout + assert frame.samples == 160 + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_u8() -> None: + layouts = [ + ("u8", "mono", (1, 160)), + ("u8", "stereo", (1, 320)), + ("u8p", "mono", (1, 160)), + ("u8p", "stereo", (2, 160)), + ] + for format, layout, size in layouts: + array = np.random.randint(0, 256, size=size, dtype="u1") + frame = AudioFrame.from_ndarray(array, format=format, layout=layout) + assert frame.format.name == format + assert frame.layout.name == layout + assert frame.samples == 160 + assertNdarraysEqual(frame.to_ndarray(), array) diff --git a/tests/test_audiolayout.py b/tests/test_audiolayout.py index 9cf257f68..35ba4c160 100644 --- a/tests/test_audiolayout.py +++ b/tests/test_audiolayout.py @@ -1,32 +1,26 @@ from av import AudioLayout -from .common import TestCase - - -class TestAudioLayout(TestCase): - def test_stereo_from_str(self): - layout = AudioLayout("stereo") - self._test_stereo(layout) - - def test_stereo_from_layout(self): - layout = AudioLayout("stereo") - layout2 = AudioLayout(layout) - self._test_stereo(layout2) - - def _test_stereo(self, layout: AudioLayout) -> None: - self.assertEqual(layout.name, "stereo") - self.assertEqual(layout.nb_channels, 2) - self.assertEqual(repr(layout), "") - - # Re-enable when FFmpeg 6.0 is dropped. - - # self.assertEqual(layout.channels[0].name, "FL") - # self.assertEqual(layout.channels[0].description, "front left") - # self.assertEqual( - # repr(layout.channels[0]), "" - # ) - # self.assertEqual(layout.channels[1].name, "FR") - # self.assertEqual(layout.channels[1].description, "front right") - # self.assertEqual( - # repr(layout.channels[1]), "" - # ) + +def _test_stereo(layout: AudioLayout) -> None: + assert layout.name == "stereo" + assert layout.nb_channels == 2 + assert repr(layout) == "" + + # Re-enable when FFmpeg 6.0 is dropped. + + # assert layout.channels[0].name == "FL" + # assert layout.channels[0].description == "front left" + # assert repr(layout.channels[0]) == "" + # assert layout.channels[1].name == "FR" + # assert layout.channels[1].description == "front right" + # assert repr(layout.channels[1]) == "" + + +def test_stereo_from_str() -> None: + layout = AudioLayout("stereo") + _test_stereo(layout) + + +def test_stereo_from_layout() -> None: + layout2 = AudioLayout(AudioLayout("stereo")) + _test_stereo(layout2) diff --git a/tests/test_audioresampler.py b/tests/test_audioresampler.py index 47dd1ddc3..cad8ebe34 100644 --- a/tests/test_audioresampler.py +++ b/tests/test_audioresampler.py @@ -1,282 +1,289 @@ from fractions import Fraction +import pytest + import av from av import AudioFrame, AudioResampler -from .common import TestCase +def test_flush_immediately() -> None: + """ + If we flush the resampler before passing any input, it returns + a `None` frame without setting up the graph. + """ -class TestAudioResampler(TestCase): - def test_flush_immediately(self) -> None: - """ - If we flush the resampler before passing any input, it returns - a `None` frame without setting up the graph. - """ + resampler = AudioResampler() - resampler = AudioResampler() + # flush + oframes = resampler.resample(None) + assert len(oframes) == 0 - # flush - oframes = resampler.resample(None) - self.assertEqual(len(oframes), 0) - def test_identity_passthrough(self) -> None: - """ - If we don't ask it to do anything, it won't. - """ +def test_identity_passthrough() -> None: + """ + If we don't ask it to do anything, it won't. + """ - resampler = AudioResampler() + resampler = AudioResampler() - # resample one frame - iframe = AudioFrame("s16", "stereo", 1024) + # resample one frame + iframe = AudioFrame("s16", "stereo", 1024) - oframes = resampler.resample(iframe) - self.assertEqual(len(oframes), 1) - self.assertIs(iframe, oframes[0]) + oframes = resampler.resample(iframe) + assert len(oframes) == 1 + assert iframe is oframes[0] - # resample another frame - iframe.pts = 1024 + # resample another frame + iframe.pts = 1024 - oframes = resampler.resample(iframe) - self.assertEqual(len(oframes), 1) - assert iframe is oframes[0] + oframes = resampler.resample(iframe) + assert len(oframes) == 1 + assert iframe is oframes[0] - # flush - oframes = resampler.resample(None) - self.assertEqual(len(oframes), 0) + # flush + oframes = resampler.resample(None) + assert len(oframes) == 0 - def test_matching_passthrough(self) -> None: - """ - If the frames match, it won't do anything. - """ - resampler = AudioResampler("s16", "stereo") +def test_matching_passthrough() -> None: + """ + If the frames match, it won't do anything. + """ - # resample one frame - iframe = AudioFrame("s16", "stereo", 1024) + resampler = AudioResampler("s16", "stereo") - oframes = resampler.resample(iframe) - self.assertEqual(len(oframes), 1) - self.assertIs(iframe, oframes[0]) + # resample one frame + iframe = AudioFrame("s16", "stereo", 1024) - # resample another frame - iframe.pts = 1024 + oframes = resampler.resample(iframe) + assert len(oframes) == 1 + assert iframe is oframes[0] - oframes = resampler.resample(iframe) - self.assertEqual(len(oframes), 1) - self.assertIs(iframe, oframes[0]) + # resample another frame + iframe.pts = 1024 - # flush - oframes = resampler.resample(None) - self.assertEqual(len(oframes), 0) + oframes = resampler.resample(iframe) + assert len(oframes) == 1 + assert iframe is oframes[0] - def test_pts_assertion_same_rate(self) -> None: - av.logging.set_level(av.logging.VERBOSE) - resampler = AudioResampler("s16", "mono") + # flush + oframes = resampler.resample(None) + assert len(oframes) == 0 - # resample one frame - iframe = AudioFrame("s16", "stereo", 1024) - iframe.sample_rate = 48000 - iframe.time_base = Fraction(1, 48000) - iframe.pts = 0 - - oframes = resampler.resample(iframe) - self.assertEqual(len(oframes), 1) - - oframe = oframes[0] - self.assertEqual(oframe.pts, 0) - self.assertEqual(oframe.time_base, iframe.time_base) - self.assertEqual(oframe.sample_rate, iframe.sample_rate) - self.assertEqual(oframe.samples, iframe.samples) - - # resample another frame - iframe.pts = 1024 - - oframes = resampler.resample(iframe) - self.assertEqual(len(oframes), 1) - - oframe = oframes[0] - self.assertEqual(oframe.pts, 1024) - self.assertEqual(oframe.time_base, iframe.time_base) - self.assertEqual(oframe.sample_rate, iframe.sample_rate) - self.assertEqual(oframe.samples, iframe.samples) - - # resample another frame with a pts gap, do not raise exception - iframe.pts = 9999 - oframes = resampler.resample(iframe) - self.assertEqual(len(oframes), 1) - - oframe = oframes[0] - self.assertEqual(oframe.pts, 9999) - self.assertEqual(oframe.time_base, iframe.time_base) - self.assertEqual(oframe.sample_rate, iframe.sample_rate) - self.assertEqual(oframe.samples, iframe.samples) - - # flush - oframes = resampler.resample(None) - self.assertEqual(len(oframes), 0) - av.logging.set_level(None) - - def test_pts_assertion_new_rate_up(self) -> None: - resampler = AudioResampler("s16", "mono", 44100) - - # resample one frame - iframe = AudioFrame("s16", "stereo", 1024) - iframe.sample_rate = 48000 - iframe.time_base = Fraction(1, 48000) - iframe.pts = 0 - - oframes = resampler.resample(iframe) - self.assertEqual(len(oframes), 1) - - oframe = oframes[0] - self.assertEqual(oframe.pts, 0) - self.assertEqual(oframe.time_base, Fraction(1, 44100)) - self.assertEqual(oframe.sample_rate, 44100) - self.assertEqual(oframe.samples, 925) - - iframe = AudioFrame("s16", "stereo", 1024) - iframe.sample_rate = 48000 - iframe.time_base = Fraction(1, 48000) - iframe.pts = 1024 - - oframes = resampler.resample(iframe) - self.assertEqual(len(oframes), 1) - - oframe = oframes[0] - self.assertEqual(oframe.pts, 925) - self.assertEqual(oframe.time_base, Fraction(1, 44100)) - self.assertEqual(oframe.sample_rate, 44100) - self.assertEqual(oframe.samples, 941) - - # flush - oframes = resampler.resample(None) - self.assertEqual(len(oframes), 1) - - oframe = oframes[0] - self.assertEqual(oframe.pts, 941 + 925) - self.assertEqual(oframe.time_base, Fraction(1, 44100)) - self.assertEqual(oframe.sample_rate, 44100) - self.assertEqual(oframe.samples, 15) - - def test_pts_assertion_new_rate_down(self) -> None: - resampler = AudioResampler("s16", "mono", 48000) - - # resample one frame - iframe = AudioFrame("s16", "stereo", 1024) - iframe.sample_rate = 44100 - iframe.time_base = Fraction(1, 44100) - iframe.pts = 0 - - oframes = resampler.resample(iframe) - self.assertEqual(len(oframes), 1) - - oframe = oframes[0] - self.assertEqual(oframe.pts, 0) - self.assertEqual(oframe.time_base, Fraction(1, 48000)) - self.assertEqual(oframe.sample_rate, 48000) - self.assertEqual(oframe.samples, 1098) - - iframe = AudioFrame("s16", "stereo", 1024) - iframe.sample_rate = 44100 - iframe.time_base = Fraction(1, 44100) - iframe.pts = 1024 - - oframes = resampler.resample(iframe) - self.assertEqual(len(oframes), 1) - - oframe = oframes[0] - self.assertEqual(oframe.pts, 1098) - self.assertEqual(oframe.time_base, Fraction(1, 48000)) - self.assertEqual(oframe.sample_rate, 48000) - self.assertEqual(oframe.samples, 1114) - - # flush - oframes = resampler.resample(None) - self.assertEqual(len(oframes), 1) - - oframe = oframes[0] - self.assertEqual(oframe.pts, 1114 + 1098) - self.assertEqual(oframe.time_base, Fraction(1, 48000)) - self.assertEqual(oframe.sample_rate, 48000) - self.assertEqual(oframe.samples, 18) - - def test_pts_assertion_new_rate_fltp(self) -> None: - resampler = AudioResampler("fltp", "mono", 8000, 1024) - - # resample one frame - iframe = AudioFrame("s16", "mono", 1024) - iframe.sample_rate = 8000 - iframe.time_base = Fraction(1, 1000) - iframe.pts = 0 - - oframes = resampler.resample(iframe) - self.assertEqual(len(oframes), 1) - - oframe = oframes[0] - self.assertEqual(oframe.pts, 0) - self.assertEqual(oframe.time_base, Fraction(1, 8000)) - self.assertEqual(oframe.sample_rate, 8000) - self.assertEqual(oframe.samples, 1024) - - iframe = AudioFrame("s16", "mono", 1024) - iframe.sample_rate = 8000 - iframe.time_base = Fraction(1, 1000) - iframe.pts = 8192 - - oframes = resampler.resample(iframe) - self.assertEqual(len(oframes), 1) - - oframe = oframes[0] - self.assertEqual(oframe.pts, 65536) - self.assertEqual(oframe.time_base, Fraction(1, 8000)) - self.assertEqual(oframe.sample_rate, 8000) - self.assertEqual(oframe.samples, 1024) - - # flush - oframes = resampler.resample(None) - self.assertEqual(len(oframes), 0) - - def test_pts_missing_time_base(self) -> None: - resampler = AudioResampler("s16", "mono", 44100) - - # resample one frame - iframe = AudioFrame("s16", "stereo", 1024) - iframe.sample_rate = 48000 - iframe.pts = 0 - - oframes = resampler.resample(iframe) - self.assertEqual(len(oframes), 1) - - oframe = oframes[0] - self.assertEqual(oframe.pts, 0) - self.assertEqual(oframe.time_base, Fraction(1, 44100)) - self.assertEqual(oframe.sample_rate, 44100) - - # flush - oframes = resampler.resample(None) - self.assertEqual(len(oframes), 1) - - oframe = oframes[0] - self.assertEqual(oframe.pts, 925) - self.assertEqual(oframe.time_base, Fraction(1, 44100)) - self.assertEqual(oframe.sample_rate, 44100) - self.assertEqual(oframe.samples, 16) - - def test_mismatched_input(self) -> None: - """ - Consecutive frames must have the same layout, sample format and sample rate. - """ - resampler = AudioResampler("s16", "mono", 44100) - - # resample one frame - iframe = AudioFrame("s16", "stereo", 1024) - iframe.sample_rate = 48000 - resampler.resample(iframe) - # resample another frame with a sample format - iframe = AudioFrame("s16", "mono", 1024) - iframe.sample_rate = 48000 - with self.assertRaises(ValueError) as cm: - resampler.resample(iframe) +def test_pts_assertion_same_rate() -> None: + av.logging.set_level(av.logging.VERBOSE) + resampler = AudioResampler("s16", "mono") + + # resample one frame + iframe = AudioFrame("s16", "stereo", 1024) + iframe.sample_rate = 48000 + iframe.time_base = Fraction(1, 48000) + iframe.pts = 0 + + oframes = resampler.resample(iframe) + assert len(oframes) == 1 + + oframe = oframes[0] + assert oframe.pts == 0 + assert oframe.time_base == iframe.time_base + assert oframe.sample_rate == iframe.sample_rate + assert oframe.samples == iframe.samples + + # resample another frame + iframe.pts = 1024 + + oframes = resampler.resample(iframe) + assert len(oframes) == 1 + + oframe = oframes[0] + assert oframe.pts == 1024 + assert oframe.time_base == iframe.time_base + assert oframe.sample_rate == iframe.sample_rate + assert oframe.samples == iframe.samples + + # resample another frame with a pts gap, do not raise exception + iframe.pts = 9999 + oframes = resampler.resample(iframe) + assert len(oframes) == 1 - assert str(cm.exception) == "Frame does not match AudioResampler setup." + oframe = oframes[0] + assert oframe.pts == 9999 + assert oframe.time_base == iframe.time_base + assert oframe.sample_rate == iframe.sample_rate + assert oframe.samples == iframe.samples + + # flush + oframes = resampler.resample(None) + assert len(oframes) == 0 + av.logging.set_level(None) + + +def test_pts_assertion_new_rate_up() -> None: + resampler = AudioResampler("s16", "mono", 44100) + + # resample one frame + iframe = AudioFrame("s16", "stereo", 1024) + iframe.sample_rate = 48000 + iframe.time_base = Fraction(1, 48000) + iframe.pts = 0 + + oframes = resampler.resample(iframe) + assert len(oframes) == 1 + + oframe = oframes[0] + assert oframe.pts == 0 + assert oframe.time_base == Fraction(1, 44100) + assert oframe.sample_rate == 44100 + assert oframe.samples == 925 + + iframe = AudioFrame("s16", "stereo", 1024) + iframe.sample_rate = 48000 + iframe.time_base = Fraction(1, 48000) + iframe.pts = 1024 + + oframes = resampler.resample(iframe) + assert len(oframes) == 1 + + oframe = oframes[0] + assert oframe.pts == 925 + assert oframe.time_base == Fraction(1, 44100) + assert oframe.sample_rate == 44100 + assert oframe.samples == 941 + + # flush + oframes = resampler.resample(None) + assert len(oframes) == 1 + + oframe = oframes[0] + assert oframe.pts == 941 + 925 + assert oframe.time_base == Fraction(1, 44100) + assert oframe.sample_rate == 44100 + assert oframe.samples == 15 + + +def test_pts_assertion_new_rate_down() -> None: + resampler = AudioResampler("s16", "mono", 48000) + + # resample one frame + iframe = AudioFrame("s16", "stereo", 1024) + iframe.sample_rate = 44100 + iframe.time_base = Fraction(1, 44100) + iframe.pts = 0 + + oframes = resampler.resample(iframe) + assert len(oframes) == 1 + + oframe = oframes[0] + assert oframe.pts == 0 + assert oframe.time_base == Fraction(1, 48000) + assert oframe.sample_rate == 48000 + assert oframe.samples == 1098 + + iframe = AudioFrame("s16", "stereo", 1024) + iframe.sample_rate = 44100 + iframe.time_base = Fraction(1, 44100) + iframe.pts = 1024 + + oframes = resampler.resample(iframe) + assert len(oframes) == 1 + + oframe = oframes[0] + assert oframe.pts == 1098 + assert oframe.time_base == Fraction(1, 48000) + assert oframe.sample_rate == 48000 + assert oframe.samples == 1114 + + # flush + oframes = resampler.resample(None) + assert len(oframes) == 1 + + oframe = oframes[0] + assert oframe.pts == 1114 + 1098 + assert oframe.time_base == Fraction(1, 48000) + assert oframe.sample_rate == 48000 + assert oframe.samples == 18 + + +def test_pts_assertion_new_rate_fltp() -> None: + resampler = AudioResampler("fltp", "mono", 8000, 1024) + + # resample one frame + iframe = AudioFrame("s16", "mono", 1024) + iframe.sample_rate = 8000 + iframe.time_base = Fraction(1, 1000) + iframe.pts = 0 + + oframes = resampler.resample(iframe) + assert len(oframes) == 1 + + oframe = oframes[0] + assert oframe.pts == 0 + assert oframe.time_base == Fraction(1, 8000) + assert oframe.sample_rate == 8000 + assert oframe.samples == 1024 + + iframe = AudioFrame("s16", "mono", 1024) + iframe.sample_rate = 8000 + iframe.time_base = Fraction(1, 1000) + iframe.pts = 8192 + + oframes = resampler.resample(iframe) + assert len(oframes) == 1 + + oframe = oframes[0] + assert oframe.pts == 65536 + assert oframe.time_base == Fraction(1, 8000) + assert oframe.sample_rate == 8000 + assert oframe.samples == 1024 + + # flush + oframes = resampler.resample(None) + assert len(oframes) == 0 + + +def test_pts_missing_time_base() -> None: + resampler = AudioResampler("s16", "mono", 44100) + + # resample one frame + iframe = AudioFrame("s16", "stereo", 1024) + iframe.sample_rate = 48000 + iframe.pts = 0 + + oframes = resampler.resample(iframe) + assert len(oframes) == 1 + + oframe = oframes[0] + assert oframe.pts == 0 + assert oframe.time_base == Fraction(1, 44100) + assert oframe.sample_rate == 44100 + + # flush + oframes = resampler.resample(None) + assert len(oframes) == 1 + + oframe = oframes[0] + assert oframe.pts == 925 + assert oframe.time_base == Fraction(1, 44100) + assert oframe.sample_rate == 44100 + assert oframe.samples == 16 + + +def test_mismatched_input() -> None: + """ + Consecutive frames must have the same layout, sample format and sample rate. + """ + resampler = AudioResampler("s16", "mono", 44100) + + # resample one frame + iframe = AudioFrame("s16", "stereo", 1024) + iframe.sample_rate = 48000 + resampler.resample(iframe) + + # resample another frame with a sample format + iframe = AudioFrame("s16", "mono", 1024) + iframe.sample_rate = 48000 + with pytest.raises( + ValueError, match="Frame does not match AudioResampler setup." + ) as cm: + resampler.resample(iframe) diff --git a/tests/test_bitstream.py b/tests/test_bitstream.py index 335abac1c..26e54599b 100644 --- a/tests/test_bitstream.py +++ b/tests/test_bitstream.py @@ -1,5 +1,7 @@ from __future__ import annotations +import pytest + import av from av import Packet from av.bitstream import BitStreamFilterContext, bitstream_filters_available @@ -15,89 +17,93 @@ def is_annexb(packet: Packet | bytes | None) -> bool: return data[:3] == b"\0\0\x01" or data[:4] == b"\0\0\0\x01" -class TestBitStreamFilters(TestCase): - def test_filters_availible(self) -> None: - self.assertIn("h264_mp4toannexb", bitstream_filters_available) +def test_filters_availible() -> None: + assert "h264_mp4toannexb" in bitstream_filters_available - def test_filter_chomp(self) -> None: - ctx = BitStreamFilterContext("chomp") - src_packets: tuple[Packet, None] = (Packet(b"\x0012345\0\0\0"), None) - assert bytes(src_packets[0]) == b"\x0012345\0\0\0" +def test_filter_chomp() -> None: + ctx = BitStreamFilterContext("chomp") - result_packets = [] - for p in src_packets: - result_packets.extend(ctx.filter(p)) + src_packets: tuple[Packet, None] = (Packet(b"\x0012345\0\0\0"), None) + assert bytes(src_packets[0]) == b"\x0012345\0\0\0" - assert len(result_packets) == 1 - assert bytes(result_packets[0]) == b"\x0012345" + result_packets = [] + for p in src_packets: + result_packets.extend(ctx.filter(p)) - def test_filter_setts(self) -> None: - ctx = BitStreamFilterContext("setts=pts=N") + assert len(result_packets) == 1 + assert bytes(result_packets[0]) == b"\x0012345" - ctx2 = BitStreamFilterContext(b"setts=pts=N") - del ctx2 - p1 = Packet(b"\0") - p1.pts = 42 - p2 = Packet(b"\0") - p2.pts = 50 - src_packets = [p1, p2, None] +def test_filter_setts() -> None: + ctx = BitStreamFilterContext("setts=pts=N") - result_packets: list[Packet] = [] - for p in src_packets: - result_packets.extend(ctx.filter(p)) + ctx2 = BitStreamFilterContext(b"setts=pts=N") + del ctx2 - assert len(result_packets) == 2 - assert result_packets[0].pts == 0 - assert result_packets[1].pts == 1 + p1 = Packet(b"\0") + p1.pts = 42 + p2 = Packet(b"\0") + p2.pts = 50 + src_packets = [p1, p2, None] - def test_filter_h264_mp4toannexb(self) -> None: - with av.open(fate_suite("h264/interlaced_crop.mp4"), "r") as container: - stream = container.streams.video[0] - ctx = BitStreamFilterContext("h264_mp4toannexb", stream) + result_packets: list[Packet] = [] + for p in src_packets: + result_packets.extend(ctx.filter(p)) - res_packets = [] - for p in container.demux(stream): - self.assertFalse(is_annexb(p)) - res_packets.extend(ctx.filter(p)) + assert len(result_packets) == 2 + assert result_packets[0].pts == 0 + assert result_packets[1].pts == 1 - assert len(res_packets) == stream.frames - for p in res_packets: - assert is_annexb(p) +def test_filter_h264_mp4toannexb() -> None: + with av.open(fate_suite("h264/interlaced_crop.mp4"), "r") as container: + stream = container.streams.video[0] + ctx = BitStreamFilterContext("h264_mp4toannexb", stream) - def test_filter_output_parameters(self) -> None: - with av.open(fate_suite("h264/interlaced_crop.mp4"), "r") as container: - stream = container.streams.video[0] + res_packets = [] + for p in container.demux(stream): + assert not is_annexb(p) + res_packets.extend(ctx.filter(p)) - assert not is_annexb(stream.codec_context.extradata) - ctx = BitStreamFilterContext("h264_mp4toannexb", stream) - assert not is_annexb(stream.codec_context.extradata) - del ctx + assert len(res_packets) == stream.frames - _ = BitStreamFilterContext("h264_mp4toannexb", stream, out_stream=stream) - assert is_annexb(stream.codec_context.extradata) + for p in res_packets: + assert is_annexb(p) - def test_filter_flush(self) -> None: - with av.open(fate_suite("h264/interlaced_crop.mp4"), "r") as container: - stream = container.streams.video[0] - ctx = BitStreamFilterContext("h264_mp4toannexb", stream) - res_packets = [] - for p in container.demux(stream): - res_packets.extend(ctx.filter(p)) - assert len(res_packets) == stream.frames +def test_filter_output_parameters() -> None: + with av.open(fate_suite("h264/interlaced_crop.mp4"), "r") as container: + stream = container.streams.video[0] + + assert not is_annexb(stream.codec_context.extradata) + ctx = BitStreamFilterContext("h264_mp4toannexb", stream) + assert not is_annexb(stream.codec_context.extradata) + del ctx - container.seek(0) - # Without flushing, we expect to get an error: "A non-NULL packet sent after an EOF." - with self.assertRaises(ValueError): - for p in container.demux(stream): - ctx.filter(p) + _ = BitStreamFilterContext("h264_mp4toannexb", stream, out_stream=stream) + assert is_annexb(stream.codec_context.extradata) - ctx.flush() - container.seek(0) + +def test_filter_flush() -> None: + with av.open(fate_suite("h264/interlaced_crop.mp4"), "r") as container: + stream = container.streams.video[0] + ctx = BitStreamFilterContext("h264_mp4toannexb", stream) + + res_packets = [] + for p in container.demux(stream): + res_packets.extend(ctx.filter(p)) + assert len(res_packets) == stream.frames + + container.seek(0) + # Without flushing, we expect to get an error: "A non-NULL packet sent after an EOF." + with pytest.raises(ValueError): for p in container.demux(stream): - res_packets.extend(ctx.filter(p)) + ctx.filter(p) + + ctx.flush() + container.seek(0) + for p in container.demux(stream): + res_packets.extend(ctx.filter(p)) - assert len(res_packets) == stream.frames * 2 + assert len(res_packets) == stream.frames * 2 diff --git a/tests/test_codec.py b/tests/test_codec.py index 357509b2d..70e688435 100644 --- a/tests/test_codec.py +++ b/tests/test_codec.py @@ -1,99 +1,90 @@ +import pytest + from av import AudioFormat, Codec, VideoFormat, codecs_available from av.codec.codec import UnknownCodecError -from .common import TestCase - - -class TestCodecs(TestCase): - def test_codec_bogus(self) -> None: - with self.assertRaises(UnknownCodecError): - Codec("bogus123") - with self.assertRaises(UnknownCodecError): - Codec("bogus123", "w") - - def test_codec_mpeg4_decoder(self) -> None: - c = Codec("mpeg4") - - assert c.name == "mpeg4" - assert c.long_name == "MPEG-4 part 2" - assert c.type == "video" - assert c.id in (12, 13) - assert c.is_decoder - assert not c.is_encoder - assert c.delay - - # audio - assert c.audio_formats is None - assert c.audio_rates is None - - # video - # formats = c.video_formats - # assert formats - # assert isinstance(formats[0], VideoFormat) - # assert any(f.name == "yuv420p" for f in formats) - - assert c.frame_rates is None - - def test_codec_mpeg4_encoder(self) -> None: - c = Codec("mpeg4", "w") - assert c.name == "mpeg4" - assert c.long_name == "MPEG-4 part 2" - assert c.type == "video" - assert c.id in (12, 13) - assert c.is_encoder - assert not c.is_decoder - assert c.delay - - # audio - assert c.audio_formats is None - assert c.audio_rates is None - - # video - formats = c.video_formats - assert formats - assert isinstance(formats[0], VideoFormat) - assert any(f.name == "yuv420p" for f in formats) - assert c.frame_rates is None - - def test_codec_opus_decoder(self) -> None: - c = Codec("opus") - - self.assertEqual(c.name, "opus") - self.assertEqual(c.long_name, "Opus") - assert c.type == "audio" - assert c.is_decoder - assert not c.is_encoder - assert c.delay - - # audio - assert c.audio_formats is None - assert c.audio_rates is None - - # video - assert c.video_formats is None - assert c.frame_rates is None - - def test_codec_opus_encoder(self) -> None: - c = Codec("opus", "w") - assert c.name in ("opus", "libopus") - assert c.long_name in ("Opus", "libopus Opus") - assert c.type == "audio" - assert c.is_encoder - assert not c.is_decoder - assert c.delay - - # audio - formats = c.audio_formats - assert formats - assert isinstance(formats[0], AudioFormat) - assert any(f.name in ("flt", "fltp") for f in formats) - - assert c.audio_rates is not None - assert 48000 in c.audio_rates - - # video - assert c.video_formats is None - assert c.frame_rates is None - - def test_codecs_available(self) -> None: - assert codecs_available + +def test_codec_bogus() -> None: + with pytest.raises(UnknownCodecError): + Codec("bogus123") + with pytest.raises(UnknownCodecError): + Codec("bogus123", "w") + + +def test_codec_mpeg4_decoder() -> None: + c = Codec("mpeg4") + + assert c.name == "mpeg4" + assert c.long_name == "MPEG-4 part 2" + assert c.type == "video" + assert c.id in (12, 13) + assert c.is_decoder + assert not c.is_encoder + assert c.delay + + assert c.audio_formats is None and c.audio_rates is None + + # formats = c.video_formats + # assert formats + # assert isinstance(formats[0], VideoFormat) + # assert any(f.name == "yuv420p" for f in formats) + + assert c.frame_rates is None + + +def test_codec_mpeg4_encoder() -> None: + c = Codec("mpeg4", "w") + assert c.name == "mpeg4" + assert c.long_name == "MPEG-4 part 2" + assert c.type == "video" + assert c.id in (12, 13) + assert c.is_encoder + assert not c.is_decoder + assert c.delay + + assert c.audio_formats is None and c.audio_rates is None + + formats = c.video_formats + assert formats + assert isinstance(formats[0], VideoFormat) + assert any(f.name == "yuv420p" for f in formats) + assert c.frame_rates is None + + +def test_codec_opus_decoder() -> None: + c = Codec("opus") + + assert c.name == "opus" + assert c.long_name == "Opus" + assert c.type == "audio" + assert c.is_decoder + assert not c.is_encoder + assert c.delay + + assert c.audio_formats is None and c.audio_rates is None + assert c.video_formats is None and c.frame_rates is None + + +def test_codec_opus_encoder() -> None: + c = Codec("opus", "w") + assert c.name in ("opus", "libopus") + assert c.long_name in ("Opus", "libopus Opus") + assert c.type == "audio" + assert c.is_encoder + assert not c.is_decoder + assert c.delay + + # audio + formats = c.audio_formats + assert formats + assert isinstance(formats[0], AudioFormat) + assert any(f.name in ("flt", "fltp") for f in formats) + + assert c.audio_rates is not None + assert 48000 in c.audio_rates + + assert c.video_formats is None and c.frame_rates is None + + +def test_codecs_available() -> None: + assert codecs_available diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 2b28730a4..24b49f09a 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -3,7 +3,8 @@ import os from fractions import Fraction from typing import Iterator, TypedDict, overload -from unittest import SkipTest + +import pytest import av from av import ( @@ -116,39 +117,39 @@ def test_decoder_extradata(self): def test_decoder_gop_size(self) -> None: ctx = av.codec.Codec("h264", "r").create("video") - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): ctx.gop_size def test_decoder_timebase(self) -> None: ctx = av.codec.Codec("h264", "r").create() - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): ctx.time_base - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): ctx.time_base = Fraction(1, 25) def test_encoder_extradata(self): ctx = av.codec.Codec("h264", "w").create() assert ctx.extradata is None - self.assertEqual(ctx.extradata_size, 0) + assert ctx.extradata_size == 0 ctx.extradata = b"123" - self.assertEqual(ctx.extradata, b"123") - self.assertEqual(ctx.extradata_size, 3) + assert ctx.extradata == b"123" + assert ctx.extradata_size == 3 def test_encoder_pix_fmt(self): ctx = av.codec.Codec("h264", "w").create() # valid format ctx.pix_fmt = "yuv420p" - self.assertEqual(ctx.pix_fmt, "yuv420p") + assert ctx.pix_fmt == "yuv420p" # invalid format with self.assertRaises(ValueError) as cm: ctx.pix_fmt = "__unknown_pix_fmt" - self.assertEqual(str(cm.exception), "not a pixel format: '__unknown_pix_fmt'") - self.assertEqual(ctx.pix_fmt, "yuv420p") + assert str(cm.exception) == "not a pixel format: '__unknown_pix_fmt'" + assert ctx.pix_fmt == "yuv420p" def test_bits_per_coded_sample(self): with av.open(fate_suite("qtrle/aletrek-rle.mov")) as container: @@ -158,21 +159,20 @@ def test_bits_per_coded_sample(self): for packet in container.demux(stream): for frame in packet.decode(): pass - self.assertEqual(packet.stream.bits_per_coded_sample, 32) + assert packet.stream.bits_per_coded_sample == 32 with av.open(fate_suite("qtrle/aletrek-rle.mov")) as container: stream = container.streams.video[0] stream.bits_per_coded_sample = 31 - with self.assertRaises(av.error.InvalidDataError): - for packet in container.demux(stream): - for frame in packet.decode(): - pass + with pytest.raises(av.error.InvalidDataError): + for frame in container.decode(stream): + pass with av.open(self.sandboxed("output.mov"), "w") as output: stream = output.add_stream("qtrle") - with self.assertRaises(ValueError): + with pytest.raises(ValueError): stream.codec_context.bits_per_coded_sample = 32 def test_parse(self): @@ -200,7 +200,7 @@ def _assert_parse(self, codec_name, path): packets.extend(ctx.parse()) parsed_source = b"".join(bytes(p) for p in packets) - self.assertEqual(len(parsed_source), len(full_source)) + assert len(parsed_source) == len(full_source) assert full_source == parsed_source @@ -218,7 +218,7 @@ def image_sequence_encode(self, codec_name: str) -> None: try: codec = Codec(codec_name, "w") except UnknownCodecError: - raise SkipTest(f"Unknown codec: {codec_name}") + pytest.skip(f"Unknown codec: {codec_name}") container = av.open(fate_suite("h264/interlaced_crop.mp4")) video_stream = container.streams.video[0] @@ -311,7 +311,7 @@ def video_encoding( try: codec = Codec(codec_name, "w") except UnknownCodecError: - raise SkipTest(f"Unknown codec: {codec_name}") + pytest.skip(f"Unknown codec: {codec_name}") container = av.open(fate_suite("h264/interlaced_crop.mp4")) video_stream = container.streams.video[0] @@ -388,7 +388,7 @@ def video_encoding( if codec_name in ("dvvideo", "dnxhd") and all( i == 1 for i in decoded_gop_sizes ): - raise SkipTest("I'm not sure why we skip this actually.") + pytest.skip() for i in decoded_gop_sizes: assert i == gop_size @@ -416,12 +416,12 @@ def _audio_encoding( try: codec = Codec(codec_name, "w") except UnknownCodecError: - raise SkipTest(f"Unknown codec: {codec_name}") + pytest.skip(f"Unknown codec: {codec_name}") ctx = codec.create(kind="audio") if ctx.codec.experimental: - raise SkipTest(f"Experimental codec: {codec_name}") + pytest.skip(f"Experimental codec: {codec_name}") assert ctx.codec.audio_formats sample_fmt = ctx.codec.audio_formats[-1].name diff --git a/tests/test_colorspace.py b/tests/test_colorspace.py index b54229643..7574c147d 100644 --- a/tests/test_colorspace.py +++ b/tests/test_colorspace.py @@ -4,35 +4,35 @@ from .common import fate_suite -class TestColorSpace: - def test_penguin_joke(self) -> None: - container = av.open( - fate_suite("amv/MTV_high_res_320x240_sample_Penguin_Joke_MTV_from_WMV.amv") - ) - stream = container.streams.video[0] - - assert stream.codec_context.color_range == 2 - assert stream.codec_context.color_range == ColorRange.JPEG - - assert stream.codec_context.color_primaries == 2 - assert stream.codec_context.color_trc == 2 - - assert stream.codec_context.colorspace == 5 - assert stream.codec_context.colorspace == Colorspace.ITU601 - - for frame in container.decode(stream): - assert frame.color_range == ColorRange.JPEG # a.k.a "pc" - assert frame.colorspace == Colorspace.ITU601 - return - - def test_sky_timelapse(self) -> None: - container = av.open( - av.datasets.curated("pexels/time-lapse-video-of-night-sky-857195.mp4") - ) - stream = container.streams.video[0] - - assert stream.codec_context.color_range == 1 - assert stream.codec_context.color_range == ColorRange.MPEG - assert stream.codec_context.color_primaries == 1 - assert stream.codec_context.color_trc == 1 - assert stream.codec_context.colorspace == 1 +def test_penguin_joke() -> None: + container = av.open( + fate_suite("amv/MTV_high_res_320x240_sample_Penguin_Joke_MTV_from_WMV.amv") + ) + stream = container.streams.video[0] + + assert stream.codec_context.color_range == 2 + assert stream.codec_context.color_range == ColorRange.JPEG + + assert stream.codec_context.color_primaries == 2 + assert stream.codec_context.color_trc == 2 + + assert stream.codec_context.colorspace == 5 + assert stream.codec_context.colorspace == Colorspace.ITU601 + + for frame in container.decode(stream): + assert frame.color_range == ColorRange.JPEG # a.k.a "pc" + assert frame.colorspace == Colorspace.ITU601 + return + + +def test_sky_timelapse() -> None: + container = av.open( + av.datasets.curated("pexels/time-lapse-video-of-night-sky-857195.mp4") + ) + stream = container.streams.video[0] + + assert stream.codec_context.color_range == 1 + assert stream.codec_context.color_range == ColorRange.MPEG + assert stream.codec_context.color_primaries == 1 + assert stream.codec_context.color_trc == 1 + assert stream.codec_context.colorspace == 1 diff --git a/tests/test_containerformat.py b/tests/test_containerformat.py index 6475cf9cd..43f1eca9f 100644 --- a/tests/test_containerformat.py +++ b/tests/test_containerformat.py @@ -1,57 +1,60 @@ from av import ContainerFormat, formats_available, open -class TestContainerFormats: - def test_matroska(self) -> None: - with open("test.mkv", "w") as container: - assert container.default_video_codec != "none" - assert container.default_audio_codec != "none" - assert container.default_subtitle_codec == "ass" - assert "ass" in container.supported_codecs - - fmt = ContainerFormat("matroska") - assert fmt.is_input and fmt.is_output - assert fmt.name == "matroska" - assert fmt.long_name == "Matroska" - assert "mkv" in fmt.extensions - assert not fmt.no_file - - def test_mov(self) -> None: - with open("test.mov", "w") as container: - assert container.default_video_codec != "none" - assert container.default_audio_codec != "none" - assert container.default_subtitle_codec == "none" - assert "h264" in container.supported_codecs - - fmt = ContainerFormat("mov") - assert fmt.is_input and fmt.is_output - assert fmt.name == "mov" - assert fmt.long_name == "QuickTime / MOV" - assert "mov" in fmt.extensions - assert not fmt.no_file - - def test_gif(self) -> None: - with open("test.gif", "w") as container: - assert container.default_video_codec == "gif" - assert container.default_audio_codec == "none" - assert container.default_subtitle_codec == "none" - assert "gif" in container.supported_codecs - - def test_stream_segment(self) -> None: - # This format goes by two names, check both. - fmt = ContainerFormat("stream_segment") - assert not fmt.is_input and fmt.is_output - assert fmt.name == "stream_segment" - assert fmt.long_name == "streaming segment muxer" - assert fmt.extensions == set() - assert fmt.no_file - - fmt = ContainerFormat("ssegment") - assert not fmt.is_input and fmt.is_output - assert fmt.name == "ssegment" - assert fmt.long_name == "streaming segment muxer" - assert fmt.extensions == set() - assert fmt.no_file - - def test_formats_available(self) -> None: - assert formats_available +def test_matroska() -> None: + with open("test.mkv", "w") as container: + assert container.default_video_codec != "none" + assert container.default_audio_codec != "none" + assert container.default_subtitle_codec == "ass" + assert "ass" in container.supported_codecs + + fmt = ContainerFormat("matroska") + assert fmt.is_input and fmt.is_output + assert fmt.name == "matroska" + assert fmt.long_name == "Matroska" + assert "mkv" in fmt.extensions + assert not fmt.no_file + + +def test_mov() -> None: + with open("test.mov", "w") as container: + assert container.default_video_codec != "none" + assert container.default_audio_codec != "none" + assert container.default_subtitle_codec == "none" + assert "h264" in container.supported_codecs + + fmt = ContainerFormat("mov") + assert fmt.is_input and fmt.is_output + assert fmt.name == "mov" + assert fmt.long_name == "QuickTime / MOV" + assert "mov" in fmt.extensions + assert not fmt.no_file + + +def test_gif() -> None: + with open("test.gif", "w") as container: + assert container.default_video_codec == "gif" + assert container.default_audio_codec == "none" + assert container.default_subtitle_codec == "none" + assert "gif" in container.supported_codecs + + +def test_stream_segment() -> None: + # This format goes by two names, check both. + fmt = ContainerFormat("stream_segment") + assert not fmt.is_input and fmt.is_output + assert fmt.name == "stream_segment" + assert fmt.long_name == "streaming segment muxer" + assert fmt.extensions == set() + assert fmt.no_file + + fmt = ContainerFormat("ssegment") + assert not fmt.is_input and fmt.is_output + assert fmt.name == "ssegment" + assert fmt.long_name == "streaming segment muxer" + assert fmt.extensions == set() + assert fmt.no_file + + +def test_formats_available() -> None: + assert formats_available diff --git a/tests/test_deprecation.py b/tests/test_deprecation.py index f8857ab73..f1b0b5d4b 100644 --- a/tests/test_deprecation.py +++ b/tests/test_deprecation.py @@ -2,48 +2,42 @@ from av import deprecation -from .common import TestCase +def test_method() -> None: + class Example: + def __init__(self, x: int = 100) -> None: + self.x = x -class TestDeprecations(TestCase): - def test_method(self): - class Example: - def __init__(self, x=100): - self.x = x + @deprecation.method + def foo(self, a: int, b: int) -> int: + return self.x + a + b - @deprecation.method - def foo(self, a, b): - return self.x + a + b + obj = Example() - obj = Example() + with warnings.catch_warnings(record=True) as captured: + assert obj.foo(20, b=3) == 123 + assert "Example.foo is deprecated" in str(captured[0].message) - with warnings.catch_warnings(record=True) as captured: - self.assertEqual(obj.foo(20, b=3), 123) - self.assertIn("Example.foo is deprecated", captured[0].message.args[0]) - def test_renamed_attr(self): - class Example: - new_value = "foo" - old_value = deprecation.renamed_attr("new_value") +def test_renamed_attr() -> None: + class Example: + new_value = "foo" + old_value = deprecation.renamed_attr("new_value") - def new_func(self, a, b): - return a + b + def new_func(self, a: int, b: int) -> int: + return a + b - old_func = deprecation.renamed_attr("new_func") + old_func = deprecation.renamed_attr("new_func") - obj = Example() + obj = Example() - with warnings.catch_warnings(record=True) as captured: - self.assertEqual(obj.old_value, "foo") - self.assertIn( - "Example.old_value is deprecated", captured[0].message.args[0] - ) + with warnings.catch_warnings(record=True) as captured: + assert obj.old_value == "foo" + assert "Example.old_value is deprecated" in str(captured[0].message) - obj.old_value = "bar" - self.assertIn( - "Example.old_value is deprecated", captured[1].message.args[0] - ) + obj.old_value = "bar" + assert "Example.old_value is deprecated" in str(captured[1].message) - with warnings.catch_warnings(record=True) as captured: - self.assertEqual(obj.old_func(1, 2), 3) - self.assertIn("Example.old_func is deprecated", captured[0].message.args[0]) + with warnings.catch_warnings(record=True) as captured: + assert obj.old_func(1, 2) == 3 + assert "Example.old_func is deprecated" in str(captured[0].message) diff --git a/tests/test_dictionary.py b/tests/test_dictionary.py index a1c2f80d8..e577c7a07 100644 --- a/tests/test_dictionary.py +++ b/tests/test_dictionary.py @@ -1,18 +1,17 @@ -from av.dictionary import Dictionary +import pytest -from .common import TestCase +from av.dictionary import Dictionary -class TestDictionary(TestCase): - def test_basics(self): - d = Dictionary() - d["key"] = "value" +def test_dictionary() -> None: + d = Dictionary() + d["key"] = "value" - self.assertEqual(d["key"], "value") - self.assertIn("key", d) - self.assertEqual(len(d), 1) - self.assertEqual(list(d), ["key"]) + assert d["key"] == "value" + assert "key" in d + assert len(d) == 1 + assert list(d) == ["key"] - self.assertEqual(d.pop("key"), "value") - self.assertRaises(KeyError, d.pop, "key") - self.assertEqual(len(d), 0) + assert d.pop("key") == "value" + pytest.raises(KeyError, d.pop, "key") + assert len(d) == 0 diff --git a/tests/test_encode.py b/tests/test_encode.py index 40b17cd73..e2ab1ba2f 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -70,11 +70,8 @@ def assert_rgb_rotate( ) -> None: # Now inspect it a little. assert len(input_.streams) == 1 - if is_dash: - self.assertTrue(input_.metadata.get("Title") == "container", input_.metadata) - else: - self.assertEqual(input_.metadata.get("title"), "container", input_.metadata) - self.assertEqual(input_.metadata.get("key"), None) + assert input_.metadata.get("Title" if is_dash else "title") == "container" + assert input_.metadata.get("key") is None stream = input_.streams[0] @@ -93,7 +90,6 @@ def assert_rgb_rotate( expected_id = 1 # actual stream properties - self.assertIsInstance(stream, VideoStream) assert isinstance(stream, VideoStream) assert stream.average_rate == expected_average_rate assert stream.base_rate == 24 @@ -119,7 +115,7 @@ class TestBasicVideoEncoding(TestCase): def test_default_options(self) -> None: with av.open(self.sandboxed("output.mov"), "w") as output: stream = output.add_stream("mpeg4") - self.assertIn(stream, output.streams.video) + assert stream in output.streams.video assert stream.average_rate == Fraction(24, 1) assert stream.time_base is None @@ -145,7 +141,7 @@ def test_encoding_with_pts(self) -> None: with av.open(path, "w") as output: stream = output.add_stream("h264", 24) - self.assertIn(stream, output.streams.video) + assert stream in output.streams.video stream.width = WIDTH stream.height = HEIGHT stream.pix_fmt = "yuv420p" @@ -176,7 +172,7 @@ class TestBasicAudioEncoding(TestCase): def test_default_options(self) -> None: with av.open(self.sandboxed("output.mov"), "w") as output: stream = output.add_stream("mp2") - self.assertIn(stream, output.streams.audio) + assert stream in output.streams.audio assert stream.time_base is None # codec context properties @@ -196,7 +192,7 @@ def test_transcode(self) -> None: sample_fmt = "s16" stream = output.add_stream("mp2", sample_rate) - self.assertIn(stream, output.streams.audio) + assert stream in output.streams.audio ctx = stream.codec_context ctx.sample_rate = sample_rate @@ -215,9 +211,7 @@ def test_transcode(self) -> None: with av.open(path) as container: assert len(container.streams) == 1 - self.assertEqual( - container.metadata.get("title"), "container", container.metadata - ) + assert container.metadata.get("title") == "container" assert container.metadata.get("key") is None assert isinstance(container.streams[0], AudioStream) @@ -301,7 +295,7 @@ def test_stream_audio_resample(self) -> None: def test_set_id_and_time_base(self) -> None: with av.open(self.sandboxed("output.mov"), "w") as output: stream = output.add_stream("mp2") - self.assertIn(stream, output.streams.audio) + assert stream in output.streams.audio # set id assert stream.id == 0 diff --git a/tests/test_enums.py b/tests/test_enums.py index e96956eca..64b76d9eb 100644 --- a/tests/test_enums.py +++ b/tests/test_enums.py @@ -1,225 +1,223 @@ import pickle -from av.enum import EnumType, define_enum +import pytest -from .common import TestCase +from av.enum import EnumType, define_enum -# This must be at the top-level. PickleableFooBar = define_enum("PickleableFooBar", __name__, [("FOO", 1)]) -class TestEnums(TestCase): - def define_foobar(self, **kwargs): - return define_enum("Foobar", __name__, (("FOO", 1), ("BAR", 2)), **kwargs) +def define_foobar(is_flags: bool = False): + return define_enum("Foobar", __name__, (("FOO", 1), ("BAR", 2)), is_flags=is_flags) + + +def test_basics(): + cls = define_foobar() + + assert isinstance(cls, EnumType) + + foo = cls.FOO + + assert isinstance(foo, cls) + assert foo.name == "FOO" and foo.value == 1 + assert not isinstance(foo, PickleableFooBar) + + +def test_access(): + cls = define_foobar() + foo1 = cls.FOO + foo2 = cls["FOO"] + foo3 = cls[1] + foo4 = cls[foo1] + assert foo1 is foo2 + assert foo1 is foo3 + assert foo1 is foo4 + + assert foo1 in cls and "FOO" in cls and 1 in cls + + pytest.raises(KeyError, lambda: cls["not a foo"]) + pytest.raises(KeyError, lambda: cls[10]) + pytest.raises(TypeError, lambda: cls[()]) + + assert cls.get("FOO") == foo1 + assert cls.get("not a foo") is None + + +def test_casting(): + cls = define_foobar() + foo = cls.FOO + + assert repr(foo) == "" + + str_foo = str(foo) + assert isinstance(str_foo, str) and str_foo == "FOO" + + int_foo = int(foo) + assert isinstance(int_foo, int) and int_foo == 1 + + +def test_iteration(): + cls = define_foobar() + assert list(cls) == [cls.FOO, cls.BAR] + + +def test_equality(): + cls = define_foobar() + foo = cls.FOO + bar = cls.BAR + + assert foo == "FOO" and foo == 1 and foo == foo + assert foo != "BAR" and foo != 2 and foo != bar - def test_basics(self): - cls = self.define_foobar() + pytest.raises(ValueError, lambda: foo == "not a foo") + pytest.raises(ValueError, lambda: foo == 10) + pytest.raises(TypeError, lambda: foo == ()) - assert isinstance(cls, EnumType) - foo = cls.FOO +def test_as_key(): + cls = define_foobar() + foo = cls.FOO - assert isinstance(foo, cls) - assert foo.name == "FOO" and foo.value == 1 - assert not isinstance(foo, PickleableFooBar) + d = {foo: "value"} + assert d[foo] == "value" + assert d.get("FOO") is None + assert d.get(1) is None - def test_access(self): - cls = self.define_foobar() - foo1 = cls.FOO - foo2 = cls["FOO"] - foo3 = cls[1] - foo4 = cls[foo1] - assert foo1 is foo2 - assert foo1 is foo3 - assert foo1 is foo4 - assert foo1 in cls - assert "FOO" in cls - assert 1 in cls +def test_pickleable(): + cls = PickleableFooBar + foo = cls.FOO - self.assertRaises(KeyError, lambda: cls["not a foo"]) - self.assertRaises(KeyError, lambda: cls[10]) - self.assertRaises(TypeError, lambda: cls[()]) + enc = pickle.dumps(foo) - self.assertEqual(cls.get("FOO"), foo1) - assert cls.get("not a foo") is None + foo2 = pickle.loads(enc) - def test_casting(self): - cls = self.define_foobar() - foo = cls.FOO + assert foo is foo2 - self.assertEqual(repr(foo), "") - str_foo = str(foo) - assert isinstance(str_foo, str) - assert str_foo == "FOO" +def test_create_unknown(): + cls = define_foobar() + baz = cls.get(3, create=True) - int_foo = int(foo) - assert isinstance(int_foo, int) - assert int_foo == 1 + assert baz.name == "FOOBAR_3" + assert baz.value == 3 - def test_iteration(self): - cls = self.define_foobar() - self.assertEqual(list(cls), [cls.FOO, cls.BAR]) - def test_equality(self): - cls = self.define_foobar() - foo = cls.FOO - bar = cls.BAR +def test_multiple_names(): + cls = define_enum( + "FFooBBar", + __name__, + (("FOO", 1), ("F", 1), ("BAR", 2), ("B", 2)), + ) - self.assertEqual(foo, "FOO") - self.assertEqual(foo, 1) - self.assertEqual(foo, foo) - self.assertNotEqual(foo, "BAR") - self.assertNotEqual(foo, 2) - self.assertNotEqual(foo, bar) - - self.assertRaises(ValueError, lambda: foo == "not a foo") - self.assertRaises(ValueError, lambda: foo == 10) - self.assertRaises(TypeError, lambda: foo == ()) + assert cls.F is cls.FOO - def test_as_key(self): - cls = self.define_foobar() - foo = cls.FOO + assert cls.F.name == "FOO" + assert cls.F.name != "F" # This is actually the string. - d = {foo: "value"} - self.assertEqual(d[foo], "value") - assert d.get("FOO") is None - assert d.get(1) is None + assert cls.F == "FOO" + assert cls.F == "F" + assert cls.F != "BAR" + assert cls.F != "B" + pytest.raises(ValueError, lambda: cls.F == "x") - def test_pickleable(self): - cls = PickleableFooBar - foo = cls.FOO - enc = pickle.dumps(foo) +def test_flag_basics(): + cls = define_enum( + "FoobarAllFlags", + __name__, + {"FOO": 1, "BAR": 2, "FOOBAR": 3}.items(), + is_flags=True, + ) + foo = cls.FOO + bar = cls.BAR - foo2 = pickle.loads(enc) + foobar = foo | bar + assert foobar is cls.FOOBAR - assert foo is foo2 + foo2 = foobar & foo + assert foo2 is foo - def test_create_unknown(self): - cls = self.define_foobar() - baz = cls.get(3, create=True) + bar2 = foobar ^ foo + assert bar2 is bar - self.assertEqual(baz.name, "FOOBAR_3") - self.assertEqual(baz.value, 3) + bar3 = foobar & ~foo + assert bar3 is bar - def test_multiple_names(self): - cls = define_enum( - "FFooBBar", - __name__, - ( - ("FOO", 1), - ("F", 1), - ("BAR", 2), - ("B", 2), - ), - ) + x = cls.FOO + x |= cls.BAR + assert x is cls.FOOBAR - assert cls.F is cls.FOO - - self.assertEqual(cls.F.name, "FOO") - self.assertNotEqual(cls.F.name, "F") # This is actually the string. - - self.assertEqual(cls.F, "FOO") - self.assertEqual(cls.F, "F") - self.assertNotEqual(cls.F, "BAR") - self.assertNotEqual(cls.F, "B") - self.assertRaises(ValueError, lambda: cls.F == "x") - - def test_flag_basics(self): - cls = define_enum( - "FoobarAllFlags", - __name__, - dict(FOO=1, BAR=2, FOOBAR=3).items(), - is_flags=True, - ) - foo = cls.FOO - bar = cls.BAR + x = cls.FOOBAR + x &= cls.FOO + assert x is cls.FOO - foobar = foo | bar - assert foobar is cls.FOOBAR - foo2 = foobar & foo - assert foo2 is foo +def test_multi_flags_basics(): + cls = define_foobar(is_flags=True) - bar2 = foobar ^ foo - assert bar2 is bar + foo = cls.FOO + bar = cls.BAR + foobar = foo | bar + assert foobar.name == "FOO|BAR" + assert foobar.value == 3 + assert foobar.flags == (foo, bar) - bar3 = foobar & ~foo - assert bar3 is bar + foobar2 = foo | bar + foobar3 = cls[3] + foobar4 = cls[foobar] - x = cls.FOO - x |= cls.BAR - assert x is cls.FOOBAR + assert foobar is foobar2 + assert foobar is foobar3 + assert foobar is foobar4 - x = cls.FOOBAR - x &= cls.FOO - assert x is cls.FOO + pytest.raises(KeyError, lambda: cls["FOO|BAR"]) - def test_multi_flags_basics(self): - cls = self.define_foobar(is_flags=True) - - foo = cls.FOO - bar = cls.BAR - foobar = foo | bar - self.assertEqual(foobar.name, "FOO|BAR") - self.assertEqual(foobar.value, 3) - self.assertEqual(foobar.flags, (foo, bar)) + assert len(cls) == 2 # It didn't get bigger + assert list(cls) == [foo, bar] - foobar2 = foo | bar - foobar3 = cls[3] - foobar4 = cls[foobar] - assert foobar is foobar2 - assert foobar is foobar3 - assert foobar is foobar4 +def test_multi_flags_create_missing(): + cls = define_foobar(is_flags=True) - self.assertRaises(KeyError, lambda: cls["FOO|BAR"]) + foobar = cls[3] + assert foobar is cls.FOO | cls.BAR - self.assertEqual(len(cls), 2) # It didn't get bigger - self.assertEqual(list(cls), [foo, bar]) + pytest.raises(KeyError, lambda: cls[4]) # Not FOO or BAR + pytest.raises(KeyError, lambda: cls[7]) # FOO and BAR and missing flag. - def test_multi_flags_create_missing(self): - cls = self.define_foobar(is_flags=True) - foobar = cls[3] - assert foobar is cls.FOO | cls.BAR +def test_properties(): + Flags = define_foobar(is_flags=True) + foobar = Flags.FOO | Flags.BAR - self.assertRaises(KeyError, lambda: cls[4]) # Not FOO or BAR - self.assertRaises(KeyError, lambda: cls[7]) # FOO and BAR and missing flag. + class Class: + def __init__(self, value): + self.value = Flags[value].value - def test_properties(self): - Flags = self.define_foobar(is_flags=True) - foobar = Flags.FOO | Flags.BAR + @Flags.property + def flags(self): + return self.value - class Class: - def __init__(self, value): - self.value = Flags[value].value + @flags.setter + def flags(self, value): + self.value = value - @Flags.property - def flags(self): - return self.value + foo = flags.flag_property("FOO") + bar = flags.flag_property("BAR") - @flags.setter - def flags(self, value): - self.value = value - - foo = flags.flag_property("FOO") - bar = flags.flag_property("BAR") - - obj = Class("FOO") + obj = Class("FOO") - assert obj.flags is Flags.FOO - self.assertTrue(obj.foo) - self.assertFalse(obj.bar) + assert obj.flags is Flags.FOO + assert obj.foo + assert not obj.bar - obj.bar = True - assert obj.flags is foobar - self.assertTrue(obj.foo) - self.assertTrue(obj.bar) + obj.bar = True + assert obj.flags is foobar + assert obj.foo + assert obj.bar - obj.foo = False - assert obj.flags is Flags.BAR - self.assertFalse(obj.foo) - self.assertTrue(obj.bar) + obj.foo = False + assert obj.flags is Flags.BAR + assert not obj.foo + assert obj.bar diff --git a/tests/test_errors.py b/tests/test_errors.py index fcd08845e..5f7440402 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -1,61 +1,63 @@ import errno -import traceback +from traceback import format_exception_only import av -from .common import TestCase, is_windows - - -class TestErrorBasics(TestCase): - def test_stringify(self) -> None: - for cls in (av.ValueError, av.FileNotFoundError, av.DecoderNotFoundError): - e = cls(1, "foo") - assert f"{e}" == "[Errno 1] foo" - assert f"{e!r}" == f"{cls.__name__}(1, 'foo')" - assert ( - traceback.format_exception_only(cls, e)[-1] - == f"av.error.{cls.__name__}: [Errno 1] foo\n" - ) - - for cls in (av.ValueError, av.FileNotFoundError, av.DecoderNotFoundError): - e = cls(1, "foo", "bar.txt") - assert f"{e}" == "[Errno 1] foo: 'bar.txt'" - assert f"{e!r}" == f"{cls.__name__}(1, 'foo', 'bar.txt')" - assert ( - traceback.format_exception_only(cls, e)[-1] - == f"av.error.{cls.__name__}: [Errno 1] foo: 'bar.txt'\n" +from .common import is_windows + + +def test_stringify() -> None: + for cls in (av.ValueError, av.FileNotFoundError, av.DecoderNotFoundError): + e = cls(1, "foo") + assert f"{e}" == "[Errno 1] foo" + assert f"{e!r}" == f"{cls.__name__}(1, 'foo')" + assert ( + format_exception_only(cls, e)[-1] + == f"av.error.{cls.__name__}: [Errno 1] foo\n" + ) + + for cls in (av.ValueError, av.FileNotFoundError, av.DecoderNotFoundError): + e = cls(1, "foo", "bar.txt") + assert f"{e}" == "[Errno 1] foo: 'bar.txt'" + assert f"{e!r}" == f"{cls.__name__}(1, 'foo', 'bar.txt')" + assert ( + format_exception_only(cls, e)[-1] + == f"av.error.{cls.__name__}: [Errno 1] foo: 'bar.txt'\n" + ) + + +def test_bases() -> None: + assert issubclass(av.ValueError, ValueError) + assert issubclass(av.ValueError, av.FFmpegError) + + assert issubclass(av.FileNotFoundError, FileNotFoundError) + assert issubclass(av.FileNotFoundError, OSError) + assert issubclass(av.FileNotFoundError, av.FFmpegError) + + +def test_filenotfound(): + """Catch using builtin class on Python 3.3""" + try: + av.open("does not exist") + except FileNotFoundError as e: + assert e.errno == errno.ENOENT + if is_windows: + assert e.strerror in ( + "Error number -2 occurred", + "No such file or directory", ) - - def test_bases(self) -> None: - assert issubclass(av.ValueError, ValueError) - assert issubclass(av.ValueError, av.FFmpegError) - - assert issubclass(av.FileNotFoundError, FileNotFoundError) - assert issubclass(av.FileNotFoundError, OSError) - assert issubclass(av.FileNotFoundError, av.FFmpegError) - - def test_filenotfound(self): - """Catch using builtin class on Python 3.3""" - try: - av.open("does not exist") - except FileNotFoundError as e: - assert e.errno == errno.ENOENT - if is_windows: - self.assertTrue( - e.strerror - in ["Error number -2 occurred", "No such file or directory"] - ) - else: - assert e.strerror == "No such file or directory" - assert e.filename == "does not exist" - else: - self.fail("no exception raised") - - def test_buffertoosmall(self): - """Throw an exception from an enum.""" - try: - av.error.err_check(-av.error.BUFFER_TOO_SMALL.value) - except av.BufferTooSmallError as e: - assert e.errno == av.error.BUFFER_TOO_SMALL.value else: - self.fail("no exception raised") + assert e.strerror == "No such file or directory" + assert e.filename == "does not exist" + else: + assert False, "No exception raised!" + + +def test_buffertoosmall(): + """Throw an exception from an enum.""" + try: + av.error.err_check(-av.error.BUFFER_TOO_SMALL.value) + except av.BufferTooSmallError as e: + assert e.errno == av.error.BUFFER_TOO_SMALL.value + else: + assert False, "No exception raised!" diff --git a/tests/test_python_io.py b/tests/test_python_io.py index 9f5e55999..8324b870b 100644 --- a/tests/test_python_io.py +++ b/tests/test_python_io.py @@ -1,19 +1,43 @@ +import functools +import types from io import BytesIO from unittest import SkipTest import av -from .common import ( - MethodLogger, - TestCase, - fate_png, - fate_suite, - has_pillow, - run_in_sandbox, -) +from .common import TestCase, fate_png, fate_suite, has_pillow, run_in_sandbox from .test_encode import assert_rgb_rotate, write_rgb_rotate +class MethodLogger: + def __init__(self, obj): + self._obj = obj + self._log = [] + + def __getattr__(self, name): + value = getattr(self._obj, name) + if isinstance( + value, + ( + types.MethodType, + types.FunctionType, + types.BuiltinFunctionType, + types.BuiltinMethodType, + ), + ): + return functools.partial(self._method, name, value) + else: + self._log.append(("__getattr__", (name,), {})) + return value + + def _method(self, name, meth, *args, **kwargs): + self._log.append((name, args, kwargs)) + return meth(*args, **kwargs) + + def _filter(self, type_): + return [log for log in self._log if log[0] == type_] + + class BrokenBuffer(BytesIO): """ Buffer which can be "broken" to simulate an I/O error. @@ -103,7 +127,7 @@ def io_open(self, url, flags, options): elif (flags & 2) == 2: mode = "wb" else: - raise RuntimeError("Unsupported io open mode {}".format(flags)) + raise RuntimeError(f"Unsupported io open mode {flags}") return MethodLogger(open(url, mode)) @@ -137,9 +161,10 @@ def test_reading_from_write_readonly(self): buf = WriteOnlyPipe(fh.read()) with self.assertRaises(ValueError) as cm: self.read(buf, seekable=False) - self.assertEqual( - str(cm.exception), - "File object has no read() method, or readable() returned False.", + + assert ( + str(cm.exception) + == "File object has no read() method, or readable() returned False." ) def test_writing_to_buffer(self): @@ -199,13 +224,13 @@ def test_writing_to_custom_io_dash(self): all_write = all( method_log._filter("write") for method_log in wrapped_custom_io._method_log ) - self.assertTrue(all_write) + assert all_write # Check that all files were closed all_closed = all( method_log._filter("close") for method_log in wrapped_custom_io._method_log ) - self.assertTrue(all_closed) + assert all_closed # Check contents. # Note that the dash demuxer doesn't support custom I/O. diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 60cf78035..7d7180ebf 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -3,114 +3,135 @@ from unittest import SkipTest import numpy +import pytest import av from av import VideoFrame -from .common import TestCase, fate_png, fate_suite, has_pillow - - -class TestOpaque: - def test_opaque(self) -> None: - with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: - video_stream = container.streams.video[0] - video_stream.codec_context.copy_opaque = True - for packet_idx, packet in enumerate(container.demux()): - packet.opaque = (time.time(), packet_idx) - for frame in packet.decode(): - assert isinstance(frame, av.frame.Frame) - - if frame.opaque is None: - continue - - assert type(frame.opaque) is tuple and len(frame.opaque) == 2 - - -class TestVideoFrameConstructors(TestCase): - def test_invalid_pixel_format(self): - with self.assertRaises(ValueError) as cm: - VideoFrame(640, 480, "__unknown_pix_fmt") - assert str(cm.exception) == "not a pixel format: '__unknown_pix_fmt'" - - def test_null_constructor(self) -> None: - frame = VideoFrame() - assert frame.width == 0 - assert frame.height == 0 - assert frame.format.name == "yuv420p" - - def test_manual_yuv_constructor(self) -> None: - frame = VideoFrame(640, 480, "yuv420p") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "yuv420p" - - def test_manual_rgb_constructor(self) -> None: - frame = VideoFrame(640, 480, "rgb24") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "rgb24" - - -class TestVideoFramePlanes: - def test_null_planes(self) -> None: - frame = VideoFrame() # yuv420p - assert len(frame.planes) == 0 - - def test_yuv420p_planes(self) -> None: - frame = VideoFrame(640, 480, "yuv420p") - assert len(frame.planes) == 3 - assert frame.planes[0].width == 640 - assert frame.planes[0].height == 480 - assert frame.planes[0].line_size == 640 - assert frame.planes[0].buffer_size == 640 * 480 - for i in range(1, 3): - assert frame.planes[i].width == 320 - assert frame.planes[i].height == 240 - assert frame.planes[i].line_size == 320 - assert frame.planes[i].buffer_size == 320 * 240 - - def test_yuv420p_planes_align(self): - # If we request 8-byte alignment for a width which is not a multiple of 8, - # the line sizes are larger than the plane width. - frame = VideoFrame(318, 238, "yuv420p") - assert len(frame.planes) == 3 - assert frame.planes[0].width == 318 - assert frame.planes[0].height == 238 - assert frame.planes[0].line_size == 320 - assert frame.planes[0].buffer_size == 320 * 238 - for i in range(1, 3): - assert frame.planes[i].width == 159 - assert frame.planes[i].height == 119 - assert frame.planes[i].line_size == 160 - assert frame.planes[i].buffer_size == 160 * 119 - - def test_rgb24_planes(self): - frame = VideoFrame(640, 480, "rgb24") - assert len(frame.planes) == 1 - assert frame.planes[0].width == 640 - assert frame.planes[0].height == 480 - assert frame.planes[0].line_size == 640 * 3 - assert frame.planes[0].buffer_size == 640 * 480 * 3 - - -class TestVideoFrameBuffers(TestCase): - def test_memoryview_read(self): - frame = VideoFrame(640, 480, "rgb24") - frame.planes[0].update(b"01234" + (b"x" * (640 * 480 * 3 - 5))) - mem = memoryview(frame.planes[0]) - assert mem.ndim == 1 - assert mem.shape == (640 * 480 * 3,) - self.assertFalse(mem.readonly) - assert mem[1] == 49 - assert mem[:7] == b"01234xx" - mem[1] = 46 - assert mem[:7] == b"0.234xx" +from .common import ( + TestCase, + assertImagesAlmostEqual, + assertNdarraysEqual, + fate_png, + fate_suite, + has_pillow, +) + + +def assertPixelValue16(plane, expected, byteorder: str) -> None: + view = memoryview(plane) + if byteorder == "big": + assert view[0] == (expected >> 8 & 0xFF) + assert view[1] == expected & 0xFF + else: + assert view[0] == expected & 0xFF + assert view[1] == (expected >> 8 & 0xFF) + + +def test_opaque() -> None: + with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: + video_stream = container.streams.video[0] + video_stream.codec_context.copy_opaque = True + for packet_idx, packet in enumerate(container.demux()): + packet.opaque = (time.time(), packet_idx) + for frame in packet.decode(): + assert isinstance(frame, av.frame.Frame) + + if frame.opaque is None: + continue + + assert type(frame.opaque) is tuple and len(frame.opaque) == 2 + + +def test_invalid_pixel_format(): + with pytest.raises( + ValueError, match="not a pixel format: '__unknown_pix_fmt'" + ) as cm: + VideoFrame(640, 480, "__unknown_pix_fmt") + + +def test_null_constructor() -> None: + frame = VideoFrame() + assert frame.width == 0 + assert frame.height == 0 + assert frame.format.name == "yuv420p" + + +def test_manual_yuv_constructor() -> None: + frame = VideoFrame(640, 480, "yuv420p") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "yuv420p" + + +def test_manual_rgb_constructor() -> None: + frame = VideoFrame(640, 480, "rgb24") + assert frame.width == 640 + assert frame.height == 480 + assert frame.format.name == "rgb24" + + +def test_null_planes() -> None: + frame = VideoFrame() # yuv420p + assert len(frame.planes) == 0 + + +def test_yuv420p_planes() -> None: + frame = VideoFrame(640, 480, "yuv420p") + assert len(frame.planes) == 3 + assert frame.planes[0].width == 640 + assert frame.planes[0].height == 480 + assert frame.planes[0].line_size == 640 + assert frame.planes[0].buffer_size == 640 * 480 + for i in range(1, 3): + assert frame.planes[i].width == 320 + assert frame.planes[i].height == 240 + assert frame.planes[i].line_size == 320 + assert frame.planes[i].buffer_size == 320 * 240 + + +def test_yuv420p_planes_align(): + # If we request 8-byte alignment for a width which is not a multiple of 8, + # the line sizes are larger than the plane width. + frame = VideoFrame(318, 238, "yuv420p") + assert len(frame.planes) == 3 + assert frame.planes[0].width == 318 + assert frame.planes[0].height == 238 + assert frame.planes[0].line_size == 320 + assert frame.planes[0].buffer_size == 320 * 238 + for i in range(1, 3): + assert frame.planes[i].width == 159 + assert frame.planes[i].height == 119 + assert frame.planes[i].line_size == 160 + assert frame.planes[i].buffer_size == 160 * 119 + + +def test_rgb24_planes(): + frame = VideoFrame(640, 480, "rgb24") + assert len(frame.planes) == 1 + assert frame.planes[0].width == 640 + assert frame.planes[0].height == 480 + assert frame.planes[0].line_size == 640 * 3 + assert frame.planes[0].buffer_size == 640 * 480 * 3 + + +def test_memoryview_read(): + frame = VideoFrame(640, 480, "rgb24") + frame.planes[0].update(b"01234" + (b"x" * (640 * 480 * 3 - 5))) + mem = memoryview(frame.planes[0]) + assert mem.ndim == 1 + assert mem.shape == (640 * 480 * 3,) + assert not mem.readonly + assert mem[1] == 49 + assert mem[:7] == b"01234xx" + mem[1] = 46 + assert mem[:7] == b"0.234xx" class TestVideoFrameImage(TestCase): def setUp(self): if not has_pillow: - raise SkipTest() + pytest.skip() def test_roundtrip(self): import PIL.Image as Image @@ -119,14 +140,10 @@ def test_roundtrip(self): frame = VideoFrame.from_image(image) img = frame.to_image() img.save(self.sandboxed("roundtrip-high.jpg")) - self.assertImagesAlmostEqual(image, img) + assertImagesAlmostEqual(image, img) def test_to_image_rgb24(self): - sizes = [ - (318, 238), - (320, 240), - (500, 500), - ] + sizes = [(318, 238), (320, 240), (500, 500)] for width, height in sizes: frame = VideoFrame(width, height, format="rgb24") @@ -152,500 +169,488 @@ def test_to_image_rgb24(self): assert img.size == (width, height) assert img.tobytes() == expected - def test_to_image_with_dimensions(self): - frame = VideoFrame(640, 480, format="rgb24") - - img = frame.to_image(width=320, height=240) - assert img.size == (320, 240) - - -class TestVideoFrameNdarray(TestCase): - def assertPixelValue16(self, plane, expected, byteorder: str): - view = memoryview(plane) - if byteorder == "big": - assert view[0] == (expected >> 8 & 0xFF) - assert view[1] == expected & 0xFF - else: - assert view[0] == expected & 0xFF - assert view[1] == (expected >> 8 & 0xFF) - - def test_basic_to_ndarray(self): - frame = VideoFrame(640, 480, "rgb24") - array = frame.to_ndarray() - assert array.shape == (480, 640, 3) - - def test_ndarray_gray(self): - array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) - for format in ["gray", "gray8"]: - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "gray" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_gray_align(self): - array = numpy.random.randint(0, 256, size=(238, 318), dtype=numpy.uint8) - for format in ["gray", "gray8"]: - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 318 - assert frame.height == 238 - assert frame.format.name == "gray" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_rgb(self): - array = numpy.random.randint(0, 256, size=(480, 640, 3), dtype=numpy.uint8) - for format in ["rgb24", "bgr24"]: - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == format - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_rgb_align(self): - array = numpy.random.randint(0, 256, size=(238, 318, 3), dtype=numpy.uint8) - for format in ["rgb24", "bgr24"]: - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 318 - assert frame.height == 238 - assert frame.format.name == format - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_rgba(self): - array = numpy.random.randint(0, 256, size=(480, 640, 4), dtype=numpy.uint8) - for format in ["argb", "rgba", "abgr", "bgra"]: - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == format - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_rgba_align(self): - array = numpy.random.randint(0, 256, size=(238, 318, 4), dtype=numpy.uint8) - for format in ["argb", "rgba", "abgr", "bgra"]: - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 318 - assert frame.height == 238 - assert frame.format.name == format - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_gbrp(self): - array = numpy.random.randint(0, 256, size=(480, 640, 3), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray(array, format="gbrp") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "gbrp" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_gbrp_align(self): - array = numpy.random.randint(0, 256, size=(238, 318, 3), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray(array, format="gbrp") - assert frame.width == 318 - assert frame.height == 238 - assert frame.format.name == "gbrp" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_gbrp10(self): - array = numpy.random.randint(0, 1024, size=(480, 640, 3), dtype=numpy.uint16) - for format in ["gbrp10be", "gbrp10le"]: - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == format - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_gbrp10_align(self): - array = numpy.random.randint(0, 1024, size=(238, 318, 3), dtype=numpy.uint16) - for format in ["gbrp10be", "gbrp10le"]: - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 318 - assert frame.height == 238 - assert frame.format.name == format - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_gbrp12(self): - array = numpy.random.randint(0, 4096, size=(480, 640, 3), dtype=numpy.uint16) - for format in ["gbrp12be", "gbrp12le"]: - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == format - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_gbrp12_align(self): - array = numpy.random.randint(0, 4096, size=(238, 318, 3), dtype=numpy.uint16) - for format in ["gbrp12be", "gbrp12le"]: - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 318 - assert frame.height == 238 - assert frame.format.name == format - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_gbrp14(self): - array = numpy.random.randint(0, 16384, size=(480, 640, 3), dtype=numpy.uint16) - for format in ["gbrp14be", "gbrp14le"]: - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == format - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_gbrp14_align(self): - array = numpy.random.randint(0, 16384, size=(238, 318, 3), dtype=numpy.uint16) - for format in ["gbrp14be", "gbrp14le"]: - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 318 - assert frame.height == 238 - assert frame.format.name == format - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_gbrp16(self): - array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) - for format in ["gbrp16be", "gbrp16le"]: - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == format - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_gbrp16_align(self): - array = numpy.random.randint(0, 65536, size=(238, 318, 3), dtype=numpy.uint16) - for format in ["gbrp16be", "gbrp16le"]: - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 318 - assert frame.height == 238 - assert frame.format.name == format - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_gbrpf32(self): - array = numpy.random.random_sample(size=(480, 640, 3)).astype(numpy.float32) - for format in ["gbrpf32be", "gbrpf32le"]: - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == format - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_gbrpf32_align(self): - array = numpy.random.random_sample(size=(238, 318, 3)).astype(numpy.float32) - for format in ["gbrpf32be", "gbrpf32le"]: - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 318 - assert frame.height == 238 - assert frame.format.name == format - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_yuv420p(self): - array = numpy.random.randint(0, 256, size=(720, 640), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray(array, format="yuv420p") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "yuv420p" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_yuv420p_align(self): - array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray(array, format="yuv420p") - assert frame.width == 318 - assert frame.height == 238 - assert frame.format.name == "yuv420p" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_yuvj420p(self): - array = numpy.random.randint(0, 256, size=(720, 640), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray(array, format="yuvj420p") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "yuvj420p" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_yuyv422(self): - array = numpy.random.randint(0, 256, size=(480, 640, 2), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray(array, format="yuyv422") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "yuyv422" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_yuv444p(self): - array = numpy.random.randint(0, 256, size=(3, 480, 640), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray(array, format="yuv444p") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "yuv444p" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_yuvj444p(self): - array = numpy.random.randint(0, 256, size=(3, 480, 640), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray(array, format="yuvj444p") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "yuvj444p" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_yuyv422_align(self): - array = numpy.random.randint(0, 256, size=(238, 318, 2), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray(array, format="yuyv422") - assert frame.width == 318 - assert frame.height == 238 - assert frame.format.name == "yuyv422" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_gray16be(self): - array = numpy.random.randint(0, 65536, size=(480, 640), dtype=numpy.uint16) - frame = VideoFrame.from_ndarray(array, format="gray16be") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "gray16be" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - # check endianness by examining value of first pixel - self.assertPixelValue16(frame.planes[0], array[0][0], "big") - - def test_ndarray_gray16le(self): - array = numpy.random.randint(0, 65536, size=(480, 640), dtype=numpy.uint16) - frame = VideoFrame.from_ndarray(array, format="gray16le") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "gray16le" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - # check endianness by examining value of first pixel - self.assertPixelValue16(frame.planes[0], array[0][0], "little") - - def test_ndarray_rgb48be(self): - array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) - frame = VideoFrame.from_ndarray(array, format="rgb48be") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "rgb48be" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - # check endianness by examining red value of first pixel - self.assertPixelValue16(frame.planes[0], array[0][0][0], "big") - - def test_ndarray_rgb48le(self): - array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) - frame = VideoFrame.from_ndarray(array, format="rgb48le") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "rgb48le" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - # check endianness by examining red value of first pixel - self.assertPixelValue16(frame.planes[0], array[0][0][0], "little") - - def test_ndarray_rgb48le_align(self): - array = numpy.random.randint(0, 65536, size=(238, 318, 3), dtype=numpy.uint16) - frame = VideoFrame.from_ndarray(array, format="rgb48le") - assert frame.width == 318 - assert frame.height == 238 - assert frame.format.name == "rgb48le" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - # check endianness by examining red value of first pixel - self.assertPixelValue16(frame.planes[0], array[0][0][0], "little") - - def test_ndarray_rgba64be(self): - array = numpy.random.randint(0, 65536, size=(480, 640, 4), dtype=numpy.uint16) - frame = VideoFrame.from_ndarray(array, format="rgba64be") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "rgba64be" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - # check endianness by examining red value of first pixel - self.assertPixelValue16(frame.planes[0], array[0][0][0], "big") - - def test_ndarray_rgba64le(self): - array = numpy.random.randint(0, 65536, size=(480, 640, 4), dtype=numpy.uint16) - frame = VideoFrame.from_ndarray(array, format="rgba64le") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "rgba64le" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - # check endianness by examining red value of first pixel - self.assertPixelValue16(frame.planes[0], array[0][0][0], "little") - - def test_ndarray_rgb8(self): - array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray(array, format="rgb8") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "rgb8" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_bgr8(self): - array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray(array, format="bgr8") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "bgr8" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_pal8(self): - array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) - palette = numpy.random.randint(0, 256, size=(256, 4), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray((array, palette), format="pal8") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "pal8" - returned = frame.to_ndarray() - self.assertTrue((type(returned) is tuple) and len(returned) == 2) - self.assertNdarraysEqual(returned[0], array) - self.assertNdarraysEqual(returned[1], palette) - - def test_ndarray_nv12(self): - array = numpy.random.randint(0, 256, size=(720, 640), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray(array, format="nv12") - assert frame.width == 640 - assert frame.height == 480 - assert frame.format.name == "nv12" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_nv12_align(self): - array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) - frame = VideoFrame.from_ndarray(array, format="nv12") - assert frame.width == 318 - assert frame.height == 238 - assert frame.format.name == "nv12" - self.assertNdarraysEqual(frame.to_ndarray(), array) - - -class TestVideoFrameNumpyBuffer(TestCase): - def test_shares_memory_gray(self): - array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) - frame = VideoFrame.from_numpy_buffer(array, "gray") - self.assertNdarraysEqual(frame.to_ndarray(), array) - - # overwrite the array, the contents thereof - array[...] = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) - # Make sure the frame reflects that - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_shares_memory_gray8(self): - array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) - frame = VideoFrame.from_numpy_buffer(array, "gray8") - self.assertNdarraysEqual(frame.to_ndarray(), array) - - # overwrite the array, the contents thereof - array[...] = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) - # Make sure the frame reflects that - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_shares_memory_rgb8(self): - array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) - frame = VideoFrame.from_numpy_buffer(array, "rgb8") - self.assertNdarraysEqual(frame.to_ndarray(), array) - - # overwrite the array, the contents thereof - array[...] = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) - # Make sure the frame reflects that - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_shares_memory_bgr8(self): - array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) - frame = VideoFrame.from_numpy_buffer(array, "bgr8") - self.assertNdarraysEqual(frame.to_ndarray(), array) - - # overwrite the array, the contents thereof - array[...] = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) - # Make sure the frame reflects that - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_shares_memory_rgb24(self): - array = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8) - frame = VideoFrame.from_numpy_buffer(array, "rgb24") - self.assertNdarraysEqual(frame.to_ndarray(), array) - - # overwrite the array, the contents thereof - array[...] = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8) - # Make sure the frame reflects that - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_shares_memory_yuv420p(self): - array = numpy.random.randint( - 0, 256, size=(512 * 6 // 4, 256), dtype=numpy.uint8 - ) - frame = VideoFrame.from_numpy_buffer(array, "yuv420p") - self.assertNdarraysEqual(frame.to_ndarray(), array) - - # overwrite the array, the contents thereof - array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) - # Make sure the frame reflects that - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_shares_memory_yuvj420p(self): - array = numpy.random.randint( - 0, 256, size=(512 * 6 // 4, 256), dtype=numpy.uint8 - ) - frame = VideoFrame.from_numpy_buffer(array, "yuvj420p") - self.assertNdarraysEqual(frame.to_ndarray(), array) - - # overwrite the array, the contents thereof - array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) - # Make sure the frame reflects that - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_shares_memory_nv12(self): - array = numpy.random.randint( - 0, 256, size=(512 * 6 // 4, 256), dtype=numpy.uint8 - ) - frame = VideoFrame.from_numpy_buffer(array, "nv12") - self.assertNdarraysEqual(frame.to_ndarray(), array) - - # overwrite the array, the contents thereof - array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) - # Make sure the frame reflects that - self.assertNdarraysEqual(frame.to_ndarray(), array) - - def test_shares_memory_bgr24(self): - array = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8) - frame = VideoFrame.from_numpy_buffer(array, "bgr24") - self.assertNdarraysEqual(frame.to_ndarray(), array) - - # overwrite the array, the contents thereof - array[...] = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8) - # Make sure the frame reflects that - self.assertNdarraysEqual(frame.to_ndarray(), array) - - -class TestVideoFrameTiming(TestCase): - def test_reformat_pts(self) -> None: - frame = VideoFrame(640, 480, "rgb24") - frame.pts = 123 - frame.time_base = Fraction("456/1") - frame = frame.reformat(320, 240) - assert frame.pts == 123 - assert frame.time_base == 456 - - -class TestVideoFrameReformat(TestCase): - def test_reformat_identity(self): - frame1 = VideoFrame(640, 480, "rgb24") - frame2 = frame1.reformat(640, 480, "rgb24") - assert frame1 is frame2 - - def test_reformat_colourspace(self): - # This is allowed. - frame = VideoFrame(640, 480, "rgb24") - frame.reformat(src_colorspace=None, dst_colorspace="smpte240") - - # I thought this was not allowed, but it seems to be. - frame = VideoFrame(640, 480, "yuv420p") - frame.reformat(src_colorspace=None, dst_colorspace="smpte240") - - def test_reformat_pixel_format_align(self): - height = 480 - for width in range(2, 258, 2): - frame_yuv = VideoFrame(width, height, "yuv420p") - for plane in frame_yuv.planes: - plane.update(b"\xff" * plane.buffer_size) - - expected_rgb = numpy.zeros(shape=(height, width, 3), dtype=numpy.uint8) - expected_rgb[:, :, 0] = 255 - expected_rgb[:, :, 1] = 124 - expected_rgb[:, :, 2] = 255 - - frame_rgb = frame_yuv.reformat(format="rgb24") - self.assertNdarraysEqual(frame_rgb.to_ndarray(), expected_rgb) + +def test_basic_to_ndarray(): + array = VideoFrame(640, 480, "rgb24").to_ndarray() + assert array.shape == (480, 640, 3) + + +def test_to_image_with_dimensions(): + img = VideoFrame(640, 480, format="rgb24").to_image(width=320, height=240) + assert img.size == (320, 240) + + +def test_ndarray_gray(): + array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) + for format in ("gray", "gray8"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "gray" + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gray_align(): + array = numpy.random.randint(0, 256, size=(238, 318), dtype=numpy.uint8) + for format in ("gray", "gray8"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == "gray" + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_rgb(): + array = numpy.random.randint(0, 256, size=(480, 640, 3), dtype=numpy.uint8) + for format in ("rgb24", "bgr24"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_rgb_align(): + array = numpy.random.randint(0, 256, size=(238, 318, 3), dtype=numpy.uint8) + for format in ("rgb24", "bgr24"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_rgba(): + array = numpy.random.randint(0, 256, size=(480, 640, 4), dtype=numpy.uint8) + for format in ("argb", "rgba", "abgr", "bgra"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_rgba_align(): + array = numpy.random.randint(0, 256, size=(238, 318, 4), dtype=numpy.uint8) + for format in ("argb", "rgba", "abgr", "bgra"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrp(): + array = numpy.random.randint(0, 256, size=(480, 640, 3), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="gbrp") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "gbrp" + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrp_align(): + array = numpy.random.randint(0, 256, size=(238, 318, 3), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="gbrp") + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == "gbrp" + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrp10(): + array = numpy.random.randint(0, 1024, size=(480, 640, 3), dtype=numpy.uint16) + for format in ("gbrp10be", "gbrp10le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrp10_align(): + array = numpy.random.randint(0, 1024, size=(238, 318, 3), dtype=numpy.uint16) + for format in ("gbrp10be", "gbrp10le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrp12(): + array = numpy.random.randint(0, 4096, size=(480, 640, 3), dtype=numpy.uint16) + for format in ("gbrp12be", "gbrp12le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrp12_align(): + array = numpy.random.randint(0, 4096, size=(238, 318, 3), dtype=numpy.uint16) + for format in ("gbrp12be", "gbrp12le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrp14(): + array = numpy.random.randint(0, 16384, size=(480, 640, 3), dtype=numpy.uint16) + for format in ("gbrp14be", "gbrp14le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrp14_align(): + array = numpy.random.randint(0, 16384, size=(238, 318, 3), dtype=numpy.uint16) + for format in ("gbrp14be", "gbrp14le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrp16(): + array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) + for format in ("gbrp16be", "gbrp16le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrp16_align(): + array = numpy.random.randint(0, 65536, size=(238, 318, 3), dtype=numpy.uint16) + for format in ("gbrp16be", "gbrp16le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrpf32(): + array = numpy.random.random_sample(size=(480, 640, 3)).astype(numpy.float32) + for format in ("gbrpf32be", "gbrpf32le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrpf32_align(): + array = numpy.random.random_sample(size=(238, 318, 3)).astype(numpy.float32) + for format in ["gbrpf32be", "gbrpf32le"]: + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_yuv420p(): + array = numpy.random.randint(0, 256, size=(720, 640), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="yuv420p") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "yuv420p" + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_yuv420p_align(): + array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="yuv420p") + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == "yuv420p" + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_yuvj420p(): + array = numpy.random.randint(0, 256, size=(720, 640), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="yuvj420p") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "yuvj420p" + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_yuyv422(): + array = numpy.random.randint(0, 256, size=(480, 640, 2), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="yuyv422") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "yuyv422" + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_yuv444p(): + array = numpy.random.randint(0, 256, size=(3, 480, 640), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="yuv444p") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "yuv444p" + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_yuvj444p(): + array = numpy.random.randint(0, 256, size=(3, 480, 640), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="yuvj444p") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "yuvj444p" + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_yuyv422_align(): + array = numpy.random.randint(0, 256, size=(238, 318, 2), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="yuyv422") + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == "yuyv422" + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gray16be(): + array = numpy.random.randint(0, 65536, size=(480, 640), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="gray16be") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "gray16be" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining value of first pixel + assertPixelValue16(frame.planes[0], array[0][0], "big") + + +def test_ndarray_gray16le(): + array = numpy.random.randint(0, 65536, size=(480, 640), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="gray16le") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "gray16le" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining value of first pixel + assertPixelValue16(frame.planes[0], array[0][0], "little") + + +def test_ndarray_rgb48be(): + array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="rgb48be") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "rgb48be" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining red value of first pixel + assertPixelValue16(frame.planes[0], array[0][0][0], "big") + + +def test_ndarray_rgb48le(): + array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="rgb48le") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "rgb48le" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining red value of first pixel + assertPixelValue16(frame.planes[0], array[0][0][0], "little") + + +def test_ndarray_rgb48le_align(): + array = numpy.random.randint(0, 65536, size=(238, 318, 3), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="rgb48le") + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == "rgb48le" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining red value of first pixel + assertPixelValue16(frame.planes[0], array[0][0][0], "little") + + +def test_ndarray_rgba64be(): + array = numpy.random.randint(0, 65536, size=(480, 640, 4), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="rgba64be") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "rgba64be" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining red value of first pixel + assertPixelValue16(frame.planes[0], array[0][0][0], "big") + + +def test_ndarray_rgba64le(): + array = numpy.random.randint(0, 65536, size=(480, 640, 4), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="rgba64le") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "rgba64le" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining red value of first pixel + assertPixelValue16(frame.planes[0], array[0][0][0], "little") + + +def test_ndarray_rgb8(): + array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="rgb8") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "rgb8" + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_bgr8(): + array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="bgr8") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "bgr8" + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_pal8(): + array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) + palette = numpy.random.randint(0, 256, size=(256, 4), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray((array, palette), format="pal8") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "pal8" + returned = frame.to_ndarray() + assert type(returned) is tuple and len(returned) == 2 + assertNdarraysEqual(returned[0], array) + assertNdarraysEqual(returned[1], palette) + + +def test_ndarray_nv12(): + array = numpy.random.randint(0, 256, size=(720, 640), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="nv12") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "nv12" + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_nv12_align(): + array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="nv12") + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == "nv12" + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_shares_memory_gray(): + array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + frame = VideoFrame.from_numpy_buffer(array, "gray") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_shares_memory_gray8(): + array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + frame = VideoFrame.from_numpy_buffer(array, "gray8") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_shares_memory_rgb8(): + array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + frame = VideoFrame.from_numpy_buffer(array, "rgb8") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_shares_memory_bgr8(): + array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + frame = VideoFrame.from_numpy_buffer(array, "bgr8") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_shares_memory_rgb24(): + array = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8) + frame = VideoFrame.from_numpy_buffer(array, "rgb24") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_shares_memory_yuv420p(): + array = numpy.random.randint(0, 256, size=(512 * 6 // 4, 256), dtype=numpy.uint8) + frame = VideoFrame.from_numpy_buffer(array, "yuv420p") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_shares_memory_yuvj420p(): + array = numpy.random.randint(0, 256, size=(512 * 6 // 4, 256), dtype=numpy.uint8) + frame = VideoFrame.from_numpy_buffer(array, "yuvj420p") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_shares_memory_nv12(): + array = numpy.random.randint(0, 256, size=(512 * 6 // 4, 256), dtype=numpy.uint8) + frame = VideoFrame.from_numpy_buffer(array, "nv12") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_shares_memory_bgr24(): + array = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8) + frame = VideoFrame.from_numpy_buffer(array, "bgr24") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_reformat_pts() -> None: + frame = VideoFrame(640, 480, "rgb24") + frame.pts = 123 + frame.time_base = Fraction("456/1") + frame = frame.reformat(320, 240) + assert frame.pts == 123 and frame.time_base == 456 + + +def test_reformat_identity(): + frame1 = VideoFrame(640, 480, "rgb24") + frame2 = frame1.reformat(640, 480, "rgb24") + assert frame1 is frame2 + + +def test_reformat_colorspace(): + # This is allowed. + frame = VideoFrame(640, 480, "rgb24") + frame.reformat(src_colorspace=None, dst_colorspace="smpte240") + + # I thought this was not allowed, but it seems to be. + frame = VideoFrame(640, 480, "yuv420p") + frame.reformat(src_colorspace=None, dst_colorspace="smpte240") + + +def test_reformat_pixel_format_align(): + height = 480 + for width in range(2, 258, 2): + frame_yuv = VideoFrame(width, height, "yuv420p") + for plane in frame_yuv.planes: + plane.update(b"\xff" * plane.buffer_size) + + expected_rgb = numpy.zeros(shape=(height, width, 3), dtype=numpy.uint8) + expected_rgb[:, :, 0] = 255 + expected_rgb[:, :, 1] = 124 + expected_rgb[:, :, 2] = 255 + + frame_rgb = frame_yuv.reformat(format="rgb24") + assertNdarraysEqual(frame_rgb.to_ndarray(), expected_rgb) From 5f850cb11071194f6a4eac4c3ace89286a6b02c4 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 24 Sep 2024 22:04:52 -0400 Subject: [PATCH 113/365] Remove deprecation module Going forward, we'll use type stubs / simple warnings for deprecations. --- av/__main__.py | 6 ++- av/codec/context.pyx | 1 - av/deprecation.py | 80 --------------------------------------- tests/test_deprecation.py | 43 --------------------- 4 files changed, 4 insertions(+), 126 deletions(-) delete mode 100644 av/deprecation.py delete mode 100644 tests/test_deprecation.py diff --git a/av/__main__.py b/av/__main__.py index 4e5d62692..bc353d147 100644 --- a/av/__main__.py +++ b/av/__main__.py @@ -1,7 +1,9 @@ +from __future__ import annotations + import argparse -def main(): +def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--codecs", action="store_true") parser.add_argument("--version", action="store_true") @@ -13,7 +15,7 @@ def main(): print(f"PyAV v{av.__version__}") - by_config = {} + by_config: dict = {} for libname, config in sorted(av._core.library_meta.items()): version = config["version"] if version[0] >= 0: diff --git a/av/codec/context.pyx b/av/codec/context.pyx index bfaa9544d..b995bc9f7 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -11,7 +11,6 @@ from av.error cimport err_check from av.packet cimport Packet from av.utils cimport avrational_to_fraction, to_avrational -from av.deprecation import AVDeprecationWarning from av.dictionary import Dictionary diff --git a/av/deprecation.py b/av/deprecation.py deleted file mode 100644 index b9a65f135..000000000 --- a/av/deprecation.py +++ /dev/null @@ -1,80 +0,0 @@ -import functools -import warnings - - -class AVDeprecationWarning(DeprecationWarning): - pass - - -class AttributeRenamedWarning(AVDeprecationWarning): - pass - - -class MethodDeprecationWarning(AVDeprecationWarning): - pass - - -# DeprecationWarning is not printed by default (unless in __main__). We -# really want these to be seen, but also to use the "correct" base classes. -# So we're putting a filter in place to show our warnings. The users can -# turn them back off if they want. -warnings.filterwarnings("default", "", AVDeprecationWarning) - - -class renamed_attr: - """Proxy for renamed attributes (or methods) on classes. - Getting and setting values will be redirected to the provided name, - and warnings will be issues every time. - - """ - - def __init__(self, new_name): - self.new_name = new_name - self._old_name = None - - def old_name(self, cls): - if self._old_name is None: - for k, v in vars(cls).items(): - if v is self: - self._old_name = k - break - return self._old_name - - def __get__(self, instance, cls): - old_name = self.old_name(cls) - warnings.warn( - "{0}.{1} is deprecated; please use {0}.{2}.".format( - cls.__name__, - old_name, - self.new_name, - ), - AttributeRenamedWarning, - stacklevel=2, - ) - return getattr(instance if instance is not None else cls, self.new_name) - - def __set__(self, instance, value): - old_name = self.old_name(instance.__class__) - warnings.warn( - "{0}.{1} is deprecated; please use {0}.{2}.".format( - instance.__class__.__name__, - old_name, - self.new_name, - ), - AttributeRenamedWarning, - stacklevel=2, - ) - setattr(instance, self.new_name, value) - - -class method: - def __init__(self, func): - functools.update_wrapper(self, func, ("__name__", "__doc__")) - self.func = func - - def __get__(self, instance, cls): - warning = MethodDeprecationWarning( - f"{cls.__name__}.{self.func.__name__} is deprecated." - ) - warnings.warn(warning, stacklevel=2) - return self.func.__get__(instance, cls) diff --git a/tests/test_deprecation.py b/tests/test_deprecation.py deleted file mode 100644 index f1b0b5d4b..000000000 --- a/tests/test_deprecation.py +++ /dev/null @@ -1,43 +0,0 @@ -import warnings - -from av import deprecation - - -def test_method() -> None: - class Example: - def __init__(self, x: int = 100) -> None: - self.x = x - - @deprecation.method - def foo(self, a: int, b: int) -> int: - return self.x + a + b - - obj = Example() - - with warnings.catch_warnings(record=True) as captured: - assert obj.foo(20, b=3) == 123 - assert "Example.foo is deprecated" in str(captured[0].message) - - -def test_renamed_attr() -> None: - class Example: - new_value = "foo" - old_value = deprecation.renamed_attr("new_value") - - def new_func(self, a: int, b: int) -> int: - return a + b - - old_func = deprecation.renamed_attr("new_func") - - obj = Example() - - with warnings.catch_warnings(record=True) as captured: - assert obj.old_value == "foo" - assert "Example.old_value is deprecated" in str(captured[0].message) - - obj.old_value = "bar" - assert "Example.old_value is deprecated" in str(captured[1].message) - - with warnings.catch_warnings(record=True) as captured: - assert obj.old_func(1, 2) == 3 - assert "Example.old_func is deprecated" in str(captured[0].message) From 0a7e549da5fe198b0a8e4df3dc10bcd582823e25 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 24 Sep 2024 22:52:13 -0400 Subject: [PATCH 114/365] Use python 3 semantics --- av/logging.pyx | 6 +++--- setup.py | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/av/logging.pyx b/av/logging.pyx index 6b6858db6..66a5095ad 100644 --- a/av/logging.pyx +++ b/av/logging.pyx @@ -38,8 +38,6 @@ API Reference """ -from __future__ import absolute_import - cimport libav as lib from libc.stdio cimport fprintf, stderr from libc.stdlib cimport free, malloc @@ -223,7 +221,9 @@ cpdef log(int level, str name, str message): cdef log_context *obj = malloc(sizeof(log_context)) obj.class_ = &log_class obj.name = name - lib.av_log(obj, level, "%s", message) + cdef bytes message_bytes = message.encode("utf-8") + + lib.av_log(obj, level, "%s", message_bytes) free(obj) diff --git a/setup.py b/setup.py index 5329223b3..52395dbe8 100644 --- a/setup.py +++ b/setup.py @@ -177,12 +177,12 @@ def parse_cflags(raw_flags): library_dirs=extension_extra["library_dirs"], sources=[pyx_path], ), - compiler_directives=dict( - c_string_type="str", - c_string_encoding="ascii", - embedsignature=True, - language_level=2, - ), + compiler_directives={ + "c_string_type": "str", + "c_string_encoding": "ascii", + "embedsignature": True, + "language_level": 3, + }, build_dir="src", include_path=["include"], ) From 3063f6b58143cdb186df2cce9526838f6ceec1e6 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 25 Sep 2024 01:31:23 -0400 Subject: [PATCH 115/365] Write release notes for 13.1.0rc1 --- CHANGELOG.rst | 28 ++++++++++++++++++++++------ av/about.py | 2 +- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 75bd7adff..d43d3ddaa 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,7 +1,7 @@ Changelog ========= -We are operating with `semantic versioning `_. +We are operating with `semantic versioning `_. .. Please try to update this file in the commits that make the changes. @@ -16,6 +16,22 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. +v13.1.0 +------- + +Features: + +- Allow passing Python objects around using `Frame.opaque` and `Packet.opaque`. +- Allow extradata to be set by encoders by @daveisfera in (:issue:`1526`). + +Fixes: + +- Remove the `deprecation` module in anticipation of `PEP 702 `_ +- Add type stubs to previously unannotated API sections. +- Improve type stubs for both `mypy` and `mypy --strict`. +- Permit only setting `time_base` with a Fraction, as mypy is unable to respect different types in getters vs setters. +- Declare `write_packet` function as const by @hmaarrfk in (:issue:`1517`). + v13.0.0 ------- @@ -415,7 +431,7 @@ Micro: Build: - Assert building against compatible FFmpeg. (:issue:`401`) -- Lock down Cython lanaguage level to avoid build warnings. (:issue:`443`) +- Lock down Cython language level to avoid build warnings. (:issue:`443`) Other: @@ -596,9 +612,9 @@ Major: of streams/containers. - ``Stream.encode`` returns a list of packets, instead of a single packet. - ``AudioFifo`` and ``AudioResampler`` will raise ``ValueError`` if input frames - inconsistant ``pts``. + inconsistent ``pts``. - ``time_base`` use has been revisited across the codebase, and may not be converted - bettween ``Stream.time_base`` and ``CodecContext.time_base`` at the same times + between ``Stream.time_base`` and ``CodecContext.time_base`` at the same times in the transcoding pipeline. - ``CodecContext.rate`` has been removed, but proxied to ``VideoCodecContext.framerate`` and ``AudioCodecContext.sample_rate``. The definition is effectively inverted from @@ -611,7 +627,7 @@ Minor: - Added ``Packet.is_keyframe`` and ``Packet.is_corrupt``. (:issue:`226`) -- Many more ``time_base``, ``pts`` and other attributes are writeable. +- Many more ``time_base``, ``pts`` and other attributes are writable. - ``Option`` exposes much more of the API (but not get/set). (:issue:`243`) - Expose metadata encoding controls. @@ -721,7 +737,7 @@ v0.2.3 Major: - Python IO. -- Agressively releases GIL +- Aggressively releases GIL - Add experimental Windows build. (:issue:`84`) diff --git a/av/about.py b/av/about.py index 1311252a5..f0a0fa039 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "13.0.0" +__version__ = "13.1.0rc1" From 339fc4859f2b75994ed0b66f5e51d4954ca0ca5e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 25 Sep 2024 02:04:13 -0400 Subject: [PATCH 116/365] Add workaround for mypy#3004 --- av/audio/codeccontext.pyi | 19 ++++++++++--------- av/audio/frame.pyi | 12 ++++++++++-- av/audio/stream.pyi | 19 ++++++++++--------- av/filter/graph.pyi | 4 ++-- tests/test_codec_context.py | 10 +++++----- tests/test_encode.py | 4 ++-- 6 files changed, 39 insertions(+), 29 deletions(-) diff --git a/av/audio/codeccontext.pyi b/av/audio/codeccontext.pyi index afd28ec34..b3ec3ce6e 100644 --- a/av/audio/codeccontext.pyi +++ b/av/audio/codeccontext.pyi @@ -7,20 +7,21 @@ from .format import AudioFormat from .frame import AudioFrame from .layout import AudioLayout +class _Format: + def __get__(self, i: object | None, owner: type | None = None) -> AudioFormat: ... + def __set__(self, instance: object, value: AudioFormat | str) -> None: ... + +class _Layout: + def __get__(self, i: object | None, owner: type | None = None) -> AudioLayout: ... + def __set__(self, instance: object, value: AudioLayout | str) -> None: ... + class AudioCodecContext(CodecContext): frame_size: int sample_rate: int rate: int type: Literal["audio"] - - @property - def format(self) -> AudioFormat: ... - @format.setter - def format(self, value: AudioFormat | str) -> None: ... - @property - def layout(self) -> AudioLayout: ... - @layout.setter - def layout(self, value: AudioLayout | str) -> None: ... + format: _Format + layout: _Layout @property def channels(self) -> int: ... def encode(self, frame: AudioFrame | None = None) -> list[Packet]: ... diff --git a/av/audio/frame.pyi b/av/audio/frame.pyi index c733a039d..7f61e4e6d 100644 --- a/av/audio/frame.pyi +++ b/av/audio/frame.pyi @@ -17,13 +17,21 @@ _SupportedNDarray = Union[ np.ndarray[Any, np.dtype[np.uint8]], # u1 ] +class _Format: + def __get__(self, i: object | None, owner: type | None = None) -> AudioFormat: ... + def __set__(self, instance: object, value: AudioFormat | str) -> None: ... + +class _Layout: + def __get__(self, i: object | None, owner: type | None = None) -> AudioLayout: ... + def __set__(self, instance: object, value: AudioLayout | str) -> None: ... + class AudioFrame(Frame): planes: tuple[AudioPlane, ...] samples: int sample_rate: int rate: int - format: AudioFormat - layout: AudioLayout + format: _Format + layout: _Layout def __init__( self, diff --git a/av/audio/stream.pyi b/av/audio/stream.pyi index 48a9d65c6..443c85fa1 100644 --- a/av/audio/stream.pyi +++ b/av/audio/stream.pyi @@ -8,6 +8,14 @@ from .format import AudioFormat from .frame import AudioFrame from .layout import AudioLayout +class _Format: + def __get__(self, i: object | None, owner: type | None = None) -> AudioFormat: ... + def __set__(self, instance: object, value: AudioFormat | str) -> None: ... + +class _Layout: + def __get__(self, i: object | None, owner: type | None = None) -> AudioLayout: ... + def __set__(self, instance: object, value: AudioLayout | str) -> None: ... + class AudioStream(Stream): codec_context: AudioCodecContext # From codec context @@ -17,14 +25,7 @@ class AudioStream(Stream): rate: int channels: int type: Literal["audio"] - - @property - def format(self) -> AudioFormat: ... - @format.setter - def format(self, value: AudioFormat | str) -> None: ... - @property - def layout(self) -> AudioLayout: ... - @layout.setter - def layout(self, value: AudioLayout | str) -> None: ... + format: _Format + layout: _Layout def encode(self, frame: AudioFrame | None = None) -> list[Packet]: ... def decode(self, packet: Packet | None = None) -> list[AudioFrame]: ... diff --git a/av/filter/graph.pyi b/av/filter/graph.pyi index 9d8480b17..e170c2ce7 100644 --- a/av/filter/graph.pyi +++ b/av/filter/graph.pyi @@ -34,8 +34,8 @@ class Graph: self, template: AudioStream | None = None, sample_rate: int | None = None, - format: AudioFormat | None = None, - layout: AudioLayout | None = None, + format: AudioFormat | str | None = None, + layout: AudioLayout | str | None = None, channels: int | None = None, name: str | None = None, time_base: Fraction | None = None, diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 24b49f09a..5e18aebb2 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -139,7 +139,7 @@ def test_encoder_extradata(self): assert ctx.extradata_size == 3 def test_encoder_pix_fmt(self): - ctx = av.codec.Codec("h264", "w").create() + ctx = av.codec.Codec("h264", "w").create("video") # valid format ctx.pix_fmt = "yuv420p" @@ -429,8 +429,8 @@ def _audio_encoding( ctx.time_base = Fraction(1) / sample_rate ctx.sample_rate = sample_rate - ctx.format = sample_fmt # type: ignore - ctx.layout = channel_layout # type: ignore + ctx.format = sample_fmt + ctx.layout = channel_layout ctx.open() @@ -463,8 +463,8 @@ def _audio_encoding( ctx = Codec(codec_name, "r").create("audio") ctx.sample_rate = sample_rate - ctx.format = sample_fmt # type: ignore - ctx.layout = channel_layout # type: ignore + ctx.format = sample_fmt + ctx.layout = channel_layout ctx.open() result_samples = 0 diff --git a/tests/test_encode.py b/tests/test_encode.py index e2ab1ba2f..617231409 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -196,8 +196,8 @@ def test_transcode(self) -> None: ctx = stream.codec_context ctx.sample_rate = sample_rate - stream.format = sample_fmt # type: ignore - ctx.layout = channel_layout # type: ignore + stream.format = sample_fmt + ctx.layout = channel_layout with av.open( fate_suite("audio-reference/chorusnoise_2ch_44kHz_s16.wav") From 6e9698f0fc6c9591df6e3ce4bac2c20a6547ae83 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 25 Sep 2024 03:35:42 -0400 Subject: [PATCH 117/365] Add more type stubs for tests --- tests/test_codec_context.py | 20 ++-- tests/test_decode.py | 41 ++++---- tests/test_file_probing.py | 25 ++--- tests/test_filters.py | 20 ++-- tests/test_python_io.py | 192 +++++++++++++++++++----------------- tests/test_videoframe.py | 52 +++++----- 6 files changed, 176 insertions(+), 174 deletions(-) diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 5e18aebb2..a6bae17a0 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -36,14 +36,14 @@ class Options(TypedDict, total=False): @overload def iter_raw_frames( - path: str, packet_sizes: list, ctx: VideoCodecContext + path: str, packet_sizes: list[int], ctx: VideoCodecContext ) -> Iterator[VideoFrame]: ... @overload def iter_raw_frames( - path: str, packet_sizes: list, ctx: AudioCodecContext + path: str, packet_sizes: list[int], ctx: AudioCodecContext ) -> Iterator[AudioFrame]: ... def iter_raw_frames( - path: str, packet_sizes: list, ctx: VideoCodecContext | AudioCodecContext + path: str, packet_sizes: list[int], ctx: VideoCodecContext | AudioCodecContext ) -> Iterator[VideoFrame | AudioFrame]: with open(path, "rb") as f: for i, size in enumerate(packet_sizes): @@ -85,14 +85,16 @@ def test_codec_tag(self): assert ctx.codec_tag == "xvid" # wrong length - with self.assertRaises(ValueError) as cm: + with pytest.raises( + ValueError, match="Codec tag should be a 4 character string" + ): ctx.codec_tag = "bob" - assert str(cm.exception) == "Codec tag should be a 4 character string." # wrong type - with self.assertRaises(ValueError) as cm: + with pytest.raises( + ValueError, match="Codec tag should be a 4 character string" + ): ctx.codec_tag = 123 - assert str(cm.exception) == "Codec tag should be a 4 character string." with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: assert container.streams[0].codec_tag == "avc1" @@ -175,14 +177,14 @@ def test_bits_per_coded_sample(self): with pytest.raises(ValueError): stream.codec_context.bits_per_coded_sample = 32 - def test_parse(self): + def test_parse(self) -> None: # This one parses into a single packet. self._assert_parse("mpeg4", fate_suite("h264/interlaced_crop.mp4")) # This one parses into many small packets. self._assert_parse("mpeg2video", fate_suite("mpeg2/mpeg2_field_encoding.ts")) - def _assert_parse(self, codec_name, path): + def _assert_parse(self, codec_name: str, path: str) -> None: fh = av.open(path) packets = [] for packet in fh.demux(video=0): diff --git a/tests/test_decode.py b/tests/test_decode.py index aafea3b22..7622693b5 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -57,7 +57,7 @@ def test_decode_audio_sample_count(self) -> None: ) assert sample_count == total_samples - def test_decoded_time_base(self): + def test_decoded_time_base(self) -> None: container = av.open(fate_suite("h264/interlaced_crop.mp4")) stream = container.streams.video[0] @@ -65,38 +65,37 @@ def test_decoded_time_base(self): for packet in container.demux(stream): for frame in packet.decode(): + assert not isinstance(frame, av.subtitles.subtitle.SubtitleSet) assert packet.time_base == frame.time_base assert stream.time_base == frame.time_base return - def test_decoded_motion_vectors(self): + def test_decoded_motion_vectors(self) -> None: container = av.open(fate_suite("h264/interlaced_crop.mp4")) stream = container.streams.video[0] codec_context = stream.codec_context codec_context.options = {"flags2": "+export_mvs"} - for packet in container.demux(stream): - for frame in packet.decode(): - vectors = frame.side_data.get("MOTION_VECTORS") - if frame.key_frame: - # Key frame don't have motion vectors - assert vectors is None - else: - assert len(vectors) > 0 - return - - def test_decoded_motion_vectors_no_flag(self): + for frame in container.decode(stream): + vectors = frame.side_data.get("MOTION_VECTORS") + if frame.key_frame: + # Key frame don't have motion vectors + assert vectors is None + else: + assert vectors is not None and len(vectors) > 0 + return + + def test_decoded_motion_vectors_no_flag(self) -> None: container = av.open(fate_suite("h264/interlaced_crop.mp4")) stream = container.streams.video[0] - for packet in container.demux(stream): - for frame in packet.decode(): - vectors = frame.side_data.get("MOTION_VECTORS") - if not frame.key_frame: - assert vectors is None - return + for frame in container.decode(stream): + vectors = frame.side_data.get("MOTION_VECTORS") + if not frame.key_frame: + assert vectors is None + return - def test_decode_video_corrupt(self): + def test_decode_video_corrupt(self) -> None: # write an empty file path = self.sandboxed("empty.h264") with open(path, "wb"): @@ -114,7 +113,7 @@ def test_decode_video_corrupt(self): assert packet_count == 1 assert frame_count == 0 - def test_decode_close_then_use(self): + def test_decode_close_then_use(self) -> None: container = av.open(fate_suite("h264/interlaced_crop.mp4")) container.close() diff --git a/tests/test_file_probing.py b/tests/test_file_probing.py index d67a69195..d8a7fd755 100644 --- a/tests/test_file_probing.py +++ b/tests/test_file_probing.py @@ -14,9 +14,7 @@ def test_container_probing(self): assert self.file.duration == 6165333 assert str(self.file.format) == "" assert self.file.format.name == "mpegts" - self.assertEqual( - self.file.format.long_name, "MPEG-TS (MPEG-2 Transport Stream)" - ) + assert self.file.format.long_name == "MPEG-TS (MPEG-2 Transport Stream)" assert self.file.metadata == {} assert self.file.size == 207740 assert self.file.start_time == 1400000 @@ -25,11 +23,8 @@ def test_container_probing(self): def test_stream_probing(self): stream = self.file.streams[0] - # check __repr__ - self.assertTrue( - str(stream).startswith( - " None: assert self.file.bit_rate == 0 assert self.file.duration is None assert str(self.file.format) == "" @@ -78,14 +73,11 @@ def test_container_probing(self): assert self.file.start_time is None assert len(self.file.streams) == 1 - def test_stream_probing(self): + def test_stream_probing(self) -> None: stream = self.file.streams[0] - # ensure __repr__ does not crash - self.assertTrue( - str(stream).startswith( - " None: def test_stream_probing(self) -> None: stream = self.file.streams[0] - # check __repr__ assert str(stream).startswith(" AudioFrame: """ Generate audio frame representing part of the sinusoidal wave - :param input_format: default: s16 - :param layout: default: stereo - :param sample_rate: default: 44100 - :param frame_size: default: 1024 - :param frame_num: frame number - :return: audio frame for sinusoidal wave audio signal slice """ frame = AudioFrame(format=input_format, layout=layout, samples=frame_size) frame.sample_rate = sample_rate @@ -31,7 +29,7 @@ def generate_audio_frame( data = np.zeros(frame_size, dtype=format_dtypes[input_format]) for j in range(frame_size): data[j] = np.sin(2 * np.pi * (frame_num + j) * (i + 1) / float(frame_size)) - frame.planes[i].update(data) + frame.planes[i].update(data) # type: ignore return frame @@ -79,8 +77,8 @@ def test_generator_graph(self): lutrgb.link_to(sink) # pads and links - self.assertIs(src.outputs[0].link.output, lutrgb.inputs[0]) - self.assertIs(lutrgb.inputs[0].link.input, src.outputs[0]) + assert src.outputs[0].link.output is lutrgb.inputs[0] + assert lutrgb.inputs[0].link.input is src.outputs[0] frame = sink.pull() assert isinstance(frame, VideoFrame) diff --git a/tests/test_python_io.py b/tests/test_python_io.py index 8324b870b..3fb9cf3dd 100644 --- a/tests/test_python_io.py +++ b/tests/test_python_io.py @@ -1,20 +1,34 @@ +from __future__ import annotations + import functools +import io import types from io import BytesIO -from unittest import SkipTest +from re import escape +from typing import TYPE_CHECKING + +import pytest import av from .common import TestCase, fate_png, fate_suite, has_pillow, run_in_sandbox from .test_encode import assert_rgb_rotate, write_rgb_rotate +if TYPE_CHECKING: + from collections.abc import Callable + class MethodLogger: - def __init__(self, obj): + def __init__(self, obj: object) -> None: self._obj = obj - self._log = [] + self._log: list[tuple[str, object]] = [] + + def __getattr__(self, name: str) -> object: + + def _method(name: str, meth: Callable, *args, **kwargs) -> object: + self._log.append((name, args)) + return meth(*args, **kwargs) - def __getattr__(self, name): value = getattr(self._obj, name) if isinstance( value, @@ -25,16 +39,12 @@ def __getattr__(self, name): types.BuiltinMethodType, ), ): - return functools.partial(self._method, name, value) + return functools.partial(_method, name, value) else: - self._log.append(("__getattr__", (name,), {})) + self._log.append(("__getattr__", (name,))) return value - def _method(self, name, meth, *args, **kwargs): - self._log.append((name, args, kwargs)) - return meth(*args, **kwargs) - - def _filter(self, type_): + def _filter(self, type_: str) -> list[tuple[str, object]]: return [log for log in self._log if log[0] == type_] @@ -72,13 +82,13 @@ class ReadOnlyPipe(BytesIO): """ @property - def name(self): + def name(self) -> int: return 123 - def seekable(self): + def seekable(self) -> bool: return False - def writable(self): + def writable(self) -> bool: return False @@ -88,16 +98,46 @@ class WriteOnlyPipe(BytesIO): """ @property - def name(self): + def name(self) -> int: return 123 - def readable(self): + def readable(self) -> bool: return False - def seekable(self): + def seekable(self) -> bool: return False +def read( + fh: io.BufferedReader | BytesIO | ReadOnlyBuffer, seekable: bool = True +) -> None: + wrapped = MethodLogger(fh) + + with av.open(wrapped, "r") as container: + assert container.format.name == "mpegts" + assert container.format.long_name == "MPEG-TS (MPEG-2 Transport Stream)" + assert len(container.streams) == 1 + if seekable: + assert container.size == 800000 + assert container.metadata == {} + + # Check method calls. + assert wrapped._filter("read") + if seekable: + assert wrapped._filter("seek") + + +def write(fh: io.BufferedWriter | BytesIO) -> None: + wrapped = MethodLogger(fh) + + with av.open(wrapped, "w", "mp4") as container: + write_rgb_rotate(container) + + # Check method calls. + assert wrapped._filter("write") + assert wrapped._filter("seek") + + # Using a custom protocol will avoid the DASH muxer detecting or defaulting to a # file: protocol and enabling the use of temporary files and renaming. CUSTOM_IO_PROTOCOL = "pyavtest://" @@ -106,16 +146,16 @@ def seekable(self): class CustomIOLogger: """Log calls to open a file as well as method calls on the files""" - def __init__(self): - self._log = [] - self._method_log = [] + def __init__(self) -> None: + self._log: list[tuple[object, dict]] = [] + self._method_log: list[MethodLogger] = [] - def __call__(self, *args, **kwargs): + def __call__(self, *args, **kwargs) -> MethodLogger: self._log.append((args, kwargs)) self._method_log.append(self.io_open(*args, **kwargs)) return self._method_log[-1] - def io_open(self, url, flags, options): + def io_open(self, url: str, flags, options: object) -> MethodLogger: # Remove the protocol prefix to reveal the local filename if CUSTOM_IO_PROTOCOL in url: url = url.split(CUSTOM_IO_PROTOCOL, 1)[1] @@ -133,55 +173,52 @@ def io_open(self, url, flags, options): class TestPythonIO(TestCase): - def test_basic_errors(self): + def test_basic_errors(self) -> None: self.assertRaises(Exception, av.open, None) self.assertRaises(Exception, av.open, None, "w") - def test_reading_from_buffer(self): + def test_reading_from_buffer(self) -> None: with open(fate_suite("mpeg2/mpeg2_field_encoding.ts"), "rb") as fh: buf = BytesIO(fh.read()) - self.read(buf, seekable=True) + read(buf, seekable=True) - def test_reading_from_buffer_no_seek(self): + def test_reading_from_buffer_no_seek(self) -> None: with open(fate_suite("mpeg2/mpeg2_field_encoding.ts"), "rb") as fh: buf = ReadOnlyBuffer(fh.read()) - self.read(buf, seekable=False) + read(buf, seekable=False) - def test_reading_from_file(self): + def test_reading_from_file(self) -> None: with open(fate_suite("mpeg2/mpeg2_field_encoding.ts"), "rb") as fh: - self.read(fh, seekable=True) + read(fh, seekable=True) - def test_reading_from_pipe_readonly(self): + def test_reading_from_pipe_readonly(self) -> None: with open(fate_suite("mpeg2/mpeg2_field_encoding.ts"), "rb") as fh: buf = ReadOnlyPipe(fh.read()) - self.read(buf, seekable=False) + read(buf, seekable=False) - def test_reading_from_write_readonly(self): + def test_reading_from_write_readonly(self) -> None: with open(fate_suite("mpeg2/mpeg2_field_encoding.ts"), "rb") as fh: buf = WriteOnlyPipe(fh.read()) - with self.assertRaises(ValueError) as cm: - self.read(buf, seekable=False) - assert ( - str(cm.exception) - == "File object has no read() method, or readable() returned False." - ) + msg = escape("File object has no read() method, or readable() returned False.") + with pytest.raises(ValueError, match=msg): + read(buf, seekable=False) - def test_writing_to_buffer(self): + def test_writing_to_buffer(self) -> None: buf = BytesIO() - self.write(buf) + write(buf) # Check contents. - self.assertTrue(buf.tell()) + assert buf.tell() buf.seek(0) - with av.open(buf) as container: + with av.open(buf, "r") as container: assert_rgb_rotate(self, container) - def test_writing_to_buffer_broken(self): + def test_writing_to_buffer_broken(self) -> None: buf = BrokenBuffer() - with self.assertRaises(OSError): + with pytest.raises(OSError): with av.open(buf, "w", "mp4") as container: write_rgb_rotate(container) @@ -198,11 +235,11 @@ def test_writing_to_buffer_broken_with_close(self): buf.broken = True # try to close file - with self.assertRaises(OSError): + with pytest.raises(OSError): container.close() @run_in_sandbox - def test_writing_to_custom_io_dash(self): + def test_writing_to_custom_io_dash(self) -> None: # Custom I/O that opens file and logs calls wrapped_custom_io = CustomIOLogger() @@ -217,8 +254,8 @@ def test_writing_to_custom_io_dash(self): # Check that at least 3 files were opened using the custom IO: # "output_filename", init-stream0.m4s and chunk-stream-0x.m4s - self.assertGreaterEqual(len(wrapped_custom_io._log), 3) - self.assertGreaterEqual(len(wrapped_custom_io._method_log), 3) + assert len(wrapped_custom_io._log) >= 3 + assert len(wrapped_custom_io._method_log) >= 3 # Check that all files were written to all_write = all( @@ -237,9 +274,9 @@ def test_writing_to_custom_io_dash(self): with av.open(output_filename, "r") as container: assert_rgb_rotate(self, container, is_dash=True) - def test_writing_to_custom_io_image2(self): + def test_writing_to_custom_io_image2(self) -> None: if not has_pillow: - raise SkipTest() + pytest.skip() import PIL.Image as Image @@ -263,7 +300,7 @@ def test_writing_to_custom_io_image2(self): stream.height = height stream.pix_fmt = "rgb24" - for frame_i in range(frame_count): + for _ in range(frame_count): for packet in stream.encode(input_frame): output.mux(packet) @@ -286,8 +323,9 @@ def test_writing_to_custom_io_image2(self): # Check contents. with av.open(sequence_filename, "r", "image2") as container: assert len(container.streams) == 1 + assert isinstance(container.streams[0], av.video.stream.VideoStream) + stream = container.streams[0] - assert isinstance(stream, av.video.stream.VideoStream) assert stream.duration == frame_count assert stream.type == "video" @@ -301,7 +339,7 @@ def test_writing_to_file(self) -> None: path = self.sandboxed("writing.mp4") with open(path, "wb") as fh: - self.write(fh) + write(fh) # Check contents. with av.open(path) as container: @@ -309,47 +347,21 @@ def test_writing_to_file(self) -> None: def test_writing_to_pipe_readonly(self) -> None: buf = ReadOnlyPipe() - with self.assertRaises(ValueError) as cm: - self.write(buf) - assert ( - str(cm.exception) - == "File object has no write() method, or writable() returned False." - ) + with pytest.raises( + ValueError, + match=escape( + "File object has no write() method, or writable() returned False." + ), + ) as cm: + write(buf) def test_writing_to_pipe_writeonly(self): av.logging.set_level(av.logging.VERBOSE) buf = WriteOnlyPipe() - with self.assertRaises(ValueError) as cm: - self.write(buf) - assert "[mp4] muxer does not support non seekable output" in str(cm.exception) + with pytest.raises( + ValueError, match=escape("[mp4] muxer does not support non seekable output") + ) as cm: + write(buf) av.logging.set_level(None) - - def read(self, fh, seekable: bool = True) -> None: - wrapped = MethodLogger(fh) - - with av.open(wrapped, "r") as container: - assert container.format.name == "mpegts" - self.assertEqual( - container.format.long_name, "MPEG-TS (MPEG-2 Transport Stream)" - ) - assert len(container.streams) == 1 - if seekable: - assert container.size == 800000 - assert container.metadata == {} - - # Check method calls. - self.assertTrue(wrapped._filter("read")) - if seekable: - self.assertTrue(wrapped._filter("seek")) - - def write(self, fh): - wrapped = MethodLogger(fh) - - with av.open(wrapped, "w", "mp4") as container: - write_rgb_rotate(container) - - # Check method calls. - self.assertTrue(wrapped._filter("write")) - self.assertTrue(wrapped._filter("seek")) diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 7d7180ebf..6b856be86 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -286,7 +286,7 @@ def test_ndarray_gbrp12_align(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_gbrp14(): +def test_ndarray_gbrp14() -> None: array = numpy.random.randint(0, 16384, size=(480, 640, 3), dtype=numpy.uint16) for format in ("gbrp14be", "gbrp14le"): frame = VideoFrame.from_ndarray(array, format=format) @@ -295,7 +295,7 @@ def test_ndarray_gbrp14(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_gbrp14_align(): +def test_ndarray_gbrp14_align() -> None: array = numpy.random.randint(0, 16384, size=(238, 318, 3), dtype=numpy.uint16) for format in ("gbrp14be", "gbrp14le"): frame = VideoFrame.from_ndarray(array, format=format) @@ -304,7 +304,7 @@ def test_ndarray_gbrp14_align(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_gbrp16(): +def test_ndarray_gbrp16() -> None: array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) for format in ("gbrp16be", "gbrp16le"): frame = VideoFrame.from_ndarray(array, format=format) @@ -313,7 +313,7 @@ def test_ndarray_gbrp16(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_gbrp16_align(): +def test_ndarray_gbrp16_align() -> None: array = numpy.random.randint(0, 65536, size=(238, 318, 3), dtype=numpy.uint16) for format in ("gbrp16be", "gbrp16le"): frame = VideoFrame.from_ndarray(array, format=format) @@ -322,7 +322,7 @@ def test_ndarray_gbrp16_align(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_gbrpf32(): +def test_ndarray_gbrpf32() -> None: array = numpy.random.random_sample(size=(480, 640, 3)).astype(numpy.float32) for format in ("gbrpf32be", "gbrpf32le"): frame = VideoFrame.from_ndarray(array, format=format) @@ -331,7 +331,7 @@ def test_ndarray_gbrpf32(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_gbrpf32_align(): +def test_ndarray_gbrpf32_align() -> None: array = numpy.random.random_sample(size=(238, 318, 3)).astype(numpy.float32) for format in ["gbrpf32be", "gbrpf32le"]: frame = VideoFrame.from_ndarray(array, format=format) @@ -340,7 +340,7 @@ def test_ndarray_gbrpf32_align(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_yuv420p(): +def test_ndarray_yuv420p() -> None: array = numpy.random.randint(0, 256, size=(720, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuv420p") assert frame.width == 640 and frame.height == 480 @@ -348,7 +348,7 @@ def test_ndarray_yuv420p(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_yuv420p_align(): +def test_ndarray_yuv420p_align() -> None: array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuv420p") assert frame.width == 318 and frame.height == 238 @@ -356,7 +356,7 @@ def test_ndarray_yuv420p_align(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_yuvj420p(): +def test_ndarray_yuvj420p() -> None: array = numpy.random.randint(0, 256, size=(720, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuvj420p") assert frame.width == 640 and frame.height == 480 @@ -364,7 +364,7 @@ def test_ndarray_yuvj420p(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_yuyv422(): +def test_ndarray_yuyv422() -> None: array = numpy.random.randint(0, 256, size=(480, 640, 2), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuyv422") assert frame.width == 640 and frame.height == 480 @@ -372,7 +372,7 @@ def test_ndarray_yuyv422(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_yuv444p(): +def test_ndarray_yuv444p() -> None: array = numpy.random.randint(0, 256, size=(3, 480, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuv444p") assert frame.width == 640 and frame.height == 480 @@ -380,7 +380,7 @@ def test_ndarray_yuv444p(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_yuvj444p(): +def test_ndarray_yuvj444p() -> None: array = numpy.random.randint(0, 256, size=(3, 480, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuvj444p") assert frame.width == 640 and frame.height == 480 @@ -388,7 +388,7 @@ def test_ndarray_yuvj444p(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_yuyv422_align(): +def test_ndarray_yuyv422_align() -> None: array = numpy.random.randint(0, 256, size=(238, 318, 2), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuyv422") assert frame.width == 318 and frame.height == 238 @@ -396,7 +396,7 @@ def test_ndarray_yuyv422_align(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_gray16be(): +def test_ndarray_gray16be() -> None: array = numpy.random.randint(0, 65536, size=(480, 640), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="gray16be") assert frame.width == 640 and frame.height == 480 @@ -429,7 +429,7 @@ def test_ndarray_rgb48be(): assertPixelValue16(frame.planes[0], array[0][0][0], "big") -def test_ndarray_rgb48le(): +def test_ndarray_rgb48le() -> None: array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="rgb48le") assert frame.width == 640 and frame.height == 480 @@ -440,7 +440,7 @@ def test_ndarray_rgb48le(): assertPixelValue16(frame.planes[0], array[0][0][0], "little") -def test_ndarray_rgb48le_align(): +def test_ndarray_rgb48le_align() -> None: array = numpy.random.randint(0, 65536, size=(238, 318, 3), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="rgb48le") assert frame.width == 318 and frame.height == 238 @@ -451,7 +451,7 @@ def test_ndarray_rgb48le_align(): assertPixelValue16(frame.planes[0], array[0][0][0], "little") -def test_ndarray_rgba64be(): +def test_ndarray_rgba64be() -> None: array = numpy.random.randint(0, 65536, size=(480, 640, 4), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="rgba64be") assert frame.width == 640 and frame.height == 480 @@ -462,7 +462,7 @@ def test_ndarray_rgba64be(): assertPixelValue16(frame.planes[0], array[0][0][0], "big") -def test_ndarray_rgba64le(): +def test_ndarray_rgba64le() -> None: array = numpy.random.randint(0, 65536, size=(480, 640, 4), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="rgba64le") assert frame.width == 640 and frame.height == 480 @@ -473,7 +473,7 @@ def test_ndarray_rgba64le(): assertPixelValue16(frame.planes[0], array[0][0][0], "little") -def test_ndarray_rgb8(): +def test_ndarray_rgb8() -> None: array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="rgb8") assert frame.width == 640 and frame.height == 480 @@ -481,7 +481,7 @@ def test_ndarray_rgb8(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_bgr8(): +def test_ndarray_bgr8() -> None: array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="bgr8") assert frame.width == 640 and frame.height == 480 @@ -501,7 +501,7 @@ def test_ndarray_pal8(): assertNdarraysEqual(returned[1], palette) -def test_ndarray_nv12(): +def test_ndarray_nv12() -> None: array = numpy.random.randint(0, 256, size=(720, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="nv12") assert frame.width == 640 and frame.height == 480 @@ -509,7 +509,7 @@ def test_ndarray_nv12(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_nv12_align(): +def test_ndarray_nv12_align() -> None: array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="nv12") assert frame.width == 318 and frame.height == 238 @@ -517,7 +517,7 @@ def test_ndarray_nv12_align(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_shares_memory_gray(): +def test_shares_memory_gray() -> None: array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) frame = VideoFrame.from_numpy_buffer(array, "gray") assertNdarraysEqual(frame.to_ndarray(), array) @@ -528,7 +528,7 @@ def test_shares_memory_gray(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_shares_memory_gray8(): +def test_shares_memory_gray8() -> None: array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) frame = VideoFrame.from_numpy_buffer(array, "gray8") assertNdarraysEqual(frame.to_ndarray(), array) @@ -539,7 +539,7 @@ def test_shares_memory_gray8(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_shares_memory_rgb8(): +def test_shares_memory_rgb8() -> None: array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) frame = VideoFrame.from_numpy_buffer(array, "rgb8") assertNdarraysEqual(frame.to_ndarray(), array) @@ -550,7 +550,7 @@ def test_shares_memory_rgb8(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_shares_memory_bgr8(): +def test_shares_memory_bgr8() -> None: array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) frame = VideoFrame.from_numpy_buffer(array, "bgr8") assertNdarraysEqual(frame.to_ndarray(), array) From b558b06a7e3a235de277c658ce29707278bafdf0 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 25 Sep 2024 04:33:20 -0400 Subject: [PATCH 118/365] Fix some incorrect type stubs --- av/__init__.py | 1 + av/codec/__init__.py | 9 ++++++ av/codec/context.pyi | 2 ++ av/container/output.pyi | 2 +- av/video/codeccontext.pyi | 2 +- av/video/format.pyi | 1 + av/video/stream.pyi | 2 +- tests/test_audiofifo.py | 14 +++----- tests/test_codec_context.py | 7 ++-- tests/test_file_probing.py | 4 +-- tests/test_logging.py | 31 ++++++++---------- tests/test_open.py | 46 +++++++++++++------------- tests/test_python_io.py | 4 +-- tests/test_videoformat.py | 64 ++++++++++++++++++------------------- 14 files changed, 97 insertions(+), 92 deletions(-) diff --git a/av/__init__.py b/av/__init__.py index 266aec3e1..b5ea598b1 100644 --- a/av/__init__.py +++ b/av/__init__.py @@ -47,6 +47,7 @@ AVError = FFmpegError # noqa: F405 __all__ = ( + "__version__", "time_base", "library_versions", "AudioCodecContext", diff --git a/av/codec/__init__.py b/av/codec/__init__.py index 7a775ed5a..f35f9b7d4 100644 --- a/av/codec/__init__.py +++ b/av/codec/__init__.py @@ -1,2 +1,11 @@ from .codec import Capabilities, Codec, Properties, codec_descriptor, codecs_available from .context import CodecContext + +__all__ = ( + "Capabilities", + "Codec", + "Properties", + "codec_descriptor", + "codecs_available", + "CodecContext", +) diff --git a/av/codec/context.pyi b/av/codec/context.pyi index 805b06faf..d2d03b7ab 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -56,6 +56,7 @@ class Flags2(EnumFlag): class CodecContext: extradata: bytes | None + extradata_size: int is_open: bool is_encoder: bool is_decoder: bool @@ -95,6 +96,7 @@ class CodecContext: ac_pred: bool interlaced_me: bool closed_gop: bool + delay: bool def open(self, strict: bool = True) -> None: ... def close(self, strict: bool = True) -> None: ... diff --git a/av/container/output.pyi b/av/container/output.pyi index a3218ae34..760532a96 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -23,7 +23,7 @@ class OutputContainer(Container): @overload def add_stream( self, - codec_name: Literal["h264", "mpeg4", "png"], + codec_name: Literal["h264", "mpeg4", "png", "qtrle"], rate: Fraction | int | float | None = None, template: None = None, options: dict[str, str] | None = None, diff --git a/av/video/codeccontext.pyi b/av/video/codeccontext.pyi index ebef8b275..7172b50c2 100644 --- a/av/video/codeccontext.pyi +++ b/av/video/codeccontext.pyi @@ -11,7 +11,7 @@ class VideoCodecContext(CodecContext): format: VideoFormat | None width: int height: int - bits_per_codec_sample: int + bits_per_coded_sample: int pix_fmt: str | None framerate: Fraction rate: Fraction diff --git a/av/video/format.pyi b/av/video/format.pyi index 17c66de82..ee16b85b8 100644 --- a/av/video/format.pyi +++ b/av/video/format.pyi @@ -9,6 +9,7 @@ class VideoFormat: is_rgb: bool width: int height: int + components: tuple[VideoFormatComponent, ...] def __init__(self, name: str, width: int = 0, height: int = 0) -> None: ... def chroma_width(self, luma_width: int = 0) -> int: ... diff --git a/av/video/stream.pyi b/av/video/stream.pyi index 94ffccd64..3028bd31c 100644 --- a/av/video/stream.pyi +++ b/av/video/stream.pyi @@ -21,7 +21,7 @@ class VideoStream(Stream): format: VideoFormat width: int height: int - bits_per_codec_sample: int + bits_per_coded_sample: int pix_fmt: str | None framerate: Fraction rate: Fraction diff --git a/tests/test_audiofifo.py b/tests/test_audiofifo.py index 9eee420a8..85e9481ee 100644 --- a/tests/test_audiofifo.py +++ b/tests/test_audiofifo.py @@ -33,11 +33,8 @@ def test_data(self) -> None: def test_pts_simple(self) -> None: fifo = av.AudioFifo() - # ensure __repr__ does not crash - self.assertTrue( - str(fifo).startswith( - " None: fifo.write(iframe) - # ensure __repr__ was updated - self.assertTrue( - str(fifo).startswith( - " at 0x" - ) + assert str(fifo).startswith( + " at 0x" ) oframe = fifo.read(512) diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index a6bae17a0..fa56a5e4f 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -72,7 +72,7 @@ def test_skip_frame_default(self): ctx = Codec("png", "w").create() assert ctx.skip_frame.name == "DEFAULT" - def test_codec_delay(self): + def test_codec_delay(self) -> None: with av.open(fate_suite("mkv/codec_delay_opus.mkv")) as container: assert container.streams.audio[0].codec_context.delay == 312 with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: @@ -131,7 +131,7 @@ def test_decoder_timebase(self) -> None: with pytest.raises(RuntimeError): ctx.time_base = Fraction(1, 25) - def test_encoder_extradata(self): + def test_encoder_extradata(self) -> None: ctx = av.codec.Codec("h264", "w").create() assert ctx.extradata is None assert ctx.extradata_size == 0 @@ -140,7 +140,7 @@ def test_encoder_extradata(self): assert ctx.extradata == b"123" assert ctx.extradata_size == 3 - def test_encoder_pix_fmt(self): + def test_encoder_pix_fmt(self) -> None: ctx = av.codec.Codec("h264", "w").create("video") # valid format @@ -161,6 +161,7 @@ def test_bits_per_coded_sample(self): for packet in container.demux(stream): for frame in packet.decode(): pass + assert isinstance(packet.stream, av.VideoStream) assert packet.stream.bits_per_coded_sample == 32 with av.open(fate_suite("qtrle/aletrek-rle.mov")) as container: diff --git a/tests/test_file_probing.py b/tests/test_file_probing.py index d8a7fd755..87d462264 100644 --- a/tests/test_file_probing.py +++ b/tests/test_file_probing.py @@ -43,9 +43,7 @@ def test_stream_probing(self): assert stream.bit_rate is None assert stream.channels == 2 assert stream.codec.name == "aac_latm" - self.assertEqual( - stream.codec.long_name, "AAC LATM (Advanced Audio Coding LATM syntax)" - ) + assert stream.codec.long_name == "AAC LATM (Advanced Audio Coding LATM syntax)" assert stream.format.bits == 32 assert stream.format.name == "fltp" assert stream.layout.name == "stereo" diff --git a/tests/test_logging.py b/tests/test_logging.py index 5602ce751..8e863791c 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -8,7 +8,7 @@ from .common import TestCase -def do_log(message): +def do_log(message: str) -> None: av.logging.log(av.logging.INFO, "test", message) @@ -16,9 +16,9 @@ class TestLogging(TestCase): def test_adapt_level(self): assert av.logging.adapt_level(av.logging.ERROR) == logging.ERROR assert av.logging.adapt_level(av.logging.WARNING) == logging.WARNING - self.assertEqual( - av.logging.adapt_level((av.logging.WARNING + av.logging.ERROR) // 2), - logging.WARNING, + assert ( + av.logging.adapt_level((av.logging.WARNING + av.logging.ERROR) // 2) + == logging.WARNING ) def test_threaded_captures(self): @@ -30,7 +30,7 @@ def test_threaded_captures(self): thread.start() thread.join() - self.assertIn((av.logging.INFO, "test", "main"), logs) + assert (av.logging.INFO, "test", "main") in logs av.logging.set_level(None) def test_global_captures(self): @@ -42,8 +42,8 @@ def test_global_captures(self): thread.start() thread.join() - self.assertIn((av.logging.INFO, "test", "main"), logs) - self.assertIn((av.logging.INFO, "test", "thread"), logs) + assert (av.logging.INFO, "test", "main") in logs + assert (av.logging.INFO, "test", "thread") in logs av.logging.set_level(None) def test_repeats(self): @@ -59,16 +59,13 @@ def test_repeats(self): logs = [log for log in logs if log[1] == "test"] - self.assertEqual( - logs, - [ - (av.logging.INFO, "test", "foo"), - (av.logging.INFO, "test", "foo"), - (av.logging.INFO, "test", "bar"), - (av.logging.INFO, "test", "bar (repeated 2 more times)"), - (av.logging.INFO, "test", "baz"), - ], - ) + assert logs == [ + (av.logging.INFO, "test", "foo"), + (av.logging.INFO, "test", "foo"), + (av.logging.INFO, "test", "bar"), + (av.logging.INFO, "test", "bar (repeated 2 more times)"), + (av.logging.INFO, "test", "baz"), + ] av.logging.set_level(None) diff --git a/tests/test_open.py b/tests/test_open.py index b77312203..9341e1f56 100644 --- a/tests/test_open.py +++ b/tests/test_open.py @@ -2,34 +2,36 @@ import av -from .common import TestCase, fate_suite +from .common import fate_suite -class TestOpen(TestCase): - def test_path_input(self): - path = Path(fate_suite("h264/interlaced_crop.mp4")) - self.assertIsInstance(path, Path) +def test_path_input() -> None: + path = Path(fate_suite("h264/interlaced_crop.mp4")) + assert isinstance(path, Path) - container = av.open(path) - self.assertIs(type(container), av.container.InputContainer) + container = av.open(path) + assert type(container) is av.container.InputContainer - def test_str_input(self): - path = fate_suite("h264/interlaced_crop.mp4") - self.assertIs(type(path), str) - container = av.open(path) - self.assertIs(type(container), av.container.InputContainer) +def test_str_input() -> None: + path = fate_suite("h264/interlaced_crop.mp4") + assert type(path) is str - def test_path_output(self): - path = Path(fate_suite("h264/interlaced_crop.mp4")) - self.assertIsInstance(path, Path) + container = av.open(path) + assert type(container) is av.container.InputContainer - container = av.open(path, "w") - self.assertIs(type(container), av.container.OutputContainer) - def test_str_output(self): - path = fate_suite("h264/interlaced_crop.mp4") - self.assertIs(type(path), str) +def test_path_output() -> None: + path = Path(fate_suite("h264/interlaced_crop.mp4")) + assert isinstance(path, Path) - container = av.open(path, "w") - self.assertIs(type(container), av.container.OutputContainer) + container = av.open(path, "w") + assert type(container) is av.container.OutputContainer + + +def test_str_output() -> None: + path = fate_suite("h264/interlaced_crop.mp4") + assert type(path) is str + + container = av.open(path, "w") + assert type(container) is av.container.OutputContainer diff --git a/tests/test_python_io.py b/tests/test_python_io.py index 3fb9cf3dd..3cb4b7720 100644 --- a/tests/test_python_io.py +++ b/tests/test_python_io.py @@ -67,7 +67,7 @@ class ReadOnlyBuffer: Minimal buffer which *only* implements the read() method. """ - def __init__(self, data): + def __init__(self, data) -> None: self.data = data def read(self, n): @@ -225,7 +225,7 @@ def test_writing_to_buffer_broken(self) -> None: # break I/O buf.broken = True - def test_writing_to_buffer_broken_with_close(self): + def test_writing_to_buffer_broken_with_close(self) -> None: buf = BrokenBuffer() with av.open(buf, "w", "mp4") as container: diff --git a/tests/test_videoformat.py b/tests/test_videoformat.py index 4670dc217..1e2e0c0f4 100644 --- a/tests/test_videoformat.py +++ b/tests/test_videoformat.py @@ -9,13 +9,13 @@ def test_invalid_pixel_format(self): VideoFormat("__unknown_pix_fmt", 640, 480) assert str(cm.exception) == "not a pixel format: '__unknown_pix_fmt'" - def test_rgb24_inspection(self): + def test_rgb24_inspection(self) -> None: fmt = VideoFormat("rgb24", 640, 480) assert fmt.name == "rgb24" assert len(fmt.components) == 3 - self.assertFalse(fmt.is_planar) - self.assertFalse(fmt.has_palette) - self.assertTrue(fmt.is_rgb) + assert not fmt.is_planar + assert not fmt.has_palette + assert fmt.is_rgb assert fmt.chroma_width() == 640 assert fmt.chroma_height() == 480 assert fmt.chroma_width(1024) == 1024 @@ -24,22 +24,22 @@ def test_rgb24_inspection(self): comp = fmt.components[i] assert comp.plane == 0 assert comp.bits == 8 - self.assertFalse(comp.is_luma) - self.assertFalse(comp.is_chroma) - self.assertFalse(comp.is_alpha) + assert not comp.is_luma + assert not comp.is_chroma + assert not comp.is_alpha assert comp.width == 640 assert comp.height == 480 - def test_yuv420p_inspection(self): + def test_yuv420p_inspection(self) -> None: fmt = VideoFormat("yuv420p", 640, 480) assert fmt.name == "yuv420p" assert len(fmt.components) == 3 self._test_yuv420(fmt) - def _test_yuv420(self, fmt): - self.assertTrue(fmt.is_planar) - self.assertFalse(fmt.has_palette) - self.assertFalse(fmt.is_rgb) + def _test_yuv420(self, fmt: VideoFormat) -> None: + assert fmt.is_planar + assert not fmt.has_palette + assert not fmt.is_rgb assert fmt.chroma_width() == 320 assert fmt.chroma_height() == 240 assert fmt.chroma_width(1024) == 512 @@ -48,33 +48,33 @@ def _test_yuv420(self, fmt): comp = fmt.components[i] assert comp.plane == i assert comp.bits == 8 - self.assertFalse(fmt.components[0].is_chroma) - self.assertTrue(fmt.components[1].is_chroma) - self.assertTrue(fmt.components[2].is_chroma) - self.assertTrue(fmt.components[0].is_luma) - self.assertFalse(fmt.components[1].is_luma) - self.assertFalse(fmt.components[2].is_luma) - self.assertFalse(fmt.components[0].is_alpha) - self.assertFalse(fmt.components[1].is_alpha) - self.assertFalse(fmt.components[2].is_alpha) + assert not fmt.components[0].is_chroma + assert fmt.components[1].is_chroma + assert fmt.components[2].is_chroma + assert fmt.components[0].is_luma + assert not fmt.components[1].is_luma + assert not fmt.components[2].is_luma + assert not fmt.components[0].is_alpha + assert not fmt.components[1].is_alpha + assert not fmt.components[2].is_alpha assert fmt.components[0].width == 640 assert fmt.components[1].width == 320 assert fmt.components[2].width == 320 - def test_yuva420p_inspection(self): + def test_yuva420p_inspection(self) -> None: fmt = VideoFormat("yuva420p", 640, 480) assert len(fmt.components) == 4 self._test_yuv420(fmt) - self.assertFalse(fmt.components[3].is_chroma) + assert not fmt.components[3].is_chroma assert fmt.components[3].width == 640 - def test_gray16be_inspection(self): + def test_gray16be_inspection(self) -> None: fmt = VideoFormat("gray16be", 640, 480) assert fmt.name == "gray16be" assert len(fmt.components) == 1 - self.assertFalse(fmt.is_planar) - self.assertFalse(fmt.has_palette) - self.assertFalse(fmt.is_rgb) + assert not fmt.is_planar + assert not fmt.has_palette + assert not fmt.is_rgb assert fmt.chroma_width() == 640 assert fmt.chroma_height() == 480 assert fmt.chroma_width(1024) == 1024 @@ -82,13 +82,13 @@ def test_gray16be_inspection(self): comp = fmt.components[0] assert comp.plane == 0 assert comp.bits == 16 - self.assertTrue(comp.is_luma) - self.assertFalse(comp.is_chroma) + assert comp.is_luma + assert not comp.is_chroma assert comp.width == 640 assert comp.height == 480 - self.assertFalse(comp.is_alpha) + assert not comp.is_alpha - def test_pal8_inspection(self): + def test_pal8_inspection(self) -> None: fmt = VideoFormat("pal8", 640, 480) assert len(fmt.components) == 1 - self.assertTrue(fmt.has_palette) + assert fmt.has_palette From e3253e109fcf0d4c835c83fe2efea163805b77a7 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 25 Sep 2024 05:21:04 -0400 Subject: [PATCH 119/365] `mypy --strict` now reports less than 100 errors (#1553) --- av/error.pyi | 3 + av/frame.pyi | 8 +- av/video/frame.pyi | 4 +- tests/test_codec_context.py | 4 +- tests/test_decode.py | 2 +- tests/test_encode.py | 10 +-- tests/test_errors.py | 4 +- tests/test_file_probing.py | 7 +- tests/test_filters.py | 7 +- tests/test_logging.py | 145 ++++++++++++++++++------------------ tests/test_python_io.py | 4 +- tests/test_videoframe.py | 48 ++++++------ 12 files changed, 128 insertions(+), 118 deletions(-) diff --git a/av/error.pyi b/av/error.pyi index 527d8c00d..088f78d45 100644 --- a/av/error.pyi +++ b/av/error.pyi @@ -11,6 +11,9 @@ def make_error( filename: str | None = None, log: tuple[int, tuple[int, str, str] | None] | None = None, ) -> None: ... +def err_check(res: int, filename: str | None = None) -> int: ... + +BUFFER_TOO_SMALL: EnumItem class ErrorType(EnumItem): BSF_NOT_FOUND: int diff --git a/av/frame.pyi b/av/frame.pyi index 6e5348119..7c34e91bf 100644 --- a/av/frame.pyi +++ b/av/frame.pyi @@ -1,4 +1,10 @@ from fractions import Fraction +from typing import TypedDict + +from av.sidedata.motionvectors import MotionVectors + +class SideData(TypedDict, total=False): + MOTION_VECTORS: MotionVectors class Frame: dts: int | None @@ -6,7 +12,7 @@ class Frame: time: float | None time_base: Fraction is_corrupt: bool - side_data: dict[str, str] + side_data: SideData opaque: object def make_writable(self) -> None: ... diff --git a/av/video/frame.pyi b/av/video/frame.pyi index de84faaa0..29f961ba4 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -46,8 +46,8 @@ class VideoFrame(Frame): width: int | None = None, height: int | None = None, format: str | None = None, - src_colorspace: int | None = None, - dst_colorspace: int | None = None, + src_colorspace: str | int | None = None, + dst_colorspace: str | int | None = None, interpolation: int | str | None = None, src_color_range: int | str | None = None, dst_color_range: int | str | None = None, diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index fa56a5e4f..6425c4dd1 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -169,7 +169,7 @@ def test_bits_per_coded_sample(self): stream.bits_per_coded_sample = 31 with pytest.raises(av.error.InvalidDataError): - for frame in container.decode(stream): + for _ in container.decode(stream): pass with av.open(self.sandboxed("output.mov"), "w") as output: @@ -396,7 +396,7 @@ def video_encoding( assert i == gop_size final_gop_size = decoded_frame_count - max(keyframe_indices) - self.assertLessEqual(final_gop_size, gop_size) + assert final_gop_size < gop_size def test_encoding_pcm_s24le(self) -> None: self.audio_encoding("pcm_s24le") diff --git a/tests/test_decode.py b/tests/test_decode.py index 7622693b5..20abdf840 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -41,7 +41,7 @@ def test_decode_audio_sample_count(self) -> None: audio_stream = next(s for s in container.streams if s.type == "audio") assert audio_stream is container.streams.audio[0] - assert isinstance(audio_stream, av.audio.AudioStream) + assert isinstance(audio_stream, av.AudioStream) sample_count = 0 diff --git a/tests/test_encode.py b/tests/test_encode.py index 617231409..89d2e3eb3 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -1,9 +1,9 @@ import io import math from fractions import Fraction -from unittest import SkipTest import numpy as np +import pytest import av from av import AudioFrame, VideoFrame @@ -19,7 +19,7 @@ def write_rgb_rotate(output: av.container.OutputContainer) -> None: if not has_pillow: - raise SkipTest("Don't have Pillow") + pytest.skip() import PIL.Image as Image @@ -233,8 +233,8 @@ def test_stream_index(self) -> None: astream = output.add_stream("mp2", 48000) assert astream in output.streams.audio - astream.layout = "stereo" # type: ignore - astream.format = "s16" # type: ignore + astream.layout = "stereo" + astream.format = "s16" assert vstream.index == 0 assert astream.index == 1 @@ -385,4 +385,4 @@ def test_max_b_frames(self) -> None: for max_b_frames in range(4): file = encode_file_with_max_b_frames(max_b_frames) actual_max_b_frames = max_b_frame_run_in_file(file) - self.assertTrue(actual_max_b_frames <= max_b_frames) + assert actual_max_b_frames <= max_b_frames diff --git a/tests/test_errors.py b/tests/test_errors.py index 5f7440402..1cad5d086 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -53,11 +53,11 @@ def test_filenotfound(): assert False, "No exception raised!" -def test_buffertoosmall(): +def test_buffertoosmall() -> None: """Throw an exception from an enum.""" try: av.error.err_check(-av.error.BUFFER_TOO_SMALL.value) - except av.BufferTooSmallError as e: + except av.error.BufferTooSmallError as e: assert e.errno == av.error.BUFFER_TOO_SMALL.value else: assert False, "No exception raised!" diff --git a/tests/test_file_probing.py b/tests/test_file_probing.py index 87d462264..f71391697 100644 --- a/tests/test_file_probing.py +++ b/tests/test_file_probing.py @@ -9,7 +9,7 @@ class TestAudioProbe(TestCase): def setUp(self): self.file = av.open(fate_suite("aac/latm_stereo_to_51.ts")) - def test_container_probing(self): + def test_container_probing(self) -> None: assert self.file.bit_rate == 269558 assert self.file.duration == 6165333 assert str(self.file.format) == "" @@ -20,9 +20,10 @@ def test_container_probing(self): assert self.file.start_time == 1400000 assert len(self.file.streams) == 1 - def test_stream_probing(self): + def test_stream_probing(self) -> None: stream = self.file.streams[0] + assert isinstance(stream, av.AudioStream) assert str(stream).startswith( " None: # write an empty file path = self.sandboxed("empty.flac") with open(path, "wb"): diff --git a/tests/test_filters.py b/tests/test_filters.py index e7bbeca1a..a87d6b5b0 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -126,7 +126,7 @@ def test_audio_buffer_sink(self): if e.errno != errno.EAGAIN: raise - def test_audio_buffer_resample(self): + def test_audio_buffer_resample(self) -> None: graph = Graph() graph.link_nodes( graph.add_abuffer( @@ -147,6 +147,7 @@ def test_audio_buffer_resample(self): ) ) out_frame = graph.pull() + assert isinstance(out_frame, av.AudioFrame) assert out_frame.format.name == "s16" assert out_frame.layout.name == "stereo" assert out_frame.sample_rate == 44100 @@ -202,9 +203,7 @@ def test_audio_buffer_volume_filter(self): input_data = input_frame.to_ndarray() output_data = out_frame.to_ndarray() - self.assertTrue( - np.allclose(input_data * 0.5, output_data), "Check that volume is reduced" - ) + assert np.allclose(input_data * 0.5, output_data) def test_video_buffer(self): input_container = av.open(format="lavfi", file="color=c=pink:duration=1:r=30") diff --git a/tests/test_logging.py b/tests/test_logging.py index 8e863791c..c8c705b1c 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -5,80 +5,81 @@ import av.error import av.logging -from .common import TestCase - def do_log(message: str) -> None: av.logging.log(av.logging.INFO, "test", message) -class TestLogging(TestCase): - def test_adapt_level(self): - assert av.logging.adapt_level(av.logging.ERROR) == logging.ERROR - assert av.logging.adapt_level(av.logging.WARNING) == logging.WARNING - assert ( - av.logging.adapt_level((av.logging.WARNING + av.logging.ERROR) // 2) - == logging.WARNING - ) - - def test_threaded_captures(self): - av.logging.set_level(av.logging.VERBOSE) - - with av.logging.Capture(local=True) as logs: - do_log("main") - thread = threading.Thread(target=do_log, args=("thread",)) - thread.start() - thread.join() - - assert (av.logging.INFO, "test", "main") in logs - av.logging.set_level(None) - - def test_global_captures(self): - av.logging.set_level(av.logging.VERBOSE) - - with av.logging.Capture(local=False) as logs: - do_log("main") - thread = threading.Thread(target=do_log, args=("thread",)) - thread.start() - thread.join() - - assert (av.logging.INFO, "test", "main") in logs - assert (av.logging.INFO, "test", "thread") in logs - av.logging.set_level(None) - - def test_repeats(self): - av.logging.set_level(av.logging.VERBOSE) - - with av.logging.Capture() as logs: - do_log("foo") - do_log("foo") - do_log("bar") - do_log("bar") - do_log("bar") - do_log("baz") - - logs = [log for log in logs if log[1] == "test"] - - assert logs == [ - (av.logging.INFO, "test", "foo"), - (av.logging.INFO, "test", "foo"), - (av.logging.INFO, "test", "bar"), - (av.logging.INFO, "test", "bar (repeated 2 more times)"), - (av.logging.INFO, "test", "baz"), - ] - - av.logging.set_level(None) - - def test_error(self): - av.logging.set_level(av.logging.VERBOSE) - - log = (av.logging.ERROR, "test", "This is a test.") - av.logging.log(*log) - try: - av.error.err_check(-errno.EPERM) - except OSError as e: - assert e.log == log - else: - self.fail() - - av.logging.set_level(None) +def test_adapt_level() -> None: + assert av.logging.adapt_level(av.logging.ERROR) == logging.ERROR + assert av.logging.adapt_level(av.logging.WARNING) == logging.WARNING + assert ( + av.logging.adapt_level((av.logging.WARNING + av.logging.ERROR) // 2) + == logging.WARNING + ) + + +def test_threaded_captures() -> None: + av.logging.set_level(av.logging.VERBOSE) + + with av.logging.Capture(local=True) as logs: + do_log("main") + thread = threading.Thread(target=do_log, args=("thread",)) + thread.start() + thread.join() + + assert (av.logging.INFO, "test", "main") in logs + av.logging.set_level(None) + + +def test_global_captures() -> None: + av.logging.set_level(av.logging.VERBOSE) + + with av.logging.Capture(local=False) as logs: + do_log("main") + thread = threading.Thread(target=do_log, args=("thread",)) + thread.start() + thread.join() + + assert (av.logging.INFO, "test", "main") in logs + assert (av.logging.INFO, "test", "thread") in logs + av.logging.set_level(None) + + +def test_repeats() -> None: + av.logging.set_level(av.logging.VERBOSE) + + with av.logging.Capture() as logs: + do_log("foo") + do_log("foo") + do_log("bar") + do_log("bar") + do_log("bar") + do_log("baz") + + logs = [log for log in logs if log[1] == "test"] + + assert logs == [ + (av.logging.INFO, "test", "foo"), + (av.logging.INFO, "test", "foo"), + (av.logging.INFO, "test", "bar"), + (av.logging.INFO, "test", "bar (repeated 2 more times)"), + (av.logging.INFO, "test", "baz"), + ] + + av.logging.set_level(None) + + +def test_error() -> None: + av.logging.set_level(av.logging.VERBOSE) + + log = (av.logging.ERROR, "test", "This is a test.") + av.logging.log(*log) + try: + av.error.err_check(-errno.EPERM) + except av.error.PermissionError as e: + assert e.log == log + else: + assert False + + av.logging.set_level(None) diff --git a/tests/test_python_io.py b/tests/test_python_io.py index 3cb4b7720..d657404d9 100644 --- a/tests/test_python_io.py +++ b/tests/test_python_io.py @@ -355,13 +355,13 @@ def test_writing_to_pipe_readonly(self) -> None: ) as cm: write(buf) - def test_writing_to_pipe_writeonly(self): + def test_writing_to_pipe_writeonly(self) -> None: av.logging.set_level(av.logging.VERBOSE) buf = WriteOnlyPipe() with pytest.raises( ValueError, match=escape("[mp4] muxer does not support non seekable output") - ) as cm: + ): write(buf) av.logging.set_level(None) diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 6b856be86..8dcdb838a 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -170,17 +170,17 @@ def test_to_image_rgb24(self): assert img.tobytes() == expected -def test_basic_to_ndarray(): +def test_basic_to_ndarray() -> None: array = VideoFrame(640, 480, "rgb24").to_ndarray() assert array.shape == (480, 640, 3) -def test_to_image_with_dimensions(): +def test_to_image_with_dimensions() -> None: img = VideoFrame(640, 480, format="rgb24").to_image(width=320, height=240) assert img.size == (320, 240) -def test_ndarray_gray(): +def test_ndarray_gray() -> None: array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) for format in ("gray", "gray8"): frame = VideoFrame.from_ndarray(array, format=format) @@ -189,7 +189,7 @@ def test_ndarray_gray(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_gray_align(): +def test_ndarray_gray_align() -> None: array = numpy.random.randint(0, 256, size=(238, 318), dtype=numpy.uint8) for format in ("gray", "gray8"): frame = VideoFrame.from_ndarray(array, format=format) @@ -198,7 +198,7 @@ def test_ndarray_gray_align(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_rgb(): +def test_ndarray_rgb() -> None: array = numpy.random.randint(0, 256, size=(480, 640, 3), dtype=numpy.uint8) for format in ("rgb24", "bgr24"): frame = VideoFrame.from_ndarray(array, format=format) @@ -207,7 +207,7 @@ def test_ndarray_rgb(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_rgb_align(): +def test_ndarray_rgb_align() -> None: array = numpy.random.randint(0, 256, size=(238, 318, 3), dtype=numpy.uint8) for format in ("rgb24", "bgr24"): frame = VideoFrame.from_ndarray(array, format=format) @@ -216,7 +216,7 @@ def test_ndarray_rgb_align(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_rgba(): +def test_ndarray_rgba() -> None: array = numpy.random.randint(0, 256, size=(480, 640, 4), dtype=numpy.uint8) for format in ("argb", "rgba", "abgr", "bgra"): frame = VideoFrame.from_ndarray(array, format=format) @@ -225,7 +225,7 @@ def test_ndarray_rgba(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_rgba_align(): +def test_ndarray_rgba_align() -> None: array = numpy.random.randint(0, 256, size=(238, 318, 4), dtype=numpy.uint8) for format in ("argb", "rgba", "abgr", "bgra"): frame = VideoFrame.from_ndarray(array, format=format) @@ -234,7 +234,7 @@ def test_ndarray_rgba_align(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_gbrp(): +def test_ndarray_gbrp() -> None: array = numpy.random.randint(0, 256, size=(480, 640, 3), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="gbrp") assert frame.width == 640 and frame.height == 480 @@ -242,7 +242,7 @@ def test_ndarray_gbrp(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_gbrp_align(): +def test_ndarray_gbrp_align() -> None: array = numpy.random.randint(0, 256, size=(238, 318, 3), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="gbrp") assert frame.width == 318 and frame.height == 238 @@ -250,7 +250,7 @@ def test_ndarray_gbrp_align(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_gbrp10(): +def test_ndarray_gbrp10() -> None: array = numpy.random.randint(0, 1024, size=(480, 640, 3), dtype=numpy.uint16) for format in ("gbrp10be", "gbrp10le"): frame = VideoFrame.from_ndarray(array, format=format) @@ -259,7 +259,7 @@ def test_ndarray_gbrp10(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_gbrp10_align(): +def test_ndarray_gbrp10_align() -> None: array = numpy.random.randint(0, 1024, size=(238, 318, 3), dtype=numpy.uint16) for format in ("gbrp10be", "gbrp10le"): frame = VideoFrame.from_ndarray(array, format=format) @@ -268,7 +268,7 @@ def test_ndarray_gbrp10_align(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_gbrp12(): +def test_ndarray_gbrp12() -> None: array = numpy.random.randint(0, 4096, size=(480, 640, 3), dtype=numpy.uint16) for format in ("gbrp12be", "gbrp12le"): frame = VideoFrame.from_ndarray(array, format=format) @@ -277,7 +277,7 @@ def test_ndarray_gbrp12(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_gbrp12_align(): +def test_ndarray_gbrp12_align() -> None: array = numpy.random.randint(0, 4096, size=(238, 318, 3), dtype=numpy.uint16) for format in ("gbrp12be", "gbrp12le"): frame = VideoFrame.from_ndarray(array, format=format) @@ -407,7 +407,7 @@ def test_ndarray_gray16be() -> None: assertPixelValue16(frame.planes[0], array[0][0], "big") -def test_ndarray_gray16le(): +def test_ndarray_gray16le() -> None: array = numpy.random.randint(0, 65536, size=(480, 640), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="gray16le") assert frame.width == 640 and frame.height == 480 @@ -418,7 +418,7 @@ def test_ndarray_gray16le(): assertPixelValue16(frame.planes[0], array[0][0], "little") -def test_ndarray_rgb48be(): +def test_ndarray_rgb48be() -> None: array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="rgb48be") assert frame.width == 640 and frame.height == 480 @@ -561,7 +561,7 @@ def test_shares_memory_bgr8() -> None: assertNdarraysEqual(frame.to_ndarray(), array) -def test_shares_memory_rgb24(): +def test_shares_memory_rgb24() -> None: array = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8) frame = VideoFrame.from_numpy_buffer(array, "rgb24") assertNdarraysEqual(frame.to_ndarray(), array) @@ -572,7 +572,7 @@ def test_shares_memory_rgb24(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_shares_memory_yuv420p(): +def test_shares_memory_yuv420p() -> None: array = numpy.random.randint(0, 256, size=(512 * 6 // 4, 256), dtype=numpy.uint8) frame = VideoFrame.from_numpy_buffer(array, "yuv420p") assertNdarraysEqual(frame.to_ndarray(), array) @@ -583,7 +583,7 @@ def test_shares_memory_yuv420p(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_shares_memory_yuvj420p(): +def test_shares_memory_yuvj420p() -> None: array = numpy.random.randint(0, 256, size=(512 * 6 // 4, 256), dtype=numpy.uint8) frame = VideoFrame.from_numpy_buffer(array, "yuvj420p") assertNdarraysEqual(frame.to_ndarray(), array) @@ -594,7 +594,7 @@ def test_shares_memory_yuvj420p(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_shares_memory_nv12(): +def test_shares_memory_nv12() -> None: array = numpy.random.randint(0, 256, size=(512 * 6 // 4, 256), dtype=numpy.uint8) frame = VideoFrame.from_numpy_buffer(array, "nv12") assertNdarraysEqual(frame.to_ndarray(), array) @@ -605,7 +605,7 @@ def test_shares_memory_nv12(): assertNdarraysEqual(frame.to_ndarray(), array) -def test_shares_memory_bgr24(): +def test_shares_memory_bgr24() -> None: array = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8) frame = VideoFrame.from_numpy_buffer(array, "bgr24") assertNdarraysEqual(frame.to_ndarray(), array) @@ -624,13 +624,13 @@ def test_reformat_pts() -> None: assert frame.pts == 123 and frame.time_base == 456 -def test_reformat_identity(): +def test_reformat_identity() -> None: frame1 = VideoFrame(640, 480, "rgb24") frame2 = frame1.reformat(640, 480, "rgb24") assert frame1 is frame2 -def test_reformat_colorspace(): +def test_reformat_colorspace() -> None: # This is allowed. frame = VideoFrame(640, 480, "rgb24") frame.reformat(src_colorspace=None, dst_colorspace="smpte240") @@ -640,7 +640,7 @@ def test_reformat_colorspace(): frame.reformat(src_colorspace=None, dst_colorspace="smpte240") -def test_reformat_pixel_format_align(): +def test_reformat_pixel_format_align() -> None: height = 480 for width in range(2, 258, 2): frame_yuv = VideoFrame(width, height, "yuv420p") From 256f1ba9eea9a078289404d856161cf5d1445205 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 25 Sep 2024 05:26:16 -0400 Subject: [PATCH 120/365] If Conda needs this, they can patch it themselves --- av/__init__.py | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/av/__init__.py b/av/__init__.py index b5ea598b1..8752c26fd 100644 --- a/av/__init__.py +++ b/av/__init__.py @@ -1,20 +1,3 @@ -import os -import sys - -# Some Python versions distributed by Conda have a buggy `os.add_dll_directory` -# which prevents binary wheels from finding the FFmpeg DLLs in the `av.libs` -# directory. We work around this by adding `av.libs` to the PATH. -if ( - os.name == "nt" - and sys.version_info[:2] in ((3, 8), (3, 9)) - and os.path.exists(os.path.join(sys.base_prefix, "conda-meta")) -): - os.environ["PATH"] = ( - os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, "av.libs")) - + os.pathsep - + os.environ["PATH"] - ) - # MUST import the core before anything else in order to initialize the underlying # library that is being wrapped. from av._core import time_base, library_versions @@ -77,6 +60,8 @@ def get_include() -> str: """ Returns the path to the `include` folder to be used when building extensions to av. """ + import os + # Installed package include_path = os.path.join(os.path.dirname(__file__), "include") if os.path.exists(include_path): From f32427406a1f940a7c2096435a37ae23af9dd2ef Mon Sep 17 00:00:00 2001 From: Dexer <73297572+DexerBR@users.noreply.github.com> Date: Wed, 25 Sep 2024 12:19:31 -0300 Subject: [PATCH 121/365] fix missing staticmethod in VideoFrame.from_bytes --- av/video/frame.pyx | 1 + 1 file changed, 1 insertion(+) diff --git a/av/video/frame.pyx b/av/video/frame.pyx index ee060e16a..a7c794a55 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -591,6 +591,7 @@ cdef class VideoFrame(Frame): return frame + @staticmethod def from_bytes(img_bytes: bytes, width: int, height: int, format="rgba", flip_horizontal=False, flip_vertical=False): frame = VideoFrame(width, height, format) if format == "rgba": From 020a0ce693490b891cdd7062876888677dfd29a2 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 25 Sep 2024 16:23:15 -0400 Subject: [PATCH 122/365] Add type stubs for common.py funcs --- av/filter/filter.pyi | 2 ++ tests/common.py | 56 ++++++++++++++++++++++++++-------------- tests/test_filters.py | 15 ++++++----- tests/test_videoframe.py | 18 ++++++------- 4 files changed, 55 insertions(+), 36 deletions(-) diff --git a/av/filter/filter.pyi b/av/filter/filter.pyi index f94c79e1c..2751e973c 100644 --- a/av/filter/filter.pyi +++ b/av/filter/filter.pyi @@ -18,4 +18,6 @@ class Filter: inputs: tuple[FilterPad, ...] outputs: tuple[FilterPad, ...] + def __init__(self, name: str) -> None: ... + filters_available: set[str] diff --git a/tests/common.py b/tests/common.py index 33b307c80..c67db04e5 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1,8 +1,11 @@ +from __future__ import annotations + import datetime import errno import functools import os import types +from typing import TYPE_CHECKING from unittest import TestCase as _Base import numpy as np @@ -16,6 +19,14 @@ except ImportError: has_pillow = False +if TYPE_CHECKING: + from typing import Any, Callable, TypeVar + + from PIL.Image import Image + + T = TypeVar("T") + + __all__ = ("fate_suite",) @@ -23,7 +34,7 @@ skip_tests = frozenset(os.environ.get("PYAV_SKIP_TESTS", "").split(",")) -def makedirs(path: str) -> None: +def safe_makedirs(path: str) -> None: try: os.makedirs(path) except OSError as e: @@ -61,22 +72,20 @@ def fate_png() -> str: return fate_suite("png1/55c99e750a5fd6_50314226.png") -def sandboxed(*args, **kwargs) -> str: - do_makedirs = kwargs.pop("makedirs", True) - base = kwargs.pop("sandbox", None) - timed = kwargs.pop("timed", False) - if kwargs: - raise TypeError("extra kwargs: %s" % ", ".join(sorted(kwargs))) - path = os.path.join(_sandbox(timed=timed) if base is None else base, *args) - if do_makedirs: - makedirs(os.path.dirname(path)) +def sandboxed( + *args: str, makedirs: bool = True, sandbox: str | None = None, timed: bool = False +) -> str: + path = os.path.join(_sandbox(timed) if sandbox is None else sandbox, *args) + if makedirs: + safe_makedirs(os.path.dirname(path)) + return path # Decorator for running a test in the sandbox directory -def run_in_sandbox(func): +def run_in_sandbox(func: Callable[..., T]) -> Callable[..., T]: @functools.wraps(func) - def _inner(self, *args, **kwargs): + def _inner(self: Any, *args: Any, **kwargs: Any) -> T: current_dir = os.getcwd() try: os.chdir(self.sandbox) @@ -104,13 +113,13 @@ def assertNdarraysEqual(a: np.ndarray, b: np.ndarray) -> None: assert False, f"ndarrays contents differ\n{msg}" -def assertImagesAlmostEqual(a, b, epsilon=0.1): +def assertImagesAlmostEqual(a: Image, b: Image, epsilon: float = 0.1) -> None: import PIL.ImageFilter as ImageFilter assert a.size == b.size a = a.filter(ImageFilter.BLUR).getdata() b = b.filter(ImageFilter.BLUR).getdata() - for i, ax, bx in zip(range(len(a)), a, b): + for i, ax, bx in zip(range(len(a)), a, b): # type: ignore diff = sum(abs(ac / 256 - bc / 256) for ac, bc in zip(ax, bx)) / 3 assert diff < epsilon, f"images differed by {diff} at index {i}; {ax} {bx}" @@ -119,14 +128,23 @@ class TestCase(_Base): @classmethod def _sandbox(cls, timed: bool = True) -> str: path = os.path.join(_sandbox(timed=timed), cls.__name__) - makedirs(path) + safe_makedirs(path) return path @property def sandbox(self) -> str: return self._sandbox(timed=True) - def sandboxed(self, *args, **kwargs) -> str: - kwargs.setdefault("sandbox", self.sandbox) - kwargs.setdefault("timed", True) - return sandboxed(*args, **kwargs) + def sandboxed( + self, + *args: str, + makedirs: bool = True, + timed: bool = True, + sandbox: str | None = None, + ) -> str: + if sandbox is None: + return sandboxed( + *args, makedirs=makedirs, timed=timed, sandbox=self.sandbox + ) + else: + return sandboxed(*args, makedirs=makedirs, timed=timed, sandbox=sandbox) diff --git a/tests/test_filters.py b/tests/test_filters.py index a87d6b5b0..202da3714 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -34,11 +34,11 @@ def generate_audio_frame( return frame -def pull_until_blocked(graph): - frames = [] +def pull_until_blocked(graph: Graph) -> list[av.VideoFrame]: + frames: list[av.VideoFrame] = [] while True: try: - frames.append(graph.pull()) + frames.append(graph.vpull()) except av.AVError as e: if e.errno != errno.EAGAIN: raise @@ -46,7 +46,7 @@ def pull_until_blocked(graph): class TestFilters(TestCase): - def test_filter_descriptor(self): + def test_filter_descriptor(self) -> None: f = Filter("testsrc") assert f.name == "testsrc" assert f.description == "Generate test pattern." @@ -86,24 +86,25 @@ def test_generator_graph(self): if has_pillow: frame.to_image().save(self.sandboxed("mandelbrot2.png")) - def test_auto_find_sink(self): + def test_auto_find_sink(self) -> None: graph = Graph() src = graph.add("testsrc") src.link_to(graph.add("buffersink")) graph.configure() - frame = graph.pull() + frame = graph.vpull() if has_pillow: frame.to_image().save(self.sandboxed("mandelbrot3.png")) - def test_delegate_sink(self): + def test_delegate_sink(self) -> None: graph = Graph() src = graph.add("testsrc") src.link_to(graph.add("buffersink")) graph.configure() frame = src.pull() + assert isinstance(frame, av.VideoFrame) if has_pillow: frame.to_image().save(self.sandboxed("mandelbrot4.png")) diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 8dcdb838a..9b77af916 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -43,10 +43,8 @@ def test_opaque() -> None: assert type(frame.opaque) is tuple and len(frame.opaque) == 2 -def test_invalid_pixel_format(): - with pytest.raises( - ValueError, match="not a pixel format: '__unknown_pix_fmt'" - ) as cm: +def test_invalid_pixel_format() -> None: + with pytest.raises(ValueError, match="not a pixel format: '__unknown_pix_fmt'"): VideoFrame(640, 480, "__unknown_pix_fmt") @@ -90,7 +88,7 @@ def test_yuv420p_planes() -> None: assert frame.planes[i].buffer_size == 320 * 240 -def test_yuv420p_planes_align(): +def test_yuv420p_planes_align() -> None: # If we request 8-byte alignment for a width which is not a multiple of 8, # the line sizes are larger than the plane width. frame = VideoFrame(318, 238, "yuv420p") @@ -106,7 +104,7 @@ def test_yuv420p_planes_align(): assert frame.planes[i].buffer_size == 160 * 119 -def test_rgb24_planes(): +def test_rgb24_planes() -> None: frame = VideoFrame(640, 480, "rgb24") assert len(frame.planes) == 1 assert frame.planes[0].width == 640 @@ -115,7 +113,7 @@ def test_rgb24_planes(): assert frame.planes[0].buffer_size == 640 * 480 * 3 -def test_memoryview_read(): +def test_memoryview_read() -> None: frame = VideoFrame(640, 480, "rgb24") frame.planes[0].update(b"01234" + (b"x" * (640 * 480 * 3 - 5))) mem = memoryview(frame.planes[0]) @@ -129,11 +127,11 @@ def test_memoryview_read(): class TestVideoFrameImage(TestCase): - def setUp(self): + def setUp(self) -> None: if not has_pillow: pytest.skip() - def test_roundtrip(self): + def test_roundtrip(self) -> None: import PIL.Image as Image image = Image.open(fate_png()) @@ -142,7 +140,7 @@ def test_roundtrip(self): img.save(self.sandboxed("roundtrip-high.jpg")) assertImagesAlmostEqual(image, img) - def test_to_image_rgb24(self): + def test_to_image_rgb24(self) -> None: sizes = [(318, 238), (320, 240), (500, 500)] for width, height in sizes: frame = VideoFrame(width, height, format="rgb24") From 39fecbd85781e8b3dcdc8fa878bb83376611e690 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 2 Oct 2024 12:34:10 -0400 Subject: [PATCH 123/365] Use ffmpeg 7.1 --- .github/workflows/smoke.yml | 10 ++++++---- scripts/ffmpeg-7.1.json | 3 +++ 2 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 scripts/ffmpeg-7.1.json diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index f5fabe342..0ef3f650f 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -36,10 +36,11 @@ jobs: config: - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.1.1", extras: true} - {os: ubuntu-latest, python: "3.9", ffmpeg: "7.0.2"} - - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.0"} + - {os: ubuntu-latest, python: "3.9", ffmpeg: "7.1"} - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1.1"} - - {os: macos-12, python: "3.9", ffmpeg: "7.0.2"} - - {os: macos-12, python: "3.9", ffmpeg: "6.1.1"} + - {os: macos-13, python: "3.9", ffmpeg: "7.1"} + - {os: macos-13, python: "3.9", ffmpeg: "7.0.2"} + - {os: macos-13, python: "3.9", ffmpeg: "6.1.1"} env: PYAV_PYTHON: python${{ matrix.config.python }} @@ -65,7 +66,7 @@ jobs: sudo apt-get install doxygen wget fi ;; - macos-12) + macos-13) brew install automake libtool nasm pkg-config libpng libvorbis libvpx opus x264 ;; esac @@ -112,6 +113,7 @@ jobs: fail-fast: false matrix: config: + - {os: windows-latest, python: "3.9", ffmpeg: "7.1"} - {os: windows-latest, python: "3.9", ffmpeg: "7.0"} - {os: windows-latest, python: "3.9", ffmpeg: "6.1"} - {os: windows-latest, python: "3.9", ffmpeg: "6.0"} diff --git a/scripts/ffmpeg-7.1.json b/scripts/ffmpeg-7.1.json new file mode 100644 index 000000000..06e80636a --- /dev/null +++ b/scripts/ffmpeg-7.1.json @@ -0,0 +1,3 @@ +{ + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.1-1/ffmpeg-{platform}.tar.gz" +} \ No newline at end of file From e3598c2444c6912c5ff1bb7b0f4f0f3af7dac2ad Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 3 Oct 2024 02:47:30 -0400 Subject: [PATCH 124/365] Use pytest --- .github/workflows/tests.yml | 4 ++-- tests/test_videoframe.py | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 27e394948..924642e0f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -69,8 +69,8 @@ jobs: CIBW_ENVIRONMENT_WINDOWS: INCLUDE=C:\\cibw\\vendor\\include LIB=C:\\cibw\\vendor\\lib PYAV_SKIP_TESTS=unicode_filename CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: delvewheel repair --add-path C:\cibw\vendor\bin -w {dest_dir} {wheel} CIBW_SKIP: "*-musllinux*" - CIBW_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m unittest discover -t {project} -s tests && mv {project}/av.disabled {project}/av - CIBW_TEST_REQUIRES: numpy + CIBW_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m pytest {package}/tests && mv {project}/av.disabled {project}/av + CIBW_TEST_REQUIRES: pytest numpy # skip tests when there are no binary wheels of numpy CIBW_TEST_SKIP: pp* *_i686 run: | diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 9b77af916..8e14dcdf3 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -174,6 +174,9 @@ def test_basic_to_ndarray() -> None: def test_to_image_with_dimensions() -> None: + if not has_pillow: + pytest.skip() + img = VideoFrame(640, 480, format="rgb24").to_image(width=320, height=240) assert img.size == (320, 240) From 340ed1c93c579985c5030f863a7d57ad45b28d9a Mon Sep 17 00:00:00 2001 From: Mark Harfouche Date: Fri, 4 Oct 2024 13:55:01 -0400 Subject: [PATCH 125/365] Add ffmpeg version info --- av/__init__.py | 3 ++- av/_core.pyi | 1 + av/_core.pyx | 6 ++++++ include/libavutil/avutil.pxd | 1 + scripts/test | 2 ++ 5 files changed, 12 insertions(+), 1 deletion(-) diff --git a/av/__init__.py b/av/__init__.py index 8752c26fd..9211d7634 100644 --- a/av/__init__.py +++ b/av/__init__.py @@ -1,6 +1,6 @@ # MUST import the core before anything else in order to initialize the underlying # library that is being wrapped. -from av._core import time_base, library_versions +from av._core import time_base, library_versions, ffmpeg_version_info # Capture logging (by importing it). from av import logging @@ -32,6 +32,7 @@ __all__ = ( "__version__", "time_base", + "ffmpeg_version_info", "library_versions", "AudioCodecContext", "AudioFifo", diff --git a/av/_core.pyi b/av/_core.pyi index 26b3bc0c8..0ee0a5626 100644 --- a/av/_core.pyi +++ b/av/_core.pyi @@ -7,5 +7,6 @@ class _Meta(TypedDict): library_meta: dict[str, _Meta] library_versions: dict[str, tuple[int, int, int]] +ffmpeg_version_info: str time_base: int diff --git a/av/_core.pyx b/av/_core.pyx index 4100c5857..edca772f5 100644 --- a/av/_core.pyx +++ b/av/_core.pyx @@ -18,6 +18,12 @@ cdef decode_version(v): return (major, minor, micro) +# Return an informative version string. +# This usually is the actual release version number or a git commit +# description. This string has no fixed format and can change any time. It +# should never be parsed by code. +ffmpeg_version_info = lib.av_version_info() + library_meta = { "libavutil": dict( version=decode_version(lib.avutil_version()), diff --git a/include/libavutil/avutil.pxd b/include/libavutil/avutil.pxd index f874761ea..58dd43922 100644 --- a/include/libavutil/avutil.pxd +++ b/include/libavutil/avutil.pxd @@ -12,6 +12,7 @@ cdef extern from "libavutil/rational.h" nogil: cdef extern from "libavutil/avutil.h" nogil: + cdef const char* av_version_info() cdef int avutil_version() cdef char* avutil_configuration() cdef char* avutil_license() diff --git a/scripts/test b/scripts/test index 270a4ec69..8244778b3 100755 --- a/scripts/test +++ b/scripts/test @@ -18,6 +18,8 @@ istest() { return $? } +$PYAV_PYTHON -c "import av; print(f'PyAV: {av.__version__}'); print(f'FFMPEG: {av.ffmpeg_version_info}')" + if istest main; then $PYAV_PYTHON -m pytest fi From 516e07fcdc9145f39ef123d6fe9a0284d3a1df42 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 5 Oct 2024 20:15:51 -0400 Subject: [PATCH 126/365] Use ffmpeg 7.1 as build default --- scripts/activate.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/activate.sh b/scripts/activate.sh index b7bdf7a6f..72bd44738 100755 --- a/scripts/activate.sh +++ b/scripts/activate.sh @@ -17,7 +17,7 @@ if [[ ! "$PYAV_LIBRARY" ]]; then return 1 fi else - PYAV_LIBRARY=ffmpeg-7.0.2 + PYAV_LIBRARY=ffmpeg-7.1 echo "No \$PYAV_LIBRARY set; defaulting to $PYAV_LIBRARY" fi fi From ab8eefa947ecacceb769de315436992a775d9752 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 5 Oct 2024 20:48:15 -0400 Subject: [PATCH 127/365] Use proper roles in changelog --- AUTHORS.py | 103 ++++++++++++++++++++++++--------------------- AUTHORS.rst | 9 ++-- CHANGELOG.rst | 81 +++++++++++++++++------------------ docs/api/codec.rst | 1 - 4 files changed, 102 insertions(+), 92 deletions(-) diff --git a/AUTHORS.py b/AUTHORS.py index fd47a713d..f17b31aeb 100644 --- a/AUTHORS.py +++ b/AUTHORS.py @@ -1,69 +1,68 @@ import math import subprocess - -print('''Contributors +print( + """Contributors ============ All contributors (by number of commits): -''') +""" +) email_map = { - # Maintainers. - 'git@mikeboers.com': 'github@mikeboers.com', - 'mboers@keypics.com': 'github@mikeboers.com', - 'mikeb@loftysky.com': 'github@mikeboers.com', - 'mikeb@markmedia.co': 'github@mikeboers.com', - 'westernx@mikeboers.com': 'github@mikeboers.com', - + "git@mikeboers.com": "github@mikeboers.com", + "mboers@keypics.com": "github@mikeboers.com", + "mikeb@loftysky.com": "github@mikeboers.com", + "mikeb@markmedia.co": "github@mikeboers.com", + "westernx@mikeboers.com": "github@mikeboers.com", # Junk. - 'mark@mark-VirtualBox.(none)': None, - + "mark@mark-VirtualBox.(none)": None, # Aliases. - 'a.davoudi@aut.ac.ir': 'davoudialireza@gmail.com', - 'tcaswell@bnl.gov': 'tcaswell@gmail.com', - 'xxr3376@gmail.com': 'xxr@megvii.com', - 'dallan@pha.jhu.edu': 'daniel.b.allan@gmail.com', - '61652821+laggykiller@users.noreply.github.com': 'chaudominic2@gmail.com', - + "a.davoudi@aut.ac.ir": "davoudialireza@gmail.com", + "tcaswell@bnl.gov": "tcaswell@gmail.com", + "xxr3376@gmail.com": "xxr@megvii.com", + "dallan@pha.jhu.edu": "daniel.b.allan@gmail.com", + "61652821+laggykiller@users.noreply.github.com": "chaudominic2@gmail.com", } name_map = { - 'caspervdw@gmail.com': 'Casper van der Wel', - 'daniel.b.allan@gmail.com': 'Dan Allan', - 'mgoacolou@cls.fr': 'Manuel Goacolou', - 'mindmark@gmail.com': 'Mark Reid', - 'moritzkassner@gmail.com': 'Moritz Kassner', - 'vidartf@gmail.com': 'Vidar Tonaas Fauske', - 'xxr@megvii.com': 'Xinran Xu', + "caspervdw@gmail.com": "Casper van der Wel", + "daniel.b.allan@gmail.com": "Dan Allan", + "mgoacolou@cls.fr": "Manuel Goacolou", + "mindmark@gmail.com": "Mark Reid", + "moritzkassner@gmail.com": "Moritz Kassner", + "vidartf@gmail.com": "Vidar Tonaas Fauske", + "xxr@megvii.com": "Xinran Xu", } github_map = { - 'billy.shambrook@gmail.com': 'billyshambrook', - 'daniel.b.allan@gmail.com': 'danielballan', - 'davoudialireza@gmail.com': 'adavoudi', - 'github@mikeboers.com': 'mikeboers', - 'jeremy.laine@m4x.org': 'jlaine', - 'kalle.litterfeldt@gmail.com': 'litterfeldt', - 'mindmark@gmail.com': 'markreidvfx', - 'moritzkassner@gmail.com': 'mkassner', - 'rush@logic.cz': 'radek-senfeld', - 'self@brendanlong.com': 'brendanlong', - 'tcaswell@gmail.com': 'tacaswell', - 'ulrik.mikaelsson@magine.com': 'rawler', - 'vidartf@gmail.com': 'vidartf', - 'willpatera@gmail.com': 'willpatera', - 'xxr@megvii.com': 'xxr3376', - 'chaudominic2@gmail.com': 'laggykiller', - 'wyattblue@auto-editor.com': 'WyattBlue', + "billy.shambrook@gmail.com": "billyshambrook", + "daniel.b.allan@gmail.com": "danielballan", + "davoudialireza@gmail.com": "adavoudi", + "github@mikeboers.com": "mikeboers", + "jeremy.laine@m4x.org": "jlaine", + "kalle.litterfeldt@gmail.com": "litterfeldt", + "mindmark@gmail.com": "markreidvfx", + "moritzkassner@gmail.com": "mkassner", + "rush@logic.cz": "radek-senfeld", + "self@brendanlong.com": "brendanlong", + "tcaswell@gmail.com": "tacaswell", + "ulrik.mikaelsson@magine.com": "rawler", + "vidartf@gmail.com": "vidartf", + "willpatera@gmail.com": "willpatera", + "xxr@megvii.com": "xxr3376", + "chaudominic2@gmail.com": "laggykiller", + "wyattblue@auto-editor.com": "WyattBlue", } email_count = {} -for line in subprocess.check_output(['git', 'log', '--format=%aN,%aE']).decode().splitlines(): - name, email = line.strip().rsplit(',', 1) +for line in ( + subprocess.check_output(["git", "log", "--format=%aN,%aE"]).decode().splitlines() +): + name, email = line.strip().rsplit(",", 1) email = email_map.get(email, email) if not email: @@ -89,7 +88,7 @@ names = name_map[email] if isinstance(names, set): - name = ', '.join(sorted(names)) + name = ", ".join(sorted(names)) else: name = names @@ -98,6 +97,16 @@ # The '-' vs '*' is so that Sphinx treats them as different lists, and # introduces a gap bettween them. if github: - print('%s %s <%s>; `@%s `_' % ('-*'[block_i % 2], name, email, github, github)) + print( + "%s %s <%s>; `@%s `_" + % ("-*"[block_i % 2], name, email, github, github) + ) else: - print('%s %s <%s>' % ('-*'[block_i % 2], name, email, )) + print( + "%s %s <%s>" + % ( + "-*"[block_i % 2], + name, + email, + ) + ) diff --git a/AUTHORS.rst b/AUTHORS.rst index bf3a6f49d..bc4db2a5b 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -6,8 +6,8 @@ All contributors (by number of commits): - Mike Boers ; `@mikeboers `_ * Jeremy Lainé ; `@jlaine `_ +* WyattBlue ; `@WyattBlue `_ -- WyattBlue ; `@WyattBlue `_ - Mark Reid ; `@markreidvfx `_ * Vidar Tonaas Fauske ; `@vidartf `_ @@ -19,11 +19,11 @@ All contributors (by number of commits): * JoeUgly <41972063+JoeUgly@users.noreply.github.com> * Justin Wong <46082645+uvjustin@users.noreply.github.com> -- Alba Mendez - Mark Harfouche +- Alba Mendez +- Dave Johansen - Xinran Xu ; `@xxr3376 `_ - Dan Allan ; `@danielballan `_ -- Dave Johansen - Moonsik Park - Santtu Keskinen - Christoph Rackwitz @@ -35,6 +35,7 @@ All contributors (by number of commits): - Wel C. van der - Will Patera ; `@willpatera `_ +* Dexer <73297572+DexerBR@users.noreply.github.com> * rutsh * Felix Vollmer * Santiago Castro @@ -55,11 +56,11 @@ All contributors (by number of commits): * zzjjbb <31069326+zzjjbb@users.noreply.github.com> * Hanz <40712686+HanzCEO@users.noreply.github.com> * Joe Schiff <41972063+JoeSchiff@users.noreply.github.com> -* Dexer <73297572+DexerBR@users.noreply.github.com> * Artturin * Ian Lee * Ryan Huang * Arthur Barros +* Carlos Ruiz * Carlos Ruiz * David Plowman * Maxime Desroches diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d43d3ddaa..941c4dabc 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -21,16 +21,17 @@ v13.1.0 Features: -- Allow passing Python objects around using `Frame.opaque` and `Packet.opaque`. -- Allow extradata to be set by encoders by @daveisfera in (:issue:`1526`). +- Allow passing Python objects around using `Frame.opaque` and `Packet.opaque` by :gh-user:`CarlosRDomin` and :gh-user:`WyattBlue` in (:pr:`1533`). +- Allow extradata to be set by encoders by :gh-user:`daveisfera` in (:pr:`1526`). +- Add getting ffmpeg version info string by :gh-user:`hmaarrfk` in (:pr:`1564`). Fixes: -- Remove the `deprecation` module in anticipation of `PEP 702 `_ +- Remove the `deprecation` module in anticipation of `PEP 702 `_. - Add type stubs to previously unannotated API sections. - Improve type stubs for both `mypy` and `mypy --strict`. - Permit only setting `time_base` with a Fraction, as mypy is unable to respect different types in getters vs setters. -- Declare `write_packet` function as const by @hmaarrfk in (:issue:`1517`). +- Declare `write_packet` function as const by :gh-user:`hmaarrfk` in (:pr:`1517`). v13.0.0 ------- @@ -62,20 +63,20 @@ v12.3.0 Features: -- Support libav's `av_log_set_level` by @materight in (:issue:`1448`). -- Add Graph.link_nodes by @WyattBlue in (:issue:`1449`). -- Add default codec properties by @WyattBlue in (:issue:`1450`). -- Remove the xvid and ass packages in ffmpeg binaries because they were unused by @WyattBlue in (:issue:`1462`). -- Add supported_codecs property to OutputContainer by @WyattBlue in (:issue:`1465`). -- Add text and dialogue property to AssSubtitle, remove TextSubtitle by @WyattBlue in (:issue:`1456`). +- Support libav's `av_log_set_level` by :gh-user:`materight` in (:pr:`1448`). +- Add Graph.link_nodes by :gh-user:`WyattBlue` in (:pr:`1449`). +- Add default codec properties by :gh-user:`WyattBlue` in (:pr:`1450`). +- Remove the xvid and ass packages in ffmpeg binaries because they were unused by :gh-user:`WyattBlue` in (:pr:`1462`). +- Add supported_codecs property to OutputContainer by :gh-user:`WyattBlue` in (:pr:`1465`). +- Add text and dialogue property to AssSubtitle, remove TextSubtitle by :gh-user:`WyattBlue` in (:pr:`1456`). Fixes: -- Include libav headers in final distribution by @materight in (:issue:`1455`). -- Fix segfault when calling subtitle_stream.decode() by @WyattBlue in (:issue:`1460`). -- flushing subtitle decoder requires a new uninitialized packet by @moonsikpark in (:issue:`1461`). -- Set default color range for VideoReformatter.format() by @elxy in (:issue:`1458`). -- Resampler: format, layout accepts `str` `int` too by @WyattBlue in (:issue:`1446`). +- Include libav headers in final distribution by :gh-user:`materight` in (:pr:`1455`). +- Fix segfault when calling subtitle_stream.decode() by :gh-user:`WyattBlue` in (:pr:`1460`). +- flushing subtitle decoder requires a new uninitialized packet by :gh-user:`moonsikpark` in (:pr:`1461`). +- Set default color range for VideoReformatter.format() by :gh-user:`elxy` in (:pr:`1458`). +- Resampler: format, layout accepts `str` `int` too by :gh-user:`WyattBlue` in (:pr:`1446`). v12.2.0 ------- @@ -98,19 +99,19 @@ Features: - Build binary wheels with webp support. - Allow disabling logs, disable logs by default. -- Add bitstream filters by @skeskinen in (:issue:`1375`) (:issue:`1379`). -- Expose CodecContext flush_buffers by @skeskinen in (:issue:`1382`). +- Add bitstream filters by :gh-user:`skeskinen` in (:pr:`1379` :issue:`1375`). +- Expose CodecContext flush_buffers by :gh-user:`skeskinen` in (:pr:`1382`). Fixes: - Fix type stubs, add missing type stubs. -- Add S12M_TIMECODE by @WyattBlue in (:issue:`1381`). -- Subtitle.text now returns bytes by @WyattBlue in (:issue:`1398`). -- Allow packet.duration to be writable by @WyattBlue in (:issue:`1399`). -- Remove deprecated `VideoStream.frame_rate` by @WyattBlue in (:issue:`1351`). -- Build with Arm for PyPy now by @WyattBlue in (:issue:`1395`). -- Fix #1378 by @WyattBlue in (:issue:`1400`). -- setup.py: use PKG_CONFIG env var to get the pkg-config to use by @Artturin in (:issue:`1387`). +- Add S12M_TIMECODE by :gh-user:`WyattBlue` in (:pr:`1381`). +- Subtitle.text now returns bytes by :gh-user:`WyattBlue` in (:pr:`1398`). +- Allow packet.duration to be writable by :gh-user:`WyattBlue` in (:pr:`1399`). +- Remove deprecated `VideoStream.frame_rate` by :gh-user:`WyattBlue` in (:pr:`1351`). +- Build with Arm for PyPy now by :gh-user:`WyattBlue` in (:pr:`1395`). +- Fix #1378 by :gh-user:`WyattBlue` in (:pr:`1400`). +- setup.py: use PKG_CONFIG env var to get the pkg-config to use by :gh-user:`Artturin` in (:pr:`1387`). v12.0.0 ------- @@ -120,28 +121,28 @@ Major: - Add type hints. - Update FFmpeg to 6.1.1 for the binary wheels. - Update libraries for the binary wheels (notably dav1d to 1.4.1). -- Deprecate VideoCodecContext.gop_size for decoders by @JoeSchiff in (:issue:`1256`). -- Deprecate frame.index by @JoeSchiff in (:issue:`1218`). +- Deprecate VideoCodecContext.gop_size for decoders by :gh-user:`JoeSchiff` in (:pr:`1256`). +- Deprecate frame.index by :gh-user:`JoeSchiff` in (:pr:`1218`). Features: -- Allow using pathlib.Path for av.open by @WyattBlue in (:issue:`1231`). -- Add `max_b_frames` property to CodecContext by @davidplowman in (:issue:`1119`). -- Add `encode_lazy` method to CodecContext by @rawler in (:issue:`1092`). -- Add `color_range` to CodecContext/Frame by @johanjeppsson in (:issue:`686`). -- Set `time_base` for AudioResampler by @daveisfera in (:issue:`1209`). -- Add support for ffmpeg's AVCodecContext::delay by @JoeSchiff in (:issue:`1279`). -- Add `color_primaries`, `color_trc`, `colorspace` to VideoStream by @WyattBlue in (:issue:`1304`). -- Add `bits_per_coded_sample` to VideoCodecContext by @rvanlaar in (:issue:`1203`). -- AssSubtitle.ass now returns as bytes by @WyattBlue in (:issue:`1333`). -- Expose DISPLAYMATRIX side data by @hyenal in (:issue:`1249`). +- Allow using pathlib.Path for av.open by :gh-user:`WyattBlue` in (:pr:`1231`). +- Add `max_b_frames` property to CodecContext by :gh-user:`davidplowman` in (:pr:`1119`). +- Add `encode_lazy` method to CodecContext by :gh-user:`rawler` in (:pr:`1092`). +- Add `color_range` to CodecContext/Frame by :gh-user:`johanjeppsson` in (:pr:`686`). +- Set `time_base` for AudioResampler by :gh-user:`daveisfera` in (:issue:`1209`). +- Add support for ffmpeg's AVCodecContext::delay by :gh-user:`JoeSchiff` in (:issue:`1279`). +- Add `color_primaries`, `color_trc`, `colorspace` to VideoStream by :gh-user:`WyattBlue` in (:pr:`1304`). +- Add `bits_per_coded_sample` to VideoCodecContext by :gh-user:`rvanlaar` in (:pr:`1203`). +- AssSubtitle.ass now returns as bytes by :gh-user:`WyattBlue` in (:pr:`1333`). +- Expose DISPLAYMATRIX side data by :gh-user:`hyenal` in (:pr:`1249`). Fixes: -- Convert deprecated Cython extension class properties to decorator syntax by @JoeSchiff -- Check None packet when setting time_base after decode by @philipnbbc in (:issue:`1281`). -- Remove deprecated `Buffer.to_bytes` by @WyattBlue in (:issue:`1286`). -- Remove deprecated `Packet.decode_one` by @WyattBlue in (:issue:`1301`). +- Convert deprecated Cython extension class properties to decorator syntax by :gh-user:`JoeSchiff`. +- Check None packet when setting time_base after decode by :gh-user:`philipnbbc` in (:pr:`1281`). +- Remove deprecated `Buffer.to_bytes` by :gh-user:`WyattBlue` in (:pr:`1286`). +- Remove deprecated `Packet.decode_one` by :gh-user:`WyattBlue` in (:pr:`1301`). v11.0.0 ------- diff --git a/docs/api/codec.rst b/docs/api/codec.rst index a3cc90e86..bd1a6b5f0 100644 --- a/docs/api/codec.rst +++ b/docs/api/codec.rst @@ -79,7 +79,6 @@ Attributes .. autoattribute:: CodecContext.profile .. autoattribute:: CodecContext.time_base -.. autoattribute:: CodecContext.ticks_per_frame .. autoattribute:: CodecContext.bit_rate .. autoattribute:: CodecContext.bit_rate_tolerance From 257f3846391af1b082bd989fc19395f4c7e7f2a1 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 5 Oct 2024 20:56:43 -0400 Subject: [PATCH 128/365] Release 13.1.0 --- av/about.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/about.py b/av/about.py index f0a0fa039..d876dfbee 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "13.1.0rc1" +__version__ = "13.1.0" From 86f66ac1c6682c5c07aeb8df4e070b0fe3723fb7 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 6 Oct 2024 00:47:26 -0400 Subject: [PATCH 129/365] Build wheels with ffmpeg 7.1 --- .github/workflows/tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 924642e0f..b51477083 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,7 +14,7 @@ jobs: - name: Build source package run: | pip install cython - python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.0.json /tmp/vendor + python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.1.json /tmp/vendor PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig python setup.py sdist - name: Upload source package uses: actions/upload-artifact@v4 @@ -61,9 +61,9 @@ jobs: env: CIBW_ARCHS: ${{ matrix.arch }} CIBW_BEFORE_ALL_LINUX: yum install -y alsa-lib libxcb - CIBW_BEFORE_BUILD: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.0.json /tmp/vendor - CIBW_BEFORE_BUILD_MACOS: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.0.json /tmp/vendor - CIBW_BEFORE_BUILD_WINDOWS: python scripts\fetch-vendor.py --config-file scripts\ffmpeg-7.0.json C:\cibw\vendor + CIBW_BEFORE_BUILD: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.1.json /tmp/vendor + CIBW_BEFORE_BUILD_MACOS: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.1.json /tmp/vendor + CIBW_BEFORE_BUILD_WINDOWS: python scripts\fetch-vendor.py --config-file scripts\ffmpeg-7.1.json C:\cibw\vendor CIBW_ENVIRONMENT_LINUX: LD_LIBRARY_PATH=/tmp/vendor/lib:$LD_LIBRARY_PATH PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig CIBW_ENVIRONMENT_MACOS: PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig LDFLAGS=-headerpad_max_install_names CIBW_ENVIRONMENT_WINDOWS: INCLUDE=C:\\cibw\\vendor\\include LIB=C:\\cibw\\vendor\\lib PYAV_SKIP_TESTS=unicode_filename From 80f3beb680785904c0f8eb9c162db1dda6da63b1 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 10 Oct 2024 16:01:10 -0400 Subject: [PATCH 130/365] Add remux subtitles example --- docs/cookbook/audio.rst | 8 ++++++ docs/cookbook/subtitles.rst | 10 ++++++++ examples/{audio_atempo.py => audio/atempo.py} | 4 --- examples/basics/remux.py | 2 ++ examples/basics/save_keyframes.py | 9 ++----- examples/subtitles/remux.py | 25 +++++++++++++++++++ 6 files changed, 47 insertions(+), 11 deletions(-) create mode 100644 docs/cookbook/audio.rst create mode 100644 docs/cookbook/subtitles.rst rename examples/{audio_atempo.py => audio/atempo.py} (91%) create mode 100644 examples/subtitles/remux.py diff --git a/docs/cookbook/audio.rst b/docs/cookbook/audio.rst new file mode 100644 index 000000000..39ccc62b8 --- /dev/null +++ b/docs/cookbook/audio.rst @@ -0,0 +1,8 @@ +Audio +===== + + +Filters +------- + +.. literalinclude:: ../../examples/audio/atempo.py diff --git a/docs/cookbook/subtitles.rst b/docs/cookbook/subtitles.rst new file mode 100644 index 000000000..e3ca79d5d --- /dev/null +++ b/docs/cookbook/subtitles.rst @@ -0,0 +1,10 @@ +Subtitles +========= + + +Remuxing +-------- + +Remuxing is copying stream(s) from one container to the other without transcoding it. By doing so, the data does not suffer any generational loss, and is the full quality that it was in the source container. + +.. literalinclude:: ../../examples/subtitles/remux.py diff --git a/examples/audio_atempo.py b/examples/audio/atempo.py similarity index 91% rename from examples/audio_atempo.py rename to examples/audio/atempo.py index 62a126705..9123a7d9c 100644 --- a/examples/audio_atempo.py +++ b/examples/audio/atempo.py @@ -6,12 +6,9 @@ output_file = av.open("output.wav", mode="w") input_stream = input_file.streams.audio[0] - output_stream = output_file.add_stream("pcm_s16le", rate=input_stream.rate) -assert isinstance(output_stream, av.audio.AudioStream) graph = av.filter.Graph() - graph.link_nodes( graph.add_abuffer(template=input_stream), graph.add("atempo", "2.0"), @@ -31,6 +28,5 @@ for packet in output_stream.encode(None): output_file.mux(packet) -# Close the files input_file.close() output_file.close() diff --git a/examples/basics/remux.py b/examples/basics/remux.py index befc24f8a..5abb199b8 100644 --- a/examples/basics/remux.py +++ b/examples/basics/remux.py @@ -1,6 +1,8 @@ import av import av.datasets +av.logging.set_level(av.logging.VERBOSE) + input_ = av.open(av.datasets.curated("pexels/time-lapse-video-of-night-sky-857195.mp4")) output = av.open("remuxed.mkv", "w") diff --git a/examples/basics/save_keyframes.py b/examples/basics/save_keyframes.py index bc47376cd..6a68de73b 100644 --- a/examples/basics/save_keyframes.py +++ b/examples/basics/save_keyframes.py @@ -7,11 +7,6 @@ stream = container.streams.video[0] stream.codec_context.skip_frame = "NONKEY" - for frame in container.decode(stream): + for i, frame in enumerate(container.decode(stream)): print(frame) - - # We use `frame.pts` as `frame.index` won't make must sense with the `skip_frame`. - frame.to_image().save( - "night-sky.{:04d}.jpg".format(frame.pts), - quality=80, - ) + frame.to_image().save(f"night-sky.{i:04d}.jpg", quality=80) diff --git a/examples/subtitles/remux.py b/examples/subtitles/remux.py new file mode 100644 index 000000000..5b3031788 --- /dev/null +++ b/examples/subtitles/remux.py @@ -0,0 +1,25 @@ +import av + +av.logging.set_level(av.logging.VERBOSE) + +input_ = av.open("resources/webvtt.mkv") +output = av.open("remuxed.vtt", "w") + +in_stream = input_.streams.subtitles[0] +out_stream = output.add_stream(template=in_stream) + +for packet in input_.demux(in_stream): + if packet.dts is None: + continue + packet.stream = out_stream + output.mux(packet) + +input_.close() +output.close() + +print("Remuxing done") + +with av.open("remuxed.vtt") as f: + for subset in f.decode(subtitles=0): + for sub in subset: + print(sub.ass) From ae3cb97027fff155e1d51caeb9990cae59e0bc67 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 10 Oct 2024 16:41:50 -0400 Subject: [PATCH 131/365] PyAV 14 preliminary changes * Remove `AVError` alias, use `FFmpegError` directly * Drop ffmpeg 6 --- .github/workflows/smoke.yml | 16 ++++++---------- CHANGELOG.rst | 8 ++++++++ av/__init__.py | 3 --- av/about.py | 2 +- av/audio/frame.pyx | 1 - av/audio/resampler.pyx | 2 +- av/utils.pyx | 1 - docs/api/utils.rst | 2 -- include/libavcodec/avcodec.pxd | 1 - scripts/ffmpeg-6.0.json | 3 --- scripts/ffmpeg-6.1.json | 3 --- tests/test_filters.py | 2 +- 12 files changed, 17 insertions(+), 27 deletions(-) delete mode 100644 scripts/ffmpeg-6.0.json delete mode 100644 scripts/ffmpeg-6.1.json diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 0ef3f650f..c40464af5 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -34,13 +34,11 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-latest, python: "3.9", ffmpeg: "6.1.1", extras: true} + - {os: ubuntu-latest, python: "3.9", ffmpeg: "7.1", extras: true} - {os: ubuntu-latest, python: "3.9", ffmpeg: "7.0.2"} - - {os: ubuntu-latest, python: "3.9", ffmpeg: "7.1"} - - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "6.1.1"} - - {os: macos-13, python: "3.9", ffmpeg: "7.1"} - - {os: macos-13, python: "3.9", ffmpeg: "7.0.2"} - - {os: macos-13, python: "3.9", ffmpeg: "6.1.1"} + - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "7.1"} + - {os: macos-14, python: "3.9", ffmpeg: "7.1"} + - {os: macos-14, python: "3.9", ffmpeg: "7.0.2"} env: PYAV_PYTHON: python${{ matrix.config.python }} @@ -61,12 +59,12 @@ jobs: ubuntu-latest) sudo apt-get update sudo apt-get install autoconf automake build-essential cmake \ - libtool pkg-config yasm zlib1g-dev libvorbis-dev libx264-dev + libtool pkg-config nasm zlib1g-dev libvorbis-dev libx264-dev if [[ "${{ matrix.config.extras }}" ]]; then sudo apt-get install doxygen wget fi ;; - macos-13) + macos-14) brew install automake libtool nasm pkg-config libpng libvorbis libvpx opus x264 ;; esac @@ -115,8 +113,6 @@ jobs: config: - {os: windows-latest, python: "3.9", ffmpeg: "7.1"} - {os: windows-latest, python: "3.9", ffmpeg: "7.0"} - - {os: windows-latest, python: "3.9", ffmpeg: "6.1"} - - {os: windows-latest, python: "3.9", ffmpeg: "6.0"} steps: - name: Checkout diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 941c4dabc..5becaf5f6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,14 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. +v14.0.0 +------- + +Major: + +- Drop FFmpeg 6. +- Remove ``AVError`` alias (use ``FFmpegError`` directly instead). + v13.1.0 ------- diff --git a/av/__init__.py b/av/__init__.py index 9211d7634..cbc3c8a2f 100644 --- a/av/__init__.py +++ b/av/__init__.py @@ -26,9 +26,6 @@ from av.video.frame import VideoFrame from av.video.stream import VideoStream -# Backwards compatibility -AVError = FFmpegError # noqa: F405 - __all__ = ( "__version__", "time_base", diff --git a/av/about.py b/av/about.py index d876dfbee..a3d32212b 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "13.1.0" +__version__ = "14.0.0rc1" diff --git a/av/audio/frame.pyx b/av/audio/frame.pyx index 8260ccd2a..1e45842ea 100644 --- a/av/audio/frame.pyx +++ b/av/audio/frame.pyx @@ -47,7 +47,6 @@ cdef class AudioFrame(Frame): self.ptr.nb_samples = nb_samples self.ptr.format = format self.ptr.ch_layout = layout - # [FFMPEG6] self.ptr.channels = layout.nb_channels # ffmpeg 6 only # Sometimes this is called twice. Oh well. self._init_user_attributes() diff --git a/av/audio/resampler.pyx b/av/audio/resampler.pyx index 5c6020762..c46ed8475 100644 --- a/av/audio/resampler.pyx +++ b/av/audio/resampler.pyx @@ -111,7 +111,7 @@ cdef class AudioResampler: output.append(self.graph.pull()) except EOFError: break - except av.AVError as e: + except av.FFmpegError as e: if e.errno != errno.EAGAIN: raise break diff --git a/av/utils.pyx b/av/utils.pyx index afc82d24d..23f2d10b9 100644 --- a/av/utils.pyx +++ b/av/utils.pyx @@ -83,5 +83,4 @@ cdef flag_in_bitfield(uint64_t bitfield, uint64_t flag): # === BACKWARDS COMPAT === -from .error import FFmpegError as AVError from .error import err_check diff --git a/docs/api/utils.rst b/docs/api/utils.rst index 820f30f0a..ecc1c6faa 100644 --- a/docs/api/utils.rst +++ b/docs/api/utils.rst @@ -14,5 +14,3 @@ Other .. automodule:: av.utils :members: - - .. autoclass:: AVError diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 805bf50cb..f414e59d4 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -384,7 +384,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: int nb_samples # Audio samples int sample_rate # Audio Sample rate - # [FFMPEG6] int channels AVChannelLayout ch_layout diff --git a/scripts/ffmpeg-6.0.json b/scripts/ffmpeg-6.0.json deleted file mode 100644 index af6b3332d..000000000 --- a/scripts/ffmpeg-6.0.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/6.0.0-1/ffmpeg-{platform}.tar.gz" -} \ No newline at end of file diff --git a/scripts/ffmpeg-6.1.json b/scripts/ffmpeg-6.1.json deleted file mode 100644 index bf0604812..000000000 --- a/scripts/ffmpeg-6.1.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/6.1.1-5/ffmpeg-{platform}.tar.gz" -} \ No newline at end of file diff --git a/tests/test_filters.py b/tests/test_filters.py index 202da3714..7722de735 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -39,7 +39,7 @@ def pull_until_blocked(graph: Graph) -> list[av.VideoFrame]: while True: try: frames.append(graph.vpull()) - except av.AVError as e: + except av.FFmpegError as e: if e.errno != errno.EAGAIN: raise return frames From 5d5cc3df49b3cfc179c0c491dac19b7c8031b9c3 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 10 Oct 2024 17:00:39 -0400 Subject: [PATCH 132/365] Stop using AVStream.side_data --- av/stream.pxd | 1 - av/stream.pyi | 2 -- av/stream.pyx | 16 ---------------- include/libavformat/avformat.pxd | 4 ---- include/libavutil/avutil.pxd | 3 --- tests/test_streams.py | 16 +++++----------- 6 files changed, 5 insertions(+), 37 deletions(-) diff --git a/av/stream.pxd b/av/stream.pxd index 8ebda5704..d39585167 100644 --- a/av/stream.pxd +++ b/av/stream.pxd @@ -21,7 +21,6 @@ cdef class Stream: # Private API. cdef _init(self, Container, lib.AVStream*, CodecContext) cdef _finalize_for_output(self) - cdef _get_side_data(self, lib.AVStream *stream) cdef _set_time_base(self, value) cdef _set_id(self, value) diff --git a/av/stream.pyi b/av/stream.pyi index 212c721a9..89f49c12f 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -20,8 +20,6 @@ class Stream: metadata: dict[str, str] id: int profile: str - nb_side_data: int - side_data: dict[str, str] index: int time_base: Fraction | None average_rate: Fraction | None diff --git a/av/stream.pyx b/av/stream.pyx index 44354b651..19ac8e703 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -85,8 +85,6 @@ cdef class Stream: self.codec_context = codec_context if self.codec_context: self.codec_context.stream_index = stream.index - - self.nb_side_data, self.side_data = self._get_side_data(stream) self.metadata = avdict_to_dict( stream.metadata, @@ -128,20 +126,6 @@ cdef class Stream: # Lets just copy what we want. err_check(lib.avcodec_parameters_from_context(self.ptr.codecpar, self.codec_context.ptr)) - cdef _get_side_data(self, lib.AVStream *stream): - # Get DISPLAYMATRIX SideData from a lib.AVStream object. - # Returns: tuple[int, dict[str, Any]] - - nb_side_data = stream.nb_side_data - side_data = {} - - for i in range(nb_side_data): - # Based on: https://www.ffmpeg.org/doxygen/trunk/dump_8c_source.html#l00430 - if stream.side_data[i].type == lib.AV_PKT_DATA_DISPLAYMATRIX: - side_data["DISPLAYMATRIX"] = lib.av_display_rotation_get(stream.side_data[i].data) - - return nb_side_data, side_data - @property def id(self): """ diff --git a/include/libavformat/avformat.pxd b/include/libavformat/avformat.pxd index 1e16378bf..5fa25043a 100644 --- a/include/libavformat/avformat.pxd +++ b/include/libavformat/avformat.pxd @@ -46,10 +46,6 @@ cdef extern from "libavformat/avformat.h" nogil: AVRational r_frame_rate AVRational sample_aspect_ratio - int nb_side_data - AVPacketSideData *side_data - - # http://ffmpeg.org/doxygen/trunk/structAVIOContext.html cdef struct AVIOContext: unsigned char* buffer diff --git a/include/libavutil/avutil.pxd b/include/libavutil/avutil.pxd index 58dd43922..ed281aeaf 100644 --- a/include/libavutil/avutil.pxd +++ b/include/libavutil/avutil.pxd @@ -4,9 +4,6 @@ from libc.stdint cimport int64_t, uint8_t, uint64_t, int32_t cdef extern from "libavutil/mathematics.h" nogil: pass -cdef extern from "libavutil/display.h" nogil: - cdef double av_display_rotation_get(const int32_t matrix[9]) - cdef extern from "libavutil/rational.h" nogil: cdef int av_reduce(int *dst_num, int *dst_den, int64_t num, int64_t den, int64_t max) diff --git a/tests/test_streams.py b/tests/test_streams.py index 64bb63c36..b6097d537 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -35,15 +35,9 @@ def test_selection(self) -> None: data = container.streams.data[0] assert data == container.streams.best("data") - def test_noside_data(self) -> None: - container = av.open(fate_suite("h264/interlaced_crop.mp4")) - video = container.streams.video[0] - - assert video.nb_side_data == 0 - - def test_side_data(self) -> None: - container = av.open(fate_suite("mov/displaymatrix.mov")) - video = container.streams.video[0] + # def test_side_data(self) -> None: + # container = av.open(fate_suite("mov/displaymatrix.mov")) + # video = container.streams.video[0] - assert video.nb_side_data == 1 - assert video.side_data["DISPLAYMATRIX"] == -90.0 + # assert video.nb_side_data == 1 + # assert video.side_data["DISPLAYMATRIX"] == -90.0 From c007052d7e79a7ea8e073e8b981a308ee94f8811 Mon Sep 17 00:00:00 2001 From: henri-gasc Date: Thu, 10 Oct 2024 22:22:14 +0000 Subject: [PATCH 133/365] Use GCC compatible CFLAGS in Makefile A quick search in GCC source code show that -Wno-incompatible-function-pointer-types does not exist as a flag. However, there is the close flag -Wno-incompatible-pointer-types --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 77178b9d1..1cd3c39a4 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ LDFLAGS ?= "" -CFLAGS ?= "-O0 -Wno-incompatible-function-pointer-types" +CFLAGS ?= "-O0 -Wno-incompatible-pointer-types -Wno-unreachable-code" PYAV_PYTHON ?= python PYAV_PIP ?= pip From 300149ba34f3c1dffc1aba7802a6fcc5f81fa396 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 10 Oct 2024 20:53:09 -0400 Subject: [PATCH 134/365] Disallow `rate` arg to be a float Additionally, only allow audio stream `rate` to be `int | None` --- av/container/output.pyi | 6 +++--- av/container/output.pyx | 13 +++++++++---- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/av/container/output.pyi b/av/container/output.pyi index 760532a96..7891e17b5 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -15,7 +15,7 @@ class OutputContainer(Container): def add_stream( self, codec_name: Literal["pcm_s16le", "aac", "mp3", "mp2"], - rate: Fraction | int | float | None = None, + rate: int | None = None, template: None = None, options: dict[str, str] | None = None, **kwargs, @@ -24,7 +24,7 @@ class OutputContainer(Container): def add_stream( self, codec_name: Literal["h264", "mpeg4", "png", "qtrle"], - rate: Fraction | int | float | None = None, + rate: Fraction | int | None = None, template: None = None, options: dict[str, str] | None = None, **kwargs, @@ -33,7 +33,7 @@ class OutputContainer(Container): def add_stream( self, codec_name: str | None = None, - rate: Fraction | int | float | None = None, + rate: Fraction | int | None = None, template: Stream | None = None, options: dict[str, str] | None = None, **kwargs, diff --git a/av/container/output.pyx b/av/container/output.pyx index 717df8fb3..9e344804c 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -49,9 +49,8 @@ cdef class OutputContainer(Container): :param str codec_name: The name of a codec. :param rate: The frame rate for video, and sample rate for audio. - Examples for video include ``24``, ``23.976``, and - ``Fraction(30000,1001)``. Examples for audio include ``48000`` - and ``44100``. + Examples for video include ``24`` and ``Fraction(30000, 1001)``. + Examples for audio include ``48000`` and ``44100``. :param template: Copy codec from another :class:`~av.stream.Stream` instance. :param dict options: Stream options. :param \\**kwargs: Set attributes of the stream. @@ -115,7 +114,13 @@ cdef class OutputContainer(Container): to_avrational(kwargs.pop("time_base"), &codec_context.time_base) except KeyError: pass - codec_context.sample_rate = rate or 48000 + + if rate is None: + codec_context.sample_rate = 48000 + elif type(rate) is int: + codec_context.sample_rate = rate + else: + raise TypeError("audio stream `rate` must be: int | None") stream.time_base = codec_context.time_base lib.av_channel_layout_default(&codec_context.ch_layout, 2) From 329c377440b49c0e659b967294a88ff9861d949d Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 12 Oct 2024 02:12:58 -0400 Subject: [PATCH 135/365] Recreate logo in higher resolution Use webp format for logo. Always underline links in the body docs. --- docs/_static/custom.css | 4 ++++ docs/_static/logo-250.png | Bin 12552 -> 0 bytes docs/_static/logo.webp | Bin 0 -> 4154 bytes docs/conf.py | 2 +- 4 files changed, 5 insertions(+), 1 deletion(-) delete mode 100644 docs/_static/logo-250.png create mode 100644 docs/_static/logo.webp diff --git a/docs/_static/custom.css b/docs/_static/custom.css index 1069c8f1d..b95a5f253 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -12,3 +12,7 @@ .ffmpeg-quicklink:after { content: "]"; } + +.body a, .document a { + text-decoration: underline !important; +} diff --git a/docs/_static/logo-250.png b/docs/_static/logo-250.png deleted file mode 100644 index 32cb6bfa1725152483fc7d83f9afc3d6966088b5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12552 zcmV+jG55}iP)004R= z004l4008;_004mL004C`008P>0026e000+nl3&F}001zfNkl@8E-A`U=M86q-75flf4gQ?&`+@kVRRAh(@K}7{YQ1C$!1QBHK(E;5n zUD?U+kDE%mbbWjY;2U5e zkXOs^K%w%+YB95a#Wb|}>3HA)bwIi@JP0hUaNfTT-`&IZdmHEl?5)PVvA~}|Ooij> zI%?|;w%=Ua^WNnaK9T1MbD3|12YdxdU zgE}SpZC6oy`iP=HW?nJ6vV5O*3ORm=Y zoR;vHEMnl;e0}i88HI`xgVBH>RO^vFYpRzk)xN0xLpZtl|2#%G59$<3pyO54o_eJ^ zBVj?ljJ&mtZ6(i+9XxyOKYD{HG_ig#Mw6xbvzZ`ZFqufnDyDs__OnKJHNM{7do?~P zBN_Te%Qq%zJVlBZ;8cydUesjrT0 z_lmcE-c*h*`w2~SB*h_c(bSC#3sd_v{*@*%^&U5xMPzCQzg25Bw_34UE%=7TqVfsg z;8!n@wDT*^2=w5x5&e%V-UvY?zeq=Jp?2%SX9f@!=o8lI(a--$$geO5(u;t4jVZ}aV$Y|Kl67LIXY_e+S&9BRPiNxdNiapb)HP ztO#<}a})5bERvf|_yk4a7txUOzpr4=XA>#TPV$UA4=%foo(ntxe19c$^0eF{tXAvs zS)+U03NZfdANOC(DQeNICM365$SXF|sA1E2&kqfH-B-789EV z?cOhn0{KN+inRvboOtWV;eA@JE-2RFjOb>ImB?UUa`W=()ah;y$vwELhz?K$yl4AW zg19*rA>WB%~S|N0kr+7*DptJ+yCL} zr6>H=B>4NNv6LgPAb{RzMrSZFuxpbkH{TGqB0aa5(BL3cUfx)$n2(HRXc!Ke+1YgI za+8PT9$ZxrWn9y#rX&{xfx;3!X0yfg=ES~<^&XMMCrkN%=t(S)&j{B}#J+3Uek3bGF?tI>gAi)QfcQ){~SZ zkGSr^e``hWP7MQU6rxCBsh*UqLS~QY*|k@@hRFkGe6{MIGZ}ZaijTl*wN|@?&A*X! zc_pZPBA3q?mN=nv81M=KtLc)sZY%^#5_Fn*M@s3WJx{;)!@&YgM&pK2 zA$D=?q9{K|~P<9fcDcdHx+s~0gU@%|4xtp_R zPI^X_2mdLeccvQAUyclK*_slpR@nC5=#CwmM(=-o{!ibYOvxS6A}-u5aa~%;;aGAu zLvHT!^}Go;kF95Z=XR1-Z$W5ibD^2a8Q!wMTMMn1_Bok3Wc&E}Kb=Xsc(zeObZ8}# zm+B0pW)?F0(ZMq&4{Y?VE_W~LoTUhcePApGiysI+2uPW5_6m9_n2S6k%7gzD(K`cs z0Kwe{3j!4Dj94u2!k8X?J2s8p|JLeVOIK|@GNeVruzv|#eZw0hWfm~7Tbs2XJ$v(g zKEn6i+4s>FjJ}}qppHy&O|hQo_jjEzvTy6}zWn*n8_Rz_^mI%_07`{`n;fUGRF98;{a>aI zYj$5k;EopjJ$5@rJ$ThCM{)tI1~8;T%f}u});_j=)+ZbPr_p5J6dK@Lf#d=vvxW40 zEm4v2ug$v0=lv+v`=iM|`76qXy};~K?)5>yT}UOWpYaTv>k?|Nvic3xX?t12!+@Pg zA+T;iAqtf0jGRf!XVQ?)Bf7VVTQlM9A7A<9?{m`|#e||%R9{>dY!X6JMgbN@$i}xO zbQpMh;}xCw>#e<5qQIwN1?ze-m`+27i9<52bG{q*($Bk6(~`T!Mh4rt&_r9fY+7zH zO&T_u^UCO^ueOU>{uD_EK7bHcO3S-Qm1SPDJ8HI)(7?yhJpsUWpgvGg2Y<*i>!1P9 z02#nJr0TjGxV~d>yCWrR72>$=zX}Kj1|gLZuL_-jpMi@=Vc}IF5h)w=iOcwXtAM+K zl&Z8{AOYAc*fwz333PK?EciEy%)s!QTTYuWu>IQ$zS}$bi_Hh$PmB&h?WL%;gC=N7fBkdU zfaY=bE<8aHFxq0`C*0lZ&AAVDnnB6OBRIEwHD-TXp1RAn8fF+FV`DZ10!T!JKu@Krm{_*4O~1U&ql7xk^0>NI40w| zcSK-#O-L>X0(zsF)a*iD7}Mj~c@z4)y>a*H8DDKX`hH?`2wp1DO*fjrxwL%RHg53e zl9~N`c5Zs8G3Q@NqR{ArR|t02byG1|&qKS{A6a*P#)mzg-SW%c1A7NFj|<0Af#hbh zh4bn84C~YWgSiiOnt^`pL!A70HD*6h_#$&-8ZY1>yCX#=0^|K?+cM~C8|Z<+{YVvY z4%sHC>je=ul}j#$;W&uQXj`paA=JjH%K^FXSu0kvBPx;6Xfl(YTg1q|tzYTeA>qBZ zzxs3R`(N*QJu1W(wMyZZPA&)nso5G@CDh-(Xx6>m0#*Mc^7n%W&;i~N_By$#6v%>h zZ%qE>*oZ~5KU(#*U`)R?Iy}hk-DQTiwA@1O?A`0Fx1Z`W1>L%bIP&@u#0aSv1=WhR z?^5ZAQXnq;s&=up0ZwBpt9>2dZlo=>c3g$bMClI-*>x=YmNeYMc`(4#dVX} zOlnpkJ=(^-{nU^yFD_WO_kov}{kAOF-y44)FFU=sItINwO)+61G5cSiGNMmx(1i&8 z9)1jcz8~sndy-p>U^oXYo}6~7WXgwg7H#@gSDHO6BG~U=1?695#cCzDNKfPVmWy5( zUT?auars>wdu<6~fK-fvYA4imriv|~R7&x`K!DGI7p?=M4|U_7@lMX|0Zz$yMjybh zj=wEYrX;@JrrcKF04E4wG@Ch}uHnT8d%gA5oFUV;A50$l-HsDqG>Z*G?WL4kGP%-o zH6>aEukhV3jB4MpbL`GW92}I6p+t>O!X?8Sf<$=3MQAzhsWj{JZ%=yfmv1t&Q-(x^ z23Ay$Vy5Qem$;FVs@LE6N$`tAY&DPt@>w}c@*n=OCzu8z~!?>yCS z-iklI@lmVr@m9H|lPeV>8F@wIm8uWVxW8@JA?<$h;-6P`qV-bX9aoOzq7=KLI|5Ay z%`=BCSTOaCJxlf=Pz;iO5aU1Yc z9e&?f8Q1It&bmi*LEvI`Ax#>DeLUsfo1S=S>2Lkt|7OpoM)gDR^H#aFk}DJfr&4oh znb`2;(((7Sy|Lc+A*3!yK^NqYI>esjW-XYLq3yKi_1-h*O?YSTH(U4Y9NMHonB-%! zUXl(h77Hg+bGf%~r&SB5^m#xmT``c;t2bZ@0HwD(;(A?Sk4=|4T1(IK2Ob4paiOCs zftO?sn*S1%Zi?+?m;k&*)o!#SNbx4FHa@uzm(K784TScYfiFF-;mp zW{K<@nnpo-EI#$^Np3ZP{sgpm;_ZW)Ig_7xf8D1SQx7~F8x>M1tk!C^l9sFChL)|@ z&3|^lXm4S6FZMmN1CxmTbE6vOOk&n?*coJI74y@t2uHos-Ew9gBZ!d&8U60@~vc-+8{Z!KaTt`Qf4Y+xPC8 zdMOL8oPsMrYE~ijqUvval8Q1&@B-fbe?n0>$IiH?SSa3-GqUj^sO}p~~@PLXui5hRlm_AcC>07j>pB9QO3mZOR*km)I)B)}dwZQs zAZ_&yv(>JgBgNZ0Lfkd*jooFepc`%f>TtcSR!gMqv8%#CTc2J|@x30?Yc&lpvIL@!gmZ`SNHq^Gq*U9TJ&%XIP+&|>_ezqDVB=sWYo z8(;poVQ$mdV3bN_g@`-JYJX#^hQE9^W8iJx;@;LAnsW}V+50*(yt#q}=l=4@@^i9n zPz6-+3!-A6Q0>AGDTu55JN<0>va9z#?c%%%_^#al|3XH4M#z{nTum08YN>DTpo(bB zDzM>-5M3b(WaXFO?WM~5c-n)Btr`^RIDGE`a?Ulvw*mInbt@Q>p!w*pPwVDA^2~x? zo;`W`uj%zd{qa&N?Q_b3w44H(BsAIn(ad4Dhp3OVscpvgghtbyekmm zEu;*r3rMM%WxoO!`hHDUtYoBA+w$)$2EM{^nrYcY)!)VQSApH0c{`9Y(wyNgClkyT zncvm7mip&58P|MA6=Aw{PIOTaC@3n$D*9#5oiri7YZI}Qqjw*p;6S^|hBrWW3L4(A zvM^xL@R^JD%uY!?@=k1IFiKIZRC_(YSW8ey!v9_z*{n~)fGzPHdFBXagJ55b!h>>f z)+rTz`QO|At+={^l;u?yt9pN=@KLIGr>lgL!MILf`CiWIR{`~rVhyUob|g2JoRzqy z2$V6AQ9)w4JVX}+fl|Gm5|elF#6cZf4C$0p#MvRo$osnme$6YBQGXF)x_xb~_kW|O zes*N?o`ZW|i4G5x3~v>lo1-bkMBtw1hP1e)cl`SL9DM2sMy-H4#$$MMK3E*VPz5QH z9b6k-i>p*cqC3J`7xxa4A4n&w46c&W#H~9L%ZBGy2suyveuu#65}WB%;F)r6F7?&%dJAEcSf%9bs)A6 za-s`6voG9;>m-k#koFd=h#fetDpQ8;KsyKLHdTYDbu|f=VJnb+*@&)Gh@|BdqVn>w zd@%XmcCX%BnoZvPKPk|5mC8SvZN@h{aeafi-pV^){VIRT=ijaWAR#&kwaVV`CWrz_ z=>;@x7=2{h{4rf0zbiYGMFy;pSHzT&;L#syKGSiD;{oxys1Jole^2OpjLLJg^1e zqze&Uj`e(K4e##>#JJfvy=92)*$!+FwNG?~D3Gfu#bUuQf6B<_JzII7;`s32NZoS_ zK5-RRT${Kaf9o&V>g7GBe)8ADCr|#iHZC#%wU?J&s;ejnWaSkR9TjuwMxh!m|P^_I8oyp7xxa5 zK_FKKw}VI-Y0l6JsU#QGlzjfBM-5hWIa`%)0ZQwf=%OG{RH~&!FY>}8W82@=CFC?` zM{Oo&doO$vE5BBs0x_L;8lyhHecGaXt+_A3Pib)Cz3*q-PA+XyWSPa*)=>L)nGVF5l@f#iKeKvz!bS<>c zncHZci+hL3c)pw!cb$xDhC7iM{;DJK-4}T&x0_sy_?klyKxZ&ep!G7}e_Q945B0Gg zBX#^XF08u)-?GR`>!p=cp9&$dJ4smE?7XFCXf8W5;S_h4?;@)b$2ou;f7sf0QSL5RiO|U?#CJv zU8xW$(Hc0OT1@Zut-H+{lz*JleSRSMyIb&Y4R#w6DnNT4f`fL`Vo8tp)|>BH{N0At z^~3y8tL%eelnRlof?~{q&&9{@?$Ywfn@>a5+9XUtv3NzeX}16sTElj>bF$KX<>KDEWfE8p(vV`rU5Ve0 zzV~N2bfNlgNjX0F^+0}|5nZVe$;>N4Yk+wZ@4IpSIPbp#3~zr!PR3n$$J?*uC_r}x zqMIM4<+ojzY-`Ye-jc7riwIKTSD}!kLKHcBF^}-zpp?z;jB7A+gb797XJ^PSX^1kw zO%YoJONq2yQ|$B}__g^T$*=uD#usByH@Fs7^FXA^Ba@LT)+8aX!td)cu1Rt-8MMcB zwH0R==-_y+j0s4N%j(-|v_(!mxJeFYvVh4SM1IcU)Cj5cyb+h^WSzUhCvHg9PV5rl7r8grs3key$ImpVA-!|7vU+Q*lg zIsWiDN|F+!VifMU&_rq9a$^=Wes?sX1AZp?)ddvpehlx#3T>?xE>bZ7Lli*imSk`x za>4(H^EAwg_yNg<6H&#vQ8t5Lb#8!$Fb726AcN@j zYy_60)60nu_*sT+0TFqXGVBawB|1HYr4}ZS9(L=PTVi&QI%y>r){K&@>lIdVeKLf_ zpCJC*CNHka@Ab#tt$#MG7lOB1S!s{*q7p4;MQG-8_jO9_)#QYkbB`aT_-uk?cyrTF zZYh;4>=C)iXjw^#CAm5!LLrtWGd`CKaWojGKF`3l|%Hj>vUPyG+d2Xa_}Xdm|O$>pDe`Bx*k z&9>Dz8kk*!=bUn)#}2Hfjo>DvTI@qs@xA z@BU0|H|wL7e~0)B1o_z;-joWFwA><;N|kx}?6Eh_e>5~-RrvW%va>s*3UYJB6~L+o zQyw&!{36j0e@5<)kCXlF%c$d}_SI}9&!%GWL8}$~m8{)#@oFHo3CKbU;8<$YWuuU` z^3AA)=dW?1dsfGF)?0Zc^-39X1nWg3~lvFmSvi~Hnc&7DLFOo^fTe$x`$FB8XAV9PMV0jbwGm;@?L zbkm%W+?7Y+ERsRBT0{pau;Lfnzr6r2TUx03pYz$fr;omU=D2>bJGe0YEs|DG#Jf>t zl53M8qCpNZYhqtsqD%b#=WkEPMh4>NZC@=`6a@;3N)QyjhS#1P)#b*fDh+2J+fMHO z&iFLO{R9;~7;_17^0{W|L5bNUKt#was6ri^96J>)hjs;T?CNd36EjKbnenqP;Oo zP=M|N1czqO{M(2*Ys}5R+5G+4_WO`ShClwz$XC&bxlcLV$&(lR$4=&yvg||pO@b|UX$nl*P?YC ziSG&==akFQZ(Zr;auQg|#&u2lR774dS!1ep^BuJPT)~l1R8COMXEh-@f&hA>rQ~R; z_E>JA?#L_mH_~i=^Hw5^AJ6CDyeB1NmKr2h;2^+}yrxQL2?(5{3K%WkwcfCvfiBF;V?LLxUUxqRS z1n-*aiCeiP|3j5;T8mzdtI?(-H zmEPVD9I8$um6?UYzXUHeh-gbSLrkmmB?&qSult9A5`amm$8l!M<#?ob8Y|RkxAKE(vC=RxnwtMEL3H+`2Wl zc1R$ufdCZ3>G`jcy?q9L@gR7WBe@bv&qH{0G0k4D_v^v#;nNnZPV!Od2@MFa4{Nm* zqYyiT*8;yRB&z;`_rmj+Aqxf4Yi<8+H}<#VFvl3djCJN*LJ1iTbXvbnrj5EYperiI51MZ$zoy zw~!z$EjEPATB%~u+NHS>0fHjDf(uO)1vJH_Xi8z;q;WS4=}~_>Ns~8oVcRhGNiIlj z(4GcW#1F*Hx{IupH*$E;eh@$vUK=DIfb$ScsoB{&=FV?BLyF}Sk~mTZLx)`e_C1)6Yi__^H$r5>w+Ml(;Fx@s2SU@-5q0k6(1z)-7m=8 zatFSR+$TAJ_AG?n{yPb;-GgRV92XY+iozdMA+=G@{dO+x&*dB%BeV@+;+RC9e)-V# zNc2NUIo!865Ycy6F@aWR!YIRoGTO4=Mm8Vh?Peke7m>VBa-!^pPFONgQz;O;&6pkSYG+!t&bGD;3|dpg(~#q+QJbTWYyS=as?HRKMlO+;@+i5bp$jx*9LbZrRP=~ zBH7>Y?=OAvGh{A*39s-9Bu9!k6Lb*&Sg*g6Zb_fJc-@gCiFU(2$E6lzDHj;$HV^(o-remjY8bf;i@1jk42#iA8ZMbrk#3xP*$f7Fvm zE~GQInf5!I9;U1$VB{H~p&!$px>hzLtu{7N^+peEDj44SRzdPIqn<0>Tuz?;k9GKs za>V5z2a<;z@x6m>G7x0+wObIZ2W--UDSsRm{bEEf?6c4W$$D)yL1fU0Jr^EqG49K_ znfjtr^P=$QA|uo2^K3tvO}v!9W+djhI@_Qfcw)k>vAaz(&lnlEdM zNu%QUn2RWUBc#Na%eHBQ#wMoPyMY2!SglqPd;daGYU%a1q654OJX6K-FUk%6 zXf;Xmj8`Q1242{ADwA{R`L{&|`&J^krbJI}p_aEEAJT8!Z4K6tJNFZb96~wt2=_rFDP(@wtomqlx9c^Ejpum98(4WU2NW5NG^+gVj5BPVSRqZzV z(FMu7BQI}$Z5@AK67tedZ=mWz8k=gN4O?-)iVq@I02SX0PV4F*?nY)dV?|NENUEK0hkBI1&DsXWOo(n>D+fyD>~+K&A?1cf|1 zqW9ENvmZsJdK4mP4Mx&63hur2rlB(jtCo>7cM&NopT#H9{j^X)35HY=ep%3P%AiPXVK=IDX89&gqT3;E|t@ zGR^85Kj3^f+A`cnD7DX0mB^Ui|;=2esD_D*$ zdU{~KcJ$4OtC+jf8HuKaaem^hoHwc}C8xnISlOea@{rqFCz zH?*fqNq!mpS`MkDu-4juy(`3G1uv1F=s!~RMh_1o89A#Wjx+Sfb($!+S1!sO zq~JPw52WzlhurvHXV@XrM&J-iu=d7k1!bsha|5j;YAb_mVWj0!FTKJcWA9M(&#iyq zj-GdQ4m0*Sc0QdZu~GEDDFd~6*JASX$Kln$CazoDBe^aKRG~SvTH76ESPo|2NwtJ^$gB5uUlnS} zCR`MFsmsaCCSsUv=N(aoWm0?qG&`~F%%Z%C2DZ0NProEySgqIlhOP^MDY$kM z{f=azc7(Rbi*5h$G8<}sL8{*4YUh-`NI3wfDyS;^sD2H6BaYL8tEHpwlSpm3BodeZ z?}|hRQ2K(-984DD$&Q*&mhuU-#t4+Ywm-4EI(a6TOA3ge(-W_HlHQfQR@w|v@B+z2 zb+zD6Kx;>GLBMFfu9w^L^_t^Y@h^zu-evM!C&yymJ%IOKZs{`u{4E zS;k-$R5nAKMOujkmHVwID<&x@ifO*03jtjueZ-Isg}cEks%+w3e-#bt9risbRe?R9 zq2)TqDi`N7Wc(fFtma9`4OOmPd?NF@hpRZ|PQl7r8S^6WC0A15yhL&>%@%bfpu_2Z z&uPzrsX($7NLYfjY2KBFwvuibsG=aXltV`~ZuP=7MD((a9-IwaXO&BP;X2&3OmDps z8QuLmTq`IA-f(i>mGNv=Ew8L~0lBN}cn6|*b>34erG?9k zTZ@r0em=Ua-DTE-;FU&`54z#gv~s+(RbN#M`xc~h$15eqKU8V3Dg-Dov1xmzGYQ~v zq!8Hhdu}O^40@-oG4b8ee(JyDJjdV5CJ}6~vwzsb7s()YB@1aTvPmr^PNWJS+oY$! z-v7SR_H8;G8xjSiD9oQ~tHI^1*dHTR_k0>DP@zd3c9b62iBjlA) z*cO6RUU(!@0!3Ylzo%^a)dy9Ir2=5FqSHGx02^#7()9vItFo(YkPS#Bs;+jV03W(} zZyNA_m%G6g-Bnq@OxqtS!uGS&qAn!bo|lG{Emjj0AeGd86A0sy%fnjB{gdlth4P>> zZbqtwR+YZF!P7lg29EsFRvYQF{v+R!Yx{X{<>4sM;wPU z(>)%ddvI;wA)6Xi5l6W)K5q{^ME9T$;K)t8%F{I-qI+;{pg&a`;h+{`wLS0<-GeKS zKpe+lETrO|bsl(#?!i@wNTei`n}*4h4hD^l3B z3TZc00FR8Yg^db z77~zvgh)U_92^`h925t|LFypO0TPga1SDjPc9r-2zOZj{|3!pE+qSBN$ChgNx9u1WvX z(AuF_`~~6;#gc}mNV!0=CjI+=sM>MZ>!YeVVfn7AF5&t(!wJtrnS4Wd9NKKxM^nXr zl2$fO#nr#sRe#B6$}8WfK8`tZv0fa`3IZ=Yok#!#+HUo9Y=A6G8l3hSQ)R6ZiQLo1=zCEK5wfL>5ng%ROVl2z?Bvvl1Av;&K1 z;IxZ2*OU{`WxNbc=<3bg3d^Ge6esEXxN-|NA6Iv=SULeUW>uI{s`>K+$HC11xsAuQ zvX6ft$}QU_1oYz* z`Qp4UXy8P&ph|$Gb|AioN!NJfX2DDOG{hZ<@RuLKfmUVbWDlzT5Ty*=Dl-~Uo16U{ zukgbn3keYS<~x3Kz2m7cCnv^W=YVK_6X$`A!j6_*8mBn_JIPL@!knDY3Mw;o%?RC6 z+}!aw@lrVbA6&nbB%lhmqk;3HR-~IgQ?3Xsc~iWM6nq^Wam+gQ$*K_4ss%Yhl1rfd=6HwNEcsKUtGs~R}KTn5=pRewgmWx_0VZTsIKwls$ z$S6lxKZj#8Vd7MOO*{(1+=?x}AiI$7TRX2qYi8gQHYzt%AoRsm#i}l|{vgb9fouixE zo1!Em0TI}}92DVE0*cp={{&rUE(y$S1w7)RTv%BYMo}|jP+y0W1zbb^6PTAyjSJJP z1vi4@C4EueL7Ny9;bC8gYsh~93;Jdj;BegtKdpo>J1tlFRlO7#;rY!X|DEOi6aPdc znVG^N4XT`=2UwX2BA^*Hs`>PF@Ms-eL(YqZd1%(Ca$#oAirRp45t2mz;O8MBFv6ol ze1$b+ZIj@~yMIE-%oIVSjcK%$UerP@LlEJU=;sdGb(w?>XEbFXHFf5kl$B(Zt_X|_ z6ycGp_-(cEP=CyU8}>qlIz!j~@?o2F(ZjiE$bRN|h6fnP&{6V=Y1ufoKB2JkGUBm+~e4f}$Wf%S6$c+|cIAX-BTx7FBF z?0QT9I^1wf8sj+%bEggPYv^^4aIPT-M%E!=vz!sbb3bOlH?rQg%cNs&X~mtHJ`-eR z7hQEh2n-l)}Yp~k;oslx#B$__jkL;N6s9%6{O9{i?^&gx`kQ;f^V@p)o zw%QMB79?8M%QT%i{+a=+l1$@%focSy&jSTM3G5+k{^X?!obn4wl%!)ub|CtIwk$(7 zfa4iQ_@$#w4UHub{rqDFoc*{?9!MQsBw0~GBv#rH9W^n8UPFRM>u6KipbY8||A>_W z9Y-rkNoF2-NjI4nE|UuB1n5Vt#~C@?nG}otLrBI=j@Pn>9z5AD+klKT{=ylohVtW# zaI!y-PVMj~{wbcoZ3p4z){qX}IeQP)Kq~VI(uxLfMH9150y=8p)9Be5hGi&OPXvh; z$K@r{#v5sYjZ)1? zJ_s`7U>pBqL#vA`b63FvMn#k`oF;kfM}{SwC#G>?N?R+bLW78Jt#%0FLT?X>uTn?%|k zCs4>Lr_WW9mf;%m`V;-KD)j_(T1+Q;VWGBz&M2fTZ+4*N2C@P0%c^L5@HR0SOGj=8 zohT#rX9rr((zvrX$F3nWZ^vS0#^Zq;xb2{MWI$K_H3EoVHMJ;&`6>vt!AuF0AUW94 z-B}t}yNuaP*3u{tHS_irF!cmU*@woJSVG;#(`)ZIx-rwD<`0R9XvxlFL6A| z9JP`>jXd16WZP73qb}9uiDuGSZnVu!iXF(YUcd3jX>|EIVB`5Y2!@HKAOY1W4++!A zB40^1FDRLpG%L-@%tNoiRQfms38>aen;=9B&?>d&74qD?aYBk+8F;}|`SYy$6}VkWAR=fo7)sv@l`k8jyE z)A(@CddulfH~YD(gUPYtD%yP7aH7?b5*CZiJm$7tIrWl4 z2+uOy4Z2-0pSZk#By;;{=JwIu?V~T|?on>m_r$SX&;GtG!`JXGt26&+76SlQP&gps z1polBA^@EMDnbE!06vjOpG&2rBB7$RnINzd2~FHC5FVrJ`V!Y z?d}2p|I`D8Yy4jMr_r1UKsi|RSwlCf@7sY%FVN_p0z0wPnzL<1Dwx_0MQp00h>doD z&$_{+oXZ)8sg1Q1s$*?Lhsb)}XjsqA)4i`6%Lu%1Um8>a zoOB=`(96c!iaq#}@_3^|1&@eb71_OA7zffD3w!Qn0VK%`yZaMa@$oGL1Rs4);GdzF zjkOe~#=?EB*ia|^WQJZg)KaOX>}{x}QyXe1@Bse%+>fLq>2pMafQqPdAhpgn<5+q3 z_6!E{K4x#W!Amxi>PJr=mU}_mcc!?6437rKGF)RXNuT&AY_i(@cT=`zub+Eym&m-{ zw9ZqxtR6ZljxqmlEA?yoG~A|iyg?p?LS?iz*Qt&|2gBIEwR8XVXL)5mSGX%MJ+hU| zlziZf7M@s%AYTLW?EfU~cjsc!(bTN{B~cH%TNY3ZzD^gFM<@e^>RVr5e${tfULs-Y zajF5ES!*_}*eaO!UNSD)@z-k{*3@x?i>H`%QB<=U82VWYR*`j&?S-3O8(U1vPHFcR0&4=;yGm}G3QJd~{B8;fUH zY@p&`(`hg2?iCDGVwYD&EKkQt@=nft|DOOXm|8FtsQo=U5=5m_Ck(&cFT^}yhOYiTOQHV#NY!;IkFjccUlg*|BHni@y z;HL9%Tn%>GT(g{OrvPDGQUrAbA~>wceEwp;`Xo4*FcZRe7>D8ID!c2v)b0K#dM(;Z zi`2Pap43m)FllBynUdhSZ}<8(v?eFMvrKycyZ}OJU+>cJI_~ANVGt+)Js{BeF}he! z#;^S4fzH7*r3-7mPsf2M1VF49HDUd}0p>*_4rKe~3~P60ex8w#8kKhu7w^@7CL~r2 zD0QMf%!;Rk9zuiO-ZDvD_PyF6x64BvUt#g;Ukda%cOFv)w0Mvh1u_B9_gpMx3khf7z@8JCYYc34KwMsVuzV{;zpKl#F=HtTCoA9u=F2r;MnvSuEPF_W%n8jde#C6 z{Zu3xwU6T7d51w&)I)ZXQSvxC&lu2I9?F3I>2nkz=V`v?It^#C z$UEaO1OuEo$X7umk_@lup`$F8}`jWt?0Q z)8!QJ@BTn)3_9422mng`^#A|&{VRa{`B1FRrBCW;2=N?;E5yiRN3<<~Diq|-@nGQ-=->B7d@-M65yl(kN^Mx E0MEtn Date: Sat, 12 Oct 2024 15:08:31 -0400 Subject: [PATCH 136/365] Fix stream_options type stub --- av/container/core.pyi | 2 +- av/container/core.pyx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/av/container/core.pyi b/av/container/core.pyi index c1d43ca4c..b031fb4b6 100644 --- a/av/container/core.pyi +++ b/av/container/core.pyi @@ -42,7 +42,7 @@ class Container: format: ContainerFormat options: dict[str, str] container_options: dict[str, str] - stream_options: list[str] + stream_options: list[dict[str, str]] streams: StreamContainer metadata: dict[str, str] open_timeout: Real | None diff --git a/av/container/core.pyx b/av/container/core.pyx index 683a3f65d..dab7c865e 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -365,7 +365,7 @@ def open( :param int buffer_size: Size of buffer for Python input/output operations in bytes. Honored only when ``file`` is a file-like object. Defaults to 32768 (32k). :param timeout: How many seconds to wait for data before giving up, as a float, or a - :ref:`(open timeout, read timeout)` tuple. + ``(open timeout, read timeout)`` tuple. :param callable io_open: Custom I/O callable for opening files/streams. This option is intended for formats that need to open additional file-like objects to ``file`` using custom I/O. @@ -378,8 +378,8 @@ def open( For devices (via ``libavdevice``), pass the name of the device to ``format``, e.g.:: - >>> # Open webcam on OS X. - >>> av.open(format='avfoundation', file='0') # doctest: +SKIP + >>> # Open webcam on MacOS. + >>> av.open('0', format='avfoundation') # doctest: +SKIP For DASH and custom I/O using ``io_open``, add a protocol prefix to the ``file`` to prevent the DASH encoder defaulting to the file protocol and using temporary files. From c055671b26ca67324b0b339a4d4e4bca8b6d3228 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 12 Oct 2024 16:52:31 -0400 Subject: [PATCH 137/365] Add docs for recording --- docs/cookbook/audio.rst | 2 ++ docs/cookbook/basics.rst | 15 ++++++++++- examples/basics/record_facecam.py | 41 +++++++++++++++++++++++++++++++ examples/basics/record_screen.py | 32 ++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 examples/basics/record_facecam.py create mode 100644 examples/basics/record_screen.py diff --git a/docs/cookbook/audio.rst b/docs/cookbook/audio.rst index 39ccc62b8..90d6bf0da 100644 --- a/docs/cookbook/audio.rst +++ b/docs/cookbook/audio.rst @@ -5,4 +5,6 @@ Audio Filters ------- +Increase the audio speed by applying the atempo filter. The speed is increased by 2. + .. literalinclude:: ../../examples/audio/atempo.py diff --git a/docs/cookbook/basics.rst b/docs/cookbook/basics.rst index 2896519de..d7c1d6320 100644 --- a/docs/cookbook/basics.rst +++ b/docs/cookbook/basics.rst @@ -15,7 +15,7 @@ If you just want to look at keyframes, you can set :attr:`.CodecContext.skip_fra Remuxing -------- -Remuxing is copying audio/video data from one container to the other without transcoding it. By doing so, the data does not suffer any generational loss, and is the full quality that it was in the source container. +Remuxing is copying audio/video data from one container to the other without transcoding. By doing so, the data does not suffer any generational loss, and is the full quality that it was in the source container. .. literalinclude:: ../../examples/basics/remux.py @@ -40,3 +40,16 @@ Also enabling :data:`~av.codec.context.ThreadType.FRAME` (or :data:`~av.codec.co .. literalinclude:: ../../examples/basics/thread_type.py On the author's machine, the second pass decodes ~5 times faster. + + +Recording the Screen +-------------------- + +.. literalinclude:: ../../examples/basics/record_screen.py + + +Recording a Facecam +------------------- + +.. literalinclude:: ../../examples/basics/record_facecam.py + diff --git a/examples/basics/record_facecam.py b/examples/basics/record_facecam.py new file mode 100644 index 000000000..2200bc546 --- /dev/null +++ b/examples/basics/record_facecam.py @@ -0,0 +1,41 @@ +import av + +av.logging.set_level(av.logging.VERBOSE) + + +""" +This is written for MacOS. Other platforms will need to init `input_` differently. +You may need to change the file "0". Use this command to list all devices: + + ffmpeg -f avfoundation -list_devices true -i "" + +""" + +input_ = av.open( + "0", + format="avfoundation", + container_options={"framerate": "30", "video_size": "1920x1080"}, +) +output = av.open("out.mkv", "w") + +output_stream = output.add_stream("h264", rate=30) +output_stream.width = input_.streams.video[0].width +output_stream.height = input_.streams.video[0].height +output_stream.pix_fmt = "yuv420p" + +try: + while True: + try: + for frame in input_.decode(video=0): + packet = output_stream.encode(frame) + output.mux(packet) + except av.BlockingIOError: + pass +except KeyboardInterrupt: + print("Recording stopped by user") + +packet = output_stream.encode(None) +output.mux(packet) + +input_.close() +output.close() diff --git a/examples/basics/record_screen.py b/examples/basics/record_screen.py new file mode 100644 index 000000000..92818e931 --- /dev/null +++ b/examples/basics/record_screen.py @@ -0,0 +1,32 @@ +import av + +av.logging.set_level(av.logging.VERBOSE) + +""" +This is written for MacOS. Other platforms will need a different file, format pair. +You may need to change the file "1". Use this command to list all devices: + + ffmpeg -f avfoundation -list_devices true -i "" + +""" + +input_ = av.open("1", format="avfoundation") +output = av.open("out.mkv", "w") + +output_stream = output.add_stream("h264", rate=30) +output_stream.width = input_.streams.video[0].width +output_stream.height = input_.streams.video[0].height +output_stream.pix_fmt = "yuv420p" + +try: + for frame in input_.decode(video=0): + packet = output_stream.encode(frame) + output.mux(packet) +except KeyboardInterrupt: + print("Recording stopped by user") + +packet = output_stream.encode(None) +output.mux(packet) + +input_.close() +output.close() From 77ebda5834a6d22f8e0957a0b8fcc83423dfa529 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 12 Oct 2024 17:28:36 -0400 Subject: [PATCH 138/365] Update caveats page --- docs/overview/caveats.rst | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/docs/overview/caveats.rst b/docs/overview/caveats.rst index 9e1f69810..49c97367e 100644 --- a/docs/overview/caveats.rst +++ b/docs/overview/caveats.rst @@ -6,27 +6,25 @@ Caveats Authority of Documentation -------------------------- -FFmpeg_ is extremely complex, and the PyAV developers have not been successful in making it 100% clear to themselves in all aspects. Our understanding of how it works and how to work with it is via reading the docs, digging through the source, perfoming experiments, and hearing from users where PyAV isn't doing the right thing. +FFmpeg_ is extremely complex, and the PyAV developers have not been successful in making it 100% clear to themselves in all aspects. Our understanding of how it works and how to work with it is via reading the docs, digging through the source, performing experiments, and hearing from users where PyAV isn't doing the right thing. -Only where this documentation is about the mechanics of PyAV can it be considered authoritative. Anywhere that we discuss something that is actually about the underlying FFmpeg libraries comes with the caveat that we can not always be 100% on it. - -It is, unfortunately, often on the user the understand and deal with the edge cases. We encourage you to bring them to our attention via GitHub_ so that we can try to make PyAV deal with it, but we can't always make it work. +Only where this documentation is about the mechanics of PyAV can it be considered authoritative. Anywhere that we discuss something that is about the underlying FFmpeg libraries comes with the caveat that we can not be 100% sure on it. It is, unfortunately, often on the user to understand and deal with edge cases. We encourage you to bring them to our attention via GitHub_ so that we can try to make PyAV deal with it. Unsupported Features -------------------- -Our goal is to provide all of the features that make sense for the contexts that PyAV would be used in. If there is something missing, please reach out on Gitter_ or open a feature request on GitHub_ (or even better a pull request). Your request will be more likely to be addressed if you can point to the relevant `FFmpeg API documentation `__. +Our goal is to provide all of the features that make sense for the contexts that PyAV would be used in. If there is something missing, please reach out on GitHub_ or open a feature request (or even better a pull request). Your request will be more likely to be addressed if you can point to the relevant `FFmpeg API documentation `__. -Sub-Interpeters ---------------- +Sub-Interpreters +---------------- Since we rely upon C callbacks in a few locations, PyAV is not fully compatible with sub-interpreters. Users have experienced lockups in WSGI web applications, for example. This is due to the ``PyGILState_Ensure`` calls made by Cython in a C callback from FFmpeg. If this is called in a thread that was not started by Python, it is very likely to break. There is no current instrumentation to detect such events. -The two main features that are able to cause lockups are: +The two main features that can cause lockups are: 1. Python IO (passing a file-like object to ``av.open``). While this is in theory possible, so far it seems like the callbacks are made in the calling thread, and so are safe. @@ -42,10 +40,9 @@ PyAV currently has a number of reference cycles that make it more difficult for Until we resolve this issue, you should explicitly call :meth:`.Container.close` or use the container as a context manager:: - with av.open(path) as fh: + with av.open(path) as container: # Do stuff with it. .. _FFmpeg: https://ffmpeg.org/ -.. _Gitter: https://app.gitter.im/#/room/#PyAV-Org_User-Help:gitter.im -.. _GitHub: https://github.com/PyAV-Org/pyav +.. _GitHub: https://github.com/PyAV-Org/PyAV From d9624b9d1d9868d83e719283e38f9f58b1c7f13e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 17 Oct 2024 19:10:59 -0400 Subject: [PATCH 139/365] Use GitHub Discussions for bug reports --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 52395dbe8..935d233aa 100644 --- a/setup.py +++ b/setup.py @@ -221,7 +221,7 @@ def parse_cflags(raw_flags): long_description_content_type="text/markdown", license="BSD", project_urls={ - "Bug Reports": "https://github.com/PyAV-Org/PyAV/issues", + "Bug Reports": "https://github.com/PyAV-Org/PyAV/discussions/new?category=4-bugs", "Documentation": "https://pyav.basswood-io.com", "Download": "https://pypi.org/project/av", }, From 26e46306e088d61b1433bf855cfee919af39ee27 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 18 Oct 2024 21:43:35 -0400 Subject: [PATCH 140/365] Remove deprecated `avcodec_close()` --- av/codec/context.pxd | 1 + av/codec/context.pyx | 17 +++++++++-------- include/libavcodec/avcodec.pxd | 3 --- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/av/codec/context.pxd b/av/codec/context.pxd index 58bc6a07d..782a66f8f 100644 --- a/av/codec/context.pxd +++ b/av/codec/context.pxd @@ -13,6 +13,7 @@ cdef class CodecContext: # Whether AVCodecContext.extradata should be de-allocated upon destruction. cdef bint extradata_set + cdef bint _is_open # Used as a signal that this is within a stream, and also for us to access # that stream. This is set "manually" by the stream after constructing diff --git a/av/codec/context.pyx b/av/codec/context.pyx index b995bc9f7..c00dec1dd 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -130,6 +130,7 @@ cdef class CodecContext: self.options = {} self.stream_index = -1 # This is set by the container immediately. + self._is_open = False cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec): self.ptr = ptr @@ -217,7 +218,7 @@ cdef class CodecContext: @property def is_open(self): - return lib.avcodec_is_open(self.ptr) + return self._is_open @property def is_encoder(self): @@ -228,7 +229,7 @@ cdef class CodecContext: return lib.av_codec_is_decoder(self.ptr.codec) cpdef open(self, bint strict=True): - if lib.avcodec_is_open(self.ptr): + if self._is_open: if strict: raise ValueError("CodecContext is already open.") return @@ -241,7 +242,7 @@ cdef class CodecContext: self._set_default_time_base() err_check(lib.avcodec_open2(self.ptr, self.codec.ptr, &options.ptr)) - + self._is_open = True self.options = dict(options) cdef _set_default_time_base(self): @@ -249,17 +250,17 @@ cdef class CodecContext: self.ptr.time_base.den = lib.AV_TIME_BASE cpdef close(self, bint strict=True): - if not lib.avcodec_is_open(self.ptr): + if not self._is_open: if strict: raise ValueError("CodecContext is already closed.") return - err_check(lib.avcodec_close(self.ptr)) + self._is_open = False + lib.avcodec_free_context(&self.ptr) def __dealloc__(self): if self.ptr and self.extradata_set: lib.av_freep(&self.ptr.extradata) if self.ptr: - lib.avcodec_close(self.ptr) lib.avcodec_free_context(&self.ptr) if self.parser: lib.av_parser_close(self.parser) @@ -565,7 +566,7 @@ cdef class CodecContext: @thread_count.setter def thread_count(self, int value): - if lib.avcodec_is_open(self.ptr): + if self._is_open: raise RuntimeError("Cannot change thread_count after codec is open.") self.ptr.thread_count = value @@ -580,7 +581,7 @@ cdef class CodecContext: @thread_type.setter def thread_type(self, value): - if lib.avcodec_is_open(self.ptr): + if self._is_open: raise RuntimeError("Cannot change thread_type after codec is open.") self.ptr.thread_type = ThreadType[value].value diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index f414e59d4..71121e976 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -291,9 +291,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVDictionary **options, ) - cdef int avcodec_is_open(AVCodecContext *ctx ) - cdef int avcodec_close(AVCodecContext *ctx) - cdef int AV_NUM_DATA_POINTERS cdef enum AVPacketSideDataType: From b7818c0f94968b889854fc014a0370d6c8cc9066 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 18 Oct 2024 22:31:30 -0400 Subject: [PATCH 141/365] Ignore pillow typings --- tests/common.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/common.py b/tests/common.py index c67db04e5..ce1f2848b 100644 --- a/tests/common.py +++ b/tests/common.py @@ -5,6 +5,7 @@ import functools import os import types +import typing from typing import TYPE_CHECKING from unittest import TestCase as _Base @@ -113,13 +114,14 @@ def assertNdarraysEqual(a: np.ndarray, b: np.ndarray) -> None: assert False, f"ndarrays contents differ\n{msg}" +@typing.no_type_check def assertImagesAlmostEqual(a: Image, b: Image, epsilon: float = 0.1) -> None: import PIL.ImageFilter as ImageFilter assert a.size == b.size a = a.filter(ImageFilter.BLUR).getdata() b = b.filter(ImageFilter.BLUR).getdata() - for i, ax, bx in zip(range(len(a)), a, b): # type: ignore + for i, ax, bx in zip(range(len(a)), a, b): diff = sum(abs(ac / 256 - bc / 256) for ac, bc in zip(ax, bx)) / 3 assert diff < epsilon, f"images differed by {diff} at index {i}; {ax} {bx}" From 5bbd3b1207414135e551ca10be62880272350a70 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 20 Oct 2024 02:06:23 -0400 Subject: [PATCH 142/365] A bit cleaner, but doesn't fix my segfault --- av/container/output.pyx | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/av/container/output.pyx b/av/container/output.pyx index 9e344804c..a0a59c111 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -79,8 +79,7 @@ cdef class OutputContainer(Container): ) # Create new stream in the AVFormatContext, set AVCodecContext values. - lib.avformat_new_stream(self.ptr, codec) - cdef lib.AVStream *stream = self.ptr.streams[self.ptr.nb_streams - 1] + cdef lib.AVStream *stream = lib.avformat_new_stream(self.ptr, codec) cdef lib.AVCodecContext *codec_context = lib.avcodec_alloc_context3(codec) # Copy from the template. @@ -160,10 +159,11 @@ cdef class OutputContainer(Container): # Finalize and open all streams. cdef Stream stream for stream in self.streams: - ctx = stream.codec_context - if not ctx.is_open: + if ctx is None: + raise ValueError(f"Stream {stream.index} has no codec context") + if not ctx.is_open: for k, v in self.options.items(): ctx.options.setdefault(k, v) ctx.open() @@ -190,10 +190,7 @@ cdef class OutputContainer(Container): cdef _Dictionary all_options = Dictionary(self.options, self.container_options) cdef _Dictionary options = all_options.copy() - self.err_check(lib.avformat_write_header( - self.ptr, - &options.ptr - )) + self.err_check(lib.avformat_write_header(self.ptr, &options.ptr)) # Track option usage... for k in all_options: From 49b7c149edb4849407510ed835e3ed598dbec4e0 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 20 Oct 2024 03:27:56 -0400 Subject: [PATCH 143/365] Fix segfault when printing closed video_stream --- av/audio/stream.pyi | 7 +++++-- av/codec/context.pxd | 4 ++-- av/codec/context.pyx | 9 +++++++-- av/container/streams.pyi | 1 - av/video/codeccontext.pyx | 4 ++++ av/video/stream.pyi | 9 ++++++--- tests/test_streams.py | 35 +++++++++++++++++++++++++++++++++++ 7 files changed, 59 insertions(+), 10 deletions(-) diff --git a/av/audio/stream.pyi b/av/audio/stream.pyi index 443c85fa1..4ba4fe48a 100644 --- a/av/audio/stream.pyi +++ b/av/audio/stream.pyi @@ -18,6 +18,9 @@ class _Layout: class AudioStream(Stream): codec_context: AudioCodecContext + def encode(self, frame: AudioFrame | None = None) -> list[Packet]: ... + def decode(self, packet: Packet | None = None) -> list[AudioFrame]: ... + # From codec context frame_size: int sample_rate: int @@ -27,5 +30,5 @@ class AudioStream(Stream): type: Literal["audio"] format: _Format layout: _Layout - def encode(self, frame: AudioFrame | None = None) -> list[Packet]: ... - def decode(self, packet: Packet | None = None) -> list[AudioFrame]: ... + + def close(self, strict: bool = True) -> None: ... diff --git a/av/codec/context.pxd b/av/codec/context.pxd index 782a66f8f..0b891877a 100644 --- a/av/codec/context.pxd +++ b/av/codec/context.pxd @@ -49,8 +49,8 @@ cdef class CodecContext: cdef _setup_decoded_frame(self, Frame, Packet) # Implemented by base for the generic send/recv API. - # Note that the user cannot send without recieving. This is because - # _prepare_frames_for_encode may expand a frame into multiple (e.g. when + # Note that the user cannot send without receiving. This is because + # `_prepare_frames_for_encode` may expand a frame into multiple (e.g. when # resampling audio to a higher rate but with fixed size frames), and the # send/recv buffer may be limited to a single frame. Ergo, we need to flush # the buffer as often as possible. diff --git a/av/codec/context.pyx b/av/codec/context.pyx index c00dec1dd..64c934f05 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -193,10 +193,11 @@ cdef class CodecContext: @property def extradata(self): + if self.ptr is NULL: + return None if self.ptr.extradata_size > 0: return (self.ptr.extradata)[:self.ptr.extradata_size] - else: - return None + return None @extradata.setter def extradata(self, data): @@ -222,10 +223,14 @@ cdef class CodecContext: @property def is_encoder(self): + if self.ptr is NULL: + return False return lib.av_codec_is_encoder(self.ptr.codec) @property def is_decoder(self): + if self.ptr is NULL: + return False return lib.av_codec_is_decoder(self.ptr.codec) cpdef open(self, bint strict=True): diff --git a/av/container/streams.pyi b/av/container/streams.pyi index 5d8647afe..fbaf1b67f 100644 --- a/av/container/streams.pyi +++ b/av/container/streams.pyi @@ -16,7 +16,6 @@ class StreamContainer: other: tuple[Stream, ...] def __init__(self) -> None: ... - def add_stream(self, stream: Stream) -> None: ... def __len__(self) -> int: ... def __iter__(self) -> Iterator[Stream]: ... @overload diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.pyx index 5764c400b..3e55472f6 100644 --- a/av/video/codeccontext.pyx +++ b/av/video/codeccontext.pyx @@ -78,6 +78,8 @@ cdef class VideoCodecContext(CodecContext): @property def width(self): + if self.ptr is NULL: + return 0 return self.ptr.width @width.setter @@ -87,6 +89,8 @@ cdef class VideoCodecContext(CodecContext): @property def height(self): + if self.ptr is NULL: + return 0 return self.ptr.height @height.setter diff --git a/av/video/stream.pyi b/av/video/stream.pyi index 3028bd31c..ad2bbc056 100644 --- a/av/video/stream.pyi +++ b/av/video/stream.pyi @@ -17,6 +17,11 @@ class VideoStream(Stream): sample_aspect_ratio: Fraction | None display_aspect_ratio: Fraction | None codec_context: VideoCodecContext + + def encode(self, frame: VideoFrame | None = None) -> list[Packet]: ... + def encode_lazy(self, frame: VideoFrame | None = None) -> Iterator[Packet]: ... + def decode(self, packet: Packet | None = None) -> list[VideoFrame]: ... + # from codec context format: VideoFormat width: int @@ -36,6 +41,4 @@ class VideoStream(Stream): colorspace: int type: Literal["video"] - def encode(self, frame: VideoFrame | None = None) -> list[Packet]: ... - def encode_lazy(self, frame: VideoFrame | None = None) -> Iterator[Packet]: ... - def decode(self, packet: Packet | None = None) -> list[VideoFrame]: ... + def close(self, strict: bool = True) -> None: ... diff --git a/tests/test_streams.py b/tests/test_streams.py index b6097d537..9828278f0 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -35,6 +35,41 @@ def test_selection(self) -> None: data = container.streams.data[0] assert data == container.streams.best("data") + def test_printing_closed_video_stream(self) -> None: + input_ = av.open( + fate_suite("amv/MTV_high_res_320x240_sample_Penguin_Joke_MTV_from_WMV.amv") + ) + container = av.open("out.mkv", "w") + + video_stream = container.add_stream("h264", rate=30) + # encoder = video_stream.codec.name + "" + + video_stream.width = input_.streams.video[0].width + video_stream.height = input_.streams.video[0].height + video_stream.pix_fmt = "yuv420p" + + for frame in input_.decode(video=0): + container.mux(video_stream.encode(frame)) + break + + encoder = "libx264" + repr = f"{video_stream}" + assert repr.startswith(f"") + + # repr = f"{video_stream}" + # assert repr.startswith(f"") + + video_stream.close() + + repr = f"{video_stream}" + assert repr.startswith(f"") + + container.close() + input_.close() + # def test_side_data(self) -> None: # container = av.open(fate_suite("mov/displaymatrix.mov")) # video = container.streams.video[0] From 2dfa0c00e6117e4f2bf13fc0ba50e5ec550f1969 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 20 Oct 2024 04:48:40 -0400 Subject: [PATCH 144/365] Remove `CodecContext.close()` Use `del` instead. This fixes many null pointer dereferences. --- av/audio/stream.pyi | 2 -- av/codec/context.pxd | 1 - av/codec/context.pyi | 1 - av/codec/context.pyx | 7 ------- av/container/output.pyx | 3 +-- av/video/stream.pyi | 2 -- tests/test_streams.py | 15 ++------------- 7 files changed, 3 insertions(+), 28 deletions(-) diff --git a/av/audio/stream.pyi b/av/audio/stream.pyi index 4ba4fe48a..f92fb52ba 100644 --- a/av/audio/stream.pyi +++ b/av/audio/stream.pyi @@ -30,5 +30,3 @@ class AudioStream(Stream): type: Literal["audio"] format: _Format layout: _Layout - - def close(self, strict: bool = True) -> None: ... diff --git a/av/codec/context.pxd b/av/codec/context.pxd index 0b891877a..2aa71d2a5 100644 --- a/av/codec/context.pxd +++ b/av/codec/context.pxd @@ -30,7 +30,6 @@ cdef class CodecContext: # Public API. cpdef open(self, bint strict=?) - cpdef close(self, bint strict=?) cdef _set_default_time_base(self) diff --git a/av/codec/context.pyi b/av/codec/context.pyi index d2d03b7ab..43c26883c 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -99,7 +99,6 @@ class CodecContext: delay: bool def open(self, strict: bool = True) -> None: ... - def close(self, strict: bool = True) -> None: ... @staticmethod def create( codec: str | Codec, mode: Literal["r", "w"] | None = None diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 64c934f05..0a618933a 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -254,13 +254,6 @@ cdef class CodecContext: self.ptr.time_base.num = 1 self.ptr.time_base.den = lib.AV_TIME_BASE - cpdef close(self, bint strict=True): - if not self._is_open: - if strict: - raise ValueError("CodecContext is already closed.") - return - self._is_open = False - lib.avcodec_free_context(&self.ptr) def __dealloc__(self): if self.ptr and self.extradata_set: diff --git a/av/container/output.pyx b/av/container/output.pyx index a0a59c111..39755ac32 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -246,8 +246,7 @@ cdef class OutputContainer(Container): def close(self): for stream in self.streams: - if stream.codec_context: - stream.codec_context.close(strict=False) + del stream close_output(self) diff --git a/av/video/stream.pyi b/av/video/stream.pyi index ad2bbc056..f0cdd5eb4 100644 --- a/av/video/stream.pyi +++ b/av/video/stream.pyi @@ -40,5 +40,3 @@ class VideoStream(Stream): color_trc: int colorspace: int type: Literal["video"] - - def close(self, strict: bool = True) -> None: ... diff --git a/tests/test_streams.py b/tests/test_streams.py index 9828278f0..075a333b2 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -35,14 +35,14 @@ def test_selection(self) -> None: data = container.streams.data[0] assert data == container.streams.best("data") - def test_printing_closed_video_stream(self) -> None: + def test_printing_video_stream(self) -> None: input_ = av.open( fate_suite("amv/MTV_high_res_320x240_sample_Penguin_Joke_MTV_from_WMV.amv") ) container = av.open("out.mkv", "w") video_stream = container.add_stream("h264", rate=30) - # encoder = video_stream.codec.name + "" + encoder = video_stream.codec.name video_stream.width = input_.streams.video[0].width video_stream.height = input_.streams.video[0].height @@ -52,21 +52,10 @@ def test_printing_closed_video_stream(self) -> None: container.mux(video_stream.encode(frame)) break - encoder = "libx264" repr = f"{video_stream}" assert repr.startswith(f"") - # repr = f"{video_stream}" - # assert repr.startswith(f"") - - video_stream.close() - - repr = f"{video_stream}" - assert repr.startswith(f"") - container.close() input_.close() From 89d131ce8850b765cf115f6f01395bc6d523beb9 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 22 Oct 2024 00:33:01 -0400 Subject: [PATCH 145/365] Set timebases in open --- av/audio/codeccontext.pyx | 5 ----- av/codec/context.pxd | 5 +---- av/codec/context.pyi | 9 ++++++--- av/codec/context.pyx | 31 ++++++++++++++----------------- av/video/codeccontext.pyx | 4 ---- 5 files changed, 21 insertions(+), 33 deletions(-) diff --git a/av/audio/codeccontext.pyx b/av/audio/codeccontext.pyx index 1723cbac7..54319ddaf 100644 --- a/av/audio/codeccontext.pyx +++ b/av/audio/codeccontext.pyx @@ -11,14 +11,9 @@ cdef class AudioCodecContext(CodecContext): cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec): CodecContext._init(self, ptr, codec) - cdef _set_default_time_base(self): - self.ptr.time_base.num = 1 - self.ptr.time_base.den = self.ptr.sample_rate - cdef _prepare_frames_for_encode(self, Frame input_frame): cdef AudioFrame frame = input_frame - cdef bint allow_var_frame_size = self.ptr.codec.capabilities & lib.AV_CODEC_CAP_VARIABLE_FRAME_SIZE # Note that the resampler will simply return an input frame if there is diff --git a/av/codec/context.pxd b/av/codec/context.pxd index 2aa71d2a5..95d799a10 100644 --- a/av/codec/context.pxd +++ b/av/codec/context.pxd @@ -8,12 +8,10 @@ from av.packet cimport Packet cdef class CodecContext: - cdef lib.AVCodecContext *ptr # Whether AVCodecContext.extradata should be de-allocated upon destruction. cdef bint extradata_set - cdef bint _is_open # Used as a signal that this is within a stream, and also for us to access # that stream. This is set "manually" by the stream after constructing @@ -30,8 +28,7 @@ cdef class CodecContext: # Public API. cpdef open(self, bint strict=?) - - cdef _set_default_time_base(self) + cdef readonly bint is_open # Wraps both versions of the transcode API, returning lists. cpdef encode(self, Frame frame=?) diff --git a/av/codec/context.pyi b/av/codec/context.pyi index 43c26883c..d6573cc80 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -57,9 +57,6 @@ class Flags2(EnumFlag): class CodecContext: extradata: bytes | None extradata_size: int - is_open: bool - is_encoder: bool - is_decoder: bool name: str codec: Codec options: dict[str, str] @@ -98,6 +95,12 @@ class CodecContext: closed_gop: bool delay: bool + @property + def is_open(self) -> bool: ... + @property + def is_encoder(self) -> bool: ... + @property + def is_decoder(self) -> bool: ... def open(self, strict: bool = True) -> None: ... @staticmethod def create( diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 0a618933a..0170aae52 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -117,7 +117,6 @@ Flags2 = define_enum("Flags2", __name__, ( cdef class CodecContext: - @staticmethod def create(codec, mode=None): cdef Codec cy_codec = codec if isinstance(codec, Codec) else Codec(codec, mode) @@ -130,7 +129,7 @@ cdef class CodecContext: self.options = {} self.stream_index = -1 # This is set by the container immediately. - self._is_open = False + self.is_open = False cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec): self.ptr = ptr @@ -217,10 +216,6 @@ cdef class CodecContext: def extradata_size(self): return self.ptr.extradata_size - @property - def is_open(self): - return self._is_open - @property def is_encoder(self): if self.ptr is NULL: @@ -234,7 +229,7 @@ cdef class CodecContext: return lib.av_codec_is_decoder(self.ptr.codec) cpdef open(self, bint strict=True): - if self._is_open: + if self.is_open: if strict: raise ValueError("CodecContext is already open.") return @@ -242,19 +237,21 @@ cdef class CodecContext: cdef _Dictionary options = Dictionary() options.update(self.options or {}) - # Assert we have a time_base for encoders. if not self.ptr.time_base.num and self.is_encoder: - self._set_default_time_base() + if self.type == "video": + self.ptr.time_base.num = self.ptr.framerate.den or 1 + self.ptr.time_base.den = self.ptr.framerate.num or lib.AV_TIME_BASE + elif self.type == "audio": + self.ptr.time_base.num = 1 + self.ptr.time_base.den = self.ptr.sample_rate + else: + self.ptr.time_base.num = 1 + self.ptr.time_base.den = lib.AV_TIME_BASE err_check(lib.avcodec_open2(self.ptr, self.codec.ptr, &options.ptr)) - self._is_open = True + self.is_open = True self.options = dict(options) - cdef _set_default_time_base(self): - self.ptr.time_base.num = 1 - self.ptr.time_base.den = lib.AV_TIME_BASE - - def __dealloc__(self): if self.ptr and self.extradata_set: lib.av_freep(&self.ptr.extradata) @@ -564,7 +561,7 @@ cdef class CodecContext: @thread_count.setter def thread_count(self, int value): - if self._is_open: + if self.is_open: raise RuntimeError("Cannot change thread_count after codec is open.") self.ptr.thread_count = value @@ -579,7 +576,7 @@ cdef class CodecContext: @thread_type.setter def thread_type(self, value): - if self._is_open: + if self.is_open: raise RuntimeError("Cannot change thread_type after codec is open.") self.ptr.thread_type = ThreadType[value].value diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.pyx index 3e55472f6..05e35daa8 100644 --- a/av/video/codeccontext.pyx +++ b/av/video/codeccontext.pyx @@ -20,10 +20,6 @@ cdef class VideoCodecContext(CodecContext): self._build_format() self.encoded_frame_count = 0 - cdef _set_default_time_base(self): - self.ptr.time_base.num = self.ptr.framerate.den or 1 - self.ptr.time_base.den = self.ptr.framerate.num or lib.AV_TIME_BASE - cdef _prepare_frames_for_encode(self, Frame input): if not input: return [None] From 637b21aaec66b9467946c9df5d6f9061bb6e4b57 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 22 Oct 2024 01:08:17 -0400 Subject: [PATCH 146/365] Type stub what is read only --- av/codec/context.pxd | 17 ++++++----------- av/codec/context.pyi | 18 +++++++++++------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/av/codec/context.pxd b/av/codec/context.pxd index 95d799a10..42b2d63e7 100644 --- a/av/codec/context.pxd +++ b/av/codec/context.pxd @@ -13,32 +13,27 @@ cdef class CodecContext: # Whether AVCodecContext.extradata should be de-allocated upon destruction. cdef bint extradata_set - # Used as a signal that this is within a stream, and also for us to access - # that stream. This is set "manually" by the stream after constructing - # this object. + # Used as a signal that this is within a stream, and also for us to access that + # stream. This is set "manually" by the stream after constructing this object. cdef int stream_index cdef lib.AVCodecParserContext *parser - cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec) + # Public API. + cdef readonly bint is_open cdef readonly Codec codec - cdef public dict options - - # Public API. cpdef open(self, bint strict=?) - cdef readonly bint is_open # Wraps both versions of the transcode API, returning lists. cpdef encode(self, Frame frame=?) cpdef decode(self, Packet packet=?) - cpdef flush_buffers(self) # Used by both transcode APIs to setup user-land objects. - # TODO: Remove the `Packet` from `_setup_decoded_frame` (because flushing - # packets are bogus). It should take all info it needs from the context and/or stream. + # TODO: Remove the `Packet` from `_setup_decoded_frame` (because flushing packets + # are bogus). It should take all info it needs from the context and/or stream. cdef _prepare_and_time_rebase_frames_for_encode(self, Frame frame) cdef _prepare_frames_for_encode(self, Frame frame) cdef _setup_encoded_packet(self, Packet) diff --git a/av/codec/context.pyi b/av/codec/context.pyi index d6573cc80..d2336ce1d 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -55,23 +55,20 @@ class Flags2(EnumFlag): RO_FLUSH_NOOP: int class CodecContext: - extradata: bytes | None - extradata_size: int name: str - codec: Codec - options: dict[str, str] type: Literal["video", "audio", "data", "subtitle", "attachment"] + options: dict[str, str] profile: str | None + extradata: bytes | None time_base: Fraction codec_tag: str bit_rate: int | None - max_bit_rate: int | None bit_rate_tolerance: int thread_count: int thread_type: Any skip_frame: Any - # flgas + # flags unaligned: bool qscale: bool four_mv: bool @@ -93,7 +90,6 @@ class CodecContext: ac_pred: bool interlaced_me: bool closed_gop: bool - delay: bool @property def is_open(self) -> bool: ... @@ -101,6 +97,14 @@ class CodecContext: def is_encoder(self) -> bool: ... @property def is_decoder(self) -> bool: ... + @property + def codec(self) -> Codec: ... + @property + def max_bit_rate(self) -> int | None: ... + @property + def delay(self) -> bool: ... + @property + def extradata_size(self) -> int: ... def open(self, strict: bool = True) -> None: ... @staticmethod def create( From a4f218f2d964d5a4af66b34d7ca6b2ce2d7ee111 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 25 Oct 2024 21:17:13 -0400 Subject: [PATCH 147/365] Fix for #1599 This eventually applies the "workaround" automatically --- av/container/input.pyx | 1 + av/container/output.pyx | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/av/container/input.pyx b/av/container/input.pyx index 88cc95ee0..7246f8245 100644 --- a/av/container/input.pyx +++ b/av/container/input.pyx @@ -13,6 +13,7 @@ from av.dictionary import Dictionary cdef close_input(InputContainer self): + self.streams = StreamContainer() if self.input_was_opened: with nogil: # This causes `self.ptr` to be set to NULL. diff --git a/av/container/output.pyx b/av/container/output.pyx index 39755ac32..f1894a617 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -18,6 +18,7 @@ log = logging.getLogger(__name__) cdef close_output(OutputContainer self): + self.streams = StreamContainer() if self._started and not self._done: # We must only ever call av_write_trailer *once*, otherwise we get a # segmentation fault. Therefore no matter whether it succeeds or not @@ -245,9 +246,6 @@ cdef class OutputContainer(Container): return lib.avcodec_get_name(self.format.optr.subtitle_codec) def close(self): - for stream in self.streams: - del stream - close_output(self) def mux(self, packets): From ee30e4f24a49bf101072b751b0c39df14d36b851 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 29 Oct 2024 16:59:34 -0400 Subject: [PATCH 148/365] Remove unused `**kwargs` --- av/audio/frame.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/audio/frame.pyx b/av/audio/frame.pyx index 1e45842ea..dd905d152 100644 --- a/av/audio/frame.pyx +++ b/av/audio/frame.pyx @@ -164,7 +164,7 @@ cdef class AudioFrame(Frame): def rate(self, value): self.ptr.sample_rate = value - def to_ndarray(self, **kwargs): + def to_ndarray(self): """Get a numpy array of this frame. .. note:: Numpy must be installed. From 46396c6bdefa7009b347f103052a05591ecd6dd0 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 31 Oct 2024 00:42:44 -0400 Subject: [PATCH 149/365] Better err messages for audio frames --- av/audio/frame.pyx | 8 +++++--- av/utils.pxd | 3 --- av/utils.pyx | 8 -------- av/video/frame.pxd | 2 -- av/video/frame.pyx | 17 ++++++++--------- tests/test_audioframe.py | 3 ++- tests/test_python_io.py | 1 - 7 files changed, 15 insertions(+), 27 deletions(-) diff --git a/av/audio/frame.pyx b/av/audio/frame.pyx index dd905d152..14356cb4e 100644 --- a/av/audio/frame.pyx +++ b/av/audio/frame.pyx @@ -2,7 +2,7 @@ from av.audio.format cimport get_audio_format from av.audio.layout cimport get_audio_layout from av.audio.plane cimport AudioPlane from av.error cimport err_check -from av.utils cimport check_ndarray, check_ndarray_shape +from av.utils cimport check_ndarray cdef object _cinit_bypass_sentinel @@ -109,10 +109,12 @@ cdef class AudioFrame(Frame): nb_channels = AudioLayout(layout).nb_channels check_ndarray(array, dtype, 2) if AudioFormat(format).is_planar: - check_ndarray_shape(array, array.shape[0] == nb_channels) + if array.shape[0] != nb_channels: + raise ValueError(f"Expected planar `array.shape[0]` to equal `{nb_channels}` but got `{array.shape[0]}`") samples = array.shape[1] else: - check_ndarray_shape(array, array.shape[0] == 1) + if array.shape[0] != 1: + raise ValueError(f"Expected packed `array.shape[0]` to equal `1` but got `{array.shape[0]}`") samples = array.shape[1] // nb_channels frame = AudioFrame(format=format, layout=layout, samples=samples) diff --git a/av/utils.pxd b/av/utils.pxd index 7f342c05b..9aeb4a2fb 100644 --- a/av/utils.pxd +++ b/av/utils.pxd @@ -5,11 +5,8 @@ from libc.stdint cimport uint64_t cdef dict avdict_to_dict(lib.AVDictionary *input, str encoding, str errors) cdef dict_to_avdict(lib.AVDictionary **dst, dict src, str encoding, str errors) - cdef object avrational_to_fraction(const lib.AVRational *input) cdef void to_avrational(object frac, lib.AVRational *input) - cdef check_ndarray(object array, object dtype, int ndim) -cdef check_ndarray_shape(object array, bint ok) cdef flag_in_bitfield(uint64_t bitfield, uint64_t flag) diff --git a/av/utils.pyx b/av/utils.pyx index 23f2d10b9..190bbf4d7 100644 --- a/av/utils.pyx +++ b/av/utils.pyx @@ -66,14 +66,6 @@ cdef check_ndarray(object array, object dtype, int ndim): raise ValueError(f"Expected numpy array with ndim `{ndim}` but got `{array.ndim}`") -cdef check_ndarray_shape(object array, bint ok): - """ - Check a numpy array has the expected shape. - """ - if not ok: - raise ValueError(f"Unexpected numpy array shape `{array.shape}`") - - cdef flag_in_bitfield(uint64_t bitfield, uint64_t flag): # Not every flag exists in every version of FFMpeg, so we define them to 0. if not flag: diff --git a/av/video/frame.pxd b/av/video/frame.pxd index 53a154a9c..779b23977 100644 --- a/av/video/frame.pxd +++ b/av/video/frame.pxd @@ -7,14 +7,12 @@ from av.video.reformatter cimport VideoReformatter cdef class VideoFrame(Frame): - # This is the buffer that is used to back everything in the AVFrame. # We don't ever actually access it directly. cdef uint8_t *_buffer cdef object _np_buffer cdef VideoReformatter reformatter - cdef readonly VideoFormat format cdef _init(self, lib.AVPixelFormat format, unsigned int width, unsigned int height) diff --git a/av/video/frame.pyx b/av/video/frame.pyx index a7c794a55..4fb463c8c 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -4,7 +4,7 @@ from libc.stdint cimport uint8_t from av.enum cimport define_enum from av.error cimport err_check -from av.utils cimport check_ndarray, check_ndarray_shape +from av.utils cimport check_ndarray from av.video.format cimport get_pix_fmt, get_video_format from av.video.plane cimport VideoPlane @@ -92,6 +92,11 @@ cdef useful_array(VideoPlane plane, unsigned int bytes_per_pixel=1, str dtype="u return arr.view(np.dtype(dtype)) +cdef check_ndarray_shape(object array, bint ok): + if not ok: + raise ValueError(f"Unexpected numpy array shape `{array.shape}`") + + cdef class VideoFrame(Frame): def __cinit__(self, width=0, height=0, format="yuv420p"): if width is _cinit_bypass_sentinel: @@ -109,18 +114,12 @@ cdef class VideoFrame(Frame): self.ptr.height = height self.ptr.format = format - # Allocate the buffer for the video frame. - # # We enforce aligned buffers, otherwise `sws_scale` can perform # poorly or even cause out-of-bounds reads and writes. if width and height: res = lib.av_image_alloc( - self.ptr.data, - self.ptr.linesize, - width, - height, - format, - 16) + self.ptr.data, self.ptr.linesize, width, height, format, 16 + ) self._buffer = self.ptr.data[0] if res: diff --git a/tests/test_audioframe.py b/tests/test_audioframe.py index 7211ad023..d67e181ce 100644 --- a/tests/test_audioframe.py +++ b/tests/test_audioframe.py @@ -124,7 +124,8 @@ def test_from_ndarray_value_error() -> None: # incorrect shape array = np.zeros(shape=(2, 160), dtype="f4") with pytest.raises( - ValueError, match=escape("Unexpected numpy array shape `(2, 160)`") + ValueError, + match=escape("Expected packed `array.shape[0]` to equal `1` but got `2`"), ) as cm: AudioFrame.from_ndarray(array, format="flt", layout="mono") diff --git a/tests/test_python_io.py b/tests/test_python_io.py index d657404d9..3169ae047 100644 --- a/tests/test_python_io.py +++ b/tests/test_python_io.py @@ -24,7 +24,6 @@ def __init__(self, obj: object) -> None: self._log: list[tuple[str, object]] = [] def __getattr__(self, name: str) -> object: - def _method(name: str, meth: Callable, *args, **kwargs) -> object: self._log.append((name, args)) return meth(*args, **kwargs) From 2f9a2797406f4b85e69da8fd71e481f0d2ec6cea Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 2 Nov 2024 23:06:29 +0100 Subject: [PATCH 150/365] Add __all__ to audio and video packages --- av/audio/__init__.pyi | 4 ++++ av/video/__init__.pyi | 2 ++ 2 files changed, 6 insertions(+) create mode 100644 av/audio/__init__.pyi diff --git a/av/audio/__init__.pyi b/av/audio/__init__.pyi new file mode 100644 index 000000000..73f2eebdd --- /dev/null +++ b/av/audio/__init__.pyi @@ -0,0 +1,4 @@ +from .frame import AudioFrame +from .stream import AudioStream + +__all__ = ("AudioFrame", "AudioStream") diff --git a/av/video/__init__.pyi b/av/video/__init__.pyi index 4a25d8837..8fa8fe7e5 100644 --- a/av/video/__init__.pyi +++ b/av/video/__init__.pyi @@ -1,2 +1,4 @@ from .frame import VideoFrame from .stream import VideoStream + +__all__ = ("VideoFrame", "VideoStream") From f8a07d0b83e2ec1479572542c168b79d7551d817 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 3 Nov 2024 13:42:54 -0500 Subject: [PATCH 151/365] Add `add_stream_from_template()` method --- av/container/output.pyi | 15 +++++++-- av/container/output.pyx | 72 ++++++++++++++++++++++++++++++++++++----- tests/test_streams.py | 19 +++++++++++ 3 files changed, 96 insertions(+), 10 deletions(-) diff --git a/av/container/output.pyi b/av/container/output.pyi index 7891e17b5..c318d93e5 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -1,7 +1,6 @@ from fractions import Fraction -from typing import Literal, Sequence, overload +from typing import Literal, Sequence, TypeVar, overload -from av.audio.layout import AudioLayout from av.audio.stream import AudioStream from av.packet import Packet from av.stream import Stream @@ -9,6 +8,8 @@ from av.video.stream import VideoStream from .core import Container +_StreamT = TypeVar("_StreamT", bound=Stream, default=Stream) + class OutputContainer(Container): def __enter__(self) -> OutputContainer: ... @overload @@ -30,6 +31,15 @@ class OutputContainer(Container): **kwargs, ) -> VideoStream: ... @overload + def add_stream( + self, + codec_name: None = None, + rate: Fraction | int | None = None, + template: _StreamT | None = None, + options: dict[str, str] | None = None, + **kwargs, + ) -> _StreamT: ... + @overload def add_stream( self, codec_name: str | None = None, @@ -38,6 +48,7 @@ class OutputContainer(Container): options: dict[str, str] | None = None, **kwargs, ) -> Stream: ... + def add_stream_from_template(self, template: _StreamT, **kwargs) -> _StreamT: ... def start_encoding(self) -> None: ... def close(self) -> None: ... def mux(self, packets: Packet | Sequence[Packet]) -> None: ... diff --git a/av/container/output.pyx b/av/container/output.pyx index f1894a617..f80c6e55e 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -46,16 +46,15 @@ cdef class OutputContainer(Container): def add_stream(self, codec_name=None, object rate=None, Stream template=None, options=None, **kwargs): """add_stream(codec_name, rate=None) - Create a new stream, and return it. + Creates a new stream from a codec name or a template, and returns it. - :param str codec_name: The name of a codec. - :param rate: The frame rate for video, and sample rate for audio. - Examples for video include ``24`` and ``Fraction(30000, 1001)``. - Examples for audio include ``48000`` and ``44100``. + :param codec_name: The name of a codec. + :type codec_name: str | Codec | None :param template: Copy codec from another :class:`~av.stream.Stream` instance. + :type template: :class:`~av.stream.Stream` | None :param dict options: Stream options. - :param \\**kwargs: Set attributes of the stream. - :returns: The new :class:`~av.stream.Stream`. + :param \\**kwargs: Set attributes for the stream. + :rtype: The new :class:`~av.stream.Stream`. """ @@ -76,7 +75,7 @@ cdef class OutputContainer(Container): # Assert that this format supports the requested codec. if not lib.avformat_query_codec(self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL): raise ValueError( - f"{self.format.name!r} format does not support {codec_name!r} codec" + f"{self.format.name!r} format does not support {codec_obj.name!r} codec" ) # Create new stream in the AVFormatContext, set AVCodecContext values. @@ -147,6 +146,63 @@ cdef class OutputContainer(Container): return py_stream + def add_stream_from_template(self, Stream template not None, **kwargs): + """ + Creates a new stream from a template. + + :param template: Copy codec from another :class:`~av.stream.Stream` instance. + :param \\**kwargs: Set attributes for the stream. + :rtype: The new :class:`~av.stream.Stream`. + + """ + + if not template.codec_context: + raise ValueError("template has no codec context") + + cdef Codec codec_obj = Codec(template.codec_context.codec.name, "w") + cdef const lib.AVCodec *codec = codec_obj.ptr + + # Assert that this format supports the requested codec. + if not lib.avformat_query_codec(self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL): + raise ValueError( + f"{self.format.name!r} format does not support {codec_obj.name!r} codec" + ) + + # Create new stream in the AVFormatContext, set AVCodecContext values. + cdef lib.AVStream *stream = lib.avformat_new_stream(self.ptr, codec) + cdef lib.AVCodecContext *codec_context = lib.avcodec_alloc_context3(codec) + + err_check(lib.avcodec_parameters_to_context(codec_context, template.ptr.codecpar)) + # Reset the codec tag assuming we are remuxing. + codec_context.codec_tag = 0 + + # Some formats want stream headers to be separate + if self.ptr.oformat.flags & lib.AVFMT_GLOBALHEADER: + codec_context.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER + + # Initialise stream codec parameters to populate the codec type. + # + # Subsequent changes to the codec context will be applied just before + # encoding starts in `start_encoding()`. + err_check(lib.avcodec_parameters_from_context(stream.codecpar, codec_context)) + + # Construct the user-land stream + cdef CodecContext py_codec_context = wrap_codec_context(codec_context, codec) + cdef Stream py_stream = wrap_stream(self, stream, py_codec_context) + self.streams.add_stream(py_stream) + + if template.type == "video": + py_stream.time_base = kwargs.pop("time_base", 1 / template.average_rate) + elif template.type == "audio": + py_stream.time_base = kwargs.pop("time_base", 1 / template.rate) + else: + py_stream.time_base = kwargs.pop("time_base", None) + + for k, v in kwargs.items(): + setattr(py_stream, k, v) + + return py_stream + cpdef start_encoding(self): """Write the file header! Called automatically.""" diff --git a/tests/test_streams.py b/tests/test_streams.py index 075a333b2..045745543 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -59,6 +59,25 @@ def test_printing_video_stream(self) -> None: container.close() input_.close() + def test_printing_video_stream2(self) -> None: + input_ = av.open(fate_suite("h264/interlaced_crop.mp4")) + input_stream = input_.streams.video[0] + container = av.open("out.mkv", "w") + + video_stream = container.add_stream_from_template(input_stream) + encoder = video_stream.codec.name + + for frame in input_.decode(video=0): + container.mux(video_stream.encode(frame)) + break + + repr = f"{video_stream}" + assert repr.startswith(f"") + + container.close() + input_.close() + # def test_side_data(self) -> None: # container = av.open(fate_suite("mov/displaymatrix.mov")) # video = container.streams.video[0] From dd389764b430647a74abbfed6dce4021b483dca9 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 3 Nov 2024 17:58:11 -0500 Subject: [PATCH 152/365] Remove `template` arg Use `add_stream_from_template()` is a replacement. This untested argument makes writing type stubs more complex than it needs to. --- CHANGELOG.rst | 5 +++++ av/container/output.pyi | 16 ++-------------- av/container/output.pyx | 29 +++++++++-------------------- 3 files changed, 16 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5becaf5f6..8e8077b79 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -23,6 +23,11 @@ Major: - Drop FFmpeg 6. - Remove ``AVError`` alias (use ``FFmpegError`` directly instead). +- Remove the `template` arg from ``OutputContainer.add_stream()``. + +Features: + +- Add ``OutputContainer.add_stream_from_template()`` by :gh-user:`WyattBlue` and :gh-user:`cdce8p`. v13.1.0 ------- diff --git a/av/container/output.pyi b/av/container/output.pyi index c318d93e5..97b67ee71 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -17,34 +17,22 @@ class OutputContainer(Container): self, codec_name: Literal["pcm_s16le", "aac", "mp3", "mp2"], rate: int | None = None, - template: None = None, options: dict[str, str] | None = None, **kwargs, ) -> AudioStream: ... @overload def add_stream( self, - codec_name: Literal["h264", "mpeg4", "png", "qtrle"], + codec_name: Literal["h264", "hevc", "mpeg4", "png", "gif", "qtrle"], rate: Fraction | int | None = None, - template: None = None, options: dict[str, str] | None = None, **kwargs, ) -> VideoStream: ... @overload def add_stream( self, - codec_name: None = None, + codec_name: str, rate: Fraction | int | None = None, - template: _StreamT | None = None, - options: dict[str, str] | None = None, - **kwargs, - ) -> _StreamT: ... - @overload - def add_stream( - self, - codec_name: str | None = None, - rate: Fraction | int | None = None, - template: Stream | None = None, options: dict[str, str] | None = None, **kwargs, ) -> Stream: ... diff --git a/av/container/output.pyx b/av/container/output.pyx index f80c6e55e..3e4c9dbb4 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -43,33 +43,28 @@ cdef class OutputContainer(Container): with nogil: lib.av_packet_free(&self.packet_ptr) - def add_stream(self, codec_name=None, object rate=None, Stream template=None, options=None, **kwargs): + def add_stream(self, codec_name, rate=None, dict options=None, **kwargs): """add_stream(codec_name, rate=None) - Creates a new stream from a codec name or a template, and returns it. + Creates a new stream from a codec name and returns it. :param codec_name: The name of a codec. - :type codec_name: str | Codec | None - :param template: Copy codec from another :class:`~av.stream.Stream` instance. - :type template: :class:`~av.stream.Stream` | None + :type codec_name: str | Codec :param dict options: Stream options. :param \\**kwargs: Set attributes for the stream. :rtype: The new :class:`~av.stream.Stream`. """ - if (codec_name is None and template is None) or (codec_name is not None and template is not None): - raise ValueError("needs one of codec_name or template") - cdef const lib.AVCodec *codec cdef Codec codec_obj - if codec_name is not None: - codec_obj = codec_name if isinstance(codec_name, Codec) else Codec(codec_name, "w") + if isinstance(codec_name, Codec): + if codec_name.mode != "w": + raise ValueError("codec_name must be an encoder.") + codec_obj = codec_name else: - if not template.codec_context: - raise ValueError("template has no codec context") - codec_obj = template.codec_context.codec + codec_obj = Codec(codec_name, "w") codec = codec_obj.ptr # Assert that this format supports the requested codec. @@ -82,14 +77,8 @@ cdef class OutputContainer(Container): cdef lib.AVStream *stream = lib.avformat_new_stream(self.ptr, codec) cdef lib.AVCodecContext *codec_context = lib.avcodec_alloc_context3(codec) - # Copy from the template. - if template is not None: - err_check(lib.avcodec_parameters_to_context(codec_context, template.ptr.codecpar)) - # Reset the codec tag assuming we are remuxing. - codec_context.codec_tag = 0 - # Now lets set some more sane video defaults - elif codec.type == lib.AVMEDIA_TYPE_VIDEO: + if codec.type == lib.AVMEDIA_TYPE_VIDEO: codec_context.pix_fmt = lib.AV_PIX_FMT_YUV420P codec_context.width = kwargs.pop("width", 640) codec_context.height = kwargs.pop("height", 480) From e86a39f01691d6dd5b639cde909ac547a9bc8e06 Mon Sep 17 00:00:00 2001 From: David Plowman Date: Thu, 13 Apr 2023 12:00:09 +0100 Subject: [PATCH 153/365] Add qmin and qmax parameters to the codec context Allows applications to control the min and max quantisers used for encoding. Also add a test to ensure the encoder is behaving as we expect. --- av/video/codeccontext.pyi | 2 ++ av/video/codeccontext.pyx | 30 +++++++++++++++++++ tests/test_encode.py | 63 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+) diff --git a/av/video/codeccontext.pyi b/av/video/codeccontext.pyi index 7172b50c2..da72053c4 100644 --- a/av/video/codeccontext.pyi +++ b/av/video/codeccontext.pyi @@ -26,6 +26,8 @@ class VideoCodecContext(CodecContext): color_primaries: int color_trc: int colorspace: int + qmin: int + qmax: int type: Literal["video"] def encode(self, frame: VideoFrame | None = None) -> list[Packet]: ... diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.pyx index 05e35daa8..d2f4c9e14 100644 --- a/av/video/codeccontext.pyx +++ b/av/video/codeccontext.pyx @@ -278,3 +278,33 @@ cdef class VideoCodecContext(CodecContext): @max_b_frames.setter def max_b_frames(self, value): self.ptr.max_b_frames = value + + @property + def qmin(self): + """ + The minimum quantiser value of an encoded stream. + + Wraps :ffmpeg:`AVCodecContext.qmin`. + + :type: int + """ + return self.ptr.qmin + + @qmin.setter + def qmin(self, value): + self.ptr.qmin = value + + @property + def qmax(self): + """ + The maximum quantiser value of an encoded stream. + + Wraps :ffmpeg:`AVCodecContext.qmax`. + + :type: int + """ + return self.ptr.qmax + + @qmax.setter + def qmax(self, value): + self.ptr.qmax = value diff --git a/tests/test_encode.py b/tests/test_encode.py index 89d2e3eb3..11d8a4ec9 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -386,3 +386,66 @@ def test_max_b_frames(self) -> None: file = encode_file_with_max_b_frames(max_b_frames) actual_max_b_frames = max_b_frame_run_in_file(file) assert actual_max_b_frames <= max_b_frames + + +def encode_frames_with_qminmax(frames: list, shape: tuple, qminmax: tuple) -> int: + """ + Encode a video with the given quantiser limits, and return how many enocded + bytes we made in total. + + frames: the frames to encode + shape: the (numpy) shape of the video frames + qminmax: two integers with 1 <= qmin <= 31 giving the min and max quantiser. + + Returns: total length of the encoded bytes. + """ + file = io.BytesIO() + container = av.open(file, mode="w", format="mp4") + stream = container.add_stream("h264", rate=30) + stream.height, stream.width, _ = shape + stream.pix_fmt = "yuv420p" + stream.codec_context.gop_size = 15 + stream.codec_context.qmin, stream.codec_context.qmax = qminmax + + bytes_encoded = 0 + for frame in frames: + for packet in stream.encode(frame): + bytes_encoded += packet.size + + for packet in stream.encode(): + bytes_encoded += packet.size + + container.close() + + return bytes_encoded + + +class TestQminQmaxEncoding(TestCase): + def test_qmin_qmax(self) -> None: + """ + Test that we can set the min and max quantisers, and the encoder is reacting + correctly to them. + + Can't see a way to get hold of the quantisers in a decoded video, so instead + we'll encode the same frames with decreasing quantisers, and check that the + file size increases (by a noticeable factor) each time. + """ + # Make a random - but repeatable - 10 frame video sequence. + np.random.seed(0) + frames = [] + shape = (480, 640, 3) + for _ in range(10): + frames.append( + av.VideoFrame.from_ndarray( + np.random.randint(0, 256, shape, dtype=np.uint8), format="rgb24" + ) + ) + + # Get the size of the encoded output for different quantisers. + quantisers = ((31, 31), (15, 15), (1, 1)) + sizes = [ + encode_frames_with_qminmax(frames, shape, qminmax) for qminmax in quantisers + ] + + factor = 1.3 # insist at least 30% larger each time + assert all(small * factor < large for small, large in zip(sizes, sizes[1:])) From d7a6eb998b65236ca95b53e3aef77824edeb16aa Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 7 Nov 2024 15:51:10 -0500 Subject: [PATCH 154/365] Upgrade mypy --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 1cd3c39a4..818c14c00 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ fate-suite: rsync -vrltLW rsync://fate-suite.ffmpeg.org/fate-suite/ tests/assets/fate-suite/ lint: - $(PIP) install -U black isort flake8 flake8-pyproject pillow numpy mypy==1.11.2 pytest + $(PIP) install -U black isort flake8 flake8-pyproject pillow numpy mypy==1.13.0 pytest black --check av examples tests setup.py flake8 av isort --check-only --diff av examples tests From 056486280fd0281ad22518fdebf30ab775bf15f3 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 2 Nov 2024 20:55:06 +0100 Subject: [PATCH 155/365] Make ErrorType a proper Enum --- av/enum.pyi | 7 ++++--- av/error.pyi | 59 +++++++++++++++++++++++++++------------------------- 2 files changed, 35 insertions(+), 31 deletions(-) diff --git a/av/enum.pyi b/av/enum.pyi index a4fe92e51..026a176b3 100644 --- a/av/enum.pyi +++ b/av/enum.pyi @@ -24,9 +24,10 @@ class EnumType(type): ) -> int | None: ... class EnumItem: - name: str - value: int - + @property + def name(self) -> str: ... + @property + def value(self) -> int: ... def __int__(self) -> int: ... def __hash__(self) -> int: ... def __reduce__( diff --git a/av/error.pyi b/av/error.pyi index 088f78d45..4c41f35e7 100644 --- a/av/error.pyi +++ b/av/error.pyi @@ -1,4 +1,5 @@ import builtins +from enum import Enum from .enum import EnumItem @@ -15,41 +16,43 @@ def err_check(res: int, filename: str | None = None) -> int: ... BUFFER_TOO_SMALL: EnumItem -class ErrorType(EnumItem): - BSF_NOT_FOUND: int - BUG: int - BUFFER_TOO_SMALL: int - DECODER_NOT_FOUND: int - DEMUXER_NOT_FOUND: int - ENCODER_NOT_FOUND: int - EOF: int - EXIT: int - EXTERNAL: int - FILTER_NOT_FOUND: int - INVALIDDATA: int - MUXER_NOT_FOUND: int - OPTION_NOT_FOUND: int - PATCHWELCOME: int - PROTOCOL_NOT_FOUND: int - UNKNOWN: int - EXPERIMENTAL: int - INPUT_CHANGED: int - OUTPUT_CHANGED: int - HTTP_BAD_REQUEST: int - HTTP_UNAUTHORIZED: int - HTTP_FORBIDDEN: int - HTTP_NOT_FOUND: int - HTTP_OTHER_4XX: int - HTTP_SERVER_ERROR: int - PYAV_CALLBACK: int +class ErrorType(EnumItem, Enum): + BSF_NOT_FOUND: EnumItem + BUG: EnumItem + BUFFER_TOO_SMALL: EnumItem + DECODER_NOT_FOUND: EnumItem + DEMUXER_NOT_FOUND: EnumItem + ENCODER_NOT_FOUND: EnumItem + EOF: EnumItem + EXIT: EnumItem + EXTERNAL: EnumItem + FILTER_NOT_FOUND: EnumItem + INVALIDDATA: EnumItem + MUXER_NOT_FOUND: EnumItem + OPTION_NOT_FOUND: EnumItem + PATCHWELCOME: EnumItem + PROTOCOL_NOT_FOUND: EnumItem + UNKNOWN: EnumItem + EXPERIMENTAL: EnumItem + INPUT_CHANGED: EnumItem + OUTPUT_CHANGED: EnumItem + HTTP_BAD_REQUEST: EnumItem + HTTP_UNAUTHORIZED: EnumItem + HTTP_FORBIDDEN: EnumItem + HTTP_NOT_FOUND: EnumItem + HTTP_OTHER_4XX: EnumItem + HTTP_SERVER_ERROR: EnumItem + PYAV_CALLBACK: EnumItem - tag: bytes + @property + def tag(self) -> bytes: ... class FFmpegError(Exception): errno: int strerror: str filename: str log: tuple[int, tuple[int, str, str] | None] + type: ErrorType def __init__( self, From cffe0763d7cee3d7605e98125b1012d1d1dfcd38 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 7 Nov 2024 17:55:55 -0500 Subject: [PATCH 156/365] Use IntEnums --- .github/workflows/smoke.yml | 2 +- Makefile | 2 +- av/video/reformatter.pyx | 77 ++++++++++++++++++------------------- docs/Makefile | 14 +++---- docs/api/codec.rst | 1 - docs/api/video.rst | 6 +++ docs/conf.py | 54 ++++++++++++++++++-------- scripts/build | 2 +- tests/test_videoframe.py | 4 +- 9 files changed, 94 insertions(+), 68 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index c40464af5..ca6a3f15c 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -34,7 +34,7 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-latest, python: "3.9", ffmpeg: "7.1", extras: true} + - {os: ubuntu-latest, python: "3.12", ffmpeg: "7.1", extras: true} - {os: ubuntu-latest, python: "3.9", ffmpeg: "7.0.2"} - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "7.1"} - {os: macos-14, python: "3.9", ffmpeg: "7.1"} diff --git a/Makefile b/Makefile index 818c14c00..2afb2e4c3 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ default: build build: - $(PIP) install --upgrade cython + $(PIP) install -U cython setuptools CFLAGS=$(CFLAGS) LDFLAGS=$(LDFLAGS) $(PYTHON) setup.py build_ext --inplace --debug clean: diff --git a/av/video/reformatter.pyx b/av/video/reformatter.pyx index 624454f0c..7e2b4c022 100644 --- a/av/video/reformatter.pyx +++ b/av/video/reformatter.pyx @@ -1,50 +1,49 @@ cimport libav as lib from libc.stdint cimport uint8_t -from av.enum cimport define_enum from av.error cimport err_check from av.video.format cimport VideoFormat from av.video.frame cimport alloc_video_frame -Interpolation = define_enum("Interpolation", __name__, ( - ("FAST_BILINEAR", lib.SWS_FAST_BILINEAR, "Fast bilinear"), - ("BILINEAR", lib.SWS_BILINEAR, "Bilinear"), - ("BICUBIC", lib.SWS_BICUBIC, "Bicubic"), - ("X", lib.SWS_X, "Experimental"), - ("POINT", lib.SWS_POINT, "Nearest neighbor / point"), - ("AREA", lib.SWS_AREA, "Area averaging"), - ("BICUBLIN", lib.SWS_BICUBLIN, "Luma bicubic / chroma bilinear"), - ("GAUSS", lib.SWS_GAUSS, "Gaussian"), - ("SINC", lib.SWS_SINC, "Sinc"), - ("LANCZOS", lib.SWS_LANCZOS, "Lanczos"), - ("SPLINE", lib.SWS_SPLINE, "Bicubic spline"), -)) - -Colorspace = define_enum("Colorspace", __name__, ( - ("ITU709", lib.SWS_CS_ITU709), - ("FCC", lib.SWS_CS_FCC), - ("ITU601", lib.SWS_CS_ITU601), - ("ITU624", lib.SWS_CS_ITU624), - ("SMPTE170M", lib.SWS_CS_SMPTE170M), - ("SMPTE240M", lib.SWS_CS_SMPTE240M), - ("DEFAULT", lib.SWS_CS_DEFAULT), - +from enum import IntEnum + + +class Interpolation(IntEnum): + FAST_BILINEAR: "Fast bilinear" = lib.SWS_FAST_BILINEAR + BILINEAR: "Bilinear" = lib.SWS_BILINEAR + BICUBIC: "Bicubic" = lib.SWS_BICUBIC + X: "Experimental" = lib.SWS_X + POINT: "Nearest neighbor / point" = lib.SWS_POINT + AREA: "Area averaging" = lib.SWS_AREA + BICUBLIN: "Luma bicubic / chroma bilinear" = lib.SWS_BICUBLIN + GAUSS: "Gaussian" = lib.SWS_GAUSS + SINC: "Sinc" = lib.SWS_SINC + LANCZOS: "Bicubic spline" = lib.SWS_LANCZOS + + +class Colorspace(IntEnum): + ITU709 = lib.SWS_CS_ITU709 + FCC = lib.SWS_CS_FCC + ITU601 = lib.SWS_CS_ITU601 + ITU624 = lib.SWS_CS_ITU624 + SMPTE170M = lib.SWS_CS_SMPTE170M + SMPTE240M = lib.SWS_CS_SMPTE240M + DEFAULT = lib.SWS_CS_DEFAULT # Lowercase for b/c. - ("itu709", lib.SWS_CS_ITU709), - ("fcc", lib.SWS_CS_FCC), - ("itu601", lib.SWS_CS_ITU601), - ("itu624", lib.SWS_CS_SMPTE170M), - ("smpte240", lib.SWS_CS_SMPTE240M), - ("default", lib.SWS_CS_DEFAULT), - -)) - -ColorRange = define_enum("ColorRange", __name__, ( - ("UNSPECIFIED", lib.AVCOL_RANGE_UNSPECIFIED, "Unspecified"), - ("MPEG", lib.AVCOL_RANGE_MPEG, "MPEG (limited) YUV range, 219*2^(n-8)"), - ("JPEG", lib.AVCOL_RANGE_JPEG, "JPEG (full) YUV range, 2^n-1"), - ("NB", lib.AVCOL_RANGE_NB, "Not part of ABI"), -)) + itu709 = lib.SWS_CS_ITU709 + fcc = lib.SWS_CS_FCC + itu601 = lib.SWS_CS_ITU601 + itu624 = lib.SWS_CS_SMPTE170M + smpte170m = lib.SWS_CS_SMPTE170M + smpte240m = lib.SWS_CS_SMPTE240M + default = lib.SWS_CS_DEFAULT + +class ColorRange(IntEnum): + UNSPECIFIED: "Unspecified" = lib.AVCOL_RANGE_UNSPECIFIED + MPEG: "MPEG (limited) YUV range, 219*2^(n-8)" = lib.AVCOL_RANGE_MPEG + JPEG: "JPEG (full) YUV range, 2^n-1" = lib.AVCOL_RANGE_JPEG + NB: "Not part of ABI" = lib.AVCOL_RANGE_NB + cdef class VideoReformatter: """An object for reformatting size and pixel format of :class:`.VideoFrame`. diff --git a/docs/Makefile b/docs/Makefile index e0662e90c..bb84c1dba 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,33 +1,31 @@ - SPHINXOPTS = SPHINXBUILD = sphinx-build BUILDDIR = _build FFMPEGDIR = _ffmpeg - PYAV_PIP ?= pip PIP := $(PYAV_PIP) - ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(SPHINXOPTS) . .PHONY: clean html open upload default default: html - TAGFILE := _build/doxygen/tagfile.xml -$(TAGFILE) : - git clone --depth=1 git://source.ffmpeg.org/ffmpeg.git $(FFMPEGDIR) - ./generate-tagfile --library $(FFMPEGDIR) -o $(TAGFILE) +$(TAGFILE): + @if [ ! -d "$(FFMPEGDIR)" ]; then \ + git clone --depth=1 git://source.ffmpeg.org/ffmpeg.git $(FFMPEGDIR); \ + fi + ./generate-tagfile --library $(FFMPEGDIR) -o $(TAGFILE) TEMPLATES := $(wildcard api/*.py development/*.py) RENDERED := $(TEMPLATES:%.py=_build/rst/%.rst) + _build/rst/%.rst: %.py $(TAGFILE) $(shell find ../include ../av -name '*.pyx' -or -name '*.pxd') @ mkdir -p $(@D) python $< > $@.tmp mv $@.tmp $@ - clean: rm -rf $(BUILDDIR) $(FFMPEGDIR) diff --git a/docs/api/codec.rst b/docs/api/codec.rst index bd1a6b5f0..770a271a4 100644 --- a/docs/api/codec.rst +++ b/docs/api/codec.rst @@ -65,7 +65,6 @@ Contexts .. automethod:: CodecContext.create .. automethod:: CodecContext.open -.. automethod:: CodecContext.close Attributes ~~~~~~~~~~ diff --git a/docs/api/video.rst b/docs/api/video.rst index 5e47b1db8..1c56788a1 100644 --- a/docs/api/video.rst +++ b/docs/api/video.rst @@ -117,3 +117,9 @@ Enums .. enumtable:: av.video.reformatter.Colorspace +.. autoclass:: av.video.reformatter.ColorRange + + Wraps the ``AVCOL*`` flags. + + .. enumtable:: av.video.reformatter.ColorRange + diff --git a/docs/conf.py b/docs/conf.py index 419b24238..59090193c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -238,24 +238,48 @@ def makerow(*texts): ) seen = set() - - for name, item in enum._by_name.items(): - if name.lower() in seen: - continue - seen.add(name.lower()) - - try: - attr = properties[item] - except KeyError: - if cls: + if hasattr(enum, "_by_name"): # Our custom enum class + enum_items = enum._by_name.items() + for name, item in enum_items: + if name.lower() in seen: + continue + seen.add(name.lower()) + + try: + attr = properties[item] + except KeyError: + if cls: + continue + attr = None + + value = f"0x{item.value:X}" + doc = item.__doc__ or "-" + tbody += makerow(attr, name, value, doc) + + return [table] + else: # Standard IntEnum + enum_items = [ + (name, item) + for name, item in vars(enum).items() + if isinstance(item, enum) + ] + for name, item in enum_items: + if name.lower() in seen: continue - attr = None + seen.add(name.lower()) + + try: + attr = properties[item] + except KeyError: + if cls: + continue + attr = None - value = f"0x{item.value:X}" - doc = item.__doc__ or "-" - tbody += makerow(attr, name, value, doc) + value = f"0x{item.value:X}" + doc = enum.__annotations__.get(name, "---")[1:-1] + tbody += makerow(attr, name, value, doc) - return [table] + return [table] doxylink = {} diff --git a/scripts/build b/scripts/build index 67b3836f2..7e27d7f74 100755 --- a/scripts/build +++ b/scripts/build @@ -21,6 +21,6 @@ which ffmpeg || exit 2 ffmpeg -version || exit 3 echo -$PYAV_PIP install -U cython 2> /dev/null +$PYAV_PIP install -U cython setuptools 2> /dev/null "$PYAV_PYTHON" scripts/comptime.py "$PYAV_PYTHON" setup.py config build_ext --inplace || exit 1 diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 8e14dcdf3..6396f1f45 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -634,11 +634,11 @@ def test_reformat_identity() -> None: def test_reformat_colorspace() -> None: # This is allowed. frame = VideoFrame(640, 480, "rgb24") - frame.reformat(src_colorspace=None, dst_colorspace="smpte240") + frame.reformat(src_colorspace=None, dst_colorspace="smpte240m") # I thought this was not allowed, but it seems to be. frame = VideoFrame(640, 480, "yuv420p") - frame.reformat(src_colorspace=None, dst_colorspace="smpte240") + frame.reformat(src_colorspace=None, dst_colorspace="smpte240m") def test_reformat_pixel_format_align() -> None: From 2341842e822b994778535b4d0613ec3c54cfb73c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 7 Nov 2024 21:38:08 -0500 Subject: [PATCH 157/365] Fix wrong colorspace lowercase enum --- CHANGELOG.rst | 7 +++++++ av/video/reformatter.pyx | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8e8077b79..5d034b223 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -24,10 +24,17 @@ Major: - Drop FFmpeg 6. - Remove ``AVError`` alias (use ``FFmpegError`` directly instead). - Remove the `template` arg from ``OutputContainer.add_stream()``. +- Remove ``CodecContext.close()`` because it's deprecated in ffmpeg. Features: - Add ``OutputContainer.add_stream_from_template()`` by :gh-user:`WyattBlue` and :gh-user:`cdce8p`. +- Add ``qmin`` and ``qmax`` parameters to the ``VideoCodecContext`` by :gh-user:`davidplowman` in (:pr:`1618`). + +Fixes: + +- Correct ``Colorspace``'s lowercase enums. +- Ensure streams in StreamContainer are released. Fixes :issue:`1599`. v13.1.0 ------- diff --git a/av/video/reformatter.pyx b/av/video/reformatter.pyx index 7e2b4c022..4511d08af 100644 --- a/av/video/reformatter.pyx +++ b/av/video/reformatter.pyx @@ -33,7 +33,7 @@ class Colorspace(IntEnum): itu709 = lib.SWS_CS_ITU709 fcc = lib.SWS_CS_FCC itu601 = lib.SWS_CS_ITU601 - itu624 = lib.SWS_CS_SMPTE170M + itu624 = lib.SWS_CS_ITU624 smpte170m = lib.SWS_CS_SMPTE170M smpte240m = lib.SWS_CS_SMPTE240M default = lib.SWS_CS_DEFAULT From 9fc1878b4d2cf9b313074f1dd972d146be52c6a7 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 10 Nov 2024 01:22:56 -0500 Subject: [PATCH 158/365] Move `key_frame` attribute to Frame class --- av/frame.pyi | 9 ++++++--- av/frame.pyx | 10 ++++++++++ av/video/frame.pyi | 1 - av/video/frame.pyx | 13 ------------- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/av/frame.pyi b/av/frame.pyi index 7c34e91bf..9af81dcfe 100644 --- a/av/frame.pyi +++ b/av/frame.pyi @@ -9,10 +9,13 @@ class SideData(TypedDict, total=False): class Frame: dts: int | None pts: int | None - time: float | None time_base: Fraction - is_corrupt: bool side_data: SideData opaque: object - + @property + def time(self) -> float | None: ... + @property + def is_corrupt(self) -> bool: ... + @property + def key_frame(self) -> bool: ... def make_writable(self) -> None: ... diff --git a/av/frame.pyx b/av/frame.pyx index a8871fd8f..1084ca45e 100644 --- a/av/frame.pyx +++ b/av/frame.pyx @@ -132,6 +132,16 @@ cdef class Frame: """ return self.ptr.decode_error_flags != 0 or bool(self.ptr.flags & lib.AV_FRAME_FLAG_CORRUPT) + @property + def key_frame(self): + """Is this frame a key frame? + + Wraps :ffmpeg:`AVFrame.key_frame`. + + """ + return bool(self.ptr.key_frame) + + @property def side_data(self): if self._side_data is None: diff --git a/av/video/frame.pyi b/av/video/frame.pyi index 29f961ba4..3300e8607 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -32,7 +32,6 @@ class VideoFrame(Frame): planes: tuple[VideoPlane, ...] width: int height: int - key_frame: bool interlaced_frame: bool pict_type: int colorspace: int diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 4fb463c8c..bfc74c40a 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -170,23 +170,11 @@ cdef class VideoFrame(Frame): """Width of the image, in pixels.""" return self.ptr.width - @property def height(self): """Height of the image, in pixels.""" return self.ptr.height - - @property - def key_frame(self): - """Is this frame a key frame? - - Wraps :ffmpeg:`AVFrame.key_frame`. - - """ - return self.ptr.key_frame - - @property def interlaced_frame(self): """Is this frame an interlaced or progressive? @@ -196,7 +184,6 @@ cdef class VideoFrame(Frame): """ return self.ptr.interlaced_frame - @property def pict_type(self): """One of :class:`.PictureType`. From 20a526231cd5b543a926a6a0a8e36312da86a447 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 10 Nov 2024 02:02:08 -0500 Subject: [PATCH 159/365] Use flags instead of deprecated properties --- av/frame.pyx | 2 +- av/video/frame.pyx | 7 ++----- include/libavcodec/avcodec.pxd | 10 +++++----- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/av/frame.pyx b/av/frame.pyx index 1084ca45e..57681bbcd 100644 --- a/av/frame.pyx +++ b/av/frame.pyx @@ -139,7 +139,7 @@ cdef class Frame: Wraps :ffmpeg:`AVFrame.key_frame`. """ - return bool(self.ptr.key_frame) + return bool(self.ptr.flags & lib.AV_FRAME_FLAG_KEY) @property diff --git a/av/video/frame.pyx b/av/video/frame.pyx index bfc74c40a..e5cd523b5 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -177,12 +177,9 @@ cdef class VideoFrame(Frame): @property def interlaced_frame(self): - """Is this frame an interlaced or progressive? + """Is this frame an interlaced or progressive?""" - Wraps :ffmpeg:`AVFrame.interlaced_frame`. - - """ - return self.ptr.interlaced_frame + return bool(self.ptr.flags & lib.AV_FRAME_FLAG_INTERLACED) @property def pict_type(self): diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 71121e976..d680e8b13 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -135,6 +135,9 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef enum: AV_FRAME_FLAG_CORRUPT + AV_FRAME_FLAG_KEY + AV_FRAME_FLAG_DISCARD + AV_FRAME_FLAG_INTERLACED cdef enum: FF_COMPLIANCE_VERY_STRICT @@ -368,19 +371,16 @@ cdef extern from "libavcodec/avcodec.h" nogil: uint8_t **extended_data int format # Should be AVPixelFormat or AVSampleFormat - int key_frame # 0 or 1. AVPictureType pict_type - int interlaced_frame # 0 or 1. - int width int height int nb_side_data AVFrameSideData **side_data - int nb_samples # Audio samples - int sample_rate # Audio Sample rate + int nb_samples + int sample_rate AVChannelLayout ch_layout From 2ec851344dd7c9f289578814c0685c09aaeb0416 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 12 Nov 2024 00:41:53 -0500 Subject: [PATCH 160/365] Remove package section --- docs/overview/installation.rst | 33 +-------------------------------- 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/docs/overview/installation.rst b/docs/overview/installation.rst index 1e57c8bef..419592530 100644 --- a/docs/overview/installation.rst +++ b/docs/overview/installation.rst @@ -11,37 +11,6 @@ Binary wheels are provided on PyPI for Linux, MacOS, and Windows linked against pip install av -Currently FFmpeg 6.1.1 is used with the following features enabled for all platforms: - -- fontconfig -- gmp -- libaom -- libass -- libbluray -- libdav1d -- libfreetype -- libmp3lame -- libopencore-amrnb -- libopencore-amrwb -- libopenjpeg -- libopus -- libspeex -- libtwolame -- libvorbis -- libvpx -- libx264 -- libx265 -- libxml2 -- libxvid -- lzma -- zlib - -The following additional features are also enabled on Linux: - -- gnutls -- libxcb - - Conda ----- @@ -55,7 +24,7 @@ See the `Conda quick install `_ docs t Bring your own FFmpeg --------------------- -PyAV can also be compiled against your own build of FFmpeg (version ``6.0`` or higher). You can force installing PyAV from source by running: +PyAV can also be compiled against your own build of FFmpeg (version ``7.0`` or higher). You can force installing PyAV from source by running: .. code-block:: bash From 850f1152d7dec2108dc606491c3a8d012678f55e Mon Sep 17 00:00:00 2001 From: davidplowman <38045873+davidplowman@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:20:52 +0000 Subject: [PATCH 161/365] Allow the profile of a codec to be set as well as queried The `profile` property of a stream can now be set. To help applications find appropriate profiles, a `profiles` property has been added which lists the available profile names. --- av/codec/context.pyi | 2 ++ av/codec/context.pyx | 49 ++++++++++++++++++++++++++++++++-- av/stream.pyi | 1 + av/stream.pyx | 12 +++++++++ include/libavcodec/avcodec.pxd | 15 ++++++----- tests/test_encode.py | 26 ++++++++++++++++++ 6 files changed, 96 insertions(+), 9 deletions(-) diff --git a/av/codec/context.pyi b/av/codec/context.pyi index d2336ce1d..8c50ba456 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -59,6 +59,8 @@ class CodecContext: type: Literal["video", "audio", "data", "subtitle", "attachment"] options: dict[str, str] profile: str | None + @property + def profiles(self) -> list[str]: ... extradata: bytes | None time_base: Fraction codec_tag: str diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 0170aae52..a1e95048c 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -497,10 +497,55 @@ cdef class CodecContext: def type(self): return self.codec.type + @property + def profiles(self): + """ + List the available profiles for this stream. + + :type: list[str] + """ + ret = [] + if not self.ptr.codec or not self.codec.desc or not self.codec.desc.profiles: + return ret + + # Profiles are always listed in the codec descriptor, but not necessarily in + # the codec itself. So use the descriptor here. + desc = self.codec.desc + cdef int i = 0 + while desc.profiles[i].profile != lib.FF_PROFILE_UNKNOWN: + ret.append(desc.profiles[i].name) + i += 1 + + return ret + @property def profile(self): - if self.ptr.codec and lib.av_get_profile_name(self.ptr.codec, self.ptr.profile): - return lib.av_get_profile_name(self.ptr.codec, self.ptr.profile) + if not self.ptr.codec or not self.codec.desc or not self.codec.desc.profiles: + return + + # Profiles are always listed in the codec descriptor, but not necessarily in + # the codec itself. So use the descriptor here. + desc = self.codec.desc + cdef int i = 0 + while desc.profiles[i].profile != lib.FF_PROFILE_UNKNOWN: + if desc.profiles[i].profile == self.ptr.profile: + return desc.profiles[i].name + i += 1 + + @profile.setter + def profile(self, value): + if not self.codec or not self.codec.desc or not self.codec.desc.profiles: + return + + # Profiles are always listed in the codec descriptor, but not necessarily in + # the codec itself. So use the descriptor here. + desc = self.codec.desc + cdef int i = 0 + while desc.profiles[i].profile != lib.FF_PROFILE_UNKNOWN: + if desc.profiles[i].name == value: + self.ptr.profile = desc.profiles[i].profile + return + i += 1 @property def time_base(self): diff --git a/av/stream.pyi b/av/stream.pyi index 89f49c12f..8c48f3ac7 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -19,6 +19,7 @@ class Stream: codec_context: CodecContext metadata: dict[str, str] id: int + profiles: list[str] profile: str index: int time_base: Fraction | None diff --git a/av/stream.pyx b/av/stream.pyx index 19ac8e703..4c450d283 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -145,6 +145,18 @@ cdef class Stream: else: self.ptr.id = value + @property + def profiles(self): + """ + List the available profiles for this stream. + + :type: list[str] + """ + if self.codec_context: + return self.codec_context.profiles + else: + return [] + @property def profile(self): """ diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index d680e8b13..54fb2293c 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -146,6 +146,9 @@ cdef extern from "libavcodec/avcodec.h" nogil: FF_COMPLIANCE_UNOFFICIAL FF_COMPLIANCE_EXPERIMENTAL + cdef enum: + FF_PROFILE_UNKNOWN = -99 + cdef enum AVCodecID: AV_CODEC_ID_NONE AV_CODEC_ID_MPEG2VIDEO @@ -178,12 +181,17 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef int av_codec_is_encoder(AVCodec*) cdef int av_codec_is_decoder(AVCodec*) + cdef struct AVProfile: + int profile + char *name + cdef struct AVCodecDescriptor: AVCodecID id char *name char *long_name int props char **mime_types + AVProfile *profiles AVCodecDescriptor* avcodec_descriptor_get(AVCodecID) @@ -266,13 +274,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef AVClass* avcodec_get_class() - cdef struct AVCodecDescriptor: - AVCodecID id - AVMediaType type - char *name - char *long_name - int props - cdef AVCodec* avcodec_find_decoder(AVCodecID id) cdef AVCodec* avcodec_find_encoder(AVCodecID id) diff --git a/tests/test_encode.py b/tests/test_encode.py index 11d8a4ec9..90d30dfce 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -449,3 +449,29 @@ def test_qmin_qmax(self) -> None: factor = 1.3 # insist at least 30% larger each time assert all(small * factor < large for small, large in zip(sizes, sizes[1:])) + + +class TestProfiles(TestCase): + def test_profiles(self) -> None: + """ + Test that we can set different encoder profiles. + """ + # Let's try a video and an audio codec. + file = io.BytesIO() + codecs = ( + ("h264", 30), + ("aac", 48000), + ) + + for codec_name, rate in codecs: + print("Testing:", codec_name) + container = av.open(file, mode="w", format="mp4") + stream = container.add_stream(codec_name, rate=rate) + assert len(stream.profiles) >= 1 # check that we're testing something! + + # It should be enough to test setting and retrieving the code. That means + # libav has recognised the profile and set it correctly. + for profile in stream.profiles: + stream.profile = profile + print("Set", profile, "got", stream.profile) + assert stream.profile == profile From 7a9712484a5fa5f060cfc4ef836487c2a7353aed Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 13 Nov 2024 04:05:05 -0500 Subject: [PATCH 162/365] Rethink container flags design --- av/container/core.pyi | 37 +++++++++-------- av/container/core.pyx | 97 ++++++++++--------------------------------- 2 files changed, 42 insertions(+), 92 deletions(-) diff --git a/av/container/core.pyi b/av/container/core.pyi index b031fb4b6..227a7d32a 100644 --- a/av/container/core.pyi +++ b/av/container/core.pyi @@ -1,9 +1,9 @@ +from enum import Flag from fractions import Fraction from pathlib import Path from types import TracebackType -from typing import Any, Callable, Literal, Type, overload +from typing import Any, Callable, ClassVar, Literal, Type, overload -from av.enum import EnumFlag from av.format import ContainerFormat from .input import InputContainer @@ -12,22 +12,22 @@ from .streams import StreamContainer Real = int | float | Fraction -class Flags(EnumFlag): - GENPTS: int - IGNIDX: int - NONBLOCK: int - IGNDTS: int - NOFILLIN: int - NOPARSE: int - NOBUFFER: int - CUSTOM_IO: int - DISCARD_CORRUPT: int - FLUSH_PACKETS: int - BITEXACT: int - SORT_DTS: int - FAST_SEEK: int - SHORTEST: int - AUTO_BSF: int +class Flags(Flag): + gen_pts: ClassVar[Flags] + ign_idx: ClassVar[Flags] + non_block: ClassVar[Flags] + ign_dts: ClassVar[Flags] + no_fillin: ClassVar[Flags] + no_parse: ClassVar[Flags] + no_buffer: ClassVar[Flags] + custom_io: ClassVar[Flags] + discard_corrupt: ClassVar[Flags] + flush_packets: ClassVar[Flags] + bitexact: ClassVar[Flags] + sort_dts: ClassVar[Flags] + fast_seek: ClassVar[Flags] + shortest: ClassVar[Flags] + auto_bsf: ClassVar[Flags] class Container: writeable: bool @@ -47,6 +47,7 @@ class Container: metadata: dict[str, str] open_timeout: Real | None read_timeout: Real | None + flags: int def __enter__(self) -> Container: ... def __exit__( diff --git a/av/container/core.pyx b/av/container/core.pyx index dab7c865e..563c79d21 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -3,6 +3,7 @@ from libc.stdint cimport int64_t import os import time +from enum import Flag from pathlib import Path cimport libav as lib @@ -11,7 +12,6 @@ from av.container.core cimport timeout_info from av.container.input cimport InputContainer from av.container.output cimport OutputContainer from av.container.pyio cimport pyio_close_custom_gil, pyio_close_gil -from av.enum cimport define_enum from av.error cimport err_check, stash_exception from av.format cimport build_container_format from av.utils cimport avdict_to_dict @@ -27,14 +27,12 @@ cdef object _cinit_sentinel = object() cdef object clock = getattr(time, "monotonic", time.time) cdef int interrupt_cb (void *p) noexcept nogil: - cdef timeout_info info = dereference( p) if info.timeout < 0: # timeout < 0 means no timeout return 0 cdef double current_time with gil: - current_time = clock() # Check if the clock has been changed. @@ -124,47 +122,26 @@ cdef int pyav_io_close_gil(lib.AVFormatContext *s, lib.AVIOContext *pb) noexcept return result -Flags = define_enum("Flags", __name__, ( - ("GENPTS", lib.AVFMT_FLAG_GENPTS, - "Generate missing pts even if it requires parsing future frames."), - ("IGNIDX", lib.AVFMT_FLAG_IGNIDX, - "Ignore index."), - ("NONBLOCK", lib.AVFMT_FLAG_NONBLOCK, - "Do not block when reading packets from input."), - ("IGNDTS", lib.AVFMT_FLAG_IGNDTS, - "Ignore DTS on frames that contain both DTS & PTS."), - ("NOFILLIN", lib.AVFMT_FLAG_NOFILLIN, - "Do not infer any values from other values, just return what is stored in the container."), - ("NOPARSE", lib.AVFMT_FLAG_NOPARSE, - """Do not use AVParsers, you also must set AVFMT_FLAG_NOFILLIN as the fillin code works on frames and no parsing -> no frames. - - Also seeking to frames can not work if parsing to find frame boundaries has been disabled."""), - ("NOBUFFER", lib.AVFMT_FLAG_NOBUFFER, - "Do not buffer frames when possible."), - ("CUSTOM_IO", lib.AVFMT_FLAG_CUSTOM_IO, - "The caller has supplied a custom AVIOContext, don't avio_close() it."), - ("DISCARD_CORRUPT", lib.AVFMT_FLAG_DISCARD_CORRUPT, - "Discard frames marked corrupted."), - ("FLUSH_PACKETS", lib.AVFMT_FLAG_FLUSH_PACKETS, - "Flush the AVIOContext every packet."), - ("BITEXACT", lib.AVFMT_FLAG_BITEXACT, - """When muxing, try to avoid writing any random/volatile data to the output. - - This includes any random IDs, real-time timestamps/dates, muxer version, etc. - This flag is mainly intended for testing."""), - ("SORT_DTS", lib.AVFMT_FLAG_SORT_DTS, - "Try to interleave outputted packets by dts (using this flag can slow demuxing down)."), - ("FAST_SEEK", lib.AVFMT_FLAG_FAST_SEEK, - "Enable fast, but inaccurate seeks for some formats."), - ("SHORTEST", lib.AVFMT_FLAG_SHORTEST, - "Stop muxing when the shortest stream stops."), - ("AUTO_BSF", lib.AVFMT_FLAG_AUTO_BSF, - "Add bitstream filters as requested by the muxer."), -), is_flags=True) +class Flags(Flag): + gen_pts: "Generate missing pts even if it requires parsing future frames." = lib.AVFMT_FLAG_GENPTS + ign_idx: "Ignore index." = lib.AVFMT_FLAG_IGNIDX + non_block: "Do not block when reading packets from input." = lib.AVFMT_FLAG_NONBLOCK + ign_dts: "Ignore DTS on frames that contain both DTS & PTS." = lib.AVFMT_FLAG_IGNDTS + no_fillin: "Do not infer any values from other values, just return what is stored in the container." = lib.AVFMT_FLAG_NOFILLIN + no_parse: "Do not use AVParsers, you also must set AVFMT_FLAG_NOFILLIN as the fillin code works on frames and no parsing -> no frames. Also seeking to frames can not work if parsing to find frame boundaries has been disabled." = lib.AVFMT_FLAG_NOPARSE + no_buffer: "Do not buffer frames when possible." = lib.AVFMT_FLAG_NOBUFFER + custom_io: "The caller has supplied a custom AVIOContext, don't avio_close() it." = lib.AVFMT_FLAG_CUSTOM_IO + discard_corrupt: "Discard frames marked corrupted." = lib.AVFMT_FLAG_DISCARD_CORRUPT + flush_packets: "Flush the AVIOContext every packet." = lib.AVFMT_FLAG_FLUSH_PACKETS + bitexact: "When muxing, try to avoid writing any random/volatile data to the output. This includes any random IDs, real-time timestamps/dates, muxer version, etc. This flag is mainly intended for testing." = lib.AVFMT_FLAG_BITEXACT + sort_dts: "Try to interleave outputted packets by dts (using this flag can slow demuxing down)." = lib.AVFMT_FLAG_SORT_DTS + fast_seek: "Enable fast, but inaccurate seeks for some formats." = lib.AVFMT_FLAG_FAST_SEEK + shortest: "Stop muxing when the shortest stream stops." = lib.AVFMT_FLAG_SHORTEST + auto_bsf: "Add bitstream filters as requested by the muxer." = lib.AVFMT_FLAG_AUTO_BSF -cdef class Container: +cdef class Container: def __cinit__(self, sentinel, file_, format_name, options, container_options, stream_options, metadata_encoding, metadata_errors, @@ -248,20 +225,13 @@ cdef class Container: cdef lib.AVInputFormat *ifmt cdef _Dictionary c_options if not self.writeable: - ifmt = self.format.iptr if self.format else NULL - c_options = Dictionary(self.options, self.container_options) self.set_timeout(self.open_timeout) self.start_timeout() with nogil: - res = lib.avformat_open_input( - &self.ptr, - name, - ifmt, - &c_options.ptr - ) + res = lib.avformat_open_input(&self.ptr, name, ifmt, &c_options.ptr) self.set_timeout(None) self.err_check(res) self.input_was_opened = True @@ -304,37 +274,16 @@ cdef class Container: if self.ptr == NULL: raise AssertionError("Container is not open") - def _get_flags(self): + @property + def flags(self): self._assert_open() return self.ptr.flags - def _set_flags(self, value): + @flags.setter + def flags(self, int value): self._assert_open() self.ptr.flags = value - flags = Flags.property( - _get_flags, - _set_flags, - """Flags property of :class:`.Flags`""" - ) - - gen_pts = flags.flag_property("GENPTS") - ign_idx = flags.flag_property("IGNIDX") - non_block = flags.flag_property("NONBLOCK") - ign_dts = flags.flag_property("IGNDTS") - no_fill_in = flags.flag_property("NOFILLIN") - no_parse = flags.flag_property("NOPARSE") - no_buffer = flags.flag_property("NOBUFFER") - custom_io = flags.flag_property("CUSTOM_IO") - discard_corrupt = flags.flag_property("DISCARD_CORRUPT") - flush_packets = flags.flag_property("FLUSH_PACKETS") - bit_exact = flags.flag_property("BITEXACT") - sort_dts = flags.flag_property("SORT_DTS") - fast_seek = flags.flag_property("FAST_SEEK") - shortest = flags.flag_property("SHORTEST") - auto_bsf = flags.flag_property("AUTO_BSF") - - def open( file, mode=None, From 16c25172d3b97a37f545f73c147de77885de2638 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 13 Nov 2024 05:14:21 -0500 Subject: [PATCH 163/365] Use builtin Enum class more often --- av/codec/context.pyi | 33 +++++++++++----------- av/codec/context.pyx | 38 ++++++++++++------------- av/enum.pyx | 35 ++--------------------- av/option.pyi | 6 ++-- av/option.pyx | 66 ++++++++++++++++++++++---------------------- 5 files changed, 74 insertions(+), 104 deletions(-) diff --git a/av/codec/context.pyi b/av/codec/context.pyi index 8c50ba456..8600deaaa 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -1,25 +1,26 @@ +from enum import Enum, Flag from fractions import Fraction -from typing import Any, Literal +from typing import ClassVar, Literal from av.enum import EnumFlag, EnumItem from av.packet import Packet from .codec import Codec -class ThreadType(EnumFlag): - NONE: int - FRAME: int - SLICE: int - AUTO: int +class ThreadType(Flag): + NONE: ClassVar[ThreadType] + FRAME: ClassVar[ThreadType] + SLICE: ClassVar[ThreadType] + AUTO: ClassVar[ThreadType] -class SkipType(EnumItem): - NONE: int - DEFAULT: int - NONREF: int - BIDIR: int - NONINTRA: int - NONKEY: int - ALL: int +class SkipType(Enum): + NONE: ClassVar[SkipType] + DEFAULT: ClassVar[SkipType] + NONREF: ClassVar[SkipType] + BIDIR: ClassVar[SkipType] + NONINTRA: ClassVar[SkipType] + NONKEY: ClassVar[SkipType] + ALL: ClassVar[SkipType] class Flags(EnumFlag): NONE: int @@ -67,8 +68,8 @@ class CodecContext: bit_rate: int | None bit_rate_tolerance: int thread_count: int - thread_type: Any - skip_frame: Any + thread_type: ThreadType + skip_frame: SkipType # flags unaligned: bool diff --git a/av/codec/context.pyx b/av/codec/context.pyx index a1e95048c..528a73872 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -11,6 +11,8 @@ from av.error cimport err_check from av.packet cimport Packet from av.utils cimport avrational_to_fraction, to_avrational +from enum import Enum, Flag + from av.dictionary import Dictionary @@ -39,22 +41,20 @@ cdef CodecContext wrap_codec_context(lib.AVCodecContext *c_ctx, const lib.AVCode return py_ctx -ThreadType = define_enum("ThreadType", __name__, ( - ("NONE", 0), - ("FRAME", lib.FF_THREAD_FRAME, "Decode more than one frame at once"), - ("SLICE", lib.FF_THREAD_SLICE, "Decode more than one part of a single frame at once"), - ("AUTO", lib.FF_THREAD_SLICE | lib.FF_THREAD_FRAME, "Decode using both FRAME and SLICE methods."), -), is_flags=True) +class ThreadType(Flag): + NONE = 0 + FRAME: "Decode more than one frame at once" = lib.FF_THREAD_FRAME + SLICE: "Decode more than one part of a single frame at once" = lib.FF_THREAD_SLICE + AUTO: "Decode using both FRAME and SLICE methods." = lib.FF_THREAD_SLICE | lib.FF_THREAD_FRAME -SkipType = define_enum("SkipType", __name__, ( - ("NONE", lib.AVDISCARD_NONE, "Discard nothing"), - ("DEFAULT", lib.AVDISCARD_DEFAULT, "Discard useless packets like 0 size packets in AVI"), - ("NONREF", lib.AVDISCARD_NONREF, "Discard all non reference"), - ("BIDIR", lib.AVDISCARD_BIDIR, "Discard all bidirectional frames"), - ("NONINTRA", lib.AVDISCARD_NONINTRA, "Discard all non intra frames"), - ("NONKEY", lib.AVDISCARD_NONKEY, "Discard all frames except keyframes"), - ("ALL", lib.AVDISCARD_ALL, "Discard all"), -)) +class SkipType(Enum): + NONE: "Discard nothing" = lib.AVDISCARD_NONE + DEFAULT: "Discard useless packets like 0 size packets in AVI" = lib.AVDISCARD_DEFAULT + NONREF: "Discard all non reference" = lib.AVDISCARD_NONREF + BIDIR: "Discard all bidirectional frames" = lib.AVDISCARD_BIDIR + NONINTRA: "Discard all non intra frames" = lib.AVDISCARD_NONINTRA + NONKEY: "Discard all frames except keyframes" = lib.AVDISCARD_NONKEY + ALL: "Discard all" = lib.AVDISCARD_ALL Flags = define_enum("Flags", __name__, ( ("NONE", 0), @@ -617,13 +617,13 @@ cdef class CodecContext: Wraps :ffmpeg:`AVCodecContext.thread_type`. """ - return ThreadType.get(self.ptr.thread_type, create=True) + return ThreadType(self.ptr.thread_type) @thread_type.setter def thread_type(self, value): if self.is_open: raise RuntimeError("Cannot change thread_type after codec is open.") - self.ptr.thread_type = ThreadType[value].value + self.ptr.thread_type = value.value @property def skip_frame(self): @@ -632,11 +632,11 @@ cdef class CodecContext: Wraps :ffmpeg:`AVCodecContext.skip_frame`. """ - return SkipType._get(self.ptr.skip_frame, create=True) + return SkipType(self.ptr.skip_frame) @skip_frame.setter def skip_frame(self, value): - self.ptr.skip_frame = SkipType[value].value + self.ptr.skip_frame = value.value @property def delay(self): diff --git a/av/enum.pyx b/av/enum.pyx index a21d66e81..9217e67d6 100644 --- a/av/enum.pyx +++ b/av/enum.pyx @@ -126,37 +126,6 @@ copyreg.constructor(_unpickle) cdef class EnumItem: - """ - Enumerations are when an attribute may only take on a single value at once, and - they are represented as integers in the FFmpeg API. We associate names with each - value that are easier to operate with. - - Consider :data:`av.codec.context.SkipType`, which is the type of the :attr:`CodecContext.skip_frame` attribute:: - - >>> fh = av.open(video_path) - >>> cc = fh.streams.video[0].codec_context - - >>> # The skip_frame attribute has a name and value: - >>> cc.skip_frame.name - 'DEFAULT' - >>> cc.skip_frame.value - 0 - - >>> # You can compare it to strings and ints: - >>> cc.skip_frame == 'DEFAULT' - True - >>> cc.skip_frame == 0 - True - - >>> # You can assign strings and ints: - >>> cc.skip_frame = 'NONKEY' - >>> cc.skip_frame == 'NONKEY' - True - >>> cc.skip_frame == 32 - True - - """ - cdef readonly str name cdef readonly int value cdef Py_hash_t _hash @@ -167,7 +136,7 @@ cdef class EnumItem: self.name = name self.value = value - self.__doc__ = doc # This is not cdef because it doesn't work if it is. + self.__doc__ = doc # We need to establish a hash that doesn't collide with anything that # would return true from `__eq__`. This is because these enums (vs @@ -196,7 +165,7 @@ cdef class EnumItem: def __eq__(self, other): if isinstance(other, str): - if self.name == other: # The quick method. + if self.name == other: return True try: diff --git a/av/option.pyi b/av/option.pyi index b7ba670f2..3132f4a02 100644 --- a/av/option.pyi +++ b/av/option.pyi @@ -1,6 +1,6 @@ -from .enum import EnumFlag, EnumItem +from enum import Enum, Flag -class OptionType(EnumItem): +class OptionType(Enum): FLAGS: int INT: int INT64: int @@ -20,7 +20,7 @@ class OptionType(EnumItem): CHANNEL_LAYOUT: int BOOL: int -class OptionFlags(EnumFlag): +class OptionFlags(Flag): ENCODING_PARAM: int DECODING_PARAM: int AUDIO_PARAM: int diff --git a/av/option.pyx b/av/option.pyx index 0c7b5fb28..e58c4c13f 100644 --- a/av/option.pyx +++ b/av/option.pyx @@ -1,8 +1,9 @@ cimport libav as lib -from av.enum cimport define_enum from av.utils cimport flag_in_bitfield +from enum import Enum, Flag + cdef object _cinit_sentinel = object() @@ -15,27 +16,26 @@ cdef Option wrap_option(tuple choices, const lib.AVOption *ptr): return obj -OptionType = define_enum("OptionType", __name__, ( - ("FLAGS", lib.AV_OPT_TYPE_FLAGS), - ("INT", lib.AV_OPT_TYPE_INT), - ("INT64", lib.AV_OPT_TYPE_INT64), - ("DOUBLE", lib.AV_OPT_TYPE_DOUBLE), - ("FLOAT", lib.AV_OPT_TYPE_FLOAT), - ("STRING", lib.AV_OPT_TYPE_STRING), - ("RATIONAL", lib.AV_OPT_TYPE_RATIONAL), - ("BINARY", lib.AV_OPT_TYPE_BINARY), - ("DICT", lib.AV_OPT_TYPE_DICT), - ("UINT64", lib.AV_OPT_TYPE_UINT64), - ("CONST", lib.AV_OPT_TYPE_CONST), - ("IMAGE_SIZE", lib.AV_OPT_TYPE_IMAGE_SIZE), - ("PIXEL_FMT", lib.AV_OPT_TYPE_PIXEL_FMT), - ("SAMPLE_FMT", lib.AV_OPT_TYPE_SAMPLE_FMT), - ("VIDEO_RATE", lib.AV_OPT_TYPE_VIDEO_RATE), - ("DURATION", lib.AV_OPT_TYPE_DURATION), - ("COLOR", lib.AV_OPT_TYPE_COLOR), - ("CHANNEL_LAYOUT", lib.AV_OPT_TYPE_CHLAYOUT), - ("BOOL", lib.AV_OPT_TYPE_BOOL), -)) +class OptionType(Enum): + FLAGS = lib.AV_OPT_TYPE_FLAGS + INT = lib.AV_OPT_TYPE_INT + INT64 = lib.AV_OPT_TYPE_INT64 + DOUBLE = lib.AV_OPT_TYPE_DOUBLE + FLOAT = lib.AV_OPT_TYPE_FLOAT + STRING = lib.AV_OPT_TYPE_STRING + RATIONAL = lib.AV_OPT_TYPE_RATIONAL + BINARY = lib.AV_OPT_TYPE_BINARY + DICT = lib.AV_OPT_TYPE_DICT + UINT64 = lib.AV_OPT_TYPE_UINT64 + CONST = lib.AV_OPT_TYPE_CONST + IMAGE_SIZE = lib.AV_OPT_TYPE_IMAGE_SIZE + PIXEL_FMT = lib.AV_OPT_TYPE_PIXEL_FMT + SAMPLE_FMT = lib.AV_OPT_TYPE_SAMPLE_FMT + VIDEO_RATE = lib.AV_OPT_TYPE_VIDEO_RATE + DURATION = lib.AV_OPT_TYPE_DURATION + COLOR = lib.AV_OPT_TYPE_COLOR + CHANNEL_LAYOUT = lib.AV_OPT_TYPE_CHLAYOUT + BOOL = lib.AV_OPT_TYPE_BOOL cdef tuple _INT_TYPES = ( lib.AV_OPT_TYPE_FLAGS, @@ -48,16 +48,16 @@ cdef tuple _INT_TYPES = ( lib.AV_OPT_TYPE_BOOL, ) -OptionFlags = define_enum("OptionFlags", __name__, ( - ("ENCODING_PARAM", lib.AV_OPT_FLAG_ENCODING_PARAM), - ("DECODING_PARAM", lib.AV_OPT_FLAG_DECODING_PARAM), - ("AUDIO_PARAM", lib.AV_OPT_FLAG_AUDIO_PARAM), - ("VIDEO_PARAM", lib.AV_OPT_FLAG_VIDEO_PARAM), - ("SUBTITLE_PARAM", lib.AV_OPT_FLAG_SUBTITLE_PARAM), - ("EXPORT", lib.AV_OPT_FLAG_EXPORT), - ("READONLY", lib.AV_OPT_FLAG_READONLY), - ("FILTERING_PARAM", lib.AV_OPT_FLAG_FILTERING_PARAM), -), is_flags=True) +class OptionFlags(Flag): + ENCODING_PARAM = lib.AV_OPT_FLAG_ENCODING_PARAM + DECODING_PARAM = lib.AV_OPT_FLAG_DECODING_PARAM + AUDIO_PARAM = lib.AV_OPT_FLAG_AUDIO_PARAM + VIDEO_PARAM = lib.AV_OPT_FLAG_VIDEO_PARAM + SUBTITLE_PARAM = lib.AV_OPT_FLAG_SUBTITLE_PARAM + EXPORT = lib.AV_OPT_FLAG_EXPORT + READONLY = lib.AV_OPT_FLAG_READONLY + FILTERING_PARAM = lib.AV_OPT_FLAG_FILTERING_PARAM + cdef class BaseOption: def __cinit__(self, sentinel): @@ -106,7 +106,7 @@ cdef class BaseOption: cdef class Option(BaseOption): @property def type(self): - return OptionType._get(self.ptr.type, create=True) + return OptionType(self.ptr.type) @property def offset(self): From caaddbf740c118f18f8959cd238afcb67d7d82c3 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 13 Nov 2024 17:46:43 -0500 Subject: [PATCH 164/365] Make PictureType an IntEnum --- av/video/frame.pyi | 18 +++++++++++------- av/video/frame.pyx | 32 +++++++++++++++----------------- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/av/video/frame.pyi b/av/video/frame.pyi index 3300e8607..d837ed606 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -1,9 +1,9 @@ -from typing import Any, Union +from enum import IntEnum +from typing import Any, ClassVar, Union import numpy as np from PIL import Image -from av.enum import EnumItem from av.frame import Frame from .format import VideoFormat @@ -15,7 +15,7 @@ _SupportedNDarray = Union[ np.ndarray[Any, np.dtype[np.float32]], ] -class PictureType(EnumItem): +class PictureType(IntEnum): NONE: int I: int P: int @@ -28,15 +28,19 @@ class PictureType(EnumItem): class VideoFrame(Frame): format: VideoFormat pts: int - time: float planes: tuple[VideoPlane, ...] - width: int - height: int - interlaced_frame: bool pict_type: int colorspace: int color_range: int + @property + def time(self) -> float: ... + @property + def width(self) -> int: ... + @property + def height(self) -> int: ... + @property + def interlaced_frame(self) -> bool: ... def __init__( self, width: int = 0, height: int = 0, format: str = "yuv420p" ) -> None: ... diff --git a/av/video/frame.pyx b/av/video/frame.pyx index e5cd523b5..80cf266f8 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -1,8 +1,8 @@ import sys +from enum import IntEnum from libc.stdint cimport uint8_t -from av.enum cimport define_enum from av.error cimport err_check from av.utils cimport check_ndarray from av.video.format cimport get_pix_fmt, get_video_format @@ -20,18 +20,15 @@ cdef VideoFrame alloc_video_frame(): """ return VideoFrame.__new__(VideoFrame, _cinit_bypass_sentinel) - -PictureType = define_enum("PictureType", __name__, ( - ("NONE", lib.AV_PICTURE_TYPE_NONE, "Undefined"), - ("I", lib.AV_PICTURE_TYPE_I, "Intra"), - ("P", lib.AV_PICTURE_TYPE_P, "Predicted"), - ("B", lib.AV_PICTURE_TYPE_B, "Bi-directional predicted"), - ("S", lib.AV_PICTURE_TYPE_S, "S(GMC)-VOP MPEG-4"), - ("SI", lib.AV_PICTURE_TYPE_SI, "Switching intra"), - ("SP", lib.AV_PICTURE_TYPE_SP, "Switching predicted"), - ("BI", lib.AV_PICTURE_TYPE_BI, "BI type"), -)) - +class PictureType(IntEnum): + NONE = lib.AV_PICTURE_TYPE_NONE # Undefined + I = lib.AV_PICTURE_TYPE_I # Intra + P = lib.AV_PICTURE_TYPE_P # Predicted + B = lib.AV_PICTURE_TYPE_B # Bi-directional predicted + S = lib.AV_PICTURE_TYPE_S # S(GMC)-VOP MPEG-4 + SI = lib.AV_PICTURE_TYPE_SI # Switching intra + SP = lib.AV_PICTURE_TYPE_SP # Switching predicted + BI = lib.AV_PICTURE_TYPE_BI # BI type cdef byteswap_array(array, bint big_endian): if (sys.byteorder == "big") != big_endian: @@ -183,16 +180,17 @@ cdef class VideoFrame(Frame): @property def pict_type(self): - """One of :class:`.PictureType`. + """Returns an integer that corresponds to the PictureType enum. - Wraps :ffmpeg:`AVFrame.pict_type`. + Wraps :ffmpeg:`AVFrame.pict_type` + :type: int """ - return PictureType.get(self.ptr.pict_type, create=True) + return self.ptr.pict_type @pict_type.setter def pict_type(self, value): - self.ptr.pict_type = PictureType[value].value + self.ptr.pict_type = value @property def colorspace(self): From 007368e45fa010b9a178f93984fcfd0d357f7a8d Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 13 Nov 2024 18:42:43 -0500 Subject: [PATCH 165/365] Update Sidedata Type Enums --- av/sidedata/sidedata.pyi | 59 +++++++++++++++++------------- av/sidedata/sidedata.pyx | 66 ++++++++++++++++++++-------------- include/libavcodec/avcodec.pxd | 14 ++++++-- 3 files changed, 86 insertions(+), 53 deletions(-) diff --git a/av/sidedata/sidedata.pyi b/av/sidedata/sidedata.pyi index e814bb222..d165513ab 100644 --- a/av/sidedata/sidedata.pyi +++ b/av/sidedata/sidedata.pyi @@ -1,41 +1,52 @@ from collections.abc import Mapping -from typing import Iterator, Sequence, overload +from enum import Enum +from typing import ClassVar, Iterator, Sequence, overload from av.buffer import Buffer -from av.enum import EnumItem from av.frame import Frame -class Type(EnumItem): - PANSCAN: int - A53_CC: int - STEREO3D: int - MATRIXENCODING: int - DOWNMIX_INFO: int - REPLAYGAIN: int - DISPLAYMATRIX: int - AFD: int - MOTION_VECTORS: int - SKIP_SAMPLES: int - AUDIO_SERVICE_TYPE: int - MASTERING_DISPLAY_METADATA: int - GOP_TIMECODE: int - SPHERICAL: int - CONTENT_LIGHT_LEVEL: int - ICC_PROFILE: int - SEI_UNREGISTERED: int - S12M_TIMECODE: int +class Type(Enum): + PANSCAN: ClassVar[Type] + A53_CC: ClassVar[Type] + STEREO3D: ClassVar[Type] + MATRIXENCODING: ClassVar[Type] + DOWNMIX_INFO: ClassVar[Type] + REPLAYGAIN: ClassVar[Type] + DISPLAYMATRIX: ClassVar[Type] + AFD: ClassVar[Type] + MOTION_VECTORS: ClassVar[Type] + SKIP_SAMPLES: ClassVar[Type] + AUDIO_SERVICE_TYPE: ClassVar[Type] + MASTERING_DISPLAY_METADATA: ClassVar[Type] + GOP_TIMECODE: ClassVar[Type] + SPHERICAL: ClassVar[Type] + CONTENT_LIGHT_LEVEL: ClassVar[Type] + ICC_PROFILE: ClassVar[Type] + S12M_TIMECODE: ClassVar[Type] + DYNAMIC_HDR_PLUS: ClassVar[Type] + REGIONS_OF_INTEREST: ClassVar[Type] + VIDEO_ENC_PARAMS: ClassVar[Type] + SEI_UNREGISTERED: ClassVar[Type] + FILM_GRAIN_PARAMS: ClassVar[Type] + DETECTION_BBOXES: ClassVar[Type] + DOVI_RPU_BUFFER: ClassVar[Type] + DOVI_METADATA: ClassVar[Type] + DYNAMIC_HDR_VIVID: ClassVar[Type] + AMBIENT_VIEWING_ENVIRONMENT: ClassVar[Type] + VIDEO_HINT: ClassVar[Type] class SideData(Buffer): type: Type - DISPLAYMATRIX: int class SideDataContainer(Mapping): frame: Frame def __len__(self) -> int: ... def __iter__(self) -> Iterator[SideData]: ... @overload - def __getitem__(self, key: int) -> SideData: ... + def __getitem__(self, key: str | int | Type) -> SideData: ... @overload def __getitem__(self, key: slice) -> Sequence[SideData]: ... @overload - def __getitem__(self, key: int | slice) -> SideData | Sequence[SideData]: ... + def __getitem__( + self, key: str | int | Type | slice + ) -> SideData | Sequence[SideData]: ... diff --git a/av/sidedata/sidedata.pyx b/av/sidedata/sidedata.pyx index 9b423a30b..753496fea 100644 --- a/av/sidedata/sidedata.pyx +++ b/av/sidedata/sidedata.pyx @@ -1,6 +1,5 @@ -from av.enum cimport define_enum - from collections.abc import Mapping +from enum import Enum from av.sidedata.motionvectors import MotionVectors @@ -8,26 +7,41 @@ from av.sidedata.motionvectors import MotionVectors cdef object _cinit_bypass_sentinel = object() -Type = define_enum("Type", __name__, ( - ("PANSCAN", lib.AV_FRAME_DATA_PANSCAN), - ("A53_CC", lib.AV_FRAME_DATA_A53_CC), - ("STEREO3D", lib.AV_FRAME_DATA_STEREO3D), - ("MATRIXENCODING", lib.AV_FRAME_DATA_MATRIXENCODING), - ("DOWNMIX_INFO", lib.AV_FRAME_DATA_DOWNMIX_INFO), - ("REPLAYGAIN", lib.AV_FRAME_DATA_REPLAYGAIN), - ("DISPLAYMATRIX", lib.AV_FRAME_DATA_DISPLAYMATRIX), - ("AFD", lib.AV_FRAME_DATA_AFD), - ("MOTION_VECTORS", lib.AV_FRAME_DATA_MOTION_VECTORS), - ("SKIP_SAMPLES", lib.AV_FRAME_DATA_SKIP_SAMPLES), - ("AUDIO_SERVICE_TYPE", lib.AV_FRAME_DATA_AUDIO_SERVICE_TYPE), - ("MASTERING_DISPLAY_METADATA", lib.AV_FRAME_DATA_MASTERING_DISPLAY_METADATA), - ("GOP_TIMECODE", lib.AV_FRAME_DATA_GOP_TIMECODE), - ("SPHERICAL", lib.AV_FRAME_DATA_SPHERICAL), - ("CONTENT_LIGHT_LEVEL", lib.AV_FRAME_DATA_CONTENT_LIGHT_LEVEL), - ("ICC_PROFILE", lib.AV_FRAME_DATA_ICC_PROFILE), - ("SEI_UNREGISTERED", lib.AV_FRAME_DATA_SEI_UNREGISTERED), - ("S12M_TIMECODE", lib.AV_FRAME_DATA_S12M_TIMECODE), -)) +class Type(Enum): + """ + Enum class representing different types of frame data in audio/video processing. + Values are mapped to corresponding AV_FRAME_DATA constants from FFmpeg. + + From: https://github.com/FFmpeg/FFmpeg/blob/master/libavutil/frame.h + """ + PANSCAN = lib.AV_FRAME_DATA_PANSCAN + A53_CC = lib.AV_FRAME_DATA_A53_CC + STEREO3D = lib.AV_FRAME_DATA_STEREO3D + MATRIXENCODING = lib.AV_FRAME_DATA_MATRIXENCODING + DOWNMIX_INFO = lib.AV_FRAME_DATA_DOWNMIX_INFO + REPLAYGAIN = lib.AV_FRAME_DATA_REPLAYGAIN + DISPLAYMATRIX = lib.AV_FRAME_DATA_DISPLAYMATRIX + AFD = lib.AV_FRAME_DATA_AFD + MOTION_VECTORS = lib.AV_FRAME_DATA_MOTION_VECTORS + SKIP_SAMPLES = lib.AV_FRAME_DATA_SKIP_SAMPLES + AUDIO_SERVICE_TYPE = lib.AV_FRAME_DATA_AUDIO_SERVICE_TYPE + MASTERING_DISPLAY_METADATA = lib.AV_FRAME_DATA_MASTERING_DISPLAY_METADATA + GOP_TIMECODE = lib.AV_FRAME_DATA_GOP_TIMECODE + SPHERICAL = lib.AV_FRAME_DATA_SPHERICAL + CONTENT_LIGHT_LEVEL = lib.AV_FRAME_DATA_CONTENT_LIGHT_LEVEL + ICC_PROFILE = lib.AV_FRAME_DATA_ICC_PROFILE + S12M_TIMECODE = lib.AV_FRAME_DATA_S12M_TIMECODE + DYNAMIC_HDR_PLUS = lib.AV_FRAME_DATA_DYNAMIC_HDR_PLUS + REGIONS_OF_INTEREST = lib.AV_FRAME_DATA_REGIONS_OF_INTEREST + VIDEO_ENC_PARAMS = lib.AV_FRAME_DATA_VIDEO_ENC_PARAMS + SEI_UNREGISTERED = lib.AV_FRAME_DATA_SEI_UNREGISTERED + FILM_GRAIN_PARAMS = lib.AV_FRAME_DATA_FILM_GRAIN_PARAMS + DETECTION_BBOXES = lib.AV_FRAME_DATA_DETECTION_BBOXES + DOVI_RPU_BUFFER = lib.AV_FRAME_DATA_DOVI_RPU_BUFFER + DOVI_METADATA = lib.AV_FRAME_DATA_DOVI_METADATA + DYNAMIC_HDR_VIVID = lib.AV_FRAME_DATA_DYNAMIC_HDR_VIVID + AMBIENT_VIEWING_ENVIRONMENT = lib.AV_FRAME_DATA_AMBIENT_VIEWING_ENVIRONMENT + VIDEO_HINT = lib.AV_FRAME_DATA_VIDEO_HINT cdef SideData wrap_side_data(Frame frame, int index): @@ -60,7 +74,7 @@ cdef class SideData(Buffer): @property def type(self): - return Type.get(self.ptr.type) or self.ptr.type + return Type(self.ptr.type) cdef class _SideDataContainer: @@ -85,9 +99,9 @@ cdef class _SideDataContainer: def __getitem__(self, key): if isinstance(key, int): return self._by_index[key] - - type_ = Type.get(key) - return self._by_type[type_] + if isinstance(key, str): + return self._by_type[Type[key]] + return self._by_type[key] class SideDataContainer(_SideDataContainer, Mapping): diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 54fb2293c..172c9cc65 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -354,10 +354,18 @@ cdef extern from "libavcodec/avcodec.h" nogil: AV_FRAME_DATA_SPHERICAL AV_FRAME_DATA_CONTENT_LIGHT_LEVEL AV_FRAME_DATA_ICC_PROFILE - AV_FRAME_DATA_QP_TABLE_PROPERTIES - AV_FRAME_DATA_QP_TABLE_DATA - AV_FRAME_DATA_SEI_UNREGISTERED AV_FRAME_DATA_S12M_TIMECODE + AV_FRAME_DATA_DYNAMIC_HDR_PLUS + AV_FRAME_DATA_REGIONS_OF_INTEREST + AV_FRAME_DATA_VIDEO_ENC_PARAMS + AV_FRAME_DATA_SEI_UNREGISTERED + AV_FRAME_DATA_FILM_GRAIN_PARAMS + AV_FRAME_DATA_DETECTION_BBOXES + AV_FRAME_DATA_DOVI_RPU_BUFFER + AV_FRAME_DATA_DOVI_METADATA + AV_FRAME_DATA_DYNAMIC_HDR_VIVID + AV_FRAME_DATA_AMBIENT_VIEWING_ENVIRONMENT + AV_FRAME_DATA_VIDEO_HINT cdef struct AVFrameSideData: AVFrameSideDataType type From e1aaee016e8c10b8b807dc8cd2319427a39896cb Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 13 Nov 2024 18:56:07 -0500 Subject: [PATCH 166/365] Edit stream type stub --- av/stream.pyi | 12 +++++------- av/stream.pyx | 9 ++++----- tests/test_file_probing.py | 2 ++ 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/av/stream.pyi b/av/stream.pyi index 8c48f3ac7..b17587e10 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -1,26 +1,24 @@ +from enum import Enum from fractions import Fraction -from typing import Literal +from typing import ClassVar, Literal from .codec import Codec, CodecContext from .container import Container -from .enum import EnumItem from .frame import Frame from .packet import Packet -class SideData(EnumItem): - DISPLAYMATRIX: int +class SideData(Enum): + DISPLAYMATRIX: ClassVar[SideData] class Stream: name: str | None - thread_type: Literal["NONE", "FRAME", "SLICE", "AUTO"] - container: Container codec: Codec codec_context: CodecContext metadata: dict[str, str] id: int profiles: list[str] - profile: str + profile: str | None index: int time_base: Fraction | None average_rate: Fraction | None diff --git a/av/stream.pyx b/av/stream.pyx index 4c450d283..59e1713c5 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -1,7 +1,8 @@ cimport libav as lib from libc.stdint cimport int32_t -from av.enum cimport define_enum +from enum import Enum + from av.error cimport err_check from av.packet cimport Packet from av.utils cimport ( @@ -14,12 +15,10 @@ from av.utils cimport ( cdef object _cinit_bypass_sentinel = object() - # If necessary more can be added from # https://ffmpeg.org/doxygen/trunk/group__lavc__packet.html#ga9a80bfcacc586b483a973272800edb97 -SideData = define_enum("SideData", __name__, ( - ("DISPLAYMATRIX", lib.AV_PKT_DATA_DISPLAYMATRIX, "Display Matrix"), -)) +class SideData(Enum): + DISPLAYMATRIX: "Display Matrix" = lib.AV_PKT_DATA_DISPLAYMATRIX cdef Stream wrap_stream(Container container, lib.AVStream *c_stream, CodecContext codec_context): """Build an av.Stream for an existing AVStream. diff --git a/tests/test_file_probing.py b/tests/test_file_probing.py index f71391697..ce04189f9 100644 --- a/tests/test_file_probing.py +++ b/tests/test_file_probing.py @@ -75,6 +75,7 @@ def test_container_probing(self) -> None: def test_stream_probing(self) -> None: stream = self.file.streams[0] + assert isinstance(stream, av.AudioStream) assert str(stream).startswith( " None: def test_stream_probing(self) -> None: stream = self.file.streams[0] + assert isinstance(stream, av.VideoStream) assert str(stream).startswith(" Date: Wed, 13 Nov 2024 19:57:56 -0500 Subject: [PATCH 167/365] convert Properties to enum.Flag, simplify codec property access --- av/codec/codec.pyi | 60 +++++++++++++++++++++++------------- av/codec/codec.pyx | 77 ++++++++++++++++++++-------------------------- 2 files changed, 72 insertions(+), 65 deletions(-) diff --git a/av/codec/codec.pyi b/av/codec/codec.pyi index 6cb421430..fe2d2fd41 100644 --- a/av/codec/codec.pyi +++ b/av/codec/codec.pyi @@ -1,5 +1,6 @@ +from enum import Flag from fractions import Fraction -from typing import Literal, overload +from typing import ClassVar, Literal, overload from av.audio.codeccontext import AudioCodecContext from av.audio.format import AudioFormat @@ -11,14 +12,14 @@ from av.video.format import VideoFormat from .context import CodecContext -class Properties(EnumFlag): - NONE: int - INTRA_ONLY: int - LOSSY: int - LOSSLESS: int - REORDER: int - BITMAP_SUB: int - TEXT_SUB: int +class Properties(Flag): + NONE: ClassVar[Properties] + INTRA_ONLY: ClassVar[Properties] + LOSSY: ClassVar[Properties] + LOSSLESS: ClassVar[Properties] + REORDER: ClassVar[Properties] + BITMAP_SUB: ClassVar[Properties] + TEXT_SUB: ClassVar[Properties] class Capabilities(EnumFlag): NONE: int @@ -46,25 +47,40 @@ class Capabilities(EnumFlag): class UnknownCodecError(ValueError): ... class Codec: - is_encoder: bool - is_decoder: bool + @property + def is_encoder(self) -> bool: ... + @property + def is_decoder(self) -> bool: ... descriptor: Descriptor - name: str - long_name: str - type: Literal["video", "audio", "data", "subtitle", "attachment"] - id: int + @property + def name(self) -> str: ... + @property + def long_name(self) -> str: ... + @property + def type(self) -> Literal["video", "audio", "data", "subtitle", "attachment"]: ... + @property + def id(self) -> int: ... frame_rates: list[Fraction] | None audio_rates: list[int] | None video_formats: list[VideoFormat] | None audio_formats: list[AudioFormat] | None - properties: Properties + + @property + def properties(self) -> int: ... + @property + def intra_only(self) -> bool: ... + @property + def lossy(self) -> bool: ... + @property + def lossless(self) -> bool: ... + @property + def reorder(self) -> bool: ... + @property + def bitmap_sub(self) -> bool: ... + @property + def text_sub(self) -> bool: ... + capabilities: Capabilities - intra_only: bool - lossy: bool - lossless: bool - reorder: bool - bitmap_sub: bool - text_sub: bool draw_horiz_band: bool dr1: bool hwaccel: bool diff --git a/av/codec/codec.pyx b/av/codec/codec.pyx index da66d7184..9b29cb178 100644 --- a/av/codec/codec.pyx +++ b/av/codec/codec.pyx @@ -4,9 +4,10 @@ from av.enum cimport define_enum from av.utils cimport avrational_to_fraction from av.video.format cimport get_video_format +from enum import Flag -cdef object _cinit_sentinel = object() +cdef object _cinit_sentinel = object() cdef Codec wrap_codec(const lib.AVCodec *ptr): cdef Codec codec = Codec(_cinit_sentinel) @@ -15,34 +16,14 @@ cdef Codec wrap_codec(const lib.AVCodec *ptr): codec._init() return codec - -Properties = define_enum("Properties", "av.codec", ( - ("NONE", 0), - ("INTRA_ONLY", lib.AV_CODEC_PROP_INTRA_ONLY, - """Codec uses only intra compression. - Video and audio codecs only."""), - ("LOSSY", lib.AV_CODEC_PROP_LOSSY, - """Codec supports lossy compression. Audio and video codecs only. - - Note: A codec may support both lossy and lossless - compression modes."""), - ("LOSSLESS", lib.AV_CODEC_PROP_LOSSLESS, - """Codec supports lossless compression. Audio and video codecs only."""), - ("REORDER", lib.AV_CODEC_PROP_REORDER, - """Codec supports frame reordering. That is, the coded order (the order in which - the encoded packets are output by the encoders / stored / input to the - decoders) may be different from the presentation order of the corresponding - frames. - - For codecs that do not have this property set, PTS and DTS should always be - equal."""), - ("BITMAP_SUB", lib.AV_CODEC_PROP_BITMAP_SUB, - """Subtitle codec is bitmap based - Decoded AVSubtitle data can be read from the AVSubtitleRect->pict field."""), - ("TEXT_SUB", lib.AV_CODEC_PROP_TEXT_SUB, - """Subtitle codec is text based. - Decoded AVSubtitle data can be read from the AVSubtitleRect->ass field."""), -), is_flags=True) +class Properties(Flag): + NONE = 0 + INTRA_ONLY = lib.AV_CODEC_PROP_INTRA_ONLY + LOSSY = lib.AV_CODEC_PROP_LOSSY + LOSSLESS = lib.AV_CODEC_PROP_LOSSLESS + REORDER = lib.AV_CODEC_PROP_REORDER + BITMAP_SUB = lib.AV_CODEC_PROP_BITMAP_SUB + TEXT_SUB = lib.AV_CODEC_PROP_TEXT_SUB Capabilities = define_enum("Capabilities", "av.codec", ( ("NONE", 0), @@ -287,21 +268,33 @@ cdef class Codec: i += 1 return ret - # NOTE: there are some overlaps, which we defer to how `ffmpeg -codecs` - # handles them (by prefering the capablity to the property). - # Also, LOSSLESS and LOSSY don't have to agree. - - @Properties.property + @property def properties(self): - """Flag property of :class:`.Properties`""" return self.desc.props - intra_only = properties.flag_property("INTRA_ONLY") - lossy = properties.flag_property("LOSSY") # Defer to capability. - lossless = properties.flag_property("LOSSLESS") # Defer to capability. - reorder = properties.flag_property("REORDER") - bitmap_sub = properties.flag_property("BITMAP_SUB") - text_sub = properties.flag_property("TEXT_SUB") + @property + def intra_only(self): + return bool(self.desc.props & lib.AV_CODEC_PROP_INTRA_ONLY) + + @property + def lossy(self): + return bool(self.desc.props & lib.AV_CODEC_PROP_LOSSY) + + @property + def lossless(self): + return bool(self.desc.props & lib.AV_CODEC_PROP_LOSSLESS) + + @property + def reorder(self): + return bool(self.desc.props & lib.AV_CODEC_PROP_REORDER) + + @property + def bitmap_sub(self): + return bool(self.desc.props & lib.AV_CODEC_PROP_BITMAP_SUB) + + @property + def text_sub(self): + return bool(self.desc.props & lib.AV_CODEC_PROP_TEXT_SUB) @Capabilities.property def capabilities(self): @@ -324,8 +317,6 @@ cdef class Codec: auto_threads = capabilities.flag_property("AUTO_THREADS") variable_frame_size = capabilities.flag_property("VARIABLE_FRAME_SIZE") avoid_probing = capabilities.flag_property("AVOID_PROBING") - # intra_only = capabilities.flag_property("INTRA_ONLY") # Dupes. - # lossless = capabilities.flag_property("LOSSLESS") # Dupes. hardware = capabilities.flag_property("HARDWARE") hybrid = capabilities.flag_property("HYBRID") encoder_reordered_opaque = capabilities.flag_property("ENCODER_REORDERED_OPAQUE") From 8bf5d03ca7b479ba1ed45d9e0d0600d99532b85d Mon Sep 17 00:00:00 2001 From: David Plowman Date: Wed, 13 Nov 2024 16:16:05 +0000 Subject: [PATCH 168/365] Make VideoFrame.from_numpy_buffer support buffers with padding Some devices have hardware that creates image buffers with padding, so adding support here means less frame buffer copying is required. Specifically, we extend the support to buffers where the pixel rows are contiguous, though the image doesn't comprise all the pixels on the row (and is therefore not strictly contiguous). We also support yuv420p images with padding. These have padding in the middle of the UV rows as well as at the end, so can't be trimmed by the application before being passed in. Instead, the true image width must be passed. Tests are also added to ensure all these cases now avoid copying. --- av/video/frame.pyi | 2 +- av/video/frame.pyx | 50 ++++++---- tests/test_videoframe.py | 201 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 236 insertions(+), 17 deletions(-) diff --git a/av/video/frame.pyi b/av/video/frame.pyi index d837ed606..0739010c1 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -62,7 +62,7 @@ class VideoFrame(Frame): def from_image(img: Image.Image) -> VideoFrame: ... @staticmethod def from_numpy_buffer( - array: _SupportedNDarray, format: str = "rgb24" + array: _SupportedNDarray, format: str = "rgb24", width: int = 0 ) -> VideoFrame: ... @staticmethod def from_ndarray(array: _SupportedNDarray, format: str = "rgb24") -> VideoFrame: ... diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 80cf266f8..862db8513 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -374,31 +374,54 @@ cdef class VideoFrame(Frame): return frame @staticmethod - def from_numpy_buffer(array, format="rgb24"): + def from_numpy_buffer(array, format="rgb24", width=0): + # Usually the width of the array is the same as the width of the image. But sometimes + # this is not possible, for example with yuv420p images that have padding. These are + # awkward because the UV rows at the bottom have padding bytes in the middle of the + # row as well as at the end. To cope with these, callers need to be able to pass the + # actual width to us. + height = array.shape[0] + if not width: + width = array.shape[1] + if format in ("rgb24", "bgr24"): check_ndarray(array, "uint8", 3) check_ndarray_shape(array, array.shape[2] == 3) - height, width = array.shape[:2] + if array.strides[1:] != (3, 1): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = (array.strides[0], ) + elif format in ("rgba", "bgra"): + check_ndarray(array, "uint8", 3) + check_ndarray_shape(array, array.shape[2] == 4) + if array.strides[1:] != (4, 1): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = (array.strides[0], ) elif format in ("gray", "gray8", "rgb8", "bgr8"): check_ndarray(array, "uint8", 2) - height, width = array.shape[:2] + if array.strides[1] != 1: + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = (array.strides[0], ) elif format in ("yuv420p", "yuvj420p", "nv12"): check_ndarray(array, "uint8", 2) check_ndarray_shape(array, array.shape[0] % 3 == 0) check_ndarray_shape(array, array.shape[1] % 2 == 0) - height, width = array.shape[:2] height = height // 6 * 4 + if array.strides[1] != 1: + raise ValueError("provided array does not have C_CONTIGUOUS rows") + if format in ("yuv420p", "yuvj420p"): + # For YUV420 planar formats, the UV plane stride is always half the Y stride. + linesizes = (array.strides[0], array.strides[0] // 2, array.strides[0] // 2) + else: + # Planes where U and V are interleaved have the same stride as Y. + linesizes = (array.strides[0], array.strides[0]) else: raise ValueError(f"Conversion from numpy array with format `{format}` is not yet supported") - if not array.flags["C_CONTIGUOUS"]: - raise ValueError("provided array must be C_CONTIGUOUS") - frame = alloc_video_frame() - frame._image_fill_pointers_numpy(array, width, height, format) + frame._image_fill_pointers_numpy(array, width, height, linesizes, format) return frame - def _image_fill_pointers_numpy(self, buffer, width, height, format): + def _image_fill_pointers_numpy(self, buffer, width, height, linesizes, format): cdef lib.AVPixelFormat c_format cdef uint8_t * c_ptr cdef size_t c_data @@ -433,13 +456,8 @@ cdef class VideoFrame(Frame): self.ptr.format = c_format self.ptr.width = width self.ptr.height = height - res = lib.av_image_fill_linesizes( - self.ptr.linesize, - self.ptr.format, - width, - ) - if res: - err_check(res) + for i, linesize in enumerate(linesizes): + self.ptr.linesize[i] = linesize res = lib.av_image_fill_pointers( self.ptr.data, diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 6396f1f45..32b6e5482 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -528,6 +528,19 @@ def test_shares_memory_gray() -> None: # Make sure the frame reflects that assertNdarraysEqual(frame.to_ndarray(), array) + # repeat the test, but with an array that is not fully contiguous, though the + # pixels in a row are + array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + array = array[:, :300] + assert not array.data.c_contiguous + frame = VideoFrame.from_numpy_buffer(array, "gray") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + def test_shares_memory_gray8() -> None: array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) @@ -539,6 +552,19 @@ def test_shares_memory_gray8() -> None: # Make sure the frame reflects that assertNdarraysEqual(frame.to_ndarray(), array) + # repeat the test, but with an array that is not fully contiguous, though the + # pixels in a row are + array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + array = array[:, :300] + assert not array.data.c_contiguous + frame = VideoFrame.from_numpy_buffer(array, "gray8") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + def test_shares_memory_rgb8() -> None: array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) @@ -550,6 +576,19 @@ def test_shares_memory_rgb8() -> None: # Make sure the frame reflects that assertNdarraysEqual(frame.to_ndarray(), array) + # repeat the test, but with an array that is not fully contiguous, though the + # pixels in a row are + array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + array = array[:, :300] + assert not array.data.c_contiguous + frame = VideoFrame.from_numpy_buffer(array, "rgb8") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + def test_shares_memory_bgr8() -> None: array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) @@ -561,6 +600,19 @@ def test_shares_memory_bgr8() -> None: # Make sure the frame reflects that assertNdarraysEqual(frame.to_ndarray(), array) + # repeat the test, but with an array that is not fully contiguous, though the + # pixels in a row are + array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + array = array[:, :300] + assert not array.data.c_contiguous + frame = VideoFrame.from_numpy_buffer(array, "bgr8") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + def test_shares_memory_rgb24() -> None: array = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8) @@ -572,6 +624,43 @@ def test_shares_memory_rgb24() -> None: # Make sure the frame reflects that assertNdarraysEqual(frame.to_ndarray(), array) + # repeat the test, but with an array that is not fully contiguous, though the + # pixels in a row are + array = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8) + array = array[:, :300, :] + assert not array.data.c_contiguous + frame = VideoFrame.from_numpy_buffer(array, "rgb24") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_shares_memory_rgba() -> None: + array = numpy.random.randint(0, 256, size=(357, 318, 4), dtype=numpy.uint8) + frame = VideoFrame.from_numpy_buffer(array, "rgba") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=(357, 318, 4), dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + + # repeat the test, but with an array that is not fully contiguous, though the + # pixels in a row are + array = numpy.random.randint(0, 256, size=(357, 318, 4), dtype=numpy.uint8) + array = array[:, :300, :] + assert not array.data.c_contiguous + frame = VideoFrame.from_numpy_buffer(array, "rgba") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + def test_shares_memory_yuv420p() -> None: array = numpy.random.randint(0, 256, size=(512 * 6 // 4, 256), dtype=numpy.uint8) @@ -583,6 +672,38 @@ def test_shares_memory_yuv420p() -> None: # Make sure the frame reflects that assertNdarraysEqual(frame.to_ndarray(), array) + # repeat the test, but with an array where there are some padding bytes + # note that the uv rows have half the padding in the middle of a row, and the + # other half at the end + height = 512 + stride = 256 + width = 200 + array = numpy.random.randint( + 0, 256, size=(height * 6 // 4, stride), dtype=numpy.uint8 + ) + uv_width = width // 2 + uv_stride = stride // 2 + + # compare carefully, avoiding all the padding bytes which to_ndarray strips out + frame = VideoFrame.from_numpy_buffer(array, "yuv420p", width=width) + frame_array = frame.to_ndarray() + assertNdarraysEqual(frame_array[:height, :width], array[:height, :width]) + assertNdarraysEqual(frame_array[height:, :uv_width], array[height:, :uv_width]) + assertNdarraysEqual( + frame_array[height:, uv_width:], + array[height:, uv_stride : uv_stride + uv_width], + ) + + # overwrite the array, and check the shared frame buffer changed too! + array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) + frame_array = frame.to_ndarray() + assertNdarraysEqual(frame_array[:height, :width], array[:height, :width]) + assertNdarraysEqual(frame_array[height:, :uv_width], array[height:, :uv_width]) + assertNdarraysEqual( + frame_array[height:, uv_width:], + array[height:, uv_stride : uv_stride + uv_width], + ) + def test_shares_memory_yuvj420p() -> None: array = numpy.random.randint(0, 256, size=(512 * 6 // 4, 256), dtype=numpy.uint8) @@ -594,6 +715,36 @@ def test_shares_memory_yuvj420p() -> None: # Make sure the frame reflects that assertNdarraysEqual(frame.to_ndarray(), array) + # repeat the test with padding, just as we did in the yuv420p case + height = 512 + stride = 256 + width = 200 + array = numpy.random.randint( + 0, 256, size=(height * 6 // 4, stride), dtype=numpy.uint8 + ) + uv_width = width // 2 + uv_stride = stride // 2 + + # compare carefully, avoiding all the padding bytes which to_ndarray strips out + frame = VideoFrame.from_numpy_buffer(array, "yuvj420p", width=width) + frame_array = frame.to_ndarray() + assertNdarraysEqual(frame_array[:height, :width], array[:height, :width]) + assertNdarraysEqual(frame_array[height:, :uv_width], array[height:, :uv_width]) + assertNdarraysEqual( + frame_array[height:, uv_width:], + array[height:, uv_stride : uv_stride + uv_width], + ) + + # overwrite the array, and check the shared frame buffer changed too! + array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) + frame_array = frame.to_ndarray() + assertNdarraysEqual(frame_array[:height, :width], array[:height, :width]) + assertNdarraysEqual(frame_array[height:, :uv_width], array[height:, :uv_width]) + assertNdarraysEqual( + frame_array[height:, uv_width:], + array[height:, uv_stride : uv_stride + uv_width], + ) + def test_shares_memory_nv12() -> None: array = numpy.random.randint(0, 256, size=(512 * 6 // 4, 256), dtype=numpy.uint8) @@ -605,6 +756,19 @@ def test_shares_memory_nv12() -> None: # Make sure the frame reflects that assertNdarraysEqual(frame.to_ndarray(), array) + # repeat the test, but with an array that is not fully contiguous, though the + # pixels in a row are + array = numpy.random.randint(0, 256, size=(512 * 6 // 4, 256), dtype=numpy.uint8) + array = array[:, :200] + assert not array.data.c_contiguous + frame = VideoFrame.from_numpy_buffer(array, "nv12") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + def test_shares_memory_bgr24() -> None: array = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8) @@ -616,6 +780,43 @@ def test_shares_memory_bgr24() -> None: # Make sure the frame reflects that assertNdarraysEqual(frame.to_ndarray(), array) + # repeat the test, but with an array that is not fully contiguous, though the + # pixels in a row are + array = numpy.random.randint(0, 256, size=(357, 318, 3), dtype=numpy.uint8) + array = array[:, :300, :] + assert not array.data.c_contiguous + frame = VideoFrame.from_numpy_buffer(array, "bgr24") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_shares_memory_bgra() -> None: + array = numpy.random.randint(0, 256, size=(357, 318, 4), dtype=numpy.uint8) + frame = VideoFrame.from_numpy_buffer(array, "bgra") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=(357, 318, 4), dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + + # repeat the test, but with an array that is not fully contiguous, though the + # pixels in a row are + array = numpy.random.randint(0, 256, size=(357, 318, 4), dtype=numpy.uint8) + array = array[:, :300, :] + assert not array.data.c_contiguous + frame = VideoFrame.from_numpy_buffer(array, "bgra") + assertNdarraysEqual(frame.to_ndarray(), array) + + # overwrite the array, the contents thereof + array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) + # Make sure the frame reflects that + assertNdarraysEqual(frame.to_ndarray(), array) + def test_reformat_pts() -> None: frame = VideoFrame(640, 480, "rgb24") From 12a2bdcaafa39c6d33e8b3d2ec8ec9c291be8f5d Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 16 Nov 2024 03:34:00 -0500 Subject: [PATCH 169/365] Add `add_data_stream()` method --- av/container/output.pyi | 11 +++++-- av/container/output.pyx | 67 +++++++++++++++++++++++++++++++++++++++-- tests/test_streams.py | 38 +++++++++++++++++++++++ 3 files changed, 111 insertions(+), 5 deletions(-) diff --git a/av/container/output.pyi b/av/container/output.pyi index 97b67ee71..a7c89452f 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -1,14 +1,16 @@ from fractions import Fraction -from typing import Literal, Sequence, TypeVar, overload +from typing import Literal, Sequence, TypeVar, Union, overload from av.audio.stream import AudioStream +from av.data.stream import DataStream from av.packet import Packet from av.stream import Stream +from av.subtitles.stream import SubtitleStream from av.video.stream import VideoStream from .core import Container -_StreamT = TypeVar("_StreamT", bound=Stream, default=Stream) +_StreamT = TypeVar("_StreamT", bound=Union[VideoStream, AudioStream, SubtitleStream]) class OutputContainer(Container): def __enter__(self) -> OutputContainer: ... @@ -35,8 +37,11 @@ class OutputContainer(Container): rate: Fraction | int | None = None, options: dict[str, str] | None = None, **kwargs, - ) -> Stream: ... + ) -> VideoStream | AudioStream | SubtitleStream: ... def add_stream_from_template(self, template: _StreamT, **kwargs) -> _StreamT: ... + def add_data_stream( + self, codec_name: str | None = None, options: dict[str, str] | None = None + ) -> DataStream: ... def start_encoding(self) -> None: ... def close(self) -> None: ... def mux(self, packets: Packet | Sequence[Packet]) -> None: ... diff --git a/av/container/output.pyx b/av/container/output.pyx index 3e4c9dbb4..a07bc79e8 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -47,6 +47,7 @@ cdef class OutputContainer(Container): """add_stream(codec_name, rate=None) Creates a new stream from a codec name and returns it. + Supports video, audio, and subtitle streams. :param codec_name: The name of a codec. :type codec_name: str | Codec @@ -137,7 +138,7 @@ cdef class OutputContainer(Container): def add_stream_from_template(self, Stream template not None, **kwargs): """ - Creates a new stream from a template. + Creates a new stream from a template. Supports video, audio, and subtitle streams. :param template: Copy codec from another :class:`~av.stream.Stream` instance. :param \\**kwargs: Set attributes for the stream. @@ -192,6 +193,65 @@ cdef class OutputContainer(Container): return py_stream + + def add_data_stream(self, codec_name=None, dict options=None): + """add_data_stream(codec_name=None) + + Creates a new data stream and returns it. + + :param codec_name: Optional name of the data codec (e.g. 'klv') + :type codec_name: str | None + :param dict options: Stream options. + :rtype: The new :class:`~av.data.stream.DataStream`. + """ + cdef const lib.AVCodec *codec = NULL + + if codec_name is not None: + codec = lib.avcodec_find_encoder_by_name(codec_name.encode()) + if codec == NULL: + raise ValueError(f"Unknown data codec: {codec_name}") + + # Assert that this format supports the requested codec + if not lib.avformat_query_codec(self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL): + raise ValueError( + f"{self.format.name!r} format does not support {codec_name!r} codec" + ) + + # Create new stream in the AVFormatContext + cdef lib.AVStream *stream = lib.avformat_new_stream(self.ptr, codec) + if stream == NULL: + raise MemoryError("Could not allocate stream") + + # Set up codec context if we have a codec + cdef lib.AVCodecContext *codec_context = NULL + if codec != NULL: + codec_context = lib.avcodec_alloc_context3(codec) + if codec_context == NULL: + raise MemoryError("Could not allocate codec context") + + # Some formats want stream headers to be separate + if self.ptr.oformat.flags & lib.AVFMT_GLOBALHEADER: + codec_context.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER + + # Initialize stream codec parameters + err_check(lib.avcodec_parameters_from_context(stream.codecpar, codec_context)) + else: + # For raw data streams, just set the codec type + stream.codecpar.codec_type = lib.AVMEDIA_TYPE_DATA + + # Construct the user-land stream + cdef CodecContext py_codec_context = None + if codec_context != NULL: + py_codec_context = wrap_codec_context(codec_context, codec) + + cdef Stream py_stream = wrap_stream(self, stream, py_codec_context) + self.streams.add_stream(py_stream) + + if options: + py_stream.options.update(options) + + return py_stream + cpdef start_encoding(self): """Write the file header! Called automatically.""" @@ -206,8 +266,11 @@ cdef class OutputContainer(Container): cdef Stream stream for stream in self.streams: ctx = stream.codec_context + # Skip codec context handling for data streams without codecs if ctx is None: - raise ValueError(f"Stream {stream.index} has no codec context") + if stream.type != "data": + raise ValueError(f"Stream {stream.index} has no codec context") + continue if not ctx.is_open: for k, v in self.options.items(): diff --git a/tests/test_streams.py b/tests/test_streams.py index 045745543..b1f2bd12f 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -1,3 +1,5 @@ +from fractions import Fraction + import av from .common import fate_suite @@ -78,6 +80,42 @@ def test_printing_video_stream2(self) -> None: container.close() input_.close() + def test_data_stream(self) -> None: + # First test writing and reading a simple data stream + container1 = av.open("data.ts", "w") + data_stream = container1.add_data_stream() + + test_data = [b"test data 1", b"test data 2", b"test data 3"] + for i, data_ in enumerate(test_data): + packet = av.Packet(data_) + packet.pts = i + packet.stream = data_stream + container1.mux(packet) + container1.close() + + # Test reading back the data stream + container = av.open("data.ts") + + # Test best stream selection + data = container.streams.best("data") + assert data == container.streams.data[0] + + # Test get method + assert [data] == container.streams.get(data=0) + assert [data] == container.streams.get(data=(0,)) + + # Verify we can read back all the packets, ignoring empty ones + packets = [p for p in container.demux(data) if bytes(p)] + assert len(packets) == len(test_data) + for packet, original_data in zip(packets, test_data): + assert bytes(packet) == original_data + + # Test string representation + repr = f"{data_stream}" + assert repr.startswith("") + + container.close() + # def test_side_data(self) -> None: # container = av.open(fate_suite("mov/displaymatrix.mov")) # video = container.streams.video[0] From 75ff931b92cfcee77a1f9b86f611de2bbdc4bf80 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 18 Nov 2024 02:50:54 -0500 Subject: [PATCH 170/365] Skip qminmax if not x264 --- tests/test_encode.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_encode.py b/tests/test_encode.py index 90d30dfce..0848e9b1a 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -399,6 +399,10 @@ def encode_frames_with_qminmax(frames: list, shape: tuple, qminmax: tuple) -> in Returns: total length of the encoded bytes. """ + + if av.codec.Codec("h264", "w").name != "libx264": + pytest.skip() + file = io.BytesIO() container = av.open(file, mode="w", format="mp4") stream = container.add_stream("h264", rate=30) From 48e72b69022911d7c04e37af72adefeb2aeff965 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 18 Nov 2024 13:33:47 -0500 Subject: [PATCH 171/365] Fix thread_type setter --- av/about.py | 2 +- av/codec/context.pyi | 2 ++ av/codec/context.pyx | 13 ++++++++----- av/video/stream.pyi | 7 ++++--- data.ts | Bin 0 -> 1128 bytes tests/test_streams.py | 10 ++++++++++ 6 files changed, 25 insertions(+), 9 deletions(-) create mode 100644 data.ts diff --git a/av/about.py b/av/about.py index a3d32212b..73437bbc8 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "14.0.0rc1" +__version__ = "14.0.0rc2" diff --git a/av/codec/context.pyi b/av/codec/context.pyi index 8600deaaa..8b7458597 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -12,6 +12,8 @@ class ThreadType(Flag): FRAME: ClassVar[ThreadType] SLICE: ClassVar[ThreadType] AUTO: ClassVar[ThreadType] + def __get__(self, i: object | None, owner: type | None = None) -> ThreadType: ... + def __set__(self, instance: object, value: int | str | ThreadType) -> None: ... class SkipType(Enum): NONE: ClassVar[SkipType] diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 528a73872..6e039a4e1 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -138,10 +138,8 @@ cdef class CodecContext: self.codec = wrap_codec(codec if codec != NULL else self.ptr.codec) # Set reasonable threading defaults. - # count == 0 -> use as many threads as there are CPUs. - # type == 2 -> thread within a frame. This does not change the API. - self.ptr.thread_count = 0 - self.ptr.thread_type = 2 + self.ptr.thread_count = 0 # use as many threads as there are CPUs. + self.ptr.thread_type = 0x02 # thread within a frame. Does not change the API. def _get_flags(self): return self.ptr.flags @@ -623,7 +621,12 @@ cdef class CodecContext: def thread_type(self, value): if self.is_open: raise RuntimeError("Cannot change thread_type after codec is open.") - self.ptr.thread_type = value.value + if type(value) is int: + self.ptr.thread_type = value + elif type(value) is str: + self.ptr.thread_type = ThreadType[value].value + else: + self.ptr.thread_type = value.value @property def skip_frame(self): diff --git a/av/video/stream.pyi b/av/video/stream.pyi index f0cdd5eb4..dd670d3cf 100644 --- a/av/video/stream.pyi +++ b/av/video/stream.pyi @@ -1,6 +1,7 @@ from fractions import Fraction -from typing import Any, Iterator, Literal +from typing import Iterator, Literal +from av.codec.context import ThreadType from av.packet import Packet from av.stream import Stream @@ -12,8 +13,6 @@ class VideoStream(Stream): bit_rate: int | None max_bit_rate: int | None bit_rate_tolerance: int - thread_count: int - thread_type: Any sample_aspect_ratio: Fraction | None display_aspect_ratio: Fraction | None codec_context: VideoCodecContext @@ -24,6 +23,8 @@ class VideoStream(Stream): # from codec context format: VideoFormat + thread_count: int + thread_type: ThreadType width: int height: int bits_per_coded_sample: int diff --git a/data.ts b/data.ts new file mode 100644 index 0000000000000000000000000000000000000000..a33b8b31531f8b173bfb0b5d0098a72173fe9d19 GIT binary patch literal 1128 zcmZ>F5ENi=`k>0dc#whNKjVJ}#y<@r9zu+4Zf>~+sp*`-sYPX($*Bg0~;pm4-8rxSvCzWC*pLCdjQbwOdEv2Zhy$|fq@M~?5kf8gj36)k#Kip zFqj7nc?O0$hC$;fREIDC(=mf^O9QJS10w@tNosM4LP}yuqJkmN?S>Gy{~Ho+XGXZ) T2 None: fate_suite("amv/MTV_high_res_320x240_sample_Penguin_Joke_MTV_from_WMV.amv") ) video = container.streams.video[0] + + video.thread_type = av.codec.context.ThreadType.AUTO + assert video.thread_type == av.codec.context.ThreadType.AUTO + + video.thread_type = 0x03 + assert video.thread_type == av.codec.context.ThreadType.AUTO + + video.thread_type = "AUTO" + assert video.thread_type == av.codec.context.ThreadType.AUTO + audio = container.streams.audio[0] assert [video] == container.streams.get(video=0) From c29cb8699fd45f33b3aea82e84f686491b5e1b39 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 18 Nov 2024 14:12:46 -0500 Subject: [PATCH 172/365] Clean up data.ts --- data.ts | Bin 1128 -> 0 bytes tests/test_streams.py | 10 ++++++++++ 2 files changed, 10 insertions(+) delete mode 100644 data.ts diff --git a/data.ts b/data.ts deleted file mode 100644 index a33b8b31531f8b173bfb0b5d0098a72173fe9d19..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1128 zcmZ>F5ENi=`k>0dc#whNKjVJ}#y<@r9zu+4Zf>~+sp*`-sYPX($*Bg0~;pm4-8rxSvCzWC*pLCdjQbwOdEv2Zhy$|fq@M~?5kf8gj36)k#Kip zFqj7nc?O0$hC$;fREIDC(=mf^O9QJS10w@tNosM4LP}yuqJkmN?S>Gy{~Ho+XGXZ) T2 None: for fate_name in ("h264/interlaced_crop.mp4",): container = av.open(fate_suite(fate_name)) From ce48bfa4b8afde021a418d79dad450d5b3b4b96e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 19 Nov 2024 03:00:50 -0500 Subject: [PATCH 173/365] time_base cannot be None --- av/container/output.pyx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/av/container/output.pyx b/av/container/output.pyx index a07bc79e8..1227c4340 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -1,5 +1,6 @@ import logging import os +from fractions import Fraction cimport libav as lib @@ -186,7 +187,7 @@ cdef class OutputContainer(Container): elif template.type == "audio": py_stream.time_base = kwargs.pop("time_base", 1 / template.rate) else: - py_stream.time_base = kwargs.pop("time_base", None) + py_stream.time_base = kwargs.pop("time_base", Fraction(0, 1)) for k, v in kwargs.items(): setattr(py_stream, k, v) From 25edc6493ea81599d140053d53d7b9dad06f1fa5 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 19 Nov 2024 03:07:52 -0500 Subject: [PATCH 174/365] Set min MacOS to 11.0 --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b51477083..92bff5f28 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -56,7 +56,7 @@ jobs: - name: Set Minimum MacOS Target if: matrix.os == 'macos-13' || matrix.os == 'macos-14' run: | - echo "MACOSX_DEPLOYMENT_TARGET=10.13" >> $GITHUB_ENV + echo "MACOSX_DEPLOYMENT_TARGET=11.0" >> $GITHUB_ENV - name: Build wheels env: CIBW_ARCHS: ${{ matrix.arch }} From 74f6aec8e9d639785976c7e6108349780673d3d1 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 19 Nov 2024 20:37:10 -0500 Subject: [PATCH 175/365] Test subtitle muxing --- av/container/output.pyx | 20 +++++++------------- tests/test_encode.py | 23 +++++++++++++++++++++++ tests/test_streams.py | 7 ------- 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/av/container/output.pyx b/av/container/output.pyx index 1227c4340..8fad652d4 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -144,14 +144,15 @@ cdef class OutputContainer(Container): :param template: Copy codec from another :class:`~av.stream.Stream` instance. :param \\**kwargs: Set attributes for the stream. :rtype: The new :class:`~av.stream.Stream`. - """ + cdef const lib.AVCodec *codec + cdef Codec codec_obj - if not template.codec_context: - raise ValueError("template has no codec context") - - cdef Codec codec_obj = Codec(template.codec_context.codec.name, "w") - cdef const lib.AVCodec *codec = codec_obj.ptr + if template.type == "subtitle": + codec_obj = template.codec_context.codec + else: + codec_obj = Codec(template.codec_context.codec.name, "w") + codec = codec_obj.ptr # Assert that this format supports the requested codec. if not lib.avformat_query_codec(self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL): @@ -182,13 +183,6 @@ cdef class OutputContainer(Container): cdef Stream py_stream = wrap_stream(self, stream, py_codec_context) self.streams.add_stream(py_stream) - if template.type == "video": - py_stream.time_base = kwargs.pop("time_base", 1 / template.average_rate) - elif template.type == "audio": - py_stream.time_base = kwargs.pop("time_base", 1 / template.rate) - else: - py_stream.time_base = kwargs.pop("time_base", Fraction(0, 1)) - for k, v in kwargs.items(): setattr(py_stream, k, v) diff --git a/tests/test_encode.py b/tests/test_encode.py index 0848e9b1a..c107aa044 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -222,6 +222,29 @@ def test_transcode(self) -> None: assert stream.sample_rate == sample_rate +class TestSubtitleEncoding: + def test_subtitle_muxing(self) -> None: + input_ = av.open(fate_suite("sub/MovText_capability_tester.mp4")) + in_stream = input_.streams.subtitles[0] + + output_bytes = io.BytesIO() + output = av.open(output_bytes, "w", format="mp4") + + out_stream = output.add_stream_from_template(in_stream) + + for packet in input_.demux(in_stream): + if packet.dts is None: + continue + packet.stream = out_stream + output.mux(packet) + + output.close() + output_bytes.seek(0) + assert output_bytes.getvalue().startswith( + b"\x00\x00\x00\x1cftypisom\x00\x00\x02\x00isomiso2mp41\x00\x00\x00\x08free" + ) + + class TestEncodeStreamSemantics(TestCase): def test_stream_index(self) -> None: with av.open(self.sandboxed("output.mov"), "w") as output: diff --git a/tests/test_streams.py b/tests/test_streams.py index ad8be2a62..b7699e622 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -135,10 +135,3 @@ def test_data_stream(self) -> None: assert repr.startswith("") container.close() - - # def test_side_data(self) -> None: - # container = av.open(fate_suite("mov/displaymatrix.mov")) - # video = container.streams.video[0] - - # assert video.nb_side_data == 1 - # assert video.side_data["DISPLAYMATRIX"] == -90.0 From ad99283e977c7bbae803e0abba45f53ef963f233 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 20 Nov 2024 20:28:22 -0500 Subject: [PATCH 176/365] Add function for getting loudnorm stats Getting these stats is otherwise impossible. --- av/filter/loudnorm.pxd | 4 + av/filter/loudnorm.pyi | 3 + av/filter/loudnorm.pyx | 63 ++++++++++++ av/filter/loudnorm_impl.c | 211 ++++++++++++++++++++++++++++++++++++++ av/filter/loudnorm_impl.h | 12 +++ setup.py | 28 ++++- tests/test_streams.py | 11 ++ 7 files changed, 331 insertions(+), 1 deletion(-) create mode 100644 av/filter/loudnorm.pxd create mode 100644 av/filter/loudnorm.pyi create mode 100644 av/filter/loudnorm.pyx create mode 100644 av/filter/loudnorm_impl.c create mode 100644 av/filter/loudnorm_impl.h diff --git a/av/filter/loudnorm.pxd b/av/filter/loudnorm.pxd new file mode 100644 index 000000000..b08d3502f --- /dev/null +++ b/av/filter/loudnorm.pxd @@ -0,0 +1,4 @@ +from av.audio.stream cimport AudioStream + + +cpdef bytes stats(str loudnorm_args, AudioStream stream) diff --git a/av/filter/loudnorm.pyi b/av/filter/loudnorm.pyi new file mode 100644 index 000000000..c680f638d --- /dev/null +++ b/av/filter/loudnorm.pyi @@ -0,0 +1,3 @@ +from av.audio.stream import AudioStream + +def stats(loudnorm_args: str, stream: AudioStream) -> bytes: ... diff --git a/av/filter/loudnorm.pyx b/av/filter/loudnorm.pyx new file mode 100644 index 000000000..662bbd230 --- /dev/null +++ b/av/filter/loudnorm.pyx @@ -0,0 +1,63 @@ +# av/filter/loudnorm.pyx + +cimport libav as lib +from cpython.bytes cimport PyBytes_FromString +from libc.stdlib cimport free + +from av.audio.codeccontext cimport AudioCodecContext +from av.audio.stream cimport AudioStream +from av.container.core cimport Container +from av.stream cimport Stream + + +cdef extern from "libavcodec/avcodec.h": + ctypedef struct AVCodecContext: + pass + +cdef extern from "libavformat/avformat.h": + ctypedef struct AVFormatContext: + pass + +cdef extern from "loudnorm_impl.h": + char* loudnorm_get_stats( + AVFormatContext* fmt_ctx, + int audio_stream_index, + const char* loudnorm_args + ) nogil + + +cpdef bytes stats(str loudnorm_args, AudioStream stream): + """ + Get loudnorm statistics for an audio stream. + + Args: + loudnorm_args (str): Arguments for the loudnorm filter (e.g. "i=-24.0:lra=7.0:tp=-2.0") + stream (AudioStream): Input audio stream to analyze + + Returns: + bytes: JSON string containing the loudnorm statistics + """ + + if "print_format=json" not in loudnorm_args: + loudnorm_args = loudnorm_args + ":print_format=json" + + cdef Container container = stream.container + cdef AVFormatContext* format_ptr = container.ptr + + container.ptr = NULL # Prevent double-free + + cdef int stream_index = stream.index + cdef bytes py_args = loudnorm_args.encode("utf-8") + cdef const char* c_args = py_args + cdef char* result + + with nogil: + result = loudnorm_get_stats(format_ptr, stream_index, c_args) + + if result == NULL: + raise RuntimeError("Failed to get loudnorm stats") + + py_result = result[:] # Make a copy of the string + free(result) # Free the C string + + return py_result diff --git a/av/filter/loudnorm_impl.c b/av/filter/loudnorm_impl.c new file mode 100644 index 000000000..e8d56ddba --- /dev/null +++ b/av/filter/loudnorm_impl.c @@ -0,0 +1,211 @@ +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 + #include +#else + #include +#endif + +#ifdef _WIN32 + static CRITICAL_SECTION json_mutex; + static CONDITION_VARIABLE json_cond; + static int mutex_initialized = 0; +#else + static pthread_mutex_t json_mutex = PTHREAD_MUTEX_INITIALIZER; + static pthread_cond_t json_cond = PTHREAD_COND_INITIALIZER; +#endif + +static char json_buffer[2048] = {0}; +static int json_captured = 0; + +// Custom logging callback +static void logging_callback(void *ptr, int level, const char *fmt, va_list vl) { + char line[2048]; + vsnprintf(line, sizeof(line), fmt, vl); + + const char *json_start = strstr(line, "{"); + if (json_start) { + #ifdef _WIN32 + EnterCriticalSection(&json_mutex); + #else + pthread_mutex_lock(&json_mutex); + #endif + + strncpy(json_buffer, json_start, sizeof(json_buffer) - 1); + json_captured = 1; + + #ifdef _WIN32 + WakeConditionVariable(&json_cond); + LeaveCriticalSection(&json_mutex); + #else + pthread_cond_signal(&json_cond); + pthread_mutex_unlock(&json_mutex); + #endif + } +} + +char* loudnorm_get_stats( + AVFormatContext* fmt_ctx, + int audio_stream_index, + const char* loudnorm_args +) { + char* result = NULL; + json_captured = 0; // Reset the captured flag + memset(json_buffer, 0, sizeof(json_buffer)); // Clear the buffer + + #ifdef _WIN32 + // Initialize synchronization objects if needed + if (!mutex_initialized) { + InitializeCriticalSection(&json_mutex); + InitializeConditionVariable(&json_cond); + mutex_initialized = 1; + } + #endif + + av_log_set_callback(logging_callback); + + AVFilterGraph *filter_graph = NULL; + AVFilterContext *src_ctx = NULL, *sink_ctx = NULL, *loudnorm_ctx = NULL; + + AVCodec *codec = NULL; + AVCodecContext *codec_ctx = NULL; + int ret; + + AVCodecParameters *codecpar = fmt_ctx->streams[audio_stream_index]->codecpar; + codec = (AVCodec *)avcodec_find_decoder(codecpar->codec_id); + codec_ctx = avcodec_alloc_context3(codec); + avcodec_parameters_to_context(codec_ctx, codecpar); + avcodec_open2(codec_ctx, codec, NULL); + + char ch_layout_str[64]; + av_channel_layout_describe(&codecpar->ch_layout, ch_layout_str, sizeof(ch_layout_str)); + + filter_graph = avfilter_graph_alloc(); + + char args[512]; + snprintf(args, sizeof(args), + "time_base=%d/%d:sample_rate=%d:sample_fmt=%s:channel_layout=%s", + fmt_ctx->streams[audio_stream_index]->time_base.num, + fmt_ctx->streams[audio_stream_index]->time_base.den, + codecpar->sample_rate, + av_get_sample_fmt_name(codec_ctx->sample_fmt), + ch_layout_str); + + avfilter_graph_create_filter(&src_ctx, avfilter_get_by_name("abuffer"), + "src", args, NULL, filter_graph); + avfilter_graph_create_filter(&sink_ctx, avfilter_get_by_name("abuffersink"), + "sink", NULL, NULL, filter_graph); + avfilter_graph_create_filter(&loudnorm_ctx, avfilter_get_by_name("loudnorm"), + "loudnorm", loudnorm_args, NULL, filter_graph); + + avfilter_link(src_ctx, 0, loudnorm_ctx, 0); + avfilter_link(loudnorm_ctx, 0, sink_ctx, 0); + avfilter_graph_config(filter_graph, NULL); + + AVPacket *packet = av_packet_alloc(); + AVFrame *frame = av_frame_alloc(); + AVFrame *filt_frame = av_frame_alloc(); + + while ((ret = av_read_frame(fmt_ctx, packet)) >= 0) { + if (packet->stream_index != audio_stream_index) { + av_packet_unref(packet); + continue; + } + + ret = avcodec_send_packet(codec_ctx, packet); + if (ret < 0) { + av_packet_unref(packet); + continue; + } + + while (ret >= 0) { + ret = avcodec_receive_frame(codec_ctx, frame); + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break; + if (ret < 0) goto end; + + ret = av_buffersrc_add_frame_flags(src_ctx, frame, AV_BUFFERSRC_FLAG_KEEP_REF); + if (ret < 0) goto end; + + while (1) { + ret = av_buffersink_get_frame(sink_ctx, filt_frame); + if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break; + if (ret < 0) goto end; + av_frame_unref(filt_frame); + } + } + av_packet_unref(packet); + } + + // Flush decoder + avcodec_send_packet(codec_ctx, NULL); + while (avcodec_receive_frame(codec_ctx, frame) >= 0) { + av_buffersrc_add_frame(src_ctx, frame); + } + + // Flush filter + av_buffersrc_add_frame(src_ctx, NULL); + while (av_buffersink_get_frame(sink_ctx, filt_frame) >= 0) { + av_frame_unref(filt_frame); + } + + // Force stats print + if (loudnorm_ctx) { + av_log_set_level(AV_LOG_INFO); + av_opt_set(loudnorm_ctx, "print_format", "json", AV_OPT_SEARCH_CHILDREN); + av_opt_set(loudnorm_ctx, "measured_i", NULL, AV_OPT_SEARCH_CHILDREN); + av_opt_set(loudnorm_ctx, "measured_lra", NULL, AV_OPT_SEARCH_CHILDREN); + av_opt_set(loudnorm_ctx, "measured_tp", NULL, AV_OPT_SEARCH_CHILDREN); + av_opt_set(loudnorm_ctx, "measured_thresh", NULL, AV_OPT_SEARCH_CHILDREN); + avfilter_init_str(loudnorm_ctx, NULL); + } + + avfilter_graph_request_oldest(filter_graph); + +end: + avcodec_free_context(&codec_ctx); + avfilter_graph_free(&filter_graph); + avformat_close_input(&fmt_ctx); + av_frame_free(&filt_frame); + av_frame_free(&frame); + av_packet_free(&packet); + + #ifdef _WIN32 + EnterCriticalSection(&json_mutex); + while (!json_captured) { + if (!SleepConditionVariableCS(&json_cond, &json_mutex, 5000)) { // 5 second timeout + fprintf(stderr, "Timeout waiting for JSON data\n"); + break; + } + } + if (json_captured) { + result = _strdup(json_buffer); // Use _strdup on Windows + } + LeaveCriticalSection(&json_mutex); + #else + struct timespec timeout; + clock_gettime(CLOCK_REALTIME, &timeout); + timeout.tv_sec += 5; // 5 second timeout + + pthread_mutex_lock(&json_mutex); + while (json_captured == 0) { + int ret = pthread_cond_timedwait(&json_cond, &json_mutex, &timeout); + if (ret == ETIMEDOUT) { + fprintf(stderr, "Timeout waiting for JSON data\n"); + break; + } + } + if (json_captured) { + result = strdup(json_buffer); + } + pthread_mutex_unlock(&json_mutex); + #endif + + av_log_set_callback(av_log_default_callback); + return result; +} \ No newline at end of file diff --git a/av/filter/loudnorm_impl.h b/av/filter/loudnorm_impl.h new file mode 100644 index 000000000..7357e4668 --- /dev/null +++ b/av/filter/loudnorm_impl.h @@ -0,0 +1,12 @@ +#ifndef AV_FILTER_LOUDNORM_H +#define AV_FILTER_LOUDNORM_H + +#include + +char* loudnorm_get_stats( + AVFormatContext* fmt_ctx, + int audio_stream_index, + const char* loudnorm_args +); + +#endif // AV_FILTER_LOUDNORM_H \ No newline at end of file diff --git a/setup.py b/setup.py index 935d233aa..685412b99 100644 --- a/setup.py +++ b/setup.py @@ -153,10 +153,36 @@ def parse_cflags(raw_flags): "library_dirs": [], } +loudnorm_extension = Extension( + "av.filter.loudnorm", + sources=[ + "av/filter/loudnorm.pyx", + "av/filter/loudnorm_impl.c", + ], + include_dirs=["av/filter"] + extension_extra["include_dirs"], + libraries=extension_extra["libraries"], + library_dirs=extension_extra["library_dirs"], +) + +# Add the cythonized loudnorm extension to ext_modules +ext_modules = cythonize( + loudnorm_extension, + compiler_directives={ + "c_string_type": "str", + "c_string_encoding": "ascii", + "embedsignature": True, + "language_level": 3, + }, + build_dir="src", + include_path=["include"], +) + # Construct the modules that we find in the "av" directory. -ext_modules = [] for dirname, dirnames, filenames in os.walk("av"): for filename in filenames: + if filename == "loudnorm.pyx": + continue + # We are looking for Cython sources. if filename.startswith(".") or os.path.splitext(filename)[1] != ".pyx": continue diff --git a/tests/test_streams.py b/tests/test_streams.py index b7699e622..c7b234d48 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -26,6 +26,17 @@ def test_stream_tuples(self) -> None: audio_streams = tuple([s for s in container.streams if s.type == "audio"]) assert audio_streams == container.streams.audio + def test_loudnorm(self) -> None: + container = av.open( + fate_suite("amv/MTV_high_res_320x240_sample_Penguin_Joke_MTV_from_WMV.amv") + ) + audio = container.streams.audio[0] + stats = av.filter.loudnorm.stats("i=-24.0:lra=7.0:tp=-2.0", audio) + + assert isinstance(stats, bytes) and len(stats) > 30 + assert b"inf" not in stats + assert b'"input_i"' in stats + def test_selection(self) -> None: container = av.open( fate_suite("amv/MTV_high_res_320x240_sample_Penguin_Joke_MTV_from_WMV.amv") From 23aec498301e50ca391c1fc37f0c6c25f857bd81 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 24 Nov 2024 00:56:09 -0500 Subject: [PATCH 177/365] `skip_frame`: Use str literals instead of Enum --- .github/workflows/smoke.yml | 6 ++--- av/codec/context.pyi | 13 +++------ av/codec/context.pyx | 53 ++++++++++++++++++++++++++++--------- tests/test_codec_context.py | 2 +- 4 files changed, 48 insertions(+), 26 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index ca6a3f15c..352634ff6 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -37,8 +37,8 @@ jobs: - {os: ubuntu-latest, python: "3.12", ffmpeg: "7.1", extras: true} - {os: ubuntu-latest, python: "3.9", ffmpeg: "7.0.2"} - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "7.1"} - - {os: macos-14, python: "3.9", ffmpeg: "7.1"} - - {os: macos-14, python: "3.9", ffmpeg: "7.0.2"} + - {os: macos-14, python: "3.9", ffmpeg: "7.1"} + - {os: macos-14, python: "3.9", ffmpeg: "7.0.2"} env: PYAV_PYTHON: python${{ matrix.config.python }} @@ -65,7 +65,7 @@ jobs: fi ;; macos-14) - brew install automake libtool nasm pkg-config libpng libvorbis libvpx opus x264 + brew install automake libtool nasm libpng libvorbis libvpx opus x264 ;; esac diff --git a/av/codec/context.pyi b/av/codec/context.pyi index 8b7458597..e58b0879d 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -15,15 +15,6 @@ class ThreadType(Flag): def __get__(self, i: object | None, owner: type | None = None) -> ThreadType: ... def __set__(self, instance: object, value: int | str | ThreadType) -> None: ... -class SkipType(Enum): - NONE: ClassVar[SkipType] - DEFAULT: ClassVar[SkipType] - NONREF: ClassVar[SkipType] - BIDIR: ClassVar[SkipType] - NONINTRA: ClassVar[SkipType] - NONKEY: ClassVar[SkipType] - ALL: ClassVar[SkipType] - class Flags(EnumFlag): NONE: int UNALIGNED: int @@ -71,7 +62,9 @@ class CodecContext: bit_rate_tolerance: int thread_count: int thread_type: ThreadType - skip_frame: SkipType + skip_frame: Literal[ + "NONE", "DEFAULT", "NONREF", "BIDIR", "NONINTRA", "NONKEY", "ALL" + ] # flags unaligned: bool diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 6e039a4e1..f420b1157 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -47,14 +47,6 @@ class ThreadType(Flag): SLICE: "Decode more than one part of a single frame at once" = lib.FF_THREAD_SLICE AUTO: "Decode using both FRAME and SLICE methods." = lib.FF_THREAD_SLICE | lib.FF_THREAD_FRAME -class SkipType(Enum): - NONE: "Discard nothing" = lib.AVDISCARD_NONE - DEFAULT: "Discard useless packets like 0 size packets in AVI" = lib.AVDISCARD_DEFAULT - NONREF: "Discard all non reference" = lib.AVDISCARD_NONREF - BIDIR: "Discard all bidirectional frames" = lib.AVDISCARD_BIDIR - NONINTRA: "Discard all non intra frames" = lib.AVDISCARD_NONINTRA - NONKEY: "Discard all frames except keyframes" = lib.AVDISCARD_NONKEY - ALL: "Discard all" = lib.AVDISCARD_ALL Flags = define_enum("Flags", __name__, ( ("NONE", 0), @@ -630,16 +622,53 @@ cdef class CodecContext: @property def skip_frame(self): - """One of :class:`.SkipType`. + """Returns one of the following str literals: - Wraps :ffmpeg:`AVCodecContext.skip_frame`. + "NONE" Discard nothing + "DEFAULT" Discard useless packets like 0 size packets in AVI + "NONREF" Discard all non reference + "BIDIR" Discard all bidirectional frames + "NONINTRA" Discard all non intra frames + "NONKEY Discard all frames except keyframes + "ALL" Discard all + Wraps :ffmpeg:`AVCodecContext.skip_frame`. """ - return SkipType(self.ptr.skip_frame) + value = self.ptr.skip_frame + if value == lib.AVDISCARD_NONE: + return "NONE" + if value == lib.AVDISCARD_DEFAULT: + return "DEFAULT" + if value == lib.AVDISCARD_NONREF: + return "NONREF" + if value == lib.AVDISCARD_BIDIR: + return "BIDIR" + if value == lib.AVDISCARD_NONINTRA: + return "NONINTRA" + if value == lib.AVDISCARD_NONKEY: + return "NONKEY" + if value == lib.AVDISCARD_ALL: + return "ALL" + return f"{value}" @skip_frame.setter def skip_frame(self, value): - self.ptr.skip_frame = value.value + if value == "NONE": + self.ptr.skip_frame = lib.AVDISCARD_NONE + elif value == "DEFAULT": + self.ptr.skip_frame = lib.AVDISCARD_DEFAULT + elif value == "NONREF": + self.ptr.skip_frame = lib.AVDISCARD_NONREF + elif value == "BIDIR": + self.ptr.skip_frame = lib.AVDISCARD_BIDIR + elif value == "NONINTRA": + self.ptr.skip_frame = lib.AVDISCARD_NONINTRA + elif value == "NONKEY": + self.ptr.skip_frame = lib.AVDISCARD_NONKEY + elif value == "ALL": + self.ptr.skip_frame = lib.AVDISCARD_ALL + else: + raise ValueError("Invalid skip_frame type") @property def delay(self): diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 6425c4dd1..1173a5865 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -70,7 +70,7 @@ def iter_raw_frames( class TestCodecContext(TestCase): def test_skip_frame_default(self): ctx = Codec("png", "w").create() - assert ctx.skip_frame.name == "DEFAULT" + assert ctx.skip_frame == "DEFAULT" def test_codec_delay(self) -> None: with av.open(fate_suite("mkv/codec_delay_opus.mkv")) as container: From 95a8c554cca06b3e49ec26d5d2764b1dd65274df Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 28 Nov 2024 17:13:17 -0500 Subject: [PATCH 178/365] Update changelog --- CHANGELOG.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5d034b223..d8687fa2f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -22,18 +22,23 @@ v14.0.0 Major: - Drop FFmpeg 6. -- Remove ``AVError`` alias (use ``FFmpegError`` directly instead). +- Drop support for MacOS <11 in our binary wheels. +- Change many instances ``EnumItem`` s (our custom enum class) with Python's standard Enums. - Remove the `template` arg from ``OutputContainer.add_stream()``. - Remove ``CodecContext.close()`` because it's deprecated in ffmpeg. +- Remove ``AVError`` alias (use ``FFmpegError`` directly instead). Features: - Add ``OutputContainer.add_stream_from_template()`` by :gh-user:`WyattBlue` and :gh-user:`cdce8p`. +- Add ``OutputContainer.add_data_stream()`` by :gh-user:`WyattBlue`. +- Add ``filter.loudnorm.stats()`` function that returns the stats of loudnorm for 2-pass filtering by :gh-user:`WyattBlue`. - Add ``qmin`` and ``qmax`` parameters to the ``VideoCodecContext`` by :gh-user:`davidplowman` in (:pr:`1618`). Fixes: - Correct ``Colorspace``'s lowercase enums. +- Updated ``sidedata.Type`` enum. - Ensure streams in StreamContainer are released. Fixes :issue:`1599`. v13.1.0 From 445d759365ba4e7e7003d7ce2163f621ff42eac9 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 1 Dec 2024 05:23:47 -0500 Subject: [PATCH 179/365] Preserve log level --- av/filter/__init__.py | 1 + av/filter/__init__.pyi | 1 + av/filter/loudnorm.pyx | 6 ++++++ av/filter/loudnorm_impl.c | 16 ++-------------- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/av/filter/__init__.py b/av/filter/__init__.py index 207d6e15e..5dd4430d4 100644 --- a/av/filter/__init__.py +++ b/av/filter/__init__.py @@ -1,2 +1,3 @@ from .filter import Filter, FilterFlags, filter_descriptor, filters_available from .graph import Graph +from .loudnorm import stats diff --git a/av/filter/__init__.pyi b/av/filter/__init__.pyi index 8a6b5a59b..5be1326c9 100644 --- a/av/filter/__init__.pyi +++ b/av/filter/__init__.pyi @@ -1,3 +1,4 @@ from .context import * from .filter import * from .graph import * +from .loudnorm import * diff --git a/av/filter/loudnorm.pyx b/av/filter/loudnorm.pyx index 662bbd230..78f320a9e 100644 --- a/av/filter/loudnorm.pyx +++ b/av/filter/loudnorm.pyx @@ -8,6 +8,7 @@ from av.audio.codeccontext cimport AudioCodecContext from av.audio.stream cimport AudioStream from av.container.core cimport Container from av.stream cimport Stream +from av.logging import get_level, set_level cdef extern from "libavcodec/avcodec.h": @@ -51,6 +52,9 @@ cpdef bytes stats(str loudnorm_args, AudioStream stream): cdef const char* c_args = py_args cdef char* result + # Save log level since C function overwrite it. + level = get_level() + with nogil: result = loudnorm_get_stats(format_ptr, stream_index, c_args) @@ -60,4 +64,6 @@ cpdef bytes stats(str loudnorm_args, AudioStream stream): py_result = result[:] # Make a copy of the string free(result) # Free the C string + set_level(level) + return py_result diff --git a/av/filter/loudnorm_impl.c b/av/filter/loudnorm_impl.c index e8d56ddba..79f960080 100644 --- a/av/filter/loudnorm_impl.c +++ b/av/filter/loudnorm_impl.c @@ -3,7 +3,6 @@ #include #include #include -#include #include #ifdef _WIN32 @@ -154,22 +153,11 @@ char* loudnorm_get_stats( av_frame_unref(filt_frame); } - // Force stats print - if (loudnorm_ctx) { - av_log_set_level(AV_LOG_INFO); - av_opt_set(loudnorm_ctx, "print_format", "json", AV_OPT_SEARCH_CHILDREN); - av_opt_set(loudnorm_ctx, "measured_i", NULL, AV_OPT_SEARCH_CHILDREN); - av_opt_set(loudnorm_ctx, "measured_lra", NULL, AV_OPT_SEARCH_CHILDREN); - av_opt_set(loudnorm_ctx, "measured_tp", NULL, AV_OPT_SEARCH_CHILDREN); - av_opt_set(loudnorm_ctx, "measured_thresh", NULL, AV_OPT_SEARCH_CHILDREN); - avfilter_init_str(loudnorm_ctx, NULL); - } - - avfilter_graph_request_oldest(filter_graph); + // Pushes graph + avfilter_graph_free(&filter_graph); end: avcodec_free_context(&codec_ctx); - avfilter_graph_free(&filter_graph); avformat_close_input(&fmt_ctx); av_frame_free(&filt_frame); av_frame_free(&frame); From 92f547b3e7feea249dc275c6d7df87b7ed67fe3a Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 1 Dec 2024 13:54:11 -0500 Subject: [PATCH 180/365] Use IntEnum for codec capabilities --- av/codec/codec.pyi | 76 ++++++++------------ av/codec/codec.pyx | 175 ++++++++++++++++----------------------------- docs/api/codec.rst | 10 +-- 3 files changed, 94 insertions(+), 167 deletions(-) diff --git a/av/codec/codec.pyi b/av/codec/codec.pyi index fe2d2fd41..3323895ed 100644 --- a/av/codec/codec.pyi +++ b/av/codec/codec.pyi @@ -1,4 +1,4 @@ -from enum import Flag +from enum import Flag, IntEnum from fractions import Fraction from typing import ClassVar, Literal, overload @@ -21,28 +21,29 @@ class Properties(Flag): BITMAP_SUB: ClassVar[Properties] TEXT_SUB: ClassVar[Properties] -class Capabilities(EnumFlag): - NONE: int - DARW_HORIZ_BAND: int - DR1: int - HWACCEL: int - DELAY: int - SMALL_LAST_FRAME: int - HWACCEL_VDPAU: int - SUBFRAMES: int - EXPERIMENTAL: int - CHANNEL_CONF: int - NEG_LINESIZES: int - FRAME_THREADS: int - SLICE_THREADS: int - PARAM_CHANGE: int - AUTO_THREADS: int - VARIABLE_FRAME_SIZE: int - AVOID_PROBING: int - HARDWARE: int - HYBRID: int - ENCODER_REORDERED_OPAQUE: int - ENCODER_FLUSH: int +class Capabilities(IntEnum): + none: int + draw_horiz_band: int + dr1: int + hwaccel: int + delay: int + small_last_frame: int + hwaccel_vdpau: int + subframes: int + experimental: int + channel_conf: int + neg_linesizes: int + frame_threads: int + slice_threads: int + param_change: int + auto_threads: int + variable_frame_size: int + avoid_probing: int + hardware: int + hybrid: int + encoder_reordered_opaque: int + encoder_flush: int + encoder_recon_frame: int class UnknownCodecError(ValueError): ... @@ -79,29 +80,12 @@ class Codec: def bitmap_sub(self) -> bool: ... @property def text_sub(self) -> bool: ... - - capabilities: Capabilities - draw_horiz_band: bool - dr1: bool - hwaccel: bool - delay: bool - small_last_frame: bool - hwaccel_vdpau: bool - subframes: bool - experimental: bool - channel_conf: bool - neg_linesizes: bool - frame_threads: bool - slice_threads: bool - param_change: bool - auto_threads: bool - variable_frame_size: bool - avoid_probing: bool - hardware: bool - hybrid: bool - encoder_reordered_opaque: bool - encoder_flush: bool - + @property + def capabilities(self) -> int: ... + @property + def experimental(self) -> bool: ... + @property + def delay(self) -> bool: ... def __init__(self, name: str, mode: Literal["r", "w"] = "r") -> None: ... @overload def create(self, kind: Literal["video"]) -> VideoCodecContext: ... diff --git a/av/codec/codec.pyx b/av/codec/codec.pyx index 9b29cb178..98a5b50d8 100644 --- a/av/codec/codec.pyx +++ b/av/codec/codec.pyx @@ -1,10 +1,9 @@ from av.audio.format cimport get_audio_format from av.descriptor cimport wrap_avclass -from av.enum cimport define_enum from av.utils cimport avrational_to_fraction from av.video.format cimport get_video_format -from enum import Flag +from enum import Flag, IntEnum cdef object _cinit_sentinel = object() @@ -25,94 +24,30 @@ class Properties(Flag): BITMAP_SUB = lib.AV_CODEC_PROP_BITMAP_SUB TEXT_SUB = lib.AV_CODEC_PROP_TEXT_SUB -Capabilities = define_enum("Capabilities", "av.codec", ( - ("NONE", 0), - ("DRAW_HORIZ_BAND", lib.AV_CODEC_CAP_DRAW_HORIZ_BAND, - """Decoder can use draw_horiz_band callback."""), - ("DR1", lib.AV_CODEC_CAP_DR1, - """Codec uses get_buffer() for allocating buffers and supports custom allocators. - If not set, it might not use get_buffer() at all or use operations that - assume the buffer was allocated by avcodec_default_get_buffer."""), - ("HWACCEL", 1 << 4), - ("DELAY", lib.AV_CODEC_CAP_DELAY, - """Encoder or decoder requires flushing with NULL input at the end in order to - give the complete and correct output. - - NOTE: If this flag is not set, the codec is guaranteed to never be fed with - with NULL data. The user can still send NULL data to the public encode - or decode function, but libavcodec will not pass it along to the codec - unless this flag is set. - - Decoders: - The decoder has a non-zero delay and needs to be fed with avpkt->data=NULL, - avpkt->size=0 at the end to get the delayed data until the decoder no longer - returns frames. - - Encoders: - The encoder needs to be fed with NULL data at the end of encoding until the - encoder no longer returns data. - - NOTE: For encoders implementing the AVCodec.encode2() function, setting this - flag also means that the encoder must set the pts and duration for - each output packet. If this flag is not set, the pts and duration will - be determined by libavcodec from the input frame."""), - ("SMALL_LAST_FRAME", lib.AV_CODEC_CAP_SMALL_LAST_FRAME, - """Codec can be fed a final frame with a smaller size. - This can be used to prevent truncation of the last audio samples."""), - ("HWACCEL_VDPAU", 1 << 7), - ("SUBFRAMES", lib.AV_CODEC_CAP_SUBFRAMES, - """Codec can output multiple frames per AVPacket - Normally demuxers return one frame at a time, demuxers which do not do - are connected to a parser to split what they return into proper frames. - This flag is reserved to the very rare category of codecs which have a - bitstream that cannot be split into frames without timeconsuming - operations like full decoding. Demuxers carrying such bitstreams thus - may return multiple frames in a packet. This has many disadvantages like - prohibiting stream copy in many cases thus it should only be considered - as a last resort."""), - ("EXPERIMENTAL", lib.AV_CODEC_CAP_EXPERIMENTAL, - """Codec is experimental and is thus avoided in favor of non experimental - encoders"""), - ("CHANNEL_CONF", lib.AV_CODEC_CAP_CHANNEL_CONF, - """Codec should fill in channel configuration and samplerate instead of container"""), - ("NEG_LINESIZES", 1 << 11), - ("FRAME_THREADS", lib.AV_CODEC_CAP_FRAME_THREADS, - """Codec supports frame-level multithreading""",), - ("SLICE_THREADS", lib.AV_CODEC_CAP_SLICE_THREADS, - """Codec supports slice-based (or partition-based) multithreading."""), - ("PARAM_CHANGE", lib.AV_CODEC_CAP_PARAM_CHANGE, - """Codec supports changed parameters at any point."""), - ("AUTO_THREADS", lib.AV_CODEC_CAP_OTHER_THREADS, - """Codec supports multithreading through a method other than slice- or - frame-level multithreading. Typically this marks wrappers around - multithreading-capable external libraries."""), - ("VARIABLE_FRAME_SIZE", lib.AV_CODEC_CAP_VARIABLE_FRAME_SIZE, - """Audio encoder supports receiving a different number of samples in each call."""), - ("AVOID_PROBING", lib.AV_CODEC_CAP_AVOID_PROBING, - """Decoder is not a preferred choice for probing. - This indicates that the decoder is not a good choice for probing. - It could for example be an expensive to spin up hardware decoder, - or it could simply not provide a lot of useful information about - the stream. - A decoder marked with this flag should only be used as last resort - choice for probing."""), - ("HARDWARE", lib.AV_CODEC_CAP_HARDWARE, - """Codec is backed by a hardware implementation. Typically used to - identify a non-hwaccel hardware decoder. For information about hwaccels, use - avcodec_get_hw_config() instead."""), - ("HYBRID", lib.AV_CODEC_CAP_HYBRID, - """Codec is potentially backed by a hardware implementation, but not - necessarily. This is used instead of AV_CODEC_CAP_HARDWARE, if the - implementation provides some sort of internal fallback."""), - ("ENCODER_REORDERED_OPAQUE", 1 << 20, # lib.AV_CODEC_CAP_ENCODER_REORDERED_OPAQUE, # FFmpeg 4.2 - """This codec takes the reordered_opaque field from input AVFrames - and returns it in the corresponding field in AVCodecContext after - encoding."""), - ("ENCODER_FLUSH", 1 << 21, # lib.AV_CODEC_CAP_ENCODER_FLUSH # FFmpeg 4.3 - """This encoder can be flushed using avcodec_flush_buffers(). If this - flag is not set, the encoder must be closed and reopened to ensure that - no frames remain pending."""), -), is_flags=True) + +class Capabilities(IntEnum): + none = 0 + draw_horiz_band = lib.AV_CODEC_CAP_DRAW_HORIZ_BAND + dr1 = lib.AV_CODEC_CAP_DR1 + hwaccel = 1 << 4 + delay = lib.AV_CODEC_CAP_DELAY + small_last_frame = lib.AV_CODEC_CAP_SMALL_LAST_FRAME + hwaccel_vdpau = 1 << 7 + subframes = lib.AV_CODEC_CAP_SUBFRAMES + experimental = lib.AV_CODEC_CAP_EXPERIMENTAL + channel_conf = lib.AV_CODEC_CAP_CHANNEL_CONF + neg_linesizes = 1 << 11 + frame_threads = lib.AV_CODEC_CAP_FRAME_THREADS + slice_threads = lib.AV_CODEC_CAP_SLICE_THREADS + param_change = lib.AV_CODEC_CAP_PARAM_CHANGE + auto_threads = lib.AV_CODEC_CAP_OTHER_THREADS + variable_frame_size = lib.AV_CODEC_CAP_VARIABLE_FRAME_SIZE + avoid_probing = lib.AV_CODEC_CAP_AVOID_PROBING + hardware = lib.AV_CODEC_CAP_HARDWARE + hybrid = lib.AV_CODEC_CAP_HYBRID + encoder_reordered_opaque = 1 << 20 + encoder_flush = 1 << 21 + encoder_recon_frame = 1 << 22 class UnknownCodecError(ValueError): @@ -296,32 +231,48 @@ cdef class Codec: def text_sub(self): return bool(self.desc.props & lib.AV_CODEC_PROP_TEXT_SUB) - @Capabilities.property + @property def capabilities(self): - """Flag property of :class:`.Capabilities`""" + """ + Get the capabilities bitmask of the codec. + + This method returns an integer representing the codec capabilities bitmask, + which can be used to check specific codec features by performing bitwise + operations with the Capabilities enum values. + + :example: + + .. code-block:: python + + from av.codec import Codec, Capabilities + + codec = Codec("h264", "w") + + # Check if the codec can be fed a final frame with a smaller size. + # This can be used to prevent truncation of the last audio samples. + small_last_frame = bool(codec.capabilities & Capabilities.small_last_frame) + + :rtype: int + """ return self.ptr.capabilities - draw_horiz_band = capabilities.flag_property("DRAW_HORIZ_BAND") - dr1 = capabilities.flag_property("DR1") - hwaccel = capabilities.flag_property("HWACCEL") - delay = capabilities.flag_property("DELAY") - small_last_frame = capabilities.flag_property("SMALL_LAST_FRAME") - hwaccel_vdpau = capabilities.flag_property("HWACCEL_VDPAU") - subframes = capabilities.flag_property("SUBFRAMES") - experimental = capabilities.flag_property("EXPERIMENTAL") - channel_conf = capabilities.flag_property("CHANNEL_CONF") - neg_linesizes = capabilities.flag_property("NEG_LINESIZES") - frame_threads = capabilities.flag_property("FRAME_THREADS") - slice_threads = capabilities.flag_property("SLICE_THREADS") - param_change = capabilities.flag_property("PARAM_CHANGE") - auto_threads = capabilities.flag_property("AUTO_THREADS") - variable_frame_size = capabilities.flag_property("VARIABLE_FRAME_SIZE") - avoid_probing = capabilities.flag_property("AVOID_PROBING") - hardware = capabilities.flag_property("HARDWARE") - hybrid = capabilities.flag_property("HYBRID") - encoder_reordered_opaque = capabilities.flag_property("ENCODER_REORDERED_OPAQUE") - encoder_flush = capabilities.flag_property("ENCODER_FLUSH") + @property + def experimental(self): + """ + Check if codec is experimental and is thus avoided in favor of non experimental encoders. + + :rtype: bool + """ + return bool(self.ptr.capabilities & lib.AV_CODEC_CAP_EXPERIMENTAL) + @property + def delay(self): + """ + If true, encoder or decoder requires flushing with `None` at the end in order to give the complete and correct output. + + :rtype: bool + """ + return bool(self.ptr.capabilities & lib.AV_CODEC_CAP_DELAY) cdef get_codec_names(): names = set() diff --git a/docs/api/codec.rst b/docs/api/codec.rst index 770a271a4..d4a241425 100644 --- a/docs/api/codec.rst +++ b/docs/api/codec.rst @@ -36,21 +36,13 @@ Flags Wraps :ffmpeg:`AVCodecDescriptor.props` (``AV_CODEC_PROP_*``). - .. enumtable:: av.codec.codec.Properties - :class: av.codec.codec.Codec - .. autoattribute:: Codec.capabilities .. autoclass:: Capabilities Wraps :ffmpeg:`AVCodec.capabilities` (``AV_CODEC_CAP_*``). - Note that ``ffmpeg -codecs`` prefers the properties versions of - ``INTRA_ONLY`` and ``LOSSLESS``. - - .. enumtable:: av.codec.codec.Capabilities - :class: av.codec.codec.Codec - + Note that ``ffmpeg -codecs`` prefers the properties versions of ``INTRA_ONLY`` and ``LOSSLESS``. Contexts -------- From 49c0605d79e5a6f1e654cd45b3bd44d4a1e8e4ba Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 1 Dec 2024 14:59:19 -0500 Subject: [PATCH 181/365] Use IntEnums for codec flags --- av/codec/codec.pyx | 2 +- av/codec/context.pyi | 90 ++++++++------------ av/codec/context.pyx | 179 +++++++++++++++++---------------------- av/enum.pyx | 40 --------- tests/test_videoframe.py | 7 +- 5 files changed, 121 insertions(+), 197 deletions(-) diff --git a/av/codec/codec.pyx b/av/codec/codec.pyx index 98a5b50d8..1493f0f7b 100644 --- a/av/codec/codec.pyx +++ b/av/codec/codec.pyx @@ -117,7 +117,7 @@ cdef class Codec: if self.is_encoder and lib.av_codec_is_decoder(self.ptr): raise RuntimeError("%s is both encoder and decoder.") - def create(self, str kind = None): + def create(self, kind = None): """Create a :class:`.CodecContext` for this codec. :param str kind: Gives a hint to static type checkers for what exact CodecContext is used. diff --git a/av/codec/context.pyi b/av/codec/context.pyi index e58b0879d..a6ca9647e 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -1,8 +1,7 @@ -from enum import Enum, Flag +from enum import Flag, IntEnum from fractions import Fraction from typing import ClassVar, Literal -from av.enum import EnumFlag, EnumItem from av.packet import Packet from .codec import Codec @@ -15,38 +14,39 @@ class ThreadType(Flag): def __get__(self, i: object | None, owner: type | None = None) -> ThreadType: ... def __set__(self, instance: object, value: int | str | ThreadType) -> None: ... -class Flags(EnumFlag): - NONE: int - UNALIGNED: int - QSCALE: int - # 4MV - OUTPUT_CORRUPT: int - QPEL: int - DROPCHANGED: int - PASS1: int - PASS2: int - LOOP_FILTER: int - GRAY: int - PSNR: int - INTERLACED_DCT: int - LOW_DELAY: int - GLOBAL_HEADER: int - BITEXACT: int - AC_PRED: int - INTERLACED_ME: int - CLOSED_GOP: int +class Flags(IntEnum): + unaligned: int + qscale: int + four_mv: int + output_corrupt: int + qpel: int + drop_changed: int + recon_frame: int + copy_opaque: int + frame_duration: int + pass1: int + pass2: int + loop_filter: int + gray: int + psnr: int + interlaced_dct: int + low_delay: int + global_header: int + bitexact: int + ac_pred: int + interlaced_me: int + closed_gop: int -class Flags2(EnumFlag): - NONE: int - FAST: int - NO_OUTPUT: int - LOCAL_HEADER: int - CHUNKS: int - IGNORE_CROP: int - SHOW_ALL: int - EXPORT_MVS: int - SKIP_MANUAL: int - RO_FLUSH_NOOP: int +class Flags2(IntEnum): + fast: int + no_output: int + local_header: int + chunks: int + ignore_crop: int + show_all: int + export_mvs: int + skip_manual: int + ro_flush_noop: int class CodecContext: name: str @@ -65,30 +65,10 @@ class CodecContext: skip_frame: Literal[ "NONE", "DEFAULT", "NONREF", "BIDIR", "NONINTRA", "NONKEY", "ALL" ] - - # flags - unaligned: bool + flags: int qscale: bool - four_mv: bool - output_corrupt: bool - qpel: bool - drop_changed: bool - recon_frame: bool copy_opaque: bool - frame_duration: bool - pass1: bool - pass2: bool - loop_filter: bool - gray: bool - psnr: bool - interlaced_dct: bool - low_delay: bool - global_header: bool - bitexact: bool - ac_pred: bool - interlaced_me: bool - closed_gop: bool - + flags2: int @property def is_open(self) -> bool: ... @property diff --git a/av/codec/context.pyx b/av/codec/context.pyx index f420b1157..29b7b80d1 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -6,12 +6,11 @@ from libc.string cimport memcpy from av.bytesource cimport ByteSource, bytesource from av.codec.codec cimport Codec, wrap_codec from av.dictionary cimport _Dictionary -from av.enum cimport define_enum from av.error cimport err_check from av.packet cimport Packet from av.utils cimport avrational_to_fraction, to_avrational -from enum import Enum, Flag +from enum import Flag, IntEnum from av.dictionary import Dictionary @@ -47,65 +46,39 @@ class ThreadType(Flag): SLICE: "Decode more than one part of a single frame at once" = lib.FF_THREAD_SLICE AUTO: "Decode using both FRAME and SLICE methods." = lib.FF_THREAD_SLICE | lib.FF_THREAD_FRAME - -Flags = define_enum("Flags", __name__, ( - ("NONE", 0), - ("UNALIGNED", lib.AV_CODEC_FLAG_UNALIGNED, - "Allow decoders to produce frames with data planes that are not aligned to CPU requirements (e.g. due to cropping)." - ), - ("QSCALE", lib.AV_CODEC_FLAG_QSCALE, "Use fixed qscale."), - ("4MV", lib.AV_CODEC_FLAG_4MV, "4 MV per MB allowed / advanced prediction for H.263."), - ("OUTPUT_CORRUPT", lib.AV_CODEC_FLAG_OUTPUT_CORRUPT, "Output even those frames that might be corrupted."), - ("QPEL", lib.AV_CODEC_FLAG_QPEL, "Use qpel MC."), - ("DROPCHANGED", 1 << 5, - "Don't output frames whose parameters differ from first decoded frame in stream." - ), - ("RECON_FRAME", lib.AV_CODEC_FLAG_RECON_FRAME, "Request the encoder to output reconstructed frames, i.e. frames that would be produced by decoding the encoded bistream."), - ("COPY_OPAQUE", lib.AV_CODEC_FLAG_COPY_OPAQUE, - """Request the decoder to propagate each packet's AVPacket.opaque and AVPacket.opaque_ref - to its corresponding output AVFrame. Request the encoder to propagate each frame's - AVFrame.opaque and AVFrame.opaque_ref values to its corresponding output AVPacket."""), - ("FRAME_DURATION", lib.AV_CODEC_FLAG_FRAME_DURATION, - """Signal to the encoder that the values of AVFrame.duration are valid and should be - used (typically for transferring them to output packets)."""), - ("PASS1", lib.AV_CODEC_FLAG_PASS1, "Use internal 2pass ratecontrol in first pass mode."), - ("PASS2", lib.AV_CODEC_FLAG_PASS2, "Use internal 2pass ratecontrol in second pass mode."), - ("LOOP_FILTER", lib.AV_CODEC_FLAG_LOOP_FILTER, "loop filter."), - ("GRAY", lib.AV_CODEC_FLAG_GRAY, "Only decode/encode grayscale."), - ("PSNR", lib.AV_CODEC_FLAG_PSNR, "error[?] variables will be set during encoding."), - ("INTERLACED_DCT", lib.AV_CODEC_FLAG_INTERLACED_DCT, "Use interlaced DCT."), - ("LOW_DELAY", lib.AV_CODEC_FLAG_LOW_DELAY, "Force low delay."), - ("GLOBAL_HEADER", lib.AV_CODEC_FLAG_GLOBAL_HEADER, - "Place global headers in extradata instead of every keyframe." - ), - ("BITEXACT", lib.AV_CODEC_FLAG_BITEXACT, "Use only bitexact stuff (except (I)DCT)."), - ("AC_PRED", lib.AV_CODEC_FLAG_AC_PRED, "H.263 advanced intra coding / MPEG-4 AC prediction"), - ("INTERLACED_ME", lib.AV_CODEC_FLAG_INTERLACED_ME, "Interlaced motion estimation"), - ("CLOSED_GOP", lib.AV_CODEC_FLAG_CLOSED_GOP), -), is_flags=True) - -Flags2 = define_enum("Flags2", __name__, ( - ("NONE", 0), - ("FAST", lib.AV_CODEC_FLAG2_FAST, - """Allow non spec compliant speedup tricks."""), - ("NO_OUTPUT", lib.AV_CODEC_FLAG2_NO_OUTPUT, - """Skip bitstream encoding."""), - ("LOCAL_HEADER", lib.AV_CODEC_FLAG2_LOCAL_HEADER, - """Place global headers at every keyframe instead of in extradata."""), - ("CHUNKS", lib.AV_CODEC_FLAG2_CHUNKS, - """Input bitstream might be truncated at a packet boundaries - instead of only at frame boundaries."""), - ("IGNORE_CROP", lib.AV_CODEC_FLAG2_IGNORE_CROP, - """Discard cropping information from SPS."""), - ("SHOW_ALL", lib.AV_CODEC_FLAG2_SHOW_ALL, - """Show all frames before the first keyframe"""), - ("EXPORT_MVS", lib.AV_CODEC_FLAG2_EXPORT_MVS, - """Export motion vectors through frame side data"""), - ("SKIP_MANUAL", lib.AV_CODEC_FLAG2_SKIP_MANUAL, - """Do not skip samples and export skip information as frame side data"""), - ("RO_FLUSH_NOOP", lib.AV_CODEC_FLAG2_RO_FLUSH_NOOP, - """Do not reset ASS ReadOrder field on flush (subtitles decoding)"""), -), is_flags=True) +class Flags(IntEnum): + unaligned = lib.AV_CODEC_FLAG_UNALIGNED + qscale = lib.AV_CODEC_FLAG_QSCALE + four_mv = lib.AV_CODEC_FLAG_4MV + output_corrupt = lib.AV_CODEC_FLAG_OUTPUT_CORRUPT + qpel = lib.AV_CODEC_FLAG_QPEL + drop_changed = 1 << 5 + recon_frame = lib.AV_CODEC_FLAG_RECON_FRAME + copy_opaque = lib.AV_CODEC_FLAG_COPY_OPAQUE + frame_duration = lib.AV_CODEC_FLAG_FRAME_DURATION + pass1 = lib.AV_CODEC_FLAG_PASS1 + pass2 = lib.AV_CODEC_FLAG_PASS2 + loop_filter = lib.AV_CODEC_FLAG_LOOP_FILTER + gray = lib.AV_CODEC_FLAG_GRAY + psnr = lib.AV_CODEC_FLAG_PSNR + interlaced_dct = lib.AV_CODEC_FLAG_INTERLACED_DCT + low_delay = lib.AV_CODEC_FLAG_LOW_DELAY + global_header = lib.AV_CODEC_FLAG_GLOBAL_HEADER + bitexact = lib.AV_CODEC_FLAG_BITEXACT + ac_pred = lib.AV_CODEC_FLAG_AC_PRED + interlaced_me = lib.AV_CODEC_FLAG_INTERLACED_ME + closed_gop = lib.AV_CODEC_FLAG_CLOSED_GOP + +class Flags2(IntEnum): + fast = lib.AV_CODEC_FLAG2_FAST + no_output = lib.AV_CODEC_FLAG2_NO_OUTPUT + local_header = lib.AV_CODEC_FLAG2_LOCAL_HEADER + chunks = lib.AV_CODEC_FLAG2_CHUNKS + ignore_crop = lib.AV_CODEC_FLAG2_IGNORE_CROP + show_all = lib.AV_CODEC_FLAG2_SHOW_ALL + export_mvs = lib.AV_CODEC_FLAG2_EXPORT_MVS + skip_manual = lib.AV_CODEC_FLAG2_SKIP_MANUAL + ro_flush_noop = lib.AV_CODEC_FLAG2_RO_FLUSH_NOOP cdef class CodecContext: @@ -133,53 +106,59 @@ cdef class CodecContext: self.ptr.thread_count = 0 # use as many threads as there are CPUs. self.ptr.thread_type = 0x02 # thread within a frame. Does not change the API. - def _get_flags(self): + @property + def flags(self): + """ + Get and set the flags bitmask of CodecContext. + + :rtype: int + """ return self.ptr.flags - def _set_flags(self, value): + @flags.setter + def flags(self, int value): self.ptr.flags = value - flags = Flags.property(_get_flags, _set_flags, "Flag property of :class:`.Flags`.") - - unaligned = flags.flag_property("UNALIGNED") - qscale = flags.flag_property("QSCALE") - four_mv = flags.flag_property("4MV") - output_corrupt = flags.flag_property("OUTPUT_CORRUPT") - qpel = flags.flag_property("QPEL") - drop_changed = flags.flag_property("DROPCHANGED") - recon_frame = flags.flag_property("RECON_FRAME") - copy_opaque = flags.flag_property("COPY_OPAQUE") - frame_duration = flags.flag_property("FRAME_DURATION") - pass1 = flags.flag_property("PASS1") - pass2 = flags.flag_property("PASS2") - loop_filter = flags.flag_property("LOOP_FILTER") - gray = flags.flag_property("GRAY") - psnr = flags.flag_property("PSNR") - interlaced_dct = flags.flag_property("INTERLACED_DCT") - low_delay = flags.flag_property("LOW_DELAY") - global_header = flags.flag_property("GLOBAL_HEADER") - bitexact = flags.flag_property("BITEXACT") - ac_pred = flags.flag_property("AC_PRED") - interlaced_me = flags.flag_property("INTERLACED_ME") - closed_gop = flags.flag_property("CLOSED_GOP") - - def _get_flags2(self): + @property + def qscale(self): + """ + Use fixed qscale. + + :rtype: bool + """ + return bool(self.ptr.flags & lib.AV_CODEC_FLAG_QSCALE) + + @qscale.setter + def qscale(self, value): + if value: + self.ptr.flags |= lib.AV_CODEC_FLAG_QSCALE + else: + self.ptr.flags &= ~lib.AV_CODEC_FLAG_QSCALE + + @property + def copy_opaque(self): + return bool(self.ptr.flags & lib.AV_CODEC_FLAG_COPY_OPAQUE) + + @copy_opaque.setter + def copy_opaque(self, value): + if value: + self.ptr.flags |= lib.AV_CODEC_FLAG_COPY_OPAQUE + else: + self.ptr.flags &= ~lib.AV_CODEC_FLAG_COPY_OPAQUE + + @property + def flags2(self): + """ + Get and set the flags2 bitmask of CodecContext. + + :rtype: int + """ return self.ptr.flags2 - def _set_flags2(self, value): + @flags2.setter + def flags2(self, int value): self.ptr.flags2 = value - flags2 = Flags2.property(_get_flags2, _set_flags2, "Flag property of :class:`.Flags2`.") - fast = flags2.flag_property("FAST") - no_output = flags2.flag_property("NO_OUTPUT") - local_header = flags2.flag_property("LOCAL_HEADER") - chunks = flags2.flag_property("CHUNKS") - ignore_crop = flags2.flag_property("IGNORE_CROP") - show_all = flags2.flag_property("SHOW_ALL") - export_mvs = flags2.flag_property("EXPORT_MVS") - skip_manual = flags2.flag_property("SKIP_MANUAL") - ro_flush_noop = flags2.flag_property("RO_FLUSH_NOOP") - @property def extradata(self): if self.ptr is NULL: diff --git a/av/enum.pyx b/av/enum.pyx index 9217e67d6..802a731ff 100644 --- a/av/enum.pyx +++ b/av/enum.pyx @@ -198,46 +198,6 @@ cdef class EnumItem: cdef class EnumFlag(EnumItem): - - """ - Flags are sets of boolean attributes, which the FFmpeg API represents as individual - bits in a larger integer which you manipulate with the bitwise operators. - We associate names with each flag that are easier to operate with. - - Consider :data:`CodecContextFlags`, whis is the type of the :attr:`CodecContext.flags` - attribute, and the set of boolean properties:: - - >>> fh = av.open(video_path) - >>> cc = fh.streams.video[0].codec_context - - >>> cc.flags - - - >>> # You can set flags via bitwise operations with the objects, names, or values: - >>> cc.flags |= cc.flags.OUTPUT_CORRUPT - >>> cc.flags |= 'GLOBAL_HEADER' - >>> cc.flags - - - >>> # You can test flags via bitwise operations with objects, names, or values: - >>> bool(cc.flags & cc.flags.OUTPUT_CORRUPT) - True - >>> bool(cc.flags & 'QSCALE') - False - - >>> # There are boolean properties for each flag: - >>> cc.output_corrupt - True - >>> cc.qscale - False - - >>> # You can set them: - >>> cc.qscale = True - >>> cc.flags - - - """ - cdef readonly tuple flags def __cinit__(self, sentinel, name, value, doc=None): diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 32b6e5482..c93a12e32 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -31,7 +31,12 @@ def assertPixelValue16(plane, expected, byteorder: str) -> None: def test_opaque() -> None: with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: video_stream = container.streams.video[0] - video_stream.codec_context.copy_opaque = True + + ctx = video_stream.codec_context + ctx.flags |= av.codec.context.Flags.copy_opaque + + assert video_stream.codec_context.copy_opaque + for packet_idx, packet in enumerate(container.demux()): packet.opaque = (time.time(), packet_idx) for frame in packet.decode(): From eb8428d016344c8c2f6bdcccc21f4d9cd4b0023f Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 1 Dec 2024 15:27:42 -0500 Subject: [PATCH 182/365] Use Flag enum for format flags --- av/format.pyi | 81 ++++++++++++++++++++-------------------------- av/format.pyx | 90 +++++++++++++++++++-------------------------------- 2 files changed, 69 insertions(+), 102 deletions(-) diff --git a/av/format.pyi b/av/format.pyi index c3506ed51..b30a84bf6 100644 --- a/av/format.pyi +++ b/av/format.pyi @@ -1,53 +1,42 @@ -__all__ = ("ContainerFormat", "formats_available") +__all__ = ("Flags", "ContainerFormat", "formats_available") -from typing import Literal +from enum import Flag +from typing import ClassVar, Literal -from .enum import EnumFlag - -class Flags(EnumFlag): - NOFILE: int - NEEDNUMBER: int - SHOW_IDS: int - GLOBALHEADER: int - NOTIMESTAMPS: int - GENERIC_INDEX: int - TS_DISCONT: int - VARIABLE_FPS: int - NODIMENSIONS: int - NOSTREAMS: int - NOBINSEARCH: int - NOGENSEARCH: int - NO_BYTE_SEEK: int - ALLOW_FLUSH: int - TS_NONSTRICT: int - TS_NEGATIVE: int - SEEK_TO_PTS: int +class Flags(Flag): + no_file: ClassVar[Flags] + need_number: ClassVar[Flags] + show_ids: ClassVar[Flags] + global_header: ClassVar[Flags] + no_timestamps: ClassVar[Flags] + generic_index: ClassVar[Flags] + ts_discont: ClassVar[Flags] + variable_fps: ClassVar[Flags] + no_dimensions: ClassVar[Flags] + no_streams: ClassVar[Flags] + no_bin_search: ClassVar[Flags] + no_gen_search: ClassVar[Flags] + no_byte_seek: ClassVar[Flags] + allow_flush: ClassVar[Flags] + ts_nonstrict: ClassVar[Flags] + ts_negative: ClassVar[Flags] + seek_to_pts: ClassVar[Flags] class ContainerFormat: def __init__(self, name: str, mode: Literal["r", "w"] | None = None) -> None: ... - name: str - long_name: str - is_input: bool - is_output: bool - extensions: set[str] - - # flags - no_file: int - need_number: int - show_ids: int - global_header: int - no_timestamps: int - generic_index: int - ts_discont: int - variable_fps: int - no_dimensions: int - no_streams: int - no_bin_search: int - no_gen_search: int - no_byte_seek: int - allow_flush: int - ts_nonstrict: int - ts_negative: int - seek_to_pts: int + @property + def name(self) -> str: ... + @property + def long_name(self) -> str: ... + @property + def is_input(self) -> bool: ... + @property + def is_output(self) -> bool: ... + @property + def extensions(self) -> set[str]: ... + @property + def flags(self) -> int: ... + @property + def no_file(self) -> bool: ... formats_available: set[str] diff --git a/av/format.pyx b/av/format.pyx index 06b533cf4..464e34f49 100644 --- a/av/format.pyx +++ b/av/format.pyx @@ -1,7 +1,8 @@ cimport libav as lib from av.descriptor cimport wrap_avclass -from av.enum cimport define_enum + +from enum import Flag cdef object _cinit_bypass_sentinel = object() @@ -16,41 +17,28 @@ cdef ContainerFormat build_container_format(lib.AVInputFormat* iptr, lib.AVOutpu return format -Flags = define_enum("Flags", __name__, ( - ("NOFILE", lib.AVFMT_NOFILE), - ("NEEDNUMBER", lib.AVFMT_NEEDNUMBER, "Needs '%d' in filename."), - ("SHOW_IDS", lib.AVFMT_SHOW_IDS, "Show format stream IDs numbers."), - ("GLOBALHEADER", lib.AVFMT_GLOBALHEADER, "Format wants global header."), - ("NOTIMESTAMPS", lib.AVFMT_NOTIMESTAMPS, "Format does not need / have any timestamps."), - ("GENERIC_INDEX", lib.AVFMT_GENERIC_INDEX, "Use generic index building code."), - ("TS_DISCONT", lib.AVFMT_TS_DISCONT, - """Format allows timestamp discontinuities. - Note, muxers always require valid (monotone) timestamps"""), - ("VARIABLE_FPS", lib.AVFMT_VARIABLE_FPS, "Format allows variable fps."), - ("NODIMENSIONS", lib.AVFMT_NODIMENSIONS, "Format does not need width/height"), - ("NOSTREAMS", lib.AVFMT_NOSTREAMS, "Format does not require any streams"), - ("NOBINSEARCH", lib.AVFMT_NOBINSEARCH, - "Format does not allow to fall back on binary search via read_timestamp"), - ("NOGENSEARCH", lib.AVFMT_NOGENSEARCH, - "Format does not allow to fall back on generic search"), - ("NO_BYTE_SEEK", lib.AVFMT_NO_BYTE_SEEK, "Format does not allow seeking by bytes"), - ("ALLOW_FLUSH", lib.AVFMT_ALLOW_FLUSH, - """Format allows flushing. If not set, the muxer will not receive a NULL - packet in the write_packet function."""), - ("TS_NONSTRICT", lib.AVFMT_TS_NONSTRICT, - """Format does not require strictly increasing timestamps, but they must - still be monotonic."""), - ("TS_NEGATIVE", lib.AVFMT_TS_NEGATIVE, - """Format allows muxing negative timestamps. If not set the timestamp - will be shifted in av_write_frame and av_interleaved_write_frame so they - start from 0. The user or muxer can override this through - AVFormatContext.avoid_negative_ts"""), - ("SEEK_TO_PTS", lib.AVFMT_SEEK_TO_PTS, "Seeking is based on PTS"), -), is_flags=True) - +class Flags(Flag): + no_file = lib.AVFMT_NOFILE + need_number: "Needs '%d' in filename." = lib.AVFMT_NEEDNUMBER + show_ids: "Show format stream IDs numbers." = lib.AVFMT_SHOW_IDS + global_header: "Format wants global header." = lib.AVFMT_GLOBALHEADER + no_timestamps: "Format does not need / have any timestamps." = lib.AVFMT_NOTIMESTAMPS + generic_index: "Use generic index building code." = lib.AVFMT_GENERIC_INDEX + ts_discont: "Format allows timestamp discontinuities" = lib.AVFMT_TS_DISCONT + variable_fps: "Format allows variable fps." = lib.AVFMT_VARIABLE_FPS + no_dimensions: "Format does not need width/height" = lib.AVFMT_NODIMENSIONS + no_streams: "Format does not require any streams" = lib.AVFMT_NOSTREAMS + no_bin_search: "Format does not allow to fall back on binary search via read_timestamp" = lib.AVFMT_NOBINSEARCH + no_gen_search: "Format does not allow to fall back on generic search" = lib.AVFMT_NOGENSEARCH + no_byte_seek: "Format does not allow seeking by bytes" = lib.AVFMT_NO_BYTE_SEEK + allow_flush: "Format allows flushing. If not set, the muxer will not receive a NULL packet in the write_packet function." = lib.AVFMT_ALLOW_FLUSH + ts_nonstrict: "Format does not require strictly increasing timestamps, but they must still be monotonic." = lib.AVFMT_TS_NONSTRICT + ts_negative: "Format allows muxing negative timestamps." = lib.AVFMT_TS_NEGATIVE + # If not set the timestamp will be shifted in `av_write_frame()` and `av_interleaved_write_frame()` + # so they start from 0. The user or muxer can override this through AVFormatContext.avoid_negative_ts + seek_to_pts: "Seeking is based on PTS" = lib.AVFMT_SEEK_TO_PTS cdef class ContainerFormat: - """Descriptor of a container format. :param str name: The name of the format. @@ -63,12 +51,11 @@ cdef class ContainerFormat: if name is _cinit_bypass_sentinel: return - # We need to hold onto the original name because AVInputFormat.name - # is actually comma-seperated, and so we need to remember which one - # this was. + # We need to hold onto the original name because AVInputFormat.name is + # actually comma-separated, and so we need to remember which one this was. self.name = name - # Searches comma-seperated names. + # Searches comma-separated names. if mode is None or mode == "r": self.iptr = lib.av_find_input_format(name) @@ -135,30 +122,21 @@ cdef class ContainerFormat: exts.update(self.optr.extensions.split(",")) return exts - @Flags.property + @property def flags(self): + """ + Get the flags bitmask for the format. + + :rtype: int + """ return ( (self.iptr.flags if self.iptr else 0) | (self.optr.flags if self.optr else 0) ) - no_file = flags.flag_property("NOFILE") - need_number = flags.flag_property("NEEDNUMBER") - show_ids = flags.flag_property("SHOW_IDS") - global_header = flags.flag_property("GLOBALHEADER") - no_timestamps = flags.flag_property("NOTIMESTAMPS") - generic_index = flags.flag_property("GENERIC_INDEX") - ts_discont = flags.flag_property("TS_DISCONT") - variable_fps = flags.flag_property("VARIABLE_FPS") - no_dimensions = flags.flag_property("NODIMENSIONS") - no_streams = flags.flag_property("NOSTREAMS") - no_bin_search = flags.flag_property("NOBINSEARCH") - no_gen_search = flags.flag_property("NOGENSEARCH") - no_byte_seek = flags.flag_property("NO_BYTE_SEEK") - allow_flush = flags.flag_property("ALLOW_FLUSH") - ts_nonstrict = flags.flag_property("TS_NONSTRICT") - ts_negative = flags.flag_property("TS_NEGATIVE") - seek_to_pts = flags.flag_property("SEEK_TO_PTS") + @property + def no_file(self): + return bool(self.flags & lib.AVFMT_NOFILE) cdef get_output_format_names(): From f00facb88df686f6ad6c076be40e055e8ed9dd5c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 1 Dec 2024 15:45:39 -0500 Subject: [PATCH 183/365] Remove our custom Enum class --- av/codec/codec.pyi | 1 - av/enum.pxd | 4 - av/enum.pyi | 68 --------- av/enum.pyx | 296 --------------------------------------- av/error.pxd | 2 - av/error.pyi | 41 ------ av/error.pyx | 164 ++++++++++++++++------ av/video/reformatter.pyi | 8 +- docs/api/codec.rst | 12 +- docs/api/enum.rst | 24 ---- docs/api/error.rst | 38 +---- docs/api/error_table.py | 25 ---- tests/test_enums.py | 223 ----------------------------- tests/test_errors.py | 6 +- 14 files changed, 136 insertions(+), 776 deletions(-) delete mode 100644 av/enum.pxd delete mode 100644 av/enum.pyi delete mode 100644 av/enum.pyx delete mode 100644 docs/api/enum.rst delete mode 100644 docs/api/error_table.py delete mode 100644 tests/test_enums.py diff --git a/av/codec/codec.pyi b/av/codec/codec.pyi index 3323895ed..32736c080 100644 --- a/av/codec/codec.pyi +++ b/av/codec/codec.pyi @@ -5,7 +5,6 @@ from typing import ClassVar, Literal, overload from av.audio.codeccontext import AudioCodecContext from av.audio.format import AudioFormat from av.descriptor import Descriptor -from av.enum import EnumFlag from av.subtitles.codeccontext import SubtitleCodecContext from av.video.codeccontext import VideoCodecContext from av.video.format import VideoFormat diff --git a/av/enum.pxd b/av/enum.pxd deleted file mode 100644 index 884fdd961..000000000 --- a/av/enum.pxd +++ /dev/null @@ -1,4 +0,0 @@ -cpdef define_enum( - name, module, items, - bint is_flags=* -) diff --git a/av/enum.pyi b/av/enum.pyi deleted file mode 100644 index 026a176b3..000000000 --- a/av/enum.pyi +++ /dev/null @@ -1,68 +0,0 @@ -from typing import Any, Callable, Iterable, Literal, Sequence, overload - -class EnumType(type): - def __init__( - self, - name: str, - bases: tuple[type, ...], - attrs: dict[str, Any], - items: Iterable[tuple[str, Any, str | None, bool]], - ) -> None: ... - def _create( - self, name: str, value: int, doc: str | None = None, by_value_only: bool = False - ) -> None: ... - def __len__(self) -> None: ... - def __iter__(self) -> None: ... - def __getitem__(self, key: str | int | EnumType) -> None: ... - def _get(self, value: int, create: bool = False) -> None: ... - def _get_multi_flags(self, value: int) -> None: ... - def get( - self, - key: str | int | EnumType, - default: int | None = None, - create: bool = False, - ) -> int | None: ... - -class EnumItem: - @property - def name(self) -> str: ... - @property - def value(self) -> int: ... - def __int__(self) -> int: ... - def __hash__(self) -> int: ... - def __reduce__( - self, - ) -> tuple[Callable[[str, str, str], EnumItem], tuple[str, str, str]]: ... - def __eq__(self, other: object) -> bool: ... - def __ne__(self, other: object) -> bool: ... - -class EnumFlag(EnumItem): - flags: tuple[EnumFlag] - - def __and__(self, other: object) -> EnumFlag: ... - def __or__(self, other: object) -> EnumFlag: ... - def __xor__(self, other: object) -> EnumFlag: ... - def __invert__(self) -> bool: ... - def __nonzero__(self) -> bool: ... - -@overload -def define_enum( - name: str, - module: str, - items: Sequence[tuple[str, int] | None], - is_flags: Literal[True], -) -> EnumFlag: ... -@overload -def define_enum( - name: str, - module: str, - items: Sequence[tuple[str, int] | None], - is_flags: Literal[False], -) -> EnumItem: ... -@overload -def define_enum( - name: str, - module: str, - items: Sequence[tuple[str, int] | None], - is_flags: bool = False, -) -> EnumItem | EnumFlag: ... diff --git a/av/enum.pyx b/av/enum.pyx deleted file mode 100644 index 802a731ff..000000000 --- a/av/enum.pyx +++ /dev/null @@ -1,296 +0,0 @@ -""" - -PyAV provides enumeration and flag classes that are similar to the stdlib ``enum`` -module that shipped with Python 3.4. - -PyAV's enums are a little more forgiving to preserve backwards compatibility -with earlier PyAV patterns. e.g., they can be freely compared to strings or -integers for names and values respectively. - -""" - -import copyreg - - -cdef sentinel = object() - - -class EnumType(type): - def __new__(mcl, name, bases, attrs, *args): - # Just adapting the method signature. - return super().__new__(mcl, name, bases, attrs) - - def __init__(self, name, bases, attrs, items): - self._by_name = {} - self._by_value = {} - self._all = [] - - for spec in items: - self._create(*spec) - - def _create(self, name, value, doc=None, by_value_only=False): - # We only have one instance per value. - try: - item = self._by_value[value] - except KeyError: - item = self(sentinel, name, value, doc) - self._by_value[value] = item - - if not by_value_only: - setattr(self, name, item) - self._all.append(item) - self._by_name[name] = item - - return item - - def __len__(self): - return len(self._all) - - def __iter__(self): - return iter(self._all) - - def __getitem__(self, key): - if isinstance(key, str): - return self._by_name[key] - - if isinstance(key, int): - try: - return self._by_value[key] - except KeyError: - pass - - if issubclass(self, EnumFlag): - return self._get_multi_flags(key) - - raise KeyError(key) - - if isinstance(key, self): - return key - - raise TypeError(f"{self.__name__} indices must be str, int, or itself") - - def _get(self, long value, bint create=False): - try: - return self._by_value[value] - except KeyError: - pass - - if not create: - return - - return self._create(f"{self.__name__.upper()}_{value}", value, by_value_only=True) - - def _get_multi_flags(self, long value): - try: - return self._by_value[value] - except KeyError: - pass - - flags = [] - cdef long to_find = value - for item in self: - if item.value & to_find: - flags.append(item) - to_find = to_find ^ item.value - if not to_find: - break - - if to_find: - raise KeyError(value) - - name = "|".join(f.name for f in flags) - cdef EnumFlag combo = self._create(name, value, by_value_only=True) - combo.flags = tuple(flags) - - return combo - - def get(self, key, default=None, create=False): - try: - return self[key] - except KeyError: - if create: - return self._get(key, create=True) - return default - - def property(self, *args, **kwargs): - return EnumProperty(self, *args, **kwargs) - - -def _unpickle(mod_name, cls_name, item_name): - mod = __import__(mod_name, fromlist=["."]) - cls = getattr(mod, cls_name) - return cls[item_name] - - -copyreg.constructor(_unpickle) - - -cdef class EnumItem: - cdef readonly str name - cdef readonly int value - cdef Py_hash_t _hash - - def __cinit__(self, sentinel_, str name, int value, doc=None): - if sentinel_ is not sentinel: - raise RuntimeError(f"Cannot instantiate {self.__class__.__name__}.") - - self.name = name - self.value = value - self.__doc__ = doc - - # We need to establish a hash that doesn't collide with anything that - # would return true from `__eq__`. This is because these enums (vs - # the stdlib ones) are weakly typed (they will compare against string - # names and int values), and if we have the same hash AND are equal, - # then they will be equivalent as keys in a dictionary, which is wierd. - cdef Py_hash_t hash_ = value + 1 - if hash_ == hash(name): - hash_ += 1 - self._hash = hash_ - - def __repr__(self): - return f"<{self.__class__.__module__}.{self.__class__.__name__}:{self.name}(0x{self.value:x})>" - - def __str__(self): - return self.name - - def __int__(self): - return self.value - - def __hash__(self): - return self._hash - - def __reduce__(self): - return (_unpickle, (self.__class__.__module__, self.__class__.__name__, self.name)) - - def __eq__(self, other): - if isinstance(other, str): - if self.name == other: - return True - - try: - other_inst = self.__class__._by_name[other] - except KeyError: - raise ValueError( - f"{self.__class__.__name__} does not have item named {other!r}" - ) - else: - return self is other_inst - - if isinstance(other, int): - if self.value == other: - return True - if other in self.__class__._by_value: - return False - raise ValueError( - f"{self.__class__.__name__} does not have item valued {other}" - ) - - if isinstance(other, self.__class__): - return self is other - - raise TypeError( - f"'==' not supported between {self.__class__.__name__} and {type(other).__name__}" - ) - - def __ne__(self, other): - return not (self == other) - - -cdef class EnumFlag(EnumItem): - cdef readonly tuple flags - - def __cinit__(self, sentinel, name, value, doc=None): - self.flags = (self, ) - - def __and__(self, other): - if not isinstance(other, int): - other = self.__class__[other].value - value = self.value & other - return self.__class__._get_multi_flags(value) - - def __or__(self, other): - if not isinstance(other, int): - other = self.__class__[other].value - value = self.value | other - return self.__class__._get_multi_flags(value) - - def __xor__(self, other): - if not isinstance(other, int): - other = self.__class__[other].value - value = self.value ^ other - return self.__class__._get_multi_flags(value) - - def __invert__(self): - # This can't result in a flag, but is helpful. - return ~self.value - - def __nonzero__(self): - return bool(self.value) - - -cdef class EnumProperty: - cdef object enum - cdef object fget - cdef object fset - cdef public __doc__ - - def __init__(self, enum, fget, fset=None, doc=None): - self.enum = enum - self.fget = fget - self.fset = fset - self.__doc__ = doc or fget.__doc__ - - def setter(self, fset): - self.fset = fset - return self - - def __get__(self, inst, owner): - if inst is not None: - value = self.fget(inst) - return self.enum.get(value, create=True) - else: - return self - - def __set__(self, inst, value): - item = self.enum.get(value) - self.fset(inst, item.value) - - def flag_property(self, name, doc=None): - - item = self.enum[name] - cdef int item_value = item.value - - class Property(property): - pass - - @Property - def _property(inst): - return bool(self.fget(inst) & item_value) - - if self.fset: - @_property.setter - def _property(inst, value): - if value: - flags = self.fget(inst) | item_value - else: - flags = self.fget(inst) & ~item_value - self.fset(inst, flags) - - _property.__doc__ = doc or item.__doc__ - _property._enum_item = item - - return _property - - -cpdef define_enum(name, module, items, bint is_flags=False): - - if is_flags: - base_cls = EnumFlag - else: - base_cls = EnumItem - - # Some items may be None if they correspond to an unsupported FFmpeg feature - cls = EnumType(name, (base_cls, ), {"__module__": module}, [i for i in items if i is not None]) - - return cls diff --git a/av/error.pxd b/av/error.pxd index 836300d22..d9a542a36 100644 --- a/av/error.pxd +++ b/av/error.pxd @@ -1,5 +1,3 @@ cdef int stash_exception(exc_info=*) - cpdef int err_check(int res, filename=*) except -1 -cpdef make_error(int res, filename=*, log=*) diff --git a/av/error.pyi b/av/error.pyi index 4c41f35e7..abbe2188c 100644 --- a/av/error.pyi +++ b/av/error.pyi @@ -1,58 +1,17 @@ import builtins from enum import Enum -from .enum import EnumItem - classes: dict[int, Exception] def code_to_tag(code: int) -> bytes: ... def tag_to_code(tag: bytes) -> int: ... -def make_error( - res: int, - filename: str | None = None, - log: tuple[int, tuple[int, str, str] | None] | None = None, -) -> None: ... def err_check(res: int, filename: str | None = None) -> int: ... -BUFFER_TOO_SMALL: EnumItem - -class ErrorType(EnumItem, Enum): - BSF_NOT_FOUND: EnumItem - BUG: EnumItem - BUFFER_TOO_SMALL: EnumItem - DECODER_NOT_FOUND: EnumItem - DEMUXER_NOT_FOUND: EnumItem - ENCODER_NOT_FOUND: EnumItem - EOF: EnumItem - EXIT: EnumItem - EXTERNAL: EnumItem - FILTER_NOT_FOUND: EnumItem - INVALIDDATA: EnumItem - MUXER_NOT_FOUND: EnumItem - OPTION_NOT_FOUND: EnumItem - PATCHWELCOME: EnumItem - PROTOCOL_NOT_FOUND: EnumItem - UNKNOWN: EnumItem - EXPERIMENTAL: EnumItem - INPUT_CHANGED: EnumItem - OUTPUT_CHANGED: EnumItem - HTTP_BAD_REQUEST: EnumItem - HTTP_UNAUTHORIZED: EnumItem - HTTP_FORBIDDEN: EnumItem - HTTP_NOT_FOUND: EnumItem - HTTP_OTHER_4XX: EnumItem - HTTP_SERVER_ERROR: EnumItem - PYAV_CALLBACK: EnumItem - - @property - def tag(self) -> bytes: ... - class FFmpegError(Exception): errno: int strerror: str filename: str log: tuple[int, tuple[int, str, str] | None] - type: ErrorType def __init__( self, diff --git a/av/error.pyx b/av/error.pyx index 97fab0e41..426d01ed0 100644 --- a/av/error.pyx +++ b/av/error.pyx @@ -1,4 +1,5 @@ cimport libav as lib +from libc.stdlib cimport free, malloc from av.logging cimport get_last_error @@ -8,8 +9,6 @@ import sys import traceback from threading import local -from av.enum import define_enum - # Will get extended with all of the exceptions. __all__ = [ "ErrorType", "FFmpegError", "LookupError", "HTTPError", "HTTPClientError", @@ -63,10 +62,6 @@ class FFmpegError(Exception): The filename that was being operated on (if available). - .. attribute:: type - - The :class:`av.error.ErrorType` enum value for the error type. - .. attribute:: log The tuple from :func:`av.logging.get_last_log`, or ``None``. @@ -74,6 +69,9 @@ class FFmpegError(Exception): """ def __init__(self, code, message, filename=None, log=None): + self.errno = code + self.strerror = message + args = [code, message] if filename or log: args.append(filename) @@ -81,15 +79,6 @@ class FFmpegError(Exception): args.append(log) super(FFmpegError, self).__init__(*args) self.args = tuple(args) # FileNotFoundError/etc. only pulls 2 args. - self.type = ErrorType.get(code, create=True) - - @property - def errno(self): - return self.args[0] - - @property - def strerror(self): - return self.args[1] @property def filename(self): @@ -165,12 +154,77 @@ _ffmpeg_specs = ( ("PYAV_CALLBACK", c_PYAV_STASHED_ERROR, "PyAVCallbackError", RuntimeError), ) +cdef sentinel = object() + + +class EnumType(type): + def __new__(mcl, name, bases, attrs, *args): + # Just adapting the method signature. + return super().__new__(mcl, name, bases, attrs) + + def __init__(self, name, bases, attrs, items): + self._by_name = {} + self._by_value = {} + self._all = [] + + for spec in items: + self._create(*spec) + + def _create(self, name, value, doc=None, by_value_only=False): + # We only have one instance per value. + try: + item = self._by_value[value] + except KeyError: + item = self(sentinel, name, value, doc) + self._by_value[value] = item -# The actual enum. -ErrorType = define_enum("ErrorType", __name__, [x[:2] for x in _ffmpeg_specs]) + return item -# It has to be monkey-patched. -ErrorType.__doc__ = """An enumeration of FFmpeg's error types. + def __len__(self): + return len(self._all) + + def __iter__(self): + return iter(self._all) + + def __getitem__(self, key): + if isinstance(key, str): + return self._by_name[key] + + if isinstance(key, int): + try: + return self._by_value[key] + except KeyError: + pass + + raise KeyError(key) + + if isinstance(key, self): + return key + + raise TypeError(f"{self.__name__} indices must be str, int, or itself") + + def _get(self, long value, bint create=False): + try: + return self._by_value[value] + except KeyError: + pass + + if not create: + return + + return self._create(f"{self.__name__.upper()}_{value}", value, by_value_only=True) + + def get(self, key, default=None, create=False): + try: + return self[key] + except KeyError: + if create: + return self._get(key, create=True) + return default + + +cdef class EnumItem: + """An enumeration of FFmpeg's error types. .. attribute:: tag @@ -181,7 +235,32 @@ ErrorType.__doc__ = """An enumeration of FFmpeg's error types. The error message that would be returned. """ -ErrorType.tag = property(lambda self: code_to_tag(self.value)) + cdef readonly str name + cdef readonly int value + + def __cinit__(self, sentinel_, str name, int value, doc=None): + if sentinel_ is not sentinel: + raise RuntimeError(f"Cannot instantiate {self.__class__.__name__}.") + + self.name = name + self.value = value + self.__doc__ = doc + + def __repr__(self): + return f"<{self.__class__.__module__}.{self.__class__.__name__}:{self.name}(0x{self.value:x})>" + + def __str__(self): + return self.name + + def __int__(self): + return self.value + + @property + def tag(self): + return code_to_tag(self.value) + + +ErrorType = EnumType("ErrorType", (EnumItem, ), {"__module__": __name__}, [x[:2] for x in _ffmpeg_specs]) for enum in ErrorType: @@ -274,6 +353,8 @@ for enum_name, code, name, base in _ffmpeg_specs: globals()[name] = cls __all__.append(name) +del _ffmpeg_specs + # Storage for stashing. cdef object _local = local() @@ -298,6 +379,27 @@ cdef int stash_exception(exc_info=None): cdef int _last_log_count = 0 + +cpdef make_error(int res, filename=None, log=None): + cdef int code = -res + cdef char* error_buffer = malloc(lib.AV_ERROR_MAX_STRING_SIZE * sizeof(char)) + if error_buffer == NULL: + raise MemoryError() + + try: + if code == c_PYAV_STASHED_ERROR: + message = PYAV_STASHED_ERROR_message + else: + lib.av_strerror(res, error_buffer, lib.AV_ERROR_MAX_STRING_SIZE) + # Fallback to OS error string if no message + message = error_buffer or os.strerror(code) + + cls = classes.get(code, UndefinedError) + return cls(code, message, filename, log) + finally: + free(error_buffer) + + cpdef int err_check(int res, filename=None) except -1: """Raise appropriate exceptions from library return code.""" @@ -329,25 +431,3 @@ cpdef int err_check(int res, filename=None) except -1: class UndefinedError(FFmpegError): """Fallback exception type in case FFmpeg returns an error we don't know about.""" pass - - -cpdef make_error(int res, filename=None, log=None): - cdef int code = -res - cdef bytes py_buffer - cdef char *c_buffer - - if code == c_PYAV_STASHED_ERROR: - message = PYAV_STASHED_ERROR_message - else: - # Jump through some hoops due to Python 2 in same codebase. - py_buffer = b"\0" * lib.AV_ERROR_MAX_STRING_SIZE - c_buffer = py_buffer - lib.av_strerror(res, c_buffer, lib.AV_ERROR_MAX_STRING_SIZE) - py_buffer = c_buffer - message = py_buffer.decode("latin1") - - # Default to the OS if we have no message; this should not get called. - message = message or os.strerror(code) - - cls = classes.get(code, UndefinedError) - return cls(code, message, filename, log) diff --git a/av/video/reformatter.pyi b/av/video/reformatter.pyi index abd545332..a601dd335 100644 --- a/av/video/reformatter.pyi +++ b/av/video/reformatter.pyi @@ -1,8 +1,8 @@ -from av.enum import EnumItem +from enum import IntEnum from .frame import VideoFrame -class Interpolation(EnumItem): +class Interpolation(IntEnum): FAST_BILINEAER: int BILINEAR: int BICUBIC: int @@ -15,7 +15,7 @@ class Interpolation(EnumItem): LANCZOS: int SPLINE: int -class Colorspace(EnumItem): +class Colorspace(IntEnum): ITU709: int FCC: int ITU601: int @@ -30,7 +30,7 @@ class Colorspace(EnumItem): smpte240: int default: int -class ColorRange(EnumItem): +class ColorRange(IntEnum): UNSPECIFIED: int MPEG: int JPEG: int diff --git a/docs/api/codec.rst b/docs/api/codec.rst index d4a241425..7e06ff8cc 100644 --- a/docs/api/codec.rst +++ b/docs/api/codec.rst @@ -90,8 +90,8 @@ Transcoding .. automethod:: CodecContext.flush_buffers -Flags -~~~~~ +Enums and Flags +~~~~~~~~~~~~~~~ .. autoattribute:: CodecContext.flags @@ -107,10 +107,6 @@ Flags .. enumtable:: av.codec.context:Flags2 :class: av.codec.context:CodecContext - -Enums -~~~~~ - .. autoclass:: av.codec.context.ThreadType Which multithreading methods to use. @@ -119,8 +115,4 @@ Enums .. enumtable:: av.codec.context.ThreadType -.. autoclass:: av.codec.context.SkipType - - .. enumtable:: av.codec.context.SkipType - diff --git a/docs/api/enum.rst b/docs/api/enum.rst deleted file mode 100644 index 5fdcec4f7..000000000 --- a/docs/api/enum.rst +++ /dev/null @@ -1,24 +0,0 @@ - -Enumerations and Flags -====================== - -.. currentmodule:: av.enum - -.. automodule:: av.enum - - -.. _enums: - -Enumerations ------------- - -.. autoclass:: EnumItem - - -.. _flags: - -Flags ------ - -.. autoclass:: EnumFlag - diff --git a/docs/api/error.rst b/docs/api/error.rst index 3f82f4ec2..557ab188e 100644 --- a/docs/api/error.rst +++ b/docs/api/error.rst @@ -5,39 +5,18 @@ Errors .. _error_behaviour: -General Behaviour +General Behavior ----------------- When PyAV encounters an FFmpeg error, it raises an appropriate exception. FFmpeg has a couple dozen of its own error types which we represent via -:ref:`error_classes` and at a lower level via :ref:`error_types`. +:ref:`error_classes`. FFmpeg will also return more typical errors such as ``ENOENT`` or ``EAGAIN``, which we do our best to translate to extensions of the builtin exceptions as defined by -`PEP 3151 `_ -(and fall back onto ``OSError`` if using Python < 3.3). - - -.. _error_types: - -Error Type Enumerations ------------------------ - -We provide :class:`av.error.ErrorType` as an enumeration of the various FFmpeg errors. -To mimick the stdlib ``errno`` module, all enumeration values are available in -the ``av.error`` module, e.g.:: - - try: - do_something() - except OSError as e: - if e.errno != av.error.FILTER_NOT_FOUND: - raise - handle_error() - - -.. autoclass:: av.error.ErrorType +`PEP 3151 `_. .. _error_classes: @@ -55,7 +34,7 @@ There are two competing ideas that have influenced the final design: 2. We want to use the builtin exceptions whenever possible. -As such, PyAV effectivly shadows as much of the builtin exception heirarchy as +As such, PyAV effectively shadows as much of the builtin exception hierarchy as it requires, extending from both the builtins and from :class:`FFmpegError`. Therefore, an argument error within FFmpeg will raise a ``av.error.ValueError``, which @@ -74,12 +53,3 @@ All of these exceptions are available on the top-level ``av`` package, e.g.:: .. autoclass:: av.FFmpegError - -Mapping Codes and Classes -------------------------- - -Here is how the classes line up with the error codes/enumerations: - -.. include:: ../_build/rst/api/error_table.rst - - diff --git a/docs/api/error_table.py b/docs/api/error_table.py deleted file mode 100644 index 3a1b5f219..000000000 --- a/docs/api/error_table.py +++ /dev/null @@ -1,25 +0,0 @@ -import av - - -rows = [("Exception Class", "Code/Enum Name", "FFmpeg Error Message")] - -for code, cls in av.error.classes.items(): - enum = av.error.ErrorType.get(code) - - if not enum: - continue - - if enum.tag == b"PyAV": - continue - - rows.append((f"``av.{cls.__name__}``", f"``av.error.{enum.name}``", enum.strerror)) - -lens = [max(len(row[i]) for row in rows) for i in range(len(rows[0]))] - -header = tuple("=" * x for x in lens) -rows.insert(0, header) -rows.insert(2, header) -rows.append(header) - -for row in rows: - print(" ".join("{:{}s}".format(cell, len_) for cell, len_ in zip(row, lens))) diff --git a/tests/test_enums.py b/tests/test_enums.py deleted file mode 100644 index 64b76d9eb..000000000 --- a/tests/test_enums.py +++ /dev/null @@ -1,223 +0,0 @@ -import pickle - -import pytest - -from av.enum import EnumType, define_enum - -PickleableFooBar = define_enum("PickleableFooBar", __name__, [("FOO", 1)]) - - -def define_foobar(is_flags: bool = False): - return define_enum("Foobar", __name__, (("FOO", 1), ("BAR", 2)), is_flags=is_flags) - - -def test_basics(): - cls = define_foobar() - - assert isinstance(cls, EnumType) - - foo = cls.FOO - - assert isinstance(foo, cls) - assert foo.name == "FOO" and foo.value == 1 - assert not isinstance(foo, PickleableFooBar) - - -def test_access(): - cls = define_foobar() - foo1 = cls.FOO - foo2 = cls["FOO"] - foo3 = cls[1] - foo4 = cls[foo1] - assert foo1 is foo2 - assert foo1 is foo3 - assert foo1 is foo4 - - assert foo1 in cls and "FOO" in cls and 1 in cls - - pytest.raises(KeyError, lambda: cls["not a foo"]) - pytest.raises(KeyError, lambda: cls[10]) - pytest.raises(TypeError, lambda: cls[()]) - - assert cls.get("FOO") == foo1 - assert cls.get("not a foo") is None - - -def test_casting(): - cls = define_foobar() - foo = cls.FOO - - assert repr(foo) == "" - - str_foo = str(foo) - assert isinstance(str_foo, str) and str_foo == "FOO" - - int_foo = int(foo) - assert isinstance(int_foo, int) and int_foo == 1 - - -def test_iteration(): - cls = define_foobar() - assert list(cls) == [cls.FOO, cls.BAR] - - -def test_equality(): - cls = define_foobar() - foo = cls.FOO - bar = cls.BAR - - assert foo == "FOO" and foo == 1 and foo == foo - assert foo != "BAR" and foo != 2 and foo != bar - - pytest.raises(ValueError, lambda: foo == "not a foo") - pytest.raises(ValueError, lambda: foo == 10) - pytest.raises(TypeError, lambda: foo == ()) - - -def test_as_key(): - cls = define_foobar() - foo = cls.FOO - - d = {foo: "value"} - assert d[foo] == "value" - assert d.get("FOO") is None - assert d.get(1) is None - - -def test_pickleable(): - cls = PickleableFooBar - foo = cls.FOO - - enc = pickle.dumps(foo) - - foo2 = pickle.loads(enc) - - assert foo is foo2 - - -def test_create_unknown(): - cls = define_foobar() - baz = cls.get(3, create=True) - - assert baz.name == "FOOBAR_3" - assert baz.value == 3 - - -def test_multiple_names(): - cls = define_enum( - "FFooBBar", - __name__, - (("FOO", 1), ("F", 1), ("BAR", 2), ("B", 2)), - ) - - assert cls.F is cls.FOO - - assert cls.F.name == "FOO" - assert cls.F.name != "F" # This is actually the string. - - assert cls.F == "FOO" - assert cls.F == "F" - assert cls.F != "BAR" - assert cls.F != "B" - pytest.raises(ValueError, lambda: cls.F == "x") - - -def test_flag_basics(): - cls = define_enum( - "FoobarAllFlags", - __name__, - {"FOO": 1, "BAR": 2, "FOOBAR": 3}.items(), - is_flags=True, - ) - foo = cls.FOO - bar = cls.BAR - - foobar = foo | bar - assert foobar is cls.FOOBAR - - foo2 = foobar & foo - assert foo2 is foo - - bar2 = foobar ^ foo - assert bar2 is bar - - bar3 = foobar & ~foo - assert bar3 is bar - - x = cls.FOO - x |= cls.BAR - assert x is cls.FOOBAR - - x = cls.FOOBAR - x &= cls.FOO - assert x is cls.FOO - - -def test_multi_flags_basics(): - cls = define_foobar(is_flags=True) - - foo = cls.FOO - bar = cls.BAR - foobar = foo | bar - assert foobar.name == "FOO|BAR" - assert foobar.value == 3 - assert foobar.flags == (foo, bar) - - foobar2 = foo | bar - foobar3 = cls[3] - foobar4 = cls[foobar] - - assert foobar is foobar2 - assert foobar is foobar3 - assert foobar is foobar4 - - pytest.raises(KeyError, lambda: cls["FOO|BAR"]) - - assert len(cls) == 2 # It didn't get bigger - assert list(cls) == [foo, bar] - - -def test_multi_flags_create_missing(): - cls = define_foobar(is_flags=True) - - foobar = cls[3] - assert foobar is cls.FOO | cls.BAR - - pytest.raises(KeyError, lambda: cls[4]) # Not FOO or BAR - pytest.raises(KeyError, lambda: cls[7]) # FOO and BAR and missing flag. - - -def test_properties(): - Flags = define_foobar(is_flags=True) - foobar = Flags.FOO | Flags.BAR - - class Class: - def __init__(self, value): - self.value = Flags[value].value - - @Flags.property - def flags(self): - return self.value - - @flags.setter - def flags(self, value): - self.value = value - - foo = flags.flag_property("FOO") - bar = flags.flag_property("BAR") - - obj = Class("FOO") - - assert obj.flags is Flags.FOO - assert obj.foo - assert not obj.bar - - obj.bar = True - assert obj.flags is foobar - assert obj.foo - assert obj.bar - - obj.foo = False - assert obj.flags is Flags.BAR - assert not obj.foo - assert obj.bar diff --git a/tests/test_errors.py b/tests/test_errors.py index 1cad5d086..f654c6b2e 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -55,9 +55,11 @@ def test_filenotfound(): def test_buffertoosmall() -> None: """Throw an exception from an enum.""" + + BUFFER_TOO_SMALL = 1397118274 try: - av.error.err_check(-av.error.BUFFER_TOO_SMALL.value) + av.error.err_check(-BUFFER_TOO_SMALL) except av.error.BufferTooSmallError as e: - assert e.errno == av.error.BUFFER_TOO_SMALL.value + assert e.errno == BUFFER_TOO_SMALL else: assert False, "No exception raised!" From e861bdd0f5e863541272b14c6679af637e3b68d7 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 2 Dec 2024 00:23:39 -0500 Subject: [PATCH 184/365] Update AUTHORS --- AUTHORS.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index bc4db2a5b..9caef903f 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -26,7 +26,9 @@ All contributors (by number of commits): - Dan Allan ; `@danielballan `_ - Moonsik Park - Santtu Keskinen +- Marc Mueller <30130371+cdce8p@users.noreply.github.com> - Christoph Rackwitz +- David Plowman - Alireza Davoudi ; `@adavoudi `_ - Jonathan Drolet - Moritz Kassner ; `@mkassner `_ @@ -52,8 +54,8 @@ All contributors (by number of commits): * Andrew Wason * Radek Senfeld ; `@radek-senfeld `_ * Benjamin Chrétien <2742231+bchretien@users.noreply.github.com> -* Marc Mueller <30130371+cdce8p@users.noreply.github.com> * zzjjbb <31069326+zzjjbb@users.noreply.github.com> +* davidplowman <38045873+davidplowman@users.noreply.github.com> * Hanz <40712686+HanzCEO@users.noreply.github.com> * Joe Schiff <41972063+JoeSchiff@users.noreply.github.com> * Artturin @@ -62,12 +64,12 @@ All contributors (by number of commits): * Arthur Barros * Carlos Ruiz * Carlos Ruiz -* David Plowman * Maxime Desroches * egao1980 * Eric Kalosa-Kenyon * elxy * Gemfield +* henri-gasc * Jonathan Martin * Johan Jeppsson Karlin * Philipp Klaus From e0f0ef415b0ec7dfd48e582cf1735eb3bf04829c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 2 Dec 2024 01:21:35 -0500 Subject: [PATCH 185/365] Release 14.0.0 --- CHANGELOG.rst | 9 +++++---- av/about.py | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d8687fa2f..8e5a1380f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -23,10 +23,10 @@ Major: - Drop FFmpeg 6. - Drop support for MacOS <11 in our binary wheels. -- Change many instances ``EnumItem`` s (our custom enum class) with Python's standard Enums. -- Remove the `template` arg from ``OutputContainer.add_stream()``. -- Remove ``CodecContext.close()`` because it's deprecated in ffmpeg. +- Deleted PyAV's custom Enum class in favor of Python's standard Enums. +- Remove ``CodecContext.close()`` and ``Stream.side_data`` because it's deprecated in ffmpeg. - Remove ``AVError`` alias (use ``FFmpegError`` directly instead). +- Remove the `template` arg from ``OutputContainer.add_stream()``. Features: @@ -34,9 +34,10 @@ Features: - Add ``OutputContainer.add_data_stream()`` by :gh-user:`WyattBlue`. - Add ``filter.loudnorm.stats()`` function that returns the stats of loudnorm for 2-pass filtering by :gh-user:`WyattBlue`. - Add ``qmin`` and ``qmax`` parameters to the ``VideoCodecContext`` by :gh-user:`davidplowman` in (:pr:`1618`). +- Allow the profile of a codec to be set as well as queried by :gh-user:`davidplowman` in (:pr:`1625`). Fixes: - +- Make ``VideoFrame.from_numpy_buffer()`` support buffers with padding by :gh-user:`davidplowman` in (:pr:`1635`). - Correct ``Colorspace``'s lowercase enums. - Updated ``sidedata.Type`` enum. - Ensure streams in StreamContainer are released. Fixes :issue:`1599`. diff --git a/av/about.py b/av/about.py index 73437bbc8..229469150 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "14.0.0rc2" +__version__ = "14.0.0" From 9560bb8674e3f3e0d164c5c667e0064a0a057d7c Mon Sep 17 00:00:00 2001 From: Mark Harfouche Date: Mon, 2 Dec 2024 10:59:53 -0500 Subject: [PATCH 186/365] Ensure to add the loudnorm_impl.h in the pypi source distribution Thanks! --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 321b65e6d..6539f496d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,5 +3,5 @@ recursive-include av *.pyx *.pxd recursive-include docs *.rst *.py recursive-include examples *.py recursive-include include *.pxd *.h -recursive-include src/av *.c +recursive-include src/av *.c *.h recursive-include tests *.py From bf03325c967e6102e26734dcde813902e32b977a Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 4 Dec 2024 01:01:29 -0500 Subject: [PATCH 187/365] Address #1663 --- av/video/reformatter.pyi | 3 ++- av/video/reformatter.pyx | 24 +++++++++++++++++++----- tests/test_videoframe.py | 29 +++++++++++++++++++++++++++-- 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/av/video/reformatter.pyi b/av/video/reformatter.pyi index a601dd335..fd5dbd053 100644 --- a/av/video/reformatter.pyi +++ b/av/video/reformatter.pyi @@ -27,7 +27,8 @@ class Colorspace(IntEnum): fcc: int itu601: int itu624: int - smpte240: int + smpte170m: int + smpte240m: int default: int class ColorRange(IntEnum): diff --git a/av/video/reformatter.pyx b/av/video/reformatter.pyx index 4511d08af..538cc4953 100644 --- a/av/video/reformatter.pyx +++ b/av/video/reformatter.pyx @@ -45,6 +45,19 @@ class ColorRange(IntEnum): NB: "Not part of ABI" = lib.AVCOL_RANGE_NB +def _resolve_enum_value(value, enum_class, default): + # Helper function to resolve enum values from different input types. + if value is None: + return default + if isinstance(value, enum_class): + return value.value + if isinstance(value, int): + return value + if isinstance(value, str): + return enum_class[value].value + raise ValueError(f"Cannot convert {value} to {enum_class.__name__}") + + cdef class VideoReformatter: """An object for reformatting size and pixel format of :class:`.VideoFrame`. @@ -83,11 +96,12 @@ cdef class VideoReformatter: """ cdef VideoFormat video_format = VideoFormat(format if format is not None else frame.format) - cdef int c_src_colorspace = (Colorspace[src_colorspace].value if src_colorspace is not None else frame.colorspace) - cdef int c_dst_colorspace = (Colorspace[dst_colorspace].value if dst_colorspace is not None else frame.colorspace) - cdef int c_interpolation = (Interpolation[interpolation] if interpolation is not None else Interpolation.BILINEAR).value - cdef int c_src_color_range = (ColorRange[src_color_range].value if src_color_range is not None else 0) - cdef int c_dst_color_range = (ColorRange[dst_color_range].value if dst_color_range is not None else 0) + + cdef int c_src_colorspace = _resolve_enum_value(src_colorspace, Colorspace, frame.colorspace) + cdef int c_dst_colorspace = _resolve_enum_value(dst_colorspace, Colorspace, frame.colorspace) + cdef int c_interpolation = _resolve_enum_value(interpolation, Interpolation, int(Interpolation.BILINEAR)) + cdef int c_src_color_range = _resolve_enum_value(src_color_range, ColorRange, 0) + cdef int c_dst_color_range = _resolve_enum_value(dst_color_range, ColorRange, 0) return self._reformat( frame, diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index c93a12e32..f044be949 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -7,6 +7,7 @@ import av from av import VideoFrame +from av.video.reformatter import ColorRange, Colorspace, Interpolation from .common import ( TestCase, @@ -145,6 +146,24 @@ def test_roundtrip(self) -> None: img.save(self.sandboxed("roundtrip-high.jpg")) assertImagesAlmostEqual(image, img) + def test_interpolation(self) -> None: + import PIL.Image as Image + + image = Image.open(fate_png()) + frame = VideoFrame.from_image(image) + assert frame.width == 330 and frame.height == 330 + + img = frame.to_image(width=200, height=100, interpolation=Interpolation.BICUBIC) + assert img.width == 200 and img.height == 100 + + img = frame.to_image(width=200, height=100, interpolation="BICUBIC") + assert img.width == 200 and img.height == 100 + + img = frame.to_image( + width=200, height=100, interpolation=int(Interpolation.BICUBIC) + ) + assert img.width == 200 and img.height == 100 + def test_to_image_rgb24(self) -> None: sizes = [(318, 238), (320, 240), (500, 500)] for width, height in sizes: @@ -838,14 +857,20 @@ def test_reformat_identity() -> None: def test_reformat_colorspace() -> None: - # This is allowed. frame = VideoFrame(640, 480, "rgb24") frame.reformat(src_colorspace=None, dst_colorspace="smpte240m") - # I thought this was not allowed, but it seems to be. + frame = VideoFrame(640, 480, "rgb24") + frame.reformat(src_colorspace=None, dst_colorspace=Colorspace.smpte240m) + frame = VideoFrame(640, 480, "yuv420p") frame.reformat(src_colorspace=None, dst_colorspace="smpte240m") + frame = VideoFrame(640, 480, "rgb24") + frame.colorspace = Colorspace.smpte240m + assert frame.colorspace == int(Colorspace.smpte240m) + assert frame.colorspace == Colorspace.smpte240m + def test_reformat_pixel_format_align() -> None: height = 480 From 382ea869920e5bca1531d2afaf8834460947dffa Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Thu, 5 Dec 2024 13:35:48 +0000 Subject: [PATCH 188/365] Cleanup `AVStream.side_data` leftovers --- av/stream.pxd | 2 -- av/stream.pyi | 8 +------- av/stream.pyx | 8 +------- 3 files changed, 2 insertions(+), 16 deletions(-) diff --git a/av/stream.pxd b/av/stream.pxd index d39585167..c847f641e 100644 --- a/av/stream.pxd +++ b/av/stream.pxd @@ -12,8 +12,6 @@ cdef class Stream: # Stream attributes. cdef readonly Container container cdef readonly dict metadata - cdef readonly int nb_side_data - cdef readonly dict side_data # CodecContext attributes. cdef readonly CodecContext codec_context diff --git a/av/stream.pyi b/av/stream.pyi index b17587e10..82bb672b2 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -1,14 +1,8 @@ -from enum import Enum from fractions import Fraction -from typing import ClassVar, Literal +from typing import Literal from .codec import Codec, CodecContext from .container import Container -from .frame import Frame -from .packet import Packet - -class SideData(Enum): - DISPLAYMATRIX: ClassVar[SideData] class Stream: name: str | None diff --git a/av/stream.pyx b/av/stream.pyx index 59e1713c5..35b85acdf 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -1,5 +1,4 @@ cimport libav as lib -from libc.stdint cimport int32_t from enum import Enum @@ -15,11 +14,6 @@ from av.utils cimport ( cdef object _cinit_bypass_sentinel = object() -# If necessary more can be added from -# https://ffmpeg.org/doxygen/trunk/group__lavc__packet.html#ga9a80bfcacc586b483a973272800edb97 -class SideData(Enum): - DISPLAYMATRIX: "Display Matrix" = lib.AV_PKT_DATA_DISPLAYMATRIX - cdef Stream wrap_stream(Container container, lib.AVStream *c_stream, CodecContext codec_context): """Build an av.Stream for an existing AVStream. @@ -84,7 +78,7 @@ cdef class Stream: self.codec_context = codec_context if self.codec_context: self.codec_context.stream_index = stream.index - + self.metadata = avdict_to_dict( stream.metadata, encoding=self.container.metadata_encoding, From 5c532dd5f025935da7114a21e34bafcc4ba6bfa6 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 6 Dec 2024 03:06:06 -0500 Subject: [PATCH 189/365] Write changelog --- AUTHORS.rst | 3 ++- CHANGELOG.rst | 10 ++++++++++ av/about.py | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 9caef903f..d5b4cb7ee 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -18,8 +18,8 @@ All contributors (by number of commits): * Tadas Dailyda * JoeUgly <41972063+JoeUgly@users.noreply.github.com> * Justin Wong <46082645+uvjustin@users.noreply.github.com> +* Mark Harfouche -- Mark Harfouche - Alba Mendez - Dave Johansen - Xinran Xu ; `@xxr3376 `_ @@ -73,6 +73,7 @@ All contributors (by number of commits): * Jonathan Martin * Johan Jeppsson Karlin * Philipp Klaus +* Lukas Geiger * Mattias Wadman * Manuel Goacolou * Julian Schweizer diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8e5a1380f..45de1e218 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,15 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. + +v14.0.1 + +Fixes: + +- Include header files in source distribution by :gh-user:`hmaarrfk` in (:pr:`1662`). +- Cleanup ``AVStream.side_data`` leftovers by :gh-user:`lgeiger` in (:pr:`1674`). +- Address :issue:`1663` by :gh-user:`WyattBlue`. + v14.0.0 ------- @@ -37,6 +46,7 @@ Features: - Allow the profile of a codec to be set as well as queried by :gh-user:`davidplowman` in (:pr:`1625`). Fixes: + - Make ``VideoFrame.from_numpy_buffer()`` support buffers with padding by :gh-user:`davidplowman` in (:pr:`1635`). - Correct ``Colorspace``'s lowercase enums. - Updated ``sidedata.Type`` enum. diff --git a/av/about.py b/av/about.py index 229469150..4fcf9b8bb 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "14.0.0" +__version__ = "14.0.1" From 55bcc75ad2ab0424785ba95bd070258f6d40a1e9 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 6 Dec 2024 03:39:49 -0500 Subject: [PATCH 190/365] `mp3` now works with `add_stream_from_template()` --- av/container/output.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/container/output.pyx b/av/container/output.pyx index 8fad652d4..a75e47d43 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -148,7 +148,7 @@ cdef class OutputContainer(Container): cdef const lib.AVCodec *codec cdef Codec codec_obj - if template.type == "subtitle": + if template.type != "video": codec_obj = template.codec_context.codec else: codec_obj = Codec(template.codec_context.codec.name, "w") From ce45f1d07f76907e431a92ebff7d49a0e4c294d9 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 6 Dec 2024 03:43:28 -0500 Subject: [PATCH 191/365] Release 14.0.1 --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 45de1e218..4a30385f9 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -24,6 +24,7 @@ Fixes: - Include header files in source distribution by :gh-user:`hmaarrfk` in (:pr:`1662`). - Cleanup ``AVStream.side_data`` leftovers by :gh-user:`lgeiger` in (:pr:`1674`). - Address :issue:`1663` by :gh-user:`WyattBlue`. +- Make ``mp3`` work with ``OutputContainer.add_stream_from_template()``. v14.0.0 ------- From 6839e9f58b162795f6cf02ebe77c847ae6042b9a Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 8 Dec 2024 19:10:23 -0500 Subject: [PATCH 192/365] Fix mypy error --- tests/test_audioframe.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_audioframe.py b/tests/test_audioframe.py index d67e181ce..c6bf87b3f 100644 --- a/tests/test_audioframe.py +++ b/tests/test_audioframe.py @@ -115,11 +115,11 @@ def test_from_ndarray_value_error() -> None: AudioFrame.from_ndarray(array, format="flt", layout="mono") # incorrect number of dimensions - array = np.zeros(shape=(1, 160, 2), dtype="f4") + array2 = np.zeros(shape=(1, 160, 2), dtype="f4") with pytest.raises( ValueError, match="Expected numpy array with ndim `2` but got `3`" ) as cm: - AudioFrame.from_ndarray(array, format="flt", layout="mono") + AudioFrame.from_ndarray(array2, format="flt", layout="mono") # incorrect shape array = np.zeros(shape=(2, 160), dtype="f4") From 6eaf7017cdae839641ed03357fc526abdd50298f Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Mon, 9 Dec 2024 00:55:08 +0000 Subject: [PATCH 193/365] Add `VideoFrame.rotation` --- av/sidedata/sidedata.pxd | 2 ++ av/sidedata/sidedata.pyx | 12 ++++++++++-- av/video/frame.pyi | 2 ++ av/video/frame.pyx | 11 +++++++++++ include/libavutil/avutil.pxd | 3 +++ tests/test_decode.py | 10 ++++++++++ 6 files changed, 38 insertions(+), 2 deletions(-) diff --git a/av/sidedata/sidedata.pxd b/av/sidedata/sidedata.pxd index 8a2f6d07c..5e6e5bf4c 100644 --- a/av/sidedata/sidedata.pxd +++ b/av/sidedata/sidedata.pxd @@ -14,6 +14,8 @@ cdef class SideData(Buffer): cdef SideData wrap_side_data(Frame frame, int index) +cdef int get_display_rotation(Frame frame) + cdef class _SideDataContainer: cdef Frame frame diff --git a/av/sidedata/sidedata.pyx b/av/sidedata/sidedata.pyx index 753496fea..24dbae119 100644 --- a/av/sidedata/sidedata.pyx +++ b/av/sidedata/sidedata.pyx @@ -1,3 +1,5 @@ +from libc.stdint cimport int32_t + from collections.abc import Mapping from enum import Enum @@ -45,13 +47,19 @@ class Type(Enum): cdef SideData wrap_side_data(Frame frame, int index): - cdef lib.AVFrameSideDataType type_ = frame.ptr.side_data[index].type - if type_ == lib.AV_FRAME_DATA_MOTION_VECTORS: + if frame.ptr.side_data[index].type == lib.AV_FRAME_DATA_MOTION_VECTORS: return MotionVectors(_cinit_bypass_sentinel, frame, index) else: return SideData(_cinit_bypass_sentinel, frame, index) +cdef int get_display_rotation(Frame frame): + for i in range(frame.ptr.nb_side_data): + if frame.ptr.side_data[i].type == lib.AV_FRAME_DATA_DISPLAYMATRIX: + return int(lib.av_display_rotation_get(frame.ptr.side_data[i].data)) + return 0 + + cdef class SideData(Buffer): def __init__(self, sentinel, Frame frame, int index): if sentinel is not _cinit_bypass_sentinel: diff --git a/av/video/frame.pyi b/av/video/frame.pyi index 0739010c1..a3eea373d 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -41,6 +41,8 @@ class VideoFrame(Frame): def height(self) -> int: ... @property def interlaced_frame(self) -> bool: ... + @property + def rotation(self) -> int: ... def __init__( self, width: int = 0, height: int = 0, format: str = "yuv420p" ) -> None: ... diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 862db8513..02cde3187 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -4,6 +4,7 @@ from enum import IntEnum from libc.stdint cimport uint8_t from av.error cimport err_check +from av.sidedata.sidedata cimport get_display_rotation from av.utils cimport check_ndarray from av.video.format cimport get_pix_fmt, get_video_format from av.video.plane cimport VideoPlane @@ -172,6 +173,16 @@ cdef class VideoFrame(Frame): """Height of the image, in pixels.""" return self.ptr.height + @property + def rotation(self): + """The rotation component of the `DISPLAYMATRIX` transformation matrix. + + Returns: + int: The angle (in degrees) by which the transformation rotates the frame + counterclockwise. The angle will be in range [-180, 180]. + """ + return get_display_rotation(self) + @property def interlaced_frame(self): """Is this frame an interlaced or progressive?""" diff --git a/include/libavutil/avutil.pxd b/include/libavutil/avutil.pxd index ed281aeaf..58dd43922 100644 --- a/include/libavutil/avutil.pxd +++ b/include/libavutil/avutil.pxd @@ -4,6 +4,9 @@ from libc.stdint cimport int64_t, uint8_t, uint64_t, int32_t cdef extern from "libavutil/mathematics.h" nogil: pass +cdef extern from "libavutil/display.h" nogil: + cdef double av_display_rotation_get(const int32_t matrix[9]) + cdef extern from "libavutil/rational.h" nogil: cdef int av_reduce(int *dst_num, int *dst_den, int64_t num, int64_t den, int64_t max) diff --git a/tests/test_decode.py b/tests/test_decode.py index 20abdf840..05f636977 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -155,3 +155,13 @@ def test_flush_decoded_video_frame_count(self) -> None: output_count += 1 assert output_count == input_count + + def test_no_side_data(self) -> None: + container = av.open(fate_suite("h264/interlaced_crop.mp4")) + frame = next(container.decode(video=0)) + assert frame.rotation == 0 + + def test_side_data(self) -> None: + container = av.open(fate_suite("mov/displaymatrix.mov")) + frame = next(container.decode(video=0)) + assert frame.rotation == -90 From 2c63608a349e9f1f5de702071100bb0c8d268860 Mon Sep 17 00:00:00 2001 From: Matthew Lai Date: Tue, 17 Dec 2024 20:23:53 +0800 Subject: [PATCH 194/365] Implement hardware decoding This implements hardware decoding continuing from the work of @rvillalba-novetta and @mikeboers with cleanup work by @WyattBlue. --- CHANGELOG.rst | 9 ++ av/__init__.py | 1 + av/__main__.py | 14 +++ av/about.py | 2 +- av/audio/codeccontext.pyx | 5 +- av/codec/codec.pxd | 2 + av/codec/codec.pyi | 1 + av/codec/codec.pyx | 38 ++++++++ av/codec/context.pxd | 12 ++- av/codec/context.pyi | 7 +- av/codec/context.pyx | 23 +++-- av/codec/hwaccel.pxd | 20 +++++ av/codec/hwaccel.pyi | 48 ++++++++++ av/codec/hwaccel.pyx | 151 ++++++++++++++++++++++++++++++++ av/container/core.pxd | 3 + av/container/core.pyi | 5 ++ av/container/core.pyx | 11 ++- av/container/input.pyx | 2 +- av/container/output.pyx | 6 +- av/video/codeccontext.pxd | 12 +++ av/video/codeccontext.pyx | 64 +++++++++++++- examples/basics/hw_decode.py | 76 ++++++++++++++++ include/libav.pxd | 3 + include/libavcodec/avcodec.pxd | 8 ++ include/libavcodec/hwaccel.pxd | 19 ++++ include/libavutil/buffer.pxd | 13 ++- include/libavutil/hwcontext.pxd | 24 +++++ scripts/build-deps | 26 ++++++ tests/test_decode.py | 67 ++++++++++++++ 29 files changed, 649 insertions(+), 23 deletions(-) create mode 100644 av/codec/hwaccel.pxd create mode 100644 av/codec/hwaccel.pyi create mode 100644 av/codec/hwaccel.pyx create mode 100644 examples/basics/hw_decode.py create mode 100644 include/libavcodec/hwaccel.pxd create mode 100644 include/libavutil/hwcontext.pxd diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4a30385f9..e65107428 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,7 +17,16 @@ We are operating with `semantic versioning `_. are merged into the "default" branch. +v14.1.0 (Unreleased) +-------------------- + +Features + +- Add hardware decoding by :gh-user:`matthewlai` and :gh-user:`WyattBlue` in (:pr:`1685`). + + v14.0.1 +------- Fixes: diff --git a/av/__init__.py b/av/__init__.py index cbc3c8a2f..e2f9e5a6d 100644 --- a/av/__init__.py +++ b/av/__init__.py @@ -17,6 +17,7 @@ from av.bitstream import BitStreamFilterContext, bitstream_filters_available from av.codec.codec import Codec, codecs_available from av.codec.context import CodecContext +from av.codec.hwaccel import HWConfig from av.container import open from av.format import ContainerFormat, formats_available from av.packet import Packet diff --git a/av/__main__.py b/av/__main__.py index bc353d147..9e2b9d0ac 100644 --- a/av/__main__.py +++ b/av/__main__.py @@ -6,6 +6,8 @@ def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--codecs", action="store_true") + parser.add_argument("--hwdevices", action="store_true") + parser.add_argument("--hwconfigs", action="store_true") parser.add_argument("--version", action="store_true") args = parser.parse_args() @@ -30,6 +32,18 @@ def main() -> None: version = config["version"] print(f"{libname:<13} {version[0]:3d}.{version[1]:3d}.{version[2]:3d}") + if args.hwdevices: + from av.codec.hwaccel import hwdevices_available + + print("Hardware device types:") + for x in hwdevices_available(): + print(" ", x) + + if args.hwconfigs: + from av.codec.codec import dump_hwconfigs + + dump_hwconfigs() + if args.codecs: from av.codec.codec import dump_codecs diff --git a/av/about.py b/av/about.py index 4fcf9b8bb..217fb25cb 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "14.0.1" +__version__ = "14.1.0" diff --git a/av/audio/codeccontext.pyx b/av/audio/codeccontext.pyx index 54319ddaf..856af555c 100644 --- a/av/audio/codeccontext.pyx +++ b/av/audio/codeccontext.pyx @@ -3,13 +3,14 @@ cimport libav as lib from av.audio.format cimport AudioFormat, get_audio_format from av.audio.frame cimport AudioFrame, alloc_audio_frame from av.audio.layout cimport AudioLayout, get_audio_layout +from av.codec.hwaccel cimport HWAccel from av.frame cimport Frame from av.packet cimport Packet cdef class AudioCodecContext(CodecContext): - cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec): - CodecContext._init(self, ptr, codec) + cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec, HWAccel hwaccel): + CodecContext._init(self, ptr, codec, hwaccel) cdef _prepare_frames_for_encode(self, Frame input_frame): diff --git a/av/codec/codec.pxd b/av/codec/codec.pxd index b9925df13..576c659b4 100644 --- a/av/codec/codec.pxd +++ b/av/codec/codec.pxd @@ -7,6 +7,8 @@ cdef class Codec: cdef const lib.AVCodecDescriptor *desc cdef readonly bint is_encoder + cdef tuple _hardware_configs + cdef _init(self, name=?) diff --git a/av/codec/codec.pyi b/av/codec/codec.pyi index 32736c080..73c1f1edb 100644 --- a/av/codec/codec.pyi +++ b/av/codec/codec.pyi @@ -108,3 +108,4 @@ class codec_descriptor: codecs_available: set[str] def dump_codecs() -> None: ... +def dump_hwconfigs() -> None: ... diff --git a/av/codec/codec.pyx b/av/codec/codec.pyx index 1493f0f7b..2dab166ab 100644 --- a/av/codec/codec.pyx +++ b/av/codec/codec.pyx @@ -1,4 +1,5 @@ from av.audio.format cimport get_audio_format +from av.codec.hwaccel cimport wrap_hwconfig from av.descriptor cimport wrap_avclass from av.utils cimport avrational_to_fraction from av.video.format cimport get_video_format @@ -117,6 +118,10 @@ cdef class Codec: if self.is_encoder and lib.av_codec_is_decoder(self.ptr): raise RuntimeError("%s is both encoder and decoder.") + def __repr__(self): + mode = "w" if self.is_encoder else "r" + return f"" + def create(self, kind = None): """Create a :class:`.CodecContext` for this codec. @@ -203,6 +208,23 @@ cdef class Codec: i += 1 return ret + @property + def hardware_configs(self): + if self._hardware_configs: + return self._hardware_configs + ret = [] + cdef int i = 0 + cdef lib.AVCodecHWConfig *ptr + while True: + ptr = lib.avcodec_get_hw_config(self.ptr, i) + if not ptr: + break + ret.append(wrap_hwconfig(ptr)) + i += 1 + ret = tuple(ret) + self._hardware_configs = ret + return ret + @property def properties(self): return self.desc.props @@ -337,3 +359,19 @@ def dump_codecs(): ) except Exception as e: print(f"...... {codec.name:<18} ERROR: {e}") + +def dump_hwconfigs(): + print("Hardware configs:") + for name in sorted(codecs_available): + try: + codec = Codec(name, "r") + except ValueError: + continue + + configs = codec.hardware_configs + if not configs: + continue + + print(" ", codec.name) + for config in configs: + print(" ", config) diff --git a/av/codec/context.pxd b/av/codec/context.pxd index 42b2d63e7..7ba89dab7 100644 --- a/av/codec/context.pxd +++ b/av/codec/context.pxd @@ -3,6 +3,7 @@ from libc.stdint cimport int64_t from av.bytesource cimport ByteSource from av.codec.codec cimport Codec +from av.codec.hwaccel cimport HWAccel from av.frame cimport Frame from av.packet cimport Packet @@ -18,11 +19,12 @@ cdef class CodecContext: cdef int stream_index cdef lib.AVCodecParserContext *parser - cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec) + cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec, HWAccel hwaccel) # Public API. cdef readonly bint is_open cdef readonly Codec codec + cdef readonly HWAccel hwaccel cdef public dict options cpdef open(self, bint strict=?) @@ -31,6 +33,9 @@ cdef class CodecContext: cpdef decode(self, Packet packet=?) cpdef flush_buffers(self) + # Used by hardware-accelerated decode. + cdef HWAccel hwaccel_ctx + # Used by both transcode APIs to setup user-land objects. # TODO: Remove the `Packet` from `_setup_decoded_frame` (because flushing packets # are bogus). It should take all info it needs from the context and/or stream. @@ -49,10 +54,11 @@ cdef class CodecContext: cdef _send_packet_and_recv(self, Packet packet) cdef _recv_frame(self) + cdef _transfer_hwframe(self, Frame frame) + # Implemented by children for the generic send/recv API, so we have the # correct subclass of Frame. cdef Frame _next_frame cdef Frame _alloc_next_frame(self) - -cdef CodecContext wrap_codec_context(lib.AVCodecContext*, const lib.AVCodec*) +cdef CodecContext wrap_codec_context(lib.AVCodecContext*, const lib.AVCodec*, HWAccel hwaccel) diff --git a/av/codec/context.pyi b/av/codec/context.pyi index a6ca9647e..543da208e 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -5,6 +5,7 @@ from typing import ClassVar, Literal from av.packet import Packet from .codec import Codec +from .hwaccel import HWAccel class ThreadType(Flag): NONE: ClassVar[ThreadType] @@ -83,10 +84,14 @@ class CodecContext: def delay(self) -> bool: ... @property def extradata_size(self) -> int: ... + @property + def is_hwaccel(self) -> bool: ... def open(self, strict: bool = True) -> None: ... @staticmethod def create( - codec: str | Codec, mode: Literal["r", "w"] | None = None + codec: str | Codec, + mode: Literal["r", "w"] | None = None, + hwaccel: HWAccel | None = None, ) -> CodecContext: ... def parse( self, raw_input: bytes | bytearray | memoryview | None = None diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 29b7b80d1..e7136b86f 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -18,7 +18,7 @@ from av.dictionary import Dictionary cdef object _cinit_sentinel = object() -cdef CodecContext wrap_codec_context(lib.AVCodecContext *c_ctx, const lib.AVCodec *c_codec): +cdef CodecContext wrap_codec_context(lib.AVCodecContext *c_ctx, const lib.AVCodec *c_codec, HWAccel hwaccel): """Build an av.CodecContext for an existing AVCodecContext.""" cdef CodecContext py_ctx @@ -35,7 +35,7 @@ cdef CodecContext wrap_codec_context(lib.AVCodecContext *c_ctx, const lib.AVCode else: py_ctx = CodecContext(_cinit_sentinel) - py_ctx._init(c_ctx, c_codec) + py_ctx._init(c_ctx, c_codec, hwaccel) return py_ctx @@ -83,10 +83,10 @@ class Flags2(IntEnum): cdef class CodecContext: @staticmethod - def create(codec, mode=None): + def create(codec, mode=None, hwaccel=None): cdef Codec cy_codec = codec if isinstance(codec, Codec) else Codec(codec, mode) cdef lib.AVCodecContext *c_ctx = lib.avcodec_alloc_context3(cy_codec.ptr) - return wrap_codec_context(c_ctx, cy_codec.ptr) + return wrap_codec_context(c_ctx, cy_codec.ptr, hwaccel) def __cinit__(self, sentinel=None, *args, **kwargs): if sentinel is not _cinit_sentinel: @@ -96,11 +96,12 @@ cdef class CodecContext: self.stream_index = -1 # This is set by the container immediately. self.is_open = False - cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec): + cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec, HWAccel hwaccel): self.ptr = ptr if self.ptr.codec and codec and self.ptr.codec != codec: raise RuntimeError("Wrapping CodecContext with mismatched codec.") self.codec = wrap_codec(codec if codec != NULL else self.ptr.codec) + self.hwaccel = hwaccel # Set reasonable threading defaults. self.ptr.thread_count = 0 # use as many threads as there are CPUs. @@ -310,6 +311,13 @@ cdef class CodecContext: return packets + @property + def is_hwaccel(self): + """ + Returns ``True`` if this codec context is hardware accelerated, ``False`` otherwise. + """ + return self.hwaccel_ctx is not None + def _send_frame_and_recv(self, Frame frame): cdef Packet packet @@ -359,10 +367,15 @@ cdef class CodecContext: return err_check(res) + frame = self._transfer_hwframe(frame) + if not res: self._next_frame = None return frame + cdef _transfer_hwframe(self, Frame frame): + return frame + cdef _recv_packet(self): cdef Packet packet = Packet() diff --git a/av/codec/hwaccel.pxd b/av/codec/hwaccel.pxd new file mode 100644 index 000000000..e68f43bb1 --- /dev/null +++ b/av/codec/hwaccel.pxd @@ -0,0 +1,20 @@ +cimport libav as lib + +from av.codec.codec cimport Codec + + +cdef class HWConfig: + cdef object __weakref__ + cdef lib.AVCodecHWConfig *ptr + cdef void _init(self, lib.AVCodecHWConfig *ptr) + +cdef HWConfig wrap_hwconfig(lib.AVCodecHWConfig *ptr) + +cdef class HWAccel: + cdef int _device_type + cdef str _device + cdef readonly Codec codec + cdef readonly HWConfig config + cdef lib.AVBufferRef *ptr + cdef public bint allow_software_fallback + cdef public dict options diff --git a/av/codec/hwaccel.pyi b/av/codec/hwaccel.pyi new file mode 100644 index 000000000..7e4748e23 --- /dev/null +++ b/av/codec/hwaccel.pyi @@ -0,0 +1,48 @@ +from enum import IntEnum + +from av.codec.codec import Codec +from av.video.format import VideoFormat + +class HWDeviceType(IntEnum): + none: int + vdpau: int + cuda: int + vaapi: int + dxva2: int + qsv: int + videotoolbox: int + d3d11va: int + drm: int + opencl: int + mediacodec: int + vulkan: int + d3d12va: int + +class HWConfigMethod(IntEnum): + none: int + hw_device_ctx: int + hw_frame_ctx: int + internal: int + ad_hoc: int + +class HWConfig: + @property + def device_type(self) -> HWDeviceType: ... + @property + def format(self) -> VideoFormat: ... + @property + def methods(self) -> HWConfigMethod: ... + @property + def is_supported(self) -> bool: ... + +class HWAccel: + def __init__( + self, + device_type: str | HWDeviceType, + device: str | None = None, + allow_software_fallback: bool = False, + options: dict[str, object] | None = None, + ) -> None: ... + def create(self, codec: Codec) -> HWAccel: ... + +def hwdevices_available() -> list[str]: ... diff --git a/av/codec/hwaccel.pyx b/av/codec/hwaccel.pyx new file mode 100644 index 000000000..1c96d02e8 --- /dev/null +++ b/av/codec/hwaccel.pyx @@ -0,0 +1,151 @@ +import weakref +from enum import IntEnum + +cimport libav as lib + +from av.codec.codec cimport Codec +from av.dictionary cimport _Dictionary +from av.error cimport err_check +from av.video.format cimport get_video_format +from av.dictionary import Dictionary + + +class HWDeviceType(IntEnum): + none = lib.AV_HWDEVICE_TYPE_NONE + vdpau = lib.AV_HWDEVICE_TYPE_VDPAU + cuda = lib.AV_HWDEVICE_TYPE_CUDA + vaapi = lib.AV_HWDEVICE_TYPE_VAAPI + dxva2 = lib.AV_HWDEVICE_TYPE_DXVA2 + qsv = lib.AV_HWDEVICE_TYPE_QSV + videotoolbox = lib.AV_HWDEVICE_TYPE_VIDEOTOOLBOX + d3d11va = lib.AV_HWDEVICE_TYPE_D3D11VA + drm = lib.AV_HWDEVICE_TYPE_DRM + opencl = lib.AV_HWDEVICE_TYPE_OPENCL + mediacodec = lib.AV_HWDEVICE_TYPE_MEDIACODEC + vulkan = lib.AV_HWDEVICE_TYPE_VULKAN + d3d12va = lib.AV_HWDEVICE_TYPE_D3D12VA + +class HWConfigMethod(IntEnum): + none = 0 + hw_device_ctx = lib.AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX # This is the only one we support. + hw_frame_ctx = lib.AV_CODEC_HW_CONFIG_METHOD_HW_FRAMES_CTX + internal = lib.AV_CODEC_HW_CONFIG_METHOD_INTERNAL + ad_hoc = lib.AV_CODEC_HW_CONFIG_METHOD_AD_HOC + + +cdef object _cinit_sentinel = object() +cdef object _singletons = weakref.WeakValueDictionary() + +cdef HWConfig wrap_hwconfig(lib.AVCodecHWConfig *ptr): + try: + return _singletons[ptr] + except KeyError: + pass + cdef HWConfig config = HWConfig(_cinit_sentinel) + config._init(ptr) + _singletons[ptr] = config + return config + + +cdef class HWConfig: + def __init__(self, sentinel): + if sentinel is not _cinit_sentinel: + raise RuntimeError("Cannot instantiate CodecContext") + + cdef void _init(self, lib.AVCodecHWConfig *ptr): + self.ptr = ptr + + def __repr__(self): + return ( + f"self.ptr:x}>" + ) + + @property + def device_type(self): + return HWDeviceType(self.ptr.device_type) + + @property + def format(self): + return get_video_format(self.ptr.pix_fmt, 0, 0) + + @property + def methods(self): + return HWConfigMethod(self.ptr.methods) + + @property + def is_supported(self): + return bool(self.ptr.methods & lib.AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX) + + +cpdef hwdevices_available(): + result = [] + + cdef lib.AVHWDeviceType x = lib.AV_HWDEVICE_TYPE_NONE + while True: + x = lib.av_hwdevice_iterate_types(x) + if x == lib.AV_HWDEVICE_TYPE_NONE: + break + result.append(lib.av_hwdevice_get_type_name(HWDeviceType(x))) + + return result + + +cdef class HWAccel: + def __init__(self, device_type, device=None, codec=None, allow_software_fallback=True, options=None): + if isinstance(device_type, HWDeviceType): + self._device_type = device_type + elif isinstance(device_type, str): + self._device_type = int(lib.av_hwdevice_find_type_by_name(device_type)) + else: + raise ValueError("Unknown type for device_type") + + self._device = device + self.allow_software_fallback = allow_software_fallback + self.options = {} if not options else dict(options) + self.ptr = NULL + self.codec = codec + self.config = None + + if codec: + self._initialize_hw_context() + + def _initialize_hw_context(self): + cdef HWConfig config + for config in self.codec.hardware_configs: + if not (config.ptr.methods & lib.AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX): + continue + if self._device_type and config.device_type != self._device_type: + continue + break + else: + raise NotImplementedError(f"No supported hardware config for {self.codec}") + + self.config = config + + cdef char *c_device = NULL + if self._device: + device_bytes = self._device.encode() + c_device = device_bytes + cdef _Dictionary c_options = Dictionary(self.options) + + err_check( + lib.av_hwdevice_ctx_create( + &self.ptr, config.ptr.device_type, c_device, c_options.ptr, 0 + ) + ) + + def create(self, Codec codec not None): + """Create a new hardware accelerator context with the given codec""" + if self.ptr: + raise RuntimeError("Hardware context already initialized") + + self.codec = codec + self._initialize_hw_context() + return self + + def __dealloc__(self): + if self.ptr: + lib.av_buffer_unref(&self.ptr) diff --git a/av/container/core.pxd b/av/container/core.pxd index 1aed54b90..87bb792b3 100644 --- a/av/container/core.pxd +++ b/av/container/core.pxd @@ -1,5 +1,6 @@ cimport libav as lib +from av.codec.hwaccel cimport HWAccel from av.container.pyio cimport PyIOFile from av.container.streams cimport StreamContainer from av.dictionary cimport _Dictionary @@ -33,6 +34,8 @@ cdef class Container: cdef readonly dict container_options cdef readonly list stream_options + cdef HWAccel hwaccel + cdef readonly StreamContainer streams cdef readonly dict metadata diff --git a/av/container/core.pyi b/av/container/core.pyi index 227a7d32a..7c681b18b 100644 --- a/av/container/core.pyi +++ b/av/container/core.pyi @@ -4,6 +4,7 @@ from pathlib import Path from types import TracebackType from typing import Any, Callable, ClassVar, Literal, Type, overload +from av.codec.hwaccel import HWAccel from av.format import ContainerFormat from .input import InputContainer @@ -73,6 +74,7 @@ def open( buffer_size: int = 32768, timeout: Real | None | tuple[Real | None, Real | None] = None, io_open: Callable[..., Any] | None = None, + hwaccel: HWAccel | None = None, ) -> InputContainer: ... @overload def open( @@ -87,6 +89,7 @@ def open( buffer_size: int = 32768, timeout: Real | None | tuple[Real | None, Real | None] = None, io_open: Callable[..., Any] | None = None, + hwaccel: HWAccel | None = None, ) -> InputContainer: ... @overload def open( @@ -101,6 +104,7 @@ def open( buffer_size: int = 32768, timeout: Real | None | tuple[Real | None, Real | None] = None, io_open: Callable[..., Any] | None = None, + hwaccel: HWAccel | None = None, ) -> OutputContainer: ... @overload def open( @@ -115,4 +119,5 @@ def open( buffer_size: int = 32768, timeout: Real | None | tuple[Real | None, Real | None] = None, io_open: Callable[..., Any] | None = None, + hwaccel: HWAccel | None = None, ) -> InputContainer | OutputContainer: ... diff --git a/av/container/core.pyx b/av/container/core.pyx index 563c79d21..201570c62 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -8,6 +8,7 @@ from pathlib import Path cimport libav as lib +from av.codec.hwaccel cimport HWAccel from av.container.core cimport timeout_info from av.container.input cimport InputContainer from av.container.output cimport OutputContainer @@ -143,7 +144,7 @@ class Flags(Flag): cdef class Container: def __cinit__(self, sentinel, file_, format_name, options, - container_options, stream_options, + container_options, stream_options, hwaccel, metadata_encoding, metadata_errors, buffer_size, open_timeout, read_timeout, io_open): @@ -164,6 +165,8 @@ cdef class Container: self.container_options = dict(container_options or ()) self.stream_options = [dict(x) for x in stream_options or ()] + self.hwaccel = hwaccel + self.metadata_encoding = metadata_encoding self.metadata_errors = metadata_errors @@ -296,6 +299,7 @@ def open( buffer_size=32768, timeout=None, io_open=None, + hwaccel=None ): """open(file, mode='r', **kwargs) @@ -322,6 +326,7 @@ def open( ``url`` is the url to open, ``flags`` is a combination of AVIO_FLAG_* and ``options`` is a dictionary of additional options. The callable should return a file-like object. + :param HWAccel hwaccel: Optional settings for hardware-accelerated decoding. :rtype: Container For devices (via ``libavdevice``), pass the name of the device to ``format``, @@ -367,7 +372,7 @@ def open( if mode.startswith("r"): return InputContainer(_cinit_sentinel, file, format, options, - container_options, stream_options, metadata_encoding, metadata_errors, + container_options, stream_options, hwaccel, metadata_encoding, metadata_errors, buffer_size, open_timeout, read_timeout, io_open, ) @@ -376,6 +381,6 @@ def open( "Provide stream options via Container.add_stream(..., options={})." ) return OutputContainer(_cinit_sentinel, file, format, options, - container_options, stream_options, metadata_encoding, metadata_errors, + container_options, stream_options, None, metadata_encoding, metadata_errors, buffer_size, open_timeout, read_timeout, io_open, ) diff --git a/av/container/input.pyx b/av/container/input.pyx index 7246f8245..aa9940452 100644 --- a/av/container/input.pyx +++ b/av/container/input.pyx @@ -77,7 +77,7 @@ cdef class InputContainer(Container): codec_context = lib.avcodec_alloc_context3(codec) err_check(lib.avcodec_parameters_to_context(codec_context, stream.codecpar)) codec_context.pkt_timebase = stream.time_base - py_codec_context = wrap_codec_context(codec_context, codec) + py_codec_context = wrap_codec_context(codec_context, codec, self.hwaccel) else: # no decoder is available py_codec_context = None diff --git a/av/container/output.pyx b/av/container/output.pyx index a75e47d43..e61ef2297 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -125,7 +125,7 @@ cdef class OutputContainer(Container): err_check(lib.avcodec_parameters_from_context(stream.codecpar, codec_context)) # Construct the user-land stream - cdef CodecContext py_codec_context = wrap_codec_context(codec_context, codec) + cdef CodecContext py_codec_context = wrap_codec_context(codec_context, codec, None) cdef Stream py_stream = wrap_stream(self, stream, py_codec_context) self.streams.add_stream(py_stream) @@ -179,7 +179,7 @@ cdef class OutputContainer(Container): err_check(lib.avcodec_parameters_from_context(stream.codecpar, codec_context)) # Construct the user-land stream - cdef CodecContext py_codec_context = wrap_codec_context(codec_context, codec) + cdef CodecContext py_codec_context = wrap_codec_context(codec_context, codec, None) cdef Stream py_stream = wrap_stream(self, stream, py_codec_context) self.streams.add_stream(py_stream) @@ -237,7 +237,7 @@ cdef class OutputContainer(Container): # Construct the user-land stream cdef CodecContext py_codec_context = None if codec_context != NULL: - py_codec_context = wrap_codec_context(codec_context, codec) + py_codec_context = wrap_codec_context(codec_context, codec, None) cdef Stream py_stream = wrap_stream(self, stream, py_codec_context) self.streams.add_stream(py_stream) diff --git a/av/video/codeccontext.pxd b/av/video/codeccontext.pxd index 9693caa9b..895ba74b1 100644 --- a/av/video/codeccontext.pxd +++ b/av/video/codeccontext.pxd @@ -1,3 +1,4 @@ +cimport libav as lib from av.codec.context cimport CodecContext from av.video.format cimport VideoFormat @@ -5,8 +6,19 @@ from av.video.frame cimport VideoFrame from av.video.reformatter cimport VideoReformatter +# The get_format callback in AVCodecContext is called by the decoder to pick a format out of a list. +# When we want accelerated decoding, we need to figure out ahead of time what the format should be, +# and find a way to pass that into our callback so we can return it to the decoder. We use the 'opaque' +# user data field in AVCodecContext for that. This is the struct we store a pointer to in that field. +cdef struct AVCodecPrivateData: + lib.AVPixelFormat hardware_pix_fmt + bint allow_software_fallback + + cdef class VideoCodecContext(CodecContext): + cdef AVCodecPrivateData _private_data + cdef VideoFormat _format cdef _build_format(self) diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.pyx index d2f4c9e14..92470c159 100644 --- a/av/video/codeccontext.pyx +++ b/av/video/codeccontext.pyx @@ -2,6 +2,8 @@ cimport libav as lib from libc.stdint cimport int64_t from av.codec.context cimport CodecContext +from av.codec.hwaccel cimport HWAccel, HWConfig +from av.error cimport err_check from av.frame cimport Frame from av.packet cimport Packet from av.utils cimport avrational_to_fraction, to_avrational @@ -10,13 +12,51 @@ from av.video.frame cimport VideoFrame, alloc_video_frame from av.video.reformatter cimport VideoReformatter +cdef lib.AVPixelFormat _get_hw_format(lib.AVCodecContext *ctx, const lib.AVPixelFormat *pix_fmts) noexcept: + # In the case where we requested accelerated decoding, the decoder first calls this function + # with a list that includes both the hardware format and software formats. + # First we try to pick the hardware format if it's in the list. + # However, if the decoder fails to initialize the hardware, it will call this function again, + # with only software formats in pix_fmts. We return ctx->sw_pix_fmt regardless in this case, + # because that should be in the candidate list. If not, we are out of ideas anyways. + cdef AVCodecPrivateData* private_data = ctx.opaque + i = 0 + while pix_fmts[i] != -1: + if pix_fmts[i] == private_data.hardware_pix_fmt: + return pix_fmts[i] + i += 1 + return ctx.sw_pix_fmt if private_data.allow_software_fallback else lib.AV_PIX_FMT_NONE + + cdef class VideoCodecContext(CodecContext): + def __cinit__(self, *args, **kwargs): self.last_w = 0 self.last_h = 0 - cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec): - CodecContext._init(self, ptr, codec) # TODO: Can this be `super`? + cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec, HWAccel hwaccel): + CodecContext._init(self, ptr, codec, hwaccel) # TODO: Can this be `super`? + + if hwaccel is not None: + try: + self.hwaccel_ctx = hwaccel.create(self.codec) + self.ptr.hw_device_ctx = lib.av_buffer_ref(self.hwaccel_ctx.ptr) + self.ptr.pix_fmt = self.hwaccel_ctx.config.ptr.pix_fmt + self.ptr.get_format = _get_hw_format + self._private_data.hardware_pix_fmt = self.hwaccel_ctx.config.ptr.pix_fmt + self._private_data.allow_software_fallback = self.hwaccel.allow_software_fallback + self.ptr.opaque = &self._private_data + except NotImplementedError: + # Some streams may not have a hardware decoder. For example, many action + # cam videos have a low resolution mjpeg stream, which is usually not + # compatible with hardware decoders. + # The user may have passed in a hwaccel because they want to decode the main + # stream with it, so we shouldn't abort even if we find a stream that can't + # be HW decoded. + # If the user wants to make sure hwaccel is actually used, they can check with the + # is_hardware_accelerated() function on each stream's codec context. + self.hwaccel_ctx = None + self._build_format() self.encoded_frame_count = 0 @@ -58,6 +98,26 @@ cdef class VideoCodecContext(CodecContext): cdef VideoFrame vframe = frame vframe._init_user_attributes() + cdef _transfer_hwframe(self, Frame frame): + if self.hwaccel_ctx is None: + return frame + + if frame.ptr.format != self.hwaccel_ctx.config.ptr.pix_fmt: + # If we get a software frame, that means we are in software fallback mode, and don't actually + # need to transfer. + return frame + + cdef Frame frame_sw + + frame_sw = self._alloc_next_frame() + + err_check(lib.av_hwframe_transfer_data(frame_sw.ptr, frame.ptr, 0)) + + # TODO: Is there anything else to transfer?! + frame_sw.pts = frame.pts + + return frame_sw + cdef _build_format(self): self._format = get_video_format(self.ptr.pix_fmt, self.ptr.width, self.ptr.height) diff --git a/examples/basics/hw_decode.py b/examples/basics/hw_decode.py new file mode 100644 index 000000000..1ce7a11af --- /dev/null +++ b/examples/basics/hw_decode.py @@ -0,0 +1,76 @@ +import os +import time + +import av +import av.datasets + +# What accelerator to use. +# Recommendations: +# Windows: +# - d3d11va (Direct3D 11) +# * available with built-in ffmpeg in PyAV binary wheels, and gives access to +# all decoders, but performance may not be as good as vendor native interfaces. +# - cuda (NVIDIA NVDEC), qsv (Intel QuickSync) +# * may be faster than d3d11va, but requires custom ffmpeg built with those libraries. +# Linux (all options require custom FFmpeg): +# - vaapi (Intel, AMD) +# - cuda (NVIDIA) +# Mac: +# - videotoolbox +# * available with built-in ffmpeg in PyAV binary wheels, and gives access to +# all accelerators available on Macs. This is the only option on MacOS. + +HW_DEVICE = os.environ["HW_DEVICE"] if "HW_DEVICE" in os.environ else None + +if "TEST_FILE_PATH" in os.environ: + test_file_path = os.environ["TEST_FILE_PATH"] +else: + test_file_path = av.datasets.curated( + "pexels/time-lapse-video-of-night-sky-857195.mp4" + ) + +if HW_DEVICE is None: + av.codec.hwaccel.dump_hwdevices() + print("Please set HW_DEVICE.") + exit() + +assert HW_DEVICE in av.codec.hwaccel.hwdevices_available, f"{HW_DEVICE} not available." + +print("Decoding in software (auto threading)...") + +container = av.open(test_file_path) + +container.streams.video[0].thread_type = "AUTO" + +start_time = time.time() +frame_count = 0 +for packet in container.demux(video=0): + for _ in packet.decode(): + frame_count += 1 + +sw_time = time.time() - start_time +sw_fps = frame_count / sw_time +assert frame_count == container.streams.video[0].frames +container.close() + +print(f"Decoded with software in {sw_time:.2f}s ({sw_fps:.2f} fps).") + +print(f"Decoding with {HW_DEVICE}") + +hwaccel = av.codec.hwaccel.HWAccel(device_type=HW_DEVICE, allow_software_fallback=False) + +# Note the additional argument here. +container = av.open(test_file_path, hwaccel=hwaccel) + +start_time = time.time() +frame_count = 0 +for packet in container.demux(video=0): + for _ in packet.decode(): + frame_count += 1 + +hw_time = time.time() - start_time +hw_fps = frame_count / hw_time +assert frame_count == container.streams.video[0].frames +container.close() + +print(f"Decoded with {HW_DEVICE} in {hw_time:.2f}s ({hw_fps:.2f} fps).") diff --git a/include/libav.pxd b/include/libav.pxd index c793b9988..e2fe323a4 100644 --- a/include/libav.pxd +++ b/include/libav.pxd @@ -4,11 +4,14 @@ include "libavutil/channel_layout.pxd" include "libavutil/dict.pxd" include "libavutil/error.pxd" include "libavutil/frame.pxd" +include "libavutil/hwcontext.pxd" include "libavutil/samplefmt.pxd" include "libavutil/motion_vector.pxd" include "libavcodec/avcodec.pxd" include "libavcodec/bsf.pxd" +include "libavcodec/hwaccel.pxd" + include "libavdevice/avdevice.pxd" include "libavformat/avformat.pxd" include "libswresample/swresample.pxd" diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 172c9cc65..bcb342373 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -213,6 +213,8 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVFrame* coded_frame + void* opaque + int bit_rate int bit_rate_tolerance int mb_decision @@ -247,6 +249,7 @@ cdef extern from "libavcodec/avcodec.h" nogil: int coded_height AVPixelFormat pix_fmt + AVPixelFormat sw_pix_fmt AVRational sample_aspect_ratio int gop_size # The number of pictures in a group of pictures, or 0 for intra_only. int max_b_frames @@ -266,6 +269,11 @@ cdef extern from "libavcodec/avcodec.h" nogil: int get_buffer(AVCodecContext *ctx, AVFrame *frame) void release_buffer(AVCodecContext *ctx, AVFrame *frame) + # Hardware acceleration + AVHWAccel *hwaccel + AVBufferRef *hw_device_ctx + AVPixelFormat (*get_format)(AVCodecContext *s, const AVPixelFormat *fmt) + # User Data void *opaque diff --git a/include/libavcodec/hwaccel.pxd b/include/libavcodec/hwaccel.pxd new file mode 100644 index 000000000..cb9ac41b6 --- /dev/null +++ b/include/libavcodec/hwaccel.pxd @@ -0,0 +1,19 @@ +cdef extern from "libavcodec/avcodec.h" nogil: + cdef enum: + AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX, + AV_CODEC_HW_CONFIG_METHOD_HW_FRAMES_CTX, + AV_CODEC_HW_CONFIG_METHOD_INTERNAL, + AV_CODEC_HW_CONFIG_METHOD_AD_HOC, + cdef struct AVCodecHWConfig: + AVPixelFormat pix_fmt + int methods + AVHWDeviceType device_type + cdef const AVCodecHWConfig* avcodec_get_hw_config(const AVCodec *codec, int index) + cdef enum: + AV_HWACCEL_CODEC_CAP_EXPERIMENTAL + cdef struct AVHWAccel: + char *name + AVMediaType type + AVCodecID id + AVPixelFormat pix_fmt + int capabilities diff --git a/include/libavutil/buffer.pxd b/include/libavutil/buffer.pxd index daf86105b..d4ff4cd17 100644 --- a/include/libavutil/buffer.pxd +++ b/include/libavutil/buffer.pxd @@ -1,9 +1,18 @@ -from libc.stdint cimport uint8_t +from libc.stdint cimport intptr_t, uint8_t cdef extern from "libavutil/buffer.h" nogil: - AVBufferRef *av_buffer_create(uint8_t *data, size_t size, void (*free)(void *opaque, uint8_t *data), void *opaque, int flags) + AVBufferRef* av_buffer_ref(AVBufferRef *buf) void av_buffer_unref(AVBufferRef **buf) + cdef struct AVBuffer: + uint8_t *data + int size + intptr_t refcount + void (*free)(void *opaque, uint8_t *data) + void *opaque + int flags cdef struct AVBufferRef: + AVBuffer *buffer uint8_t *data + int size diff --git a/include/libavutil/hwcontext.pxd b/include/libavutil/hwcontext.pxd new file mode 100644 index 000000000..beda15a2c --- /dev/null +++ b/include/libavutil/hwcontext.pxd @@ -0,0 +1,24 @@ +cdef extern from "libavutil/hwcontext.h" nogil: + + enum AVHWDeviceType: + AV_HWDEVICE_TYPE_NONE + AV_HWDEVICE_TYPE_VDPAU + AV_HWDEVICE_TYPE_CUDA + AV_HWDEVICE_TYPE_VAAPI + AV_HWDEVICE_TYPE_DXVA2 + AV_HWDEVICE_TYPE_QSV + AV_HWDEVICE_TYPE_VIDEOTOOLBOX + AV_HWDEVICE_TYPE_D3D11VA + AV_HWDEVICE_TYPE_DRM + AV_HWDEVICE_TYPE_OPENCL + AV_HWDEVICE_TYPE_MEDIACODEC + AV_HWDEVICE_TYPE_VULKAN + AV_HWDEVICE_TYPE_D3D12VA + + cdef int av_hwdevice_ctx_create(AVBufferRef **device_ctx, AVHWDeviceType type, const char *device, AVDictionary *opts, int flags) + + cdef AVHWDeviceType av_hwdevice_find_type_by_name(const char *name) + cdef const char *av_hwdevice_get_type_name(AVHWDeviceType type) + cdef AVHWDeviceType av_hwdevice_iterate_types(AVHWDeviceType prev) + + cdef int av_hwframe_transfer_data(AVFrame *dst, const AVFrame *src, int flags) diff --git a/scripts/build-deps b/scripts/build-deps index 4cb90f074..de4a6e547 100755 --- a/scripts/build-deps +++ b/scripts/build-deps @@ -13,6 +13,31 @@ if [[ -e "$PYAV_LIBRARY_PREFIX/bin/ffmpeg" ]]; then exit 0 fi +# Add CUDA support if available +CONFFLAGS_NVIDIA="" +if [[ -e /usr/local/cuda ]]; then + # Get Nvidia headers for ffmpeg + cd $PYAV_LIBRARY_ROOT + if [[ ! -e "$PYAV_LIBRARY_ROOT/nv-codec-headers" ]]; then + git clone https://github.com/FFmpeg/nv-codec-headers.git + cd nv-codec-headers + make -j4 + make PREFIX="$PYAV_LIBRARY_PREFIX" install + fi + + PKG_CONFIG_PATH="/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH" + CONFFLAGS_NVIDIA="--enable-cuda \ + --enable-cuvid \ + --enable-nvenc \ + --enable-nonfree \ + --enable-libnpp \ + --extra-cflags=-I/usr/local/cuda/include \ + --extra-ldflags=-L/usr/local/cuda/lib64" +else + echo "WARNING: Did not find cuda libraries in /usr/local/cuda..." + echo " Building without NVIDIA NVENC/NVDEC support" +fi + mkdir -p "$PYAV_LIBRARY_ROOT" mkdir -p "$PYAV_LIBRARY_PREFIX" @@ -44,6 +69,7 @@ echo ./configure --enable-sse \ --enable-avx \ --enable-avx2 \ + $CONFFLAGS_NVIDIA \ --prefix="$PYAV_LIBRARY_PREFIX" \ || exit 2 echo diff --git a/tests/test_decode.py b/tests/test_decode.py index 05f636977..fc293d201 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -1,10 +1,47 @@ +import functools +import os +import pathlib from fractions import Fraction +import numpy as np +import pytest + import av from .common import TestCase, fate_suite +@functools.cache +def make_h264_test_video(path: str) -> None: + """Generates a black H264 test video for testing hardware decoding.""" + + # We generate a file here that's designed to be as compatible as possible with hardware + # encoders. Hardware encoders are sometimes very picky and the errors we get are often + # opaque, so there is nothing much we (PyAV) can do. The user needs to figure that out + # if they want to use hwaccel. We only want to test the PyAV plumbing here. + # Our video is H264, 1280x720p (note that some decoders have a minimum resolution limit), 24fps, + # 8-bit yuv420p. + pathlib.Path(path).parent.mkdir(parents=True, exist_ok=True) + output_container = av.open(path, "w") + stream = output_container.add_stream("libx264", rate=24) + assert isinstance(stream, av.VideoStream) + stream.width = 1280 + stream.height = 720 + stream.pix_fmt = "yuv420p" + + for _ in range(24): + frame = av.VideoFrame.from_ndarray( + np.zeros((720, 1280, 3), dtype=np.uint8), format="rgb24" + ) + for packet in stream.encode(frame): + output_container.mux(packet) + + for packet in stream.encode(): + output_container.mux(packet) + + output_container.close() + + class TestDecode(TestCase): def test_decoded_video_frame_count(self) -> None: container = av.open(fate_suite("h264/interlaced_crop.mp4")) @@ -165,3 +202,33 @@ def test_side_data(self) -> None: container = av.open(fate_suite("mov/displaymatrix.mov")) frame = next(container.decode(video=0)) assert frame.rotation == -90 + + def test_hardware_decode(self) -> None: + hwdevices_available = av.codec.hwaccel.hwdevices_available() + if "HWACCEL_DEVICE_TYPE" not in os.environ: + pytest.skip( + "Set the HWACCEL_DEVICE_TYPE to run this test. " + f"Options are {' '.join(hwdevices_available)}" + ) + + HWACCEL_DEVICE_TYPE = os.environ["HWACCEL_DEVICE_TYPE"] + assert ( + HWACCEL_DEVICE_TYPE in hwdevices_available + ), f"{HWACCEL_DEVICE_TYPE} not available" + + test_video_path = "tests/assets/black.mp4" + make_h264_test_video(test_video_path) + + hwaccel = av.codec.hwaccel.HWAccel( + device_type=HWACCEL_DEVICE_TYPE, allow_software_fallback=False + ) + + container = av.open(test_video_path, hwaccel=hwaccel) + video_stream = container.streams.video[0] + assert video_stream.codec_context.is_hwaccel + + frame_count = 0 + for frame in container.decode(video_stream): + frame_count += 1 + + assert frame_count == video_stream.frames From a4854a312996e87b6728caa940e2599af3f14d86 Mon Sep 17 00:00:00 2001 From: Lukas Geiger Date: Tue, 17 Dec 2024 12:27:38 +0000 Subject: [PATCH 195/365] Update CHANGELOG.rst --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e65107428..eed7b2528 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -23,6 +23,7 @@ v14.1.0 (Unreleased) Features - Add hardware decoding by :gh-user:`matthewlai` and :gh-user:`WyattBlue` in (:pr:`1685`). +- Add ``VideoFrame.rotation`` by :gh-user:`lgeiger` in (:pr:`1675`). v14.0.1 From af30b3d02b76f3b56b4eb5f3c1f38746d3838ea5 Mon Sep 17 00:00:00 2001 From: Matthew Lai Date: Tue, 24 Dec 2024 14:17:52 +0800 Subject: [PATCH 196/365] Fixed HWAccel so we don't share contexts between streams --- av/codec/hwaccel.pyx | 26 +++++++++++++++----------- av/container/input.pyx | 7 +++++++ av/video/codeccontext.pyx | 2 +- examples/basics/hw_decode.py | 15 ++++++++------- tests/test_decode.py | 26 ++++++++++++++++---------- 5 files changed, 47 insertions(+), 29 deletions(-) diff --git a/av/codec/hwaccel.pyx b/av/codec/hwaccel.pyx index 1c96d02e8..b80c194af 100644 --- a/av/codec/hwaccel.pyx +++ b/av/codec/hwaccel.pyx @@ -7,6 +7,7 @@ from av.codec.codec cimport Codec from av.dictionary cimport _Dictionary from av.error cimport err_check from av.video.format cimport get_video_format + from av.dictionary import Dictionary @@ -94,11 +95,13 @@ cpdef hwdevices_available(): cdef class HWAccel: - def __init__(self, device_type, device=None, codec=None, allow_software_fallback=True, options=None): + def __init__(self, device_type, device=None, allow_software_fallback=True, options=None): if isinstance(device_type, HWDeviceType): self._device_type = device_type elif isinstance(device_type, str): self._device_type = int(lib.av_hwdevice_find_type_by_name(device_type)) + elif isinstance(device_type, int): + self._device_type = device_type else: raise ValueError("Unknown type for device_type") @@ -106,22 +109,18 @@ cdef class HWAccel: self.allow_software_fallback = allow_software_fallback self.options = {} if not options else dict(options) self.ptr = NULL - self.codec = codec self.config = None - if codec: - self._initialize_hw_context() - - def _initialize_hw_context(self): + def _initialize_hw_context(self, Codec codec not None): cdef HWConfig config - for config in self.codec.hardware_configs: + for config in codec.hardware_configs: if not (config.ptr.methods & lib.AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX): continue if self._device_type and config.device_type != self._device_type: continue break else: - raise NotImplementedError(f"No supported hardware config for {self.codec}") + raise NotImplementedError(f"No supported hardware config for {codec}") self.config = config @@ -142,9 +141,14 @@ cdef class HWAccel: if self.ptr: raise RuntimeError("Hardware context already initialized") - self.codec = codec - self._initialize_hw_context() - return self + ret = HWAccel( + device_type=self._device_type, + device=self._device, + allow_software_fallback=self.allow_software_fallback, + options=self.options + ) + ret._initialize_hw_context(codec) + return ret def __dealloc__(self): if self.ptr: diff --git a/av/container/input.pyx b/av/container/input.pyx index aa9940452..1ba4750d7 100644 --- a/av/container/input.pyx +++ b/av/container/input.pyx @@ -68,6 +68,8 @@ cdef class InputContainer(Container): lib.av_dict_free(&c_options[i]) free(c_options) + at_least_one_accelerated_context = False + self.streams = StreamContainer() for i in range(self.ptr.nb_streams): stream = self.ptr.streams[i] @@ -78,11 +80,16 @@ cdef class InputContainer(Container): err_check(lib.avcodec_parameters_to_context(codec_context, stream.codecpar)) codec_context.pkt_timebase = stream.time_base py_codec_context = wrap_codec_context(codec_context, codec, self.hwaccel) + if py_codec_context.is_hwaccel: + at_least_one_accelerated_context = True else: # no decoder is available py_codec_context = None self.streams.add_stream(wrap_stream(self, stream, py_codec_context)) + if self.hwaccel and not self.hwaccel.allow_software_fallback and not at_least_one_accelerated_context: + raise RuntimeError("Hardware accelerated decode requested but no stream is compatible") + self.metadata = avdict_to_dict(self.ptr.metadata, self.metadata_encoding, self.metadata_errors) def __dealloc__(self): diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.pyx index 92470c159..c9d8eb4c0 100644 --- a/av/video/codeccontext.pyx +++ b/av/video/codeccontext.pyx @@ -54,7 +54,7 @@ cdef class VideoCodecContext(CodecContext): # stream with it, so we shouldn't abort even if we find a stream that can't # be HW decoded. # If the user wants to make sure hwaccel is actually used, they can check with the - # is_hardware_accelerated() function on each stream's codec context. + # is_hwaccel() function on each stream's codec context. self.hwaccel_ctx = None self._build_format() diff --git a/examples/basics/hw_decode.py b/examples/basics/hw_decode.py index 1ce7a11af..605ee1841 100644 --- a/examples/basics/hw_decode.py +++ b/examples/basics/hw_decode.py @@ -3,6 +3,7 @@ import av import av.datasets +from av.codec.hwaccel import HWAccel, hwdevices_available # What accelerator to use. # Recommendations: @@ -30,11 +31,10 @@ ) if HW_DEVICE is None: - av.codec.hwaccel.dump_hwdevices() - print("Please set HW_DEVICE.") + print(f"Please set HW_DEVICE. Options are: {hwdevices_available()}") exit() -assert HW_DEVICE in av.codec.hwaccel.hwdevices_available, f"{HW_DEVICE} not available." +assert HW_DEVICE in hwdevices_available(), f"{HW_DEVICE} not available." print("Decoding in software (auto threading)...") @@ -53,11 +53,12 @@ assert frame_count == container.streams.video[0].frames container.close() -print(f"Decoded with software in {sw_time:.2f}s ({sw_fps:.2f} fps).") +print( + f"Decoded with software in {sw_time:.2f}s ({sw_fps:.2f} fps).\n" + f"Decoding with {HW_DEVICE}" +) -print(f"Decoding with {HW_DEVICE}") - -hwaccel = av.codec.hwaccel.HWAccel(device_type=HW_DEVICE, allow_software_fallback=False) +hwaccel = HWAccel(device_type=HW_DEVICE, allow_software_fallback=False) # Note the additional argument here. container = av.open(test_file_path, hwaccel=hwaccel) diff --git a/tests/test_decode.py b/tests/test_decode.py index fc293d201..c1846af69 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -13,7 +13,7 @@ @functools.cache def make_h264_test_video(path: str) -> None: - """Generates a black H264 test video for testing hardware decoding.""" + """Generates a black H264 test video with two streams for testing hardware decoding.""" # We generate a file here that's designed to be as compatible as possible with hardware # encoders. Hardware encoders are sometimes very picky and the errors we get are often @@ -23,21 +23,27 @@ def make_h264_test_video(path: str) -> None: # 8-bit yuv420p. pathlib.Path(path).parent.mkdir(parents=True, exist_ok=True) output_container = av.open(path, "w") - stream = output_container.add_stream("libx264", rate=24) - assert isinstance(stream, av.VideoStream) - stream.width = 1280 - stream.height = 720 - stream.pix_fmt = "yuv420p" + + streams = [] + for _ in range(2): + stream = output_container.add_stream("libx264", rate=24) + assert isinstance(stream, av.VideoStream) + stream.width = 1280 + stream.height = 720 + stream.pix_fmt = "yuv420p" + streams.append(stream) for _ in range(24): frame = av.VideoFrame.from_ndarray( np.zeros((720, 1280, 3), dtype=np.uint8), format="rgb24" ) - for packet in stream.encode(frame): - output_container.mux(packet) + for stream in streams: + for packet in stream.encode(frame): + output_container.mux(packet) - for packet in stream.encode(): - output_container.mux(packet) + for stream in streams: + for packet in stream.encode(): + output_container.mux(packet) output_container.close() From f3c7c3dd7288bca2926df94a23baf721dd238b34 Mon Sep 17 00:00:00 2001 From: Matthew Lai Date: Wed, 25 Dec 2024 21:16:18 +0800 Subject: [PATCH 197/365] Removed outdated comment --- av/video/reformatter.pyx | 3 --- 1 file changed, 3 deletions(-) diff --git a/av/video/reformatter.pyx b/av/video/reformatter.pyx index 538cc4953..a0c576d12 100644 --- a/av/video/reformatter.pyx +++ b/av/video/reformatter.pyx @@ -139,9 +139,6 @@ cdef class VideoReformatter: ): return frame - # Try and reuse existing SwsContextProxy - # VideoStream.decode will copy its SwsContextProxy to VideoFrame - # So all Video frames from the same VideoStream should have the same one with nogil: self.ptr = lib.sws_getCachedContext( self.ptr, From 8bec947774b6cca4193f0f48f8df93cc131d4895 Mon Sep 17 00:00:00 2001 From: Joe Schiff <41972063+JoeSchiff@users.noreply.github.com> Date: Thu, 26 Dec 2024 18:32:13 -0500 Subject: [PATCH 198/365] Remove tests/requirements.txt mention --- docs/overview/installation.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/overview/installation.rst b/docs/overview/installation.rst index 419592530..cf1364c1a 100644 --- a/docs/overview/installation.rst +++ b/docs/overview/installation.rst @@ -90,9 +90,6 @@ Building from the latest source # Prep a virtualenv. source scripts/activate.sh - # Install basic requirements. - pip install -r tests/requirements.txt - # Optionally build FFmpeg. ./scripts/build-deps From 892c9990673c472e821865ed54ee118ba6723bc3 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 28 Dec 2024 14:59:46 -0500 Subject: [PATCH 199/365] Mark pointer as const --- av/codec/codec.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/codec/codec.pyx b/av/codec/codec.pyx index 2dab166ab..73f085e8d 100644 --- a/av/codec/codec.pyx +++ b/av/codec/codec.pyx @@ -214,7 +214,7 @@ cdef class Codec: return self._hardware_configs ret = [] cdef int i = 0 - cdef lib.AVCodecHWConfig *ptr + cdef const lib.AVCodecHWConfig *ptr while True: ptr = lib.avcodec_get_hw_config(self.ptr, i) if not ptr: From 355f1b47cc78467e1907fb7d1305f081496875df Mon Sep 17 00:00:00 2001 From: Kim Minjong Date: Thu, 9 Jan 2025 14:25:48 +0900 Subject: [PATCH 200/365] Change the raise condition in resampler --- av/audio/resampler.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/audio/resampler.pyx b/av/audio/resampler.pyx index c46ed8475..69d790bad 100644 --- a/av/audio/resampler.pyx +++ b/av/audio/resampler.pyx @@ -95,7 +95,7 @@ cdef class AudioResampler: if self.frame_size > 0: self.graph.set_audio_frame_size(self.frame_size) - elif frame is not None: + if frame is not None: if ( frame.format.sample_fmt != self.template.format.sample_fmt or frame.layout != self.template.layout or From f659cfc0884fda59174759e686910bc45f23cbb8 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 9 Jan 2025 13:36:44 -0500 Subject: [PATCH 201/365] Test with pypy3.10 --- .github/workflows/smoke.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 352634ff6..e4bd3af4b 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -36,7 +36,7 @@ jobs: config: - {os: ubuntu-latest, python: "3.12", ffmpeg: "7.1", extras: true} - {os: ubuntu-latest, python: "3.9", ffmpeg: "7.0.2"} - - {os: ubuntu-latest, python: pypy3.9, ffmpeg: "7.1"} + - {os: ubuntu-latest, python: "pypy3.10", ffmpeg: "7.1"} - {os: macos-14, python: "3.9", ffmpeg: "7.1"} - {os: macos-14, python: "3.9", ffmpeg: "7.0.2"} From d52757109526988cddfa78a39ace15830b92f772 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 11 Jan 2025 15:01:34 -0500 Subject: [PATCH 202/365] Use new template function --- examples/basics/remux.py | 2 +- examples/subtitles/remux.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/basics/remux.py b/examples/basics/remux.py index 5abb199b8..64ac00c99 100644 --- a/examples/basics/remux.py +++ b/examples/basics/remux.py @@ -9,7 +9,7 @@ # Make an output stream using the input as a template. This copies the stream # setup from one to the other. in_stream = input_.streams.video[0] -out_stream = output.add_stream(template=in_stream) +out_stream = output.add_stream_from_template(in_stream) for packet in input_.demux(in_stream): print(packet) diff --git a/examples/subtitles/remux.py b/examples/subtitles/remux.py index 5b3031788..32ee7d6d3 100644 --- a/examples/subtitles/remux.py +++ b/examples/subtitles/remux.py @@ -6,7 +6,7 @@ output = av.open("remuxed.vtt", "w") in_stream = input_.streams.subtitles[0] -out_stream = output.add_stream(template=in_stream) +out_stream = output.add_stream_from_template(in_stream) for packet in input_.demux(in_stream): if packet.dts is None: From 6bee7992d2919f2d6a43e5036c09f4b6cb00383e Mon Sep 17 00:00:00 2001 From: robinechuca Date: Wed, 15 Jan 2025 18:13:43 +0100 Subject: [PATCH 203/365] Support grayf32le and gbrapf32le in numpy convertion --- CHANGELOG.rst | 1 + av/video/frame.pyx | 63 ++++++++++++++++++++++++++++++---------- tests/test_videoframe.py | 36 +++++++++++++++++++++++ 3 files changed, 84 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index eed7b2528..7baadb4a8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -24,6 +24,7 @@ Features - Add hardware decoding by :gh-user:`matthewlai` and :gh-user:`WyattBlue` in (:pr:`1685`). - Add ``VideoFrame.rotation`` by :gh-user:`lgeiger` in (:pr:`1675`). +- Support grayf32le and gbrapf32le in numpy convertion by :gh-user:`robinechuca` in (:pr:`1712`). v14.0.1 diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 02cde3187..6e4a1dbdf 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -295,6 +295,8 @@ cdef class VideoFrame(Frame): .. note:: For ``pal8``, an ``(image, palette)`` tuple will be returned, with the palette being in ARGB (PyAV will swap bytes if needed). + .. note:: For ``gbrp`` formats, channels are flipped to RGB order. + """ cdef VideoFrame frame = self.reformat(**kwargs) @@ -312,7 +314,7 @@ cdef class VideoFrame(Frame): return np.hstack(( useful_array(frame.planes[0]), useful_array(frame.planes[1]), - useful_array(frame.planes[2]) + useful_array(frame.planes[2]), )).reshape(-1, frame.height, frame.width) elif frame.format.name == "yuyv422": assert frame.width % 2 == 0 @@ -320,21 +322,28 @@ cdef class VideoFrame(Frame): return useful_array(frame.planes[0], 2).reshape(frame.height, frame.width, -1) elif frame.format.name == "gbrp": array = np.empty((frame.height, frame.width, 3), dtype="uint8") - array[:, :, 0] = useful_array(frame.planes[2], 1).reshape(-1, frame.width) - array[:, :, 1] = useful_array(frame.planes[0], 1).reshape(-1, frame.width) - array[:, :, 2] = useful_array(frame.planes[1], 1).reshape(-1, frame.width) + array[:, :, 0] = useful_array(frame.planes[2], 1).reshape(frame.height, frame.width) + array[:, :, 1] = useful_array(frame.planes[0], 1).reshape(frame.height, frame.width) + array[:, :, 2] = useful_array(frame.planes[1], 1).reshape(frame.height, frame.width) return array elif frame.format.name in ("gbrp10be", "gbrp12be", "gbrp14be", "gbrp16be", "gbrp10le", "gbrp12le", "gbrp14le", "gbrp16le"): array = np.empty((frame.height, frame.width, 3), dtype="uint16") - array[:, :, 0] = useful_array(frame.planes[2], 2, "uint16").reshape(-1, frame.width) - array[:, :, 1] = useful_array(frame.planes[0], 2, "uint16").reshape(-1, frame.width) - array[:, :, 2] = useful_array(frame.planes[1], 2, "uint16").reshape(-1, frame.width) + array[:, :, 0] = useful_array(frame.planes[2], 2, "uint16").reshape(frame.height, frame.width) + array[:, :, 1] = useful_array(frame.planes[0], 2, "uint16").reshape(frame.height, frame.width) + array[:, :, 2] = useful_array(frame.planes[1], 2, "uint16").reshape(frame.height, frame.width) return byteswap_array(array, frame.format.name.endswith("be")) elif frame.format.name in ("gbrpf32be", "gbrpf32le"): array = np.empty((frame.height, frame.width, 3), dtype="float32") - array[:, :, 0] = useful_array(frame.planes[2], 4, "float32").reshape(-1, frame.width) - array[:, :, 1] = useful_array(frame.planes[0], 4, "float32").reshape(-1, frame.width) - array[:, :, 2] = useful_array(frame.planes[1], 4, "float32").reshape(-1, frame.width) + array[:, :, 0] = useful_array(frame.planes[2], 4, "float32").reshape(frame.height, frame.width) + array[:, :, 1] = useful_array(frame.planes[0], 4, "float32").reshape(frame.height, frame.width) + array[:, :, 2] = useful_array(frame.planes[1], 4, "float32").reshape(frame.height, frame.width) + return byteswap_array(array, frame.format.name.endswith("be")) + elif frame.format.name in ("gbrapf32be", "gbrapf32le"): + array = np.empty((frame.height, frame.width, 4), dtype="float32") + array[:, :, 0] = useful_array(frame.planes[2], 4, "float32").reshape(frame.height, frame.width) + array[:, :, 1] = useful_array(frame.planes[0], 4, "float32").reshape(frame.height, frame.width) + array[:, :, 2] = useful_array(frame.planes[1], 4, "float32").reshape(frame.height, frame.width) + array[:, :, 3] = useful_array(frame.planes[3], 4, "float32").reshape(frame.height, frame.width) return byteswap_array(array, frame.format.name.endswith("be")) elif frame.format.name in ("rgb24", "bgr24"): return useful_array(frame.planes[0], 3).reshape(frame.height, frame.width, -1) @@ -345,17 +354,22 @@ cdef class VideoFrame(Frame): elif frame.format.name in ("gray16be", "gray16le"): return byteswap_array( useful_array(frame.planes[0], 2, "uint16").reshape(frame.height, frame.width), - frame.format.name == "gray16be", + frame.format.name.endswith("be"), + ) + elif frame.format.name in ("grayf32be", "grayf32le"): + return byteswap_array( + useful_array(frame.planes[0], 4, "float32").reshape(frame.height, frame.width), + frame.format.name.endswith("be"), ) elif frame.format.name in ("rgb48be", "rgb48le"): return byteswap_array( useful_array(frame.planes[0], 6, "uint16").reshape(frame.height, frame.width, -1), - frame.format.name == "rgb48be", + frame.format.name.endswith("be"), ) elif frame.format.name in ("rgba64be", "rgba64le"): return byteswap_array( useful_array(frame.planes[0], 8, "uint16").reshape(frame.height, frame.width, -1), - frame.format.name == "rgba64be", + frame.format.name.endswith("be"), ) elif frame.format.name == "pal8": image = useful_array(frame.planes[0]).reshape(frame.height, frame.width) @@ -491,6 +505,8 @@ cdef class VideoFrame(Frame): must be in the system's native byte order. .. note:: for ``pal8``, an ``(image, palette)`` pair must be passed. `palette` must have shape (256, 4) and is given in ARGB format (PyAV will swap bytes if needed). + + .. note:: for ``gbrp`` formats, channels are assumed to be given in RGB order. """ if format == "pal8": array, palette = array @@ -568,19 +584,34 @@ cdef class VideoFrame(Frame): elif format in ("gray16be", "gray16le"): check_ndarray(array, "uint16", 2) frame = VideoFrame(array.shape[1], array.shape[0], format) - copy_array_to_plane(byteswap_array(array, format == "gray16be"), frame.planes[0], 2) + copy_array_to_plane(byteswap_array(array, format.endswith("be")), frame.planes[0], 2) + return frame + elif format in ("grayf32be", "grayf32le"): + check_ndarray(array, "float32", 2) + frame = VideoFrame(array.shape[1], array.shape[0], format) + copy_array_to_plane(byteswap_array(array, format.endswith("be")), frame.planes[0], 4) return frame elif format in ("rgb48be", "rgb48le"): check_ndarray(array, "uint16", 3) check_ndarray_shape(array, array.shape[2] == 3) frame = VideoFrame(array.shape[1], array.shape[0], format) - copy_array_to_plane(byteswap_array(array, format == "rgb48be"), frame.planes[0], 6) + copy_array_to_plane(byteswap_array(array, format.endswith("be")), frame.planes[0], 6) return frame elif format in ("rgba64be", "rgba64le"): check_ndarray(array, "uint16", 3) check_ndarray_shape(array, array.shape[2] == 4) frame = VideoFrame(array.shape[1], array.shape[0], format) - copy_array_to_plane(byteswap_array(array, format == "rgba64be"), frame.planes[0], 8) + copy_array_to_plane(byteswap_array(array, format.endswith("be")), frame.planes[0], 8) + return frame + elif format in ("gbrapf32be", "gbrapf32le"): + check_ndarray(array, "float32", 3) + check_ndarray_shape(array, array.shape[2] == 4) + + frame = VideoFrame(array.shape[1], array.shape[0], format) + copy_array_to_plane(byteswap_array(array[:, :, 1], format.endswith("be")), frame.planes[0], 4) + copy_array_to_plane(byteswap_array(array[:, :, 2], format.endswith("be")), frame.planes[1], 4) + copy_array_to_plane(byteswap_array(array[:, :, 0], format.endswith("be")), frame.planes[2], 4) + copy_array_to_plane(byteswap_array(array[:, :, 3], format.endswith("be")), frame.planes[3], 4) return frame elif format == "nv12": check_ndarray(array, "uint8", 2) diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index f044be949..250641676 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -223,6 +223,24 @@ def test_ndarray_gray_align() -> None: assertNdarraysEqual(frame.to_ndarray(), array) +def test_ndarray_grayf32() -> None: + array = numpy.random.random_sample(size=(480, 640)).astype(numpy.float32) + for format in ("grayf32be", "grayf32le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_grayf32_align() -> None: + array = numpy.random.random_sample(size=(238, 318)).astype(numpy.float32) + for format in ("grayf32be", "grayf32le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + def test_ndarray_rgb() -> None: array = numpy.random.randint(0, 256, size=(480, 640, 3), dtype=numpy.uint8) for format in ("rgb24", "bgr24"): @@ -365,6 +383,24 @@ def test_ndarray_gbrpf32_align() -> None: assertNdarraysEqual(frame.to_ndarray(), array) +def test_ndarray_gbrapf32() -> None: + array = numpy.random.random_sample(size=(480, 640, 4)).astype(numpy.float32) + for format in ("gbrapf32be", "gbrapf32le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrapf32_allign() -> None: + array = numpy.random.random_sample(size=(238, 318, 4)).astype(numpy.float32) + for format in ("gbrapf32be", "gbrapf32le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + def test_ndarray_yuv420p() -> None: array = numpy.random.randint(0, 256, size=(720, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuv420p") From c216de336b132de8ec139b3222513bf11a100198 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 16 Jan 2025 12:44:59 -0500 Subject: [PATCH 204/365] Change copyright year --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 59090193c..789fc0756 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -43,7 +43,7 @@ # General information about the project. project = "PyAV" -copyright = "2024, The PyAV Team" +copyright = "2025, The PyAV Team" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the From 1c7160528a06ce813027e41cc274e2875f4d6bd5 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 18 Jan 2025 23:13:07 -0500 Subject: [PATCH 205/365] Add `stream.disposition` and enum --- CHANGELOG.rst | 1 + av/stream.pyi | 23 +++++++++++++++++++++++ av/stream.pyx | 31 ++++++++++++++++++++++++++++++- include/libav.pxd | 1 - include/libavcodec/avcodec.pxd | 14 ++++++++++++++ include/libavformat/avformat.pxd | 4 +--- include/libavutil/avutil.pxd | 1 + tests/test_colorspace.py | 2 ++ 8 files changed, 72 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7baadb4a8..2f0bd681c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -23,6 +23,7 @@ v14.1.0 (Unreleased) Features - Add hardware decoding by :gh-user:`matthewlai` and :gh-user:`WyattBlue` in (:pr:`1685`). +- Add ``Stream.disposition`` and ``Disposition`` enum by :gh-user:`WyattBlue` in (:pr:`1720`). - Add ``VideoFrame.rotation`` by :gh-user:`lgeiger` in (:pr:`1675`). - Support grayf32le and gbrapf32le in numpy convertion by :gh-user:`robinechuca` in (:pr:`1712`). diff --git a/av/stream.pyi b/av/stream.pyi index 82bb672b2..a2a2e439c 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -1,9 +1,31 @@ +from enum import Flag from fractions import Fraction from typing import Literal from .codec import Codec, CodecContext from .container import Container +class Disposition(Flag): + default: int + dub: int + original: int + comment: int + lyrics: int + karaoke: int + forced: int + hearing_impaired: int + visual_impaired: int + clean_effects: int + attached_pic: int + timed_thumbnails: int + non_diegetic: int + captions: int + descriptions: int + metadata: int + dependent: int + still_image: int + multilayer: int + class Stream: name: str | None container: Container @@ -20,6 +42,7 @@ class Stream: guessed_rate: Fraction | None start_time: int | None duration: int | None + disposition: Disposition frames: int language: str | None type: Literal["video", "audio", "data", "subtitle", "attachment"] diff --git a/av/stream.pyx b/av/stream.pyx index 35b85acdf..d0ecf37ad 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -1,6 +1,6 @@ cimport libav as lib -from enum import Enum +from enum import Flag from av.error cimport err_check from av.packet cimport Packet @@ -12,6 +12,28 @@ from av.utils cimport ( ) +class Disposition(Flag): + default = 1 << 0 + dub = 1 << 1 + original = 1 << 2 + comment = 1 << 3 + lyrics = 1 << 4 + karaoke = 1 << 5 + forced = 1 << 6 + hearing_impaired = 1 << 7 + visual_impaired = 1 << 8 + clean_effects = 1 << 9 + attached_pic = 1 << 10 + timed_thumbnails = 1 << 11 + non_diegetic = 1 << 12 + captions = 1 << 16 + descriptions = 1 << 17 + metadata = 1 << 18 + dependent = 1 << 19 + still_image = 1 << 20 + multilayer = 1 << 21 + + cdef object _cinit_bypass_sentinel = object() cdef Stream wrap_stream(Container container, lib.AVStream *c_stream, CodecContext codec_context): @@ -96,6 +118,9 @@ cdef class Stream: if name == "id": self._set_id(value) return + if name == "disposition": + self.ptr.disposition = value + return # Convenience setter for codec context properties. if self.codec_context is not None: @@ -230,6 +255,10 @@ cdef class Stream: """ return self.metadata.get("language") + @property + def disposition(self): + return Disposition(self.ptr.disposition) + @property def type(self): """ diff --git a/include/libav.pxd b/include/libav.pxd index e2fe323a4..568913208 100644 --- a/include/libav.pxd +++ b/include/libav.pxd @@ -24,6 +24,5 @@ include "libavfilter/buffersrc.pxd" cdef extern from "stdio.h" nogil: - cdef int snprintf(char *output, int n, const char *format, ...) cdef int vsnprintf(char *output, int n, const char *format, va_list args) diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index bcb342373..ee832dd04 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -8,6 +8,16 @@ cdef extern from "libavcodec/codec_id.h": AVCodecID av_codec_get_id(const AVCodecTag *const *tags, uint32_t tag) +cdef extern from "libavcodec/packet.h" nogil: + AVPacketSideData* av_packet_side_data_new( + AVPacketSideData **sides, + int *nb_sides, + AVPacketSideDataType type, + size_t size, + int free_opaque + ) + + cdef extern from "libavutil/channel_layout.h": ctypedef enum AVChannelOrder: AV_CHANNEL_ORDER_UNSPEC @@ -542,6 +552,10 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef struct AVCodecParameters: AVMediaType codec_type AVCodecID codec_id + AVPacketSideData *coded_side_data + int nb_coded_side_data + uint8_t *extradata + int extradata_size cdef int avcodec_parameters_copy( AVCodecParameters *dst, diff --git a/include/libavformat/avformat.pxd b/include/libavformat/avformat.pxd index 5fa25043a..cec89bd38 100644 --- a/include/libavformat/avformat.pxd +++ b/include/libavformat/avformat.pxd @@ -30,18 +30,16 @@ cdef extern from "libavformat/avformat.h" nogil: cdef struct AVStream: int index int id + int disposition AVCodecParameters *codecpar - AVRational time_base int64_t start_time int64_t duration int64_t nb_frames int64_t cur_dts - AVDictionary *metadata - AVRational avg_frame_rate AVRational r_frame_rate AVRational sample_aspect_ratio diff --git a/include/libavutil/avutil.pxd b/include/libavutil/avutil.pxd index 58dd43922..49be65f69 100644 --- a/include/libavutil/avutil.pxd +++ b/include/libavutil/avutil.pxd @@ -30,6 +30,7 @@ cdef extern from "libavutil/avutil.h" nogil: cdef enum AVPixelFormat: AV_PIX_FMT_NONE AV_PIX_FMT_YUV420P + AV_PIX_FMT_RGBA AV_PIX_FMT_RGB24 PIX_FMT_RGB24 PIX_FMT_RGBA diff --git a/tests/test_colorspace.py b/tests/test_colorspace.py index 7574c147d..c76416c80 100644 --- a/tests/test_colorspace.py +++ b/tests/test_colorspace.py @@ -31,6 +31,8 @@ def test_sky_timelapse() -> None: ) stream = container.streams.video[0] + assert stream.disposition == av.stream.Disposition.default + assert stream.codec_context.color_range == 1 assert stream.codec_context.color_range == ColorRange.MPEG assert stream.codec_context.color_primaries == 1 From fdcb032ddbd898e6b6c59b098f9ef69988f5c54a Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 20 Jan 2025 12:22:13 -0500 Subject: [PATCH 206/365] Use new ffmpeg build --- scripts/ffmpeg-7.1.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ffmpeg-7.1.json b/scripts/ffmpeg-7.1.json index 06e80636a..a9fdf419d 100644 --- a/scripts/ffmpeg-7.1.json +++ b/scripts/ffmpeg-7.1.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.1-1/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.1-3/ffmpeg-{platform}.tar.gz" } \ No newline at end of file From c63aedeb3f6bf1afa57d2d67662bbe0fbc1cb08e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 20 Jan 2025 21:07:28 -0500 Subject: [PATCH 207/365] Build aarch64 natively --- .github/workflows/tests.yml | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 92bff5f28..33ffc0b4b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,10 +10,10 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.9" + python-version: "3.13" - name: Build source package run: | - pip install cython + pip install setuptools cython python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.1.json /tmp/vendor PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig python setup.py sdist - name: Upload source package @@ -32,7 +32,7 @@ jobs: arch: arm64 - os: macos-13 arch: x86_64 - - os: ubuntu-latest + - os: ubuntu-24.04-arm arch: aarch64 - os: ubuntu-latest arch: i686 @@ -44,10 +44,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: "3.9" - - name: Set up QEMU - if: matrix.os == 'ubuntu-latest' - uses: docker/setup-qemu-action@v3 + python-version: "3.13" - name: Install packages if: matrix.os == 'macos-13' run: | From 73baa3dcdb3886623b105082fd195775c3f1eb3d Mon Sep 17 00:00:00 2001 From: robinechuca Date: Wed, 22 Jan 2025 14:34:42 +0100 Subject: [PATCH 208/365] Add support for yuv[a]p16 pix_fmt --- av/video/frame.pyi | 8 +- av/video/frame.pyx | 301 ++++++++++++++++++++------------------- tests/test_videoframe.py | 36 +++++ 3 files changed, 197 insertions(+), 148 deletions(-) diff --git a/av/video/frame.pyi b/av/video/frame.pyi index a3eea373d..f58554b8f 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -59,7 +59,9 @@ class VideoFrame(Frame): ) -> VideoFrame: ... def to_rgb(self, **kwargs: Any) -> VideoFrame: ... def to_image(self, **kwargs: Any) -> Image.Image: ... - def to_ndarray(self, **kwargs: Any) -> _SupportedNDarray: ... + def to_ndarray( + self, force_channel_last: bool = False, **kwargs: Any + ) -> _SupportedNDarray: ... @staticmethod def from_image(img: Image.Image) -> VideoFrame: ... @staticmethod @@ -67,7 +69,9 @@ class VideoFrame(Frame): array: _SupportedNDarray, format: str = "rgb24", width: int = 0 ) -> VideoFrame: ... @staticmethod - def from_ndarray(array: _SupportedNDarray, format: str = "rgb24") -> VideoFrame: ... + def from_ndarray( + array: _SupportedNDarray, format: str = "rgb24", channel_last: bool = False + ) -> VideoFrame: ... @staticmethod def from_bytes( data: bytes, diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 6e4a1dbdf..5cdfb233b 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -282,18 +282,26 @@ cdef class VideoFrame(Frame): return Image.frombytes("RGB", (plane.width, plane.height), bytes(o_buf), "raw", "RGB", 0, 1) - def to_ndarray(self, **kwargs): + def to_ndarray(self, force_channel_last=False, **kwargs): """Get a numpy array of this frame. Any ``**kwargs`` are passed to :meth:`.VideoReformatter.reformat`. + The array returned is generally of dimension (height, width, channels). + + :param bool force_channel_last: If False (default), the shape for the yuv444p and yuvj444p + will be (channels, height, width) rather than (height, width, channels) as usual. + This is for backward compatibility and also for keeping that + `bytes(to_ndarray(frame))` should be the same as the ffmpeg cli + when returning the pix_fmt with `-c:v rawvideo`. + .. note:: Numpy must be installed. - .. note:: For formats which return an array of ``uint16`, the samples - will be in the system's native byte order. + .. note:: For formats which return an array of ``uint16`` or ``float32``, + the samples will be in the system's native byte order. .. note:: For ``pal8``, an ``(image, palette)`` tuple will be returned, - with the palette being in ARGB (PyAV will swap bytes if needed). + with the palette being in ARGB (PyAV will swap bytes if needed). .. note:: For ``gbrp`` formats, channels are flipped to RGB order. @@ -302,88 +310,94 @@ cdef class VideoFrame(Frame): import numpy as np - if frame.format.name in ("yuv420p", "yuvj420p"): - assert frame.width % 2 == 0 - assert frame.height % 2 == 0 - return np.hstack(( - useful_array(frame.planes[0]), - useful_array(frame.planes[1]), - useful_array(frame.planes[2]) - )).reshape(-1, frame.width) - elif frame.format.name in ("yuv444p", "yuvj444p"): - return np.hstack(( + # check size + if frame.format.name in {"yuv420p", "yuvj420p", "yuyv422"}: + assert frame.width % 2 == 0, "the width has to be even for this pixel format" + assert frame.height % 2 == 0, "the height has to be even for this pixel format" + + # cases planes are simply concatenated in shape (height, width, channels) + itemsize, dtype = { + "abgr": (4, "uint8"), + "argb": (4, "uint8"), + "bgr24": (3, "uint8"), + "bgr8": (1, "uint8"), + "bgra": (4, "uint8"), + "gbrapf32be": (4, "float32"), + "gbrapf32le": (4, "float32"), + "gbrp": (1, "uint8"), + "gbrp10be": (2, "uint16"), + "gbrp10le": (2, "uint16"), + "gbrp12be": (2, "uint16"), + "gbrp12le": (2, "uint16"), + "gbrp14be": (2, "uint16"), + "gbrp14le": (2, "uint16"), + "gbrp16be": (2, "uint16"), + "gbrp16le": (2, "uint16"), + "gbrpf32be": (4, "float32"), + "gbrpf32le": (4, "float32"), + "gray": (1, "uint8"), + "gray16be": (2, "uint16"), + "gray16le": (2, "uint16"), + "gray8": (1, "uint8"), + "grayf32be": (4, "float32"), + "grayf32le": (4, "float32"), + "rgb24": (3, "uint8"), + "rgb48be": (6, "uint16"), + "rgb48le": (6, "uint16"), + "rgb8": (1, "uint8"), + "rgba": (4, "uint8"), + "rgba64be": (8, "uint16"), + "rgba64le": (8, "uint16"), + "yuv444p": (1, "uint8"), + "yuv444p16be": (2, "uint16"), + "yuv444p16le": (2, "uint16"), + "yuva444p16be": (2, "uint16"), + "yuva444p16le": (2, "uint16"), + "yuvj444p": (1, "uint8"), + "yuyv422": (2, "uint8"), + }.get(frame.format.name, (None, None)) + if itemsize is not None: + layers = [ + useful_array(plan, itemsize, dtype) + .reshape(frame.height, frame.width, -1) + for plan in frame.planes + ] + if len(layers) == 1: # shortcut, avoid memory copy + array = layers[0] + else: # general case + array = np.concatenate(layers, axis=2) + array = byteswap_array(array, frame.format.name.endswith("be")) + if array.shape[2] == 1: # skip last channel for gray images + return array.squeeze(2) + if frame.format.name.startswith("gbr"): # gbr -> rgb + buffer = array[:, :, 0].copy() + array[:, :, 0] = array[:, :, 2] + array[:, :, 2] = array[:, :, 1] + array[:, :, 1] = buffer + if not force_channel_last and frame.format.name in {"yuv444p", "yuvj444p"}: + array = np.moveaxis(array, 2, 0) + return array + + # special cases + if frame.format.name in {"yuv420p", "yuvj420p"}: + return np.hstack([ useful_array(frame.planes[0]), useful_array(frame.planes[1]), useful_array(frame.planes[2]), - )).reshape(-1, frame.height, frame.width) - elif frame.format.name == "yuyv422": - assert frame.width % 2 == 0 - assert frame.height % 2 == 0 - return useful_array(frame.planes[0], 2).reshape(frame.height, frame.width, -1) - elif frame.format.name == "gbrp": - array = np.empty((frame.height, frame.width, 3), dtype="uint8") - array[:, :, 0] = useful_array(frame.planes[2], 1).reshape(frame.height, frame.width) - array[:, :, 1] = useful_array(frame.planes[0], 1).reshape(frame.height, frame.width) - array[:, :, 2] = useful_array(frame.planes[1], 1).reshape(frame.height, frame.width) - return array - elif frame.format.name in ("gbrp10be", "gbrp12be", "gbrp14be", "gbrp16be", "gbrp10le", "gbrp12le", "gbrp14le", "gbrp16le"): - array = np.empty((frame.height, frame.width, 3), dtype="uint16") - array[:, :, 0] = useful_array(frame.planes[2], 2, "uint16").reshape(frame.height, frame.width) - array[:, :, 1] = useful_array(frame.planes[0], 2, "uint16").reshape(frame.height, frame.width) - array[:, :, 2] = useful_array(frame.planes[1], 2, "uint16").reshape(frame.height, frame.width) - return byteswap_array(array, frame.format.name.endswith("be")) - elif frame.format.name in ("gbrpf32be", "gbrpf32le"): - array = np.empty((frame.height, frame.width, 3), dtype="float32") - array[:, :, 0] = useful_array(frame.planes[2], 4, "float32").reshape(frame.height, frame.width) - array[:, :, 1] = useful_array(frame.planes[0], 4, "float32").reshape(frame.height, frame.width) - array[:, :, 2] = useful_array(frame.planes[1], 4, "float32").reshape(frame.height, frame.width) - return byteswap_array(array, frame.format.name.endswith("be")) - elif frame.format.name in ("gbrapf32be", "gbrapf32le"): - array = np.empty((frame.height, frame.width, 4), dtype="float32") - array[:, :, 0] = useful_array(frame.planes[2], 4, "float32").reshape(frame.height, frame.width) - array[:, :, 1] = useful_array(frame.planes[0], 4, "float32").reshape(frame.height, frame.width) - array[:, :, 2] = useful_array(frame.planes[1], 4, "float32").reshape(frame.height, frame.width) - array[:, :, 3] = useful_array(frame.planes[3], 4, "float32").reshape(frame.height, frame.width) - return byteswap_array(array, frame.format.name.endswith("be")) - elif frame.format.name in ("rgb24", "bgr24"): - return useful_array(frame.planes[0], 3).reshape(frame.height, frame.width, -1) - elif frame.format.name in ("argb", "rgba", "abgr", "bgra"): - return useful_array(frame.planes[0], 4).reshape(frame.height, frame.width, -1) - elif frame.format.name in ("gray", "gray8", "rgb8", "bgr8"): - return useful_array(frame.planes[0]).reshape(frame.height, frame.width) - elif frame.format.name in ("gray16be", "gray16le"): - return byteswap_array( - useful_array(frame.planes[0], 2, "uint16").reshape(frame.height, frame.width), - frame.format.name.endswith("be"), - ) - elif frame.format.name in ("grayf32be", "grayf32le"): - return byteswap_array( - useful_array(frame.planes[0], 4, "float32").reshape(frame.height, frame.width), - frame.format.name.endswith("be"), - ) - elif frame.format.name in ("rgb48be", "rgb48le"): - return byteswap_array( - useful_array(frame.planes[0], 6, "uint16").reshape(frame.height, frame.width, -1), - frame.format.name.endswith("be"), - ) - elif frame.format.name in ("rgba64be", "rgba64le"): - return byteswap_array( - useful_array(frame.planes[0], 8, "uint16").reshape(frame.height, frame.width, -1), - frame.format.name.endswith("be"), - ) - elif frame.format.name == "pal8": + ]).reshape(-1, frame.width) + if frame.format.name == "pal8": image = useful_array(frame.planes[0]).reshape(frame.height, frame.width) palette = np.frombuffer(frame.planes[1], "i4").astype(">i4").reshape(-1, 1).view(np.uint8) return image, palette - elif frame.format.name == "nv12": - return np.hstack(( + if frame.format.name == "nv12": + return np.hstack([ useful_array(frame.planes[0]), - useful_array(frame.planes[1], 2) - )).reshape(-1, frame.width) - else: - raise ValueError( - f"Conversion to numpy array with format `{frame.format.name}` is not yet supported" - ) + useful_array(frame.planes[1], 2), + ]).reshape(-1, frame.width) + + raise ValueError( + f"Conversion to numpy array with format `{frame.format.name}` is not yet supported" + ) @staticmethod def from_image(img): @@ -497,17 +511,71 @@ cdef class VideoFrame(Frame): self._init_user_attributes() @staticmethod - def from_ndarray(array, format="rgb24"): + def from_ndarray(array, format="rgb24", channel_last=False): """ Construct a frame from a numpy array. - .. note:: For formats which expect an array of ``uint16``, the samples - must be in the system's native byte order. + :param bool channel_last: If False (default), the shape for the yuv444p and yuvj444p + is given by (channels, height, width) rather than (height, width, channels). + + .. note:: For formats which expect an array of ``uint16``, + the samples must be in the system's native byte order. .. note:: for ``pal8``, an ``(image, palette)`` pair must be passed. `palette` must have shape (256, 4) and is given in ARGB format (PyAV will swap bytes if needed). .. note:: for ``gbrp`` formats, channels are assumed to be given in RGB order. + """ + import numpy as np + + # case layers are concatenated + channels, itemsize, dtype = { + "yuv444p": (3, 1, "uint8"), + "yuvj444p": (3, 1, "uint8"), + "gbrp": (3, 1, "uint8"), + "gbrp10be": (3, 2, "uint16"), + "gbrp12be": (3, 2, "uint16"), + "gbrp14be": (3, 2, "uint16"), + "gbrp16be": (3, 2, "uint16"), + "gbrp10le": (3, 2, "uint16"), + "gbrp12le": (3, 2, "uint16"), + "gbrp14le": (3, 2, "uint16"), + "gbrp16le": (3, 2, "uint16"), + "gbrpf32be": (3, 4, "float32"), + "gbrpf32le": (3, 4, "float32"), + "gray": (1, 1, "uint8"), + "gray8": (1, 1, "uint8"), + "rgb8": (1, 1, "uint8"), + "bgr8": (1, 1, "uint8"), + "gray16be": (1, 2, "uint16"), + "gray16le": (1, 2, "uint16"), + "grayf32be": (1, 4, "float32"), + "grayf32le": (1, 4, "float32"), + "gbrapf32be": (4, 4, "float32"), + "gbrapf32le": (4, 4, "float32"), + "yuv444p16be": (3, 2, "uint16"), + "yuv444p16le": (3, 2, "uint16"), + "yuva444p16be": (4, 2, "uint16"), + "yuva444p16le": (4, 2, "uint16"), + }.get(format, (None, None, None)) + if channels is not None: + if array.ndim == 2: # (height, width) -> (height, width, 1) + array = array[:, :, None] + check_ndarray(array, dtype, 3) + if not channel_last and format in {"yuv444p", "yuvj444p"}: + array = np.moveaxis(array, 0, 2) # (channels, h, w) -> (h, w, channels) + check_ndarray_shape(array, array.shape[2] == channels) + array = byteswap_array(array, format.endswith("be")) + frame = VideoFrame(array.shape[1], array.shape[0], format) + if frame.format.name.startswith("gbr"): # rgb -> gbr + array = np.concatenate([ # not inplace to avoid bad surprises + array[:, :, 1:3], array[:, :, 0:1], array[:, :, 3:], + ], axis=2) + for i in range(channels): + copy_array_to_plane(array[:, :, i], frame.planes[i], itemsize) + return frame + + # other cases if format == "pal8": array, palette = array check_ndarray(array, "uint8", 2) @@ -518,7 +586,7 @@ cdef class VideoFrame(Frame): copy_array_to_plane(array, frame.planes[0], 1) frame.planes[1].update(palette.view(">i4").astype("i4").tobytes()) return frame - elif format in ("yuv420p", "yuvj420p"): + elif format in {"yuv420p", "yuvj420p"}: check_ndarray(array, "uint8", 2) check_ndarray_shape(array, array.shape[0] % 3 == 0) check_ndarray_shape(array, array.shape[1] % 2 == 0) @@ -531,88 +599,29 @@ cdef class VideoFrame(Frame): copy_array_to_plane(flat[u_start:v_start], frame.planes[1], 1) copy_array_to_plane(flat[v_start:], frame.planes[2], 1) return frame - elif format in ("yuv444p", "yuvj444p"): - check_ndarray(array, "uint8", 3) - check_ndarray_shape(array, array.shape[0] == 3) - - frame = VideoFrame(array.shape[2], array.shape[1], format) - array = array.reshape(3, -1) - copy_array_to_plane(array[0], frame.planes[0], 1) - copy_array_to_plane(array[1], frame.planes[1], 1) - copy_array_to_plane(array[2], frame.planes[2], 1) - return frame elif format == "yuyv422": check_ndarray(array, "uint8", 3) check_ndarray_shape(array, array.shape[0] % 2 == 0) check_ndarray_shape(array, array.shape[1] % 2 == 0) check_ndarray_shape(array, array.shape[2] == 2) - elif format == "gbrp": - check_ndarray(array, "uint8", 3) - check_ndarray_shape(array, array.shape[2] == 3) - - frame = VideoFrame(array.shape[1], array.shape[0], format) - copy_array_to_plane(array[:, :, 1], frame.planes[0], 1) - copy_array_to_plane(array[:, :, 2], frame.planes[1], 1) - copy_array_to_plane(array[:, :, 0], frame.planes[2], 1) - return frame - elif format in ("gbrp10be", "gbrp12be", "gbrp14be", "gbrp16be", "gbrp10le", "gbrp12le", "gbrp14le", "gbrp16le"): - check_ndarray(array, "uint16", 3) - check_ndarray_shape(array, array.shape[2] == 3) - - frame = VideoFrame(array.shape[1], array.shape[0], format) - copy_array_to_plane(byteswap_array(array[:, :, 1], format.endswith("be")), frame.planes[0], 2) - copy_array_to_plane(byteswap_array(array[:, :, 2], format.endswith("be")), frame.planes[1], 2) - copy_array_to_plane(byteswap_array(array[:, :, 0], format.endswith("be")), frame.planes[2], 2) - return frame - elif format in ("gbrpf32be", "gbrpf32le"): - check_ndarray(array, "float32", 3) - check_ndarray_shape(array, array.shape[2] == 3) - - frame = VideoFrame(array.shape[1], array.shape[0], format) - copy_array_to_plane(byteswap_array(array[:, :, 1], format.endswith("be")), frame.planes[0], 4) - copy_array_to_plane(byteswap_array(array[:, :, 2], format.endswith("be")), frame.planes[1], 4) - copy_array_to_plane(byteswap_array(array[:, :, 0], format.endswith("be")), frame.planes[2], 4) - return frame - elif format in ("rgb24", "bgr24"): + elif format in {"rgb24", "bgr24"}: check_ndarray(array, "uint8", 3) check_ndarray_shape(array, array.shape[2] == 3) - elif format in ("argb", "rgba", "abgr", "bgra"): + elif format in {"argb", "rgba", "abgr", "bgra"}: check_ndarray(array, "uint8", 3) check_ndarray_shape(array, array.shape[2] == 4) - elif format in ("gray", "gray8", "rgb8", "bgr8"): - check_ndarray(array, "uint8", 2) - elif format in ("gray16be", "gray16le"): - check_ndarray(array, "uint16", 2) - frame = VideoFrame(array.shape[1], array.shape[0], format) - copy_array_to_plane(byteswap_array(array, format.endswith("be")), frame.planes[0], 2) - return frame - elif format in ("grayf32be", "grayf32le"): - check_ndarray(array, "float32", 2) - frame = VideoFrame(array.shape[1], array.shape[0], format) - copy_array_to_plane(byteswap_array(array, format.endswith("be")), frame.planes[0], 4) - return frame - elif format in ("rgb48be", "rgb48le"): + elif format in {"rgb48be", "rgb48le"}: check_ndarray(array, "uint16", 3) check_ndarray_shape(array, array.shape[2] == 3) frame = VideoFrame(array.shape[1], array.shape[0], format) copy_array_to_plane(byteswap_array(array, format.endswith("be")), frame.planes[0], 6) return frame - elif format in ("rgba64be", "rgba64le"): + elif format in {"rgba64be", "rgba64le"}: check_ndarray(array, "uint16", 3) check_ndarray_shape(array, array.shape[2] == 4) frame = VideoFrame(array.shape[1], array.shape[0], format) copy_array_to_plane(byteswap_array(array, format.endswith("be")), frame.planes[0], 8) return frame - elif format in ("gbrapf32be", "gbrapf32le"): - check_ndarray(array, "float32", 3) - check_ndarray_shape(array, array.shape[2] == 4) - - frame = VideoFrame(array.shape[1], array.shape[0], format) - copy_array_to_plane(byteswap_array(array[:, :, 1], format.endswith("be")), frame.planes[0], 4) - copy_array_to_plane(byteswap_array(array[:, :, 2], format.endswith("be")), frame.planes[1], 4) - copy_array_to_plane(byteswap_array(array[:, :, 0], format.endswith("be")), frame.planes[2], 4) - copy_array_to_plane(byteswap_array(array[:, :, 3], format.endswith("be")), frame.planes[3], 4) - return frame elif format == "nv12": check_ndarray(array, "uint8", 2) check_ndarray_shape(array, array.shape[0] % 3 == 0) diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 250641676..8cb520b82 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -449,6 +449,42 @@ def test_ndarray_yuvj444p() -> None: assertNdarraysEqual(frame.to_ndarray(), array) +def test_ndarray_yuv444p16() -> None: + array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) + for format in ("yuv444p16be", "yuv444p16le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_yuv444p16_allign() -> None: + array = numpy.random.randint(0, 65536, size=(238, 318, 3), dtype=numpy.uint16) + for format in ("yuv444p16be", "yuv444p16le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_yuva444p16() -> None: + array = numpy.random.randint(0, 65536, size=(480, 640, 4), dtype=numpy.uint16) + for format in ("yuva444p16be", "yuva444p16le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_yuva444p16_allign() -> None: + array = numpy.random.randint(0, 65536, size=(238, 318, 4), dtype=numpy.uint16) + for format in ("yuva444p16be", "yuva444p16le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + def test_ndarray_yuyv422_align() -> None: array = numpy.random.randint(0, 256, size=(238, 318, 2), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuyv422") From 787917bfba314ddb616efaad5535b40191108110 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 22 Jan 2025 09:03:58 -0500 Subject: [PATCH 209/365] Make parameters same, test channel_last --- av/video/frame.pyi | 2 +- av/video/frame.pyx | 12 +++++------- tests/test_videoframe.py | 15 +++++++++++++-- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/av/video/frame.pyi b/av/video/frame.pyi index f58554b8f..3533c8e3f 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -60,7 +60,7 @@ class VideoFrame(Frame): def to_rgb(self, **kwargs: Any) -> VideoFrame: ... def to_image(self, **kwargs: Any) -> Image.Image: ... def to_ndarray( - self, force_channel_last: bool = False, **kwargs: Any + self, channel_last: bool = False, **kwargs: Any ) -> _SupportedNDarray: ... @staticmethod def from_image(img: Image.Image) -> VideoFrame: ... diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 5cdfb233b..36f4deae1 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -282,18 +282,16 @@ cdef class VideoFrame(Frame): return Image.frombytes("RGB", (plane.width, plane.height), bytes(o_buf), "raw", "RGB", 0, 1) - def to_ndarray(self, force_channel_last=False, **kwargs): + def to_ndarray(self, channel_last=False, **kwargs): """Get a numpy array of this frame. Any ``**kwargs`` are passed to :meth:`.VideoReformatter.reformat`. The array returned is generally of dimension (height, width, channels). - :param bool force_channel_last: If False (default), the shape for the yuv444p and yuvj444p - will be (channels, height, width) rather than (height, width, channels) as usual. - This is for backward compatibility and also for keeping that - `bytes(to_ndarray(frame))` should be the same as the ffmpeg cli - when returning the pix_fmt with `-c:v rawvideo`. + :param bool channel_last: If True, the shape of array will be + (height, width, channels) rather than (channels, height, width) for + the "yuv444p" and "yuvj444p" formats. .. note:: Numpy must be installed. @@ -374,7 +372,7 @@ cdef class VideoFrame(Frame): array[:, :, 0] = array[:, :, 2] array[:, :, 2] = array[:, :, 1] array[:, :, 1] = buffer - if not force_channel_last and frame.format.name in {"yuv444p", "yuvj444p"}: + if not channel_last and frame.format.name in {"yuv444p", "yuvj444p"}: array = np.moveaxis(array, 2, 0) return array diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 8cb520b82..9581a7c41 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -440,6 +440,17 @@ def test_ndarray_yuv444p() -> None: assert frame.format.name == "yuv444p" assertNdarraysEqual(frame.to_ndarray(), array) + array = numpy.random.randint(0, 256, size=(3, 480, 640), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, channel_last=False, format="yuv444p") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "yuv444p" + assertNdarraysEqual(frame.to_ndarray(channel_last=False), array) + assert array.shape != frame.to_ndarray(channel_last=True).shape + assert ( + frame.to_ndarray(channel_last=False).shape + != frame.to_ndarray(channel_last=True).shape + ) + def test_ndarray_yuvj444p() -> None: array = numpy.random.randint(0, 256, size=(3, 480, 640), dtype=numpy.uint8) @@ -458,7 +469,7 @@ def test_ndarray_yuv444p16() -> None: assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_yuv444p16_allign() -> None: +def test_ndarray_yuv444p16_align() -> None: array = numpy.random.randint(0, 65536, size=(238, 318, 3), dtype=numpy.uint16) for format in ("yuv444p16be", "yuv444p16le"): frame = VideoFrame.from_ndarray(array, format=format) @@ -476,7 +487,7 @@ def test_ndarray_yuva444p16() -> None: assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_yuva444p16_allign() -> None: +def test_ndarray_yuva444p16_align() -> None: array = numpy.random.randint(0, 65536, size=(238, 318, 4), dtype=numpy.uint16) for format in ("yuva444p16be", "yuva444p16le"): frame = VideoFrame.from_ndarray(array, format=format) From b073eefa276b2a6787ff1356e85a80f04bc3ffdb Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 25 Jan 2025 14:56:31 -0500 Subject: [PATCH 210/365] Disable building pypy wheels --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 33ffc0b4b..9ef90a4e3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -65,11 +65,11 @@ jobs: CIBW_ENVIRONMENT_MACOS: PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig LDFLAGS=-headerpad_max_install_names CIBW_ENVIRONMENT_WINDOWS: INCLUDE=C:\\cibw\\vendor\\include LIB=C:\\cibw\\vendor\\lib PYAV_SKIP_TESTS=unicode_filename CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: delvewheel repair --add-path C:\cibw\vendor\bin -w {dest_dir} {wheel} - CIBW_SKIP: "*-musllinux*" + CIBW_SKIP: "pp* *-musllinux*" CIBW_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m pytest {package}/tests && mv {project}/av.disabled {project}/av CIBW_TEST_REQUIRES: pytest numpy # skip tests when there are no binary wheels of numpy - CIBW_TEST_SKIP: pp* *_i686 + CIBW_TEST_SKIP: "*_i686" run: | pip install cibuildwheel delvewheel cibuildwheel --output-dir dist From cc06a6c698f68f47d5522059e75e9bd098cd3e23 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 25 Jan 2025 15:12:10 -0500 Subject: [PATCH 211/365] Release 14.1.0 --- CHANGELOG.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2f0bd681c..ead5b6a74 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,16 +17,16 @@ We are operating with `semantic versioning `_. are merged into the "default" branch. -v14.1.0 (Unreleased) --------------------- +v14.1.0 +------- -Features +Features: - Add hardware decoding by :gh-user:`matthewlai` and :gh-user:`WyattBlue` in (:pr:`1685`). - Add ``Stream.disposition`` and ``Disposition`` enum by :gh-user:`WyattBlue` in (:pr:`1720`). - Add ``VideoFrame.rotation`` by :gh-user:`lgeiger` in (:pr:`1675`). - Support grayf32le and gbrapf32le in numpy convertion by :gh-user:`robinechuca` in (:pr:`1712`). - +- Support yuv[a]p16 formats in numpy convertion by :gh-user:`robinechuca` in (:pr:`1722`). v14.0.1 ------- From dd2d71a96715b4e01aa653912f91b47cca1c1d99 Mon Sep 17 00:00:00 2001 From: z-khan Date: Thu, 30 Jan 2025 02:50:24 +1100 Subject: [PATCH 212/365] Allow user to select audio codec when opening an audio device --- av/container/core.pyi | 39 ++++++++++++++++++++++++++- av/container/core.pyx | 46 +++++++++++++++++++++++++++++++- include/libavcodec/avcodec.pxd | 34 +++++++++++++++++++++++ include/libavformat/avformat.pxd | 1 + 4 files changed, 118 insertions(+), 2 deletions(-) diff --git a/av/container/core.pyi b/av/container/core.pyi index 7c681b18b..7ba2511cf 100644 --- a/av/container/core.pyi +++ b/av/container/core.pyi @@ -1,4 +1,4 @@ -from enum import Flag +from enum import Flag, IntEnum from fractions import Fraction from pathlib import Path from types import TracebackType @@ -30,6 +30,43 @@ class Flags(Flag): shortest: ClassVar[Flags] auto_bsf: ClassVar[Flags] +class AudioCodec(IntEnum): + none: int + pcm_alaw: int + pcm_bluray: int + pcm_dvd: int + pcm_f16le: int + pcm_f24le: int + pcm_f32be: int + pcm_f32le: int + pcm_f64be: int + pcm_f64le: int + pcm_lxf: int + pcm_mulaw: int + pcm_s16be: int + pcm_s16be_planar: int + pcm_s16le: int + pcm_s16le_planar: int + pcm_s24be: int + pcm_s24daud: int + pcm_s24le: int + pcm_s24le_planar: int + pcm_s32be: int + pcm_s32le: int + pcm_s32le_planar: int + pcm_s64be: int + pcm_s64le: int + pcm_s8: int + pcm_s8_planar: int + pcm_u16be: int + pcm_u16le: int + pcm_u24be: int + pcm_u24le: int + pcm_u32be: int + pcm_u32le: int + pcm_u8: int + pcm_vidc: int + class Container: writeable: bool name: str diff --git a/av/container/core.pyx b/av/container/core.pyx index 201570c62..3da402f2f 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -3,7 +3,7 @@ from libc.stdint cimport int64_t import os import time -from enum import Flag +from enum import Flag, IntEnum from pathlib import Path cimport libav as lib @@ -141,6 +141,44 @@ class Flags(Flag): shortest: "Stop muxing when the shortest stream stops." = lib.AVFMT_FLAG_SHORTEST auto_bsf: "Add bitstream filters as requested by the muxer." = lib.AVFMT_FLAG_AUTO_BSF +class AudioCodec(IntEnum): + """Enumeration for audio codec IDs.""" + none = lib.AV_CODEC_ID_NONE # No codec. + pcm_alaw = lib.AV_CODEC_ID_PCM_ALAW # PCM A-law. + pcm_bluray = lib.AV_CODEC_ID_PCM_BLURAY # PCM Blu-ray. + pcm_dvd = lib.AV_CODEC_ID_PCM_DVD # PCM DVD. + pcm_f16le = lib.AV_CODEC_ID_PCM_F16LE # PCM F16 little-endian. + pcm_f24le = lib.AV_CODEC_ID_PCM_F24LE # PCM F24 little-endian. + pcm_f32be = lib.AV_CODEC_ID_PCM_F32BE # PCM F32 big-endian. + pcm_f32le = lib.AV_CODEC_ID_PCM_F32LE # PCM F32 little-endian. + pcm_f64be = lib.AV_CODEC_ID_PCM_F64BE # PCM F64 big-endian. + pcm_f64le = lib.AV_CODEC_ID_PCM_F64LE # PCM F64 little-endian. + pcm_lxf = lib.AV_CODEC_ID_PCM_LXF # PCM LXF. + pcm_mulaw = lib.AV_CODEC_ID_PCM_MULAW # PCM μ-law. + pcm_s16be = lib.AV_CODEC_ID_PCM_S16BE # PCM signed 16-bit big-endian. + pcm_s16be_planar = lib.AV_CODEC_ID_PCM_S16BE_PLANAR # PCM signed 16-bit big-endian planar. + pcm_s16le = lib.AV_CODEC_ID_PCM_S16LE # PCM signed 16-bit little-endian. + pcm_s16le_planar = lib.AV_CODEC_ID_PCM_S16LE_PLANAR # PCM signed 16-bit little-endian planar. + pcm_s24be = lib.AV_CODEC_ID_PCM_S24BE # PCM signed 24-bit big-endian. + pcm_s24daud = lib.AV_CODEC_ID_PCM_S24DAUD # PCM signed 24-bit D-Cinema audio. + pcm_s24le = lib.AV_CODEC_ID_PCM_S24LE # PCM signed 24-bit little-endian. + pcm_s24le_planar = lib.AV_CODEC_ID_PCM_S24LE_PLANAR # PCM signed 24-bit little-endian planar. + pcm_s32be = lib.AV_CODEC_ID_PCM_S32BE # PCM signed 32-bit big-endian. + pcm_s32le = lib.AV_CODEC_ID_PCM_S32LE # PCM signed 32-bit little-endian. + pcm_s32le_planar = lib.AV_CODEC_ID_PCM_S32LE_PLANAR # PCM signed 32-bit little-endian planar. + pcm_s64be = lib.AV_CODEC_ID_PCM_S64BE # PCM signed 64-bit big-endian. + pcm_s64le = lib.AV_CODEC_ID_PCM_S64LE # PCM signed 64-bit little-endian. + pcm_s8 = lib.AV_CODEC_ID_PCM_S8 # PCM signed 8-bit. + pcm_s8_planar = lib.AV_CODEC_ID_PCM_S8_PLANAR # PCM signed 8-bit planar. + pcm_u16be = lib.AV_CODEC_ID_PCM_U16BE # PCM unsigned 16-bit big-endian. + pcm_u16le = lib.AV_CODEC_ID_PCM_U16LE # PCM unsigned 16-bit little-endian. + pcm_u24be = lib.AV_CODEC_ID_PCM_U24BE # PCM unsigned 24-bit big-endian. + pcm_u24le = lib.AV_CODEC_ID_PCM_U24LE # PCM unsigned 24-bit little-endian. + pcm_u32be = lib.AV_CODEC_ID_PCM_U32BE # PCM unsigned 32-bit big-endian. + pcm_u32le = lib.AV_CODEC_ID_PCM_U32LE # PCM unsigned 32-bit little-endian. + pcm_u8 = lib.AV_CODEC_ID_PCM_U8 # PCM unsigned 8-bit. + pcm_vidc = lib.AV_CODEC_ID_PCM_VIDC # PCM VIDC. + cdef class Container: def __cinit__(self, sentinel, file_, format_name, options, @@ -176,7 +214,10 @@ cdef class Container: self.buffer_size = buffer_size self.io_open = io_open + acodec = None # no audio codec specified if format_name is not None: + if ":" in format_name: + format_name, acodec = format_name.split(":") self.format = ContainerFormat(format_name) self.input_was_opened = False @@ -211,6 +252,9 @@ cdef class Container: self.ptr.interrupt_callback.callback = interrupt_cb self.ptr.interrupt_callback.opaque = &self.interrupt_callback_info + if acodec is not None: + self.ptr.audio_codec_id = getattr(AudioCodec, acodec) + self.ptr.flags |= lib.AVFMT_FLAG_GENPTS self.ptr.opaque = self diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index ee832dd04..5bed3583d 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -163,6 +163,40 @@ cdef extern from "libavcodec/avcodec.h" nogil: AV_CODEC_ID_NONE AV_CODEC_ID_MPEG2VIDEO AV_CODEC_ID_MPEG1VIDEO + AV_CODEC_ID_PCM_ALAW + AV_CODEC_ID_PCM_BLURAY + AV_CODEC_ID_PCM_DVD + AV_CODEC_ID_PCM_F16LE + AV_CODEC_ID_PCM_F24LE + AV_CODEC_ID_PCM_F32BE + AV_CODEC_ID_PCM_F32LE + AV_CODEC_ID_PCM_F64BE + AV_CODEC_ID_PCM_F64LE + AV_CODEC_ID_PCM_LXF + AV_CODEC_ID_PCM_MULAW + AV_CODEC_ID_PCM_S16BE + AV_CODEC_ID_PCM_S16BE_PLANAR + AV_CODEC_ID_PCM_S16LE + AV_CODEC_ID_PCM_S16LE_PLANAR + AV_CODEC_ID_PCM_S24BE + AV_CODEC_ID_PCM_S24DAUD + AV_CODEC_ID_PCM_S24LE + AV_CODEC_ID_PCM_S24LE_PLANAR + AV_CODEC_ID_PCM_S32BE + AV_CODEC_ID_PCM_S32LE + AV_CODEC_ID_PCM_S32LE_PLANAR + AV_CODEC_ID_PCM_S64BE + AV_CODEC_ID_PCM_S64LE + AV_CODEC_ID_PCM_S8 + AV_CODEC_ID_PCM_S8_PLANAR + AV_CODEC_ID_PCM_U16BE + AV_CODEC_ID_PCM_U16LE + AV_CODEC_ID_PCM_U24BE + AV_CODEC_ID_PCM_U24LE + AV_CODEC_ID_PCM_U32BE + AV_CODEC_ID_PCM_U32LE + AV_CODEC_ID_PCM_U8 + AV_CODEC_ID_PCM_VIDC cdef enum AVDiscard: AVDISCARD_NONE diff --git a/include/libavformat/avformat.pxd b/include/libavformat/avformat.pxd index cec89bd38..6c23e99b4 100644 --- a/include/libavformat/avformat.pxd +++ b/include/libavformat/avformat.pxd @@ -191,6 +191,7 @@ cdef extern from "libavformat/avformat.h" nogil: int flags int64_t max_analyze_duration + AVCodecID audio_codec_id void *opaque int (*io_open)( From b4b13e248365ece781e06d2bf6848f7472de6f47 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 30 Jan 2025 13:46:19 -0500 Subject: [PATCH 213/365] Remove issue templates --- .github/ISSUE_TEMPLATE/build-bug-report.md | 67 ------------------- .../ISSUE_TEMPLATE/ffmpeg-feature-request.md | 56 ---------------- .../ISSUE_TEMPLATE/pyav-feature-request.md | 26 ------- .github/ISSUE_TEMPLATE/runtime-bug-report.md | 63 ----------------- 4 files changed, 212 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/build-bug-report.md delete mode 100644 .github/ISSUE_TEMPLATE/ffmpeg-feature-request.md delete mode 100644 .github/ISSUE_TEMPLATE/pyav-feature-request.md delete mode 100644 .github/ISSUE_TEMPLATE/runtime-bug-report.md diff --git a/.github/ISSUE_TEMPLATE/build-bug-report.md b/.github/ISSUE_TEMPLATE/build-bug-report.md deleted file mode 100644 index da0c17add..000000000 --- a/.github/ISSUE_TEMPLATE/build-bug-report.md +++ /dev/null @@ -1,67 +0,0 @@ ---- -name: Build bug report -about: Report on an issue while building or installing PyAV. -title: "FOO does not build." -labels: build -assignees: '' - ---- - -**IMPORTANT:** Be sure to replace all template sections {{ like this }} or your issue may be discarded. - - -## Overview - -{{ A clear and concise description of what the bug is. }} - - -## Expected behavior - -{{ A clear and concise description of what you expected to happen. }} - - -## Actual behavior - -{{ A clear and concise description of what actually happened. }} - -Build report: -``` -{{ Complete output of `python setup.py build`. Reports that do not show compiler commands will not be accepted (e.g. results from `pip install av`). }} -``` - - -## Investigation - -{{ What you did to isolate the problem. }} - - -## Reproduction - -{{ Steps to reproduce the behavior. }} - - -## Versions - -- OS: {{ e.g. macOS 10.13.6 }} -- PyAV runtime: -``` -{{ Complete output of `python -m av --version` if you can run it. }} -``` -- PyAV build: -``` -{{ Complete output of `python setup.py config --verbose`. }} -``` - -## Research - -I have done the following: - -- [ ] Checked the [PyAV documentation](https://pyav.basswood-io.com) -- [ ] Searched on [Google](https://www.google.com/search?q=pyav+how+do+I+foo) -- [ ] Searched on [Stack Overflow](https://stackoverflow.com/search?q=pyav) -- [ ] Looked through [old GitHub issues](https://github.com/PyAV-Org/PyAV/issues?&q=is%3Aissue) - - -## Additional context - -{{ Add any other context about the problem here. }} diff --git a/.github/ISSUE_TEMPLATE/ffmpeg-feature-request.md b/.github/ISSUE_TEMPLATE/ffmpeg-feature-request.md deleted file mode 100644 index d47b011e1..000000000 --- a/.github/ISSUE_TEMPLATE/ffmpeg-feature-request.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -name: FFmpeg feature request -about: Request a feature of FFmpeg be exposed or supported by PyAV. -title: "Allow FOO to BAR" -assignees: '' - ---- - -**IMPORTANT:** Be sure to replace all template sections {{ like this }} or your issue may be discarded. - - -## Overview - -{{ A clear and concise description of what the feature is. }} - - -## Existing FFmpeg API - -{{ Link to appropriate FFmpeg documentation, ideally the API doxygen files at https://ffmpeg.org/doxygen/trunk/ }} - - -## Expected PyAV API - -{{ A description of how you think PyAV should behave. }} - -Example: -``` -{{ An example of how you think PyAV should behave. }} -``` - - -## Investigation - -{{ What you did to isolate the problem. }} - - -## Reproduction - -{{ Steps to reproduce the behavior. If the problem is media specific, include a link to it. Only send media that you have the rights to. }} - - -## Versions - -- OS: {{ e.g. macOS 10.13.6 }} -- PyAV runtime: -``` -{{ Complete output of `python -m av --version`. If this command won't run, you are likely dealing with the build issue and should use the appropriate template. }} -``` -- PyAV build: -``` -{{ Complete output of `python setup.py config --verbose`. }} -``` - -## Additional context - -{{ Add any other context about the problem here. }} diff --git a/.github/ISSUE_TEMPLATE/pyav-feature-request.md b/.github/ISSUE_TEMPLATE/pyav-feature-request.md deleted file mode 100644 index cd024d61d..000000000 --- a/.github/ISSUE_TEMPLATE/pyav-feature-request.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -name: PyAV feature request -about: Request a feature of PyAV that is not provided by FFmpeg. -title: "Allow FOO to BAR" -assignees: '' - ---- - -**IMPORTANT:** Be sure to replace all template sections {{ like this }} or your issue may be discarded. - - -## Desired Behavior - -{{ A description of how you think PyAV should behave. }} - - -## Example API - -``` -{{ An example of how you think PyAV should behave. }} -``` - - -## Additional context - -{{ Add any other context about the problem here. }} diff --git a/.github/ISSUE_TEMPLATE/runtime-bug-report.md b/.github/ISSUE_TEMPLATE/runtime-bug-report.md deleted file mode 100644 index 9c8632a7d..000000000 --- a/.github/ISSUE_TEMPLATE/runtime-bug-report.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -name: Runtime bug report -about: Report on an issue while running PyAV. -title: "The FOO does not BAR." -assignees: '' - ---- - -**IMPORTANT:** Be sure to replace all template sections {{ like this }} or your issue may be discarded. - - -## Overview - -{{ A clear and concise description of what the bug is. }} - - -## Expected behavior - -{{ A clear and concise description of what you expected to happen. }} - - -## Actual behavior - -{{ A clear and concise description of what actually happened. }} - -Traceback: -``` -{{ Include complete tracebacks if there are any exceptions. }} -``` - - -## Investigation - -{{ What you did to isolate the problem. }} - - -## Reproduction - -{{ Steps to reproduce the behavior. If the problem is media specific, include a link to it. Only send media that you have the rights to. }} - - -## Versions - -- OS: {{ e.g. macOS 10.13.6 }} -- PyAV runtime: -``` -{{ Complete output of `python -m av --version`. If this command won't run, you are likely dealing with the build issue and should use the appropriate template. }} -``` -- [ ] I am/tried using the binary wheels -- [ ] I compiled from source - -## Research - -I have done the following: - -- [ ] Checked the [PyAV documentation](https://pyav.basswood-io.com) -- [ ] Searched on [Google](https://www.google.com/search?q=pyav+how+do+I+foo) -- [ ] Searched on [Stack Overflow](https://stackoverflow.com/search?q=pyav) -- [ ] Looked through [old GitHub issues](https://github.com/PyAV-Org/PyAV/issues?&q=is%3Aissue) - -## Additional context - -{{ Add any other context about the problem here. }} From 970963e3264b0966be71935e35ee57eddca1c637 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 1 Feb 2025 14:21:37 -0500 Subject: [PATCH 214/365] Update authors --- AUTHORS.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index d5b4cb7ee..846cf0d93 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -31,12 +31,15 @@ All contributors (by number of commits): - David Plowman - Alireza Davoudi ; `@adavoudi `_ - Jonathan Drolet +- Lukas Geiger +- Matthew Lai - Moritz Kassner ; `@mkassner `_ - Thomas A Caswell ; `@tacaswell `_ - Ulrik Mikaelsson ; `@rawler `_ - Wel C. van der - Will Patera ; `@willpatera `_ +* Joe Schiff <41972063+JoeSchiff@users.noreply.github.com> * Dexer <73297572+DexerBR@users.noreply.github.com> * rutsh * Felix Vollmer @@ -53,11 +56,11 @@ All contributors (by number of commits): * Pablo Prietz * Andrew Wason * Radek Senfeld ; `@radek-senfeld `_ +* robinechuca * Benjamin Chrétien <2742231+bchretien@users.noreply.github.com> * zzjjbb <31069326+zzjjbb@users.noreply.github.com> * davidplowman <38045873+davidplowman@users.noreply.github.com> * Hanz <40712686+HanzCEO@users.noreply.github.com> -* Joe Schiff <41972063+JoeSchiff@users.noreply.github.com> * Artturin * Ian Lee * Ryan Huang @@ -73,7 +76,7 @@ All contributors (by number of commits): * Jonathan Martin * Johan Jeppsson Karlin * Philipp Klaus -* Lukas Geiger +* Kim Minjong * Mattias Wadman * Manuel Goacolou * Julian Schweizer @@ -99,3 +102,4 @@ All contributors (by number of commits): * Koichi Akabe * David Joy * Sviatoslav Sydorenko (Святослав Сидоренко) +* z-khan From ad6ffe42482b825899b611594a8aeb980dab0f72 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 1 Feb 2025 15:11:24 -0500 Subject: [PATCH 215/365] Inline make_error --- av/container/core.pyi | 1 - av/error.pyx | 39 +++++++++++++++++---------------------- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/av/container/core.pyi b/av/container/core.pyi index 7ba2511cf..fa996d0d2 100644 --- a/av/container/core.pyi +++ b/av/container/core.pyi @@ -94,7 +94,6 @@ class Container: exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> bool: ... - def err_check(self, value: int) -> int: ... def set_timeout(self, timeout: Real | None) -> None: ... def start_timeout(self) -> None: ... diff --git a/av/error.pyx b/av/error.pyx index 426d01ed0..3ef0f44a4 100644 --- a/av/error.pyx +++ b/av/error.pyx @@ -379,27 +379,6 @@ cdef int stash_exception(exc_info=None): cdef int _last_log_count = 0 - -cpdef make_error(int res, filename=None, log=None): - cdef int code = -res - cdef char* error_buffer = malloc(lib.AV_ERROR_MAX_STRING_SIZE * sizeof(char)) - if error_buffer == NULL: - raise MemoryError() - - try: - if code == c_PYAV_STASHED_ERROR: - message = PYAV_STASHED_ERROR_message - else: - lib.av_strerror(res, error_buffer, lib.AV_ERROR_MAX_STRING_SIZE) - # Fallback to OS error string if no message - message = error_buffer or os.strerror(code) - - cls = classes.get(code, UndefinedError) - return cls(code, message, filename, log) - finally: - free(error_buffer) - - cpdef int err_check(int res, filename=None) except -1: """Raise appropriate exceptions from library return code.""" @@ -425,7 +404,23 @@ cpdef int err_check(int res, filename=None) except -1: else: log = None - raise make_error(res, filename, log) + cdef int code = -res + cdef char* error_buffer = malloc(lib.AV_ERROR_MAX_STRING_SIZE * sizeof(char)) + if error_buffer == NULL: + raise MemoryError() + + try: + if code == c_PYAV_STASHED_ERROR: + message = PYAV_STASHED_ERROR_message + else: + lib.av_strerror(res, error_buffer, lib.AV_ERROR_MAX_STRING_SIZE) + # Fallback to OS error string if no message + message = error_buffer or os.strerror(code) + + cls = classes.get(code, UndefinedError) + raise cls(code, message, filename, log) + finally: + free(error_buffer) class UndefinedError(FFmpegError): From 8b3b5c9e254ef85cbef5ebe37e11c2c1aa8c8241 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 1 Feb 2025 15:24:54 -0500 Subject: [PATCH 216/365] Use literal dict --- av/error.pyx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/av/error.pyx b/av/error.pyx index 3ef0f44a4..ea2b9d216 100644 --- a/av/error.pyx +++ b/av/error.pyx @@ -338,13 +338,13 @@ for enum_name, code, name, base in _ffmpeg_specs: name = name or enum_name.title().replace("_", "") + "Error" if base is None: - bases = (FFmpegError, ) + bases = (FFmpegError,) elif issubclass(base, FFmpegError): - bases = (base, ) + bases = (base,) else: bases = (FFmpegError, base) - cls = type(name, bases, dict(__module__=__name__)) + cls = type(name, bases, {"__module__": __name__}) # Register in builder. classes[code] = cls From 824987ba1229c006a00b0f5d19ec149cee65ae27 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 1 Feb 2025 18:33:40 -0500 Subject: [PATCH 217/365] Add mode to Codec, display C func in err --- av/codec/codec.pyi | 2 ++ av/codec/codec.pyx | 6 +++++- av/codec/context.pyx | 8 ++++---- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/av/codec/codec.pyi b/av/codec/codec.pyi index 73c1f1edb..3a6c3a866 100644 --- a/av/codec/codec.pyi +++ b/av/codec/codec.pyi @@ -51,6 +51,8 @@ class Codec: def is_encoder(self) -> bool: ... @property def is_decoder(self) -> bool: ... + @property + def mode(self) -> Literal["r", "w"]: ... descriptor: Descriptor @property def name(self) -> str: ... diff --git a/av/codec/codec.pyx b/av/codec/codec.pyx index 73f085e8d..3e9e028f8 100644 --- a/av/codec/codec.pyx +++ b/av/codec/codec.pyx @@ -119,7 +119,7 @@ cdef class Codec: raise RuntimeError("%s is both encoder and decoder.") def __repr__(self): - mode = "w" if self.is_encoder else "r" + mode = self.mode return f"" def create(self, kind = None): @@ -130,6 +130,10 @@ cdef class Codec: from .context import CodecContext return CodecContext.create(self) + @property + def mode(self): + return "w" if self.is_encoder else "r" + @property def is_decoder(self): return not self.is_encoder diff --git a/av/codec/context.pyx b/av/codec/context.pyx index e7136b86f..a56f14ddd 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -324,7 +324,7 @@ cdef class CodecContext: cdef int res with nogil: res = lib.avcodec_send_frame(self.ptr, frame.ptr if frame is not None else NULL) - err_check(res) + err_check(res, "avcodec_send_frame()") packet = self._recv_packet() while packet: @@ -337,7 +337,7 @@ cdef class CodecContext: cdef int res with nogil: res = lib.avcodec_send_packet(self.ptr, packet.ptr if packet is not None else NULL) - err_check(res) + err_check(res, "avcodec_send_packet()") out = [] while True: @@ -365,7 +365,7 @@ cdef class CodecContext: if res == -EAGAIN or res == lib.AVERROR_EOF: return - err_check(res) + err_check(res, "avcodec_receive_frame()") frame = self._transfer_hwframe(frame) @@ -384,7 +384,7 @@ cdef class CodecContext: res = lib.avcodec_receive_packet(self.ptr, packet.ptr) if res == -EAGAIN or res == lib.AVERROR_EOF: return - err_check(res) + err_check(res, "avcodec_receive_packet()") if not res: return packet From a961fd42ff519d83993854b0356c9cdb4ca09450 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 2 Feb 2025 12:18:59 -0500 Subject: [PATCH 218/365] Remove unused code for passing in a Codec object --- av/container/output.pyx | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/av/container/output.pyx b/av/container/output.pyx index e61ef2297..c2055486e 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -51,23 +51,15 @@ cdef class OutputContainer(Container): Supports video, audio, and subtitle streams. :param codec_name: The name of a codec. - :type codec_name: str | Codec + :type codec_name: str :param dict options: Stream options. :param \\**kwargs: Set attributes for the stream. :rtype: The new :class:`~av.stream.Stream`. """ - cdef const lib.AVCodec *codec - cdef Codec codec_obj - - if isinstance(codec_name, Codec): - if codec_name.mode != "w": - raise ValueError("codec_name must be an encoder.") - codec_obj = codec_name - else: - codec_obj = Codec(codec_name, "w") - codec = codec_obj.ptr + cdef Codec codec_obj = Codec(codec_name, "w") + cdef const lib.AVCodec *codec = codec_obj.ptr # Assert that this format supports the requested codec. if not lib.avformat_query_codec(self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL): From 6294e28a7d19d98eea9e0a8e6f17c94c93e3b30a Mon Sep 17 00:00:00 2001 From: z-khan Date: Mon, 3 Feb 2025 23:59:14 +1100 Subject: [PATCH 219/365] PYAV_LIBRARY_ROOT: make directory before change directory $PYAV_LIBRARY_ROOT should be created before calling cd $PYAV_LIBRARY_ROOT if building with NVIDIA Codec support. --- scripts/build-deps | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/scripts/build-deps b/scripts/build-deps index de4a6e547..9cc795305 100755 --- a/scripts/build-deps +++ b/scripts/build-deps @@ -13,6 +13,9 @@ if [[ -e "$PYAV_LIBRARY_PREFIX/bin/ffmpeg" ]]; then exit 0 fi +mkdir -p "$PYAV_LIBRARY_ROOT" +mkdir -p "$PYAV_LIBRARY_PREFIX" + # Add CUDA support if available CONFFLAGS_NVIDIA="" if [[ -e /usr/local/cuda ]]; then @@ -38,12 +41,8 @@ else echo " Building without NVIDIA NVENC/NVDEC support" fi - -mkdir -p "$PYAV_LIBRARY_ROOT" -mkdir -p "$PYAV_LIBRARY_PREFIX" cd "$PYAV_LIBRARY_ROOT" - # Download and expand the source. if [[ ! -d $PYAV_LIBRARY ]]; then url="https://ffmpeg.org/releases/$PYAV_LIBRARY.tar.gz" From 14ea9c1bab8089c3ad1b4f7f2242c24875435266 Mon Sep 17 00:00:00 2001 From: Matteo Destro Date: Tue, 4 Feb 2025 14:58:19 +0000 Subject: [PATCH 220/365] Add support for flags in hwaccel --- av/codec/hwaccel.pxd | 1 + av/codec/hwaccel.pyi | 1 + av/codec/hwaccel.pyx | 5 +++-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/av/codec/hwaccel.pxd b/av/codec/hwaccel.pxd index e68f43bb1..46efdaf3b 100644 --- a/av/codec/hwaccel.pxd +++ b/av/codec/hwaccel.pxd @@ -18,3 +18,4 @@ cdef class HWAccel: cdef lib.AVBufferRef *ptr cdef public bint allow_software_fallback cdef public dict options + cdef public int flags diff --git a/av/codec/hwaccel.pyi b/av/codec/hwaccel.pyi index 7e4748e23..84f1da964 100644 --- a/av/codec/hwaccel.pyi +++ b/av/codec/hwaccel.pyi @@ -42,6 +42,7 @@ class HWAccel: device: str | None = None, allow_software_fallback: bool = False, options: dict[str, object] | None = None, + flags: int | None = None, ) -> None: ... def create(self, codec: Codec) -> HWAccel: ... diff --git a/av/codec/hwaccel.pyx b/av/codec/hwaccel.pyx index b80c194af..257e6e7b2 100644 --- a/av/codec/hwaccel.pyx +++ b/av/codec/hwaccel.pyx @@ -95,7 +95,7 @@ cpdef hwdevices_available(): cdef class HWAccel: - def __init__(self, device_type, device=None, allow_software_fallback=True, options=None): + def __init__(self, device_type, device=None, allow_software_fallback=True, options=None, flags=None): if isinstance(device_type, HWDeviceType): self._device_type = device_type elif isinstance(device_type, str): @@ -108,6 +108,7 @@ cdef class HWAccel: self._device = device self.allow_software_fallback = allow_software_fallback self.options = {} if not options else dict(options) + self.flags = 0 if not flags else flags self.ptr = NULL self.config = None @@ -132,7 +133,7 @@ cdef class HWAccel: err_check( lib.av_hwdevice_ctx_create( - &self.ptr, config.ptr.device_type, c_device, c_options.ptr, 0 + &self.ptr, config.ptr.device_type, c_device, c_options.ptr, self.flags ) ) From bf41ee097637af7ec1eca40500bfec0fbada45ad Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 4 Feb 2025 11:50:14 -0500 Subject: [PATCH 221/365] Put conda over installing from source --- README.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 10cd7709d..7e21198e7 100644 --- a/README.md +++ b/README.md @@ -17,30 +17,33 @@ But where you can't work without it, PyAV is a critical tool. Installation ------------ -Due to the complexity of the dependencies, PyAV is not always the easiest Python package to install from source. Since release 8.0.0 binary wheels are provided on [PyPI][pypi] for Linux, Mac and Windows linked against a modern FFmpeg. You can install these wheels by running: +Binary wheels are provided on [PyPI][pypi] for Linux, MacOS and Windows linked against the latest stable version of ffmpeg. You can install these wheels by running: ```bash pip install av ``` -If you want to use your existing FFmpeg, the source version of PyAV is on [PyPI][pypi] too: +Another way of installing PyAV is via [conda-forge][conda-forge]: ```bash -pip install av --no-binary av +conda install av -c conda-forge ``` -Installing from source is not supported on Windows. +See the [Conda install][conda-install] docs to get started with (mini)Conda. + Alternative installation methods -------------------------------- -Another way of installing PyAV is via [conda-forge][conda-forge]: +Due to the complexity of the dependencies, PyAV is not always the easiest Python package to install from source. If you want to use your existing ffmpeg (must be the correct major version), the source version of PyAV is on [PyPI][pypi]: ```bash -conda install av -c conda-forge +pip install av --no-binary av ``` -See the [Conda install][conda-install] docs to get started with (mini)Conda. +> [!WARNING] +> This installation method won't work for Windows or Debian based systems. + And if you want to build from the absolute source (POSIX only): From 796d8afb657203bc5f1050db76b79e8b0d02de31 Mon Sep 17 00:00:00 2001 From: z-khan Date: Thu, 6 Feb 2025 12:58:20 +1100 Subject: [PATCH 222/365] Add "Bayer" pixel formats --- av/video/format.pyi | 5 ++++- av/video/format.pyx | 5 +++++ av/video/frame.pyx | 35 ++++++++++++++++++++++++++++++++++- 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/av/video/format.pyi b/av/video/format.pyi index ee16b85b8..e102ef4c0 100644 --- a/av/video/format.pyi +++ b/av/video/format.pyi @@ -6,7 +6,10 @@ class VideoFormat: has_palette: bool is_bit_stream: bool is_planar: bool - is_rgb: bool + @property + def is_rgb(self) -> bool: ... + @property + def is_bayer(self) -> bool: ... width: int height: int components: tuple[VideoFormatComponent, ...] diff --git a/av/video/format.pyx b/av/video/format.pyx index 6ae66c3a2..8779f7151 100644 --- a/av/video/format.pyx +++ b/av/video/format.pyx @@ -104,7 +104,12 @@ cdef class VideoFormat: def is_rgb(self): """The pixel format contains RGB-like data (as opposed to YUV/grayscale).""" return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_RGB) + + @property + def is_bayer(self): + """The pixel format contains Bayer data.""" + return bool(self.ptr.flags & lib.AV_PIX_FMT_FLAG_BAYER) cpdef chroma_width(self, int luma_width=0): """chroma_width(luma_width=0) diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 36f4deae1..b74d58d1c 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -317,6 +317,18 @@ cdef class VideoFrame(Frame): itemsize, dtype = { "abgr": (4, "uint8"), "argb": (4, "uint8"), + "bayer_bggr8": (1, "uint8"), + "bayer_gbrg8": (1, "uint8"), + "bayer_grbg8": (1, "uint8"), + "bayer_rggb8": (1, "uint8"), + "bayer_bggr16le": (2, "uint16"), + "bayer_bggr16be": (2, "uint16"), + "bayer_gbrg16le": (2, "uint16"), + "bayer_gbrg16be": (2, "uint16"), + "bayer_grbg16le": (2, "uint16"), + "bayer_grbg16be": (2, "uint16"), + "bayer_rggb16le": (2, "uint16"), + "bayer_rggb16be": (2, "uint16"), "bgr24": (3, "uint8"), "bgr8": (1, "uint8"), "bgra": (4, "uint8"), @@ -451,6 +463,13 @@ cdef class VideoFrame(Frame): else: # Planes where U and V are interleaved have the same stride as Y. linesizes = (array.strides[0], array.strides[0]) + elif format in {"bayer_bggr8", "bayer_rggb8", "bayer_gbrg8", "bayer_grbg8","bayer_bggr16le", "bayer_rggb16le", "bayer_gbrg16le", "bayer_grbg16le","bayer_bggr16be", "bayer_rggb16be", "bayer_gbrg16be", "bayer_grbg16be"}: + check_ndarray(array, "uint8" if format.endswith("8") else "uint16", 2) + + if array.strides[1] != (1 if format.endswith("8") else 2): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + + linesizes = (array.strides[0],) else: raise ValueError(f"Conversion from numpy array with format `{format}` is not yet supported") @@ -554,7 +573,19 @@ cdef class VideoFrame(Frame): "yuv444p16be": (3, 2, "uint16"), "yuv444p16le": (3, 2, "uint16"), "yuva444p16be": (4, 2, "uint16"), - "yuva444p16le": (4, 2, "uint16"), + "yuva444p16le": (4, 2, "uint16"), + "bayer_bggr8": (1, 1, "uint8"), + "bayer_rggb8": (1, 1, "uint8"), + "bayer_grbg8": (1, 1, "uint8"), + "bayer_gbrg8": (1, 1, "uint8"), + "bayer_bggr16be": (1, 2, "uint16"), + "bayer_bggr16le": (1, 2, "uint16"), + "bayer_rggb16be": (1, 2, "uint16"), + "bayer_rggb16le": (1, 2, "uint16"), + "bayer_grbg16be": (1, 2, "uint16"), + "bayer_grbg16le": (1, 2, "uint16"), + "bayer_gbrg16be": (1, 2, "uint16"), + "bayer_gbrg16le": (1, 2, "uint16"), }.get(format, (None, None, None)) if channels is not None: if array.ndim == 2: # (height, width) -> (height, width, 1) @@ -644,6 +675,8 @@ cdef class VideoFrame(Frame): frame = VideoFrame(width, height, format) if format == "rgba": copy_bytes_to_plane(img_bytes, frame.planes[0], 4, flip_horizontal, flip_vertical) + elif format in ("bayer_bggr8", "bayer_rggb8", "bayer_gbrg8", "bayer_grbg8","bayer_bggr16le", "bayer_rggb16le", "bayer_gbrg16le", "bayer_grbg16le","bayer_bggr16be", "bayer_rggb16be", "bayer_gbrg16be", "bayer_grbg16be"): + copy_bytes_to_plane(img_bytes, frame.planes[0], 1 if format.endswith("8") else 2, flip_horizontal, flip_vertical) else: raise NotImplementedError(f"Format '{format}' is not supported.") return frame From a371f43c165902b59e3fb96d2ca8aa5b4c823ea3 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 5 Feb 2025 21:30:00 -0500 Subject: [PATCH 223/365] Use new Enum syntax --- av/codec/codec.pyi | 60 +++++++++++------------ av/codec/context.pyi | 70 +++++++++++++-------------- av/codec/hwaccel.pyi | 37 +++++++------- av/container/core.pyi | 102 +++++++++++++++++++-------------------- av/format.pyi | 36 +++++++------- av/option.pyi | 53 ++++++++++---------- av/sidedata/sidedata.pyi | 58 +++++++++++----------- av/stream.pyi | 40 +++++++-------- av/video/frame.pyi | 16 +++--- av/video/reformatter.pyi | 59 +++++++++++----------- 10 files changed, 267 insertions(+), 264 deletions(-) diff --git a/av/codec/codec.pyi b/av/codec/codec.pyi index 3a6c3a866..9f80a4909 100644 --- a/av/codec/codec.pyi +++ b/av/codec/codec.pyi @@ -1,6 +1,6 @@ from enum import Flag, IntEnum from fractions import Fraction -from typing import ClassVar, Literal, overload +from typing import ClassVar, Literal, cast, overload from av.audio.codeccontext import AudioCodecContext from av.audio.format import AudioFormat @@ -12,37 +12,37 @@ from av.video.format import VideoFormat from .context import CodecContext class Properties(Flag): - NONE: ClassVar[Properties] - INTRA_ONLY: ClassVar[Properties] - LOSSY: ClassVar[Properties] - LOSSLESS: ClassVar[Properties] - REORDER: ClassVar[Properties] - BITMAP_SUB: ClassVar[Properties] - TEXT_SUB: ClassVar[Properties] + NONE = cast(ClassVar[Properties], ...) + INTRA_ONLY = cast(ClassVar[Properties], ...) + LOSSY = cast(ClassVar[Properties], ...) + LOSSLESS = cast(ClassVar[Properties], ...) + REORDER = cast(ClassVar[Properties], ...) + BITMAP_SUB = cast(ClassVar[Properties], ...) + TEXT_SUB = cast(ClassVar[Properties], ...) class Capabilities(IntEnum): - none: int - draw_horiz_band: int - dr1: int - hwaccel: int - delay: int - small_last_frame: int - hwaccel_vdpau: int - subframes: int - experimental: int - channel_conf: int - neg_linesizes: int - frame_threads: int - slice_threads: int - param_change: int - auto_threads: int - variable_frame_size: int - avoid_probing: int - hardware: int - hybrid: int - encoder_reordered_opaque: int - encoder_flush: int - encoder_recon_frame: int + none = cast(int, ...) + draw_horiz_band = cast(int, ...) + dr1 = cast(int, ...) + hwaccel = cast(int, ...) + delay = cast(int, ...) + small_last_frame = cast(int, ...) + hwaccel_vdpau = cast(int, ...) + subframes = cast(int, ...) + experimental = cast(int, ...) + channel_conf = cast(int, ...) + neg_linesizes = cast(int, ...) + frame_threads = cast(int, ...) + slice_threads = cast(int, ...) + param_change = cast(int, ...) + auto_threads = cast(int, ...) + variable_frame_size = cast(int, ...) + avoid_probing = cast(int, ...) + hardware = cast(int, ...) + hybrid = cast(int, ...) + encoder_reordered_opaque = cast(int, ...) + encoder_flush = cast(int, ...) + encoder_recon_frame = cast(int, ...) class UnknownCodecError(ValueError): ... diff --git a/av/codec/context.pyi b/av/codec/context.pyi index 543da208e..ac57126ef 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -1,6 +1,6 @@ from enum import Flag, IntEnum from fractions import Fraction -from typing import ClassVar, Literal +from typing import ClassVar, Literal, cast from av.packet import Packet @@ -8,46 +8,46 @@ from .codec import Codec from .hwaccel import HWAccel class ThreadType(Flag): - NONE: ClassVar[ThreadType] - FRAME: ClassVar[ThreadType] - SLICE: ClassVar[ThreadType] - AUTO: ClassVar[ThreadType] + NONE = cast(ClassVar[ThreadType], ...) + FRAME = cast(ClassVar[ThreadType], ...) + SLICE = cast(ClassVar[ThreadType], ...) + AUTO = cast(ClassVar[ThreadType], ...) def __get__(self, i: object | None, owner: type | None = None) -> ThreadType: ... def __set__(self, instance: object, value: int | str | ThreadType) -> None: ... class Flags(IntEnum): - unaligned: int - qscale: int - four_mv: int - output_corrupt: int - qpel: int - drop_changed: int - recon_frame: int - copy_opaque: int - frame_duration: int - pass1: int - pass2: int - loop_filter: int - gray: int - psnr: int - interlaced_dct: int - low_delay: int - global_header: int - bitexact: int - ac_pred: int - interlaced_me: int - closed_gop: int + unaligned = cast(int, ...) + qscale = cast(int, ...) + four_mv = cast(int, ...) + output_corrupt = cast(int, ...) + qpel = cast(int, ...) + drop_changed = cast(int, ...) + recon_frame = cast(int, ...) + copy_opaque = cast(int, ...) + frame_duration = cast(int, ...) + pass1 = cast(int, ...) + pass2 = cast(int, ...) + loop_filter = cast(int, ...) + gray = cast(int, ...) + psnr = cast(int, ...) + interlaced_dct = cast(int, ...) + low_delay = cast(int, ...) + global_header = cast(int, ...) + bitexact = cast(int, ...) + ac_pred = cast(int, ...) + interlaced_me = cast(int, ...) + closed_gop = cast(int, ...) class Flags2(IntEnum): - fast: int - no_output: int - local_header: int - chunks: int - ignore_crop: int - show_all: int - export_mvs: int - skip_manual: int - ro_flush_noop: int + fast = cast(int, ...) + no_output = cast(int, ...) + local_header = cast(int, ...) + chunks = cast(int, ...) + ignore_crop = cast(int, ...) + show_all = cast(int, ...) + export_mvs = cast(int, ...) + skip_manual = cast(int, ...) + ro_flush_noop = cast(int, ...) class CodecContext: name: str diff --git a/av/codec/hwaccel.pyi b/av/codec/hwaccel.pyi index 84f1da964..8bdc0a6e0 100644 --- a/av/codec/hwaccel.pyi +++ b/av/codec/hwaccel.pyi @@ -1,29 +1,30 @@ from enum import IntEnum +from typing import cast from av.codec.codec import Codec from av.video.format import VideoFormat class HWDeviceType(IntEnum): - none: int - vdpau: int - cuda: int - vaapi: int - dxva2: int - qsv: int - videotoolbox: int - d3d11va: int - drm: int - opencl: int - mediacodec: int - vulkan: int - d3d12va: int + none = cast(int, ...) + vdpau = cast(int, ...) + cuda = cast(int, ...) + vaapi = cast(int, ...) + dxva2 = cast(int, ...) + qsv = cast(int, ...) + videotoolbox = cast(int, ...) + d3d11va = cast(int, ...) + drm = cast(int, ...) + opencl = cast(int, ...) + mediacodec = cast(int, ...) + vulkan = cast(int, ...) + d3d12va = cast(int, ...) class HWConfigMethod(IntEnum): - none: int - hw_device_ctx: int - hw_frame_ctx: int - internal: int - ad_hoc: int + none = cast(int, ...) + hw_device_ctx = cast(int, ...) + hw_frame_ctx = cast(int, ...) + internal = cast(int, ...) + ad_hoc = cast(int, ...) class HWConfig: @property diff --git a/av/container/core.pyi b/av/container/core.pyi index fa996d0d2..d61d07110 100644 --- a/av/container/core.pyi +++ b/av/container/core.pyi @@ -2,7 +2,7 @@ from enum import Flag, IntEnum from fractions import Fraction from pathlib import Path from types import TracebackType -from typing import Any, Callable, ClassVar, Literal, Type, overload +from typing import Any, Callable, ClassVar, Literal, Type, cast, overload from av.codec.hwaccel import HWAccel from av.format import ContainerFormat @@ -14,58 +14,58 @@ from .streams import StreamContainer Real = int | float | Fraction class Flags(Flag): - gen_pts: ClassVar[Flags] - ign_idx: ClassVar[Flags] - non_block: ClassVar[Flags] - ign_dts: ClassVar[Flags] - no_fillin: ClassVar[Flags] - no_parse: ClassVar[Flags] - no_buffer: ClassVar[Flags] - custom_io: ClassVar[Flags] - discard_corrupt: ClassVar[Flags] - flush_packets: ClassVar[Flags] - bitexact: ClassVar[Flags] - sort_dts: ClassVar[Flags] - fast_seek: ClassVar[Flags] - shortest: ClassVar[Flags] - auto_bsf: ClassVar[Flags] + gen_pts = cast(ClassVar[Flags], ...) + ign_idx = cast(ClassVar[Flags], ...) + non_block = cast(ClassVar[Flags], ...) + ign_dts = cast(ClassVar[Flags], ...) + no_fillin = cast(ClassVar[Flags], ...) + no_parse = cast(ClassVar[Flags], ...) + no_buffer = cast(ClassVar[Flags], ...) + custom_io = cast(ClassVar[Flags], ...) + discard_corrupt = cast(ClassVar[Flags], ...) + flush_packets = cast(ClassVar[Flags], ...) + bitexact = cast(ClassVar[Flags], ...) + sort_dts = cast(ClassVar[Flags], ...) + fast_seek = cast(ClassVar[Flags], ...) + shortest = cast(ClassVar[Flags], ...) + auto_bsf = cast(ClassVar[Flags], ...) class AudioCodec(IntEnum): - none: int - pcm_alaw: int - pcm_bluray: int - pcm_dvd: int - pcm_f16le: int - pcm_f24le: int - pcm_f32be: int - pcm_f32le: int - pcm_f64be: int - pcm_f64le: int - pcm_lxf: int - pcm_mulaw: int - pcm_s16be: int - pcm_s16be_planar: int - pcm_s16le: int - pcm_s16le_planar: int - pcm_s24be: int - pcm_s24daud: int - pcm_s24le: int - pcm_s24le_planar: int - pcm_s32be: int - pcm_s32le: int - pcm_s32le_planar: int - pcm_s64be: int - pcm_s64le: int - pcm_s8: int - pcm_s8_planar: int - pcm_u16be: int - pcm_u16le: int - pcm_u24be: int - pcm_u24le: int - pcm_u32be: int - pcm_u32le: int - pcm_u8: int - pcm_vidc: int + none = cast(int, ...) + pcm_alaw = cast(int, ...) + pcm_bluray = cast(int, ...) + pcm_dvd = cast(int, ...) + pcm_f16le = cast(int, ...) + pcm_f24le = cast(int, ...) + pcm_f32be = cast(int, ...) + pcm_f32le = cast(int, ...) + pcm_f64be = cast(int, ...) + pcm_f64le = cast(int, ...) + pcm_lxf = cast(int, ...) + pcm_mulaw = cast(int, ...) + pcm_s16be = cast(int, ...) + pcm_s16be_planar = cast(int, ...) + pcm_s16le = cast(int, ...) + pcm_s16le_planar = cast(int, ...) + pcm_s24be = cast(int, ...) + pcm_s24daud = cast(int, ...) + pcm_s24le = cast(int, ...) + pcm_s24le_planar = cast(int, ...) + pcm_s32be = cast(int, ...) + pcm_s32le = cast(int, ...) + pcm_s32le_planar = cast(int, ...) + pcm_s64be = cast(int, ...) + pcm_s64le = cast(int, ...) + pcm_s8 = cast(int, ...) + pcm_s8_planar = cast(int, ...) + pcm_u16be = cast(int, ...) + pcm_u16le = cast(int, ...) + pcm_u24be = cast(int, ...) + pcm_u24le = cast(int, ...) + pcm_u32be = cast(int, ...) + pcm_u32le = cast(int, ...) + pcm_u8 = cast(int, ...) + pcm_vidc = cast(int, ...) class Container: writeable: bool diff --git a/av/format.pyi b/av/format.pyi index b30a84bf6..81c5c42ca 100644 --- a/av/format.pyi +++ b/av/format.pyi @@ -1,26 +1,26 @@ __all__ = ("Flags", "ContainerFormat", "formats_available") from enum import Flag -from typing import ClassVar, Literal +from typing import ClassVar, Literal, cast class Flags(Flag): - no_file: ClassVar[Flags] - need_number: ClassVar[Flags] - show_ids: ClassVar[Flags] - global_header: ClassVar[Flags] - no_timestamps: ClassVar[Flags] - generic_index: ClassVar[Flags] - ts_discont: ClassVar[Flags] - variable_fps: ClassVar[Flags] - no_dimensions: ClassVar[Flags] - no_streams: ClassVar[Flags] - no_bin_search: ClassVar[Flags] - no_gen_search: ClassVar[Flags] - no_byte_seek: ClassVar[Flags] - allow_flush: ClassVar[Flags] - ts_nonstrict: ClassVar[Flags] - ts_negative: ClassVar[Flags] - seek_to_pts: ClassVar[Flags] + no_file = cast(ClassVar[Flags], ...) + need_number = cast(ClassVar[Flags], ...) + show_ids = cast(ClassVar[Flags], ...) + global_header = cast(ClassVar[Flags], ...) + no_timestamps = cast(ClassVar[Flags], ...) + generic_index = cast(ClassVar[Flags], ...) + ts_discont = cast(ClassVar[Flags], ...) + variable_fps = cast(ClassVar[Flags], ...) + no_dimensions = cast(ClassVar[Flags], ...) + no_streams = cast(ClassVar[Flags], ...) + no_bin_search = cast(ClassVar[Flags], ...) + no_gen_search = cast(ClassVar[Flags], ...) + no_byte_seek = cast(ClassVar[Flags], ...) + allow_flush = cast(ClassVar[Flags], ...) + ts_nonstrict = cast(ClassVar[Flags], ...) + ts_negative = cast(ClassVar[Flags], ...) + seek_to_pts = cast(ClassVar[Flags], ...) class ContainerFormat: def __init__(self, name: str, mode: Literal["r", "w"] | None = None) -> None: ... diff --git a/av/option.pyi b/av/option.pyi index 3132f4a02..f989a1138 100644 --- a/av/option.pyi +++ b/av/option.pyi @@ -1,34 +1,35 @@ from enum import Enum, Flag +from typing import cast class OptionType(Enum): - FLAGS: int - INT: int - INT64: int - DOUBLE: int - FLOAT: int - STRING: int - RATIONAL: int - BINARY: int - DICT: int - CONST: int - IMAGE_SIZE: int - PIXEL_FMT: int - SAMPLE_FMT: int - VIDEO_RATE: int - DURATION: int - COLOR: int - CHANNEL_LAYOUT: int - BOOL: int + FLAGS = cast(int, ...) + INT = cast(int, ...) + INT64 = cast(int, ...) + DOUBLE = cast(int, ...) + FLOAT = cast(int, ...) + STRING = cast(int, ...) + RATIONAL = cast(int, ...) + BINARY = cast(int, ...) + DICT = cast(int, ...) + CONST = cast(int, ...) + IMAGE_SIZE = cast(int, ...) + PIXEL_FMT = cast(int, ...) + SAMPLE_FMT = cast(int, ...) + VIDEO_RATE = cast(int, ...) + DURATION = cast(int, ...) + COLOR = cast(int, ...) + CHANNEL_LAYOUT = cast(int, ...) + BOOL = cast(int, ...) class OptionFlags(Flag): - ENCODING_PARAM: int - DECODING_PARAM: int - AUDIO_PARAM: int - VIDEO_PARAM: int - SUBTITLE_PARAM: int - EXPORT: int - READONLY: int - FILTERING_PARAM: int + ENCODING_PARAM = cast(int, ...) + DECODING_PARAM = cast(int, ...) + AUDIO_PARAM = cast(int, ...) + VIDEO_PARAM = cast(int, ...) + SUBTITLE_PARAM = cast(int, ...) + EXPORT = cast(int, ...) + READONLY = cast(int, ...) + FILTERING_PARAM = cast(int, ...) class BaseOption: name: str diff --git a/av/sidedata/sidedata.pyi b/av/sidedata/sidedata.pyi index d165513ab..0093fabd0 100644 --- a/av/sidedata/sidedata.pyi +++ b/av/sidedata/sidedata.pyi @@ -1,39 +1,39 @@ from collections.abc import Mapping from enum import Enum -from typing import ClassVar, Iterator, Sequence, overload +from typing import ClassVar, Iterator, Sequence, cast, overload from av.buffer import Buffer from av.frame import Frame class Type(Enum): - PANSCAN: ClassVar[Type] - A53_CC: ClassVar[Type] - STEREO3D: ClassVar[Type] - MATRIXENCODING: ClassVar[Type] - DOWNMIX_INFO: ClassVar[Type] - REPLAYGAIN: ClassVar[Type] - DISPLAYMATRIX: ClassVar[Type] - AFD: ClassVar[Type] - MOTION_VECTORS: ClassVar[Type] - SKIP_SAMPLES: ClassVar[Type] - AUDIO_SERVICE_TYPE: ClassVar[Type] - MASTERING_DISPLAY_METADATA: ClassVar[Type] - GOP_TIMECODE: ClassVar[Type] - SPHERICAL: ClassVar[Type] - CONTENT_LIGHT_LEVEL: ClassVar[Type] - ICC_PROFILE: ClassVar[Type] - S12M_TIMECODE: ClassVar[Type] - DYNAMIC_HDR_PLUS: ClassVar[Type] - REGIONS_OF_INTEREST: ClassVar[Type] - VIDEO_ENC_PARAMS: ClassVar[Type] - SEI_UNREGISTERED: ClassVar[Type] - FILM_GRAIN_PARAMS: ClassVar[Type] - DETECTION_BBOXES: ClassVar[Type] - DOVI_RPU_BUFFER: ClassVar[Type] - DOVI_METADATA: ClassVar[Type] - DYNAMIC_HDR_VIVID: ClassVar[Type] - AMBIENT_VIEWING_ENVIRONMENT: ClassVar[Type] - VIDEO_HINT: ClassVar[Type] + PANSCAN = cast(ClassVar[Type], ...) + A53_CC = cast(ClassVar[Type], ...) + STEREO3D = cast(ClassVar[Type], ...) + MATRIXENCODING = cast(ClassVar[Type], ...) + DOWNMIX_INFO = cast(ClassVar[Type], ...) + REPLAYGAIN = cast(ClassVar[Type], ...) + DISPLAYMATRIX = cast(ClassVar[Type], ...) + AFD = cast(ClassVar[Type], ...) + MOTION_VECTORS = cast(ClassVar[Type], ...) + SKIP_SAMPLES = cast(ClassVar[Type], ...) + AUDIO_SERVICE_TYPE = cast(ClassVar[Type], ...) + MASTERING_DISPLAY_METADATA = cast(ClassVar[Type], ...) + GOP_TIMECODE = cast(ClassVar[Type], ...) + SPHERICAL = cast(ClassVar[Type], ...) + CONTENT_LIGHT_LEVEL = cast(ClassVar[Type], ...) + ICC_PROFILE = cast(ClassVar[Type], ...) + S12M_TIMECODE = cast(ClassVar[Type], ...) + DYNAMIC_HDR_PLUS = cast(ClassVar[Type], ...) + REGIONS_OF_INTEREST = cast(ClassVar[Type], ...) + VIDEO_ENC_PARAMS = cast(ClassVar[Type], ...) + SEI_UNREGISTERED = cast(ClassVar[Type], ...) + FILM_GRAIN_PARAMS = cast(ClassVar[Type], ...) + DETECTION_BBOXES = cast(ClassVar[Type], ...) + DOVI_RPU_BUFFER = cast(ClassVar[Type], ...) + DOVI_METADATA = cast(ClassVar[Type], ...) + DYNAMIC_HDR_VIVID = cast(ClassVar[Type], ...) + AMBIENT_VIEWING_ENVIRONMENT = cast(ClassVar[Type], ...) + VIDEO_HINT = cast(ClassVar[Type], ...) class SideData(Buffer): type: Type diff --git a/av/stream.pyi b/av/stream.pyi index a2a2e439c..88dc7c00b 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -1,30 +1,30 @@ from enum import Flag from fractions import Fraction -from typing import Literal +from typing import Literal, cast from .codec import Codec, CodecContext from .container import Container class Disposition(Flag): - default: int - dub: int - original: int - comment: int - lyrics: int - karaoke: int - forced: int - hearing_impaired: int - visual_impaired: int - clean_effects: int - attached_pic: int - timed_thumbnails: int - non_diegetic: int - captions: int - descriptions: int - metadata: int - dependent: int - still_image: int - multilayer: int + default = cast(int, ...) + dub = cast(int, ...) + original = cast(int, ...) + comment = cast(int, ...) + lyrics = cast(int, ...) + karaoke = cast(int, ...) + forced = cast(int, ...) + hearing_impaired = cast(int, ...) + visual_impaired = cast(int, ...) + clean_effects = cast(int, ...) + attached_pic = cast(int, ...) + timed_thumbnails = cast(int, ...) + non_diegetic = cast(int, ...) + captions = cast(int, ...) + descriptions = cast(int, ...) + metadata = cast(int, ...) + dependent = cast(int, ...) + still_image = cast(int, ...) + multilayer = cast(int, ...) class Stream: name: str | None diff --git a/av/video/frame.pyi b/av/video/frame.pyi index 3533c8e3f..d650cf44c 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -16,14 +16,14 @@ _SupportedNDarray = Union[ ] class PictureType(IntEnum): - NONE: int - I: int - P: int - B: int - S: int - SI: int - SP: int - BI: int + NONE = 0 + I = 1 + P = 2 + B = 3 + S = 4 + SI = 5 + SP = 6 + BI = 7 class VideoFrame(Frame): format: VideoFormat diff --git a/av/video/reformatter.pyi b/av/video/reformatter.pyi index fd5dbd053..5d83fcbe3 100644 --- a/av/video/reformatter.pyi +++ b/av/video/reformatter.pyi @@ -1,41 +1,42 @@ from enum import IntEnum +from typing import cast from .frame import VideoFrame class Interpolation(IntEnum): - FAST_BILINEAER: int - BILINEAR: int - BICUBIC: int - X: int - POINT: int - AREA: int - BICUBLIN: int - GAUSS: int - SINC: int - LANCZOS: int - SPLINE: int + FAST_BILINEAER = cast(int, ...) + BILINEAR = cast(int, ...) + BICUBIC = cast(int, ...) + X = cast(int, ...) + POINT = cast(int, ...) + AREA = cast(int, ...) + BICUBLIN = cast(int, ...) + GAUSS = cast(int, ...) + SINC = cast(int, ...) + LANCZOS = cast(int, ...) + SPLINE = cast(int, ...) class Colorspace(IntEnum): - ITU709: int - FCC: int - ITU601: int - ITU624: int - SMPTE170M: int - SMPTE240M: int - DEFAULT: int - itu709: int - fcc: int - itu601: int - itu624: int - smpte170m: int - smpte240m: int - default: int + ITU709 = cast(int, ...) + FCC = cast(int, ...) + ITU601 = cast(int, ...) + ITU624 = cast(int, ...) + SMPTE170M = cast(int, ...) + SMPTE240M = cast(int, ...) + DEFAULT = cast(int, ...) + itu709 = cast(int, ...) + fcc = cast(int, ...) + itu601 = cast(int, ...) + itu624 = cast(int, ...) + smpte170m = cast(int, ...) + smpte240m = cast(int, ...) + default = cast(int, ...) class ColorRange(IntEnum): - UNSPECIFIED: int - MPEG: int - JPEG: int - NB: int + UNSPECIFIED = 0 + MPEG = 1 + JPEG = 2 + NB = 3 class VideoReformatter: def reformat( From 6b03e4ea3bc4b7e92b872a38d59d87fece8f9cf0 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 5 Feb 2025 21:46:21 -0500 Subject: [PATCH 224/365] Upgrade mypy to 1.15 --- Makefile | 2 +- av/error.pyi | 4 ++-- av/error.pyx | 8 +++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 2afb2e4c3..667ff3116 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ fate-suite: rsync -vrltLW rsync://fate-suite.ffmpeg.org/fate-suite/ tests/assets/fate-suite/ lint: - $(PIP) install -U black isort flake8 flake8-pyproject pillow numpy mypy==1.13.0 pytest + $(PIP) install -U black isort flake8 flake8-pyproject pillow numpy mypy==1.15.0 pytest black --check av examples tests setup.py flake8 av isort --check-only --diff av examples tests diff --git a/av/error.pyi b/av/error.pyi index abbe2188c..e18f7334c 100644 --- a/av/error.pyi +++ b/av/error.pyi @@ -8,8 +8,8 @@ def tag_to_code(tag: bytes) -> int: ... def err_check(res: int, filename: str | None = None) -> int: ... class FFmpegError(Exception): - errno: int - strerror: str + errno: int | None + strerror: str | None filename: str log: tuple[int, tuple[int, str, str] | None] diff --git a/av/error.pyx b/av/error.pyx index ea2b9d216..b8b415207 100644 --- a/av/error.pyx +++ b/av/error.pyx @@ -95,11 +95,13 @@ class FFmpegError(Exception): pass def __str__(self): - msg = f"[Errno {self.errno}] {self.strerror}" - + msg = "" + if self.errno is not None: + msg = f"{msg}[Errno {self.errno}] " + if self.strerror is not None: + msg = f"{msg}{self.strerror}" if self.filename: msg = f"{msg}: {self.filename!r}" - if self.log: msg = f"{msg}; last error log: [{self.log[1].strip()}] {self.log[2].strip()}" From 003c6ed3bd94e16110fc76c8e0a1dbe5dd2ae1ec Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 7 Feb 2025 09:03:38 -0500 Subject: [PATCH 225/365] Check av_buffersrc_add_frame --- av/filter/loudnorm_impl.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/av/filter/loudnorm_impl.c b/av/filter/loudnorm_impl.c index 79f960080..f6e22e4ce 100644 --- a/av/filter/loudnorm_impl.c +++ b/av/filter/loudnorm_impl.c @@ -144,11 +144,13 @@ char* loudnorm_get_stats( // Flush decoder avcodec_send_packet(codec_ctx, NULL); while (avcodec_receive_frame(codec_ctx, frame) >= 0) { - av_buffersrc_add_frame(src_ctx, frame); + ret = av_buffersrc_add_frame(src_ctx, frame); + if (ret < 0) goto end; } // Flush filter - av_buffersrc_add_frame(src_ctx, NULL); + ret = av_buffersrc_add_frame(src_ctx, NULL); + if (ret < 0) goto end; while (av_buffersink_get_frame(sink_ctx, filt_frame) >= 0) { av_frame_unref(filt_frame); } @@ -196,4 +198,4 @@ char* loudnorm_get_stats( av_log_set_callback(av_log_default_callback); return result; -} \ No newline at end of file +} From 3b1b44dcd9047584d2a91c9c525b4949b2b5e502 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 9 Feb 2025 17:21:11 -0500 Subject: [PATCH 226/365] Drop MacOS 11 --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9ef90a4e3..5bf768a28 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -53,7 +53,7 @@ jobs: - name: Set Minimum MacOS Target if: matrix.os == 'macos-13' || matrix.os == 'macos-14' run: | - echo "MACOSX_DEPLOYMENT_TARGET=11.0" >> $GITHUB_ENV + echo "MACOSX_DEPLOYMENT_TARGET=12.0" >> $GITHUB_ENV - name: Build wheels env: CIBW_ARCHS: ${{ matrix.arch }} From e228c9c3f06d309dea19e71701522e3d6d71c4f8 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 11 Feb 2025 14:38:37 -0500 Subject: [PATCH 227/365] Pin ubuntu version --- .github/workflows/smoke.yml | 10 +++++----- .github/workflows/tests.yml | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index e4bd3af4b..416f6fecb 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -14,7 +14,7 @@ on: - '**.txt' jobs: style: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v4 @@ -34,9 +34,9 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-latest, python: "3.12", ffmpeg: "7.1", extras: true} - - {os: ubuntu-latest, python: "3.9", ffmpeg: "7.0.2"} - - {os: ubuntu-latest, python: "pypy3.10", ffmpeg: "7.1"} + - {os: ubuntu-24.04, python: "3.12", ffmpeg: "7.1", extras: true} + - {os: ubuntu-24.04, python: "3.9", ffmpeg: "7.0.2"} + - {os: ubuntu-24.04, python: "pypy3.10", ffmpeg: "7.1"} - {os: macos-14, python: "3.9", ffmpeg: "7.1"} - {os: macos-14, python: "3.9", ffmpeg: "7.0.2"} @@ -56,7 +56,7 @@ jobs: - name: OS Packages run: | case ${{ matrix.config.os }} in - ubuntu-latest) + ubuntu-24.04) sudo apt-get update sudo apt-get install autoconf automake build-essential cmake \ libtool pkg-config nasm zlib1g-dev libvorbis-dev libx264-dev diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5bf768a28..a6c5232b3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,7 +5,7 @@ on: workflow_dispatch: jobs: package-source: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 @@ -34,9 +34,9 @@ jobs: arch: x86_64 - os: ubuntu-24.04-arm arch: aarch64 - - os: ubuntu-latest + - os: ubuntu-24.04 arch: i686 - - os: ubuntu-latest + - os: ubuntu-24.04 arch: x86_64 - os: windows-latest arch: AMD64 From ebb839db6921946770cc767f176dc2cf9e6a6b7a Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 12 Feb 2025 18:21:20 -0500 Subject: [PATCH 228/365] Add supported_np_pix_fmts --- av/video/frame.pyi | 2 ++ av/video/frame.pyx | 13 +++++++++++++ tests/test_videoframe.py | 18 ++++++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/av/video/frame.pyi b/av/video/frame.pyi index d650cf44c..bba60cc5d 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -15,6 +15,8 @@ _SupportedNDarray = Union[ np.ndarray[Any, np.dtype[np.float32]], ] +supported_np_pix_fmts: set[str] + class PictureType(IntEnum): NONE = 0 I = 1 diff --git a/av/video/frame.pyx b/av/video/frame.pyx index b74d58d1c..bb8f83486 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -12,6 +12,19 @@ from av.video.plane cimport VideoPlane cdef object _cinit_bypass_sentinel +# `pix_fmt`s supported by Frame.to_ndarray() and Frame.from_ndarray() +supported_np_pix_fmts = { + "abgr", "argb", "bayer_bggr16be", "bayer_bggr16le", "bayer_bggr8", "bayer_gbrg16be", + "bayer_gbrg16le", "bayer_gbrg8", "bayer_grbg16be", "bayer_grbg16le", "bayer_grbg8", + "bayer_rggb16be", "bayer_rggb16le", "bayer_rggb8", "bgr24", "bgr8", "bgra", + "gbrapf32be", "gbrapf32le", "gbrp", "gbrp10be", "gbrp10le", "gbrp12be", "gbrp12le", + "gbrp14be", "gbrp14le", "gbrp16be", "gbrp16le", "gbrpf32be", "gbrpf32le", "gray", + "gray16be", "gray16le", "gray8", "grayf32be", "grayf32le", "nv12", "pal8", "rgb24", + "rgb48be", "rgb48le", "rgb8", "rgba", "rgba64be", "rgba64le", "yuv420p", "yuv444p", + "yuv444p16be", "yuv444p16le", "yuva444p16be", "yuva444p16le", "yuvj420p", + "yuvj444p", "yuyv422", +} + cdef VideoFrame alloc_video_frame(): """Get a mostly uninitialized VideoFrame. diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 9581a7c41..5ae603f64 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -209,6 +209,7 @@ def test_ndarray_gray() -> None: array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) for format in ("gray", "gray8"): frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == "gray" assertNdarraysEqual(frame.to_ndarray(), array) @@ -218,6 +219,7 @@ def test_ndarray_gray_align() -> None: array = numpy.random.randint(0, 256, size=(238, 318), dtype=numpy.uint8) for format in ("gray", "gray8"): frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts assert frame.width == 318 and frame.height == 238 assert frame.format.name == "gray" assertNdarraysEqual(frame.to_ndarray(), array) @@ -227,6 +229,7 @@ def test_ndarray_grayf32() -> None: array = numpy.random.random_sample(size=(480, 640)).astype(numpy.float32) for format in ("grayf32be", "grayf32le"): frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -236,6 +239,7 @@ def test_ndarray_grayf32_align() -> None: array = numpy.random.random_sample(size=(238, 318)).astype(numpy.float32) for format in ("grayf32be", "grayf32le"): frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts assert frame.width == 318 and frame.height == 238 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -245,6 +249,7 @@ def test_ndarray_rgb() -> None: array = numpy.random.randint(0, 256, size=(480, 640, 3), dtype=numpy.uint8) for format in ("rgb24", "bgr24"): frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -254,6 +259,7 @@ def test_ndarray_rgb_align() -> None: array = numpy.random.randint(0, 256, size=(238, 318, 3), dtype=numpy.uint8) for format in ("rgb24", "bgr24"): frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts assert frame.width == 318 and frame.height == 238 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -263,6 +269,7 @@ def test_ndarray_rgba() -> None: array = numpy.random.randint(0, 256, size=(480, 640, 4), dtype=numpy.uint8) for format in ("argb", "rgba", "abgr", "bgra"): frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -272,6 +279,7 @@ def test_ndarray_rgba_align() -> None: array = numpy.random.randint(0, 256, size=(238, 318, 4), dtype=numpy.uint8) for format in ("argb", "rgba", "abgr", "bgra"): frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts assert frame.width == 318 and frame.height == 238 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -297,6 +305,7 @@ def test_ndarray_gbrp10() -> None: array = numpy.random.randint(0, 1024, size=(480, 640, 3), dtype=numpy.uint16) for format in ("gbrp10be", "gbrp10le"): frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -306,6 +315,7 @@ def test_ndarray_gbrp10_align() -> None: array = numpy.random.randint(0, 1024, size=(238, 318, 3), dtype=numpy.uint16) for format in ("gbrp10be", "gbrp10le"): frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts assert frame.width == 318 and frame.height == 238 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -315,6 +325,7 @@ def test_ndarray_gbrp12() -> None: array = numpy.random.randint(0, 4096, size=(480, 640, 3), dtype=numpy.uint16) for format in ("gbrp12be", "gbrp12le"): frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -324,6 +335,7 @@ def test_ndarray_gbrp12_align() -> None: array = numpy.random.randint(0, 4096, size=(238, 318, 3), dtype=numpy.uint16) for format in ("gbrp12be", "gbrp12le"): frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts assert frame.width == 318 and frame.height == 238 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -333,6 +345,7 @@ def test_ndarray_gbrp14() -> None: array = numpy.random.randint(0, 16384, size=(480, 640, 3), dtype=numpy.uint16) for format in ("gbrp14be", "gbrp14le"): frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -342,6 +355,7 @@ def test_ndarray_gbrp14_align() -> None: array = numpy.random.randint(0, 16384, size=(238, 318, 3), dtype=numpy.uint16) for format in ("gbrp14be", "gbrp14le"): frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts assert frame.width == 318 and frame.height == 238 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -351,6 +365,7 @@ def test_ndarray_gbrp16() -> None: array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) for format in ("gbrp16be", "gbrp16le"): frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -360,6 +375,7 @@ def test_ndarray_gbrp16_align() -> None: array = numpy.random.randint(0, 65536, size=(238, 318, 3), dtype=numpy.uint16) for format in ("gbrp16be", "gbrp16le"): frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts assert frame.width == 318 and frame.height == 238 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -603,6 +619,7 @@ def test_ndarray_pal8(): frame = VideoFrame.from_ndarray((array, palette), format="pal8") assert frame.width == 640 and frame.height == 480 assert frame.format.name == "pal8" + assert frame.format.name in av.video.frame.supported_np_pix_fmts returned = frame.to_ndarray() assert type(returned) is tuple and len(returned) == 2 assertNdarraysEqual(returned[0], array) @@ -614,6 +631,7 @@ def test_ndarray_nv12() -> None: frame = VideoFrame.from_ndarray(array, format="nv12") assert frame.width == 640 and frame.height == 480 assert frame.format.name == "nv12" + assert frame.format.name in av.video.frame.supported_np_pix_fmts assertNdarraysEqual(frame.to_ndarray(), array) From 0eca76eec24574fa965930f9a241754c1b374b83 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 12 Feb 2025 20:38:29 -0500 Subject: [PATCH 229/365] Add yuv422p10le pix_fmt --- av/video/frame.pyx | 42 ++++++++++++++++++++++++++++++++++++---- tests/test_videoframe.py | 9 +++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/av/video/frame.pyx b/av/video/frame.pyx index bb8f83486..4647ab81f 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -20,9 +20,9 @@ supported_np_pix_fmts = { "gbrapf32be", "gbrapf32le", "gbrp", "gbrp10be", "gbrp10le", "gbrp12be", "gbrp12le", "gbrp14be", "gbrp14le", "gbrp16be", "gbrp16le", "gbrpf32be", "gbrpf32le", "gray", "gray16be", "gray16le", "gray8", "grayf32be", "grayf32le", "nv12", "pal8", "rgb24", - "rgb48be", "rgb48le", "rgb8", "rgba", "rgba64be", "rgba64le", "yuv420p", "yuv444p", - "yuv444p16be", "yuv444p16le", "yuva444p16be", "yuva444p16le", "yuvj420p", - "yuvj444p", "yuyv422", + "rgb48be", "rgb48le", "rgb8", "rgba", "rgba64be", "rgba64le", "yuv420p", + "yuv422p10le", "yuv444p", "yuv444p16be", "yuv444p16le", "yuva444p16be", + "yuva444p16le", "yuvj420p", "yuvj444p", "yuyv422", } cdef VideoFrame alloc_video_frame(): @@ -322,7 +322,7 @@ cdef class VideoFrame(Frame): import numpy as np # check size - if frame.format.name in {"yuv420p", "yuvj420p", "yuyv422"}: + if frame.format.name in {"yuv420p", "yuvj420p", "yuyv422", "yuv422p10le"}: assert frame.width % 2 == 0, "the width has to be even for this pixel format" assert frame.height % 2 == 0, "the height has to be even for this pixel format" @@ -408,6 +408,18 @@ cdef class VideoFrame(Frame): useful_array(frame.planes[1]), useful_array(frame.planes[2]), ]).reshape(-1, frame.width) + if frame.format.name == "yuv422p10le": + # Read planes as uint16 at their original width + y = useful_array(frame.planes[0], 2, "uint16").reshape(frame.height, frame.width) + u = useful_array(frame.planes[1], 2, "uint16").reshape(frame.height, frame.width // 2) + v = useful_array(frame.planes[2], 2, "uint16").reshape(frame.height, frame.width // 2) + + # Double the width of U and V by repeating each value + u_full = np.repeat(u, 2, axis=1) + v_full = np.repeat(v, 2, axis=1) + if channel_last: + return np.stack([y, u_full, v_full], axis=2) + return np.stack([y, u_full, v_full], axis=0) if frame.format.name == "pal8": image = useful_array(frame.planes[0]).reshape(frame.height, frame.width) palette = np.frombuffer(frame.planes[1], "i4").astype(">i4").reshape(-1, 1).view(np.uint8) @@ -641,6 +653,28 @@ cdef class VideoFrame(Frame): copy_array_to_plane(flat[u_start:v_start], frame.planes[1], 1) copy_array_to_plane(flat[v_start:], frame.planes[2], 1) return frame + elif format == "yuv422p10le": + if not isinstance(array, np.ndarray) or array.dtype != np.uint16: + raise ValueError("Array must be uint16 type") + + # Convert to channel-first if needed + if channel_last and array.shape[2] == 3: + array = np.moveaxis(array, 2, 0) + elif not (array.shape[0] == 3): + raise ValueError("Array must have shape (3, height, width) or (height, width, 3)") + + height, width = array.shape[1:] + if width % 2 != 0 or height % 2 != 0: + raise ValueError("Width and height must be even") + + frame = VideoFrame(width, height, format) + copy_array_to_plane(array[0], frame.planes[0], 2) + # Subsample U and V by taking every other column + u = array[1, :, ::2].copy() # Need copy to ensure C-contiguous + v = array[2, :, ::2].copy() # Need copy to ensure C-contiguous + copy_array_to_plane(u, frame.planes[1], 2) + copy_array_to_plane(v, frame.planes[2], 2) + return frame elif format == "yuyv422": check_ndarray(array, "uint8", 3) check_ndarray_shape(array, array.shape[0] % 2 == 0) diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 5ae603f64..bef69550b 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -485,6 +485,15 @@ def test_ndarray_yuv444p16() -> None: assertNdarraysEqual(frame.to_ndarray(), array) +def test_ndarray_yuv422p10le() -> None: + array = numpy.random.randint(0, 65536, size=(3, 480, 640), dtype=numpy.uint16) + for format in ("yuv422p10le",): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assert format in av.video.frame.supported_np_pix_fmts + + def test_ndarray_yuv444p16_align() -> None: array = numpy.random.randint(0, 65536, size=(238, 318, 3), dtype=numpy.uint16) for format in ("yuv444p16be", "yuv444p16le"): From ac64a70bf25e6ea699ab0408ae38c19efac082de Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 14 Feb 2025 15:18:05 -0500 Subject: [PATCH 230/365] Add warnings when compiling from source --- setup.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 685412b99..3d4c11fac 100644 --- a/setup.py +++ b/setup.py @@ -6,6 +6,31 @@ import shlex import subprocess import sys +from time import sleep + + +def is_virtualenv(): + return sys.base_prefix != sys.prefix + + +if platform.system() == "Darwin": + major_version = int(platform.mac_ver()[0].split(".")[0]) + if major_version < 12: + print( + "\033[1;91mWarning!\033[0m You are using an EOL, unsupported, and out-of-date OS." + ) + sleep(3) + +# Don't show message when using our project tooling. +if not is_virtualenv() or os.getenv("_PYAV_ACTIVATED", "") != "1": + print( + "\n\033[1;91mWarning!\033[0m You are installing from source.\n" + "It is \033[1;37mEXPECTED\033[0m that it will fail. You are \033[1;37mREQUIRED\033[0m" + " to use ffmpeg 7.\nYou \033[1;37mMUST\033[0m have Cython, pkg-config, and a C compiler.\n" + ) + if os.getenv("GITHUB_ACTIONS") != "true": + sleep(3) + from Cython.Build import cythonize from Cython.Compiler.AutoDocTransforms import EmbedSignature @@ -141,7 +166,7 @@ def parse_cflags(raw_flags): cythonize = lambda ext, **kwargs: [ext] use_pkg_config = False -# Locate FFmpeg libraries and headers. +# Locate ffmpeg libraries and headers. if FFMPEG_DIR is not None: extension_extra = get_config_from_directory(FFMPEG_DIR) elif use_pkg_config: From b2693f5250ad62a2f3147960363a77aebb4cdac8 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 14 Feb 2025 16:41:05 -0500 Subject: [PATCH 231/365] Update authors --- AUTHORS.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 846cf0d93..1792ba863 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -5,8 +5,8 @@ All contributors (by number of commits): - Mike Boers ; `@mikeboers `_ -* Jeremy Lainé ; `@jlaine `_ * WyattBlue ; `@WyattBlue `_ +* Jeremy Lainé ; `@jlaine `_ - Mark Reid ; `@markreidvfx `_ @@ -38,6 +38,7 @@ All contributors (by number of commits): - Ulrik Mikaelsson ; `@rawler `_ - Wel C. van der - Will Patera ; `@willpatera `_ +- z-khan * Joe Schiff <41972063+JoeSchiff@users.noreply.github.com> * Dexer <73297572+DexerBR@users.noreply.github.com> @@ -77,6 +78,7 @@ All contributors (by number of commits): * Johan Jeppsson Karlin * Philipp Klaus * Kim Minjong +* Matteo Destro * Mattias Wadman * Manuel Goacolou * Julian Schweizer @@ -102,4 +104,3 @@ All contributors (by number of commits): * Koichi Akabe * David Joy * Sviatoslav Sydorenko (Святослав Сидоренко) -* z-khan From e2a489857f90833c6ed2ac466f7d89508857eb18 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 14 Feb 2025 16:46:50 -0500 Subject: [PATCH 232/365] Update Changelog --- CHANGELOG.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ead5b6a74..79cca4c55 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,6 +17,21 @@ We are operating with `semantic versioning `_. are merged into the "default" branch. +v14.2.0 (Unreleased) +-------------------- + +Features: + +- Add support for external flags in hwacccel by :gh-user:`materight` in (:pr:`1751`). +- Add Bayer pixel formats by :gh-user:`z-khan` in (:pr:`1755`). +- Add support for yuv422p10le pix_fmt by :gh-user:`WyattBlue` in (:pr:`1767`). +- Add ``supported_np_pix_fmts`` by :gh-user:`WyattBlue` in (:pr:`1766`). + +Misc. +- Drop support for MacOS 11 by :gh-user:`WyattBlue` in (:pr:`1764`). +- Add warnings when compiling from source by :gh-user:`WyattBlue` in (:pr:`1773`). + + v14.1.0 ------- From 7d10213de6d221f878ea73fd2bcc5a35c271b0be Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 16 Feb 2025 11:27:48 -0500 Subject: [PATCH 233/365] Exclude author files --- AUTHORS.py | 1 - setup.py | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/AUTHORS.py b/AUTHORS.py index f17b31aeb..797e423ed 100644 --- a/AUTHORS.py +++ b/AUTHORS.py @@ -78,7 +78,6 @@ last = None block_i = 0 for email, count in sorted(email_count.items(), key=lambda x: (-x[1], x[0])): - # This is the natural log, because of course it should be. ;) order = int(math.log(count)) if last and last != order: diff --git a/setup.py b/setup.py index 3d4c11fac..d0bee300b 100644 --- a/setup.py +++ b/setup.py @@ -279,7 +279,9 @@ def parse_cflags(raw_flags): author="Mike Boers", author_email="pyav@mikeboers.com", url="https://github.com/PyAV-Org/PyAV", - packages=find_packages(exclude=["build*", "examples*", "tests*", "include*"]) + packages=find_packages( + exclude=["build*", "examples*", "tests*", "include*", "AUTHORS*"] + ) + list(package_dir.keys()), package_dir=package_dir, package_data=package_data, From 4f61921242be73eb8e612c5334528757f489ca7e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 16 Feb 2025 16:28:39 -0500 Subject: [PATCH 234/365] Stop publishing sdist sdist takes ~4MB of space, is neglected by PyPA, and is a bad experience for both the developer and the user. PyAV does not have universal support for every arch, and removing source builds in PyPI does not change that. --- .github/workflows/tests.yml | 20 +-------- MANIFEST.in | 7 --- README.md | 16 ++----- pyproject.toml | 51 +++++++++++++++++++++- setup.py | 85 +++++-------------------------------- 5 files changed, 63 insertions(+), 116 deletions(-) delete mode 100644 MANIFEST.in diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a6c5232b3..e9e4003b9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,24 +4,6 @@ on: types: [published] workflow_dispatch: jobs: - package-source: - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - name: Build source package - run: | - pip install setuptools cython - python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.1.json /tmp/vendor - PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig python setup.py sdist - - name: Upload source package - uses: actions/upload-artifact@v4 - with: - name: dist-source - path: dist/ - package-wheel: runs-on: ${{ matrix.os }} strategy: @@ -82,7 +64,7 @@ jobs: publish: runs-on: ubuntu-latest - needs: [package-source, package-wheel] + needs: [package-wheel] steps: - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 6539f496d..000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,7 +0,0 @@ -include *.txt *.md -recursive-include av *.pyx *.pxd -recursive-include docs *.rst *.py -recursive-include examples *.py -recursive-include include *.pxd *.h -recursive-include src/av *.c *.h -recursive-include tests *.py diff --git a/README.md b/README.md index 7e21198e7..232fd6743 100644 --- a/README.md +++ b/README.md @@ -32,20 +32,10 @@ conda install av -c conda-forge See the [Conda install][conda-install] docs to get started with (mini)Conda. -Alternative installation methods --------------------------------- +Installing From Source +---------------------- -Due to the complexity of the dependencies, PyAV is not always the easiest Python package to install from source. If you want to use your existing ffmpeg (must be the correct major version), the source version of PyAV is on [PyPI][pypi]: - -```bash -pip install av --no-binary av -``` - -> [!WARNING] -> This installation method won't work for Windows or Debian based systems. - - -And if you want to build from the absolute source (POSIX only): +Here's how to build PyAV from source source. You must use [MSYS2](https://www.msys2.org/) when using Windows. ```bash git clone https://github.com/PyAV-Org/PyAV.git diff --git a/pyproject.toml b/pyproject.toml index 73e3cf334..15ed77023 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,52 @@ [build-system] -requires = ["setuptools", "cython"] +requires = ["setuptools>61", "cython>=3,<4"] + +[project] +name = "av" +description = "Pythonic bindings for FFmpeg's libraries." +readme = "README.md" +license = {text = "BSD-3-Clause"} +authors = [ + {name = "WyattBlue", email = "wyattblue@auto-editor.com"}, + {name = "Jeremy Lainé", email = "jeremy.laine@m4x.org"}, +] +requires-python = ">=3.9" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Natural Language :: English", + "Operating System :: MacOS :: MacOS X", + "Operating System :: POSIX", + "Operating System :: Unix", + "Operating System :: Microsoft :: Windows", + "Programming Language :: Cython", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Multimedia :: Sound/Audio", + "Topic :: Multimedia :: Sound/Audio :: Conversion", + "Topic :: Multimedia :: Video", + "Topic :: Multimedia :: Video :: Conversion", +] +dynamic = ["version"] + +[tool.setuptools] +zip-safe = false + +[tool.setuptools.dynamic] +version = {attr = "av.about.__version__"} + +[project.urls] +"Bug Tracker" = "https://github.com/PyAV-Org/PyAV/discussions/new?category=4-bugs" +"Source Code" = "https://github.com/PyAV-Org/PyAV" +homepage = "https://pyav.basswood-io.com" + +[project.scripts] +"pyav" = "av.__main__:main" [tool.isort] profile = "black" @@ -13,4 +60,4 @@ max-line-length = 142 per-file-ignores = [ "__init__.py:E402,F401", "*.pyx,*.pxd:E211,E225,E227,E402,E999", -] +] \ No newline at end of file diff --git a/setup.py b/setup.py index d0bee300b..bef384515 100644 --- a/setup.py +++ b/setup.py @@ -21,22 +21,21 @@ def is_virtualenv(): ) sleep(3) -# Don't show message when using our project tooling. -if not is_virtualenv() or os.getenv("_PYAV_ACTIVATED", "") != "1": - print( - "\n\033[1;91mWarning!\033[0m You are installing from source.\n" - "It is \033[1;37mEXPECTED\033[0m that it will fail. You are \033[1;37mREQUIRED\033[0m" - " to use ffmpeg 7.\nYou \033[1;37mMUST\033[0m have Cython, pkg-config, and a C compiler.\n" - ) - if os.getenv("GITHUB_ACTIONS") != "true": - sleep(3) +print( + "\n\033[1;91mWarning!\033[0m You are installing from source.\n" + "It is \033[1;37mEXPECTED\033[0m that it will fail. You are \033[1;37mREQUIRED\033[0m" + " to use ffmpeg 7.\nYou \033[1;37mMUST\033[0m have Cython, pkg-config, and a C compiler.\n" +) +if os.getenv("GITHUB_ACTIONS") == "true" or is_virtualenv(): + pass +else: + print("\033[1;91mWarning!\033[0m You are not using a virtual environment") from Cython.Build import cythonize from Cython.Compiler.AutoDocTransforms import EmbedSignature from setuptools import Extension, find_packages, setup - FFMPEG_LIBRARIES = [ "avformat", "avcodec", @@ -202,12 +201,8 @@ def parse_cflags(raw_flags): include_path=["include"], ) -# Construct the modules that we find in the "av" directory. for dirname, dirnames, filenames in os.walk("av"): for filename in filenames: - if filename == "loudnorm.pyx": - continue - # We are looking for Cython sources. if filename.startswith(".") or os.path.splitext(filename)[1] != ".pyx": continue @@ -242,74 +237,14 @@ def parse_cflags(raw_flags): for cfile in ext.sources: insert_enum_in_generated_files(cfile) -# Read package metadata -about = {} -about_file = os.path.join(os.path.dirname(__file__), "av", "about.py") -with open(about_file, encoding="utf-8") as fp: - exec(fp.read(), about) package_folders = pathlib.Path("av").glob("**/") package_data = { ".".join(pckg.parts): ["*.pxd", "*.pyi", "*.typed"] for pckg in package_folders } -# Add include/ headers to av.include -package_dir = { - ".".join(["av", *pckg.parts]): str(pckg) - for pckg in pathlib.Path("include").glob("**/") -} -package_data.update({pckg: ["*.pxd"] for pckg in package_dir}) - - -with open("README.md") as f: - long_description = f.read() - setup( - name="av", - version=about["__version__"], - description="Pythonic bindings for FFmpeg's libraries.", - long_description=long_description, - long_description_content_type="text/markdown", - license="BSD", - project_urls={ - "Bug Reports": "https://github.com/PyAV-Org/PyAV/discussions/new?category=4-bugs", - "Documentation": "https://pyav.basswood-io.com", - "Download": "https://pypi.org/project/av", - }, - author="Mike Boers", - author_email="pyav@mikeboers.com", - url="https://github.com/PyAV-Org/PyAV", - packages=find_packages( - exclude=["build*", "examples*", "tests*", "include*", "AUTHORS*"] - ) - + list(package_dir.keys()), - package_dir=package_dir, + packages=find_packages(include=["av*"]), package_data=package_data, - python_requires=">=3.9", - zip_safe=False, ext_modules=ext_modules, - entry_points={ - "console_scripts": ["pyav = av.__main__:main"], - }, - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "License :: OSI Approved :: BSD License", - "Natural Language :: English", - "Operating System :: MacOS :: MacOS X", - "Operating System :: POSIX", - "Operating System :: Unix", - "Operating System :: Microsoft :: Windows", - "Programming Language :: Cython", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: Multimedia :: Sound/Audio", - "Topic :: Multimedia :: Sound/Audio :: Conversion", - "Topic :: Multimedia :: Video", - "Topic :: Multimedia :: Video :: Conversion", - ], ) From 4a7287a3343f16cf8f7761f88e61149729cb4e43 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 18 Feb 2025 13:57:50 -0500 Subject: [PATCH 235/365] Add opaque param to add_stream_from_template --- av/container/output.pyi | 4 +++- av/container/output.pyx | 16 +++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/av/container/output.pyi b/av/container/output.pyi index a7c89452f..3169f486b 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -38,7 +38,9 @@ class OutputContainer(Container): options: dict[str, str] | None = None, **kwargs, ) -> VideoStream | AudioStream | SubtitleStream: ... - def add_stream_from_template(self, template: _StreamT, **kwargs) -> _StreamT: ... + def add_stream_from_template( + self, template: _StreamT, opaque: bool | None = None, **kwargs + ) -> _StreamT: ... def add_data_stream( self, codec_name: str | None = None, options: dict[str, str] | None = None ) -> DataStream: ... diff --git a/av/container/output.pyx b/av/container/output.pyx index c2055486e..7140c64b5 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -129,20 +129,24 @@ cdef class OutputContainer(Container): return py_stream - def add_stream_from_template(self, Stream template not None, **kwargs): + def add_stream_from_template(self, Stream template not None, opaque=None, **kwargs): """ Creates a new stream from a template. Supports video, audio, and subtitle streams. :param template: Copy codec from another :class:`~av.stream.Stream` instance. + :param opaque: If True, copy opaque data from the template's codec context. :param \\**kwargs: Set attributes for the stream. :rtype: The new :class:`~av.stream.Stream`. """ cdef const lib.AVCodec *codec cdef Codec codec_obj - if template.type != "video": + if opaque is None: + opaque = template.type != "video" + + if opaque: # Copy ctx from template. codec_obj = template.codec_context.codec - else: + else: # Construct new codec object. codec_obj = Codec(template.codec_context.codec.name, "w") codec = codec_obj.ptr @@ -164,10 +168,8 @@ cdef class OutputContainer(Container): if self.ptr.oformat.flags & lib.AVFMT_GLOBALHEADER: codec_context.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER - # Initialise stream codec parameters to populate the codec type. - # - # Subsequent changes to the codec context will be applied just before - # encoding starts in `start_encoding()`. + # Initialize stream codec parameters to populate the codec type. Subsequent changes to + # the codec context will be applied just before encoding starts in `start_encoding()`. err_check(lib.avcodec_parameters_from_context(stream.codecpar, codec_context)) # Construct the user-land stream From 05b38be84f3281ccb7e77455d65e20b33d21194b Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 18 Feb 2025 15:07:48 -0500 Subject: [PATCH 236/365] Use up-to-date Sphinx --- docs/Makefile | 11 +- docs/conf.py | 210 +++---------------- docs/development/includes.py | 365 ---------------------------------- docs/development/includes.rst | 2 - docs/generate-tagfile | 32 --- 5 files changed, 28 insertions(+), 592 deletions(-) delete mode 100644 docs/development/includes.py delete mode 100644 docs/development/includes.rst delete mode 100755 docs/generate-tagfile diff --git a/docs/Makefile b/docs/Makefile index bb84c1dba..7efd36bac 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,7 +1,6 @@ SPHINXOPTS = SPHINXBUILD = sphinx-build BUILDDIR = _build -FFMPEGDIR = _ffmpeg PYAV_PIP ?= pip PIP := $(PYAV_PIP) ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(SPHINXOPTS) . @@ -10,14 +9,6 @@ ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(SPHINXOPTS) . default: html -TAGFILE := _build/doxygen/tagfile.xml - -$(TAGFILE): - @if [ ! -d "$(FFMPEGDIR)" ]; then \ - git clone --depth=1 git://source.ffmpeg.org/ffmpeg.git $(FFMPEGDIR); \ - fi - ./generate-tagfile --library $(FFMPEGDIR) -o $(TAGFILE) - TEMPLATES := $(wildcard api/*.py development/*.py) RENDERED := $(TEMPLATES:%.py=_build/rst/%.rst) @@ -30,7 +21,7 @@ clean: rm -rf $(BUILDDIR) $(FFMPEGDIR) html: $(RENDERED) $(TAGFILE) - $(PIP) install -U sphinx==5.1.0 + $(PIP) install -U sphinx $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html test: diff --git a/docs/conf.py b/docs/conf.py index 789fc0756..1f2541f32 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,23 +1,10 @@ -import logging import os import re import sys -import xml.etree.ElementTree as etree +import sphinx from docutils import nodes -from sphinx import addnodes from sphinx.util.docutils import SphinxDirective -import sphinx - - -logging.basicConfig() - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath("..")) - -# -- General configuration ----------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. @@ -31,37 +18,24 @@ "sphinx.ext.doctest", ] - # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] - -# The suffix of source filenames. source_suffix = ".rst" - -# The master toctree document. master_doc = "index" - -# General information about the project. project = "PyAV" copyright = "2025, The PyAV Team" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. -# + about = {} with open("../av/about.py") as fp: exec(fp.read(), about) -# The full version, including alpha/beta/rc tags. release = about["__version__"] - -# The short X.Y version. version = release.split("-")[0] - exclude_patterns = ["_build"] - -# The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # -- Options for HTML output --------------------------------------------------- @@ -85,7 +59,6 @@ doctest_global_setup = """ - import errno import os @@ -109,29 +82,20 @@ def sandboxed(*args, **kwargs): os.chdir(here) video_path = curated('pexels/time-lapse-video-of-night-sky-857195.mp4') - """ -doctest_global_cleanup = """ - -os.chdir(_cwd) - -""" - - +doctest_global_cleanup = "os.chdir(_cwd)" doctest_test_doctest_blocks = "" - extlinks = { - "ffstruct": ("http://ffmpeg.org/doxygen/trunk/struct%s.html", "struct "), - "issue": ("https://github.com/PyAV-Org/PyAV/issues/%s", "#"), - "pr": ("https://github.com/PyAV-Org/PyAV/pull/%s", "#"), - "gh-user": ("https://github.com/%s", "@"), + "ffmpeg": ("https://ffmpeg.org/doxygen/trunk/%s.html", "%s"), + "ffstruct": ("https://ffmpeg.org/doxygen/trunk/struct%s.html", "struct %s"), + "issue": ("https://github.com/PyAV-Org/PyAV/issues/%s", "#%s"), + "pr": ("https://github.com/PyAV-Org/PyAV/pull/%s", "#%s"), + "gh-user": ("https://github.com/%s", "@%s"), } -intersphinx_mapping = { - "https://docs.python.org/3": None, -} +intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} autodoc_member_order = "bysource" autodoc_default_options = { @@ -238,152 +202,32 @@ def makerow(*texts): ) seen = set() - if hasattr(enum, "_by_name"): # Our custom enum class - enum_items = enum._by_name.items() - for name, item in enum_items: - if name.lower() in seen: - continue - seen.add(name.lower()) - - try: - attr = properties[item] - except KeyError: - if cls: - continue - attr = None - - value = f"0x{item.value:X}" - doc = item.__doc__ or "-" - tbody += makerow(attr, name, value, doc) - - return [table] - else: # Standard IntEnum - enum_items = [ - (name, item) - for name, item in vars(enum).items() - if isinstance(item, enum) - ] - for name, item in enum_items: - if name.lower() in seen: - continue - seen.add(name.lower()) - - try: - attr = properties[item] - except KeyError: - if cls: - continue - attr = None - - value = f"0x{item.value:X}" - doc = enum.__annotations__.get(name, "---")[1:-1] - tbody += makerow(attr, name, value, doc) - - return [table] - - -doxylink = {} -ffmpeg_tagfile = os.path.abspath( - os.path.join(__file__, "..", "_build", "doxygen", "tagfile.xml") -) -if not os.path.exists(ffmpeg_tagfile): - print("ERROR: Missing FFmpeg tagfile.") - exit(1) -doxylink["ffmpeg"] = (ffmpeg_tagfile, "https://ffmpeg.org/doxygen/trunk/") - - -def doxylink_create_handler(app, file_name, url_base): - print("Finding all names in Doxygen tagfile", file_name) - - doc = etree.parse(file_name) - root = doc.getroot() - - parent_map = {} # ElementTree doesn't five us access to parents. - urls = {} - - for node in root.findall(".//name/.."): - for child in node: - parent_map[child] = node - - kind = node.attrib["kind"] - if kind not in ("function", "struct", "variable"): - continue - - name = node.find("name").text - - if kind not in ("function",): - parent = parent_map.get(node) - parent_name = parent.find("name") if parent else None - if parent_name is not None: - name = f"{parent_name.text}.{name}" - - filenode = node.find("filename") - if filenode is not None: - url = filenode.text - else: - url = "{}#{}".format( - node.find("anchorfile").text, - node.find("anchor").text, - ) - - urls.setdefault(kind, {})[name] = url - - def get_url(name): - # These are all the kinds that seem to exist. - for kind in ( - "function", - "struct", - "variable", # These are struct members. - # 'class', - # 'define', - # 'enumeration', - # 'enumvalue', - # 'file', - # 'group', - # 'page', - # 'typedef', - # 'union', - ): + enum_items = [ + (name, item) + for name, item in vars(enum).items() + if isinstance(item, enum) + ] + for name, item in enum_items: + if name.lower() in seen: + continue + seen.add(name.lower()) + try: - return urls[kind][name] + attr = properties[item] except KeyError: - pass - - def _doxylink_handler(name, rawtext, text, lineno, inliner, options={}, content=[]): - m = re.match(r"^(.+?)(?:<(.+?)>)?$", text) - title, name = m.groups() - name = name or title - - url = get_url(name) - if not url: - if name == "AVFrame.color_primaries": - url = "structAVFrame.html#a59a3f830494f2ed1133103a1bc9481e7" - elif name == "AVFrame.color_trc": - url = "structAVFrame.html#ab09abb126e3922bc1d010cf044087939" - else: - print("ERROR: Could not find", name) - exit(1) - - node = addnodes.literal_strong(title, title) - if url: - url = url_base + url - node = nodes.reference("", "", node, refuri=url) + if cls: + continue + attr = None - return [node], [] + value = f"0x{item.value:X}" + doc = enum.__annotations__.get(name, "---")[1:-1] + tbody += makerow(attr, name, value, doc) - return _doxylink_handler + return [table] def setup(app): app.add_css_file("custom.css") - app.add_directive("flagtable", EnumTable) app.add_directive("enumtable", EnumTable) app.add_directive("pyinclude", PyInclude) - - skip = os.environ.get("PYAV_SKIP_DOXYLINK") - for role, (filename, url_base) in doxylink.items(): - if skip: - app.add_role(role, lambda *args: ([], [])) - else: - app.add_role(role, doxylink_create_handler(app, filename, url_base)) diff --git a/docs/development/includes.py b/docs/development/includes.py deleted file mode 100644 index 8f350a81e..000000000 --- a/docs/development/includes.py +++ /dev/null @@ -1,365 +0,0 @@ -import json -import os -import re -import sys - -import xml.etree.ElementTree as etree - -from Cython.Compiler.Main import CompilationOptions, Context -from Cython.Compiler.TreeFragment import parse_from_strings -from Cython.Compiler.Visitor import TreeVisitor -from Cython.Compiler import Nodes - -os.chdir(os.path.abspath(os.path.join(__file__, '..', '..', '..'))) - - -class Visitor(TreeVisitor): - - def __init__(self, state=None): - super(Visitor, self).__init__() - self.state = dict(state or {}) - self.events = [] - - def record_event(self, node, **kw): - state = self.state.copy() - state.update(**kw) - state['node'] = node - state['pos'] = node.pos - state['end_pos'] = node.end_pos() - self.events.append(state) - - def visit_Node(self, node): - self.visitchildren(node) - - def visit_ModuleNode(self, node): - self.state['module'] = node.full_module_name - self.visitchildren(node) - self.state.pop('module') - - def visit_CDefExternNode(self, node): - self.state['extern_from'] = node.include_file - self.visitchildren(node) - self.state.pop('extern_from') - - def visit_CStructOrUnionDefNode(self, node): - self.record_event(node, type='struct', name=node.name) - self.state['struct'] = node.name - self.visitchildren(node) - self.state.pop('struct') - - def visit_CFuncDeclaratorNode(self, node): - if isinstance(node.base, Nodes.CNameDeclaratorNode): - self.record_event(node, type='function', name=node.base.name) - else: - self.visitchildren(node) - - def visit_CVarDefNode(self, node): - - if isinstance(node.declarators[0], Nodes.CNameDeclaratorNode): - - # Grab the type name. - # TODO: Do a better job. - type_ = node.base_type - if hasattr(type_, 'name'): - type_name = type_.name - elif hasattr(type_, 'base_type'): - type_name = type_.base_type.name - else: - type_name = str(type_) - - self.record_event(node, type='variable', name=node.declarators[0].name, - vartype=type_name) - - else: - self.visitchildren(node) - - def visit_CClassDefNode(self, node): - self.state['class'] = node.class_name - self.visitchildren(node) - self.state.pop('class') - - def visit_PropertyNode(self, node): - self.state['property'] = node.name - self.visitchildren(node) - self.state.pop('property') - - def visit_DefNode(self, node): - self.state['function'] = node.name - self.visitchildren(node) - self.state.pop('function') - - def visit_AttributeNode(self, node): - if getattr(node.obj, 'name', None) == 'lib': - self.record_event(node, type='use', name=node.attribute) - else: - self.visitchildren(node) - - -def extract(path, **kwargs): - - name = os.path.splitext(os.path.relpath(path))[0].replace('/', '.') - - options = CompilationOptions() - options.include_path.append('include') - options.language_level = 2 - options.compiler_directives = dict( - c_string_type='str', - c_string_encoding='ascii', - ) - - context = Context( - options.include_path, - options.compiler_directives, - options.cplus, - options.language_level, - options=options, - ) - - tree = parse_from_strings( - name, open(path).read(), context, - level='module_pxd' if path.endswith('.pxd') else None, - **kwargs) - - extractor = Visitor({'file': path}) - extractor.visit(tree) - return extractor.events - - -def iter_cython(path): - '''Yield all ``.pyx`` and ``.pxd`` files in the given root.''' - for dir_path, dir_names, file_names in os.walk(path): - for file_name in file_names: - if file_name.startswith('.'): - continue - if os.path.splitext(file_name)[1] not in ('.pyx', '.pxd'): - continue - yield os.path.join(dir_path, file_name) - - -doxygen = {} -doxygen_base = 'https://ffmpeg.org/doxygen/trunk' -tagfile_path = 'docs/_build/doxygen/tagfile.xml' - -tagfile_json = tagfile_path + '.json' -if os.path.exists(tagfile_json): - print('Loading pre-parsed Doxygen tagfile:', tagfile_json, file=sys.stderr) - doxygen = json.load(open(tagfile_json)) - - -if not doxygen: - - print('Parsing Doxygen tagfile:', tagfile_path, file=sys.stderr) - if not os.path.exists(tagfile_path): - print(' MISSING!', file=sys.stderr) - else: - - root = etree.parse(tagfile_path) - - def inspect_member(node, name_prefix=''): - name = name_prefix + node.find('name').text - anchorfile = node.find('anchorfile').text - anchor = node.find('anchor').text - - url = '%s/%s#%s' % (doxygen_base, anchorfile, anchor) - - doxygen[name] = {'url': url} - - if node.attrib['kind'] == 'function': - ret_type = node.find('type').text - arglist = node.find('arglist').text - sig = '%s %s%s' % (ret_type, name, arglist) - doxygen[name]['sig'] = sig - - for struct in root.iter('compound'): - if struct.attrib['kind'] != 'struct': - continue - name_prefix = struct.find('name').text + '.' - for node in struct.iter('member'): - inspect_member(node, name_prefix) - - for node in root.iter('member'): - inspect_member(node) - - - json.dump(doxygen, open(tagfile_json, 'w'), sort_keys=True, indent=4) - - -print('Parsing Cython source for references...', file=sys.stderr) -lib_references = {} -for path in iter_cython('av'): - try: - events = extract(path) - except Exception as e: - print(" %s in %s" % (e.__class__.__name__, path), file=sys.stderr) - print(" %s" % e, file=sys.stderr) - continue - for event in events: - if event['type'] == 'use': - lib_references.setdefault(event['name'], []).append(event) - - - - - - - -defs_by_extern = {} -for path in iter_cython('include'): - - # This one has "include" directives, which is not supported when - # parsing from a string. - if path == 'include/libav.pxd': - continue - - # Extract all #: comments from the source files. - comments_by_line = {} - for i, line in enumerate(open(path)): - m = re.match(r'^\s*#: ?', line) - if m: - comment = line[m.end():].rstrip() - comments_by_line[i + 1] = line[m.end():] - - # Extract Cython definitions from the source files. - for event in extract(path): - - extern = event.get('extern_from') or path.replace('include/', '') - defs_by_extern.setdefault(extern, []).append(event) - - # Collect comments above and below - comments = event['_comments'] = [] - line = event['pos'][1] - 1 - while line in comments_by_line: - comments.insert(0, comments_by_line.pop(line)) - line -= 1 - line = event['end_pos'][1] + 1 - while line in comments_by_line: - comments.append(comments_by_line.pop(line)) - line += 1 - - # Figure out the Sphinx headline. - if event['type'] == 'function': - event['_sort_key'] = 2 - sig = doxygen.get(event['name'], {}).get('sig') - if sig: - sig = re.sub(r'\).+', ')', sig) # strip trailer - event['_headline'] = '.. c:function:: %s' % sig - else: - event['_headline'] = '.. c:function:: %s()' % event['name'] - - elif event['type'] == 'variable': - struct = event.get('struct') - if struct: - event['_headline'] = '.. c:member:: %s %s' % (event['vartype'], event['name']) - event['_sort_key'] = 1.1 - else: - event['_headline'] = '.. c:var:: %s' % event['name'] - event['_sort_key'] = 3 - - elif event['type'] == 'struct': - event['_headline'] = '.. c:type:: struct %s' % event['name'] - event['_sort_key'] = 1 - event['_doxygen_url'] = '%s/struct%s.html' % (doxygen_base, event['name']) - - else: - print('Unknown event type %s' % event['type'], file=sys.stderr) - - name = event['name'] - if event.get('struct'): - name = '%s.%s' % (event['struct'], name) - - # Doxygen URLs - event.setdefault('_doxygen_url', doxygen.get(name, {}).get('url')) - - # Find use references. - ref_events = lib_references.get(name, []) - if ref_events: - - ref_pairs = [] - for ref in sorted(ref_events, key=lambda e: e['name']): - - chunks = [ - ref.get('module'), - ref.get('class'), - ] - chunks = filter(None, chunks) - prefix = '.'.join(chunks) + '.' if chunks else '' - - if ref.get('property'): - ref_pairs.append((ref['property'], ':attr:`%s%s`' % (prefix, ref['property']))) - elif ref.get('function'): - name = ref['function'] - if name in ('__init__', '__cinit__', '__dealloc__'): - ref_pairs.append((name, ':class:`%s%s <%s>`' % (prefix, name, prefix.rstrip('.')))) - else: - ref_pairs.append((name, ':func:`%s%s`' % (prefix, name))) - else: - continue - - unique_refs = event['_references'] = [] - seen = set() - for name, ref in sorted(ref_pairs): - if name in seen: - continue - seen.add(name) - unique_refs.append(ref) - - - - -print(''' - -.. - This file is generated by includes.py; any modifications will be destroyed! - -Wrapped C Types and Functions -============================= - -''') - -for extern, events in sorted(defs_by_extern.items()): - did_header = False - - for event in events: - - headline = event.get('_headline') - comments = event.get('_comments') - refs = event.get('_references', []) - url = event.get('_doxygen_url') - indent = ' ' if event.get('struct') else '' - - if not headline: - continue - if ( - not filter(None, (x.strip() for x in comments if x.strip())) and - not refs and - event['type'] not in ('struct', ) - ): - pass - - if not did_header: - print('``%s``' % extern) - print('-' * (len(extern) + 4)) - print() - did_header = True - - if url: - print() - print(indent + '.. rst-class:: ffmpeg-quicklink') - print() - print(indent + ' `FFmpeg Docs <%s>`__' % url) - - print(indent + headline) - print() - - if comments: - for line in comments: - print(indent + ' ' + line) - print() - - if refs: - print(indent + ' Referenced by: ', end='') - for i, ref in enumerate(refs): - print((', ' if i else '') + ref, end='') - print('.') - - print() diff --git a/docs/development/includes.rst b/docs/development/includes.rst deleted file mode 100644 index 6b2c989cb..000000000 --- a/docs/development/includes.rst +++ /dev/null @@ -1,2 +0,0 @@ - -.. include:: ../_build/rst/development/includes.rst diff --git a/docs/generate-tagfile b/docs/generate-tagfile deleted file mode 100755 index 1f729de5c..000000000 --- a/docs/generate-tagfile +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python - -import argparse -import os -import subprocess - - -parser = argparse.ArgumentParser() -parser.add_argument("-l", "--library", required=True) -parser.add_argument("-o", "--output", required=True) -args = parser.parse_args() - -output = os.path.abspath(args.output) -outdir = os.path.dirname(output) -if not os.path.exists(outdir): - os.makedirs(outdir) - -proc = subprocess.Popen(["doxygen", "-"], stdin=subprocess.PIPE, cwd=args.library) -proc.communicate( - """ - -#@INCLUDE = doc/Doxyfile -GENERATE_TAGFILE = {} -GENERATE_HTML = no -GENERATE_LATEX = no -CASE_SENSE_NAMES = yes -INPUT = libavcodec libavdevice libavfilter libavformat libavresample libavutil libswresample libswscale - -""".format( - output - ).encode() -) From 663249d72616cfc08518d6bf295f98761a0bb4dd Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 18 Feb 2025 15:42:15 -0500 Subject: [PATCH 237/365] Set the default bit_rate to 0 --- av/container/output.pyx | 4 ++-- tests/test_encode.py | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/av/container/output.pyx b/av/container/output.pyx index 7140c64b5..2a12c40f2 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -76,7 +76,7 @@ cdef class OutputContainer(Container): codec_context.pix_fmt = lib.AV_PIX_FMT_YUV420P codec_context.width = kwargs.pop("width", 640) codec_context.height = kwargs.pop("height", 480) - codec_context.bit_rate = kwargs.pop("bit_rate", 1024000) + codec_context.bit_rate = kwargs.pop("bit_rate", 0) codec_context.bit_rate_tolerance = kwargs.pop("bit_rate_tolerance", 128000) try: to_avrational(kwargs.pop("time_base"), &codec_context.time_base) @@ -90,7 +90,7 @@ cdef class OutputContainer(Container): # Some sane audio defaults elif codec.type == lib.AVMEDIA_TYPE_AUDIO: codec_context.sample_fmt = codec.sample_fmts[0] - codec_context.bit_rate = kwargs.pop("bit_rate", 128000) + codec_context.bit_rate = kwargs.pop("bit_rate", 0) codec_context.bit_rate_tolerance = kwargs.pop("bit_rate_tolerance", 32000) try: to_avrational(kwargs.pop("time_base"), &codec_context.time_base) diff --git a/tests/test_encode.py b/tests/test_encode.py index c107aa044..23cbdc275 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -120,7 +120,6 @@ def test_default_options(self) -> None: assert stream.time_base is None # codec context properties - assert stream.bit_rate == 1024000 assert stream.format.height == 480 assert stream.format.name == "yuv420p" assert stream.format.width == 640 @@ -176,7 +175,6 @@ def test_default_options(self) -> None: assert stream.time_base is None # codec context properties - assert stream.bit_rate == 128000 assert stream.format.name == "s16" assert stream.sample_rate == 48000 From a2477d5b0670f4e56aac52323e9fa7f8039d29f5 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 19 Feb 2025 01:15:22 -0500 Subject: [PATCH 238/365] Use new ffmpeg binary --- scripts/ffmpeg-7.1.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ffmpeg-7.1.json b/scripts/ffmpeg-7.1.json index a9fdf419d..dd4f9a778 100644 --- a/scripts/ffmpeg-7.1.json +++ b/scripts/ffmpeg-7.1.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.1-3/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7a-community/ffmpeg-{platform}.tar.gz" } \ No newline at end of file From 8d9b94c38e22d1b845d1c2e1e071ccb2096a7e1a Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 19 Feb 2025 03:31:21 -0500 Subject: [PATCH 239/365] Add Codec.canonical_name --- CHANGELOG.rst | 1 + av/codec/codec.pyi | 2 ++ av/codec/codec.pyx | 9 +++++++++ tests/test_codec.py | 1 + 4 files changed, 13 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 79cca4c55..dc57483c0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -26,6 +26,7 @@ Features: - Add Bayer pixel formats by :gh-user:`z-khan` in (:pr:`1755`). - Add support for yuv422p10le pix_fmt by :gh-user:`WyattBlue` in (:pr:`1767`). - Add ``supported_np_pix_fmts`` by :gh-user:`WyattBlue` in (:pr:`1766`). +- Add ``Codec.canonical_name`` by :gh-user:`WyattBlue`. Misc. - Drop support for MacOS 11 by :gh-user:`WyattBlue` in (:pr:`1764`). diff --git a/av/codec/codec.pyi b/av/codec/codec.pyi index 9f80a4909..4270c641f 100644 --- a/av/codec/codec.pyi +++ b/av/codec/codec.pyi @@ -57,6 +57,8 @@ class Codec: @property def name(self) -> str: ... @property + def canonical_name(self) -> str: ... + @property def long_name(self) -> str: ... @property def type(self) -> Literal["video", "audio", "data", "subtitle", "attachment"]: ... diff --git a/av/codec/codec.pyx b/av/codec/codec.pyx index 3e9e028f8..a28db758e 100644 --- a/av/codec/codec.pyx +++ b/av/codec/codec.pyx @@ -1,3 +1,5 @@ +cimport libav as lib + from av.audio.format cimport get_audio_format from av.codec.hwaccel cimport wrap_hwconfig from av.descriptor cimport wrap_avclass @@ -144,6 +146,13 @@ cdef class Codec: @property def name(self): return self.ptr.name or "" + @property + def canonical_name(self): + """ + Returns the name of the codec, not a specific encoder. + """ + return lib.avcodec_get_name(self.ptr.id) + @property def long_name(self): return self.ptr.long_name or "" diff --git a/tests/test_codec.py b/tests/test_codec.py index 70e688435..9d72a243c 100644 --- a/tests/test_codec.py +++ b/tests/test_codec.py @@ -68,6 +68,7 @@ def test_codec_opus_decoder() -> None: def test_codec_opus_encoder() -> None: c = Codec("opus", "w") assert c.name in ("opus", "libopus") + assert c.canonical_name == "opus" assert c.long_name in ("Opus", "libopus Opus") assert c.type == "audio" assert c.is_encoder From fb6cf4ed7b41e863371f1640a2ff0a3ea703deb2 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 20 Feb 2025 14:04:40 -0500 Subject: [PATCH 240/365] Remove outdated installion section --- av/about.py | 2 +- docs/Makefile | 11 +++---- docs/conf.py | 42 ++++++++++++++++++++++--- docs/overview/installation.rst | 57 ---------------------------------- 4 files changed, 43 insertions(+), 69 deletions(-) diff --git a/av/about.py b/av/about.py index 217fb25cb..4c6a9313e 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "14.1.0" +__version__ = "14.2.0rc1" diff --git a/docs/Makefile b/docs/Makefile index 7efd36bac..e45e1d03a 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,5 +1,4 @@ SPHINXOPTS = -SPHINXBUILD = sphinx-build BUILDDIR = _build PYAV_PIP ?= pip PIP := $(PYAV_PIP) @@ -17,15 +16,13 @@ _build/rst/%.rst: %.py $(TAGFILE) $(shell find ../include ../av -name '*.pyx' -o python $< > $@.tmp mv $@.tmp $@ -clean: - rm -rf $(BUILDDIR) $(FFMPEGDIR) - -html: $(RENDERED) $(TAGFILE) +html: $(RENDERED) $(PIP) install -U sphinx - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + rm -rf $(BUILDDIR) + sphinx-build -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html test: - PYAV_SKIP_DOXYLINK=1 $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + PYAV_SKIP_DOXYLINK=1 sphinx-build -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest open: open _build/html/index.html diff --git a/docs/conf.py b/docs/conf.py index 1f2541f32..b88dc7bd3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -88,7 +88,6 @@ def sandboxed(*args, **kwargs): doctest_test_doctest_blocks = "" extlinks = { - "ffmpeg": ("https://ffmpeg.org/doxygen/trunk/%s.html", "%s"), "ffstruct": ("https://ffmpeg.org/doxygen/trunk/struct%s.html", "struct %s"), "issue": ("https://github.com/PyAV-Org/PyAV/issues/%s", "#%s"), "pr": ("https://github.com/PyAV-Org/PyAV/pull/%s", "#%s"), @@ -203,9 +202,7 @@ def makerow(*texts): seen = set() enum_items = [ - (name, item) - for name, item in vars(enum).items() - if isinstance(item, enum) + (name, item) for name, item in vars(enum).items() if isinstance(item, enum) ] for name, item in enum_items: if name.lower() in seen: @@ -226,8 +223,45 @@ def makerow(*texts): return [table] +def ffmpeg_role(name, rawtext, text, lineno, inliner, options={}, content=[]): + """ + Custom role for FFmpeg API links. + Converts :ffmpeg:`AVSomething` into proper FFmpeg API documentation links. + """ + + base_url = "https://ffmpeg.org/doxygen/7.0/struct{}.html" + + try: + struct_name, member = text.split(".") + except Exception: + struct_name = None + + if struct_name is None: + url = base_url.format(text) + else: + fragment = { + "AVCodecContext.thread_count": "#aa852b6227d0778b62e9cc4034ad3720c", + "AVCodecContext.thread_type": "#a7651614f4309122981d70e06a4b42fcb", + "AVCodecContext.skip_frame": "#af869b808363998c80adf7df6a944a5a6", + "AVCodec.capabilities": "#af51f7ff3dac8b730f46b9713e49a2518", + "AVCodecDescriptor.props": "#a9949288403a12812cd6e3892ac45f40f", + }.get(text, f"#{member}") + + url = base_url.format(struct_name) + fragment + + node = nodes.reference(rawtext, text, refuri=url, **options) + return [node], [] + + def setup(app): app.add_css_file("custom.css") + app.add_role("ffmpeg", ffmpeg_role) app.add_directive("flagtable", EnumTable) app.add_directive("enumtable", EnumTable) app.add_directive("pyinclude", PyInclude) + + return { + "version": "1.0", + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/docs/overview/installation.rst b/docs/overview/installation.rst index cf1364c1a..eea041203 100644 --- a/docs/overview/installation.rst +++ b/docs/overview/installation.rst @@ -21,63 +21,6 @@ Another way to install PyAV is via `conda-forge ` See the `Conda quick install `_ docs to get started with (mini)Conda. -Bring your own FFmpeg ---------------------- - -PyAV can also be compiled against your own build of FFmpeg (version ``7.0`` or higher). You can force installing PyAV from source by running: - -.. code-block:: bash - - pip install av --no-binary av - -PyAV depends upon several libraries from FFmpeg: - -- ``libavcodec`` -- ``libavdevice`` -- ``libavfilter`` -- ``libavformat`` -- ``libavutil`` -- ``libswresample`` -- ``libswscale`` - -and a few other tools in general: - -- ``pkg-config`` -- Python's development headers - - -MacOS -^^^^^ - -On **MacOS**, Homebrew_ saves the day:: - - brew install ffmpeg pkg-config - -.. _homebrew: http://brew.sh/ - - -Ubuntu >= 18.04 LTS -^^^^^^^^^^^^^^^^^^^ - -On **Ubuntu 18.04 LTS** everything can come from the default sources:: - - # General dependencies - sudo apt-get install -y python-dev pkg-config - - # Library components - sudo apt-get install -y \ - libavformat-dev libavcodec-dev libavdevice-dev \ - libavutil-dev libswscale-dev libswresample-dev libavfilter-dev - - -Windows -^^^^^^^ - -It is possible to build PyAV on Windows without Conda by installing FFmpeg yourself, e.g. from the `shared and dev packages `_. - -Unpack them somewhere (like ``C:\ffmpeg``), and then :ref:`tell PyAV where they are located `. - - Building from the latest source ------------------------------- From c04f6e6a19b42b5669835ca77fa4a0aa98170299 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 20 Feb 2025 15:07:24 -0500 Subject: [PATCH 241/365] Add more doc links --- .github/workflows/smoke.yml | 3 --- docs/conf.py | 7 +++++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 416f6fecb..40dcb3fe6 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -35,10 +35,8 @@ jobs: matrix: config: - {os: ubuntu-24.04, python: "3.12", ffmpeg: "7.1", extras: true} - - {os: ubuntu-24.04, python: "3.9", ffmpeg: "7.0.2"} - {os: ubuntu-24.04, python: "pypy3.10", ffmpeg: "7.1"} - {os: macos-14, python: "3.9", ffmpeg: "7.1"} - - {os: macos-14, python: "3.9", ffmpeg: "7.0.2"} env: PYAV_PYTHON: python${{ matrix.config.python }} @@ -112,7 +110,6 @@ jobs: matrix: config: - {os: windows-latest, python: "3.9", ffmpeg: "7.1"} - - {os: windows-latest, python: "3.9", ffmpeg: "7.0"} steps: - name: Checkout diff --git a/docs/conf.py b/docs/conf.py index b88dc7bd3..ba5e36500 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -243,8 +243,15 @@ def ffmpeg_role(name, rawtext, text, lineno, inliner, options={}, content=[]): "AVCodecContext.thread_count": "#aa852b6227d0778b62e9cc4034ad3720c", "AVCodecContext.thread_type": "#a7651614f4309122981d70e06a4b42fcb", "AVCodecContext.skip_frame": "#af869b808363998c80adf7df6a944a5a6", + "AVCodecContext.qmin": "#a3f63bc9141e25bf7f1cda0cef7cd4a60", + "AVCodecContext.qmax": "#ab015db3b7fcd227193a7c17283914187", "AVCodec.capabilities": "#af51f7ff3dac8b730f46b9713e49a2518", "AVCodecDescriptor.props": "#a9949288403a12812cd6e3892ac45f40f", + "AVCodecContext.bits_per_coded_sample": "#a3866500f51fabfa90faeae894c6e955c", + "AVFrame.color_range": "#a853afbad220bbc58549b4860732a3aa5", + "AVFrame.color_primaries": "#a59a3f830494f2ed1133103a1bc9481e7", + "AVFrame.color_trc": "#ab09abb126e3922bc1d010cf044087939", + "AVFrame.colorspace": "#a9262c231f1f64869439b4fe587fe1710", }.get(text, f"#{member}") url = base_url.format(struct_name) + fragment From 7495bfe1731912728836e38f60006c543f53297b Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 20 Feb 2025 22:09:00 -0500 Subject: [PATCH 242/365] Fix all broken links --- docs/conf.py | 11 +++++++++-- docs/cookbook/numpy.rst | 2 +- docs/overview/installation.rst | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index ba5e36500..07b942a31 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -88,7 +88,6 @@ def sandboxed(*args, **kwargs): doctest_test_doctest_blocks = "" extlinks = { - "ffstruct": ("https://ffmpeg.org/doxygen/trunk/struct%s.html", "struct %s"), "issue": ("https://github.com/PyAV-Org/PyAV/issues/%s", "#%s"), "pr": ("https://github.com/PyAV-Org/PyAV/pull/%s", "#%s"), "gh-user": ("https://github.com/%s", "@%s"), @@ -237,7 +236,15 @@ def ffmpeg_role(name, rawtext, text, lineno, inliner, options={}, content=[]): struct_name = None if struct_name is None: - url = base_url.format(text) + fragment = { + "avformat_seek_file": "group__lavf__decoding.html#ga3b40fc8d2fda6992ae6ea2567d71ba30", + "av_find_best_stream": "avformat_8c.html#a8d4609a8f685ad894c1503ffd1b610b4", + "av_frame_make_writable": "group__lavu__frame.html#gadd5417c06f5a6b419b0dbd8f0ff363fd", + "avformat_write_header": "group__lavf__encoding.html#ga18b7b10bb5b94c4842de18166bc677cb", + "av_guess_frame_rate": "group__lavf__misc.html#ga698e6aa73caa9616851092e2be15875d", + "av_guess_sample_aspect_ratio": "group__lavf__misc.html#gafa6fbfe5c1bf6792fd6e33475b6056bd", + }.get(text, f"struct{text}.html") + url = "https://ffmpeg.org/doxygen/7.0/" + fragment else: fragment = { "AVCodecContext.thread_count": "#aa852b6227d0778b62e9cc4034ad3720c", diff --git a/docs/cookbook/numpy.rst b/docs/cookbook/numpy.rst index d4887945c..6842efefc 100644 --- a/docs/cookbook/numpy.rst +++ b/docs/cookbook/numpy.rst @@ -7,7 +7,7 @@ Video Barcode A video barcode shows the change in colour and tone over time. Time is represented on the horizontal axis, while the vertical remains the vertical direction in the image. -See http://moviebarcode.tumblr.com/ for examples from Hollywood movies, and here is an example from a sunset timelapse: +See https://moviebarcode.tumblr.com/ for examples from Hollywood movies, and here is an example from a sunset timelapse: .. image:: ../_static/examples/numpy/barcode.jpg diff --git a/docs/overview/installation.rst b/docs/overview/installation.rst index eea041203..8eb5ce553 100644 --- a/docs/overview/installation.rst +++ b/docs/overview/installation.rst @@ -18,7 +18,7 @@ Another way to install PyAV is via `conda-forge ` conda install av -c conda-forge -See the `Conda quick install `_ docs to get started with (mini)Conda. +See the `Conda quick install `_ docs to get started with (mini)Conda. Building from the latest source From ca7f5ea2cbe694929fec2544735a86c9f90953c4 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 20 Feb 2025 22:34:55 -0500 Subject: [PATCH 243/365] Add copy button to code blocks --- CHANGELOG.rst | 3 ++- docs/Makefile | 2 +- docs/conf.py | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index dc57483c0..7c8ece6b6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -28,7 +28,8 @@ Features: - Add ``supported_np_pix_fmts`` by :gh-user:`WyattBlue` in (:pr:`1766`). - Add ``Codec.canonical_name`` by :gh-user:`WyattBlue`. -Misc. +Misc: + - Drop support for MacOS 11 by :gh-user:`WyattBlue` in (:pr:`1764`). - Add warnings when compiling from source by :gh-user:`WyattBlue` in (:pr:`1773`). diff --git a/docs/Makefile b/docs/Makefile index e45e1d03a..6654f1062 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -17,7 +17,7 @@ _build/rst/%.rst: %.py $(TAGFILE) $(shell find ../include ../av -name '*.pyx' -o mv $@.tmp $@ html: $(RENDERED) - $(PIP) install -U sphinx + $(PIP) install -U sphinx sphinx-copybutton rm -rf $(BUILDDIR) sphinx-build -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html diff --git a/docs/conf.py b/docs/conf.py index 07b942a31..b32212e97 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,6 +16,7 @@ "sphinx.ext.viewcode", "sphinx.ext.extlinks", "sphinx.ext.doctest", + "sphinx_copybutton", # Add copy button to code blocks ] # Add any paths that contain templates here, relative to this directory. From 318695612a05a599d86fb48255a0ce1b4e8d91d9 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 21 Feb 2025 11:38:17 -0500 Subject: [PATCH 244/365] Don't let sidebar text overflow --- av/stream.pyx | 14 ++++++-------- av/video/stream.pyx | 10 +++++----- docs/_static/custom.css | 7 +++++++ docs/conf.py | 7 +++++++ 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/av/stream.pyx b/av/stream.pyx index d0ecf37ad..90f10d038 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -202,7 +202,7 @@ cdef class Stream: """ The unit of time (in fractional seconds) in which timestamps are expressed. - :type: :class:`~fractions.Fraction` or ``None`` + :type: fractions.Fraction | None """ return avrational_to_fraction(&self.ptr.time_base) @@ -219,7 +219,7 @@ cdef class Stream: The presentation timestamp in :attr:`time_base` units of the first frame in this stream. - :type: :class:`int` or ``None`` + :type: int | None """ if self.ptr.start_time != lib.AV_NOPTS_VALUE: return self.ptr.start_time @@ -229,7 +229,7 @@ cdef class Stream: """ The duration of this stream in :attr:`time_base` units. - :type: :class:`int` or ``None`` + :type: int | None """ if self.ptr.duration != lib.AV_NOPTS_VALUE: @@ -242,7 +242,7 @@ cdef class Stream: Returns ``0`` if it is not known. - :type: :class:`int` + :type: int """ return self.ptr.nb_frames @@ -251,7 +251,7 @@ cdef class Stream: """ The language of the stream. - :type: :class:`str` or ``None`` + :type: str | None """ return self.metadata.get("language") @@ -264,8 +264,6 @@ cdef class Stream: """ The type of the stream. - Examples: ``'audio'``, ``'video'``, ``'subtitle'``. - - :type: str + :type: Literal["audio", "video", "subtitle", "data", "attachment"] """ return lib.av_get_media_type_string(self.ptr.codecpar.codec_type) diff --git a/av/video/stream.pyx b/av/video/stream.pyx index 6ab685167..383cc13a6 100644 --- a/av/video/stream.pyx +++ b/av/video/stream.pyx @@ -57,7 +57,7 @@ cdef class VideoStream(Stream): This is calculated when the file is opened by looking at the first few frames and averaging their rate. - :type: :class:`~fractions.Fraction` or ``None`` + :type: fractions.Fraction | None """ return avrational_to_fraction(&self.ptr.avg_frame_rate) @@ -70,7 +70,7 @@ cdef class VideoStream(Stream): frames can be represented accurately. See :ffmpeg:`AVStream.r_frame_rate` for more. - :type: :class:`~fractions.Fraction` or ``None`` + :type: fractions.Fraction | None """ return avrational_to_fraction(&self.ptr.r_frame_rate) @@ -81,7 +81,7 @@ cdef class VideoStream(Stream): This is a wrapper around :ffmpeg:`av_guess_frame_rate`, and uses multiple heuristics to decide what is "the" frame rate. - :type: :class:`~fractions.Fraction` or ``None`` + :type: fractions.Fraction | None """ # The two NULL arguments aren't used in FFmpeg >= 4.0 cdef lib.AVRational val = lib.av_guess_frame_rate(NULL, self.ptr, NULL) @@ -94,7 +94,7 @@ cdef class VideoStream(Stream): This is a wrapper around :ffmpeg:`av_guess_sample_aspect_ratio`, and uses multiple heuristics to decide what is "the" sample aspect ratio. - :type: :class:`~fractions.Fraction` or ``None`` + :type: fractions.Fraction | None """ cdef lib.AVRational sar = lib.av_guess_sample_aspect_ratio(self.container.ptr, self.ptr, NULL) return avrational_to_fraction(&sar) @@ -105,7 +105,7 @@ cdef class VideoStream(Stream): This is calculated from :meth:`.VideoStream.guessed_sample_aspect_ratio`. - :type: :class:`~fractions.Fraction` or ``None`` + :type: fractions.Fraction | None """ cdef lib.AVRational dar diff --git a/docs/_static/custom.css b/docs/_static/custom.css index b95a5f253..136af619b 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -1,3 +1,10 @@ +body { + text-rendering: optimizeLegibility; +} + +.sphinxsidebarwrapper { + overflow: scroll; +} .ffmpeg-quicklink { float: right; diff --git a/docs/conf.py b/docs/conf.py index b32212e97..d971572e1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -6,6 +6,9 @@ from docutils import nodes from sphinx.util.docutils import SphinxDirective +sys.path.insert(0, os.path.abspath("..")) + + # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ @@ -58,6 +61,10 @@ # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] +html_theme_options = { + "sidebarwidth": "250px", +} + doctest_global_setup = """ import errno From 524786b235006ff3cd81642c77a853541d1228a0 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 22 Feb 2025 19:18:17 -0500 Subject: [PATCH 245/365] Re-add publishing the sdist @jlaine and @hmaarrfk are responsible for maintaining this format. --- .github/workflows/tests.yml | 20 ++++++++++++- MANIFEST.in | 7 +++++ README.md | 15 +++++++++- docs/overview/installation.rst | 55 ++++++++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 MANIFEST.in diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e9e4003b9..a6c5232b3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,6 +4,24 @@ on: types: [published] workflow_dispatch: jobs: + package-source: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + - name: Build source package + run: | + pip install setuptools cython + python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.1.json /tmp/vendor + PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig python setup.py sdist + - name: Upload source package + uses: actions/upload-artifact@v4 + with: + name: dist-source + path: dist/ + package-wheel: runs-on: ${{ matrix.os }} strategy: @@ -64,7 +82,7 @@ jobs: publish: runs-on: ubuntu-latest - needs: [package-wheel] + needs: [package-source, package-wheel] steps: - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 000000000..b87a010d3 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,7 @@ +include *.txt *.md +recursive-include av *.pyx *.pxd +recursive-include docs *.rst *.py +recursive-include examples *.py +recursive-include include *.pxd *.h +recursive-include src/av *.c *.h +recursive-include tests *.py \ No newline at end of file diff --git a/README.md b/README.md index 232fd6743..59e291c74 100644 --- a/README.md +++ b/README.md @@ -32,10 +32,23 @@ conda install av -c conda-forge See the [Conda install][conda-install] docs to get started with (mini)Conda. +Alternative installation methods +-------------------------------- + +Due to the complexity of the dependencies, PyAV is not always the easiest Python package to install from source. If you want to use your existing ffmpeg (must be the correct major version), the source version of PyAV is on [PyPI][pypi]: + +> [!WARNING] +> You must be in a posix env, and have the correct version of ffmpeg installed on your system. + +```bash +pip install av --no-binary av +``` + + Installing From Source ---------------------- -Here's how to build PyAV from source source. You must use [MSYS2](https://www.msys2.org/) when using Windows. +Here's how to build PyAV from source. You must use [MSYS2](https://www.msys2.org/) when using Windows. ```bash git clone https://github.com/PyAV-Org/PyAV.git diff --git a/docs/overview/installation.rst b/docs/overview/installation.rst index 8eb5ce553..5fa7fc860 100644 --- a/docs/overview/installation.rst +++ b/docs/overview/installation.rst @@ -21,6 +21,61 @@ Another way to install PyAV is via `conda-forge ` See the `Conda quick install `_ docs to get started with (mini)Conda. +Bring your own FFmpeg +--------------------- + +PyAV can also be compiled against your own build of FFmpeg (version ``7.0`` or higher). You can force installing PyAV from source by running: + +.. code-block:: bash + pip install av --no-binary av +PyAV depends upon several libraries from FFmpeg: + +- ``libavcodec`` +- ``libavdevice`` +- ``libavfilter`` +- ``libavformat`` +- ``libavutil`` +- ``libswresample`` +- ``libswscale`` + +and a few other tools in general: + +- ``pkg-config`` +- Python's development headers + + +MacOS +^^^^^ + +On **MacOS**, Homebrew_ saves the day:: + + brew install ffmpeg pkg-config + +.. _homebrew: http://brew.sh/ + + +Ubuntu >= 18.04 LTS +^^^^^^^^^^^^^^^^^^^ + +On **Ubuntu 18.04 LTS** everything can come from the default sources:: + + # General dependencies + sudo apt-get install -y python-dev pkg-config + + # Library components + sudo apt-get install -y \ + libavformat-dev libavcodec-dev libavdevice-dev \ + libavutil-dev libswscale-dev libswresample-dev libavfilter-dev + + +Windows +^^^^^^^ + +It is possible to build PyAV on Windows without Conda by installing FFmpeg yourself, e.g. from the `shared and dev packages `_. + +Unpack them somewhere (like ``C:\ffmpeg``), and then :ref:`tell PyAV where they are located `. + + Building from the latest source ------------------------------- From bc0ca08670b2672fc7a7fc8c1c73f2803f5ff6ec Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 25 Feb 2025 08:11:21 -0500 Subject: [PATCH 246/365] Bump version to 14.2.0 --- CHANGELOG.rst | 5 ++--- av/about.py | 2 +- av/codec/context.pyx | 2 +- scripts/ffmpeg-7.1.json | 4 ++-- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7c8ece6b6..a5d57b4c7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,8 +17,8 @@ We are operating with `semantic versioning `_. are merged into the "default" branch. -v14.2.0 (Unreleased) --------------------- +v14.2.0 +------- Features: @@ -31,7 +31,6 @@ Features: Misc: - Drop support for MacOS 11 by :gh-user:`WyattBlue` in (:pr:`1764`). -- Add warnings when compiling from source by :gh-user:`WyattBlue` in (:pr:`1773`). v14.1.0 diff --git a/av/about.py b/av/about.py index 4c6a9313e..331dde6ff 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "14.2.0rc1" +__version__ = "14.2.0" diff --git a/av/codec/context.pyx b/av/codec/context.pyx index a56f14ddd..5ca8f24a4 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -218,7 +218,7 @@ cdef class CodecContext: self.ptr.time_base.num = 1 self.ptr.time_base.den = lib.AV_TIME_BASE - err_check(lib.avcodec_open2(self.ptr, self.codec.ptr, &options.ptr)) + err_check(lib.avcodec_open2(self.ptr, self.codec.ptr, &options.ptr), "avcodec_open2(" + self.codec.name + ")") self.is_open = True self.options = dict(options) diff --git a/scripts/ffmpeg-7.1.json b/scripts/ffmpeg-7.1.json index dd4f9a778..ee386661d 100644 --- a/scripts/ffmpeg-7.1.json +++ b/scripts/ffmpeg-7.1.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7a-community/ffmpeg-{platform}.tar.gz" -} \ No newline at end of file + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.1-4/ffmpeg-{platform}.tar.gz" +} From baa1bce146135e0cafe8a822fbf4ad1090ca06a0 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 9 Mar 2025 13:12:29 -0400 Subject: [PATCH 247/365] Update recording examples --- examples/basics/record_facecam.py | 8 +++++++- examples/basics/record_screen.py | 18 ++++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/examples/basics/record_facecam.py b/examples/basics/record_facecam.py index 2200bc546..0bba6b36a 100644 --- a/examples/basics/record_facecam.py +++ b/examples/basics/record_facecam.py @@ -18,7 +18,13 @@ ) output = av.open("out.mkv", "w") -output_stream = output.add_stream("h264", rate=30) +# Prefer x264, but use Apple hardware if not available. +try: + encoder = av.Codec("libx264", "w").name +except av.FFmpegError: + encoder = "h264_videotoolbox" + +output_stream = output.add_stream(encoder, rate=30) output_stream.width = input_.streams.video[0].width output_stream.height = input_.streams.video[0].height output_stream.pix_fmt = "yuv420p" diff --git a/examples/basics/record_screen.py b/examples/basics/record_screen.py index 92818e931..14fdfc428 100644 --- a/examples/basics/record_screen.py +++ b/examples/basics/record_screen.py @@ -13,15 +13,25 @@ input_ = av.open("1", format="avfoundation") output = av.open("out.mkv", "w") -output_stream = output.add_stream("h264", rate=30) +# Prefer x264, but use Apple hardware if not available. +try: + encoder = av.Codec("libx264", "w").name +except av.FFmpegError: + encoder = "h264_videotoolbox" + +output_stream = output.add_stream(encoder, rate=30) output_stream.width = input_.streams.video[0].width output_stream.height = input_.streams.video[0].height output_stream.pix_fmt = "yuv420p" try: - for frame in input_.decode(video=0): - packet = output_stream.encode(frame) - output.mux(packet) + while True: + try: + for frame in input_.decode(video=0): + packet = output_stream.encode(frame) + output.mux(packet) + except av.BlockingIOError: + pass except KeyboardInterrupt: print("Recording stopped by user") From 0897b7bb993db90d23e85c93c245c355762d3679 Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Tue, 11 Mar 2025 20:46:27 -0600 Subject: [PATCH 248/365] Fix typing because dts can be None --- av/packet.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/packet.pyi b/av/packet.pyi index 9bdbb8c62..baa234d7b 100644 --- a/av/packet.pyi +++ b/av/packet.pyi @@ -10,7 +10,7 @@ class Packet(Buffer): stream_index: int time_base: Fraction pts: int | None - dts: int + dts: int | None pos: int | None size: int duration: int | None From abf2f76464c24e915b6261cbbc632dabaa3b205f Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 12 Mar 2025 00:30:53 -0400 Subject: [PATCH 249/365] Remove unused variables --- av/container/core.pyx | 4 ++-- av/container/pyio.pyx | 12 ++++++------ av/logging.pyx | 2 +- av/subtitles/subtitle.pyx | 1 - av/video/frame.pyx | 1 - 5 files changed, 9 insertions(+), 11 deletions(-) diff --git a/av/container/core.pyx b/av/container/core.pyx index 3da402f2f..2b9a1244b 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -95,7 +95,7 @@ cdef int pyav_io_open_gil(lib.AVFormatContext *s, pb[0] = pyio_file.iocontext return 0 - except Exception as e: + except Exception: return stash_exception() @@ -117,7 +117,7 @@ cdef int pyav_io_close_gil(lib.AVFormatContext *s, lib.AVIOContext *pb) noexcept else: result = pyio_close_gil(pb) - except Exception as e: + except Exception: stash_exception() result = lib.AVERROR_UNKNOWN # Or another appropriate error code diff --git a/av/container/pyio.pyx b/av/container/pyio.pyx index 821ae9c0c..c8b82f96a 100644 --- a/av/container/pyio.pyx +++ b/av/container/pyio.pyx @@ -89,7 +89,7 @@ cdef int pyio_read_gil(void *opaque, uint8_t *buf, int buf_size) noexcept: if not res: return lib.AVERROR_EOF return len(res) - except Exception as e: + except Exception: return stash_exception() @@ -108,7 +108,7 @@ cdef int pyio_write_gil(void *opaque, const uint8_t *buf, int buf_size) noexcept bytes_written = ret_value if isinstance(ret_value, int) else buf_size self.pos += bytes_written return bytes_written - except Exception as e: + except Exception: return stash_exception() @@ -140,7 +140,7 @@ cdef int64_t pyio_seek_gil(void *opaque, int64_t offset, int whence): else: res = self.ftell() return res - except Exception as e: + except Exception: return stash_exception() @@ -148,7 +148,7 @@ cdef int pyio_close_gil(lib.AVIOContext *pb): try: return lib.avio_close(pb) - except Exception as e: + except Exception: stash_exception() @@ -158,12 +158,12 @@ cdef int pyio_close_custom_gil(lib.AVIOContext *pb): self = pb.opaque # Flush bytes in the AVIOContext buffers to the custom I/O - result = lib.avio_flush(pb) + lib.avio_flush(pb) if self.fclose is not None: self.fclose() return 0 - except Exception as e: + except Exception: stash_exception() diff --git a/av/logging.pyx b/av/logging.pyx index 66a5095ad..9cb232d2a 100644 --- a/av/logging.pyx +++ b/av/logging.pyx @@ -318,7 +318,7 @@ cdef void log_callback(void *ptr, int level, const char *format, lib.va_list arg with gil: try: log_callback_gil(level, name, message) - except Exception as e: + except Exception: fprintf(stderr, "av.logging: exception while handling %s[%d]: %s\n", name, level, message) # For some reason lib.PyErr_PrintEx(0) won't work. diff --git a/av/subtitles/subtitle.pyx b/av/subtitles/subtitle.pyx index 373bb529b..a713daa22 100644 --- a/av/subtitles/subtitle.pyx +++ b/av/subtitles/subtitle.pyx @@ -16,7 +16,6 @@ cdef class SubtitleSet: """ def __cinit__(self, SubtitleProxy proxy): self.proxy = proxy - cdef int i self.rects = tuple(build_subtitle(self, i) for i in range(self.proxy.struct.num_rects)) def __repr__(self): diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 4647ab81f..84a35cc49 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -55,7 +55,6 @@ cdef copy_bytes_to_plane(img_bytes, VideoPlane plane, unsigned int bytes_per_pix cdef const uint8_t[:] i_buf = img_bytes cdef size_t i_pos = 0 cdef size_t i_stride = plane.width * bytes_per_pixel - cdef size_t i_size = plane.height * i_stride cdef uint8_t[:] o_buf = plane cdef size_t o_pos = 0 From 85d4a5ab1962f0167cf9a7e0673d5f48203b7f8b Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 12 Mar 2025 00:37:47 -0400 Subject: [PATCH 250/365] Set binding=False This makes code faster and smaller, at the expense of disallowing overriding Cython class methods. --- setup.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/setup.py b/setup.py index bef384515..f1ae841dd 100644 --- a/setup.py +++ b/setup.py @@ -188,15 +188,18 @@ def parse_cflags(raw_flags): library_dirs=extension_extra["library_dirs"], ) +compiler_directives = { + "c_string_type": "str", + "c_string_encoding": "ascii", + "embedsignature": True, + "binding": False, + "language_level": 3, +} + # Add the cythonized loudnorm extension to ext_modules ext_modules = cythonize( loudnorm_extension, - compiler_directives={ - "c_string_type": "str", - "c_string_encoding": "ascii", - "embedsignature": True, - "language_level": 3, - }, + compiler_directives=compiler_directives, build_dir="src", include_path=["include"], ) @@ -223,12 +226,7 @@ def parse_cflags(raw_flags): library_dirs=extension_extra["library_dirs"], sources=[pyx_path], ), - compiler_directives={ - "c_string_type": "str", - "c_string_encoding": "ascii", - "embedsignature": True, - "language_level": 3, - }, + compiler_directives=compiler_directives, build_dir="src", include_path=["include"], ) From 086dda0bb84e922c8318550a41db5a936cdc20c3 Mon Sep 17 00:00:00 2001 From: Marcell Pardavi Date: Tue, 25 Mar 2025 14:18:29 +0100 Subject: [PATCH 251/365] Use Python 3 compatible stderr logging in Cython --- av/error.pyx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/av/error.pyx b/av/error.pyx index b8b415207..c3d8a3e7b 100644 --- a/av/error.pyx +++ b/av/error.pyx @@ -1,4 +1,5 @@ cimport libav as lib +from libc.stdio cimport fprintf, stderr from libc.stdlib cimport free, malloc from av.logging cimport get_last_error @@ -367,7 +368,7 @@ cdef int stash_exception(exc_info=None): existing = getattr(_local, "exc_info", None) if existing is not None: - print >> sys.stderr, "PyAV library exception being dropped:" + fprintf(stderr, "PyAV library exception being dropped:\n") traceback.print_exception(*existing) _err_count -= 1 # Balance out the +=1 that is coming. From bca1f10f3cebdd8723b29b929303851bca9dee1d Mon Sep 17 00:00:00 2001 From: Nikhil Idiculla Date: Wed, 26 Mar 2025 12:17:22 -0700 Subject: [PATCH 252/365] Add yuv420p10le pix_fmt --- av/video/frame.pyx | 36 ++++++++++++++++++++++++++++++++++-- tests/test_videoframe.py | 9 +++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 84a35cc49..785603a1a 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -21,7 +21,7 @@ supported_np_pix_fmts = { "gbrp14be", "gbrp14le", "gbrp16be", "gbrp16le", "gbrpf32be", "gbrpf32le", "gray", "gray16be", "gray16le", "gray8", "grayf32be", "grayf32le", "nv12", "pal8", "rgb24", "rgb48be", "rgb48le", "rgb8", "rgba", "rgba64be", "rgba64le", "yuv420p", - "yuv422p10le", "yuv444p", "yuv444p16be", "yuv444p16le", "yuva444p16be", + "yuv420p10le", "yuv422p10le", "yuv444p", "yuv444p16be", "yuv444p16le", "yuva444p16be", "yuva444p16le", "yuvj420p", "yuvj444p", "yuyv422", } @@ -321,7 +321,7 @@ cdef class VideoFrame(Frame): import numpy as np # check size - if frame.format.name in {"yuv420p", "yuvj420p", "yuyv422", "yuv422p10le"}: + if frame.format.name in {"yuv420p", "yuvj420p", "yuyv422", "yuv420p10le", "yuv422p10le"}: assert frame.width % 2 == 0, "the width has to be even for this pixel format" assert frame.height % 2 == 0, "the height has to be even for this pixel format" @@ -407,6 +407,16 @@ cdef class VideoFrame(Frame): useful_array(frame.planes[1]), useful_array(frame.planes[2]), ]).reshape(-1, frame.width) + if frame.format.name == "yuv420p10le": + # Read planes as uint16: + y = useful_array(frame.planes[0], 2, "uint16").reshape(frame.height, frame.width) + u = useful_array(frame.planes[1], 2, "uint16").reshape(frame.height // 2, frame.width // 2) + v = useful_array(frame.planes[2], 2, "uint16").reshape(frame.height // 2, frame.width // 2) + u_full = np.repeat(np.repeat(u, 2, axis=1), 2, axis=0) + v_full = np.repeat(np.repeat(u, 2, axis=1), 2, axis=0) + if channel_last: + return np.stack([y, u_full, v_full], axis=2) + return np.stack([y, u_full, v_full], axis=0) if frame.format.name == "yuv422p10le": # Read planes as uint16 at their original width y = useful_array(frame.planes[0], 2, "uint16").reshape(frame.height, frame.width) @@ -652,6 +662,28 @@ cdef class VideoFrame(Frame): copy_array_to_plane(flat[u_start:v_start], frame.planes[1], 1) copy_array_to_plane(flat[v_start:], frame.planes[2], 1) return frame + elif format == "yuv420p10le": + if not isinstance(array, np.ndarray) or array.dtype != np.uint16: + raise ValueError("Array must be uint16 type") + + # Convert to channel-first if needed: + if channel_last and array.shape[2] == 3: + array = np.moveaxis(array, 2, 0) + elif not (array.shape[0] == 3): + raise ValueError("Array must have shape (3, height, width) or (height, width, 3)") + + height, width = array.shape[1:] + if width % 2 != 0 or height % 2 != 0: + raise ValueError("Width and height must be even") + + frame = VideoFrame(width, height, format) + copy_array_to_plane(array[0], frame.planes[0], 2) + # Subsample U and V by taking every other row and column: + u = array[1, ::2, ::2].copy() # Need copy to ensure C-contiguous + v = array[2, ::2, ::2].copy() # Need copy to ensure C-contiguous + copy_array_to_plane(u, frame.planes[1], 2) + copy_array_to_plane(v, frame.planes[2], 2) + return frame elif format == "yuv422p10le": if not isinstance(array, np.ndarray) or array.dtype != np.uint16: raise ValueError("Array must be uint16 type") diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index bef69550b..9327a4ccd 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -485,6 +485,15 @@ def test_ndarray_yuv444p16() -> None: assertNdarraysEqual(frame.to_ndarray(), array) +def test_ndarray_yuv420p10le() -> None: + array = numpy.random.randint(0, 65536, size=(3, 480, 640), dtype=numpy.uint16) + for format in ("yuv420p10le",): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assert format in av.video.frame.supported_np_pix_fmts + + def test_ndarray_yuv422p10le() -> None: array = numpy.random.randint(0, 65536, size=(3, 480, 640), dtype=numpy.uint16) for format in ("yuv422p10le",): From 3353ec22d7a8828fe5185d432e965244f361655b Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Fri, 28 Mar 2025 12:12:29 -0600 Subject: [PATCH 253/365] Allow venv to be used for build --- scripts/activate.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/activate.sh b/scripts/activate.sh index 72bd44738..9a45b964e 100755 --- a/scripts/activate.sh +++ b/scripts/activate.sh @@ -23,7 +23,10 @@ if [[ ! "$PYAV_LIBRARY" ]]; then fi export PYAV_LIBRARY -if [[ ! "$PYAV_PYTHON" ]]; then +if [[ "$VIRTUAL_ENV" ]]; then + PYAV_PYTHON="${VIRTUAL_ENV}/bin/python3" + echo "Using activated venv: $VIRTUAL_ENV" +elif [[ ! "$PYAV_PYTHON" ]]; then PYAV_PYTHON="${PYAV_PYTHON-python3}" echo 'No $PYAV_PYTHON set; defaulting to python3.' fi @@ -44,6 +47,9 @@ if [[ "$GITHUB_ACTION" ]]; then # GitHub has a very self-contained environment. Lets just work in that. echo "We're on CI, so not setting up another virtualenv." +elif [[ "$VIRTUAL_ENV" ]]; then + # Using activated venv + true else export PYAV_VENV_NAME="$(uname -s).$(uname -r).$("$PYAV_PYTHON" -c ' From 1a04fab45f373f801889139f38033dad51474a2d Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Fri, 28 Mar 2025 12:11:26 -0600 Subject: [PATCH 254/365] Use commands that work on macOS --- scripts/activate.sh | 6 +++++- scripts/build-deps | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/activate.sh b/scripts/activate.sh index 9a45b964e..06e956028 100755 --- a/scripts/activate.sh +++ b/scripts/activate.sh @@ -6,7 +6,11 @@ if [[ "$0" == "${BASH_SOURCE[0]}" ]]; then exit 1 fi -export PYAV_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.."; pwd)" +if [[ "$(uname)" == "Darwin" ]]; then + export PYAV_ROOT="$(realpath -- "$(dirname -- "$(readlink -f -- "$0")")/..")" +else + export PYAV_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.."; pwd)" +fi if [[ ! "$PYAV_LIBRARY" ]]; then if [[ "$1" ]]; then diff --git a/scripts/build-deps b/scripts/build-deps index 9cc795305..4291140dd 100755 --- a/scripts/build-deps +++ b/scripts/build-deps @@ -47,7 +47,7 @@ cd "$PYAV_LIBRARY_ROOT" if [[ ! -d $PYAV_LIBRARY ]]; then url="https://ffmpeg.org/releases/$PYAV_LIBRARY.tar.gz" echo Downloading $url - wget --no-check-certificate "$url" || exit 1 + curl "$url" --output ${PYAV_LIBRARY}.tar.gz || exit 1 tar -xzf $PYAV_LIBRARY.tar.gz rm $PYAV_LIBRARY.tar.gz echo From a670d5fcbb165098c111316710bcfbb1de1f2599 Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Fri, 28 Mar 2025 13:51:48 -0600 Subject: [PATCH 255/365] Change check for zsh instead of macOS --- scripts/activate.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/activate.sh b/scripts/activate.sh index 06e956028..9dd41ba2b 100755 --- a/scripts/activate.sh +++ b/scripts/activate.sh @@ -6,7 +6,7 @@ if [[ "$0" == "${BASH_SOURCE[0]}" ]]; then exit 1 fi -if [[ "$(uname)" == "Darwin" ]]; then +if [[ -n "$ZSH_VERSION" ]]; then export PYAV_ROOT="$(realpath -- "$(dirname -- "$(readlink -f -- "$0")")/..")" else export PYAV_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.."; pwd)" From b6a3e5e779932e04e8a5462786566ca8c1086dde Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 28 Mar 2025 22:26:04 -0400 Subject: [PATCH 256/365] Mention bitrate in changelog --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a5d57b4c7..828230ac8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -31,6 +31,7 @@ Features: Misc: - Drop support for MacOS 11 by :gh-user:`WyattBlue` in (:pr:`1764`). +- Set default bitrate to 0, allowing variable bitrate by :gh-user:`WyattBlue`. v14.1.0 From 18de7be6a6f68544b82fcc8722c8a962df1b98c0 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 29 Mar 2025 09:45:33 -0400 Subject: [PATCH 257/365] Support writing files in Cython's pure python mode --- .github/workflows/smoke.yml | 3 +- .github/workflows/tests.yml | 2 +- Makefile | 2 +- av/filter/loudnorm.pxd | 15 ++++++++ av/filter/loudnorm.py | 48 +++++++++++++++++++++++++ av/filter/loudnorm.pyx | 69 ------------------------------------ av/{packet.pyx => packet.py} | 64 +++++++++++++++++---------------- pyproject.toml | 2 +- scripts/build | 2 +- setup.py | 22 +++++++----- 10 files changed, 117 insertions(+), 112 deletions(-) create mode 100644 av/filter/loudnorm.py delete mode 100644 av/filter/loudnorm.pyx rename av/{packet.pyx => packet.py} (79%) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 40dcb3fe6..5882bf1cf 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -121,8 +121,9 @@ jobs: . $CONDA/etc/profile.d/conda.sh conda config --set always_yes true conda config --add channels conda-forge + conda config --add channels scientific-python-nightly-wheels conda create -q -n pyav \ - cython \ + cython==3.1.0b0 \ numpy \ pillow \ pytest \ diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a6c5232b3..bf30096d5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,7 +13,7 @@ jobs: python-version: "3.13" - name: Build source package run: | - pip install setuptools cython + pip install -U --pre cython setuptools python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.1.json /tmp/vendor PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig python setup.py sdist - name: Upload source package diff --git a/Makefile b/Makefile index 667ff3116..84a36f2a8 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ default: build build: - $(PIP) install -U cython setuptools + $(PIP) install -U --pre cython setuptools CFLAGS=$(CFLAGS) LDFLAGS=$(LDFLAGS) $(PYTHON) setup.py build_ext --inplace --debug clean: diff --git a/av/filter/loudnorm.pxd b/av/filter/loudnorm.pxd index b08d3502f..2729dd8be 100644 --- a/av/filter/loudnorm.pxd +++ b/av/filter/loudnorm.pxd @@ -1,4 +1,19 @@ from av.audio.stream cimport AudioStream +cdef extern from "libavcodec/avcodec.h": + ctypedef struct AVCodecContext: + pass + +cdef extern from "libavformat/avformat.h": + ctypedef struct AVFormatContext: + pass + +cdef extern from "loudnorm_impl.h": + char* loudnorm_get_stats( + AVFormatContext* fmt_ctx, + int audio_stream_index, + const char* loudnorm_args + ) nogil + cpdef bytes stats(str loudnorm_args, AudioStream stream) diff --git a/av/filter/loudnorm.py b/av/filter/loudnorm.py new file mode 100644 index 000000000..0be991ab6 --- /dev/null +++ b/av/filter/loudnorm.py @@ -0,0 +1,48 @@ +import cython +from cython.cimports.av.audio.stream import AudioStream +from cython.cimports.av.container.core import Container +from cython.cimports.libc.stdlib import free + +from av.logging import get_level, set_level + + +@cython.ccall +def stats(loudnorm_args: str, stream: AudioStream) -> bytes: + """ + Get loudnorm statistics for an audio stream. + + Args: + loudnorm_args (str): Arguments for the loudnorm filter (e.g. "i=-24.0:lra=7.0:tp=-2.0") + stream (AudioStream): Input audio stream to analyze + + Returns: + bytes: JSON string containing the loudnorm statistics + """ + + if "print_format=json" not in loudnorm_args: + loudnorm_args = loudnorm_args + ":print_format=json" + + container: Container = stream.container + format_ptr: cython.pointer[AVFormatContext] = container.ptr + container.ptr = cython.NULL # Prevent double-free + + stream_index: cython.int = stream.index + py_args: bytes = loudnorm_args.encode("utf-8") + c_args: cython.p_const_char = py_args + result: cython.p_char + + # Save log level since C function overwrite it. + level = get_level() + + with cython.nogil: + result = loudnorm_get_stats(format_ptr, stream_index, c_args) + + if result == cython.NULL: + raise RuntimeError("Failed to get loudnorm stats") + + py_result = result[:] # Make a copy of the string + free(result) # Free the C string + + set_level(level) + + return py_result diff --git a/av/filter/loudnorm.pyx b/av/filter/loudnorm.pyx deleted file mode 100644 index 78f320a9e..000000000 --- a/av/filter/loudnorm.pyx +++ /dev/null @@ -1,69 +0,0 @@ -# av/filter/loudnorm.pyx - -cimport libav as lib -from cpython.bytes cimport PyBytes_FromString -from libc.stdlib cimport free - -from av.audio.codeccontext cimport AudioCodecContext -from av.audio.stream cimport AudioStream -from av.container.core cimport Container -from av.stream cimport Stream -from av.logging import get_level, set_level - - -cdef extern from "libavcodec/avcodec.h": - ctypedef struct AVCodecContext: - pass - -cdef extern from "libavformat/avformat.h": - ctypedef struct AVFormatContext: - pass - -cdef extern from "loudnorm_impl.h": - char* loudnorm_get_stats( - AVFormatContext* fmt_ctx, - int audio_stream_index, - const char* loudnorm_args - ) nogil - - -cpdef bytes stats(str loudnorm_args, AudioStream stream): - """ - Get loudnorm statistics for an audio stream. - - Args: - loudnorm_args (str): Arguments for the loudnorm filter (e.g. "i=-24.0:lra=7.0:tp=-2.0") - stream (AudioStream): Input audio stream to analyze - - Returns: - bytes: JSON string containing the loudnorm statistics - """ - - if "print_format=json" not in loudnorm_args: - loudnorm_args = loudnorm_args + ":print_format=json" - - cdef Container container = stream.container - cdef AVFormatContext* format_ptr = container.ptr - - container.ptr = NULL # Prevent double-free - - cdef int stream_index = stream.index - cdef bytes py_args = loudnorm_args.encode("utf-8") - cdef const char* c_args = py_args - cdef char* result - - # Save log level since C function overwrite it. - level = get_level() - - with nogil: - result = loudnorm_get_stats(format_ptr, stream_index, c_args) - - if result == NULL: - raise RuntimeError("Failed to get loudnorm stats") - - py_result = result[:] # Make a copy of the string - free(result) # Free the C string - - set_level(level) - - return py_result diff --git a/av/packet.pyx b/av/packet.py similarity index 79% rename from av/packet.pyx rename to av/packet.py index b5c9251eb..c49085c57 100644 --- a/av/packet.pyx +++ b/av/packet.py @@ -1,27 +1,30 @@ -cimport libav as lib +import cython +from cython.cimports import libav as lib +from cython.cimports.av.bytesource import bytesource +from cython.cimports.av.error import err_check +from cython.cimports.av.opaque import opaque_container +from cython.cimports.av.utils import avrational_to_fraction, to_avrational -from av.bytesource cimport bytesource -from av.error cimport err_check -from av.opaque cimport opaque_container -from av.utils cimport avrational_to_fraction, to_avrational - - -cdef class Packet(Buffer): +@cython.cclass +class Packet(Buffer): """A packet of encoded data within a :class:`~av.format.Stream`. This may, or may not include a complete object within a stream. :meth:`decode` must be called to extract encoded data. - """ def __cinit__(self, input=None): - with nogil: + with cython.nogil: self.ptr = lib.av_packet_alloc() + def __dealloc__(self): + with cython.nogil: + lib.av_packet_free(cython.address(self.ptr)) + def __init__(self, input=None): - cdef size_t size = 0 - cdef ByteSource source = None + size: cython.size_t = 0 + source: ByteSource = None if input is None: return @@ -41,24 +44,24 @@ def __init__(self, input=None): # instead of its data. # self.source = source - def __dealloc__(self): - with nogil: - lib.av_packet_free(&self.ptr) - def __repr__(self): stream = self._stream.index if self._stream else 0 return ( - f"" ) # Buffer protocol. - cdef size_t _buffer_size(self): + @cython.cfunc + def _buffer_size(self) -> cython.size_t: return self.ptr.size - cdef void* _buffer_ptr(self): + + @cython.cfunc + def _buffer_ptr(self) -> cython.p_void: return self.ptr.data - cdef _rebase_time(self, lib.AVRational dst): + @cython.cfunc + def _rebase_time(self, dst: lib.AVRational): if not dst.num: raise ValueError("Cannot rebase to zero time.") @@ -92,7 +95,7 @@ def stream(self): return self._stream @stream.setter - def stream(self, Stream stream): + def stream(self, stream: Stream): self._stream = stream self.ptr.stream_index = stream.ptr.index @@ -103,11 +106,11 @@ def time_base(self): :type: fractions.Fraction """ - return avrational_to_fraction(&self._time_base) + return avrational_to_fraction(cython.address(self._time_base)) @time_base.setter def time_base(self, value): - to_avrational(value, &self._time_base) + to_avrational(value, cython.address(self._time_base)) @property def pts(self): @@ -116,7 +119,7 @@ def pts(self): This is the time at which the packet should be shown to the user. - :type: int + :type: int | None """ if self.ptr.pts != lib.AV_NOPTS_VALUE: return self.ptr.pts @@ -133,7 +136,7 @@ def dts(self): """ The decoding timestamp in :attr:`time_base` units for this packet. - :type: int + :type: int | None """ if self.ptr.dts != lib.AV_NOPTS_VALUE: return self.ptr.dts @@ -152,7 +155,7 @@ def pos(self): Returns `None` if it is not known. - :type: int + :type: int | None """ if self.ptr.pos != -1: return self.ptr.pos @@ -221,14 +224,15 @@ def is_disposable(self): @property def opaque(self): - if self.ptr.opaque_ref is not NULL: - return opaque_container.get( self.ptr.opaque_ref.data) + if self.ptr.opaque_ref is not cython.NULL: + return opaque_container.get( + cython.cast(cython.p_char, self.ptr.opaque_ref.data) + ) @opaque.setter def opaque(self, v): - lib.av_buffer_unref(&self.ptr.opaque_ref) + lib.av_buffer_unref(cython.address(self.ptr.opaque_ref)) if v is None: return self.ptr.opaque_ref = opaque_container.add(v) - diff --git a/pyproject.toml b/pyproject.toml index 15ed77023..0bfb6cce9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>61", "cython>=3,<4"] +requires = ["setuptools>61", "cython>=3.1.0a1,<4"] [project] name = "av" diff --git a/scripts/build b/scripts/build index 7e27d7f74..3b9346d60 100755 --- a/scripts/build +++ b/scripts/build @@ -21,6 +21,6 @@ which ffmpeg || exit 2 ffmpeg -version || exit 3 echo -$PYAV_PIP install -U cython setuptools 2> /dev/null +$PYAV_PIP install -U --pre cython setuptools 2> /dev/null "$PYAV_PYTHON" scripts/comptime.py "$PYAV_PYTHON" setup.py config build_ext --inplace || exit 1 diff --git a/setup.py b/setup.py index f1ae841dd..d5ad4fb31 100644 --- a/setup.py +++ b/setup.py @@ -177,13 +177,15 @@ def parse_cflags(raw_flags): "library_dirs": [], } +IMPORT_NAME = "av" + loudnorm_extension = Extension( - "av.filter.loudnorm", + f"{IMPORT_NAME}.filter.loudnorm", sources=[ - "av/filter/loudnorm.pyx", - "av/filter/loudnorm_impl.c", + f"{IMPORT_NAME}/filter/loudnorm.py", + f"{IMPORT_NAME}/filter/loudnorm_impl.c", ], - include_dirs=["av/filter"] + extension_extra["include_dirs"], + include_dirs=[f"{IMPORT_NAME}/filter"] + extension_extra["include_dirs"], libraries=extension_extra["libraries"], library_dirs=extension_extra["library_dirs"], ) @@ -204,10 +206,14 @@ def parse_cflags(raw_flags): include_path=["include"], ) -for dirname, dirnames, filenames in os.walk("av"): +for dirname, dirnames, filenames in os.walk(IMPORT_NAME): for filename in filenames: # We are looking for Cython sources. - if filename.startswith(".") or os.path.splitext(filename)[1] != ".pyx": + if filename.startswith("."): + continue + if filename in {"__init__.py", "__main__.py", "about.py", "datasets.py"}: + continue + if os.path.splitext(filename)[1] not in {".pyx", ".py"}: continue pyx_path = os.path.join(dirname, filename) @@ -236,13 +242,13 @@ def parse_cflags(raw_flags): insert_enum_in_generated_files(cfile) -package_folders = pathlib.Path("av").glob("**/") +package_folders = pathlib.Path(IMPORT_NAME).glob("**/") package_data = { ".".join(pckg.parts): ["*.pxd", "*.pyi", "*.typed"] for pckg in package_folders } setup( - packages=find_packages(include=["av*"]), + packages=find_packages(include=[f"{IMPORT_NAME}*"]), package_data=package_data, ext_modules=ext_modules, ) From 29b44beb86796b841526b3be5b4771c29ded2cdc Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 29 Mar 2025 10:25:12 -0400 Subject: [PATCH 258/365] Use ruff for formatting --- .github/workflows/smoke.yml | 2 +- Makefile | 5 ++--- pyproject.toml | 9 --------- tests/test_decode.py | 6 +++--- 4 files changed, 6 insertions(+), 16 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 5882bf1cf..e8aac3330 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -123,7 +123,6 @@ jobs: conda config --add channels conda-forge conda config --add channels scientific-python-nightly-wheels conda create -q -n pyav \ - cython==3.1.0b0 \ numpy \ pillow \ pytest \ @@ -135,6 +134,7 @@ jobs: run: | . $CONDA/etc/profile.d/conda.sh conda activate pyav + pip install 'Cython==3.1.0a1' python scripts\\fetch-vendor.py --config-file scripts\\ffmpeg-${{ matrix.config.ffmpeg }}.json $CONDA_PREFIX\\Library python scripts\\comptime.py ${{ matrix.config.ffmpeg }} python setup.py build_ext --inplace --ffmpeg-dir=$CONDA_PREFIX\\Library diff --git a/Makefile b/Makefile index 84a36f2a8..4b4f58ab7 100644 --- a/Makefile +++ b/Makefile @@ -28,9 +28,8 @@ fate-suite: rsync -vrltLW rsync://fate-suite.ffmpeg.org/fate-suite/ tests/assets/fate-suite/ lint: - $(PIP) install -U black isort flake8 flake8-pyproject pillow numpy mypy==1.15.0 pytest - black --check av examples tests setup.py - flake8 av + $(PIP) install -U ruff isort pillow numpy mypy==1.15.0 pytest + ruff format --check av examples tests setup.py isort --check-only --diff av examples tests mypy av tests diff --git a/pyproject.toml b/pyproject.toml index 0bfb6cce9..458beee81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,12 +52,3 @@ homepage = "https://pyav.basswood-io.com" profile = "black" known_first_party = ["av"] skip = ["av/__init__.py"] - -[tool.flake8] -filename = ["*.py", "*.pyx", "*.pxd"] -ignore = ["E203", "W503"] -max-line-length = 142 -per-file-ignores = [ - "__init__.py:E402,F401", - "*.pyx,*.pxd:E211,E225,E227,E402,E999", -] \ No newline at end of file diff --git a/tests/test_decode.py b/tests/test_decode.py index c1846af69..7ad722bfd 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -218,9 +218,9 @@ def test_hardware_decode(self) -> None: ) HWACCEL_DEVICE_TYPE = os.environ["HWACCEL_DEVICE_TYPE"] - assert ( - HWACCEL_DEVICE_TYPE in hwdevices_available - ), f"{HWACCEL_DEVICE_TYPE} not available" + assert HWACCEL_DEVICE_TYPE in hwdevices_available, ( + f"{HWACCEL_DEVICE_TYPE} not available" + ) test_video_path = "tests/assets/black.mp4" make_h264_test_video(test_video_path) From dc0e6d0700c9487739292ace58a8d3d9ea4be80f Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 29 Mar 2025 10:40:10 -0400 Subject: [PATCH 259/365] Make audio/codeccontext pure --- .../{codeccontext.pyx => codeccontext.py} | 55 +++++++++---------- 1 file changed, 27 insertions(+), 28 deletions(-) rename av/audio/{codeccontext.pyx => codeccontext.py} (63%) diff --git a/av/audio/codeccontext.pyx b/av/audio/codeccontext.py similarity index 63% rename from av/audio/codeccontext.pyx rename to av/audio/codeccontext.py index 856af555c..2dc629917 100644 --- a/av/audio/codeccontext.pyx +++ b/av/audio/codeccontext.py @@ -1,21 +1,20 @@ -cimport libav as lib - -from av.audio.format cimport AudioFormat, get_audio_format -from av.audio.frame cimport AudioFrame, alloc_audio_frame -from av.audio.layout cimport AudioLayout, get_audio_layout -from av.codec.hwaccel cimport HWAccel -from av.frame cimport Frame -from av.packet cimport Packet - - -cdef class AudioCodecContext(CodecContext): - cdef _init(self, lib.AVCodecContext *ptr, const lib.AVCodec *codec, HWAccel hwaccel): - CodecContext._init(self, ptr, codec, hwaccel) - - cdef _prepare_frames_for_encode(self, Frame input_frame): - - cdef AudioFrame frame = input_frame - cdef bint allow_var_frame_size = self.ptr.codec.capabilities & lib.AV_CODEC_CAP_VARIABLE_FRAME_SIZE +import cython +from cython.cimports import libav as lib +from cython.cimports.av.audio.format import AudioFormat, get_audio_format +from cython.cimports.av.audio.frame import AudioFrame, alloc_audio_frame +from cython.cimports.av.audio.layout import AudioLayout, get_audio_layout +from cython.cimports.av.frame import Frame +from cython.cimports.av.packet import Packet + + +@cython.cclass +class AudioCodecContext(CodecContext): + @cython.cfunc + def _prepare_frames_for_encode(self, input_frame: Frame | None): + frame: AudioFrame | None = input_frame + allow_var_frame_size: cython.bint = ( + self.ptr.codec.capabilities & lib.AV_CODEC_CAP_VARIABLE_FRAME_SIZE + ) # Note that the resampler will simply return an input frame if there is # no resampling to be done. The control flow was just a little easier this way. @@ -24,22 +23,22 @@ format=self.format, layout=self.layout, rate=self.ptr.sample_rate, - frame_size=None if allow_var_frame_size else self.ptr.frame_size + frame_size=None if allow_var_frame_size else self.ptr.frame_size, ) frames = self.resampler.resample(frame) - - # flush if input frame is None if input_frame is None: - frames.append(None) + frames.append(None) # flush if input frame is None return frames - cdef Frame _alloc_next_frame(self): + @cython.cfunc + def _alloc_next_frame(self) -> Frame: return alloc_audio_frame() - cdef _setup_decoded_frame(self, Frame frame, Packet packet): + @cython.cfunc + def _setup_decoded_frame(self, frame: Frame, packet: Packet): CodecContext._setup_decoded_frame(self, frame, packet) - cdef AudioFrame aframe = frame + aframe: AudioFrame = frame aframe._init_user_attributes() @property @@ -61,7 +60,7 @@ def sample_rate(self): return self.ptr.sample_rate @sample_rate.setter - def sample_rate(self, int value): + def sample_rate(self, value: cython.int): self.ptr.sample_rate = value @property @@ -88,7 +87,7 @@ def layout(self): @layout.setter def layout(self, value): - cdef AudioLayout layout = AudioLayout(value) + layout: AudioLayout = AudioLayout(value) self.ptr.ch_layout = layout.layout @property @@ -102,5 +101,5 @@ def format(self): @format.setter def format(self, value): - cdef AudioFormat format = AudioFormat(value) + format: AudioFormat = AudioFormat(value) self.ptr.sample_fmt = format.sample_fmt From 4e01ca7474210b164fd630f47571455a42ce09f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Tue, 1 Apr 2025 09:28:04 +0200 Subject: [PATCH 260/365] Add typing overloads for `CodecContext.create` The return type of `CodecContext.create` can be narrowed based on the name of the requested codec, allowing us to know whether we are dealing with an `AudioCodecContext` or a `VideoCodecContext`. As the same logic is used by `OutputContainer.add_stream`, use a shared list of audio / video codec names. We add the following codec names: - audio : libopus, pcm_alaw, pcm_mulaw - video : libvpx, libx264 --- av/audio/__init__.pyi | 12 ++++++++++++ av/codec/context.pyi | 21 ++++++++++++++++++++- av/container/output.pyi | 9 +++++---- av/video/__init__.pyi | 13 +++++++++++++ 4 files changed, 50 insertions(+), 5 deletions(-) diff --git a/av/audio/__init__.pyi b/av/audio/__init__.pyi index 73f2eebdd..daefab6c9 100644 --- a/av/audio/__init__.pyi +++ b/av/audio/__init__.pyi @@ -1,4 +1,16 @@ +from typing import Literal + from .frame import AudioFrame from .stream import AudioStream +_AudioCodecName = Literal[ + "aac", + "libopus", + "mp2", + "mp3", + "pcm_alaw", + "pcm_mulaw", + "pcm_s16le", +] + __all__ = ("AudioFrame", "AudioStream") diff --git a/av/codec/context.pyi b/av/codec/context.pyi index ac57126ef..77810d9ed 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -1,8 +1,12 @@ from enum import Flag, IntEnum from fractions import Fraction -from typing import ClassVar, Literal, cast +from typing import ClassVar, Literal, cast, overload +from av.audio import _AudioCodecName +from av.audio.codeccontext import AudioCodecContext from av.packet import Packet +from av.video import _VideoCodecName +from av.video.codeccontext import VideoCodecContext from .codec import Codec from .hwaccel import HWAccel @@ -87,6 +91,21 @@ class CodecContext: @property def is_hwaccel(self) -> bool: ... def open(self, strict: bool = True) -> None: ... + @overload + @staticmethod + def create( + codec: _AudioCodecName, + mode: Literal["r", "w"] | None = None, + hwaccel: HWAccel | None = None, + ) -> AudioCodecContext: ... + @overload + @staticmethod + def create( + codec: _VideoCodecName, + mode: Literal["r", "w"] | None = None, + hwaccel: HWAccel | None = None, + ) -> VideoCodecContext: ... + @overload @staticmethod def create( codec: str | Codec, diff --git a/av/container/output.pyi b/av/container/output.pyi index 3169f486b..568345cd2 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -1,11 +1,12 @@ from fractions import Fraction -from typing import Literal, Sequence, TypeVar, Union, overload +from typing import Sequence, TypeVar, Union, overload +from av.audio import _AudioCodecName from av.audio.stream import AudioStream from av.data.stream import DataStream from av.packet import Packet -from av.stream import Stream from av.subtitles.stream import SubtitleStream +from av.video import _VideoCodecName from av.video.stream import VideoStream from .core import Container @@ -17,7 +18,7 @@ class OutputContainer(Container): @overload def add_stream( self, - codec_name: Literal["pcm_s16le", "aac", "mp3", "mp2"], + codec_name: _AudioCodecName, rate: int | None = None, options: dict[str, str] | None = None, **kwargs, @@ -25,7 +26,7 @@ class OutputContainer(Container): @overload def add_stream( self, - codec_name: Literal["h264", "hevc", "mpeg4", "png", "gif", "qtrle"], + codec_name: _VideoCodecName, rate: Fraction | int | None = None, options: dict[str, str] | None = None, **kwargs, diff --git a/av/video/__init__.pyi b/av/video/__init__.pyi index 8fa8fe7e5..58a19a63f 100644 --- a/av/video/__init__.pyi +++ b/av/video/__init__.pyi @@ -1,4 +1,17 @@ +from typing import Literal + from .frame import VideoFrame from .stream import VideoStream +_VideoCodecName = Literal[ + "gif", + "h264", + "hevc", + "libvpx", + "libx264", + "mpeg4", + "png", + "qtrle", +] + __all__ = ("VideoFrame", "VideoStream") From bc9ae5b9092f75520aa0e7f646c9d9ba355b7957 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 1 Apr 2025 23:25:28 -0400 Subject: [PATCH 261/365] Use ffmpeg 7.1.1 --- .github/workflows/smoke.yml | 6 +++--- CHANGELOG.rst | 7 +++++++ scripts/activate.sh | 2 +- scripts/ffmpeg-7.1.json | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index e8aac3330..e83adf62d 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -34,9 +34,9 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-24.04, python: "3.12", ffmpeg: "7.1", extras: true} - - {os: ubuntu-24.04, python: "pypy3.10", ffmpeg: "7.1"} - - {os: macos-14, python: "3.9", ffmpeg: "7.1"} + - {os: ubuntu-24.04, python: "3.12", ffmpeg: "7.1.1", extras: true} + - {os: ubuntu-24.04, python: "pypy3.10", ffmpeg: "7.1.1"} + - {os: macos-14, python: "3.9", ffmpeg: "7.1.1"} env: PYAV_PYTHON: python${{ matrix.config.python }} diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 828230ac8..663cf4839 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,6 +17,13 @@ We are operating with `semantic versioning `_. are merged into the "default" branch. +v14.2.1 +------- + +Fixes: + +- Uses ffmpeg 7.1.1, fixes deadlocks. + v14.2.0 ------- diff --git a/scripts/activate.sh b/scripts/activate.sh index 9dd41ba2b..ab22d79d9 100755 --- a/scripts/activate.sh +++ b/scripts/activate.sh @@ -21,7 +21,7 @@ if [[ ! "$PYAV_LIBRARY" ]]; then return 1 fi else - PYAV_LIBRARY=ffmpeg-7.1 + PYAV_LIBRARY=ffmpeg-7.1.1 echo "No \$PYAV_LIBRARY set; defaulting to $PYAV_LIBRARY" fi fi diff --git a/scripts/ffmpeg-7.1.json b/scripts/ffmpeg-7.1.json index ee386661d..2af06b2cb 100644 --- a/scripts/ffmpeg-7.1.json +++ b/scripts/ffmpeg-7.1.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.1-4/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.1.1-1/ffmpeg-{platform}.tar.gz" } From 6940c5e048fcf17894d5a3caa72849aaa2d63829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Sat, 5 Apr 2025 10:34:45 +0200 Subject: [PATCH 262/365] Provide binary wheels for musl-based Linux distros such as Alpine --- .github/workflows/tests.yml | 3 +-- CHANGELOG.rst | 4 ++++ scripts/fetch-vendor.py | 5 ++++- scripts/ffmpeg-7.1.json | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bf30096d5..a38af2c08 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -57,7 +57,6 @@ jobs: - name: Build wheels env: CIBW_ARCHS: ${{ matrix.arch }} - CIBW_BEFORE_ALL_LINUX: yum install -y alsa-lib libxcb CIBW_BEFORE_BUILD: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.1.json /tmp/vendor CIBW_BEFORE_BUILD_MACOS: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.1.json /tmp/vendor CIBW_BEFORE_BUILD_WINDOWS: python scripts\fetch-vendor.py --config-file scripts\ffmpeg-7.1.json C:\cibw\vendor @@ -65,7 +64,7 @@ jobs: CIBW_ENVIRONMENT_MACOS: PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig LDFLAGS=-headerpad_max_install_names CIBW_ENVIRONMENT_WINDOWS: INCLUDE=C:\\cibw\\vendor\\include LIB=C:\\cibw\\vendor\\lib PYAV_SKIP_TESTS=unicode_filename CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: delvewheel repair --add-path C:\cibw\vendor\bin -w {dest_dir} {wheel} - CIBW_SKIP: "pp* *-musllinux*" + CIBW_SKIP: "pp*" CIBW_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m pytest {package}/tests && mv {project}/av.disabled {project}/av CIBW_TEST_REQUIRES: pytest numpy # skip tests when there are no binary wheels of numpy diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 663cf4839..1580e0a63 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -20,6 +20,10 @@ We are operating with `semantic versioning `_. v14.2.1 ------- +Features: + +- Provide binary wheels for musl-based distros such as Alpine by :gh-user:`jlaine` in (:pr:`1856`). + Fixes: - Uses ffmpeg 7.1.1, fixes deadlocks. diff --git a/scripts/fetch-vendor.py b/scripts/fetch-vendor.py index 5041f4265..47aa2a421 100644 --- a/scripts/fetch-vendor.py +++ b/scripts/fetch-vendor.py @@ -11,7 +11,10 @@ def get_platform(): system = platform.system() machine = platform.machine() if system == "Linux": - return f"manylinux_{machine}" + if platform.libc_ver()[0] == "glibc": + return f"manylinux_{machine}" + else: + return f"musllinux_{machine}" elif system == "Darwin": # cibuildwheel sets ARCHFLAGS: # https://github.com/pypa/cibuildwheel/blob/5255155bc57eb6224354356df648dc42e31a0028/cibuildwheel/macos.py#L207-L220 diff --git a/scripts/ffmpeg-7.1.json b/scripts/ffmpeg-7.1.json index 2af06b2cb..42601a31d 100644 --- a/scripts/ffmpeg-7.1.json +++ b/scripts/ffmpeg-7.1.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.1.1-1/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.1.1-2/ffmpeg-{platform}.tar.gz" } From 81fd054d3d92714ed7ce973962397399e8af5b53 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 6 Apr 2025 02:31:22 -0400 Subject: [PATCH 263/365] Update authors --- AUTHORS.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 1792ba863..df46c6c11 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -16,12 +16,12 @@ All contributors (by number of commits): * Casper van der Wel * Philip de Nier * Tadas Dailyda +* Dave Johansen * JoeUgly <41972063+JoeUgly@users.noreply.github.com> * Justin Wong <46082645+uvjustin@users.noreply.github.com> * Mark Harfouche - Alba Mendez -- Dave Johansen - Xinran Xu ; `@xxr3376 `_ - Dan Allan ; `@danielballan `_ - Moonsik Park @@ -78,10 +78,12 @@ All contributors (by number of commits): * Johan Jeppsson Karlin * Philipp Klaus * Kim Minjong +* Marcell Pardavi * Matteo Destro * Mattias Wadman * Manuel Goacolou * Julian Schweizer +* Nikhil Idiculla * Ömer Sezgin Uğurlu * Orivej Desh * Philipp Krähenbühl From 4c29ccd4badbbdfb5b57ff0b114b7f6a21b79a2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Sun, 6 Apr 2025 11:27:47 +0200 Subject: [PATCH 264/365] Bump version to 14.3.0 --- CHANGELOG.rst | 10 +++++++--- av/about.py | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1580e0a63..71ddc1214 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,15 +17,19 @@ We are operating with `semantic versioning `_. are merged into the "default" branch. -v14.2.1 +v14.3.0 ------- Features: +- Support yuv420p10le in numpy conversion by :gh-user:`tsnl` in (:pr:`1833`). +- Add typing overloads for ``CodecContext.create`` by :gh-user:`jlaine` in (:pr:`1853`). - Provide binary wheels for musl-based distros such as Alpine by :gh-user:`jlaine` in (:pr:`1856`). Fixes: +- Fix ``Packet.dts`` typing by :gh-user:`daveisfera` in (:pr:`1815`). +- Use Python 3 compatible stderr logging by :gh-user:`pmarcelll` in (:pr:`1832`). - Uses ffmpeg 7.1.1, fixes deadlocks. v14.2.0 @@ -53,8 +57,8 @@ Features: - Add hardware decoding by :gh-user:`matthewlai` and :gh-user:`WyattBlue` in (:pr:`1685`). - Add ``Stream.disposition`` and ``Disposition`` enum by :gh-user:`WyattBlue` in (:pr:`1720`). - Add ``VideoFrame.rotation`` by :gh-user:`lgeiger` in (:pr:`1675`). -- Support grayf32le and gbrapf32le in numpy convertion by :gh-user:`robinechuca` in (:pr:`1712`). -- Support yuv[a]p16 formats in numpy convertion by :gh-user:`robinechuca` in (:pr:`1722`). +- Support grayf32le and gbrapf32le in numpy conversion by :gh-user:`robinechuca` in (:pr:`1712`). +- Support yuv[a]p16 formats in numpy conversion by :gh-user:`robinechuca` in (:pr:`1722`). v14.0.1 ------- diff --git a/av/about.py b/av/about.py index 331dde6ff..f79d47d48 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "14.2.0" +__version__ = "14.3.0" From 1cd9c8a4e5257fa3a267a5617490cd92fe352590 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Sun, 6 Apr 2025 17:33:02 +0200 Subject: [PATCH 265/365] Update license information according to PEP 639 --- pyproject.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 458beee81..26ebae4ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ requires = ["setuptools>61", "cython>=3.1.0a1,<4"] name = "av" description = "Pythonic bindings for FFmpeg's libraries." readme = "README.md" -license = {text = "BSD-3-Clause"} +license = "BSD-3-Clause" authors = [ {name = "WyattBlue", email = "wyattblue@auto-editor.com"}, {name = "Jeremy Lainé", email = "jeremy.laine@m4x.org"}, @@ -14,7 +14,6 @@ requires-python = ">=3.9" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", - "License :: OSI Approved :: BSD License", "Natural Language :: English", "Operating System :: MacOS :: MacOS X", "Operating System :: POSIX", From cc45b2c504697402d0b6d15d6f4e8010c4dcdbb3 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sun, 13 Apr 2025 12:15:10 +0200 Subject: [PATCH 266/365] Bump setuptools requirement for PEP 639 support --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 26ebae4ef..9710de837 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>61", "cython>=3.1.0a1,<4"] +requires = ["setuptools>=77.0", "cython>=3.1.0a1,<4"] [project] name = "av" From f3504bad6194263208a331a6687dc0328f4a9571 Mon Sep 17 00:00:00 2001 From: z-khan Date: Sat, 19 Apr 2025 23:08:12 +1000 Subject: [PATCH 267/365] update cuda config flags update cuda config flag to "--enable-cuda-nvcc" as per Video Codec SDK v13.0 https://docs.nvidia.com/video-technologies/video-codec-sdk/13.0/ffmpeg-with-nvidia-gpu/index.html --- scripts/build-deps | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/scripts/build-deps b/scripts/build-deps index 4291140dd..d278c27c0 100755 --- a/scripts/build-deps +++ b/scripts/build-deps @@ -29,9 +29,7 @@ if [[ -e /usr/local/cuda ]]; then fi PKG_CONFIG_PATH="/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH" - CONFFLAGS_NVIDIA="--enable-cuda \ - --enable-cuvid \ - --enable-nvenc \ + CONFFLAGS_NVIDIA="--enable-cuda-nvcc \ --enable-nonfree \ --enable-libnpp \ --extra-cflags=-I/usr/local/cuda/include \ From fc1402cd1d4dd8570782769d74e13144d5fd2ec6 Mon Sep 17 00:00:00 2001 From: z-khan Date: Thu, 24 Apr 2025 07:24:31 +1000 Subject: [PATCH 268/365] Add BGR48, BGRA64 pixel formats Extend `supported_np_pix_fmts` pixel formats with `bgr48be` `bgr48le` `bgra64be` `bgra64le`. Add handling for `argb` `abgr` `grayf32le` `grayf32be` pixel formats. --------- Co-authored-by: WyattBlue --- av/video/frame.pyx | 49 +++++++++++++------ tests/test_videoframe.py | 103 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 15 deletions(-) diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 785603a1a..04a14fd8b 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -16,7 +16,7 @@ cdef object _cinit_bypass_sentinel supported_np_pix_fmts = { "abgr", "argb", "bayer_bggr16be", "bayer_bggr16le", "bayer_bggr8", "bayer_gbrg16be", "bayer_gbrg16le", "bayer_gbrg8", "bayer_grbg16be", "bayer_grbg16le", "bayer_grbg8", - "bayer_rggb16be", "bayer_rggb16le", "bayer_rggb8", "bgr24", "bgr8", "bgra", + "bayer_rggb16be", "bayer_rggb16le", "bayer_rggb8", "bgr24", "bgr48be", "bgr48le", "bgr8", "bgra", "bgra64be", "bgra64le", "gbrapf32be", "gbrapf32le", "gbrp", "gbrp10be", "gbrp10le", "gbrp12be", "gbrp12le", "gbrp14be", "gbrp14le", "gbrp16be", "gbrp16le", "gbrpf32be", "gbrpf32le", "gray", "gray16be", "gray16le", "gray8", "grayf32be", "grayf32le", "nv12", "pal8", "rgb24", @@ -342,6 +342,8 @@ cdef class VideoFrame(Frame): "bayer_rggb16le": (2, "uint16"), "bayer_rggb16be": (2, "uint16"), "bgr24": (3, "uint8"), + "bgr48be": (6, "uint16"), + "bgr48le": (6, "uint16"), "bgr8": (1, "uint8"), "bgra": (4, "uint8"), "gbrapf32be": (4, "float32"), @@ -370,6 +372,8 @@ cdef class VideoFrame(Frame): "rgba": (4, "uint8"), "rgba64be": (8, "uint16"), "rgba64le": (8, "uint16"), + "bgra64be": (8, "uint16"), + "bgra64le": (8, "uint16"), "yuv444p": (1, "uint8"), "yuv444p16be": (2, "uint16"), "yuv444p16le": (2, "uint16"), @@ -467,43 +471,58 @@ cdef class VideoFrame(Frame): if not width: width = array.shape[1] - if format in ("rgb24", "bgr24"): + if format in {"rgb24", "bgr24"}: check_ndarray(array, "uint8", 3) check_ndarray_shape(array, array.shape[2] == 3) if array.strides[1:] != (3, 1): raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = (array.strides[0], ) + elif format in {"rgb48le", "rgb48be", "bgr48le", "bgr48be"}: + check_ndarray(array, "uint16", 3) + check_ndarray_shape(array, array.shape[2] == 3) + if array.strides[1:] != (6, 2): + raise ValueError("provided array does not have C_CONTIGUOUS rows") linesizes = (array.strides[0], ) - elif format in ("rgba", "bgra"): + elif format in {"rgba", "bgra", "argb", "abgr"}: check_ndarray(array, "uint8", 3) check_ndarray_shape(array, array.shape[2] == 4) if array.strides[1:] != (4, 1): raise ValueError("provided array does not have C_CONTIGUOUS rows") linesizes = (array.strides[0], ) - elif format in ("gray", "gray8", "rgb8", "bgr8"): + elif format in {"rgba64le", "rgba64be", "bgra64le", "bgra64be"}: + check_ndarray(array, "uint16", 3) + check_ndarray_shape(array, array.shape[2] == 4) + if array.strides[1:] != (8, 2): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = (array.strides[0], ) + elif format in {"gray", "gray8", "rgb8", "bgr8","bayer_bggr8", "bayer_rggb8", "bayer_gbrg8", "bayer_grbg8"}: check_ndarray(array, "uint8", 2) if array.strides[1] != 1: raise ValueError("provided array does not have C_CONTIGUOUS rows") linesizes = (array.strides[0], ) - elif format in ("yuv420p", "yuvj420p", "nv12"): + elif format in {"gray16le", "gray16be", "bayer_rggb16le", "bayer_gbrg16le", "bayer_grbg16le","bayer_bggr16be", "bayer_rggb16be", "bayer_gbrg16be", "bayer_grbg16be"}: + check_ndarray(array, "uint16", 2) + if array.strides[1] != 2: + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = (array.strides[0], ) + elif format in {"grayf32le", "grayf32be"}: + check_ndarray(array, "float32", 2) + if array.strides[1] != 4: + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = (array.strides[0], ) + elif format in {"yuv420p", "yuvj420p", "nv12"}: check_ndarray(array, "uint8", 2) check_ndarray_shape(array, array.shape[0] % 3 == 0) check_ndarray_shape(array, array.shape[1] % 2 == 0) height = height // 6 * 4 if array.strides[1] != 1: raise ValueError("provided array does not have C_CONTIGUOUS rows") - if format in ("yuv420p", "yuvj420p"): + if format in {"yuv420p", "yuvj420p"}: # For YUV420 planar formats, the UV plane stride is always half the Y stride. linesizes = (array.strides[0], array.strides[0] // 2, array.strides[0] // 2) else: # Planes where U and V are interleaved have the same stride as Y. linesizes = (array.strides[0], array.strides[0]) - elif format in {"bayer_bggr8", "bayer_rggb8", "bayer_gbrg8", "bayer_grbg8","bayer_bggr16le", "bayer_rggb16le", "bayer_gbrg16le", "bayer_grbg16le","bayer_bggr16be", "bayer_rggb16be", "bayer_gbrg16be", "bayer_grbg16be"}: - check_ndarray(array, "uint8" if format.endswith("8") else "uint16", 2) - - if array.strides[1] != (1 if format.endswith("8") else 2): - raise ValueError("provided array does not have C_CONTIGUOUS rows") - - linesizes = (array.strides[0],) else: raise ValueError(f"Conversion from numpy array with format `{format}` is not yet supported") @@ -717,13 +736,13 @@ cdef class VideoFrame(Frame): elif format in {"argb", "rgba", "abgr", "bgra"}: check_ndarray(array, "uint8", 3) check_ndarray_shape(array, array.shape[2] == 4) - elif format in {"rgb48be", "rgb48le"}: + elif format in {"rgb48be", "rgb48le","bgr48be", "bgr48le"}: check_ndarray(array, "uint16", 3) check_ndarray_shape(array, array.shape[2] == 3) frame = VideoFrame(array.shape[1], array.shape[0], format) copy_array_to_plane(byteswap_array(array, format.endswith("be")), frame.planes[0], 6) return frame - elif format in {"rgba64be", "rgba64le"}: + elif format in {"rgba64be", "rgba64le", "bgra64be", "bgra64le"}: check_ndarray(array, "uint16", 3) check_ndarray_shape(array, array.shape[2] == 4) frame = VideoFrame(array.shape[1], array.shape[0], format) diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 9327a4ccd..26549b31b 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -285,6 +285,35 @@ def test_ndarray_rgba_align() -> None: assertNdarraysEqual(frame.to_ndarray(), array) +def test_ndarray_bayer8() -> None: + array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) + for format in ("bayer_bggr8", "bayer_gbrg8", "bayer_grbg8", "bayer_rggb8"): + frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_bayer16() -> None: + array = numpy.random.randint(0, 65536, size=(480, 640), dtype=numpy.uint16) + for format in ( + "bayer_bggr16be", + "bayer_bggr16le", + "bayer_gbrg16be", + "bayer_gbrg16le", + "bayer_grbg16be", + "bayer_grbg16le", + "bayer_rggb16be", + "bayer_rggb16le", + ): + frame = VideoFrame.from_ndarray(array, format=format) + assert format in av.video.frame.supported_np_pix_fmts + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + def test_ndarray_gbrp() -> None: array = numpy.random.randint(0, 256, size=(480, 640, 3), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="gbrp") @@ -571,6 +600,17 @@ def test_ndarray_rgb48be() -> None: assertPixelValue16(frame.planes[0], array[0][0][0], "big") +def test_ndarray_bgr48be() -> None: + array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="bgr48be") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "bgr48be" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining blue value of first pixel + assertPixelValue16(frame.planes[0], array[0][0][0], "big") + + def test_ndarray_rgb48le() -> None: array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="rgb48le") @@ -582,6 +622,17 @@ def test_ndarray_rgb48le() -> None: assertPixelValue16(frame.planes[0], array[0][0][0], "little") +def test_ndarray_bgr48le() -> None: + array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="bgr48le") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "bgr48le" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining blue value of first pixel + assertPixelValue16(frame.planes[0], array[0][0][0], "little") + + def test_ndarray_rgb48le_align() -> None: array = numpy.random.randint(0, 65536, size=(238, 318, 3), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="rgb48le") @@ -593,6 +644,17 @@ def test_ndarray_rgb48le_align() -> None: assertPixelValue16(frame.planes[0], array[0][0][0], "little") +def test_ndarray_bgr48le_align() -> None: + array = numpy.random.randint(0, 65536, size=(238, 318, 3), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="bgr48le") + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == "bgr48le" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining blue value of first pixel + assertPixelValue16(frame.planes[0], array[0][0][0], "little") + + def test_ndarray_rgba64be() -> None: array = numpy.random.randint(0, 65536, size=(480, 640, 4), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="rgba64be") @@ -604,6 +666,17 @@ def test_ndarray_rgba64be() -> None: assertPixelValue16(frame.planes[0], array[0][0][0], "big") +def test_ndarray_bgra64be() -> None: + array = numpy.random.randint(0, 65536, size=(480, 640, 4), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="bgra64be") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "bgra64be" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining blue value of first pixel + assertPixelValue16(frame.planes[0], array[0][0][0], "big") + + def test_ndarray_rgba64le() -> None: array = numpy.random.randint(0, 65536, size=(480, 640, 4), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="rgba64le") @@ -615,6 +688,17 @@ def test_ndarray_rgba64le() -> None: assertPixelValue16(frame.planes[0], array[0][0][0], "little") +def test_ndarray_bgra64le() -> None: + array = numpy.random.randint(0, 65536, size=(480, 640, 4), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="bgra64le") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "bgra64le" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining blue value of first pixel + assertPixelValue16(frame.planes[0], array[0][0][0], "little") + + def test_ndarray_rgb8() -> None: array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="rgb8") @@ -805,6 +889,25 @@ def test_shares_memory_rgba() -> None: assertNdarraysEqual(frame.to_ndarray(), array) +def test_shares_memory_bayer8() -> None: + for format in ("bayer_rggb8", "bayer_bggr8", "bayer_grbg8", "bayer_gbrg8"): + array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + frame = VideoFrame.from_numpy_buffer(array, format) + assertNdarraysEqual(frame.to_ndarray(), array) + + array[...] = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + assertNdarraysEqual(frame.to_ndarray(), array) + + array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) + array = array[:, :300] + assert not array.data.c_contiguous + frame = VideoFrame.from_numpy_buffer(array, format) + assertNdarraysEqual(frame.to_ndarray(), array) + + array[...] = numpy.random.randint(0, 256, size=array.shape, dtype=numpy.uint8) + assertNdarraysEqual(frame.to_ndarray(), array) + + def test_shares_memory_yuv420p() -> None: array = numpy.random.randint(0, 256, size=(512 * 6 // 4, 256), dtype=numpy.uint8) frame = VideoFrame.from_numpy_buffer(array, "yuv420p") From d00c98d30631f65165140c69e5c7c61732d182c3 Mon Sep 17 00:00:00 2001 From: Max Ehrlich Date: Tue, 29 Apr 2025 20:51:22 +0000 Subject: [PATCH 269/365] Expose the duration field for frames Signed-off-by: Max Ehrlich --- av/frame.pyi | 1 + av/frame.pyx | 21 +++++++++++++++++++++ av/video/frame.pyi | 1 + include/libavcodec/avcodec.pxd | 2 ++ tests/test_videoframe.py | 15 +++++++++++++++ 5 files changed, 40 insertions(+) diff --git a/av/frame.pyi b/av/frame.pyi index 9af81dcfe..38a273afc 100644 --- a/av/frame.pyi +++ b/av/frame.pyi @@ -9,6 +9,7 @@ class SideData(TypedDict, total=False): class Frame: dts: int | None pts: int | None + duration: int | None time_base: Fraction side_data: SideData opaque: object diff --git a/av/frame.pyx b/av/frame.pyx index 57681bbcd..fefdd2dee 100644 --- a/av/frame.pyx +++ b/av/frame.pyx @@ -53,6 +53,9 @@ cdef class Frame: if self.ptr.pts != lib.AV_NOPTS_VALUE: self.ptr.pts = lib.av_rescale_q(self.ptr.pts, self._time_base, dst) + if self.ptr.duration != 0: + self.ptr.duration = lib.av_rescale_q(self.ptr.duration, self._time_base, dst) + self._time_base = dst @property @@ -95,6 +98,24 @@ cdef class Frame: else: self.ptr.pts = value + @property + def duration(self): + """ + The duration of the frame in :attr:`time_base` units + + :type: int + """ + if self.ptr.duration == 0: + return None + return self.ptr.duration + + @duration.setter + def duration(self, value): + if value is None: + self.ptr.duration = 0 + else: + self.ptr.duration = value + @property def time(self): """ diff --git a/av/video/frame.pyi b/av/video/frame.pyi index bba60cc5d..313e184f9 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -30,6 +30,7 @@ class PictureType(IntEnum): class VideoFrame(Frame): format: VideoFormat pts: int + duration: int planes: tuple[VideoPlane, ...] pict_type: int colorspace: int diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 5bed3583d..0c8713cf8 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -461,6 +461,8 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVColorTransferCharacteristic color_trc AVColorSpace colorspace + int64_t duration + cdef AVFrame* avcodec_alloc_frame() cdef struct AVPacket: diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 26549b31b..677bc1fc1 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -49,6 +49,21 @@ def test_opaque() -> None: assert type(frame.opaque) is tuple and len(frame.opaque) == 2 +def test_frame_duration_matches_packet() -> None: + with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: + packet_durations = [ + (p.pts, p.duration) for p in container.demux() if p.pts is not None + ] + packet_durations.sort(key=lambda x: x[0]) + + with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: + frame_durations = [(f.pts, f.duration) for f in container.decode(video=0)] + frame_durations.sort(key=lambda x: x[0]) + + assert len(packet_durations) == len(frame_durations) + assert all(pd[1] == fd[1] for pd, fd in zip(packet_durations, frame_durations)) + + def test_invalid_pixel_format() -> None: with pytest.raises(ValueError, match="not a pixel format: '__unknown_pix_fmt'"): VideoFrame(640, 480, "__unknown_pix_fmt") From f1070b88f359ceff4db8381f930a70eef3baeffe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Fri, 16 May 2025 15:34:30 +0200 Subject: [PATCH 270/365] Update FFmpeg build to use libvpx 1.15.1 and libxml2 2.14.3 libvpx 1.14.0 is vulnerable to CVE-2024-5197. libxml2 2.9.13 is vulnerable to CVE-2022-2309, CVE-2023-29469, CVE-2017-5130, CVE-2023-45322, CVE-2024-25062, CVE-2022-40303, CVE-2022-40304, CVE-2023-28484, CVE-2022-29824 Fixes: #1892 Fixes: #1894 --- scripts/ffmpeg-7.1.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ffmpeg-7.1.json b/scripts/ffmpeg-7.1.json index 42601a31d..26641cedc 100644 --- a/scripts/ffmpeg-7.1.json +++ b/scripts/ffmpeg-7.1.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.1.1-2/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.1.1-4/ffmpeg-{platform}.tar.gz" } From e6a399a8bcb13595a68589307349c19d528c502e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Fri, 16 May 2025 19:37:17 +0200 Subject: [PATCH 271/365] Bump version to 14.4.0 --- CHANGELOG.rst | 11 +++++++++++ av/about.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 71ddc1214..a340bb088 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,17 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. +v14.4.0 +------- + +Features: + +- Expose the duration field for frames by :gh-user:`Queuecumber` in (:pr:`1880`). +- Add support for BGR48 and BGRA64 pixel formats by :gh-user:`z-khan` in (:pr:`1873`). + +Fixes: + +- Fix security vulnerabilities in binary wheels by :gh-user:`jlaine` in (:pr:`1896`). v14.3.0 ------- diff --git a/av/about.py b/av/about.py index f79d47d48..15fd90e44 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "14.3.0" +__version__ = "14.4.0" From efe9ce263019a843443a735c55313bd740d45874 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 20 May 2025 23:42:54 -0400 Subject: [PATCH 272/365] Make container/output pure This also syncs the QoL changes I've made in basswood-av. "Pure" stands for Cython's "Pure Python" mode, a way to write Cython in the Python grammar. --- av/container/output.pxd | 1 - av/container/{output.pyx => output.py} | 236 +++++++++++++------------ 2 files changed, 127 insertions(+), 110 deletions(-) rename av/container/{output.pyx => output.py} (58%) diff --git a/av/container/output.pxd b/av/container/output.pxd index a4299891c..51d3f308e 100644 --- a/av/container/output.pxd +++ b/av/container/output.pxd @@ -5,7 +5,6 @@ from av.stream cimport Stream cdef class OutputContainer(Container): - cdef bint _started cdef bint _done cdef lib.AVPacket *packet_ptr diff --git a/av/container/output.pyx b/av/container/output.py similarity index 58% rename from av/container/output.pyx rename to av/container/output.py index 2a12c40f2..64a4f4a53 100644 --- a/av/container/output.pyx +++ b/av/container/output.py @@ -1,24 +1,22 @@ -import logging import os from fractions import Fraction -cimport libav as lib - -from av.codec.codec cimport Codec -from av.codec.context cimport CodecContext, wrap_codec_context -from av.container.streams cimport StreamContainer -from av.dictionary cimport _Dictionary -from av.error cimport err_check -from av.packet cimport Packet -from av.stream cimport Stream, wrap_stream -from av.utils cimport dict_to_avdict, to_avrational +import cython +from cython.cimports import libav as lib +from cython.cimports.av.codec.codec import Codec +from cython.cimports.av.codec.context import CodecContext, wrap_codec_context +from cython.cimports.av.container.streams import StreamContainer +from cython.cimports.av.dictionary import _Dictionary +from cython.cimports.av.error import err_check +from cython.cimports.av.packet import Packet +from cython.cimports.av.stream import Stream, wrap_stream +from cython.cimports.av.utils import dict_to_avdict, to_avrational from av.dictionary import Dictionary -log = logging.getLogger(__name__) - -cdef close_output(OutputContainer self): +@cython.cfunc +def close_output(self: OutputContainer): self.streams = StreamContainer() if self._started and not self._done: # We must only ever call av_write_trailer *once*, otherwise we get a @@ -28,23 +26,24 @@ self.err_check(lib.av_write_trailer(self.ptr)) finally: if self.file is None and not (self.ptr.oformat.flags & lib.AVFMT_NOFILE): - lib.avio_closep(&self.ptr.pb) + lib.avio_closep(cython.address(self.ptr.pb)) self._done = True -cdef class OutputContainer(Container): +@cython.cclass +class OutputContainer(Container): def __cinit__(self, *args, **kwargs): self.streams = StreamContainer() self.metadata = {} - with nogil: + with cython.nogil: self.packet_ptr = lib.av_packet_alloc() def __dealloc__(self): close_output(self) - with nogil: - lib.av_packet_free(&self.packet_ptr) + with cython.nogil: + lib.av_packet_free(cython.address(self.packet_ptr)) - def add_stream(self, codec_name, rate=None, dict options=None, **kwargs): + def add_stream(self, codec_name, rate=None, options: dict | None = None, **kwargs): """add_stream(codec_name, rate=None) Creates a new stream from a codec name and returns it. @@ -58,67 +57,69 @@ def add_stream(self, codec_name, rate=None, dict options=None, **kwargs): """ - cdef Codec codec_obj = Codec(codec_name, "w") - cdef const lib.AVCodec *codec = codec_obj.ptr + codec_obj: Codec = Codec(codec_name, "w") + codec: cython.pointer[cython.const[lib.AVCodec]] = codec_obj.ptr # Assert that this format supports the requested codec. - if not lib.avformat_query_codec(self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL): + if not lib.avformat_query_codec( + self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL + ): raise ValueError( f"{self.format.name!r} format does not support {codec_obj.name!r} codec" ) # Create new stream in the AVFormatContext, set AVCodecContext values. - cdef lib.AVStream *stream = lib.avformat_new_stream(self.ptr, codec) - cdef lib.AVCodecContext *codec_context = lib.avcodec_alloc_context3(codec) + stream: cython.pointer[lib.AVStream] = lib.avformat_new_stream(self.ptr, codec) + ctx: cython.pointer[lib.AVCodecContext] = lib.avcodec_alloc_context3(codec) # Now lets set some more sane video defaults if codec.type == lib.AVMEDIA_TYPE_VIDEO: - codec_context.pix_fmt = lib.AV_PIX_FMT_YUV420P - codec_context.width = kwargs.pop("width", 640) - codec_context.height = kwargs.pop("height", 480) - codec_context.bit_rate = kwargs.pop("bit_rate", 0) - codec_context.bit_rate_tolerance = kwargs.pop("bit_rate_tolerance", 128000) + ctx.pix_fmt = lib.AV_PIX_FMT_YUV420P + ctx.width = kwargs.pop("width", 640) + ctx.height = kwargs.pop("height", 480) + ctx.bit_rate = kwargs.pop("bit_rate", 0) + ctx.bit_rate_tolerance = kwargs.pop("bit_rate_tolerance", 128000) try: - to_avrational(kwargs.pop("time_base"), &codec_context.time_base) + to_avrational(kwargs.pop("time_base"), cython.address(ctx.time_base)) except KeyError: pass - to_avrational(rate or 24, &codec_context.framerate) + to_avrational(rate or 24, cython.address(ctx.framerate)) - stream.avg_frame_rate = codec_context.framerate - stream.time_base = codec_context.time_base + stream.avg_frame_rate = ctx.framerate + stream.time_base = ctx.time_base # Some sane audio defaults elif codec.type == lib.AVMEDIA_TYPE_AUDIO: - codec_context.sample_fmt = codec.sample_fmts[0] - codec_context.bit_rate = kwargs.pop("bit_rate", 0) - codec_context.bit_rate_tolerance = kwargs.pop("bit_rate_tolerance", 32000) + ctx.sample_fmt = codec.sample_fmts[0] + ctx.bit_rate = kwargs.pop("bit_rate", 0) + ctx.bit_rate_tolerance = kwargs.pop("bit_rate_tolerance", 32000) try: - to_avrational(kwargs.pop("time_base"), &codec_context.time_base) + to_avrational(kwargs.pop("time_base"), cython.address(ctx.time_base)) except KeyError: pass if rate is None: - codec_context.sample_rate = 48000 + ctx.sample_rate = 48000 elif type(rate) is int: - codec_context.sample_rate = rate + ctx.sample_rate = rate else: raise TypeError("audio stream `rate` must be: int | None") - stream.time_base = codec_context.time_base - lib.av_channel_layout_default(&codec_context.ch_layout, 2) + stream.time_base = ctx.time_base + lib.av_channel_layout_default(cython.address(ctx.ch_layout), 2) # Some formats want stream headers to be separate if self.ptr.oformat.flags & lib.AVFMT_GLOBALHEADER: - codec_context.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER + ctx.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER # Initialise stream codec parameters to populate the codec type. # # Subsequent changes to the codec context will be applied just before # encoding starts in `start_encoding()`. - err_check(lib.avcodec_parameters_from_context(stream.codecpar, codec_context)) + err_check(lib.avcodec_parameters_from_context(stream.codecpar, ctx)) # Construct the user-land stream - cdef CodecContext py_codec_context = wrap_codec_context(codec_context, codec, None) - cdef Stream py_stream = wrap_stream(self, stream, py_codec_context) + py_codec_context: CodecContext = wrap_codec_context(ctx, codec, None) + py_stream: Stream = wrap_stream(self, stream, py_codec_context) self.streams.add_stream(py_stream) if options: @@ -129,7 +130,7 @@ def add_stream(self, codec_name, rate=None, dict options=None, **kwargs): return py_stream - def add_stream_from_template(self, Stream template not None, opaque=None, **kwargs): + def add_stream_from_template(self, template: Stream, opaque=None, **kwargs): """ Creates a new stream from a template. Supports video, audio, and subtitle streams. @@ -138,43 +139,44 @@ def add_stream_from_template(self, Stream template not None, opaque=None, **kwar :param \\**kwargs: Set attributes for the stream. :rtype: The new :class:`~av.stream.Stream`. """ - cdef const lib.AVCodec *codec - cdef Codec codec_obj - if opaque is None: opaque = template.type != "video" + codec_obj: Codec if opaque: # Copy ctx from template. codec_obj = template.codec_context.codec - else: # Construct new codec object. + else: # Construct new codec object. codec_obj = Codec(template.codec_context.codec.name, "w") - codec = codec_obj.ptr + + codec: cython.pointer[cython.const[lib.AVCodec]] = codec_obj.ptr # Assert that this format supports the requested codec. - if not lib.avformat_query_codec(self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL): + if not lib.avformat_query_codec( + self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL + ): raise ValueError( f"{self.format.name!r} format does not support {codec_obj.name!r} codec" ) # Create new stream in the AVFormatContext, set AVCodecContext values. - cdef lib.AVStream *stream = lib.avformat_new_stream(self.ptr, codec) - cdef lib.AVCodecContext *codec_context = lib.avcodec_alloc_context3(codec) + stream: cython.pointer[lib.AVStream] = lib.avformat_new_stream(self.ptr, codec) + ctx: cython.pointer[lib.AVCodecContext] = lib.avcodec_alloc_context3(codec) - err_check(lib.avcodec_parameters_to_context(codec_context, template.ptr.codecpar)) + err_check(lib.avcodec_parameters_to_context(ctx, template.ptr.codecpar)) # Reset the codec tag assuming we are remuxing. - codec_context.codec_tag = 0 + ctx.codec_tag = 0 # Some formats want stream headers to be separate if self.ptr.oformat.flags & lib.AVFMT_GLOBALHEADER: - codec_context.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER + ctx.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER # Initialize stream codec parameters to populate the codec type. Subsequent changes to # the codec context will be applied just before encoding starts in `start_encoding()`. - err_check(lib.avcodec_parameters_from_context(stream.codecpar, codec_context)) + err_check(lib.avcodec_parameters_from_context(stream.codecpar, ctx)) # Construct the user-land stream - cdef CodecContext py_codec_context = wrap_codec_context(codec_context, codec, None) - cdef Stream py_stream = wrap_stream(self, stream, py_codec_context) + py_codec_context: CodecContext = wrap_codec_context(ctx, codec, None) + py_stream: Stream = wrap_stream(self, stream, py_codec_context) self.streams.add_stream(py_stream) for k, v in kwargs.items(): @@ -182,8 +184,7 @@ def add_stream_from_template(self, Stream template not None, opaque=None, **kwar return py_stream - - def add_data_stream(self, codec_name=None, dict options=None): + def add_data_stream(self, codec_name=None, options: dict | None = None): """add_data_stream(codec_name=None) Creates a new data stream and returns it. @@ -193,47 +194,49 @@ def add_data_stream(self, codec_name=None, dict options=None): :param dict options: Stream options. :rtype: The new :class:`~av.data.stream.DataStream`. """ - cdef const lib.AVCodec *codec = NULL + codec: cython.pointer[cython.const[lib.AVCodec]] = cython.NULL if codec_name is not None: codec = lib.avcodec_find_encoder_by_name(codec_name.encode()) - if codec == NULL: + if codec == cython.NULL: raise ValueError(f"Unknown data codec: {codec_name}") # Assert that this format supports the requested codec - if not lib.avformat_query_codec(self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL): + if not lib.avformat_query_codec( + self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL + ): raise ValueError( f"{self.format.name!r} format does not support {codec_name!r} codec" ) # Create new stream in the AVFormatContext - cdef lib.AVStream *stream = lib.avformat_new_stream(self.ptr, codec) - if stream == NULL: + stream: cython.pointer[lib.AVStream] = lib.avformat_new_stream(self.ptr, codec) + if stream == cython.NULL: raise MemoryError("Could not allocate stream") # Set up codec context if we have a codec - cdef lib.AVCodecContext *codec_context = NULL - if codec != NULL: - codec_context = lib.avcodec_alloc_context3(codec) - if codec_context == NULL: + ctx: cython.pointer[lib.AVCodecContext] = cython.NULL + if codec != cython.NULL: + ctx = lib.avcodec_alloc_context3(codec) + if ctx == cython.NULL: raise MemoryError("Could not allocate codec context") # Some formats want stream headers to be separate if self.ptr.oformat.flags & lib.AVFMT_GLOBALHEADER: - codec_context.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER + ctx.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER # Initialize stream codec parameters - err_check(lib.avcodec_parameters_from_context(stream.codecpar, codec_context)) + err_check(lib.avcodec_parameters_from_context(stream.codecpar, ctx)) else: # For raw data streams, just set the codec type stream.codecpar.codec_type = lib.AVMEDIA_TYPE_DATA # Construct the user-land stream - cdef CodecContext py_codec_context = None - if codec_context != NULL: - py_codec_context = wrap_codec_context(codec_context, codec, None) + py_codec_context: CodecContext | None = None + if ctx != cython.NULL: + py_codec_context = wrap_codec_context(ctx, codec, None) - cdef Stream py_stream = wrap_stream(self, stream, py_codec_context) + py_stream: Stream = wrap_stream(self, stream, py_codec_context) self.streams.add_stream(py_stream) if options: @@ -241,18 +244,18 @@ def add_data_stream(self, codec_name=None, dict options=None): return py_stream - cpdef start_encoding(self): + @cython.ccall + def start_encoding(self): """Write the file header! Called automatically.""" - if self._started: return # TODO: This does NOT handle options coming from 3 sources. # This is only a rough approximation of what would be cool to do. - used_options = set() + used_options: set = set() + stream: Stream # Finalize and open all streams. - cdef Stream stream for stream in self.streams: ctx = stream.codec_context # Skip codec context handling for data streams without codecs @@ -274,29 +277,38 @@ def add_data_stream(self, codec_name=None, dict options=None): stream._finalize_for_output() # Open the output file, if needed. - cdef bytes name_obj = os.fsencode(self.name if self.file is None else "") - cdef char *name = name_obj - if self.ptr.pb == NULL and not self.ptr.oformat.flags & lib.AVFMT_NOFILE: - err_check(lib.avio_open(&self.ptr.pb, name, lib.AVIO_FLAG_WRITE)) + name_obj: bytes = os.fsencode(self.name if self.file is None else "") + name: cython.p_char = name_obj + if self.ptr.pb == cython.NULL and not self.ptr.oformat.flags & lib.AVFMT_NOFILE: + err_check( + lib.avio_open(cython.address(self.ptr.pb), name, lib.AVIO_FLAG_WRITE) + ) # Copy the metadata dict. dict_to_avdict( - &self.ptr.metadata, self.metadata, + cython.address(self.ptr.metadata), + self.metadata, encoding=self.metadata_encoding, - errors=self.metadata_errors + errors=self.metadata_errors, ) - cdef _Dictionary all_options = Dictionary(self.options, self.container_options) - cdef _Dictionary options = all_options.copy() - self.err_check(lib.avformat_write_header(self.ptr, &options.ptr)) + all_options: _Dictionary = Dictionary(self.options, self.container_options) + options: _Dictionary = all_options.copy() + self.err_check(lib.avformat_write_header(self.ptr, cython.address(options.ptr))) # Track option usage... for k in all_options: if k not in options: used_options.add(k) + # ... and warn if any weren't used. - unused_options = {k: v for k, v in self.options.items() if k not in used_options} + unused_options = { + k: v for k, v in self.options.items() if k not in used_options + } if unused_options: + import logging + + log = logging.getLogger(__name__) log.warning("Some options were not used: %s" % unused_options) self._started = True @@ -306,21 +318,25 @@ def supported_codecs(self): """ Returns a set of all codecs this format supports. """ - result = set() - cdef const lib.AVCodec *codec = NULL - cdef void *opaque = NULL + result: set = set() + codec: cython.pointer[cython.const[lib.AVCodec]] = cython.NULL + opaque: cython.p_void = cython.NULL while True: - codec = lib.av_codec_iterate(&opaque) - if codec == NULL: + codec = lib.av_codec_iterate(cython.address(opaque)) + if codec == cython.NULL: break - if lib.avformat_query_codec(self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL) == 1: + if ( + lib.avformat_query_codec( + self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL + ) + == 1 + ): result.add(codec.name) return result - @property def default_video_codec(self): """ @@ -346,29 +362,31 @@ def close(self): close_output(self) def mux(self, packets): - # We accept either a Packet, or a sequence of packets. This should - # smooth out the transition to the new encode API which returns a - # sequence of packets. + # We accept either a Packet, or a sequence of packets. This should smooth out + # the transition to the new encode API which returns a sequence of packets. if isinstance(packets, Packet): self.mux_one(packets) else: for packet in packets: self.mux_one(packet) - def mux_one(self, Packet packet not None): + def mux_one(self, packet: Packet): self.start_encoding() # Assert the packet is in stream time. - if packet.ptr.stream_index < 0 or packet.ptr.stream_index >= self.ptr.nb_streams: + if ( + packet.ptr.stream_index < 0 + or cython.cast(cython.uint, packet.ptr.stream_index) >= self.ptr.nb_streams + ): raise ValueError("Bad Packet stream_index.") - cdef lib.AVStream *stream = self.ptr.streams[packet.ptr.stream_index] + + stream: cython.pointer[lib.AVStream] = self.ptr.streams[packet.ptr.stream_index] packet._rebase_time(stream.time_base) - # Make another reference to the packet, as av_interleaved_write_frame + # Make another reference to the packet, as `av_interleaved_write_frame()` # takes ownership of the reference. self.err_check(lib.av_packet_ref(self.packet_ptr, packet.ptr)) - cdef int ret - with nogil: - ret = lib.av_interleaved_write_frame(self.ptr, self.packet_ptr) + with cython.nogil: + ret: cython.int = lib.av_interleaved_write_frame(self.ptr, self.packet_ptr) self.err_check(ret) From 633f237a6916d17b85fe937f95e28af651fbaf0e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 21 May 2025 00:33:06 -0400 Subject: [PATCH 273/365] Copy template flags if creating a new Codec object This fixes #1903 --- av/container/output.py | 9 ++++++++- tests/test_remux.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 tests/test_remux.py diff --git a/av/container/output.py b/av/container/output.py index 64a4f4a53..d035b0265 100644 --- a/av/container/output.py +++ b/av/container/output.py @@ -130,7 +130,9 @@ def add_stream(self, codec_name, rate=None, options: dict | None = None, **kwarg return py_stream - def add_stream_from_template(self, template: Stream, opaque=None, **kwargs): + def add_stream_from_template( + self, template: Stream, opaque: bool | None = None, **kwargs + ): """ Creates a new stream from a template. Supports video, audio, and subtitle streams. @@ -170,6 +172,11 @@ def add_stream_from_template(self, template: Stream, opaque=None, **kwargs): if self.ptr.oformat.flags & lib.AVFMT_GLOBALHEADER: ctx.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER + # Copy flags If we're creating a new codec object. This fixes some muxing issues. + # Overwriting `ctx.flags |= lib.AV_CODEC_FLAG_GLOBAL_HEADER` is intentional. + if not opaque: + ctx.flags = template.codec_context.flags + # Initialize stream codec parameters to populate the codec type. Subsequent changes to # the codec context will be applied just before encoding starts in `start_encoding()`. err_check(lib.avcodec_parameters_from_context(stream.codecpar, ctx)) diff --git a/tests/test_remux.py b/tests/test_remux.py new file mode 100644 index 000000000..1eca2dfd2 --- /dev/null +++ b/tests/test_remux.py @@ -0,0 +1,33 @@ +import av +import av.datasets + + +def test_video_remux() -> None: + input_path = av.datasets.curated("pexels/time-lapse-video-of-night-sky-857195.mp4") + input_ = av.open(input_path) + output = av.open("remuxed.mkv", "w") + + in_stream = input_.streams.video[0] + out_stream = output.add_stream_from_template(in_stream) + + for packet in input_.demux(in_stream): + if packet.dts is None: + continue + + packet.stream = out_stream + output.mux(packet) + + input_.close() + output.close() + + with av.open("remuxed.mkv") as container: + # Assert output is a valid media file + assert len(container.streams.video) == 1 + assert len(container.streams.audio) == 0 + assert container.streams.video[0].codec.name == "h264" + + packet_count = 0 + for packet in container.demux(video=0): + packet_count += 1 + + assert packet_count > 50 From 7299106973cd0b3a9ea0894f6436dc04762fbd81 Mon Sep 17 00:00:00 2001 From: Jiabei Zhu Date: Fri, 6 Jun 2025 20:24:08 -0400 Subject: [PATCH 274/365] Fix typo for AudioFormat.is_packed --- av/audio/format.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/audio/format.pyx b/av/audio/format.pyx index f86e7b604..c67f2fb9c 100644 --- a/av/audio/format.pyx +++ b/av/audio/format.pyx @@ -82,7 +82,7 @@ cdef class AudioFormat: @property def is_packed(self): - """Is this a planar format? + """Is this a packed format? Strictly opposite of :attr:`is_planar`. From 3739f5986acceaf8c56e35a7fbc35e0a2f684a62 Mon Sep 17 00:00:00 2001 From: zzjjbb <31069326+zzjjbb@users.noreply.github.com> Date: Fri, 6 Jun 2025 21:33:42 -0500 Subject: [PATCH 275/365] Fix memory error for AudioFifo properties Validate self.ptr before access: uninitialized properties should return None rather than crash the Python interpreter --- av/audio/fifo.pyx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/av/audio/fifo.pyx b/av/audio/fifo.pyx index 9b1380270..2ceb55f77 100644 --- a/av/audio/fifo.pyx +++ b/av/audio/fifo.pyx @@ -176,13 +176,19 @@ cdef class AudioFifo: @property def format(self): """The :class:`.AudioFormat` of this FIFO.""" + if not self.ptr: + raise AttributeError(f"'{__name__}.AudioFifo' object has no attribute 'format'") return self.template.format @property def layout(self): """The :class:`.AudioLayout` of this FIFO.""" + if not self.ptr: + raise AttributeError(f"'{__name__}.AudioFifo' object has no attribute 'layout'") return self.template.layout @property def sample_rate(self): + if not self.ptr: + raise AttributeError(f"'{__name__}.AudioFifo' object has no attribute 'sample_rate'") return self.template.sample_rate @property From 10b93f95c5422c7608418b322fa698bc651f50e7 Mon Sep 17 00:00:00 2001 From: Mattia Procopio Date: Tue, 10 Jun 2025 08:51:08 +0200 Subject: [PATCH 276/365] Try another port until one is nnot used --- tests/test_timeout.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/tests/test_timeout.py b/tests/test_timeout.py index 0640237f9..fa1d36d9a 100644 --- a/tests/test_timeout.py +++ b/tests/test_timeout.py @@ -7,7 +7,6 @@ from .common import TestCase, fate_suite -PORT = 8002 CONTENT = open(fate_suite("mpeg2/mpeg2_field_encoding.ts"), "rb").read() # Needs to be long enough for all host OSes to deal. TIMEOUT = 0.25 @@ -34,8 +33,17 @@ def log_message(self, format: object, *args: object) -> None: class TestTimeout(TestCase): + port = 8002 + def setUp(cls) -> None: - cls._server = HttpServer(("", PORT), SlowRequestHandler) + while True: + try: + cls._server = HttpServer(("", cls.port), SlowRequestHandler) + except OSError: + cls.port += 1 + else: + break + cls._thread = threading.Thread(target=cls._server.handle_request) cls._thread.daemon = True # Make sure the tests will exit. cls._thread.start() @@ -46,14 +54,16 @@ def tearDown(cls) -> None: def test_no_timeout(self) -> None: start = time.time() - av.open(f"http://localhost:{PORT}/mpeg2_field_encoding.ts") + av.open(f"http://localhost:{self.port}/mpeg2_field_encoding.ts") duration = time.time() - start assert duration > DELAY def test_open_timeout(self) -> None: with self.assertRaises(av.ExitError): start = time.time() - av.open(f"http://localhost:{PORT}/mpeg2_field_encoding.ts", timeout=TIMEOUT) + av.open( + f"http://localhost:{self.port}/mpeg2_field_encoding.ts", timeout=TIMEOUT + ) duration = time.time() - start assert duration < DELAY @@ -62,7 +72,7 @@ def test_open_timeout_2(self) -> None: with self.assertRaises(av.ExitError): start = time.time() av.open( - f"http://localhost:{PORT}/mpeg2_field_encoding.ts", + f"http://localhost:{self.port}/mpeg2_field_encoding.ts", timeout=(TIMEOUT, None), ) From 506f6ac4d5621e8d9ef39b32acd94ac0e6c3ad32 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 18 Jun 2025 21:01:08 -0400 Subject: [PATCH 277/365] Make SubtitleStream.decode() return list of subs.. Make `SubtitleStream.decode()` return list of subtitles directly, without the intermediate `SubtitleSet`. --- CHANGELOG.rst | 7 + av/subtitles/codeccontext.pxd | 3 +- av/subtitles/codeccontext.py | 55 +++++++ av/subtitles/codeccontext.pyi | 3 + av/subtitles/codeccontext.pyx | 23 --- av/subtitles/stream.py | 23 +++ av/subtitles/stream.pyi | 7 +- av/subtitles/stream.pyx | 23 --- av/subtitles/{subtitle.pyx => subtitle.py} | 163 ++++++++++++--------- tests/test_subtitles.py | 43 +++--- 10 files changed, 215 insertions(+), 135 deletions(-) create mode 100644 av/subtitles/codeccontext.py delete mode 100644 av/subtitles/codeccontext.pyx create mode 100644 av/subtitles/stream.py delete mode 100644 av/subtitles/stream.pyx rename av/subtitles/{subtitle.pyx => subtitle.py} (51%) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a340bb088..3a69a86ab 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,13 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. +v15.0.0 (Unreleased) +-------------------- + +Major: + +- Make ``SubtitleStream.decode()`` return the list of subtitles directly, without the intermediate ``SubtitleSet``. + v14.4.0 ------- diff --git a/av/subtitles/codeccontext.pxd b/av/subtitles/codeccontext.pxd index 42141aa4f..c94744e45 100644 --- a/av/subtitles/codeccontext.pxd +++ b/av/subtitles/codeccontext.pxd @@ -1,5 +1,6 @@ from av.codec.context cimport CodecContext +from av.packet cimport Packet cdef class SubtitleCodecContext(CodecContext): - pass + cpdef decode2(self, Packet packet) diff --git a/av/subtitles/codeccontext.py b/av/subtitles/codeccontext.py new file mode 100644 index 000000000..0b3dda063 --- /dev/null +++ b/av/subtitles/codeccontext.py @@ -0,0 +1,55 @@ +import cython +from cython.cimports import libav as lib +from cython.cimports.av.error import err_check +from cython.cimports.av.packet import Packet +from cython.cimports.av.subtitles.subtitle import SubtitleProxy, SubtitleSet + + +@cython.cclass +class SubtitleCodecContext(CodecContext): + @cython.cfunc + def _send_packet_and_recv(self, packet: Packet | None): + if packet is None: + raise RuntimeError("packet cannot be None") + + proxy: SubtitleProxy = SubtitleProxy() + got_frame: cython.int = 0 + + err_check( + lib.avcodec_decode_subtitle2( + self.ptr, + cython.address(proxy.struct), + cython.address(got_frame), + packet.ptr, + ) + ) + + if got_frame: + return SubtitleSet(proxy) + return [] + + @cython.ccall + def decode2(self, packet: Packet): + """ + Returns SubtitleSet if you really need it. + """ + if not self.codec.ptr: + raise ValueError("cannot decode unknown codec") + + self.open(strict=False) + + proxy: SubtitleProxy = SubtitleProxy() + got_frame: cython.int = 0 + + err_check( + lib.avcodec_decode_subtitle2( + self.ptr, + cython.address(proxy.struct), + cython.address(got_frame), + packet.ptr, + ) + ) + + if got_frame: + return SubtitleSet(proxy) + return None diff --git a/av/subtitles/codeccontext.pyi b/av/subtitles/codeccontext.pyi index 0762c19f0..90c700935 100644 --- a/av/subtitles/codeccontext.pyi +++ b/av/subtitles/codeccontext.pyi @@ -1,6 +1,9 @@ from typing import Literal from av.codec.context import CodecContext +from av.packet import Packet +from av.subtitles.subtitle import SubtitleSet class SubtitleCodecContext(CodecContext): type: Literal["subtitle"] + def decode2(self, packet: Packet) -> SubtitleSet | None: ... diff --git a/av/subtitles/codeccontext.pyx b/av/subtitles/codeccontext.pyx deleted file mode 100644 index c0712c92c..000000000 --- a/av/subtitles/codeccontext.pyx +++ /dev/null @@ -1,23 +0,0 @@ -cimport libav as lib - -from av.error cimport err_check -from av.packet cimport Packet -from av.subtitles.subtitle cimport SubtitleProxy, SubtitleSet - - -cdef class SubtitleCodecContext(CodecContext): - cdef _send_packet_and_recv(self, Packet packet): - if packet is None: - raise RuntimeError("packet cannot be None") - - cdef SubtitleProxy proxy = SubtitleProxy() - cdef int got_frame = 0 - - err_check( - lib.avcodec_decode_subtitle2(self.ptr, &proxy.struct, &got_frame, packet.ptr) - ) - - if got_frame: - return [SubtitleSet(proxy)] - else: - return [] diff --git a/av/subtitles/stream.py b/av/subtitles/stream.py new file mode 100644 index 000000000..525440e9d --- /dev/null +++ b/av/subtitles/stream.py @@ -0,0 +1,23 @@ +import cython +from cython.cimports.av.packet import Packet +from cython.cimports.av.stream import Stream + + +@cython.cclass +class SubtitleStream(Stream): + def __getattr__(self, name): + return getattr(self.codec_context, name) + + @cython.ccall + def decode(self, packet: Packet | None = None): + """ + Decode a :class:`.Packet` and returns a subtitle object. + + :rtype: list[AssSubtitle] | list[BitmapSubtitle] + + .. seealso:: This is a passthrough to :meth:`.CodecContext.decode`. + """ + if not packet: + packet = Packet() + + return self.codec_context.decode(packet) diff --git a/av/subtitles/stream.pyi b/av/subtitles/stream.pyi index cb1ac34a2..ac8083802 100644 --- a/av/subtitles/stream.pyi +++ b/av/subtitles/stream.pyi @@ -1,6 +1,9 @@ from av.packet import Packet from av.stream import Stream -from av.subtitles.subtitle import SubtitleSet +from av.subtitles.subtitle import AssSubtitle, BitmapSubtitle, SubtitleSet class SubtitleStream(Stream): - def decode(self, packet: Packet | None = None) -> list[SubtitleSet]: ... + def decode( + self, packet: Packet | None = None + ) -> list[AssSubtitle] | list[BitmapSubtitle]: ... + def decode2(self, packet: Packet) -> SubtitleSet | None: ... diff --git a/av/subtitles/stream.pyx b/av/subtitles/stream.pyx deleted file mode 100644 index 9f90b9871..000000000 --- a/av/subtitles/stream.pyx +++ /dev/null @@ -1,23 +0,0 @@ -from av.packet cimport Packet -from av.stream cimport Stream - - -cdef class SubtitleStream(Stream): - """ - A :class:`SubtitleStream` can contain many :class:`SubtitleSet` objects accessible via decoding. - """ - def __getattr__(self, name): - return getattr(self.codec_context, name) - - cpdef decode(self, Packet packet=None): - """ - Decode a :class:`.Packet` and return a list of :class:`.SubtitleSet`. - - :rtype: list[SubtitleSet] - - .. seealso:: This is a passthrough to :meth:`.CodecContext.decode`. - """ - if not packet: - packet = Packet() - - return self.codec_context.decode(packet) diff --git a/av/subtitles/subtitle.pyx b/av/subtitles/subtitle.py similarity index 51% rename from av/subtitles/subtitle.pyx rename to av/subtitles/subtitle.py index a713daa22..1acadf0b5 100644 --- a/av/subtitles/subtitle.pyx +++ b/av/subtitles/subtitle.py @@ -1,34 +1,49 @@ -from cpython cimport PyBuffer_FillInfo +import cython +from cython.cimports.cpython import PyBuffer_FillInfo, PyBytes_FromString +from cython.cimports.libc.stdint import uint64_t -cdef extern from "Python.h": - bytes PyBytes_FromString(char*) - - -cdef class SubtitleProxy: +@cython.cclass +class SubtitleProxy: def __dealloc__(self): - lib.avsubtitle_free(&self.struct) + lib.avsubtitle_free(cython.address(self.struct)) -cdef class SubtitleSet: +@cython.cclass +class SubtitleSet: """ A :class:`SubtitleSet` can contain many :class:`Subtitle` objects. + + Wraps :ffmpeg:`AVSubtitle`. """ - def __cinit__(self, SubtitleProxy proxy): + + def __cinit__(self, proxy: SubtitleProxy): self.proxy = proxy - self.rects = tuple(build_subtitle(self, i) for i in range(self.proxy.struct.num_rects)) + self.rects = tuple( + build_subtitle(self, i) for i in range(self.proxy.struct.num_rects) + ) def __repr__(self): - return f"<{self.__class__.__module__}.{self.__class__.__name__} at 0x{id(self):x}>" + return ( + f"<{self.__class__.__module__}.{self.__class__.__name__} at 0x{id(self):x}>" + ) @property - def format(self): return self.proxy.struct.format + def format(self): + return self.proxy.struct.format + @property - def start_display_time(self): return self.proxy.struct.start_display_time + def start_display_time(self): + return self.proxy.struct.start_display_time + @property - def end_display_time(self): return self.proxy.struct.end_display_time + def end_display_time(self): + return self.proxy.struct.end_display_time + @property - def pts(self): return self.proxy.struct.pts + def pts(self): + """Same as packet pts, in av.time_base.""" + return self.proxy.struct.pts def __len__(self): return len(self.rects) @@ -40,33 +55,37 @@ def __getitem__(self, i): return self.rects[i] -cdef Subtitle build_subtitle(SubtitleSet subtitle, int index): +@cython.cfunc +def build_subtitle(subtitle: SubtitleSet, index: cython.int) -> Subtitle: """Build an av.Stream for an existing AVStream. - The AVStream MUST be fully constructed and ready for use before this is - called. - + The AVStream MUST be fully constructed and ready for use before this is called. """ - - if index < 0 or index >= subtitle.proxy.struct.num_rects: + if index < 0 or cython.cast(cython.uint, index) >= subtitle.proxy.struct.num_rects: raise ValueError("subtitle rect index out of range") - cdef lib.AVSubtitleRect *ptr = subtitle.proxy.struct.rects[index] + + ptr: cython.pointer[lib.AVSubtitleRect] = subtitle.proxy.struct.rects[index] if ptr.type == lib.SUBTITLE_BITMAP: return BitmapSubtitle(subtitle, index) - elif ptr.type == lib.SUBTITLE_ASS or ptr.type == lib.SUBTITLE_TEXT: + if ptr.type == lib.SUBTITLE_ASS or ptr.type == lib.SUBTITLE_TEXT: return AssSubtitle(subtitle, index) - else: - raise ValueError("unknown subtitle type %r" % ptr.type) + + raise ValueError("unknown subtitle type %r" % ptr.type) -cdef class Subtitle: +@cython.cclass +class Subtitle: """ An abstract base class for each concrete type of subtitle. Wraps :ffmpeg:`AVSubtitleRect` """ - def __cinit__(self, SubtitleSet subtitle, int index): - if index < 0 or index >= subtitle.proxy.struct.num_rects: + + def __cinit__(self, subtitle: SubtitleSet, index: cython.int): + if ( + index < 0 + or cython.cast(cython.uint, index) >= subtitle.proxy.struct.num_rects + ): raise ValueError("subtitle rect index out of range") self.proxy = subtitle.proxy self.ptr = self.proxy.struct.rects[index] @@ -83,15 +102,14 @@ def __cinit__(self, SubtitleSet subtitle, int index): raise ValueError(f"unknown subtitle type {self.ptr.type!r}") def __repr__(self): - return f"<{self.__class__.__module__}.{self.__class__.__name__} at 0x{id(self):x}>" + return f"" -cdef class BitmapSubtitle(Subtitle): - def __cinit__(self, SubtitleSet subtitle, int index): +@cython.cclass +class BitmapSubtitle(Subtitle): + def __cinit__(self, subtitle: SubtitleSet, index: cython.int): self.planes = tuple( - BitmapSubtitlePlane(self, i) - for i in range(4) - if self.ptr.linesize[i] + BitmapSubtitlePlane(self, i) for i in range(4) if self.ptr.linesize[i] ) def __repr__(self): @@ -101,15 +119,24 @@ def __repr__(self): ) @property - def x(self): return self.ptr.x + def x(self): + return self.ptr.x + @property - def y(self): return self.ptr.y + def y(self): + return self.ptr.y + @property - def width(self): return self.ptr.w + def width(self): + return self.ptr.w + @property - def height(self): return self.ptr.h + def height(self): + return self.ptr.h + @property - def nb_colors(self): return self.ptr.nb_colors + def nb_colors(self): + return self.ptr.nb_colors def __len__(self): return len(self.planes) @@ -121,8 +148,9 @@ def __getitem__(self, i): return self.planes[i] -cdef class BitmapSubtitlePlane: - def __cinit__(self, BitmapSubtitle subtitle, int index): +@cython.cclass +class BitmapSubtitlePlane: + def __cinit__(self, subtitle: BitmapSubtitle, index: cython.int): if index >= 4: raise ValueError("BitmapSubtitles have only 4 planes") if not subtitle.ptr.linesize[index]: @@ -131,29 +159,28 @@ def __cinit__(self, BitmapSubtitle subtitle, int index): self.subtitle = subtitle self.index = index self.buffer_size = subtitle.ptr.w * subtitle.ptr.h - self._buffer = subtitle.ptr.data[index] + self._buffer = cython.cast(cython.p_void, subtitle.ptr.data[index]) # New-style buffer support. - def __getbuffer__(self, Py_buffer *view, int flags): + def __getbuffer__(self, view: cython.pointer[Py_buffer], flags: cython.int): PyBuffer_FillInfo(view, self, self._buffer, self.buffer_size, 0, flags) -cdef class AssSubtitle(Subtitle): +@cython.cclass +class AssSubtitle(Subtitle): """ Represents an ASS/Text subtitle format, as opposed to a bitmap Subtitle format. """ + def __repr__(self): - return ( - f"<{self.__class__.__module__}.{self.__class__.__name__} " - f"{self.text!r} at 0x{id(self):x}>" - ) + return f"" @property def ass(self): """ Returns the subtitle in the ASS/SSA format. Used by the vast majority of subtitle formats. """ - if self.ptr.ass is not NULL: + if self.ptr.ass is not cython.NULL: return PyBytes_FromString(self.ptr.ass) return b"" @@ -162,42 +189,44 @@ def dialogue(self): """ Extract the dialogue from the ass format. Strip comments. """ - comma_count = 0 - i = 0 - cdef bytes ass_text = self.ass - cdef bytes result = b"" - - while comma_count < 8 and i < len(ass_text): - if bytes([ass_text[i]]) == b",": + comma_count: cython.short = 0 + i: uint64_t = 0 + state: cython.bint = False + ass_text: bytes = self.ass + char, next_char = cython.declare(cython.char) + result: bytearray = bytearray() + text_len: cython.Py_ssize_t = len(ass_text) + + while comma_count < 8 and i < text_len: + if ass_text[i] == b","[0]: comma_count += 1 i += 1 - state = False - while i < len(ass_text): - char = bytes([ass_text[i]]) - next_char = b"" if i + 1 >= len(ass_text) else bytes([ass_text[i + 1]]) + while i < text_len: + char = ass_text[i] + next_char = 0 if i + 1 >= text_len else ass_text[i + 1] - if char == b"\\" and next_char == b"N": - result += b"\n" + if char == b"\\"[0] and next_char == b"N"[0]: + result.append(b"\n"[0]) i += 2 continue if not state: - if char == b"{" and next_char != b"\\": + if char == b"{"[0] and next_char != b"\\"[0]: state = True else: - result += char - elif char == b"}": + result.append(char) + elif char == b"}"[0]: state = False i += 1 - return result + return bytes(result) @property def text(self): """ Rarely used attribute. You're probably looking for dialogue. """ - if self.ptr.text is not NULL: + if self.ptr.text is not cython.NULL: return PyBytes_FromString(self.ptr.text) return b"" diff --git a/tests/test_subtitles.py b/tests/test_subtitles.py index e2b5ab512..66e6f9cfa 100644 --- a/tests/test_subtitles.py +++ b/tests/test_subtitles.py @@ -1,3 +1,5 @@ +from typing import cast + import av from av.subtitles.subtitle import AssSubtitle, BitmapSubtitle @@ -8,43 +10,46 @@ class TestSubtitle: def test_movtext(self) -> None: path = fate_suite("sub/MovText_capability_tester.mp4") - subs = [] + subs: list[AssSubtitle] = [] with av.open(path) as container: for packet in container.demux(): - subs.extend(packet.decode()) + subs.extend(cast(list[AssSubtitle], packet.decode())) assert len(subs) == 3 - subset = subs[0] - assert subset.format == 1 - assert subset.pts == 970000 - assert subset.start_display_time == 0 - assert subset.end_display_time == 1570 - - sub = subset[0] + sub = subs[0] assert isinstance(sub, AssSubtitle) assert sub.type == b"ass" assert sub.text == b"" assert sub.ass == b"0,0,Default,,0,0,0,,- Test 1.\\N- Test 2." assert sub.dialogue == b"- Test 1.\n- Test 2." + def test_subset(self) -> None: + path = fate_suite("sub/MovText_capability_tester.mp4") + + with av.open(path) as container: + subs = container.streams.subtitles[0] + for packet in container.demux(subs): + subset = subs.decode2(packet) + if subset is not None: + assert not isinstance(subset, av.subtitles.subtitle.Subtitle) + assert isinstance(subset, av.subtitles.subtitle.SubtitleSet) + assert subset.format == 1 + assert hasattr(subset, "pts") + assert subset.start_display_time == 0 + assert hasattr(subset, "end_display_time") + def test_vobsub(self) -> None: path = fate_suite("sub/vobsub.sub") - subs = [] + subs: list[BitmapSubtitle] = [] with av.open(path) as container: for packet in container.demux(): - subs.extend(packet.decode()) + subs.extend(cast(list[BitmapSubtitle], packet.decode())) assert len(subs) == 43 - subset = subs[0] - assert subset.format == 0 - assert subset.pts == 132499044 - assert subset.start_display_time == 0 - assert subset.end_display_time == 4960 - - sub = subset[0] + sub = subs[0] assert isinstance(sub, BitmapSubtitle) assert sub.type == b"bitmap" assert sub.x == 259 @@ -60,7 +65,7 @@ def test_vobsub(self) -> None: def test_subtitle_flush(self) -> None: path = fate_suite("sub/MovText_capability_tester.mp4") - subs = [] + subs: list[object] = [] with av.open(path) as container: stream = container.streams.subtitles[0] for packet in container.demux(stream): From 9dd293536d2da0f6380b7f44e4afe91408ab3d02 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 23 Jun 2025 19:55:34 -0400 Subject: [PATCH 278/365] Make audio/format pure --- av/audio/format.pxd | 4 ---- av/audio/{format.pyx => format.py} | 25 ++++++++++++------------- 2 files changed, 12 insertions(+), 17 deletions(-) rename av/audio/{format.pyx => format.py} (84%) diff --git a/av/audio/format.pxd b/av/audio/format.pxd index 4160aa85b..c4d4bc552 100644 --- a/av/audio/format.pxd +++ b/av/audio/format.pxd @@ -2,10 +2,6 @@ cimport libav as lib cdef class AudioFormat: - cdef lib.AVSampleFormat sample_fmt - cdef _init(self, lib.AVSampleFormat sample_fmt) - - cdef AudioFormat get_audio_format(lib.AVSampleFormat format) diff --git a/av/audio/format.pyx b/av/audio/format.py similarity index 84% rename from av/audio/format.pyx rename to av/audio/format.py index c67f2fb9c..7d5de1ad9 100644 --- a/av/audio/format.pyx +++ b/av/audio/format.py @@ -1,41 +1,40 @@ import sys +import cython -cdef str container_format_postfix = "le" if sys.byteorder == "little" else "be" +container_format_postfix: str = "le" if sys.byteorder == "little" else "be" +_cinit_bypass_sentinel = object() -cdef object _cinit_bypass_sentinel - -cdef AudioFormat get_audio_format(lib.AVSampleFormat c_format): +@cython.cfunc +def get_audio_format(c_format: lib.AVSampleFormat) -> AudioFormat: """Get an AudioFormat without going through a string.""" if c_format < 0: return None - cdef AudioFormat format = AudioFormat.__new__(AudioFormat, _cinit_bypass_sentinel) - format._init(c_format) + format: AudioFormat = AudioFormat(_cinit_bypass_sentinel) + format.sample_fmt = c_format return format -cdef class AudioFormat: +@cython.cclass +class AudioFormat: """Descriptor of audio formats.""" def __cinit__(self, name): if name is _cinit_bypass_sentinel: return - cdef lib.AVSampleFormat sample_fmt + sample_fmt: lib.AVSampleFormat if isinstance(name, AudioFormat): - sample_fmt = (name).sample_fmt + sample_fmt = cython.cast(AudioFormat, name).sample_fmt else: sample_fmt = lib.av_get_sample_fmt(name) if sample_fmt < 0: raise ValueError(f"Not a sample format: {name!r}") - self._init(sample_fmt) - - cdef _init(self, lib.AVSampleFormat sample_fmt): self.sample_fmt = sample_fmt def __repr__(self): @@ -49,7 +48,7 @@ def name(self): 's16p' """ - return lib.av_get_sample_fmt_name(self.sample_fmt) + return lib.av_get_sample_fmt_name(self.sample_fmt) @property def bytes(self): From 7b2ee3235c382a7e7855f9b00bb32530a57eb850 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 23 Jun 2025 20:11:36 -0400 Subject: [PATCH 279/365] Make audio/frame pure --- av/audio/frame.pyi | 8 +-- av/audio/frame.pyx | 120 ++++++++++++++++++++++++++------------------- 2 files changed, 74 insertions(+), 54 deletions(-) diff --git a/av/audio/frame.pyi b/av/audio/frame.pyi index 7f61e4e6d..6aeb86b4d 100644 --- a/av/audio/frame.pyi +++ b/av/audio/frame.pyi @@ -35,13 +35,15 @@ class AudioFrame(Frame): def __init__( self, - format: str = "s16", - layout: str = "stereo", + format: AudioFormat | str = "s16", + layout: AudioLayout | str = "stereo", samples: int = 0, align: int = 1, ) -> None: ... @staticmethod def from_ndarray( - array: _SupportedNDarray, format: str = "s16", layout: str = "stereo" + array: _SupportedNDarray, + format: AudioFormat | str = "s16", + layout: AudioLayout | str = "stereo", ) -> AudioFrame: ... def to_ndarray(self) -> _SupportedNDarray: ... diff --git a/av/audio/frame.pyx b/av/audio/frame.pyx index 14356cb4e..cf3ab5934 100644 --- a/av/audio/frame.pyx +++ b/av/audio/frame.pyx @@ -1,11 +1,16 @@ -from av.audio.format cimport get_audio_format -from av.audio.layout cimport get_audio_layout -from av.audio.plane cimport AudioPlane -from av.error cimport err_check -from av.utils cimport check_ndarray +import cython +from cython.cimports.av.audio.format import get_audio_format +from cython.cimports.av.audio.layout import get_audio_layout +from cython.cimports.av.audio.plane import AudioPlane +from cython.cimports.av.error import err_check +from cython.cimports.av.utils import check_ndarray +_cinit_bypass_sentinel = object() -cdef object _cinit_bypass_sentinel + +@cython.cfunc +def alloc_audio_frame() -> AudioFrame: + return AudioFrame(_cinit_bypass_sentinel) format_dtypes = { @@ -22,30 +27,28 @@ format_dtypes = { } -cdef AudioFrame alloc_audio_frame(): - """Get a mostly uninitialized AudioFrame. - - You MUST call AudioFrame._init(...) or AudioFrame._init_user_attributes() - before exposing to the user. - - """ - return AudioFrame.__new__(AudioFrame, _cinit_bypass_sentinel) - - -cdef class AudioFrame(Frame): +@cython.cclass +class AudioFrame(Frame): """A frame of audio.""" def __cinit__(self, format="s16", layout="stereo", samples=0, align=1): if format is _cinit_bypass_sentinel: return - cdef AudioFormat cy_format = AudioFormat(format) - cdef AudioLayout cy_layout = AudioLayout(layout) + cy_format: AudioFormat = AudioFormat(format) + cy_layout: AudioLayout = AudioLayout(layout) self._init(cy_format.sample_fmt, cy_layout.layout, samples, align) - cdef _init(self, lib.AVSampleFormat format, lib.AVChannelLayout layout, unsigned int nb_samples, unsigned int align): + @cython.cfunc + def _init( + self, + format: lib.AVSampleFormat, + layout: lib.AVChannelLayout, + nb_samples: cython.uint, + align: cython.uint, + ): self.ptr.nb_samples = nb_samples - self.ptr.format = format + self.ptr.format = format self.ptr.ch_layout = layout # Sometimes this is called twice. Oh well. @@ -53,41 +56,44 @@ cdef class AudioFrame(Frame): if self.layout.nb_channels != 0 and nb_samples: # Cleanup the old buffer. - lib.av_freep(&self._buffer) + lib.av_freep(cython.address(self._buffer)) # Get a new one. - self._buffer_size = err_check(lib.av_samples_get_buffer_size( - NULL, - self.layout.nb_channels, - nb_samples, - format, - align - )) - self._buffer = lib.av_malloc(self._buffer_size) + self._buffer_size = err_check( + lib.av_samples_get_buffer_size( + cython.NULL, self.layout.nb_channels, nb_samples, format, align + ) + ) + self._buffer = cython.cast( + cython.pointer[uint8_t], lib.av_malloc(self._buffer_size) + ) if not self._buffer: raise MemoryError("cannot allocate AudioFrame buffer") # Connect the data pointers to the buffer. - err_check(lib.avcodec_fill_audio_frame( - self.ptr, - self.layout.nb_channels, - self.ptr.format, - self._buffer, - self._buffer_size, - align - )) + err_check( + lib.avcodec_fill_audio_frame( + self.ptr, + self.layout.nb_channels, + cython.cast(lib.AVSampleFormat, self.ptr.format), + self._buffer, + self._buffer_size, + align, + ) + ) def __dealloc__(self): - lib.av_freep(&self._buffer) + lib.av_freep(cython.address(self._buffer)) - cdef _init_user_attributes(self): + @cython.cfunc + def _init_user_attributes(self): self.layout = get_audio_layout(self.ptr.ch_layout) - self.format = get_audio_format(self.ptr.format) + self.format = get_audio_format(cython.cast(lib.AVSampleFormat, self.ptr.format)) def __repr__(self): return ( - f"" ) @staticmethod @@ -97,6 +103,10 @@ cdef class AudioFrame(Frame): """ import numpy as np + py_format = format if isinstance(format, AudioFormat) else AudioFormat(format) + py_layout = layout if isinstance(layout, AudioLayout) else AudioLayout(layout) + format = py_format.name + # map avcodec type to numpy type try: dtype = np.dtype(format_dtypes[format]) @@ -106,18 +116,22 @@ cdef class AudioFrame(Frame): ) # check input format - nb_channels = AudioLayout(layout).nb_channels + nb_channels = py_layout.nb_channels check_ndarray(array, dtype, 2) - if AudioFormat(format).is_planar: + if py_format.is_planar: if array.shape[0] != nb_channels: - raise ValueError(f"Expected planar `array.shape[0]` to equal `{nb_channels}` but got `{array.shape[0]}`") + raise ValueError( + f"Expected planar `array.shape[0]` to equal `{nb_channels}` but got `{array.shape[0]}`" + ) samples = array.shape[1] else: if array.shape[0] != 1: - raise ValueError(f"Expected packed `array.shape[0]` to equal `1` but got `{array.shape[0]}`") + raise ValueError( + f"Expected packed `array.shape[0]` to equal `1` but got `{array.shape[0]}`" + ) samples = array.shape[1] // nb_channels - frame = AudioFrame(format=format, layout=layout, samples=samples) + frame = AudioFrame(format=py_format, layout=py_layout, samples=samples) for i, plane in enumerate(frame.planes): plane.update(array[i, :]) return frame @@ -129,7 +143,7 @@ cdef class AudioFrame(Frame): :type: tuple """ - cdef int plane_count = 0 + plane_count: cython.int = 0 while self.ptr.extended_data[plane_count]: plane_count += 1 @@ -177,11 +191,15 @@ cdef class AudioFrame(Frame): try: dtype = np.dtype(format_dtypes[self.format.name]) except KeyError: - raise ValueError(f"Conversion from {self.format.name!r} format to numpy array is not supported.") + raise ValueError( + f"Conversion from {self.format.name!r} format to numpy array is not supported." + ) if self.format.is_planar: count = self.samples else: count = self.samples * self.layout.nb_channels - return np.vstack([np.frombuffer(x, dtype=dtype, count=count) for x in self.planes]) + return np.vstack( + [np.frombuffer(x, dtype=dtype, count=count) for x in self.planes] + ) From ef999f7c2710afa9a290a45e2850178950388881 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 23 Jun 2025 20:22:51 -0400 Subject: [PATCH 280/365] Make audio/resampler pure --- av/audio/{frame.pyx => frame.py} | 0 av/audio/resampler.pxd | 5 +-- av/audio/{resampler.pyx => resampler.py} | 53 +++++++++++++----------- 3 files changed, 29 insertions(+), 29 deletions(-) rename av/audio/{frame.pyx => frame.py} (100%) rename av/audio/{resampler.pyx => resampler.py} (76%) diff --git a/av/audio/frame.pyx b/av/audio/frame.py similarity index 100% rename from av/audio/frame.pyx rename to av/audio/frame.py diff --git a/av/audio/resampler.pxd b/av/audio/resampler.pxd index d3601403d..20b74186e 100644 --- a/av/audio/resampler.pxd +++ b/av/audio/resampler.pxd @@ -5,9 +5,7 @@ from av.filter.graph cimport Graph cdef class AudioResampler: - cdef readonly bint is_passthrough - cdef AudioFrame template # Destination descriptors @@ -17,5 +15,4 @@ cdef class AudioResampler: cdef readonly unsigned int frame_size cdef Graph graph - - cpdef resample(self, AudioFrame) + cpdef list resample(self, AudioFrame) diff --git a/av/audio/resampler.pyx b/av/audio/resampler.py similarity index 76% rename from av/audio/resampler.pyx rename to av/audio/resampler.py index 69d790bad..d56ec06ed 100644 --- a/av/audio/resampler.pyx +++ b/av/audio/resampler.py @@ -1,12 +1,14 @@ -from av.filter.context cimport FilterContext +from errno import EAGAIN -import errno +import cython +from cython.cimports.av.filter.context import FilterContext +from cython.cimports.av.filter.graph import Graph -import av.filter +from av.error import FFmpegError -cdef class AudioResampler: - +@cython.cclass +class AudioResampler: """AudioResampler(format=None, layout=None, rate=None) :param AudioFormat format: The target format, or string that parses to one @@ -14,23 +16,23 @@ :param AudioLayout layout: The target layout, or an int/string that parses to one (e.g. ``"stereo"``). :param int rate: The target sample rate. - - """ def __cinit__(self, format=None, layout=None, rate=None, frame_size=None): if format is not None: - self.format = format if isinstance(format, AudioFormat) else AudioFormat(format) + self.format = ( + format if isinstance(format, AudioFormat) else AudioFormat(format) + ) if layout is not None: self.layout = AudioLayout(layout) - self.rate = int(rate) if rate else 0 + self.rate = int(rate) if rate else 0 self.frame_size = int(frame_size) if frame_size else 0 - self.graph = None - cpdef resample(self, AudioFrame frame): + @cython.ccall + def resample(self, frame: AudioFrame | None) -> list: """resample(frame) Convert the ``sample_rate``, ``channel_layout`` and/or ``format`` of @@ -60,30 +62,31 @@ def __cinit__(self, format=None, layout=None, rate=None, frame_size=None): # Check if we can passthrough or if there is actually work to do. if ( - frame.format.sample_fmt == self.format.sample_fmt and - frame.layout == self.layout and - frame.sample_rate == self.rate and - self.frame_size == 0 + frame.format.sample_fmt == self.format.sample_fmt + and frame.layout == self.layout + and frame.sample_rate == self.rate + and self.frame_size == 0 ): self.is_passthrough = True return [frame] # handle resampling with aformat filter # (similar to configure_output_audio_filter from ffmpeg) - self.graph = av.filter.Graph() + self.graph = Graph() extra_args = {} if frame.time_base is not None: - extra_args["time_base"] = str(frame.time_base) + extra_args["time_base"] = f"{frame.time_base}" + abuffer = self.graph.add( "abuffer", - sample_rate=str(frame.sample_rate), + sample_rate=f"{frame.sample_rate}", sample_fmt=AudioFormat(frame.format).name, channel_layout=frame.layout.name, **extra_args, ) aformat = self.graph.add( "aformat", - sample_rates=str(self.rate), + sample_rates=f"{self.rate}", sample_fmts=self.format.name, channel_layouts=self.layout.name, ) @@ -97,22 +100,22 @@ def __cinit__(self, format=None, layout=None, rate=None, frame_size=None): if frame is not None: if ( - frame.format.sample_fmt != self.template.format.sample_fmt or - frame.layout != self.template.layout or - frame.sample_rate != self.template.rate + frame.format.sample_fmt != self.template.format.sample_fmt + or frame.layout != self.template.layout + or frame.sample_rate != self.template.rate ): raise ValueError("Frame does not match AudioResampler setup.") self.graph.push(frame) - output = [] + output: list = [] while True: try: output.append(self.graph.pull()) except EOFError: break - except av.FFmpegError as e: - if e.errno != errno.EAGAIN: + except FFmpegError as e: + if e.errno != EAGAIN: raise break From 92ada7ced22fb8cfea102b541d5a5994a1b92d75 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 23 Jun 2025 20:33:00 -0400 Subject: [PATCH 281/365] Make audio/stream pure --- av/audio/{stream.pyx => stream.py} | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) rename av/audio/{stream.pyx => stream.py} (75%) diff --git a/av/audio/stream.pyx b/av/audio/stream.py similarity index 75% rename from av/audio/stream.pyx rename to av/audio/stream.py index 4d633edce..7c150c84b 100644 --- a/av/audio/stream.pyx +++ b/av/audio/stream.py @@ -1,9 +1,10 @@ -from av.packet cimport Packet +import cython +from cython.cimports.av.audio.frame import AudioFrame +from cython.cimports.av.packet import Packet -from .frame cimport AudioFrame - -cdef class AudioStream(Stream): +@cython.cclass +class AudioStream(Stream): def __repr__(self): form = self.format.name if self.format else None return ( @@ -14,7 +15,8 @@ def __repr__(self): def __getattr__(self, name): return getattr(self.codec_context, name) - cpdef encode(self, AudioFrame frame=None): + @cython.ccall + def encode(self, frame: AudioFrame | None = None): """ Encode an :class:`.AudioFrame` and return a list of :class:`.Packet`. @@ -24,14 +26,15 @@ def __getattr__(self, name): """ packets = self.codec_context.encode(frame) - cdef Packet packet + packet: Packet for packet in packets: packet._stream = self packet.ptr.stream_index = self.ptr.index return packets - cpdef decode(self, Packet packet=None): + @cython.ccall + def decode(self, packet: Packet | None = None): """ Decode a :class:`.Packet` and return a list of :class:`.AudioFrame`. From 5313c4104ed74350bc1c2b4751776cd177e6654a Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 23 Jun 2025 20:53:24 -0400 Subject: [PATCH 282/365] Make audio/plane pure --- av/audio/plane.pxd | 2 -- av/audio/plane.py | 13 +++++++++++++ av/audio/plane.pyx | 11 ----------- 3 files changed, 13 insertions(+), 13 deletions(-) create mode 100644 av/audio/plane.py delete mode 100644 av/audio/plane.pyx diff --git a/av/audio/plane.pxd b/av/audio/plane.pxd index 316c84031..de912ac22 100644 --- a/av/audio/plane.pxd +++ b/av/audio/plane.pxd @@ -2,7 +2,5 @@ from av.plane cimport Plane cdef class AudioPlane(Plane): - cdef readonly size_t buffer_size - cdef size_t _buffer_size(self) diff --git a/av/audio/plane.py b/av/audio/plane.py new file mode 100644 index 000000000..bdaf15708 --- /dev/null +++ b/av/audio/plane.py @@ -0,0 +1,13 @@ +import cython +from cython.cimports.av.audio.frame import AudioFrame + + +@cython.cclass +class AudioPlane(Plane): + def __cinit__(self, frame: AudioFrame, index: cython.int): + # Only the first linesize is ever populated, but it applies to every plane. + self.buffer_size = self.frame.ptr.linesize[0] + + @cython.cfunc + def _buffer_size(self) -> cython.size_t: + return self.buffer_size diff --git a/av/audio/plane.pyx b/av/audio/plane.pyx deleted file mode 100644 index 92c508cbd..000000000 --- a/av/audio/plane.pyx +++ /dev/null @@ -1,11 +0,0 @@ -from av.audio.frame cimport AudioFrame - - -cdef class AudioPlane(Plane): - - def __cinit__(self, AudioFrame frame, int index): - # Only the first linesize is ever populated, but it applies to every plane. - self.buffer_size = self.frame.ptr.linesize[0] - - cdef size_t _buffer_size(self): - return self.buffer_size From 44d8095f844f34498ab7b19d371fe2a391cdc8a4 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 23 Jun 2025 21:19:26 -0400 Subject: [PATCH 283/365] Make video/stream pure --- av/video/{stream.pyx => stream.py} | 60 ++++++++++++++++-------------- 1 file changed, 33 insertions(+), 27 deletions(-) rename av/video/{stream.pyx => stream.py} (65%) diff --git a/av/video/stream.pyx b/av/video/stream.py similarity index 65% rename from av/video/stream.pyx rename to av/video/stream.py index 383cc13a6..0a7032b09 100644 --- a/av/video/stream.pyx +++ b/av/video/stream.py @@ -1,12 +1,12 @@ -cimport libav as lib +import cython +from cython.cimports import libav as lib +from cython.cimports.av.packet import Packet +from cython.cimports.av.utils import avrational_to_fraction, to_avrational +from cython.cimports.av.video.frame import VideoFrame -from av.packet cimport Packet -from av.utils cimport avrational_to_fraction, to_avrational -from .frame cimport VideoFrame - - -cdef class VideoStream(Stream): +@cython.cclass +class VideoStream(Stream): def __repr__(self): return ( f"= 4.0 - cdef lib.AVRational val = lib.av_guess_frame_rate(NULL, self.ptr, NULL) - return avrational_to_fraction(&val) + val: lib.AVRational = lib.av_guess_frame_rate( + cython.NULL, self.ptr, cython.NULL + ) + return avrational_to_fraction(cython.address(val)) @property def sample_aspect_ratio(self): @@ -96,9 +98,11 @@ def sample_aspect_ratio(self): :type: fractions.Fraction | None """ - cdef lib.AVRational sar = lib.av_guess_sample_aspect_ratio(self.container.ptr, self.ptr, NULL) - return avrational_to_fraction(&sar) - + sar: lib.AVRational = lib.av_guess_sample_aspect_ratio( + self.container.ptr, self.ptr, cython.NULL + ) + return avrational_to_fraction(cython.address(sar)) + @property def display_aspect_ratio(self): """The guessed display aspect ratio (DAR) of this stream. @@ -107,11 +111,13 @@ def display_aspect_ratio(self): :type: fractions.Fraction | None """ - cdef lib.AVRational dar - + dar = cython.declare(lib.AVRational) lib.av_reduce( - &dar.num, &dar.den, + cython.address(dar.num), + cython.address(dar.den), self.format.width * self.sample_aspect_ratio.num, - self.format.height * self.sample_aspect_ratio.den, 1024*1024) + self.format.height * self.sample_aspect_ratio.den, + 1024 * 1024, + ) - return avrational_to_fraction(&dar) + return avrational_to_fraction(cython.address(dar)) From ae04e1efff870b969799aafa314ef95a0ab26c00 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 23 Jun 2025 22:24:04 -0400 Subject: [PATCH 284/365] video/frame: apply BasswoodAV changes This also applies "add GBRP, GBRAP, RGBA formats" by z-khan Co-authored-by: z-khan --- av/video/frame.pxd | 1 + av/video/{frame.pyx => frame.py} | 766 +++++++++++++++++++++++-------- av/video/frame.pyi | 9 +- tests/test_videoframe.py | 469 ++++++++++++------- 4 files changed, 886 insertions(+), 359 deletions(-) rename av/video/{frame.pyx => frame.py} (54%) diff --git a/av/video/frame.pxd b/av/video/frame.pxd index 779b23977..d352a7ab8 100644 --- a/av/video/frame.pxd +++ b/av/video/frame.pxd @@ -17,5 +17,6 @@ cdef class VideoFrame(Frame): cdef _init(self, lib.AVPixelFormat format, unsigned int width, unsigned int height) cdef _init_user_attributes(self) + cpdef save(self, object filepath) cdef VideoFrame alloc_video_frame() diff --git a/av/video/frame.pyx b/av/video/frame.py similarity index 54% rename from av/video/frame.pyx rename to av/video/frame.py index 04a14fd8b..bc4bf9154 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.py @@ -1,38 +1,115 @@ import sys from enum import IntEnum -from libc.stdint cimport uint8_t +import cython +from cython.cimports.av.error import err_check +from cython.cimports.av.sidedata.sidedata import get_display_rotation +from cython.cimports.av.utils import check_ndarray +from cython.cimports.av.video.format import get_pix_fmt, get_video_format +from cython.cimports.av.video.plane import VideoPlane +from cython.cimports.libc.stdint import uint8_t -from av.error cimport err_check -from av.sidedata.sidedata cimport get_display_rotation -from av.utils cimport check_ndarray -from av.video.format cimport get_pix_fmt, get_video_format -from av.video.plane cimport VideoPlane - - -cdef object _cinit_bypass_sentinel +_cinit_bypass_sentinel = object() # `pix_fmt`s supported by Frame.to_ndarray() and Frame.from_ndarray() supported_np_pix_fmts = { - "abgr", "argb", "bayer_bggr16be", "bayer_bggr16le", "bayer_bggr8", "bayer_gbrg16be", - "bayer_gbrg16le", "bayer_gbrg8", "bayer_grbg16be", "bayer_grbg16le", "bayer_grbg8", - "bayer_rggb16be", "bayer_rggb16le", "bayer_rggb8", "bgr24", "bgr48be", "bgr48le", "bgr8", "bgra", "bgra64be", "bgra64le", - "gbrapf32be", "gbrapf32le", "gbrp", "gbrp10be", "gbrp10le", "gbrp12be", "gbrp12le", - "gbrp14be", "gbrp14le", "gbrp16be", "gbrp16le", "gbrpf32be", "gbrpf32le", "gray", - "gray16be", "gray16le", "gray8", "grayf32be", "grayf32le", "nv12", "pal8", "rgb24", - "rgb48be", "rgb48le", "rgb8", "rgba", "rgba64be", "rgba64le", "yuv420p", - "yuv420p10le", "yuv422p10le", "yuv444p", "yuv444p16be", "yuv444p16le", "yuva444p16be", - "yuva444p16le", "yuvj420p", "yuvj444p", "yuyv422", + "abgr", + "argb", + "bayer_bggr16be", + "bayer_bggr16le", + "bayer_bggr8", + "bayer_gbrg16be", + "bayer_gbrg16le", + "bayer_gbrg8", + "bayer_grbg16be", + "bayer_grbg16le", + "bayer_grbg8", + "bayer_rggb16be", + "bayer_rggb16le", + "bayer_rggb8", + "bgr24", + "bgr48be", + "bgr48le", + "bgr8", + "bgra", + "bgra64be", + "bgra64le", + "gbrap", + "gbrap10be", + "gbrap10le", + "gbrap12be", + "gbrap12le", + "gbrap14be", + "gbrap14le", + "gbrap16be", + "gbrap16le", + "gbrapf32be", + "gbrapf32le", + "gbrp", + "gbrp10be", + "gbrp10le", + "gbrp12be", + "gbrp12le", + "gbrp14be", + "gbrp14le", + "gbrp16be", + "gbrp16le", + "gbrp9be", + "gbrp9le", + "gbrpf32be", + "gbrpf32le", + "gray", + "gray10be", + "gray10le", + "gray12be", + "gray12le", + "gray14be", + "gray14le", + "gray16be", + "gray16le", + "gray8", + "gray9be", + "gray9le", + "grayf32be", + "grayf32le", + "nv12", + "pal8", + "rgb24", + "rgb48be", + "rgb48le", + "rgb8", + "rgba", + "rgba64be", + "rgba64le", + "rgbaf16be", + "rgbaf16le", + "rgbaf32be", + "rgbaf32le", + "rgbf32be", + "rgbf32le", + "yuv420p", + "yuv422p10le", + "yuv444p", + "yuv444p16be", + "yuv444p16le", + "yuva444p16be", + "yuva444p16le", + "yuvj420p", + "yuvj444p", + "yuyv422", } -cdef VideoFrame alloc_video_frame(): + +@cython.cfunc +def alloc_video_frame() -> VideoFrame: """Get a mostly uninitialized VideoFrame. You MUST call VideoFrame._init(...) or VideoFrame._init_user_attributes() before exposing to the user. """ - return VideoFrame.__new__(VideoFrame, _cinit_bypass_sentinel) + return VideoFrame(_cinit_bypass_sentinel) + class PictureType(IntEnum): NONE = lib.AV_PICTURE_TYPE_NONE # Undefined @@ -44,23 +121,31 @@ class PictureType(IntEnum): SP = lib.AV_PICTURE_TYPE_SP # Switching predicted BI = lib.AV_PICTURE_TYPE_BI # BI type -cdef byteswap_array(array, bint big_endian): + +@cython.cfunc +def byteswap_array(array, big_endian: cython.bint): if (sys.byteorder == "big") != big_endian: return array.byteswap() - else: - return array - - -cdef copy_bytes_to_plane(img_bytes, VideoPlane plane, unsigned int bytes_per_pixel, bint flip_horizontal, bint flip_vertical): - cdef const uint8_t[:] i_buf = img_bytes - cdef size_t i_pos = 0 - cdef size_t i_stride = plane.width * bytes_per_pixel - - cdef uint8_t[:] o_buf = plane - cdef size_t o_pos = 0 - cdef size_t o_stride = abs(plane.line_size) - - cdef int start_row, end_row, step + return array + + +@cython.cfunc +def copy_bytes_to_plane( + img_bytes, + plane: VideoPlane, + bytes_per_pixel: cython.uint, + flip_horizontal: cython.bint, + flip_vertical: cython.bint, +): + i_buf: cython.const[uint8_t][:] = img_bytes + i_pos: cython.size_t = 0 + i_stride: cython.size_t = plane.width * bytes_per_pixel + + o_buf: uint8_t[:] = plane + o_pos: cython.size_t = 0 + o_stride: cython.size_t = abs(plane.line_size) + + start_row, end_row, step = cython.declare(cython.int) if flip_vertical: start_row = plane.height - 1 end_row = -1 @@ -70,56 +155,65 @@ class PictureType(IntEnum): end_row = plane.height step = 1 - cdef int i, j + i, j = cython.declare(cython.int) for row in range(start_row, end_row, step): i_pos = row * i_stride if flip_horizontal: for i in range(0, i_stride, bytes_per_pixel): for j in range(bytes_per_pixel): - o_buf[o_pos + i + j] = i_buf[i_pos + i_stride - i - bytes_per_pixel + j] + o_buf[o_pos + i + j] = i_buf[ + i_pos + i_stride - i - bytes_per_pixel + j + ] else: - o_buf[o_pos:o_pos + i_stride] = i_buf[i_pos:i_pos + i_stride] + o_buf[o_pos : o_pos + i_stride] = i_buf[i_pos : i_pos + i_stride] o_pos += o_stride -cdef copy_array_to_plane(array, VideoPlane plane, unsigned int bytes_per_pixel): - cdef bytes imgbytes = array.tobytes() +@cython.cfunc +def copy_array_to_plane(array, plane: VideoPlane, bytes_per_pixel: cython.uint): + imgbytes: bytes = array.tobytes() copy_bytes_to_plane(imgbytes, plane, bytes_per_pixel, False, False) -cdef useful_array(VideoPlane plane, unsigned int bytes_per_pixel=1, str dtype="uint8"): +@cython.cfunc +def useful_array( + plane: VideoPlane, bytes_per_pixel: cython.uint = 1, dtype: str = "uint8" +): """ Return the useful part of the VideoPlane as a single dimensional array. We are simply discarding any padding which was added for alignment. """ import numpy as np - cdef size_t total_line_size = abs(plane.line_size) - cdef size_t useful_line_size = plane.width * bytes_per_pixel + + total_line_size: cython.size_t = abs(plane.line_size) + useful_line_size: cython.size_t = plane.width * bytes_per_pixel arr = np.frombuffer(plane, np.uint8) if total_line_size != useful_line_size: arr = arr.reshape(-1, total_line_size)[:, 0:useful_line_size].reshape(-1) return arr.view(np.dtype(dtype)) -cdef check_ndarray_shape(object array, bint ok): +@cython.cfunc +def check_ndarray_shape(array: object, ok: cython.bint): if not ok: raise ValueError(f"Unexpected numpy array shape `{array.shape}`") -cdef class VideoFrame(Frame): +@cython.cclass +class VideoFrame(Frame): def __cinit__(self, width=0, height=0, format="yuv420p"): if width is _cinit_bypass_sentinel: return - cdef lib.AVPixelFormat c_format = get_pix_fmt(format) - + c_format: lib.AVPixelFormat = get_pix_fmt(format) self._init(c_format, width, height) - cdef _init(self, lib.AVPixelFormat format, unsigned int width, unsigned int height): - cdef int res = 0 + @cython.cfunc + def _init(self, format: lib.AVPixelFormat, width: cython.uint, height: cython.uint): + res: cython.int = 0 - with nogil: + with cython.nogil: self.ptr.width = width self.ptr.height = height self.ptr.format = format @@ -137,13 +231,18 @@ def __cinit__(self, width=0, height=0, format="yuv420p"): self._init_user_attributes() - cdef _init_user_attributes(self): - self.format = get_video_format(self.ptr.format, self.ptr.width, self.ptr.height) + @cython.cfunc + def _init_user_attributes(self): + self.format = get_video_format( + cython.cast(lib.AVPixelFormat, self.ptr.format), + self.ptr.width, + self.ptr.height, + ) def __dealloc__(self): # The `self._buffer` member is only set if *we* allocated the buffer in `_init`, # as opposed to a buffer allocated by a decoder. - lib.av_freep(&self._buffer) + lib.av_freep(cython.address(self._buffer)) # Let go of the reference from the numpy buffers if we made one self._np_buffer = None @@ -158,11 +257,10 @@ def planes(self): """ A tuple of :class:`.VideoPlane` objects. """ - # We need to detect which planes actually exist, but also contrain - # ourselves to the maximum plane count (as determined only by VideoFrames - # so far), in case the library implementation does not set the last - # plane to NULL. - cdef int max_plane_count = 0 + # We need to detect which planes actually exist, but also constrain ourselves to + # the maximum plane count (as determined only by VideoFrames so far), in case + # the library implementation does not set the last plane to NULL. + max_plane_count: cython.int = 0 for i in range(self.format.ptr.nb_components): count = self.format.ptr.comp[i].plane + 1 if max_plane_count < count: @@ -170,7 +268,7 @@ def planes(self): if self.format.name == "pal8": max_plane_count = 2 - cdef int plane_count = 0 + plane_count: cython.int = 0 while plane_count < max_plane_count and self.ptr.extended_data[plane_count]: plane_count += 1 return tuple([VideoPlane(self, i) for i in range(plane_count)]) @@ -267,6 +365,34 @@ def to_rgb(self, **kwargs): """ return self.reformat(format="rgb24", **kwargs) + @cython.ccall + def save(self, filepath: object): + """Save a VideoFrame as a JPG or PNG. + + :param filepath: str | Path + """ + is_jpg: cython.bint + + if filepath.endswith(".png"): + is_jpg = False + elif filepath.endswith(".jpg") or filepath.endswith(".jpeg"): + is_jpg = True + else: + raise ValueError("filepath must end with png or jpg.") + + encoder: str = "mjpeg" if is_jpg else "png" + pix_fmt: str = "yuvj420p" if is_jpg else "rgb24" + + from av.container.core import open + + with open(filepath, "w", options={"update": "1"}) as output: + output_stream = output.add_stream(encoder, pix_fmt=pix_fmt) + output_stream.width = self.width + output_stream.height = self.height + + output.mux(output_stream.encode(self.reformat(format=pix_fmt))) + output.mux(output_stream.encode(None)) + def to_image(self, **kwargs): """Get an RGB ``PIL.Image`` of this frame. @@ -276,23 +402,26 @@ def to_image(self, **kwargs): """ from PIL import Image - cdef VideoPlane plane = self.reformat(format="rgb24", **kwargs).planes[0] - cdef const uint8_t[:] i_buf = plane - cdef size_t i_pos = 0 - cdef size_t i_stride = plane.line_size + plane: VideoPlane = self.reformat(format="rgb24", **kwargs).planes[0] - cdef size_t o_pos = 0 - cdef size_t o_stride = plane.width * 3 - cdef size_t o_size = plane.height * o_stride - cdef bytearray o_buf = bytearray(o_size) + i_buf: cython.const[uint8_t][:] = plane + i_pos: cython.size_t = 0 + i_stride: cython.size_t = plane.line_size + + o_pos: cython.size_t = 0 + o_stride: cython.size_t = plane.width * 3 + o_size: cython.size_t = plane.height * o_stride + o_buf: bytearray = bytearray(o_size) while o_pos < o_size: - o_buf[o_pos:o_pos + o_stride] = i_buf[i_pos:i_pos + o_stride] + o_buf[o_pos : o_pos + o_stride] = i_buf[i_pos : i_pos + o_stride] i_pos += i_stride o_pos += o_stride - return Image.frombytes("RGB", (plane.width, plane.height), bytes(o_buf), "raw", "RGB", 0, 1) + return Image.frombytes( + "RGB", (plane.width, plane.height), bytes(o_buf), "raw", "RGB", 0, 1 + ) def to_ndarray(self, channel_last=False, **kwargs): """Get a numpy array of this frame. @@ -307,7 +436,7 @@ def to_ndarray(self, channel_last=False, **kwargs): .. note:: Numpy must be installed. - .. note:: For formats which return an array of ``uint16`` or ``float32``, + .. note:: For formats which return an array of ``uint16``, ``float16`` or ``float32``, the samples will be in the system's native byte order. .. note:: For ``pal8``, an ``(image, palette)`` tuple will be returned, @@ -316,14 +445,18 @@ def to_ndarray(self, channel_last=False, **kwargs): .. note:: For ``gbrp`` formats, channels are flipped to RGB order. """ - cdef VideoFrame frame = self.reformat(**kwargs) + frame: VideoFrame = self.reformat(**kwargs) import numpy as np # check size - if frame.format.name in {"yuv420p", "yuvj420p", "yuyv422", "yuv420p10le", "yuv422p10le"}: - assert frame.width % 2 == 0, "the width has to be even for this pixel format" - assert frame.height % 2 == 0, "the height has to be even for this pixel format" + if frame.format.name in {"yuv420p", "yuvj420p", "yuyv422", "yuv422p10le"}: + assert frame.width % 2 == 0, ( + "the width has to be even for this pixel format" + ) + assert frame.height % 2 == 0, ( + "the height has to be even for this pixel format" + ) # cases planes are simply concatenated in shape (height, width, channels) itemsize, dtype = { @@ -346,6 +479,17 @@ def to_ndarray(self, channel_last=False, **kwargs): "bgr48le": (6, "uint16"), "bgr8": (1, "uint8"), "bgra": (4, "uint8"), + "bgra64be": (8, "uint16"), + "bgra64le": (8, "uint16"), + "gbrap": (1, "uint8"), + "gbrap10be": (2, "uint16"), + "gbrap10le": (2, "uint16"), + "gbrap12be": (2, "uint16"), + "gbrap12le": (2, "uint16"), + "gbrap14be": (2, "uint16"), + "gbrap14le": (2, "uint16"), + "gbrap16be": (2, "uint16"), + "gbrap16le": (2, "uint16"), "gbrapf32be": (4, "float32"), "gbrapf32le": (4, "float32"), "gbrp": (1, "uint8"), @@ -357,12 +501,22 @@ def to_ndarray(self, channel_last=False, **kwargs): "gbrp14le": (2, "uint16"), "gbrp16be": (2, "uint16"), "gbrp16le": (2, "uint16"), + "gbrp9be": (2, "uint16"), + "gbrp9le": (2, "uint16"), "gbrpf32be": (4, "float32"), "gbrpf32le": (4, "float32"), "gray": (1, "uint8"), + "gray10be": (2, "uint16"), + "gray10le": (2, "uint16"), + "gray12be": (2, "uint16"), + "gray12le": (2, "uint16"), + "gray14be": (2, "uint16"), + "gray14le": (2, "uint16"), "gray16be": (2, "uint16"), "gray16le": (2, "uint16"), "gray8": (1, "uint8"), + "gray9be": (2, "uint16"), + "gray9le": (2, "uint16"), "grayf32be": (4, "float32"), "grayf32le": (4, "float32"), "rgb24": (3, "uint8"), @@ -372,8 +526,12 @@ def to_ndarray(self, channel_last=False, **kwargs): "rgba": (4, "uint8"), "rgba64be": (8, "uint16"), "rgba64le": (8, "uint16"), - "bgra64be": (8, "uint16"), - "bgra64le": (8, "uint16"), + "rgbaf16be": (8, "float16"), + "rgbaf16le": (8, "float16"), + "rgbaf32be": (16, "float32"), + "rgbaf32le": (16, "float32"), + "rgbf32be": (12, "float32"), + "rgbf32le": (12, "float32"), "yuv444p": (1, "uint8"), "yuv444p16be": (2, "uint16"), "yuv444p16le": (2, "uint16"), @@ -384,8 +542,9 @@ def to_ndarray(self, channel_last=False, **kwargs): }.get(frame.format.name, (None, None)) if itemsize is not None: layers = [ - useful_array(plan, itemsize, dtype) - .reshape(frame.height, frame.width, -1) + useful_array(plan, itemsize, dtype).reshape( + frame.height, frame.width, -1 + ) for plan in frame.planes ] if len(layers) == 1: # shortcut, avoid memory copy @@ -406,26 +565,24 @@ def to_ndarray(self, channel_last=False, **kwargs): # special cases if frame.format.name in {"yuv420p", "yuvj420p"}: - return np.hstack([ - useful_array(frame.planes[0]), - useful_array(frame.planes[1]), - useful_array(frame.planes[2]), - ]).reshape(-1, frame.width) - if frame.format.name == "yuv420p10le": - # Read planes as uint16: - y = useful_array(frame.planes[0], 2, "uint16").reshape(frame.height, frame.width) - u = useful_array(frame.planes[1], 2, "uint16").reshape(frame.height // 2, frame.width // 2) - v = useful_array(frame.planes[2], 2, "uint16").reshape(frame.height // 2, frame.width // 2) - u_full = np.repeat(np.repeat(u, 2, axis=1), 2, axis=0) - v_full = np.repeat(np.repeat(u, 2, axis=1), 2, axis=0) - if channel_last: - return np.stack([y, u_full, v_full], axis=2) - return np.stack([y, u_full, v_full], axis=0) + return np.hstack( + [ + useful_array(frame.planes[0]), + useful_array(frame.planes[1]), + useful_array(frame.planes[2]), + ] + ).reshape(-1, frame.width) if frame.format.name == "yuv422p10le": # Read planes as uint16 at their original width - y = useful_array(frame.planes[0], 2, "uint16").reshape(frame.height, frame.width) - u = useful_array(frame.planes[1], 2, "uint16").reshape(frame.height, frame.width // 2) - v = useful_array(frame.planes[2], 2, "uint16").reshape(frame.height, frame.width // 2) + y = useful_array(frame.planes[0], 2, "uint16").reshape( + frame.height, frame.width + ) + u = useful_array(frame.planes[1], 2, "uint16").reshape( + frame.height, frame.width // 2 + ) + v = useful_array(frame.planes[2], 2, "uint16").reshape( + frame.height, frame.width // 2 + ) # Double the width of U and V by repeating each value u_full = np.repeat(u, 2, axis=1) @@ -435,13 +592,20 @@ def to_ndarray(self, channel_last=False, **kwargs): return np.stack([y, u_full, v_full], axis=0) if frame.format.name == "pal8": image = useful_array(frame.planes[0]).reshape(frame.height, frame.width) - palette = np.frombuffer(frame.planes[1], "i4").astype(">i4").reshape(-1, 1).view(np.uint8) + palette = ( + np.frombuffer(frame.planes[1], "i4") + .astype(">i4") + .reshape(-1, 1) + .view(np.uint8) + ) return image, palette if frame.format.name == "nv12": - return np.hstack([ - useful_array(frame.planes[0]), - useful_array(frame.planes[1], 2), - ]).reshape(-1, frame.width) + return np.hstack( + [ + useful_array(frame.planes[0]), + useful_array(frame.planes[1], 2), + ] + ).reshape(-1, frame.width) raise ValueError( f"Conversion to numpy array with format `{frame.format.name}` is not yet supported" @@ -455,18 +619,29 @@ def from_image(img): if img.mode != "RGB": img = img.convert("RGB") - cdef VideoFrame frame = VideoFrame(img.size[0], img.size[1], "rgb24") + frame: VideoFrame = VideoFrame(img.size[0], img.size[1], "rgb24") copy_array_to_plane(img, frame.planes[0], 3) return frame @staticmethod def from_numpy_buffer(array, format="rgb24", width=0): - # Usually the width of the array is the same as the width of the image. But sometimes - # this is not possible, for example with yuv420p images that have padding. These are - # awkward because the UV rows at the bottom have padding bytes in the middle of the - # row as well as at the end. To cope with these, callers need to be able to pass the - # actual width to us. + """ + Construct a frame from a numpy buffer. + + :param int width: optional width of actual image, if different from the array width. + + .. note:: For formats which expect an array of ``uint16``, ``float16`` or ``float32``, + the samples must be in the system's native byte order. + + .. note:: for ``gbrp`` formats, channels are assumed to be given in RGB order. + + .. note:: For formats where width of the array is not the same as the width of the image, + for example with yuv420p images the UV rows at the bottom have padding bytes in the middle of the + row as well as at the end. To cope with these, callers need to be able to pass the actual width. + """ + import numpy as np + height = array.shape[0] if not width: width = array.shape[1] @@ -476,40 +651,169 @@ def from_numpy_buffer(array, format="rgb24", width=0): check_ndarray_shape(array, array.shape[2] == 3) if array.strides[1:] != (3, 1): raise ValueError("provided array does not have C_CONTIGUOUS rows") - linesizes = (array.strides[0], ) + linesizes = (array.strides[0],) elif format in {"rgb48le", "rgb48be", "bgr48le", "bgr48be"}: check_ndarray(array, "uint16", 3) check_ndarray_shape(array, array.shape[2] == 3) if array.strides[1:] != (6, 2): raise ValueError("provided array does not have C_CONTIGUOUS rows") - linesizes = (array.strides[0], ) + linesizes = (array.strides[0],) + elif format in {"rgbf32le", "rgbf32be"}: + check_ndarray(array, "float32", 3) + check_ndarray_shape(array, array.shape[2] == 3) + if array.strides[1:] != (12, 4): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = (array.strides[0],) elif format in {"rgba", "bgra", "argb", "abgr"}: check_ndarray(array, "uint8", 3) check_ndarray_shape(array, array.shape[2] == 4) if array.strides[1:] != (4, 1): raise ValueError("provided array does not have C_CONTIGUOUS rows") - linesizes = (array.strides[0], ) + linesizes = (array.strides[0],) elif format in {"rgba64le", "rgba64be", "bgra64le", "bgra64be"}: check_ndarray(array, "uint16", 3) check_ndarray_shape(array, array.shape[2] == 4) if array.strides[1:] != (8, 2): raise ValueError("provided array does not have C_CONTIGUOUS rows") - linesizes = (array.strides[0], ) - elif format in {"gray", "gray8", "rgb8", "bgr8","bayer_bggr8", "bayer_rggb8", "bayer_gbrg8", "bayer_grbg8"}: + linesizes = (array.strides[0],) + elif format in {"rgbaf16le", "rgbaf16be"}: + check_ndarray(array, "float16", 3) + check_ndarray_shape(array, array.shape[2] == 4) + if array.strides[1:] != (8, 2): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = (array.strides[0],) + elif format in {"rgbaf32le", "rgbaf32be"}: + check_ndarray(array, "float32", 3) + check_ndarray_shape(array, array.shape[2] == 4) + if array.strides[1:] != (16, 4): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = (array.strides[0],) + elif format in { + "gray", + "gray8", + "rgb8", + "bgr8", + "bayer_bggr8", + "bayer_gbrg8", + "bayer_grbg8", + "bayer_rggb8", + }: check_ndarray(array, "uint8", 2) if array.strides[1] != 1: raise ValueError("provided array does not have C_CONTIGUOUS rows") - linesizes = (array.strides[0], ) - elif format in {"gray16le", "gray16be", "bayer_rggb16le", "bayer_gbrg16le", "bayer_grbg16le","bayer_bggr16be", "bayer_rggb16be", "bayer_gbrg16be", "bayer_grbg16be"}: + linesizes = (array.strides[0],) + elif format in { + "gray9be", + "gray9le", + "gray10be", + "gray10le", + "gray12be", + "gray12le", + "gray14be", + "gray14le", + "gray16be", + "gray16le", + "bayer_bggr16be", + "bayer_bggr16le", + "bayer_gbrg16be", + "bayer_gbrg16le", + "bayer_grbg16be", + "bayer_grbg16le", + "bayer_rggb16be", + "bayer_rggb16le", + }: check_ndarray(array, "uint16", 2) if array.strides[1] != 2: raise ValueError("provided array does not have C_CONTIGUOUS rows") - linesizes = (array.strides[0], ) + linesizes = (array.strides[0],) elif format in {"grayf32le", "grayf32be"}: check_ndarray(array, "float32", 2) if array.strides[1] != 4: raise ValueError("provided array does not have C_CONTIGUOUS rows") - linesizes = (array.strides[0], ) + linesizes = (array.strides[0],) + elif format in {"gbrp"}: + check_ndarray(array, "uint8", 3) + check_ndarray_shape(array, array.shape[2] == 3) + if array.strides[1:] != (3, 1): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = ( + array.strides[0] // 3, + array.strides[0] // 3, + array.strides[0] // 3, + ) + elif format in { + "gbrp9be", + "gbrp9le", + "gbrp10be", + "gbrp10le", + "gbrp12be", + "gbrp12le", + "gbrp14be", + "gbrp14le", + "gbrp16be", + "gbrp16le", + }: + check_ndarray(array, "uint16", 3) + check_ndarray_shape(array, array.shape[2] == 3) + if array.strides[1:] != (6, 2): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = ( + array.strides[0] // 3, + array.strides[0] // 3, + array.strides[0] // 3, + ) + elif format in {"gbrpf32be", "gbrpf32le"}: + check_ndarray(array, "float32", 3) + check_ndarray_shape(array, array.shape[2] == 3) + if array.strides[1:] != (12, 4): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = ( + array.strides[0] // 3, + array.strides[0] // 3, + array.strides[0] // 3, + ) + elif format in {"gbrap"}: + check_ndarray(array, "uint8", 3) + check_ndarray_shape(array, array.shape[2] == 4) + if array.strides[1:] != (4, 1): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = ( + array.strides[0] // 4, + array.strides[0] // 4, + array.strides[0] // 4, + array.strides[0] // 4, + ) + elif format in { + "gbrap10be", + "gbrap10le", + "gbrap12be", + "gbrap12le", + "gbrap14be", + "gbrap14le", + "gbrap16be", + "gbrap16le", + }: + check_ndarray(array, "uint16", 3) + check_ndarray_shape(array, array.shape[2] == 4) + if array.strides[1:] != (8, 2): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = ( + array.strides[0] // 4, + array.strides[0] // 4, + array.strides[0] // 4, + array.strides[0] // 4, + ) + elif format in {"gbrapf32be", "gbrapf32le"}: + check_ndarray(array, "float32", 3) + check_ndarray_shape(array, array.shape[2] == 4) + if array.strides[1:] != (16, 4): + raise ValueError("provided array does not have C_CONTIGUOUS rows") + linesizes = ( + array.strides[0] // 4, + array.strides[0] // 4, + array.strides[0] // 4, + array.strides[0] // 4, + ) elif format in {"yuv420p", "yuvj420p", "nv12"}: check_ndarray(array, "uint8", 2) check_ndarray_shape(array, array.shape[0] % 3 == 0) @@ -519,26 +823,37 @@ def from_numpy_buffer(array, format="rgb24", width=0): raise ValueError("provided array does not have C_CONTIGUOUS rows") if format in {"yuv420p", "yuvj420p"}: # For YUV420 planar formats, the UV plane stride is always half the Y stride. - linesizes = (array.strides[0], array.strides[0] // 2, array.strides[0] // 2) + linesizes = ( + array.strides[0], + array.strides[0] // 2, + array.strides[0] // 2, + ) else: # Planes where U and V are interleaved have the same stride as Y. linesizes = (array.strides[0], array.strides[0]) else: - raise ValueError(f"Conversion from numpy array with format `{format}` is not yet supported") + raise ValueError( + f"Conversion from numpy array with format `{format}` is not yet supported" + ) + + if format.startswith("gbrap"): # rgba -> gbra + array = np.ascontiguousarray(np.moveaxis(array[..., [1, 2, 0, 3]], -1, 0)) + elif format.startswith("gbrp"): # rgb -> gbr + array = np.ascontiguousarray(np.moveaxis(array[..., [1, 2, 0]], -1, 0)) - frame = alloc_video_frame() + frame = VideoFrame(_cinit_bypass_sentinel) frame._image_fill_pointers_numpy(array, width, height, linesizes, format) return frame def _image_fill_pointers_numpy(self, buffer, width, height, linesizes, format): - cdef lib.AVPixelFormat c_format - cdef uint8_t * c_ptr - cdef size_t c_data + c_format: lib.AVPixelFormat + c_ptr: cython.pointer[uint8_t] + c_data: cython.size_t - # If you want to use the numpy notation - # then you need to include the following two lines at the top of the file + # If you want to use the numpy notation, then you need to include the following lines at the top of the file: # cimport numpy as cnp # cnp.import_array() + # And add the numpy include directories to the setup.py files # hint np.get_include() # cdef cnp.ndarray[ @@ -548,19 +863,15 @@ def _image_fill_pointers_numpy(self, buffer, width, height, linesizes, format): # c_ptr = &c_buffer[0] # c_ptr = ((buffer.ctypes.data)) - # Using buffer.ctypes.data helps avoid any kind of - # usage of the c-api from numpy, which avoid the need to add numpy - # as a compile time dependency - # Without this double cast, you get an error that looks like - # c_ptr = (buffer.ctypes.data) - # TypeError: expected bytes, int found + # Using buffer.ctypes.data helps avoid any kind of usage of the c-api from + # numpy, which avoid the need to add numpy as a compile time dependency. + c_data = buffer.ctypes.data - c_ptr = (c_data) + c_ptr = cython.cast(cython.pointer[uint8_t], c_data) c_format = get_pix_fmt(format) - lib.av_freep(&self._buffer) + lib.av_freep(cython.address(self._buffer)) - # Hold on to a reference for the numpy buffer - # so that it doesn't get accidentally garbage collected + # Hold on to a reference for the numpy buffer so that it doesn't get accidentally garbage collected self._np_buffer = buffer self.ptr.format = c_format self.ptr.width = width @@ -570,7 +881,7 @@ def _image_fill_pointers_numpy(self, buffer, width, height, linesizes, format): res = lib.av_image_fill_pointers( self.ptr.data, - self.ptr.format, + cython.cast(lib.AVPixelFormat, self.ptr.format), self.ptr.height, c_ptr, self.ptr.linesize, @@ -588,10 +899,11 @@ def from_ndarray(array, format="rgb24", channel_last=False): :param bool channel_last: If False (default), the shape for the yuv444p and yuvj444p is given by (channels, height, width) rather than (height, width, channels). - .. note:: For formats which expect an array of ``uint16``, + .. note:: For formats which expect an array of ``uint16``, ``float16`` or ``float32``, the samples must be in the system's native byte order. - .. note:: for ``pal8``, an ``(image, palette)`` pair must be passed. `palette` must have shape (256, 4) and is given in ARGB format (PyAV will swap bytes if needed). + .. note:: for ``pal8``, an ``(image, palette)`` pair must be passed. `palette` must + have shape (256, 4) and is given in ARGB format (PyAV will swap bytes if needed). .. note:: for ``gbrp`` formats, channels are assumed to be given in RGB order. @@ -600,45 +912,64 @@ def from_ndarray(array, format="rgb24", channel_last=False): # case layers are concatenated channels, itemsize, dtype = { - "yuv444p": (3, 1, "uint8"), - "yuvj444p": (3, 1, "uint8"), + "bayer_bggr16be": (1, 2, "uint16"), + "bayer_bggr16le": (1, 2, "uint16"), + "bayer_bggr8": (1, 1, "uint8"), + "bayer_gbrg16be": (1, 2, "uint16"), + "bayer_gbrg16le": (1, 2, "uint16"), + "bayer_gbrg8": (1, 1, "uint8"), + "bayer_grbg16be": (1, 2, "uint16"), + "bayer_grbg16le": (1, 2, "uint16"), + "bayer_grbg8": (1, 1, "uint8"), + "bayer_rggb16be": (1, 2, "uint16"), + "bayer_rggb16le": (1, 2, "uint16"), + "bayer_rggb8": (1, 1, "uint8"), + "bgr8": (1, 1, "uint8"), + "gbrap": (4, 1, "uint8"), + "gbrap10be": (4, 2, "uint16"), + "gbrap10le": (4, 2, "uint16"), + "gbrap12be": (4, 2, "uint16"), + "gbrap12le": (4, 2, "uint16"), + "gbrap14be": (4, 2, "uint16"), + "gbrap14le": (4, 2, "uint16"), + "gbrap16be": (4, 2, "uint16"), + "gbrap16le": (4, 2, "uint16"), + "gbrapf32be": (4, 4, "float32"), + "gbrapf32le": (4, 4, "float32"), "gbrp": (3, 1, "uint8"), "gbrp10be": (3, 2, "uint16"), - "gbrp12be": (3, 2, "uint16"), - "gbrp14be": (3, 2, "uint16"), - "gbrp16be": (3, 2, "uint16"), "gbrp10le": (3, 2, "uint16"), + "gbrp12be": (3, 2, "uint16"), "gbrp12le": (3, 2, "uint16"), + "gbrp14be": (3, 2, "uint16"), "gbrp14le": (3, 2, "uint16"), + "gbrp16be": (3, 2, "uint16"), "gbrp16le": (3, 2, "uint16"), + "gbrp9be": (3, 2, "uint16"), + "gbrp9le": (3, 2, "uint16"), "gbrpf32be": (3, 4, "float32"), "gbrpf32le": (3, 4, "float32"), "gray": (1, 1, "uint8"), - "gray8": (1, 1, "uint8"), - "rgb8": (1, 1, "uint8"), - "bgr8": (1, 1, "uint8"), + "gray10be": (1, 2, "uint16"), + "gray10le": (1, 2, "uint16"), + "gray12be": (1, 2, "uint16"), + "gray12le": (1, 2, "uint16"), + "gray14be": (1, 2, "uint16"), + "gray14le": (1, 2, "uint16"), "gray16be": (1, 2, "uint16"), "gray16le": (1, 2, "uint16"), + "gray8": (1, 1, "uint8"), + "gray9be": (1, 2, "uint16"), + "gray9le": (1, 2, "uint16"), "grayf32be": (1, 4, "float32"), "grayf32le": (1, 4, "float32"), - "gbrapf32be": (4, 4, "float32"), - "gbrapf32le": (4, 4, "float32"), + "rgb8": (1, 1, "uint8"), + "yuv444p": (3, 1, "uint8"), "yuv444p16be": (3, 2, "uint16"), "yuv444p16le": (3, 2, "uint16"), "yuva444p16be": (4, 2, "uint16"), - "yuva444p16le": (4, 2, "uint16"), - "bayer_bggr8": (1, 1, "uint8"), - "bayer_rggb8": (1, 1, "uint8"), - "bayer_grbg8": (1, 1, "uint8"), - "bayer_gbrg8": (1, 1, "uint8"), - "bayer_bggr16be": (1, 2, "uint16"), - "bayer_bggr16le": (1, 2, "uint16"), - "bayer_rggb16be": (1, 2, "uint16"), - "bayer_rggb16le": (1, 2, "uint16"), - "bayer_grbg16be": (1, 2, "uint16"), - "bayer_grbg16le": (1, 2, "uint16"), - "bayer_gbrg16be": (1, 2, "uint16"), - "bayer_gbrg16le": (1, 2, "uint16"), + "yuva444p16le": (4, 2, "uint16"), + "yuvj444p": (3, 1, "uint8"), }.get(format, (None, None, None)) if channels is not None: if array.ndim == 2: # (height, width) -> (height, width, 1) @@ -650,9 +981,14 @@ def from_ndarray(array, format="rgb24", channel_last=False): array = byteswap_array(array, format.endswith("be")) frame = VideoFrame(array.shape[1], array.shape[0], format) if frame.format.name.startswith("gbr"): # rgb -> gbr - array = np.concatenate([ # not inplace to avoid bad surprises - array[:, :, 1:3], array[:, :, 0:1], array[:, :, 3:], - ], axis=2) + array = np.concatenate( + [ # not inplace to avoid bad surprises + array[:, :, 1:3], + array[:, :, 0:1], + array[:, :, 3:], + ], + axis=2, + ) for i in range(channels): copy_array_to_plane(array[:, :, i], frame.planes[i], itemsize) return frame @@ -681,28 +1017,6 @@ def from_ndarray(array, format="rgb24", channel_last=False): copy_array_to_plane(flat[u_start:v_start], frame.planes[1], 1) copy_array_to_plane(flat[v_start:], frame.planes[2], 1) return frame - elif format == "yuv420p10le": - if not isinstance(array, np.ndarray) or array.dtype != np.uint16: - raise ValueError("Array must be uint16 type") - - # Convert to channel-first if needed: - if channel_last and array.shape[2] == 3: - array = np.moveaxis(array, 2, 0) - elif not (array.shape[0] == 3): - raise ValueError("Array must have shape (3, height, width) or (height, width, 3)") - - height, width = array.shape[1:] - if width % 2 != 0 or height % 2 != 0: - raise ValueError("Width and height must be even") - - frame = VideoFrame(width, height, format) - copy_array_to_plane(array[0], frame.planes[0], 2) - # Subsample U and V by taking every other row and column: - u = array[1, ::2, ::2].copy() # Need copy to ensure C-contiguous - v = array[2, ::2, ::2].copy() # Need copy to ensure C-contiguous - copy_array_to_plane(u, frame.planes[1], 2) - copy_array_to_plane(v, frame.planes[2], 2) - return frame elif format == "yuv422p10le": if not isinstance(array, np.ndarray) or array.dtype != np.uint16: raise ValueError("Array must be uint16 type") @@ -711,7 +1025,9 @@ def from_ndarray(array, format="rgb24", channel_last=False): if channel_last and array.shape[2] == 3: array = np.moveaxis(array, 2, 0) elif not (array.shape[0] == 3): - raise ValueError("Array must have shape (3, height, width) or (height, width, 3)") + raise ValueError( + "Array must have shape (3, height, width) or (height, width, 3)" + ) height, width = array.shape[1:] if width % 2 != 0 or height % 2 != 0: @@ -736,17 +1052,45 @@ def from_ndarray(array, format="rgb24", channel_last=False): elif format in {"argb", "rgba", "abgr", "bgra"}: check_ndarray(array, "uint8", 3) check_ndarray_shape(array, array.shape[2] == 4) - elif format in {"rgb48be", "rgb48le","bgr48be", "bgr48le"}: + elif format in {"rgb48be", "rgb48le", "bgr48be", "bgr48le"}: check_ndarray(array, "uint16", 3) check_ndarray_shape(array, array.shape[2] == 3) frame = VideoFrame(array.shape[1], array.shape[0], format) - copy_array_to_plane(byteswap_array(array, format.endswith("be")), frame.planes[0], 6) + copy_array_to_plane( + byteswap_array(array, format.endswith("be")), frame.planes[0], 6 + ) + return frame + elif format in {"rgbf32be", "rgbf32le"}: + check_ndarray(array, "float32", 3) + check_ndarray_shape(array, array.shape[2] == 3) + frame = VideoFrame(array.shape[1], array.shape[0], format) + copy_array_to_plane( + byteswap_array(array, format.endswith("be")), frame.planes[0], 12 + ) return frame elif format in {"rgba64be", "rgba64le", "bgra64be", "bgra64le"}: check_ndarray(array, "uint16", 3) check_ndarray_shape(array, array.shape[2] == 4) frame = VideoFrame(array.shape[1], array.shape[0], format) - copy_array_to_plane(byteswap_array(array, format.endswith("be")), frame.planes[0], 8) + copy_array_to_plane( + byteswap_array(array, format.endswith("be")), frame.planes[0], 8 + ) + return frame + elif format in {"rgbaf16be", "rgbaf16le"}: + check_ndarray(array, "float16", 3) + check_ndarray_shape(array, array.shape[2] == 4) + frame = VideoFrame(array.shape[1], array.shape[0], format) + copy_array_to_plane( + byteswap_array(array, format.endswith("be")), frame.planes[0], 8 + ) + return frame + elif format in {"rgbaf32be", "rgbaf32le"}: + check_ndarray(array, "float32", 3) + check_ndarray_shape(array, array.shape[2] == 4) + frame = VideoFrame(array.shape[1], array.shape[0], format) + copy_array_to_plane( + byteswap_array(array, format.endswith("be")), frame.planes[0], 16 + ) return frame elif format == "nv12": check_ndarray(array, "uint8", 2) @@ -760,20 +1104,52 @@ def from_ndarray(array, format="rgb24", channel_last=False): copy_array_to_plane(flat[uv_start:], frame.planes[1], 2) return frame else: - raise ValueError(f"Conversion from numpy array with format `{format}` is not yet supported") + raise ValueError( + f"Conversion from numpy array with format `{format}` is not yet supported" + ) frame = VideoFrame(array.shape[1], array.shape[0], format) - copy_array_to_plane(array, frame.planes[0], 1 if array.ndim == 2 else array.shape[2]) + copy_array_to_plane( + array, frame.planes[0], 1 if array.ndim == 2 else array.shape[2] + ) return frame @staticmethod - def from_bytes(img_bytes: bytes, width: int, height: int, format="rgba", flip_horizontal=False, flip_vertical=False): + def from_bytes( + img_bytes: bytes, + width: int, + height: int, + format="rgba", + flip_horizontal=False, + flip_vertical=False, + ): frame = VideoFrame(width, height, format) if format == "rgba": - copy_bytes_to_plane(img_bytes, frame.planes[0], 4, flip_horizontal, flip_vertical) - elif format in ("bayer_bggr8", "bayer_rggb8", "bayer_gbrg8", "bayer_grbg8","bayer_bggr16le", "bayer_rggb16le", "bayer_gbrg16le", "bayer_grbg16le","bayer_bggr16be", "bayer_rggb16be", "bayer_gbrg16be", "bayer_grbg16be"): - copy_bytes_to_plane(img_bytes, frame.planes[0], 1 if format.endswith("8") else 2, flip_horizontal, flip_vertical) + copy_bytes_to_plane( + img_bytes, frame.planes[0], 4, flip_horizontal, flip_vertical + ) + elif format in { + "bayer_bggr8", + "bayer_rggb8", + "bayer_gbrg8", + "bayer_grbg8", + "bayer_bggr16le", + "bayer_rggb16le", + "bayer_gbrg16le", + "bayer_grbg16le", + "bayer_bggr16be", + "bayer_rggb16be", + "bayer_gbrg16be", + "bayer_grbg16be", + }: + copy_bytes_to_plane( + img_bytes, + frame.planes[0], + 1 if format.endswith("8") else 2, + flip_horizontal, + flip_vertical, + ) else: raise NotImplementedError(f"Format '{format}' is not supported.") return frame diff --git a/av/video/frame.pyi b/av/video/frame.pyi index 313e184f9..c6868aabc 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -1,8 +1,8 @@ from enum import IntEnum +from pathlib import Path from typing import Any, ClassVar, Union import numpy as np -from PIL import Image from av.frame import Frame @@ -12,6 +12,7 @@ from .plane import VideoPlane _SupportedNDarray = Union[ np.ndarray[Any, np.dtype[np.uint8]], np.ndarray[Any, np.dtype[np.uint16]], + np.ndarray[Any, np.dtype[np.float16]], np.ndarray[Any, np.dtype[np.float32]], ] @@ -30,7 +31,6 @@ class PictureType(IntEnum): class VideoFrame(Frame): format: VideoFormat pts: int - duration: int planes: tuple[VideoPlane, ...] pict_type: int colorspace: int @@ -61,12 +61,13 @@ class VideoFrame(Frame): dst_color_range: int | str | None = None, ) -> VideoFrame: ... def to_rgb(self, **kwargs: Any) -> VideoFrame: ... - def to_image(self, **kwargs: Any) -> Image.Image: ... + def save(self, filepath: str | Path) -> None: ... + def to_image(self, **kwargs): ... def to_ndarray( self, channel_last: bool = False, **kwargs: Any ) -> _SupportedNDarray: ... @staticmethod - def from_image(img: Image.Image) -> VideoFrame: ... + def from_image(img): ... @staticmethod def from_numpy_buffer( array: _SupportedNDarray, format: str = "rgb24", width: int = 0 diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 677bc1fc1..9c9773ae8 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -7,16 +7,11 @@ import av from av import VideoFrame +from av.frame import Frame +from av.video.frame import supported_np_pix_fmts from av.video.reformatter import ColorRange, Colorspace, Interpolation -from .common import ( - TestCase, - assertImagesAlmostEqual, - assertNdarraysEqual, - fate_png, - fate_suite, - has_pillow, -) +from .common import TestCase, assertNdarraysEqual, fate_png, fate_suite def assertPixelValue16(plane, expected, byteorder: str) -> None: @@ -29,26 +24,6 @@ def assertPixelValue16(plane, expected, byteorder: str) -> None: assert view[1] == (expected >> 8 & 0xFF) -def test_opaque() -> None: - with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: - video_stream = container.streams.video[0] - - ctx = video_stream.codec_context - ctx.flags |= av.codec.context.Flags.copy_opaque - - assert video_stream.codec_context.copy_opaque - - for packet_idx, packet in enumerate(container.demux()): - packet.opaque = (time.time(), packet_idx) - for frame in packet.decode(): - assert isinstance(frame, av.frame.Frame) - - if frame.opaque is None: - continue - - assert type(frame.opaque) is tuple and len(frame.opaque) == 2 - - def test_frame_duration_matches_packet() -> None: with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: packet_durations = [ @@ -147,64 +122,24 @@ def test_memoryview_read() -> None: assert mem[:7] == b"0.234xx" -class TestVideoFrameImage(TestCase): - def setUp(self) -> None: - if not has_pillow: - pytest.skip() +def test_interpolation() -> None: + container = av.open(fate_png()) + for _ in container.decode(video=0): + frame = _ + break - def test_roundtrip(self) -> None: - import PIL.Image as Image + assert frame.width == 330 and frame.height == 330 - image = Image.open(fate_png()) - frame = VideoFrame.from_image(image) - img = frame.to_image() - img.save(self.sandboxed("roundtrip-high.jpg")) - assertImagesAlmostEqual(image, img) + img = frame.reformat(width=200, height=100, interpolation=Interpolation.BICUBIC) + assert img.width == 200 and img.height == 100 - def test_interpolation(self) -> None: - import PIL.Image as Image + img = frame.reformat(width=200, height=100, interpolation="BICUBIC") + assert img.width == 200 and img.height == 100 - image = Image.open(fate_png()) - frame = VideoFrame.from_image(image) - assert frame.width == 330 and frame.height == 330 - - img = frame.to_image(width=200, height=100, interpolation=Interpolation.BICUBIC) - assert img.width == 200 and img.height == 100 - - img = frame.to_image(width=200, height=100, interpolation="BICUBIC") - assert img.width == 200 and img.height == 100 - - img = frame.to_image( - width=200, height=100, interpolation=int(Interpolation.BICUBIC) - ) - assert img.width == 200 and img.height == 100 - - def test_to_image_rgb24(self) -> None: - sizes = [(318, 238), (320, 240), (500, 500)] - for width, height in sizes: - frame = VideoFrame(width, height, format="rgb24") - - # fill video frame data - for plane in frame.planes: - ba = bytearray(plane.buffer_size) - pos = 0 - for row in range(height): - for i in range(plane.line_size): - ba[pos] = i % 256 - pos += 1 - plane.update(ba) - - # construct expected image data - expected = bytearray(height * width * 3) - pos = 0 - for row in range(height): - for i in range(width * 3): - expected[pos] = i % 256 - pos += 1 - - img = frame.to_image() - assert img.size == (width, height) - assert img.tobytes() == expected + img = frame.reformat( + width=200, height=100, interpolation=int(Interpolation.BICUBIC) + ) + assert img.width == 200 and img.height == 100 def test_basic_to_ndarray() -> None: @@ -212,19 +147,11 @@ def test_basic_to_ndarray() -> None: assert array.shape == (480, 640, 3) -def test_to_image_with_dimensions() -> None: - if not has_pillow: - pytest.skip() - - img = VideoFrame(640, 480, format="rgb24").to_image(width=320, height=240) - assert img.size == (320, 240) - - def test_ndarray_gray() -> None: array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) for format in ("gray", "gray8"): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == "gray" assertNdarraysEqual(frame.to_ndarray(), array) @@ -234,17 +161,127 @@ def test_ndarray_gray_align() -> None: array = numpy.random.randint(0, 256, size=(238, 318), dtype=numpy.uint8) for format in ("gray", "gray8"): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 318 and frame.height == 238 assert frame.format.name == "gray" assertNdarraysEqual(frame.to_ndarray(), array) +def test_ndarray_gray9be() -> None: + array = numpy.random.randint(0, 512, size=(480, 640), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="gray9be") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "gray9be" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining value of first pixel + assertPixelValue16(frame.planes[0], array[0][0], "big") + + +def test_ndarray_gray9le() -> None: + array = numpy.random.randint(0, 512, size=(480, 640), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="gray9le") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "gray9le" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining value of first pixel + assertPixelValue16(frame.planes[0], array[0][0], "little") + + +def test_ndarray_gray10be() -> None: + array = numpy.random.randint(0, 1024, size=(480, 640), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="gray10be") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "gray10be" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining value of first pixel + assertPixelValue16(frame.planes[0], array[0][0], "big") + + +def test_ndarray_gray10le() -> None: + array = numpy.random.randint(0, 1024, size=(480, 640), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="gray10le") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "gray10le" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining value of first pixel + assertPixelValue16(frame.planes[0], array[0][0], "little") + + +def test_ndarray_gray12be() -> None: + array = numpy.random.randint(0, 4096, size=(480, 640), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="gray12be") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "gray12be" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining value of first pixel + assertPixelValue16(frame.planes[0], array[0][0], "big") + + +def test_ndarray_gray12le() -> None: + array = numpy.random.randint(0, 4096, size=(480, 640), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="gray12le") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "gray12le" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining value of first pixel + assertPixelValue16(frame.planes[0], array[0][0], "little") + + +def test_ndarray_gray14be() -> None: + array = numpy.random.randint(0, 16384, size=(480, 640), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="gray14be") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "gray14be" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining value of first pixel + assertPixelValue16(frame.planes[0], array[0][0], "big") + + +def test_ndarray_gray14le() -> None: + array = numpy.random.randint(0, 16384, size=(480, 640), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="gray14le") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "gray14le" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining value of first pixel + assertPixelValue16(frame.planes[0], array[0][0], "little") + + +def test_ndarray_gray16be() -> None: + array = numpy.random.randint(0, 65536, size=(480, 640), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="gray16be") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "gray16be" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining value of first pixel + assertPixelValue16(frame.planes[0], array[0][0], "big") + + +def test_ndarray_gray16le() -> None: + array = numpy.random.randint(0, 65536, size=(480, 640), dtype=numpy.uint16) + frame = VideoFrame.from_ndarray(array, format="gray16le") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "gray16le" + assertNdarraysEqual(frame.to_ndarray(), array) + + # check endianness by examining value of first pixel + assertPixelValue16(frame.planes[0], array[0][0], "little") + + def test_ndarray_grayf32() -> None: array = numpy.random.random_sample(size=(480, 640)).astype(numpy.float32) for format in ("grayf32be", "grayf32le"): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -254,7 +291,7 @@ def test_ndarray_grayf32_align() -> None: array = numpy.random.random_sample(size=(238, 318)).astype(numpy.float32) for format in ("grayf32be", "grayf32le"): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 318 and frame.height == 238 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -264,7 +301,7 @@ def test_ndarray_rgb() -> None: array = numpy.random.randint(0, 256, size=(480, 640, 3), dtype=numpy.uint8) for format in ("rgb24", "bgr24"): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -274,17 +311,26 @@ def test_ndarray_rgb_align() -> None: array = numpy.random.randint(0, 256, size=(238, 318, 3), dtype=numpy.uint8) for format in ("rgb24", "bgr24"): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 318 and frame.height == 238 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) +def test_ndarray_rgbf32() -> None: + array = numpy.random.random_sample(size=(480, 640, 3)).astype(numpy.float32) + for format in ("rgbf32be", "rgbf32le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + def test_ndarray_rgba() -> None: array = numpy.random.randint(0, 256, size=(480, 640, 4), dtype=numpy.uint8) for format in ("argb", "rgba", "abgr", "bgra"): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -294,7 +340,7 @@ def test_ndarray_rgba_align() -> None: array = numpy.random.randint(0, 256, size=(238, 318, 4), dtype=numpy.uint8) for format in ("argb", "rgba", "abgr", "bgra"): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 318 and frame.height == 238 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -304,7 +350,7 @@ def test_ndarray_bayer8() -> None: array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) for format in ("bayer_bggr8", "bayer_gbrg8", "bayer_grbg8", "bayer_rggb8"): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -323,12 +369,126 @@ def test_ndarray_bayer16() -> None: "bayer_rggb16le", ): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) +def test_ndarray_gbrap() -> None: + array = numpy.random.randint(0, 256, size=(480, 640, 4), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="gbrap") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "gbrap" + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrap_align() -> None: + array = numpy.random.randint(0, 256, size=(238, 318, 4), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="gbrap") + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == "gbrap" + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrap10() -> None: + array = numpy.random.randint(0, 1024, size=(480, 640, 4), dtype=numpy.uint16) + for format in ("gbrap10be", "gbrap10le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert format in supported_np_pix_fmts + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrap10_align() -> None: + array = numpy.random.randint(0, 1024, size=(238, 318, 4), dtype=numpy.uint16) + for format in ("gbrap10be", "gbrap10le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert format in supported_np_pix_fmts + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrap12() -> None: + array = numpy.random.randint(0, 4096, size=(480, 640, 4), dtype=numpy.uint16) + for format in ("gbrap12be", "gbrap12le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert format in supported_np_pix_fmts + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrap12_align() -> None: + array = numpy.random.randint(0, 4096, size=(238, 318, 4), dtype=numpy.uint16) + for format in ("gbrap12be", "gbrap12le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert format in supported_np_pix_fmts + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrap14() -> None: + array = numpy.random.randint(0, 16384, size=(480, 640, 4), dtype=numpy.uint16) + for format in ("gbrap14be", "gbrap14le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert format in supported_np_pix_fmts + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrap14_align() -> None: + array = numpy.random.randint(0, 16384, size=(238, 318, 4), dtype=numpy.uint16) + for format in ("gbrap14be", "gbrap14le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert format in supported_np_pix_fmts + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrap16() -> None: + array = numpy.random.randint(0, 65536, size=(480, 640, 4), dtype=numpy.uint16) + for format in ("gbrap16be", "gbrap16le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert format in supported_np_pix_fmts + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrap16_align() -> None: + array = numpy.random.randint(0, 65536, size=(238, 318, 4), dtype=numpy.uint16) + for format in ("gbrap16be", "gbrap16le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert format in supported_np_pix_fmts + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrapf32() -> None: + array = numpy.random.random_sample(size=(480, 640, 4)).astype(numpy.float32) + for format in ("gbrapf32be", "gbrapf32le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrapf32_align() -> None: + array = numpy.random.random_sample(size=(238, 318, 4)).astype(numpy.float32) + for format in ("gbrapf32be", "gbrapf32le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + def test_ndarray_gbrp() -> None: array = numpy.random.randint(0, 256, size=(480, 640, 3), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="gbrp") @@ -345,11 +505,31 @@ def test_ndarray_gbrp_align() -> None: assertNdarraysEqual(frame.to_ndarray(), array) +def test_ndarray_gbrp9() -> None: + array = numpy.random.randint(0, 512, size=(480, 640, 3), dtype=numpy.uint16) + for format in ("gbrp9be", "gbrp9le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert format in supported_np_pix_fmts + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_gbrp9_align() -> None: + array = numpy.random.randint(0, 512, size=(238, 318, 3), dtype=numpy.uint16) + for format in ("gbrp9be", "gbrp9le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert format in supported_np_pix_fmts + assert frame.width == 318 and frame.height == 238 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + def test_ndarray_gbrp10() -> None: array = numpy.random.randint(0, 1024, size=(480, 640, 3), dtype=numpy.uint16) for format in ("gbrp10be", "gbrp10le"): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -359,7 +539,7 @@ def test_ndarray_gbrp10_align() -> None: array = numpy.random.randint(0, 1024, size=(238, 318, 3), dtype=numpy.uint16) for format in ("gbrp10be", "gbrp10le"): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 318 and frame.height == 238 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -369,7 +549,7 @@ def test_ndarray_gbrp12() -> None: array = numpy.random.randint(0, 4096, size=(480, 640, 3), dtype=numpy.uint16) for format in ("gbrp12be", "gbrp12le"): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -379,7 +559,7 @@ def test_ndarray_gbrp12_align() -> None: array = numpy.random.randint(0, 4096, size=(238, 318, 3), dtype=numpy.uint16) for format in ("gbrp12be", "gbrp12le"): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 318 and frame.height == 238 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -389,7 +569,7 @@ def test_ndarray_gbrp14() -> None: array = numpy.random.randint(0, 16384, size=(480, 640, 3), dtype=numpy.uint16) for format in ("gbrp14be", "gbrp14le"): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -399,7 +579,7 @@ def test_ndarray_gbrp14_align() -> None: array = numpy.random.randint(0, 16384, size=(238, 318, 3), dtype=numpy.uint16) for format in ("gbrp14be", "gbrp14le"): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 318 and frame.height == 238 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -409,7 +589,7 @@ def test_ndarray_gbrp16() -> None: array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) for format in ("gbrp16be", "gbrp16le"): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 640 and frame.height == 480 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -419,7 +599,7 @@ def test_ndarray_gbrp16_align() -> None: array = numpy.random.randint(0, 65536, size=(238, 318, 3), dtype=numpy.uint16) for format in ("gbrp16be", "gbrp16le"): frame = VideoFrame.from_ndarray(array, format=format) - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts assert frame.width == 318 and frame.height == 238 assert frame.format.name == format assertNdarraysEqual(frame.to_ndarray(), array) @@ -443,24 +623,6 @@ def test_ndarray_gbrpf32_align() -> None: assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_gbrapf32() -> None: - array = numpy.random.random_sample(size=(480, 640, 4)).astype(numpy.float32) - for format in ("gbrapf32be", "gbrapf32le"): - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 640 and frame.height == 480 - assert frame.format.name == format - assertNdarraysEqual(frame.to_ndarray(), array) - - -def test_ndarray_gbrapf32_allign() -> None: - array = numpy.random.random_sample(size=(238, 318, 4)).astype(numpy.float32) - for format in ("gbrapf32be", "gbrapf32le"): - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 318 and frame.height == 238 - assert frame.format.name == format - assertNdarraysEqual(frame.to_ndarray(), array) - - def test_ndarray_yuv420p() -> None: array = numpy.random.randint(0, 256, size=(720, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuv420p") @@ -529,22 +691,13 @@ def test_ndarray_yuv444p16() -> None: assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_yuv420p10le() -> None: - array = numpy.random.randint(0, 65536, size=(3, 480, 640), dtype=numpy.uint16) - for format in ("yuv420p10le",): - frame = VideoFrame.from_ndarray(array, format=format) - assert frame.width == 640 and frame.height == 480 - assert frame.format.name == format - assert format in av.video.frame.supported_np_pix_fmts - - def test_ndarray_yuv422p10le() -> None: array = numpy.random.randint(0, 65536, size=(3, 480, 640), dtype=numpy.uint16) for format in ("yuv422p10le",): frame = VideoFrame.from_ndarray(array, format=format) assert frame.width == 640 and frame.height == 480 assert frame.format.name == format - assert format in av.video.frame.supported_np_pix_fmts + assert format in supported_np_pix_fmts def test_ndarray_yuv444p16_align() -> None: @@ -582,28 +735,6 @@ def test_ndarray_yuyv422_align() -> None: assertNdarraysEqual(frame.to_ndarray(), array) -def test_ndarray_gray16be() -> None: - array = numpy.random.randint(0, 65536, size=(480, 640), dtype=numpy.uint16) - frame = VideoFrame.from_ndarray(array, format="gray16be") - assert frame.width == 640 and frame.height == 480 - assert frame.format.name == "gray16be" - assertNdarraysEqual(frame.to_ndarray(), array) - - # check endianness by examining value of first pixel - assertPixelValue16(frame.planes[0], array[0][0], "big") - - -def test_ndarray_gray16le() -> None: - array = numpy.random.randint(0, 65536, size=(480, 640), dtype=numpy.uint16) - frame = VideoFrame.from_ndarray(array, format="gray16le") - assert frame.width == 640 and frame.height == 480 - assert frame.format.name == "gray16le" - assertNdarraysEqual(frame.to_ndarray(), array) - - # check endianness by examining value of first pixel - assertPixelValue16(frame.planes[0], array[0][0], "little") - - def test_ndarray_rgb48be() -> None: array = numpy.random.randint(0, 65536, size=(480, 640, 3), dtype=numpy.uint16) frame = VideoFrame.from_ndarray(array, format="rgb48be") @@ -714,6 +845,24 @@ def test_ndarray_bgra64le() -> None: assertPixelValue16(frame.planes[0], array[0][0][0], "little") +def test_ndarray_rgbaf16() -> None: + array = numpy.random.random_sample(size=(480, 640, 4)).astype(numpy.float16) + for format in ("rgbaf16be", "rgbaf16le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + +def test_ndarray_rgbaf32() -> None: + array = numpy.random.random_sample(size=(480, 640, 4)).astype(numpy.float32) + for format in ("rgbaf32be", "rgbaf32le"): + frame = VideoFrame.from_ndarray(array, format=format) + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == format + assertNdarraysEqual(frame.to_ndarray(), array) + + def test_ndarray_rgb8() -> None: array = numpy.random.randint(0, 256, size=(480, 640), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="rgb8") @@ -736,7 +885,7 @@ def test_ndarray_pal8(): frame = VideoFrame.from_ndarray((array, palette), format="pal8") assert frame.width == 640 and frame.height == 480 assert frame.format.name == "pal8" - assert frame.format.name in av.video.frame.supported_np_pix_fmts + assert frame.format.name in supported_np_pix_fmts returned = frame.to_ndarray() assert type(returned) is tuple and len(returned) == 2 assertNdarraysEqual(returned[0], array) @@ -748,7 +897,7 @@ def test_ndarray_nv12() -> None: frame = VideoFrame.from_ndarray(array, format="nv12") assert frame.width == 640 and frame.height == 480 assert frame.format.name == "nv12" - assert frame.format.name in av.video.frame.supported_np_pix_fmts + assert frame.format.name in supported_np_pix_fmts assertNdarraysEqual(frame.to_ndarray(), array) From ab4e548ba67a52515658257de3184de04a02c569 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 23 Jun 2025 22:46:37 -0400 Subject: [PATCH 285/365] Use mypy 1.16.1 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 4b4f58ab7..135d091ad 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ fate-suite: rsync -vrltLW rsync://fate-suite.ffmpeg.org/fate-suite/ tests/assets/fate-suite/ lint: - $(PIP) install -U ruff isort pillow numpy mypy==1.15.0 pytest + $(PIP) install -U ruff isort pillow numpy mypy==1.16.1 pytest ruff format --check av examples tests setup.py isort --check-only --diff av examples tests mypy av tests From a92b91c7837daa219f42191e67ebd4949d5edc38 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 25 Jun 2025 22:38:30 -0400 Subject: [PATCH 286/365] PyAV-ffmpeg 7.1.1-5 Deleting `test_writing_to_custom_io_dash` because it depends on libxml2, an insecure library. --- scripts/build-deps | 2 +- scripts/ffmpeg-7.1.json | 2 +- tests/test_python_io.py | 36 ------------------------------------ 3 files changed, 2 insertions(+), 38 deletions(-) diff --git a/scripts/build-deps b/scripts/build-deps index d278c27c0..f34ed5a5b 100755 --- a/scripts/build-deps +++ b/scripts/build-deps @@ -57,11 +57,11 @@ echo ./configure --disable-doc \ --disable-static \ --disable-stripping \ + --disable-libxml2 \ --enable-debug=3 \ --enable-gpl \ --enable-version3 \ --enable-libx264 \ - --enable-libxml2 \ --enable-shared \ --enable-sse \ --enable-avx \ diff --git a/scripts/ffmpeg-7.1.json b/scripts/ffmpeg-7.1.json index 26641cedc..8a606aea3 100644 --- a/scripts/ffmpeg-7.1.json +++ b/scripts/ffmpeg-7.1.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.1.1-4/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.1.1-5/ffmpeg-{platform}.tar.gz" } diff --git a/tests/test_python_io.py b/tests/test_python_io.py index 3169ae047..5a4d1becf 100644 --- a/tests/test_python_io.py +++ b/tests/test_python_io.py @@ -237,42 +237,6 @@ def test_writing_to_buffer_broken_with_close(self) -> None: with pytest.raises(OSError): container.close() - @run_in_sandbox - def test_writing_to_custom_io_dash(self) -> None: - # Custom I/O that opens file and logs calls - wrapped_custom_io = CustomIOLogger() - - output_filename = "custom_io_output.mpd" - - # Write a DASH package using the custom IO. Prefix the name with CUSTOM_IO_PROTOCOL to - # avoid temporary file and renaming. - with av.open( - CUSTOM_IO_PROTOCOL + output_filename, "w", io_open=wrapped_custom_io - ) as container: - write_rgb_rotate(container) - - # Check that at least 3 files were opened using the custom IO: - # "output_filename", init-stream0.m4s and chunk-stream-0x.m4s - assert len(wrapped_custom_io._log) >= 3 - assert len(wrapped_custom_io._method_log) >= 3 - - # Check that all files were written to - all_write = all( - method_log._filter("write") for method_log in wrapped_custom_io._method_log - ) - assert all_write - - # Check that all files were closed - all_closed = all( - method_log._filter("close") for method_log in wrapped_custom_io._method_log - ) - assert all_closed - - # Check contents. - # Note that the dash demuxer doesn't support custom I/O. - with av.open(output_filename, "r") as container: - assert_rgb_rotate(self, container, is_dash=True) - def test_writing_to_custom_io_image2(self) -> None: if not has_pillow: pytest.skip() From e343eae6045f5ca6b9a0f0bccb7537a2244159aa Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 26 Jun 2025 00:21:48 -0400 Subject: [PATCH 287/365] Get rid of setup.py warning --- setup.py | 33 ++++++++------------------------- 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/setup.py b/setup.py index d5ad4fb31..16ca7f59b 100644 --- a/setup.py +++ b/setup.py @@ -6,30 +6,19 @@ import shlex import subprocess import sys -from time import sleep + +if platform.system() == "Darwin": + major_version = int(platform.mac_ver()[0].split(".")[0]) + if major_version < 12: + raise OSError("You are using an EOL, unsupported, and out-of-date OS.") def is_virtualenv(): return sys.base_prefix != sys.prefix -if platform.system() == "Darwin": - major_version = int(platform.mac_ver()[0].split(".")[0]) - if major_version < 12: - print( - "\033[1;91mWarning!\033[0m You are using an EOL, unsupported, and out-of-date OS." - ) - sleep(3) - -print( - "\n\033[1;91mWarning!\033[0m You are installing from source.\n" - "It is \033[1;37mEXPECTED\033[0m that it will fail. You are \033[1;37mREQUIRED\033[0m" - " to use ffmpeg 7.\nYou \033[1;37mMUST\033[0m have Cython, pkg-config, and a C compiler.\n" -) -if os.getenv("GITHUB_ACTIONS") == "true" or is_virtualenv(): - pass -else: - print("\033[1;91mWarning!\033[0m You are not using a virtual environment") +if not (os.getenv("GITHUB_ACTIONS") == "true" or is_virtualenv()): + raise ValueError("You are not using a virtual environment") from Cython.Build import cythonize @@ -159,16 +148,10 @@ def parse_cflags(raw_flags): FFMPEG_DIR = arg.split("=")[1] del sys.argv[i] -# Do not cythonize or use pkg-config when cleaning. -use_pkg_config = platform.system() != "Windows" -if len(sys.argv) > 1 and sys.argv[1] == "clean": - cythonize = lambda ext, **kwargs: [ext] - use_pkg_config = False - # Locate ffmpeg libraries and headers. if FFMPEG_DIR is not None: extension_extra = get_config_from_directory(FFMPEG_DIR) -elif use_pkg_config: +elif platform.system() != "Windows": extension_extra = get_config_from_pkg_config() else: extension_extra = { From 77bb0ef18dff2423ae6622f7856d04336554125d Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 26 Jun 2025 00:30:58 -0400 Subject: [PATCH 288/365] Fix this warning: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit src/av/video/frame.c: In function ‘__pyx_f_2av_5video_5frame_copy_bytes_to_plane’: src/av/video/frame.c:18808:41: warning: comparison of integer expressions of different signedness: ‘int’ and ‘unsigned int’ [-Wsign-compare] 18808 | for (__pyx_t_15 = 0; __pyx_t_15 < __pyx_t_16; __pyx_t_15+=1) { | --- av/video/frame.py | 1 - 1 file changed, 1 deletion(-) diff --git a/av/video/frame.py b/av/video/frame.py index bc4bf9154..970ecc4b8 100644 --- a/av/video/frame.py +++ b/av/video/frame.py @@ -155,7 +155,6 @@ def copy_bytes_to_plane( end_row = plane.height step = 1 - i, j = cython.declare(cython.int) for row in range(start_row, end_row, step): i_pos = row * i_stride if flip_horizontal: From f964e7a3664cd6f443e0dbc50aa1067bd97d19b9 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 26 Jun 2025 00:49:41 -0400 Subject: [PATCH 289/365] Update AUTHORS.rst --- AUTHORS.rst | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index df46c6c11..b85c8ce1b 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -23,10 +23,11 @@ All contributors (by number of commits): - Alba Mendez - Xinran Xu ; `@xxr3376 `_ +- z-khan +- Marc Mueller <30130371+cdce8p@users.noreply.github.com> - Dan Allan ; `@danielballan `_ - Moonsik Park - Santtu Keskinen -- Marc Mueller <30130371+cdce8p@users.noreply.github.com> - Christoph Rackwitz - David Plowman - Alireza Davoudi ; `@adavoudi `_ @@ -38,8 +39,8 @@ All contributors (by number of commits): - Ulrik Mikaelsson ; `@rawler `_ - Wel C. van der - Will Patera ; `@willpatera `_ -- z-khan +* zzjjbb <31069326+zzjjbb@users.noreply.github.com> * Joe Schiff <41972063+JoeSchiff@users.noreply.github.com> * Dexer <73297572+DexerBR@users.noreply.github.com> * rutsh @@ -59,7 +60,6 @@ All contributors (by number of commits): * Radek Senfeld ; `@radek-senfeld `_ * robinechuca * Benjamin Chrétien <2742231+bchretien@users.noreply.github.com> -* zzjjbb <31069326+zzjjbb@users.noreply.github.com> * davidplowman <38045873+davidplowman@users.noreply.github.com> * Hanz <40712686+HanzCEO@users.noreply.github.com> * Artturin @@ -81,12 +81,14 @@ All contributors (by number of commits): * Marcell Pardavi * Matteo Destro * Mattias Wadman +* Max Ehrlich * Manuel Goacolou * Julian Schweizer * Nikhil Idiculla * Ömer Sezgin Uğurlu * Orivej Desh * Philipp Krähenbühl +* Mattia Procopio * ramoncaldeira * Roland van Laar * Santiago Castro @@ -106,3 +108,4 @@ All contributors (by number of commits): * Koichi Akabe * David Joy * Sviatoslav Sydorenko (Святослав Сидоренко) +* Jiabei Zhu From d6ae316897e9712f3d68e1055b510af0f51a37c0 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 26 Jun 2025 01:56:33 -0400 Subject: [PATCH 290/365] Consolidate stream classes into single module - Move AttachmentStream and DataStream from separate modules into av/stream.py - Convert stream.pyx from Cython to pure Python - Add missing 'options' property to Stream type hints This consolidation reduces code duplication and simplifies the module structure without changing the public API. --- av/attachments/__init__.py | 0 av/attachments/stream.pxd | 5 -- av/attachments/stream.pyi | 8 --- av/attachments/stream.pyx | 26 ------- av/container/output.pyi | 2 +- av/container/streams.pyi | 4 +- av/data/__init__.pxd | 0 av/data/__init__.py | 0 av/data/stream.pxd | 5 -- av/data/stream.pyi | 6 -- av/data/stream.pyx | 16 ----- av/stream.pxd | 8 ++- av/{stream.pyx => stream.py} | 127 ++++++++++++++++++++++++----------- av/stream.pyi | 12 +++- 14 files changed, 108 insertions(+), 111 deletions(-) delete mode 100644 av/attachments/__init__.py delete mode 100644 av/attachments/stream.pxd delete mode 100644 av/attachments/stream.pyi delete mode 100644 av/attachments/stream.pyx delete mode 100644 av/data/__init__.pxd delete mode 100644 av/data/__init__.py delete mode 100644 av/data/stream.pxd delete mode 100644 av/data/stream.pyi delete mode 100644 av/data/stream.pyx rename av/{stream.pyx => stream.py} (72%) diff --git a/av/attachments/__init__.py b/av/attachments/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/av/attachments/stream.pxd b/av/attachments/stream.pxd deleted file mode 100644 index 81f788b77..000000000 --- a/av/attachments/stream.pxd +++ /dev/null @@ -1,5 +0,0 @@ -from av.stream cimport Stream - - -cdef class AttachmentStream(Stream): - pass diff --git a/av/attachments/stream.pyi b/av/attachments/stream.pyi deleted file mode 100644 index 3d660e4a0..000000000 --- a/av/attachments/stream.pyi +++ /dev/null @@ -1,8 +0,0 @@ -from typing import Literal - -from av.stream import Stream - -class AttachmentStream(Stream): - type: Literal["attachment"] - @property - def mimetype(self) -> str | None: ... diff --git a/av/attachments/stream.pyx b/av/attachments/stream.pyx deleted file mode 100644 index de7d10119..000000000 --- a/av/attachments/stream.pyx +++ /dev/null @@ -1,26 +0,0 @@ -from av.stream cimport Stream - - -cdef class AttachmentStream(Stream): - """ - An :class:`AttachmentStream` represents a stream of attachment data within a media container. - Typically used to attach font files that are referenced in ASS/SSA Subtitle Streams. - """ - - @property - def name(self): - """ - Returns the file name of the attachment. - - :rtype: str | None - """ - return self.metadata.get("filename") - - @property - def mimetype(self): - """ - Returns the MIME type of the attachment. - - :rtype: str | None - """ - return self.metadata.get("mimetype") diff --git a/av/container/output.pyi b/av/container/output.pyi index 568345cd2..b370095de 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -3,8 +3,8 @@ from typing import Sequence, TypeVar, Union, overload from av.audio import _AudioCodecName from av.audio.stream import AudioStream -from av.data.stream import DataStream from av.packet import Packet +from av.stream import DataStream from av.subtitles.stream import SubtitleStream from av.video import _VideoCodecName from av.video.stream import VideoStream diff --git a/av/container/streams.pyi b/av/container/streams.pyi index fbaf1b67f..52b98818f 100644 --- a/av/container/streams.pyi +++ b/av/container/streams.pyi @@ -1,9 +1,7 @@ from typing import Iterator, Literal, overload -from av.attachments.stream import AttachmentStream from av.audio.stream import AudioStream -from av.data.stream import DataStream -from av.stream import Stream +from av.stream import AttachmentStream, DataStream, Stream from av.subtitles.stream import SubtitleStream from av.video.stream import VideoStream diff --git a/av/data/__init__.pxd b/av/data/__init__.pxd deleted file mode 100644 index e69de29bb..000000000 diff --git a/av/data/__init__.py b/av/data/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/av/data/stream.pxd b/av/data/stream.pxd deleted file mode 100644 index 012792a4a..000000000 --- a/av/data/stream.pxd +++ /dev/null @@ -1,5 +0,0 @@ -from av.stream cimport Stream - - -cdef class DataStream(Stream): - pass diff --git a/av/data/stream.pyi b/av/data/stream.pyi deleted file mode 100644 index 45a669d4f..000000000 --- a/av/data/stream.pyi +++ /dev/null @@ -1,6 +0,0 @@ -from av.frame import Frame -from av.packet import Packet -from av.stream import Stream - -class DataStream(Stream): - name: str | None diff --git a/av/data/stream.pyx b/av/data/stream.pyx deleted file mode 100644 index 4cc957762..000000000 --- a/av/data/stream.pyx +++ /dev/null @@ -1,16 +0,0 @@ -cimport libav as lib - - -cdef class DataStream(Stream): - def __repr__(self): - return ( - f"'} at 0x{id(self):x}>" - ) - - @property - def name(self): - cdef const lib.AVCodecDescriptor *desc = lib.avcodec_descriptor_get(self.ptr.codecpar.codec_id) - if desc == NULL: - return None - return desc.name diff --git a/av/stream.pxd b/av/stream.pxd index c847f641e..9aa6616e5 100644 --- a/av/stream.pxd +++ b/av/stream.pxd @@ -19,8 +19,14 @@ cdef class Stream: # Private API. cdef _init(self, Container, lib.AVStream*, CodecContext) cdef _finalize_for_output(self) - cdef _set_time_base(self, value) cdef _set_id(self, value) cdef Stream wrap_stream(Container, lib.AVStream*, CodecContext) + + +cdef class DataStream(Stream): + pass + +cdef class AttachmentStream(Stream): + pass diff --git a/av/stream.pyx b/av/stream.py similarity index 72% rename from av/stream.pyx rename to av/stream.py index 90f10d038..b5c22588b 100644 --- a/av/stream.pyx +++ b/av/stream.py @@ -1,10 +1,10 @@ -cimport libav as lib - from enum import Flag -from av.error cimport err_check -from av.packet cimport Packet -from av.utils cimport ( +import cython +from cython.cimports import libav as lib +from cython.cimports.av.error import err_check +from cython.cimports.av.packet import Packet +from cython.cimports.av.utils import ( avdict_to_dict, avrational_to_fraction, dict_to_avdict, @@ -34,35 +34,38 @@ class Disposition(Flag): multilayer = 1 << 21 -cdef object _cinit_bypass_sentinel = object() +_cinit_bypass_sentinel = cython.declare(object, object()) -cdef Stream wrap_stream(Container container, lib.AVStream *c_stream, CodecContext codec_context): - """Build an av.Stream for an existing AVStream. - The AVStream MUST be fully constructed and ready for use before this is - called. +@cython.cfunc +def wrap_stream( + container: Container, + c_stream: cython.pointer[lib.AVStream], + codec_context: CodecContext, +) -> Stream: + """Build an av.Stream for an existing AVStream. + The AVStream MUST be fully constructed and ready for use before this is called. """ # This better be the right one... assert container.ptr.streams[c_stream.index] == c_stream - cdef Stream py_stream + py_stream: Stream + + from av.audio.stream import AudioStream + from av.subtitles.stream import SubtitleStream + from av.video.stream import VideoStream if c_stream.codecpar.codec_type == lib.AVMEDIA_TYPE_VIDEO: - from av.video.stream import VideoStream py_stream = VideoStream.__new__(VideoStream, _cinit_bypass_sentinel) elif c_stream.codecpar.codec_type == lib.AVMEDIA_TYPE_AUDIO: - from av.audio.stream import AudioStream py_stream = AudioStream.__new__(AudioStream, _cinit_bypass_sentinel) elif c_stream.codecpar.codec_type == lib.AVMEDIA_TYPE_SUBTITLE: - from av.subtitles.stream import SubtitleStream py_stream = SubtitleStream.__new__(SubtitleStream, _cinit_bypass_sentinel) elif c_stream.codecpar.codec_type == lib.AVMEDIA_TYPE_ATTACHMENT: - from av.attachments.stream import AttachmentStream py_stream = AttachmentStream.__new__(AttachmentStream, _cinit_bypass_sentinel) elif c_stream.codecpar.codec_type == lib.AVMEDIA_TYPE_DATA: - from av.data.stream import DataStream py_stream = DataStream.__new__(DataStream, _cinit_bypass_sentinel) else: py_stream = Stream.__new__(Stream, _cinit_bypass_sentinel) @@ -71,7 +74,8 @@ class Disposition(Flag): return py_stream -cdef class Stream: +@cython.cclass +class Stream: """ A single stream of audio, video or subtitles within a :class:`.Container`. @@ -93,7 +97,13 @@ def __cinit__(self, name): return raise RuntimeError("cannot manually instantiate Stream") - cdef _init(self, Container container, lib.AVStream *stream, CodecContext codec_context): + @cython.cfunc + def _init( + self, + container: Container, + stream: cython.pointer[lib.AVStream], + codec_context: CodecContext, + ): self.container = container self.ptr = stream @@ -121,18 +131,19 @@ def __setattr__(self, name, value): if name == "disposition": self.ptr.disposition = value return + if name == "time_base": + to_avrational(value, cython.address(self.ptr.time_base)) + return # Convenience setter for codec context properties. if self.codec_context is not None: setattr(self.codec_context, name, value) - if name == "time_base": - self._set_time_base(value) - - cdef _finalize_for_output(self): - + @cython.cfunc + def _finalize_for_output(self): dict_to_avdict( - &self.ptr.metadata, self.metadata, + cython.address(self.ptr.metadata), + self.metadata, encoding=self.container.metadata_encoding, errors=self.container.metadata_errors, ) @@ -140,9 +151,12 @@ def __setattr__(self, name, value): if not self.ptr.time_base.num: self.ptr.time_base = self.codec_context.ptr.time_base - # It prefers if we pass it parameters via this other object. - # Lets just copy what we want. - err_check(lib.avcodec_parameters_from_context(self.ptr.codecpar, self.codec_context.ptr)) + # It prefers if we pass it parameters via this other object. Let's just copy what we want. + err_check( + lib.avcodec_parameters_from_context( + self.ptr.codecpar, self.codec_context.ptr + ) + ) @property def id(self): @@ -154,10 +168,8 @@ def id(self): """ return self.ptr.id - cdef _set_id(self, value): - """ - Setter used by __setattr__ for the id property. - """ + @cython.cfunc + def _set_id(self, value): if value is None: self.ptr.id = 0 else: @@ -196,7 +208,6 @@ def index(self): """ return self.ptr.index - @property def time_base(self): """ @@ -205,13 +216,7 @@ def time_base(self): :type: fractions.Fraction | None """ - return avrational_to_fraction(&self.ptr.time_base) - - cdef _set_time_base(self, value): - """ - Setter used by __setattr__ for the time_base property. - """ - to_avrational(value, &self.ptr.time_base) + return avrational_to_fraction(cython.address(self.ptr.time_base)) @property def start_time(self): @@ -267,3 +272,47 @@ def type(self): :type: Literal["audio", "video", "subtitle", "data", "attachment"] """ return lib.av_get_media_type_string(self.ptr.codecpar.codec_type) + + +@cython.cclass +class DataStream(Stream): + def __repr__(self): + return ( + f"'} at 0x{id(self):x}>" + ) + + @property + def name(self): + desc: cython.pointer[cython.const[lib.AVCodecDescriptor]] = ( + lib.avcodec_descriptor_get(self.ptr.codecpar.codec_id) + ) + if desc == cython.NULL: + return None + return desc.name + + +@cython.cclass +class AttachmentStream(Stream): + """ + An :class:`AttachmentStream` represents a stream of attachment data within a media container. + Typically used to attach font files that are referenced in ASS/SSA Subtitle Streams. + """ + + @property + def name(self): + """ + Returns the file name of the attachment. + + :rtype: str | None + """ + return self.metadata.get("filename") + + @property + def mimetype(self): + """ + Returns the MIME type of the attachment. + + :rtype: str | None + """ + return self.metadata.get("mimetype") diff --git a/av/stream.pyi b/av/stream.pyi index 88dc7c00b..18e6fdca9 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -1,6 +1,6 @@ from enum import Flag from fractions import Fraction -from typing import Literal, cast +from typing import Any, Literal, cast from .codec import Codec, CodecContext from .container import Container @@ -36,6 +36,7 @@ class Stream: profiles: list[str] profile: str | None index: int + options: dict[str, Any] time_base: Fraction | None average_rate: Fraction | None base_rate: Fraction | None @@ -46,3 +47,12 @@ class Stream: frames: int language: str | None type: Literal["video", "audio", "data", "subtitle", "attachment"] + +class DataStream(Stream): + type: Literal["data"] + name: str | None + +class AttachmentStream(Stream): + type: Literal["attachment"] + @property + def mimetype(self) -> str | None: ... From 8346c20885c3555d056772845d0e302311a2cbb1 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 26 Jun 2025 14:53:45 -0400 Subject: [PATCH 291/365] Fix grammar issues --- av/logging.pyx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/av/logging.pyx b/av/logging.pyx index 9cb232d2a..30a253fe0 100644 --- a/av/logging.pyx +++ b/av/logging.pyx @@ -1,6 +1,6 @@ """ -FFmpeg has a logging system that it uses extensively. It's very noisy so PyAV turns it -off by default. This, unfortunately has the effect of making raised errors having less +FFmpeg has a logging system that it uses extensively. It's very noisy, so PyAV turns it +off by default. This unfortunately has the effect of making raised errors have less detailed messages. It's therefore recommended to use VERBOSE when developing. .. _enable_logging: @@ -8,7 +8,7 @@ detailed messages. It's therefore recommended to use VERBOSE when developing. Enabling Logging ~~~~~~~~~~~~~~~~~ -You can hook the logging system with Python by setting the log level:: +You can hook into the logging system with Python by setting the log level:: import av @@ -25,10 +25,10 @@ quickly with:: logging.basicConfig() -Note that handling logs with Python sometimes doesn't play nice multi-threads workflows. +Note that handling logs with Python sometimes doesn't play nicely with multi-threaded workflows. An alternative is :func:`restore_default_callback`. -This will restores FFmpeg's logging default system, which prints to the terminal. +This restores FFmpeg's default logging system, which prints to the terminal. Like with setting the log level to ``None``, this may also result in raised errors having less detailed messages. From bc95b9ef38e29de0982f400b4b4b8d90278291f6 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 26 Jun 2025 16:41:09 -0400 Subject: [PATCH 292/365] Release v15 Release Candidate 1 --- CHANGELOG.rst | 20 +++++++++++++++++--- av/about.py | 2 +- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3a69a86ab..e3cb6ecf0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,12 +16,26 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. -v15.0.0 (Unreleased) --------------------- +v15.0.0rc1 +---------- Major: -- Make ``SubtitleStream.decode()`` return the list of subtitles directly, without the intermediate ``SubtitleSet``. +- Make ``SubtitleStream.decode()`` return the list of subtitles directly, making the API nicer. +- Binary wheels: Remove libxml2 because of security concerns. +- Binary wheels: Remove xz because lzma encoding is very niche in media decoding and lesser security concerns. + +Features: + +- Add GBRP, GBRAP, RGBA formats by :gh-user:`z-khan`. +- Add ``VideoFrame.save()``, a convenient way to save frames to PNGs or JPEGs, by :gh-user:`WyattBlue`. + +Fixes: + +- Binary wheels: Re-enable libx265, use version 4.1 by :gh-user:`WyattBlue`. +- Fix memory error for AudioFifo properties by :gh-user:`zzjjbb`. +- Copy template flags if creating a new Codec object by :gh-user:`WyattBlue` +- ``AudioFrame.from_ndarray()``: Support python classes for parameters too, by :gh-user:`WyattBlue`. v14.4.0 ------- diff --git a/av/about.py b/av/about.py index 15fd90e44..2607c1260 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "14.4.0" +__version__ = "15.0.0rc1" From d2e95a184b3e452d2e16fe61bf38aa0e2f06c643 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 27 Jun 2025 21:33:59 -0400 Subject: [PATCH 293/365] Any -> object --- av/stream.pyi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/av/stream.pyi b/av/stream.pyi index 18e6fdca9..e6188c7b0 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -1,6 +1,6 @@ from enum import Flag from fractions import Fraction -from typing import Any, Literal, cast +from typing import Literal, cast from .codec import Codec, CodecContext from .container import Container @@ -36,7 +36,7 @@ class Stream: profiles: list[str] profile: str | None index: int - options: dict[str, Any] + options: dict[str, object] time_base: Fraction | None average_rate: Fraction | None base_rate: Fraction | None From 9d223f176e1558085fd4aea91fd99fa64eb5e960 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 27 Jun 2025 21:44:21 -0400 Subject: [PATCH 294/365] Add annotations to encode_frames_with_qminmax --- av/stream.pyi | 3 +++ tests/test_encode.py | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/av/stream.pyi b/av/stream.pyi index e6188c7b0..8289a6358 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -48,6 +48,9 @@ class Stream: language: str | None type: Literal["video", "audio", "data", "subtitle", "attachment"] + # From context + codec_tag: str + class DataStream(Stream): type: Literal["data"] name: str | None diff --git a/tests/test_encode.py b/tests/test_encode.py index 23cbdc275..9284a9c82 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import io import math from fractions import Fraction @@ -409,7 +411,9 @@ def test_max_b_frames(self) -> None: assert actual_max_b_frames <= max_b_frames -def encode_frames_with_qminmax(frames: list, shape: tuple, qminmax: tuple) -> int: +def encode_frames_with_qminmax( + frames: list[VideoFrame], shape: tuple[int, int, int], qminmax: tuple[int, int] +) -> int: """ Encode a video with the given quantiser limits, and return how many enocded bytes we made in total. From ca2c8c84a22adab80b6baa5dfc266777c613aa2e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 27 Jun 2025 22:48:28 -0400 Subject: [PATCH 295/365] Drop support for MacOS 12 or under --- .github/workflows/tests.yml | 2 +- CHANGELOG.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a38af2c08..1de9f0036 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -53,7 +53,7 @@ jobs: - name: Set Minimum MacOS Target if: matrix.os == 'macos-13' || matrix.os == 'macos-14' run: | - echo "MACOSX_DEPLOYMENT_TARGET=12.0" >> $GITHUB_ENV + echo "MACOSX_DEPLOYMENT_TARGET=13.0" >> $GITHUB_ENV - name: Build wheels env: CIBW_ARCHS: ${{ matrix.arch }} diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e3cb6ecf0..b5ee063d7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -22,6 +22,7 @@ v15.0.0rc1 Major: - Make ``SubtitleStream.decode()`` return the list of subtitles directly, making the API nicer. +- Drop support for MacOS 12 or under. - Binary wheels: Remove libxml2 because of security concerns. - Binary wheels: Remove xz because lzma encoding is very niche in media decoding and lesser security concerns. From 3ebe7d25a82a80811145d0552835f04d8eba2ede Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 3 Jul 2025 01:02:31 -0400 Subject: [PATCH 296/365] pyav-ffmpeg 7.1.1-6 --- scripts/ffmpeg-7.1.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ffmpeg-7.1.json b/scripts/ffmpeg-7.1.json index 8a606aea3..82ee313d6 100644 --- a/scripts/ffmpeg-7.1.json +++ b/scripts/ffmpeg-7.1.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.1.1-5/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.1.1-6/ffmpeg-{platform}.tar.gz" } From 25f94f1b2bc9062f67739119766f1633dfee52c4 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 3 Jul 2025 03:02:38 -0400 Subject: [PATCH 297/365] Release 15.0.0 --- CHANGELOG.rst | 4 ++-- av/about.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b5ee063d7..9beb09ffd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,8 +16,8 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. -v15.0.0rc1 ----------- +v15.0.0 +------- Major: diff --git a/av/about.py b/av/about.py index 2607c1260..d5793afcb 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "15.0.0rc1" +__version__ = "15.0.0" From 2186a380ef7d7b0dcc3427d0208f31edccc821ea Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 4 Jul 2025 15:48:19 -0400 Subject: [PATCH 298/365] Remove checks in setup.py --- setup.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/setup.py b/setup.py index 16ca7f59b..a96e0a019 100644 --- a/setup.py +++ b/setup.py @@ -7,20 +7,6 @@ import subprocess import sys -if platform.system() == "Darwin": - major_version = int(platform.mac_ver()[0].split(".")[0]) - if major_version < 12: - raise OSError("You are using an EOL, unsupported, and out-of-date OS.") - - -def is_virtualenv(): - return sys.base_prefix != sys.prefix - - -if not (os.getenv("GITHUB_ACTIONS") == "true" or is_virtualenv()): - raise ValueError("You are not using a virtual environment") - - from Cython.Build import cythonize from Cython.Compiler.AutoDocTransforms import EmbedSignature from setuptools import Extension, find_packages, setup From 2c20d39559fa59004615b92b0af113c922f767c1 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 4 Jul 2025 21:23:08 -0400 Subject: [PATCH 299/365] Avoid setting channel variable --- av/audio/layout.pyx | 6 ++---- setup.py | 19 ------------------- 2 files changed, 2 insertions(+), 23 deletions(-) diff --git a/av/audio/layout.pyx b/av/audio/layout.pyx index ea259d0fd..0936075ef 100644 --- a/av/audio/layout.pyx +++ b/av/audio/layout.pyx @@ -52,16 +52,14 @@ cdef class AudioLayout: @property def channels(self): - cdef lib.AVChannel channel cdef char buf[16] cdef char buf2[128] results = [] for index in range(self.layout.nb_channels): - channel = lib.av_channel_layout_channel_from_index(&self.layout, index); - size = lib.av_channel_name(buf, sizeof(buf), channel) - 1 - size2 = lib.av_channel_description(buf2, sizeof(buf2), channel) - 1 + size = lib.av_channel_name(buf, sizeof(buf), lib.av_channel_layout_channel_from_index(&self.layout, index)) - 1 + size2 = lib.av_channel_description(buf2, sizeof(buf2), lib.av_channel_layout_channel_from_index(&self.layout, index)) - 1 results.append( AudioChannel( PyBytes_FromStringAndSize(buf, size).decode("utf-8"), diff --git a/setup.py b/setup.py index a96e0a019..ebc2146fe 100644 --- a/setup.py +++ b/setup.py @@ -26,21 +26,6 @@ old_embed_signature = EmbedSignature._embed_signature -def insert_enum_in_generated_files(source): - # Work around Cython failing to add `enum` to `AVChannel` type. - # TODO: Make Cython bug report - if source.endswith(".c"): - with open(source, "r") as file: - content = file.read() - - # Replace "AVChannel __pyx_v_channel;" with "enum AVChannel __pyx_v_channel;" - modified_content = re.sub( - r"\b(? Date: Fri, 4 Jul 2025 22:09:54 -0400 Subject: [PATCH 300/365] Remove attachment docs --- docs/api/attachments.rst | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 docs/api/attachments.rst diff --git a/docs/api/attachments.rst b/docs/api/attachments.rst deleted file mode 100644 index 6e59a202b..000000000 --- a/docs/api/attachments.rst +++ /dev/null @@ -1,8 +0,0 @@ - -Attachments -=========== - -.. automodule:: av.attachments.stream - - .. autoclass:: AttachmentStream - :members: From cf6d0520c3a806b6ab9ca5caf3398ac47c809aae Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 7 Jul 2025 11:53:20 -0400 Subject: [PATCH 301/365] Use AVPacket.time_base --- av/codec/context.pyx | 4 ++-- av/container/input.pyx | 4 ++-- av/packet.pxd | 6 ------ av/packet.py | 15 +++++++-------- include/libavcodec/avcodec.pxd | 6 +----- 5 files changed, 12 insertions(+), 23 deletions(-) diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 5ca8f24a4..49087e779 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -428,7 +428,7 @@ cdef class CodecContext: # # NOTE: if the CodecContext's time_base is altered during encoding, all bets # are off! - packet._time_base = self.ptr.time_base + packet.ptr.time_base = self.ptr.time_base cpdef decode(self, Packet packet=None): """Decode a list of :class:`.Frame` from the given :class:`.Packet`. @@ -469,7 +469,7 @@ cdef class CodecContext: # TODO: Somehow get this from the stream so we can not pass the # packet here (because flushing packets are bogus). if packet is not None: - frame._time_base = packet._time_base + frame._time_base = packet.ptr.time_base @property def name(self): diff --git a/av/container/input.pyx b/av/container/input.pyx index 1ba4750d7..943081e15 100644 --- a/av/container/input.pyx +++ b/av/container/input.pyx @@ -183,7 +183,7 @@ cdef class InputContainer(Container): if packet.ptr.stream_index < len(self.streams): packet._stream = self.streams[packet.ptr.stream_index] # Keep track of this so that remuxing is easier. - packet._time_base = packet._stream.ptr.time_base + packet.ptr.time_base = packet._stream.ptr.time_base yield packet # Flush! @@ -191,7 +191,7 @@ cdef class InputContainer(Container): if include_stream[i]: packet = Packet() packet._stream = self.streams[i] - packet._time_base = packet._stream.ptr.time_base + packet.ptr.time_base = packet._stream.ptr.time_base yield packet finally: diff --git a/av/packet.pxd b/av/packet.pxd index ca21e6b76..f1517a3d4 100644 --- a/av/packet.pxd +++ b/av/packet.pxd @@ -6,15 +6,9 @@ from av.stream cimport Stream cdef class Packet(Buffer): - cdef lib.AVPacket* ptr - cdef Stream _stream - - # We track our own time. - cdef lib.AVRational _time_base cdef _rebase_time(self, lib.AVRational) - # Hold onto the original reference. cdef ByteSource source cdef size_t _buffer_size(self) diff --git a/av/packet.py b/av/packet.py index c49085c57..81f1aaa4d 100644 --- a/av/packet.py +++ b/av/packet.py @@ -65,16 +65,15 @@ def _rebase_time(self, dst: lib.AVRational): if not dst.num: raise ValueError("Cannot rebase to zero time.") - if not self._time_base.num: - self._time_base = dst + if not self.ptr.time_base.num: + self.ptr.time_base = dst return - if self._time_base.num == dst.num and self._time_base.den == dst.den: + if self.ptr.time_base.num == dst.num and self.ptr.time_base.den == dst.den: return - lib.av_packet_rescale_ts(self.ptr, self._time_base, dst) - - self._time_base = dst + lib.av_packet_rescale_ts(self.ptr, self.ptr.time_base, dst) + self.ptr.time_base = dst def decode(self): """ @@ -106,11 +105,11 @@ def time_base(self): :type: fractions.Fraction """ - return avrational_to_fraction(cython.address(self._time_base)) + return avrational_to_fraction(cython.address(self.ptr.time_base)) @time_base.setter def time_base(self, value): - to_avrational(value, cython.address(self._time_base)) + to_avrational(value, cython.address(self.ptr.time_base)) @property def pts(self): diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 0c8713cf8..134ae6f07 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -466,19 +466,15 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef AVFrame* avcodec_alloc_frame() cdef struct AVPacket: - int64_t pts int64_t dts uint8_t *data - + AVRational time_base int size int stream_index int flags - int duration - int64_t pos - void *opaque AVBufferRef *opaque_ref From 93a100548daacd7ec01824b106440856f08bb153 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 16 Jul 2025 12:24:18 -0600 Subject: [PATCH 302/365] Install cython from scientific-python nightlies in smoke tests --- .github/workflows/smoke.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index e83adf62d..d6637ce62 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -127,14 +127,14 @@ jobs: pillow \ pytest \ python=${{ matrix.config.python }} \ - setuptools + setuptools \ + cython - name: Build shell: bash run: | . $CONDA/etc/profile.d/conda.sh conda activate pyav - pip install 'Cython==3.1.0a1' python scripts\\fetch-vendor.py --config-file scripts\\ffmpeg-${{ matrix.config.ffmpeg }}.json $CONDA_PREFIX\\Library python scripts\\comptime.py ${{ matrix.config.ffmpeg }} python setup.py build_ext --inplace --ffmpeg-dir=$CONDA_PREFIX\\Library From 8e973abffdeb7149e25e15019ea9ea20f12a30bd Mon Sep 17 00:00:00 2001 From: Dave Johansen Date: Mon, 21 Jul 2025 18:25:09 -0600 Subject: [PATCH 303/365] Add set_image for updating the image content of an existing frame --- av/video/frame.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/av/video/frame.py b/av/video/frame.py index 970ecc4b8..0d60fd8e9 100644 --- a/av/video/frame.py +++ b/av/video/frame.py @@ -610,16 +610,22 @@ def to_ndarray(self, channel_last=False, **kwargs): f"Conversion to numpy array with format `{frame.format.name}` is not yet supported" ) - @staticmethod - def from_image(img): + def set_image(self, img): """ - Construct a frame from a ``PIL.Image``. + Update content from a ``PIL.Image``. """ if img.mode != "RGB": img = img.convert("RGB") + copy_array_to_plane(img, self.planes[0], 3) + + @staticmethod + def from_image(img): + """ + Construct a frame from a ``PIL.Image``. + """ frame: VideoFrame = VideoFrame(img.size[0], img.size[1], "rgb24") - copy_array_to_plane(img, frame.planes[0], 3) + frame.set_image(img) return frame From e2ec340cab48201fc3a72aa029fdae4035f3c4ed Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 21 Jul 2025 21:24:05 -0400 Subject: [PATCH 304/365] Index opaque by id and make `key_frame` writable --- CHANGELOG.rst | 7 ++++ av/{frame.pyx => frame.py} | 85 +++++++++++++++++++++----------------- av/frame.pyi | 4 +- av/opaque.pxd | 6 +-- av/opaque.pyx | 44 +++++++++++++------- tests/test_videoframe.py | 15 +++++++ 6 files changed, 102 insertions(+), 59 deletions(-) rename av/{frame.pyx => frame.py} (67%) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9beb09ffd..395910dc4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,13 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. +v15.1.0 +------- + +Features: + +- Make the `Frame.key_frame` flag writable. + v15.0.0 ------- diff --git a/av/frame.pyx b/av/frame.py similarity index 67% rename from av/frame.pyx rename to av/frame.py index fefdd2dee..8100c39d6 100644 --- a/av/frame.pyx +++ b/av/frame.py @@ -1,11 +1,13 @@ -from av.error cimport err_check -from av.opaque cimport opaque_container -from av.utils cimport avrational_to_fraction, to_avrational +import cython +from cython.cimports.av.error import err_check +from cython.cimports.av.opaque import opaque_container +from cython.cimports.av.utils import avrational_to_fraction, to_avrational from av.sidedata.sidedata import SideDataContainer -cdef class Frame: +@cython.cclass +class Frame: """ Base class for audio and video frames. @@ -13,20 +15,19 @@ """ def __cinit__(self, *args, **kwargs): - with nogil: + with cython.nogil: self.ptr = lib.av_frame_alloc() def __dealloc__(self): - with nogil: - # This calls av_frame_unref, and then frees the pointer. - # Thats it. - lib.av_frame_free(&self.ptr) + with cython.nogil: + lib.av_frame_free(cython.address(self.ptr)) def __repr__(self): - return f"av.{self.__class__.__name__} pts={self.pts} at 0x{id(self):x}>" + return f"" - cdef _copy_internal_attributes(self, Frame source, bint data_layout=True): - """Mimic another frame.""" + @cython.cfunc + def _copy_internal_attributes(self, source: Frame, data_layout: cython.bint = True): + # Mimic another frame self._time_base = source._time_base lib.av_frame_copy_props(self.ptr, source.ptr) if data_layout: @@ -36,10 +37,12 @@ def __repr__(self): self.ptr.height = source.ptr.height self.ptr.ch_layout = source.ptr.ch_layout - cdef _init_user_attributes(self): + @cython.cfunc + def _init_user_attributes(self): pass # Dummy to match the API of the others. - cdef _rebase_time(self, lib.AVRational dst): + @cython.cfunc + def _rebase_time(self, dst: lib.AVRational): if not dst.num: raise ValueError("Cannot rebase to zero time.") @@ -54,7 +57,9 @@ def __repr__(self): self.ptr.pts = lib.av_rescale_q(self.ptr.pts, self._time_base, dst) if self.ptr.duration != 0: - self.ptr.duration = lib.av_rescale_q(self.ptr.duration, self._time_base, dst) + self.ptr.duration = lib.av_rescale_q( + self.ptr.duration, self._time_base, dst + ) self._time_base = dst @@ -65,7 +70,7 @@ def dts(self): (if frame threading isn't used) This is also the Presentation time of this frame calculated from only :attr:`.Packet.dts` values without pts values. - :type: int + :type: int | None """ if self.ptr.pkt_dts == lib.AV_NOPTS_VALUE: return None @@ -85,7 +90,7 @@ def pts(self): This is the time at which the frame should be shown to the user. - :type: int + :type: int | None """ if self.ptr.pts == lib.AV_NOPTS_VALUE: return None @@ -105,16 +110,11 @@ def duration(self): :type: int """ - if self.ptr.duration == 0: - return None return self.ptr.duration @duration.setter def duration(self, value): - if value is None: - self.ptr.duration = 0 - else: - self.ptr.duration = value + self.ptr.duration = value @property def time(self): @@ -123,26 +123,25 @@ def time(self): This is the time at which the frame should be shown to the user. - :type: float + :type: float | None """ if self.ptr.pts == lib.AV_NOPTS_VALUE: return None - else: - return float(self.ptr.pts) * self._time_base.num / self._time_base.den + return float(self.ptr.pts) * self._time_base.num / self._time_base.den @property def time_base(self): """ The unit of time (in fractional seconds) in which timestamps are expressed. - :type: fractions.Fraction + :type: fractions.Fraction | None """ if self._time_base.num: - return avrational_to_fraction(&self._time_base) + return avrational_to_fraction(cython.address(self._time_base)) @time_base.setter def time_base(self, value): - to_avrational(value, &self._time_base) + to_avrational(value, cython.address(self._time_base)) @property def is_corrupt(self): @@ -151,7 +150,9 @@ def is_corrupt(self): :type: bool """ - return self.ptr.decode_error_flags != 0 or bool(self.ptr.flags & lib.AV_FRAME_FLAG_CORRUPT) + return self.ptr.decode_error_flags != 0 or bool( + self.ptr.flags & lib.AV_FRAME_FLAG_CORRUPT + ) @property def key_frame(self): @@ -162,6 +163,13 @@ def key_frame(self): """ return bool(self.ptr.flags & lib.AV_FRAME_FLAG_KEY) + @key_frame.setter + def key_frame(self, v): + # PyAV makes no guarantees this does anything. + if v: + self.ptr.flags |= lib.AV_FRAME_FLAG_KEY + else: + self.ptr.flags &= ~lib.AV_FRAME_FLAG_KEY @property def side_data(self): @@ -174,20 +182,19 @@ def make_writable(self): Ensures that the frame data is writable. Copy the data to new buffer if it is not. This is a wrapper around :ffmpeg:`av_frame_make_writable`. """ - cdef int ret - - ret = lib.av_frame_make_writable(self.ptr) + ret: cython.int = lib.av_frame_make_writable(self.ptr) err_check(ret) @property def opaque(self): - if self.ptr.opaque_ref is not NULL: - return opaque_container.get( self.ptr.opaque_ref.data) + if self.ptr.opaque_ref is not cython.NULL: + return opaque_container.get( + cython.cast(cython.p_char, self.ptr.opaque_ref.data) + ) @opaque.setter def opaque(self, v): - lib.av_buffer_unref(&self.ptr.opaque_ref) + lib.av_buffer_unref(cython.address(self.ptr.opaque_ref)) - if v is None: - return - self.ptr.opaque_ref = opaque_container.add(v) + if v is not None: + self.ptr.opaque_ref = opaque_container.add(v) diff --git a/av/frame.pyi b/av/frame.pyi index 38a273afc..f085fc0f4 100644 --- a/av/frame.pyi +++ b/av/frame.pyi @@ -9,8 +9,8 @@ class SideData(TypedDict, total=False): class Frame: dts: int | None pts: int | None - duration: int | None - time_base: Fraction + duration: int + time_base: Fraction | None side_data: SideData opaque: object @property diff --git a/av/opaque.pxd b/av/opaque.pxd index f5c38d7fa..76174931f 100644 --- a/av/opaque.pxd +++ b/av/opaque.pxd @@ -2,11 +2,11 @@ cimport libav as lib cdef class OpaqueContainer: - cdef dict _by_name + cdef dict _objects cdef lib.AVBufferRef *add(self, object v) - cdef object get(self, bytes name) - cdef object pop(self, bytes name) + cdef object get(self, char *name) + cdef object pop(self, char *name) cdef OpaqueContainer opaque_container diff --git a/av/opaque.pyx b/av/opaque.pyx index 1e6769898..619169edb 100644 --- a/av/opaque.pyx +++ b/av/opaque.pyx @@ -1,7 +1,6 @@ cimport libav as lib from libc.stdint cimport uint8_t - -from uuid import uuid4 +from libc.string cimport memcpy cdef void key_free(void *opaque, uint8_t *data) noexcept nogil: @@ -11,22 +10,37 @@ cdef void key_free(void *opaque, uint8_t *data) noexcept nogil: cdef class OpaqueContainer: - """A container that holds references to Python objects, indexed by uuid""" - def __cinit__(self): - self._by_name = {} + self._objects = {} + + cdef lib.AVBufferRef *add(self, object v): + # Use object's memory address as key + cdef size_t key = id(v) + self._objects[key] = v + + cdef uint8_t *data = lib.av_malloc(sizeof(size_t)) + if data == NULL: + raise MemoryError("Failed to allocate memory for key") + + memcpy(data, &key, sizeof(size_t)) + + # Create the buffer with our free callback + cdef lib.AVBufferRef *buffer_ref = lib.av_buffer_create( + data, sizeof(size_t), key_free, NULL, 0 + ) + + if buffer_ref == NULL: + raise MemoryError("Failed to create AVBufferRef") - cdef lib.AVBufferRef *add(self, v): - cdef bytes uuid = str(uuid4()).encode("utf-8") - cdef lib.AVBufferRef *ref = lib.av_buffer_create(uuid, len(uuid), &key_free, NULL, 0) - self._by_name[uuid] = v - return ref + return buffer_ref - cdef object get(self, bytes name): - return self._by_name.get(name) + cdef object get(self, char *name): + cdef size_t key = (name)[0] + return self._objects.get(key) - cdef object pop(self, bytes name): - return self._by_name.pop(name) + cdef object pop(self, char *name): + cdef size_t key = (name)[0] + return self._objects.pop(key, None) -cdef opaque_container = OpaqueContainer() +cdef OpaqueContainer opaque_container = OpaqueContainer() diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 9c9773ae8..90d91ab45 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -122,6 +122,21 @@ def test_memoryview_read() -> None: assert mem[:7] == b"0.234xx" +def test_opaque() -> None: + frame = VideoFrame(640, 480, "rgb24") + frame.opaque = 3 + assert frame.opaque == 3 + frame.opaque = "a" + assert frame.opaque == "a" + + greeting = "Hello World!" + frame.opaque = greeting + assert frame.opaque is greeting + + frame.opaque = None + assert frame.opaque is None + + def test_interpolation() -> None: container = av.open(fate_png()) for _ in container.decode(video=0): From 3cb9b6383a2bdc626f005b5b13789d4c75fabfb5 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 21 Jul 2025 22:45:35 -0400 Subject: [PATCH 305/365] Fix opaque container on PyPy --- av/opaque.pyx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/av/opaque.pyx b/av/opaque.pyx index 619169edb..0c2a4507b 100644 --- a/av/opaque.pyx +++ b/av/opaque.pyx @@ -1,5 +1,5 @@ cimport libav as lib -from libc.stdint cimport uint8_t +from libc.stdint cimport intptr_t, uint8_t from libc.string cimport memcpy @@ -15,18 +15,18 @@ cdef class OpaqueContainer: cdef lib.AVBufferRef *add(self, object v): # Use object's memory address as key - cdef size_t key = id(v) + cdef intptr_t key = id(v) self._objects[key] = v - cdef uint8_t *data = lib.av_malloc(sizeof(size_t)) + cdef uint8_t *data = lib.av_malloc(sizeof(intptr_t)) if data == NULL: raise MemoryError("Failed to allocate memory for key") - memcpy(data, &key, sizeof(size_t)) + memcpy(data, &key, sizeof(intptr_t)) # Create the buffer with our free callback cdef lib.AVBufferRef *buffer_ref = lib.av_buffer_create( - data, sizeof(size_t), key_free, NULL, 0 + data, sizeof(intptr_t), key_free, NULL, 0 ) if buffer_ref == NULL: @@ -35,11 +35,11 @@ cdef class OpaqueContainer: return buffer_ref cdef object get(self, char *name): - cdef size_t key = (name)[0] + cdef intptr_t key = (name)[0] return self._objects.get(key) cdef object pop(self, char *name): - cdef size_t key = (name)[0] + cdef intptr_t key = (name)[0] return self._objects.pop(key, None) From 2f6c495778c083f7cb9f712beef025734cff9fb6 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 4 Aug 2025 16:28:03 -0400 Subject: [PATCH 306/365] Replace FFmpeg version macro with direct API call --- av/filter/pad.pyx | 4 +--- include/libavfilter/avfilter.pxd | 15 +-------------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/av/filter/pad.pyx b/av/filter/pad.pyx index 873b31b04..cf889ea69 100644 --- a/av/filter/pad.pyx +++ b/av/filter/pad.pyx @@ -72,9 +72,7 @@ cdef tuple alloc_filter_pads(Filter filter, const lib.AVFilterPad *ptr, bint is_ cdef int i = 0 cdef int count if context is None: - # This is a custom function defined using a macro in avfilter.pxd. Its usage - # can be changed after we stop supporting FFmpeg < 5.0. - count = lib.pyav_get_num_pads(filter.ptr, not is_input, ptr) + count = lib.avfilter_filter_pad_count(filter.ptr, not is_input) else: count = (context.ptr.nb_inputs if is_input else context.ptr.nb_outputs) diff --git a/include/libavfilter/avfilter.pxd b/include/libavfilter/avfilter.pxd index dd3e91ddf..90f349005 100644 --- a/include/libavfilter/avfilter.pxd +++ b/include/libavfilter/avfilter.pxd @@ -1,14 +1,4 @@ - cdef extern from "libavfilter/avfilter.h" nogil: - """ - #if (LIBAVFILTER_VERSION_INT >= 525156) - // avfilter_filter_pad_count is available since version 8.3.100 of libavfilter (FFmpeg 5.0) - #define _avfilter_get_num_pads(filter, is_output, pads) (avfilter_filter_pad_count(filter, is_output)) - #else - // avfilter_filter_pad_count has been deprecated as of version 8.3.100 of libavfilter (FFmpeg 5.0) - #define _avfilter_get_num_pads(filter, is_output, pads) (avfilter_pad_count(pads)) - #endif - """ cdef int avfilter_version() cdef char* avfilter_configuration() cdef char* avfilter_license() @@ -20,10 +10,9 @@ cdef extern from "libavfilter/avfilter.h" nogil: const char* avfilter_pad_get_name(const AVFilterPad *pads, int index) AVMediaType avfilter_pad_get_type(const AVFilterPad *pads, int index) - int pyav_get_num_pads "_avfilter_get_num_pads" (const AVFilter *filter, int is_output, const AVFilterPad *pads) + cdef unsigned avfilter_filter_pad_count(const AVFilter *filter, int is_output) cdef struct AVFilter: - AVClass *priv_class const char *name @@ -48,7 +37,6 @@ cdef extern from "libavfilter/avfilter.h" nogil: cdef struct AVFilterLink # Defined later. cdef struct AVFilterContext: - AVClass *av_class AVFilter *filter @@ -68,7 +56,6 @@ cdef extern from "libavfilter/avfilter.h" nogil: cdef AVClass* avfilter_get_class() cdef struct AVFilterLink: - AVFilterContext *src AVFilterPad *srcpad AVFilterContext *dst From 3de331d5621a1d491ba0d45fc1a096c7cbaa10d6 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 4 Aug 2025 17:19:32 -0400 Subject: [PATCH 307/365] Combine pad and link --- av/filter/context.pyi | 4 -- av/filter/context.pyx | 2 +- av/filter/filter.pyi | 5 --- av/filter/filter.pyx | 2 +- av/filter/link.pxd | 20 +++++++++- av/filter/link.pyi | 5 +-- av/filter/link.pyx | 77 +++++++++++++++++++++++++++++++++++- av/filter/pad.pxd | 23 ----------- av/filter/pad.pyi | 10 ----- av/filter/pad.pyx | 90 ------------------------------------------- tests/test_filters.py | 6 --- 11 files changed, 97 insertions(+), 147 deletions(-) delete mode 100644 av/filter/pad.pxd delete mode 100644 av/filter/pad.pyi delete mode 100644 av/filter/pad.pyx diff --git a/av/filter/context.pyi b/av/filter/context.pyi index 7c00087a9..13ee480c2 100644 --- a/av/filter/context.pyi +++ b/av/filter/context.pyi @@ -1,12 +1,8 @@ from av.filter import Graph from av.frame import Frame -from .pad import FilterContextPad - class FilterContext: name: str | None - inputs: tuple[FilterContextPad, ...] - outputs: tuple[FilterContextPad, ...] def init(self, args: str | None = None, **kwargs: str | None) -> None: ... def link_to( diff --git a/av/filter/context.pyx b/av/filter/context.pyx index b820d3d18..ba5981ff1 100644 --- a/av/filter/context.pyx +++ b/av/filter/context.pyx @@ -4,7 +4,7 @@ from av.audio.frame cimport alloc_audio_frame from av.dictionary cimport _Dictionary from av.dictionary import Dictionary from av.error cimport err_check -from av.filter.pad cimport alloc_filter_pads +from av.filter.link cimport alloc_filter_pads from av.frame cimport Frame from av.utils cimport avrational_to_fraction from av.video.frame cimport alloc_video_frame diff --git a/av/filter/filter.pyi b/av/filter/filter.pyi index 2751e973c..42d9cd7c9 100644 --- a/av/filter/filter.pyi +++ b/av/filter/filter.pyi @@ -1,12 +1,9 @@ from av.descriptor import Descriptor from av.option import Option -from .pad import FilterPad - class Filter: name: str description: str - descriptor: Descriptor options: tuple[Option, ...] | None flags: int @@ -15,8 +12,6 @@ class Filter: timeline_support: bool slice_threads: bool command_support: bool - inputs: tuple[FilterPad, ...] - outputs: tuple[FilterPad, ...] def __init__(self, name: str) -> None: ... diff --git a/av/filter/filter.pyx b/av/filter/filter.pyx index d4880dc15..5f4963b2a 100644 --- a/av/filter/filter.pyx +++ b/av/filter/filter.pyx @@ -1,7 +1,7 @@ cimport libav as lib from av.descriptor cimport wrap_avclass -from av.filter.pad cimport alloc_filter_pads +from av.filter.link cimport alloc_filter_pads cdef object _cinit_sentinel = object() diff --git a/av/filter/link.pxd b/av/filter/link.pxd index a6a4b1c09..08771362b 100644 --- a/av/filter/link.pxd +++ b/av/filter/link.pxd @@ -1,11 +1,12 @@ cimport libav as lib +from av.filter.context cimport FilterContext +from av.filter.filter cimport Filter from av.filter.graph cimport Graph -from av.filter.pad cimport FilterContextPad +from av.filter.link cimport FilterContextPad, FilterLink cdef class FilterLink: - cdef readonly Graph graph cdef lib.AVFilterLink *ptr @@ -14,3 +15,18 @@ cdef class FilterLink: cdef FilterLink wrap_filter_link(Graph graph, lib.AVFilterLink *ptr) + +cdef class FilterPad: + cdef readonly Filter filter + cdef readonly FilterContext context + cdef readonly bint is_input + cdef readonly int index + + cdef const lib.AVFilterPad *base_ptr + + +cdef class FilterContextPad(FilterPad): + cdef FilterLink _link + + +cdef tuple alloc_filter_pads(Filter, const lib.AVFilterPad *ptr, bint is_input, FilterContext context=?) diff --git a/av/filter/link.pyi b/av/filter/link.pyi index dd420ad91..9d199a272 100644 --- a/av/filter/link.pyi +++ b/av/filter/link.pyi @@ -1,5 +1,2 @@ -from .pad import FilterContextPad - class FilterLink: - input: FilterContextPad - output: FilterContextPad + pass diff --git a/av/filter/link.pyx b/av/filter/link.pyx index 78b7da30f..905082d15 100644 --- a/av/filter/link.pyx +++ b/av/filter/link.pyx @@ -7,7 +7,6 @@ cdef _cinit_sentinel = object() cdef class FilterLink: - def __cinit__(self, sentinel): if sentinel is not _cinit_sentinel: raise RuntimeError("cannot instantiate FilterLink") @@ -51,3 +50,79 @@ cdef FilterLink wrap_filter_link(Graph graph, lib.AVFilterLink *ptr): link.graph = graph link.ptr = ptr return link + + + +cdef class FilterPad: + def __cinit__(self, sentinel): + if sentinel is not _cinit_sentinel: + raise RuntimeError("cannot construct FilterPad") + + def __repr__(self): + _filter = self.filter.name + _io = "inputs" if self.is_input else "outputs" + + return f"" + + @property + def is_output(self): + return not self.is_input + + @property + def name(self): + return lib.avfilter_pad_get_name(self.base_ptr, self.index) + + +cdef class FilterContextPad(FilterPad): + def __repr__(self): + _filter = self.filter.name + _io = "inputs" if self.is_input else "outputs" + context = self.context.name + + return f"" + + @property + def link(self): + if self._link: + return self._link + cdef lib.AVFilterLink **links = self.context.ptr.inputs if self.is_input else self.context.ptr.outputs + cdef lib.AVFilterLink *link = links[self.index] + if not link: + return + self._link = wrap_filter_link(self.context.graph, link) + return self._link + + @property + def linked(self): + cdef FilterLink link = self.link + if link: + return link.input if self.is_input else link.output + + +cdef tuple alloc_filter_pads(Filter filter, const lib.AVFilterPad *ptr, bint is_input, FilterContext context=None): + if not ptr: + return () + + pads = [] + + # We need to be careful and check our bounds if we know what they are, + # since the arrays on a AVFilterContext are not NULL terminated. + cdef int i = 0 + cdef int count + if context is None: + count = lib.avfilter_filter_pad_count(filter.ptr, not is_input) + else: + count = (context.ptr.nb_inputs if is_input else context.ptr.nb_outputs) + + cdef FilterPad pad + while (i < count): + pad = FilterPad(_cinit_sentinel) if context is None else FilterContextPad(_cinit_sentinel) + pads.append(pad) + pad.filter = filter + pad.context = context + pad.is_input = is_input + pad.base_ptr = ptr + pad.index = i + i += 1 + + return tuple(pads) diff --git a/av/filter/pad.pxd b/av/filter/pad.pxd deleted file mode 100644 index 15ac950fc..000000000 --- a/av/filter/pad.pxd +++ /dev/null @@ -1,23 +0,0 @@ -cimport libav as lib - -from av.filter.context cimport FilterContext -from av.filter.filter cimport Filter -from av.filter.link cimport FilterLink - - -cdef class FilterPad: - - cdef readonly Filter filter - cdef readonly FilterContext context - cdef readonly bint is_input - cdef readonly int index - - cdef const lib.AVFilterPad *base_ptr - - -cdef class FilterContextPad(FilterPad): - - cdef FilterLink _link - - -cdef tuple alloc_filter_pads(Filter, const lib.AVFilterPad *ptr, bint is_input, FilterContext context=?) diff --git a/av/filter/pad.pyi b/av/filter/pad.pyi deleted file mode 100644 index 1a6c9bda6..000000000 --- a/av/filter/pad.pyi +++ /dev/null @@ -1,10 +0,0 @@ -from .link import FilterLink - -class FilterPad: - is_output: bool - name: str - type: str - -class FilterContextPad(FilterPad): - link: FilterLink | None - linked: FilterContextPad | None diff --git a/av/filter/pad.pyx b/av/filter/pad.pyx deleted file mode 100644 index cf889ea69..000000000 --- a/av/filter/pad.pyx +++ /dev/null @@ -1,90 +0,0 @@ -from av.filter.link cimport wrap_filter_link - - -cdef object _cinit_sentinel = object() - - -cdef class FilterPad: - def __cinit__(self, sentinel): - if sentinel is not _cinit_sentinel: - raise RuntimeError("cannot construct FilterPad") - - def __repr__(self): - _filter = self.filter.name - _io = "inputs" if self.is_input else "outputs" - - return f"" - - @property - def is_output(self): - return not self.is_input - - @property - def name(self): - return lib.avfilter_pad_get_name(self.base_ptr, self.index) - - @property - def type(self): - """ - The media type of this filter pad. - - Examples: `'audio'`, `'video'`, `'subtitle'`. - - :type: str - """ - return lib.av_get_media_type_string(lib.avfilter_pad_get_type(self.base_ptr, self.index)) - - -cdef class FilterContextPad(FilterPad): - def __repr__(self): - _filter = self.filter.name - _io = "inputs" if self.is_input else "outputs" - context = self.context.name - - return f"" - - @property - def link(self): - if self._link: - return self._link - cdef lib.AVFilterLink **links = self.context.ptr.inputs if self.is_input else self.context.ptr.outputs - cdef lib.AVFilterLink *link = links[self.index] - if not link: - return - self._link = wrap_filter_link(self.context.graph, link) - return self._link - - @property - def linked(self): - cdef FilterLink link = self.link - if link: - return link.input if self.is_input else link.output - - -cdef tuple alloc_filter_pads(Filter filter, const lib.AVFilterPad *ptr, bint is_input, FilterContext context=None): - if not ptr: - return () - - pads = [] - - # We need to be careful and check our bounds if we know what they are, - # since the arrays on a AVFilterContext are not NULL terminated. - cdef int i = 0 - cdef int count - if context is None: - count = lib.avfilter_filter_pad_count(filter.ptr, not is_input) - else: - count = (context.ptr.nb_inputs if is_input else context.ptr.nb_outputs) - - cdef FilterPad pad - while (i < count): - pad = FilterPad(_cinit_sentinel) if context is None else FilterContextPad(_cinit_sentinel) - pads.append(pad) - pad.filter = filter - pad.context = context - pad.is_input = is_input - pad.base_ptr = ptr - pad.index = i - i += 1 - - return tuple(pads) diff --git a/tests/test_filters.py b/tests/test_filters.py index 7722de735..886c22a01 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -51,18 +51,12 @@ def test_filter_descriptor(self) -> None: assert f.name == "testsrc" assert f.description == "Generate test pattern." assert not f.dynamic_inputs - assert len(f.inputs) == 0 assert not f.dynamic_outputs - assert len(f.outputs) == 1 - assert f.outputs[0].name == "default" - assert f.outputs[0].type == "video" def test_dynamic_filter_descriptor(self): f = Filter("split") assert not f.dynamic_inputs - assert len(f.inputs) == 1 assert f.dynamic_outputs - assert len(f.outputs) == 0 def test_generator_graph(self): graph = Graph() From eaa675d67ee8802ea651c49cadf3da5313ded417 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 4 Aug 2025 17:56:51 -0400 Subject: [PATCH 308/365] Remove FilterFlags --- av/filter/__init__.py | 2 +- av/filter/filter.pyi | 4 ---- av/filter/filter.pyx | 24 ------------------------ include/libavfilter/avfilter.pxd | 7 ------- tests/test_filters.py | 7 ------- 5 files changed, 1 insertion(+), 43 deletions(-) diff --git a/av/filter/__init__.py b/av/filter/__init__.py index 5dd4430d4..b63a95f25 100644 --- a/av/filter/__init__.py +++ b/av/filter/__init__.py @@ -1,3 +1,3 @@ -from .filter import Filter, FilterFlags, filter_descriptor, filters_available +from .filter import Filter, filter_descriptor, filters_available from .graph import Graph from .loudnorm import stats diff --git a/av/filter/filter.pyi b/av/filter/filter.pyi index 42d9cd7c9..e9f9737f3 100644 --- a/av/filter/filter.pyi +++ b/av/filter/filter.pyi @@ -7,10 +7,6 @@ class Filter: descriptor: Descriptor options: tuple[Option, ...] | None flags: int - dynamic_inputs: bool - dynamic_outputs: bool - timeline_support: bool - slice_threads: bool command_support: bool def __init__(self, name: str) -> None: ... diff --git a/av/filter/filter.pyx b/av/filter/filter.pyx index 5f4963b2a..d66b2e46a 100644 --- a/av/filter/filter.pyx +++ b/av/filter/filter.pyx @@ -13,14 +13,6 @@ cdef Filter wrap_filter(const lib.AVFilter *ptr): return filter_ -cpdef enum FilterFlags: - DYNAMIC_INPUTS = lib.AVFILTER_FLAG_DYNAMIC_INPUTS - DYNAMIC_OUTPUTS = lib.AVFILTER_FLAG_DYNAMIC_OUTPUTS - SLICE_THREADS = lib.AVFILTER_FLAG_SLICE_THREADS - SUPPORT_TIMELINE_GENERIC = lib.AVFILTER_FLAG_SUPPORT_TIMELINE_GENERIC - SUPPORT_TIMELINE_INTERNAL = lib.AVFILTER_FLAG_SUPPORT_TIMELINE_INTERNAL - - cdef class Filter: def __cinit__(self, name): if name is _cinit_sentinel: @@ -55,22 +47,6 @@ cdef class Filter: def flags(self): return self.ptr.flags - @property - def dynamic_inputs(self): - return bool(self.ptr.flags & lib.AVFILTER_FLAG_DYNAMIC_INPUTS) - - @property - def dynamic_outputs(self): - return bool(self.ptr.flags & lib.AVFILTER_FLAG_DYNAMIC_OUTPUTS) - - @property - def timeline_support(self): - return bool(self.ptr.flags & lib.AVFILTER_FLAG_SUPPORT_TIMELINE_GENERIC) - - @property - def slice_threads(self): - return bool(self.ptr.flags & lib.AVFILTER_FLAG_SLICE_THREADS) - @property def command_support(self): return self.ptr.process_command != NULL diff --git a/include/libavfilter/avfilter.pxd b/include/libavfilter/avfilter.pxd index 90f349005..de7bfb445 100644 --- a/include/libavfilter/avfilter.pxd +++ b/include/libavfilter/avfilter.pxd @@ -24,13 +24,6 @@ cdef extern from "libavfilter/avfilter.h" nogil: const AVFilterPad *outputs int (*process_command)(AVFilterContext *, const char *cmd, const char *arg, char *res, int res_len, int flags) - cdef enum: - AVFILTER_FLAG_DYNAMIC_INPUTS - AVFILTER_FLAG_DYNAMIC_OUTPUTS - AVFILTER_FLAG_SLICE_THREADS - AVFILTER_FLAG_SUPPORT_TIMELINE_GENERIC - AVFILTER_FLAG_SUPPORT_TIMELINE_INTERNAL - cdef AVFilter* avfilter_get_by_name(const char *name) cdef const AVFilter* av_filter_iterate(void **opaque) diff --git a/tests/test_filters.py b/tests/test_filters.py index 886c22a01..bd74a633a 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -50,13 +50,6 @@ def test_filter_descriptor(self) -> None: f = Filter("testsrc") assert f.name == "testsrc" assert f.description == "Generate test pattern." - assert not f.dynamic_inputs - assert not f.dynamic_outputs - - def test_dynamic_filter_descriptor(self): - f = Filter("split") - assert not f.dynamic_inputs - assert f.dynamic_outputs def test_generator_graph(self): graph = Graph() From 22d6a7ac30d10eba804cb59672d8bb8dd1623998 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 4 Aug 2025 19:28:45 -0400 Subject: [PATCH 309/365] Remove unused swr functions --- include/libswresample/swresample.pxd | 35 ---------------------------- 1 file changed, 35 deletions(-) diff --git a/include/libswresample/swresample.pxd b/include/libswresample/swresample.pxd index 65b8314df..5bd8a8629 100644 --- a/include/libswresample/swresample.pxd +++ b/include/libswresample/swresample.pxd @@ -1,39 +1,4 @@ -from libc.stdint cimport int64_t, uint8_t - - cdef extern from "libswresample/swresample.h" nogil: - cdef int swresample_version() cdef char* swresample_configuration() cdef char* swresample_license() - - cdef struct SwrContext: - pass - - cdef SwrContext* swr_alloc_set_opts( - SwrContext *ctx, - int64_t out_ch_layout, - AVSampleFormat out_sample_fmt, - int out_sample_rate, - int64_t in_ch_layout, - AVSampleFormat in_sample_fmt, - int in_sample_rate, - int log_offset, - void *log_ctx # logging context, can be NULL - ) - - cdef int swr_convert( - SwrContext *ctx, - uint8_t ** out_buffer, - int out_count, - uint8_t **in_buffer, - int in_count - ) - # Gets the delay the next input sample will - # experience relative to the next output sample. - cdef int64_t swr_get_delay(SwrContext *s, int64_t base) - - cdef SwrContext* swr_alloc() - cdef int swr_init(SwrContext* ctx) - cdef void swr_free(SwrContext **ctx) - cdef void swr_close(SwrContext *ctx) From 0b85c60defbea05ee8118a0605f7c31254589a0b Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 23 Aug 2025 12:23:26 -0400 Subject: [PATCH 310/365] Build with ffmpeg 8 --- .github/workflows/smoke.yml | 8 ++++---- av/codec/codec.pyx | 1 - av/codec/context.pyx | 6 +++--- av/container/core.pyx | 1 - av/filter/filter.pyi | 1 - av/filter/filter.pyx | 4 ---- av/format.pyx | 1 - include/libavcodec/avcodec.pxd | 10 +++------- include/libavfilter/avfilter.pxd | 8 ++------ include/libavformat/avformat.pxd | 2 -- scripts/activate.sh | 2 +- scripts/{ffmpeg-7.0.json => ffmpeg-8.0.json} | 4 ++-- 12 files changed, 15 insertions(+), 33 deletions(-) rename scripts/{ffmpeg-7.0.json => ffmpeg-8.0.json} (61%) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index d6637ce62..3c355a949 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -34,9 +34,9 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-24.04, python: "3.12", ffmpeg: "7.1.1", extras: true} - - {os: ubuntu-24.04, python: "pypy3.10", ffmpeg: "7.1.1"} - - {os: macos-14, python: "3.9", ffmpeg: "7.1.1"} + - {os: ubuntu-24.04, python: "3.12", ffmpeg: "8.0", extras: true} + - {os: ubuntu-24.04, python: "pypy3.10", ffmpeg: "8.0"} + - {os: macos-14, python: "3.9", ffmpeg: "8.0"} env: PYAV_PYTHON: python${{ matrix.config.python }} @@ -109,7 +109,7 @@ jobs: fail-fast: false matrix: config: - - {os: windows-latest, python: "3.9", ffmpeg: "7.1"} + - {os: windows-latest, python: "3.9", ffmpeg: "8.0"} steps: - name: Checkout diff --git a/av/codec/codec.pyx b/av/codec/codec.pyx index a28db758e..aeab61cc3 100644 --- a/av/codec/codec.pyx +++ b/av/codec/codec.pyx @@ -36,7 +36,6 @@ class Capabilities(IntEnum): delay = lib.AV_CODEC_CAP_DELAY small_last_frame = lib.AV_CODEC_CAP_SMALL_LAST_FRAME hwaccel_vdpau = 1 << 7 - subframes = lib.AV_CODEC_CAP_SUBFRAMES experimental = lib.AV_CODEC_CAP_EXPERIMENTAL channel_conf = lib.AV_CODEC_CAP_CHANNEL_CONF neg_linesizes = 1 << 11 diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 49087e779..1d9c412b2 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -494,7 +494,7 @@ cdef class CodecContext: # the codec itself. So use the descriptor here. desc = self.codec.desc cdef int i = 0 - while desc.profiles[i].profile != lib.FF_PROFILE_UNKNOWN: + while desc.profiles[i].profile != lib.AV_PROFILE_UNKNOWN: ret.append(desc.profiles[i].name) i += 1 @@ -509,7 +509,7 @@ cdef class CodecContext: # the codec itself. So use the descriptor here. desc = self.codec.desc cdef int i = 0 - while desc.profiles[i].profile != lib.FF_PROFILE_UNKNOWN: + while desc.profiles[i].profile != lib.AV_PROFILE_UNKNOWN: if desc.profiles[i].profile == self.ptr.profile: return desc.profiles[i].name i += 1 @@ -523,7 +523,7 @@ cdef class CodecContext: # the codec itself. So use the descriptor here. desc = self.codec.desc cdef int i = 0 - while desc.profiles[i].profile != lib.FF_PROFILE_UNKNOWN: + while desc.profiles[i].profile != lib.AV_PROFILE_UNKNOWN: if desc.profiles[i].name == value: self.ptr.profile = desc.profiles[i].profile return diff --git a/av/container/core.pyx b/av/container/core.pyx index 2b9a1244b..d8950c76f 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -138,7 +138,6 @@ class Flags(Flag): bitexact: "When muxing, try to avoid writing any random/volatile data to the output. This includes any random IDs, real-time timestamps/dates, muxer version, etc. This flag is mainly intended for testing." = lib.AVFMT_FLAG_BITEXACT sort_dts: "Try to interleave outputted packets by dts (using this flag can slow demuxing down)." = lib.AVFMT_FLAG_SORT_DTS fast_seek: "Enable fast, but inaccurate seeks for some formats." = lib.AVFMT_FLAG_FAST_SEEK - shortest: "Stop muxing when the shortest stream stops." = lib.AVFMT_FLAG_SHORTEST auto_bsf: "Add bitstream filters as requested by the muxer." = lib.AVFMT_FLAG_AUTO_BSF class AudioCodec(IntEnum): diff --git a/av/filter/filter.pyi b/av/filter/filter.pyi index e9f9737f3..f07f317fc 100644 --- a/av/filter/filter.pyi +++ b/av/filter/filter.pyi @@ -7,7 +7,6 @@ class Filter: descriptor: Descriptor options: tuple[Option, ...] | None flags: int - command_support: bool def __init__(self, name: str) -> None: ... diff --git a/av/filter/filter.pyx b/av/filter/filter.pyx index d66b2e46a..b6e7005f0 100644 --- a/av/filter/filter.pyx +++ b/av/filter/filter.pyx @@ -47,10 +47,6 @@ cdef class Filter: def flags(self): return self.ptr.flags - @property - def command_support(self): - return self.ptr.process_command != NULL - @property def inputs(self): if self._inputs is None: diff --git a/av/format.pyx b/av/format.pyx index 464e34f49..2eb386649 100644 --- a/av/format.pyx +++ b/av/format.pyx @@ -31,7 +31,6 @@ class Flags(Flag): no_bin_search: "Format does not allow to fall back on binary search via read_timestamp" = lib.AVFMT_NOBINSEARCH no_gen_search: "Format does not allow to fall back on generic search" = lib.AVFMT_NOGENSEARCH no_byte_seek: "Format does not allow seeking by bytes" = lib.AVFMT_NO_BYTE_SEEK - allow_flush: "Format allows flushing. If not set, the muxer will not receive a NULL packet in the write_packet function." = lib.AVFMT_ALLOW_FLUSH ts_nonstrict: "Format does not require strictly increasing timestamps, but they must still be monotonic." = lib.AVFMT_TS_NONSTRICT ts_negative: "Format allows muxing negative timestamps." = lib.AVFMT_TS_NEGATIVE # If not set the timestamp will be shifted in `av_write_frame()` and `av_interleaved_write_frame()` diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 134ae6f07..2bce8c2c1 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -80,14 +80,10 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef enum: AV_CODEC_CAP_DRAW_HORIZ_BAND AV_CODEC_CAP_DR1 - # AV_CODEC_CAP_HWACCEL AV_CODEC_CAP_DELAY AV_CODEC_CAP_SMALL_LAST_FRAME - # AV_CODEC_CAP_HWACCEL_VDPAU - AV_CODEC_CAP_SUBFRAMES AV_CODEC_CAP_EXPERIMENTAL AV_CODEC_CAP_CHANNEL_CONF - # AV_CODEC_CAP_NEG_LINESIZES AV_CODEC_CAP_FRAME_THREADS AV_CODEC_CAP_SLICE_THREADS AV_CODEC_CAP_PARAM_CHANGE @@ -98,6 +94,9 @@ cdef extern from "libavcodec/avcodec.h" nogil: AV_CODEC_CAP_HYBRID AV_CODEC_CAP_ENCODER_REORDERED_OPAQUE + cdef enum: + AV_PROFILE_UNKNOWN = -99 + cdef enum: FF_THREAD_FRAME FF_THREAD_SLICE @@ -156,9 +155,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: FF_COMPLIANCE_UNOFFICIAL FF_COMPLIANCE_EXPERIMENTAL - cdef enum: - FF_PROFILE_UNKNOWN = -99 - cdef enum AVCodecID: AV_CODEC_ID_NONE AV_CODEC_ID_MPEG2VIDEO diff --git a/include/libavfilter/avfilter.pxd b/include/libavfilter/avfilter.pxd index de7bfb445..b1ced248b 100644 --- a/include/libavfilter/avfilter.pxd +++ b/include/libavfilter/avfilter.pxd @@ -13,16 +13,12 @@ cdef extern from "libavfilter/avfilter.h" nogil: cdef unsigned avfilter_filter_pad_count(const AVFilter *filter, int is_output) cdef struct AVFilter: - AVClass *priv_class - const char *name const char *description - - const int flags - const AVFilterPad *inputs const AVFilterPad *outputs - int (*process_command)(AVFilterContext *, const char *cmd, const char *arg, char *res, int res_len, int flags) + const AVClass *priv_class + int flags cdef AVFilter* avfilter_get_by_name(const char *name) cdef const AVFilter* av_filter_iterate(void **opaque) diff --git a/include/libavformat/avformat.pxd b/include/libavformat/avformat.pxd index 6c23e99b4..09eede855 100644 --- a/include/libavformat/avformat.pxd +++ b/include/libavformat/avformat.pxd @@ -125,7 +125,6 @@ cdef extern from "libavformat/avformat.h" nogil: AVFMT_NOBINSEARCH AVFMT_NOGENSEARCH AVFMT_NO_BYTE_SEEK - AVFMT_ALLOW_FLUSH AVFMT_TS_NONSTRICT AVFMT_TS_NEGATIVE AVFMT_SEEK_TO_PTS @@ -145,7 +144,6 @@ cdef extern from "libavformat/avformat.h" nogil: AVFMT_FLAG_BITEXACT AVFMT_FLAG_SORT_DTS AVFMT_FLAG_FAST_SEEK - AVFMT_FLAG_SHORTEST AVFMT_FLAG_AUTO_BSF cdef int av_probe_input_buffer( diff --git a/scripts/activate.sh b/scripts/activate.sh index ab22d79d9..81240cafd 100755 --- a/scripts/activate.sh +++ b/scripts/activate.sh @@ -21,7 +21,7 @@ if [[ ! "$PYAV_LIBRARY" ]]; then return 1 fi else - PYAV_LIBRARY=ffmpeg-7.1.1 + PYAV_LIBRARY=ffmpeg-8.0 echo "No \$PYAV_LIBRARY set; defaulting to $PYAV_LIBRARY" fi fi diff --git a/scripts/ffmpeg-7.0.json b/scripts/ffmpeg-8.0.json similarity index 61% rename from scripts/ffmpeg-7.0.json rename to scripts/ffmpeg-8.0.json index 6cbe76b2c..915f5bfc2 100644 --- a/scripts/ffmpeg-7.0.json +++ b/scripts/ffmpeg-8.0.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.0.2-1/ffmpeg-{platform}.tar.gz" -} \ No newline at end of file + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0-1/ffmpeg-{platform}.tar.gz" +} From 724741146893ab0abfd289e38a93b1d9f60014ea Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 27 Aug 2025 18:31:44 -0400 Subject: [PATCH 311/365] Release 15.1.0 --- CHANGELOG.rst | 1 + av/about.py | 2 +- av/codec/context.pyi | 1 - av/codec/context.pyx | 1 - 4 files changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 395910dc4..ce6dc7356 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -21,6 +21,7 @@ v15.1.0 Features: +- Support FFmpeg 8 - Make the `Frame.key_frame` flag writable. v15.0.0 diff --git a/av/about.py b/av/about.py index d5793afcb..9a75d4360 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "15.0.0" +__version__ = "15.1.0" diff --git a/av/codec/context.pyi b/av/codec/context.pyi index 77810d9ed..e0c447186 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -25,7 +25,6 @@ class Flags(IntEnum): four_mv = cast(int, ...) output_corrupt = cast(int, ...) qpel = cast(int, ...) - drop_changed = cast(int, ...) recon_frame = cast(int, ...) copy_opaque = cast(int, ...) frame_duration = cast(int, ...) diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 1d9c412b2..ac294ee10 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -52,7 +52,6 @@ class Flags(IntEnum): four_mv = lib.AV_CODEC_FLAG_4MV output_corrupt = lib.AV_CODEC_FLAG_OUTPUT_CORRUPT qpel = lib.AV_CODEC_FLAG_QPEL - drop_changed = 1 << 5 recon_frame = lib.AV_CODEC_FLAG_RECON_FRAME copy_opaque = lib.AV_CODEC_FLAG_COPY_OPAQUE frame_duration = lib.AV_CODEC_FLAG_FRAME_DURATION From e3f2efd85e8c36c1f7b1a14660e05b5c7909a6e6 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 29 Aug 2025 23:49:14 -0400 Subject: [PATCH 312/365] Don't build i686 binaries Build is failing because it can't find libcurl: ValueError: Cannot repair wheel, because required library "libssl.so.10" could not be located Someone who cares about this can fix it --- .github/workflows/tests.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1de9f0036..788340c60 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -34,8 +34,6 @@ jobs: arch: x86_64 - os: ubuntu-24.04-arm arch: aarch64 - - os: ubuntu-24.04 - arch: i686 - os: ubuntu-24.04 arch: x86_64 - os: windows-latest @@ -67,8 +65,6 @@ jobs: CIBW_SKIP: "pp*" CIBW_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m pytest {package}/tests && mv {project}/av.disabled {project}/av CIBW_TEST_REQUIRES: pytest numpy - # skip tests when there are no binary wheels of numpy - CIBW_TEST_SKIP: "*_i686" run: | pip install cibuildwheel delvewheel cibuildwheel --output-dir dist From 7de657ac3cb702609082eaac9212bd37b58fc9ff Mon Sep 17 00:00:00 2001 From: Kim Minjong Date: Sat, 30 Aug 2025 22:53:00 +0900 Subject: [PATCH 313/365] Add process_command This allows dynamic control of filter parameters without stopping and restarting the filtering process. Wraps avfilter_process_command --------- Co-authored-by: WyattBlue --- av/filter/context.pyi | 3 +++ av/filter/context.pyx | 40 ++++++++++++++++++++++++++++++++ include/libavfilter/avfilter.pxd | 9 +++++++ 3 files changed, 52 insertions(+) diff --git a/av/filter/context.pyi b/av/filter/context.pyi index 13ee480c2..d37febdb9 100644 --- a/av/filter/context.pyi +++ b/av/filter/context.pyi @@ -12,3 +12,6 @@ class FilterContext: def graph(self) -> Graph: ... def push(self, frame: Frame) -> None: ... def pull(self) -> Frame: ... + def process_command( + self, cmd: str, arg: str | None = None, res_len: int = 1024, flags: int = 0 + ) -> str | None: ... diff --git a/av/filter/context.pyx b/av/filter/context.pyx index ba5981ff1..85c23863b 100644 --- a/av/filter/context.pyx +++ b/av/filter/context.pyx @@ -1,3 +1,5 @@ +cimport libav as lib + import weakref from av.audio.frame cimport alloc_audio_frame @@ -132,3 +134,41 @@ cdef class FilterContext: frame._init_user_attributes() frame.time_base = avrational_to_fraction(&self.ptr.inputs[0].time_base) return frame + + def process_command(self, cmd, arg=None, int res_len=1024, int flags=0): + if not cmd: + raise ValueError("Invalid cmd") + + cdef char *c_cmd = NULL + cdef char *c_arg = NULL + + c_cmd = cmd + if arg is not None: + c_arg = arg + + cdef char *c_res = NULL + cdef int ret + cdef bytearray res_buf = None + cdef unsigned char[:] view + cdef bytes b + cdef int nul + + if res_len > 0: + res_buf = bytearray(res_len) + view = res_buf + c_res = &view[0] + else: + c_res = NULL + + with nogil: + ret = lib.avfilter_process_command(self.ptr, c_cmd, c_arg, c_res, res_len, flags) + err_check(ret) + + if res_buf is not None: + b = bytes(res_buf) + nul = b.find(b'\x00') + if nul >= 0: + b = b[:nul] + if b: + return b.decode("utf-8", "strict") + return None diff --git a/include/libavfilter/avfilter.pxd b/include/libavfilter/avfilter.pxd index b1ced248b..8d4c05f6f 100644 --- a/include/libavfilter/avfilter.pxd +++ b/include/libavfilter/avfilter.pxd @@ -62,6 +62,15 @@ cdef extern from "libavfilter/avfilter.h" nogil: # custom cdef set pyav_get_available_filters() + int avfilter_process_command(AVFilterContext *filter, + const char *cmd, + const char *arg, + char *res, + int res_len, + int flags) + + cdef int AVFILTER_CMD_FLAG_FAST + cdef extern from "libavfilter/buffersink.h" nogil: cdef void av_buffersink_set_frame_size(AVFilterContext *ctx, unsigned frame_size) From a2a7996f346f9220c70b9f4db40d8fd47d4ed44c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 1 Sep 2025 09:01:40 -0400 Subject: [PATCH 314/365] Add ffmpeg-latest.json So no one forgets to update tests.yml in the future --- .github/workflows/smoke.yml | 2 +- .github/workflows/tests.yml | 8 ++++---- scripts/ffmpeg-latest.json | 3 +++ 3 files changed, 8 insertions(+), 5 deletions(-) create mode 100644 scripts/ffmpeg-latest.json diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 3c355a949..b6189e26b 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -109,7 +109,7 @@ jobs: fail-fast: false matrix: config: - - {os: windows-latest, python: "3.9", ffmpeg: "8.0"} + - {os: windows-latest, python: "3.9", ffmpeg: "latest"} steps: - name: Checkout diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 788340c60..94e5d58e1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,7 +14,7 @@ jobs: - name: Build source package run: | pip install -U --pre cython setuptools - python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.1.json /tmp/vendor + python scripts/fetch-vendor.py --config-file scripts/ffmpeg-latest.json /tmp/vendor PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig python setup.py sdist - name: Upload source package uses: actions/upload-artifact@v4 @@ -55,9 +55,9 @@ jobs: - name: Build wheels env: CIBW_ARCHS: ${{ matrix.arch }} - CIBW_BEFORE_BUILD: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.1.json /tmp/vendor - CIBW_BEFORE_BUILD_MACOS: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-7.1.json /tmp/vendor - CIBW_BEFORE_BUILD_WINDOWS: python scripts\fetch-vendor.py --config-file scripts\ffmpeg-7.1.json C:\cibw\vendor + CIBW_BEFORE_BUILD: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-latest.json /tmp/vendor + CIBW_BEFORE_BUILD_MACOS: python scripts/fetch-vendor.py --config-file scripts/ffmpeg-latest.json /tmp/vendor + CIBW_BEFORE_BUILD_WINDOWS: python scripts\fetch-vendor.py --config-file scripts\ffmpeg-latest.json C:\cibw\vendor CIBW_ENVIRONMENT_LINUX: LD_LIBRARY_PATH=/tmp/vendor/lib:$LD_LIBRARY_PATH PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig CIBW_ENVIRONMENT_MACOS: PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig LDFLAGS=-headerpad_max_install_names CIBW_ENVIRONMENT_WINDOWS: INCLUDE=C:\\cibw\\vendor\\include LIB=C:\\cibw\\vendor\\lib PYAV_SKIP_TESTS=unicode_filename diff --git a/scripts/ffmpeg-latest.json b/scripts/ffmpeg-latest.json new file mode 100644 index 000000000..915f5bfc2 --- /dev/null +++ b/scripts/ffmpeg-latest.json @@ -0,0 +1,3 @@ +{ + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0-1/ffmpeg-{platform}.tar.gz" +} From 5f338961d0c7c2c87e32a8e89fd0fbce96a23772 Mon Sep 17 00:00:00 2001 From: DE-AI <81620697+DE-AI@users.noreply.github.com> Date: Mon, 1 Sep 2025 17:41:48 +0200 Subject: [PATCH 315/365] Binding to AVChapter and chapters functions in Container class --- Makefile | 2 +- av/container/core.pyi | 11 +++++++-- av/container/core.pyx | 18 +++++++++++++- include/libavformat/avformat.pxd | 11 +++++++++ tests/test_chapters.py | 41 ++++++++++++++++++++++++++++++++ 5 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 tests/test_chapters.py diff --git a/Makefile b/Makefile index 135d091ad..4858add5b 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ fate-suite: rsync -vrltLW rsync://fate-suite.ffmpeg.org/fate-suite/ tests/assets/fate-suite/ lint: - $(PIP) install -U ruff isort pillow numpy mypy==1.16.1 pytest + $(PIP) install -U ruff isort pillow numpy mypy==1.17.1 pytest ruff format --check av examples tests setup.py isort --check-only --diff av examples tests mypy av tests diff --git a/av/container/core.pyi b/av/container/core.pyi index d61d07110..8cd2a9dc5 100644 --- a/av/container/core.pyi +++ b/av/container/core.pyi @@ -2,7 +2,7 @@ from enum import Flag, IntEnum from fractions import Fraction from pathlib import Path from types import TracebackType -from typing import Any, Callable, ClassVar, Literal, Type, cast, overload +from typing import Any, Callable, ClassVar, Literal, Type, TypedDict, cast, overload from av.codec.hwaccel import HWAccel from av.format import ContainerFormat @@ -67,6 +67,13 @@ class AudioCodec(IntEnum): pcm_u8 = cast(int, ...) pcm_vidc = cast(int, ...) +class _Chapter(TypedDict): + id: int + start: int + end: int + time_base: Fraction | None + metadata: dict[str, str] + class Container: writeable: bool name: str @@ -86,7 +93,6 @@ class Container: open_timeout: Real | None read_timeout: Real | None flags: int - def __enter__(self) -> Container: ... def __exit__( self, @@ -96,6 +102,7 @@ class Container: ) -> bool: ... def set_timeout(self, timeout: Real | None) -> None: ... def start_timeout(self) -> None: ... + def chapters(self) -> list[_Chapter]: ... @overload def open( diff --git a/av/container/core.pyx b/av/container/core.pyx index d8950c76f..de7a07dda 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -15,7 +15,7 @@ from av.container.output cimport OutputContainer from av.container.pyio cimport pyio_close_custom_gil, pyio_close_gil from av.error cimport err_check, stash_exception from av.format cimport build_container_format -from av.utils cimport avdict_to_dict +from av.utils cimport avdict_to_dict, avrational_to_fraction from av.dictionary import Dictionary from av.logging import Capture as LogCapture @@ -330,6 +330,22 @@ cdef class Container: self._assert_open() self.ptr.flags = value + def chapters(self): + self._assert_open() + cdef list result = [] + cdef int i + + for i in range(self.ptr.nb_chapters): + ch = self.ptr.chapters[i] + result.append({ + "id": ch.id, + "start": ch.start, + "end": ch.end, + "time_base": avrational_to_fraction(&ch.time_base), + "metadata": avdict_to_dict(ch.metadata, self.metadata_encoding, self.metadata_errors), + }) + return result + def open( file, mode=None, diff --git a/include/libavformat/avformat.pxd b/include/libavformat/avformat.pxd index 09eede855..3816b46fa 100644 --- a/include/libavformat/avformat.pxd +++ b/include/libavformat/avformat.pxd @@ -44,6 +44,14 @@ cdef extern from "libavformat/avformat.h" nogil: AVRational r_frame_rate AVRational sample_aspect_ratio + cdef struct AVChapter: + int id + int64_t start + int64_t end + AVRational time_base + AVDictionary *metadata + + # http://ffmpeg.org/doxygen/trunk/structAVIOContext.html cdef struct AVIOContext: unsigned char* buffer @@ -173,6 +181,9 @@ cdef extern from "libavformat/avformat.h" nogil: unsigned int nb_streams AVStream **streams + unsigned int nb_chapters + AVChapter **chapters + AVInputFormat *iformat AVOutputFormat *oformat diff --git a/tests/test_chapters.py b/tests/test_chapters.py new file mode 100644 index 000000000..6a3d371b2 --- /dev/null +++ b/tests/test_chapters.py @@ -0,0 +1,41 @@ +from fractions import Fraction + +import av + +from .common import fate_suite + + +def test_chapters() -> None: + expected = [ + { + "id": 1, + "start": 0, + "end": 5000, + "time_base": Fraction(1, 1000), + "metadata": {"title": "start"}, + }, + { + "id": 2, + "start": 5000, + "end": 10500, + "time_base": Fraction(1, 1000), + "metadata": {"title": "Five Seconds"}, + }, + { + "id": 3, + "start": 10500, + "end": 15000, + "time_base": Fraction(1, 1000), + "metadata": {"title": "Ten point 5 seconds"}, + }, + { + "id": 4, + "start": 15000, + "end": 19849, + "time_base": Fraction(1, 1000), + "metadata": {"title": "15 sec - over soon"}, + }, + ] + path = fate_suite("vorbis/vorbis_chapter_extension_demo.ogg") + with av.open(path) as container: + assert container.chapters() == expected From 0d988f038042507a5c3fe854b24061749df30905 Mon Sep 17 00:00:00 2001 From: Kian-Meng Ang Date: Tue, 2 Sep 2025 00:59:48 +0800 Subject: [PATCH 316/365] Fix typos Found via `codespell -L wel,domin,ehr,bu,setts` --- AUTHORS.py | 2 +- CHANGELOG.rst | 2 +- av/container/core.pyx | 2 +- av/container/input.pyx | 2 +- av/error.pyx | 4 ++-- av/sidedata/motionvectors.pyx | 2 +- av/sidedata/sidedata.pyx | 2 +- av/video/codeccontext.pyx | 4 ++-- av/video/format.pyx | 2 +- docs/api/video.rst | 2 +- tests/test_bitstream.py | 2 +- tests/test_encode.py | 2 +- 12 files changed, 14 insertions(+), 14 deletions(-) diff --git a/AUTHORS.py b/AUTHORS.py index 797e423ed..ad0746641 100644 --- a/AUTHORS.py +++ b/AUTHORS.py @@ -94,7 +94,7 @@ github = github_map.get(email) # The '-' vs '*' is so that Sphinx treats them as different lists, and - # introduces a gap bettween them. + # introduces a gap between them. if github: print( "%s %s <%s>; `@%s `_" diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ce6dc7356..04130c2b7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -559,7 +559,7 @@ Build: Other: - Incremental improvements to docs and tests. -- Examples directory will now always be runnable as-is, and embeded in the docs (in a copy-pastable form). +- Examples directory will now always be runnable as-is, and embedded in the docs (in a copy-pastable form). v6.0.0 diff --git a/av/container/core.pyx b/av/container/core.pyx index de7a07dda..076faea15 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -130,7 +130,7 @@ class Flags(Flag): non_block: "Do not block when reading packets from input." = lib.AVFMT_FLAG_NONBLOCK ign_dts: "Ignore DTS on frames that contain both DTS & PTS." = lib.AVFMT_FLAG_IGNDTS no_fillin: "Do not infer any values from other values, just return what is stored in the container." = lib.AVFMT_FLAG_NOFILLIN - no_parse: "Do not use AVParsers, you also must set AVFMT_FLAG_NOFILLIN as the fillin code works on frames and no parsing -> no frames. Also seeking to frames can not work if parsing to find frame boundaries has been disabled." = lib.AVFMT_FLAG_NOPARSE + no_parse: "Do not use AVParsers, you also must set AVFMT_FLAG_NOFILLIN as the fill in code works on frames and no parsing -> no frames. Also seeking to frames can not work if parsing to find frame boundaries has been disabled." = lib.AVFMT_FLAG_NOPARSE no_buffer: "Do not buffer frames when possible." = lib.AVFMT_FLAG_NOBUFFER custom_io: "The caller has supplied a custom AVIOContext, don't avio_close() it." = lib.AVFMT_FLAG_CUSTOM_IO discard_corrupt: "Discard frames marked corrupted." = lib.AVFMT_FLAG_DISCARD_CORRUPT diff --git a/av/container/input.pyx b/av/container/input.pyx index 943081e15..56af7b05c 100644 --- a/av/container/input.pyx +++ b/av/container/input.pyx @@ -241,7 +241,7 @@ cdef class InputContainer(Container): In most cases, the defaults of ``backwards = True`` and ``any_frame = False`` are the best course of action, followed by you demuxing/decoding to - the position that you want. This is becase to properly decode video frames + the position that you want. This is because to properly decode video frames you need to start from the previous keyframe. .. seealso:: :ffmpeg:`avformat_seek_file` for discussion of the flags. diff --git a/av/error.pyx b/av/error.pyx index c3d8a3e7b..123ed735f 100644 --- a/av/error.pyx +++ b/av/error.pyx @@ -267,7 +267,7 @@ ErrorType = EnumType("ErrorType", (EnumItem, ), {"__module__": __name__}, [x[:2] for enum in ErrorType: - # Mimick the errno module. + # Mimic the errno module. globals()[enum.name] = enum if enum.value == c_PYAV_STASHED_ERROR: enum.strerror = PYAV_STASHED_ERROR_message @@ -275,7 +275,7 @@ for enum in ErrorType: enum.strerror = lib.av_err2str(-enum.value) -# Mimick the builtin exception types. +# Mimic the builtin exception types. # See https://www.python.org/dev/peps/pep-3151/#new-exception-classes # Use the named ones we have, otherwise default to OSError for anything in errno. diff --git a/av/sidedata/motionvectors.pyx b/av/sidedata/motionvectors.pyx index b0b0b705f..8a4200232 100644 --- a/av/sidedata/motionvectors.pyx +++ b/av/sidedata/motionvectors.pyx @@ -55,7 +55,7 @@ class MotionVectors(_MotionVectors, Sequence): cdef class MotionVector: def __init__(self, sentinel, _MotionVectors parent, int index): if sentinel is not _cinit_bypass_sentinel: - raise RuntimeError("cannot manually instatiate MotionVector") + raise RuntimeError("cannot manually instantiate MotionVector") self.parent = parent cdef lib.AVMotionVector *base = parent.ptr.data self.ptr = base + index diff --git a/av/sidedata/sidedata.pyx b/av/sidedata/sidedata.pyx index 24dbae119..65b1387f0 100644 --- a/av/sidedata/sidedata.pyx +++ b/av/sidedata/sidedata.pyx @@ -63,7 +63,7 @@ cdef int get_display_rotation(Frame frame): cdef class SideData(Buffer): def __init__(self, sentinel, Frame frame, int index): if sentinel is not _cinit_bypass_sentinel: - raise RuntimeError("cannot manually instatiate SideData") + raise RuntimeError("cannot manually instantiate SideData") self.frame = frame self.ptr = frame.ptr.side_data[index] self.metadata = wrap_dictionary(self.ptr.metadata) diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.pyx index c9d8eb4c0..e82e56e91 100644 --- a/av/video/codeccontext.pyx +++ b/av/video/codeccontext.pyx @@ -217,13 +217,13 @@ cdef class VideoCodecContext(CodecContext): :type: int """ if self.is_decoder: - raise RuntimeError("Cannnot access 'gop_size' as a decoder") + raise RuntimeError("Cannot access 'gop_size' as a decoder") return self.ptr.gop_size @gop_size.setter def gop_size(self, int value): if self.is_decoder: - raise RuntimeError("Cannnot access 'gop_size' as a decoder") + raise RuntimeError("Cannot access 'gop_size' as a decoder") self.ptr.gop_size = value @property diff --git a/av/video/format.pyx b/av/video/format.pyx index 8779f7151..4e66a8836 100644 --- a/av/video/format.pyx +++ b/av/video/format.pyx @@ -158,7 +158,7 @@ cdef class VideoFormatComponent: @property def is_luma(self): - """Is this compoment a luma channel?""" + """Is this component a luma channel?""" return self.index == 0 and ( self.format.ptr.nb_components == 1 or self.format.ptr.nb_components == 2 or diff --git a/docs/api/video.rst b/docs/api/video.rst index 1c56788a1..7e97e38c8 100644 --- a/docs/api/video.rst +++ b/docs/api/video.rst @@ -113,7 +113,7 @@ Enums .. autoclass:: av.video.reformatter.Colorspace Wraps the ``SWS_CS_*`` flags. There is a bit of overlap in - these names which comes from FFmpeg and backards compatibility. + these names which comes from FFmpeg and backwards compatibility. .. enumtable:: av.video.reformatter.Colorspace diff --git a/tests/test_bitstream.py b/tests/test_bitstream.py index 26e54599b..dde7723c7 100644 --- a/tests/test_bitstream.py +++ b/tests/test_bitstream.py @@ -17,7 +17,7 @@ def is_annexb(packet: Packet | bytes | None) -> bool: return data[:3] == b"\0\0\x01" or data[:4] == b"\0\0\0\x01" -def test_filters_availible() -> None: +def test_filters_available() -> None: assert "h264_mp4toannexb" in bitstream_filters_available diff --git a/tests/test_encode.py b/tests/test_encode.py index 9284a9c82..d64cadc20 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -415,7 +415,7 @@ def encode_frames_with_qminmax( frames: list[VideoFrame], shape: tuple[int, int, int], qminmax: tuple[int, int] ) -> int: """ - Encode a video with the given quantiser limits, and return how many enocded + Encode a video with the given quantiser limits, and return how many encoded bytes we made in total. frames: the frames to encode From 8105052b77c359fa8f26623e6ccd4d110b2ebf50 Mon Sep 17 00:00:00 2001 From: Kesh Ikuma <79113787+tikuma-lsuhsc@users.noreply.github.com> Date: Mon, 1 Sep 2025 22:23:58 -0500 Subject: [PATCH 317/365] Add packet side-data handling mechanism Adds ability to extract (copy) a side data from one packet and apply it to another packet --------- Co-authored-by: WyattBlue --- av/packet.pxd | 7 ++ av/packet.py | 219 +++++++++++++++++++++++++++++++++ av/packet.pyi | 64 ++++++++++ include/libavcodec/avcodec.pxd | 15 ++- tests/test_packet.py | 45 +++++++ 5 files changed, 349 insertions(+), 1 deletion(-) diff --git a/av/packet.pxd b/av/packet.pxd index f1517a3d4..8e8ad034d 100644 --- a/av/packet.pxd +++ b/av/packet.pxd @@ -1,3 +1,5 @@ +from cython.cimports.libc.stdint import uint8_t + cimport libav as lib from av.buffer cimport Buffer @@ -5,6 +7,11 @@ from av.bytesource cimport ByteSource from av.stream cimport Stream +cdef class PacketSideData: + cdef uint8_t *data + cdef size_t size + cdef lib.AVPacketSideDataType dtype + cdef class Packet(Buffer): cdef lib.AVPacket* ptr cdef Stream _stream diff --git a/av/packet.py b/av/packet.py index 81f1aaa4d..e1051b653 100644 --- a/av/packet.py +++ b/av/packet.py @@ -1,9 +1,183 @@ +from typing import Iterator, Literal, get_args + import cython from cython.cimports import libav as lib from cython.cimports.av.bytesource import bytesource from cython.cimports.av.error import err_check from cython.cimports.av.opaque import opaque_container from cython.cimports.av.utils import avrational_to_fraction, to_avrational +from cython.cimports.libc.string import memcpy + +# Check https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/packet.h#L41 +# for new additions in the future ffmpeg releases +# Note: the order must follow that of the AVPacketSideDataType enum def +PktSideDataT = Literal[ + "palette", + "new_extradata", + "param_change", + "h263_mb_info", + "replay_gain", + "display_matrix", + "stereo_3d", + "audio_service_type", + "quality_stats", + "fallback_track", + "cpb_properties", + "skip_samples", + "jp_dual_mono", + "strings_metadata", + "subtitle_position", + "matroska_block_additional", + "webvtt_identifier", + "webvtt_settings", + "metadata_update", + "mpegts_stream_id", + "mastering_display_metadata", + "spherical", + "content_light_level", + "a53_cc", + "encryption_init_info", + "encryption_info", + "afd", + "prft", + "icc_profile", + "dovi_conf", + "s12m_timecode", + "dynamic_hdr10_plus", + "iamf_mix_gain_param", + "iamf_info_param", + "iamf_recon_gain_info_param", + "ambient_viewing_environment", + "frame_cropping", + "lcevc", + "3d_reference_displays", + "rtcp_sr", +] + + +def packet_sidedata_type_to_literal(dtype: lib.AVPacketSideDataType) -> PktSideDataT: + return get_args(PktSideDataT)[cython.cast(int, dtype)] + + +def packet_sidedata_type_from_literal(dtype: PktSideDataT) -> lib.AVPacketSideDataType: + return get_args(PktSideDataT).index(dtype) + + +@cython.cclass +class PacketSideData: + @staticmethod + def from_packet(packet: Packet, data_type: PktSideDataT) -> PacketSideData: + """create new PacketSideData by copying an existing packet's side data + + :param packet: Source packet + :type packet: :class:`~av.packet.Packet` + :param data_type: side data type + :return: newly created copy of the side data if the side data of the + requested type is found in the packet, else an empty object + :rtype: :class:`~av.packet.PacketSideData` + """ + + dtype = packet_sidedata_type_from_literal(data_type) + return _packet_sidedata_from_packet(packet.ptr, dtype) + + def __cinit__(self, dtype: lib.AVPacketSideDataType, size: cython.size_t): + self.dtype = dtype + with cython.nogil: + if size: + self.data = cython.cast(cython.p_uchar, lib.av_malloc(size)) + if self.data == cython.NULL: + raise MemoryError("Failed to allocate memory") + else: + self.data = cython.NULL + self.size = size + + def __dealloc__(self): + with cython.nogil: + lib.av_freep(cython.address(self.data)) + + def to_packet(self, packet: Packet, move: cython.bint = False): + """copy or move side data to the specified packet + + :param packet: Target packet + :type packet: :class:`~av.packet.Packet` + :param move: True to move the data from this object to the packet, + defaults to False. + :type move: bool + """ + if self.size == 0: + # nothing to add, should clear existing side_data in packet? + return + + data = self.data + + with cython.nogil: + if not move: + data = cython.cast(cython.p_uchar, lib.av_malloc(self.size)) + if data == cython.NULL: + raise MemoryError("Failed to allocate memory") + memcpy(data, self.data, self.size) + + res = lib.av_packet_add_side_data(packet.ptr, self.dtype, data, self.size) + err_check(res) + + if move: + self.data = cython.NULL + self.size = 0 + + @property + def data_type(self) -> str: + """ + The type of this packet side data. + + :type: str + """ + return packet_sidedata_type_to_literal(self.dtype) + + @property + def data_desc(self) -> str: + """ + The description of this packet side data type. + + :type: str + """ + + return lib.av_packet_side_data_name(self.dtype) + + @property + def data_size(self) -> int: + """ + The size in bytes of this packet side data. + + :type: int + """ + return self.size + + def __bool__(self) -> bool: + """ + True if this object holds side data. + + :type: bool + """ + return self.data != cython.NULL + + +@cython.cfunc +def _packet_sidedata_from_packet( + packet: cython.pointer[lib.AVPacket], dtype: lib.AVPacketSideDataType +) -> PacketSideData: + with cython.nogil: + c_ptr = lib.av_packet_side_data_get( + packet.side_data, packet.side_data_elems, dtype + ) + found: cython.bint = c_ptr != cython.NULL + + sdata = PacketSideData(dtype, c_ptr.size if found else 0) + + with cython.nogil: + if found: + memcpy(sdata.data, c_ptr.data, c_ptr.size) + + return sdata @cython.cclass @@ -235,3 +409,48 @@ def opaque(self, v): if v is None: return self.ptr.opaque_ref = opaque_container.add(v) + + def has_sidedata(self, dtype: str) -> bool: + """True if this packet has the specified side data + + :param dtype: side data type + :type dtype: str + """ + + dtype2 = packet_sidedata_type_from_literal(dtype) + return ( + lib.av_packet_side_data_get( + self.ptr.side_data, self.ptr.side_data_elems, dtype2 + ) + != cython.NULL + ) + + def get_sidedata(self, dtype: str) -> PacketSideData: + """get a copy of the side data + + :param dtype: side data type (:method:`~av.packet.PacketSideData.sidedata_types` for the full list of options) + :type dtype: str + :return: newly created copy of the side data if the side data of the + requested type is found in the packet, else an empty object + :rtype: :class:`~av.packet.PacketSideData` + """ + return PacketSideData.from_packet(self, dtype) + + def set_sidedata(self, sidedata: PacketSideData, move: cython.bint = False): + """copy or move side data to this packet + + :param sidedata: Source packet side data + :type sidedata: :class:`~av.packet.PacketSideData` + :param move: If True, move the data from `sidedata` object, defaults to False + :type move: bool + """ + sidedata.to_packet(self, move) + + def iter_sidedata(self) -> Iterator[PacketSideData]: + """iterate over side data of this packet. + + :yield: :class:`~av.packet.PacketSideData` object + """ + + for i in range(self.ptr.side_data_elems): + yield _packet_sidedata_from_packet(self.ptr, self.ptr.side_data[i].type) diff --git a/av/packet.pyi b/av/packet.pyi index baa234d7b..8dab20065 100644 --- a/av/packet.pyi +++ b/av/packet.pyi @@ -1,10 +1,70 @@ from fractions import Fraction +from typing import Iterator, Literal from av.subtitles.subtitle import SubtitleSet from .buffer import Buffer from .stream import Stream +# Sync with definition in 'packet.py' +PktSideDataT = Literal[ + "palette", + "new_extradata", + "param_change", + "h263_mb_info", + "replay_gain", + "display_matrix", + "stereo_3d", + "audio_service_type", + "quality_stats", + "fallback_track", + "cpb_properties", + "skip_samples", + "jp_dual_mono", + "strings_metadata", + "subtitle_position", + "matroska_block_additional", + "webvtt_identifier", + "webvtt_settings", + "metadata_update", + "mpegts_stream_id", + "mastering_display_metadata", + "spherical", + "content_light_level", + "a53_cc", + "encryption_init_info", + "encryption_info", + "afd", + "prft", + "icc_profile", + "dovi_conf", + "s12m_timecode", + "dynamic_hdr10_plus", + "iamf_mix_gain_param", + "iamf_info_param", + "iamf_recon_gain_info_param", + "ambient_viewing_environment", + "frame_cropping", + "lcevc", + "3d_reference_displays", + "rtcp_sr", +] + +class PacketSideData: + @staticmethod + def from_packet(packet: Packet, dtype: PktSideDataT) -> PacketSideData: ... + def to_packet(self, packet: Packet, move: bool = False): ... + @property + def data_type(self) -> str: ... + @property + def data_desc(self) -> str: ... + @property + def data_size(self) -> int: ... + def __bool__(self) -> bool: ... + +def packet_sidedata_type_to_literal(dtype: int) -> PktSideDataT: ... +def packet_sidedata_type_from_literal(dtype: PktSideDataT) -> int: ... + class Packet(Buffer): stream: Stream stream_index: int @@ -23,3 +83,7 @@ class Packet(Buffer): def __init__(self, input: int | bytes | None = None) -> None: ... def decode(self) -> list[SubtitleSet]: ... + def has_sidedata(self, dtype: PktSideDataT) -> bool: ... + def get_sidedata(self, dtype: PktSideDataT) -> PacketSideData: ... + def set_sidedata(self, sidedata: PacketSideData, move: bool = False) -> None: ... + def iter_sidedata(self) -> Iterator[PacketSideData]: ... diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 2bce8c2c1..43ecb0260 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -1,4 +1,4 @@ -from libc.stdint cimport int8_t, int64_t, uint16_t, uint32_t +from libc.stdint cimport int8_t, int64_t, uint16_t, uint32_t, uint8_t cdef extern from "libavcodec/codec.h": struct AVCodecTag: @@ -17,6 +17,17 @@ cdef extern from "libavcodec/packet.h" nogil: int free_opaque ) + const AVPacketSideData *av_packet_side_data_get(const AVPacketSideData *sd, + int nb_sd, + AVPacketSideDataType type) + + uint8_t* av_packet_get_side_data(const AVPacket *pkt, AVPacketSideDataType type, + size_t *size) + + int av_packet_add_side_data(AVPacket *pkt, AVPacketSideDataType type, + uint8_t *data, size_t size) + + const char *av_packet_side_data_name(AVPacketSideDataType type) cdef extern from "libavutil/channel_layout.h": ctypedef enum AVChannelOrder: @@ -469,6 +480,8 @@ cdef extern from "libavcodec/avcodec.h" nogil: int size int stream_index int flags + AVPacketSideData *side_data + int side_data_elems int duration int64_t pos void *opaque diff --git a/tests/test_packet.py b/tests/test_packet.py index 423396f71..d5552238c 100644 --- a/tests/test_packet.py +++ b/tests/test_packet.py @@ -1,3 +1,5 @@ +from typing import get_args + import av from .common import fate_suite @@ -48,3 +50,46 @@ def test_set_duration(self) -> None: packet.duration += 10 assert packet.duration == old_duration + 10 + + +class TestPacketSideData: + def test_data_types(self) -> None: + dtypes = get_args(av.packet.PktSideDataT) + ffmpeg_ver = [int(v) for v in av.ffmpeg_version_info.split(".", 2)[:2]] + for dtype in dtypes: + av_enum = av.packet.packet_sidedata_type_from_literal(dtype) + assert dtype == av.packet.packet_sidedata_type_to_literal(av_enum) + + if (ffmpeg_ver[0] < 8 and dtype == "lcevc") or ( + ffmpeg_ver[0] < 9 and dtype == "rtcp_sr" + ): + break + + def test_iter(self) -> None: + with av.open(fate_suite("h264/extradata-reload-multi-stsd.mov")) as container: + for pkt in container.demux(): + for sdata in pkt.iter_sidedata(): + assert pkt.dts == 2 and sdata.data_type == "new_extradata" + + def test_palette(self) -> None: + with av.open(fate_suite("h264/extradata-reload-multi-stsd.mov")) as container: + iterpackets = container.demux() + pkt = next(pkt for pkt in iterpackets if pkt.has_sidedata("new_extradata")) + + sdata = pkt.get_sidedata("new_extradata") + assert sdata.data_type == "new_extradata" + assert bool(sdata) + assert sdata.data_size > 0 + assert sdata.data_desc == "New Extradata" + + nxt = next(iterpackets) # has no palette + + assert not nxt.has_sidedata("new_extradata") + + sdata1 = nxt.get_sidedata("new_extradata") + assert sdata1.data_type == "new_extradata" + assert not bool(sdata1) + assert sdata1.data_size == 0 + + nxt.set_sidedata(sdata, move=True) + assert not bool(sdata) From ac1fd125f354a31867f2ad519f8151ddc9f354e7 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 3 Sep 2025 09:51:42 -0600 Subject: [PATCH 318/365] Declare free-threaded support and support 3.13t --- .github/workflows/smoke.yml | 2 ++ .github/workflows/tests.yml | 1 + setup.py | 1 + 3 files changed, 4 insertions(+) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index b6189e26b..da2c4dee0 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -34,6 +34,7 @@ jobs: fail-fast: false matrix: config: + - {os: ubuntu-24.04, python: "3.14t", ffmpeg: "8.0", extras: true} - {os: ubuntu-24.04, python: "3.12", ffmpeg: "8.0", extras: true} - {os: ubuntu-24.04, python: "pypy3.10", ffmpeg: "8.0"} - {os: macos-14, python: "3.9", ffmpeg: "8.0"} @@ -50,6 +51,7 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.config.python }} + allow-prereleases: true - name: OS Packages run: | diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 94e5d58e1..cb38c2bfc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -65,6 +65,7 @@ jobs: CIBW_SKIP: "pp*" CIBW_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m pytest {package}/tests && mv {project}/av.disabled {project}/av CIBW_TEST_REQUIRES: pytest numpy + CIBW_ENABLE: cpython-freethreading run: | pip install cibuildwheel delvewheel cibuildwheel --output-dir dist diff --git a/setup.py b/setup.py index ebc2146fe..ce6bcc388 100644 --- a/setup.py +++ b/setup.py @@ -150,6 +150,7 @@ def parse_cflags(raw_flags): "embedsignature": True, "binding": False, "language_level": 3, + "freethreading_compatible": True, } # Add the cythonized loudnorm extension to ext_modules From 6dd3dfd3b9d045903dd9e5ec8e0b1546fc67894d Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 3 Sep 2025 16:45:19 -0400 Subject: [PATCH 319/365] Add changelog items --- CHANGELOG.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 04130c2b7..7a445525e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,20 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. +v16.0.0 +------- +(unreleased) + +Major: +- Drop Python 3.9, Support Python 3.14. + +Features: +- Declare free-threaded support and support 3.13t by :gh-user:`ngoldbaum` in (:pr:`2005`). +- Add ``Filter.Context.process_command()`` method by :gh-user:`caffeinism` in (:pr:`2000`). + +Fixes: +- Fix typos + v15.1.0 ------- From 1aced8f0ebf32f127ac3468c2ff7acb4ffe79ed4 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 3 Sep 2025 16:59:38 -0400 Subject: [PATCH 320/365] Make utils pure --- av/option.pyx | 10 ++++-- av/utils.pxd | 5 +-- av/utils.py | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++ av/utils.pyx | 78 ---------------------------------------------- 4 files changed, 95 insertions(+), 84 deletions(-) create mode 100644 av/utils.py delete mode 100644 av/utils.pyx diff --git a/av/option.pyx b/av/option.pyx index e58c4c13f..604d81a53 100644 --- a/av/option.pyx +++ b/av/option.pyx @@ -1,6 +1,5 @@ cimport libav as lib - -from av.utils cimport flag_in_bitfield +from libc.stdint cimport uint64_t from enum import Enum, Flag @@ -16,6 +15,13 @@ cdef Option wrap_option(tuple choices, const lib.AVOption *ptr): return obj +cdef flag_in_bitfield(uint64_t bitfield, uint64_t flag): + # Not every flag exists in every version of FFMpeg, so we define them to 0. + if not flag: + return None + return bool(bitfield & flag) + + class OptionType(Enum): FLAGS = lib.AV_OPT_TYPE_FLAGS INT = lib.AV_OPT_TYPE_INT diff --git a/av/utils.pxd b/av/utils.pxd index 9aeb4a2fb..60279e513 100644 --- a/av/utils.pxd +++ b/av/utils.pxd @@ -1,5 +1,4 @@ cimport libav as lib -from libc.stdint cimport uint64_t cdef dict avdict_to_dict(lib.AVDictionary *input, str encoding, str errors) @@ -7,6 +6,4 @@ cdef dict_to_avdict(lib.AVDictionary **dst, dict src, str encoding, str errors) cdef object avrational_to_fraction(const lib.AVRational *input) cdef void to_avrational(object frac, lib.AVRational *input) - -cdef check_ndarray(object array, object dtype, int ndim) -cdef flag_in_bitfield(uint64_t bitfield, uint64_t flag) +cdef void check_ndarray(object array, object dtype, int ndim) diff --git a/av/utils.py b/av/utils.py new file mode 100644 index 000000000..3516be0e3 --- /dev/null +++ b/av/utils.py @@ -0,0 +1,86 @@ +# type: ignore +from fractions import Fraction + +import cython +from cython.cimports import libav as lib +from cython.cimports.av.error import err_check + +# === DICTIONARIES === +# ==================== + + +@cython.cfunc +def _decode(s: cython.pointer[cython.char], encoding, errors) -> str: + return cython.cast(bytes, s).decode(encoding, errors) + + +@cython.cfunc +def _encode(s, encoding, errors) -> bytes: + return s.encode(encoding, errors) + + +@cython.cfunc +def avdict_to_dict( + input: cython.pointer[lib.AVDictionary], encoding: str, errors: str +) -> dict: + element: cython.pointer[lib.AVDictionaryEntry] = cython.NULL + output: dict = {} + while True: + element = lib.av_dict_get(input, "", element, lib.AV_DICT_IGNORE_SUFFIX) + if element == cython.NULL: + break + output[_decode(element.key, encoding, errors)] = _decode( + element.value, encoding, errors + ) + + return output + + +@cython.cfunc +def dict_to_avdict( + dst: cython.pointer[cython.pointer[lib.AVDictionary]], + src: dict, + encoding: str, + errors: str, +): + lib.av_dict_free(dst) + for key, value in src.items(): + err_check( + lib.av_dict_set( + dst, key.encode(encoding, errors), value.encode(encoding, errors), 0 + ) + ) + + +# === FRACTIONS === +# ================= + + +@cython.cfunc +def avrational_to_fraction( + input: cython.pointer[cython.const[lib.AVRational]], +) -> object: + if input.num and input.den: + return Fraction(input.num, input.den) + return None + + +@cython.cfunc +def to_avrational(frac: object, input: cython.pointer[lib.AVRational]) -> cython.void: + input.num = frac.numerator + input.den = frac.denominator + + +@cython.cfunc +def check_ndarray(array: object, dtype: object, ndim: cython.int) -> cython.void: + """ + Check a numpy array has the expected data type and number of dimensions. + """ + if array.dtype != dtype: + raise ValueError( + f"Expected numpy array with dtype `{dtype}` but got `{array.dtype}`" + ) + if array.ndim != ndim: + raise ValueError( + f"Expected numpy array with ndim `{ndim}` but got `{array.ndim}`" + ) diff --git a/av/utils.pyx b/av/utils.pyx deleted file mode 100644 index 190bbf4d7..000000000 --- a/av/utils.pyx +++ /dev/null @@ -1,78 +0,0 @@ -from libc.stdint cimport uint64_t - -from fractions import Fraction - -cimport libav as lib - -from av.error cimport err_check - -# === DICTIONARIES === -# ==================== - -cdef _decode(char *s, encoding, errors): - return (s).decode(encoding, errors) - -cdef bytes _encode(s, encoding, errors): - return s.encode(encoding, errors) - -cdef dict avdict_to_dict(lib.AVDictionary *input, str encoding, str errors): - cdef lib.AVDictionaryEntry *element = NULL - cdef dict output = {} - while True: - element = lib.av_dict_get(input, "", element, lib.AV_DICT_IGNORE_SUFFIX) - if element == NULL: - break - output[_decode(element.key, encoding, errors)] = _decode(element.value, encoding, errors) - return output - - -cdef dict_to_avdict(lib.AVDictionary **dst, dict src, str encoding, str errors): - lib.av_dict_free(dst) - for key, value in src.items(): - err_check( - lib.av_dict_set( - dst, - _encode(key, encoding, errors), - _encode(value, encoding, errors), - 0 - ) - ) - - -# === FRACTIONS === -# ================= - -cdef object avrational_to_fraction(const lib.AVRational *input): - if input.num and input.den: - return Fraction(input.num, input.den) - - -cdef void to_avrational(object frac, lib.AVRational *input): - input.num = frac.numerator - input.den = frac.denominator - - -# === OTHER === -# ============= - - -cdef check_ndarray(object array, object dtype, int ndim): - """ - Check a numpy array has the expected data type and number of dimensions. - """ - if array.dtype != dtype: - raise ValueError(f"Expected numpy array with dtype `{dtype}` but got `{array.dtype}`") - if array.ndim != ndim: - raise ValueError(f"Expected numpy array with ndim `{ndim}` but got `{array.ndim}`") - - -cdef flag_in_bitfield(uint64_t bitfield, uint64_t flag): - # Not every flag exists in every version of FFMpeg, so we define them to 0. - if not flag: - return None - return bool(bitfield & flag) - - -# === BACKWARDS COMPAT === - -from .error import err_check From 106089447cab9dc6d3ca2d1ab7e0aeff76d7a6a4 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 3 Sep 2025 18:19:58 -0400 Subject: [PATCH 321/365] Drop support for Python 3.9 --- .github/workflows/smoke.yml | 4 ++-- pyproject.toml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index da2c4dee0..fe5b82687 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -37,7 +37,7 @@ jobs: - {os: ubuntu-24.04, python: "3.14t", ffmpeg: "8.0", extras: true} - {os: ubuntu-24.04, python: "3.12", ffmpeg: "8.0", extras: true} - {os: ubuntu-24.04, python: "pypy3.10", ffmpeg: "8.0"} - - {os: macos-14, python: "3.9", ffmpeg: "8.0"} + - {os: macos-14, python: "3.10", ffmpeg: "8.0"} env: PYAV_PYTHON: python${{ matrix.config.python }} @@ -111,7 +111,7 @@ jobs: fail-fast: false matrix: config: - - {os: windows-latest, python: "3.9", ffmpeg: "latest"} + - {os: windows-latest, python: "3.10", ffmpeg: "latest"} steps: - name: Checkout diff --git a/pyproject.toml b/pyproject.toml index 9710de837..33cde6b2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=77.0", "cython>=3.1.0a1,<4"] +requires = ["setuptools>=77.0", "cython>=3.1.0,<4"] [project] name = "av" @@ -10,7 +10,7 @@ authors = [ {name = "WyattBlue", email = "wyattblue@auto-editor.com"}, {name = "Jeremy Lainé", email = "jeremy.laine@m4x.org"}, ] -requires-python = ">=3.9" +requires-python = ">=3.10" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", @@ -20,11 +20,11 @@ classifiers = [ "Operating System :: Unix", "Operating System :: Microsoft :: Windows", "Programming Language :: Cython", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Multimedia :: Sound/Audio", "Topic :: Multimedia :: Sound/Audio :: Conversion", From 4d603ec63b4d4f55e2bfe97bd2dd0f04cb41f361 Mon Sep 17 00:00:00 2001 From: DE-AI <81620697+DE-AI@users.noreply.github.com> Date: Thu, 4 Sep 2025 14:15:20 +0200 Subject: [PATCH 322/365] Implement set_chapters method --- av/container/core.pyi | 5 +++-- av/container/core.pyx | 51 +++++++++++++++++++++++++++++++++++++++++- tests/test_chapters.py | 17 ++++++++++++++ 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/av/container/core.pyi b/av/container/core.pyi index 8cd2a9dc5..f923ba01e 100644 --- a/av/container/core.pyi +++ b/av/container/core.pyi @@ -67,7 +67,7 @@ class AudioCodec(IntEnum): pcm_u8 = cast(int, ...) pcm_vidc = cast(int, ...) -class _Chapter(TypedDict): +class Chapter(TypedDict): id: int start: int end: int @@ -102,7 +102,8 @@ class Container: ) -> bool: ... def set_timeout(self, timeout: Real | None) -> None: ... def start_timeout(self) -> None: ... - def chapters(self) -> list[_Chapter]: ... + def chapters(self) -> list[Chapter]: ... + def set_chapters(self, chapters: list[Chapter]) -> None: ... @overload def open( diff --git a/av/container/core.pyx b/av/container/core.pyx index 076faea15..299ac6ed3 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -15,7 +15,12 @@ from av.container.output cimport OutputContainer from av.container.pyio cimport pyio_close_custom_gil, pyio_close_gil from av.error cimport err_check, stash_exception from av.format cimport build_container_format -from av.utils cimport avdict_to_dict, avrational_to_fraction +from av.utils cimport ( + avdict_to_dict, + avrational_to_fraction, + dict_to_avdict, + to_avrational, +) from av.dictionary import Dictionary from av.logging import Capture as LogCapture @@ -123,6 +128,17 @@ cdef int pyav_io_close_gil(lib.AVFormatContext *s, lib.AVIOContext *pb) noexcept return result +cdef void _free_chapters(lib.AVFormatContext *ctx) noexcept nogil: + cdef int i + if ctx.chapters != NULL: + for i in range(ctx.nb_chapters): + if ctx.chapters[i] != NULL: + if ctx.chapters[i].metadata != NULL: + lib.av_dict_free(&ctx.chapters[i].metadata) + lib.av_freep(&ctx.chapters[i]) + lib.av_freep(&ctx.chapters) + ctx.nb_chapters = 0 + class Flags(Flag): gen_pts: "Generate missing pts even if it requires parsing future frames." = lib.AVFMT_FLAG_GENPTS @@ -346,6 +362,39 @@ cdef class Container: }) return result + def set_chapters(self, chapters): + self._assert_open() + + cdef int count = len(chapters) + cdef int i + cdef lib.AVChapter **ch_array + cdef lib.AVChapter *ch + cdef dict entry + + with nogil: + _free_chapters(self.ptr) + + ch_array = lib.av_malloc(count * sizeof(lib.AVChapter *)) + if ch_array == NULL: + raise MemoryError("av_malloc failed for chapters") + + for i in range(count): + entry = chapters[i] + ch = lib.av_malloc(sizeof(lib.AVChapter)) + if ch == NULL: + raise MemoryError("av_malloc failed for chapter") + ch.id = entry["id"] + ch.start = entry["start"] + ch.end = entry["end"] + to_avrational(entry["time_base"], &ch.time_base) + ch.metadata = NULL + if "metadata" in entry: + dict_to_avdict(&ch.metadata, entry["metadata"], self.metadata_encoding, self.metadata_errors) + ch_array[i] = ch + + self.ptr.nb_chapters = count + self.ptr.chapters = ch_array + def open( file, mode=None, diff --git a/tests/test_chapters.py b/tests/test_chapters.py index 6a3d371b2..c8a3626ce 100644 --- a/tests/test_chapters.py +++ b/tests/test_chapters.py @@ -39,3 +39,20 @@ def test_chapters() -> None: path = fate_suite("vorbis/vorbis_chapter_extension_demo.ogg") with av.open(path) as container: assert container.chapters() == expected + + +def test_set_chapters() -> None: + chapters: list[av.container.Chapter] = [ + { + "id": 1, + "start": 0, + "end": 5000, + "time_base": Fraction(1, 1000), + "metadata": {"title": "start"}, + } + ] + + path = fate_suite("h264/interlaced_crop.mp4") + with av.open(path) as container: + container.set_chapters(chapters) + assert container.chapters() == chapters From 5076f8dc104001d31dd07c30999a07bbb95c86db Mon Sep 17 00:00:00 2001 From: Curtis Doty Date: Thu, 18 Sep 2025 07:41:56 -0700 Subject: [PATCH 323/365] Prevent rsync error on missing subdir --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 4858add5b..b37d83e57 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,7 @@ clean: fate-suite: # Grab ALL of the samples from the ffmpeg site. + mkdir -p tests/assets/fate-suite/ rsync -vrltLW rsync://fate-suite.ffmpeg.org/fate-suite/ tests/assets/fate-suite/ lint: From 84859928338bd9c7c42c41e9f7da34873acbffc2 Mon Sep 17 00:00:00 2001 From: velsinki <40809145+velsinki@users.noreply.github.com> Date: Thu, 18 Sep 2025 17:13:53 +0200 Subject: [PATCH 324/365] Remove VideoFrame.pts stub Since `VideoFrame` does not override the `pts` setter/getter, it can also be `None` here. Instead of writing it out in the stubs, we can just reuse the definition from `Frame` by removing the stub in `VideoFrame`. --- av/video/frame.pyi | 1 - 1 file changed, 1 deletion(-) diff --git a/av/video/frame.pyi b/av/video/frame.pyi index c6868aabc..a7575e3bd 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -30,7 +30,6 @@ class PictureType(IntEnum): class VideoFrame(Frame): format: VideoFormat - pts: int planes: tuple[VideoPlane, ...] pict_type: int colorspace: int From 4e786dd8251c1c6c1f93b1215b79828ec08de805 Mon Sep 17 00:00:00 2001 From: velsinki <40809145+velsinki@users.noreply.github.com> Date: Thu, 18 Sep 2025 17:18:47 +0200 Subject: [PATCH 325/365] Allow None in FilterContext.push type stub `None` is correctly handled to signify EOF to the buffer, but this should also be visible in the type stub. --- av/filter/context.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/filter/context.pyi b/av/filter/context.pyi index d37febdb9..2350019f3 100644 --- a/av/filter/context.pyi +++ b/av/filter/context.pyi @@ -10,7 +10,7 @@ class FilterContext: ) -> None: ... @property def graph(self) -> Graph: ... - def push(self, frame: Frame) -> None: ... + def push(self, frame: Frame | None) -> None: ... def pull(self) -> Frame: ... def process_command( self, cmd: str, arg: str | None = None, res_len: int = 1024, flags: int = 0 From c51a78db95bf41ba4c73bb3db4c45c1f7cd9ea38 Mon Sep 17 00:00:00 2001 From: velsinki <40809145+velsinki@users.noreply.github.com> Date: Thu, 18 Sep 2025 19:10:16 +0200 Subject: [PATCH 326/365] Fix VideoFrame.pts typing in tests Fixes 84859928338bd9c7c42c41e9f7da34873acbffc2 --- tests/test_seek.py | 2 ++ tests/test_videoframe.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_seek.py b/tests/test_seek.py index d0e9e1f2e..ac28f6580 100644 --- a/tests/test_seek.py +++ b/tests/test_seek.py @@ -104,6 +104,8 @@ def test_decode_half(self) -> None: frame_count = 0 for frame in container.decode(video_stream): + assert frame.pts is not None + if current_frame is None: current_frame = timestamp_to_frame(frame.pts, video_stream) else: diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index 90d91ab45..a977cd828 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -32,7 +32,9 @@ def test_frame_duration_matches_packet() -> None: packet_durations.sort(key=lambda x: x[0]) with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: - frame_durations = [(f.pts, f.duration) for f in container.decode(video=0)] + frame_durations = [ + (f.pts, f.duration) for f in container.decode(video=0) if f.pts is not None + ] frame_durations.sort(key=lambda x: x[0]) assert len(packet_durations) == len(frame_durations) From cddcef804adc275096302f91e31e4c062a61f5f6 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 21 Sep 2025 23:36:02 -0400 Subject: [PATCH 327/365] Use macos-15-intel runner --- .github/workflows/tests.yml | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cb38c2bfc..d2ceecad6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -30,7 +30,7 @@ jobs: include: - os: macos-14 arch: arm64 - - os: macos-13 + - os: macos-15-intel arch: x86_64 - os: ubuntu-24.04-arm arch: aarch64 @@ -43,15 +43,10 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.13" - - name: Install packages - if: matrix.os == 'macos-13' - run: | - brew update - brew install pkg-config - name: Set Minimum MacOS Target - if: matrix.os == 'macos-13' || matrix.os == 'macos-14' + if: runner.os == 'macOS' run: | - echo "MACOSX_DEPLOYMENT_TARGET=13.0" >> $GITHUB_ENV + echo "MACOSX_DEPLOYMENT_TARGET=14.0" >> $GITHUB_ENV - name: Build wheels env: CIBW_ARCHS: ${{ matrix.arch }} From 45eaebac0c6601a48ba81fbdcd2dee59cc6d3578 Mon Sep 17 00:00:00 2001 From: Curtis Doty Date: Thu, 2 Oct 2025 13:58:21 -0700 Subject: [PATCH 328/365] Prevent segfault on data stream template --- av/container/output.py | 5 +++++ tests/test_streams.py | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/av/container/output.py b/av/container/output.py index d035b0265..7738086ad 100644 --- a/av/container/output.py +++ b/av/container/output.py @@ -144,6 +144,11 @@ def add_stream_from_template( if opaque is None: opaque = template.type != "video" + if template.codec_context is None: + raise ValueError( + f"template stream of type {template.type} has no codec context" + ) + codec_obj: Codec if opaque: # Copy ctx from template. codec_obj = template.codec_context.codec diff --git a/tests/test_streams.py b/tests/test_streams.py index c7b234d48..d07085de4 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -1,5 +1,6 @@ import os from fractions import Fraction +from typing import Any, cast import pytest @@ -146,3 +147,23 @@ def test_data_stream(self) -> None: assert repr.startswith("") container.close() + + def test_data_stream_from_template(self) -> None: + """Test that adding a data stream from a template raises ValueError.""" + + # Open an existing container with a data stream + input_container = av.open(fate_suite("mxf/track_01_v02.mxf")) + input_data_stream = input_container.streams.data[0] + + # Create a new container and ensure using a data stream as a template raises ValueError + output_container = av.open("out.mkv", "w") + with pytest.raises(ValueError): + # input_data_stream is a DataStream at runtime; the test asserts that + # using it as a template raises ValueError. The static type stubs + # intentionally restrict which Stream subclasses are valid templates, + # so cast to Any here to keep the runtime check while satisfying + # the type checker. + output_container.add_stream_from_template(cast(Any, input_data_stream)) + + input_container.close() + output_container.close() From d87faf7b32eae594af802f5cd368fa3072539d38 Mon Sep 17 00:00:00 2001 From: Curtis Doty Date: Thu, 2 Oct 2025 14:43:32 -0700 Subject: [PATCH 329/365] Your bash is not my bash Let's respect the environment. If a developer is running their own version of the shell, respect it. Don't hard-code. --- scripts/activate.sh | 3 +-- scripts/build | 2 +- scripts/build-deps | 2 +- scripts/test | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/scripts/activate.sh b/scripts/activate.sh index 81240cafd..95f60a33d 100755 --- a/scripts/activate.sh +++ b/scripts/activate.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#! /usr/bin/env bash # Make sure this is sourced. if [[ "$0" == "${BASH_SOURCE[0]}" ]]; then @@ -78,7 +78,6 @@ print("{}{}.{}".format(platform.python_implementation().lower(), *sys.version_in fi - # Just a flag so that we know this was supposedly run. export _PYAV_ACTIVATED=1 diff --git a/scripts/build b/scripts/build index 3b9346d60..4ee15d003 100755 --- a/scripts/build +++ b/scripts/build @@ -1,4 +1,4 @@ -#!/bin/bash +#! /usr/bin/env bash if [[ ! "$_PYAV_ACTIVATED" ]]; then export here="$(cd "$(dirname "${BASH_SOURCE[0]}")"; pwd)" diff --git a/scripts/build-deps b/scripts/build-deps index f34ed5a5b..5f32cf140 100755 --- a/scripts/build-deps +++ b/scripts/build-deps @@ -1,4 +1,4 @@ -#!/bin/bash +#! /usr/bin/env bash if [[ ! "$_PYAV_ACTIVATED" ]]; then export here="$(cd "$(dirname "${BASH_SOURCE[0]}")"; pwd)" diff --git a/scripts/test b/scripts/test index 8244778b3..e91288baf 100755 --- a/scripts/test +++ b/scripts/test @@ -1,4 +1,4 @@ -#!/bin/bash +#! /usr/bin/env bash # Exit as soon as something errors. set -e From 61e9fc6280668c6e3abfa7e476e7ed6e7a05f0ea Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 2 Oct 2025 19:13:27 -0400 Subject: [PATCH 330/365] CI: Fix windows workflow --- .github/workflows/tests.yml | 6 +++++- scripts/ffmpeg-8.0.json | 2 +- scripts/ffmpeg-latest.json | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d2ceecad6..c2b8d1189 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -46,7 +46,11 @@ jobs: - name: Set Minimum MacOS Target if: runner.os == 'macOS' run: | - echo "MACOSX_DEPLOYMENT_TARGET=14.0" >> $GITHUB_ENV + if [ "${{ matrix.arch }}" = "x86_64" ]; then + echo "MACOSX_DEPLOYMENT_TARGET=11.0" >> $GITHUB_ENV + else + echo "MACOSX_DEPLOYMENT_TARGET=14.0" >> $GITHUB_ENV + fi - name: Build wheels env: CIBW_ARCHS: ${{ matrix.arch }} diff --git a/scripts/ffmpeg-8.0.json b/scripts/ffmpeg-8.0.json index 915f5bfc2..fc5719b8a 100644 --- a/scripts/ffmpeg-8.0.json +++ b/scripts/ffmpeg-8.0.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0-1/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0-2/ffmpeg-{platform}.tar.gz" } diff --git a/scripts/ffmpeg-latest.json b/scripts/ffmpeg-latest.json index 915f5bfc2..fc5719b8a 100644 --- a/scripts/ffmpeg-latest.json +++ b/scripts/ffmpeg-latest.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0-1/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0-2/ffmpeg-{platform}.tar.gz" } From ca699102f14f09a72cf5865f2049ab88df72f8e0 Mon Sep 17 00:00:00 2001 From: Santtu Keskinen Date: Fri, 3 Oct 2025 20:35:43 +0300 Subject: [PATCH 331/365] Writable and copyable attachment and data streams --- av/container/output.py | 104 +++++++++++++++++++++++++++------ av/container/output.pyi | 7 ++- av/stream.py | 17 ++++++ av/stream.pyi | 2 + tests/test_streams.py | 124 +++++++++++++++++++++++++++++++++------- 5 files changed, 215 insertions(+), 39 deletions(-) diff --git a/av/container/output.py b/av/container/output.py index 7738086ad..145437752 100644 --- a/av/container/output.py +++ b/av/container/output.py @@ -134,7 +134,7 @@ def add_stream_from_template( self, template: Stream, opaque: bool | None = None, **kwargs ): """ - Creates a new stream from a template. Supports video, audio, and subtitle streams. + Creates a new stream from a template. Supports video, audio, subtitle, data and attachment streams. :param template: Copy codec from another :class:`~av.stream.Stream` instance. :param opaque: If True, copy opaque data from the template's codec context. @@ -145,9 +145,7 @@ def add_stream_from_template( opaque = template.type != "video" if template.codec_context is None: - raise ValueError( - f"template stream of type {template.type} has no codec context" - ) + return self._add_stream_without_codec_from_template(template, **kwargs) codec_obj: Codec if opaque: # Copy ctx from template. @@ -196,6 +194,79 @@ def add_stream_from_template( return py_stream + def _add_stream_without_codec_from_template( + self, template: Stream, **kwargs + ) -> Stream: + codec_type: cython.int = template.ptr.codecpar.codec_type + if codec_type not in {lib.AVMEDIA_TYPE_ATTACHMENT, lib.AVMEDIA_TYPE_DATA}: + raise ValueError( + f"template stream of type {template.type} has no codec context" + ) + + stream: cython.pointer[lib.AVStream] = lib.avformat_new_stream( + self.ptr, cython.NULL + ) + if stream == cython.NULL: + raise MemoryError("Could not allocate stream") + + err_check(lib.avcodec_parameters_copy(stream.codecpar, template.ptr.codecpar)) + + # Mirror basic properties that are not derived from a codec context. + stream.time_base = template.ptr.time_base + stream.start_time = template.ptr.start_time + stream.duration = template.ptr.duration + stream.disposition = template.ptr.disposition + + py_stream: Stream = wrap_stream(self, stream, None) + self.streams.add_stream(py_stream) + + py_stream.metadata = dict(template.metadata) + + for k, v in kwargs.items(): + setattr(py_stream, k, v) + + return py_stream + + def add_attachment(self, name: str, mimetype: str, data: bytes): + """ + Create an attachment stream and embed its payload into the container header. + + - Only supported by formats that support attachments (e.g. Matroska). + - No per-packet muxing is required; attachments are written at header time. + """ + # Create stream with no codec (attachments are codec-less). + stream: cython.pointer[lib.AVStream] = lib.avformat_new_stream( + self.ptr, cython.NULL + ) + if stream == cython.NULL: + raise MemoryError("Could not allocate stream") + + stream.codecpar.codec_type = lib.AVMEDIA_TYPE_ATTACHMENT + stream.codecpar.codec_id = lib.AV_CODEC_ID_NONE + + # Allocate and copy payload into codecpar.extradata. + payload_size: cython.size_t = len(data) + if payload_size: + buf = cython.cast(cython.p_uchar, lib.av_malloc(payload_size + 1)) + if buf == cython.NULL: + raise MemoryError("Could not allocate attachment data") + # Copy bytes. + for i in range(payload_size): + buf[i] = data[i] + buf[payload_size] = 0 + stream.codecpar.extradata = cython.cast(cython.p_uchar, buf) + stream.codecpar.extradata_size = payload_size + + # Wrap as user-land stream. + meta_ptr = cython.address(stream.metadata) + err_check(lib.av_dict_set(meta_ptr, b"filename", name.encode(), 0)) + mime_bytes = mimetype.encode() + err_check(lib.av_dict_set(meta_ptr, b"mimetype", mime_bytes, 0)) + + py_stream: Stream = wrap_stream(self, stream, None) + self.streams.add_stream(py_stream) + return py_stream + def add_data_stream(self, codec_name=None, options: dict | None = None): """add_data_stream(codec_name=None) @@ -270,21 +341,20 @@ def start_encoding(self): # Finalize and open all streams. for stream in self.streams: ctx = stream.codec_context - # Skip codec context handling for data streams without codecs + # Skip codec context handling for streams without codecs (e.g. data/attachments). if ctx is None: - if stream.type != "data": + if stream.type not in {"data", "attachment"}: raise ValueError(f"Stream {stream.index} has no codec context") - continue - - if not ctx.is_open: - for k, v in self.options.items(): - ctx.options.setdefault(k, v) - ctx.open() - - # Track option consumption. - for k in self.options: - if k not in ctx.options: - used_options.add(k) + else: + if not ctx.is_open: + for k, v in self.options.items(): + ctx.options.setdefault(k, v) + ctx.open() + + # Track option consumption. + for k in self.options: + if k not in ctx.options: + used_options.add(k) stream._finalize_for_output() diff --git a/av/container/output.pyi b/av/container/output.pyi index b370095de..b3cbe1873 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -4,14 +4,14 @@ from typing import Sequence, TypeVar, Union, overload from av.audio import _AudioCodecName from av.audio.stream import AudioStream from av.packet import Packet -from av.stream import DataStream +from av.stream import AttachmentStream, DataStream, Stream from av.subtitles.stream import SubtitleStream from av.video import _VideoCodecName from av.video.stream import VideoStream from .core import Container -_StreamT = TypeVar("_StreamT", bound=Union[VideoStream, AudioStream, SubtitleStream]) +_StreamT = TypeVar("_StreamT", bound=Stream) class OutputContainer(Container): def __enter__(self) -> OutputContainer: ... @@ -42,6 +42,9 @@ class OutputContainer(Container): def add_stream_from_template( self, template: _StreamT, opaque: bool | None = None, **kwargs ) -> _StreamT: ... + def add_attachment( + self, name: str, mimetype: str, data: bytes + ) -> AttachmentStream: ... def add_data_stream( self, codec_name: str | None = None, options: dict[str, str] | None = None ) -> DataStream: ... diff --git a/av/stream.py b/av/stream.py index b5c22588b..a43c43496 100644 --- a/av/stream.py +++ b/av/stream.py @@ -148,6 +148,9 @@ def _finalize_for_output(self): errors=self.container.metadata_errors, ) + if self.codec_context is None: + return + if not self.ptr.time_base.num: self.ptr.time_base = self.codec_context.ptr.time_base @@ -316,3 +319,17 @@ def mimetype(self): :rtype: str | None """ return self.metadata.get("mimetype") + + @property + def data(self): + """Return the raw attachment payload as bytes.""" + extradata: cython.p_uchar = self.ptr.codecpar.extradata + size: cython.Py_ssize_t = self.ptr.codecpar.extradata_size + if extradata == cython.NULL or size <= 0: + return b"" + + payload = bytearray(size) + for i in range(size): + payload[i] = extradata[i] + + return bytes(payload) diff --git a/av/stream.pyi b/av/stream.pyi index 8289a6358..f6ca196c8 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -59,3 +59,5 @@ class AttachmentStream(Stream): type: Literal["attachment"] @property def mimetype(self) -> str | None: ... + @property + def data(self) -> bytes: ... diff --git a/tests/test_streams.py b/tests/test_streams.py index d07085de4..f82ce384b 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -1,10 +1,10 @@ import os from fractions import Fraction -from typing import Any, cast import pytest import av +import av.datasets from .common import fate_suite @@ -13,7 +13,14 @@ class TestStreams: @pytest.fixture(autouse=True) def cleanup(self): yield - for file in ("data.ts", "out.mkv"): + for file in ( + "data.ts", + "data_source.ts", + "data_copy.ts", + "out.mkv", + "video_with_attachment.mkv", + "remuxed_attachment.mkv", + ): if os.path.exists(file): os.remove(file) @@ -149,21 +156,98 @@ def test_data_stream(self) -> None: container.close() def test_data_stream_from_template(self) -> None: - """Test that adding a data stream from a template raises ValueError.""" - - # Open an existing container with a data stream - input_container = av.open(fate_suite("mxf/track_01_v02.mxf")) - input_data_stream = input_container.streams.data[0] - - # Create a new container and ensure using a data stream as a template raises ValueError - output_container = av.open("out.mkv", "w") - with pytest.raises(ValueError): - # input_data_stream is a DataStream at runtime; the test asserts that - # using it as a template raises ValueError. The static type stubs - # intentionally restrict which Stream subclasses are valid templates, - # so cast to Any here to keep the runtime check while satisfying - # the type checker. - output_container.add_stream_from_template(cast(Any, input_data_stream)) - - input_container.close() - output_container.close() + source_path = "data_source.ts" + payloads = [b"payload-a", b"payload-b", b"payload-c"] + + with av.open(source_path, "w") as source: + source_stream = source.add_data_stream() + for i, payload in enumerate(payloads): + packet = av.Packet(payload) + packet.pts = i + packet.stream = source_stream + source.mux(packet) + + copied_payloads: list[bytes] = [] + + with av.open(source_path) as input_container: + input_data_stream = input_container.streams.data[0] + + with av.open("data_copy.ts", "w") as output_container: + output_data_stream = output_container.add_stream_from_template( + input_data_stream + ) + + for packet in input_container.demux(input_data_stream): + payload = bytes(packet) + if not payload: + continue + copied_payloads.append(payload) + clone = av.Packet(payload) + clone.pts = packet.pts + clone.dts = packet.dts + clone.time_base = packet.time_base + clone.stream = output_data_stream + output_container.mux(clone) + + with av.open("data_copy.ts") as remuxed: + output_stream = remuxed.streams.data[0] + assert output_stream.codec_context is None + + remuxed_payloads: list[bytes] = [] + for packet in remuxed.demux(output_stream): + payload = bytes(packet) + if payload: + remuxed_payloads.append(payload) + + assert remuxed_payloads == copied_payloads + + def test_attachment_stream(self) -> None: + input_path = av.datasets.curated( + "pexels/time-lapse-video-of-night-sky-857195.mp4" + ) + input_ = av.open(input_path) + out1_path = "video_with_attachment.mkv" + + with av.open(out1_path, "w") as out1: + out1.add_attachment( + name="attachment.txt", mimetype="text/plain", data=b"hello\n" + ) + + in_v = input_.streams.video[0] + out_v = out1.add_stream_from_template(in_v) + + for packet in input_.demux(in_v): + if packet.dts is None: + continue + packet.stream = out_v + out1.mux(packet) + + input_.close() + + with av.open(out1_path) as c: + attachments = c.streams.attachments + assert len(attachments) == 1 + att = attachments[0] + assert att.name == "attachment.txt" + assert att.mimetype == "text/plain" + assert att.data == b"hello\n" + + out2_path = "remuxed_attachment.mkv" + with av.open(out1_path) as ic, av.open(out2_path, "w") as oc: + stream_map = {} + for s in ic.streams: + stream_map[s.index] = oc.add_stream_from_template(s) + + for packet in ic.demux(ic.streams.video): + if packet.dts is None: + continue + packet.stream = stream_map[packet.stream.index] + oc.mux(packet) + + with av.open(out2_path) as c: + attachments = c.streams.attachments + assert len(attachments) == 1 + att = attachments[0] + assert att.name == "attachment.txt" + assert att.mimetype == "text/plain" + assert att.data == b"hello\n" From 00493da0f49746bf06619d92cdf957737aab205e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 4 Oct 2025 20:35:30 -0400 Subject: [PATCH 332/365] Release 16.0.0 --- AUTHORS.rst | 8 +++++++- CHANGELOG.rst | 12 ++++++++++-- av/about.py | 2 +- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index b85c8ce1b..5de3b44c8 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -28,6 +28,8 @@ All contributors (by number of commits): - Dan Allan ; `@danielballan `_ - Moonsik Park - Santtu Keskinen +- velsinki <40809145+velsinki@users.noreply.github.com> +- Curtis Doty - Christoph Rackwitz - David Plowman - Alireza Davoudi ; `@adavoudi `_ @@ -43,6 +45,7 @@ All contributors (by number of commits): * zzjjbb <31069326+zzjjbb@users.noreply.github.com> * Joe Schiff <41972063+JoeSchiff@users.noreply.github.com> * Dexer <73297572+DexerBR@users.noreply.github.com> +* DE-AI <81620697+DE-AI@users.noreply.github.com> * rutsh * Felix Vollmer * Santiago Castro @@ -50,11 +53,13 @@ All contributors (by number of commits): * Ihor Liubymov * Johannes Erdfelt * Karl Litterfeldt ; `@litterfeldt `_ +* Kim Minjong * Martin Larralde * Simon-Martin Schröder * Matteo Destro * mephi42 * Miles Kaufmann +* Nathan Goldbaum * Pablo Prietz * Andrew Wason * Radek Senfeld ; `@radek-senfeld `_ @@ -62,6 +67,7 @@ All contributors (by number of commits): * Benjamin Chrétien <2742231+bchretien@users.noreply.github.com> * davidplowman <38045873+davidplowman@users.noreply.github.com> * Hanz <40712686+HanzCEO@users.noreply.github.com> +* Kesh Ikuma <79113787+tikuma-lsuhsc@users.noreply.github.com> * Artturin * Ian Lee * Ryan Huang @@ -76,8 +82,8 @@ All contributors (by number of commits): * henri-gasc * Jonathan Martin * Johan Jeppsson Karlin +* Kian-Meng Ang * Philipp Klaus -* Kim Minjong * Marcell Pardavi * Matteo Destro * Mattias Wadman diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7a445525e..eb4d1b7e4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,16 +18,24 @@ We are operating with `semantic versioning `_. v16.0.0 ------- -(unreleased) Major: + - Drop Python 3.9, Support Python 3.14. +- Drop support for i686 Linux. Features: -- Declare free-threaded support and support 3.13t by :gh-user:`ngoldbaum` in (:pr:`2005`). + - Add ``Filter.Context.process_command()`` method by :gh-user:`caffeinism` in (:pr:`2000`). +- Add packet side-data handling mechanism by :gh-user:`tikuma-lsuhsc ` in (:pr:`2003`). +- Implemented set_chapters method by :gh-user:`DE-AI` in (:pr:`2004`). +- Declare free-threaded support and support 3.13t by :gh-user:`ngoldbaum` in (:pr:`2005`). +- Add writable and copyable attachment and data streams by :gh-user:`skeskinen` in (:pr:`2026`). Fixes: + +- Declare free-threaded support and support 3.13t by :gh-user:`ngoldbaum` in (:pr:`2005`). +- Allow ``None`` in ``FilterContext.push()`` type stub by :gh-user:`velsinki` in (:pr:`2015`). - Fix typos v15.1.0 diff --git a/av/about.py b/av/about.py index 9a75d4360..5e5e9109a 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "15.1.0" +__version__ = "16.0.0" From 8e9e59781c922b7a166334028fb3ad805622f7ee Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 10 Oct 2025 04:45:01 -0400 Subject: [PATCH 333/365] Fix #2029 Add new HWDeviceType enums, skip test when version isn't SemVar. --- av/about.py | 2 +- av/codec/hwaccel.pyx | 3 +++ tests/test_packet.py | 7 +++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/av/about.py b/av/about.py index 5e5e9109a..610c11155 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "16.0.0" +__version__ = "16.0.1" diff --git a/av/codec/hwaccel.pyx b/av/codec/hwaccel.pyx index 257e6e7b2..e9a6bd194 100644 --- a/av/codec/hwaccel.pyx +++ b/av/codec/hwaccel.pyx @@ -25,6 +25,9 @@ class HWDeviceType(IntEnum): mediacodec = lib.AV_HWDEVICE_TYPE_MEDIACODEC vulkan = lib.AV_HWDEVICE_TYPE_VULKAN d3d12va = lib.AV_HWDEVICE_TYPE_D3D12VA + amf = 13 # FFmpeg >=8 + ohcodec = 14 + # TODO: When ffmpeg major is changed, check this enum. class HWConfigMethod(IntEnum): none = 0 diff --git a/tests/test_packet.py b/tests/test_packet.py index d5552238c..b0920ed3f 100644 --- a/tests/test_packet.py +++ b/tests/test_packet.py @@ -1,4 +1,5 @@ from typing import get_args +from unittest import SkipTest import av @@ -55,6 +56,12 @@ def test_set_duration(self) -> None: class TestPacketSideData: def test_data_types(self) -> None: dtypes = get_args(av.packet.PktSideDataT) + + if av.ffmpeg_version_info.startswith("n") or av.ffmpeg_version_info.count( + "." + ) not in (1, 2): + raise SkipTest(f"Expect version to be SemVar: {av.ffmpeg_version_info}") + ffmpeg_ver = [int(v) for v in av.ffmpeg_version_info.split(".", 2)[:2]] for dtype in dtypes: av_enum = av.packet.packet_sidedata_type_from_literal(dtype) From 7687256d87f22f78cb75bb4aba94dc6e504c9983 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 13 Oct 2025 07:33:44 -0400 Subject: [PATCH 334/365] Release 16.0.1 --- CHANGELOG.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index eb4d1b7e4..dce62d944 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,13 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. +v16.0.1 +------- + +Fixes: + +- Add new hwaccel enums by :gh-user:`WyattBlue` in (:pr:`2030`). + v16.0.0 ------- From 5f61ed9ebeb51fd18f4d72594043f14092e59ae5 Mon Sep 17 00:00:00 2001 From: Curtis Doty Date: Sat, 22 Nov 2025 14:10:38 -0800 Subject: [PATCH 335/365] Add libjpeg-dev dep to smoke --- .github/workflows/smoke.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index fe5b82687..fe59b4821 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -1,5 +1,6 @@ name: smoke on: + workflow_dispatch: push: branches: main paths-ignore: @@ -58,8 +59,18 @@ jobs: case ${{ matrix.config.os }} in ubuntu-24.04) sudo apt-get update - sudo apt-get install autoconf automake build-essential cmake \ - libtool pkg-config nasm zlib1g-dev libvorbis-dev libx264-dev + sudo apt-get install \ + autoconf \ + automake \ + build-essential \ + cmake \ + libjpeg-dev \ + libtool \ + libvorbis-dev \ + libx264-dev \ + nasm \ + pkg-config \ + zlib1g-dev if [[ "${{ matrix.config.extras }}" ]]; then sudo apt-get install doxygen wget fi From 979880e73cad64661e89a6d516918a77bf13ca68 Mon Sep 17 00:00:00 2001 From: Curtis Doty Date: Sat, 22 Nov 2025 16:32:08 -0800 Subject: [PATCH 336/365] Refactor AUTHORS (#2043) --- AUTHORS.py | 191 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 131 insertions(+), 60 deletions(-) diff --git a/AUTHORS.py b/AUTHORS.py index ad0746641..1d68e08b1 100644 --- a/AUTHORS.py +++ b/AUTHORS.py @@ -1,16 +1,23 @@ +""" Generate the AUTHORS.rst file from git commit history. + +This module reads git commit logs and produces a formatted list of contributors +grouped by their contribution count, mapping email aliases and GitHub usernames. +""" + +from dataclasses import dataclass import math -import subprocess +import subprocess # noqa: S404 -print( - """Contributors -============ -All contributors (by number of commits): -""" -) +def main() -> None: + """ Generate and print the AUTHORS.rst content. """ + + contributors = get_git_contributors() + print_contributors(contributors) +# ------------------------------------------------------------------------------ -email_map = { +EMAIL_ALIASES: dict[str, str | None] = { # Maintainers. "git@mikeboers.com": "github@mikeboers.com", "mboers@keypics.com": "github@mikeboers.com", @@ -27,7 +34,7 @@ "61652821+laggykiller@users.noreply.github.com": "chaudominic2@gmail.com", } -name_map = { +CANONICAL_NAMES: dict[str, str] = { "caspervdw@gmail.com": "Casper van der Wel", "daniel.b.allan@gmail.com": "Dan Allan", "mgoacolou@cls.fr": "Manuel Goacolou", @@ -37,7 +44,7 @@ "xxr@megvii.com": "Xinran Xu", } -github_map = { +GITHUB_USERNAMES: dict[str, str] = { "billy.shambrook@gmail.com": "billyshambrook", "daniel.b.allan@gmail.com": "danielballan", "davoudialireza@gmail.com": "adavoudi", @@ -55,57 +62,121 @@ "xxr@megvii.com": "xxr3376", "chaudominic2@gmail.com": "laggykiller", "wyattblue@auto-editor.com": "WyattBlue", + "Curtis@GreenKey.net": "dotysan", } -email_count = {} -for line in ( - subprocess.check_output(["git", "log", "--format=%aN,%aE"]).decode().splitlines() -): - name, email = line.strip().rsplit(",", 1) - - email = email_map.get(email, email) - if not email: - continue - - names = name_map.setdefault(email, set()) - if isinstance(names, set): - names.add(name) - - email_count[email] = email_count.get(email, 0) + 1 - - -last = None -block_i = 0 -for email, count in sorted(email_count.items(), key=lambda x: (-x[1], x[0])): - # This is the natural log, because of course it should be. ;) - order = int(math.log(count)) - if last and last != order: - block_i += 1 - print() - last = order - - names = name_map[email] - if isinstance(names, set): - name = ", ".join(sorted(names)) - else: - name = names - - github = github_map.get(email) - - # The '-' vs '*' is so that Sphinx treats them as different lists, and - # introduces a gap between them. - if github: - print( - "%s %s <%s>; `@%s `_" - % ("-*"[block_i % 2], name, email, github, github) - ) - else: - print( - "%s %s <%s>" - % ( - "-*"[block_i % 2], - name, - email, +@dataclass +class Contributor: + """ Represents a contributor with their email, names, and GitHub username. """ + + email: str + names: set[str] + github: str | None = None + commit_count: int = 0 + + @property + def display_name(self) -> str: + """ Return the formatted display name for the contributor. + + Returns: + Comma-separated sorted list of contributor names. + """ + + return ", ".join(sorted(self.names)) + + def format_line(self, bullet: str) -> str: + """ Format the contributor line for RST output. + + Args: + bullet: The bullet character to use (- or *). + + Returns: + Formatted RST line with contributor info. + """ + + if self.github: + return ( + f"{bullet} {self.display_name} <{self.email}>; " + f"`@{self.github} `_" + ) + return f"{bullet} {self.display_name} <{self.email}>" + + +def get_git_contributors() -> dict[str, Contributor]: + """ Parse git log and return contributors grouped by canonical email. + + Returns: + Dictionary mapping canonical emails to Contributor objects. + """ + + contributors: dict[str, Contributor] = {} + git_log = subprocess.check_output( + ["git", "log", "--format=%aN,%aE"], # noqa: S607 + text=True, + ).splitlines() + + for line in git_log: + name, email = line.strip().rsplit(",", 1) + canonical_email = EMAIL_ALIASES.get(email, email) + + if not canonical_email: + continue + + if canonical_email not in contributors: + contributors[canonical_email] = Contributor( + email=canonical_email, + names=set(), + github=GITHUB_USERNAMES.get(canonical_email), ) - ) + + contributor = contributors[canonical_email] + contributor.names.add(name) + contributor.commit_count += 1 + + for email, canonical_name in CANONICAL_NAMES.items(): + if email in contributors: + contributors[email].names = {canonical_name} + + return contributors + + +def print_contributors(contributors: dict[str, Contributor]) -> None: + """Print contributors grouped by logarithmic order of commits. + + Args: + contributors: Dictionary of contributors to print. + """ + + print("""\ + Contributors + ============ + + All contributors (by number of commits): + """.replace(" ", "")) + + sorted_contributors = sorted( + contributors.values(), + key=lambda c: (-c.commit_count, c.email), + ) + + last_order: int | None = None + block_index = 0 + + for contributor in sorted_contributors: + # This is the natural log, because of course it should be. ;) + order = int(math.log(contributor.commit_count)) + + if last_order and last_order != order: + block_index += 1 + print() + last_order = order + + # The '-' vs '*' is so that Sphinx treats them as different lists, and + # introduces a gap between them. + bullet = "-*"[block_index % 2] + print(contributor.format_line(bullet)) + + +if __name__ == "__main__": + main() From b22003ada81ad33d8ddefb5bc6728787ad4fb68d Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 26 Nov 2025 20:27:33 -0500 Subject: [PATCH 337/365] Use checkout 6 --- .github/workflows/smoke.yml | 6 +++--- .github/workflows/tests.yml | 6 +++--- pyproject.toml | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index fe59b4821..f75bf1b06 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-24.04 steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Python uses: actions/setup-python@v5 @@ -45,7 +45,7 @@ jobs: PYAV_LIBRARY: ffmpeg-${{ matrix.config.ffmpeg }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 name: Checkout - name: Python ${{ matrix.config.python }} @@ -126,7 +126,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Conda shell: bash diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c2b8d1189..7611e4f18 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,7 +7,7 @@ jobs: package-source: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: actions/setup-python@v5 with: python-version: "3.13" @@ -39,7 +39,7 @@ jobs: - os: windows-latest arch: AMD64 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: actions/setup-python@v5 with: python-version: "3.13" @@ -79,7 +79,7 @@ jobs: runs-on: ubuntu-latest needs: [package-source, package-wheel] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: actions/download-artifact@v4 with: merge-multiple: true diff --git a/pyproject.toml b/pyproject.toml index 33cde6b2c..24bdc90a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,7 @@ zip-safe = false version = {attr = "av.about.__version__"} [project.urls] -"Bug Tracker" = "https://github.com/PyAV-Org/PyAV/discussions/new?category=4-bugs" +"Bug Tracker" = "https://github.com/PyAV-Org/PyAV/issues" "Source Code" = "https://github.com/PyAV-Org/PyAV" homepage = "https://pyav.basswood-io.com" From 4c4a54c0115abc71f04fd6e403b275ae846ce34b Mon Sep 17 00:00:00 2001 From: Santtu Keskinen Date: Thu, 4 Dec 2025 17:30:02 -0800 Subject: [PATCH 338/365] Add read/write access to PacketSideData --- av/packet.pxd | 6 +++- av/packet.py | 16 ++++++++++- av/packet.pyi | 2 +- tests/test_packet.py | 65 +++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 85 insertions(+), 4 deletions(-) diff --git a/av/packet.pxd b/av/packet.pxd index 8e8ad034d..2640a5aeb 100644 --- a/av/packet.pxd +++ b/av/packet.pxd @@ -7,11 +7,15 @@ from av.bytesource cimport ByteSource from av.stream cimport Stream -cdef class PacketSideData: +cdef class PacketSideData(Buffer): cdef uint8_t *data cdef size_t size cdef lib.AVPacketSideDataType dtype + cdef size_t _buffer_size(self) + cdef void* _buffer_ptr(self) + cdef bint _buffer_writable(self) + cdef class Packet(Buffer): cdef lib.AVPacket* ptr cdef Stream _stream diff --git a/av/packet.py b/av/packet.py index e1051b653..d21b432ea 100644 --- a/av/packet.py +++ b/av/packet.py @@ -2,6 +2,7 @@ import cython from cython.cimports import libav as lib +from cython.cimports.av.buffer import Buffer from cython.cimports.av.bytesource import bytesource from cython.cimports.av.error import err_check from cython.cimports.av.opaque import opaque_container @@ -64,7 +65,7 @@ def packet_sidedata_type_from_literal(dtype: PktSideDataT) -> lib.AVPacketSideDa @cython.cclass -class PacketSideData: +class PacketSideData(Buffer): @staticmethod def from_packet(packet: Packet, data_type: PktSideDataT) -> PacketSideData: """create new PacketSideData by copying an existing packet's side data @@ -152,6 +153,19 @@ def data_size(self) -> int: """ return self.size + # Buffer protocol implementation + @cython.cfunc + def _buffer_size(self) -> cython.size_t: + return self.size + + @cython.cfunc + def _buffer_ptr(self) -> cython.p_void: + return self.data + + @cython.cfunc + def _buffer_writable(self) -> cython.bint: + return True + def __bool__(self) -> bool: """ True if this object holds side data. diff --git a/av/packet.pyi b/av/packet.pyi index 8dab20065..6b1a271c2 100644 --- a/av/packet.pyi +++ b/av/packet.pyi @@ -50,7 +50,7 @@ PktSideDataT = Literal[ "rtcp_sr", ] -class PacketSideData: +class PacketSideData(Buffer): @staticmethod def from_packet(packet: Packet, dtype: PktSideDataT) -> PacketSideData: ... def to_packet(self, packet: Packet, move: bool = False): ... diff --git a/tests/test_packet.py b/tests/test_packet.py index b0920ed3f..044dfd907 100644 --- a/tests/test_packet.py +++ b/tests/test_packet.py @@ -1,9 +1,10 @@ +import struct from typing import get_args from unittest import SkipTest import av -from .common import fate_suite +from .common import fate_suite, sandboxed class TestProperties: @@ -100,3 +101,65 @@ def test_palette(self) -> None: nxt.set_sidedata(sdata, move=True) assert not bool(sdata) + + def test_buffer_protocol(self) -> None: + with av.open(fate_suite("h264/extradata-reload-multi-stsd.mov")) as container: + for pkt in container.demux(): + if pkt.has_sidedata("new_extradata"): + sdata = pkt.get_sidedata("new_extradata") + + raw = bytes(sdata) + assert len(raw) == sdata.data_size > 0 + assert sdata.buffer_size == sdata.data_size + assert sdata.buffer_ptr != 0 + assert bytes(memoryview(sdata)) == raw + + # Modify and verify changes stick + sdata.update(b"\xde\xad\xbe\xef" + raw[4:]) + assert bytes(sdata)[:4] == b"\xde\xad\xbe\xef" + + pkt.set_sidedata(sdata) + assert ( + bytes(pkt.get_sidedata("new_extradata"))[:4] + == b"\xde\xad\xbe\xef" + ) + return + + raise AssertionError("No packet with new_extradata side data found") + + def test_skip_samples_remux(self) -> None: + # Source file has skip_end=706 on last packet. Setting to 0 should + # result in 706 more decoded samples. And the file duration reported by + # the container should also increase. + output_path = sandboxed("skip_samples_modified.mkv") + + with av.open(fate_suite("mkv/codec_delay_opus.mkv")) as c: + original_samples = sum(f.samples for f in c.decode(c.streams.audio[0])) + + with av.open(fate_suite("mkv/codec_delay_opus.mkv")) as inp: + original_duration = inp.duration + audio_stream = inp.streams.audio[0] + with av.open(output_path, "w") as out: + out_stream = out.add_stream_from_template(audio_stream) + for pkt in inp.demux(audio_stream): + if pkt.dts is None: + continue + if pkt.has_sidedata("skip_samples"): + sdata = pkt.get_sidedata("skip_samples") + raw = bytes(sdata) + skip_end = struct.unpack(" original_duration From 6a0cf682201150abf735215e3834f4fd7d4716ba Mon Sep 17 00:00:00 2001 From: Santtu Keskinen Date: Wed, 3 Dec 2025 21:12:24 +0200 Subject: [PATCH 339/365] Add subtitle encoding support --- av/subtitles/codeccontext.pxd | 1 + av/subtitles/codeccontext.py | 96 ++++++++++++++++++++++ av/subtitles/codeccontext.pyi | 2 + av/subtitles/subtitle.py | 113 ++++++++++++++++++++++++-- av/subtitles/subtitle.pyi | 10 ++- include/libavcodec/avcodec.pxd | 4 + include/libavutil/avutil.pxd | 2 + tests/test_subtitles.py | 144 ++++++++++++++++++++++++++++++++- 8 files changed, 365 insertions(+), 7 deletions(-) diff --git a/av/subtitles/codeccontext.pxd b/av/subtitles/codeccontext.pxd index c94744e45..01c8fbf1f 100644 --- a/av/subtitles/codeccontext.pxd +++ b/av/subtitles/codeccontext.pxd @@ -3,4 +3,5 @@ from av.packet cimport Packet cdef class SubtitleCodecContext(CodecContext): + cdef bint subtitle_header_set cpdef decode2(self, Packet packet) diff --git a/av/subtitles/codeccontext.py b/av/subtitles/codeccontext.py index 0b3dda063..78c274df2 100644 --- a/av/subtitles/codeccontext.py +++ b/av/subtitles/codeccontext.py @@ -1,12 +1,108 @@ import cython from cython.cimports import libav as lib +from cython.cimports.av.bytesource import ByteSource, bytesource from cython.cimports.av.error import err_check from cython.cimports.av.packet import Packet from cython.cimports.av.subtitles.subtitle import SubtitleProxy, SubtitleSet +from cython.cimports.cpython.bytes import PyBytes_FromStringAndSize +from cython.cimports.libc.string import memcpy, strlen @cython.cclass class SubtitleCodecContext(CodecContext): + @property + def subtitle_header(self) -> bytes | None: + """Get the subtitle header data (ASS/SSA format for text subtitles).""" + if ( + self.ptr.subtitle_header == cython.NULL + or self.ptr.subtitle_header_size <= 0 + ): + return None + return PyBytes_FromStringAndSize( + cython.cast(cython.p_char, self.ptr.subtitle_header), + self.ptr.subtitle_header_size, + ) + + @subtitle_header.setter + def subtitle_header(self, data: bytes | None) -> None: + """Set the subtitle header data.""" + source: ByteSource + if data is None: + lib.av_freep(cython.address(self.ptr.subtitle_header)) + self.ptr.subtitle_header_size = 0 + else: + source = bytesource(data) + self.ptr.subtitle_header = cython.cast( + cython.p_uchar, + lib.av_realloc( + self.ptr.subtitle_header, + source.length + lib.AV_INPUT_BUFFER_PADDING_SIZE, + ), + ) + if not self.ptr.subtitle_header: + raise MemoryError("Cannot allocate subtitle_header") + memcpy(self.ptr.subtitle_header, source.ptr, source.length) + self.ptr.subtitle_header_size = source.length + self.subtitle_header_set = True + + def __dealloc__(self) -> None: + if self.ptr and self.subtitle_header_set: + lib.av_freep(cython.address(self.ptr.subtitle_header)) + + def encode_subtitle(self, subtitle: SubtitleSet) -> Packet: + """ + Encode a SubtitleSet into a Packet. + + Args: + subtitle: The SubtitleSet to encode + + Returns: + A Packet containing the encoded subtitle data + """ + if not self.codec.ptr: + raise ValueError("Cannot encode with unknown codec") + + self.open(strict=False) + + # Calculate buffer size from subtitle text length + buf_size: cython.size_t = 0 + i: cython.uint + for i in range(subtitle.proxy.struct.num_rects): + rect = subtitle.proxy.struct.rects[i] + if rect.ass != cython.NULL: + buf_size += strlen(rect.ass) + if rect.text != cython.NULL: + buf_size += strlen(rect.text) + buf_size += 1024 # padding for format overhead + + buf: cython.p_uchar = cython.cast(cython.p_uchar, lib.av_malloc(buf_size)) + if buf == cython.NULL: + raise MemoryError("Failed to allocate subtitle encode buffer") + + ret: cython.int = lib.avcodec_encode_subtitle( + self.ptr, + buf, + buf_size, + cython.address(subtitle.proxy.struct), + ) + + if ret < 0: + lib.av_free(buf) + err_check(ret, "avcodec_encode_subtitle()") + + packet: Packet = Packet(ret) + memcpy(packet.ptr.data, buf, ret) + lib.av_free(buf) + + packet.ptr.pts = subtitle.proxy.struct.pts + packet.ptr.dts = subtitle.proxy.struct.pts + packet.ptr.duration = ( + subtitle.proxy.struct.end_display_time + - subtitle.proxy.struct.start_display_time + ) + + return packet + @cython.cfunc def _send_packet_and_recv(self, packet: Packet | None): if packet is None: diff --git a/av/subtitles/codeccontext.pyi b/av/subtitles/codeccontext.pyi index 90c700935..7f8a49d09 100644 --- a/av/subtitles/codeccontext.pyi +++ b/av/subtitles/codeccontext.pyi @@ -6,4 +6,6 @@ from av.subtitles.subtitle import SubtitleSet class SubtitleCodecContext(CodecContext): type: Literal["subtitle"] + subtitle_header: bytes | None def decode2(self, packet: Packet) -> SubtitleSet | None: ... + def encode_subtitle(self, subtitle: SubtitleSet) -> Packet: ... diff --git a/av/subtitles/subtitle.py b/av/subtitles/subtitle.py index 1acadf0b5..68f85fd1d 100644 --- a/av/subtitles/subtitle.py +++ b/av/subtitles/subtitle.py @@ -1,6 +1,7 @@ import cython from cython.cimports.cpython import PyBuffer_FillInfo, PyBytes_FromString -from cython.cimports.libc.stdint import uint64_t +from cython.cimports.libc.stdint import int64_t, uint64_t +from cython.cimports.libc.string import memcpy, strlen @cython.cclass @@ -9,6 +10,9 @@ def __dealloc__(self): lib.avsubtitle_free(cython.address(self.struct)) +_cinit_bypass_sentinel = object() + + @cython.cclass class SubtitleSet: """ @@ -17,11 +21,94 @@ class SubtitleSet: Wraps :ffmpeg:`AVSubtitle`. """ - def __cinit__(self, proxy: SubtitleProxy): - self.proxy = proxy - self.rects = tuple( - build_subtitle(self, i) for i in range(self.proxy.struct.num_rects) + def __cinit__(self, proxy_or_sentinel=None): + if proxy_or_sentinel is _cinit_bypass_sentinel: + # Creating empty SubtitleSet for encoding + self.proxy = SubtitleProxy() + self.rects = () + elif isinstance(proxy_or_sentinel, SubtitleProxy): + # Creating from decoded subtitle + self.proxy = proxy_or_sentinel + self.rects = tuple( + build_subtitle(self, i) for i in range(self.proxy.struct.num_rects) + ) + else: + raise TypeError( + "SubtitleSet requires a SubtitleProxy or use SubtitleSet.create()" + ) + + @staticmethod + def create( + text: bytes, + start: int, + end: int, + pts: int = 0, + subtitle_format: int = 1, + ) -> "SubtitleSet": + """ + Create a SubtitleSet for encoding. + + Args: + text: The subtitle text in ASS dialogue format + (e.g. b"0,0,Default,,0,0,0,,Hello World") + start: Start display time as offset from pts (typically 0) + end: End display time as offset from pts (i.e., duration) + pts: Presentation timestamp in stream time_base units + subtitle_format: Subtitle format (default 1 for text) + + Note: + All timing values should be in stream time_base units. + For MKV (time_base=1/1000), units are milliseconds. + For MP4 (time_base=1/1000000), units are microseconds. + + Returns: + A SubtitleSet ready for encoding + """ + subset: SubtitleSet = SubtitleSet(_cinit_bypass_sentinel) + + subset.proxy.struct.format = subtitle_format + subset.proxy.struct.start_display_time = start + subset.proxy.struct.end_display_time = end + subset.proxy.struct.pts = pts + + subset.proxy.struct.num_rects = 1 + subset.proxy.struct.rects = cython.cast( + cython.pointer[cython.pointer[lib.AVSubtitleRect]], + lib.av_mallocz(cython.sizeof(cython.pointer[lib.AVSubtitleRect])), + ) + if subset.proxy.struct.rects == cython.NULL: + raise MemoryError("Failed to allocate subtitle rects array") + + rect: cython.pointer[lib.AVSubtitleRect] = cython.cast( + cython.pointer[lib.AVSubtitleRect], + lib.av_mallocz(cython.sizeof(lib.AVSubtitleRect)), ) + if rect == cython.NULL: + lib.av_free(subset.proxy.struct.rects) + subset.proxy.struct.rects = cython.NULL + raise MemoryError("Failed to allocate subtitle rect") + + subset.proxy.struct.rects[0] = rect + + rect.x = 0 + rect.y = 0 + rect.w = 0 + rect.h = 0 + rect.nb_colors = 0 + rect.type = lib.SUBTITLE_ASS + rect.text = cython.NULL + rect.flags = 0 + + text_len: cython.Py_ssize_t = len(text) + rect.ass = cython.cast(cython.p_char, lib.av_malloc(text_len + 1)) + if rect.ass == cython.NULL: + raise MemoryError("Failed to allocate subtitle text") + memcpy(rect.ass, cython.cast(cython.p_char, text), text_len) + rect.ass[text_len] = 0 + + subset.rects = (AssSubtitle(subset, 0),) + + return subset def __repr__(self): return ( @@ -32,19 +119,35 @@ def __repr__(self): def format(self): return self.proxy.struct.format + @format.setter + def format(self, value: int): + self.proxy.struct.format = value + @property def start_display_time(self): return self.proxy.struct.start_display_time + @start_display_time.setter + def start_display_time(self, value: int): + self.proxy.struct.start_display_time = value + @property def end_display_time(self): return self.proxy.struct.end_display_time + @end_display_time.setter + def end_display_time(self, value: int): + self.proxy.struct.end_display_time = value + @property def pts(self): """Same as packet pts, in av.time_base.""" return self.proxy.struct.pts + @pts.setter + def pts(self, value: int): + self.proxy.struct.pts = value + def __len__(self): return len(self.rects) diff --git a/av/subtitles/subtitle.pyi b/av/subtitles/subtitle.pyi index 2a35d0a55..d3d4201fd 100644 --- a/av/subtitles/subtitle.pyi +++ b/av/subtitles/subtitle.pyi @@ -5,8 +5,16 @@ class SubtitleSet: start_display_time: int end_display_time: int pts: int - rects: tuple[Subtitle] + rects: tuple[Subtitle, ...] + @staticmethod + def create( + text: bytes, + start: int, + end: int, + pts: int = 0, + subtitle_format: int = 1, + ) -> SubtitleSet: ... def __len__(self) -> int: ... def __iter__(self) -> Iterator[Subtitle]: ... def __getitem__(self, i: int) -> Subtitle: ... diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 43ecb0260..b2e78baf0 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -289,6 +289,10 @@ cdef extern from "libavcodec/avcodec.h" nogil: int extradata_size uint8_t *extradata + # Subtitle header (ASS/SSA format for text subtitles) + uint8_t *subtitle_header + int subtitle_header_size + int delay AVCodec *codec diff --git a/include/libavutil/avutil.pxd b/include/libavutil/avutil.pxd index 49be65f69..1bfbd2c5d 100644 --- a/include/libavutil/avutil.pxd +++ b/include/libavutil/avutil.pxd @@ -110,9 +110,11 @@ cdef extern from "libavutil/avutil.h" nogil: cdef double M_PI cdef void* av_malloc(size_t size) + cdef void* av_mallocz(size_t size) cdef void *av_calloc(size_t nmemb, size_t size) cdef void *av_realloc(void *ptr, size_t size) + cdef void av_free(void *ptr) cdef void av_freep(void *ptr) cdef int av_get_bytes_per_sample(AVSampleFormat sample_fmt) diff --git a/tests/test_subtitles.py b/tests/test_subtitles.py index 66e6f9cfa..d2a199924 100644 --- a/tests/test_subtitles.py +++ b/tests/test_subtitles.py @@ -1,9 +1,12 @@ +import io from typing import cast import av +from av.codec.context import CodecContext +from av.subtitles.codeccontext import SubtitleCodecContext from av.subtitles.subtitle import AssSubtitle, BitmapSubtitle -from .common import fate_suite +from .common import TestCase, fate_suite class TestSubtitle: @@ -73,3 +76,142 @@ def test_subtitle_flush(self) -> None: subs.extend(stream.decode()) assert len(subs) == 3 + + def test_subtitle_header_read(self) -> None: + """Test reading subtitle_header from a decoded subtitle stream.""" + path = fate_suite("sub/MovText_capability_tester.mp4") + + with av.open(path) as container: + stream = container.streams.subtitles[0] + ctx = cast(SubtitleCodecContext, stream.codec_context) + header = ctx.subtitle_header + assert header is None or isinstance(header, bytes) + + def test_subtitle_header_write(self) -> None: + """Test setting subtitle_header on encoder context.""" + ctx = cast(SubtitleCodecContext, CodecContext.create("mov_text", "w")) + assert ctx.subtitle_header is None + + ass_header = b"[Script Info]\nScriptType: v4.00+\n" + ctx.subtitle_header = ass_header + assert ctx.subtitle_header == ass_header + + new_header = b"[Script Info]\nScriptType: v4.00\n" + ctx.subtitle_header = new_header + assert ctx.subtitle_header == new_header + + ctx.subtitle_header = None + assert ctx.subtitle_header is None + + +class TestSubtitleEncoding(TestCase): + def test_subtitle_set_create(self) -> None: + """Test creating SubtitleSet for encoding.""" + from av.subtitles.subtitle import SubtitleSet + + text = b"0,0,Default,,0,0,0,,Hello World" + subtitle = SubtitleSet.create(text=text, start=0, end=2000, pts=0) + + assert subtitle.format == 1 + assert subtitle.start_display_time == 0 + assert subtitle.end_display_time == 2000 + assert subtitle.pts == 0 + assert len(subtitle) == 1 + assert cast(AssSubtitle, subtitle[0]).ass == text + + def test_subtitle_encode_mp4(self) -> None: + """Test encoding subtitles to MP4 container.""" + from av.subtitles.subtitle import SubtitleSet + + ass_header = b"""[Script Info] +ScriptType: v4.00+ +PlayResX: 640 +PlayResY: 480 + +[V4+ Styles] +Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding +Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1 + +[Events] +Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text +""" + + output = io.BytesIO() + with av.open(output, "w", format="mp4") as container: + # MP4 requires video for subtitles + video_stream = container.add_stream("libx264", rate=30) + video_stream.width = 640 + video_stream.height = 480 + video_stream.pix_fmt = "yuv420p" + + sub_stream = container.add_stream("mov_text") + sub_ctx = cast(SubtitleCodecContext, sub_stream.codec_context) + sub_ctx.subtitle_header = ass_header + + container.start_encoding() + + frame = av.VideoFrame(640, 480, "yuv420p") + frame.pts = 0 + for packet in video_stream.encode(frame): + container.mux(packet) + + time_base = sub_stream.time_base + assert time_base is not None + subtitle = SubtitleSet.create( + text=b"0,0,Default,,0,0,0,,Hello World", + start=0, + end=int(2 / time_base), + pts=0, + ) + packet = sub_ctx.encode_subtitle(subtitle) + packet.stream = sub_stream + container.mux(packet) + + for packet in video_stream.encode(): + container.mux(packet) + + output.seek(0) + with av.open(output) as container: + assert len(container.streams.subtitles) == 1 + + def test_subtitle_encode_mkv_srt(self) -> None: + """Test encoding SRT subtitles to MKV container.""" + from av.subtitles.subtitle import SubtitleSet + + minimal_header = b"[Script Info]\n" + + output = io.BytesIO() + with av.open(output, "w", format="matroska") as container: + sub_stream = container.add_stream("srt") + sub_ctx = cast(SubtitleCodecContext, sub_stream.codec_context) + sub_ctx.subtitle_header = minimal_header + + container.start_encoding() + + time_base = sub_stream.time_base + assert time_base is not None + for text, start_sec, duration_sec in [ + (b"0,0,Default,,0,0,0,,First subtitle", 0, 2), + (b"0,0,Default,,0,0,0,,Second subtitle", 2, 2), + (b"0,0,Default,,0,0,0,,Third subtitle", 4, 2), + ]: + subtitle = SubtitleSet.create( + text=text, + start=0, + end=int(duration_sec / time_base), + pts=int(start_sec / time_base), + ) + packet = sub_ctx.encode_subtitle(subtitle) + packet.stream = sub_stream + container.mux(packet) + + output.seek(0) + with av.open(output, mode="r") as input_container: + assert len(input_container.streams.subtitles) == 1 + subs: list[AssSubtitle] = [] + for packet in input_container.demux(): + subs.extend(cast(list[AssSubtitle], packet.decode())) + assert len(subs) == 3 + assert b"First subtitle" in subs[0].dialogue + assert b"Second subtitle" in subs[1].dialogue + assert b"Third subtitle" in subs[2].dialogue From d979acb9fe28890e35ae3f0cfe801dfab0479c50 Mon Sep 17 00:00:00 2001 From: Mattias Wadman Date: Fri, 12 Dec 2025 14:39:18 +0100 Subject: [PATCH 340/365] Add yuv422p support for video frame to_ndarray and from_ndarray --- av/video/frame.py | 23 +++++++++++++++++++++-- tests/test_videoframe.py | 8 ++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/av/video/frame.py b/av/video/frame.py index 0d60fd8e9..174683727 100644 --- a/av/video/frame.py +++ b/av/video/frame.py @@ -449,7 +449,13 @@ def to_ndarray(self, channel_last=False, **kwargs): import numpy as np # check size - if frame.format.name in {"yuv420p", "yuvj420p", "yuyv422", "yuv422p10le"}: + if frame.format.name in { + "yuv420p", + "yuvj420p", + "yuyv422", + "yuv422p10le", + "yuv422p", + }: assert frame.width % 2 == 0, ( "the width has to be even for this pixel format" ) @@ -563,7 +569,7 @@ def to_ndarray(self, channel_last=False, **kwargs): return array # special cases - if frame.format.name in {"yuv420p", "yuvj420p"}: + if frame.format.name in {"yuv420p", "yuvj420p", "yuv422p"}: return np.hstack( [ useful_array(frame.planes[0]), @@ -1022,6 +1028,19 @@ def from_ndarray(array, format="rgb24", channel_last=False): copy_array_to_plane(flat[u_start:v_start], frame.planes[1], 1) copy_array_to_plane(flat[v_start:], frame.planes[2], 1) return frame + elif format == "yuv422p": + check_ndarray(array, "uint8", 2) + check_ndarray_shape(array, array.shape[0] % 4 == 0) + check_ndarray_shape(array, array.shape[1] % 2 == 0) + + frame = VideoFrame(array.shape[1], array.shape[0] // 2, format) + u_start = frame.width * frame.height + v_start = u_start + u_start // 2 + flat = array.reshape(-1) + copy_array_to_plane(flat[0:u_start], frame.planes[0], 1) + copy_array_to_plane(flat[u_start:v_start], frame.planes[1], 1) + copy_array_to_plane(flat[v_start:], frame.planes[2], 1) + return frame elif format == "yuv422p10le": if not isinstance(array, np.ndarray) or array.dtype != np.uint16: raise ValueError("Array must be uint16 type") diff --git a/tests/test_videoframe.py b/tests/test_videoframe.py index a977cd828..80e59614f 100644 --- a/tests/test_videoframe.py +++ b/tests/test_videoframe.py @@ -648,6 +648,14 @@ def test_ndarray_yuv420p() -> None: assertNdarraysEqual(frame.to_ndarray(), array) +def test_ndarray_yuv422p() -> None: + array = numpy.random.randint(0, 256, size=(960, 640), dtype=numpy.uint8) + frame = VideoFrame.from_ndarray(array, format="yuv422p") + assert frame.width == 640 and frame.height == 480 + assert frame.format.name == "yuv422p" + assertNdarraysEqual(frame.to_ndarray(), array) + + def test_ndarray_yuv420p_align() -> None: array = numpy.random.randint(0, 256, size=(357, 318), dtype=numpy.uint8) frame = VideoFrame.from_ndarray(array, format="yuv420p") From 340b40ee5075ec8408bb60e1c119eea3d4eb3b4c Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 12 Dec 2025 11:49:15 -0500 Subject: [PATCH 341/365] Fix #2053 --- av/container/core.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/container/core.pyi b/av/container/core.pyi index f923ba01e..26e76ed2c 100644 --- a/av/container/core.pyi +++ b/av/container/core.pyi @@ -99,7 +99,7 @@ class Container: exc_type: Type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, - ) -> bool: ... + ) -> None: ... def set_timeout(self, timeout: Real | None) -> None: ... def start_timeout(self) -> None: ... def chapters(self) -> list[Chapter]: ... From f10e3aee0fa0315bfbdb1f64b901b406fa87c95b Mon Sep 17 00:00:00 2001 From: Nils DEYBACH <68770774+ndeybach@users.noreply.github.com> Date: Tue, 16 Dec 2025 12:14:46 +0100 Subject: [PATCH 342/365] Normalize line endings via .gitattributes --- .gitattributes | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..24b8f0829 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# POSIX-sensitive files: always LF +*.sh text eol=lf +Makefile text eol=lf \ No newline at end of file From 3e5b4a10eef4ad6e64782864bd4ce77baec7a9b9 Mon Sep 17 00:00:00 2001 From: Nils DEYBACH <68770774+ndeybach@users.noreply.github.com> Date: Tue, 16 Dec 2025 12:40:17 +0100 Subject: [PATCH 343/365] Add binding for "avcodec_find_best_pix_fmt_of_list" (#2058) --- av/codec/__init__.py | 10 +++++- av/codec/codec.pyi | 26 +++++++++++++- av/codec/codec.pyx | 59 +++++++++++++++++++++++++++++++- include/libavcodec/avcodec.pxd | 7 ++++ tests/test_codec.py | 62 ++++++++++++++++++++++++++++++++++ 5 files changed, 161 insertions(+), 3 deletions(-) diff --git a/av/codec/__init__.py b/av/codec/__init__.py index f35f9b7d4..178ed5106 100644 --- a/av/codec/__init__.py +++ b/av/codec/__init__.py @@ -1,4 +1,11 @@ -from .codec import Capabilities, Codec, Properties, codec_descriptor, codecs_available +from .codec import ( + Capabilities, + Codec, + Properties, + codec_descriptor, + codecs_available, + find_best_pix_fmt_of_list, +) from .context import CodecContext __all__ = ( @@ -7,5 +14,6 @@ "Properties", "codec_descriptor", "codecs_available", + "find_best_pix_fmt_of_list", "CodecContext", ) diff --git a/av/codec/codec.pyi b/av/codec/codec.pyi index 4270c641f..49b0bc43c 100644 --- a/av/codec/codec.pyi +++ b/av/codec/codec.pyi @@ -1,6 +1,6 @@ from enum import Flag, IntEnum from fractions import Fraction -from typing import ClassVar, Literal, cast, overload +from typing import ClassVar, Literal, Sequence, cast, overload from av.audio.codeccontext import AudioCodecContext from av.audio.format import AudioFormat @@ -113,3 +113,27 @@ codecs_available: set[str] def dump_codecs() -> None: ... def dump_hwconfigs() -> None: ... + +PixFmtLike = str | VideoFormat + +def find_best_pix_fmt_of_list( + pix_fmts: Sequence[PixFmtLike], + src_pix_fmt: PixFmtLike, + has_alpha: bool = False, +) -> tuple[VideoFormat | None, int]: + """ + Find the best pixel format to convert to given a source format. + + Wraps :ffmpeg:`avcodec_find_best_pix_fmt_of_list`. + + :param pix_fmts: Iterable of pixel formats to choose from (str or VideoFormat). + :param src_pix_fmt: Source pixel format (str or VideoFormat). + :param bool has_alpha: Whether the source alpha channel is used. + :return: (best_format, loss): best_format is the best matching pixel format from + the list, or None if no suitable format was found; loss is Combination of flags informing you what kind of losses will occur. + :rtype: (VideoFormat | None, int) + + Note on loss: it is a bitmask of FFmpeg loss flags describing what kinds of information would be lost converting from src_pix_fmt to best_format (e.g. loss of alpha, chroma, colorspace, resolution, bit depth, etc.). Multiple losses can be present at once, so the value is meant to be interpreted with bitwise & against FFmpeg's FF_LOSS_* constants. + For exact behavior see: libavutil/pixdesc.c/get_pix_fmt_score() in ffmpeg source code. + """ + ... diff --git a/av/codec/codec.pyx b/av/codec/codec.pyx index aeab61cc3..29040c64f 100644 --- a/av/codec/codec.pyx +++ b/av/codec/codec.pyx @@ -4,9 +4,10 @@ from av.audio.format cimport get_audio_format from av.codec.hwaccel cimport wrap_hwconfig from av.descriptor cimport wrap_avclass from av.utils cimport avrational_to_fraction -from av.video.format cimport get_video_format +from av.video.format cimport VideoFormat, get_pix_fmt, get_video_format from enum import Flag, IntEnum +from libc.stdlib cimport free, malloc cdef object _cinit_sentinel = object() @@ -387,3 +388,59 @@ def dump_hwconfigs(): print(" ", codec.name) for config in configs: print(" ", config) + + +def find_best_pix_fmt_of_list(pix_fmts, src_pix_fmt, has_alpha=False): + """ + Find the best pixel format to convert to given a source format. + + Wraps :ffmpeg:`avcodec_find_best_pix_fmt_of_list`. + + :param pix_fmts: Iterable of pixel formats to choose from (str or VideoFormat). + :param src_pix_fmt: Source pixel format (str or VideoFormat). + :param bool has_alpha: Whether the source alpha channel is used. + :return: (best_format, loss) + :rtype: (VideoFormat | None, int) + """ + cdef lib.AVPixelFormat src + cdef lib.AVPixelFormat best + cdef lib.AVPixelFormat *c_list = NULL + cdef Py_ssize_t n + cdef Py_ssize_t i + cdef object item + cdef int c_loss + + if pix_fmts is None: + raise TypeError("pix_fmts must not be None") + + pix_fmts = tuple(pix_fmts) + if not pix_fmts: + return None, 0 + + if isinstance(src_pix_fmt, VideoFormat): + src = (src_pix_fmt).pix_fmt + else: + src = get_pix_fmt(src_pix_fmt) + + n = len(pix_fmts) + c_list = malloc((n + 1) * sizeof(lib.AVPixelFormat)) + if c_list == NULL: + raise MemoryError() + + try: + for i in range(n): + item = pix_fmts[i] + if isinstance(item, VideoFormat): + c_list[i] = (item).pix_fmt + else: + c_list[i] = get_pix_fmt(item) + c_list[n] = lib.AV_PIX_FMT_NONE + + c_loss = 0 + best = lib.avcodec_find_best_pix_fmt_of_list( + c_list, src, 1 if has_alpha else 0, &c_loss + ) + return get_video_format(best, 0, 0), c_loss + finally: + if c_list != NULL: + free(c_list) diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index b2e78baf0..9f158b88a 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -75,6 +75,13 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef char* avcodec_configuration() cdef char* avcodec_license() + AVPixelFormat avcodec_find_best_pix_fmt_of_list( + const AVPixelFormat *pix_fmt_list, + AVPixelFormat src_pix_fmt, + int has_alpha, + int *loss_ptr, + ) + cdef size_t AV_INPUT_BUFFER_PADDING_SIZE cdef int64_t AV_NOPTS_VALUE diff --git a/tests/test_codec.py b/tests/test_codec.py index 9d72a243c..17432c926 100644 --- a/tests/test_codec.py +++ b/tests/test_codec.py @@ -1,6 +1,7 @@ import pytest from av import AudioFormat, Codec, VideoFormat, codecs_available +from av.codec import find_best_pix_fmt_of_list from av.codec.codec import UnknownCodecError @@ -89,3 +90,64 @@ def test_codec_opus_encoder() -> None: def test_codecs_available() -> None: assert codecs_available + + +def test_find_best_pix_fmt_of_list_empty() -> None: + best, loss = find_best_pix_fmt_of_list([], "rgb24") + assert best is None + assert loss == 0 + + +@pytest.mark.parametrize( + "pix_fmts,src_pix_fmt,expected_best", + [ + (["rgb24", "yuv420p"], "rgb24", "rgb24"), + (["rgb24"], "yuv420p", "rgb24"), + (["yuv420p"], "rgb24", "yuv420p"), + ([VideoFormat("yuv420p")], VideoFormat("rgb24"), "yuv420p"), + ( + ["yuv420p", "yuv444p", "gray", "rgb24", "rgba", "bgra", "yuyv422"], + "rgba", + "rgba", + ), + ], +) +def test_find_best_pix_fmt_of_list_best(pix_fmts, src_pix_fmt, expected_best) -> None: + best, loss = find_best_pix_fmt_of_list(pix_fmts, src_pix_fmt) + assert best is not None + assert best.name == expected_best + assert isinstance(loss, int) + + +@pytest.mark.parametrize( + "pix_fmts,src_pix_fmt", + [ + (["__unknown_pix_fmt"], "rgb24"), + (["rgb24"], "__unknown_pix_fmt"), + ], +) +def test_find_best_pix_fmt_of_list_unknown_pix_fmt(pix_fmts, src_pix_fmt) -> None: + with pytest.raises(ValueError, match="not a pixel format"): + find_best_pix_fmt_of_list(pix_fmts, src_pix_fmt) + + +@pytest.mark.parametrize( + "pix_fmts,src_pix_fmt", + [ + (["rgb24", "bgr24", "gray", "yuv420p", "yuv444p", "yuyv422"], "nv12"), + (["yuv420p", "yuv444p", "gray", "yuv420p"], "rgb24"), + (["rgb24", "rgba", "bgra", "rgb24", "gray"], "yuv420p"), + ], +) +def test_find_best_pix_fmt_of_list_picks_from_list(pix_fmts, src_pix_fmt) -> None: + best, loss = find_best_pix_fmt_of_list(pix_fmts, src_pix_fmt) + assert best is not None + assert best.name in set(pix_fmts) + assert isinstance(loss, int) + + +def test_find_best_pix_fmt_of_list_alpha_loss_flagged_when_used() -> None: + best, loss = find_best_pix_fmt_of_list(["rgb24"], "rgba", has_alpha=True) + assert best is not None + assert best.name == "rgb24" + assert loss != 0 From d88cec040cbe426e4f19ca41c6ac23cede0b8832 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 16 Dec 2025 20:59:10 -0500 Subject: [PATCH 344/365] Fix #2057 --- include/libavcodec/avcodec.pxd | 2 -- 1 file changed, 2 deletions(-) diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 9f158b88a..1edc26ef6 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -336,8 +336,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: AVBufferRef *hw_device_ctx AVPixelFormat (*get_format)(AVCodecContext *s, const AVPixelFormat *fmt) - # User Data - void *opaque cdef AVCodecContext* avcodec_alloc_context3(AVCodec *codec) cdef void avcodec_free_context(AVCodecContext **ctx) From 7a125bc4da02fef2d23115750a404717b55209a2 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 17 Dec 2025 00:08:22 -0500 Subject: [PATCH 345/365] opaque uintptr_t, fix #2036 --- av/opaque.pyx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/av/opaque.pyx b/av/opaque.pyx index 0c2a4507b..626b02985 100644 --- a/av/opaque.pyx +++ b/av/opaque.pyx @@ -1,5 +1,5 @@ cimport libav as lib -from libc.stdint cimport intptr_t, uint8_t +from libc.stdint cimport uint8_t, uintptr_t from libc.string cimport memcpy @@ -15,18 +15,18 @@ cdef class OpaqueContainer: cdef lib.AVBufferRef *add(self, object v): # Use object's memory address as key - cdef intptr_t key = id(v) + cdef uintptr_t key = id(v) self._objects[key] = v - cdef uint8_t *data = lib.av_malloc(sizeof(intptr_t)) + cdef uint8_t *data = lib.av_malloc(sizeof(uintptr_t)) if data == NULL: raise MemoryError("Failed to allocate memory for key") - memcpy(data, &key, sizeof(intptr_t)) + memcpy(data, &key, sizeof(uintptr_t)) # Create the buffer with our free callback cdef lib.AVBufferRef *buffer_ref = lib.av_buffer_create( - data, sizeof(intptr_t), key_free, NULL, 0 + data, sizeof(uintptr_t), key_free, NULL, 0 ) if buffer_ref == NULL: @@ -35,11 +35,11 @@ cdef class OpaqueContainer: return buffer_ref cdef object get(self, char *name): - cdef intptr_t key = (name)[0] + cdef uintptr_t key = (name)[0] return self._objects.get(key) cdef object pop(self, char *name): - cdef intptr_t key = (name)[0] + cdef uintptr_t key = (name)[0] return self._objects.pop(key, None) From 0f9fa2e217e9dda77f0462fe8cb0e8f4f54d5fb8 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 17 Dec 2025 01:26:49 -0500 Subject: [PATCH 346/365] Update changelog --- CHANGELOG.rst | 18 +++++++++++++++++- docs/api/filter.rst | 8 -------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index dce62d944..f02f3ea44 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,22 @@ We are operating with `semantic versioning `_. Note that they these tags will not actually close the issue/PR until they are merged into the "default" branch. + +next +---- + +Features: + +- Add AMD AMF hardware decoding by :gh-user:`ladaapp2`. +- Add subtitle encoding support by :gh-user:`skeskinen` in (:pr:`2050`). +- Add read/write access to PacketSideData by :gh-user:`skeskinen` in (:pr:`2051`). +- Add yuv422p support for video frame to_ndarray and from_ndarray by :gh-user:`wader` in (:pr:`2054`). +- Add binding for ``avcodec_find_best_pix_fmt_of_list()`` by :gh-user:`ndeybach` (:pr:`2058`). + +Fixes: + +- Fix #2036, #2053, #2057 by :gh-user:`WyattBlue`. + v16.0.1 ------- @@ -34,7 +50,7 @@ Major: Features: - Add ``Filter.Context.process_command()`` method by :gh-user:`caffeinism` in (:pr:`2000`). -- Add packet side-data handling mechanism by :gh-user:`tikuma-lsuhsc ` in (:pr:`2003`). +- Add packet side-data handling mechanism by :gh-user:`tikuma-lsuhsc` in (:pr:`2003`). - Implemented set_chapters method by :gh-user:`DE-AI` in (:pr:`2004`). - Declare free-threaded support and support 3.13t by :gh-user:`ngoldbaum` in (:pr:`2005`). - Add writable and copyable attachment and data streams by :gh-user:`skeskinen` in (:pr:`2026`). diff --git a/docs/api/filter.rst b/docs/api/filter.rst index d126fda67..8674fdd9e 100644 --- a/docs/api/filter.rst +++ b/docs/api/filter.rst @@ -24,11 +24,3 @@ Filters .. autoclass:: FilterLink :members: - -.. automodule:: av.filter.pad - - .. autoclass:: FilterPad - :members: - - .. autoclass:: FilterContextPad - :members: From bf32ce5f1e0092a1732d3e2cef8622cbf74efb4e Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 17 Dec 2025 01:31:14 -0500 Subject: [PATCH 347/365] Use ffmpeg 8.0.1 --- .github/workflows/smoke.yml | 11 +++++------ scripts/build | 1 - scripts/comptime.py | 32 -------------------------------- scripts/ffmpeg-8.0.json | 2 +- scripts/ffmpeg-latest.json | 2 +- 5 files changed, 7 insertions(+), 41 deletions(-) delete mode 100644 scripts/comptime.py diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index f75bf1b06..a8bed529b 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -35,10 +35,10 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-24.04, python: "3.14t", ffmpeg: "8.0", extras: true} - - {os: ubuntu-24.04, python: "3.12", ffmpeg: "8.0", extras: true} - - {os: ubuntu-24.04, python: "pypy3.10", ffmpeg: "8.0"} - - {os: macos-14, python: "3.10", ffmpeg: "8.0"} + - {os: ubuntu-24.04, python: "3.14t", ffmpeg: "8.0.1", extras: true} + - {os: ubuntu-24.04, python: "3.12", ffmpeg: "8.0.1", extras: true} + - {os: ubuntu-24.04, python: "pypy3.10", ffmpeg: "8.0.1"} + - {os: macos-14, python: "3.10", ffmpeg: "8.0.1"} env: PYAV_PYTHON: python${{ matrix.config.python }} @@ -76,7 +76,7 @@ jobs: fi ;; macos-14) - brew install automake libtool nasm libpng libvorbis libvpx opus x264 + brew install automake libtool libpng libvorbis libvpx opus x264 ;; esac @@ -149,7 +149,6 @@ jobs: . $CONDA/etc/profile.d/conda.sh conda activate pyav python scripts\\fetch-vendor.py --config-file scripts\\ffmpeg-${{ matrix.config.ffmpeg }}.json $CONDA_PREFIX\\Library - python scripts\\comptime.py ${{ matrix.config.ffmpeg }} python setup.py build_ext --inplace --ffmpeg-dir=$CONDA_PREFIX\\Library - name: Test diff --git a/scripts/build b/scripts/build index 4ee15d003..e3bd7a073 100755 --- a/scripts/build +++ b/scripts/build @@ -22,5 +22,4 @@ ffmpeg -version || exit 3 echo $PYAV_PIP install -U --pre cython setuptools 2> /dev/null -"$PYAV_PYTHON" scripts/comptime.py "$PYAV_PYTHON" setup.py config build_ext --inplace || exit 1 diff --git a/scripts/comptime.py b/scripts/comptime.py deleted file mode 100644 index a03a465f9..000000000 --- a/scripts/comptime.py +++ /dev/null @@ -1,32 +0,0 @@ -import os -import sys - -def replace_in_file(file_path): - try: - with open(file_path, "r", encoding="utf-8") as file: - content = file.read() - - modified_content = content.replace("# [FFMPEG6] ", "") - - with open(file_path, "w") as file: - file.write(modified_content) - except UnicodeDecodeError: - pass - - -def process_directory(directory): - for root, dirs, files in os.walk(directory): - for file in files: - file_path = os.path.join(root, file) - replace_in_file(file_path) - - -version = os.environ.get("PYAV_LIBRARY") -if version is None: - is_6 = sys.argv[1].startswith("6") -else: - is_6 = version.startswith("ffmpeg-6") - -if is_6: - process_directory("av") - process_directory("include") diff --git a/scripts/ffmpeg-8.0.json b/scripts/ffmpeg-8.0.json index fc5719b8a..f8af81c7a 100644 --- a/scripts/ffmpeg-8.0.json +++ b/scripts/ffmpeg-8.0.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0-2/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0.1-1/ffmpeg-{platform}.tar.gz" } diff --git a/scripts/ffmpeg-latest.json b/scripts/ffmpeg-latest.json index fc5719b8a..f8af81c7a 100644 --- a/scripts/ffmpeg-latest.json +++ b/scripts/ffmpeg-latest.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0-2/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0.1-1/ffmpeg-{platform}.tar.gz" } From 8fb7d1de07924649c22f940616fc9bb3b1356b85 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 17 Dec 2025 20:05:21 -0500 Subject: [PATCH 348/365] Return type should not be string literal --- av/subtitles/subtitle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/subtitles/subtitle.py b/av/subtitles/subtitle.py index 68f85fd1d..cdf0e16ec 100644 --- a/av/subtitles/subtitle.py +++ b/av/subtitles/subtitle.py @@ -44,7 +44,7 @@ def create( end: int, pts: int = 0, subtitle_format: int = 1, - ) -> "SubtitleSet": + ) -> SubtitleSet: """ Create a SubtitleSet for encoding. From c5cd12b6b561f1e88754d8179d7cdc9457511e15 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 3 Jan 2026 20:59:11 -0500 Subject: [PATCH 349/365] Fix #2049 --- av/container/output.py | 20 +++++++++++++----- tests/test_streams.py | 46 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/av/container/output.py b/av/container/output.py index 145437752..e57f269e8 100644 --- a/av/container/output.py +++ b/av/container/output.py @@ -278,15 +278,23 @@ def add_data_stream(self, codec_name=None, options: dict | None = None): :rtype: The new :class:`~av.data.stream.DataStream`. """ codec: cython.pointer[cython.const[lib.AVCodec]] = cython.NULL + codec_descriptor: cython.pointer[lib.AVCodecDescriptor] = cython.NULL if codec_name is not None: codec = lib.avcodec_find_encoder_by_name(codec_name.encode()) if codec == cython.NULL: - raise ValueError(f"Unknown data codec: {codec_name}") + codec = lib.avcodec_find_decoder_by_name(codec_name.encode()) + if codec == cython.NULL: + codec_descriptor = lib.avcodec_descriptor_get_by_name( + codec_name.encode() + ) + if codec_descriptor == cython.NULL: + raise ValueError(f"Unknown data codec: {codec_name}") - # Assert that this format supports the requested codec + # Verify format supports this codec + codec_id = codec.id if codec != cython.NULL else codec_descriptor.id if not lib.avformat_query_codec( - self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL + self.ptr.oformat, codec_id, lib.FF_COMPLIANCE_NORMAL ): raise ValueError( f"{self.format.name!r} format does not support {codec_name!r} codec" @@ -297,7 +305,7 @@ def add_data_stream(self, codec_name=None, options: dict | None = None): if stream == cython.NULL: raise MemoryError("Could not allocate stream") - # Set up codec context if we have a codec + # Set up codec context and parameters ctx: cython.pointer[lib.AVCodecContext] = cython.NULL if codec != cython.NULL: ctx = lib.avcodec_alloc_context3(codec) @@ -311,8 +319,10 @@ def add_data_stream(self, codec_name=None, options: dict | None = None): # Initialize stream codec parameters err_check(lib.avcodec_parameters_from_context(stream.codecpar, ctx)) else: - # For raw data streams, just set the codec type + # No codec available - set basic parameters for data stream stream.codecpar.codec_type = lib.AVMEDIA_TYPE_DATA + if codec_descriptor != cython.NULL: + stream.codecpar.codec_id = codec_descriptor.id # Construct the user-land stream py_codec_context: CodecContext | None = None diff --git a/tests/test_streams.py b/tests/test_streams.py index f82ce384b..9387d68cc 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -17,6 +17,8 @@ def cleanup(self): "data.ts", "data_source.ts", "data_copy.ts", + "data_with_codec.ts", + "data_invalid.ts", "out.mkv", "video_with_attachment.mkv", "remuxed_attachment.mkv", @@ -201,6 +203,50 @@ def test_data_stream_from_template(self) -> None: assert remuxed_payloads == copied_payloads + def test_data_stream_with_codec(self) -> None: + """Test adding a data stream with a specific codec name.""" + # Test that invalid codec names raise appropriate errors + with pytest.raises(ValueError, match="Unknown data codec"): + container = av.open("data_invalid.ts", "w") + try: + container.add_data_stream("not_a_real_codec_name_12345") + finally: + container.close() + + # Test that add_data_stream with codec parameter works correctly + # We use "bin_data" which is a data codec that's always available + output_path = "data_with_codec.ts" + with av.open(output_path, "w") as container: + # Try to create a data stream with a codec + # bin_data is a simple passthrough codec for binary data + data_stream = container.add_data_stream("bin_data") + klv_stream = container.add_data_stream("klv") + + assert data_stream.type == "data" + assert klv_stream.type == "data" + # Note: codec_context may be None for descriptor-only data codecs + + test_data = [b"test1", b"test2", b"test3"] + for i, data in enumerate(test_data): + packet = av.Packet(data) + packet.pts = i + packet.stream = data_stream + container.mux(packet) + + with av.open(output_path) as newcontainer: + data_stream = newcontainer.streams.data[0] + klv_stream = newcontainer.streams.data[1] + assert data_stream.type == "data" + assert klv_stream.type == "data" + assert "bin_data" in str(data_stream) + assert "klv" in str(klv_stream) + assert data_stream.name == "bin_data" + assert klv_stream.name == "klv" + try: + os.remove(output_path) + except Exception: + pass + def test_attachment_stream(self) -> None: input_path = av.datasets.curated( "pexels/time-lapse-video-of-night-sky-857195.mp4" From 17137ed8e21019b4a97f6ad787580a1dce436675 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 5 Jan 2026 23:09:41 -0500 Subject: [PATCH 350/365] Use latest ffmpeg --- CHANGELOG.rst | 5 +++-- scripts/ffmpeg-8.0.json | 2 +- scripts/ffmpeg-latest.json | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f02f3ea44..bd15c4e75 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,11 +17,12 @@ We are operating with `semantic versioning `_. are merged into the "default" branch. -next ----- +v16.1.0 +------- Features: +- Add support for Intel QSV codecs by :gh-user:`ladaapp`. - Add AMD AMF hardware decoding by :gh-user:`ladaapp2`. - Add subtitle encoding support by :gh-user:`skeskinen` in (:pr:`2050`). - Add read/write access to PacketSideData by :gh-user:`skeskinen` in (:pr:`2051`). diff --git a/scripts/ffmpeg-8.0.json b/scripts/ffmpeg-8.0.json index f8af81c7a..54968d400 100644 --- a/scripts/ffmpeg-8.0.json +++ b/scripts/ffmpeg-8.0.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0.1-1/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0.1-2/ffmpeg-{platform}.tar.gz" } diff --git a/scripts/ffmpeg-latest.json b/scripts/ffmpeg-latest.json index f8af81c7a..54968d400 100644 --- a/scripts/ffmpeg-latest.json +++ b/scripts/ffmpeg-latest.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0.1-1/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0.1-2/ffmpeg-{platform}.tar.gz" } From b9164b434f040f34314130e8afd0cd7eb4618802 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Tue, 6 Jan 2026 16:21:56 -0500 Subject: [PATCH 351/365] Use ffmpeg 8.0.1-3 --- scripts/fetch-vendor.py | 20 ++++++-------------- scripts/ffmpeg-7.1.json | 3 --- scripts/ffmpeg-8.0.json | 2 +- scripts/ffmpeg-latest.json | 2 +- 4 files changed, 8 insertions(+), 19 deletions(-) delete mode 100644 scripts/ffmpeg-7.1.json diff --git a/scripts/fetch-vendor.py b/scripts/fetch-vendor.py index 47aa2a421..2fb15c676 100644 --- a/scripts/fetch-vendor.py +++ b/scripts/fetch-vendor.py @@ -9,25 +9,18 @@ def get_platform(): system = platform.system() - machine = platform.machine() + is_arm64 = platform.machine() in ("arm64", "aarch64") if system == "Linux": if platform.libc_ver()[0] == "glibc": - return f"manylinux_{machine}" + return "manylinux-aarch64" if is_arm64 else "manylinux-x86_64" else: - return f"musllinux_{machine}" + return "musllinux-aarch64" if is_arm64 else "musllinux-x86_64" elif system == "Darwin": - # cibuildwheel sets ARCHFLAGS: - # https://github.com/pypa/cibuildwheel/blob/5255155bc57eb6224354356df648dc42e31a0028/cibuildwheel/macos.py#L207-L220 - if "ARCHFLAGS" in os.environ: - machine = os.environ["ARCHFLAGS"].split()[1] - return f"macosx_{machine}" + return "macos-arm64" if is_arm64 else "macos-x86_64" elif system == "Windows": - if struct.calcsize("P") * 8 == 64: - return "win_amd64" - else: - return "win32" + return "windows-aarch64" if is_arm64 else "windows-x86_64" else: - raise Exception(f"Unsupported system {system}") + return "unknown" parser = argparse.ArgumentParser(description="Fetch and extract tarballs") @@ -58,6 +51,5 @@ def get_platform(): ["curl", "--location", "--output", tarball_file, "--silent", tarball_url] ) -# extract tarball logging.info(f"Extracting {tarball_name}") subprocess.check_call(["tar", "-C", args.destination_dir, "-xf", tarball_file]) diff --git a/scripts/ffmpeg-7.1.json b/scripts/ffmpeg-7.1.json deleted file mode 100644 index 82ee313d6..000000000 --- a/scripts/ffmpeg-7.1.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/7.1.1-6/ffmpeg-{platform}.tar.gz" -} diff --git a/scripts/ffmpeg-8.0.json b/scripts/ffmpeg-8.0.json index 54968d400..5ec867122 100644 --- a/scripts/ffmpeg-8.0.json +++ b/scripts/ffmpeg-8.0.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0.1-2/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0.1-3/ffmpeg-{platform}.tar.gz" } diff --git a/scripts/ffmpeg-latest.json b/scripts/ffmpeg-latest.json index 54968d400..5ec867122 100644 --- a/scripts/ffmpeg-latest.json +++ b/scripts/ffmpeg-latest.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0.1-2/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0.1-3/ffmpeg-{platform}.tar.gz" } From a14f73069583a4fcc0d5093bde8058e6bd38dbbb Mon Sep 17 00:00:00 2001 From: benedikt-grl Date: Wed, 7 Jan 2026 20:42:44 +0100 Subject: [PATCH 352/365] Add support for VideoEncParams and VideoBlockParams (#2070) --- av/sidedata/encparams.pxd | 11 ++ av/sidedata/encparams.pyi | 27 ++++ av/sidedata/encparams.pyx | 165 +++++++++++++++++++++++++ av/sidedata/sidedata.pyx | 3 + include/libav.pxd | 1 + include/libavutil/video_enc_params.pxd | 25 ++++ tests/test_decode.py | 27 ++++ 7 files changed, 259 insertions(+) create mode 100644 av/sidedata/encparams.pxd create mode 100644 av/sidedata/encparams.pyi create mode 100644 av/sidedata/encparams.pyx create mode 100644 include/libavutil/video_enc_params.pxd diff --git a/av/sidedata/encparams.pxd b/av/sidedata/encparams.pxd new file mode 100644 index 000000000..031b59a42 --- /dev/null +++ b/av/sidedata/encparams.pxd @@ -0,0 +1,11 @@ +cimport libav as lib + +from av.sidedata.sidedata cimport SideData + + +cdef class VideoEncParams(SideData): + pass + + +cdef class VideoBlockParams: + cdef lib.AVVideoBlockParams *ptr diff --git a/av/sidedata/encparams.pyi b/av/sidedata/encparams.pyi new file mode 100644 index 000000000..de962ba51 --- /dev/null +++ b/av/sidedata/encparams.pyi @@ -0,0 +1,27 @@ +from enum import IntEnum +from typing import Any, cast + +import numpy as np + +class VideoEncParamsType(IntEnum): + NONE = cast(int, ...) + VP9 = cast(int, ...) + H264 = cast(int, ...) + MPEG2 = cast(int, ...) + +class VideoEncParams: + nb_blocks: int + blocks_offset: int + block_size: int + codec_type: VideoEncParamsType + qp: int + delta_qp: int + def block_params(self, idx: int) -> VideoBlockParams: ... + def qp_map(self) -> np.ndarray[Any, Any]: ... + +class VideoBlockParams: + src_x: int + src_y: int + w: int + h: int + delta_qp: int diff --git a/av/sidedata/encparams.pyx b/av/sidedata/encparams.pyx new file mode 100644 index 000000000..380dc53d4 --- /dev/null +++ b/av/sidedata/encparams.pyx @@ -0,0 +1,165 @@ +cimport libav as lib +from libc.stdint cimport int32_t, uint8_t + +from enum import IntEnum + +VideoEncParamsType = IntEnum( + "AVVideoEncParamsType", + { + "NONE": lib.AV_VIDEO_ENC_PARAMS_NONE, + "VP9": lib.AV_VIDEO_ENC_PARAMS_VP9, + "H264": lib.AV_VIDEO_ENC_PARAMS_H264, + "MPEG2": lib.AV_VIDEO_ENC_PARAMS_MPEG2, + }, +) + +cdef class VideoEncParams(SideData): + def __repr__(self): + return f"" + + @property + def nb_blocks(self): + """ + Number of blocks in the array + May be 0, in which case no per-block information is present. In this case + the values of blocks_offset / block_size are unspecified and should not + be accessed. + """ + return ( self.ptr.data).nb_blocks + + @property + def blocks_offset(self): + """ + Offset in bytes from the beginning of this structure at which the array of blocks starts. + """ + return ( self.ptr.data).blocks_offset + + @property + def block_size(self): + """ + Size of each block in bytes. May not match sizeof(AVVideoBlockParams). + """ + return ( self.ptr.data).block_size + + @property + def codec_type(self): + """ + Type of the parameters (the codec they are used with). + """ + cdef lib.AVVideoEncParamsType t = ( self.ptr.data).type + return VideoEncParamsType(t) + + @property + def qp(self): + """ + Base quantisation parameter for the frame. The final quantiser for a + given block in a given plane is obtained from this value, possibly + combined with `delta_qp` and the per-block delta in a manner + documented for each type. + """ + return ( self.ptr.data).qp + + @property + def delta_qp(self): + """ + Quantisation parameter offset from the base (per-frame) qp for a given + plane (first index) and AC/DC coefficients (second index). + """ + cdef lib.AVVideoEncParams *p = self.ptr.data + return [[p.delta_qp[i][j] for j in range(2)] for i in range(4)] + + def block_params(self, idx): + """ + Get the encoding parameters for a given block + """ + # Validate given index + if idx < 0 or idx >= self.nb_blocks: + raise ValueError("Expected idx in range [0, nb_blocks)") + + return VideoBlockParams(self, idx) + + def qp_map(self): + """ + Convenience method that creates a 2-D map with the quantization parameters per macroblock. + Only for MPEG2 and H264 encoded videos. + """ + import numpy as np + + cdef int mb_h = (self.frame.ptr.height + 15) // 16 + cdef int mb_w = (self.frame.ptr.width + 15) // 16 + cdef int nb_mb = mb_h * mb_w + cdef int block_idx + cdef int y + cdef int x + cdef VideoBlockParams block + + # Validate number of blocks + if self.nb_blocks != nb_mb: + raise RuntimeError("Expected frame size to match number of blocks in side data") + + # Validate type + cdef lib.AVVideoEncParamsType type = ( self.ptr.data).type + if type != lib.AVVideoEncParamsType.AV_VIDEO_ENC_PARAMS_MPEG2 and type != lib.AVVideoEncParamsType.AV_VIDEO_ENC_PARAMS_H264: + raise ValueError("Expected MPEG2 or H264") + + # Create a 2-D map with the number of macroblocks + cdef int32_t[:, ::1] map = np.empty((mb_h, mb_w), dtype=np.int32) + + # Fill map with quantization parameter per macroblock + for block_idx in range(nb_mb): + block = VideoBlockParams(self, block_idx) + y = block.src_y // 16 + x = block.src_x // 16 + map[y, x] = self.qp + block.delta_qp + + return np.asarray(map) + + +cdef class VideoBlockParams: + def __init__(self, VideoEncParams video_enc_params, int idx) -> None: + cdef uint8_t* base = video_enc_params.ptr.data + cdef Py_ssize_t offset = video_enc_params.blocks_offset + idx * video_enc_params.block_size + self.ptr = (base + offset) + + def __repr__(self): + return f"" + + @property + def src_x(self): + """ + Horizontal distance in luma pixels from the top-left corner of the visible frame + to the top-left corner of the block. + Can be negative if top/right padding is present on the coded frame. + """ + return self.ptr.src_x + + @property + def src_y(self): + """ + Vertical distance in luma pixels from the top-left corner of the visible frame + to the top-left corner of the block. + Can be negative if top/right padding is present on the coded frame. + """ + return self.ptr.src_y + + @property + def w(self): + """ + Width of the block in luma pixels + """ + return self.ptr.w + + @property + def h(self): + """ + Height of the block in luma pixels + """ + return self.ptr.h + + @property + def delta_qp(self): + """ + Difference between this block's final quantization parameter and the + corresponding per-frame value. + """ + return self.ptr.delta_qp diff --git a/av/sidedata/sidedata.pyx b/av/sidedata/sidedata.pyx index 65b1387f0..96c3d72f5 100644 --- a/av/sidedata/sidedata.pyx +++ b/av/sidedata/sidedata.pyx @@ -3,6 +3,7 @@ from libc.stdint cimport int32_t from collections.abc import Mapping from enum import Enum +from av.sidedata.encparams import VideoEncParams from av.sidedata.motionvectors import MotionVectors @@ -49,6 +50,8 @@ class Type(Enum): cdef SideData wrap_side_data(Frame frame, int index): if frame.ptr.side_data[index].type == lib.AV_FRAME_DATA_MOTION_VECTORS: return MotionVectors(_cinit_bypass_sentinel, frame, index) + elif frame.ptr.side_data[index].type == lib.AV_FRAME_DATA_VIDEO_ENC_PARAMS: + return VideoEncParams(_cinit_bypass_sentinel, frame, index) else: return SideData(_cinit_bypass_sentinel, frame, index) diff --git a/include/libav.pxd b/include/libav.pxd index 568913208..5e074a1b8 100644 --- a/include/libav.pxd +++ b/include/libav.pxd @@ -7,6 +7,7 @@ include "libavutil/frame.pxd" include "libavutil/hwcontext.pxd" include "libavutil/samplefmt.pxd" include "libavutil/motion_vector.pxd" +include "libavutil/video_enc_params.pxd" include "libavcodec/avcodec.pxd" include "libavcodec/bsf.pxd" diff --git a/include/libavutil/video_enc_params.pxd b/include/libavutil/video_enc_params.pxd new file mode 100644 index 000000000..aec452666 --- /dev/null +++ b/include/libavutil/video_enc_params.pxd @@ -0,0 +1,25 @@ +from libc.stdint cimport uint32_t, int32_t +from libc.stddef cimport size_t + + +cdef extern from "libavutil/video_enc_params.h" nogil: + cdef enum AVVideoEncParamsType: + AV_VIDEO_ENC_PARAMS_NONE + AV_VIDEO_ENC_PARAMS_VP9 + AV_VIDEO_ENC_PARAMS_H264 + AV_VIDEO_ENC_PARAMS_MPEG2 + + cdef struct AVVideoEncParams: + uint32_t nb_blocks + size_t blocks_offset + size_t block_size + AVVideoEncParamsType type + int32_t qp + int32_t delta_qp[4][2] + + cdef struct AVVideoBlockParams: + int32_t src_x + int32_t src_y + int32_t w + int32_t h + int32_t delta_qp \ No newline at end of file diff --git a/tests/test_decode.py b/tests/test_decode.py index 7ad722bfd..685b743f5 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -2,6 +2,7 @@ import os import pathlib from fractions import Fraction +from typing import cast import numpy as np import pytest @@ -138,6 +139,32 @@ def test_decoded_motion_vectors_no_flag(self) -> None: assert vectors is None return + def test_decoded_video_enc_params(self) -> None: + container = av.open(fate_suite("h264/interlaced_crop.mp4")) + stream = container.streams.video[0] + stream.codec_context.options = {"export_side_data": "venc_params"} + + for frame in container.decode(stream): + video_enc_params = cast( + av.sidedata.encparams.VideoEncParams, + frame.side_data.get("VIDEO_ENC_PARAMS"), + ) + assert video_enc_params is not None + assert video_enc_params.nb_blocks == 40 * 24 + + first_block = video_enc_params.block_params(0) + assert video_enc_params.qp + first_block.delta_qp == 29 + return + + def test_decoded_video_enc_params_no_flag(self) -> None: + container = av.open(fate_suite("h264/interlaced_crop.mp4")) + stream = container.streams.video[0] + # When no additional flag is given, there should be no side data with the video encoding params + + for frame in container.decode(stream): + video_enc_params = frame.side_data.get("VIDEO_ENC_PARAMS") + assert video_enc_params is None + def test_decode_video_corrupt(self) -> None: # write an empty file path = self.sandboxed("empty.h264") From 7e0138cd0b84a1ffcae0f39f0f94c7f3ab9673ed Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 8 Jan 2026 16:18:44 -0500 Subject: [PATCH 353/365] Update authors --- AUTHORS.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/AUTHORS.rst b/AUTHORS.rst index 5de3b44c8..ea0148bf8 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -21,15 +21,15 @@ All contributors (by number of commits): * Justin Wong <46082645+uvjustin@users.noreply.github.com> * Mark Harfouche +- Santtu Keskinen - Alba Mendez +- Curtis Doty ; `@dotysan `_ - Xinran Xu ; `@xxr3376 `_ - z-khan - Marc Mueller <30130371+cdce8p@users.noreply.github.com> - Dan Allan ; `@danielballan `_ - Moonsik Park -- Santtu Keskinen - velsinki <40809145+velsinki@users.noreply.github.com> -- Curtis Doty - Christoph Rackwitz - David Plowman - Alireza Davoudi ; `@adavoudi `_ @@ -44,6 +44,7 @@ All contributors (by number of commits): * zzjjbb <31069326+zzjjbb@users.noreply.github.com> * Joe Schiff <41972063+JoeSchiff@users.noreply.github.com> +* Nils DEYBACH <68770774+ndeybach@users.noreply.github.com> * Dexer <73297572+DexerBR@users.noreply.github.com> * DE-AI <81620697+DE-AI@users.noreply.github.com> * rutsh @@ -57,6 +58,7 @@ All contributors (by number of commits): * Martin Larralde * Simon-Martin Schröder * Matteo Destro +* Mattias Wadman * mephi42 * Miles Kaufmann * Nathan Goldbaum @@ -72,6 +74,7 @@ All contributors (by number of commits): * Ian Lee * Ryan Huang * Arthur Barros +* benedikt-grl * Carlos Ruiz * Carlos Ruiz * Maxime Desroches @@ -86,7 +89,6 @@ All contributors (by number of commits): * Philipp Klaus * Marcell Pardavi * Matteo Destro -* Mattias Wadman * Max Ehrlich * Manuel Goacolou * Julian Schweizer From bae50fa636afc1e5fd4ede2961c824aaa9a5a81d Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 9 Jan 2026 14:08:20 -0500 Subject: [PATCH 354/365] Release 16.1.0 --- av/about.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/av/about.py b/av/about.py index 610c11155..5d194c730 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "16.0.1" +__version__ = "16.1.0" From eb52a8b6eeabfbde37fbaf7e9e4fd929be471080 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E5=B0=8F=E7=99=BD?= <296015668@qq.com> Date: Sun, 11 Jan 2026 09:07:33 +0800 Subject: [PATCH 355/365] Update get_platform() to support more archs --- scripts/fetch-vendor.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/scripts/fetch-vendor.py b/scripts/fetch-vendor.py index 2fb15c676..41801df2b 100644 --- a/scripts/fetch-vendor.py +++ b/scripts/fetch-vendor.py @@ -9,12 +9,11 @@ def get_platform(): system = platform.system() - is_arm64 = platform.machine() in ("arm64", "aarch64") + machine = platform.machine().lower() + is_arm64 = machine in {"arm64", "aarch64"} if system == "Linux": - if platform.libc_ver()[0] == "glibc": - return "manylinux-aarch64" if is_arm64 else "manylinux-x86_64" - else: - return "musllinux-aarch64" if is_arm64 else "musllinux-x86_64" + prefix = "manylinux-" if platform.libc_ver()[0] == "glibc" else "musllinux-" + return prefix + machine elif system == "Darwin": return "macos-arm64" if is_arm64 else "macos-x86_64" elif system == "Windows": From c720be9fe790636216cce09945e39ba269434193 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 10 Jan 2026 19:46:28 -0500 Subject: [PATCH 356/365] Build limited API 3.11+, closes #2033 --- .github/workflows/smoke.yml | 7 +++---- .github/workflows/tests.yml | 7 +++---- pyproject.toml | 3 +-- setup.py | 11 +++++++++++ 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index a8bed529b..d6651f4ee 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -35,10 +35,9 @@ jobs: fail-fast: false matrix: config: - - {os: ubuntu-24.04, python: "3.14t", ffmpeg: "8.0.1", extras: true} - {os: ubuntu-24.04, python: "3.12", ffmpeg: "8.0.1", extras: true} - - {os: ubuntu-24.04, python: "pypy3.10", ffmpeg: "8.0.1"} - - {os: macos-14, python: "3.10", ffmpeg: "8.0.1"} + # - {os: ubuntu-24.04, python: "pypy3.11", ffmpeg: "8.0.1"} + - {os: macos-14, python: "3.11", ffmpeg: "8.0.1"} env: PYAV_PYTHON: python${{ matrix.config.python }} @@ -122,7 +121,7 @@ jobs: fail-fast: false matrix: config: - - {os: windows-latest, python: "3.10", ffmpeg: "latest"} + - {os: windows-latest, python: "3.11", ffmpeg: "latest"} steps: - name: Checkout diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7611e4f18..efa8de710 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -40,9 +40,9 @@ jobs: arch: AMD64 steps: - uses: actions/checkout@v6 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: - python-version: "3.13" + python-version: "3.14" - name: Set Minimum MacOS Target if: runner.os == 'macOS' run: | @@ -61,10 +61,9 @@ jobs: CIBW_ENVIRONMENT_MACOS: PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig LDFLAGS=-headerpad_max_install_names CIBW_ENVIRONMENT_WINDOWS: INCLUDE=C:\\cibw\\vendor\\include LIB=C:\\cibw\\vendor\\lib PYAV_SKIP_TESTS=unicode_filename CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: delvewheel repair --add-path C:\cibw\vendor\bin -w {dest_dir} {wheel} - CIBW_SKIP: "pp*" + CIBW_SKIP: "pp* cp312* cp313* cp314*" CIBW_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m pytest {package}/tests && mv {project}/av.disabled {project}/av CIBW_TEST_REQUIRES: pytest numpy - CIBW_ENABLE: cpython-freethreading run: | pip install cibuildwheel delvewheel cibuildwheel --output-dir dist diff --git a/pyproject.toml b/pyproject.toml index 24bdc90a9..7ab4ae2ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ authors = [ {name = "WyattBlue", email = "wyattblue@auto-editor.com"}, {name = "Jeremy Lainé", email = "jeremy.laine@m4x.org"}, ] -requires-python = ">=3.10" +requires-python = ">=3.11" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", @@ -20,7 +20,6 @@ classifiers = [ "Operating System :: Unix", "Operating System :: Microsoft :: Windows", "Programming Language :: Cython", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", diff --git a/setup.py b/setup.py index ce6bcc388..1db60b649 100644 --- a/setup.py +++ b/setup.py @@ -21,6 +21,12 @@ "swresample", ] +if sys.implementation.name == "cpython": + py_limited_api = True + options = {"bdist_wheel": {"py_limited_api": "cp311"}} +else: + py_limited_api = False + options = {} # Monkey-patch Cython to not overwrite embedded signatures. old_embed_signature = EmbedSignature._embed_signature @@ -142,6 +148,8 @@ def parse_cflags(raw_flags): include_dirs=[f"{IMPORT_NAME}/filter"] + extension_extra["include_dirs"], libraries=extension_extra["libraries"], library_dirs=extension_extra["library_dirs"], + define_macros=[("Py_LIMITED_API", 0x030B0000)], + py_limited_api=py_limited_api, ) compiler_directives = { @@ -186,6 +194,8 @@ def parse_cflags(raw_flags): libraries=extension_extra["libraries"], library_dirs=extension_extra["library_dirs"], sources=[pyx_path], + define_macros=[("Py_LIMITED_API", 0x030B0000)], + py_limited_api=py_limited_api, ), compiler_directives=compiler_directives, build_dir="src", @@ -202,4 +212,5 @@ def parse_cflags(raw_flags): packages=find_packages(include=[f"{IMPORT_NAME}*"]), package_data=package_data, ext_modules=ext_modules, + options=options, ) From 3a42e8f9cec9a5813a06becd7f9ee14f5fd078e5 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 11 Jan 2026 02:24:19 -0500 Subject: [PATCH 357/365] Publish source code first --- .github/workflows/tests.yml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index efa8de710..ca29cb4b3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,19 +8,20 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v6 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: - python-version: "3.13" + python-version: "3.14" - name: Build source package run: | - pip install -U --pre cython setuptools + pip install -U cython setuptools python scripts/fetch-vendor.py --config-file scripts/ffmpeg-latest.json /tmp/vendor PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig python setup.py sdist - - name: Upload source package - uses: actions/upload-artifact@v4 + - name: Publish to PyPI + if: github.event_name == 'release' && github.event.action == 'published' + uses: pypa/gh-action-pypi-publish@release/v1 with: - name: dist-source - path: dist/ + user: __token__ + password: ${{ secrets.PYPI_TOKEN }} package-wheel: runs-on: ${{ matrix.os }} @@ -76,7 +77,7 @@ jobs: publish: runs-on: ubuntu-latest - needs: [package-source, package-wheel] + needs: [package-wheel] steps: - uses: actions/checkout@v6 - uses: actions/download-artifact@v4 From ed09666cc63329550d4957653c7067b3fca3a0d3 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 11 Jan 2026 05:29:16 -0500 Subject: [PATCH 358/365] Enable pypy for smoke again --- .github/workflows/smoke.yml | 2 +- setup.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index d6651f4ee..0b75e3a98 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -36,7 +36,7 @@ jobs: matrix: config: - {os: ubuntu-24.04, python: "3.12", ffmpeg: "8.0.1", extras: true} - # - {os: ubuntu-24.04, python: "pypy3.11", ffmpeg: "8.0.1"} + - {os: ubuntu-24.04, python: "pypy3.11", ffmpeg: "8.0.1"} - {os: macos-14, python: "3.11", ffmpeg: "8.0.1"} env: diff --git a/setup.py b/setup.py index 1db60b649..9be9acb27 100644 --- a/setup.py +++ b/setup.py @@ -24,9 +24,11 @@ if sys.implementation.name == "cpython": py_limited_api = True options = {"bdist_wheel": {"py_limited_api": "cp311"}} + define_macros = [("Py_LIMITED_API", 0x030B0000)] else: py_limited_api = False options = {} + define_macros = [] # Monkey-patch Cython to not overwrite embedded signatures. old_embed_signature = EmbedSignature._embed_signature @@ -148,7 +150,7 @@ def parse_cflags(raw_flags): include_dirs=[f"{IMPORT_NAME}/filter"] + extension_extra["include_dirs"], libraries=extension_extra["libraries"], library_dirs=extension_extra["library_dirs"], - define_macros=[("Py_LIMITED_API", 0x030B0000)], + define_macros=define_macros, py_limited_api=py_limited_api, ) @@ -194,7 +196,7 @@ def parse_cflags(raw_flags): libraries=extension_extra["libraries"], library_dirs=extension_extra["library_dirs"], sources=[pyx_path], - define_macros=[("Py_LIMITED_API", 0x030B0000)], + define_macros=define_macros, py_limited_api=py_limited_api, ), compiler_directives=compiler_directives, From cac1cab0aa8799379c3d918471de468d101fbe30 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 11 Jan 2026 15:16:28 -0500 Subject: [PATCH 359/365] Use ubuntu-slim for linting --- .github/workflows/smoke.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 0b75e3a98..fa78a285c 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -15,16 +15,14 @@ on: - '**.txt' jobs: style: - runs-on: ubuntu-24.04 + runs-on: ubuntu-slim steps: - name: Checkout uses: actions/checkout@v6 - - name: Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: - python-version: "3.12" - + python-version: "3.14" - name: Linters run: make lint @@ -48,7 +46,7 @@ jobs: name: Checkout - name: Python ${{ matrix.config.python }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.config.python }} allow-prereleases: true @@ -71,7 +69,7 @@ jobs: pkg-config \ zlib1g-dev if [[ "${{ matrix.config.extras }}" ]]; then - sudo apt-get install doxygen wget + sudo apt-get install doxygen fi ;; macos-14) From ec7a9d9054a93b687a247f5696105ce8b54249d9 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 11 Jan 2026 15:38:46 -0500 Subject: [PATCH 360/365] Make plane, dictionary pure python --- av/dictionary.pxd | 2 -- av/dictionary.py | 62 ++++++++++++++++++++++++++++++++++++++ av/dictionary.pyx | 57 ----------------------------------- av/plane.pxd | 2 -- av/{plane.pyx => plane.py} | 10 ++++-- 5 files changed, 69 insertions(+), 64 deletions(-) create mode 100644 av/dictionary.py delete mode 100644 av/dictionary.pyx rename av/{plane.pyx => plane.py} (72%) diff --git a/av/dictionary.pxd b/av/dictionary.pxd index 1c59df448..47c100adc 100644 --- a/av/dictionary.pxd +++ b/av/dictionary.pxd @@ -2,9 +2,7 @@ cimport libav as lib cdef class _Dictionary: - cdef lib.AVDictionary *ptr - cpdef _Dictionary copy(self) diff --git a/av/dictionary.py b/av/dictionary.py new file mode 100644 index 000000000..2080266ed --- /dev/null +++ b/av/dictionary.py @@ -0,0 +1,62 @@ +from collections.abc import MutableMapping + +import cython +from cython.cimports.av.error import err_check + + +@cython.cclass +class _Dictionary: + def __cinit__(self, *args, **kwargs): + for arg in args: + self.update(arg) + if kwargs: + self.update(kwargs) + + def __dealloc__(self): + if self.ptr != cython.NULL: + lib.av_dict_free(cython.address(self.ptr)) + + def __getitem__(self, key: cython.str): + element = cython.declare( + cython.pointer[lib.AVDictionaryEntry], + lib.av_dict_get(self.ptr, key, cython.NULL, 0), + ) + if element == cython.NULL: + raise KeyError(key) + return element.value + + def __setitem__(self, key: cython.str, value: cython.str): + err_check(lib.av_dict_set(cython.address(self.ptr), key, value, 0)) + + def __delitem__(self, key: cython.str): + err_check(lib.av_dict_set(cython.address(self.ptr), key, cython.NULL, 0)) + + def __len__(self): + return err_check(lib.av_dict_count(self.ptr)) + + def __iter__(self): + element = cython.declare(cython.pointer[lib.AVDictionaryEntry], cython.NULL) + while True: + element = lib.av_dict_get(self.ptr, "", element, lib.AV_DICT_IGNORE_SUFFIX) + if element == cython.NULL: + break + yield element.key + + def __repr__(self): + return f"bv.Dictionary({dict(self)!r})" + + def copy(self): + other = cython.declare(_Dictionary, Dictionary()) + lib.av_dict_copy(cython.address(other.ptr), self.ptr, 0) + return other + + +class Dictionary(_Dictionary, MutableMapping): + pass + + +@cython.cfunc +def wrap_dictionary(input_: cython.pointer[lib.AVDictionary]) -> _Dictionary: + output = cython.declare(_Dictionary, Dictionary()) + output.ptr = input_ + return output diff --git a/av/dictionary.pyx b/av/dictionary.pyx deleted file mode 100644 index 15de38381..000000000 --- a/av/dictionary.pyx +++ /dev/null @@ -1,57 +0,0 @@ -from collections.abc import MutableMapping - -from av.error cimport err_check - - -cdef class _Dictionary: - def __cinit__(self, *args, **kwargs): - for arg in args: - self.update(arg) - if kwargs: - self.update(kwargs) - - def __dealloc__(self): - if self.ptr != NULL: - lib.av_dict_free(&self.ptr) - - def __getitem__(self, str key): - cdef lib.AVDictionaryEntry *element = lib.av_dict_get(self.ptr, key, NULL, 0) - if element != NULL: - return element.value - else: - raise KeyError(key) - - def __setitem__(self, str key, str value): - err_check(lib.av_dict_set(&self.ptr, key, value, 0)) - - def __delitem__(self, str key): - err_check(lib.av_dict_set(&self.ptr, key, NULL, 0)) - - def __len__(self): - return err_check(lib.av_dict_count(self.ptr)) - - def __iter__(self): - cdef lib.AVDictionaryEntry *element = NULL - while True: - element = lib.av_dict_get(self.ptr, "", element, lib.AV_DICT_IGNORE_SUFFIX) - if element == NULL: - break - yield element.key - - def __repr__(self): - return f"av.Dictionary({dict(self)!r})" - - cpdef _Dictionary copy(self): - cdef _Dictionary other = Dictionary() - lib.av_dict_copy(&other.ptr, self.ptr, 0) - return other - - -class Dictionary(_Dictionary, MutableMapping): - pass - - -cdef _Dictionary wrap_dictionary(lib.AVDictionary *input_): - cdef _Dictionary output = Dictionary() - output.ptr = input_ - return output diff --git a/av/plane.pxd b/av/plane.pxd index df3847d7b..2066bfea8 100644 --- a/av/plane.pxd +++ b/av/plane.pxd @@ -3,9 +3,7 @@ from av.frame cimport Frame cdef class Plane(Buffer): - cdef Frame frame cdef int index - cdef size_t _buffer_size(self) cdef void* _buffer_ptr(self) diff --git a/av/plane.pyx b/av/plane.py similarity index 72% rename from av/plane.pyx rename to av/plane.py index c733b20a7..f84a99c3d 100644 --- a/av/plane.pyx +++ b/av/plane.py @@ -1,12 +1,15 @@ +import cython -cdef class Plane(Buffer): + +@cython.cclass +class Plane(Buffer): """ Base class for audio and video planes. See also :class:`~av.audio.plane.AudioPlane` and :class:`~av.video.plane.VideoPlane`. """ - def __cinit__(self, Frame frame, int index): + def __cinit__(self, frame: Frame, index: cython.int): self.frame = frame self.index = index @@ -16,5 +19,6 @@ def __repr__(self): f"buffer_ptr=0x{self.buffer_ptr:x}; at 0x{id(self):x}>" ) - cdef void* _buffer_ptr(self): + @cython.cfunc + def _buffer_ptr(self) -> cython.p_void: return self.frame.ptr.extended_data[self.index] From 01d950904d2cb2de620ec51849b3d32a4b1a0287 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sun, 11 Jan 2026 16:03:18 -0500 Subject: [PATCH 361/365] Make error.pyx pure python --- av/error.pxd | 1 - av/{error.pyx => error.py} | 246 +++++++++++++++++-------------------- av/error.pyi | 2 +- tests/test_bitstream.py | 2 +- tests/test_errors.py | 8 +- tests/test_python_io.py | 3 +- 6 files changed, 122 insertions(+), 140 deletions(-) rename av/{error.pyx => error.py} (65%) diff --git a/av/error.pxd b/av/error.pxd index d9a542a36..17c37f609 100644 --- a/av/error.pxd +++ b/av/error.pxd @@ -1,3 +1,2 @@ - cdef int stash_exception(exc_info=*) cpdef int err_check(int res, filename=*) except -1 diff --git a/av/error.pyx b/av/error.py similarity index 65% rename from av/error.pyx rename to av/error.py index 123ed735f..8827aafc2 100644 --- a/av/error.pyx +++ b/av/error.py @@ -1,37 +1,47 @@ -cimport libav as lib -from libc.stdio cimport fprintf, stderr -from libc.stdlib cimport free, malloc - -from av.logging cimport get_last_error - import errno import os import sys import traceback from threading import local +import cython +from cython.cimports import libav as lib +from cython.cimports.av.logging import get_last_error +from cython.cimports.libc.stdio import fprintf, stderr +from cython.cimports.libc.stdlib import free, malloc + # Will get extended with all of the exceptions. __all__ = [ - "ErrorType", "FFmpegError", "LookupError", "HTTPError", "HTTPClientError", + "ErrorType", + "FFmpegError", + "LookupError", + "HTTPError", + "HTTPClientError", "UndefinedError", ] +sentinel = cython.declare(object, object()) -cpdef code_to_tag(int code): +@cython.ccall +def code_to_tag(code: cython.int) -> bytes: """Convert an integer error code into 4-byte tag. >>> code_to_tag(1953719668) b'test' """ - return bytes(( - code & 0xff, - (code >> 8) & 0xff, - (code >> 16) & 0xff, - (code >> 24) & 0xff, - )) - -cpdef tag_to_code(bytes tag): + return bytes( + ( + code & 0xFF, + (code >> 8) & 0xFF, + (code >> 16) & 0xFF, + (code >> 24) & 0xFF, + ) + ) + + +@cython.ccall +def tag_to_code(tag: bytes) -> cython.int: """Convert a 4-byte error tag into an integer code. >>> tag_to_code(b'test') @@ -40,12 +50,7 @@ """ if len(tag) != 4: raise ValueError("Error tags are 4 bytes.") - return ( - (tag[0]) + - (tag[1] << 8) + - (tag[2] << 16) + - (tag[3] << 24) - ) + return (tag[0]) + (tag[1] << 8) + (tag[2] << 16) + (tag[3] << 24) class FFmpegError(Exception): @@ -78,8 +83,8 @@ def __init__(self, code, message, filename=None, log=None): args.append(filename) if log: args.append(log) - super(FFmpegError, self).__init__(*args) - self.args = tuple(args) # FileNotFoundError/etc. only pulls 2 args. + super().__init__(*args) + self.args = tuple(args) @property def filename(self): @@ -104,14 +109,16 @@ def __str__(self): if self.filename: msg = f"{msg}: {self.filename!r}" if self.log: - msg = f"{msg}; last error log: [{self.log[1].strip()}] {self.log[2].strip()}" + msg = ( + f"{msg}; last error log: [{self.log[1].strip()}] {self.log[2].strip()}" + ) return msg # Our custom error, used in callbacks. -cdef int c_PYAV_STASHED_ERROR = tag_to_code(b"PyAV") -cdef str PYAV_STASHED_ERROR_message = "Error in PyAV callback" +c_PYAV_STASHED_ERROR: cython.int = tag_to_code(b"PyAV") +PYAV_STASHED_ERROR_message: str = "Error in PyAV callback" # Bases for the FFmpeg-based exceptions. @@ -128,6 +135,8 @@ class HTTPClientError(FFmpegError): # Tuples of (enum_name, enum_value, exc_name, exc_base). +# tuple[str, int, str | None, Exception | none] +# fmt: off _ffmpeg_specs = ( ("BSF_NOT_FOUND", -lib.AVERROR_BSF_NOT_FOUND, "BSFNotFoundError", LookupError), ("BUG", -lib.AVERROR_BUG, None, RuntimeError), @@ -156,8 +165,7 @@ class HTTPClientError(FFmpegError): ("HTTP_SERVER_ERROR", -lib.AVERROR_HTTP_SERVER_ERROR, "HTTPServerError", HTTPError), ("PYAV_CALLBACK", c_PYAV_STASHED_ERROR, "PyAVCallbackError", RuntimeError), ) - -cdef sentinel = object() +# fmt: on class EnumType(type): @@ -189,59 +197,24 @@ def __len__(self): def __iter__(self): return iter(self._all) - def __getitem__(self, key): - if isinstance(key, str): - return self._by_name[key] - if isinstance(key, int): - try: - return self._by_value[key] - except KeyError: - pass - - raise KeyError(key) - - if isinstance(key, self): - return key - - raise TypeError(f"{self.__name__} indices must be str, int, or itself") - - def _get(self, long value, bint create=False): - try: - return self._by_value[value] - except KeyError: - pass - - if not create: - return - - return self._create(f"{self.__name__.upper()}_{value}", value, by_value_only=True) - - def get(self, key, default=None, create=False): - try: - return self[key] - except KeyError: - if create: - return self._get(key, create=True) - return default - - -cdef class EnumItem: +@cython.cclass +class EnumItem: """An enumeration of FFmpeg's error types. -.. attribute:: tag + .. attribute:: tag - The FFmpeg byte tag for the error. + The FFmpeg byte tag for the error. -.. attribute:: strerror + .. attribute:: strerror - The error message that would be returned. + The error message that would be returned. + """ -""" - cdef readonly str name - cdef readonly int value + name = cython.declare(str, visibility="readonly") + value = cython.declare(cython.int, visibility="readonly") - def __cinit__(self, sentinel_, str name, int value, doc=None): + def __cinit__(self, sentinel_, name: str, value: cython.int, doc=None): if sentinel_ is not sentinel: raise RuntimeError(f"Cannot instantiate {self.__class__.__name__}.") @@ -263,42 +236,25 @@ def tag(self): return code_to_tag(self.value) -ErrorType = EnumType("ErrorType", (EnumItem, ), {"__module__": __name__}, [x[:2] for x in _ffmpeg_specs]) +ErrorType = EnumType( + "ErrorType", (EnumItem,), {"__module__": __name__}, [x[:2] for x in _ffmpeg_specs] +) for enum in ErrorType: - # Mimic the errno module. + # Mimick the errno module. globals()[enum.name] = enum if enum.value == c_PYAV_STASHED_ERROR: enum.strerror = PYAV_STASHED_ERROR_message else: enum.strerror = lib.av_err2str(-enum.value) - -# Mimic the builtin exception types. -# See https://www.python.org/dev/peps/pep-3151/#new-exception-classes -# Use the named ones we have, otherwise default to OSError for anything in errno. - -# See this command for the count of POSIX codes used: -# -# egrep -IR 'AVERROR\(E[A-Z]+\)' vendor/ffmpeg-4.2 |\ -# sed -E 's/.*AVERROR\((E[A-Z]+)\).*/\1/' | \ -# sort | uniq -c -# -# The biggest ones that don't map to PEP 3151 builtins: -# -# 2106 EINVAL -> ValueError -# 649 EIO -> IOError (if it is distinct from OSError) -# 4080 ENOMEM -> MemoryError -# 340 ENOSYS -> NotImplementedError -# 35 ERANGE -> OverflowError - -classes = {} +classes: dict = {} def _extend_builtin(name, codes): base = getattr(__builtins__, name, OSError) - cls = type(name, (FFmpegError, base), dict(__module__=__name__)) + cls = type(name, (FFmpegError, base), {"__module__": __name__}) # Register in builder. for code in codes: @@ -311,30 +267,56 @@ def _extend_builtin(name, codes): return cls -# PEP 3151 builtins. _extend_builtin("PermissionError", (errno.EACCES, errno.EPERM)) -_extend_builtin("BlockingIOError", (errno.EAGAIN, errno.EALREADY, errno.EINPROGRESS, errno.EWOULDBLOCK)) -_extend_builtin("ChildProcessError", (errno.ECHILD, )) -_extend_builtin("ConnectionAbortedError", (errno.ECONNABORTED, )) -_extend_builtin("ConnectionRefusedError", (errno.ECONNREFUSED, )) -_extend_builtin("ConnectionResetError", (errno.ECONNRESET, )) -_extend_builtin("FileExistsError", (errno.EEXIST, )) -_extend_builtin("InterruptedError", (errno.EINTR, )) -_extend_builtin("IsADirectoryError", (errno.EISDIR, )) -_extend_builtin("FileNotFoundError", (errno.ENOENT, )) -_extend_builtin("NotADirectoryError", (errno.ENOTDIR, )) +_extend_builtin( + "BlockingIOError", + (errno.EAGAIN, errno.EALREADY, errno.EINPROGRESS, errno.EWOULDBLOCK), +) +_extend_builtin("ChildProcessError", (errno.ECHILD,)) +_extend_builtin("ConnectionAbortedError", (errno.ECONNABORTED,)) +_extend_builtin("ConnectionRefusedError", (errno.ECONNREFUSED,)) +_extend_builtin("ConnectionResetError", (errno.ECONNRESET,)) +_extend_builtin("FileExistsError", (errno.EEXIST,)) +_extend_builtin("InterruptedError", (errno.EINTR,)) +_extend_builtin("IsADirectoryError", (errno.EISDIR,)) +_extend_builtin("FileNotFoundError", (errno.ENOENT,)) +_extend_builtin("NotADirectoryError", (errno.ENOTDIR,)) _extend_builtin("BrokenPipeError", (errno.EPIPE, errno.ESHUTDOWN)) -_extend_builtin("ProcessLookupError", (errno.ESRCH, )) -_extend_builtin("TimeoutError", (errno.ETIMEDOUT, )) +_extend_builtin("ProcessLookupError", (errno.ESRCH,)) +_extend_builtin("TimeoutError", (errno.ETIMEDOUT,)) +_extend_builtin("MemoryError", (errno.ENOMEM,)) +_extend_builtin("NotImplementedError", (errno.ENOSYS,)) +_extend_builtin("OverflowError", (errno.ERANGE,)) +_extend_builtin("OSError", [code for code in errno.errorcode if code not in classes]) -# Other obvious ones. -_extend_builtin("ValueError", (errno.EINVAL, )) -_extend_builtin("MemoryError", (errno.ENOMEM, )) -_extend_builtin("NotImplementedError", (errno.ENOSYS, )) -_extend_builtin("OverflowError", (errno.ERANGE, )) -# The rest of them (for now) -_extend_builtin("OSError", [code for code in errno.errorcode if code not in classes]) +class ArgumentError(FFmpegError): + def __str__(self): + msg = "" + if self.strerror is not None: + msg = f"{msg}{self.strerror}" + if self.filename: + msg = f"{msg}: {self.filename!r}" + if self.errno is not None: + msg = f"{msg} returned {self.errno}" + if self.log: + msg = ( + f"{msg}; last error log: [{self.log[1].strip()}] {self.log[2].strip()}" + ) + + return msg + + +class UndefinedError(FFmpegError): + """Fallback exception type in case FFmpeg returns an error we don't know about.""" + + pass + + +classes[errno.EINVAL] = ArgumentError +globals()["ArgumentError"] = ArgumentError +__all__.append("ArgumentError") + # Classes for the FFmpeg errors. for enum_name, code, name, base in _ffmpeg_specs: @@ -360,10 +342,12 @@ def _extend_builtin(name, codes): # Storage for stashing. -cdef object _local = local() -cdef int _err_count = 0 +_local: object = local() +_err_count: cython.int = 0 + -cdef int stash_exception(exc_info=None): +@cython.cfunc +def stash_exception(exc_info=None) -> cython.int: global _err_count existing = getattr(_local, "exc_info", None) @@ -380,9 +364,12 @@ def _extend_builtin(name, codes): return -c_PYAV_STASHED_ERROR -cdef int _last_log_count = 0 +_last_log_count: cython.int = 0 -cpdef int err_check(int res, filename=None) except -1: + +@cython.ccall +@cython.exceptval(-1, check=False) +def err_check(res: cython.int, filename=None) -> cython.int: """Raise appropriate exceptions from library return code.""" global _err_count @@ -394,7 +381,7 @@ def _extend_builtin(name, codes): if exc_info is not None: _err_count -= 1 _local.exc_info = None - raise exc_info[0], exc_info[1], exc_info[2] + raise exc_info[1].with_traceback(exc_info[2]) if res >= 0: return res @@ -407,9 +394,11 @@ def _extend_builtin(name, codes): else: log = None - cdef int code = -res - cdef char* error_buffer = malloc(lib.AV_ERROR_MAX_STRING_SIZE * sizeof(char)) - if error_buffer == NULL: + code: cython.int = -res + error_buffer: cython.p_char = cython.cast( + cython.p_char, malloc(lib.AV_ERROR_MAX_STRING_SIZE * cython.sizeof(char)) + ) + if error_buffer == cython.NULL: raise MemoryError() try: @@ -424,8 +413,3 @@ def _extend_builtin(name, codes): raise cls(code, message, filename, log) finally: free(error_buffer) - - -class UndefinedError(FFmpegError): - """Fallback exception type in case FFmpeg returns an error we don't know about.""" - pass diff --git a/av/error.pyi b/av/error.pyi index e18f7334c..354c5fd3a 100644 --- a/av/error.pyi +++ b/av/error.pyi @@ -69,4 +69,4 @@ class OSError(FFmpegError, builtins.OSError): ... class PermissionError(FFmpegError, builtins.PermissionError): ... class ProcessLookupError(FFmpegError, builtins.ProcessLookupError): ... class TimeoutError(FFmpegError, builtins.TimeoutError): ... -class ValueError(FFmpegError, builtins.ValueError): ... +class ArgumentError(FFmpegError): ... diff --git a/tests/test_bitstream.py b/tests/test_bitstream.py index dde7723c7..5646b46b6 100644 --- a/tests/test_bitstream.py +++ b/tests/test_bitstream.py @@ -97,7 +97,7 @@ def test_filter_flush() -> None: container.seek(0) # Without flushing, we expect to get an error: "A non-NULL packet sent after an EOF." - with pytest.raises(ValueError): + with pytest.raises(av.ArgumentError): for p in container.demux(stream): ctx.filter(p) diff --git a/tests/test_errors.py b/tests/test_errors.py index f654c6b2e..68e2681e1 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -7,7 +7,7 @@ def test_stringify() -> None: - for cls in (av.ValueError, av.FileNotFoundError, av.DecoderNotFoundError): + for cls in (av.FileNotFoundError, av.DecoderNotFoundError): e = cls(1, "foo") assert f"{e}" == "[Errno 1] foo" assert f"{e!r}" == f"{cls.__name__}(1, 'foo')" @@ -16,7 +16,7 @@ def test_stringify() -> None: == f"av.error.{cls.__name__}: [Errno 1] foo\n" ) - for cls in (av.ValueError, av.FileNotFoundError, av.DecoderNotFoundError): + for cls in (av.FileNotFoundError, av.DecoderNotFoundError): e = cls(1, "foo", "bar.txt") assert f"{e}" == "[Errno 1] foo: 'bar.txt'" assert f"{e!r}" == f"{cls.__name__}(1, 'foo', 'bar.txt')" @@ -27,9 +27,7 @@ def test_stringify() -> None: def test_bases() -> None: - assert issubclass(av.ValueError, ValueError) - assert issubclass(av.ValueError, av.FFmpegError) - + assert issubclass(av.ArgumentError, av.FFmpegError) assert issubclass(av.FileNotFoundError, FileNotFoundError) assert issubclass(av.FileNotFoundError, OSError) assert issubclass(av.FileNotFoundError, av.FFmpegError) diff --git a/tests/test_python_io.py b/tests/test_python_io.py index 5a4d1becf..c240a45e6 100644 --- a/tests/test_python_io.py +++ b/tests/test_python_io.py @@ -323,7 +323,8 @@ def test_writing_to_pipe_writeonly(self) -> None: buf = WriteOnlyPipe() with pytest.raises( - ValueError, match=escape("[mp4] muxer does not support non seekable output") + av.ArgumentError, + match=escape("[mp4] muxer does not support non seekable output"), ): write(buf) From ab03fa9c712975ad7264c7b496f43dde3769d9e3 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 16 Jan 2026 00:51:17 -0500 Subject: [PATCH 362/365] New changelog structure --- CHANGELOG.rst | 942 ++------------------------------------------------ 1 file changed, 31 insertions(+), 911 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index bd15c4e75..86eb44f2e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,18 +4,36 @@ Changelog We are operating with `semantic versioning `_. .. - Please try to update this file in the commits that make the changes. + Update this file in your commit that makes a change (besides maintainence). - To make merging/rebasing easier, we don't manually break lines in here - when they are too long, so any particular change is just one line. + To make merging/rebasing easier, don't manually break lines in here when they are too long. + To make tracking easier, please add either ``closes #123`` or ``fixes #123`` to the first line of the commit message, when closing/fixing a GitHub issue. + Changelog entries will be limited to the latest major version and (next) to prevent exponential growth in file storage. + Use the Oxford comma. - To make tracking easier, please add either ``closes #123`` or ``fixes #123`` - to the first line of the commit message. There are more syntaxes at: - . + Entries look like this: - Note that they these tags will not actually close the issue/PR until they - are merged into the "default" branch. + v21.67.42 + --------- + Major: + - Breaking changes (MAJOR) go here, including for binary wheels. + Features: + - Features (MINOR) changes go here. + + Fixes: + - Bug fixes (PATCH) go here. + - $CHANGE by :gh-user:`mikeboers` in (:pr:`1`). + +v17.0.0 (Next) +-------------- + +Major: + +- Limited API binary wheels are now built. +- 3.13t (free-threading) will be dropped because of storage limitations. +- When an FFmpeg C function indicates an error, raise av.ArgumentError instead of ValueError/av.ValueError. This helps disambiguate why an exception is being thrown. +- Save space by removing libaom (av1 encoder/decoder); dav1d, stvav1, and hardware, are available. v16.1.0 ------- @@ -62,907 +80,9 @@ Fixes: - Allow ``None`` in ``FilterContext.push()`` type stub by :gh-user:`velsinki` in (:pr:`2015`). - Fix typos -v15.1.0 -------- - -Features: - -- Support FFmpeg 8 -- Make the `Frame.key_frame` flag writable. - -v15.0.0 -------- - -Major: - -- Make ``SubtitleStream.decode()`` return the list of subtitles directly, making the API nicer. -- Drop support for MacOS 12 or under. -- Binary wheels: Remove libxml2 because of security concerns. -- Binary wheels: Remove xz because lzma encoding is very niche in media decoding and lesser security concerns. - -Features: - -- Add GBRP, GBRAP, RGBA formats by :gh-user:`z-khan`. -- Add ``VideoFrame.save()``, a convenient way to save frames to PNGs or JPEGs, by :gh-user:`WyattBlue`. - -Fixes: - -- Binary wheels: Re-enable libx265, use version 4.1 by :gh-user:`WyattBlue`. -- Fix memory error for AudioFifo properties by :gh-user:`zzjjbb`. -- Copy template flags if creating a new Codec object by :gh-user:`WyattBlue` -- ``AudioFrame.from_ndarray()``: Support python classes for parameters too, by :gh-user:`WyattBlue`. - -v14.4.0 -------- - -Features: - -- Expose the duration field for frames by :gh-user:`Queuecumber` in (:pr:`1880`). -- Add support for BGR48 and BGRA64 pixel formats by :gh-user:`z-khan` in (:pr:`1873`). - -Fixes: - -- Fix security vulnerabilities in binary wheels by :gh-user:`jlaine` in (:pr:`1896`). - -v14.3.0 -------- - -Features: - -- Support yuv420p10le in numpy conversion by :gh-user:`tsnl` in (:pr:`1833`). -- Add typing overloads for ``CodecContext.create`` by :gh-user:`jlaine` in (:pr:`1853`). -- Provide binary wheels for musl-based distros such as Alpine by :gh-user:`jlaine` in (:pr:`1856`). - -Fixes: - -- Fix ``Packet.dts`` typing by :gh-user:`daveisfera` in (:pr:`1815`). -- Use Python 3 compatible stderr logging by :gh-user:`pmarcelll` in (:pr:`1832`). -- Uses ffmpeg 7.1.1, fixes deadlocks. - -v14.2.0 -------- - -Features: - -- Add support for external flags in hwacccel by :gh-user:`materight` in (:pr:`1751`). -- Add Bayer pixel formats by :gh-user:`z-khan` in (:pr:`1755`). -- Add support for yuv422p10le pix_fmt by :gh-user:`WyattBlue` in (:pr:`1767`). -- Add ``supported_np_pix_fmts`` by :gh-user:`WyattBlue` in (:pr:`1766`). -- Add ``Codec.canonical_name`` by :gh-user:`WyattBlue`. - -Misc: - -- Drop support for MacOS 11 by :gh-user:`WyattBlue` in (:pr:`1764`). -- Set default bitrate to 0, allowing variable bitrate by :gh-user:`WyattBlue`. - - -v14.1.0 -------- - -Features: - -- Add hardware decoding by :gh-user:`matthewlai` and :gh-user:`WyattBlue` in (:pr:`1685`). -- Add ``Stream.disposition`` and ``Disposition`` enum by :gh-user:`WyattBlue` in (:pr:`1720`). -- Add ``VideoFrame.rotation`` by :gh-user:`lgeiger` in (:pr:`1675`). -- Support grayf32le and gbrapf32le in numpy conversion by :gh-user:`robinechuca` in (:pr:`1712`). -- Support yuv[a]p16 formats in numpy conversion by :gh-user:`robinechuca` in (:pr:`1722`). - -v14.0.1 -------- - -Fixes: - -- Include header files in source distribution by :gh-user:`hmaarrfk` in (:pr:`1662`). -- Cleanup ``AVStream.side_data`` leftovers by :gh-user:`lgeiger` in (:pr:`1674`). -- Address :issue:`1663` by :gh-user:`WyattBlue`. -- Make ``mp3`` work with ``OutputContainer.add_stream_from_template()``. - -v14.0.0 -------- - -Major: - -- Drop FFmpeg 6. -- Drop support for MacOS <11 in our binary wheels. -- Deleted PyAV's custom Enum class in favor of Python's standard Enums. -- Remove ``CodecContext.close()`` and ``Stream.side_data`` because it's deprecated in ffmpeg. -- Remove ``AVError`` alias (use ``FFmpegError`` directly instead). -- Remove the `template` arg from ``OutputContainer.add_stream()``. - -Features: - -- Add ``OutputContainer.add_stream_from_template()`` by :gh-user:`WyattBlue` and :gh-user:`cdce8p`. -- Add ``OutputContainer.add_data_stream()`` by :gh-user:`WyattBlue`. -- Add ``filter.loudnorm.stats()`` function that returns the stats of loudnorm for 2-pass filtering by :gh-user:`WyattBlue`. -- Add ``qmin`` and ``qmax`` parameters to the ``VideoCodecContext`` by :gh-user:`davidplowman` in (:pr:`1618`). -- Allow the profile of a codec to be set as well as queried by :gh-user:`davidplowman` in (:pr:`1625`). - -Fixes: - -- Make ``VideoFrame.from_numpy_buffer()`` support buffers with padding by :gh-user:`davidplowman` in (:pr:`1635`). -- Correct ``Colorspace``'s lowercase enums. -- Updated ``sidedata.Type`` enum. -- Ensure streams in StreamContainer are released. Fixes :issue:`1599`. - -v13.1.0 -------- - -Features: - -- Allow passing Python objects around using `Frame.opaque` and `Packet.opaque` by :gh-user:`CarlosRDomin` and :gh-user:`WyattBlue` in (:pr:`1533`). -- Allow extradata to be set by encoders by :gh-user:`daveisfera` in (:pr:`1526`). -- Add getting ffmpeg version info string by :gh-user:`hmaarrfk` in (:pr:`1564`). - -Fixes: - -- Remove the `deprecation` module in anticipation of `PEP 702 `_. -- Add type stubs to previously unannotated API sections. -- Improve type stubs for both `mypy` and `mypy --strict`. -- Permit only setting `time_base` with a Fraction, as mypy is unable to respect different types in getters vs setters. -- Declare `write_packet` function as const by :gh-user:`hmaarrfk` in (:pr:`1517`). - -v13.0.0 -------- - -Major: - -- Drop FFmpeg 5, Support FFmpeg 7. -- Drop Python 3.8, Support Python 3.13. -- Update FFmpeg to 7.0.2 for the binary wheels. -- Disallow initializing an AudioLayout object with an int. -- Disallow accessing gop_size, timebase as a decoder (Raised deprecation warning before). -- Remove `ticks_per_frame` property because it was deprecated in FFmpeg. - -Features: - -- Add AttachmentStream class. -- Add `best()` method to StreamContainer. -- Add `set_audio_frame_size()` method to Graph object. -- Add `nb_channels` property to AudioLayout object. -- Add `from_bytes()` method to VideoFrame object. - -Fixes: - -- Fix VideoCC's repl breaking when `self._format` is None. -- Fix getting `pix_fmt` property when VideoCC's `self._format` is None. - -v12.3.0 -------- - -Features: - -- Support libav's `av_log_set_level` by :gh-user:`materight` in (:pr:`1448`). -- Add Graph.link_nodes by :gh-user:`WyattBlue` in (:pr:`1449`). -- Add default codec properties by :gh-user:`WyattBlue` in (:pr:`1450`). -- Remove the xvid and ass packages in ffmpeg binaries because they were unused by :gh-user:`WyattBlue` in (:pr:`1462`). -- Add supported_codecs property to OutputContainer by :gh-user:`WyattBlue` in (:pr:`1465`). -- Add text and dialogue property to AssSubtitle, remove TextSubtitle by :gh-user:`WyattBlue` in (:pr:`1456`). - -Fixes: - -- Include libav headers in final distribution by :gh-user:`materight` in (:pr:`1455`). -- Fix segfault when calling subtitle_stream.decode() by :gh-user:`WyattBlue` in (:pr:`1460`). -- flushing subtitle decoder requires a new uninitialized packet by :gh-user:`moonsikpark` in (:pr:`1461`). -- Set default color range for VideoReformatter.format() by :gh-user:`elxy` in (:pr:`1458`). -- Resampler: format, layout accepts `str` `int` too by :gh-user:`WyattBlue` in (:pr:`1446`). - -v12.2.0 -------- - -Features: - -- Add a `make_writable` method to `Frame` instances (:issue:`1414`). -- Use `av_guess_sample_aspect_ratio` to report sample and display aspect ratios. - -Fixes: - -- Fix a crash when assigning an `AudioLayout` to `AudioCodecContext.layout` (:issue:`1434`). -- Remove a circular reference which caused `AudioSampler` to occupy memory until garbage collected (:issue:`1429`). -- Fix more type stubs, remove incorrect `__init__.pyi`. - -v12.1.0 -------- - -Features: - -- Build binary wheels with webp support. -- Allow disabling logs, disable logs by default. -- Add bitstream filters by :gh-user:`skeskinen` in (:pr:`1379` :issue:`1375`). -- Expose CodecContext flush_buffers by :gh-user:`skeskinen` in (:pr:`1382`). - -Fixes: - -- Fix type stubs, add missing type stubs. -- Add S12M_TIMECODE by :gh-user:`WyattBlue` in (:pr:`1381`). -- Subtitle.text now returns bytes by :gh-user:`WyattBlue` in (:pr:`1398`). -- Allow packet.duration to be writable by :gh-user:`WyattBlue` in (:pr:`1399`). -- Remove deprecated `VideoStream.frame_rate` by :gh-user:`WyattBlue` in (:pr:`1351`). -- Build with Arm for PyPy now by :gh-user:`WyattBlue` in (:pr:`1395`). -- Fix #1378 by :gh-user:`WyattBlue` in (:pr:`1400`). -- setup.py: use PKG_CONFIG env var to get the pkg-config to use by :gh-user:`Artturin` in (:pr:`1387`). - -v12.0.0 -------- - -Major: - -- Add type hints. -- Update FFmpeg to 6.1.1 for the binary wheels. -- Update libraries for the binary wheels (notably dav1d to 1.4.1). -- Deprecate VideoCodecContext.gop_size for decoders by :gh-user:`JoeSchiff` in (:pr:`1256`). -- Deprecate frame.index by :gh-user:`JoeSchiff` in (:pr:`1218`). - -Features: - -- Allow using pathlib.Path for av.open by :gh-user:`WyattBlue` in (:pr:`1231`). -- Add `max_b_frames` property to CodecContext by :gh-user:`davidplowman` in (:pr:`1119`). -- Add `encode_lazy` method to CodecContext by :gh-user:`rawler` in (:pr:`1092`). -- Add `color_range` to CodecContext/Frame by :gh-user:`johanjeppsson` in (:pr:`686`). -- Set `time_base` for AudioResampler by :gh-user:`daveisfera` in (:issue:`1209`). -- Add support for ffmpeg's AVCodecContext::delay by :gh-user:`JoeSchiff` in (:issue:`1279`). -- Add `color_primaries`, `color_trc`, `colorspace` to VideoStream by :gh-user:`WyattBlue` in (:pr:`1304`). -- Add `bits_per_coded_sample` to VideoCodecContext by :gh-user:`rvanlaar` in (:pr:`1203`). -- AssSubtitle.ass now returns as bytes by :gh-user:`WyattBlue` in (:pr:`1333`). -- Expose DISPLAYMATRIX side data by :gh-user:`hyenal` in (:pr:`1249`). - -Fixes: - -- Convert deprecated Cython extension class properties to decorator syntax by :gh-user:`JoeSchiff`. -- Check None packet when setting time_base after decode by :gh-user:`philipnbbc` in (:pr:`1281`). -- Remove deprecated `Buffer.to_bytes` by :gh-user:`WyattBlue` in (:pr:`1286`). -- Remove deprecated `Packet.decode_one` by :gh-user:`WyattBlue` in (:pr:`1301`). - -v11.0.0 -------- - -Major: - -- Add support for FFmpeg 6.0, drop support for FFmpeg < 5.0. -- Add support for Python 3.12, drop support for Python < 3.8. -- Build binary wheels against libvpx 1.13.1 to fix CVE-2023-5217. -- Build binary wheels against FFmpeg 6.0. - -Features: - -- Add support for the `ENCODER_FLUSH` encoder flag (:issue:`1067`). -- Add VideoFrame ndarray operations for yuv444p/yuvj444p formats (:issue:`788`). -- Add setters for `AVFrame.dts`, `AVPacket.is_keyframe` and `AVPacket.is_corrupt` (:issue:`1179`). - -Fixes: - -- Fix build using Cython 3 (:issue:`1140`). -- Populate new streams with codec parameters (:issue:`1044`). -- Explicitly set `python_requires` to avoid installing on incompatible Python (:issue:`1057`). -- Make `AudioFifo.__repr__` safe before the first frame (:issue:`1130`). -- Guard input container members against use after closes (:issue:`1137`). - -v10.0.0 -------- - -Major: - -- Add support for FFmpeg 5.0 and 5.1 (:issue:`817`). -- Drop support for FFmpeg < 4.3. -- Deprecate `CodecContext.time_base` for decoders (:issue:`966`). -- Deprecate `VideoStream.framerate` and `VideoStream.rate` (:issue:`1005`). -- Stop proxying `Codec` from `Stream` instances (:issue:`1037`). - -Features: - -- Update FFmpeg to 5.1.2 for the binary wheels. -- Provide binary wheels for Python 3.11 (:issue:`1019`). -- Add VideoFrame ndarray operations for gbrp formats (:issue:`986`). -- Add VideoFrame ndarray operations for gbrpf32 formats (:issue:`1028`). -- Add VideoFrame ndarray operations for nv12 format (:issue:`996`). - -Fixes: - -- Fix conversion to numpy array for multi-byte formats (:issue:`981`). -- Safely iterate over filter pads (:issue:`1000`). - -v9.2.0 ------- - -Features: - -- Update binary wheels to enable libvpx support. -- Add an `io_open` argument to `av.open` for multi-file custom I/O. -- Add support for AV_FRAME_DATA_SEI_UNREGISTERED (:issue:`723`). -- Ship .pxd files to allow other libraries to `cimport av` (:issue:`716`). - -Fixes: - -- Fix an `ImportError` when using Python 3.8/3.9 via Conda (:issue:`952`). -- Fix a muxing memory leak which was introduced in v9.1.0 (:issue:`959`). - -v9.1.1 ------- - -Fixes: - -- Update binary wheels to update dependencies on Windows, disable ALSA on Linux. - -v9.1.0 ------- - -Features: - -- Add VideoFrame ndarray operations for rgb48be, rgb48le, rgb64be, rgb64le pixel formats. -- Add VideoFrame ndarray operations for gray16be, gray16le pixel formats (:issue:`674`). -- Make it possible to use av.open() on a pipe (:issue:`738`). -- Use the "ASS without timings" format when decoding subtitles. - -Fixes: - -- Update binary wheels to fix security vulnerabilities (:issue:`921`) and enable ALSA on Linux (:issue:`941`). -- Fix crash when closing an output container an encountering an I/O error (:issue:`613`). -- Fix crash when probing corrupt raw format files (:issue:`590`). -- Fix crash when manipulating streams with an unknown codec (:issue:`689`). -- Remove obsolete KEEP_SIDE_DATA and MP4A_LATM flags which are gone in FFmpeg 5.0. -- Deprecate `to_bytes()` method of Packet, Plane and SideData, use `bytes(packet)` instead. - -v9.0.2 ------- - -Minor: - -- Update FFmpeg to 4.4.1 for the binary wheels. -- Fix framerate when writing video with FFmpeg 4.4 (:issue:`876`). - -v9.0.1 ------- - -Minor: - -- Update binary wheels to fix security vulnerabilities (:issue:`901`). - -v9.0.0 ------- - -Major: - -- Re-implement AudioResampler with aformat and buffersink (:issue:`761`). - AudioResampler.resample() now returns a list of frames. -- Remove deprecated methods: AudioFrame.to_nd_array, VideoFrame.to_nd_array and Stream.seek. - -Minor: - -- Provide binary wheels for macOS/arm64 and Linux/aarch64. -- Simplify setup.py, require Cython. -- Update the installation instructions in favor of PyPI. -- Fix VideoFrame.to_image with height & width (:issue:`878`). -- Fix setting Stream time_base (:issue:`784`). -- Replace deprecated av_init_packet with av_packet_alloc (:issue:`872`). -- Validate pixel format in VideoCodecContext.pix_fmt setter (:issue:`815`). -- Fix AudioFrame ndarray conversion endianness (:issue:`833`). -- Improve time_base support with filters (:issue:`765`). -- Allow flushing filters by sending `None` (:issue:`886`). -- Avoid unnecessary vsnprintf() calls in log_callback() (:issue:`877`). -- Make Frame.from_ndarray raise ValueError instead of AssertionError. - -v8.1.0 ------- - -Minor: - -- Update FFmpeg to 4.3.2 for the binary wheels. -- Provide binary wheels for Python 3.10 (:issue:`820`). -- Stop providing binary wheels for end-of-life Python 3.6. -- Fix args order in Frame.__repr__ (:issue:`749`). -- Fix documentation to remove unavailable QUIET log level (:issue:`719`). -- Expose codec_context.codec_tag (:issue:`741`). -- Add example for encoding with a custom PTS (:issue:`725`). -- Use av_packet_rescale_ts in Packet._rebase_time() (:issue:`737`). -- Do not hardcode errno values in test suite (:issue:`729`). -- Use av_guess_format for output container format (:issue:`691`). -- Fix setting CodecContext.extradata (:issue:`658`, :issue:`740`). -- Fix documentation code block indentation (:issue:`783`). -- Fix link to Conda installation instructions (:issue:`782`). -- Export AudioStream from av.audio (:issue:`775`). -- Fix setting CodecContext.extradata (:issue:`801`). - -v8.0.3 ------- - -Minor: - -- Update FFmpeg to 4.3.1 for the binary wheels. - -v8.0.2 ------- - -Minor: - -- Enable GnuTLS support in the FFmpeg build used for binary wheels (:issue:`675`). -- Make binary wheels compatible with Mac OS X 10.9+ (:issue:`662`). -- Drop Python 2.x buffer protocol code. -- Remove references to previous repository location. - -v8.0.1 ------- - -Minor: - -- Enable additional FFmpeg features in the binary wheels. -- Use os.fsencode for both input and output file names (:issue:`600`). - -v8.0.0 ------- - -Major: - -- Drop support for Python 2 and Python 3.4. -- Provide binary wheels for Linux, Mac and Windows. - -Minor: - -- Remove shims for obsolete FFmpeg versions (:issue:`588`). -- Add yuvj420p format for :meth:`VideoFrame.from_ndarray` and :meth:`VideoFrame.to_ndarray` (:issue:`583`). -- Add support for palette formats in :meth:`VideoFrame.from_ndarray` and :meth:`VideoFrame.to_ndarray` (:issue:`601`). -- Fix Python 3.8 deprecation warning related to abstract base classes (:issue:`616`). -- Remove ICC profiles from logos (:issue:`622`). - -Fixes: - -- Avoid infinite timeout in :func:`av.open` (:issue:`589`). - -v7.0.1 ------- - -Fixes: - -- Removed deprecated ``AV_FRAME_DATA_QP_TABLE_*`` enums. (:issue:`607`) - - -v7.0.0 ------- - -Major: - -- Drop support for FFmpeg < 4.0. (:issue:`559`) -- Introduce per-error exceptions, and mirror the builtin exception hierarchy. It is recommended to examine your error handling code, as common FFmpeg errors will result in `ValueError` baseclasses now. (:issue:`563`) -- Data stream's `encode` and `decode` return empty lists instead of none allowing common API use patterns with data streams. -- Remove ``whence`` parameter from :meth:`InputContainer.seek` as non-time seeking doesn't seem to actually be supported by any FFmpeg formats. - -Minor: - -- Users can disable the logging system to avoid lockups in sub-interpreters. (:issue:`545`) -- Filters support audio in general, and a new :meth:`.Graph.add_abuffer`. (:issue:`562`) -- :func:`av.open` supports `timeout` parameters. (:issue:`480` and :issue:`316`) -- Expose :attr:`Stream.base_rate` and :attr:`Stream.guessed_rate`. (:issue:`564`) -- :meth:`.VideoFrame.reformat` can specify interpolation. -- Expose many sets of flags. - -Fixes: - -- Fix typing in :meth:`.CodecContext.parse` and make it more robust. -- Fix wrong attribute in ByteSource. (:issue:`340`) -- Remove exception that would break audio remuxing. (:issue:`537`) -- Log messages include last FFmpeg error log in more helpful way. -- Use AVCodecParameters so FFmpeg doesn't complain. (:issue:`222`) - - -v6.2.0 ------- - -Major: - -- Allow :meth:`av.open` to be used as a context manager. -- Fix compatibility with PyPy, the full test suite now passes. (:issue:`130`) - -Minor: - -- Add :meth:`.InputContainer.close` method. (:issue:`317`, :issue:`456`) -- Ensure audio output gets flushes when using a FIFO. (:issue:`511`) -- Make Python I/O buffer size configurable. (:issue:`512`) -- Make :class:`.AudioFrame` and :class:`VideoFrame` more garbage-collector friendly by breaking a reference cycle. (:issue:`517`) - -Build: - -- Do not install the `scratchpad` package. - - -v6.1.2 ------- - -Micro: - -- Fix a numpy deprecation warning in :meth:`.AudioFrame.to_ndarray`. - - -v6.1.1 ------- - -Micro: - -- Fix alignment in :meth:`.VideoFrame.from_ndarray`. (:issue:`478`) -- Fix error message in :meth:`.Buffer.update`. - -Build: - -- Fix more compiler warnings. - - -v6.1.0 ------- - -Minor: - -- ``av.datasets`` for sample data that is pulled from either FFmpeg's FATE suite, or our documentation server. -- :meth:`.InputContainer.seek` gets a ``stream`` argument to specify the ``time_base`` the requested ``offset`` is in. - -Micro: - -- Avoid infinite look in ``Stream.__getattr__``. (:issue:`450`) -- Correctly handle Python I/O with no ``seek`` method. -- Remove ``Datastream.seek`` override (:issue:`299`) - -Build: - -- Assert building against compatible FFmpeg. (:issue:`401`) -- Lock down Cython language level to avoid build warnings. (:issue:`443`) - -Other: - -- Incremental improvements to docs and tests. -- Examples directory will now always be runnable as-is, and embedded in the docs (in a copy-pastable form). - - -v6.0.0 ------- - -Major: - -- Drop support for FFmpeg < 3.2. -- Remove ``VideoFrame.to_qimage`` method, as it is too tied to PyQt4. (:issue:`424`) - -Minor: - -- Add support for all known sample formats in :meth:`.AudioFrame.to_ndarray` and add :meth:`.AudioFrame.to_ndarray`. (:issue:`422`) -- Add support for more image formats in :meth:`.VideoFrame.to_ndarray` and :meth:`.VideoFrame.from_ndarray`. (:issue:`415`) - -Micro: - -- Fix a memory leak in :meth:`.OutputContainer.mux_one`. (:issue:`431`) -- Ensure :meth:`.OutputContainer.close` is called at destruction. (:issue:`427`) -- Fix a memory leak in :class:`.OutputContainer` initialisation. (:issue:`427`) -- Make all video frames created by PyAV use 8-byte alignment. (:issue:`425`) -- Behave properly in :meth:`.VideoFrame.to_image` and :meth:`.VideoFrame.from_image` when ``width != line_width``. (:issue:`425`) -- Fix manipulations on video frames whose width does not match the line stride. (:issue:`423`) -- Fix several :attr:`.Plane.line_size` misunderstandings. (:issue:`421`) -- Consistently decode dictionary contents. (:issue:`414`) -- Always use send/recv en/decoding mechanism. This removes the ``count`` parameter, which was not used in the send/recv pipeline. (:issue:`413`) -- Remove various deprecated iterators. (:issue:`412`) -- Fix a memory leak when using Python I/O. (:issue:`317`) -- Make :meth:`.OutputContainer.mux_one` call `av_interleaved_write_frame` with the GIL released. - -Build: - -- Remove the "reflection" mechanism, and rely on FFmpeg version we build against to decide which methods to call. (:issue:`416`) -- Fix many more ``const`` warnings. - - -v0.x.y ------- - -.. note:: - - Below here we used ``v0.x.y``. - - We incremented ``x`` to signal a major change (i.e. backwards - incompatibilities) and incremented ``y`` as a minor change (i.e. backwards - compatible features). - - Once we wanted more subtlety and felt we had matured enough, we jumped - past the implications of ``v1.0.0`` straight to ``v6.0.0`` - (as if we had not been stuck in ``v0.x.y`` all along). - - -v0.5.3 ------- - -Minor: - -- Expose :attr:`.VideoFrame.pict_type` as :class:`.PictureType` enum. - (:pr:`402`) -- Expose :attr:`.Codec.video_rates` and :attr:`.Codec.audio_rates`. - (:pr:`381`) - -Patch: - -- Fix :attr:`.Packet.time_base` handling during flush. - (:pr:`398`) -- :meth:`.VideoFrame.reformat` can throw exceptions when requested colorspace - transforms aren't possible. -- Wrapping the stream object used to overwrite the ``pix_fmt`` attribute. - (:pr:`390`) - -Runtime: - -- Deprecate ``VideoFrame.ptr`` in favour of :attr:`VideoFrame.buffer_ptr`. -- Deprecate ``Plane.update_buffer()`` and ``Packet.update_buffer`` in favour of - :meth:`.Plane.update`. - (:pr:`407`) -- Deprecate ``Plane.update_from_string()`` in favour of :meth:`.Plane.update`. - (:pr:`407`) -- Deprecate ``AudioFrame.to_nd_array()`` and ``VideoFrame.to_nd_array()`` in - favour of :meth:`.AudioFrame.to_ndarray` and :meth:`.VideoFrame.to_ndarray`. - (:pr:`404`) - -Build: - -- CI covers more cases, including macOS. - (:pr:`373` and :pr:`399`) -- Fix many compilation warnings. - (:issue:`379`, :pr:`380`, :pr:`387`, and :pr:`388`) - -Docs: - -- Docstrings for many commonly used attributes. - (:pr:`372` and :pr:`409`) - - -v0.5.2 ------- - -Build: - -- Fixed Windows build, which broke in v0.5.1. -- Compiler checks are not cached by default. This behaviour is retained if you - ``source scripts/activate.sh`` to develop PyAV. - (:issue:`256`) -- Changed to ``PYAV_SETUP_REFLECT_DEBUG=1`` from ``PYAV_DEBUG_BUILD=1``. - - -v0.5.1 ------- - -Build: - -- Set ``PYAV_DEBUG_BUILD=1`` to force a verbose reflection (mainly for being - installed via ``pip``, which is why this is worth a release). - - -v0.5.0 ------- - -Major: - -- Dropped support for Libav in general. - (:issue:`110`) -- No longer uses libavresample. - -Minor: - -- ``av.open`` has ``container_options`` and ``stream_options``. -- ``Frame`` includes ``pts`` in ``repr``. - -Patch: - -- EnumItem's hash calculation no longer overflows. - (:issue:`339`, :issue:`341` and :issue:`342`.) -- Frame.time_base was not being set in most cases during decoding. - (:issue:`364`) -- CodecContext.options no longer needs to be manually initialized. -- CodexContext.thread_type accepts its enums. - - -v0.4.1 ------- - -Minor: - -- Add `Frame.interlaced_frame` to indicate if the frame is interlaced. - (:issue:`327` by :gh-user:`MPGek`) -- Add FLTP support to ``Frame.to_nd_array()``. - (:issue:`288` by :gh-user:`rawler`) -- Expose ``CodecContext.extradata`` for codecs that have extra data, e.g. - Huffman tables. - (:issue:`287` by :gh-user:`adavoudi`) - -Patch: - -- Packets retain their refcount after muxing. - (:issue:`334`) -- `Codec` construction is more robust to find more codecs. - (:issue:`332` by :gh-user:`adavoudi`) -- Refined frame corruption detection. - (:issue:`291` by :gh-user:`Litterfeldt`) -- Unicode filenames are okay. - (:issue:`82`) - - -v0.4.0 ------- - -Major: - -- ``CodecContext`` has taken over encoding/decoding, and can work in isolation - of streams/containers. -- ``Stream.encode`` returns a list of packets, instead of a single packet. -- ``AudioFifo`` and ``AudioResampler`` will raise ``ValueError`` if input frames - inconsistent ``pts``. -- ``time_base`` use has been revisited across the codebase, and may not be converted - between ``Stream.time_base`` and ``CodecContext.time_base`` at the same times - in the transcoding pipeline. -- ``CodecContext.rate`` has been removed, but proxied to ``VideoCodecContext.framerate`` - and ``AudioCodecContext.sample_rate``. The definition is effectively inverted from - the old one (i.e. for 24fps it used to be ``1/24`` and is now ``24/1``). -- Fractions (e.g. ``time_base``, ``rate``) will be ``None`` if they are invalid. -- ``InputContainer.seek`` and ``Stream.seek`` will raise TypeError if given - a float, when previously they converted it from seconds. - -Minor: - -- Added ``Packet.is_keyframe`` and ``Packet.is_corrupt``. - (:issue:`226`) -- Many more ``time_base``, ``pts`` and other attributes are writable. -- ``Option`` exposes much more of the API (but not get/set). - (:issue:`243`) -- Expose metadata encoding controls. - (:issue:`250`) -- Expose ``CodecContext.skip_frame``. - (:issue:`259`) - -Patch: - -- Build doesn't fail if you don't have git installed. - (:issue:`184`) -- Developer environment works better with Python3. - (:issue:`248`) -- Fix Container deallocation resulting in segfaults. - (:issue:`253`) - - -v0.3.3 ------- - -Patch: - -- Fix segfault due to buffer overflow in handling of stream options. - (:issue:`163` and :issue:`169`) -- Fix segfault due to seek not properly checking if codecs were open before - using avcodec_flush_buffers. - (:issue:`201`) - - -v0.3.2 ------- - -Minor: - -- Expose basics of avfilter via ``Filter``. -- Add ``Packet.time_base``. -- Add ``AudioFrame.to_nd_array`` to match same on ``VideoFrame``. -- Update Windows build process. - -Patch: - -- Further improvements to the logging system. - (:issue:`128`) - - -v0.3.1 ------- - -Minor: - -- ``av.logging.set_log_after_shutdown`` renamed to ``set_print_after_shutdown`` -- Repeating log messages will be skipped, much like ffmpeg's does by default - -Patch: - -- Fix memory leak in logging system when under heavy logging loads while - threading. - (:issue:`128` with help from :gh-user:`mkassner` and :gh-user:`ksze`) - - -v0.3.0 ------- - -Major: - -- Python IO can write -- Improve build system to use Python's C compiler for function detection; - build system is much more robust -- MSVC support. - (:issue:`115` by :gh-user:`vidartf`) -- Continuous integration on Windows via AppVeyor. (by :gh-user:`vidartf`) - -Minor: - -- Add ``Packet.decode_one()`` to skip packet flushing for codecs that would - otherwise error -- ``StreamContainer`` for easier selection of streams -- Add buffer protocol support to Packet - -Patch: - -- Fix bug when using Python IO on files larger than 2GB. - (:issue:`109` by :gh-user:`xxr3376`) -- Fix usage of changed Pillow API - -Known Issues: - -- VideoFrame is suspected to leak memory in narrow cases on Linux. - (:issue:`128`) - - -v0.2.4 ------- - -- fix library search path for current Libav/Ubuntu 14.04. - (:issue:`97`) -- explicitly include all sources to combat 0.2.3 release problem. - (:issue:`100`) - - -v0.2.3 ------- - -.. warning:: There was an issue with the PyPI distribution in which it required - Cython to be installed. - -Major: - -- Python IO. -- Aggressively releases GIL -- Add experimental Windows build. - (:issue:`84`) - -Minor: - -- Several new Stream/Packet/Frame attributes - -Patch: - -- Fix segfault in audio handling. - (:issue:`86` and :issue:`93`) -- Fix use of PIL/Pillow API. - (:issue:`85`) -- Fix bad assumptions about plane counts. - (:issue:`76`) - - -v0.2.2 ------- - -- Cythonization in setup.py; mostly a development issue. -- Fix for av.InputContainer.size over 2**31. - - -v0.2.1 ------- - -- Python 3 compatibility! -- Build process fails if missing libraries. -- Fix linking of libavdevices. - - -v0.2.0 ------- - -.. warning:: This version has an issue linking in libavdevices, and very likely - will not work for you. - -It sure has been a long time since this was released, and there was a lot of -arbitrary changes that come with us wrapping an API as we are discovering it. -Changes include, but are not limited to: - -- Audio encoding. -- Exposing planes and buffers. -- Descriptors for channel layouts, video and audio formats, etc.. -- Seeking. -- Many many more properties on all of the objects. -- Device support (e.g. webcams). - - -v0.1.0 ------- +15.X and Below +-------------- +.. + or see older git commits -- FIRST PUBLIC RELEASE! -- Container/video/audio formats. -- Audio layouts. -- Decoding video/audio/subtitles. -- Encoding video. -- Audio FIFOs and resampling. +`15.X Changelog ` From 2b4088fc9089289e3f9fdaec7740dd7b8abe8532 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 16 Jan 2026 01:10:11 -0500 Subject: [PATCH 363/365] Update the copyright year --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index d971572e1..02c0d9f4b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,7 +27,7 @@ source_suffix = ".rst" master_doc = "index" project = "PyAV" -copyright = "2025, The PyAV Team" +copyright = "2026, The PyAV Team" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the From 04fe98051a03418644d413d5b37979ad53046326 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 16 Jan 2026 01:37:51 -0500 Subject: [PATCH 364/365] Build cp314t and cp310 along with limited wheels --- .github/workflows/smoke.yml | 2 ++ .github/workflows/tests.yml | 2 +- pyproject.toml | 3 ++- setup.py | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index fa78a285c..218e36241 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -7,12 +7,14 @@ on: - '**.md' - '**.rst' - '**.txt' + - '.github/workflows/tests.yml' pull_request: branches: main paths-ignore: - '**.md' - '**.rst' - '**.txt' + - '.github/workflows/tests.yml' jobs: style: runs-on: ubuntu-slim diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ca29cb4b3..e151e3c56 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -62,7 +62,7 @@ jobs: CIBW_ENVIRONMENT_MACOS: PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig LDFLAGS=-headerpad_max_install_names CIBW_ENVIRONMENT_WINDOWS: INCLUDE=C:\\cibw\\vendor\\include LIB=C:\\cibw\\vendor\\lib PYAV_SKIP_TESTS=unicode_filename CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: delvewheel repair --add-path C:\cibw\vendor\bin -w {dest_dir} {wheel} - CIBW_SKIP: "pp* cp312* cp313* cp314*" + CIBW_BUILD: "cp310* cp311* cp314t*" CIBW_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m pytest {package}/tests && mv {project}/av.disabled {project}/av CIBW_TEST_REQUIRES: pytest numpy run: | diff --git a/pyproject.toml b/pyproject.toml index 7ab4ae2ae..24bdc90a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ authors = [ {name = "WyattBlue", email = "wyattblue@auto-editor.com"}, {name = "Jeremy Lainé", email = "jeremy.laine@m4x.org"}, ] -requires-python = ">=3.11" +requires-python = ">=3.10" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", @@ -20,6 +20,7 @@ classifiers = [ "Operating System :: Unix", "Operating System :: Microsoft :: Windows", "Programming Language :: Cython", + "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", diff --git a/setup.py b/setup.py index 9be9acb27..0e00f9873 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ "swresample", ] -if sys.implementation.name == "cpython": +if sys.implementation.name == "cpython" and (3, 14) > sys.version_info > (3, 11): py_limited_api = True options = {"bdist_wheel": {"py_limited_api": "cp311"}} define_macros = [("Py_LIMITED_API", 0x030B0000)] From 27d2e2300045284811939ec94455c6be8d9e5aee Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Fri, 16 Jan 2026 02:31:37 -0500 Subject: [PATCH 365/365] Build windows aarch64 --- .github/workflows/tests.yml | 3 +++ scripts/activate.sh | 2 +- scripts/ffmpeg-8.0.json | 2 +- scripts/ffmpeg-latest.json | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e151e3c56..4b4e1f45d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -39,6 +39,8 @@ jobs: arch: x86_64 - os: windows-latest arch: AMD64 + - os: windows-11-arm + arch: ARM64 steps: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 @@ -63,6 +65,7 @@ jobs: CIBW_ENVIRONMENT_WINDOWS: INCLUDE=C:\\cibw\\vendor\\include LIB=C:\\cibw\\vendor\\lib PYAV_SKIP_TESTS=unicode_filename CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: delvewheel repair --add-path C:\cibw\vendor\bin -w {dest_dir} {wheel} CIBW_BUILD: "cp310* cp311* cp314t*" + CIBW_SKIP: "cp310-win_arm64" CIBW_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m pytest {package}/tests && mv {project}/av.disabled {project}/av CIBW_TEST_REQUIRES: pytest numpy run: | diff --git a/scripts/activate.sh b/scripts/activate.sh index 95f60a33d..a58a64b94 100755 --- a/scripts/activate.sh +++ b/scripts/activate.sh @@ -21,7 +21,7 @@ if [[ ! "$PYAV_LIBRARY" ]]; then return 1 fi else - PYAV_LIBRARY=ffmpeg-8.0 + PYAV_LIBRARY=ffmpeg-8.0.1 echo "No \$PYAV_LIBRARY set; defaulting to $PYAV_LIBRARY" fi fi diff --git a/scripts/ffmpeg-8.0.json b/scripts/ffmpeg-8.0.json index 5ec867122..8e56abfd1 100644 --- a/scripts/ffmpeg-8.0.json +++ b/scripts/ffmpeg-8.0.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0.1-3/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0.1-4/ffmpeg-{platform}.tar.gz" } diff --git a/scripts/ffmpeg-latest.json b/scripts/ffmpeg-latest.json index 5ec867122..8e56abfd1 100644 --- a/scripts/ffmpeg-latest.json +++ b/scripts/ffmpeg-latest.json @@ -1,3 +1,3 @@ { - "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0.1-3/ffmpeg-{platform}.tar.gz" + "url": "https://github.com/PyAV-Org/pyav-ffmpeg/releases/download/8.0.1-4/ffmpeg-{platform}.tar.gz" }