From c25219e60f27091aa1ee6bb69731d6a7ba3b29bf Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 18 Jan 2025 21:37:38 -0500 Subject: [PATCH 1/3] test --- av/container/output.pyx | 94 +++++++++++++++++++++++++++++++- include/libav.pxd | 5 +- include/libavcodec/avcodec.pxd | 14 +++++ include/libavformat/avformat.pxd | 4 +- include/libavutil/avutil.pxd | 1 + 5 files changed, 113 insertions(+), 5 deletions(-) diff --git a/av/container/output.pyx b/av/container/output.pyx index e61ef2297..a3bde803c 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -4,6 +4,9 @@ from fractions import Fraction cimport libav as lib +from libc.string cimport memcpy +from libc.stdint cimport uint8_t + from av.codec.codec cimport Codec from av.codec.context cimport CodecContext, wrap_codec_context from av.container.streams cimport StreamContainer @@ -247,6 +250,95 @@ cdef class OutputContainer(Container): return py_stream + def add_attachment(self, file_data, filename, mimetype=None): + """Add a file as an attachment stream. + + Creates a new attachment stream and adds the provided file data to it. + This is particularly useful for formats like Matroska that support file attachments + (e.g., fonts, images, etc.) + + :param file_data: The raw file data to attach + :type file_data: bytes + :param filename: Name of the attached file + :type filename: str + :param mimetype: MIME type of the file (optional, will attempt to guess if not provided) + :type mimetype: str | None + :rtype: The new :class:`~av.attachments.stream.AttachmentStream` + """ + cdef lib.AVStream *stream + cdef size_t data_len + cdef uint8_t *attachment + cdef Stream py_stream + cdef lib.AVCodecContext *codec_context + + if not isinstance(file_data, bytes): + raise TypeError("file_data must be bytes") + + if not filename: + raise ValueError("filename must be provided") + + # For PNG attachments, we create a stream with the PNG codec + codec = lib.avcodec_find_encoder_by_name("png") + if codec == NULL: + raise ValueError("PNG codec not available") + + # Create new stream + stream = lib.avformat_new_stream(self.ptr, codec) + if stream == NULL: + raise MemoryError("Could not allocate stream") + + # Create and setup codec context + codec_context = lib.avcodec_alloc_context3(codec) + if codec_context == NULL: + raise MemoryError("Could not allocate codec context") + + try: + # Set basic video parameters + codec_context.width = 1 # Minimal valid dimensions + codec_context.height = 1 + codec_context.time_base.num = 1 + codec_context.time_base.den = 1 + codec_context.pix_fmt = lib.AV_PIX_FMT_RGBA + + # Apply codec parameters to stream + err_check(lib.avcodec_parameters_from_context(stream.codecpar, codec_context)) + + # Mark as attachment + stream.disposition = 0x0400 + + # Allocate and copy attachment data + data_len = len(file_data) + attachment = lib.av_malloc(data_len) + if attachment == NULL: + raise MemoryError("Could not allocate attachment buffer") + + # Copy the data + memcpy(attachment, file_data, data_len) + + # Set the extradata to hold our attachment + stream.codecpar.extradata = attachment + stream.codecpar.extradata_size = data_len + + # Add metadata + err_check(lib.av_dict_set(&stream.metadata, "filename", filename.encode('utf-8'), 0)) + + if mimetype is not None: + err_check(lib.av_dict_set(&stream.metadata, "mimetype", mimetype.encode('utf-8'), 0)) + + # Create Python stream object + py_stream = wrap_stream(self, stream, wrap_codec_context(codec_context, codec, None)) + self.streams.add_stream(py_stream) + + return py_stream + + except Exception: + if attachment != NULL: + lib.av_free(attachment) + if codec_context != NULL: + lib.avcodec_free_context(&codec_context) + raise + + cpdef start_encoding(self): """Write the file header! Called automatically.""" @@ -263,7 +355,7 @@ cdef class OutputContainer(Container): ctx = stream.codec_context # Skip codec context handling for data streams without codecs if ctx is None: - if stream.type != "data": + if stream.type not in ("data", "attachment"): raise ValueError(f"Stream {stream.index} has no codec context") continue diff --git a/include/libav.pxd b/include/libav.pxd index e2fe323a4..716a58195 100644 --- a/include/libav.pxd +++ b/include/libav.pxd @@ -23,7 +23,10 @@ include "libavfilter/buffersink.pxd" include "libavfilter/buffersrc.pxd" -cdef extern from "stdio.h" nogil: +cdef extern from "libavutil/mem.h": + void* av_malloc(size_t size) nogil + void av_free(void* ptr) nogil +cdef extern from "stdio.h" nogil: cdef int snprintf(char *output, int n, const char *format, ...) cdef int vsnprintf(char *output, int n, const char *format, va_list args) diff --git a/include/libavcodec/avcodec.pxd b/include/libavcodec/avcodec.pxd index bcb342373..ee832dd04 100644 --- a/include/libavcodec/avcodec.pxd +++ b/include/libavcodec/avcodec.pxd @@ -8,6 +8,16 @@ cdef extern from "libavcodec/codec_id.h": AVCodecID av_codec_get_id(const AVCodecTag *const *tags, uint32_t tag) +cdef extern from "libavcodec/packet.h" nogil: + AVPacketSideData* av_packet_side_data_new( + AVPacketSideData **sides, + int *nb_sides, + AVPacketSideDataType type, + size_t size, + int free_opaque + ) + + cdef extern from "libavutil/channel_layout.h": ctypedef enum AVChannelOrder: AV_CHANNEL_ORDER_UNSPEC @@ -542,6 +552,10 @@ cdef extern from "libavcodec/avcodec.h" nogil: cdef struct AVCodecParameters: AVMediaType codec_type AVCodecID codec_id + AVPacketSideData *coded_side_data + int nb_coded_side_data + uint8_t *extradata + int extradata_size cdef int avcodec_parameters_copy( AVCodecParameters *dst, diff --git a/include/libavformat/avformat.pxd b/include/libavformat/avformat.pxd index 5fa25043a..cec89bd38 100644 --- a/include/libavformat/avformat.pxd +++ b/include/libavformat/avformat.pxd @@ -30,18 +30,16 @@ cdef extern from "libavformat/avformat.h" nogil: cdef struct AVStream: int index int id + int disposition AVCodecParameters *codecpar - AVRational time_base int64_t start_time int64_t duration int64_t nb_frames int64_t cur_dts - AVDictionary *metadata - AVRational avg_frame_rate AVRational r_frame_rate AVRational sample_aspect_ratio diff --git a/include/libavutil/avutil.pxd b/include/libavutil/avutil.pxd index 58dd43922..49be65f69 100644 --- a/include/libavutil/avutil.pxd +++ b/include/libavutil/avutil.pxd @@ -30,6 +30,7 @@ cdef extern from "libavutil/avutil.h" nogil: cdef enum AVPixelFormat: AV_PIX_FMT_NONE AV_PIX_FMT_YUV420P + AV_PIX_FMT_RGBA AV_PIX_FMT_RGB24 PIX_FMT_RGB24 PIX_FMT_RGBA From 8b10ef86b82cef572970edf8b0c0114f582c2246 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 18 Jan 2025 21:57:01 -0500 Subject: [PATCH 2/3] test2 --- av/container/output.pyx | 61 +++++++++-------------------------------- 1 file changed, 13 insertions(+), 48 deletions(-) diff --git a/av/container/output.pyx b/av/container/output.pyx index a3bde803c..616d40ae6 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -250,26 +250,13 @@ cdef class OutputContainer(Container): return py_stream + def add_attachment(self, file_data, filename, mimetype=None): - """Add a file as an attachment stream. - - Creates a new attachment stream and adds the provided file data to it. - This is particularly useful for formats like Matroska that support file attachments - (e.g., fonts, images, etc.) - - :param file_data: The raw file data to attach - :type file_data: bytes - :param filename: Name of the attached file - :type filename: str - :param mimetype: MIME type of the file (optional, will attempt to guess if not provided) - :type mimetype: str | None - :rtype: The new :class:`~av.attachments.stream.AttachmentStream` - """ + """Add a file as an attachment stream.""" cdef lib.AVStream *stream cdef size_t data_len cdef uint8_t *attachment cdef Stream py_stream - cdef lib.AVCodecContext *codec_context if not isinstance(file_data, bytes): raise TypeError("file_data must be bytes") @@ -277,34 +264,14 @@ cdef class OutputContainer(Container): if not filename: raise ValueError("filename must be provided") - # For PNG attachments, we create a stream with the PNG codec - codec = lib.avcodec_find_encoder_by_name("png") - if codec == NULL: - raise ValueError("PNG codec not available") - - # Create new stream - stream = lib.avformat_new_stream(self.ptr, codec) + # Create new stream without codec + stream = lib.avformat_new_stream(self.ptr, NULL) if stream == NULL: raise MemoryError("Could not allocate stream") - # Create and setup codec context - codec_context = lib.avcodec_alloc_context3(codec) - if codec_context == NULL: - raise MemoryError("Could not allocate codec context") - try: - # Set basic video parameters - codec_context.width = 1 # Minimal valid dimensions - codec_context.height = 1 - codec_context.time_base.num = 1 - codec_context.time_base.den = 1 - codec_context.pix_fmt = lib.AV_PIX_FMT_RGBA - - # Apply codec parameters to stream - err_check(lib.avcodec_parameters_from_context(stream.codecpar, codec_context)) - - # Mark as attachment - stream.disposition = 0x0400 + # Mark as attachment type + stream.codecpar.codec_type = lib.AVMEDIA_TYPE_ATTACHMENT # Allocate and copy attachment data data_len = len(file_data) @@ -315,30 +282,28 @@ cdef class OutputContainer(Container): # Copy the data memcpy(attachment, file_data, data_len) - # Set the extradata to hold our attachment + # Store attachment in codecpar extradata stream.codecpar.extradata = attachment stream.codecpar.extradata_size = data_len # Add metadata err_check(lib.av_dict_set(&stream.metadata, "filename", filename.encode('utf-8'), 0)) + err_check(lib.av_dict_set(&stream.metadata, "mimetype", mimetype.encode('utf-8'), 0)) - if mimetype is not None: - err_check(lib.av_dict_set(&stream.metadata, "mimetype", mimetype.encode('utf-8'), 0)) + # Explicitly set time_base to avoid duration issues + stream.time_base.num = 1 + stream.time_base.den = 1 # Create Python stream object - py_stream = wrap_stream(self, stream, wrap_codec_context(codec_context, codec, None)) + py_stream = wrap_stream(self, stream, None) self.streams.add_stream(py_stream) return py_stream except Exception: - if attachment != NULL: - lib.av_free(attachment) - if codec_context != NULL: - lib.avcodec_free_context(&codec_context) + lib.av_free(attachment) raise - cpdef start_encoding(self): """Write the file header! Called automatically.""" From d772138f64e7544f13be6e565f951eb1cfdebdb7 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Sat, 18 Jan 2025 23:01:44 -0500 Subject: [PATCH 3/3] Add stream.disposition --- CHANGELOG.rst | 1 + av/container/output.pyx | 59 +--------------------------------------- av/stream.pyi | 23 ++++++++++++++++ av/stream.pyx | 31 ++++++++++++++++++++- include/libav.pxd | 4 --- tests/test_colorspace.py | 2 ++ 6 files changed, 57 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7baadb4a8..2f0bd681c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -23,6 +23,7 @@ v14.1.0 (Unreleased) Features - Add hardware decoding by :gh-user:`matthewlai` and :gh-user:`WyattBlue` in (:pr:`1685`). +- Add ``Stream.disposition`` and ``Disposition`` enum by :gh-user:`WyattBlue` in (:pr:`1720`). - Add ``VideoFrame.rotation`` by :gh-user:`lgeiger` in (:pr:`1675`). - Support grayf32le and gbrapf32le in numpy convertion by :gh-user:`robinechuca` in (:pr:`1712`). diff --git a/av/container/output.pyx b/av/container/output.pyx index 616d40ae6..e61ef2297 100644 --- a/av/container/output.pyx +++ b/av/container/output.pyx @@ -4,9 +4,6 @@ from fractions import Fraction cimport libav as lib -from libc.string cimport memcpy -from libc.stdint cimport uint8_t - from av.codec.codec cimport Codec from av.codec.context cimport CodecContext, wrap_codec_context from av.container.streams cimport StreamContainer @@ -250,60 +247,6 @@ cdef class OutputContainer(Container): return py_stream - - def add_attachment(self, file_data, filename, mimetype=None): - """Add a file as an attachment stream.""" - cdef lib.AVStream *stream - cdef size_t data_len - cdef uint8_t *attachment - cdef Stream py_stream - - if not isinstance(file_data, bytes): - raise TypeError("file_data must be bytes") - - if not filename: - raise ValueError("filename must be provided") - - # Create new stream without codec - stream = lib.avformat_new_stream(self.ptr, NULL) - if stream == NULL: - raise MemoryError("Could not allocate stream") - - try: - # Mark as attachment type - stream.codecpar.codec_type = lib.AVMEDIA_TYPE_ATTACHMENT - - # Allocate and copy attachment data - data_len = len(file_data) - attachment = lib.av_malloc(data_len) - if attachment == NULL: - raise MemoryError("Could not allocate attachment buffer") - - # Copy the data - memcpy(attachment, file_data, data_len) - - # Store attachment in codecpar extradata - stream.codecpar.extradata = attachment - stream.codecpar.extradata_size = data_len - - # Add metadata - err_check(lib.av_dict_set(&stream.metadata, "filename", filename.encode('utf-8'), 0)) - err_check(lib.av_dict_set(&stream.metadata, "mimetype", mimetype.encode('utf-8'), 0)) - - # Explicitly set time_base to avoid duration issues - stream.time_base.num = 1 - stream.time_base.den = 1 - - # Create Python stream object - py_stream = wrap_stream(self, stream, None) - self.streams.add_stream(py_stream) - - return py_stream - - except Exception: - lib.av_free(attachment) - raise - cpdef start_encoding(self): """Write the file header! Called automatically.""" @@ -320,7 +263,7 @@ cdef class OutputContainer(Container): ctx = stream.codec_context # Skip codec context handling for data streams without codecs if ctx is None: - if stream.type not in ("data", "attachment"): + if stream.type != "data": raise ValueError(f"Stream {stream.index} has no codec context") continue diff --git a/av/stream.pyi b/av/stream.pyi index 82bb672b2..a2a2e439c 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -1,9 +1,31 @@ +from enum import Flag from fractions import Fraction from typing import Literal from .codec import Codec, CodecContext from .container import Container +class Disposition(Flag): + default: int + dub: int + original: int + comment: int + lyrics: int + karaoke: int + forced: int + hearing_impaired: int + visual_impaired: int + clean_effects: int + attached_pic: int + timed_thumbnails: int + non_diegetic: int + captions: int + descriptions: int + metadata: int + dependent: int + still_image: int + multilayer: int + class Stream: name: str | None container: Container @@ -20,6 +42,7 @@ class Stream: guessed_rate: Fraction | None start_time: int | None duration: int | None + disposition: Disposition frames: int language: str | None type: Literal["video", "audio", "data", "subtitle", "attachment"] diff --git a/av/stream.pyx b/av/stream.pyx index 35b85acdf..d0ecf37ad 100644 --- a/av/stream.pyx +++ b/av/stream.pyx @@ -1,6 +1,6 @@ cimport libav as lib -from enum import Enum +from enum import Flag from av.error cimport err_check from av.packet cimport Packet @@ -12,6 +12,28 @@ from av.utils cimport ( ) +class Disposition(Flag): + default = 1 << 0 + dub = 1 << 1 + original = 1 << 2 + comment = 1 << 3 + lyrics = 1 << 4 + karaoke = 1 << 5 + forced = 1 << 6 + hearing_impaired = 1 << 7 + visual_impaired = 1 << 8 + clean_effects = 1 << 9 + attached_pic = 1 << 10 + timed_thumbnails = 1 << 11 + non_diegetic = 1 << 12 + captions = 1 << 16 + descriptions = 1 << 17 + metadata = 1 << 18 + dependent = 1 << 19 + still_image = 1 << 20 + multilayer = 1 << 21 + + cdef object _cinit_bypass_sentinel = object() cdef Stream wrap_stream(Container container, lib.AVStream *c_stream, CodecContext codec_context): @@ -96,6 +118,9 @@ cdef class Stream: if name == "id": self._set_id(value) return + if name == "disposition": + self.ptr.disposition = value + return # Convenience setter for codec context properties. if self.codec_context is not None: @@ -230,6 +255,10 @@ cdef class Stream: """ return self.metadata.get("language") + @property + def disposition(self): + return Disposition(self.ptr.disposition) + @property def type(self): """ diff --git a/include/libav.pxd b/include/libav.pxd index 716a58195..568913208 100644 --- a/include/libav.pxd +++ b/include/libav.pxd @@ -23,10 +23,6 @@ include "libavfilter/buffersink.pxd" include "libavfilter/buffersrc.pxd" -cdef extern from "libavutil/mem.h": - void* av_malloc(size_t size) nogil - void av_free(void* ptr) nogil - cdef extern from "stdio.h" nogil: cdef int snprintf(char *output, int n, const char *format, ...) cdef int vsnprintf(char *output, int n, const char *format, va_list args) diff --git a/tests/test_colorspace.py b/tests/test_colorspace.py index 7574c147d..c76416c80 100644 --- a/tests/test_colorspace.py +++ b/tests/test_colorspace.py @@ -31,6 +31,8 @@ def test_sky_timelapse() -> None: ) stream = container.streams.video[0] + assert stream.disposition == av.stream.Disposition.default + assert stream.codec_context.color_range == 1 assert stream.codec_context.color_range == ColorRange.MPEG assert stream.codec_context.color_primaries == 1