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 @@ -45,10 +45,13 @@ Features:

- ``av.dump_codecs()`` now lists every codec FFmpeg knows of rather than only those with an encoder or a decoder, so data and attachment codecs appear, matching ``ffmpeg -codecs``. Its legend gains the ``..D...`` and ``..T...`` media types.
- ``ContainerFormat.fixed_framesize`` reports whether a format wants fixed size audio frames.
- :class:`.CodecContext` exposes more of ``AVCodecContext``: ``pkt_timebase``, ``frame_num``, ``active_thread_type``, ``bits_per_raw_sample``, ``compression_level``, ``rc_buffer_size``, ``min_bit_rate``, a setter for ``max_bit_rate``, the audio ``initial_padding``, ``trailing_padding``, and ``seek_preroll``, and ``stats_in``/``stats_out`` for two-pass encoding. ``VideoCodecContext`` gains ``chroma_sample_location``, ``refs``, and ``mb_decision``; ``AudioCodecContext`` gains ``block_align``.
- ``CodecContext.coded_side_data`` and ``CodecContext.decoded_side_data`` expose the context's global side data as dicts of ``bytes``, keyed by packet side data name and :class:`~av.sidedata.sidedata.Type` respectively. Stream wide HDR metadata, such as mastering display and content light level, arrives in ``decoded_side_data`` once a frame has been decoded.
- Enums gained the members FFmpeg has since added: ``Properties.FIELDS``, ``Properties.ENHANCEMENT``, ``PixFmtLoss.EXCESS_RESOLUTION``, ``PixFmtLoss.EXCESS_DEPTH``, ``Flags2.icc_profiles``, ``format.Flags.experimental``, ``Interpolation.STRICT``, ``Interpolation.UNSTABLE``, ``ColorTrc.V_LOG``, ``ColorPrimaries.V_GAMUT``, and the ``LCEVC``, ``VIEW_ID``, ``THREE_D_REFERENCE_DISPLAYS``, and ``EXIF`` members of ``sidedata.Type``.

Fixes:

- ``CodecContext.bit_rate_tolerance`` returns its value instead of always ``None``; the getter was missing its ``return``.
- 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.
Expand Down
11 changes: 11 additions & 0 deletions av/audio/codeccontext.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,14 @@ def format(self, value):
self._assert_not_open("format")
format: AudioFormat = AudioFormat(value)
self.ptr.sample_fmt = format.sample_fmt

@property
def block_align(self):
"""
Number of bytes per coded audio frame, for formats with a fixed one.

Wraps :ffmpeg:`AVCodecContext.block_align`.

:type: int
"""
return self.ptr.block_align
2 changes: 2 additions & 0 deletions av/audio/codeccontext.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ class AudioCodecContext(CodecContext):
layout: _Layout
@property
def channels(self) -> int: ...
@property
def block_align(self) -> int: ...
def encode(self, frame: AudioFrame | None = None) -> list[Packet]: ...
def encode_lazy(self, frame: AudioFrame | None = None) -> Iterator[Packet]: ...
def decode(self, packet: Packet | None = None) -> list[AudioFrame]: ...
1 change: 1 addition & 0 deletions av/codec/context.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ cdef class CodecContext:
cdef public dict options
cdef HWAccel hwaccel_ctx
cdef Frame _next_frame
cdef bytes _stats_in # keeps the buffer ptr.stats_in points at alive

cdef uint8_t _ctxflags # ctxEnum: template_initialized
# True when created via add_stream_from_template(); start_encoding() skips
Expand Down
212 changes: 211 additions & 1 deletion av/codec/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,25 @@
from cython.cimports.av.packet import Packet
from cython.cimports.av.rational import from_avrational
from cython.cimports.av.utils import to_avrational
from cython.cimports.cpython.bytes import (
PyBytes_FromString,
PyBytes_FromStringAndSize,
)
from cython.cimports.libc.errno import EAGAIN
from cython.cimports.libc.stdint import uint8_t
from cython.cimports.libc.string import memcpy, strcmp

from av.error import InvalidDataError
from av.packet import packet_sidedata_type_to_literal

_cinit_sentinel = cython.declare(object, object())


@cython.cfunc
def _to_bytes(data: cython.pointer[uint8_t], size: cython.size_t) -> bytes:
return PyBytes_FromStringAndSize(cython.cast(cython.p_char, data), size)


@cython.cfunc
def wrap_codec_context(
c_ctx: cython.pointer[lib.AVCodecContext],
Expand Down Expand Up @@ -908,14 +918,61 @@ def bit_rate(self, value: cython.longlong):

@property
def max_bit_rate(self):
"""Maximum bitrate, or ``None`` if unset.

Wraps :ffmpeg:`AVCodecContext.rc_max_rate`.
"""
if self.ptr.rc_max_rate > 0:
return self.ptr.rc_max_rate
else:
return None

@max_bit_rate.setter
def max_bit_rate(self, value: cython.longlong):
self.ptr.rc_max_rate = value

@property
def min_bit_rate(self):
"""Minimum bitrate, or ``None`` if unset.

Wraps :ffmpeg:`AVCodecContext.rc_min_rate`.
"""
if self.ptr.rc_min_rate > 0:
return self.ptr.rc_min_rate
else:
return None

@min_bit_rate.setter
def min_bit_rate(self, value: cython.longlong):
self.ptr.rc_min_rate = value

@property
def rc_buffer_size(self):
"""Decoder bitstream buffer size (VBV), in bits.

Wraps :ffmpeg:`AVCodecContext.rc_buffer_size`.
"""
return self.ptr.rc_buffer_size

@rc_buffer_size.setter
def rc_buffer_size(self, value: cython.int):
self.ptr.rc_buffer_size = value

@property
def compression_level(self):
"""Codec-defined compression level; ``-1`` means default.

Wraps :ffmpeg:`AVCodecContext.compression_level`.
"""
return self.ptr.compression_level

@compression_level.setter
def compression_level(self, value: cython.int):
self.ptr.compression_level = value

@property
def bit_rate_tolerance(self):
self.ptr.bit_rate_tolerance
return self.ptr.bit_rate_tolerance

@bit_rate_tolerance.setter
def bit_rate_tolerance(self, value: cython.int):
Expand Down Expand Up @@ -956,6 +1013,15 @@ def thread_type(self, value):
else:
self.ptr.thread_type = value.value

@property
def active_thread_type(self):
"""The threading actually in use, which may differ from
:attr:`thread_type` once the codec is open.

Wraps :ffmpeg:`AVCodecContext.active_thread_type`.
"""
return ThreadType(self.ptr.active_thread_type)

@property
def skip_frame(self):
"""Returns one of the following str literals:
Expand Down Expand Up @@ -1014,3 +1080,147 @@ def delay(self):

"""
return self.ptr.delay

@property
def pkt_timebase(self):
"""Timebase of the packets fed to this context, as a
:class:`~fractions.Fraction`.

Decoders use it to set :attr:`.Frame.time_base`. Containers set it for
you; set it yourself when driving a bare CodecContext.

Wraps :ffmpeg:`AVCodecContext.pkt_timebase`.
"""
return from_avrational(self.ptr.pkt_timebase)

@pkt_timebase.setter
def pkt_timebase(self, value):
to_avrational(value, cython.address(self.ptr.pkt_timebase))

@property
def frame_num(self):
"""Number of frames passed to/from this context so far.

Wraps :ffmpeg:`AVCodecContext.frame_num`.
"""
return self.ptr.frame_num

@property
def bits_per_raw_sample(self):
"""Bit depth of the samples/components before encoding, e.g. ``10`` for
10-bit video. ``0`` when unknown.

This is the real bit depth; :attr:`.VideoCodecContext.bits_per_coded_sample`
is how many bits the bitstream spends on it.

Wraps :ffmpeg:`AVCodecContext.bits_per_raw_sample`.
"""
return self.ptr.bits_per_raw_sample

@bits_per_raw_sample.setter
def bits_per_raw_sample(self, value: cython.int):
self.ptr.bits_per_raw_sample = value

@property
def initial_padding(self):
"""Audio only. Samples the decoder should skip at the start of the
stream, i.e. the encoder delay. Needed for gapless playback.

Wraps :ffmpeg:`AVCodecContext.initial_padding`.
"""
return self.ptr.initial_padding

@property
def trailing_padding(self):
"""Audio only. Samples to discard at the end of the stream.

Wraps :ffmpeg:`AVCodecContext.trailing_padding`.
"""
return self.ptr.trailing_padding

@trailing_padding.setter
def trailing_padding(self, value: cython.int):
self.ptr.trailing_padding = value

@property
def seek_preroll(self):
"""Number of samples to decode before the target seek point for the
output to be correct, in ``1 / AV_TIME_BASE`` units.

Wraps :ffmpeg:`AVCodecContext.seek_preroll`.
"""
return self.ptr.seek_preroll

@property
def stats_out(self):
"""Pass-one statistics produced by the encoder, or ``None``.

Concatenate this after every :meth:`encode` call of the first pass and
feed the result back as :attr:`stats_in` on the second.

Wraps :ffmpeg:`AVCodecContext.stats_out`.
"""
if self.ptr.stats_out == cython.NULL:
return None
return PyBytes_FromString(self.ptr.stats_out).decode("utf-8", "replace")

@property
def stats_in(self):
"""Pass-one statistics to feed the second pass of a two-pass encode.

Must be set before :meth:`open`.

Wraps :ffmpeg:`AVCodecContext.stats_in`.
"""
if self.ptr.stats_in == cython.NULL:
return None
return PyBytes_FromString(self.ptr.stats_in).decode("utf-8", "replace")

@stats_in.setter
def stats_in(self, value):
self._assert_not_open("stats_in")
if value is None:
self._stats_in = None
self.ptr.stats_in = cython.NULL
return

# libavcodec never frees stats_in, so we keep the bytes alive ourselves.
self._stats_in = value.encode("utf-8") if type(value) is str else bytes(value)
self.ptr.stats_in = self._stats_in

@property
def coded_side_data(self):
"""Global side data attached to the coded bitstream, as a
``dict`` of packet side data name to ``bytes``.

Wraps :ffmpeg:`AVCodecContext.coded_side_data`.
"""
i: cython.int
return {
packet_sidedata_type_to_literal(
self.ptr.coded_side_data[i].type
): _to_bytes(
self.ptr.coded_side_data[i].data, self.ptr.coded_side_data[i].size
)
for i in range(self.ptr.nb_coded_side_data)
}

@property
def decoded_side_data(self):
"""Global side data produced by the decoder, as a ``dict`` of
:class:`av.sidedata.sidedata.Type` to ``bytes``.

This is where stream-wide HDR metadata (mastering display, content
light level) shows up after the first frame is decoded.

Wraps :ffmpeg:`AVCodecContext.decoded_side_data`.
"""
from av.sidedata.sidedata import Type

i: cython.int
return {
Type(self.ptr.decoded_side_data[i].type): _to_bytes(
self.ptr.decoded_side_data[i].data, self.ptr.decoded_side_data[i].size
)
for i in range(self.ptr.nb_decoded_side_data)
}
32 changes: 31 additions & 1 deletion av/codec/context.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ from typing import ClassVar, Literal, cast, overload

from av.audio import _AudioCodecName
from av.audio.codeccontext import AudioCodecContext
from av.packet import Packet
from av.packet import Packet, PktSideDataT
from av.rational import AVRational
from av.sidedata.sidedata import Type as FrameSideDataType
from av.subtitles import _SubtitleCodecName
from av.subtitles.codeccontext import SubtitleCodecContext
from av.video import _VideoCodecName
Expand Down Expand Up @@ -134,6 +135,11 @@ class CodecContext:
global_quality: int
bit_rate: int | None
bit_rate_tolerance: int
rc_buffer_size: int
compression_level: int
bits_per_raw_sample: int
trailing_padding: int
stats_in: str | None
thread_count: int
thread_type: ThreadType
skip_frame: Literal[
Expand All @@ -153,6 +159,30 @@ class CodecContext:
def codec(self) -> Codec: ...
@property
def max_bit_rate(self) -> int | None: ...
@max_bit_rate.setter
def max_bit_rate(self, value: int) -> None: ...
@property
def min_bit_rate(self) -> int | None: ...
@min_bit_rate.setter
def min_bit_rate(self, value: int) -> None: ...
@property
def pkt_timebase(self) -> AVRational: ...
@pkt_timebase.setter
def pkt_timebase(self, value: AVRational | Fraction | int) -> None: ...
@property
def frame_num(self) -> int: ...
@property
def active_thread_type(self) -> ThreadType: ...
@property
def initial_padding(self) -> int: ...
@property
def seek_preroll(self) -> int: ...
@property
def stats_out(self) -> str | None: ...
@property
def coded_side_data(self) -> dict[PktSideDataT, bytes]: ...
@property
def decoded_side_data(self) -> dict[FrameSideDataType, bytes]: ...
@property
def delay(self) -> bool: ...
@property
Expand Down
Loading
Loading