From e3b0645f1a7e7b524c7b5998d6e75f40a8e18fe6 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 20 Jul 2024 20:41:35 -0400 Subject: [PATCH] Sync with upstream --- av/__init__.py | 12 ++++ av/about.py | 2 +- av/audio/frame.pyx | 9 +-- av/audio/resampler.pyi | 4 +- av/codec/context.pyx | 109 +++++++++---------------------- av/container/core.pyx | 2 +- av/container/output.pyi | 8 +++ av/container/output.pyx | 43 ++++++++++++ av/filter/graph.pyi | 1 + av/filter/graph.pyx | 9 ++- av/format.pyi | 4 ++ av/frame.pxd | 7 -- av/frame.pyx | 3 +- av/logging.pyi | 1 + av/logging.pyx | 11 ++++ av/subtitles/codeccontext.pyx | 7 +- av/subtitles/stream.pxd | 3 +- av/subtitles/stream.pyi | 5 +- av/subtitles/stream.pyx | 17 +++++ av/subtitles/subtitle.pyi | 13 ++-- av/subtitles/subtitle.pyx | 69 ++++++++++++++----- av/video/frame.pyx | 4 +- av/video/reformatter.pyx | 8 ++- docs/api/subtitles.rst | 9 +-- include/libavcodec/avcodec.pxd | 17 ++--- include/libavformat/avformat.pxd | 2 + include/libavutil/avutil.pxd | 1 + tests/test_codec_context.py | 11 ---- tests/test_containerformat.py | 29 ++++++-- tests/test_filters.py | 15 ++--- tests/test_subtitles.py | 18 ++++- 31 files changed, 278 insertions(+), 175 deletions(-) diff --git a/av/__init__.py b/av/__init__.py index b4705dc69..68b9fed09 100644 --- a/av/__init__.py +++ b/av/__init__.py @@ -41,3 +41,15 @@ # Backwards compatibility AVError = FFmpegError # noqa: F405 + + +def get_include() -> str: + """ + Returns the path to the `include` folder to be used when building extensions to av. + """ + # Installed package + include_path = os.path.join(os.path.dirname(__file__), "include") + if os.path.exists(include_path): + return include_path + # Running from source directory + return os.path.join(os.path.dirname(__file__), os.pardir, "include") diff --git a/av/about.py b/av/about.py index 59c3246f6..e4400ae60 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "12.2.0" +__version__ = "12.3.0" diff --git a/av/audio/frame.pyx b/av/audio/frame.pyx index ac3638230..1225f8de2 100644 --- a/av/audio/frame.pyx +++ b/av/audio/frame.pyx @@ -94,7 +94,7 @@ cdef class AudioFrame(Frame): def __repr__(self): return ( - f" None: ... diff --git a/av/codec/context.pyx b/av/codec/context.pyx index f8c1263c6..dc71bb2a5 100644 --- a/av/codec/context.pyx +++ b/av/codec/context.pyx @@ -25,7 +25,6 @@ cdef CodecContext wrap_codec_context(lib.AVCodecContext *c_ctx, const lib.AVCode cdef CodecContext py_ctx - # TODO: This. if c_ctx.codec_type == lib.AVMEDIA_TYPE_VIDEO: from av.video.codeccontext import VideoCodecContext py_ctx = VideoCodecContext(_cinit_sentinel) @@ -45,69 +44,46 @@ cdef CodecContext wrap_codec_context(lib.AVCodecContext *c_ctx, const lib.AVCode ThreadType = define_enum("ThreadType", __name__, ( ("NONE", 0), - ("FRAME", lib.FF_THREAD_FRAME, - """Decode more than one frame at once"""), - ("SLICE", lib.FF_THREAD_SLICE, - """Decode more than one part of a single frame at once"""), - ("AUTO", lib.FF_THREAD_SLICE | lib.FF_THREAD_FRAME, - """Decode using both FRAME and SLICE methods."""), + ("FRAME", lib.FF_THREAD_FRAME, "Decode more than one frame at once"), + ("SLICE", lib.FF_THREAD_SLICE, "Decode more than one part of a single frame at once"), + ("AUTO", lib.FF_THREAD_SLICE | lib.FF_THREAD_FRAME, "Decode using both FRAME and SLICE methods."), ), is_flags=True) SkipType = define_enum("SkipType", __name__, ( - ("NONE", lib.AVDISCARD_NONE, - """Discard nothing"""), - ("DEFAULT", lib.AVDISCARD_DEFAULT, - """Discard useless packets like 0 size packets in AVI"""), - ("NONREF", lib.AVDISCARD_NONREF, - """Discard all non reference"""), - ("BIDIR", lib.AVDISCARD_BIDIR, - """Discard all bidirectional frames"""), - ("NONINTRA", lib.AVDISCARD_NONINTRA, - """Discard all non intra frames"""), - ("NONKEY", lib.AVDISCARD_NONKEY, - """Discard all frames except keyframes"""), - ("ALL", lib.AVDISCARD_ALL, - """Discard all"""), + ("NONE", lib.AVDISCARD_NONE, "Discard nothing"), + ("DEFAULT", lib.AVDISCARD_DEFAULT, "Discard useless packets like 0 size packets in AVI"), + ("NONREF", lib.AVDISCARD_NONREF, "Discard all non reference"), + ("BIDIR", lib.AVDISCARD_BIDIR, "Discard all bidirectional frames"), + ("NONINTRA", lib.AVDISCARD_NONINTRA, "Discard all non intra frames"), + ("NONKEY", lib.AVDISCARD_NONKEY, "Discard all frames except keyframes"), + ("ALL", lib.AVDISCARD_ALL, "Discard all"), )) Flags = define_enum("Flags", __name__, ( ("NONE", 0), ("UNALIGNED", lib.AV_CODEC_FLAG_UNALIGNED, - """Allow decoders to produce frames with data planes that are not aligned - to CPU requirements (e.g. due to cropping)."""), - ("QSCALE", lib.AV_CODEC_FLAG_QSCALE, - """Use fixed qscale."""), - ("4MV", lib.AV_CODEC_FLAG_4MV, - """4 MV per MB allowed / advanced prediction for H.263."""), - ("OUTPUT_CORRUPT", lib.AV_CODEC_FLAG_OUTPUT_CORRUPT, - """Output even those frames that might be corrupted."""), - ("QPEL", lib.AV_CODEC_FLAG_QPEL, - """Use qpel MC."""), + "Allow decoders to produce frames with data planes that are not aligned to CPU requirements (e.g. due to cropping)." + ), + ("QSCALE", lib.AV_CODEC_FLAG_QSCALE, "Use fixed qscale."), + ("4MV", lib.AV_CODEC_FLAG_4MV, "4 MV per MB allowed / advanced prediction for H.263."), + ("OUTPUT_CORRUPT", lib.AV_CODEC_FLAG_OUTPUT_CORRUPT, "Output even those frames that might be corrupted."), + ("QPEL", lib.AV_CODEC_FLAG_QPEL, "Use qpel MC."), ("DROPCHANGED", 1 << 5, - """Don't output frames whose parameters differ from first - decoded frame in stream."""), - ("PASS1", lib.AV_CODEC_FLAG_PASS1, - """Use internal 2pass ratecontrol in first pass mode."""), - ("PASS2", lib.AV_CODEC_FLAG_PASS2, - """Use internal 2pass ratecontrol in second pass mode."""), - ("LOOP_FILTER", lib.AV_CODEC_FLAG_LOOP_FILTER, - """loop filter."""), - ("GRAY", lib.AV_CODEC_FLAG_GRAY, - """Only decode/encode grayscale."""), - ("PSNR", lib.AV_CODEC_FLAG_PSNR, - """error[?] variables will be set during encoding."""), - ("INTERLACED_DCT", lib.AV_CODEC_FLAG_INTERLACED_DCT, - """Use interlaced DCT."""), - ("LOW_DELAY", lib.AV_CODEC_FLAG_LOW_DELAY, - """Force low delay."""), + "Don't output frames whose parameters differ from first decoded frame in stream." + ), + ("PASS1", lib.AV_CODEC_FLAG_PASS1, "Use internal 2pass ratecontrol in first pass mode."), + ("PASS2", lib.AV_CODEC_FLAG_PASS2, "Use internal 2pass ratecontrol in second pass mode."), + ("LOOP_FILTER", lib.AV_CODEC_FLAG_LOOP_FILTER, "loop filter."), + ("GRAY", lib.AV_CODEC_FLAG_GRAY, "Only decode/encode grayscale."), + ("PSNR", lib.AV_CODEC_FLAG_PSNR, "error[?] variables will be set during encoding."), + ("INTERLACED_DCT", lib.AV_CODEC_FLAG_INTERLACED_DCT, "Use interlaced DCT."), + ("LOW_DELAY", lib.AV_CODEC_FLAG_LOW_DELAY, "Force low delay."), ("GLOBAL_HEADER", lib.AV_CODEC_FLAG_GLOBAL_HEADER, - """Place global headers in extradata instead of every keyframe."""), - ("BITEXACT", lib.AV_CODEC_FLAG_BITEXACT, - """Use only bitexact stuff (except (I)DCT)."""), - ("AC_PRED", lib.AV_CODEC_FLAG_AC_PRED, - """H.263 advanced intra coding / MPEG-4 AC prediction"""), - ("INTERLACED_ME", lib.AV_CODEC_FLAG_INTERLACED_ME, - """Interlaced motion estimation"""), + "Place global headers in extradata instead of every keyframe." + ), + ("BITEXACT", lib.AV_CODEC_FLAG_BITEXACT, "Use only bitexact stuff (except (I)DCT)."), + ("AC_PRED", lib.AV_CODEC_FLAG_AC_PRED, "H.263 advanced intra coding / MPEG-4 AC prediction"), + ("INTERLACED_ME", lib.AV_CODEC_FLAG_INTERLACED_ME, "Interlaced motion estimation"), ("CLOSED_GOP", lib.AV_CODEC_FLAG_CLOSED_GOP), ), is_flags=True) @@ -168,11 +144,7 @@ cdef class CodecContext: def _set_flags(self, value): self.ptr.flags = value - flags = Flags.property( - _get_flags, - _set_flags, - """Flag property of :class:`.Flags`.""" - ) + flags = Flags.property(_get_flags, _set_flags, "Flag property of :class:`.Flags`.") unaligned = flags.flag_property("UNALIGNED") qscale = flags.flag_property("QSCALE") @@ -199,12 +171,7 @@ cdef class CodecContext: def _set_flags2(self, value): self.ptr.flags2 = value - flags2 = Flags2.property( - _get_flags2, - _set_flags2, - """Flag property of :class:`.Flags2`.""" - ) - + flags2 = Flags2.property(_get_flags2, _set_flags2, "Flag property of :class:`.Flags2`.") fast = flags2.flag_property("FAST") no_output = flags2.flag_property("NO_OUTPUT") local_header = flags2.flag_property("LOCAL_HEADER") @@ -261,14 +228,6 @@ cdef class CodecContext: raise ValueError("CodecContext is already open.") return - # We might pass partial frames. - # TODO: What is this for?! This is causing problems with raw decoding - # as the internal parser doesn't seem to see a frame until it sees - # the next one. - # if self.codec.ptr.capabilities & lib.CODEC_CAP_TRUNCATED: - # self.ptr.flags |= lib.CODEC_FLAG_TRUNCATED - - # TODO: Do this better. cdef _Dictionary options = Dictionary() options.update(self.options or {}) @@ -377,7 +336,6 @@ cdef class CodecContext: in_size -= consumed if not in_size: - # Aaaand now we're done. break return packets @@ -522,7 +480,6 @@ cdef class CodecContext: lib.avcodec_flush_buffers(self.ptr) cdef _setup_decoded_frame(self, Frame frame, Packet packet): - # Propagate our manual times. # While decoding, frame times are in stream time_base, which PyAV # is carrying around. @@ -531,8 +488,6 @@ cdef class CodecContext: if packet is not None: frame._time_base = packet._time_base - frame.index = self.ptr.frame_number - 1 - @property def name(self): return self.codec.name diff --git a/av/container/core.pyx b/av/container/core.pyx index 548fe4fc2..df1b25939 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -365,7 +365,7 @@ def open( :param int buffer_size: Size of buffer for Python input/output operations in bytes. Honored only when ``file`` is a file-like object. Defaults to 32768 (32k). :param timeout: How many seconds to wait for data before giving up, as a float, or a - :ref:`(open timeout, read timeout) ` tuple. + :ref:`(open timeout, read timeout)` tuple. :param callable io_open: Custom I/O callable for opening files/streams. This option is intended for formats that need to open additional file-like objects to ``file`` using custom I/O. diff --git a/av/container/output.pyi b/av/container/output.pyi index 6df32c897..9e46b413a 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -19,3 +19,11 @@ class OutputContainer(Container): def close(self) -> None: ... def mux(self, packets: Packet | Sequence[Packet]) -> None: ... def mux_one(self, packet: Packet) -> None: ... + @property + def default_video_codec(self) -> str: ... + @property + def default_audio_codec(self) -> str: ... + @property + def default_subtitle_codec(self) -> str: ... + @property + def supported_codecs(self) -> set[str]: ... diff --git a/av/container/output.pyx b/av/container/output.pyx index 55e8b5006..c4e39263d 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -1,6 +1,8 @@ import logging import os +cimport libav as lib + from av.codec.codec cimport Codec from av.codec.context cimport CodecContext, wrap_codec_context from av.container.streams cimport StreamContainer @@ -192,6 +194,47 @@ cdef class OutputContainer(Container): self._started = True + @property + def supported_codecs(self): + """ + Returns a set of all codecs this format supports. + """ + result = set() + cdef const lib.AVCodec *codec = NULL + cdef void *opaque = NULL + + while True: + codec = lib.av_codec_iterate(&opaque) + if codec == NULL: + break + + if lib.avformat_query_codec(self.ptr.oformat, codec.id, lib.FF_COMPLIANCE_NORMAL) == 1: + result.add(codec.name) + + return result + + + @property + def default_video_codec(self): + """ + Returns the default video codec this container recommends. + """ + return lib.avcodec_get_name(self.format.optr.video_codec) + + @property + def default_audio_codec(self): + """ + Returns the default audio codec this container recommends. + """ + return lib.avcodec_get_name(self.format.optr.audio_codec) + + @property + def default_subtitle_codec(self): + """ + Returns the default subtitle codec this container recommends. + """ + return lib.avcodec_get_name(self.format.optr.subtitle_codec) + def close(self): for stream in self.streams: if stream.codec_context: diff --git a/av/filter/graph.pyi b/av/filter/graph.pyi index 625364f35..337be2bee 100644 --- a/av/filter/graph.pyi +++ b/av/filter/graph.pyi @@ -17,6 +17,7 @@ class Graph: def __init__(self) -> None: ... def configure(self, auto_buffer: bool = True, force: bool = False) -> None: ... + def link_nodes(self, *nodes: FilterContext) -> Graph: ... def add( self, filter: str | Filter, args: Any = None, **kwargs: str ) -> FilterContext: ... diff --git a/av/filter/graph.pyx b/av/filter/graph.pyx index e2d95e4cd..e0effd12f 100644 --- a/av/filter/graph.pyx +++ b/av/filter/graph.pyx @@ -45,6 +45,13 @@ cdef class Graph: # We get auto-inserted stuff here. self._auto_register() + def link_nodes(self, *nodes): + """ + Links nodes together for simple filter graphs. + """ + for c, n in zip(nodes, nodes[1:]): + c.link_to(n) + return self def add(self, filter, args=None, **kwargs): cdef Filter cy_filter @@ -68,7 +75,7 @@ cdef class Graph: # There might have been automatic contexts added (e.g. resamplers, # fifos, and scalers). It is more likely to see them after the graph - # is configured, but we wan't to be safe. + # is configured, but we want to be safe. self._auto_register() return ctx diff --git a/av/format.pyi b/av/format.pyi index d2aef4764..c3506ed51 100644 --- a/av/format.pyi +++ b/av/format.pyi @@ -1,5 +1,7 @@ __all__ = ("ContainerFormat", "formats_available") +from typing import Literal + from .enum import EnumFlag class Flags(EnumFlag): @@ -22,10 +24,12 @@ class Flags(EnumFlag): SEEK_TO_PTS: int class ContainerFormat: + def __init__(self, name: str, mode: Literal["r", "w"] | None = None) -> None: ... name: str long_name: str is_input: bool is_output: bool + extensions: set[str] # flags no_file: int diff --git a/av/frame.pxd b/av/frame.pxd index 34c302536..6d7214b7c 100644 --- a/av/frame.pxd +++ b/av/frame.pxd @@ -5,17 +5,10 @@ from av.sidedata.sidedata cimport _SideDataContainer cdef class Frame: - cdef lib.AVFrame *ptr - # We define our own time. cdef lib.AVRational _time_base cdef _rebase_time(self, lib.AVRational) - cdef _SideDataContainer _side_data - - cdef readonly int index - cdef _copy_internal_attributes(self, Frame source, bint data_layout=?) - cdef _init_user_attributes(self) diff --git a/av/frame.pyx b/av/frame.pyx index 489eef540..7fa8a701e 100644 --- a/av/frame.pyx +++ b/av/frame.pyx @@ -22,11 +22,10 @@ cdef class Frame: lib.av_frame_free(&self.ptr) def __repr__(self): - return f"av.{self.__class__.__name__} #{self.index} pts={self.pts} at 0x{id(self):x}>" + return f"av.{self.__class__.__name__} pts={self.pts} at 0x{id(self):x}>" cdef _copy_internal_attributes(self, Frame source, bint data_layout=True): """Mimic another frame.""" - self.index = source.index self._time_base = source._time_base lib.av_frame_copy_props(self.ptr, source.ptr) if data_layout: diff --git a/av/logging.pyi b/av/logging.pyi index 1db5e4a3b..8c32de77d 100644 --- a/av/logging.pyi +++ b/av/logging.pyi @@ -13,6 +13,7 @@ CRITICAL: int def adapt_level(level: int) -> int: ... def get_level() -> int | None: ... def set_level(level: int | None) -> None: ... +def set_libav_level(level: int) -> None: ... def restore_default_callback() -> None: ... def get_skip_repeated() -> bool: ... def set_skip_repeated(v: bool) -> None: ... diff --git a/av/logging.pyx b/av/logging.pyx index 1006d094c..6b6858db6 100644 --- a/av/logging.pyx +++ b/av/logging.pyx @@ -119,6 +119,17 @@ def set_level(level): raise ValueError("level must be: int | None") +def set_libav_level(level): + """Set libav's log level. It can be set to constants available in this + module: ``PANIC``, ``FATAL``, ``ERROR``, ``WARNING``, ``INFO``, + ``VERBOSE``, ``DEBUG``. + + When PyAV logging is disabled, setting this will change the level of + the logs printed to the terminal. + """ + lib.av_log_set_level(level) + + def restore_default_callback(): """Revert back to FFmpeg's log callback, which prints to the terminal.""" lib.av_log_set_callback(lib.av_log_default_callback) diff --git a/av/subtitles/codeccontext.pyx b/av/subtitles/codeccontext.pyx index 10e8a6009..c0712c92c 100644 --- a/av/subtitles/codeccontext.pyx +++ b/av/subtitles/codeccontext.pyx @@ -7,13 +7,14 @@ from av.subtitles.subtitle cimport SubtitleProxy, SubtitleSet cdef class SubtitleCodecContext(CodecContext): cdef _send_packet_and_recv(self, Packet packet): + if packet is None: + raise RuntimeError("packet cannot be None") + cdef SubtitleProxy proxy = SubtitleProxy() cdef int got_frame = 0 err_check( - lib.avcodec_decode_subtitle2( - self.ptr, &proxy.struct, &got_frame, packet.ptr if packet else NULL - ) + lib.avcodec_decode_subtitle2(self.ptr, &proxy.struct, &got_frame, packet.ptr) ) if got_frame: diff --git a/av/subtitles/stream.pxd b/av/subtitles/stream.pxd index e21dceb23..745032af9 100644 --- a/av/subtitles/stream.pxd +++ b/av/subtitles/stream.pxd @@ -1,5 +1,6 @@ +from av.packet cimport Packet from av.stream cimport Stream cdef class SubtitleStream(Stream): - pass + cpdef decode(self, Packet packet=?) diff --git a/av/subtitles/stream.pyi b/av/subtitles/stream.pyi index 38f4ae6cb..cb1ac34a2 100644 --- a/av/subtitles/stream.pyi +++ b/av/subtitles/stream.pyi @@ -1,3 +1,6 @@ +from av.packet import Packet from av.stream import Stream +from av.subtitles.subtitle import SubtitleSet -class SubtitleStream(Stream): ... +class SubtitleStream(Stream): + def decode(self, packet: Packet | None = None) -> list[SubtitleSet]: ... diff --git a/av/subtitles/stream.pyx b/av/subtitles/stream.pyx index 1deed5867..9f90b9871 100644 --- a/av/subtitles/stream.pyx +++ b/av/subtitles/stream.pyx @@ -1,6 +1,23 @@ +from av.packet cimport Packet +from av.stream cimport Stream + + cdef class SubtitleStream(Stream): """ A :class:`SubtitleStream` can contain many :class:`SubtitleSet` objects accessible via decoding. """ def __getattr__(self, name): return getattr(self.codec_context, name) + + cpdef decode(self, Packet packet=None): + """ + Decode a :class:`.Packet` and return a list of :class:`.SubtitleSet`. + + :rtype: list[SubtitleSet] + + .. seealso:: This is a passthrough to :meth:`.CodecContext.decode`. + """ + if not packet: + packet = Packet() + + return self.codec_context.decode(packet) diff --git a/av/subtitles/subtitle.pyi b/av/subtitles/subtitle.pyi index 2ac9195ec..2a35d0a55 100644 --- a/av/subtitles/subtitle.pyi +++ b/av/subtitles/subtitle.pyi @@ -27,10 +27,11 @@ class BitmapSubtitlePlane: index: int buffer_size: int -class TextSubtitle(Subtitle): - type: Literal[b"text"] - text: bytes - class AssSubtitle(Subtitle): - type: Literal[b"ass"] - ass: bytes + type: Literal[b"ass", b"text"] + @property + def ass(self) -> bytes: ... + @property + def dialogue(self) -> bytes: ... + @property + def text(self) -> bytes: ... diff --git a/av/subtitles/subtitle.pyx b/av/subtitles/subtitle.pyx index 7ced655f6..373bb529b 100644 --- a/av/subtitles/subtitle.pyx +++ b/av/subtitles/subtitle.pyx @@ -55,9 +55,7 @@ cdef Subtitle build_subtitle(SubtitleSet subtitle, int index): if ptr.type == lib.SUBTITLE_BITMAP: return BitmapSubtitle(subtitle, index) - elif ptr.type == lib.SUBTITLE_TEXT: - return TextSubtitle(subtitle, index) - elif ptr.type == lib.SUBTITLE_ASS: + elif ptr.type == lib.SUBTITLE_ASS or ptr.type == lib.SUBTITLE_TEXT: return AssSubtitle(subtitle, index) else: raise ValueError("unknown subtitle type %r" % ptr.type) @@ -141,7 +139,10 @@ cdef class BitmapSubtitlePlane: PyBuffer_FillInfo(view, self, self._buffer, self.buffer_size, 0, flags) -cdef class TextSubtitle(Subtitle): +cdef class AssSubtitle(Subtitle): + """ + Represents an ASS/Text subtitle format, as opposed to a bitmap Subtitle format. + """ def __repr__(self): return ( f"<{self.__class__.__module__}.{self.__class__.__name__} " @@ -149,21 +150,55 @@ cdef class TextSubtitle(Subtitle): ) @property - def text(self): - if self.ptr.text is not NULL: - return PyBytes_FromString(self.ptr.text) + def ass(self): + """ + Returns the subtitle in the ASS/SSA format. Used by the vast majority of subtitle formats. + """ + if self.ptr.ass is not NULL: + return PyBytes_FromString(self.ptr.ass) return b"" - -cdef class AssSubtitle(Subtitle): - def __repr__(self): - return ( - f"<{self.__class__.__module__}.{self.__class__.__name__} " - f"{self.ass!r} at 0x{id(self):x}>" - ) + @property + def dialogue(self): + """ + Extract the dialogue from the ass format. Strip comments. + """ + comma_count = 0 + i = 0 + cdef bytes ass_text = self.ass + cdef bytes result = b"" + + while comma_count < 8 and i < len(ass_text): + if bytes([ass_text[i]]) == b",": + comma_count += 1 + i += 1 + + state = False + while i < len(ass_text): + char = bytes([ass_text[i]]) + next_char = b"" if i + 1 >= len(ass_text) else bytes([ass_text[i + 1]]) + + if char == b"\\" and next_char == b"N": + result += b"\n" + i += 2 + continue + + if not state: + if char == b"{" and next_char != b"\\": + state = True + else: + result += char + elif char == b"}": + state = False + i += 1 + + return result @property - def ass(self): - if self.ptr.ass is not NULL: - return PyBytes_FromString(self.ptr.ass) + def text(self): + """ + Rarely used attribute. You're probably looking for dialogue. + """ + if self.ptr.text is not NULL: + return PyBytes_FromString(self.ptr.text) return b"" diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 6ff982491..e64eb8573 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -124,8 +124,8 @@ cdef class VideoFrame(Frame): def __repr__(self): return ( - f"" + f"" ) @property diff --git a/av/video/reformatter.pyx b/av/video/reformatter.pyx index 11ee1bf74..624454f0c 100644 --- a/av/video/reformatter.pyx +++ b/av/video/reformatter.pyx @@ -76,9 +76,9 @@ cdef class VideoReformatter: :type dst_colorspace: :class:`Colorspace` or ``str`` :param interpolation: The interpolation method to use, or ``None`` for ``BILINEAR``. :type interpolation: :class:`Interpolation` or ``str`` - :param src_color_range: Current color range, or ``None`` for the frame color range. + :param src_color_range: Current color range, or ``None`` for the ``UNSPECIFIED``. :type src_color_range: :class:`color range` or ``str`` - :param dst_color_range: Desired color range, or ``None`` for the frame color range. + :param dst_color_range: Desired color range, or ``None`` for the ``UNSPECIFIED``. :type dst_color_range: :class:`color range` or ``str`` """ @@ -110,6 +110,10 @@ cdef class VideoReformatter: if frame.ptr.format < 0: raise ValueError("Frame does not have format set.") + # The definition of color range in pixfmt.h and swscale.h is different. + src_color_range = 1 if src_color_range == ColorRange.JPEG.value else 0 + dst_color_range = 1 if dst_color_range == ColorRange.JPEG.value else 0 + cdef lib.AVPixelFormat src_format = frame.ptr.format # Shortcut! diff --git a/docs/api/subtitles.rst b/docs/api/subtitles.rst index 19d75621c..949896d6d 100644 --- a/docs/api/subtitles.rst +++ b/docs/api/subtitles.rst @@ -15,14 +15,11 @@ Subtitles .. autoclass:: Subtitle :members: - .. autoclass:: BitmapSubtitle - :members: - - .. autoclass:: BitmapSubtitlePlane + .. autoclass:: AssSubtitle :members: - .. autoclass:: TextSubtitle + .. autoclass:: BitmapSubtitle :members: - .. autoclass:: AssSubtitle + .. autoclass:: BitmapSubtitlePlane :members: diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 16d88a473..d4335b2a9 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -1,15 +1,14 @@ from libc.stdint cimport int8_t, int64_t, uint16_t, uint32_t +cdef extern from "libavcodec/codec.h": + struct AVCodecTag: + pass + +cdef extern from "libavcodec/codec_id.h": + AVCodecID av_codec_get_id(const AVCodecTag *const *tags, uint32_t tag) + cdef extern from "libavcodec/avcodec.h" nogil: - """ - // AV_FRAME_DATA_SEI_UNREGISTERED available since version 56.54.100 of libavutil (FFmpeg >= 4.4) - #define HAS_AV_FRAME_DATA_SEI_UNREGISTERED (LIBAVUTIL_VERSION_INT >= 3683940) - - #if !HAS_AV_FRAME_DATA_SEI_UNREGISTERED - #define AV_FRAME_DATA_SEI_UNREGISTERED -1 - #endif - """ cdef set pyav_get_available_codecs() cdef int avcodec_version() @@ -174,8 +173,6 @@ cdef extern from "libavcodec/avcodec.h" nogil: int global_quality int compression_level - int frame_number - int qmin int qmax int rc_max_rate diff --git a/include/libavformat/avformat.pxd b/include/libavformat/avformat.pxd index 9d9061cc2..f51ba269b 100644 --- a/include/libavformat/avformat.pxd +++ b/include/libavformat/avformat.pxd @@ -115,6 +115,8 @@ cdef extern from "libavformat/avformat.h" nogil: # const AVCodecTag* const *codec_tag const AVClass *priv_class + int avformat_query_codec(const AVOutputFormat *oformat, AVCodecID codec_id, int std_compliance) + # AVInputFormat.flags and AVOutputFormat.flags cdef enum: AVFMT_NOFILE diff --git a/include/libavutil/avutil.pxd b/include/libavutil/avutil.pxd index f9af7a7b0..b4184d0de 100644 --- a/include/libavutil/avutil.pxd +++ b/include/libavutil/avutil.pxd @@ -399,3 +399,4 @@ cdef extern from "libavutil/log.h" nogil: ctypedef void(*av_log_callback)(void *, int, const char *, va_list) void av_log_default_callback(void *, int, const char *, va_list) void av_log_set_callback (av_log_callback callback) + void av_log_set_level(int level) diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index d30401f4e..59a10a297 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -96,17 +96,6 @@ def test_decoder_gop_size(self): "Using VideoCodecContext.gop_size for decoders is deprecated.", ) - def test_frame_index(self): - container = av.open(fate_suite("h264/interlaced_crop.mp4")) - stream = container.streams[0] - for frame in container.decode(stream): - with warnings.catch_warnings(record=True) as captured: - self.assertIsInstance(frame.index, int) - self.assertEqual( - captured[0].message.args[0], - "Using `frame.index` is deprecated.", - ) - def test_decoder_timebase(self): ctx = av.codec.Codec("h264", "r").create() diff --git a/tests/test_containerformat.py b/tests/test_containerformat.py index dea3d29dc..5b6d31c35 100644 --- a/tests/test_containerformat.py +++ b/tests/test_containerformat.py @@ -1,10 +1,16 @@ -from av import ContainerFormat, formats_available +from av import ContainerFormat, formats_available, open from .common import TestCase class TestContainerFormats(TestCase): - def test_matroska(self): + def test_matroska(self) -> None: + with open("test.mkv", "w") as container: + self.assertNotEqual(container.default_video_codec, "none") + self.assertNotEqual(container.default_audio_codec, "none") + self.assertEqual(container.default_subtitle_codec, "ass") + self.assertIn("ass", container.supported_codecs) + fmt = ContainerFormat("matroska") self.assertTrue(fmt.is_input) self.assertTrue(fmt.is_output) @@ -13,7 +19,13 @@ def test_matroska(self): self.assertIn("mkv", fmt.extensions) self.assertFalse(fmt.no_file) - def test_mov(self): + def test_mov(self) -> None: + with open("test.mov", "w") as container: + self.assertNotEqual(container.default_video_codec, "none") + self.assertNotEqual(container.default_audio_codec, "none") + self.assertEqual(container.default_subtitle_codec, "none") + self.assertIn("h264", container.supported_codecs) + fmt = ContainerFormat("mov") self.assertTrue(fmt.is_input) self.assertTrue(fmt.is_output) @@ -22,7 +34,14 @@ def test_mov(self): self.assertIn("mov", fmt.extensions) self.assertFalse(fmt.no_file) - def test_stream_segment(self): + def test_gif(self) -> None: + with open("test.gif", "w") as container: + self.assertEqual(container.default_video_codec, "gif") + self.assertEqual(container.default_audio_codec, "none") + self.assertEqual(container.default_subtitle_codec, "none") + self.assertIn("gif", container.supported_codecs) + + def test_stream_segment(self) -> None: # This format goes by two names, check both. fmt = ContainerFormat("stream_segment") self.assertFalse(fmt.is_input) @@ -40,5 +59,5 @@ def test_stream_segment(self): self.assertEqual(fmt.extensions, set()) self.assertTrue(fmt.no_file) - def test_formats_available(self): + def test_formats_available(self) -> None: self.assertTrue(formats_available) diff --git a/tests/test_filters.py b/tests/test_filters.py index 3d9e6e9d0..1f3b8ae99 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -128,14 +128,9 @@ def test_audio_buffer_sink(self): if e.errno != errno.EAGAIN: raise - @staticmethod - def link_nodes(*nodes): - for c, n in zip(nodes, nodes[1:]): - c.link_to(n) - def test_audio_buffer_resample(self): graph = Graph() - self.link_nodes( + graph.link_nodes( graph.add_abuffer( format="fltp", sample_rate=48000, @@ -146,8 +141,7 @@ def test_audio_buffer_resample(self): "aformat", "sample_fmts=s16:sample_rates=44100:channel_layouts=stereo" ), graph.add("abuffersink"), - ) - graph.configure() + ).configure() graph.push( generate_audio_frame( @@ -161,7 +155,7 @@ def test_audio_buffer_resample(self): def test_audio_buffer_volume_filter(self): graph = Graph() - self.link_nodes( + graph.link_nodes( graph.add_abuffer( format="fltp", sample_rate=48000, @@ -170,8 +164,7 @@ def test_audio_buffer_volume_filter(self): ), graph.add("volume", volume="0.5"), graph.add("abuffersink"), - ) - graph.configure() + ).configure() input_frame = generate_audio_frame( 0, input_format="fltp", layout="stereo", sample_rate=48000 diff --git a/tests/test_subtitles.py b/tests/test_subtitles.py index fe4f2ee3d..9a5e9ceb1 100644 --- a/tests/test_subtitles.py +++ b/tests/test_subtitles.py @@ -5,7 +5,7 @@ class TestSubtitle(TestCase): - def test_movtext(self): + def test_movtext(self) -> None: path = fate_suite("sub/MovText_capability_tester.mp4") subs = [] @@ -23,8 +23,12 @@ def test_movtext(self): sub = subset[0] self.assertIsInstance(sub, AssSubtitle) + assert isinstance(sub, AssSubtitle) + self.assertEqual(sub.type, b"ass") + self.assertEqual(sub.text, b"") self.assertEqual(sub.ass, b"0,0,Default,,0,0,0,,- Test 1.\\N- Test 2.") + self.assertEqual(sub.dialogue, b"- Test 1.\n- Test 2.") def test_vobsub(self): path = fate_suite("sub/vobsub.sub") @@ -54,3 +58,15 @@ def test_vobsub(self): bms = sub.planes self.assertEqual(len(bms), 1) self.assertEqual(len(memoryview(bms[0])), 4800) + + def test_subtitle_flush(self) -> None: + path = fate_suite("sub/MovText_capability_tester.mp4") + + subs = [] + with av.open(path) as container: + stream = container.streams.subtitles[0] + for packet in container.demux(stream): + subs.extend(stream.decode(packet)) + subs.extend(stream.decode()) + + self.assertEqual(len(subs), 3)