Skip to content

Commit 51a65a8

Browse files
committed
Fix side_data type tables lagging FFmpeg
1 parent e21a345 commit 51a65a8

8 files changed

Lines changed: 74 additions & 20 deletions

File tree

CHANGELOG.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,11 @@ Features:
4747
- ``ContainerFormat.fixed_framesize`` reports whether a format wants fixed size audio frames.
4848
- :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``.
4949
- ``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.
50-
- 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``.
50+
- 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.
5151

5252
Fixes:
5353

54+
- ``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``.
5455
- ``CodecContext.bit_rate_tolerance`` returns its value instead of always ``None``; the getter was missing its ``return``.
5556
- A rejected ``add_stream()`` or ``add_mux_stream()`` no longer breaks the container.
5657
- ``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/codec/context.py

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,8 +1083,7 @@ def delay(self):
10831083

10841084
@property
10851085
def pkt_timebase(self):
1086-
"""Timebase of the packets fed to this context, as a
1087-
:class:`~fractions.Fraction`.
1086+
"""Timebase of the packets fed to this context.
10881087
10891088
Decoders use it to set :attr:`.Frame.time_base`. Containers set it for
10901089
you; set it yourself when driving a bare CodecContext.
@@ -1123,16 +1122,24 @@ def bits_per_raw_sample(self, value: cython.int):
11231122

11241123
@property
11251124
def initial_padding(self):
1126-
"""Audio only. Samples the decoder should skip at the start of the
1127-
stream, i.e. the encoder delay. Needed for gapless playback.
1125+
"""Audio only. Priming samples the encoder inserted at the start of the
1126+
stream, which must be discarded to recover the original audio. Needed
1127+
for gapless playback.
1128+
1129+
Set by libavcodec when encoding, and taken from the stream parameters
1130+
when decoding.
11281131
11291132
Wraps :ffmpeg:`AVCodecContext.initial_padding`.
11301133
"""
11311134
return self.ptr.initial_padding
11321135

11331136
@property
11341137
def trailing_padding(self):
1135-
"""Audio only. Samples to discard at the end of the stream.
1138+
"""Audio only. Padding samples appended by the encoder, which must be
1139+
discarded from the end of the stream to recover the original audio.
1140+
1141+
libavcodec neither sets nor acts on this; it only travels between the
1142+
context and the container's stream parameters.
11361143
11371144
Wraps :ffmpeg:`AVCodecContext.trailing_padding`.
11381145
"""
@@ -1144,8 +1151,8 @@ def trailing_padding(self, value: cython.int):
11441151

11451152
@property
11461153
def seek_preroll(self):
1147-
"""Number of samples to decode before the target seek point for the
1148-
output to be correct, in ``1 / AV_TIME_BASE`` units.
1154+
"""Audio only. Number of samples to skip after a discontinuity, such as
1155+
a seek, before the decoded output is correct.
11491156
11501157
Wraps :ffmpeg:`AVCodecContext.seek_preroll`.
11511158
"""
@@ -1184,8 +1191,13 @@ def stats_in(self, value):
11841191
self.ptr.stats_in = cython.NULL
11851192
return
11861193

1194+
if type(value) is str:
1195+
value = value.encode("utf-8")
1196+
elif not isinstance(value, (bytes, bytearray)):
1197+
raise TypeError("stats_in must be str, bytes, or None")
1198+
11871199
# libavcodec never frees stats_in, so we keep the bytes alive ourselves.
1188-
self._stats_in = value.encode("utf-8") if type(value) is str else bytes(value)
1200+
self._stats_in = bytes(value)
11891201
self.ptr.stats_in = self._stats_in
11901202

11911203
@property
@@ -1196,14 +1208,16 @@ def coded_side_data(self):
11961208
Wraps :ffmpeg:`AVCodecContext.coded_side_data`.
11971209
"""
11981210
i: cython.int
1199-
return {
1200-
packet_sidedata_type_to_literal(
1201-
self.ptr.coded_side_data[i].type
1202-
): _to_bytes(
1211+
out = {}
1212+
for i in range(self.ptr.nb_coded_side_data):
1213+
try:
1214+
key = packet_sidedata_type_to_literal(self.ptr.coded_side_data[i].type)
1215+
except IndexError:
1216+
continue
1217+
out[key] = _to_bytes(
12031218
self.ptr.coded_side_data[i].data, self.ptr.coded_side_data[i].size
12041219
)
1205-
for i in range(self.ptr.nb_coded_side_data)
1206-
}
1220+
return out
12071221

12081222
@property
12091223
def decoded_side_data(self):
@@ -1218,9 +1232,10 @@ def decoded_side_data(self):
12181232
from av.sidedata.sidedata import Type
12191233

12201234
i: cython.int
1221-
return {
1222-
Type(self.ptr.decoded_side_data[i].type): _to_bytes(
1235+
out = {}
1236+
for i in range(self.ptr.nb_decoded_side_data):
1237+
key = Type(self.ptr.decoded_side_data[i].type)
1238+
out[key] = _to_bytes(
12231239
self.ptr.decoded_side_data[i].data, self.ptr.decoded_side_data[i].size
12241240
)
1225-
for i in range(self.ptr.nb_decoded_side_data)
1226-
}
1241+
return out

av/packet.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@
5656
"lcevc",
5757
"3d_reference_displays",
5858
"rtcp_sr",
59+
"exif",
60+
"dynamic_hdr_smpte_2094_app5",
61+
"hevc_conf",
5962
]
6063

6164

av/packet.pyi

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ PktSideDataT = Literal[
5656
"lcevc",
5757
"3d_reference_displays",
5858
"rtcp_sr",
59+
"exif",
60+
"dynamic_hdr_smpte_2094_app5",
61+
"hevc_conf",
5962
]
6063

6164
class PacketSideData(Buffer):

av/sidedata/sidedata.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,23 @@ class Type(Enum):
5151
THREE_D_REFERENCE_DISPLAYS = lib.AV_FRAME_DATA_3D_REFERENCE_DISPLAYS
5252
EXIF = lib.AV_FRAME_DATA_EXIF
5353

54+
@classmethod
55+
def _missing_(cls, value):
56+
"""Name types added by an FFmpeg newer than the one PyAV was written against.
57+
58+
The members above only cover what the oldest supported FFmpeg defines,
59+
so a frame decoded by a newer one can carry a type that is not here.
60+
Give it an ``UNKNOWN_<value>`` member instead of raising ``ValueError``
61+
and taking :attr:`av.Frame.side_data` down with it.
62+
"""
63+
if not isinstance(value, int):
64+
return None
65+
66+
member = object.__new__(cls)
67+
member._name_ = f"UNKNOWN_{value}"
68+
member._value_ = value
69+
return cls._value2member_map_.setdefault(value, member)
70+
5471

5572
@cython.cfunc
5673
def wrap_side_data(frame: Frame, index: cython.int) -> SideData:

include/avcodec.pxd

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -540,4 +540,3 @@ cdef extern from "libavcodec/packet.h" nogil:
540540
AVPacket *pkt, AVPacketSideDataType type, uint8_t *data, size_t size
541541
)
542542
const char *av_packet_side_data_name(AVPacketSideDataType type)
543-
const char *av_frame_side_data_name(AVFrameSideDataType type)

tests/test_codec_context.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,11 @@ def test_encoder_scalars_roundtrip(self) -> None:
649649
assert ctx.pkt_timebase == Fraction(1, 1000)
650650
assert ctx.frame_num == 0
651651

652+
def test_stats_in_rejects_junk(self) -> None:
653+
ctx = av.CodecContext.create("libx264", "w")
654+
with pytest.raises(TypeError):
655+
ctx.stats_in = 5 # type: ignore[assignment]
656+
652657
def test_stats_in_out(self) -> None:
653658
ctx = av.CodecContext.create("libx264", "w")
654659
assert ctx.stats_in is None

tests/test_decode.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,17 @@ def test_side_data_mapping_protocol(self) -> None:
325325
assert list(side_data[:]) == list(side_data.values())
326326
return
327327

328+
def test_side_data_type_unknown(self) -> None:
329+
"""A type only a newer FFmpeg names must not take Type() down."""
330+
unknown = Type(1 << 20)
331+
assert unknown.name == "UNKNOWN_1048576"
332+
assert unknown.value == 1 << 20
333+
assert Type(1 << 20) is unknown
334+
assert "UNKNOWN_1048576" not in Type.__members__
335+
336+
with pytest.raises(ValueError):
337+
Type("not a side data type") # type: ignore[arg-type]
338+
328339
def test_no_side_data(self) -> None:
329340
container = av.open(fate_suite("h264/interlaced_crop.mp4"))
330341
frame = next(container.decode(video=0))

0 commit comments

Comments
 (0)