From 6bdf8edecac83b70000597792190120e74077410 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Thu, 14 Mar 2024 22:06:20 -0400 Subject: [PATCH] Sync with upstream --- .github/workflows/tests.yml | 9 +- av/about.py | 2 +- av/audio/stream.pxd | 6 +- av/audio/stream.pyi | 14 +- av/audio/stream.pyx | 35 ++- av/container/core.pyx | 1 - av/container/input.pyi | 3 +- av/container/input.pyx | 14 +- av/packet.pyi | 3 + av/packet.pyx | 16 +- av/stream.pyi | 2 - av/stream.pyx | 61 ----- av/subtitles/subtitle.pyi | 2 +- av/subtitles/subtitle.pyx | 8 +- av/video/codeccontext.pyi | 10 +- av/video/codeccontext.pyx | 19 ++ av/video/frame.pyi | 5 +- av/video/frame.pyx | 4 +- av/video/reformatter.pyi | 4 +- av/video/stream.pxd | 6 +- av/video/stream.pyi | 31 ++- av/video/stream.pyx | 77 +++++- include/libavcodec/avcodec.pxd | 5 + scratchpad/README | 4 - scratchpad/__init__.py | 3 - scratchpad/audio.py | 96 ------- scratchpad/audio_player.py | 77 ------ scratchpad/average.py | 56 ----- scratchpad/cctx_decode.py | 28 --- scratchpad/cctx_encode.py | 47 ---- scratchpad/container-gc.py | 44 ---- scratchpad/decode.py | 160 ------------ scratchpad/encode.py | 90 ------- scratchpad/encode_frames.py | 39 --- scratchpad/filter_audio.py | 122 --------- scratchpad/frame_seek_example.py | 415 ------------------------------- scratchpad/glproxy.py | 97 -------- scratchpad/graph.py | 18 -- scratchpad/player.py | 101 -------- scratchpad/qtproxy.py | 19 -- scratchpad/remux.py | 65 ----- scratchpad/resource_use.py | 61 ----- scratchpad/save_subtitles.py | 60 ----- scratchpad/seekmany.py | 48 ---- scratchpad/show_frames_opencv.py | 20 -- scratchpad/sidedata.py | 25 -- tests/test_codec_context.py | 25 ++ tests/test_file_probing.py | 14 +- tests/test_packet.py | 41 +++ tests/test_subtitles.py | 2 +- 50 files changed, 291 insertions(+), 1823 deletions(-) delete mode 100644 scratchpad/README delete mode 100644 scratchpad/__init__.py delete mode 100644 scratchpad/audio.py delete mode 100644 scratchpad/audio_player.py delete mode 100644 scratchpad/average.py delete mode 100644 scratchpad/cctx_decode.py delete mode 100644 scratchpad/cctx_encode.py delete mode 100644 scratchpad/container-gc.py delete mode 100644 scratchpad/decode.py delete mode 100644 scratchpad/encode.py delete mode 100644 scratchpad/encode_frames.py delete mode 100644 scratchpad/filter_audio.py delete mode 100644 scratchpad/frame_seek_example.py delete mode 100644 scratchpad/glproxy.py delete mode 100644 scratchpad/graph.py delete mode 100644 scratchpad/player.py delete mode 100644 scratchpad/qtproxy.py delete mode 100644 scratchpad/remux.py delete mode 100644 scratchpad/resource_use.py delete mode 100644 scratchpad/save_subtitles.py delete mode 100644 scratchpad/seekmany.py delete mode 100644 scratchpad/show_frames_opencv.py delete mode 100644 scratchpad/sidedata.py create mode 100644 tests/test_packet.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c8db71f87..0117415ab 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v4 name: Checkout - name: Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: 3.12 @@ -57,7 +57,7 @@ jobs: name: Checkout - name: Python ${{ matrix.config.python }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.config.python }} @@ -72,7 +72,6 @@ jobs: libtheora-dev libvorbis-dev libx264-dev ;; macos-latest) - brew update brew install automake libtool nasm pkg-config shtool wget brew install libass libjpeg libpng libvorbis libvpx opus theora x264 ;; @@ -148,7 +147,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: 3.9 - name: Build source package @@ -182,7 +181,7 @@ jobs: arch: AMD64 steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: 3.9 - name: Set up QEMU diff --git a/av/about.py b/av/about.py index bc5813e71..95f0d76d8 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "12.0.4" +__version__ = "12.0.5" diff --git a/av/audio/stream.pxd b/av/audio/stream.pxd index f8f68c263..8462061f8 100644 --- a/av/audio/stream.pxd +++ b/av/audio/stream.pxd @@ -1,5 +1,9 @@ +from av.packet cimport Packet from av.stream cimport Stream +from .frame cimport AudioFrame + cdef class AudioStream(Stream): - pass + cpdef encode(self, AudioFrame frame=?) + cpdef decode(self, Packet packet=?) diff --git a/av/audio/stream.pyi b/av/audio/stream.pyi index cf4173759..9e972c56d 100644 --- a/av/audio/stream.pyi +++ b/av/audio/stream.pyi @@ -6,11 +6,19 @@ from av.stream import Stream from .codeccontext import AudioCodecContext from .format import AudioFormat from .frame import AudioFrame +from .layout import AudioLayout class AudioStream(Stream): - type: Literal["audio"] - format: AudioFormat codec_context: AudioCodecContext + # From codec context + frame_size: int + sample_rate: int + rate: int + channels: int + channel_layout: int + layout: AudioLayout + format: AudioFormat + type: Literal["audio"] - def encode(self, frame: AudioFrame | None = None) -> list[Packet]: ... # type: ignore[override] + def encode(self, frame: AudioFrame | None = None) -> list[Packet]: ... def decode(self, packet: Packet | None = None) -> list[AudioFrame]: ... diff --git a/av/audio/stream.pyx b/av/audio/stream.pyx index 3c6b76fcf..7185f389d 100644 --- a/av/audio/stream.pyx +++ b/av/audio/stream.pyx @@ -1,7 +1,40 @@ +from av.packet cimport Packet + +from .frame cimport AudioFrame + + cdef class AudioStream(Stream): def __repr__(self): form = self.format.name if self.format else None return ( - f"" ) + + cpdef encode(self, AudioFrame frame=None): + """ + Encode an :class:`.AudioFrame` and return a list of :class:`.Packet`. + + :rtype: list[Packet] + + .. seealso:: This is mostly a passthrough to :meth:`.CodecContext.encode`. + """ + + packets = self.codec_context.encode(frame) + cdef Packet packet + for packet in packets: + packet._stream = self + packet.ptr.stream_index = self.ptr.index + + return packets + + cpdef decode(self, Packet packet=None): + """ + Decode a :class:`.Packet` and return a list of :class:`.AudioFrame`. + + :rtype: list[AudioFrame] + + .. seealso:: This is a passthrough to :meth:`.CodecContext.decode`. + """ + + return self.codec_context.decode(packet) diff --git a/av/container/core.pyx b/av/container/core.pyx index 240db340e..548fe4fc2 100755 --- a/av/container/core.pyx +++ b/av/container/core.pyx @@ -366,7 +366,6 @@ def open( 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. - :type timeout: float or 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/input.pyi b/av/container/input.pyi index 2c1328943..f85eaec46 100644 --- a/av/container/input.pyi +++ b/av/container/input.pyi @@ -1,4 +1,4 @@ -from typing import Any, Iterator, Literal, overload +from typing import Any, Iterator, overload from av.audio.frame import AudioFrame from av.audio.stream import AudioStream @@ -34,7 +34,6 @@ class InputContainer(Container): self, offset: int, *, - whence: Literal["time"] = "time", backward: bool = True, any_frame: bool = False, stream: Stream | VideoStream | AudioStream | None = None, diff --git a/av/container/input.pyx b/av/container/input.pyx index 47cd98c4d..acf02fbab 100644 --- a/av/container/input.pyx +++ b/av/container/input.pyx @@ -208,10 +208,10 @@ cdef class InputContainer(Container): for frame in packet.decode(): yield frame - def seek(self, offset, *, str whence="time", bint backward=True, - bint any_frame=False, Stream stream=None, - bint unsupported_frame_offset=False, - bint unsupported_byte_offset=False): + def seek( + self, offset, *, bint backward=True, bint any_frame=False, Stream stream=None, + bint unsupported_frame_offset=False, bint unsupported_byte_offset=False + ): """seek(offset, *, backward=True, any_frame=False, stream=None) Seek to a (key)frame nearsest to the given timestamp. @@ -252,12 +252,6 @@ cdef class InputContainer(Container): cdef int flags = 0 cdef int ret - # We used to support whence in 'time', 'frame', and 'byte', but later - # realized that FFmpeg doens't implement the frame or byte ones. - # We don't even document this anymore, but do allow 'time' to pass through. - if whence != "time": - raise ValueError("whence != 'time' is no longer supported") - if backward: flags |= lib.AVSEEK_FLAG_BACKWARD if any_frame: diff --git a/av/packet.pyi b/av/packet.pyi index 690dc8374..cca33009c 100644 --- a/av/packet.pyi +++ b/av/packet.pyi @@ -16,6 +16,9 @@ class Packet: duration: int | None is_keyframe: bool is_corrupt: bool + is_discard: bool + is_trusted: bool + is_disposable: bool def __init__(self, input: int | None = None) -> None: ... def decode(self) -> Iterator[SubtitleSet]: ... diff --git a/av/packet.pyx b/av/packet.pyx index 116fab6b7..24fd5581b 100644 --- a/av/packet.pyx +++ b/av/packet.pyx @@ -189,7 +189,8 @@ cdef class Packet(Buffer): self.ptr.flags &= ~(lib.AV_PKT_FLAG_KEY) @property - def is_corrupt(self): return bool(self.ptr.flags & lib.AV_PKT_FLAG_CORRUPT) + def is_corrupt(self): + return bool(self.ptr.flags & lib.AV_PKT_FLAG_CORRUPT) @is_corrupt.setter def is_corrupt(self, v): @@ -197,3 +198,16 @@ cdef class Packet(Buffer): self.ptr.flags |= lib.AV_PKT_FLAG_CORRUPT else: self.ptr.flags &= ~(lib.AV_PKT_FLAG_CORRUPT) + + @property + def is_discard(self): + return bool(self.ptr.flags & lib.AV_PKT_FLAG_DISCARD) + + @property + def is_trusted(self): + return bool(self.ptr.flags & lib.AV_PKT_FLAG_TRUSTED) + + @property + def is_disposable(self): + return bool(self.ptr.flags & lib.AV_PKT_FLAG_DISPOSABLE) + diff --git a/av/stream.pyi b/av/stream.pyi index ffe4f8722..4c4c05030 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -31,5 +31,3 @@ class Stream: frames: int language: str | None type: Literal["video", "audio", "data", "subtitle", "attachment"] - - def encode(self, frame: Frame | None = None) -> list[Packet]: ... diff --git a/av/stream.pyx b/av/stream.pyx index 0ed3e50df..1b5e967dd 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -146,25 +146,6 @@ cdef class Stream: # Lets just copy what we want. err_check(lib.avcodec_parameters_from_context(self.ptr.codecpar, self.codec_context.ptr)) - def encode(self, frame=None): - """ - Encode an :class:`.AudioFrame` or :class:`.VideoFrame` and return a list - of :class:`.Packet`. - - :return: :class:`list` of :class:`.Packet`. - - .. seealso:: This is mostly a passthrough to :meth:`.CodecContext.encode`. - """ - if self.codec_context is None: - raise RuntimeError("Stream.encode requires a valid CodecContext") - - packets = self.codec_context.encode(frame) - cdef Packet packet - for packet in packets: - packet._stream = self - packet.ptr.stream_index = self.ptr.index - return packets - cdef _get_side_data(self, lib.AVStream *stream): # Get DISPLAYMATRIX SideData from a lib.AVStream object. # Returns: tuple[int, dict[str, Any]] @@ -236,48 +217,6 @@ cdef class Stream: """ to_avrational(value, &self.ptr.time_base) - @property - def average_rate(self): - """ - The average frame rate of this video stream. - - This is calculated when the file is opened by looking at the first - few frames and averaging their rate. - - :type: :class:`~fractions.Fraction` or ``None`` - - - """ - return avrational_to_fraction(&self.ptr.avg_frame_rate) - - @property - def base_rate(self): - """ - The base frame rate of this stream. - - This is calculated as the lowest framerate at which the timestamps of - frames can be represented accurately. See :ffmpeg:`AVStream.r_frame_rate` - for more. - - :type: :class:`~fractions.Fraction` or ``None`` - - """ - return avrational_to_fraction(&self.ptr.r_frame_rate) - - @property - def guessed_rate(self): - """The guessed frame rate of this stream. - - This is a wrapper around :ffmpeg:`av_guess_frame_rate`, and uses multiple - heuristics to decide what is "the" frame rate. - - :type: :class:`~fractions.Fraction` or ``None`` - - """ - # The two NULL arguments aren't used in FFmpeg >= 4.0 - cdef lib.AVRational val = lib.av_guess_frame_rate(NULL, self.ptr, NULL) - return avrational_to_fraction(&val) - @property def start_time(self): """ diff --git a/av/subtitles/subtitle.pyi b/av/subtitles/subtitle.pyi index 0363c2399..6dcd16e8d 100644 --- a/av/subtitles/subtitle.pyi +++ b/av/subtitles/subtitle.pyi @@ -33,4 +33,4 @@ class TextSubtitle(Subtitle): class AssSubtitle(Subtitle): type: Literal[b"ass"] - ass: str + ass: bytes diff --git a/av/subtitles/subtitle.pyx b/av/subtitles/subtitle.pyx index 5b2fc8a4a..d8d45e3fc 100644 --- a/av/subtitles/subtitle.pyx +++ b/av/subtitles/subtitle.pyx @@ -1,6 +1,10 @@ from cpython cimport PyBuffer_FillInfo +cdef extern from "Python.h": + bytes PyBytes_FromString(char*) + + cdef class SubtitleProxy: def __dealloc__(self): lib.avsubtitle_free(&self.struct) @@ -153,4 +157,6 @@ cdef class AssSubtitle(Subtitle): @property def ass(self): - return self.ptr.ass + if self.ptr.ass is not NULL: + return PyBytes_FromString(self.ptr.ass) + return b"" diff --git a/av/video/codeccontext.pyi b/av/video/codeccontext.pyi index a749ad3f9..4576f5155 100644 --- a/av/video/codeccontext.pyi +++ b/av/video/codeccontext.pyi @@ -4,18 +4,20 @@ from typing import Iterator, Literal from av.codec.context import CodecContext from av.packet import Packet +from .format import VideoFormat from .frame import VideoFrame class VideoCodecContext(CodecContext): + format: VideoFormat width: int height: int bits_per_codec_sample: int - pix_fmt: str + pix_fmt: str | None framerate: Fraction rate: Fraction gop_size: int - sample_aspect_ratio: Fraction - display_aspect_ratio: Fraction + sample_aspect_ratio: Fraction | None + display_aspect_ratio: Fraction | None has_b_frames: bool coded_width: int coded_height: int @@ -23,8 +25,8 @@ class VideoCodecContext(CodecContext): color_primaries: int color_trc: int colorspace: int - type: Literal["video"] + def encode(self, frame: VideoFrame | None = None) -> list[Packet]: ... def encode_lazy(self, frame: VideoFrame | None = None) -> Iterator[Packet]: ... def decode(self, packet: Packet | None = None) -> list[VideoFrame]: ... diff --git a/av/video/codeccontext.pyx b/av/video/codeccontext.pyx index 2e101d935..5ef49ee4c 100644 --- a/av/video/codeccontext.pyx +++ b/av/video/codeccontext.pyx @@ -97,6 +97,25 @@ cdef class VideoCodecContext(CodecContext): self.ptr.height = value self._build_format() + @property + def bits_per_coded_sample(self): + """ + The number of bits per sample in the codedwords. It's mandatory for this to be set for some formats to decode properly. + + Wraps :ffmpeg:`AVCodecContext.bits_per_coded_sample`. + + :type: int + """ + return self.ptr.bits_per_coded_sample + + @bits_per_coded_sample.setter + def bits_per_coded_sample(self, int value): + if self.is_encoder: + raise ValueError("Not supported for encoders") + + self.ptr.bits_per_coded_sample = value + self._build_format() + @property def pix_fmt(self): """ diff --git a/av/video/frame.pyi b/av/video/frame.pyi index da6575328..17cacf05a 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -8,7 +8,6 @@ from av.frame import Frame from .format import VideoFormat from .plane import VideoPlane -from .reformatter import ColorRange, Colorspace _SupportedNDarray = Union[ np.ndarray[Any, np.dtype[np.uint8]], @@ -47,8 +46,8 @@ class VideoFrame(Frame): width: int | None = None, height: int | None = None, format: str | None = None, - src_colorspace: Colorspace | None = None, - dst_colorspace: Colorspace | None = None, + src_colorspace: int | None = None, + dst_colorspace: int | None = None, interpolation: int | str | None = None, src_color_range: int | str | None = None, dst_color_range: int | str | None = None, diff --git a/av/video/frame.pyx b/av/video/frame.pyx index 4425e18a9..6ff982491 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -464,9 +464,7 @@ cdef class VideoFrame(Frame): .. note:: For formats which expect an array of ``uint16``, the samples must be in the system's native byte order. - .. note:: for ``pal8``, an ``(image, palette)`` pair must be passed. - `palette` must have shape (256, 4) and is given in ARGB format - (PyAV will swap bytes if needed). + .. note:: for ``pal8``, an ``(image, palette)`` pair must be passed. `palette` must have shape (256, 4) and is given in ARGB format (PyAV will swap bytes if needed). """ if format == "pal8": array, palette = array diff --git a/av/video/reformatter.pyi b/av/video/reformatter.pyi index a68dc9718..abd545332 100644 --- a/av/video/reformatter.pyi +++ b/av/video/reformatter.pyi @@ -43,8 +43,8 @@ class VideoReformatter: width: int | None = None, height: int | None = None, format: str | None = None, - src_colorspace: Colorspace | None = None, - dst_colorspace: Colorspace | None = None, + src_colorspace: int | None = None, + dst_colorspace: int | None = None, interpolation: int | str | None = None, src_color_range: int | str | None = None, dst_color_range: int | str | None = None, diff --git a/av/video/stream.pxd b/av/video/stream.pxd index 01b8d9d41..f0dcfb9b2 100644 --- a/av/video/stream.pxd +++ b/av/video/stream.pxd @@ -1,5 +1,9 @@ +from av.packet cimport Packet from av.stream cimport Stream +from .frame cimport VideoFrame + cdef class VideoStream(Stream): - pass + cpdef encode(self, VideoFrame frame=?) + cpdef decode(self, Packet packet=?) diff --git a/av/video/stream.pyi b/av/video/stream.pyi index b0266328a..7ff1a4034 100644 --- a/av/video/stream.pyi +++ b/av/video/stream.pyi @@ -9,20 +9,31 @@ from .format import VideoFormat from .frame import VideoFrame class VideoStream(Stream): - width: int - height: int - format: VideoFormat - pix_fmt: str | None - sample_aspect_ratio: Fraction | None - codec_context: VideoCodecContext - type: Literal["video"] - - # from codec context bit_rate: int | None max_bit_rate: int | None bit_rate_tolerance: int thread_count: int thread_type: Any + codec_context: VideoCodecContext + # from codec context + format: VideoFormat + width: int + height: int + bits_per_codec_sample: int + pix_fmt: str | None + framerate: Fraction + rate: Fraction + gop_size: int + sample_aspect_ratio: Fraction | None + display_aspect_ratio: Fraction | None + has_b_frames: bool + coded_width: int + coded_height: int + color_range: int + color_primaries: int + color_trc: int + colorspace: int + type: Literal["video"] - def encode(self, frame: VideoFrame | None = None) -> list[Packet]: ... # type: ignore[override] + def encode(self, frame: VideoFrame | None = None) -> list[Packet]: ... def decode(self, packet: Packet | None = None) -> list[VideoFrame]: ... diff --git a/av/video/stream.pyx b/av/video/stream.pyx index 08949be2e..af80f27a1 100644 --- a/av/video/stream.pyx +++ b/av/video/stream.pyx @@ -1,7 +1,82 @@ +cimport libav as lib + +from av.packet cimport Packet +from av.utils cimport avrational_to_fraction, to_avrational + +from .frame cimport VideoFrame + + cdef class VideoStream(Stream): def __repr__(self): return ( - f"" ) + + cpdef encode(self, VideoFrame frame=None): + """ + Encode an :class:`.VideoFrame` and return a list of :class:`.Packet`. + + :rtype: list[Packet] + + .. seealso:: This is mostly a passthrough to :meth:`.CodecContext.encode`. + """ + + packets = self.codec_context.encode(frame) + cdef Packet packet + for packet in packets: + packet._stream = self + packet.ptr.stream_index = self.ptr.index + + return packets + + + cpdef decode(self, Packet packet=None): + """ + Decode a :class:`.Packet` and return a list of :class:`.VideoFrame`. + + :rtype: list[VideoFrame] + + .. seealso:: This is a passthrough to :meth:`.CodecContext.decode`. + """ + + return self.codec_context.decode(packet) + + @property + def average_rate(self): + """ + The average frame rate of this video stream. + + This is calculated when the file is opened by looking at the first + few frames and averaging their rate. + + :type: :class:`~fractions.Fraction` or ``None`` + """ + return avrational_to_fraction(&self.ptr.avg_frame_rate) + + @property + def base_rate(self): + """ + The base frame rate of this stream. + + This is calculated as the lowest framerate at which the timestamps of + frames can be represented accurately. See :ffmpeg:`AVStream.r_frame_rate` + for more. + + :type: :class:`~fractions.Fraction` or ``None`` + """ + return avrational_to_fraction(&self.ptr.r_frame_rate) + + @property + def guessed_rate(self): + """The guessed frame rate of this stream. + + This is a wrapper around :ffmpeg:`av_guess_frame_rate`, and uses multiple + heuristics to decide what is "the" frame rate. + + :type: :class:`~fractions.Fraction` or ``None`` + """ + # The two NULL arguments aren't used in FFmpeg >= 4.0 + cdef lib.AVRational val = lib.av_guess_frame_rate(NULL, self.ptr, NULL) + return avrational_to_fraction(&val) diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 7d841559d..49758be4c 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -90,6 +90,9 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef enum: AV_PKT_FLAG_KEY AV_PKT_FLAG_CORRUPT + AV_PKT_FLAG_DISCARD + AV_PKT_FLAG_TRUSTED + AV_PKT_FLAG_DISPOSABLE cdef enum: AV_FRAME_FLAG_CORRUPT @@ -168,6 +171,8 @@ cdef extern from "libavcodec/avcodec.h" nogil: int bit_rate_tolerance int mb_decision + int bits_per_coded_sample + int global_quality int compression_level diff --git a/scratchpad/README b/scratchpad/README deleted file mode 100644 index 83d571e01..000000000 --- a/scratchpad/README +++ /dev/null @@ -1,4 +0,0 @@ -This directory is for the PyAV developers to dump partial or exprimental tests. -The contents of this directory are not guaranteed to work, or make sense in any way. - -Have fun! diff --git a/scratchpad/__init__.py b/scratchpad/__init__.py deleted file mode 100644 index ab4bc5141..000000000 --- a/scratchpad/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -import logging - -logging.basicConfig(level=logging.WARNING) diff --git a/scratchpad/audio.py b/scratchpad/audio.py deleted file mode 100644 index fa28d5bf1..000000000 --- a/scratchpad/audio.py +++ /dev/null @@ -1,96 +0,0 @@ -import argparse -import subprocess - -import av - - -def print_data(frame): - for i, plane in enumerate(frame.planes or ()): - data = bytes(plane) - print('\tPLANE %d, %d bytes' % (i, len(data))) - data = data.encode('hex') - for i in range(0, len(data), 128): - print('\t\t\t%s' % data[i:i + 128]) - - -arg_parser = argparse.ArgumentParser() -arg_parser.add_argument('path') -arg_parser.add_argument('-p', '--play', action='store_true') -arg_parser.add_argument('-d', '--data', action='store_true') -arg_parser.add_argument('-f', '--format') -arg_parser.add_argument('-l', '--layout') -arg_parser.add_argument('-r', '--rate', type=int) -arg_parser.add_argument('-s', '--size', type=int, default=1024) -arg_parser.add_argument('-c', '--count', type=int, default=5) -args = arg_parser.parse_args() - -ffplay = None - -container = av.open(args.path) -stream = next(s for s in container.streams if s.type == 'audio') - -fifo = av.AudioFifo() if args.size else None -resampler = av.AudioResampler( - format=av.AudioFormat(args.format or stream.format.name).packed if args.format else None, - layout=int(args.layout) if args.layout and args.layout.isdigit() else args.layout, - rate=args.rate, -) if (args.format or args.layout or args.rate) else None - -read_count = 0 -fifo_count = 0 -sample_count = 0 - -for i, packet in enumerate(container.demux(stream)): - - for frame in packet.decode(): - - read_count += 1 - print('>>>> %04d' % read_count, frame) - if args.data: - print_data(frame) - - frames = [frame] - - if resampler: - for i, frame in enumerate(frames): - frame = resampler.resample(frame) - print('RESAMPLED', frame) - if args.data: - print_data(frame) - frames[i] = frame - - if fifo: - - to_process = frames - frames = [] - - for frame in to_process: - fifo.write(frame) - while frame: - frame = fifo.read(args.size) - if frame: - fifo_count += 1 - print('|||| %04d' % fifo_count, frame) - if args.data: - print_data(frame) - frames.append(frame) - - if frames and args.play: - if not ffplay: - cmd = ['ffplay', - '-f', frames[0].format.packed.container_name, - '-ar', str(args.rate or stream.rate), - '-ac', str(len(resampler.layout.channels if resampler else stream.layout.channels)), - '-vn', '-', - ] - print('PLAY', ' '.join(cmd)) - ffplay = subprocess.Popen(cmd, stdin=subprocess.PIPE) - try: - for frame in frames: - ffplay.stdin.write(bytes(frame.planes[0])) - except OSError as e: - print(e) - exit() - - if args.count and read_count >= args.count: - exit() diff --git a/scratchpad/audio_player.py b/scratchpad/audio_player.py deleted file mode 100644 index a7cf6b067..000000000 --- a/scratchpad/audio_player.py +++ /dev/null @@ -1,77 +0,0 @@ -import argparse -import time - -from qtproxy import Q - -import av - - -parser = argparse.ArgumentParser() -parser.add_argument('path') -args = parser.parse_args() - -container = av.open(args.path) -stream = next(s for s in container.streams if s.type == 'audio') - -fifo = av.AudioFifo() -resampler = av.AudioResampler( - format=av.AudioFormat('s16').packed, - layout='stereo', - rate=48000, -) - - - -qformat = Q.AudioFormat() -qformat.setByteOrder(Q.AudioFormat.LittleEndian) -qformat.setChannelCount(2) -qformat.setCodec('audio/pcm') -qformat.setSampleRate(48000) -qformat.setSampleSize(16) -qformat.setSampleType(Q.AudioFormat.SignedInt) - -output = Q.AudioOutput(qformat) -output.setBufferSize(2 * 2 * 48000) - -device = output.start() - -print(qformat, output, device) - -def decode_iter(): - try: - for pi, packet in enumerate(container.demux(stream)): - for fi, frame in enumerate(packet.decode()): - yield pi, fi, frame - except: - return - -for pi, fi, frame in decode_iter(): - - frame = resampler.resample(frame) - print(pi, fi, frame, output.state()) - - bytes_buffered = output.bufferSize() - output.bytesFree() - us_processed = output.processedUSecs() - us_buffered = 1000000 * bytes_buffered / (2 * 16 / 8) / 48000 - print(f'pts: {frame.time or 0:.3f}, played: {us_processed / 1000000.0:.3f}, buffered: {us_buffered / 1000000.0:.3f}') - - - data = bytes(frame.planes[0]) - while data: - written = device.write(data) - if written: - # print 'wrote', written - data = data[written:] - else: - # print 'did not accept data; sleeping' - time.sleep(0.033) - - if False and pi % 100 == 0: - output.reset() - print(output.state(), output.error()) - device = output.start() - - # time.sleep(0.05) - -while output.state() == Q.Audio.ActiveState: - time.sleep(0.1) diff --git a/scratchpad/average.py b/scratchpad/average.py deleted file mode 100644 index ec8cdf899..000000000 --- a/scratchpad/average.py +++ /dev/null @@ -1,56 +0,0 @@ -import argparse -import os - -import cv2 - -from av import open - - -parser = argparse.ArgumentParser() -parser.add_argument('-f', '--format') -parser.add_argument('-n', '--frames', type=int, default=0) -parser.add_argument('path', nargs='+') -args = parser.parse_args() - -max_size = 24 * 60 # One minute's worth. - - -def frame_iter(video): - count = 0 - streams = [s for s in video.streams if s.type == b'video'] - streams = [streams[0]] - for packet in video.demux(streams): - for frame in packet.decode(): - yield frame - count += 1 - if args.frames and count > args.frames: - return - - -for src_path in args.path: - - print('reading', src_path) - - basename = os.path.splitext(os.path.basename(src_path))[0] - dir_name = os.path.join('sandbox', basename) - if not os.path.exists(dir_name): - os.makedirs(dir_name) - - video = open(src_path, format=args.format) - frames = frame_iter(video) - - sum_ = None - - for fi, frame in enumerate(frame_iter(video)): - - if sum_ is None: - sum_ = frame.to_ndarray().astype(float) - else: - sum_ += frame.to_ndarray().astype(float) - - sum_ /= (fi + 1) - - dst_path = os.path.join('sandbox', os.path.basename(src_path) + '-avg.jpeg') - print('writing', (fi + 1), 'frames to', dst_path) - - cv2.imwrite(dst_path, sum_) diff --git a/scratchpad/cctx_decode.py b/scratchpad/cctx_decode.py deleted file mode 100644 index 2a4c727cf..000000000 --- a/scratchpad/cctx_decode.py +++ /dev/null @@ -1,28 +0,0 @@ -import logging - -from av.codec import CodecContext - -logging.basicConfig() - - -cc = CodecContext.create('mpeg4', 'r') -print(cc) - - -fh = open('test.mp4') - -frame_count = 0 - -while True: - - chunk = fh.read(819200) - for packet in cc.parse(chunk or None, allow_stream=True): - print(packet) - for frame in cc.decode(packet) or (): - print(frame) - img = frame.to_image() - img.save('sandbox/test.%04d.jpg' % frame_count) - frame_count += 1 - - if not chunk: - break # EOF! diff --git a/scratchpad/cctx_encode.py b/scratchpad/cctx_encode.py deleted file mode 100644 index 165203c1c..000000000 --- a/scratchpad/cctx_encode.py +++ /dev/null @@ -1,47 +0,0 @@ -import logging - -from PIL import Image, ImageDraw, ImageFont - -from av.codec import CodecContext -from av.video import VideoFrame -from tests.common import fate_suite - - -logging.basicConfig() - - -cc = CodecContext.create('flv', 'w') -print(cc) - -base_img = Image.open(fate_suite('png1/lena-rgb24.png')) -font = ImageFont.truetype("/System/Library/Fonts/Menlo.ttc", 15) - - -fh = open('test.flv', 'wb') - -for i in range(30): - - print(i) - img = base_img.copy() - draw = ImageDraw.Draw(img) - draw.text((10, 10), "FRAME %02d" % i, font=font) - - frame = VideoFrame.from_image(img) - frame = frame.reformat(format='yuv420p') - print(' ', frame) - - packet = cc.encode(frame) - print(' ', packet) - - fh.write(bytes(packet)) - -print('Flushing...') - -while True: - packet = cc.encode() - if not packet: - break - print(' ', packet) - fh.write(bytes(packet)) - -print('Done!') diff --git a/scratchpad/container-gc.py b/scratchpad/container-gc.py deleted file mode 100644 index a5bd2130d..000000000 --- a/scratchpad/container-gc.py +++ /dev/null @@ -1,44 +0,0 @@ -import resource -import gc - - -import av -import av.datasets - -path = av.datasets.curated('pexels/time-lapse-video-of-night-sky-857195.mp4') - - -def format_bytes(n): - order = 0 - while n > 1024: - order += 1 - n //= 1024 - return '%d%sB' % (n, ('', 'k', 'M', 'G', 'T', 'P')[order]) - -after = resource.getrusage(resource.RUSAGE_SELF) - -count = 0 - -streams = [] - -while True: - - container = av.open(path) - # streams.append(container.streams.video[0]) - - del container - gc.collect() - - count += 1 - if not count % 100: - pass - # streams.clear() - # gc.collect() - - before = after - after = resource.getrusage(resource.RUSAGE_SELF) - print('{:6d} {} ({})'.format( - count, - format_bytes(after.ru_maxrss), - format_bytes(after.ru_maxrss - before.ru_maxrss), - )) diff --git a/scratchpad/decode.py b/scratchpad/decode.py deleted file mode 100644 index 23ed0cb53..000000000 --- a/scratchpad/decode.py +++ /dev/null @@ -1,160 +0,0 @@ -import argparse -import logging -import subprocess - - -from av import open, time_base - - -logging.basicConfig(level=logging.DEBUG) - - -def format_time(time, time_base): - if time is None: - return 'None' - return f'{time_base * time:.3f}s ({time_base * time} or {time_base.numerator * time}/{time_base.denominator})' - - -arg_parser = argparse.ArgumentParser() -arg_parser.add_argument('path') -arg_parser.add_argument('-f', '--format') -arg_parser.add_argument('-a', '--audio', action='store_true') -arg_parser.add_argument('-v', '--video', action='store_true') -arg_parser.add_argument('-s', '--subs', action='store_true') -arg_parser.add_argument('-d', '--data', action='store_true') -arg_parser.add_argument('--dump-packets', action='store_true') -arg_parser.add_argument('--dump-planes', action='store_true') -arg_parser.add_argument('-p', '--play', action='store_true') -arg_parser.add_argument('-t', '--thread-type') -arg_parser.add_argument('-o', '--option', action='append', default=[]) -arg_parser.add_argument('-c', '--count', type=int, default=5) -args = arg_parser.parse_args() - - -proc = None - -options = dict(x.split('=') for x in args.option) -container = open(args.path, format=args.format, options=options) - -print('container:', container) -print('\tformat:', container.format) -print('\tduration:', float(container.duration) / time_base) -print('\tmetadata:') -for k, v in sorted(container.metadata.items()): - print(f'\t\t{k}: {v!r}') -print() - -print(len(container.streams), 'stream(s):') -for i, stream in enumerate(container.streams): - - if args.thread_type: - stream.codec_context.thread_type = args.thread_type - - print('\t%r' % stream) - print('\t\ttime_base: %r' % stream.time_base) - print('\t\trate: %r' % stream.rate) - print('\t\tstart_time: %r' % stream.start_time) - print('\t\tduration: %s' % format_time(stream.duration, stream.time_base)) - print('\t\tbit_rate: %r' % stream.bit_rate) - print('\t\tbit_rate_tolerance: %r' % stream.bit_rate_tolerance) - - codec_context = stream.codec_context - if codec_context: - print('\t\tcodec_context:', codec_context) - print('\t\t\ttime_base:', codec_context.time_base) - - if stream.type == b'audio': - print('\t\taudio:') - print('\t\t\tformat:', stream.format) - print('\t\t\tchannels: %s' % stream.channels) - - elif stream.type == 'video': - print('\t\tvideo:') - print('\t\t\tformat:', stream.format) - print('\t\t\taverage_rate: %r' % stream.average_rate) - - print('\t\tmetadata:') - for k, v in sorted(stream.metadata.items()): - print(f'\t\t\t{k}: {v!r}') - - print() - - -streams = [s for s in container.streams if - (s.type == 'audio' and args.audio) or - (s.type == 'video' and args.video) or - (s.type == 'subtitle' and args.subs) or - (s.type == 'data' and args.data) -] - - -frame_count = 0 - -for i, packet in enumerate(container.demux(streams)): - - print('%02d %r' % (i, packet)) - print('\ttime_base: %s' % packet.time_base) - print('\tduration: %s' % format_time(packet.duration, packet.stream.time_base)) - print('\tpts: %s' % format_time(packet.pts, packet.stream.time_base)) - print('\tdts: %s' % format_time(packet.dts, packet.stream.time_base)) - print('\tkey: %s' % packet.is_keyframe) - - if args.dump_packets: - print(bytes(packet)) - - for frame in packet.decode(): - - frame_count += 1 - - print('\tdecoded:', frame) - print('\t\ttime_base: %s' % frame.time_base) - print('\t\tpts:', format_time(frame.pts, packet.stream.time_base)) - - if packet.stream.type == 'video': - pass - - elif packet.stream.type == 'audio': - print('\t\tsamples:', frame.samples) - print('\t\tformat:', frame.format.name) - print('\t\tlayout:', frame.layout.name) - - elif packet.stream.type == 'subtitle': - - sub = frame - - print('\t\tformat:', sub.format) - print('\t\tstart_display_time:', format_time(sub.start_display_time, packet.stream.time_base)) - print('\t\tend_display_time:', format_time(sub.end_display_time, packet.stream.time_base)) - print('\t\trects: %d' % len(sub.rects)) - for rect in sub.rects: - print('\t\t\t%r' % rect) - if rect.type == 'ass': - print('\t\t\t\tass: %r' % rect.ass) - - if args.play and packet.stream.type == 'audio': - if not proc: - cmd = ['ffplay', - '-f', 's16le', - '-ar', str(packet.stream.time_base), - '-vn', '-', - ] - proc = subprocess.Popen(cmd, stdin=subprocess.PIPE) - try: - proc.stdin.write(bytes(frame.planes[0])) - except OSError as e: - print(e) - exit() - - if args.dump_planes: - print('\t\tplanes') - for i, plane in enumerate(frame.planes or ()): - data = bytes(plane) - print('\t\t\tPLANE %d, %d bytes' % (i, len(data))) - data = data.encode('hex') - for i in range(0, len(data), 128): - print('\t\t\t%s' % data[i:i + 128]) - - if args.count and frame_count >= args.count: - exit() - - print() diff --git a/scratchpad/encode.py b/scratchpad/encode.py deleted file mode 100644 index 390415dcf..000000000 --- a/scratchpad/encode.py +++ /dev/null @@ -1,90 +0,0 @@ -import argparse -import os -import sys - -import av -from tests.common import sandboxed - - -arg_parser = argparse.ArgumentParser() -arg_parser.add_argument('-v', '--verbose', action='store_true') -arg_parser.add_argument('input', nargs=1) -args = arg_parser.parse_args() - -input_file = av.open(args.input[0]) -input_video_stream = None # next((s for s in input_file.streams if s.type == 'video'), None) -input_audio_stream = next((s for s in input_file.streams if s.type == 'audio'), None) - -# open output file -output_file_path = sandboxed('encoded-' + os.path.basename(args.input[0])) -output_file = av.open(output_file_path, 'w') -output_video_stream = output_file.add_stream("mpeg4", 24) if input_video_stream else None -output_audio_stream = output_file.add_stream("mp3") if input_audio_stream else None - - -frame_count = 0 - - -for packet in input_file.demux([s for s in (input_video_stream, input_audio_stream) if s]): - - - if args.verbose: - print('in ', packet) - - for frame in packet.decode(): - - if args.verbose: - print('\t%s' % frame) - - if packet.stream.type == b'video': - if frame_count % 10 == 0: - if frame_count: - print() - print(('%03d:' % frame_count), end=' ') - sys.stdout.write('.') - sys.stdout.flush() - - frame_count += 1 - - # Signal to generate it's own timestamps. - frame.pts = None - - stream = output_audio_stream if packet.stream.type == b'audio' else output_video_stream - output_packets = [output_audio_stream.encode(frame)] - while output_packets[-1]: - output_packets.append(output_audio_stream.encode(None)) - - for p in output_packets: - if p: - if args.verbose: - print('OUT', p) - output_file.mux(p) - - if frame_count >= 100: - break - -print('-' * 78) - -# Finally we need to flush out the frames that are buffered in the encoder. -# To do that we simply call encode with no args until we get a None returned -if output_audio_stream: - while True: - output_packet = output_audio_stream.encode(None) - if output_packet: - if args.verbose: - print('<<<', output_packet) - output_file.mux(output_packet) - else: - break - -if output_video_stream: - while True: - output_packet = output_video_stream.encode(None) - if output_packet: - if args.verbose: - print('<<<', output_packet) - output_file.mux(output_packet) - else: - break - -output_file.close() diff --git a/scratchpad/encode_frames.py b/scratchpad/encode_frames.py deleted file mode 100644 index 8916fee83..000000000 --- a/scratchpad/encode_frames.py +++ /dev/null @@ -1,39 +0,0 @@ -import argparse -import os - -import av -import cv2 - - -arg_parser = argparse.ArgumentParser() -arg_parser.add_argument('-r', '--rate', default='23.976') -arg_parser.add_argument('-f', '--format', default='yuv420p') -arg_parser.add_argument('-w', '--width', type=int) -arg_parser.add_argument('--height', type=int) -arg_parser.add_argument('-b', '--bitrate', type=int, default=8000000) -arg_parser.add_argument('-c', '--codec', default='mpeg4') -arg_parser.add_argument('inputs', nargs='+') -arg_parser.add_argument('output', nargs=1) -args = arg_parser.parse_args() - - -output = av.open(args.output[0], 'w') -stream = output.add_stream(args.codec, args.rate) -stream.bit_rate = args.bitrate -stream.pix_fmt = args.format - -for i, path in enumerate(args.inputs): - - print(os.path.basename(path)) - - img = cv2.imread(path) - - if not i: - stream.height = args.height or (args.width * img.shape[0] / img.shape[1]) or img.shape[0] - stream.width = args.width or img.shape[1] - - frame = av.VideoFrame.from_ndarray(img, format='bgr24') - packet = stream.encode(frame) - output.mux(packet) - -output.close() diff --git a/scratchpad/filter_audio.py b/scratchpad/filter_audio.py deleted file mode 100644 index 5483e62a5..000000000 --- a/scratchpad/filter_audio.py +++ /dev/null @@ -1,122 +0,0 @@ -""" -Simple audio filtering example ported from C code: - https://github.com/FFmpeg/FFmpeg/blob/master/doc/examples/filter_audio.c -""" -from fractions import Fraction -import hashlib -import sys - -import numpy as np - -import av -import av.audio.frame as af -import av.filter - - -FRAME_SIZE = 1024 - -INPUT_SAMPLE_RATE = 48000 -INPUT_FORMAT = 'fltp' -INPUT_CHANNEL_LAYOUT = '5.0(side)' # -> AV_CH_LAYOUT_5POINT0 - -OUTPUT_SAMPLE_RATE = 44100 -OUTPUT_FORMAT = 's16' # notice, packed audio format, expect only one plane in output -OUTPUT_CHANNEL_LAYOUT = 'stereo' # -> AV_CH_LAYOUT_STEREO - -VOLUME_VAL = 0.90 - - -def init_filter_graph(): - graph = av.filter.Graph() - - output_format = 'sample_fmts={}:sample_rates={}:channel_layouts={}'.format( - OUTPUT_FORMAT, - OUTPUT_SAMPLE_RATE, - OUTPUT_CHANNEL_LAYOUT - ) - print(f'Output format: {output_format}') - - # initialize filters - filter_chain = [ - graph.add_abuffer(format=INPUT_FORMAT, - sample_rate=INPUT_SAMPLE_RATE, - layout=INPUT_CHANNEL_LAYOUT, - time_base=Fraction(1, INPUT_SAMPLE_RATE)), - # initialize filter with keyword parameters - graph.add('volume', volume=str(VOLUME_VAL)), - # or compound string configuration - graph.add('aformat', output_format), - graph.add('abuffersink') - ] - - # link up the filters into a chain - print('Filter graph:') - for c, n in zip(filter_chain, filter_chain[1:]): - print(f'\t{c} -> {n}') - c.link_to(n) - - # initialize the filter graph - graph.configure() - - return graph - - -def get_input(frame_num): - """ - Manually construct and update AudioFrame. - Consider using AudioFrame.from_ndarry for most real life numpy->AudioFrame conversions. - - :param frame_num: - :return: - """ - frame = av.AudioFrame(format=INPUT_FORMAT, layout=INPUT_CHANNEL_LAYOUT, samples=FRAME_SIZE) - frame.sample_rate = INPUT_SAMPLE_RATE - frame.pts = frame_num * FRAME_SIZE - - for i in range(len(frame.layout.channels)): - data = np.zeros(FRAME_SIZE, dtype=af.format_dtypes[INPUT_FORMAT]) - for j in range(FRAME_SIZE): - data[j] = np.sin(2 * np.pi * (frame_num + j) * (i + 1) / float(FRAME_SIZE)) - frame.planes[i].update(data) - - return frame - - -def process_output(frame): - data = frame.to_ndarray() - for i in range(data.shape[0]): - m = hashlib.md5(data[i, :].tobytes()) - print(f'Plane: {i:0d} checksum: {m.hexdigest()}') - - -def main(duration): - frames_count = int(duration * INPUT_SAMPLE_RATE / FRAME_SIZE) - - graph = init_filter_graph() - - for f in range(frames_count): - frame = get_input(f) - - # submit the frame for processing - graph.push(frame) - - # pull frames from graph until graph has done processing or is waiting for a new input - while True: - try: - out_frame = graph.pull() - process_output(out_frame) - except (BlockingIOError, av.EOFError): - break - - # process any remaining buffered frames - while True: - try: - out_frame = graph.pull() - process_output(out_frame) - except (BlockingIOError, av.EOFError): - break - - -if __name__ == '__main__': - duration = 1.0 if len(sys.argv) < 2 else float(sys.argv[1]) - main(duration) diff --git a/scratchpad/frame_seek_example.py b/scratchpad/frame_seek_example.py deleted file mode 100644 index 5385cd1bb..000000000 --- a/scratchpad/frame_seek_example.py +++ /dev/null @@ -1,415 +0,0 @@ -""" -Note this example only really works accurately on constant frame rate media. -""" -from PyQt4 import QtGui -from PyQt4 import QtCore -from PyQt4.QtCore import Qt - -import sys -import av - - -AV_TIME_BASE = 1000000 - -def pts_to_frame(pts, time_base, frame_rate, start_time): - return int(pts * time_base * frame_rate) - int(start_time * time_base * frame_rate) - -def get_frame_rate(stream): - - if stream.average_rate.denominator and stream.average_rate.numerator: - return float(stream.average_rate) - if stream.time_base.denominator and stream.time_base.numerator: - return 1.0 / float(stream.time_base) - else: - raise ValueError("Unable to determine FPS") - -def get_frame_count(f, stream): - - if stream.frames: - return stream.frames - elif stream.duration: - return pts_to_frame(stream.duration, float(stream.time_base), get_frame_rate(stream), 0) - elif f.duration: - return pts_to_frame(f.duration, 1 / float(AV_TIME_BASE), get_frame_rate(stream), 0) - - else: - raise ValueError("Unable to determine number for frames") - -class FrameGrabber(QtCore.QObject): - - frame_ready = QtCore.pyqtSignal(object, object) - update_frame_range = QtCore.pyqtSignal(object) - - def __init__(self, parent=None): - super().__init__(parent) - self.file = None - self.stream = None - self.frame = None - self.active_frame = None - self.start_time = 0 - self.pts_seen = False - self.nb_frames = None - - self.rate = None - self.time_base = None - - def next_frame(self): - - frame_index = None - - rate = self.rate - time_base = self.time_base - - self.pts_seen = False - - for packet in self.file.demux(self.stream): - #print " pkt", packet.pts, packet.dts, packet - if packet.pts: - self.pts_seen = True - - for frame in packet.decode(): - - if frame_index is None: - - if self.pts_seen: - pts = frame.pts - else: - pts = frame.dts - - if pts is not None: - frame_index = pts_to_frame(pts, time_base, rate, self.start_time) - - elif frame_index is not None: - frame_index += 1 - - - yield frame_index, frame - - - @QtCore.pyqtSlot(object) - def request_frame(self, target_frame): - - frame = self.get_frame(target_frame) - if not frame: - return - - rgba = frame.reformat(frame.width, frame.height, "rgb24", 'itu709') - #print rgba.to_image().save("test.png") - # could use the buffer interface here instead, some versions of PyQt don't support it for some reason - # need to track down which version they added support for it - self.frame = bytearray(rgba.planes[0]) - bytesPerPixel = 3 - img = QtGui.QImage(self.frame, rgba.width, rgba.height, rgba.width * bytesPerPixel, QtGui.QImage.Format_RGB888) - - #img = QtGui.QImage(rgba.planes[0], rgba.width, rgba.height, QtGui.QImage.Format_RGB888) - - #pixmap = QtGui.QPixmap.fromImage(img) - self.frame_ready.emit(img, target_frame) - - def get_frame(self, target_frame): - - if target_frame != self.active_frame: - return - print('seeking to', target_frame) - - seek_frame = target_frame - - rate = self.rate - time_base = self.time_base - - frame = None - reseek = 250 - - original_target_frame_pts = None - - while reseek >= 0: - - # convert seek_frame to pts - target_sec = seek_frame * 1 / rate - target_pts = int(target_sec / time_base) + self.start_time - - if original_target_frame_pts is None: - original_target_frame_pts = target_pts - - self.stream.seek(int(target_pts)) - - frame_index = None - - frame_cache = [] - - for i, (frame_index, frame) in enumerate(self.next_frame()): - - # optimization if the time slider has changed, the requested frame no longer valid - if target_frame != self.active_frame: - return - - print(" ", i, "at frame", frame_index, "at ts:", frame.pts, frame.dts, "target:", target_pts, 'orig', original_target_frame_pts) - - if frame_index is None: - pass - - elif frame_index >= target_frame: - break - - frame_cache.append(frame) - - # Check if we over seeked, if we over seekd we need to seek to a earlier time - # but still looking for the target frame - if frame_index != target_frame: - - if frame_index is None: - over_seek = '?' - else: - over_seek = frame_index - target_frame - if frame_index > target_frame: - - print(over_seek, frame_cache) - if over_seek <= len(frame_cache): - print("over seeked by %i, using cache" % over_seek) - frame = frame_cache[-over_seek] - break - - - seek_frame -= 1 - reseek -= 1 - print("over seeked by %s, backtracking.. seeking: %i target: %i retry: %i" % (str(over_seek), seek_frame, target_frame, reseek)) - - else: - break - - if reseek < 0: - raise ValueError("seeking failed %i" % frame_index) - - # frame at this point should be the correct frame - - if frame: - - return frame - - else: - raise ValueError("seeking failed %i" % target_frame) - - def get_frame_count(self): - - frame_count = None - - if self.stream.frames: - frame_count = self.stream.frames - elif self.stream.duration: - frame_count = pts_to_frame(self.stream.duration, float(self.stream.time_base), get_frame_rate(self.stream), 0) - elif self.file.duration: - frame_count = pts_to_frame(self.file.duration, 1 / float(AV_TIME_BASE), get_frame_rate(self.stream), 0) - else: - raise ValueError("Unable to determine number for frames") - - seek_frame = frame_count - - retry = 100 - - while retry: - target_sec = seek_frame * 1 / self.rate - target_pts = int(target_sec / self.time_base) + self.start_time - - self.stream.seek(int(target_pts)) - - frame_index = None - - for frame_index, frame in self.next_frame(): - print(frame_index, frame) - continue - - if frame_index is not None: - break - else: - seek_frame -= 1 - retry -= 1 - - - print("frame count seeked", frame_index, "container frame count", frame_count) - - return frame_index or frame_count - - @QtCore.pyqtSlot(object) - def set_file(self, path): - self.file = av.open(path) - self.stream = next(s for s in self.file.streams if s.type == b'video') - self.rate = get_frame_rate(self.stream) - self.time_base = float(self.stream.time_base) - - - index, first_frame = next(self.next_frame()) - self.stream.seek(self.stream.start_time) - - # find the pts of the first frame - index, first_frame = next(self.next_frame()) - - if self.pts_seen: - pts = first_frame.pts - else: - pts = first_frame.dts - - self.start_time = pts or first_frame.dts - - print("First pts", pts, self.stream.start_time, first_frame) - - #self.nb_frames = get_frame_count(self.file, self.stream) - self.nb_frames = self.get_frame_count() - - self.update_frame_range.emit(self.nb_frames) - - - - - -class DisplayWidget(QtGui.QLabel): - def __init__(self, parent=None): - super().__init__(parent) - #self.setScaledContents(True) - self.setMinimumSize(1920 / 10, 1080 / 10) - - size_policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) - size_policy.setHeightForWidth(True) - - self.setSizePolicy(size_policy) - - self.setAlignment(Qt.AlignHCenter | Qt.AlignBottom) - - self.pixmap = None - self.setMargin(10) - - def heightForWidth(self, width): - return width * 9 / 16.0 - - @QtCore.pyqtSlot(object, object) - def setPixmap(self, img, index): - #if index == self.current_index: - self.pixmap = QtGui.QPixmap.fromImage(img) - - #super(DisplayWidget, self).setPixmap(self.pixmap) - super().setPixmap(self.pixmap.scaled(self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) - - def sizeHint(self): - width = self.width() - return QtCore.QSize(width, self.heightForWidth(width)) - - def resizeEvent(self, event): - if self.pixmap: - super().setPixmap(self.pixmap.scaled(self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)) - - -class VideoPlayerWidget(QtGui.QWidget): - - request_frame = QtCore.pyqtSignal(object) - - load_file = QtCore.pyqtSignal(object) - - def __init__(self, parent=None): - super().__init__(parent) - self.display = DisplayWidget() - self.timeline = QtGui.QScrollBar(Qt.Horizontal) - self.frame_grabber = FrameGrabber() - - self.frame_control = QtGui.QSpinBox() - self.frame_control.setFixedWidth(100) - - self.timeline.valueChanged.connect(self.frame_changed) - self.frame_control.valueChanged.connect(self.frame_changed) - - self.request_frame.connect(self.frame_grabber.request_frame) - self.load_file.connect(self.frame_grabber.set_file) - - self.frame_grabber.frame_ready.connect(self.display.setPixmap) - self.frame_grabber.update_frame_range.connect(self.set_frame_range) - - self.frame_grabber_thread = QtCore.QThread() - - self.frame_grabber.moveToThread(self.frame_grabber_thread) - self.frame_grabber_thread.start() - - control_layout = QtGui.QHBoxLayout() - control_layout.addWidget(self.frame_control) - control_layout.addWidget(self.timeline) - - layout = QtGui.QVBoxLayout() - layout.addWidget(self.display) - layout.addLayout(control_layout) - self.setLayout(layout) - self.setAcceptDrops(True) - - def set_file(self, path): - #self.frame_grabber.set_file(path) - self.load_file.emit(path) - self.frame_changed(0) - - @QtCore.pyqtSlot(object) - def set_frame_range(self, maximum): - print("frame range =", maximum) - self.timeline.setMaximum(maximum) - self.frame_control.setMaximum(maximum) - - def frame_changed(self, value): - self.timeline.blockSignals(True) - self.frame_control.blockSignals(True) - - self.timeline.setValue(value) - self.frame_control.setValue(value) - - self.timeline.blockSignals(False) - self.frame_control.blockSignals(False) - - #self.display.current_index = value - self.frame_grabber.active_frame = value - - self.request_frame.emit(value) - - def keyPressEvent(self, event): - if event.key() in (Qt.Key_Right, Qt.Key_Left): - direction = 1 - if event.key() == Qt.Key_Left: - direction = -1 - - if event.modifiers() == Qt.ShiftModifier: - print('shift') - direction *= 10 - - self.timeline.setValue(self.timeline.value() + direction) - - else: - super().keyPressEvent(event) - - def mousePressEvent(self, event): - # clear focus of spinbox - focused_widget = QtGui.QApplication.focusWidget() - if focused_widget: - focused_widget.clearFocus() - - super().mousePressEvent(event) - - def dragEnterEvent(self, event): - event.accept() - - def dropEvent(self, event): - - mime = event.mimeData() - event.accept() - - - if mime.hasUrls(): - path = str(mime.urls()[0].path()) - self.set_file(path) - def closeEvent(self, event): - - self.frame_grabber.active_frame = -1 - self.frame_grabber_thread.quit() - self.frame_grabber_thread.wait() - - event.accept() - - -if __name__ == "__main__": - app = QtGui.QApplication(sys.argv) - window = VideoPlayerWidget() - test_file = sys.argv[1] - window.set_file(test_file) - window.show() - sys.exit(app.exec_()) diff --git a/scratchpad/glproxy.py b/scratchpad/glproxy.py deleted file mode 100644 index 181ff1bac..000000000 --- a/scratchpad/glproxy.py +++ /dev/null @@ -1,97 +0,0 @@ -'''Mikes wrapper for the visualizer???''' -from contextlib import contextmanager - -from OpenGL.GLUT import * -from OpenGL.GLU import * -from OpenGL.GL import * -import OpenGL - - -__all__ = ''' - gl - glu - glut -'''.strip().split() - - -class ModuleProxy: - - def __init__(self, name, module): - self.name = name - self.module = module - - def __getattr__(self, name): - if name.isupper(): - return getattr(self.module, self.name.upper() + '_' + name) - else: - # convert to camel case - name = name.split('_') - name = [x[0].upper() + x[1:] for x in name] - name = ''.join(name) - return getattr(self.module, self.name + name) - - -class GLProxy(ModuleProxy): - - @contextmanager - def matrix(self): - self.module.glPushMatrix() - try: - yield - finally: - self.module.glPopMatrix() - - @contextmanager - def attrib(self, *args): - mask = 0 - for arg in args: - if isinstance(arg, str): - arg = getattr(self.module, 'GL_%s_BIT' % arg.upper()) - mask |= arg - self.module.glPushAttrib(mask) - try: - yield - finally: - self.module.glPopAttrib() - - def enable(self, *args, **kwargs): - self._enable(True, args, kwargs) - return self._apply_on_exit(self._enable, False, args, kwargs) - - def disable(self, *args, **kwargs): - self._enable(False, args, kwargs) - return self._apply_on_exit(self._enable, True, args, kwargs) - - def _enable(self, enable, args, kwargs): - todo = [] - for arg in args: - if isinstance(arg, str): - arg = getattr(self.module, 'GL_%s' % arg.upper()) - todo.append((arg, enable)) - for key, value in kwargs.iteritems(): - flag = getattr(self.module, 'GL_%s' % key.upper()) - value = value if enable else not value - todo.append((flag, value)) - for flag, value in todo: - if value: - self.module.glEnable(flag) - else: - self.module.glDisable(flag) - - def begin(self, arg): - if isinstance(arg, str): - arg = getattr(self.module, 'GL_%s' % arg.upper()) - self.module.glBegin(arg) - return self._apply_on_exit(self.module.glEnd) - - @contextmanager - def _apply_on_exit(self, func, *args, **kwargs): - try: - yield - finally: - func(*args, **kwargs) - - -gl = GLProxy('gl', OpenGL.GL) -glu = ModuleProxy('glu', OpenGL.GLU) -glut = ModuleProxy('glut', OpenGL.GLUT) diff --git a/scratchpad/graph.py b/scratchpad/graph.py deleted file mode 100644 index d1a209c37..000000000 --- a/scratchpad/graph.py +++ /dev/null @@ -1,18 +0,0 @@ -from av.filter.graph import Graph - -g = Graph() -print(g.dump()) - -f = g.pull() - -print(f) - -f = f.reformat(format='rgb24') - -print(f) - -img = f.to_image() - -print(img) - -img.save('graph.png') diff --git a/scratchpad/player.py b/scratchpad/player.py deleted file mode 100644 index c986b9904..000000000 --- a/scratchpad/player.py +++ /dev/null @@ -1,101 +0,0 @@ -import argparse -import ctypes -import time - -from qtproxy import Q -from glproxy import gl - -import av - -WIDTH = 960 -HEIGHT = 540 - - -class PlayerGLWidget(Q.GLWidget): - - def initializeGL(self): - print('initialize GL') - gl.clearColor(0, 0, 0, 0) - - gl.enable(gl.TEXTURE_2D) - - # gl.texEnv(gl.TEXTURE_ENV, gl.TEXTURE_ENV_MODE, gl.DECAL) - self.tex_id = gl.genTextures(1) - gl.bindTexture(gl.TEXTURE_2D, self.tex_id) - gl.texParameter(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) - gl.texParameter(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST) - - print('texture id', self.tex_id) - - def setImage(self, w, h, img): - gl.texImage2D(gl.TEXTURE_2D, 0, 3, w, h, 0, gl.RGB, gl.UNSIGNED_BYTE, img) - - def resizeGL(self, w, h): - print('resize to', w, h) - gl.viewport(0, 0, w, h) - # gl.matrixMode(gl.PROJECTION) - # gl.loadIdentity() - # gl.ortho(0, w, 0, h, -10, 10) - # gl.matrixMode(gl.MODELVIEW) - - def paintGL(self): - # print 'paint!' - gl.clear(gl.COLOR_BUFFER_BIT) - with gl.begin('polygon'): - gl.texCoord(0, 0) - gl.vertex(-1, 1) - gl.texCoord(1, 0) - gl.vertex(1, 1) - gl.texCoord(1, 1) - gl.vertex(1, -1) - gl.texCoord(0, 1) - gl.vertex(-1, -1) - - - -parser = argparse.ArgumentParser() -parser.add_argument('-f', '--format') -parser.add_argument('path') -args = parser.parse_args() - - -def _iter_images(): - video = av.open(args.path, format=args.format) - stream = next(s for s in video.streams if s.type == b'video') - for packet in video.demux(stream): - for frame in packet.decode(): - yield frame.reformat(frame.width, frame.height, 'rgb24') - -image_iter = _iter_images() - -app = Q.Application([]) - -glwidget = PlayerGLWidget() -glwidget.setFixedWidth(WIDTH) -glwidget.setFixedHeight(HEIGHT) -glwidget.show() -glwidget.raise_() - -start_time = 0 -count = 0 - -timer = Q.Timer() -timer.setInterval(1000 / 30) -@timer.timeout.connect -def on_timeout(*args): - - global start_time, count - start_time = start_time or time.time() - - frame = next(image_iter) - ptr = ctypes.c_void_p(frame.planes[0].ptr) - glwidget.setImage(frame.width, frame.height, ptr) - glwidget.updateGL() - - count += 1 - elapsed = time.time() - start_time - print(frame.pts, frame.dts, '%.2ffps' % (count / elapsed)) - -timer.start() - -app.exec_() diff --git a/scratchpad/qtproxy.py b/scratchpad/qtproxy.py deleted file mode 100644 index 3ffb07955..000000000 --- a/scratchpad/qtproxy.py +++ /dev/null @@ -1,19 +0,0 @@ -from PyQt4 import QtCore, QtGui, QtOpenGL, QtMultimedia - - -class QtProxy: - - def __init__(self, *modules): - self._modules = modules - - def __getattr__(self, base_name): - for mod in self._modules: - for prefix in ('Q', '', 'Qt'): - name = prefix + base_name - obj = getattr(mod, name, None) - if obj is not None: - setattr(self, base_name, obj) - return obj - raise AttributeError(base_name) - -Q = QtProxy(QtGui, QtCore, QtCore.Qt, QtOpenGL, QtMultimedia) diff --git a/scratchpad/remux.py b/scratchpad/remux.py deleted file mode 100644 index 99de07c2c..000000000 --- a/scratchpad/remux.py +++ /dev/null @@ -1,65 +0,0 @@ -import argparse -import logging - -import av - - -logging.basicConfig(level=logging.DEBUG) - - -arg_parser = argparse.ArgumentParser() -arg_parser.add_argument("input") -arg_parser.add_argument("output") -arg_parser.add_argument("-F", "--iformat") -arg_parser.add_argument("-O", "--ioption", action="append", default=[]) -arg_parser.add_argument("-f", "--oformat") -arg_parser.add_argument("-o", "--ooption", action="append", default=[]) -arg_parser.add_argument("-a", "--noaudio", action="store_true") -arg_parser.add_argument("-v", "--novideo", action="store_true") -arg_parser.add_argument("-s", "--nosubs", action="store_true") -arg_parser.add_argument("-d", "--nodata", action="store_true") -arg_parser.add_argument("-c", "--count", type=int, default=0) -args = arg_parser.parse_args() - - -input_ = av.open( - args.input, - format=args.iformat, - options=dict(x.split("=") for x in args.ioption), -) -output = av.open( - args.output, - "w", - format=args.oformat, - options=dict(x.split("=") for x in args.ooption), -) - -in_to_out = {} - -for i, stream in enumerate(input_.streams): - if ( - (stream.type == "audio" and not args.noaudio) - or (stream.type == "video" and not args.novideo) - or (stream.type == "subtitle" and not args.nosubtitle) - or (stream.type == "data" and not args.nodata) - ): - in_to_out[stream] = output.add_stream(template=stream) - -for i, packet in enumerate(input_.demux(list(in_to_out.keys()))): - - if args.count and i >= args.count: - break - print("%02d %r" % (i, packet)) - print("\tin: ", packet.stream) - - if packet.dts is None: - continue - - packet.stream = in_to_out[packet.stream] - - print("\tout:", packet.stream) - - output.mux(packet) - - -output.close() diff --git a/scratchpad/resource_use.py b/scratchpad/resource_use.py deleted file mode 100644 index aba327a41..000000000 --- a/scratchpad/resource_use.py +++ /dev/null @@ -1,61 +0,0 @@ -import argparse -import resource -import gc - -import av - - -parser = argparse.ArgumentParser() -parser.add_argument('-c', '--count', type=int, default=5) -parser.add_argument('-f', '--frames', type=int, default=100) -parser.add_argument('--print', dest='print_', action='store_true') -parser.add_argument('--to-rgb', action='store_true') -parser.add_argument('--to-image', action='store_true') -parser.add_argument('--gc', '-g', action='store_true') -parser.add_argument('input') -args = parser.parse_args() - -def format_bytes(n): - order = 0 - while n > 1024: - order += 1 - n //= 1024 - return '%d%sB' % (n, ('', 'k', 'M', 'G', 'T', 'P')[order]) - -usage = [] - -for round_ in range(args.count): - - print('Round %d/%d:' % (round_ + 1, args.count)) - - if args.gc: - gc.collect() - - usage.append(resource.getrusage(resource.RUSAGE_SELF)) - - fh = av.open(args.input) - vs = next(s for s in fh.streams if s.type == 'video') - - fi = 0 - for packet in fh.demux([vs]): - for frame in packet.decode(): - if args.print_: - print(frame) - if args.to_rgb: - print(frame.to_rgb()) - if args.to_image: - print(frame.to_image()) - fi += 1 - if fi > args.frames: - break - - frame = packet = fh = vs = None - - - -usage.append(resource.getrusage(resource.RUSAGE_SELF)) - -for i in range(len(usage) - 1): - before = usage[i] - after = usage[i + 1] - print(f'{format_bytes(after.ru_maxrss)} ({format_bytes(after.ru_maxrss - before.ru_maxrss)})') diff --git a/scratchpad/save_subtitles.py b/scratchpad/save_subtitles.py deleted file mode 100644 index d937e8152..000000000 --- a/scratchpad/save_subtitles.py +++ /dev/null @@ -1,60 +0,0 @@ -""" - -As you can see, the subtitle API needs some work. - -""" - -import os -import sys - -from PIL import Image - -from av import open - - -if not os.path.exists('subtitles'): - os.makedirs('subtitles') - - -video = open(sys.argv[1]) - -streams = [s for s in video.streams if s.type == b'subtitle'] -if not streams: - print('no subtitles') - exit(1) - -print(streams) - -count = 0 -for pi, packet in enumerate(video.demux([streams[0]])): - - print('packet', pi) - for si, subtitle in enumerate(packet.decode()): - - print('\tsubtitle', si, subtitle) - - for ri, rect in enumerate(subtitle.rects): - if rect.type == 'ass': - print('\t\tass: ', rect, rect.ass.rstrip('\n')) - if rect.type == 'text': - print('\t\ttext: ', rect, rect.text.rstrip('\n')) - if rect.type == 'bitmap': - print('\t\tbitmap: ', rect, rect.width, rect.height, rect.pict_buffers) - buffers = [b for b in rect.pict_buffers if b is not None] - if buffers: - imgs = [ - Image.frombuffer('L', (rect.width, rect.height), buffer, "raw", "L", 0, 1) - for buffer in buffers - ] - if len(imgs) == 1: - img = imgs[0] - elif len(imgs) == 2: - img = Image.merge('LA', imgs) - else: - img = Image.merge('RGBA', imgs) - img.save('subtitles/%04d.png' % count) - - count += 1 - if count > 10: - pass - # exit() diff --git a/scratchpad/seekmany.py b/scratchpad/seekmany.py deleted file mode 100644 index a12b88a14..000000000 --- a/scratchpad/seekmany.py +++ /dev/null @@ -1,48 +0,0 @@ -import sys - -import av - -container = av.open(sys.argv[1]) -duration = container.duration -stream = container.streams.video[0] - -print('container.duration', duration, float(duration) / av.time_base) -print('container.time_base', av.time_base) -print('stream.duration', stream.duration) -print('stream.time_base', stream.time_base) -print('codec.time_base', stream.codec_context.time_base) -print('scale', float(stream.codec_context.time_base / stream.time_base)) -print() - -exit() - -real_duration = float(duration) / av.time_base -steps = 120 -tolerance = real_duration / (steps * 4) -print('real_duration', real_duration) -print() - -def iter_frames(): - for packet in container.demux(stream): - yield from packet.decode() - -for i in range(steps): - - time = real_duration * i / steps - min_time = time - tolerance - - pts = time / stream.time_base - - print('seeking', time, pts) - stream.seek(int(pts)) - - skipped = 0 - for frame in iter_frames(): - ftime = float(frame.pts * stream.time_base) - if ftime >= min_time: - break - skipped += 1 - else: - print(' WARNING: iterated to the end') - - print(' ', skipped, frame.pts, float(frame.pts * stream.time_base)) # WTF is this stream.time_base? diff --git a/scratchpad/show_frames_opencv.py b/scratchpad/show_frames_opencv.py deleted file mode 100644 index dc7d73f38..000000000 --- a/scratchpad/show_frames_opencv.py +++ /dev/null @@ -1,20 +0,0 @@ -import sys - -import cv2 -from av import open - - -video = open(sys.argv[1]) - -stream = next(s for s in video.streams if s.type == 'video') - -for packet in video.demux(stream): - for frame in packet.decode(): - # some other formats gray16be, bgr24, rgb24 - img = frame.to_ndarray(format='bgr24') - cv2.imshow("Test", img) - - if cv2.waitKey(1) == 27: - break - -cv2.destroyAllWindows() diff --git a/scratchpad/sidedata.py b/scratchpad/sidedata.py deleted file mode 100644 index a1157d7dd..000000000 --- a/scratchpad/sidedata.py +++ /dev/null @@ -1,25 +0,0 @@ -import sys - -import av - - -fh = av.open(sys.argv[1]) -fh.streams.video[0].export_mvs = True -# fh.streams.video[0].flags2 |= 'EXPORT_MVS' - -for pi, packet in enumerate(fh.demux()): - for fi, frame in enumerate(packet.decode()): - - for di, data in enumerate(frame.side_data): - - print(pi, fi, di, data) - - print(data.to_ndarray()) - - for mi, vec in enumerate(data): - - print(mi, vec) - - if mi > 10: - exit() - diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 8cbfe0d53..1a2b189cc 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -146,6 +146,31 @@ def test_encoder_pix_fmt(self): self.assertEqual(str(cm.exception), "not a pixel format: '__unknown_pix_fmt'") self.assertEqual(ctx.pix_fmt, "yuv420p") + def test_bits_per_coded_sample(self): + with av.open(fate_suite("qtrle/aletrek-rle.mov")) as container: + stream = container.streams.video[0] + stream.bits_per_coded_sample = 32 + + for packet in container.demux(stream): + for frame in packet.decode(): + pass + self.assertEqual(packet.stream.bits_per_coded_sample, 32) + + with av.open(fate_suite("qtrle/aletrek-rle.mov")) as container: + stream = container.streams.video[0] + stream.bits_per_coded_sample = 31 + + with self.assertRaises(av.error.InvalidDataError): + for packet in container.demux(stream): + for frame in packet.decode(): + pass + + with av.open(self.sandboxed("output.mov"), "w") as output: + stream = output.add_stream("qtrle") + + with self.assertRaises(ValueError): + stream.codec_context.bits_per_coded_sample = 32 + def test_parse(self): # This one parses into a single packet. self._assert_parse("mpeg4", fate_suite("h264/interlaced_crop.mp4")) diff --git a/tests/test_file_probing.py b/tests/test_file_probing.py index c3d239c50..928a5bd1d 100644 --- a/tests/test_file_probing.py +++ b/tests/test_file_probing.py @@ -34,20 +34,12 @@ def test_stream_probing(self): ) # actual stream properties - self.assertEqual(stream.average_rate, None) - self.assertEqual(stream.base_rate, None) - self.assertEqual(stream.guessed_rate, None) self.assertEqual(stream.duration, 554880) self.assertEqual(stream.frames, 0) self.assertEqual(stream.id, 256) self.assertEqual(stream.index, 0) self.assertEqual(stream.language, "eng") - self.assertEqual( - stream.metadata, - { - "language": "eng", - }, - ) + self.assertEqual(stream.metadata, {"language": "eng"}) self.assertEqual(stream.profile, "LC") self.assertEqual(stream.start_time, 126000) self.assertEqual(stream.time_base, Fraction(1, 90000)) @@ -98,9 +90,6 @@ def test_stream_probing(self): ) # actual stream properties - self.assertEqual(stream.average_rate, None) - self.assertEqual(stream.base_rate, None) - self.assertEqual(stream.guessed_rate, None) self.assertEqual(stream.duration, None) self.assertEqual(stream.frames, 0) self.assertEqual(stream.id, 0) @@ -239,7 +228,6 @@ def test_stream_probing(self): ) # actual stream properties - self.assertEqual(stream.average_rate, None) self.assertEqual(stream.duration, 8140) self.assertEqual(stream.frames, 6) self.assertEqual(stream.id, 1) diff --git a/tests/test_packet.py b/tests/test_packet.py new file mode 100644 index 000000000..8a2b6266c --- /dev/null +++ b/tests/test_packet.py @@ -0,0 +1,41 @@ +import av + +from .common import TestCase, fate_suite + + +class TestProperties(TestCase): + def test_is_keyframe(self): + with av.open(fate_suite("h264/interlaced_crop.mp4")) as container: + stream = container.streams.video[0] + for i, packet in enumerate(container.demux(stream)): + if i in (0, 21, 45, 69, 93, 117): + self.assertTrue(packet.is_keyframe) + else: + self.assertFalse(packet.is_keyframe) + + def test_is_corrupt(self): + with av.open(fate_suite("mov/white_zombie_scrunch-part.mov")) as container: + stream = container.streams.video[0] + for i, packet in enumerate(container.demux(stream)): + if i == 65: + self.assertTrue(packet.is_corrupt) + else: + self.assertFalse(packet.is_corrupt) + + def test_is_discard(self): + with av.open(fate_suite("mov/mov-1elist-ends-last-bframe.mov")) as container: + stream = container.streams.video[0] + for i, packet in enumerate(container.demux(stream)): + if i == 46: + self.assertTrue(packet.is_discard) + else: + self.assertFalse(packet.is_discard) + + def test_is_disposable(self): + with av.open(fate_suite("hap/HAPQA_NoSnappy_127x1.mov")) as container: + stream = container.streams.video[0] + for i, packet in enumerate(container.demux(stream)): + if i == 0: + self.assertTrue(packet.is_disposable) + else: + self.assertFalse(packet.is_disposable) diff --git a/tests/test_subtitles.py b/tests/test_subtitles.py index c457124f2..fe4f2ee3d 100644 --- a/tests/test_subtitles.py +++ b/tests/test_subtitles.py @@ -24,7 +24,7 @@ def test_movtext(self): sub = subset[0] self.assertIsInstance(sub, AssSubtitle) self.assertEqual(sub.type, b"ass") - self.assertEqual(sub.ass, "0,0,Default,,0,0,0,,- Test 1.\\N- Test 2.") + self.assertEqual(sub.ass, b"0,0,Default,,0,0,0,,- Test 1.\\N- Test 2.") def test_vobsub(self): path = fate_suite("sub/vobsub.sub")