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)