Commit 7b00cbd7ed for ffmpeg
commit 7b00cbd7edf64c439209fb473dd21943c49a7178
Author: Baptiste Coudurier <baptiste.coudurier@gmail.com>
Date: Fri Oct 16 13:02:54 2026 -0700
avformat/demux: ignore corrupt packets when estimating the frame rate
A corrupt packet may hold only part of a frame, which skews the
average frame rate estimated from the few probed packets.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Baptiste Coudurier <baptiste.coudurier@gmail.com>
diff --git a/libavformat/Makefile b/libavformat/Makefile
index 17da02eee7..1ccaad2733 100644
--- a/libavformat/Makefile
+++ b/libavformat/Makefile
@@ -788,7 +788,8 @@ SKIPHEADERS-$(CONFIG_IMF_DEMUXER) += imf.h
SKIPHEADERS-$(CONFIG_FFRTMPCRYPT_PROTOCOL) += rtmpdh.h
SKIPHEADERS-$(CONFIG_NETWORK) += ip.h network.h rtsp.h
-TESTPROGS = id3v2 \
+TESTPROGS = demux_probe \
+ id3v2 \
mkdir \
rename \
seek \
diff --git a/libavformat/demux.c b/libavformat/demux.c
index 1468726b09..3f85744583 100644
--- a/libavformat/demux.c
+++ b/libavformat/demux.c
@@ -2973,8 +2973,15 @@ int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options)
sti->info->codec_info_duration = FFMIN(pkt->pts - st->start_time, sti->info->codec_info_duration + pkt->duration);
} else
sti->info->codec_info_duration += pkt->duration;
- sti->info->codec_info_duration_fields += sti->parser && sti->need_parsing && fields
- ? sti->parser->repeat_pict + 1 : 2;
+ /* A corrupt packet may hold only part of a frame. Keep its
+ * duration in the probing budget, but exclude it from FPS. */
+ if (avctx->codec_type == AVMEDIA_TYPE_VIDEO &&
+ !(pkt->flags & AV_PKT_FLAG_CORRUPT) &&
+ pkt->duration < INT64_MAX - sti->info->fps_duration) {
+ sti->info->fps_duration += pkt->duration;
+ sti->info->codec_info_duration_fields += sti->parser && sti->need_parsing && fields
+ ? sti->parser->repeat_pict + 1 : 2;
+ }
}
}
if (st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
@@ -3074,18 +3081,18 @@ int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options)
/* estimate average framerate if not set by demuxer */
if (sti->info->codec_info_duration_fields &&
!st->avg_frame_rate.num &&
- sti->info->codec_info_duration) {
+ sti->info->fps_duration) {
int best_fps = 0;
double best_error = 0.01;
AVRational codec_frame_rate = avctx->framerate;
- if (sti->info->codec_info_duration >= INT64_MAX / st->time_base.num / 2||
+ if (sti->info->fps_duration >= INT64_MAX / st->time_base.num / 2||
sti->info->codec_info_duration_fields >= INT64_MAX / st->time_base.den ||
- sti->info->codec_info_duration < 0)
+ sti->info->fps_duration < 0)
continue;
av_reduce(&st->avg_frame_rate.num, &st->avg_frame_rate.den,
sti->info->codec_info_duration_fields * (int64_t) st->time_base.den,
- sti->info->codec_info_duration * 2 * (int64_t) st->time_base.num, 60000);
+ sti->info->fps_duration * 2 * (int64_t) st->time_base.num, 60000);
/* Round guessed framerate to a "standard" framerate if it's
* within 1% of the original estimate. */
diff --git a/libavformat/demux.h b/libavformat/demux.h
index ed64afd325..37d08fb2ca 100644
--- a/libavformat/demux.h
+++ b/libavformat/demux.h
@@ -204,7 +204,8 @@ typedef struct FFStreamInfo {
int duration_count;
int64_t rfps_duration_sum;
double (*duration_error)[2][MAX_STD_TIMEBASES];
- int64_t codec_info_duration;
+ int64_t codec_info_duration; /**< Duration used for the probing budget. */
+ int64_t fps_duration; /**< Duration of non-corrupt FPS samples. */
int64_t codec_info_duration_fields;
int frame_delay_evidence;
diff --git a/libavformat/tests/.gitignore b/libavformat/tests/.gitignore
index b5fd1ff009..c3afb57677 100644
--- a/libavformat/tests/.gitignore
+++ b/libavformat/tests/.gitignore
@@ -1,4 +1,5 @@
/auenc
+/demux_probe
/id3v2
/id3v2enc
/fifo_muxer
diff --git a/libavformat/tests/demux_probe.c b/libavformat/tests/demux_probe.c
new file mode 100644
index 0000000000..e482975789
--- /dev/null
+++ b/libavformat/tests/demux_probe.c
@@ -0,0 +1,105 @@
+/*
+ * 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/error.h"
+#include "libavutil/log.h"
+#include "libavutil/pixfmt.h"
+#include "libavformat/avformat.h"
+#include "libavformat/demux.h"
+
+static int packets_read;
+static int corrupt_after;
+static int corrupt_duration;
+
+static int read_header(AVFormatContext *s)
+{
+ AVStream *st = avformat_new_stream(s, NULL);
+
+ if (!st)
+ return AVERROR(ENOMEM);
+ st->time_base = (AVRational) { 1, 15 };
+ st->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
+ st->codecpar->codec_id = AV_CODEC_ID_RAWVIDEO;
+ st->codecpar->format = AV_PIX_FMT_GRAY8;
+ st->codecpar->width = 1;
+ st->codecpar->height = 1;
+ return 0;
+}
+
+static int read_packet(AVFormatContext *s, AVPacket *pkt)
+{
+ int ret;
+
+ if (packets_read >= 450)
+ return AVERROR_EOF;
+ ret = av_new_packet(pkt, 1);
+ if (ret < 0)
+ return ret;
+ pkt->data[0] = 0;
+ pkt->stream_index = 0;
+ pkt->pts = pkt->dts = packets_read;
+ pkt->duration = 1;
+ if (packets_read >= corrupt_after) {
+ pkt->pts = pkt->dts = corrupt_after +
+ (packets_read - corrupt_after) * corrupt_duration;
+ pkt->duration = corrupt_duration;
+ pkt->flags |= AV_PKT_FLAG_CORRUPT;
+ }
+ packets_read++;
+ return 0;
+}
+
+static const FFInputFormat test_demuxer = {
+ .p.name = "test",
+ .p.flags = AVFMT_NOFILE,
+ .read_header = read_header,
+ .read_packet = read_packet,
+};
+
+int main(void)
+{
+ av_log_set_level(AV_LOG_ERROR);
+ for (int corrupt = 0; corrupt < 3; corrupt++) {
+ AVFormatContext *s = avformat_alloc_context();
+ int ret;
+
+ if (!s)
+ return 1;
+ packets_read = 0;
+ corrupt_after = corrupt ? 5 : 450;
+ corrupt_duration = corrupt == 2 ? 2 : 1;
+ s->max_analyze_duration = 500000;
+ /* Keep probing for FPS until the analysis-duration limit stops it. */
+ s->fps_probe_size = 1000;
+ ret = avformat_open_input(&s, NULL, &test_demuxer.p, NULL);
+ if (ret >= 0)
+ ret = avformat_find_stream_info(s, NULL);
+ if (ret >= 0) {
+ AVRational fps = s->streams[0]->avg_frame_rate;
+ printf("%s: packets=%d fps=%d/%d\n",
+ corrupt == 2 ? "corrupt-long" : corrupt ? "corrupt" : "clean",
+ packets_read, fps.num, fps.den);
+ }
+ avformat_close_input(&s);
+ if (ret < 0)
+ return 1;
+ }
+ return 0;
+}
diff --git a/tests/fate/libavformat.mak b/tests/fate/libavformat.mak
index 6daa10df86..70d5439437 100644
--- a/tests/fate/libavformat.mak
+++ b/tests/fate/libavformat.mak
@@ -1,3 +1,7 @@
+FATE_LIBAVFORMAT += fate-demux-probe
+fate-demux-probe: libavformat/tests/demux_probe$(EXESUF)
+fate-demux-probe: CMD = run libavformat/tests/demux_probe$(EXESUF)
+
#FATE_LIBAVFORMAT-$(HAVE_PTHREADS) += fate-async
#fate-async: libavformat/tests/async$(EXESUF)
#fate-async: CMD = run libavformat/tests/async
diff --git a/tests/fate/mxf.mak b/tests/fate/mxf.mak
index e64da46d77..9b56bfa8c8 100644
--- a/tests/fate/mxf.mak
+++ b/tests/fate/mxf.mak
@@ -29,6 +29,12 @@ FATE_MXF_PROBE-$(call DEMDEC, MXF, H264, H264_PARSER EXTRACT_EXTRADATA_BSF) += f
fate-mxf-probe-h264-extradata: SRC = $(TARGET_SAMPLES)/h264/SonyXAVC_LongGOP_green_pixelation_early_Frames.MXF
fate-mxf-probe-h264-extradata: CMD = run ffprobe$(PROGSSUF)$(EXESUF) -bitexact -v 0 -fflags +discardcorrupt -select_streams v -show_entries stream=avg_frame_rate,extradata,extradata_size,extradata_hash -show_data_hash md5 -i "$(SRC)"
+# The companion paired-field parser fix changes the reference to 25/1.
+# Keep the truncated final packet to exercise corrupt-packet FPS accounting.
+FATE_MXF_PROBE-$(call DEMDEC, MXF, H264, H264_PARSER EXTRACT_EXTRADATA_BSF) += fate-mxf-probe-h264-corrupt
+fate-mxf-probe-h264-corrupt: SRC = $(TARGET_SAMPLES)/h264/SonyXAVC_LongGOP_green_pixelation_early_Frames.MXF
+fate-mxf-probe-h264-corrupt: CMD = run ffprobe$(PROGSSUF)$(EXESUF) -bitexact -v 0 -select_streams v -show_entries stream=avg_frame_rate -i "$(SRC)"
+
FATE_MXF_PROBE-$(call DEMDEC, MXF, DNXHD) += fate-mxf-probe-dnxhd
fate-mxf-probe-dnxhd: SRC = $(TARGET_SAMPLES)/mxf/multiple_components.mxf
fate-mxf-probe-dnxhd: CMD = run $(PROBE_FORMAT_STREAMS_COMMAND) -i "$(SRC)"
diff --git a/tests/ref/fate/demux-probe b/tests/ref/fate/demux-probe
new file mode 100644
index 0000000000..a52d10edf5
--- /dev/null
+++ b/tests/ref/fate/demux-probe
@@ -0,0 +1,3 @@
+clean: packets=11 fps=15/1
+corrupt: packets=11 fps=15/1
+corrupt-long: packets=9 fps=15/1
diff --git a/tests/ref/fate/mxf-probe-h264-corrupt b/tests/ref/fate/mxf-probe-h264-corrupt
new file mode 100644
index 0000000000..f9519d6f97
--- /dev/null
+++ b/tests/ref/fate/mxf-probe-h264-corrupt
@@ -0,0 +1,3 @@
+[STREAM]
+avg_frame_rate=25/2
+[/STREAM]