diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 797133111..1e7a60595 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -40,6 +40,7 @@ Features: - Add ``options`` parameter to ``AudioResampler`` for passing ``libswresample`` options (e.g. ``resampler``, ``filter_size``, ``cutoff``) by :gh-user:`WyattBlue` (:issue:`2262`). - Support ``yuv420p10le`` in ``VideoFrame.to_ndarray`` and ``VideoFrame.from_ndarray`` by :gh-user:`WyattBlue` (:issue:`1981`). - Add ``at`` parameter to ``Graph.push`` and ``Graph.vpush`` to push a frame to a single buffer source by index, for multi-input filters like ``overlay`` by :gh-user:`WyattBlue`. +- ``find_best_pix_fmt_of_list`` now returns the loss as a ``PixFmtLoss`` ``enum.IntFlag`` instead of a plain ``int`` by :gh-user:`WyattBlue` (:issue:`2300`). Fixes: diff --git a/av/codec/__init__.py b/av/codec/__init__.py index f8d4d75ae..f8f589de8 100644 --- a/av/codec/__init__.py +++ b/av/codec/__init__.py @@ -1,6 +1,7 @@ from .codec import ( Capabilities, Codec, + PixFmtLoss, Properties, codecs_available, find_best_pix_fmt_of_list, @@ -10,6 +11,7 @@ __all__ = ( "Capabilities", "Codec", + "PixFmtLoss", "Properties", "codecs_available", "find_best_pix_fmt_of_list", diff --git a/av/codec/codec.py b/av/codec/codec.py index 616980641..edda0c29d 100644 --- a/av/codec/codec.py +++ b/av/codec/codec.py @@ -1,4 +1,4 @@ -from enum import Flag, IntEnum +from enum import Flag, IntEnum, IntFlag import cython from cython.cimports import libav as lib @@ -54,6 +54,22 @@ class Capabilities(IntEnum): encoder_recon_frame = 1 << 22 +class PixFmtLoss(IntFlag): + """Flags describing what is lost when converting between pixel formats. + + Returned by :func:`find_best_pix_fmt_of_list`. Mirrors FFmpeg's + ``FF_LOSS_*`` flags. + """ + + NONE = 0 + RESOLUTION = 0x0001 # loss due to resolution change + DEPTH = 0x0002 # loss due to color depth change + COLORSPACE = 0x0004 # loss due to color space conversion + ALPHA = 0x0008 # loss of alpha bit + COLORQUANT = 0x0010 # loss due to color quantization + CHROMA = 0x0020 # loss of chroma (e.g. RGB to gray conversion) + + class UnknownCodecError(ValueError): pass @@ -419,7 +435,7 @@ def find_best_pix_fmt_of_list(pix_fmts, src_pix_fmt, has_alpha=False): :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) + :rtype: (VideoFormat | None, PixFmtLoss) """ src: lib.AVPixelFormat best: lib.AVPixelFormat @@ -434,7 +450,7 @@ def find_best_pix_fmt_of_list(pix_fmts, src_pix_fmt, has_alpha=False): pix_fmts = tuple(pix_fmts) if not pix_fmts: - return None, 0 + return None, PixFmtLoss.NONE if isinstance(src_pix_fmt, VideoFormat): src = cython.cast(VideoFormat, src_pix_fmt).pix_fmt @@ -462,7 +478,7 @@ def find_best_pix_fmt_of_list(pix_fmts, src_pix_fmt, has_alpha=False): best = lib.avcodec_find_best_pix_fmt_of_list( c_list, src, 1 if has_alpha else 0, cython.address(c_loss) ) - return get_video_format(best, 0, 0), c_loss + return get_video_format(best, 0, 0), PixFmtLoss(c_loss) finally: if c_list != cython.NULL: free(c_list) diff --git a/av/codec/codec.pyi b/av/codec/codec.pyi index 32c83371e..a9876c4f5 100644 --- a/av/codec/codec.pyi +++ b/av/codec/codec.pyi @@ -1,5 +1,5 @@ from collections.abc import Sequence -from enum import Flag, IntEnum +from enum import Flag, IntEnum, IntFlag from fractions import Fraction from typing import ClassVar, Literal, cast, overload @@ -44,6 +44,15 @@ class Capabilities(IntEnum): encoder_flush = cast(int, ...) encoder_recon_frame = cast(int, ...) +class PixFmtLoss(IntFlag): + NONE = cast(ClassVar[PixFmtLoss], ...) + RESOLUTION = cast(ClassVar[PixFmtLoss], ...) + DEPTH = cast(ClassVar[PixFmtLoss], ...) + COLORSPACE = cast(ClassVar[PixFmtLoss], ...) + ALPHA = cast(ClassVar[PixFmtLoss], ...) + COLORQUANT = cast(ClassVar[PixFmtLoss], ...) + CHROMA = cast(ClassVar[PixFmtLoss], ...) + class UnknownCodecError(ValueError): ... class Codec: @@ -117,7 +126,7 @@ def find_best_pix_fmt_of_list( pix_fmts: Sequence[PixFmtLike], src_pix_fmt: PixFmtLike, has_alpha: bool = False, -) -> tuple[VideoFormat | None, int]: +) -> tuple[VideoFormat | None, PixFmtLoss]: """ Find the best pixel format to convert to given a source format. @@ -127,10 +136,15 @@ def find_best_pix_fmt_of_list( :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) + the list, or None if no suitable format was found; loss is a combination of + :class:`PixFmtLoss` flags informing you what kind of losses will occur. + :rtype: (VideoFormat | None, PixFmtLoss) - 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. + Note on loss: it is an :class:`enum.IntFlag` 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 can be tested with bitwise & against the :class:`PixFmtLoss` + members. For exact behavior see: libavutil/pixdesc.c/get_pix_fmt_score() in ffmpeg source code. """ ... diff --git a/docs/api/codec.rst b/docs/api/codec.rst index 43bb235c2..7b6c261e3 100644 --- a/docs/api/codec.rst +++ b/docs/api/codec.rst @@ -43,6 +43,19 @@ Flags Note that ``ffmpeg -codecs`` prefers the properties versions of ``INTRA_ONLY`` and ``LOSSLESS``. +Pixel Format Selection +---------------------- + +.. autofunction:: find_best_pix_fmt_of_list + +.. autoclass:: PixFmtLoss + + Wraps FFmpeg's ``FF_LOSS_*`` flags. Returned by + :func:`find_best_pix_fmt_of_list` to describe what is lost when converting + from the source pixel format to the chosen one. Being an + :class:`enum.IntFlag`, members can be combined and tested with bitwise + operators. + Contexts -------- diff --git a/tests/test_codec.py b/tests/test_codec.py index 17432c926..2f8b95e0b 100644 --- a/tests/test_codec.py +++ b/tests/test_codec.py @@ -1,7 +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 import PixFmtLoss, find_best_pix_fmt_of_list from av.codec.codec import UnknownCodecError @@ -96,6 +96,7 @@ 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 + assert loss is PixFmtLoss.NONE @pytest.mark.parametrize( @@ -151,3 +152,16 @@ def test_find_best_pix_fmt_of_list_alpha_loss_flagged_when_used() -> None: assert best is not None assert best.name == "rgb24" assert loss != 0 + assert isinstance(loss, PixFmtLoss) + assert loss & PixFmtLoss.ALPHA + + +def test_find_best_pix_fmt_of_list_loss_flags() -> None: + # An identical format loses nothing. + _, loss = find_best_pix_fmt_of_list(["yuv420p"], "yuv420p") + assert loss is PixFmtLoss.NONE + + # Converting color to grayscale drops the chroma planes. + _, loss = find_best_pix_fmt_of_list(["gray"], "rgb24") + assert isinstance(loss, PixFmtLoss) + assert loss & PixFmtLoss.CHROMA