Frame-Accurate Seeking in FFmpeg with Complex Filters
Cutting video files at exact frames using FFmpeg often introduces
dropped frames, audio desynchronization, or frozen video when combined
with complex filtergraphs (-filter_complex). This happens
because conventional stream-seeking relies on keyframes (I-frames),
whereas complex filters require decoded, uncompressed frames with
synchronized presentation timestamps (PTS). This guide outlines the
exact commands and filter configurations needed to achieve
frame-accurate cuts using internal trimming filters and hybrid seeking
methods.
Why Standard Seeking Fails with Complex Filters
FFmpeg provides two main seeking flags:
- Input seeking (
-ssbefore-i): Jumps to the nearest preceding keyframe. It is fast, but it does not land precisely on non-keyframes unless full decoding is forced. When piped into-filter_complex, discarded keyframe packets can throw off filter frame counters. - Output seeking (
-ssafter-i): Decodes every frame up to the timestamp, discarding unneeded frames. While accurate for simple transcodes, it often fails inside complex filtergraphs because filters process the input stream before output seeking is applied, leading to timing mismatches between multiple inputs.
To achieve frame accuracy inside -filter_complex, you
must handle trimming within the filtergraph itself using the
trim and atrim filters, followed by a
timestamp reset.
The Native Filter
Approach: trim and setpts
The most reliable way to cut video at an exact frame when applying filters is to feed the video into the filtergraph and perform the cut using frame numbers or exact time offsets.
Syntax for Exact Timestamps:
ffmpeg -i input.mp4 -filter_complex \
"[0:v]trim=start=12.345:end=24.678,setpts=PTS-STARTPTS[v]; \
[0:a]atrim=start=12.345:end=24.678,asetpts=PTS-STARTPTS[a]" \
-map "[v]" -map "[a]" output.mp4Syntax for Exact Frame Numbers:
If you know the exact frame counts (e.g., cutting from frame 300 to 600):
ffmpeg -i input.mp4 -filter_complex \
"[0:v]trim=start_frame=300:end_frame=600,setpts=PTS-STARTPTS[v]; \
[0:a]atrim=start=10.01:end=20.02,asetpts=PTS-STARTPTS[a]" \
-map "[v]" -map "[a]" output.mp4Why
setpts=PTS-STARTPTS is Mandatory
The trim filter drops frames outside the specified
range, but it retains the original presentation timestamps of the
remaining frames. Without setpts=PTS-STARTPTS (and
asetpts=PTS-STARTPTS for audio), the output container will
treat the first frame as occurring at the original timestamp (e.g.,
second 12.345), resulting in a frozen initial frame or missing audio
until that playback position is reached.
The Hybrid Method: Fast Seeking on Large Files
Decoding an entire multi-hour file from the beginning just to cut a segment at the end is computationally inefficient. You can combine fast input seeking with filter-based accuracy by seeking to an arbitrary point before your target cut, then executing the frame-accurate trim internally.
If your target cut is at 01:30:15.500 to
01:30:25.500:
- Jump roughly 30 seconds prior using input seeking
(
-ss 01:29:45.000). - Calculate the relative offset for the filter:
- Target Start:
30.500seconds - Target End:
40.500seconds
- Target Start:
ffmpeg -ss 01:29:45.000 -i input.mp4 -filter_complex \
"[0:v]trim=start=30.500:end=40.500,setpts=PTS-STARTPTS[v]; \
[0:a]atrim=start=30.500:end=40.500,asetpts=PTS-STARTPTS[a]" \
-map "[v]" -map "[a]" output.mp4This prevents FFmpeg from decoding the first 90 minutes of the file while maintaining frame-level precision on the cut.
Handling Variable Frame Rate (VFR) Sources
If your source video uses a variable frame rate (common in smartphone
recordings and screen captures), frame indices may not correlate
steadily with time. Force a constant frame rate (CFR) before trimming
using the fps filter to guarantee deterministic frame
targeting:
ffmpeg -i input.mp4 -filter_complex \
"[0:v]fps=fps=30,trim=start_frame=150:end_frame=300,setpts=PTS-STARTPTS[v]; \
[0:a]atrim=start=5:end=10,asetpts=PTS-STARTPTS[a]" \
-map "[v]" -map "[a]" output.mp4