Commit 87d50f5f12 for ffmpeg
commit 87d50f5f12428114ef5cc2ffe8b4498d1ec85006
Author: Romain Beauxis <romain.beauxis@gmail.com>
Date: Sat Nov 7 16:54:22 2026 -0600
avformat: add AVSEEK_FLAG_PREROLL
A caller that decodes after a seek needs the audio pre-roll before
the target, while a caller that copies the packets needs the seek to
land where it asked.
Add AVSEEK_FLAG_PREROLL, and a preroll value of seek_flags to set it
on every seek, for the caller to say so.
The pre-roll and seek landing tests set it, as every seek still moves
back by the pre-roll.
diff --git a/doc/APIchanges b/doc/APIchanges
index a007a9e9a5..4827b4ea29 100644
--- a/doc/APIchanges
+++ b/doc/APIchanges
@@ -2,6 +2,10 @@ The last version increases of all libraries were on 2026-06-23.
API changes, most recent first:
+2026-11-xx - xxxxxxxxxx - lavf 63.20.100 - avformat.h
+ Add AVSEEK_FLAG_PREROLL, for avformat_seek_file() to move the target back
+ by the audio pre-roll.
+
2026-11-xx - xxxxxxxxxx - lavf 63.19.100 - avformat.h
Add AVFormatContext.seek_flags.
Deprecate AVFormatContext.seek2any in favor of AVSEEK_FLAG_ANY in
diff --git a/doc/formats.texi b/doc/formats.texi
index 584eaf8d55..36b8b9718b 100644
--- a/doc/formats.texi
+++ b/doc/formats.texi
@@ -83,6 +83,9 @@ Possible values:
@table @samp
@item any
Allow seeking to non-keyframes on demuxer level when supported.
+@item preroll
+Move the target of a seek back by the audio pre-roll, for a caller that
+decodes. The seek can then land on an earlier video keyframe.
@end table
@item seek2any @var{integer} (@emph{input})
diff --git a/libavformat/avformat.h b/libavformat/avformat.h
index 68900b9734..80d4c5d220 100644
--- a/libavformat/avformat.h
+++ b/libavformat/avformat.h
@@ -2063,7 +2063,8 @@ typedef struct AVFormatContext {
int recursion_limit;
/**
- * Flags added to the flags of every avformat_seek_file(): AVSEEK_FLAG_ANY.
+ * Flags added to the flags of every avformat_seek_file(): AVSEEK_FLAG_ANY
+ * and AVSEEK_FLAG_PREROLL.
* - encoding: unused
* - decoding: Set by user
*/
@@ -2743,6 +2744,7 @@ void avformat_close_input(AVFormatContext **s);
#define AVSEEK_FLAG_BYTE 2 ///< seeking based on position in bytes
#define AVSEEK_FLAG_ANY 4 ///< seek to any frame, even non-keyframes
#define AVSEEK_FLAG_FRAME 8 ///< seeking based on frame number
+#define AVSEEK_FLAG_PREROLL 16 ///< move the target back by the audio pre-roll, see avformat_seek_file()
/**
* @addtogroup lavf_encoding
diff --git a/libavformat/options_table.h b/libavformat/options_table.h
index eccc85bc63..c93069c7a9 100644
--- a/libavformat/options_table.h
+++ b/libavformat/options_table.h
@@ -60,6 +60,7 @@ static const AVOption avformat_options[] = {
#endif
{"seek_flags", "flags added to every seek", OFFSET(seek_flags), AV_OPT_TYPE_FLAGS, {.i64 = 0 }, 0, INT_MAX, D, .unit = "seek_flags"},
{"any", "allow seeking to non-keyframes on demuxer level when supported", 0, AV_OPT_TYPE_CONST, {.i64 = AVSEEK_FLAG_ANY }, INT_MIN, INT_MAX, D, .unit = "seek_flags"},
+{"preroll", "move the target of a seek back by the audio pre-roll, for decoding", 0, AV_OPT_TYPE_CONST, {.i64 = AVSEEK_FLAG_PREROLL }, INT_MIN, INT_MAX, D, .unit = "seek_flags"},
{"analyzeduration", "specify how many microseconds are analyzed to probe the input", OFFSET(max_analyze_duration), AV_OPT_TYPE_INT64, {.i64 = 0 }, 0, (double)INT64_MAX, D},
{"cryptokey", "decryption key", OFFSET(key), AV_OPT_TYPE_BINARY, {.dbl = 0}, 0, 0, D},
{"indexmem", "max memory used for timestamp index (per stream)", OFFSET(max_index_size), AV_OPT_TYPE_INT, {.i64 = 1<<20 }, 0, INT_MAX, D},
diff --git a/libavformat/seek.c b/libavformat/seek.c
index dd3a237983..06e1e6c452 100644
--- a/libavformat/seek.c
+++ b/libavformat/seek.c
@@ -771,7 +771,7 @@ FF_DISABLE_DEPRECATION_WARNINGS
flags |= AVSEEK_FLAG_ANY;
FF_ENABLE_DEPRECATION_WARNINGS
#endif
- flags |= s->seek_flags & AVSEEK_FLAG_ANY;
+ flags |= s->seek_flags & (AVSEEK_FLAG_ANY | AVSEEK_FLAG_PREROLL);
flags &= ~AVSEEK_FLAG_BACKWARD;
if (!(flags & (AVSEEK_FLAG_BYTE | AVSEEK_FLAG_FRAME))) {
diff --git a/libavformat/version.h b/libavformat/version.h
index e063e12b98..de9cc8e31d 100644
--- a/libavformat/version.h
+++ b/libavformat/version.h
@@ -31,7 +31,7 @@
#include "version_major.h"
-#define LIBAVFORMAT_VERSION_MINOR 19
+#define LIBAVFORMAT_VERSION_MINOR 20
#define LIBAVFORMAT_VERSION_MICRO 100
#define LIBAVFORMAT_VERSION_INT AV_VERSION_INT(LIBAVFORMAT_VERSION_MAJOR, \
diff --git a/tests/fate/aac.mak b/tests/fate/aac.mak
index 0267bca240..be0420d951 100644
--- a/tests/fate/aac.mak
+++ b/tests/fate/aac.mak
@@ -360,8 +360,8 @@ fate-aac-seek-preroll-eld: CMD = run ffprobe$(PROGSSUF)$(EXESUF) -show_entries s
# Where a seek lands, by the same lengths of frame.
FATE_AAC_PREROLL-$(call DEMDEC, MOV, AAC) += fate-aac-seek-landing-sbr \
fate-aac-seek-landing-frame-length-short
-fate-aac-seek-landing-sbr: CMD = run ffprobe$(PROGSSUF)$(EXESUF) -read_intervals 10.3%+\#3 -show_entries packet=pts,dts,duration -print_format compact $(TARGET_SAMPLES)/aac/al_sbr_cm_48_2.mp4
-fate-aac-seek-landing-frame-length-short: CMD = run ffprobe$(PROGSSUF)$(EXESUF) -read_intervals 3.3%+\#3 -show_entries packet=pts,dts,duration -print_format compact $(TARGET_SAMPLES)/aac/al04sf_48.mp4
+fate-aac-seek-landing-sbr: CMD = run ffprobe$(PROGSSUF)$(EXESUF) -seek_flags preroll -read_intervals 10.3%+\#3 -show_entries packet=pts,dts,duration -print_format compact $(TARGET_SAMPLES)/aac/al_sbr_cm_48_2.mp4
+fate-aac-seek-landing-frame-length-short: CMD = run ffprobe$(PROGSSUF)$(EXESUF) -seek_flags preroll -read_intervals 3.3%+\#3 -show_entries packet=pts,dts,duration -print_format compact $(TARGET_SAMPLES)/aac/al04sf_48.mp4
FATE_SAMPLES_FFPROBE += $(FATE_AAC_PREROLL-yes)
diff --git a/tests/fate/mov.mak b/tests/fate/mov.mak
index 4914133f19..2a4de3f463 100644
--- a/tests/fate/mov.mak
+++ b/tests/fate/mov.mak
@@ -163,7 +163,7 @@ fate-mov-aac-2048-priming-preroll: CMD = run ffprobe$(PROGSSUF)$(EXESUF) -show_e
# Where a seek lands.
FATE_MOV_FFPROBE-$(call DEMDEC, MOV, AAC) += fate-mov-aac-2048-priming-seek-landing
-fate-mov-aac-2048-priming-seek-landing: CMD = run ffprobe$(PROGSSUF)$(EXESUF) -read_intervals 2.3%+\#3 -show_entries packet=pts,dts,duration -print_format compact $(TARGET_SAMPLES)/mov/aac-2048-priming.mov
+fate-mov-aac-2048-priming-seek-landing: CMD = run ffprobe$(PROGSSUF)$(EXESUF) -seek_flags preroll -read_intervals 2.3%+\#3 -show_entries packet=pts,dts,duration -print_format compact $(TARGET_SAMPLES)/mov/aac-2048-priming.mov
# Seeking into the stream must not drop samples to the container's priming.
FATE_MOV-$(call FRAMECRC, MOV, AAC, PCM_S16LE_ENCODER ARESAMPLE_FILTER VOLUME_FILTER) += fate-mov-aac-2048-priming-seek
diff --git a/tests/fate/preroll.mak b/tests/fate/preroll.mak
index 6c9cbd8597..c28515aefa 100644
--- a/tests/fate/preroll.mak
+++ b/tests/fate/preroll.mak
@@ -8,16 +8,17 @@
# bit-exact across platforms.
PREROLL_SEEK_TS = 0.53
+PREROLL_OPT = -seek_flags preroll
# 1: test name, 2: demuxer, 3: decoder, 4: sample
define FATE_PREROLL_TEST
FATE_PREROLL_FFPROBE-$(call DEMDEC, $(2), $(3)) += fate-preroll-$(1)-declared fate-preroll-$(1)-landing
fate-preroll-$(1)-declared: CMD = run ffprobe$(PROGSSUF)$(EXESUF) -select_streams a:0 -show_entries stream=codec_name,sample_rate,initial_padding,seek_preroll -print_format compact $(TARGET_SAMPLES)/$(4)
-fate-preroll-$(1)-landing: CMD = run ffprobe$(PROGSSUF)$(EXESUF) -select_streams a:0 -read_intervals $(PREROLL_SEEK_TS)%+\#3 -show_entries packet=pts,dts,duration -print_format compact $(TARGET_SAMPLES)/$(4)
+fate-preroll-$(1)-landing: CMD = run ffprobe$(PROGSSUF)$(EXESUF) $(PREROLL_OPT) -select_streams a:0 -read_intervals $(PREROLL_SEEK_TS)%+\#3 -show_entries packet=pts,dts,duration -print_format compact $(TARGET_SAMPLES)/$(4)
FATE_PREROLL_FFMPEG-$(call FRAMECRC, $(2), $(3), PCM_S16LE_ENCODER ARESAMPLE_FILTER VOLUME_FILTER) += fate-preroll-$(1)-decode
-fate-preroll-$(1)-decode: CMD = framecrc -ss $(PREROLL_SEEK_TS) -t 0.15 -i $(TARGET_SAMPLES)/$(4) -map 0:a:0 -af volume=0,aresample -c:a pcm_s16le
+fate-preroll-$(1)-decode: CMD = framecrc $(PREROLL_OPT) -ss $(PREROLL_SEEK_TS) -t 0.15 -i $(TARGET_SAMPLES)/$(4) -map 0:a:0 -af volume=0,aresample -c:a pcm_s16le
FATE_PREROLL_FFMPEG-$(call FRAMECRC, $(2), $(3), PCM_S16LE_ENCODER ARESAMPLE_FILTER VOLUME_FILTER) += fate-preroll-$(1)-decode-noaccurate
-fate-preroll-$(1)-decode-noaccurate: CMD = framecrc -noaccurate_seek -ss $(PREROLL_SEEK_TS) -t 0.15 -i $(TARGET_SAMPLES)/$(4) -map 0:a:0 -af volume=0,aresample -c:a pcm_s16le
+fate-preroll-$(1)-decode-noaccurate: CMD = framecrc $(PREROLL_OPT) -noaccurate_seek -ss $(PREROLL_SEEK_TS) -t 0.15 -i $(TARGET_SAMPLES)/$(4) -map 0:a:0 -af volume=0,aresample -c:a pcm_s16le
endef
$(eval $(call FATE_PREROLL_TEST,aac-mov,MOV,AAC,gapless/102400samples_qt-lc-aac.m4a))
@@ -37,34 +38,34 @@ $(eval $(call FATE_PREROLL_TEST,mp3-itunes,MP3,MP3FLOAT,gapless/gapless-itunes.m
# A seek that lands inside the priming, which here spans the first three
# packets: the rest of the priming, or the pre-roll if longer, is skipped.
FATE_PREROLL_FFMPEG-$(call FRAMECRC, CAF, AAC, PCM_S16LE_ENCODER ARESAMPLE_FILTER VOLUME_FILTER) += fate-preroll-aac-caf-decode-in-priming
-fate-preroll-aac-caf-decode-in-priming: CMD = framecrc -noaccurate_seek -ss 0.01 -t 0.1 -i $(TARGET_SAMPLES)/caf/aac.caf -af volume=0,aresample -c:a pcm_s16le
+fate-preroll-aac-caf-decode-in-priming: CMD = framecrc $(PREROLL_OPT) -noaccurate_seek -ss 0.01 -t 0.1 -i $(TARGET_SAMPLES)/caf/aac.caf -af volume=0,aresample -c:a pcm_s16le
# A forward seek, as ffplay does: the range starts before the target and is
# open at the end.
FATE_PREROLL_SEEK-$(call DEMDEC, MOV, AAC) += fate-preroll-aac-mov-seek-forward
-fate-preroll-aac-mov-seek-forward: CMD = run libavformat/tests/seek$(EXESUF) $(TARGET_SAMPLES)/gapless/102400samples_qt-lc-aac.m4a -seekforw 530000 -seekmin 400000 -frames 2 -duration 3
+fate-preroll-aac-mov-seek-forward: CMD = run libavformat/tests/seek$(EXESUF) $(TARGET_SAMPLES)/gapless/102400samples_qt-lc-aac.m4a -seekforw 530000 -seekmin 400000 -frames 2 -duration 3 $(PREROLL_OPT)
# A seek past the end lands on the last packet, which carries the padding
# to discard at the end of the stream.
FATE_PREROLL_FFPROBE-$(call DEMDEC, CAF, AAC) += fate-preroll-aac-caf-landing-end
-fate-preroll-aac-caf-landing-end: CMD = run ffprobe$(PROGSSUF)$(EXESUF) -select_streams a:0 -read_intervals 5.45%+\#2 -show_entries packet=pts,duration:packet_side_data=skip_samples,discard_padding -print_format compact $(TARGET_SAMPLES)/caf/aac.caf
+fate-preroll-aac-caf-landing-end: CMD = run ffprobe$(PROGSSUF)$(EXESUF) $(PREROLL_OPT) -select_streams a:0 -read_intervals 5.45%+\#2 -show_entries packet=pts,duration:packet_side_data=skip_samples,discard_padding -print_format compact $(TARGET_SAMPLES)/caf/aac.caf
FATE_PREROLL_FFPROBE-$(call DEMDEC, MATROSKA, OPUS) += fate-preroll-opus-mkv-landing-end
-fate-preroll-opus-mkv-landing-end: CMD = run ffprobe$(PROGSSUF)$(EXESUF) -select_streams a:0 -read_intervals 1.1%+\#2 -show_entries packet=pts,duration:packet_side_data=skip_samples,discard_padding -print_format compact $(TARGET_SAMPLES)/mkv/codec_delay_opus.mkv
+fate-preroll-opus-mkv-landing-end: CMD = run ffprobe$(PROGSSUF)$(EXESUF) $(PREROLL_OPT) -select_streams a:0 -read_intervals 1.1%+\#2 -show_entries packet=pts,duration:packet_side_data=skip_samples,discard_padding -print_format compact $(TARGET_SAMPLES)/mkv/codec_delay_opus.mkv
# A seek on the video stream, just after a keyframe: the audio pre-roll
# still needs room.
FATE_PREROLL_SEEK-$(call DEMDEC, MOV, AAC H264) += fate-preroll-aac-mov-seek-video
-fate-preroll-aac-mov-seek-video: CMD = run libavformat/tests/seek$(EXESUF) $(TARGET_SAMPLES)/h264/twofields_packet.mp4 -stream_id 0 -seekback 182000 -frames 4 -duration 3
+fate-preroll-aac-mov-seek-video: CMD = run libavformat/tests/seek$(EXESUF) $(TARGET_SAMPLES)/h264/twofields_packet.mp4 -stream_id 0 -seekback 182000 -frames 4 -duration 3 $(PREROLL_OPT)
# A seek on the stream that starts later, close enough to its start that
# the seek targets it: the other stream is already past its own start.
FATE_PREROLL_SEEK-$(call ALLYES, MOV_DEMUXER MOV_MUXER AAC_DECODER FILE_PROTOCOL) += fate-preroll-aac-mov-seek-staggered
-fate-preroll-aac-mov-seek-staggered: CMD = seek_staggered $(TARGET_SAMPLES)/gapless/102400samples_qt-lc-aac.m4a 1 -stream_id 1 -seekback 42500 -frames 4 -duration 3
+fate-preroll-aac-mov-seek-staggered: CMD = seek_staggered $(TARGET_SAMPLES)/gapless/102400samples_qt-lc-aac.m4a 1 -stream_id 1 -seekback 42500 -frames 4 -duration 3 $(PREROLL_OPT)
# A seek through the concat demuxer, which seeks in the file it reads.
FATE_PREROLL_SEEK-$(call ALLYES, CONCAT_DEMUXER MOV_DEMUXER AAC_DECODER FILE_PROTOCOL) += fate-preroll-aac-concat-seek
fate-preroll-aac-concat-seek: tests/data/samples/gapless/102400samples_qt-lc-aac.m4a
-fate-preroll-aac-concat-seek: CMD = seek_concat $(SRC_PATH)/tests/preroll.ffconcat ../samples/gapless_102400samples_qt-lc-aac.m4a -seekback 530000 -frames 3 -duration 3
+fate-preroll-aac-concat-seek: CMD = seek_concat $(SRC_PATH)/tests/preroll.ffconcat ../samples/gapless_102400samples_qt-lc-aac.m4a -seekback 530000 -frames 3 -duration 3 $(PREROLL_OPT)
# An inpoint on a video keyframe: the cut starts at that keyframe.
FATE_PREROLL_FFPROBE-$(call ALLYES, CONCAT_DEMUXER MATROSKA_DEMUXER H264_DECODER AAC_DECODER FILE_PROTOCOL PIPE_PROTOCOL) += fate-preroll-aac-concat-inpoint
@@ -72,7 +73,7 @@ fate-preroll-aac-concat-inpoint: CMD = printf "ffconcat version 1.0\nfile file:%
# A seek close to the start of a stream whose start time is unknown.
FATE_PREROLL_SEEK-$(call DEMDEC, AAC, AAC) += fate-preroll-aac-adts-seek-start
-fate-preroll-aac-adts-seek-start: CMD = run libavformat/tests/seek$(EXESUF) $(TARGET_SAMPLES)/aac/id3v2.aac -seekback 10000 -frames 2 -duration 3
+fate-preroll-aac-adts-seek-start: CMD = run libavformat/tests/seek$(EXESUF) $(TARGET_SAMPLES)/aac/id3v2.aac -seekback 10000 -frames 2 -duration 3 $(PREROLL_OPT)
$(FATE_PREROLL_SEEK-yes): libavformat/tests/seek$(EXESUF)