Set display time of individual frames in FFmpeg
It took me quite a while of Googling and trial-and-error but I finally figured out how to actually do this. Thanks to Gyan for pointing me in the right direction.

I tried to do more of the process as one-liners but it always turned into a non-functional, undebuggable mess. If you think you could script this, please do so, test it, and then post it as a new answer.



Print pkt_duration_time line and save it into a file:

ffprobe -show_frames input.gif | grep pkt_duration_time= &gt;frames.txt

Modify the line to the format that FFmpeg's concat filter requires:

sed 's/pkt_duration_time=/duration /g' frametimes.txt &gt;frametimes.txt

Print a list of your input files (frames) located in folder/ and modify the list to be the format FFmpeg's concat filter requires:

ls --quoting-style=shell folder/ &gt;filenames.txt &amp;&amp; sed -i -e 's/^/file /' filenames.txt

Combine your frame-time file and filenames file to a concat file that FFmpeg accepts:

paste -d \\n filenames.txt frametimes.txt &gt; folder/concatfile.txt

We saved the concat file to where the input files are located because otherwise FFmpeg won't accept it. One more thing before we can encode the video: the data for the last frame needs to be present in the concat file twice for some reason. I don't have a CLI for that so just copy-paste it manually.

Now we can use FFmpeg to turn the image files to an animated video with correct frame times:

cd folder/

ffmpeg -f concat -safe 0 -i concatfile.txt -pix_fmt yuv420p -c:v libvpx-vp9 -b:v 0 -cpu-used 0 -tile-columns 2 -row-mt 1 -lag-in-frames 25 -threads 16 -crf 35 ../output.webm


The -safe 0 switch is required because otherwise FFmpeg complains about unsafe file names. At least it did in my case, possibly because the file names contained square brackets.
The -pix_fmt yuv420p ensures the correct color space is used. If you omit this, a color space that isn't standard for web video will be used and in the case of VP9 the resulting file will use Profile 1 which doesn't have as wide hardware decoding support as Profile 0. You could use -profile:v 0 to make sure that this doesn't happen without you noticing.


The rest of the parameters are quality and rate control features for libvpx-vp9. Remember to at least use -b:v 0, otherwise CRF won't behave as you'd expect. Also note that setting -tile-columns and -threads manually is required, otherwise encoding is limited to a single thread.