From e2b484137ec7615bb7b58900ebfabefd755b15be Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 4 Sep 2024 01:36:49 -0400 Subject: [PATCH] Sync with upstream --- .github/workflows/smoke.yml | 10 ++++----- av/about.py | 2 +- av/audio/layout.pyi | 8 +++++++ av/audio/layout.pyx | 38 +++++++++++++++++++++++++++++--- av/video/frame.pyi | 9 ++++++++ av/video/frame.pyx | 40 +++++++++++++++++++++++++++++----- include/libavcodec/avcodec.pxd | 3 +++ setup.py | 19 ++++++++++++++++ tests/test_audiolayout.py | 5 ++++- 9 files changed, 117 insertions(+), 17 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 03a32bd1c..48be041e2 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -57,16 +57,13 @@ jobs: ubuntu-latest) sudo apt-get update sudo apt-get install autoconf automake build-essential cmake \ - libtool mercurial pkg-config texinfo wget yasm zlib1g-dev - sudo apt-get install libfreetype6-dev libjpeg-dev \ - libtheora-dev libvorbis-dev libx264-dev + libtool pkg-config yasm zlib1g-dev libvorbis-dev libx264-dev if [[ "${{ matrix.config.extras }}" ]]; then - sudo apt-get install doxygen + sudo apt-get install doxygen wget fi ;; macos-12) - brew install automake libtool nasm pkg-config shtool texi2html wget - brew install libjpeg libpng libvorbis libvpx opus theora x264 + brew install automake libtool nasm pkg-config libpng libvorbis libvpx opus x264 ;; esac @@ -138,6 +135,7 @@ jobs: . $CONDA/etc/profile.d/conda.sh conda activate pyav python scripts\\fetch-vendor.py --config-file scripts\\ffmpeg-${{ matrix.config.ffmpeg }}.json $CONDA_PREFIX\\Library + python scripts\\comptime.py ${{ matrix.config.ffmpeg }} python setup.py build_ext --inplace --ffmpeg-dir=$CONDA_PREFIX\\Library - name: Test diff --git a/av/about.py b/av/about.py index 79a759ae1..1311252a5 100644 --- a/av/about.py +++ b/av/about.py @@ -1 +1 @@ -__version__ = "13.0.0rc1" +__version__ = "13.0.0" diff --git a/av/audio/layout.pyi b/av/audio/layout.pyi index 073cd1723..9fdf0ac15 100644 --- a/av/audio/layout.pyi +++ b/av/audio/layout.pyi @@ -1,4 +1,12 @@ +from dataclasses import dataclass + class AudioLayout: name: str nb_channels: int + channels: tuple[AudioChannel, ...] def __init__(self, layout: str | AudioLayout): ... + +@dataclass +class AudioChannel: + name: str + description: str diff --git a/av/audio/layout.pyx b/av/audio/layout.pyx index 59753138e..ea259d0fd 100644 --- a/av/audio/layout.pyx +++ b/av/audio/layout.pyx @@ -1,5 +1,16 @@ cimport libav as lib +from cpython.bytes cimport PyBytes_FromStringAndSize +from dataclasses import dataclass + + +@dataclass +class AudioChannel: + name: str + description: str + + def __repr__(self): + return f"" cdef object _cinit_bypass_sentinel @@ -40,13 +51,34 @@ cdef class AudioLayout: return self.layout.nb_channels @property - def name(self): + def channels(self): + cdef lib.AVChannel channel + cdef char buf[16] + cdef char buf2[128] + + results = [] + + for index in range(self.layout.nb_channels): + channel = lib.av_channel_layout_channel_from_index(&self.layout, index); + size = lib.av_channel_name(buf, sizeof(buf), channel) - 1 + size2 = lib.av_channel_description(buf2, sizeof(buf2), channel) - 1 + results.append( + AudioChannel( + PyBytes_FromStringAndSize(buf, size).decode("utf-8"), + PyBytes_FromStringAndSize(buf2, size2).decode("utf-8"), + ) + ) + + return tuple(results) + + @property + def name(self) -> str: """The canonical name of the audio layout.""" - cdef char layout_name[128] # Adjust buffer size as needed + cdef char layout_name[128] cdef int ret ret = lib.av_channel_layout_describe(&self.layout, layout_name, sizeof(layout_name)) if ret < 0: raise RuntimeError(f"Failed to get layout name: {ret}") - return layout_name + return layout_name \ No newline at end of file diff --git a/av/video/frame.pyi b/av/video/frame.pyi index 17cacf05a..de84faaa0 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -63,3 +63,12 @@ class VideoFrame(Frame): ) -> VideoFrame: ... @staticmethod def from_ndarray(array: _SupportedNDarray, format: str = "rgb24") -> VideoFrame: ... + @staticmethod + def from_bytes( + data: bytes, + width: int, + height: int, + format: str = "rgba", + flip_horizontal: bool = False, + flip_vertical: bool = False, + ) -> VideoFrame: ... diff --git a/av/video/frame.pyx b/av/video/frame.pyx index c27ead7b4..ee060e16a 100644 --- a/av/video/frame.pyx +++ b/av/video/frame.pyx @@ -40,9 +40,8 @@ cdef byteswap_array(array, bint big_endian): return array -cdef copy_array_to_plane(array, VideoPlane plane, unsigned int bytes_per_pixel): - cdef bytes imgbytes = array.tobytes() - cdef const uint8_t[:] i_buf = imgbytes +cdef copy_bytes_to_plane(img_bytes, VideoPlane plane, unsigned int bytes_per_pixel, bint flip_horizontal, bint flip_vertical): + cdef const uint8_t[:] i_buf = img_bytes cdef size_t i_pos = 0 cdef size_t i_stride = plane.width * bytes_per_pixel cdef size_t i_size = plane.height * i_stride @@ -51,12 +50,33 @@ cdef copy_array_to_plane(array, VideoPlane plane, unsigned int bytes_per_pixel): cdef size_t o_pos = 0 cdef size_t o_stride = abs(plane.line_size) - while i_pos < i_size: - o_buf[o_pos:o_pos + i_stride] = i_buf[i_pos:i_pos + i_stride] - i_pos += i_stride + cdef int start_row, end_row, step + if flip_vertical: + start_row = plane.height - 1 + end_row = -1 + step = -1 + else: + start_row = 0 + end_row = plane.height + step = 1 + + cdef int i, j + for row in range(start_row, end_row, step): + i_pos = row * i_stride + if flip_horizontal: + for i in range(0, i_stride, bytes_per_pixel): + for j in range(bytes_per_pixel): + o_buf[o_pos + i + j] = i_buf[i_pos + i_stride - i - bytes_per_pixel + j] + else: + o_buf[o_pos:o_pos + i_stride] = i_buf[i_pos:i_pos + i_stride] o_pos += o_stride +cdef copy_array_to_plane(array, VideoPlane plane, unsigned int bytes_per_pixel): + cdef bytes imgbytes = array.tobytes() + copy_bytes_to_plane(imgbytes, plane, bytes_per_pixel, False, False) + + cdef useful_array(VideoPlane plane, unsigned int bytes_per_pixel=1, str dtype="uint8"): """ Return the useful part of the VideoPlane as a single dimensional array. @@ -570,3 +590,11 @@ cdef class VideoFrame(Frame): copy_array_to_plane(array, frame.planes[0], 1 if array.ndim == 2 else array.shape[2]) return frame + + def from_bytes(img_bytes: bytes, width: int, height: int, format="rgba", flip_horizontal=False, flip_vertical=False): + frame = VideoFrame(width, height, format) + if format == "rgba": + copy_bytes_to_plane(img_bytes, frame.planes[0], 4, flip_horizontal, flip_vertical) + else: + raise NotImplementedError(f"Format '{format}' is not supported.") + return frame diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index 9e9cc46a7..9add5ae2d 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -42,6 +42,9 @@ cdef extern from "libavutil/channel_layout.h": void av_channel_layout_uninit(AVChannelLayout *channel_layout) int av_channel_layout_copy(AVChannelLayout *dst, const AVChannelLayout *src) int av_channel_layout_describe(const AVChannelLayout *channel_layout, char *buf, size_t buf_size) + int av_channel_name(char *buf, size_t buf_size, AVChannel channel_id) + int av_channel_description(char *buf, size_t buf_size, AVChannel channel_id) + AVChannel av_channel_layout_channel_from_index(AVChannelLayout *channel_layout, unsigned int idx) cdef extern from "libavcodec/avcodec.h" nogil: diff --git a/setup.py b/setup.py index 69e6c467d..6544304ae 100644 --- a/setup.py +++ b/setup.py @@ -26,6 +26,21 @@ old_embed_signature = EmbedSignature._embed_signature +def insert_enum_in_generated_files(source): + # Work around Cython failing to add `enum` to `AVChannel` type. + # TODO: Make Cython bug report + if source.endswith(".c"): + with open(source, "r") as file: + content = file.read() + + # Replace "AVChannel __pyx_v_channel;" with "enum AVChannel __pyx_v_channel;" + modified_content = re.sub( + r"\b(? None: self.assertEqual(layout.name, "stereo") self.assertEqual(layout.nb_channels, 2) self.assertEqual(repr(layout), "") + + # Re-enable when FFmpeg 6.0 is dropped. + # self.assertEqual(layout.channels[0].name, "FL") # self.assertEqual(layout.channels[0].description, "front left") # self.assertEqual(