diff --git a/CHANGELOG.rst b/CHANGELOG.rst index bb3c77abf..c49039fa2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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_`` 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``. diff --git a/av/codec/context.py b/av/codec/context.py index 86803a961..ed5bf9e51 100644 --- a/av/codec/context.py +++ b/av/codec/context.py @@ -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. @@ -1123,8 +1122,12 @@ 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`. """ @@ -1132,7 +1135,11 @@ def initial_padding(self): @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`. """ @@ -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`. """ @@ -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 @@ -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): @@ -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 diff --git a/av/packet.py b/av/packet.py index d2be299c7..6b8ea0e60 100644 --- a/av/packet.py +++ b/av/packet.py @@ -56,6 +56,9 @@ "lcevc", "3d_reference_displays", "rtcp_sr", + "exif", + "dynamic_hdr_smpte_2094_app5", + "hevc_conf", ] diff --git a/av/packet.pyi b/av/packet.pyi index ce11d7040..add0a78d5 100644 --- a/av/packet.pyi +++ b/av/packet.pyi @@ -56,6 +56,9 @@ PktSideDataT = Literal[ "lcevc", "3d_reference_displays", "rtcp_sr", + "exif", + "dynamic_hdr_smpte_2094_app5", + "hevc_conf", ] class PacketSideData(Buffer): diff --git a/av/sidedata/sidedata.py b/av/sidedata/sidedata.py index 307524f0e..443b7bbb8 100644 --- a/av/sidedata/sidedata.py +++ b/av/sidedata/sidedata.py @@ -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_`` 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: diff --git a/include/avcodec.pxd b/include/avcodec.pxd index 5d08c6525..18a9a1027 100644 --- a/include/avcodec.pxd +++ b/include/avcodec.pxd @@ -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) diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 44b06eab7..c964ce9a0 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -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 diff --git a/tests/test_decode.py b/tests/test_decode.py index 35ce9feff..8fbaa3ccc 100644 --- a/tests/test_decode.py +++ b/tests/test_decode.py @@ -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))