PR #25497 opened by diogochaves
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25497
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25497.patch

The ffmpeg CLI sometimes never exits when a filtergraph with several inputs has 
one output finish before the others: the output is all written, but every 
thread is idle and `main` sits in `sch_wait()`. The command below hangs in 
about one run in five on master (72/300) and 9.0.2 (55/300); with this patch, 
0/300 on both.

```
ffmpeg -f lavfi -i color=s=640x240:r=30 -f lavfi -i testsrc2=s=320x240:r=60 \
       -f lavfi -i testsrc2=s=320x240:r=60 -f lavfi -i sine \
       -filter_complex 
"[1:v]fps=30[s1];[2:v]fps=30[s2];[0:v][s1]overlay[o1];[o1][s2]overlay=x=320[v];[3:a]anull[a]"
 \
       -map "[v]" -map "[a]" -t 1 -f null -
```

What happens: the video output reaches `-t`, the graph closes the video inputs, 
and `close_input()` marks them receive-finished, so the thread queue drops 
their EOF and `eof_in[]` stays 0. `choose_input()` can then pick a closed input 
(on a tie, the one with the lower index), `sch_filter_receive()` waits on it, 
and `schedule_update_locked()` follows `best_input` to that input's finished 
demuxer, so the audio demuxer stays choked. In the hung process: `best_input = 
0`, inputs 0-2 receive-finished, demuxer 3 choked in `waiter_wait()`, `next_in 
= 0`, `eof_in = {0,1,1,0}`.

It also happens with real inputs: I first hit it composing two screen 
recordings plus audio (libx264 + AAC to MP4), 11/300 runs on 9.0.2, 0/300 with 
the patch. n8.0 hangs too on the command above (11/300), so it's older than 
38a5fcc02c, but closing inputs early made it much easier to hit. The output is 
unchanged (same framemd5 and decoded MD5s with and without the patch), FATE 
with samples passes, and the patch applies to release/9.0 as is.

I think this is also the hang in #23757: its repro hung here once in 52 runs on 
master, with the same state (filter thread waiting on closed input 0, the 
PNG-loop demuxers paused).

I tracked this down with the help of an AI coding agent; the backtraces, 
core-dump state and run counts are from my own runs.



>From 60857f5aa2139035116af939ce21d899045d2ce4 Mon Sep 17 00:00:00 2001
From: Diogo Chaves <[email protected]>
Date: Tue, 20 Oct 2026 15:42:10 -0300
Subject: [PATCH] fftools/ffmpeg_filter: do not wait for input on closed
 filtergraph inputs

Once the filtergraph stops accepting data on an input (e.g. one output
reached its -t limit and the EOF propagated back to the buffersrc),
close_input() marks it as receive-finished in the scheduler. The thread
queue then drops that input's EOF, so eof_in[] is never set for it.

choose_input() only skipped inputs with eof_in[] set, so it could still
pick such a closed input, e.g. when its frozen failed-request count ties
with the input that is actually needed and it has the lower index.
sch_filter_receive() then waits for a frame that will never come, and the
scheduler resolves the best input to that input's finished source, so it
chokes the demuxers that feed the inputs the graph still needs. All
threads end up idle and ffmpeg never exits.

Skip closed inputs, and inputs whose buffersrc already reports EOF, as
long as an open input is left to choose.

Hangs in about one run in five with:
ffmpeg -f lavfi -i color=s=640x240:r=30 \
       -f lavfi -i testsrc2=s=320x240:r=60 \
       -f lavfi -i testsrc2=s=320x240:r=60 -f lavfi -i sine \
       -filter_complex 
"[1:v]fps=30[s1];[2:v]fps=30[s2];[0:v][s1]overlay[o1];[o1][s2]overlay=x=320[v];[3:a]anull[a]"
 \
       -map "[v]" -map "[a]" -t 1 -f null -

Signed-off-by: Diogo Chaves <[email protected]>
---
 fftools/ffmpeg_filter.c | 16 +++++++++++++++-
 1 file changed, 15 insertions(+), 1 deletion(-)

diff --git a/fftools/ffmpeg_filter.c b/fftools/ffmpeg_filter.c
index 25c930f1380..70c44421233 100644
--- a/fftools/ffmpeg_filter.c
+++ b/fftools/ffmpeg_filter.c
@@ -2493,14 +2493,24 @@ static void send_command(FilterGraph *fg, AVFilterGraph 
*graph,
 static int choose_input(const FilterGraph *fg, const FilterGraphThread *fgt)
 {
     int nb_requests, nb_requests_max = -1;
-    int best_input = -1;
+    int best_input = -1, closed_input = -1;
 
     for (int i = 0; i < fg->nb_inputs; i++) {
         InputFilter *ifilter = fg->inputs[i];
+        InputFilterPriv *ifp = ifp_from_ifilter(ifilter);
 
         if (fgt->eof_in[i])
             continue;
 
+        // The graph no longer accepts data on this input. Once it is closed,
+        // the scheduler drops its EOF, so waiting for it would never return
+        // and would choke the sources that can still make progress.
+        if (ifp->eof || av_buffersrc_get_status(ifilter->filter)) {
+            if (closed_input < 0)
+                closed_input = i;
+            continue;
+        }
+
         nb_requests = av_buffersrc_get_nb_failed_requests(ifilter->filter);
         if (nb_requests > nb_requests_max) {
             nb_requests_max = nb_requests;
@@ -2508,6 +2518,10 @@ static int choose_input(const FilterGraph *fg, const 
FilterGraphThread *fgt)
         }
     }
 
+    // only closed inputs remain; the scheduler returns EOF once all are closed
+    if (best_input < 0)
+        best_input = closed_input;
+
     av_assert0(best_input >= 0);
 
     return best_input;
-- 
2.54.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to