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 (-ss before -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 (-ss after -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.mp4

Syntax 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.mp4

Why 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:

  1. Jump roughly 30 seconds prior using input seeking (-ss 01:29:45.000).
  2. Calculate the relative offset for the filter:
    • Target Start: 30.500 seconds
    • Target End: 40.500 seconds
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.mp4

This 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