diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 76e7b2127..15a7fff89 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -31,6 +31,7 @@ v17.1.0 (next) Features: - Use FFmpeg 8.1.1 in the binary wheels. - Expose ``AVCodecContext.global_quality`` by :gh-user:`WyattBlue` in (:pr:`2246`). +- Expose ``Stream.discard`` so demuxing and seeking can skip unwanted streams (:issue:`2272`). Fixes: - Add ``cython.final`` to leaf classes, ensuring that they are not subclassed. diff --git a/av/stream.py b/av/stream.py index 23749e699..f96030e26 100644 --- a/av/stream.py +++ b/av/stream.py @@ -1,4 +1,4 @@ -from enum import IntFlag +from enum import IntEnum, IntFlag import cython from cython.cimports import libav as lib @@ -34,6 +34,16 @@ class Disposition(IntFlag): multilayer = 1 << 21 +class Discard(IntEnum): + none = lib.AVDISCARD_NONE + default = lib.AVDISCARD_DEFAULT + nonref = lib.AVDISCARD_NONREF + bidir = lib.AVDISCARD_BIDIR + nonintra = lib.AVDISCARD_NONINTRA + nonkey = lib.AVDISCARD_NONKEY + all = lib.AVDISCARD_ALL + + _cinit_bypass_sentinel = cython.declare(object, object()) @@ -132,6 +142,9 @@ def __setattr__(self, name, value): if name == "disposition": self.ptr.disposition = value return + if name == "discard": + self.ptr.discard = Discard(value).value + return if name == "time_base": to_avrational(value, cython.address(self.ptr.time_base)) return @@ -268,6 +281,19 @@ def language(self): def disposition(self): return Disposition(self.ptr.disposition) + @property + def discard(self): + """ + Controls which packets of this stream are discarded by the demuxer. + + Set this to e.g. :attr:`Discard.all` on streams you don't need so that + :meth:`.Container.demux` and :meth:`.Container.seek` skip them, avoiding + the cost of synchronizing streams you never read. + + :type: Discard + """ + return Discard(self.ptr.discard) + @property def type(self): """ diff --git a/av/stream.pyi b/av/stream.pyi index 680166fd6..f9148021f 100644 --- a/av/stream.pyi +++ b/av/stream.pyi @@ -1,4 +1,4 @@ -from enum import IntFlag +from enum import IntEnum, IntFlag from fractions import Fraction from typing import Literal, cast @@ -27,6 +27,15 @@ class Disposition(IntFlag): still_image = cast(int, ...) multilayer = cast(int, ...) +class Discard(IntEnum): + none = cast(int, ...) + default = cast(int, ...) + nonref = cast(int, ...) + bidir = cast(int, ...) + nonintra = cast(int, ...) + nonkey = cast(int, ...) + all = cast(int, ...) + class Stream: name: str | None container: Container @@ -46,6 +55,7 @@ class Stream: start_time: int | None duration: int | None disposition: Disposition + discard: Discard frames: int language: str | None type: Literal["video", "audio", "data", "subtitle", "attachment", "unknown"] diff --git a/docs/api/stream.rst b/docs/api/stream.rst index 99a30b136..98d8fc43d 100644 --- a/docs/api/stream.rst +++ b/docs/api/stream.rst @@ -92,5 +92,7 @@ Others .. autoattribute:: Stream.language +.. autoattribute:: Stream.discard + diff --git a/include/avformat.pxd b/include/avformat.pxd index 5aca5e287..1cd07d814 100644 --- a/include/avformat.pxd +++ b/include/avformat.pxd @@ -25,6 +25,7 @@ cdef extern from "libavformat/avformat.h" nogil: int index int id int disposition + AVDiscard discard AVCodecParameters *codecpar AVRational time_base int64_t start_time diff --git a/tests/test_streams.py b/tests/test_streams.py index a9f5e5bdc..1afc67222 100644 --- a/tests/test_streams.py +++ b/tests/test_streams.py @@ -136,6 +136,45 @@ def test_selection(self) -> None: data = container.streams.data[0] assert data == container.streams.best("data") + def test_discard(self) -> None: + from av.stream import Discard + + container = av.open( + fate_suite("amv/MTV_high_res_320x240_sample_Penguin_Joke_MTV_from_WMV.amv") + ) + audio = container.streams.audio[0] + + # Default discard policy. + assert audio.discard == Discard.default + + # Setter accepts the enum and round-trips. + audio.discard = Discard.all + assert audio.discard == Discard.all + + audio.discard = Discard.nonkey + assert audio.discard == Discard.nonkey + container.close() + + # Discarding a stream makes demux skip (almost) all of its packets. + def audio_packets(discard: Discard | None) -> int: + c = av.open( + fate_suite( + "amv/MTV_high_res_320x240_sample_Penguin_Joke_MTV_from_WMV.amv" + ) + ) + if discard is not None: + c.streams.audio[0].discard = discard + count = sum( + 1 for p in c.demux() if p.dts is not None and p.stream.type == "audio" + ) + c.close() + return count + + baseline = audio_packets(None) + discarded = audio_packets(Discard.all) + assert baseline > 0 + assert discarded < baseline + def test_printing_video_stream(self) -> None: input_ = av.open( fate_suite("amv/MTV_high_res_320x240_sample_Penguin_Joke_MTV_from_WMV.amv")