From aac3a3fcfad633b5a37bee8cf7c31418e78c6076 Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Mon, 26 Oct 2026 00:04:15 -0400 Subject: [PATCH] Fix filter graph crashes when linking or adding after configure link_to() now raises ValueError for filters from different graphs instead of hitting av_assert0 in avfilter_link(). Adding or linking filters marks the graph unconfigured, so the next push or pull configures them instead of segfaulting. --- CHANGELOG.rst | 2 ++ av/filter/context.py | 4 +++ av/filter/graph.py | 2 ++ scripts/build-deps | 2 +- tests/test_filters.py | 61 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 70 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0530f4315..b3419780c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -39,6 +39,8 @@ Fixes: - ``Frame.time_base`` returns ``AVRational(0, 1)`` instead of ``None`` when unset, like other ``AVRational`` properties. - Fix type stubs that did not match runtime: ``average_rate``, ``base_rate``, and ``guessed_rate`` are only on ``VideoStream``; ``codec``, ``codec_tag``, and ``options`` are only on audio, video, and subtitle streams; ``Stream.codec_context`` may be ``None``; ``AudioStream.bit_rate``, ``InputContainer.start_time``, ``VideoFrame.time``, and ``FilterPad.context`` may be ``None``; ``Codec.hardware_configs`` is a tuple; ``CodecContext.delay`` is an ``int``; ``AudioStream.max_bit_rate`` is added; and the inaccessible ``Plane.frame``, ``Plane.index``, and ``Container.buffer_size`` are removed. - Fix more type stubs that did not match runtime: ``SubtitleSet`` items are ``AssSubtitle | BitmapSubtitle``; ``BitmapSubtitle`` supports ``len()``, iteration, and indexing; ``BitmapSubtitlePlane`` supports the buffer protocol; ``VideoFormat`` supports ``int()``; ``CodecContext.encode``, ``encode_lazy``, and ``decode`` have return types; ``SubtitleCodecContext.decode`` requires a packet; ``VideoStream.format`` may be ``None``; stream ``name`` is a ``str`` on audio, video, and subtitle streams; and ``VideoFrame.to_image``, ``set_image``, and ``from_image`` use ``PIL.Image.Image``. +- ``FilterContext.link_to`` raises ``ValueError`` when linking filters from different graphs instead of aborting the process. +- Filters added or linked after ``Graph.configure()`` are now configured on the next push or pull instead of segfaulting. v19.0.2 ------- diff --git a/av/filter/context.py b/av/filter/context.py index d660f8db2..c04719216 100644 --- a/av/filter/context.py +++ b/av/filter/context.py @@ -96,7 +96,11 @@ def link_to( output_idx: cython.int = 0, input_idx: cython.int = 0, ): + # avfilter_link() aborts the process on a cross-graph link. + if input_.graph is not self.graph: + raise ValueError("cannot link filters from different graphs") err_check(lib.avfilter_link(self.ptr, output_idx, input_.ptr, input_idx)) + self.graph.configured = False def push(self, frame: Frame | None): res: cython.int diff --git a/av/filter/graph.py b/av/filter/graph.py index 4293dfbf9..edeb1b4b1 100644 --- a/av/filter/graph.py +++ b/av/filter/graph.py @@ -126,6 +126,8 @@ def add(self, filter, args=None, **kwargs): ctx: FilterContext = wrap_filter_context(self, cy_filter, ptr) ctx.init(args, **kwargs) self._register_context(ctx) + # The new filter must be configured before it can push or pull. + self.configured = False # There might have been automatic contexts added (e.g. resamplers, # fifos, and scalers). It is more likely to see them after the graph diff --git a/scripts/build-deps b/scripts/build-deps index 28018636c..defb59ccf 100755 --- a/scripts/build-deps +++ b/scripts/build-deps @@ -83,7 +83,7 @@ echo ./configure --disable-bsfs \ --enable-bsf=chomp,extract_extradata,h264_mp4toannexb,setts \ --disable-filters \ - --enable-filter=abuffer,abuffersink,aformat,aresample,atempo,buffer,buffersink,bwdif,color,ebur128,loudnorm,lutrgb,overlay,palettegen,scale,testsrc,vflip,volume \ + --enable-filter=abuffer,abuffersink,aformat,aresample,atempo,buffer,buffersink,bwdif,color,ebur128,loudnorm,lutrgb,null,overlay,palettegen,scale,testsrc,vflip,volume \ --disable-avx512 \ $CONFFLAGS_NVIDIA \ $CONFFLAGS_RISCV \ diff --git a/tests/test_filters.py b/tests/test_filters.py index a86803c16..a8822077a 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -407,3 +407,64 @@ def test_graph_threads(self) -> None: with self.assertRaises(RuntimeError): graph.threads = 2 + + def test_link_across_graphs(self) -> None: + a = Graph().add("null") + b = Graph().add("null") + + with pytest.raises(ValueError, match="different graphs"): + a.link_to(b) + + def test_add_after_configure(self) -> None: + graph = Graph() + graph.configure() + assert graph.configured + + src = graph.add_buffer( + width=64, height=64, format="rgb24", time_base=Fraction(1, 24) + ) + sink = graph.add("buffersink") + assert not graph.configured + src.link_to(sink) + + frame = VideoFrame(64, 64, "rgb24") + frame.pts = 0 + src.push(frame) + assert graph.configured + out = sink.pull() + assert (out.width, out.height, out.pts) == (64, 64, 0) + + def test_pull_unlinked_sink_after_configure(self) -> None: + graph = Graph() + graph.configure() + sink = graph.add("buffersink") + + with pytest.raises(av.FFmpegError): + sink.pull() + + def test_extend_running_graph(self) -> None: + graph = Graph() + src = graph.add_buffer( + width=32, height=32, format="rgb24", time_base=Fraction(1, 24) + ) + sink = graph.add("buffersink") + src.link_to(sink) + frame = VideoFrame(32, 32, "rgb24") + frame.pts = 0 + src.push(frame) + assert sink.pull().pts == 0 + + src2 = graph.add_buffer( + width=16, height=16, format="gray", time_base=Fraction(1, 24) + ) + sink2 = graph.add("buffersink") + src2.link_to(sink2) + frame2 = VideoFrame(16, 16, "gray") + frame2.pts = 0 + src2.push(frame2) + out = sink2.pull() + assert isinstance(out, VideoFrame) and out.format.name == "gray" + + frame.pts = 1 + src.push(frame) + assert sink.pull().pts == 1