How to use FFmpeg to generate live thumbnail previews of a video

Not a static thumbnail matrix but a live-action thumbnail matrix

I mass-download videos from online channels. The download script does add metadata to the filename as well but I still find myself lost. The video names may not be informative enough. I needed a video preview that included not just the beginning of the video but several stops towards the end as well.

Screenshot of thumbnail video

I have provided two options in the script. One can load the matrix in memory and play it from there using ffplayffmpeg's own media player. The other will save the matrix to a temporary file and then plays it using the default media player. (The script does delete this temporary file after one minute.) If you do not have ffplay installed, then set bWriteToFile=0 and the video will play from a temporary file.

Le script is as follows:

#!/bin/bash
set -u

# Get file info
sFile="$*"
sFileDir=$(dirname "$sFile")
sFileFullName=$(basename "$sFile")
sFileName="${sFileFullName%.*}"
sExt="${sFileFullName##*.}"

# Changeables
bWriteToFile=1
#bWriteToFile=0
iLength=20
iDur=0
iWidth=0
iHeight=0
iRows=3
iCols=3
iDurLimit=$(( iRows*iCols*iLength*2 ))
#echo $iDurLimit

# Get video info
sProbeData=$(ffprobe -loglevel quiet -print_format "default=nw=1:nk=1" \
                -select_streams v:0 \
                -show_entries "stream=width,height,duration" \
                -i "${sFile}")

sDur=$(echo $sProbeData | cut -d " " -f3)
sWidth=$(echo $sProbeData | cut -d " " -f1)
sHeight=$(echo $sProbeData | cut -d " " -f2)

if [ "$sDur" = "N/A" ]; then
  sDur=$(ffprobe -loglevel quiet -print_format "default=nw=1:nk=1" \
                 -select_streams v:0 \
                 -show_entries "stream_tags=duration" \
                 -i "${sFile}")
  if [ -z "${sDur}" ]; then
    sDur1=$(ffprobe "${sFile}" 2>&1 | grep "Duration: ")
    if [ -z "${sDur1}" ]; then
            notify-send "Live Preview" "Duration stream information not found"
            echo "Live Preview: Duration stream information not found"
            exit
    else
          sDur2=${sDur1%%,*}
          sDur=${sDur2:11}
    fi
  fi

  sDur=${sDur%.*}
  iMin=$(echo $sDur | cut -d ":" -f2)
  iHour=$(echo $sDur | cut -d ":" -f1)
  iSec=$(echo $sDur | cut -d ":" -f3)
  iHour=${iHour#0}
  iMin=${iMin#0}
  iSec=${iSec#0}
  let iHour=iHour*60*60
  let iMin=iMin*60
  #echo $iHour $iMin $iSec
  let iDur=iHour+iMin+iSec
  #echo $iDur $iHour $iMin $iSec
else
  iDur=${sDur%.*}
fi

if [[ $sWidth =~ [^0-9] ]] || [[ $sHeight =~ [^0-9] ]]  || \
   [[ $sHeight =~ [^0-9] ]]; then
  notify-send "Live Gallery" "Invalid video info found"
    echo "Live Preview: Invalid video info found"
    exit
else
  let iWidth=sWidth
  let iHeight=sHeight
fi

#echo "Duration is $iDur"
#echo "Resolution is ${iWidth}x${iHeight}"

if [ $iDur -lt $iDurLimit ]; then
  notify-send "Live Gallery" "Not worth it"
    echo "Live Preview: Not worth it"
    exit
fi

# Build ffmpeg filter string
let iSplits=iRows*iCols
#echo $iSplits

if [ $bWriteToFile -eq 1 ]; then
  sFilter="movie=filename=${sFile},split=${iSplits}"
else
  sFilter="[0:v:0]split=${iSplits}"
fi

i=0
while [ $i -lt $iSplits ]; do
  sFilter="${sFilter}[v${i}]"
  let i=i+1
done

if [ $bWriteToFile -eq 1 ]; then
  sFilter="${sFilter};amovie=filename=${sFile},asplit=${iSplits}"
else
  sFilter="[0:a:0]asplit=${iSplits}"
fi

i=0
while [ $i -lt $iSplits ]; do
  sFilter="${sFilter}[a${i}]"
  let i=i+1
done

let iBreaks=$(( 1 + (iRows*iCols) ))
let iInterval=iDur/iBreaks
let iStart=iInterval
let iLast=iDur-iInterval
i=0
while [ $iStart -le $iLast ]; do
  sFilter="${sFilter};[v${i}]trim=start=${iStart}:duration=${iLength},setpts=PTS-STARTPTS,scale=${iWidth}/${iCols}:${iHeight}/${iRows}:force_original_aspect_ratio=decrease:force_divisible_by=2[sv${i}];[a${i}]atrim=start=${iStart}:duration=${iLength},asetpts=PTS-STARTPTS[sa${i}]"
  let iStart=iStart+iInterval
  let i=i+1
done
sFilter="${sFilter};"

let i=0
while [ $i -lt $iRows ]; do
  let j=0
  while [ $j -lt $iCols ]; do
    let k=(i*iCols)+j
    sFilter="${sFilter}[sv${k}]"
    let j=j+1
  done
  sFilter="${sFilter}hstack=inputs=${iCols}[r${i}];"
  let i=i+1
done

i=0
while [ $i -lt $iRows ]; do
  sFilter="${sFilter}[r${i}]"
  let i=i+1
done

sFilter="${sFilter}vstack=inputs=${iRows},pad=${iWidth}:${iHeight}:(ow-iw)/2:(oh-ih)/2:black[out0];"

i=0
while [ $i -lt $iSplits ]; do
  sFilter="${sFilter}[sa${i}]"
  let i=i+1
done
sFilter="${sFilter}amix=inputs=${iSplits}[out1]"

# Run it
if [ $bWriteToFile -eq 1 ]; then
  ffplay -autoexit \
         -window_title "Live preview of ${sFileName}.${sExt}" \
         -f lavfi -i "${sFilter}"
else
  sTempDir=$(dirname $(mktemp -u))
  ffmpeg -i "$sFile" \
         -filter_complex "${sFilter}" \
         -map "[out0]" -map "[out1]" \
         -c:v libx265 -preset ultrafast -c:a flac -ac 2 -y \
         "${sTempDir}/${sFileName}-PREVIEW.mkv" && \
      xdg-open "${sTempDir}/${sFileName}-PREVIEW.mkv" && \
      sleep 60 && \
      rm "${sTempDir}/${sFileName}-PREVIEW.mkv"
  #Replace 'libx265 -preset ultrafast' with 'ffv1' if your 
  #media player can play it
fi

I use this script in a Caja Action Configuration.

Caja Actions Configuration screenshot

The script is scalable. On any computer, particularly if you want to listen to the downmixed audio as well, a 2x2 matrix should be optimal. On my computer, anything more than 3x3 causes it to run out of memory. If it was a static thumbnail matrix, the script would have executed a few seconds. But, this script generates video thumbnails and it takes more than just a few seconds. Still, it is a time saver.

Screenshot of thumbnail video

Video Demo

Rumble


Become an FFmpeg PRO by reading my book Quick Start Guide to FFmpeg.

Book photo

Link: | Magic Link:

Comments are not enabled yet.

For older posts, check the archives.