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: