Skip to content
Merged
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
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Major:
- Drop support for Python 3.11. Binary wheels are now built for Python 3.12 and later.
- Remove the undocumented ``CodecContext.hwaccel`` attribute. It held the ``HWAccel`` settings object passed in, not the live device context; use ``CodecContext.is_hwaccel`` to check whether hardware acceleration is in use.
- Rational attributes (``time_base``, ``average_rate``, ``base_rate``, ``guessed_rate``, ``framerate``, ``rate``, ``sample_aspect_ratio``, and ``display_aspect_ratio``) now return :class:`av.AVRational` rather than ``fractions.Fraction``, and are never ``None``: an unset value is the falsy ``AVRational(0, 1)``. Test them with ``if not stream.time_base:`` instead of ``is None``. Setters still accept a ``fractions.Fraction``.
- Closing an :class:`.OutputContainer` now frees its context, so using one afterwards raises ``Container is not open`` instead of continuing against a finished file. Muxing has no ``avformat_close_input()`` to do this for it, so ``add_stream()``, ``mux()``, ``start_encoding()`` and the rest kept working after ``close()``, and streams held from it stayed readable.
- Remove ``Capabilities.hwaccel``, ``Capabilities.hwaccel_vdpau``, and ``Capabilities.neg_linesizes``, none of which FFmpeg defines any more.
- Remove the ``metadata_encoding`` and ``metadata_errors`` arguments to :func:`av.open`, and the matching attributes. Metadata is now always read and written as UTF-8 with ``surrogateescape``, which is byte exact: a tag in another encoding survives as surrogates and is recovered per key with ``value.encode("utf-8", "surrogateescape").decode("cp1251")``. Previously one encoding had to be chosen for a whole container, so a file mixing encodings across tags could not be represented at all.
- Remove the ``stream_options`` argument to :func:`av.open` and the matching attribute. They only ever reached ``avformat_find_stream_info()``, and only for formats that expose their streams before it runs, so they raised for MPEG and friends; output containers rejected them outright. Pass ``options`` for every stream, set ``stream.codec_context.options`` for one, and ``Container.add_stream(..., options={})`` when writing.
Expand All @@ -48,6 +49,8 @@ Features:

Fixes:

- A rejected ``add_stream()`` or ``add_mux_stream()`` no longer breaks the container.
- ``InputContainer.size`` returns ``None`` when the size cannot be determined rather than the negative ``AVERROR`` it was passing through, which read as a plausible byte count. A non-seekable input, such as a pipe, reported ``-78``.
- ``av.dump_codecs()`` no longer drops the canonical names ``h264``, ``hevc``, ``av1``, ``dirac``, and ``ilbc``, each of which was overwritten by the row of whichever encoder it resolved to.
- ``FilterLink.input`` and ``FilterLink.output`` now follow the filters the graph auto-inserts while configuring. They cached the pad they first resolved, so reading one before ``Graph.configure()`` reported the filter the link no longer pointed at.
- Fix a segfault when a ``FilterLink`` outlives its ``Graph``. It held the graph by weak reference and dereferenced ``AVFilterLink`` before consulting it, so ``link.input`` and ``link.output`` read freed memory. It now holds the graph, as a ``FilterContext`` already did.
Expand Down
1 change: 1 addition & 0 deletions av/container/core.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ class Container:
read_timeout: Real | None
flags: int
video_codec_id: int
def dumps_format(self) -> str: ...
def __enter__(self) -> Self: ...
def __exit__(
self,
Expand Down
53 changes: 36 additions & 17 deletions av/container/input.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from cython.cimports.av.codec.context import CodecContext, wrap_codec_context
from cython.cimports.av.container.streams import StreamContainer
from cython.cimports.av.dictionary import Dictionary
from cython.cimports.av.error import err_check
from cython.cimports.av.packet import Packet
from cython.cimports.av.stream import Stream, wrap_stream
from cython.cimports.av.utils import avdict_to_dict
Expand All @@ -29,6 +28,7 @@ def __cinit__(self, *args, **kwargs):
stream: cython.pointer[lib.AVStream]
codec: cython.pointer[cython.const[lib.AVCodec]]
codec_context: cython.pointer[lib.AVCodecContext]
ret: cython.int

# Hand `options` to every stream that is already known. Only allocate
# c_options when they are: some formats (e.g. MPEG) do not expose their
Expand All @@ -42,6 +42,8 @@ def __cinit__(self, *args, **kwargs):
cython.pointer[cython.pointer[lib.AVDictionary]],
malloc(nb_streams_before * cython.sizeof(cython.p_void)),
)
if c_options == cython.NULL:
raise MemoryError()
for i in range(nb_streams_before):
c_options[i] = cython.NULL
lib.av_dict_copy(cython.address(c_options[i]), base_dict.ptr, 0)
Expand All @@ -51,13 +53,14 @@ def __cinit__(self, *args, **kwargs):
with cython.nogil:
ret = lib.avformat_find_stream_info(self.ptr, c_options)
self.set_timeout(None)
self.err_check(ret)

if c_options:
for i in range(nb_streams_before):
lib.av_dict_free(cython.address(c_options[i]))
free(c_options)

self.err_check(ret)

at_least_one_accelerated_context = False

self.streams = StreamContainer()
Expand All @@ -66,9 +69,12 @@ def __cinit__(self, *args, **kwargs):
codec = lib.avcodec_find_decoder(stream.codecpar.codec_id)
if codec:
codec_context = lib.avcodec_alloc_context3(codec)
err_check(
lib.avcodec_parameters_to_context(codec_context, stream.codecpar)
)
if codec_context == cython.NULL:
raise MemoryError()
ret = lib.avcodec_parameters_to_context(codec_context, stream.codecpar)
if ret < 0:
lib.avcodec_free_context(cython.address(codec_context))
self.err_check(ret)
codec_context.pkt_timebase = stream.time_base
py_codec_context = wrap_codec_context(
codec_context, codec, self.hwaccel
Expand Down Expand Up @@ -127,8 +133,17 @@ def bit_rate(self):

@property
def size(self):
"""Size of the input in bytes, or ``None`` if it cannot be determined.

A non-seekable input, such as a pipe or a file-like object without
``seek``, has no size to report.

Wraps :ffmpeg:`avio_size`.
"""
self._assert_open()
return lib.avio_size(self.ptr.pb)
size: int64_t = lib.avio_size(self.ptr.pb)
if size >= 0:
return size

def close(self):
close_input(self)
Expand All @@ -152,11 +167,12 @@ def demux(self, *args, **kwargs):
self._assert_open()

streams: list[Stream] = self.streams.get(*args, **kwargs)
if self.ptr.nb_streams == 0:
nb_streams: cython.uint = self.ptr.nb_streams
if nb_streams == 0:
return
include_stream: cython.pointer[uint8_t] = cython.cast(
cython.pointer[uint8_t],
malloc(self.ptr.nb_streams * cython.sizeof(uint8_t)),
malloc(nb_streams * cython.sizeof(uint8_t)),
)
if include_stream == cython.NULL:
raise MemoryError()
Expand All @@ -168,11 +184,11 @@ def demux(self, *args, **kwargs):

self.set_timeout(self.read_timeout)
try:
for i in range(self.ptr.nb_streams):
for i in range(nb_streams):
include_stream[i] = 0
for stream in streams:
i = stream.index
if i >= self.ptr.nb_streams:
if i >= nb_streams:
raise ValueError(f"stream index {i} out of range")
include_stream[i] = 1

Expand All @@ -194,11 +210,14 @@ def demux(self, *args, **kwargs):
except EOFError:
break

if include_stream[read_packet.stream_index]:
# If AVFMTCTX_NOHEADER is set in ctx_flags, then new streams
# may also appear in av_read_frame().
# http://ffmpeg.org/doxygen/trunk/structAVFormatContext.html
# TODO: find better way to handle this
# If AVFMTCTX_NOHEADER is set in ctx_flags, then new streams
# may also appear in av_read_frame(). They are past the end of
# include_stream, and nothing selected them anyway.
# http://ffmpeg.org/doxygen/trunk/structAVFormatContext.html
if (
read_packet.stream_index < nb_streams
and include_stream[read_packet.stream_index]
):
if read_packet.stream_index < len(self.streams):
# Move the encoded data out of the read buffer into a
# fresh Packet for the caller.
Expand All @@ -211,7 +230,7 @@ def demux(self, *args, **kwargs):
yield packet

# Flush!
for i in range(self.ptr.nb_streams):
for i in range(nb_streams):
if include_stream[i]:
packet = Packet()
packet._stream = self.streams[i]
Expand Down Expand Up @@ -300,7 +319,7 @@ def seek(
stream_index: cython.int = stream.index if stream else -1
with cython.nogil:
ret = lib.av_seek_frame(self.ptr, stream_index, c_offset, flags)
err_check(ret)
self.err_check(ret)

self.flush_buffers()

Expand Down
2 changes: 1 addition & 1 deletion av/container/input.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class InputContainer(Container):
start_time_realtime: int | None
duration: int | None
bit_rate: int
size: int
size: int | None

@overload
def demux(self, video_stream: VideoStream) -> Iterator[Packet[VideoStream]]: ...
Expand Down
Loading