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]
