This is an automated email from the git hooks/post-receive script.
Git pushed a commit to branch master
in repository ffmpeg.
The following commit(s) were added to refs/heads/master by this push:
new 361174e5ea avfilter/vf_fps: preserve first output frame at EOF
361174e5ea is described below
commit 361174e5ea6bd65e34554afa1d098dda2a9bcbba
Author: Skanda Vyas <[email protected]>
AuthorDate: Wed Sep 16 19:15:04 2026 -0700
Commit: michaelni <[email protected]>
CommitDate: Sat Oct 3 15:53:40 2026 +0000
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.
Keep the selected EOF timestamp rounding, but allow this first-frame
exception to extend output by one output frame period. Preserve start_time
trimming by checking the last input frame and EOF before rounding. A frame
located at start_time remains eligible even when its duration is zero.
Document the first-frame exception and add FATE coverage for a short
single-frame input and trimming at start_time, including a frame spanning
the start and a zero-duration frame at the start.
Fixes ticket #7361.
Signed-off-by: Skanda Vyas <[email protected]>
Assisted-by: Fairy
---
doc/filters.texi | 6 ++++++
libavfilter/vf_fps.c | 25 +++++++++++++++++++---
tests/fate/filter-video.mak | 13 ++++++++++-
.../{filter-palettegen-1 => filter-fps-single} | 4 ++--
...palettegen-1 => filter-fps-single-before-start} | 3 +--
...palettegen-1 => filter-fps-single-end-at-start} | 3 +--
6 files changed, 44 insertions(+), 10 deletions(-)
diff --git a/doc/filters.texi b/doc/filters.texi
index 81e9e280ed..406ead7198 100644
--- a/doc/filters.texi
+++ b/doc/filters.texi
@@ -14818,6 +14818,12 @@ Pass through last frame if input duration has not been
reached yet.
@end table
The default is @code{round}.
+If timestamp rounding would otherwise discard every frame, one frame is
+retained when its rounded timestamp equals both the first output timestamp
+and the rounded end of input. This exception extends the output beyond the
+rounded end of input by one output frame period. Frames whose entire interval
+is trimmed by @option{start_time} remain discarded.
+
@end table
Alternatively, the options can be specified as a flat string:
diff --git a/libavfilter/vf_fps.c b/libavfilter/vf_fps.c
index 0257a8ef78..e81d7b1668 100644
--- a/libavfilter/vf_fps.c
+++ b/libavfilter/vf_fps.c
@@ -83,6 +83,8 @@ typedef struct FPSContext {
/* Runtime state */
int status; ///< buffered input status
int64_t status_pts; ///< buffered input status timestamp
+ int64_t last_in_pts; ///< last frame timestamp in input timebase
+ int last_frame_trimmed; ///< last frame ends at or before start_time
AVFrame *frames[2]; ///< buffered frames
int frames_count; ///< number of buffered frames
@@ -245,7 +247,7 @@ static int read_frame(AVFilterContext *ctx, FPSContext *s,
AVFilterLink *inlink,
/* Convert frame pts to output timebase.
* The dance with offsets is required to match the rounding behaviour of
the
* previous version of the fps filter when using the start_time option. */
- in_pts = frame->pts;
+ in_pts = s->last_in_pts = frame->pts;
frame->pts = s->out_pts_off + av_rescale_q_rnd(in_pts - s->in_pts_off,
inlink->time_base,
outlink->time_base,
s->rounding |
AV_ROUND_PASS_MINMAX);
@@ -264,6 +266,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 +286,20 @@ 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->last_frame_trimmed &&
+ 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);
@@ -317,6 +328,14 @@ static int write_frame(AVFilterContext *ctx, FPSContext
*s, AVFilterLink *outlin
static void update_eof_pts(AVFilterContext *ctx, FPSContext *s, AVFilterLink
*inlink, AVFilterLink *outlink, int64_t status_pts)
{
int eof_rounding = (s->eof_action == EOF_ACTION_PASS) ? AV_ROUND_UP :
s->rounding;
+
+ /* Check trimming before rounding can collapse a frame before start_time
+ * and the end of its interval onto the first output timestamp. A frame
+ * starting at start_time is still eligible even with zero duration. */
+ s->last_frame_trimmed = s->start_time != DBL_MAX &&
+ s->start_time != AV_NOPTS_VALUE &&
+ s->last_in_pts < s->in_pts_off &&
+ status_pts <= s->in_pts_off;
s->status_pts = av_rescale_q_rnd(status_pts, inlink->time_base,
outlink->time_base,
eof_rounding | AV_ROUND_PASS_MINMAX);
diff --git a/tests/fate/filter-video.mak b/tests/fate/filter-video.mak
index b746f6a0e8..564c970e60 100644
--- a/tests/fate/filter-video.mak
+++ b/tests/fate/filter-video.mak
@@ -501,7 +501,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
@@ -511,6 +511,17 @@ 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
+
+FATE_FILTER-$(call FILTERFRAMECRC, FPS TESTSRC2 SETPTS) +=
fate-filter-fps-single-before-start fate-filter-fps-single-end-at-start
fate-filter-fps-single-zero-duration
+fate-filter-fps-single-before-start: CMD = framecrc -lavfi
testsrc2=s=16x16:r=25:d=0.04,setpts=PTS-2,fps=1:start_time=0
+fate-filter-fps-single-end-at-start: CMD = framecrc -lavfi
testsrc2=s=16x16:r=25:d=0.04,setpts=PTS-1,fps=1:start_time=0
+fate-filter-fps-single-zero-duration: CMD = framecrc -lavfi
testsrc2=s=16x16:r=25:d=0.04,setpts=0:strip_fps=1,fps=1:start_time=0
+fate-filter-fps-single-zero-duration: REF =
$(SRC_PATH)/tests/ref/fate/filter-fps-single
+
+FATE_FILTER-$(call FILTERFRAMECRC, FPS TESTSRC2 SETPTS SETTB) +=
fate-filter-fps-single-cross-start
+fate-filter-fps-single-cross-start: CMD = framecrc -lavfi
testsrc2=s=16x16:r=25/2:d=0.08,settb=1/25,setpts=PTS-1,fps=1:start_time=0
+fate-filter-fps-single-cross-start: REF =
$(SRC_PATH)/tests/ref/fate/filter-fps-single
FATE_LIBAVFILTER-yes += fate-filter-drawutils
fate-filter-drawutils: libavfilter/tests/drawutils$(EXESUF)
diff --git a/tests/ref/fate/filter-palettegen-1
b/tests/ref/fate/filter-fps-single
similarity index 50%
copy from tests/ref/fate/filter-palettegen-1
copy to tests/ref/fate/filter-fps-single
index 1e5c9ee002..d315556861 100644
--- a/tests/ref/fate/filter-palettegen-1
+++ b/tests/ref/fate/filter-fps-single
@@ -1,6 +1,6 @@
-#tb 0: 1001/24000
+#tb 0: 1/1
#media_type 0: video
#codec_id 0: rawvideo
#dimensions 0: 16x16
#sar 0: 1/1
-0, 0, 0, 1, 1024, 0xa285dd77
+0, 0, 0, 1, 384, 0x92e591ca
diff --git a/tests/ref/fate/filter-palettegen-1
b/tests/ref/fate/filter-fps-single-before-start
similarity index 50%
copy from tests/ref/fate/filter-palettegen-1
copy to tests/ref/fate/filter-fps-single-before-start
index 1e5c9ee002..2d09a982e2 100644
--- a/tests/ref/fate/filter-palettegen-1
+++ b/tests/ref/fate/filter-fps-single-before-start
@@ -1,6 +1,5 @@
-#tb 0: 1001/24000
+#tb 0: 1/1
#media_type 0: video
#codec_id 0: rawvideo
#dimensions 0: 16x16
#sar 0: 1/1
-0, 0, 0, 1, 1024, 0xa285dd77
diff --git a/tests/ref/fate/filter-palettegen-1
b/tests/ref/fate/filter-fps-single-end-at-start
similarity index 50%
copy from tests/ref/fate/filter-palettegen-1
copy to tests/ref/fate/filter-fps-single-end-at-start
index 1e5c9ee002..2d09a982e2 100644
--- a/tests/ref/fate/filter-palettegen-1
+++ b/tests/ref/fate/filter-fps-single-end-at-start
@@ -1,6 +1,5 @@
-#tb 0: 1001/24000
+#tb 0: 1/1
#media_type 0: video
#codec_id 0: rawvideo
#dimensions 0: 16x16
#sar 0: 1/1
-0, 0, 0, 1, 1024, 0xa285dd77
--
To stop receiving notification emails like this one, please contact
[email protected].
_______________________________________________
ffmpeg-cvslog mailing list -- [email protected]
To unsubscribe send an email to [email protected]