diff --git a/av/codec/context.pyi b/av/codec/context.pyi index d2336ce1d..8c50ba456 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -59,6 +59,8 @@ class CodecContext: type: Literal["video", "audio", "data", "subtitle", "attachment"] options: dict[str, str] profile: str | None + @property + def profiles(self) -> list[str]: ... extradata: bytes | None time_base: Fraction codec_tag: str diff --git a/av/codec/context.pyx b/av/codec/context.pyx index 0170aae52..a1e95048c 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -497,10 +497,55 @@ cdef class CodecContext: def type(self): return self.codec.type + @property + def profiles(self): + """ + List the available profiles for this stream. + + :type: list[str] + """ + ret = [] + if not self.ptr.codec or not self.codec.desc or not self.codec.desc.profiles: + return ret + + # Profiles are always listed in the codec descriptor, but not necessarily in + # the codec itself. So use the descriptor here. + desc = self.codec.desc + cdef int i = 0 + while desc.profiles[i].profile != lib.FF_PROFILE_UNKNOWN: + ret.append(desc.profiles[i].name) + i += 1 + + return ret + @property def profile(self): - if self.ptr.codec and lib.av_get_profile_name(self.ptr.codec, self.ptr.profile): - return lib.av_get_profile_name(self.ptr.codec, self.ptr.profile) + if not self.ptr.codec or not self.codec.desc or not self.codec.desc.profiles: + return + + # Profiles are always listed in the codec descriptor, but not necessarily in + # the codec itself. So use the descriptor here. + desc = self.codec.desc + cdef int i = 0 + while desc.profiles[i].profile != lib.FF_PROFILE_UNKNOWN: + if desc.profiles[i].profile == self.ptr.profile: + return desc.profiles[i].name + i += 1 + + @profile.setter + def profile(self, value): + if not self.codec or not self.codec.desc or not self.codec.desc.profiles: + return + + # Profiles are always listed in the codec descriptor, but not necessarily in + # the codec itself. So use the descriptor here. + desc = self.codec.desc + cdef int i = 0 + while desc.profiles[i].profile != lib.FF_PROFILE_UNKNOWN: + if desc.profiles[i].name == value: + self.ptr.profile = desc.profiles[i].profile + return + i += 1 @property def time_base(self): diff --git a/av/stream.pyi b/av/stream.pyi index 89f49c12f..8c48f3ac7 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -19,6 +19,7 @@ class Stream: codec_context: CodecContext metadata: dict[str, str] id: int + profiles: list[str] profile: str index: int time_base: Fraction | None diff --git a/av/stream.pyx b/av/stream.pyx index 19ac8e703..4c450d283 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -145,6 +145,18 @@ cdef class Stream: else: self.ptr.id = value + @property + def profiles(self): + """ + List the available profiles for this stream. + + :type: list[str] + """ + if self.codec_context: + return self.codec_context.profiles + else: + return [] + @property def profile(self): """ diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index d680e8b13..54fb2293c 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -146,6 +146,9 @@ cdef extern from "libavcodec/avcodec.h" nogil: FF_COMPLIANCE_UNOFFICIAL FF_COMPLIANCE_EXPERIMENTAL + cdef enum: + FF_PROFILE_UNKNOWN = -99 + cdef enum AVCodecID: AV_CODEC_ID_NONE AV_CODEC_ID_MPEG2VIDEO @@ -178,12 +181,17 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef int av_codec_is_encoder(AVCodec*) cdef int av_codec_is_decoder(AVCodec*) + cdef struct AVProfile: + int profile + char *name + cdef struct AVCodecDescriptor: AVCodecID id char *name char *long_name int props char **mime_types + AVProfile *profiles AVCodecDescriptor* avcodec_descriptor_get(AVCodecID) @@ -266,13 +274,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef AVClass* avcodec_get_class() - cdef struct AVCodecDescriptor: - AVCodecID id - AVMediaType type - char *name - char *long_name - int props - cdef AVCodec* avcodec_find_decoder(AVCodecID id) cdef AVCodec* avcodec_find_encoder(AVCodecID id) diff --git a/tests/test_encode.py b/tests/test_encode.py index 11d8a4ec9..90d30dfce 100644 --- a/tests/test_encode.py +++ b/tests/test_encode.py @@ -449,3 +449,29 @@ def test_qmin_qmax(self) -> None: factor = 1.3 # insist at least 30% larger each time assert all(small * factor < large for small, large in zip(sizes, sizes[1:])) + + +class TestProfiles(TestCase): + def test_profiles(self) -> None: + """ + Test that we can set different encoder profiles. + """ + # Let's try a video and an audio codec. + file = io.BytesIO() + codecs = ( + ("h264", 30), + ("aac", 48000), + ) + + for codec_name, rate in codecs: + print("Testing:", codec_name) + container = av.open(file, mode="w", format="mp4") + stream = container.add_stream(codec_name, rate=rate) + assert len(stream.profiles) >= 1 # check that we're testing something! + + # It should be enough to test setting and retrieving the code. That means + # libav has recognised the profile and set it correctly. + for profile in stream.profiles: + stream.profile = profile + print("Set", profile, "got", stream.profile) + assert stream.profile == profile