This can be done on constant frame rate video - my method below may work on VFR video, but I don't have a suitable video to test.
Basic command:
ffmpeg -i 120.mp4 -vf "select='eq(n,0)+if(gt(t-prev_selected_t,1/30.01),1,0)'" -vsync 0 out.mp4
On a 120 fps stream,
Video: h264 (High) ... 120 fps, 120 tbr, 15360 tbn, 240 tbc
this produces
Video: h264 (High) ... 30.08 fps, 30 tbr, 15360 tbn, 240 tbc
What the select filter does, after selecting the first frame, is only select further frames if their interval from the previously selected frame is 1/30th of a second or more. In practice, this decimates videos to below 30 fps.
Two things to note:
1) the duration of the last frame is truncated, so the fps
value will be slightly greater than the target. But all other frames cycle at 30 fps.
2) The output framerate is the highest number obtained by dividing the source rate by an integer. So, if in the command, 30.01
is replaced by 26
, the result will be 24 tbr
since neither 25 nor 26 can be obtained by dividing 120 by an integer. This has the benefit of avoiding motion stutter in the output. The divisor in the command should be slightly greater than the ceiling i.e. 30.01
instead of 30
. Else, the results are wobbly.
If the input is 30 fps or less, then all frames are selected.
Addendum:
If the output of the above command is piped to another ffmpeg instance, the output fps
is exact and the codec rate tbc
is reset as well.
ffmpeg -i 120.mp4 -vf "select='eq(n,0)+if(gt(t-prev_selected_t,1/30.01),1,0)'" -vsync 0
-c:v rawvideo -c:a pcm_s16le -f nut - | ffmpeg -f nut -i - out.mp4