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: 2 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,11 @@ Features:
- ``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``.
- 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``, the ``LCEVC``, ``VIEW_ID``, ``THREE_D_REFERENCE_DISPLAYS``, and ``EXIF`` members of ``sidedata.Type``, and the ``exif``, ``dynamic_hdr_smpte_2094_app5``, and ``hevc_conf`` packet side data names.

Fixes:

- ``Frame.side_data`` and ``PacketSideData.data_type`` no longer raise on side data types FFmpeg has added since PyAV last listed them. The packet side data names were missing three, so reading, say, the ``AV_PKT_DATA_HEVC_CONF`` an HEVC stream in MP4 or Matroska carries raised ``IndexError``. A frame side data type that no ``sidedata.Type`` member names, which is anything a newer FFmpeg than PyAV was built against added, now becomes an ``UNKNOWN_<value>`` member rather than raising ``ValueError``.
- ``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``.
Expand Down
51 changes: 33 additions & 18 deletions av/codec/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -1083,8 +1083,7 @@ def delay(self):

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

Decoders use it to set :attr:`.Frame.time_base`. Containers set it for
you; set it yourself when driving a bare CodecContext.
Expand Down Expand Up @@ -1123,16 +1122,24 @@ def bits_per_raw_sample(self, value: cython.int):

@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.
"""Audio only. Priming samples the encoder inserted at the start of the
stream, which must be discarded to recover the original audio. Needed
for gapless playback.

Set by libavcodec when encoding, and taken from the stream parameters
when decoding.

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.
"""Audio only. Padding samples appended by the encoder, which must be
discarded from the end of the stream to recover the original audio.

libavcodec neither sets nor acts on this; it only travels between the
context and the container's stream parameters.

Wraps :ffmpeg:`AVCodecContext.trailing_padding`.
"""
Expand All @@ -1144,8 +1151,8 @@ def trailing_padding(self, value: cython.int):

@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.
"""Audio only. Number of samples to skip after a discontinuity, such as
a seek, before the decoded output is correct.

Wraps :ffmpeg:`AVCodecContext.seek_preroll`.
"""
Expand Down Expand Up @@ -1184,8 +1191,13 @@ def stats_in(self, value):
self.ptr.stats_in = cython.NULL
return

if type(value) is str:
value = value.encode("utf-8")
elif not isinstance(value, (bytes, bytearray)):
raise TypeError("stats_in must be str, bytes, or None")

# 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._stats_in = bytes(value)
self.ptr.stats_in = self._stats_in

@property
Expand All @@ -1196,14 +1208,16 @@ def coded_side_data(self):
Wraps :ffmpeg:`AVCodecContext.coded_side_data`.
"""
i: cython.int
return {
packet_sidedata_type_to_literal(
self.ptr.coded_side_data[i].type
): _to_bytes(
out = {}
for i in range(self.ptr.nb_coded_side_data):
try:
key = packet_sidedata_type_to_literal(self.ptr.coded_side_data[i].type)
except IndexError:
continue
out[key] = _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)
}
return out

@property
def decoded_side_data(self):
Expand All @@ -1218,9 +1232,10 @@ def decoded_side_data(self):
from av.sidedata.sidedata import Type

i: cython.int
return {
Type(self.ptr.decoded_side_data[i].type): _to_bytes(
out = {}
for i in range(self.ptr.nb_decoded_side_data):
key = Type(self.ptr.decoded_side_data[i].type)
out[key] = _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)
}
return out
3 changes: 3 additions & 0 deletions av/packet.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@
"lcevc",
"3d_reference_displays",
"rtcp_sr",
"exif",
"dynamic_hdr_smpte_2094_app5",
"hevc_conf",
]


Expand Down
3 changes: 3 additions & 0 deletions av/packet.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ PktSideDataT = Literal[
"lcevc",
"3d_reference_displays",
"rtcp_sr",
"exif",
"dynamic_hdr_smpte_2094_app5",
"hevc_conf",
]

class PacketSideData(Buffer):
Expand Down
17 changes: 17 additions & 0 deletions av/sidedata/sidedata.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,23 @@ class Type(Enum):
THREE_D_REFERENCE_DISPLAYS = lib.AV_FRAME_DATA_3D_REFERENCE_DISPLAYS
EXIF = lib.AV_FRAME_DATA_EXIF

@classmethod
def _missing_(cls, value):
"""Name types added by an FFmpeg newer than the one PyAV was written against.

The members above only cover what the oldest supported FFmpeg defines,
so a frame decoded by a newer one can carry a type that is not here.
Give it an ``UNKNOWN_<value>`` member instead of raising ``ValueError``
and taking :attr:`av.Frame.side_data` down with it.
"""
if not isinstance(value, int):
return None

member = object.__new__(cls)
member._name_ = f"UNKNOWN_{value}"
member._value_ = value
return cls._value2member_map_.setdefault(value, member)


@cython.cfunc
def wrap_side_data(frame: Frame, index: cython.int) -> SideData:
Expand Down
1 change: 0 additions & 1 deletion include/avcodec.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -540,4 +540,3 @@ cdef extern from "libavcodec/packet.h" nogil:
AVPacket *pkt, AVPacketSideDataType type, uint8_t *data, size_t size
)
const char *av_packet_side_data_name(AVPacketSideDataType type)
const char *av_frame_side_data_name(AVFrameSideDataType type)
5 changes: 5 additions & 0 deletions tests/test_codec_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,11 @@ def test_encoder_scalars_roundtrip(self) -> None:
assert ctx.pkt_timebase == Fraction(1, 1000)
assert ctx.frame_num == 0

def test_stats_in_rejects_junk(self) -> None:
ctx = av.CodecContext.create("libx264", "w")
with pytest.raises(TypeError):
ctx.stats_in = 5 # type: ignore[assignment]

def test_stats_in_out(self) -> None:
ctx = av.CodecContext.create("libx264", "w")
assert ctx.stats_in is None
Expand Down
11 changes: 11 additions & 0 deletions tests/test_decode.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,17 @@ def test_side_data_mapping_protocol(self) -> None:
assert list(side_data[:]) == list(side_data.values())
return

def test_side_data_type_unknown(self) -> None:
"""A type only a newer FFmpeg names must not take Type() down."""
unknown = Type(1 << 20)
assert unknown.name == "UNKNOWN_1048576"
assert unknown.value == 1 << 20
assert Type(1 << 20) is unknown
assert "UNKNOWN_1048576" not in Type.__members__

with pytest.raises(ValueError):
Type("not a side data type") # type: ignore[arg-type]

def test_no_side_data(self) -> None:
container = av.open(fate_suite("h264/interlaced_crop.mp4"))
frame = next(container.decode(video=0))
Expand Down
Loading