diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3b9107b1f..15036ff1a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -34,6 +34,10 @@ v19.0.3 (Unreleased) Fixes: - Support slicing ``Container.streams``, which raised ``TypeError`` despite being typed. +- ``HWConfig.methods`` no longer raises ``ValueError`` for configs that support several methods; ``HWConfigMethod`` is now an ``IntFlag``. +- ``CodecContext.is_encoder``, ``CodecContext.is_decoder``, ``Container.input_was_opened``, and ``VideoFormatComponent.is_chroma`` now return ``bool`` instead of ``int``. +- ``Frame.time_base`` returns ``AVRational(0, 1)`` instead of ``None`` when unset, like other ``AVRational`` properties. +- Fix type stubs that did not match runtime: ``average_rate``, ``base_rate``, and ``guessed_rate`` are only on ``VideoStream``; ``codec``, ``codec_tag``, and ``options`` are only on audio, video, and subtitle streams; ``Stream.codec_context`` may be ``None``; ``AudioStream.bit_rate``, ``InputContainer.start_time``, ``VideoFrame.time``, and ``FilterPad.context`` may be ``None``; ``Codec.hardware_configs`` is a tuple; ``CodecContext.delay`` is an ``int``; ``AudioStream.max_bit_rate`` is added; and the inaccessible ``Plane.frame``, ``Plane.index``, and ``Container.buffer_size`` are removed. v19.0.2 ------- diff --git a/av/audio/resampler.py b/av/audio/resampler.py index a90aee956..ad723865c 100644 --- a/av/audio/resampler.py +++ b/av/audio/resampler.py @@ -93,7 +93,7 @@ def resample(self, frame: AudioFrame | None) -> list[AudioFrame]: self.graph = Graph() self.graph.threads = self.threads extra_args = {} - if frame.time_base is not None: + if frame.time_base: extra_args["time_base"] = f"{frame.time_base}" abuffer = self.graph.add( diff --git a/av/audio/stream.pyi b/av/audio/stream.pyi index 88dfbc35b..835b742d2 100644 --- a/av/audio/stream.pyi +++ b/av/audio/stream.pyi @@ -1,6 +1,7 @@ from collections.abc import Iterator from typing import Literal +from av.codec.codec import Codec from av.packet import Packet from av.stream import Stream @@ -24,9 +25,13 @@ class AudioStream(Stream): def decode(self, packet: Packet | None = None) -> list[AudioFrame]: ... # From codec context + codec: Codec + codec_tag: str + options: dict[str, str] frame_size: int sample_rate: int - bit_rate: int + bit_rate: int | None + max_bit_rate: int | None rate: int channels: int type: Literal["audio"] diff --git a/av/codec/codec.pyi b/av/codec/codec.pyi index 5203b6101..46d061d84 100644 --- a/av/codec/codec.pyi +++ b/av/codec/codec.pyi @@ -79,7 +79,7 @@ class Codec: audio_rates: list[int] | None video_formats: list[VideoFormat] | None audio_formats: list[AudioFormat] | None - hardware_configs: list[HWConfig] + hardware_configs: tuple[HWConfig, ...] @property def properties(self) -> int: ... diff --git a/av/codec/context.py b/av/codec/context.py index ed5bf9e51..99a07fe3c 100644 --- a/av/codec/context.py +++ b/av/codec/context.py @@ -392,13 +392,13 @@ def extradata_size(self): def is_encoder(self): if self.ptr is cython.NULL: return False - return lib.av_codec_is_encoder(self.ptr.codec) + return bool(lib.av_codec_is_encoder(self.ptr.codec)) @property def is_decoder(self): if self.ptr is cython.NULL: return False - return lib.av_codec_is_decoder(self.ptr.codec) + return bool(lib.av_codec_is_decoder(self.ptr.codec)) @property def codec(self): diff --git a/av/codec/context.pyi b/av/codec/context.pyi index e9b5764af..cb9e67f0a 100644 --- a/av/codec/context.pyi +++ b/av/codec/context.pyi @@ -188,7 +188,7 @@ class CodecContext: @property def decoded_side_data(self) -> dict[FrameSideDataType, bytes]: ... @property - def delay(self) -> bool: ... + def delay(self) -> int: ... @property def extradata_size(self) -> int: ... @property diff --git a/av/codec/hwaccel.py b/av/codec/hwaccel.py index 44e78c0f1..aaca6fb87 100644 --- a/av/codec/hwaccel.py +++ b/av/codec/hwaccel.py @@ -1,5 +1,5 @@ import weakref -from enum import IntEnum +from enum import IntEnum, IntFlag import cython import cython.cimports.libav as lib @@ -27,7 +27,7 @@ class HWDeviceType(IntEnum): ohcodec = lib.AV_HWDEVICE_TYPE_OHCODEC -class HWConfigMethod(IntEnum): +class HWConfigMethod(IntFlag): none = 0 hw_device_ctx = ( lib.AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX diff --git a/av/codec/hwaccel.pyi b/av/codec/hwaccel.pyi index 28a9d4292..48b113d64 100644 --- a/av/codec/hwaccel.pyi +++ b/av/codec/hwaccel.pyi @@ -1,4 +1,4 @@ -from enum import IntEnum +from enum import IntEnum, IntFlag from typing import cast from av.codec.codec import Codec @@ -21,7 +21,7 @@ class HWDeviceType(IntEnum): amf = cast(int, ...) ohcodec = cast(int, ...) -class HWConfigMethod(IntEnum): +class HWConfigMethod(IntFlag): none = cast(int, ...) hw_device_ctx = cast(int, ...) hw_frame_ctx = cast(int, ...) diff --git a/av/container/core.py b/av/container/core.py index 0736c9678..01c392d93 100755 --- a/av/container/core.py +++ b/av/container/core.py @@ -404,7 +404,7 @@ def flags(self, value: cython.int): @property def input_was_opened(self): - return self._myflag & 2 + return bool(self._myflag & 2) @property def metadata(self) -> dict: diff --git a/av/container/core.pyi b/av/container/core.pyi index 1bbd82b26..642124406 100644 --- a/av/container/core.pyi +++ b/av/container/core.pyi @@ -92,7 +92,6 @@ class Chapter(TypedDict): class Container: name: str file: Any - buffer_size: int io_open: Any open_files: Any format: ContainerFormat diff --git a/av/container/input.pyi b/av/container/input.pyi index 449b56f10..ce6439cb0 100644 --- a/av/container/input.pyi +++ b/av/container/input.pyi @@ -14,7 +14,7 @@ from .core import Container @final class InputContainer(Container): - start_time: int + start_time: int | None start_time_realtime: int | None duration: int | None bit_rate: int diff --git a/av/filter/graph.py b/av/filter/graph.py index b94e02313..4293dfbf9 100644 --- a/av/filter/graph.py +++ b/av/filter/graph.py @@ -182,7 +182,7 @@ def add_buffer( raise ValueError("missing height") if format is None: raise ValueError("missing format") - if time_base is None: + if not time_base: warnings.warn( "missing time_base. Guessing 1/1000 time base. " "This is deprecated and may be removed in future releases.", @@ -231,7 +231,7 @@ def add_abuffer( raise ValueError("missing format") if layout is None and channels is None: raise ValueError("missing layout or channels") - if time_base is None: + if not time_base: time_base = AVRational(1, sample_rate) kwargs = { diff --git a/av/filter/link.pyi b/av/filter/link.pyi index 2bd839492..5c5212b66 100644 --- a/av/filter/link.pyi +++ b/av/filter/link.pyi @@ -4,7 +4,7 @@ from av.filter.graph import Graph class FilterPad: filter: Filter - context: FilterContext + context: FilterContext | None is_input: bool index: int def __eq__(self, other: object) -> bool: ... @@ -17,6 +17,7 @@ class FilterPad: def type(self) -> str: ... class FilterContextPad(FilterPad): + context: FilterContext @property def link(self) -> FilterLink | None: ... @property diff --git a/av/frame.py b/av/frame.py index 5d60aeefa..9e6a87a62 100644 --- a/av/frame.py +++ b/av/frame.py @@ -1,7 +1,7 @@ import cython from cython.cimports.av.error import err_check from cython.cimports.av.opaque import opaque_container -from cython.cimports.av.rational import from_avrational +from cython.cimports.av.rational import AVRational, from_avrational from cython.cimports.av.utils import ( avdict_to_dict, to_avrational, @@ -146,10 +146,13 @@ def time_base(self): """ The unit of time (in fractional seconds) in which timestamps are expressed. + ``AVRational(0, 1)`` if unset. + :type: AVRational """ - if self._time_base.num: - return from_avrational(self._time_base) + if not self._time_base.num: + return AVRational(0, 1) + return from_avrational(self._time_base) @time_base.setter def time_base(self, value): diff --git a/av/plane.pyi b/av/plane.pyi index 99594d11a..b32fcdb11 100644 --- a/av/plane.pyi +++ b/av/plane.pyi @@ -2,7 +2,4 @@ from .buffer import Buffer from .frame import Frame class Plane(Buffer): - frame: Frame - index: int - def __init__(self, frame: Frame, index: int) -> None: ... diff --git a/av/stream.pyi b/av/stream.pyi index bbbdbbc3c..a83af5b81 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -4,7 +4,7 @@ from typing import Literal, cast from av.rational import AVRational -from .codec import Codec, CodecContext +from .codec import CodecContext from .container import Container from .index import IndexEntries @@ -41,8 +41,7 @@ class Discard(IntEnum): class Stream: name: str | None container: Container - codec: Codec - codec_context: CodecContext + codec_context: CodecContext | None metadata: dict[str, str] @property def index_entries(self) -> IndexEntries: ... @@ -50,14 +49,10 @@ class Stream: profiles: list[str] profile: str | None index: int - options: dict[str, object] @property def time_base(self) -> AVRational: ... @time_base.setter def time_base(self, value: AVRational | Fraction | int) -> None: ... - average_rate: AVRational - base_rate: AVRational - guessed_rate: AVRational start_time: int | None duration: int | None disposition: Disposition @@ -66,14 +61,13 @@ class Stream: language: str | None type: Literal["video", "audio", "data", "subtitle", "attachment", "unknown"] - # From context - codec_tag: str - class DataStream(Stream): + codec_context: None type: Literal["data"] name: str | None class AttachmentStream(Stream): + codec_context: None type: Literal["attachment"] @property def mimetype(self) -> str | None: ... diff --git a/av/subtitles/stream.pyi b/av/subtitles/stream.pyi index 4a8ffe910..f5b0baa45 100644 --- a/av/subtitles/stream.pyi +++ b/av/subtitles/stream.pyi @@ -1,5 +1,6 @@ from typing import final +from av.codec.codec import Codec from av.packet import Packet from av.stream import Stream from av.subtitles.subtitle import AssSubtitle, BitmapSubtitle, SubtitleSet @@ -9,6 +10,11 @@ from .codeccontext import SubtitleCodecContext @final class SubtitleStream(Stream): codec_context: SubtitleCodecContext + + # From codec context + codec: Codec + codec_tag: str + options: dict[str, str] def decode( self, packet: Packet | None = None ) -> list[AssSubtitle] | list[BitmapSubtitle]: ... diff --git a/av/video/format.py b/av/video/format.py index 40d5f2fa1..265e7f43f 100644 --- a/av/video/format.py +++ b/av/video/format.py @@ -184,7 +184,7 @@ def is_luma(self): @property def is_chroma(self): """Is this component a chroma channel?""" - return (self.index == 1 or self.index == 2) and ( + return (self.index == 1 or self.index == 2) and bool( self.format.ptr.log2_chroma_w or self.format.ptr.log2_chroma_h ) diff --git a/av/video/frame.pyi b/av/video/frame.pyi index 1abc8e1d8..4704cbfe5 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -59,7 +59,7 @@ class VideoFrame(Frame): @property def sw_format(self) -> VideoFormat | None: ... @property - def time(self) -> float: ... + def time(self) -> float | None: ... @property def width(self) -> int: ... @property diff --git a/av/video/stream.pyi b/av/video/stream.pyi index 9be73b477..c02cafd30 100644 --- a/av/video/stream.pyi +++ b/av/video/stream.pyi @@ -1,6 +1,7 @@ from collections.abc import Iterator, Sequence from typing import Literal +from av.codec.codec import Codec from av.codec.context import ThreadType from av.packet import Packet from av.rational import AVRational @@ -26,7 +27,14 @@ class VideoStream(Stream): self, degrees: float, hflip: bool = ..., vflip: bool = ... ) -> None: ... + average_rate: AVRational + base_rate: AVRational + guessed_rate: AVRational + # from codec context + codec: Codec + codec_tag: str + options: dict[str, str] format: VideoFormat thread_count: int thread_type: ThreadType diff --git a/tests/test_audiofifo.py b/tests/test_audiofifo.py index 85e9481ee..ce8be1273 100644 --- a/tests/test_audiofifo.py +++ b/tests/test_audiofifo.py @@ -113,5 +113,5 @@ def test_missing_time_base(self) -> None: oframe = fifo.read(512) assert oframe is not None - assert oframe.pts is None and oframe.time_base is None + assert oframe.pts is None and not oframe.time_base assert oframe.sample_rate == iframe.sample_rate diff --git a/tests/test_codec.py b/tests/test_codec.py index 2203f4cba..aa46afcf1 100644 --- a/tests/test_codec.py +++ b/tests/test_codec.py @@ -3,6 +3,7 @@ from av import AudioFormat, Codec, VideoFormat, codecs_available from av.codec import PixFmtLoss, find_best_pix_fmt_of_list from av.codec.codec import UnknownCodecError +from av.codec.hwaccel import HWConfigMethod def test_codec_bogus() -> None: @@ -165,3 +166,16 @@ def test_find_best_pix_fmt_of_list_loss_flags() -> None: _, loss = find_best_pix_fmt_of_list(["gray"], "rgb24") assert isinstance(loss, PixFmtLoss) assert loss & PixFmtLoss.CHROMA + + +def test_codec_hardware_config_methods() -> None: + # methods is a bitmask; a config supporting several of them must not raise. + for name in codecs_available: + for mode in ("r", "w"): + try: + codec = Codec(name, mode) + except UnknownCodecError: + continue + assert isinstance(codec.hardware_configs, tuple) + for config in codec.hardware_configs: + assert isinstance(config.methods, HWConfigMethod) diff --git a/tests/test_decode.py b/tests/test_decode.py index 603a63e73..21e39c153 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -75,6 +75,7 @@ def test_decode_stream_without_codec_context(self) -> None: def test_mux_stream_without_codec_context(self) -> None: with av.open(io.BytesIO(), "w", format="mp4") as output: stream = output.add_mux_stream("h264", width=16, height=16) + assert isinstance(stream, av.VideoStream) assert stream.codec_context is None with pytest.raises(av.EncoderNotFoundError): stream.encode(None) diff --git a/tests/test_file_probing.py b/tests/test_file_probing.py index 11570deed..2cb338bec 100644 --- a/tests/test_file_probing.py +++ b/tests/test_file_probing.py @@ -183,7 +183,7 @@ def test_container_probing(self) -> None: assert len(self.file.streams) == 1 def test_stream_probing(self) -> None: - stream = self.file.streams[0] + stream = self.file.streams.subtitles[0] assert str(stream).startswith(" None: out_stream = output.add_mux_stream( in_stream.codec_context.name, width=width, height=height ) + assert isinstance(out_stream, av.VideoStream) assert out_stream.codec_context is None assert out_stream.type == "video"