From 72650ff4df624b428acfc18f2786c499c69a524b Mon Sep 17 00:00:00 2001 From: Carlos Ruiz Date: Mon, 29 Jun 2026 11:47:11 -0700 Subject: [PATCH] Allow configuring hardware encoder sw_format --- av/codec/codec.pyi | 2 ++ av/codec/context.py | 11 +++++++-- av/codec/hwaccel.pyi | 2 +- av/video/codeccontext.py | 10 ++++++++ av/video/codeccontext.pyi | 5 +++- tests/test_encode.py | 49 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 75 insertions(+), 4 deletions(-) diff --git a/av/codec/codec.pyi b/av/codec/codec.pyi index a9876c4f5..8631790b2 100644 --- a/av/codec/codec.pyi +++ b/av/codec/codec.pyi @@ -10,6 +10,7 @@ from av.video.codeccontext import VideoCodecContext from av.video.format import VideoFormat from .context import CodecContext +from .hwaccel import HWConfig class Properties(Flag): NONE = cast(ClassVar[Properties], ...) @@ -78,6 +79,7 @@ class Codec: audio_rates: list[int] | None video_formats: list[VideoFormat] | None audio_formats: list[AudioFormat] | None + hardware_configs: list[HWConfig] @property def properties(self) -> int: ... diff --git a/av/codec/context.py b/av/codec/context.py index 27ab8bbef..6fefb1857 100644 --- a/av/codec/context.py +++ b/av/codec/context.py @@ -396,9 +396,16 @@ def _setup_encode_hwframes(self) -> cython.void: return # Already set up. hw_format: lib.AVPixelFormat = self.hwaccel_ctx.config.ptr.pix_fmt - sw_format: lib.AVPixelFormat = cython.cast(lib.AVPixelFormat, self.ptr.pix_fmt) + sw_format: lib.AVPixelFormat = cython.cast( + lib.AVPixelFormat, self.ptr.sw_pix_fmt + ) + + # The codec context's sw_pix_fmt holds the software format the user + # wants the hardware frames context to use. Fall back to pix_fmt to + # preserve the existing stream.pix_fmt configuration path. + if sw_format == lib.AV_PIX_FMT_NONE: + sw_format = cython.cast(lib.AVPixelFormat, self.ptr.pix_fmt) - # The codec context's pix_fmt holds the *software* format the user feeds in. # If they left it as the hardware format (or unset), pick a sane default. if sw_format == hw_format or sw_format == lib.AV_PIX_FMT_NONE: sw_format = lib.av_get_pix_fmt(b"nv12") diff --git a/av/codec/hwaccel.pyi b/av/codec/hwaccel.pyi index a9135f683..b3b0c3ff0 100644 --- a/av/codec/hwaccel.pyi +++ b/av/codec/hwaccel.pyi @@ -30,7 +30,7 @@ class HWConfig: @property def device_type(self) -> HWDeviceType: ... @property - def format(self) -> VideoFormat: ... + def format(self) -> VideoFormat | None: ... @property def methods(self) -> HWConfigMethod: ... @property diff --git a/av/video/codeccontext.py b/av/video/codeccontext.py index ef39c0870..f213b9860 100644 --- a/av/video/codeccontext.py +++ b/av/video/codeccontext.py @@ -261,12 +261,22 @@ def sw_format(self): :type: VideoFormat | None """ if not self.ptr.hw_frames_ctx: + if self.ptr.sw_pix_fmt != lib.AV_PIX_FMT_NONE: + return get_video_format( + cython.cast(lib.AVPixelFormat, self.ptr.sw_pix_fmt), + self.ptr.width, + self.ptr.height, + ) return None frames_ctx: cython.pointer[lib.AVHWFramesContext] = cython.cast( cython.pointer[lib.AVHWFramesContext], self.ptr.hw_frames_ctx.data ) return get_video_format(frames_ctx.sw_format, self.ptr.width, self.ptr.height) + @sw_format.setter + def sw_format(self, value): + self.ptr.sw_pix_fmt = get_pix_fmt(value) + @property def framerate(self): """ diff --git a/av/video/codeccontext.pyi b/av/video/codeccontext.pyi index 6b5e27d59..603dc4dcb 100644 --- a/av/video/codeccontext.pyi +++ b/av/video/codeccontext.pyi @@ -14,7 +14,10 @@ class VideoCodecContext(CodecContext): height: int bits_per_coded_sample: int pix_fmt: str | None - sw_format: VideoFormat | None + @property + def sw_format(self) -> VideoFormat | None: ... + @sw_format.setter + def sw_format(self, value: str) -> None: ... framerate: Fraction rate: Fraction gop_size: int diff --git a/tests/test_encode.py b/tests/test_encode.py index bf432a495..294cc815b 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -517,6 +517,13 @@ def test_profiles(self) -> None: } +def get_hwaccel_format(encoder: str, device_type: str) -> str: + for config in av.Codec(encoder, "w").hardware_configs: + if config.device_type.name == device_type and config.format is not None: + return config.format.name + pytest.skip(f"No hardware format for {device_type} on {encoder}") + + def test_hardware_encode() -> None: hwdevices_available = av.codec.hwaccel.hwdevices_available() if "HWACCEL_DEVICE_TYPE" not in os.environ: @@ -569,3 +576,45 @@ def test_hardware_encode() -> None: with av.open(file, "r") as in_container: decoded = sum(1 for _ in in_container.decode(video=0)) assert decoded == n_frames + + +def test_hardware_encode_honors_sw_format() -> None: + hwdevices_available = av.codec.hwaccel.hwdevices_available() + if "HWACCEL_DEVICE_TYPE" not in os.environ: + pytest.skip( + "Set the HWACCEL_DEVICE_TYPE to run this test. " + f"Options are {' '.join(hwdevices_available)}" + ) + + device_type = os.environ["HWACCEL_DEVICE_TYPE"] + assert device_type in hwdevices_available, f"{device_type} not available" + + encoder = _HWACCEL_ENCODERS.get(device_type) + if encoder is None: + pytest.skip(f"No hardware encoder mapped for {device_type}") + hw_format = get_hwaccel_format(encoder, device_type) + + hwaccel = av.codec.hwaccel.HWAccel( + device_type=device_type, allow_software_fallback=False + ) + container = av.open(io.BytesIO(), mode="w", format="mp4") + stream = container.add_stream(encoder, rate=30, hwaccel=hwaccel) + assert isinstance(stream, VideoStream) + stream.width = 320 + stream.height = 240 + stream.pix_fmt = hw_format + stream.codec_context.sw_format = "yuv420p" + + assert stream.codec_context.sw_format is not None + assert stream.codec_context.sw_format.name == "yuv420p" + + frame = VideoFrame(320, 240, "rgb24") + for packet in stream.encode(frame): + container.mux(packet) + + assert stream.codec_context.pix_fmt == hw_format + assert stream.codec_context.sw_format is not None + assert stream.codec_context.sw_format.name == "yuv420p" + for packet in stream.encode(): + container.mux(packet) + container.close()