Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion av/audio/codeccontext.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def _prepare_frames_for_encode(
) -> list[Frame | None]:
frame: AudioFrame | None = input_frame
allow_var_frame_size: cython.bint = (
self.ptr.codec.capabilities & lib.AV_CODEC_CAP_VARIABLE_FRAME_SIZE
self._codec.capabilities & lib.AV_CODEC_CAP_VARIABLE_FRAME_SIZE
)

# Note that the resampler will simply return an input frame if there is
Expand Down
2 changes: 2 additions & 0 deletions av/codec/context.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ cdef class CodecContext:
# Fields are laid out in declaration order: pointers first, then the small
# scalars, so there are no padding holes between them.
cdef lib.AVCodecContext *ptr
# avcodec_open2() may clear AVCodecContext.codec when opening fails.
cdef const lib.AVCodec *_codec
cdef lib.AVCodecParserContext *parser
cdef public dict options
cdef HWAccel hwaccel_ctx
Expand Down
37 changes: 22 additions & 15 deletions av/codec/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ def supported_options(self):
descriptors only; set options through :attr:`options`.
"""
ctx: cython.pointer[lib.AVCodecContext] = lib.avcodec_alloc_context3(
self.ptr.codec
self._codec
)
child: cython.p_void
private: list[CodecOption] = []
Expand All @@ -290,6 +290,9 @@ def _init(
self.ptr = ptr
if self.ptr.codec and codec and self.ptr.codec != codec:
raise RuntimeError("Wrapping CodecContext with mismatched codec.")
# Keep the selected implementation independently of FFmpeg's mutable
# context: a failed avcodec_open2() clears ptr.codec during cleanup.
self._codec = codec if codec != cython.NULL else self.ptr.codec

# Set reasonable threading defaults.
self.ptr.thread_count = 0 # use as many threads as there are CPUs.
Expand Down Expand Up @@ -390,19 +393,19 @@ def extradata_size(self):

@property
def is_encoder(self):
if self.ptr is cython.NULL:
if self.ptr is cython.NULL or self._codec is cython.NULL:
return False
return bool(lib.av_codec_is_encoder(self.ptr.codec))
return bool(lib.av_codec_is_encoder(self._codec))

@property
def is_decoder(self):
if self.ptr is cython.NULL:
if self.ptr is cython.NULL or self._codec is cython.NULL:
return False
return bool(lib.av_codec_is_decoder(self.ptr.codec))
return bool(lib.av_codec_is_decoder(self._codec))

@property
def codec(self):
return wrap_codec(self.ptr.codec)
return wrap_codec(self._codec)

@property
def is_open(self):
Expand All @@ -412,6 +415,8 @@ def is_open(self):

@cython.ccall
def open(self, strict: cython.bint = True):
if self._codec is cython.NULL:
raise ValueError("Cannot open an unknown codec.")
if lib.avcodec_is_open(self.ptr):
if strict:
raise ValueError("CodecContext is already open.")
Expand All @@ -434,8 +439,8 @@ 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})',
lib.avcodec_open2(self.ptr, self._codec, cython.address(options.ptr)),
f'avcodec_open2("{self._codec.name}", {self.options})',
)
self.options = dict(options)

Expand Down Expand Up @@ -476,9 +481,11 @@ def parse(self, raw_input=None):
"""

if not self.parser:
self.parser = lib.av_parser_init(self.ptr.codec.id)
if self._codec is cython.NULL:
raise ValueError("Cannot parse with an unknown codec.")
self.parser = lib.av_parser_init(self._codec.id)
if not self.parser:
raise ValueError(f"No parser for {self.ptr.codec.name}")
raise ValueError(f"No parser for {self._codec.name}")

source: ByteSource = bytesource(raw_input, allow_none=True)

Expand Down Expand Up @@ -746,7 +753,7 @@ def decode(self, packet: Packet | None = None):

@cython.cfunc
def _decode(self, packet: Packet | None):
if not self.ptr.codec:
if not self._codec:
raise ValueError("cannot decode unknown codec")

self.open(strict=False)
Expand Down Expand Up @@ -798,7 +805,7 @@ def _setup_decoded_frame(self, frame: Frame, packet: Packet | None) -> cython.vo

@property
def name(self):
return self.ptr.codec.name or ""
return (self._codec.name or "") if self._codec != cython.NULL else ""

@property
def type(self):
Expand All @@ -815,7 +822,7 @@ def profiles(self):
desc: cython.pointer[cython.const[lib.AVCodecDescriptor]] = (
lib.avcodec_descriptor_get(self.ptr.codec_id)
)
if not self.ptr.codec or desc == cython.NULL or not desc.profiles:
if not self._codec or desc == cython.NULL or not desc.profiles:
return ret

i: cython.int = 0
Expand All @@ -830,7 +837,7 @@ def profile(self):
desc: cython.pointer[cython.const[lib.AVCodecDescriptor]] = (
lib.avcodec_descriptor_get(self.ptr.codec_id)
)
if not self.ptr.codec or desc == cython.NULL or not desc.profiles:
if not self._codec or desc == cython.NULL or not desc.profiles:
return

i: cython.int = 0
Expand All @@ -844,7 +851,7 @@ def profile(self, value):
desc: cython.pointer[cython.const[lib.AVCodecDescriptor]] = (
lib.avcodec_descriptor_get(self.ptr.codec_id)
)
if not self.ptr.codec or desc == cython.NULL or not desc.profiles:
if not self._codec or desc == cython.NULL or not desc.profiles:
return

i: cython.int = 0
Expand Down
6 changes: 3 additions & 3 deletions av/subtitles/codeccontext.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def encode_subtitle(self, subtitle: SubtitleSet) -> Packet:
Returns:
A Packet containing the encoded subtitle data
"""
if not self.ptr.codec:
if not self._codec:
raise ValueError("Cannot encode with unknown codec")

self.open(strict=False)
Expand Down Expand Up @@ -96,7 +96,7 @@ def encode_subtitle(self, subtitle: SubtitleSet) -> Packet:
def _decode(self, packet: Packet | None):
"""Decode a subtitle packet, returning a list of :class:`.Subtitle` objects
if a subtitle was decoded, or an empty list otherwise."""
if not self.ptr.codec:
if not self._codec:
raise ValueError("cannot decode unknown codec")

self.open(strict=False)
Expand Down Expand Up @@ -136,7 +136,7 @@ def decode2(self, packet: Packet):
"""
Returns SubtitleSet if you really need it.
"""
if not self.ptr.codec:
if not self._codec:
raise ValueError("cannot decode unknown codec")

self.open(strict=False)
Expand Down
71 changes: 71 additions & 0 deletions tests/codec_context_retry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""Subprocess regression for codec identity after a real FFmpeg open failure."""

from __future__ import annotations

import io
import sys
from fractions import Fraction

# Exercise the exact compiled package imported by the parent, including in-place builds.
sys.path.insert(0, sys.argv[1])


def main() -> None:
import av

buffer = io.BytesIO()
with av.open(buffer, mode="w", format="adts") as output:
stream = output.add_stream("aac", rate=48000)
assert isinstance(stream, av.AudioStream)
context = stream.codec_context
assert context is not None
context.layout = "stereo"
context.format = "s16"

for attempt in range(3):
print(f"invalid-open-{attempt}", flush=True)
try:
context.open()
except av.error.ArgumentError:
pass
else:
raise AssertionError(
"AAC must reject the unsupported s16 configuration"
)
assert not context.is_open
assert context.name == "aac"
assert context.codec.name == "aac"
assert context.is_encoder and not context.is_decoder
assert context.supported_options.generic

# Correcting the configuration must retain the selected implementation
# and permit real encoding, rather than permanently poisoning the context.
# FFmpeg also clears ch_layout during failed-open cleanup, so restore
# the complete valid audio configuration before retrying.
context.layout = "stereo"
context.format = "fltp"
frame = av.AudioFrame(format="s16", layout="stereo", samples=1024)
frame.sample_rate = 48000
frame.time_base = Fraction(1, 48000)
frame.pts = 0
for plane in frame.planes:
plane.update(bytes(plane.buffer_size))
packets = list(stream.encode(frame))
packets.extend(stream.encode(None))
assert packets
for packet in packets:
output.mux(packet)
assert buffer.getvalue()
buffer.seek(0)
with av.open(buffer, mode="r") as source:
decoded = list(source.decode(audio=0))
assert decoded
assert all(
frame.sample_rate == 48000 and frame.layout.name == "stereo"
for frame in decoded
)
print("real-codec-retry-and-recovery-pass", flush=True)


if __name__ == "__main__":
main()
16 changes: 16 additions & 0 deletions tests/test_codec_context.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from __future__ import annotations

import os
import subprocess
import sys
from collections.abc import Iterator
from fractions import Fraction
from pathlib import Path
from typing import Any, TypedDict, overload

import pytest
Expand Down Expand Up @@ -71,6 +74,19 @@ def iter_raw_frames(


class TestCodecContext(TestCase):
def test_failed_open_preserves_selected_codec(self) -> None:
child = Path(__file__).with_name("codec_context_retry.py")
package_root = Path(av.__file__).resolve().parent.parent
result = subprocess.run(
[sys.executable, "-I", "-B", str(child), str(package_root)],
capture_output=True,
text=True,
timeout=30,
check=False,
)
assert result.returncode == 0, (result.returncode, result.stdout, result.stderr)
assert "real-codec-retry-and-recovery-pass" in result.stdout

def test_supported_options(self) -> None:
ctx = Codec("flac", "w").create()
supported = ctx.supported_options
Expand Down
Loading