Fully offline: the same work in ffmpeg
The browser version is already local, and ffmpeg in a terminal removes even the dependency on an open tab. Here are ready commands for macOS, Windows and Linux that reproduce every mode of the tool.
When you have many files, very large ones, or you want a script, ffmpeg on your own machine is the better tool. Below are the same modes as on the site: duplicate, blend and motion-compensated interpolation. Swap in your own file names.
In every command F is the slow-down factor (8 or 16), FPS is the output frame rate (60 for x8, 30 for x16), and INPUT and OUTPUT are the source and result file names. In the Windows commands those same two files are called IN and OUT: in PowerShell $input is a reserved automatic variable, so it cannot be reused as your own.
Press "Copy" to take the whole command.
Install ffmpeg
Install ffmpeg once and everything after that runs offline. Check the install with ffmpeg -version.
macOS
brew install ffmpeg
ffmpeg -version
brew tap homebrew-ffmpeg/ffmpeg && brew install homebrew-ffmpeg/ffmpeg/ffmpeg --with-zimg
The Homebrew build includes libx264, libx265 and videotoolbox, so every slow-down mode works straight away. But zimg is NOT in it: there is no zscale filter, and the HDR tone mapping chain will not run. Check with: ffmpeg -hide_banner -filters | grep zscale. If that prints nothing and you do need HDR, install the tap build with the third command. If you do not have Homebrew yet, install it from brew.sh.
Windows
winget install Gyan.FFmpeg
ffmpeg -version
chcp 65001
Close the terminal and open a new one after installing, otherwise PATH is stale. The Gyan.FFmpeg build includes libx264, libx265, zimg and nvenc, qsv and amf support. The third command switches cmd.exe to UTF-8: without it, Cyrillic and emoji in file names turn into question marks.
Linux
sudo apt update && sudo apt install ffmpeg
ffmpeg -version
curl -L -O https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz && tar xf ffmpeg-release-amd64-static.tar.xz && sudo install -m 755 ffmpeg-*-amd64-static/ffmpeg ffmpeg-*-amd64-static/ffprobe /usr/local/bin/
On Debian and Ubuntu the repository version is often a year or more behind. If ffmpeg -version reports 4.x, use the johnvansickle.com static build in the third command: it unpacks into a single directory, conflicts with nothing, and includes libx264, libx265 and zimg. On Fedora use dnf install ffmpeg, on Arch pacman -S ffmpeg.
Inspect the file
Start with the codec, resolution, frame rate and rotation: every command below depends on them.
macOS
INPUT="input.mp4"; OUTPUT="output.mp4"; F=8; FPS=60
ffprobe -v error -select_streams v:0 -show_entries stream=codec_name,width,height,r_frame_rate,avg_frame_rate,nb_frames,color_transfer:stream_tags=rotate -of json "$INPUT"
A color_transfer of arib-std-b67 means HLG and smpte2084 means PQ. Both are HDR: see the tone mapping step.
Windows
$IN="input.mp4"; $OUT="output.mp4"; $F=8; $FPS=60
ffprobe -v error -select_streams v:0 -show_entries stream=codec_name,width,height,r_frame_rate,avg_frame_rate,nb_frames,color_transfer:stream_tags=rotate -of json "$IN"
A color_transfer of arib-std-b67 means HLG and smpte2084 means PQ. Both are HDR: see the tone mapping step.
Linux
INPUT="input.mp4"; OUTPUT="output.mp4"; F=8; FPS=60
ffprobe -v error -select_streams v:0 -show_entries stream=codec_name,width,height,r_frame_rate,avg_frame_rate,nb_frames,color_transfer:stream_tags=rotate -of json "$INPUT"
A color_transfer of arib-std-b67 means HLG and smpte2084 means PQ. Both are HDR: see the tone mapping step.
Slow down by duplicating frames
The fastest and most reliable route: frames are repeated, there are no artifacts, and motion looks stepped.
macOS
INPUT="input.mp4"; OUTPUT="output.mp4"; F=8; FPS=60
ffmpeg -i "$INPUT" -vf "setpts=${F}*PTS,fps=${FPS}" -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -movflags +faststart -an "$OUTPUT"
setpts MUST come before fps. Swap them and fps runs on the original timestamps, setpts then stretches an already-decimated stream, and you get no slow motion at all.
Windows
$IN="input.mp4"; $OUT="output.mp4"; $F=8; $FPS=60
ffmpeg -i "$IN" -vf "setpts=${F}*PTS,fps=${FPS}" -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -movflags +faststart -an "$OUT"
setpts MUST come before fps. Swap them and fps runs on the original timestamps, setpts then stretches an already-decimated stream, and you get no slow motion at all.
Linux
INPUT="input.mp4"; OUTPUT="output.mp4"; F=8; FPS=60
ffmpeg -i "$INPUT" -vf "setpts=${F}*PTS,fps=${FPS}" -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -movflags +faststart -an "$OUTPUT"
setpts MUST come before fps. Swap them and fps runs on the original timestamps, setpts then stretches an already-decimated stream, and you get no slow motion at all.
Slow down with blending
Neighbouring frames are mixed, so stutter turns into a soft smear. Almost as fast as duplication.
macOS
INPUT="input.mp4"; OUTPUT="output.mp4"; F=8; FPS=60
ffmpeg -i "$INPUT" -vf "setpts=${F}*PTS,minterpolate=fps=${FPS}:mi_mode=blend" -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -movflags +faststart -an "$OUTPUT"
Same ordering rule: setpts, then minterpolate. The fps option inside minterpolate sets the output rate, so no separate fps filter is needed.
Windows
$IN="input.mp4"; $OUT="output.mp4"; $F=8; $FPS=60
ffmpeg -i "$IN" -vf "setpts=${F}*PTS,minterpolate=fps=${FPS}:mi_mode=blend" -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -movflags +faststart -an "$OUT"
Same ordering rule: setpts, then minterpolate. The fps option inside minterpolate sets the output rate, so no separate fps filter is needed.
Linux
INPUT="input.mp4"; OUTPUT="output.mp4"; F=8; FPS=60
ffmpeg -i "$INPUT" -vf "setpts=${F}*PTS,minterpolate=fps=${FPS}:mi_mode=blend" -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -movflags +faststart -an "$OUTPUT"
Same ordering rule: setpts, then minterpolate. The fps option inside minterpolate sets the output rate, so no separate fps filter is needed.
Motion-compensated interpolation
The minterpolate filter builds in-between frames from motion vectors. Best result, long processing, and water, foliage and cuts can come out distorted.
macOS
INPUT="input.mp4"; OUTPUT="output.mp4"; F=8; FPS=60
ffmpeg -i "$INPUT" -vf "setpts=${F}*PTS,minterpolate=fps=${FPS}:mi_mode=mci:mc_mode=aobmc:me_mode=bidir:vsbmc=1" -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -movflags +faststart -an "$OUTPUT"
Run ten seconds first with -ss 0 -t 10, look at the result, and only then start the whole file. Redoing a two-hour render hurts.
Windows
$IN="input.mp4"; $OUT="output.mp4"; $F=8; $FPS=60
ffmpeg -i "$IN" -vf "setpts=${F}*PTS,minterpolate=fps=${FPS}:mi_mode=mci:mc_mode=aobmc:me_mode=bidir:vsbmc=1" -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -movflags +faststart -an "$OUT"
Run ten seconds first with -ss 0 -t 10, look at the result, and only then start the whole file. Redoing a two-hour render hurts.
Linux
INPUT="input.mp4"; OUTPUT="output.mp4"; F=8; FPS=60
ffmpeg -i "$INPUT" -vf "setpts=${F}*PTS,minterpolate=fps=${FPS}:mi_mode=mci:mc_mode=aobmc:me_mode=bidir:vsbmc=1" -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -movflags +faststart -an "$OUTPUT"
Run ten seconds first with -ss 0 -t 10, look at the result, and only then start the whole file. Redoing a two-hour render hurts.
Variable frame rate
Screen recordings and phone footage often drift in frame rate. Force a constant rate first, or the slow motion will drift away from the audio.
macOS
INPUT="input.mp4"; OUTPUT="output.mp4"; F=8; FPS=60
ffmpeg -i "$INPUT" -vf "fps=30,setpts=${F}*PTS,minterpolate=fps=${FPS}:mi_mode=mci" -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -movflags +faststart -an "$OUTPUT"
The 30 in fps=30 is the SOURCE rate from ffprobe, not the target. The target lives inside minterpolate. It is easy to swap the two, and then the file simply does not slow down.
Windows
$IN="input.mp4"; $OUT="output.mp4"; $F=8; $FPS=60
ffmpeg -i "$IN" -vf "fps=30,setpts=${F}*PTS,minterpolate=fps=${FPS}:mi_mode=mci" -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -movflags +faststart -an "$OUT"
The 30 in fps=30 is the SOURCE rate from ffprobe, not the target. The target lives inside minterpolate. It is easy to swap the two, and then the file simply does not slow down.
Linux
INPUT="input.mp4"; OUTPUT="output.mp4"; F=8; FPS=60
ffmpeg -i "$INPUT" -vf "fps=30,setpts=${F}*PTS,minterpolate=fps=${FPS}:mi_mode=mci" -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -movflags +faststart -an "$OUTPUT"
The 30 in fps=30 is the SOURCE rate from ffprobe, not the target. The target lives inside minterpolate. It is easy to swap the two, and then the file simply does not slow down.
HDR to SDR
HLG or PQ footage from an iPhone looks flat in ordinary players. Convert the color to BT.709. Check that your build has the zscale filter: the stock Homebrew build does not.
macOS
INPUT="input.mp4"; OUTPUT="output.mp4"; F=8; FPS=60
ffmpeg -i "$INPUT" -vf "zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=bt709:t=bt709:m=bt709:r=tv,format=yuv420p,setpts=${F}*PTS,fps=${FPS}" -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -movflags +faststart -an "$OUTPUT"
ffmpeg -hide_banner -filters | grep zscale
zscale exists only in builds configured with --enable-libzimg. Check with: ffmpeg -hide_banner -filters | grep zscale. macOS is where it is usually missing: the stock Homebrew build ships without zimg, while the Gyan builds for Windows, the Debian and Ubuntu packages and the johnvansickle static builds all have it. If it is missing you have two options. On a machine with OpenCL you can use tonemap_opencl (fiddlier, needs -init_hw_device opencl). Or drop the whole chain down to format=yuv420p: the file will open everywhere, but the colours stay wrong, highlights clip to white and saturation drops. Do not stay quiet about that in the result.
Windows
$IN="input.mp4"; $OUT="output.mp4"; $F=8; $FPS=60
ffmpeg -i "$IN" -vf "zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=bt709:t=bt709:m=bt709:r=tv,format=yuv420p,setpts=${F}*PTS,fps=${FPS}" -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -movflags +faststart -an "$OUT"
ffmpeg -hide_banner -filters | Select-String zscale
zscale exists only in builds configured with --enable-libzimg. Check with: ffmpeg -hide_banner -filters | grep zscale. macOS is where it is usually missing: the stock Homebrew build ships without zimg, while the Gyan builds for Windows, the Debian and Ubuntu packages and the johnvansickle static builds all have it. If it is missing you have two options. On a machine with OpenCL you can use tonemap_opencl (fiddlier, needs -init_hw_device opencl). Or drop the whole chain down to format=yuv420p: the file will open everywhere, but the colours stay wrong, highlights clip to white and saturation drops. Do not stay quiet about that in the result.
Linux
INPUT="input.mp4"; OUTPUT="output.mp4"; F=8; FPS=60
ffmpeg -i "$INPUT" -vf "zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=bt709:t=bt709:m=bt709:r=tv,format=yuv420p,setpts=${F}*PTS,fps=${FPS}" -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -movflags +faststart -an "$OUTPUT"
ffmpeg -hide_banner -filters | grep zscale
zscale exists only in builds configured with --enable-libzimg. Check with: ffmpeg -hide_banner -filters | grep zscale. macOS is where it is usually missing: the stock Homebrew build ships without zimg, while the Gyan builds for Windows, the Debian and Ubuntu packages and the johnvansickle static builds all have it. If it is missing you have two options. On a machine with OpenCL you can use tonemap_opencl (fiddlier, needs -init_hw_device opencl). Or drop the whole chain down to format=yuv420p: the file will open everywhere, but the colours stay wrong, highlights clip to white and saturation drops. Do not stay quiet about that in the result.
Rotation
If the picture is sideways, bake the rotation into the pixels and clear the flag from the metadata.
macOS
INPUT="input.mp4"; OUTPUT="output.mp4"; F=8; FPS=60
ffmpeg -i "$INPUT" -vf "transpose=1,setpts=${F}*PTS,fps=${FPS}" -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -movflags +faststart -metadata:s:v rotate=0 -an "$OUTPUT"
transpose=1 is 90 degrees clockwise, transpose=2 is counter-clockwise, and for 180 use transpose=1,transpose=1 or hflip,vflip. The rotate tag from ffprobe tells you which: 90 means transpose=1, 270 means transpose=2. Width and height swap in the process.
Windows
$IN="input.mp4"; $OUT="output.mp4"; $F=8; $FPS=60
ffmpeg -i "$IN" -vf "transpose=1,setpts=${F}*PTS,fps=${FPS}" -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -movflags +faststart -metadata:s:v rotate=0 -an "$OUT"
transpose=1 is 90 degrees clockwise, transpose=2 is counter-clockwise, and for 180 use transpose=1,transpose=1 or hflip,vflip. The rotate tag from ffprobe tells you which: 90 means transpose=1, 270 means transpose=2. Width and height swap in the process.
Linux
INPUT="input.mp4"; OUTPUT="output.mp4"; F=8; FPS=60
ffmpeg -i "$INPUT" -vf "transpose=1,setpts=${F}*PTS,fps=${FPS}" -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -movflags +faststart -metadata:s:v rotate=0 -an "$OUTPUT"
transpose=1 is 90 degrees clockwise, transpose=2 is counter-clockwise, and for 180 use transpose=1,transpose=1 or hflip,vflip. The rotate tag from ffprobe tells you which: 90 means transpose=1, 270 means transpose=2. Width and height swap in the process.
Audio
Dropping the track is usually simplest. If you need sound, stretch it with a chain of atempo filters, though at 8x it becomes a drone.
macOS
INPUT="input.mp4"; OUTPUT="output.mp4"; F=8; FPS=60
ffmpeg -i "$INPUT" -filter_complex "[0:v]setpts=${F}*PTS,minterpolate=fps=${FPS}:mi_mode=blend[v];[0:a]atempo=0.5,atempo=0.5,atempo=0.5[a]" -map "[v]" -map "[a]" -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -movflags +faststart -c:a aac -b:a 192k "$OUTPUT"
ffmpeg -i "$INPUT" -filter_complex "[0:v]setpts=16*PTS,minterpolate=fps=30:mi_mode=blend[v];[0:a]atempo=0.5,atempo=0.5,atempo=0.5,atempo=0.5[a]" -map "[v]" -map "[a]" -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -movflags +faststart -c:a aac -b:a 192k "$OUTPUT"
ffmpeg -i "$INPUT" -filter_complex "[0:v]setpts=0.125*PTS[v];[0:a]atempo=2.0,atempo=2.0,atempo=2.0[a]" -map "[v]" -map "[a]" -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -movflags +faststart -c:a aac -b:a 192k "$OUTPUT"
The product of the atempo chain must equal 1/F or the audio will drift away from the picture. Check by multiplying: 0.5 cubed is 0.125, which is 1/8. The three commands above are x8, x16 and 8x faster; for other factors just add or remove links.
Windows
$IN="input.mp4"; $OUT="output.mp4"; $F=8; $FPS=60
ffmpeg -i "$IN" -filter_complex "[0:v]setpts=${F}*PTS,minterpolate=fps=${FPS}:mi_mode=blend[v];[0:a]atempo=0.5,atempo=0.5,atempo=0.5[a]" -map "[v]" -map "[a]" -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -movflags +faststart -c:a aac -b:a 192k "$OUT"
The product of the atempo chain must equal 1/F or the audio will drift away from the picture. Check by multiplying: 0.5 cubed is 0.125, which is 1/8. The three commands above are x8, x16 and 8x faster; for other factors just add or remove links.
Linux
INPUT="input.mp4"; OUTPUT="output.mp4"; F=8; FPS=60
ffmpeg -i "$INPUT" -filter_complex "[0:v]setpts=${F}*PTS,minterpolate=fps=${FPS}:mi_mode=blend[v];[0:a]atempo=0.5,atempo=0.5,atempo=0.5[a]" -map "[v]" -map "[a]" -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -movflags +faststart -c:a aac -b:a 192k "$OUTPUT"
ffmpeg -i "$INPUT" -filter_complex "[0:v]setpts=16*PTS,minterpolate=fps=30:mi_mode=blend[v];[0:a]atempo=0.5,atempo=0.5,atempo=0.5,atempo=0.5[a]" -map "[v]" -map "[a]" -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -movflags +faststart -c:a aac -b:a 192k "$OUTPUT"
ffmpeg -i "$INPUT" -filter_complex "[0:v]setpts=0.125*PTS[v];[0:a]atempo=2.0,atempo=2.0,atempo=2.0[a]" -map "[v]" -map "[a]" -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -movflags +faststart -c:a aac -b:a 192k "$OUTPUT"
The product of the atempo chain must equal 1/F or the audio will drift away from the picture. Check by multiplying: 0.5 cubed is 0.125, which is 1/8. The three commands above are x8, x16 and 8x faster; for other factors just add or remove links.
Progress and timing
For long jobs, write progress to a file so you can see how much is left.
macOS
INPUT="input.mp4"; OUTPUT="output.mp4"; F=8; FPS=60
ffmpeg -progress pipe:1 -nostats -i "$INPUT" -vf "setpts=${F}*PTS,fps=${FPS}" -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -movflags +faststart -an "$OUTPUT"
ffmpeg -progress progress.txt -nostats -i "$INPUT" -vf "setpts=${F}*PTS,fps=${FPS}" -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -movflags +faststart -an "$OUTPUT"
Example: a 12 second source with F=8 and FPS=60 gives ceil(12 * 8 * 60) = 5760 output frames. If frame= climbs by 20 per second, that is about 5 minutes of work. The second command writes the same data to progress.txt, which is handy when ffmpeg runs in the background.
Windows
$IN="input.mp4"; $OUT="output.mp4"; $F=8; $FPS=60
ffmpeg -progress pipe:1 -nostats -i "$IN" -vf "setpts=${F}*PTS,fps=${FPS}" -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -movflags +faststart -an "$OUT"
Example: a 12 second source with F=8 and FPS=60 gives ceil(12 * 8 * 60) = 5760 output frames. If frame= climbs by 20 per second, that is about 5 minutes of work. The second command writes the same data to progress.txt, which is handy when ffmpeg runs in the background.
Linux
INPUT="input.mp4"; OUTPUT="output.mp4"; F=8; FPS=60
ffmpeg -progress pipe:1 -nostats -i "$INPUT" -vf "setpts=${F}*PTS,fps=${FPS}" -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -movflags +faststart -an "$OUTPUT"
ffmpeg -progress progress.txt -nostats -i "$INPUT" -vf "setpts=${F}*PTS,fps=${FPS}" -c:v libx264 -crf 18 -preset slow -pix_fmt yuv420p -movflags +faststart -an "$OUTPUT"
Example: a 12 second source with F=8 and FPS=60 gives ceil(12 * 8 * 60) = 5760 output frames. If frame= climbs by 20 per second, that is about 5 minutes of work. The second command writes the same data to progress.txt, which is handy when ffmpeg runs in the background.
Which encoders you actually have
Start by checking what your ffmpeg was built with. Being listed guarantees nothing: an encoder can be compiled in while the hardware for it is simply absent. Only a one-frame test run tells you the truth.
macOS
ffmpeg -hide_banner -encoders | grep -E "videotoolbox|nvenc|qsv|amf|vaapi"
ffmpeg -hide_banner -f lavfi -i testsrc=size=1280x720:rate=30 -frames:v 1 -c:v h264_videotoolbox -f null -
The honest trade-off: libx264 -crf 18 stays the default because at equal bitrate it gives the better picture. A hardware encoder is the "fast" option and quality at the same bitrate will be lower, especially on motion. And more to the point: in motion mode the bottleneck is not the encoder, it is minterpolate, which runs on the CPU. There a hardware encoder buys almost nothing, because the encoder sits idle waiting for frames anyway.
Windows
ffmpeg -hide_banner -encoders | Select-String "videotoolbox|nvenc|qsv|amf|vaapi"
ffmpeg -hide_banner -f lavfi -i testsrc=size=1280x720:rate=30 -frames:v 1 -c:v h264_nvenc -f null -
The honest trade-off: libx264 -crf 18 stays the default because at equal bitrate it gives the better picture. A hardware encoder is the "fast" option and quality at the same bitrate will be lower, especially on motion. And more to the point: in motion mode the bottleneck is not the encoder, it is minterpolate, which runs on the CPU. There a hardware encoder buys almost nothing, because the encoder sits idle waiting for frames anyway.
Linux
ffmpeg -hide_banner -encoders | grep -E "videotoolbox|nvenc|qsv|amf|vaapi"
ffmpeg -hide_banner -vaapi_device /dev/dri/renderD128 -f lavfi -i testsrc=size=1280x720:rate=30 -frames:v 1 -vf "format=nv12,hwupload" -c:v h264_vaapi -f null -
The honest trade-off: libx264 -crf 18 stays the default because at equal bitrate it gives the better picture. A hardware encoder is the "fast" option and quality at the same bitrate will be lower, especially on motion. And more to the point: in motion mode the bottleneck is not the encoder, it is minterpolate, which runs on the CPU. There a hardware encoder buys almost nothing, because the encoder sits idle waiting for frames anyway.
Hardware encoding
A hardware encoder is several times faster, but at equal bitrate the picture is worse, especially on motion. In "Motion" mode the bottleneck is minterpolate on the CPU anyway, so it buys you almost nothing there.
macOS
INPUT="input.mp4"; OUTPUT="output.mp4"; F=8; FPS=60
ffmpeg -i "$INPUT" -vf "setpts=${F}*PTS,fps=${FPS}" -c:v h264_videotoolbox -b:v 12M -pix_fmt yuv420p -movflags +faststart -an "$OUTPUT"
ffmpeg -i "$INPUT" -vf "setpts=${F}*PTS,fps=${FPS}" -c:v hevc_videotoolbox -tag:v hvc1 -b:v 8M -movflags +faststart -an "$OUTPUT"
ffmpeg -i "$INPUT" -vf "setpts=${F}*PTS,fps=${FPS}" -c:v libx265 -crf 20 -preset slow -tag:v hvc1 -pix_fmt yuv420p -movflags +faststart -an "$OUTPUT"
VideoToolbox does not understand -crf: quality is set by bitrate with -b:v, or -q:v on Apple Silicon. It works on both Intel and M-series.
Windows
$IN="input.mp4"; $OUT="output.mp4"; $F=8; $FPS=60
ffmpeg -i "$IN" -vf "setpts=${F}*PTS,fps=${FPS}" -c:v h264_nvenc -rc vbr -cq 23 -b:v 0 -pix_fmt yuv420p -movflags +faststart -an "$OUT"
ffmpeg -i "$IN" -vf "setpts=${F}*PTS,fps=${FPS}" -c:v h264_qsv -global_quality 23 -pix_fmt nv12 -movflags +faststart -an "$OUT"
ffmpeg -i "$IN" -vf "setpts=${F}*PTS,fps=${FPS}" -c:v h264_amf -rc cqp -qp_i 22 -qp_p 24 -pix_fmt yuv420p -movflags +faststart -an "$OUT"
nvenc uses -cq with -rc vbr -b:v 0 instead of crf, qsv uses -global_quality, and amf uses -rc cqp with -qp_i and -qp_p. For AMD: ffmpeg -i "$IN" -vf "setpts=${F}*PTS,fps=${FPS}" -c:v h264_amf -rc cqp -qp_i 22 -qp_p 24 -pix_fmt yuv420p -movflags +faststart -an "$OUT"
Linux
INPUT="input.mp4"; OUTPUT="output.mp4"; F=8; FPS=60
ffmpeg -i "$INPUT" -vf "setpts=${F}*PTS,fps=${FPS}" -c:v h264_nvenc -rc vbr -cq 23 -b:v 0 -pix_fmt yuv420p -movflags +faststart -an "$OUTPUT"
ffmpeg -vaapi_device /dev/dri/renderD128 -i "$INPUT" -vf "setpts=${F}*PTS,fps=${FPS},format=nv12,hwupload" -c:v h264_vaapi -b:v 12M -movflags +faststart -an "$OUTPUT"
VA-API needs an explicit device and an upload of the frames into GPU memory: hence -vaapi_device /dev/dri/renderD128 and the format=nv12,hwupload tail on the filter. Check that the device exists with ls /dev/dri/. nvenc needs the proprietary NVIDIA drivers; the open nouveau driver cannot encode.
The same thing without a browser
Once ffmpeg is installed no internet is involved at all, neither to start nor to process. That suits air-gapped setups, machines with no network and batch jobs where the data must stay on one specific computer.
These commands target ffmpeg 6 and newer. Some filter names differ in older builds.