Skip to content

Commit 3e5b4a1

Browse files
authored
Add binding for "avcodec_find_best_pix_fmt_of_list" (#2058)
1 parent f10e3ae commit 3e5b4a1

File tree

5 files changed

+161
-3
lines changed

5 files changed

+161
-3
lines changed

av/codec/__init__.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
from .codec import Capabilities, Codec, Properties, codec_descriptor, codecs_available
1+
from .codec import (
2+
Capabilities,
3+
Codec,
4+
Properties,
5+
codec_descriptor,
6+
codecs_available,
7+
find_best_pix_fmt_of_list,
8+
)
29
from .context import CodecContext
310

411
__all__ = (
@@ -7,5 +14,6 @@
714
"Properties",
815
"codec_descriptor",
916
"codecs_available",
17+
"find_best_pix_fmt_of_list",
1018
"CodecContext",
1119
)

av/codec/codec.pyi

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from enum import Flag, IntEnum
22
from fractions import Fraction
3-
from typing import ClassVar, Literal, cast, overload
3+
from typing import ClassVar, Literal, Sequence, cast, overload
44

55
from av.audio.codeccontext import AudioCodecContext
66
from av.audio.format import AudioFormat
@@ -113,3 +113,27 @@ codecs_available: set[str]
113113

114114
def dump_codecs() -> None: ...
115115
def dump_hwconfigs() -> None: ...
116+
117+
PixFmtLike = str | VideoFormat
118+
119+
def find_best_pix_fmt_of_list(
120+
pix_fmts: Sequence[PixFmtLike],
121+
src_pix_fmt: PixFmtLike,
122+
has_alpha: bool = False,
123+
) -> tuple[VideoFormat | None, int]:
124+
"""
125+
Find the best pixel format to convert to given a source format.
126+
127+
Wraps :ffmpeg:`avcodec_find_best_pix_fmt_of_list`.
128+
129+
:param pix_fmts: Iterable of pixel formats to choose from (str or VideoFormat).
130+
:param src_pix_fmt: Source pixel format (str or VideoFormat).
131+
:param bool has_alpha: Whether the source alpha channel is used.
132+
:return: (best_format, loss): best_format is the best matching pixel format from
133+
the list, or None if no suitable format was found; loss is Combination of flags informing you what kind of losses will occur.
134+
:rtype: (VideoFormat | None, int)
135+
136+
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.
137+
For exact behavior see: libavutil/pixdesc.c/get_pix_fmt_score() in ffmpeg source code.
138+
"""
139+
...

av/codec/codec.pyx

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ from av.audio.format cimport get_audio_format
44
from av.codec.hwaccel cimport wrap_hwconfig
55
from av.descriptor cimport wrap_avclass
66
from av.utils cimport avrational_to_fraction
7-
from av.video.format cimport get_video_format
7+
from av.video.format cimport VideoFormat, get_pix_fmt, get_video_format
88

99
from enum import Flag, IntEnum
10+
from libc.stdlib cimport free, malloc
1011

1112

1213
cdef object _cinit_sentinel = object()
@@ -387,3 +388,59 @@ def dump_hwconfigs():
387388
print(" ", codec.name)
388389
for config in configs:
389390
print(" ", config)
391+
392+
393+
def find_best_pix_fmt_of_list(pix_fmts, src_pix_fmt, has_alpha=False):
394+
"""
395+
Find the best pixel format to convert to given a source format.
396+
397+
Wraps :ffmpeg:`avcodec_find_best_pix_fmt_of_list`.
398+
399+
:param pix_fmts: Iterable of pixel formats to choose from (str or VideoFormat).
400+
:param src_pix_fmt: Source pixel format (str or VideoFormat).
401+
:param bool has_alpha: Whether the source alpha channel is used.
402+
:return: (best_format, loss)
403+
:rtype: (VideoFormat | None, int)
404+
"""
405+
cdef lib.AVPixelFormat src
406+
cdef lib.AVPixelFormat best
407+
cdef lib.AVPixelFormat *c_list = NULL
408+
cdef Py_ssize_t n
409+
cdef Py_ssize_t i
410+
cdef object item
411+
cdef int c_loss
412+
413+
if pix_fmts is None:
414+
raise TypeError("pix_fmts must not be None")
415+
416+
pix_fmts = tuple(pix_fmts)
417+
if not pix_fmts:
418+
return None, 0
419+
420+
if isinstance(src_pix_fmt, VideoFormat):
421+
src = (<VideoFormat>src_pix_fmt).pix_fmt
422+
else:
423+
src = get_pix_fmt(<str>src_pix_fmt)
424+
425+
n = len(pix_fmts)
426+
c_list = <lib.AVPixelFormat *>malloc((n + 1) * sizeof(lib.AVPixelFormat))
427+
if c_list == NULL:
428+
raise MemoryError()
429+
430+
try:
431+
for i in range(n):
432+
item = pix_fmts[i]
433+
if isinstance(item, VideoFormat):
434+
c_list[i] = (<VideoFormat>item).pix_fmt
435+
else:
436+
c_list[i] = get_pix_fmt(<str>item)
437+
c_list[n] = lib.AV_PIX_FMT_NONE
438+
439+
c_loss = 0
440+
best = lib.avcodec_find_best_pix_fmt_of_list(
441+
c_list, src, 1 if has_alpha else 0, &c_loss
442+
)
443+
return get_video_format(best, 0, 0), c_loss
444+
finally:
445+
if c_list != NULL:
446+
free(c_list)

include/libavcodec/avcodec.pxd

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,13 @@ cdef extern from "libavcodec/avcodec.h" nogil:
7575
cdef char* avcodec_configuration()
7676
cdef char* avcodec_license()
7777

78+
AVPixelFormat avcodec_find_best_pix_fmt_of_list(
79+
const AVPixelFormat *pix_fmt_list,
80+
AVPixelFormat src_pix_fmt,
81+
int has_alpha,
82+
int *loss_ptr,
83+
)
84+
7885
cdef size_t AV_INPUT_BUFFER_PADDING_SIZE
7986
cdef int64_t AV_NOPTS_VALUE
8087

tests/test_codec.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import pytest
22

33
from av import AudioFormat, Codec, VideoFormat, codecs_available
4+
from av.codec import find_best_pix_fmt_of_list
45
from av.codec.codec import UnknownCodecError
56

67

@@ -89,3 +90,64 @@ def test_codec_opus_encoder() -> None:
8990

9091
def test_codecs_available() -> None:
9192
assert codecs_available
93+
94+
95+
def test_find_best_pix_fmt_of_list_empty() -> None:
96+
best, loss = find_best_pix_fmt_of_list([], "rgb24")
97+
assert best is None
98+
assert loss == 0
99+
100+
101+
@pytest.mark.parametrize(
102+
"pix_fmts,src_pix_fmt,expected_best",
103+
[
104+
(["rgb24", "yuv420p"], "rgb24", "rgb24"),
105+
(["rgb24"], "yuv420p", "rgb24"),
106+
(["yuv420p"], "rgb24", "yuv420p"),
107+
([VideoFormat("yuv420p")], VideoFormat("rgb24"), "yuv420p"),
108+
(
109+
["yuv420p", "yuv444p", "gray", "rgb24", "rgba", "bgra", "yuyv422"],
110+
"rgba",
111+
"rgba",
112+
),
113+
],
114+
)
115+
def test_find_best_pix_fmt_of_list_best(pix_fmts, src_pix_fmt, expected_best) -> None:
116+
best, loss = find_best_pix_fmt_of_list(pix_fmts, src_pix_fmt)
117+
assert best is not None
118+
assert best.name == expected_best
119+
assert isinstance(loss, int)
120+
121+
122+
@pytest.mark.parametrize(
123+
"pix_fmts,src_pix_fmt",
124+
[
125+
(["__unknown_pix_fmt"], "rgb24"),
126+
(["rgb24"], "__unknown_pix_fmt"),
127+
],
128+
)
129+
def test_find_best_pix_fmt_of_list_unknown_pix_fmt(pix_fmts, src_pix_fmt) -> None:
130+
with pytest.raises(ValueError, match="not a pixel format"):
131+
find_best_pix_fmt_of_list(pix_fmts, src_pix_fmt)
132+
133+
134+
@pytest.mark.parametrize(
135+
"pix_fmts,src_pix_fmt",
136+
[
137+
(["rgb24", "bgr24", "gray", "yuv420p", "yuv444p", "yuyv422"], "nv12"),
138+
(["yuv420p", "yuv444p", "gray", "yuv420p"], "rgb24"),
139+
(["rgb24", "rgba", "bgra", "rgb24", "gray"], "yuv420p"),
140+
],
141+
)
142+
def test_find_best_pix_fmt_of_list_picks_from_list(pix_fmts, src_pix_fmt) -> None:
143+
best, loss = find_best_pix_fmt_of_list(pix_fmts, src_pix_fmt)
144+
assert best is not None
145+
assert best.name in set(pix_fmts)
146+
assert isinstance(loss, int)
147+
148+
149+
def test_find_best_pix_fmt_of_list_alpha_loss_flagged_when_used() -> None:
150+
best, loss = find_best_pix_fmt_of_list(["rgb24"], "rgba", has_alpha=True)
151+
assert best is not None
152+
assert best.name == "rgb24"
153+
assert loss != 0

0 commit comments

Comments
 (0)