PR #24892 opened by Baptiste Coudurier (bcoudurier) URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24892 Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24892.patch
# Summary of changes Input `-t` currently uses a trim duration measured from the first frame received by the filter. Reinitializing the filtergraph restarts that duration, allowing the stream to run past the requested limit. Use absolute end timestamps for input and output trims so the cutoff survives reinitialization. Include `-itsoffset` in the accurate-seek boundary and shift input trim boundaries when `-isync` adjusts timestamps. Initialize loopback decoder inputs without trim limits. # Validation Passed the build and these FATE tests: - `fate-ffmpeg-input-t-reinit` - `fate-ffmpeg-input-t-isync-positive` - `fate-ffmpeg-input-t-isync-negative` Also checked audio and video with both synchronization offset directions, with and without seeking; accurate seeking with positive and negative `-itsoffset`; output trimming through reinitialization; and loopback decoding. No new FATE samples are required. >From 395f25f803985dd1ed4ff1ec24abd34058434e63 Mon Sep 17 00:00:00 2001 From: Baptiste Coudurier <[email protected]> Date: Mon, 5 Oct 2026 14:26:58 -0700 Subject: [PATCH 1/2] fftools/ffmpeg_demux: apply -itsoffset to the accurate seek trim start The trim start ignored -itsoffset, so with -itsoffset 2 -ss 1 the frames decoded before the seek point were kept. Assisted-by: Claude Opus 5.5 Signed-off-by: Baptiste Coudurier <[email protected]> --- fftools/ffmpeg_demux.c | 1 + 1 file changed, 1 insertion(+) diff --git a/fftools/ffmpeg_demux.c b/fftools/ffmpeg_demux.c index 8ae5486582..13ec8cc4f6 100644 --- a/fftools/ffmpeg_demux.c +++ b/fftools/ffmpeg_demux.c @@ -1290,6 +1290,7 @@ int ist_filter_add(InputStream *ist, InputFilter *ifilter, int is_simple, if (!start_at_zero && d->f.ctx->start_time != AV_NOPTS_VALUE) tsoffset += d->f.ctx->start_time; } + tsoffset += d->f.input_ts_offset; opts->trim_start_us = ((d->f.start_time == AV_NOPTS_VALUE) || !d->accurate_seek) ? AV_NOPTS_VALUE : tsoffset; opts->trim_end_us = d->recording_time; -- 2.52.0 >From a71adff60b69caf0771d0c517ddf2fa64a764607 Mon Sep 17 00:00:00 2001 From: Baptiste Coudurier <[email protected]> Date: Mon, 5 Oct 2026 14:30:08 -0700 Subject: [PATCH 2/2] fftools/ffmpeg: use an absolute end for the -t trims The input -t limit was set as a trim duration, counted from the first frame the filter receives. When the filtergraph is reconfigured mid-stream, the new trim filter counts again from there and the stream runs past the limit. Use an absolute end timestamp instead, which survives reconfiguration. Do the same for output -t, whose absolute end encoders already enforce. Loopback decoders now set no trim, as a zeroed end would drop every frame. Adjust input trim boundaries along with -isync timestamp offsets, which are applied after filter inputs are bound. Cover positive and negative synchronization offsets in FATE. Assisted-by: Claude Opus 5.5 Signed-off-by: Baptiste Coudurier <[email protected]> --- fftools/ffmpeg.h | 4 ++- fftools/ffmpeg_dec.c | 3 +++ fftools/ffmpeg_demux.c | 3 ++- fftools/ffmpeg_filter.c | 28 +++++++++++++------- fftools/ffmpeg_mux_init.c | 4 ++- fftools/ffmpeg_opt.c | 8 ++++++ tests/fate/ffmpeg.mak | 8 ++++++ tests/ref/fate/ffmpeg-input-t-isync-negative | 15 +++++++++++ tests/ref/fate/ffmpeg-input-t-isync-positive | 15 +++++++++++ tests/ref/fate/ffmpeg-input-t-reinit | 15 +++++++++++ 10 files changed, 91 insertions(+), 12 deletions(-) create mode 100644 tests/ref/fate/ffmpeg-input-t-isync-negative create mode 100644 tests/ref/fate/ffmpeg-input-t-isync-positive create mode 100644 tests/ref/fate/ffmpeg-input-t-reinit diff --git a/fftools/ffmpeg.h b/fftools/ffmpeg.h index f51867435d..61369bb678 100644 --- a/fftools/ffmpeg.h +++ b/fftools/ffmpeg.h @@ -300,7 +300,7 @@ typedef struct OutputFilterOptions { const AVCodec *enc; int64_t trim_start_us; - int64_t trim_duration_us; + int64_t trim_end_us; int64_t ts_offset; /* Desired output timebase. @@ -838,6 +838,8 @@ int fg_create(FilterGraph **pfg, char **graph_desc, Scheduler *sch, void fg_free(FilterGraph **pfg); +void ifilter_adjust_trim(InputFilter *ifilter, int64_t adjustment); + void fg_send_command(FilterGraph *fg, double time, const char *target, const char *command, const char *arg, int all_filters); diff --git a/fftools/ffmpeg_dec.c b/fftools/ffmpeg_dec.c index 1c9ef14f25..b3d56bc1c9 100644 --- a/fftools/ffmpeg_dec.c +++ b/fftools/ffmpeg_dec.c @@ -1792,5 +1792,8 @@ int dec_filter_add(Decoder *d, InputFilter *ifilter, InputFilterOptions *opts, if (!opts->name) return AVERROR(ENOMEM); + opts->trim_start_us = AV_NOPTS_VALUE; + opts->trim_end_us = INT64_MAX; + return dec_request_view(d, vs, src); } diff --git a/fftools/ffmpeg_demux.c b/fftools/ffmpeg_demux.c index 13ec8cc4f6..f112622f56 100644 --- a/fftools/ffmpeg_demux.c +++ b/fftools/ffmpeg_demux.c @@ -1293,7 +1293,8 @@ int ist_filter_add(InputStream *ist, InputFilter *ifilter, int is_simple, tsoffset += d->f.input_ts_offset; opts->trim_start_us = ((d->f.start_time == AV_NOPTS_VALUE) || !d->accurate_seek) ? AV_NOPTS_VALUE : tsoffset; - opts->trim_end_us = d->recording_time; + opts->trim_end_us = d->recording_time == INT64_MAX ? INT64_MAX : + tsoffset + d->recording_time; opts->name = av_strdup(ds->dec_name); if (!opts->name) diff --git a/fftools/ffmpeg_filter.c b/fftools/ffmpeg_filter.c index a0a6e082b3..9695aafb5d 100644 --- a/fftools/ffmpeg_filter.c +++ b/fftools/ffmpeg_filter.c @@ -248,7 +248,7 @@ typedef struct OutputFilterPriv { AVRational enc_timebase; int64_t trim_start_us; - int64_t trim_duration_us; + int64_t trim_end_us; // offset for output timestamps, in AV_TIME_BASE_Q int64_t ts_offset; int64_t next_pts; @@ -708,6 +708,16 @@ static OutputFilter *ofilter_alloc(FilterGraph *fg, enum AVMediaType type) return ofilter; } +void ifilter_adjust_trim(InputFilter *ifilter, int64_t adjustment) +{ + InputFilterPriv *ifp = ifp_from_ifilter(ifilter); + + if (ifp->opts.trim_start_us != AV_NOPTS_VALUE) + ifp->opts.trim_start_us += adjustment; + if (ifp->opts.trim_end_us != INT64_MAX) + ifp->opts.trim_end_us += adjustment; +} + static int ifilter_bind_ist(InputFilter *ifilter, InputStream *ist, const ViewSpecifier *vs) { @@ -928,8 +938,8 @@ int ofilter_bind_enc(OutputFilter *ofilter, unsigned sched_idx_enc, ofp->ts_offset = opts->ts_offset; ofp->enc_timebase = opts->output_tb; - ofp->trim_start_us = opts->trim_start_us; - ofp->trim_duration_us = opts->trim_duration_us; + ofp->trim_start_us = opts->trim_start_us; + ofp->trim_end_us = opts->trim_end_us; ofilter->output_name = av_strdup(opts->name); if (!ofilter->output_name) @@ -1648,7 +1658,7 @@ int fg_finalise_bindings(void) return 0; } -static int insert_trim(void *logctx, int64_t start_time, int64_t duration, +static int insert_trim(void *logctx, int64_t start_time, int64_t end_time, AVFilterContext **last_filter, int *pad_idx, const char *filter_name) { @@ -1659,7 +1669,7 @@ static int insert_trim(void *logctx, int64_t start_time, int64_t duration, const char *name = (type == AVMEDIA_TYPE_VIDEO) ? "trim" : "atrim"; int ret = 0; - if (duration == INT64_MAX && start_time == AV_NOPTS_VALUE) + if (end_time == INT64_MAX && start_time == AV_NOPTS_VALUE) return 0; trim = avfilter_get_by_name(name); @@ -1673,8 +1683,8 @@ static int insert_trim(void *logctx, int64_t start_time, int64_t duration, if (!ctx) return AVERROR(ENOMEM); - if (duration != INT64_MAX) { - ret = av_opt_set_int(ctx, "durationi", duration, + if (end_time != INT64_MAX) { + ret = av_opt_set_int(ctx, "endi", end_time, AV_OPT_SEARCH_CHILDREN); } if (ret >= 0 && start_time != AV_NOPTS_VALUE) { @@ -1844,7 +1854,7 @@ static int configure_output_video_filter(FilterGraphPriv *fgp, AVFilterGraph *gr } snprintf(name, sizeof(name), "trim_out_%s", ofilter->output_name); - ret = insert_trim(fgp, ofp->trim_start_us, ofp->trim_duration_us, + ret = insert_trim(fgp, ofp->trim_start_us, ofp->trim_end_us, &last_filter, &pad_idx, name); if (ret < 0) return ret; @@ -1923,7 +1933,7 @@ static int configure_output_audio_filter(FilterGraphPriv *fgp, AVFilterGraph *gr AUTO_INSERT_FILTER("-apad", "apad", ofilter->apad); snprintf(name, sizeof(name), "trim for output %s", ofilter->output_name); - ret = insert_trim(fgp, ofp->trim_start_us, ofp->trim_duration_us, + ret = insert_trim(fgp, ofp->trim_start_us, ofp->trim_end_us, &last_filter, &pad_idx, name); if (ret < 0) goto fail; diff --git a/fftools/ffmpeg_mux_init.c b/fftools/ffmpeg_mux_init.c index aef27e2a8f..ee5d3fd061 100644 --- a/fftools/ffmpeg_mux_init.c +++ b/fftools/ffmpeg_mux_init.c @@ -982,7 +982,9 @@ ost_bind_filter(const Muxer *mux, MuxStream *ms, OutputFilter *ofilter, .swr_opts = o->g->swr_opts, .output_tb = enc_tb, .trim_start_us = mux->of.start_time, - .trim_duration_us = mux->of.recording_time, + .trim_end_us = mux->of.recording_time == INT64_MAX ? INT64_MAX : + (mux->of.start_time == AV_NOPTS_VALUE ? + 0 : mux->of.start_time) + mux->of.recording_time, .ts_offset = mux->of.start_time == AV_NOPTS_VALUE ? 0 : mux->of.start_time, .vs = vs, diff --git a/fftools/ffmpeg_opt.c b/fftools/ffmpeg_opt.c index 265bfba35f..048f2da728 100644 --- a/fftools/ffmpeg_opt.c +++ b/fftools/ffmpeg_opt.c @@ -451,6 +451,14 @@ static int apply_sync_offsets(void) self->ts_offset += adjustment; + // Filter inputs are bound before synchronization offsets are known. + for (int j = 0; j < self->nb_streams; j++) { + InputStream *ist = self->streams[j]; + + for (int k = 0; k < ist->nb_filters; k++) + ifilter_adjust_trim(ist->filters[k], adjustment); + } + av_log(NULL, AV_LOG_INFO, "Adjusted ts offset for Input #%d by %"PRId64" us to sync with Input #%d.\n", i, adjustment, self->input_sync_ref); } else { av_log(NULL, AV_LOG_INFO, "Unable to identify start times for Inputs #%d and %d both. No sync adjustment made.\n", i, self->input_sync_ref); diff --git a/tests/fate/ffmpeg.mak b/tests/fate/ffmpeg.mak index b749af4a7e..b1a5793aec 100644 --- a/tests/fate/ffmpeg.mak +++ b/tests/fate/ffmpeg.mak @@ -19,6 +19,14 @@ fate-ffmpeg-lavfi: CMD = framecrc -lavfi color=d=1:r=5 -fflags +bitexact FATE_FFMPEG-$(call FILTERFRAMECRC, TESTSRC2 FORMAT SETPARAMS SCALE, LAVFI_INDEV WRAPPED_AVFRAME_DECODER MPEG2VIDEO_ENCODER) += fate-ffmpeg-chroma-location fate-ffmpeg-chroma-location: CMD = framecrc -auto_conversion_filters -f lavfi -i testsrc2=s=64x64:r=25:d=1,format=yuv420p,setparams=chroma_location=topleft -sws_flags +accurate_rnd+bitexact -flags +bitexact -idct simple -dct fastint -threads 1 -c:v mpeg2video -qscale:v 2 -frames:v 1 +# input -t must still apply after the filtergraph is reconfigured mid-stream +FATE_FFMPEG-$(call FILTERFRAMECRC, TESTSRC2 FORMAT SCALE TRIM, LAVFI_INDEV WRAPPED_AVFRAME_DECODER) += fate-ffmpeg-input-t-reinit +fate-ffmpeg-input-t-reinit: CMD = framecrc -auto_conversion_filters -t 1 -f lavfi -i "testsrc2=s=64x64:r=10:d=2,format=yuv420p,scale=eval=frame:w=if(lt(t\,0.5)\,64\,32):h=-2" -sws_flags +accurate_rnd+bitexact -flags +bitexact + +FATE_FFMPEG-$(call FILTERFRAMECRC, TESTSRC2 SETPTS TRIM, LAVFI_INDEV WRAPPED_AVFRAME_DECODER) += fate-ffmpeg-input-t-isync-positive fate-ffmpeg-input-t-isync-negative +fate-ffmpeg-input-t-isync-positive: CMD = framecrc -f lavfi -i testsrc2=s=64x64:r=10:d=3 -isync 0 -t 1 -f lavfi -i "testsrc2=s=64x64:r=10:d=3,setpts=PTS+2/TB" -map 1:v -fps_mode passthrough +fate-ffmpeg-input-t-isync-negative: CMD = framecrc -f lavfi -i "testsrc2=s=64x64:r=10:d=3,setpts=PTS+2/TB" -isync 0 -t 1 -f lavfi -i testsrc2=s=64x64:r=10:d=3 -map 1:v -fps_mode passthrough + FATE_FFMPEG-$(call ENCDEC2, MPEG4, RAWVIDEO, AVI, RAWVIDEO_DEMUXER FRAMECRC_MUXER) += fate-force_key_frames fate-force_key_frames: tests/data/vsynth1.yuv fate-force_key_frames: CMD = enc_dec \ diff --git a/tests/ref/fate/ffmpeg-input-t-isync-negative b/tests/ref/fate/ffmpeg-input-t-isync-negative new file mode 100644 index 0000000000..b577f259bb --- /dev/null +++ b/tests/ref/fate/ffmpeg-input-t-isync-negative @@ -0,0 +1,15 @@ +#tb 0: 1/10 +#media_type 0: video +#codec_id 0: rawvideo +#dimensions 0: 64x64 +#sar 0: 1/1 +0, -20, -20, 1, 6144, 0x42205435 +0, -19, -19, 1, 6144, 0x0cfb5751 +0, -18, -18, 1, 6144, 0x0ff84e55 +0, -17, -17, 1, 6144, 0x752449d4 +0, -16, -16, 1, 6144, 0x6ed446d6 +0, -15, -15, 1, 6144, 0xf11243d1 +0, -14, -14, 1, 6144, 0xcc634812 +0, -13, -13, 1, 6144, 0x94ed4ec6 +0, -12, -12, 1, 6144, 0x76444fe9 +0, -11, -11, 1, 6144, 0x06f2513e diff --git a/tests/ref/fate/ffmpeg-input-t-isync-positive b/tests/ref/fate/ffmpeg-input-t-isync-positive new file mode 100644 index 0000000000..df9ede0173 --- /dev/null +++ b/tests/ref/fate/ffmpeg-input-t-isync-positive @@ -0,0 +1,15 @@ +#tb 0: 1/10 +#media_type 0: video +#codec_id 0: rawvideo +#dimensions 0: 64x64 +#sar 0: 1/1 +0, 20, 20, 1, 6144, 0x42205435 +0, 21, 21, 1, 6144, 0x0cfb5751 +0, 22, 22, 1, 6144, 0x0ff84e55 +0, 23, 23, 1, 6144, 0x752449d4 +0, 24, 24, 1, 6144, 0x6ed446d6 +0, 25, 25, 1, 6144, 0xf11243d1 +0, 26, 26, 1, 6144, 0xcc634812 +0, 27, 27, 1, 6144, 0x94ed4ec6 +0, 28, 28, 1, 6144, 0x76444fe9 +0, 29, 29, 1, 6144, 0x06f2513e diff --git a/tests/ref/fate/ffmpeg-input-t-reinit b/tests/ref/fate/ffmpeg-input-t-reinit new file mode 100644 index 0000000000..01cc70e3f4 --- /dev/null +++ b/tests/ref/fate/ffmpeg-input-t-reinit @@ -0,0 +1,15 @@ +#tb 0: 1/10 +#media_type 0: video +#codec_id 0: rawvideo +#dimensions 0: 64x64 +#sar 0: 1/1 +0, 0, 0, 1, 6144, 0x42205435 +0, 1, 1, 1, 6144, 0x0cfb5751 +0, 2, 2, 1, 6144, 0x0ff84e55 +0, 3, 3, 1, 6144, 0x752449d4 +0, 4, 4, 1, 6144, 0x6ed446d6 +0, 5, 5, 1, 6144, 0x9ddc4294 +0, 6, 6, 1, 6144, 0xf10246a9 +0, 7, 7, 1, 6144, 0xabc94d93 +0, 8, 8, 1, 6144, 0x2be24e7b +0, 9, 9, 1, 6144, 0x98fd5044 -- 2.52.0 _______________________________________________ ffmpeg-devel mailing list -- [email protected] To unsubscribe send an email to [email protected]
