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

When input 0's last frame has been mixed, another input can trigger 
output_frame() before input 0's EOF is acknowledged. Reading the empty frame 
list overwrites the correctly advanced next_pts with AV_NOPTS_VALUE, causing 
subsequent output to lose timestamps and potentially making concat fail.

Only update next_pts when the frame list is nonempty, following the guard 
proposed in #25721. Add a deterministic public-API FATE test covering both EOF 
orderings and zero and nonzero initial timestamps. Expected output lives in 
tests/ref/fate/filter-amix-pts.

Validation on Linux AArch64 with GCC 13.3.0: the new test fails without the 
guard and passes with it; fate-filter-amix-simple, fate-filter-amix-first, and 
fate-filter-amix-transition pass. The fixed reproduction graphs completed 100 
timestamp checks and 100 concat runs without failures, retaining the expected 
six- and eight-second sample counts.

Fixes #25721.


>From 74e777ebd50612e472973d73428de597585b7145 Mon Sep 17 00:00:00 2001
From: Michael Niedermayer <[email protected]>
Date: Fri, 6 Nov 2026 18:11:45 +0000
Subject: [PATCH] avfilter/af_amix: preserve next_pts while waiting for the
 first input's EOF

After the last frame of input 0 is mixed, another input can trigger
output_frame() before input 0's EOF is acknowledged. The empty frame
list supplies AV_NOPTS_VALUE, overwriting the timestamp already advanced
past the last mixed frame. Subsequent output then keeps missing PTS.

Only update next_pts when the frame list is nonempty, as proposed in
issue #25721. Add a deterministic buffersrc/buffersink FATE test covering
both EOF orderings and zero and nonzero initial timestamps.

Fixes: #25721
Assisted-by: Fairy
---
 libavfilter/Makefile           |   1 +
 libavfilter/af_amix.c          |   5 +-
 libavfilter/tests/amix.c       | 147 +++++++++++++++++++++++++++++++++
 tests/fate/filter-audio.mak    |   7 +-
 tests/ref/fate/filter-amix-pts |  38 +++++++++
 5 files changed, 196 insertions(+), 2 deletions(-)
 create mode 100644 libavfilter/tests/amix.c
 create mode 100644 tests/ref/fate/filter-amix-pts

diff --git a/libavfilter/Makefile b/libavfilter/Makefile
index 732332d5b2b..f5418eeac26 100644
--- a/libavfilter/Makefile
+++ b/libavfilter/Makefile
@@ -690,6 +690,7 @@ SKIPHEADERS-$(CONFIG_SCALE_CUDA_FILTER)      += 
vf_scale_cuda.h
 TOOLS     = graph2dot
 TESTPROGS = drawutils filtfmts formats integral
 
+TESTPROGS-$(CONFIG_AMIX_FILTER)   += amix
 TESTPROGS-$(CONFIG_DRAWVG_FILTER) += drawvg
 
 TOOLS-$(CONFIG_LIBZMQ) += zmqsend
diff --git a/libavfilter/af_amix.c b/libavfilter/af_amix.c
index 082d69b97fc..aef438aeabd 100644
--- a/libavfilter/af_amix.c
+++ b/libavfilter/af_amix.c
@@ -316,7 +316,10 @@ static int output_frame(AVFilterLink *outlink)
             }
         }
 
-        s->next_pts = frame_list_next_pts(s->frame_list);
+        /* Preserve the next timestamp if input 0 has no queued frames
+         * but its EOF has not been acknowledged yet. */
+        if (s->frame_list->nb_frames)
+            s->next_pts = frame_list_next_pts(s->frame_list);
     } else {
         /* first input closed: use the available samples */
         nb_samples = INT_MAX;
diff --git a/libavfilter/tests/amix.c b/libavfilter/tests/amix.c
new file mode 100644
index 00000000000..d6ef5ed4816
--- /dev/null
+++ b/libavfilter/tests/amix.c
@@ -0,0 +1,147 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include <stdio.h>
+
+#include "libavutil/channel_layout.h"
+#include "libavutil/error.h"
+#include "libavutil/frame.h"
+#include "libavutil/samplefmt.h"
+#include "libavutil/timestamp.h"
+#include "libavfilter/avfilter.h"
+#include "libavfilter/buffersink.h"
+#include "libavfilter/buffersrc.h"
+
+static int send_frame(AVFilterContext *src, int64_t pts)
+{
+    AVFrame *frame = av_frame_alloc();
+    int ret;
+
+    if (!frame)
+        return AVERROR(ENOMEM);
+    frame->format = AV_SAMPLE_FMT_FLT;
+    frame->sample_rate = 48000;
+    frame->ch_layout = (AVChannelLayout)AV_CHANNEL_LAYOUT_MONO;
+    frame->nb_samples = 1024;
+    frame->pts = pts;
+    ret = av_frame_get_buffer(frame, 0);
+    if (ret >= 0) {
+        av_samples_set_silence(frame->extended_data, 0, frame->nb_samples,
+                              1, frame->format);
+        ret = av_buffersrc_add_frame(src, frame);
+    }
+    av_frame_free(&frame);
+    return ret;
+}
+
+static int drain(AVFilterContext *sink, AVFrame *frame)
+{
+    int ret;
+
+    while ((ret = av_buffersink_get_frame(sink, frame)) >= 0) {
+        printf("pts:%s samples:%d\n", av_ts2str(frame->pts), 
frame->nb_samples);
+        av_frame_unref(frame);
+    }
+    if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
+        printf("%s\n", ret == AVERROR(EAGAIN) ? "EAGAIN" : "EOF");
+        return 0;
+    }
+    return ret;
+}
+
+static int test(int late_eof, int64_t pts)
+{
+    AVFilterGraph *graph = avfilter_graph_alloc();
+    AVFrame *frame = av_frame_alloc();
+    AVFilterContext *src[2], *mix, *sink;
+    int ret = AVERROR(ENOMEM);
+
+    if (!graph || !frame)
+        goto end;
+    graph->nb_threads = 1;
+    printf("late_eof:%d start_pts:%s\n", late_eof, av_ts2str(pts));
+    for (int i = 0; i < 2; i++) {
+        ret = avfilter_graph_create_filter(&src[i], 
avfilter_get_by_name("abuffer"),
+                                          i ? "input1" : "input0",
+                                          "sample_fmt=flt:sample_rate=48000:"
+                                          
"channel_layout=mono:time_base=1/48000",
+                                          NULL, graph);
+        if (ret < 0)
+            goto end;
+    }
+    ret = avfilter_graph_create_filter(&mix, avfilter_get_by_name("amix"),
+                                      "mix", "duration=longest", NULL, graph);
+    if (ret < 0)
+        goto end;
+    ret = avfilter_graph_create_filter(&sink, 
avfilter_get_by_name("abuffersink"),
+                                      "sink", NULL, NULL, graph);
+    if (ret < 0)
+        goto end;
+    for (int i = 0; i < 2; i++) {
+        ret = avfilter_link(src[i], 0, mix, i);
+        if (ret < 0)
+            goto end;
+    }
+    if ((ret = avfilter_link(mix, 0, sink, 0)) < 0 ||
+        (ret = avfilter_graph_config(graph, NULL)) < 0 ||
+        (ret = send_frame(src[0], pts)) < 0 ||
+        (ret = send_frame(src[1], 0)) < 0 ||
+        (ret = drain(sink, frame)) < 0)
+        goto end;
+
+    /* Feed input 1 while input 0's frame list is empty. In the late EOF
+     * case, input 0 is still live when this frame is consumed. */
+    if (!late_eof) {
+        if ((ret = av_buffersrc_add_frame(src[0], NULL)) < 0 ||
+            (ret = drain(sink, frame)) < 0)
+            goto end;
+    }
+    if ((ret = send_frame(src[1], 1024)) < 0 ||
+        (ret = drain(sink, frame)) < 0)
+        goto end;
+    if (late_eof && (ret = av_buffersrc_add_frame(src[0], NULL)) < 0)
+        goto end;
+    if ((ret = drain(sink, frame)) < 0 ||
+        (ret = send_frame(src[1], 2048)) < 0 ||
+        (ret = drain(sink, frame)) < 0 ||
+        (ret = av_buffersrc_add_frame(src[1], NULL)) < 0 ||
+        (ret = drain(sink, frame)) < 0)
+        goto end;
+
+end:
+    av_frame_free(&frame);
+    avfilter_graph_free(&graph);
+    return ret;
+}
+
+int main(void)
+{
+    const int64_t pts[] = { 0, 48000 };
+    int ret;
+
+    for (int i = 0; i < FF_ARRAY_ELEMS(pts); i++) {
+        for (int late_eof = 0; late_eof < 2; late_eof++) {
+            ret = test(late_eof, pts[i]);
+            if (ret < 0) {
+                fprintf(stderr, "%s\n", av_err2str(ret));
+                return 1;
+            }
+        }
+    }
+    return 0;
+}
diff --git a/tests/fate/filter-audio.mak b/tests/fate/filter-audio.mak
index f1035bba24a..dd709d51a81 100644
--- a/tests/fate/filter-audio.mak
+++ b/tests/fate/filter-audio.mak
@@ -458,6 +458,10 @@ fate-filter-crazychannels: 
tests/data/filtergraphs/crazychannels
 fate-filter-crazychannels: CMD = framecrc -auto_conversion_filters 
-/filter_complex $(TARGET_PATH)/tests/data/filtergraphs/crazychannels
 FATE_AFILTER-$(call FILTERFRAMECRC, ARESAMPLE SINE JOIN ATRIM CHANNELMAP 
CHANNELSPLIT, FILE_PROTOCOL) += fate-filter-crazychannels
 
+FATE_AFILTER_API-$(CONFIG_AMIX_FILTER) += fate-filter-amix-pts
+fate-filter-amix-pts: libavfilter/tests/amix$(EXESUF)
+fate-filter-amix-pts: CMD = run libavfilter/tests/amix$(EXESUF)
+
 FATE_AFILTER-yes += fate-filter-formats
 fate-filter-formats: libavfilter/tests/formats$(EXESUF)
 fate-filter-formats: CMD = run libavfilter/tests/formats$(EXESUF)
@@ -465,4 +469,5 @@ fate-filter-formats: CMD = run 
libavfilter/tests/formats$(EXESUF)
 FATE_AFILTER-yes := $(if $(call FRAMECRC), $(FATE_AFILTER-yes))
 FATE_SAMPLES_AVCONV += $(FATE_AFILTER_SAMPLES-yes)
 FATE_FFMPEG += $(FATE_AFILTER-yes)
-fate-afilter: $(FATE_AFILTER-yes) $(FATE_AFILTER_SAMPLES-yes)
+FATE-$(CONFIG_AVFILTER) += $(FATE_AFILTER_API-yes)
+fate-afilter: $(FATE_AFILTER-yes) $(FATE_AFILTER_SAMPLES-yes) 
$(FATE_AFILTER_API-yes)
diff --git a/tests/ref/fate/filter-amix-pts b/tests/ref/fate/filter-amix-pts
new file mode 100644
index 00000000000..be337c721f6
--- /dev/null
+++ b/tests/ref/fate/filter-amix-pts
@@ -0,0 +1,38 @@
+late_eof:0 start_pts:0
+pts:0 samples:1024
+EAGAIN
+EAGAIN
+pts:1024 samples:1024
+EAGAIN
+EAGAIN
+pts:2048 samples:1024
+EAGAIN
+EOF
+late_eof:1 start_pts:0
+pts:0 samples:1024
+EAGAIN
+EAGAIN
+pts:1024 samples:1024
+EAGAIN
+pts:2048 samples:1024
+EAGAIN
+EOF
+late_eof:0 start_pts:48000
+pts:48000 samples:1024
+EAGAIN
+EAGAIN
+pts:49024 samples:1024
+EAGAIN
+EAGAIN
+pts:50048 samples:1024
+EAGAIN
+EOF
+late_eof:1 start_pts:48000
+pts:48000 samples:1024
+EAGAIN
+EAGAIN
+pts:49024 samples:1024
+EAGAIN
+pts:50048 samples:1024
+EAGAIN
+EOF
-- 
2.54.0

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

Reply via email to