I try to get a solution to make screens from videos with ffmpeg.
Most of the found examples involve decoding the whole video to get the images. This is - for larger videos - rather slow.
A better attempt was described roughly in: meaningful-thumbnails-for-a-video-using-ffmpeg
The pseudo code there was:
for X in 1..N
T = integer( (X - 0.5) * D / N )
run `ffmpeg -ss <T> -i <movie>
-vf select="eq(pict_type\,I)" -vframes 1 image<X>.jpg`
Where:
- D - video duration read from ffmpeg -i alone or ffprobe which has nice JSON output writer btw
- N - total number of thumbnails you want
- X - thumbnail number, from 1 to N
- T - time point for tumbnail
I've come up with a working solution based on this 'pseudo-code' and combined it with thumbnail montage in imagemagick:
#!/bin/bash
# some of them not used here
MOVIE=$1
D= # D - video duration
N=30 # N - wanted number of thumbnails
X=1 # X - thumbnail number
T= # T - time point for thumbnail
Y= # Y - nth frame to take
Z= # Z - total number of frames
W= # W - fps of the video
SP= # SP - starting point of extraction
OUTPUT=$2
# some of them from another approach - setting defaults
if [ -z "$N" ]; then N=30; fi
if [ -z "$COLS" ]; then COLS=3; fi
if [ -z "$ROWS" ]; then ROWS=10; fi
if [ -z "$HEIGHT" ]; then HEIGHT=360; fi
if [ -z "$SIZE" ]; then SIZE=3600; fi
# get video name without the path and extension
MOVIE_NAME=$(basename $MOVIE)
OUT_DIR=$(pwd)
if [ -z "$OUTPUT" ]; then OUTPUT=$(echo ${MOVIE_NAME%.*}_preview.jpg); fi
# get duration of input:
D=$(echo "$(ffprobe -hide_banner -i $MOVIE 2>&1 | sed -n "s/.*: \(.*\), start:.*/\1/p")" | sed 's/:/*60+/g;s/*60/&&/' | bc)
D=$(echo "$D/1" | bc) # get rid of the floating point part (make integer)
# get fps of input:
W=$(ffprobe $MOVIE 2>&1| grep ",* fps" | cut -d "," -f 5 | cut -d " " -f 2)
# get frame number of input:
Z=$(ffprobe -hide_banner -show_streams $MOVIE 2>&1 | grep nb_frames | head -n1 | sed 's/.*=//')
# as a fallback we'll calculate the frame count
# from duration and framerate, very unprecise, though
if [ "$Z" = "N/A" ]; then Z=$(echo "$D * $W / 1" | bc); fi
echo "Duration is: $D seconds / $Z frames @ $W fps"
# generate thumbnails in the /tmp folder
TMPDIR=/tmp/thumbnails-${RANDOM}/
mkdir $TMPDIR
for (( c=X; c<=N; c++ ))
do
Y=$(echo "($Z / $N)/1" | bc)
T=$(echo "(($c - 0.5) * $Y/1)" | bc)
SP=$(echo "$T / $W" | bc)
ffmpeg -hide_banner -ss $SP -i $MOVIE -an -sn -vf select="eq(pict_type\,I),scale=-1:360" -vframes 1 ${TMPDIR}thumb00$c.jpg
done
# mount the pics in one page neatly
montage ${TMPDIR}thumb*.jpg -background white -geometry +5+5 -tile ${COLS}x ${TMPDIR}output.jpg
rm -R $TMPDIR
echo $OUT_FILEPATH
This works but I'm struggeling with the created file names.
As the ffmpeg invokation happens in a 'for loop' the obvious name%03d.jpg pattern will not work for output file.
Output of the last iteration:
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'test.mp4':
Metadata:
major_brand : isom
minor_version : 512
compatible_brands: isomiso2avc1mp41
encoder : Lavf54.59.106
Duration: 00:08:41.17, start: 0.023220, bitrate: 1866 kb/s
Stream #0:0(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 1280x720, 1731 kb/s, 25 fps, 25 tbr, 12800 tbn, 50 tbc
(default)
Metadata:
handler_name : VideoHandler
Stream #0:1(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 127 kb/s (default)
Metadata:
handler_name : SoundHandler
[swscaler @ 0x15a85a0] deprecated pixel format used, make sure you did set range correctly
Output #0, image2, to '/tmp/thumbnails-13957/thumb0030.jpg':
Metadata:
major_brand : isom
minor_version : 512
compatible_brands: isomiso2avc1mp41
encoder : Lavf56.40.101
Stream #0:0(und): Video: mjpeg, yuvj420p(pc), 640x360, q=2-31, 200 kb/s, 25 fps, 25 tbn, 25 tbc (default)
Metadata:
handler_name : VideoHandler
encoder : Lavc56.60.100 mjpeg
Stream mapping:
Stream #0:0 -> #0:0 (h264 (native) -> mjpeg (native))
Press [q] to stop, [?] for help
frame= 1 fps=0.0 q=3.2 Lsize=N/A time=00:00:00.04 bitrate=N/A dup=1 drop=1
video:8kB audio:0kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: unknown
I've tried and put the iteration variable with leading zeros in the output file name thumb00$c.jpg. This works so far - but: Since the iteration goes over 10 my file names are not any longer in order, which means the following montage command of imagemagick puts them in the wrong order.
This is what I get:
-rw-rw-r-- 1 gpm gpm 17303 Sep 8 19:32 thumb0010.jpg
-rw-rw-r-- 1 gpm gpm 16474 Sep 8 19:32 thumb0011.jpg
- - " -
-rw-rw-r-- 1 gpm gpm 6323 Sep 8 19:32 thumb001.jpg
-rw-rw-r-- 1 gpm gpm 14789 Sep 8 19:32 thumb0020.jpg
-rw-rw-r-- 1 gpm gpm 18429 Sep 8 19:32 thumb0021.jpg
- - " -
-rw-rw-r-- 1 gpm gpm 18870 Sep 8 19:32 thumb002.jpg
-rw-rw-r-- 1 gpm gpm 7926 Sep 8 19:32 thumb0030.jpg
-rw-rw-r-- 1 gpm gpm 18312 Sep 8 19:32 thumb003.jpg
-rw-rw-r-- 1 gpm gpm 18274 Sep 8 19:32 thumb004.jpg
As one can see the leading zeros are there but the files are not in order.
I'm lost here.
How can I get proper increasing filenames out of that?