diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2814b76ab..2ad54e184 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -33,6 +33,7 @@ v18.1.0 (Unreleased) Features: +- Add ``CodecContext.supported_options`` to discover generic and codec-specific options by :gh-user:`WyattBlue` (:issue:`2032`). - Add ``Packet.rescale_ts()`` to rescale packet PTS, DTS, and duration to a new ``AVRational`` time base by :gh-user:`WyattBlue`. - Support reusing the thread's current CUDA context via a ``current_ctx`` flag on ``CudaContext`` and ``VideoFrame.from_dlpack``, for interop with libraries like PyTorch that initialize CUDA first by :gh-user:`Yozer` (:pr:`2339`). - ``VideoFrame.from_dlpack`` no longer requires restating ``primary_ctx``/``current_ctx`` when passing an explicit ``cuda_context``; the flags are only validated when explicitly given by :gh-user:`WyattBlue`. diff --git a/av/codec/context.pxd b/av/codec/context.pxd index 8108ecb1a..48d0bb9bd 100644 --- a/av/codec/context.pxd +++ b/av/codec/context.pxd @@ -28,6 +28,7 @@ cdef class CodecContext: # Wraps both versions of the transcode API, returning lists. cpdef encode(self, Frame frame=?) cpdef decode(self, Packet packet=?) + cdef _decode(self, Packet packet) cpdef flush_buffers(self) # Used by hardware-accelerated decode. diff --git a/av/codec/context.py b/av/codec/context.py index 3d3c2ba91..65f130dc5 100644 --- a/av/codec/context.py +++ b/av/codec/context.py @@ -1,4 +1,5 @@ -from enum import Flag, IntEnum +from dataclasses import dataclass +from enum import Flag, IntEnum, IntFlag import cython from cython.cimports import libav as lib @@ -10,7 +11,7 @@ from cython.cimports.av.utils import avrational_to_fraction, to_avrational from cython.cimports.libc.errno import EAGAIN from cython.cimports.libc.stdint import uint8_t -from cython.cimports.libc.string import memcpy +from cython.cimports.libc.string import memcpy, strcmp from av.error import InvalidDataError @@ -90,6 +91,142 @@ class Flags2(IntEnum): ro_flush_noop = lib.AV_CODEC_FLAG2_RO_FLUSH_NOOP +class OptionType(IntEnum): + FLAGS = lib.AV_OPT_TYPE_FLAGS + INT = lib.AV_OPT_TYPE_INT + INT64 = lib.AV_OPT_TYPE_INT64 + DOUBLE = lib.AV_OPT_TYPE_DOUBLE + FLOAT = lib.AV_OPT_TYPE_FLOAT + STRING = lib.AV_OPT_TYPE_STRING + RATIONAL = lib.AV_OPT_TYPE_RATIONAL + BINARY = lib.AV_OPT_TYPE_BINARY + DICT = lib.AV_OPT_TYPE_DICT + UINT64 = lib.AV_OPT_TYPE_UINT64 + CONST = lib.AV_OPT_TYPE_CONST + IMAGE_SIZE = lib.AV_OPT_TYPE_IMAGE_SIZE + PIXEL_FMT = lib.AV_OPT_TYPE_PIXEL_FMT + SAMPLE_FMT = lib.AV_OPT_TYPE_SAMPLE_FMT + VIDEO_RATE = lib.AV_OPT_TYPE_VIDEO_RATE + DURATION = lib.AV_OPT_TYPE_DURATION + COLOR = lib.AV_OPT_TYPE_COLOR + CHANNEL_LAYOUT = lib.AV_OPT_TYPE_CHLAYOUT + BOOL = lib.AV_OPT_TYPE_BOOL + UINT = lib.AV_OPT_TYPE_UINT + + +class OptionFlags(IntFlag): + ENCODING_PARAM = lib.AV_OPT_FLAG_ENCODING_PARAM + DECODING_PARAM = lib.AV_OPT_FLAG_DECODING_PARAM + AUDIO_PARAM = lib.AV_OPT_FLAG_AUDIO_PARAM + VIDEO_PARAM = lib.AV_OPT_FLAG_VIDEO_PARAM + SUBTITLE_PARAM = lib.AV_OPT_FLAG_SUBTITLE_PARAM + EXPORT = lib.AV_OPT_FLAG_EXPORT + READONLY = lib.AV_OPT_FLAG_READONLY + BITSTREAM_FILTER_PARAM = lib.AV_OPT_FLAG_BSF_PARAM + RUNTIME_PARAM = lib.AV_OPT_FLAG_RUNTIME_PARAM + FILTERING_PARAM = lib.AV_OPT_FLAG_FILTERING_PARAM + DEPRECATED = lib.AV_OPT_FLAG_DEPRECATED + CHILD_CONSTS = lib.AV_OPT_FLAG_CHILD_CONSTS + + +@dataclass(frozen=True, slots=True) +class CodecOptionChoice: + """A named value accepted by a codec option.""" + + name: str + help: str + + +@dataclass(frozen=True, slots=True) +class CodecOption: + """Description of a generic or codec-specific option.""" + + name: str + help: str + type: OptionType | int + is_array: bool + default: str | None + min: float + max: float + flags: OptionFlags + choices: tuple[CodecOptionChoice, ...] + + +@dataclass(frozen=True, slots=True) +class CodecOptionSet: + """Generic and codec-specific options supported by a codec context.""" + + generic: tuple[CodecOption, ...] + private: tuple[CodecOption, ...] + + +@cython.cfunc +def _get_option_default( + obj: cython.p_void, name: cython.pointer[cython.const[cython.char]] +): + value: cython.pointer[uint8_t] = cython.NULL + if lib.av_opt_get(obj, name, 0, cython.address(value)) < 0: + return None + try: + return cython.cast(cython.p_char, value) if value != cython.NULL else None + finally: + lib.av_free(value) + + +@cython.cfunc +def _get_supported_options(obj: cython.p_void): + options: list = [] + ptr: cython.pointer[cython.const[lib.AVOption]] = lib.av_opt_next(obj, cython.NULL) + choice_ptr: cython.pointer[cython.const[lib.AVOption]] + option_type: object + + while ptr != cython.NULL: + if ptr.type != lib.AV_OPT_TYPE_CONST: + choices: list = [] + if ptr.unit != cython.NULL: + choice_ptr = lib.av_opt_next(obj, cython.NULL) + while choice_ptr != cython.NULL: + if ( + choice_ptr.type == lib.AV_OPT_TYPE_CONST + and choice_ptr.unit != cython.NULL + and strcmp(choice_ptr.unit, ptr.unit) == 0 + ): + choices.append( + CodecOptionChoice( + choice_ptr.name, + choice_ptr.help + if choice_ptr.help != cython.NULL + else "", + ) + ) + choice_ptr = lib.av_opt_next(obj, choice_ptr) + + raw_type = cython.cast(cython.int, ptr.type) + is_array = bool(raw_type & lib.AV_OPT_TYPE_FLAG_ARRAY) + raw_type &= ~lib.AV_OPT_TYPE_FLAG_ARRAY + try: + option_type = OptionType(raw_type) + except ValueError: + option_type = raw_type + + options.append( + CodecOption( + ptr.name, + ptr.help if ptr.help != cython.NULL else "", + option_type, + is_array, + _get_option_default(obj, ptr.name), + ptr.min, + ptr.max, + OptionFlags(ptr.flags), + tuple(choices), + ) + ) + ptr = lib.av_opt_next(obj, ptr) + + return tuple(options) + + @cython.cclass class CodecContext: @staticmethod @@ -108,6 +245,31 @@ def __cinit__(self, sentinel=None, *args, **kwargs): self.stream_index = -1 # This is set by the container immediately. self.is_open = False + @property + def supported_options(self): + """Options supported by this codec context. + + ``generic`` contains options provided by :ffmpeg:`AVCodecContext`, while + ``private`` contains options provided by the selected codec. Values are + descriptors only; set options through :attr:`options`. + """ + ctx: cython.pointer[lib.AVCodecContext] = lib.avcodec_alloc_context3( + self.codec.ptr + ) + child: cython.p_void + private: list = [] + if ctx == cython.NULL: + raise MemoryError("Cannot allocate codec context") + try: + generic = _get_supported_options(ctx) + child = lib.av_opt_child_next(ctx, cython.NULL) + while child != cython.NULL: + private.extend(_get_supported_options(child)) + child = lib.av_opt_child_next(ctx, child) + return CodecOptionSet(generic, tuple(private)) + finally: + lib.avcodec_free_context(cython.address(ctx)) + @cython.cfunc def _init( self, @@ -555,6 +717,10 @@ def decode(self, packet: Packet | None = None): from multiple threads, give each thread its own :class:`CodecContext`. """ + return self._decode(packet) + + @cython.cfunc + def _decode(self, packet: Packet | None): if not self.codec.ptr: raise ValueError("cannot decode unknown codec") diff --git a/av/codec/context.pyi b/av/codec/context.pyi index e74502571..d500b1761 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -1,4 +1,5 @@ -from enum import Flag, IntEnum +from dataclasses import dataclass +from enum import Flag, IntEnum, IntFlag from fractions import Fraction from typing import ClassVar, Literal, cast, overload @@ -52,10 +53,70 @@ class Flags2(IntEnum): skip_manual = cast(int, ...) ro_flush_noop = cast(int, ...) +class OptionType(IntEnum): + FLAGS = cast(int, ...) + INT = cast(int, ...) + INT64 = cast(int, ...) + DOUBLE = cast(int, ...) + FLOAT = cast(int, ...) + STRING = cast(int, ...) + RATIONAL = cast(int, ...) + BINARY = cast(int, ...) + DICT = cast(int, ...) + UINT64 = cast(int, ...) + CONST = cast(int, ...) + IMAGE_SIZE = cast(int, ...) + PIXEL_FMT = cast(int, ...) + SAMPLE_FMT = cast(int, ...) + VIDEO_RATE = cast(int, ...) + DURATION = cast(int, ...) + COLOR = cast(int, ...) + CHANNEL_LAYOUT = cast(int, ...) + BOOL = cast(int, ...) + UINT = cast(int, ...) + +class OptionFlags(IntFlag): + ENCODING_PARAM = cast(int, ...) + DECODING_PARAM = cast(int, ...) + AUDIO_PARAM = cast(int, ...) + VIDEO_PARAM = cast(int, ...) + SUBTITLE_PARAM = cast(int, ...) + EXPORT = cast(int, ...) + READONLY = cast(int, ...) + BITSTREAM_FILTER_PARAM = cast(int, ...) + RUNTIME_PARAM = cast(int, ...) + FILTERING_PARAM = cast(int, ...) + DEPRECATED = cast(int, ...) + CHILD_CONSTS = cast(int, ...) + +@dataclass(frozen=True, slots=True) +class CodecOptionChoice: + name: str + help: str + +@dataclass(frozen=True, slots=True) +class CodecOption: + name: str + help: str + type: OptionType | int + is_array: bool + default: str | None + min: float + max: float + flags: OptionFlags + choices: tuple[CodecOptionChoice, ...] + +@dataclass(frozen=True, slots=True) +class CodecOptionSet: + generic: tuple[CodecOption, ...] + private: tuple[CodecOption, ...] + class CodecContext: name: str type: Literal["video", "audio", "data", "subtitle", "attachment"] options: dict[str, str] + @property + def supported_options(self) -> CodecOptionSet: ... profile: str | None level: int @property diff --git a/av/subtitles/codeccontext.pxd b/av/subtitles/codeccontext.pxd index 0be45ed10..fb5748294 100644 --- a/av/subtitles/codeccontext.pxd +++ b/av/subtitles/codeccontext.pxd @@ -4,5 +4,5 @@ from av.packet cimport Packet cdef class SubtitleCodecContext(CodecContext): cdef bint subtitle_header_set - cpdef decode(self, Packet packet=?) + cdef _decode(self, Packet packet) cpdef decode2(self, Packet packet) diff --git a/av/subtitles/codeccontext.py b/av/subtitles/codeccontext.py index 618badfb1..34dc868cf 100644 --- a/av/subtitles/codeccontext.py +++ b/av/subtitles/codeccontext.py @@ -92,8 +92,8 @@ def encode_subtitle(self, subtitle: SubtitleSet) -> Packet: return packet - @cython.ccall - def decode(self, packet: Packet | None = None): + @cython.cfunc + def _decode(self, packet: Packet | None): """Decode a subtitle packet, returning a list of :class:`.Subtitle` objects if a subtitle was decoded, or an empty list otherwise.""" if not self.codec.ptr: diff --git a/docs/api/codec.rst b/docs/api/codec.rst index a3f9b27a2..a8340bcb2 100644 --- a/docs/api/codec.rst +++ b/docs/api/codec.rst @@ -66,6 +66,13 @@ Contexts .. autoattribute:: CodecContext.codec .. autoattribute:: CodecContext.options +.. autoattribute:: CodecContext.supported_options + +.. autoclass:: CodecOptionSet +.. autoclass:: CodecOption +.. autoclass:: CodecOptionChoice +.. autoclass:: OptionType +.. autoclass:: OptionFlags .. automethod:: CodecContext.create .. automethod:: CodecContext.open @@ -161,4 +168,3 @@ frames passed to ``encode`` are uploaded to the device automatically:: See ``examples/basics/hw_decode.py`` for a complete example, including recommended device types per platform. - diff --git a/include/avutil.pxd b/include/avutil.pxd index 2cf76c230..8d671c98b 100644 --- a/include/avutil.pxd +++ b/include/avutil.pxd @@ -305,6 +305,8 @@ cdef extern from "libavutil/opt.h" nogil: AV_OPT_TYPE_COLOR AV_OPT_TYPE_CHLAYOUT AV_OPT_TYPE_BOOL + AV_OPT_TYPE_UINT + AV_OPT_TYPE_FLAG_ARRAY cdef union AVOption_default_val: int64_t i64 @@ -320,7 +322,11 @@ cdef extern from "libavutil/opt.h" nogil: AV_OPT_FLAG_SUBTITLE_PARAM AV_OPT_FLAG_EXPORT AV_OPT_FLAG_READONLY + AV_OPT_FLAG_BSF_PARAM + AV_OPT_FLAG_RUNTIME_PARAM AV_OPT_FLAG_FILTERING_PARAM + AV_OPT_FLAG_DEPRECATED + AV_OPT_FLAG_CHILD_CONSTS cdef struct AVOption: const char *name @@ -333,6 +339,12 @@ cdef extern from "libavutil/opt.h" nogil: int flags const char *unit + cdef const AVOption *av_opt_next(const void *obj, const AVOption *prev) + cdef void *av_opt_child_next(void *obj, void *prev) + cdef int av_opt_get( + void *obj, const char *name, int search_flags, uint8_t **out_val + ) + cdef extern from "libavutil/pixdesc.h" nogil: # See: http://ffmpeg.org/doxygen/trunk/structAVComponentDescriptor.html cdef struct AVComponentDescriptor: diff --git a/setup.py b/setup.py index 2cd23d5a8..872c5f8e8 100644 --- a/setup.py +++ b/setup.py @@ -141,12 +141,6 @@ def parse_cflags(raw_flags): IMPORT_NAME = "av" -# Newer compilers treat incompatible pointer types as an error by default; -# downgrade it back to a warning so FFmpeg API churn doesn't break the build. -extra_compile_args = [] -if platform.system() != "Windows": - extra_compile_args.append("-Wno-error=incompatible-pointer-types") - loudnorm_extension = Extension( f"{IMPORT_NAME}.filter.loudnorm", sources=[ @@ -157,7 +151,6 @@ def parse_cflags(raw_flags): libraries=extension_extra["libraries"], library_dirs=extension_extra["library_dirs"], define_macros=define_macros, - extra_compile_args=extra_compile_args, py_limited_api=py_limited_api, ) @@ -204,7 +197,6 @@ def parse_cflags(raw_flags): library_dirs=extension_extra["library_dirs"], sources=[pyx_path], define_macros=define_macros, - extra_compile_args=extra_compile_args, py_limited_api=py_limited_api, ), compiler_directives=compiler_directives, diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index db8c71bb3..272a1d45a 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -20,6 +20,7 @@ VideoFrame, ) from av.codec.codec import UnknownCodecError +from av.codec.context import OptionFlags, OptionType from av.video.frame import PictureType from .common import TestCase, fate_suite @@ -70,6 +71,36 @@ def iter_raw_frames( class TestCodecContext(TestCase): + def test_supported_options(self) -> None: + ctx = Codec("flac", "w").create() + supported = ctx.supported_options + generic = {option.name: option for option in supported.generic} + private = {option.name: option for option in supported.private} + + bit_rate = generic["b"] + assert bit_rate.type is OptionType.INT64 + assert bit_rate.default is not None + assert bit_rate.flags & OptionFlags.ENCODING_PARAM + assert bit_rate.flags & OptionFlags.AUDIO_PARAM + + strict_choices = {choice.name for choice in generic["strict"].choices} + assert "experimental" in strict_choices + + lpc_type = private["lpc_type"] + assert lpc_type.type is OptionType.INT + assert lpc_type.default == "-1" + assert {choice.name for choice in lpc_type.choices} >= { + "none", + "fixed", + "levinson", + } + + # Descriptors report FFmpeg's defaults, not values changed on this context. + ctx.bit_rate = 123456 + changed_supported = ctx.supported_options + changed_generic = {option.name: option for option in changed_supported.generic} + assert changed_generic["b"].default == bit_rate.default + def test_global_quality(self): ctx = Codec("mpeg4", "w").create() ctx.global_quality = 5