From 0fc3b6224f3365bc5168026be1fb04f1658c53a2 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 26 Sep 2026 20:42:05 -0400 Subject: [PATCH 1/2] Pass flags through HWAccel.create() create() builds the context it returns from a new HWAccel, and copied every argument but flags, so av_hwdevice_ctx_create() always got 0. --- CHANGELOG.rst | 1 + av/codec/hwaccel.py | 1 + tests/test_decode.py | 19 +++++++++++++++++++ 3 files changed, 21 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 00e83571d..03097b74f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -67,6 +67,7 @@ Fixes: - ``VideoFrame.reformat()`` (and so ``to_ndarray(format=...)``, ``to_rgb()``, ``to_image()``) now shares one ``SwsContext`` per thread instead of allocating one per frame. FFmpeg 8's swscale retains megabytes of graph state per context, which showed up as large RSS growth when many frames were alive at once. - Writing to a network URL no longer blocks every other Python thread, and ``timeout`` now applies to opening an output container. ``avio_open()``, ``avformat_write_header()``, ``av_write_trailer()``, and ``avio_closep()`` held the GIL, so an unreachable RTMP server froze the whole process, and the interrupt callback was only installed for demuxing, so nothing could end the wait. :meth:`.OutputContainer.close` now raises rather than freeing a context another thread is still muxing or closing. By :gh-user:`adrianrfreedman` in (:pr:`2412`). - ``timeout`` now applies to muxing and closing an output container, not just to opening it. Only opening armed the interrupt callback, so a peer that accepted the connection and then stopped reading left ``av_interleaved_write_frame()`` and ``av_write_trailer()`` blocked forever. Each mux gets the full timeout, and a close shares one across writing the trailer and flushing, so neither can outlast it. Only unseekable outputs are covered: writing a seekable file takes as long as the file is big, so a deadline meant for a peer would abandon it part-written. By :gh-user:`adrianrfreedman` in (:pr:`2414`). +- :meth:`.HWAccel.create` now passes ``flags`` on to the context it creates. They were dropped, so ``av_hwdevice_ctx_create()`` always got ``0`` and CUDA's ``AV_CUDA_USE_PRIMARY_CONTEXT`` could not be requested through them. 18.X and Below diff --git a/av/codec/hwaccel.py b/av/codec/hwaccel.py index 05c26cfc7..44e78c0f1 100644 --- a/av/codec/hwaccel.py +++ b/av/codec/hwaccel.py @@ -275,6 +275,7 @@ def create(self, codec: Codec, for_encoding: bool = False) -> HWAccel: device=self._device, allow_software_fallback=self.allow_software_fallback, options=self.options, + flags=self.flags, is_hw_owned=self.is_hw_owned, ) ret._initialize_hw_context(codec, for_encoding=for_encoding) diff --git a/tests/test_decode.py b/tests/test_decode.py index 8fbaa3ccc..603a63e73 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -376,6 +376,25 @@ def test_hardware_decode(self) -> None: assert frame_count == video_stream.frames + def test_hardware_create_keeps_flags(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" + ) + + # AV_CUDA_USE_PRIMARY_CONTEXT. It is the only flag FFmpeg defines for + # device creation, and every other device type ignores it. + hwaccel = av.codec.hwaccel.HWAccel(device_type=HWACCEL_DEVICE_TYPE, flags=1) + created = hwaccel.create(av.Codec("h264", "r")) + assert created.flags == 1 + @pytest.mark.parametrize("is_hw_owned", [False, True]) def test_hardware_decode_download_preserves_frame_props(is_hw_owned: bool) -> None: From 0336a752750fc8f2ed955f33c35a1a42bd0ad459 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 26 Sep 2026 20:39:22 -0400 Subject: [PATCH 2/2] Match more type stubs to the runtime VideoStream.__getattr__ raises AttributeError for framerate and rate on purpose, so the stub let code type-check that failed at runtime. Drop them, along with Buffer.__bytes__ and InputContainer.flush_buffers(), which do not exist from Python either. HWAccel defaults allow_software_fallback to True, not False, and accepts an int device type. ArgumentError is also a ValueError. Add HWAccel.config, FilterContext.filter, Subtitle.type, and VideoCodecContext.reformatter, which the runtime has and the stubs did not. --- av/buffer.pyi | 1 - av/codec/hwaccel.pyi | 5 +++-- av/container/input.pyi | 1 - av/error.pyi | 2 +- av/filter/context.pyi | 2 ++ av/subtitles/subtitle.pyi | 3 ++- av/video/codeccontext.pyi | 2 ++ av/video/stream.pyi | 9 --------- 8 files changed, 10 insertions(+), 15 deletions(-) diff --git a/av/buffer.pyi b/av/buffer.pyi index bc1090d1d..24f287e85 100644 --- a/av/buffer.pyi +++ b/av/buffer.pyi @@ -6,4 +6,3 @@ class Buffer: buffer_ptr: int def update(self, input: bytes) -> None: ... def __buffer__(self, flags: int) -> memoryview: ... - def __bytes__(self) -> bytes: ... diff --git a/av/codec/hwaccel.pyi b/av/codec/hwaccel.pyi index ece8d4f41..28a9d4292 100644 --- a/av/codec/hwaccel.pyi +++ b/av/codec/hwaccel.pyi @@ -56,6 +56,7 @@ class HWAccel: options: dict[str, object] flags: int allow_software_fallback: bool + config: HWConfig | None @property def is_hw_owned(self) -> bool: ... @@ -63,9 +64,9 @@ class HWAccel: def device_id(self) -> int: ... def __init__( self, - device_type: str | HWDeviceType, + device_type: str | int | HWDeviceType, device: str | int | None = None, - allow_software_fallback: bool = False, + allow_software_fallback: bool = True, options: dict[str, object] | None = None, flags: int | None = None, is_hw_owned: bool = False, diff --git a/av/container/input.pyi b/av/container/input.pyi index 2ec10193c..819defd1b 100644 --- a/av/container/input.pyi +++ b/av/container/input.pyi @@ -87,4 +87,3 @@ class InputContainer(Container): unsupported_frame_offset: bool = False, unsupported_byte_offset: bool = False, ) -> None: ... - def flush_buffers(self) -> None: ... diff --git a/av/error.pyi b/av/error.pyi index 811cc35cc..f7fb76099 100644 --- a/av/error.pyi +++ b/av/error.pyi @@ -70,4 +70,4 @@ class OSError(FFmpegError, builtins.OSError): ... class PermissionError(FFmpegError, builtins.PermissionError): ... class ProcessLookupError(FFmpegError, builtins.ProcessLookupError): ... class TimeoutError(FFmpegError, builtins.TimeoutError): ... -class ArgumentError(FFmpegError): ... +class ArgumentError(FFmpegError, builtins.ValueError): ... diff --git a/av/filter/context.pyi b/av/filter/context.pyi index 8c8462f95..4c6856678 100644 --- a/av/filter/context.pyi +++ b/av/filter/context.pyi @@ -1,9 +1,11 @@ from av.filter import Graph +from av.filter.filter import Filter from av.filter.link import FilterContextPad from av.frame import Frame class FilterContext: name: str | None + filter: Filter @property def inputs(self) -> tuple[FilterContextPad, ...]: ... diff --git a/av/subtitles/subtitle.pyi b/av/subtitles/subtitle.pyi index 5f277eba0..3208aa5d9 100644 --- a/av/subtitles/subtitle.pyi +++ b/av/subtitles/subtitle.pyi @@ -20,7 +20,8 @@ class SubtitleSet: def __iter__(self) -> Iterator[Subtitle]: ... def __getitem__(self, i: int) -> Subtitle: ... -class Subtitle: ... +class Subtitle: + type: bytes class BitmapSubtitle(Subtitle): type: Literal[b"bitmap"] diff --git a/av/video/codeccontext.pyi b/av/video/codeccontext.pyi index 27d902c75..259c4e594 100644 --- a/av/video/codeccontext.pyi +++ b/av/video/codeccontext.pyi @@ -8,6 +8,7 @@ from av.rational import AVRational from .format import VideoFormat from .frame import VideoFrame +from .reformatter import VideoReformatter class VideoCodecContext(CodecContext): format: VideoFormat | None @@ -15,6 +16,7 @@ class VideoCodecContext(CodecContext): height: int bits_per_coded_sample: int pix_fmt: str | None + reformatter: VideoReformatter | None @property def sw_format(self) -> VideoFormat | None: ... @sw_format.setter diff --git a/av/video/stream.pyi b/av/video/stream.pyi index 1b4517b7f..9be73b477 100644 --- a/av/video/stream.pyi +++ b/av/video/stream.pyi @@ -1,5 +1,4 @@ from collections.abc import Iterator, Sequence -from fractions import Fraction from typing import Literal from av.codec.context import ThreadType @@ -35,14 +34,6 @@ class VideoStream(Stream): height: int bits_per_coded_sample: int pix_fmt: str | None - @property - def framerate(self) -> AVRational: ... - @framerate.setter - def framerate(self, value: AVRational | Fraction | int) -> None: ... - @property - def rate(self) -> AVRational: ... - @rate.setter - def rate(self, value: AVRational | Fraction | int) -> None: ... gop_size: int has_b_frames: bool max_b_frames: int