From 203da84f6292550444e367bdf466840025d3a967 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 4 Nov 2026 18:28:49 -0500 Subject: [PATCH] Preserve selected codec across a failed CodecContext.open() A failed avcodec_open2() runs FFmpeg's cleanup (ff_codec_close), which sets avctx->codec to NULL. That left the context in a broken state: codec, name, is_encoder, and is_decoder would raise or crash, and the context could not be reused. Capture the codec pointer before avcodec_open2() and restore it on failure, in open() alone, so every reader of ptr.codec stays correct and the context can be reopened after fixing the offending parameters. The failed open still resets other fields (notably layout drops to 0 channels), so a retry must re-apply the parameters set before; this is documented in the open() docstring. --- CHANGELOG.rst | 1 + av/codec/context.py | 21 ++++++++++++++++++--- tests/test_codec_context.py | 24 ++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0232959d7..db17ef752 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -53,6 +53,7 @@ Fixes: - ``SubtitleCodecContext.decode(None)`` flushes the decoder instead of raising ``RuntimeError``, so delayed decoders such as EIA-608 closed captions return their final subtitle. - ``AudioFrame.planes`` no longer counts one plane too many for 8-channel planar audio (e.g. ``fltp`` with ``7.1``), which made ``planes``, ``to_ndarray()``, and ``from_ndarray()`` raise ``ValueError``, and no longer reads past the end of ``extended_data`` for more channels. By :gh-user:`ChandraChaan` in (:pr:`2502`). - ``AudioFifo.write()`` now raises ``ValueError`` for a frame whose channel layout differs from the first frame's, instead of reading the wrong number of channels and scrambling or over-reading the samples. By :gh-user:`ChandraChaan` in (:pr:`2505`). +- A failed ``CodecContext.open()`` no longer clears the selected codec, so ``codec``, ``name``, ``is_encoder``, and ``is_decoder`` keep working and the context can be reused after fixing the offending parameters. v19.0.2 ------- diff --git a/av/codec/context.py b/av/codec/context.py index 8a27d979f..e3a3ec86e 100644 --- a/av/codec/context.py +++ b/av/codec/context.py @@ -412,6 +412,16 @@ def is_open(self): @cython.ccall def open(self, strict: cython.bint = True): + """open(strict=True) + + Open the codec, readying the context to encode or decode. + + The selected codec is preserved if opening fails, so the context stays + inspectable and can be retried after fixing the offending parameters. + FFmpeg's failed-open cleanup still resets other fields, though (notably + ``layout`` drops to 0 channels), so a retry must re-apply the parameters + that were set before the failed attempt. + """ if lib.avcodec_is_open(self.ptr): if strict: raise ValueError("CodecContext is already open.") @@ -433,10 +443,15 @@ def open(self, strict: cython.bint = True): self._setup_encode_hwframes() - err_check( - lib.avcodec_open2(self.ptr, self.ptr.codec, cython.address(options.ptr)), - f'avcodec_open2("{self.ptr.codec.name}", {self.options})', + # AVCodec structs are static globals, so holding the pointer across the + # call is safe. + codec: cython.pointer[cython.const[lib.AVCodec]] = self.ptr.codec + ret: cython.int = lib.avcodec_open2( + self.ptr, codec, cython.address(options.ptr) ) + if ret < 0: + self.ptr.codec = codec + err_check(ret, f'avcodec_open2("{codec.name}", {self.options})') self.options = dict(options) def __dealloc__(self): diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 01384d1fd..ce1bb20cf 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -351,6 +351,30 @@ def test_parse_assigns_packet_timing(self) -> None: # so at least some packets should carry it through. assert any(p.pos is not None for p in packets) + def test_failed_open_preserves_codec(self) -> None: + # A failed avcodec_open2() runs FFmpeg's cleanup, which clears + # avctx->codec. open() must restore it so the context stays usable. + ctx = av.AudioCodecContext.create("aac", "w") + ctx.format = av.AudioFormat("s16") # AAC wants fltp, so open() fails. + ctx.layout = "stereo" + ctx.sample_rate = 44100 + + with pytest.raises(av.error.FFmpegError): + ctx.open() + + # The selected codec is still known after the failure. + assert ctx.codec is not None + assert ctx.codec.name == "aac" + assert ctx.is_encoder and not ctx.is_decoder + assert not ctx.is_open + + # Fixing the offending parameters and retrying works on the same context. + ctx.format = av.AudioFormat("fltp") + ctx.layout = "stereo" + ctx.sample_rate = 44100 + ctx.open() + assert ctx.is_open + class TestEncoding(TestCase): def test_encoding_png(self) -> None: