Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions av/codec/codec.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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], ...)
Expand Down Expand Up @@ -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: ...
Expand Down
11 changes: 9 additions & 2 deletions av/codec/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion av/codec/hwaccel.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions av/video/codeccontext.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
5 changes: 4 additions & 1 deletion av/video/codeccontext.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
49 changes: 49 additions & 0 deletions tests/test_encode.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this used to return None (hw_frames_ctx was NULL), now we allow getting (and setting) the value


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"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this used to return "nv12" (and would make the encoder use nv12 as the pixel format, with no way to control the value), now the sw_format property is respected

for packet in stream.encode():
container.mux(packet)
container.close()
Loading