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

Fixes ticket #7361.

# Summary of changes

When the input duration rounds to zero in the output timebase, the
rounded EOF timestamp can equal next_pts before any frame has been
output. In that case, preserve a buffered frame whose timestamp matches
next_pts instead of dropping it.

This preserves the existing eof_action rounding semantics while
preventing a valid first output frame from being dropped.

Adds a self-contained FATE regression test covering a short
single-frame input.


>From 7f41a43814e1e2f63a0294477c537de6f6aeb4d0 Mon Sep 17 00:00:00 2001
From: Skanda Vyas <[email protected]>
Date: Wed, 16 Sep 2026 19:15:04 -0700
Subject: [PATCH] avfilter/vf_fps: preserve first output frame at EOF

When the input duration rounds to zero in the output timebase, the
rounded EOF timestamp can equal next_pts before any frame has been
output. In that case, do not discard a buffered frame whose timestamp
matches next_pts.

This preserves the existing eof_action rounding semantics while
preventing a valid first output frame from being dropped.

Add a FATE regression test covering a short single-frame input.

Fixes ticket #7361.

Signed-off-by: Skanda Vyas <[email protected]>
---
 libavfilter/vf_fps.c             | 12 ++++++++++--
 tests/fate/filter-video.mak      |  3 ++-
 tests/ref/fate/filter-fps-single |  6 ++++++
 3 files changed, 18 insertions(+), 3 deletions(-)
 create mode 100644 tests/ref/fate/filter-fps-single

diff --git a/libavfilter/vf_fps.c b/libavfilter/vf_fps.c
index 0257a8ef78..2b116ed1c2 100644
--- a/libavfilter/vf_fps.c
+++ b/libavfilter/vf_fps.c
@@ -264,6 +264,8 @@ static int read_frame(AVFilterContext *ctx, FPSContext *s, 
AVFilterLink *inlink,
 static int write_frame(AVFilterContext *ctx, FPSContext *s, AVFilterLink 
*outlink, int *again)
 {
     AVFrame *frame;
+    int eof_before_or_at_next;
+    int first_output_due;
 
     av_assert1(s->frames_count == 2 || (s->status && s->frames_count == 1));
 
@@ -282,13 +284,19 @@ static int write_frame(AVFilterContext *ctx, FPSContext 
*s, AVFilterLink *outlin
         }
     }
 
+    eof_before_or_at_next = s->status && s->status_pts <= s->next_pts;
+    first_output_due      = !s->frames_out && !s->cur_frame_out &&
+                            s->frames[0]->pts == s->next_pts &&
+                            s->status_pts == s->next_pts;
+
     /* There are two conditions where we want to drop a frame:
      * - If we have two buffered frames and the second frame is acceptable
      *   as the next output frame, then drop the first buffered frame.
      * - If we have status (EOF) set, drop frames when we hit the
-     *   status timestamp. */
+     *   status timestamp, unless no frame has been output yet and the
+     *   buffered frame is due at the next output timestamp. */
     if ((s->frames_count == 2 && s->frames[1]->pts <= s->next_pts) ||
-        (s->status            && s->status_pts     <= s->next_pts)) {
+        (eof_before_or_at_next && !first_output_due)) {
 
         frame = shift_frame(ctx, s);
         av_frame_free(&frame);
diff --git a/tests/fate/filter-video.mak b/tests/fate/filter-video.mak
index b0284c9813..23b8a9f9eb 100644
--- a/tests/fate/filter-video.mak
+++ b/tests/fate/filter-video.mak
@@ -490,7 +490,7 @@ fate-filter-mpdecimate-mode-1: CMD = framecrc -lavfi 
testsrc2=r=4:d=5,fps=5,mpde
 FATE_FILTER-$(call FILTERFRAMECRC, TESTSRC2 FPS MPDECIMATE) += 
fate-filter-mpdecimate-mode-1-min-3
 fate-filter-mpdecimate-mode-1-min-3: CMD = framecrc -lavfi 
testsrc2=r=2:d=7,fps=7,mpdecimate=mode=1:min=3 -pix_fmt yuv420p
 
-FATE_FILTER-$(call FILTERFRAMECRC, FPS TESTSRC2) += $(addprefix 
fate-filter-fps-, up up-round-down up-round-up down down-round-down 
down-round-up down-eof-pass start-drop start-fill)
+FATE_FILTER-$(call FILTERFRAMECRC, FPS TESTSRC2) += $(addprefix 
fate-filter-fps-, up up-round-down up-round-up down down-round-down 
down-round-up down-eof-pass start-drop start-fill single)
 fate-filter-fps-up: CMD = framecrc -lavfi testsrc2=r=3:d=2,fps=7
 fate-filter-fps-up-round-down: CMD = framecrc -lavfi 
testsrc2=r=3:d=2,fps=7:round=down
 fate-filter-fps-up-round-up: CMD = framecrc -lavfi 
testsrc2=r=3:d=2,fps=7:round=up
@@ -500,6 +500,7 @@ fate-filter-fps-down-round-up: CMD = framecrc -lavfi 
testsrc2=r=7:d=3.5,fps=3:ro
 fate-filter-fps-down-eof-pass: CMD = framecrc -lavfi 
testsrc2=r=7:d=3.5,fps=3:eof_action=pass
 fate-filter-fps-start-drop: CMD = framecrc -lavfi 
testsrc2=r=7:d=3.5,fps=3:start_time=1.5
 fate-filter-fps-start-fill: CMD = framecrc -lavfi 
testsrc2=r=7:d=1.5,setpts=PTS+14,fps=3:start_time=1.5
+fate-filter-fps-single: CMD = framecrc -lavfi 
testsrc2=s=16x16:r=25:d=0.04,fps=1
 
 DRAWVG_SCRIPT_ALL = $(SRC_PATH)/tests/ref/lavf/drawvg.all
 
diff --git a/tests/ref/fate/filter-fps-single b/tests/ref/fate/filter-fps-single
new file mode 100644
index 0000000000..d315556861
--- /dev/null
+++ b/tests/ref/fate/filter-fps-single
@@ -0,0 +1,6 @@
+#tb 0: 1/1
+#media_type 0: video
+#codec_id 0: rawvideo
+#dimensions 0: 16x16
+#sar 0: 1/1
+0,          0,          0,        1,      384, 0x92e591ca
-- 
2.52.0

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

Reply via email to