1

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?

G.P.M
  • 33

2 Answers2

1

You can use the printf command to zero-pad a number out to a fixed width.

printf [-v var] format [arguments]
       Write  the  formatted arguments to the standard output under the
       control of the format.  The -v option causes the  output  to  be
       assigned  to  the  variable var rather than being printed to the
       standard output.
   The format is a character string which contains three  types  of
   objects:  plain  characters, which are simply copied to standard
   output, character escape  sequences,  which  are  converted  and
   copied  to  the standard output, and format specifications, each
   of which causes printing of the next  successive  argument.

Bash's implementation will accept the d specifier, with a width and the 0 flag from C's printf() function.

Example:

$ for c in {8..11}; do
> printf -v result '%03d' $c
> echo $result
> done
008
009
010
011

So, instead of hardcoding the number of zeros in your output file name like thumb00$c.jpg, you can use printf to figure out the needed number of zeros to bring a number to a specified width:

thumb$(printf '%03d' $c).jpg

Explanation:

  • $() is process substitution. It lets you use the std output of a command as part of another command.
  • printf runs the printf command.
  • '%03d' defines the format ofprintf`'s output.
    • % means we want to use format specifier.
    • 0 means we want to zero pad.
    • 3 is the length to which we pad.
    • d is the format specifier, in particular it means we're going to be passing printf another parameter and that parameter should be used as a signed decimal integer.
  • $c is the parameter we're sending to printf.
8bittree
  • 2,958
1

Looks like what you want to do is divide the file into N intervals, one for each thumbnail and pick the first keyframe after the midpoint of each interval. This can be done quickly with a single run of ffmpeg, given the duration of each interval ( D/N; let's call it I ).

ffmpeg -discard nokey -skip_frame nokey -i input -vf select='eq(n,0)+gte(mod(t,I),I/2)*gte(t-prev_selected_t,I/2)',trim=1 -vsync 0 -vframes N image%d.jpg

-discard nokey tells the demuxer to skip non-keyframes. -skip_frame nokey does that for the decoder. For MP4..etc, this will run about 15-20 times faster than if these options were omitted.

Gyan
  • 38,955