From 63d8ca906e125f9a89438db256396d3221e98970 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 8 Apr 2026 00:43:33 -0400 Subject: [PATCH 1/3] Make VideoCodecContext 12 bytes smaller --- av/codec/context.pxd | 13 +++++-------- av/codec/context.py | 4 +--- av/container/output.py | 4 ++-- av/video/codeccontext.pxd | 2 -- av/video/codeccontext.py | 4 ---- 5 files changed, 8 insertions(+), 19 deletions(-) diff --git a/av/codec/context.pxd b/av/codec/context.pxd index 08d24761e..7fd26d56e 100644 --- a/av/codec/context.pxd +++ b/av/codec/context.pxd @@ -1,5 +1,5 @@ cimport libav as lib -from libc.stdint cimport int64_t +from libc.stdint cimport int64_t, uint8_t from av.buffer cimport ByteSource from av.codec.codec cimport Codec @@ -11,13 +11,6 @@ from av.packet cimport Packet cdef class CodecContext: cdef lib.AVCodecContext *ptr - # Whether AVCodecContext.extradata should be de-allocated upon destruction. - cdef bint extradata_set - - # True when created via add_stream_from_template(); start_encoding() skips - # avcodec_open2() and lets encode()/decode() open the codec lazily if needed. - cdef readonly bint _template_initialized - # Used as a signal that this is within a stream, and also for us to access that # stream. This is set "manually" by the stream after constructing this object. cdef int stream_index @@ -40,6 +33,10 @@ cdef class CodecContext: # Used by hardware-accelerated decode. cdef HWAccel hwaccel_ctx + cdef uint8_t _ctxflags # ctxEnum: template_initialized + # True when created via add_stream_from_template(); start_encoding() skips + # avcodec_open2() and lets encode()/decode() open the codec lazily if needed. + # Used by both transcode APIs to setup user-land objects. # TODO: Remove the `Packet` from `_setup_decoded_frame` (because flushing packets # are bogus). It should take all info it needs from the context and/or stream. diff --git a/av/codec/context.py b/av/codec/context.py index c348353cd..fc9721754 100644 --- a/av/codec/context.py +++ b/av/codec/context.py @@ -206,7 +206,6 @@ def extradata(self, data): raise MemoryError("Cannot allocate extradata") memcpy(self.ptr.extradata, source.ptr, source.length) self.ptr.extradata_size = source.length - self.extradata_set = True @property def extradata_size(self): @@ -253,9 +252,8 @@ def open(self, strict: cython.bint = True): self.options = dict(options) def __dealloc__(self): - if self.ptr and self.extradata_set: - lib.av_freep(cython.address(self.ptr.extradata)) if self.ptr: + lib.av_freep(cython.address(self.ptr.extradata)) lib.avcodec_free_context(cython.address(self.ptr)) if self.parser: lib.av_parser_close(self.parser) diff --git a/av/container/output.py b/av/container/output.py index 61a370c06..79389c2cd 100644 --- a/av/container/output.py +++ b/av/container/output.py @@ -275,7 +275,7 @@ def add_stream_from_template( # Construct the user-land stream py_codec_context: CodecContext = wrap_codec_context(ctx, codec, None) - py_codec_context._template_initialized = True + py_codec_context._ctxflags |= 1 # _template_initialized = True py_stream: Stream = wrap_stream(self, stream, py_codec_context) self.streams.add_stream(py_stream) @@ -446,7 +446,7 @@ def start_encoding(self): for k, v in self.options.items(): ctx.options.setdefault(k, v) - if not ctx._template_initialized: + if not (ctx._ctxflags & 1): # template_initialized ctx.open() # Track option consumption. diff --git a/av/video/codeccontext.pxd b/av/video/codeccontext.pxd index 8d128348a..bbefcda56 100644 --- a/av/video/codeccontext.pxd +++ b/av/video/codeccontext.pxd @@ -19,8 +19,6 @@ cdef class VideoCodecContext(CodecContext): cdef AVCodecPrivateData _private_data cdef VideoFormat _format cdef _build_format(self) - cdef int last_w - cdef int last_h cdef readonly VideoReformatter reformatter cdef readonly int encoded_frame_count cdef VideoFrame next_frame diff --git a/av/video/codeccontext.py b/av/video/codeccontext.py index e62bf8fae..a054d48d7 100644 --- a/av/video/codeccontext.py +++ b/av/video/codeccontext.py @@ -39,10 +39,6 @@ def _get_hw_format( @cython.cclass class VideoCodecContext(CodecContext): - def __cinit__(self, *args, **kwargs): - self.last_w = 0 - self.last_h = 0 - @cython.cfunc def _init( self, From 35dd7f66b29adbe5c13851871008b02eea8717f5 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 8 Apr 2026 01:07:11 -0400 Subject: [PATCH 2/3] VideoCodecContext: build format lazily --- av/video/codeccontext.pxd | 2 -- av/video/codeccontext.py | 28 +++++++++------------------- 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/av/video/codeccontext.pxd b/av/video/codeccontext.pxd index bbefcda56..831e5c659 100644 --- a/av/video/codeccontext.pxd +++ b/av/video/codeccontext.pxd @@ -17,8 +17,6 @@ cdef struct AVCodecPrivateData: cdef class VideoCodecContext(CodecContext): cdef AVCodecPrivateData _private_data - cdef VideoFormat _format - cdef _build_format(self) cdef readonly VideoReformatter reformatter cdef readonly int encoded_frame_count cdef VideoFrame next_frame diff --git a/av/video/codeccontext.py b/av/video/codeccontext.py index a054d48d7..49ed82f52 100644 --- a/av/video/codeccontext.py +++ b/av/video/codeccontext.py @@ -72,7 +72,6 @@ def _init( # is_hwaccel() function on each stream's codec context. self.hwaccel_ctx = None - self._build_format() self.encoded_frame_count = 0 @cython.cfunc @@ -80,13 +79,10 @@ def _prepare_frames_for_encode(self, input: Frame | None) -> list: if input is None or not input: return [None] - if self._format is None: - raise ValueError("self._format is None, cannot encode") - vframe: VideoFrame = input # Reformat if it doesn't match. if ( - vframe.format.pix_fmt != self._format.pix_fmt + vframe.format.pix_fmt != self.pix_fmt or vframe.width != self.ptr.width or vframe.height != self.ptr.height ): @@ -97,7 +93,7 @@ def _prepare_frames_for_encode(self, input: Frame | None) -> list: vframe, self.ptr.width, self.ptr.height, - self._format, + self.format, threads=self.ptr.thread_count, ) @@ -137,24 +133,19 @@ def _transfer_hwframe(self, frame: Frame): frame_sw.pts = frame.pts return frame_sw - @cython.cfunc - def _build_format(self): - self._format = get_video_format( + @property + def format(self): + return get_video_format( cython.cast(lib.AVPixelFormat, self.ptr.pix_fmt), self.ptr.width, self.ptr.height, ) - @property - def format(self): - return self._format - @format.setter def format(self, format: VideoFormat): self.ptr.pix_fmt = format.pix_fmt self.ptr.width = format.width self.ptr.height = format.height - self._build_format() # Kinda wasteful. @property def width(self): @@ -165,7 +156,6 @@ def width(self): @width.setter def width(self, value: cython.uint): self.ptr.width = value - self._build_format() @property def height(self): @@ -176,7 +166,6 @@ def height(self): @height.setter def height(self, value: cython.uint): self.ptr.height = value - self._build_format() @property def bits_per_coded_sample(self): @@ -195,7 +184,6 @@ def bits_per_coded_sample(self, value: cython.int): raise ValueError("Not supported for encoders") self.ptr.bits_per_coded_sample = value - self._build_format() @property def pix_fmt(self): @@ -204,12 +192,14 @@ def pix_fmt(self): :type: str | None """ - return getattr(self._format, "name", None) + desc: cython.pointer[cython.const[lib.AVPixFmtDescriptor]] = ( + lib.av_pix_fmt_desc_get(cython.cast(lib.AVPixelFormat, self.ptr.pix_fmt)) + ) + return cython.cast(str, desc.name) @pix_fmt.setter def pix_fmt(self, value): self.ptr.pix_fmt = get_pix_fmt(value) - self._build_format() @property def framerate(self): From f50c69dcff900cc1d957d25f32b9b161f26da41b Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 8 Apr 2026 01:33:23 -0400 Subject: [PATCH 3/3] Output Container: avoid swallowing warning --- av/container/output.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/av/container/output.py b/av/container/output.py index 79389c2cd..d0c51cc6b 100644 --- a/av/container/output.py +++ b/av/container/output.py @@ -16,6 +16,11 @@ def close_output(self: OutputContainer): self.streams = StreamContainer() if self._myflag & 12 == 4: # enum.started and not enum.done + # If the underlying Python IO file was already closed (e.g. during GC + # finalization where cycle ordering is undefined), skip the trailer. + if self.file is not None and getattr(self.file.file, "closed", False): + self._myflag |= 8 # enum.done = True + return # We must only ever call av_write_trailer *once*, otherwise we get a # segmentation fault. Therefore no matter whether it succeeds or not # we must absolutely set enum.done. @@ -36,10 +41,7 @@ def __cinit__(self, *args, **kwargs): self.packet_ptr = lib.av_packet_alloc() def __del__(self): - try: - close_output(self) - except Exception: - pass + close_output(self) def __dealloc__(self): with cython.nogil: