Generate a preview video of a larger video

Original Article:
davidwalsh.name/video-preview

Displaying a preview image before loading a video is common practice with web media these days; oftentimes that image preview is the video’s first frame or an important frame from the video.  If you’ve visited an adult site recently (ahem), you will have noticed that not only do you get a preview image but hovering over said image shows a small sample video with snippets at different points of the movie.  Naturally I was interested in how all of this worked and concluded the following:

  • They create the preview image using a tool like ffmpeg
  • Upon hover of image, the preview video is loaded asynchronously in the background
  • Once the preview image has loaded, the video is displayed over the image and autoplayed
  • When the mouse leaves, the video is hidden (in case the user hovers over the image again)

Achieving the <image> and <video> element swap is simple but I wanted to know how PornHub, the largest adult site network, generated their preview videos.  The idea behind the preview video is simple, of course, but I wanted to teach myself more about ffmpeg and bash scripting.  Here’s the proof:

Video preview generated from a local download of Kung Fury.

Let’s deconstruct my code to learn how this (very basic) preview video can be created!

Running the Script

Our script, preview.sh, will be called as follows:

# ./preview.sh {input.format} {output.format}
$ ./preview.sh kung-fury.mp4 kung-fury-preview.mp4

In a perfect world the script would accept loads more arguments, like desired video size or any of the settings we’ll assume within the script, but I wanted to focus on getting the meat of the script working; adding command line arguments would be a routine exercise when everything else was a go.

The Script: preview.sh

The script starts with some very basic validation:

sourcefile=$1
destfile=$2

# Overly simple validation
if [ ! -e "$sourcefile" ]; then
  echo 'Please provide an existing input file.'
  exit
fi

if [ "$destfile" == "" ]; then
  echo 'Please provide an output preview file name.'
  exit
fi

Next we have a series of variables relating to video length, desired points in the video to cut previews, the video size, etc.:

# Detect destination file extension
extension=${destfile#*.}

# Get video length in seconds
length=$(ffprobe $sourcefile  -show_format 2>&1 | sed -n 's/duration=//p' | awk '{print int($0)}')

# Start 20 seconds into the video to avoid opening credits (arbitrary)
starttimeseconds=20

# Mini-snippets will be 2 seconds in length
snippetlengthinseconds=2

# We'll aim for 5 snippets spread throughout the video
desiredsnippets=5

# Ensure the video is long enough to even bother previewing
minlength=$(($snippetlengthinseconds*$desiredsnippets))

# Video dimensions (these could probably be command line arguments)
dimensions=640:-1

# Temporary directory and text file where we'll store snippets
# These will be cleaned up and removed when the preview image is generated
tempdir=snippets
listfile=list.txt

If the video is too short to generate a preview from, simply bail:

# Display and check video length
echo 'Video length: ' $length
if [ "$length" -lt "$minlength" ]
then
  echo 'Video is too short.  Exiting.'
  exit
fi

Next we generate a series of “snippet” videos at calculated times within the video, saving these snippet videos to a temporary directory:

# Loop and generate video snippets
mkdir $tempdir
interval=$(($length/$desiredsnippets-$starttimeseconds))
for i in $(seq 1 $desiredsnippets)
  do
    # Format the second marks into hh:mm:ss format
    start=$(($(($i*$interval))+$starttimeseconds))
    formattedstart=$(printf "%02d:%02d:%02d\n" $(($start/3600)) $(($start%3600/60)) $(($start%60)))
    echo 'Generating preview part ' $i $formattedstart
    # Generate the snippet at calculated time
    ffmpeg -i $sourcefile -vf scale=$dimensions -preset fast -qmin 1 -qmax 1 -ss $formattedstart -t $snippetlengthinseconds $tempdir/$i.$extension &>/dev/null
done

Note: copying and slicing videos with ffmpeg is difficult if you aren’t a codec and media expert, which I certainly am not.  These basic settings do allow the job to get done but maybe not in the most optimal way.  Please comment below if you know a way to improve performance or video quality.

The last step is concatenating the snippet videos in to the final preview video:

# Concat videos
echo 'Generating final preview file'

# Generate a text file with one snippet video location per line
# (https://trac.ffmpeg.org/wiki/Concatenate)
for f in $tempdir/*; do echo "file '$f'" >> $listfile; done

# Concatenate the files based on the generated list
ffmpeg -f concat -safe 0 -i $listfile -c copy $destfile &>/dev/null

echo 'Done!  Check ' $destfile '!'

# Cleanup
rm -rf $tempdir $listfile

Video concatenation is easy in this case because we’re concatenating snippets from the same original video.  Concatenation would be much more difficult if we were using videos of different frame rates, sizes, etc.

The final script in all its glory:

sourcefile=$1
destfile=$2

# Overly simple validation
if [ ! -e "$sourcefile" ]; then
  echo 'Please provide an existing input file.'
  exit
fi

if [ "$destfile" == "" ]; then
  echo 'Please provide an output preview file name.'
  exit
fi

# Detect destination file extension
extension=${destfile#*.}

# Get video length in seconds
length=$(ffprobe $sourcefile  -show_format 2>&1 | sed -n 's/duration=//p' | awk '{print int($0)}')

# Start 20 seconds into the video to avoid opening credits (arbitrary)
starttimeseconds=20

# Mini-snippets will be 2 seconds in length
snippetlengthinseconds=2

# We'll aim for 5 snippets spread throughout the video
desiredsnippets=5

# Ensure the video is long enough to even bother previewing
minlength=$(($snippetlengthinseconds*$desiredsnippets))

# Video dimensions (these could probably be command line arguments)
dimensions=640:-1

# Temporary directory and text file where we'll store snippets
# These will be cleaned up and removed when the preview image is generated
tempdir=snippets
listfile=list.txt

# Display and check video length
echo 'Video length: ' $length
if [ "$length" -lt "$minlength" ]
then
  echo 'Video is too short.  Exiting.'
  exit
fi

# Loop and generate video snippets
mkdir $tempdir
interval=$(($length/$desiredsnippets-$starttimeseconds))
for i in $(seq 1 $desiredsnippets)
  do
    # Format the second marks into hh:mm:ss format
    start=$(($(($i*$interval))+$starttimeseconds))
    formattedstart=$(printf "%02d:%02d:%02d\n" $(($start/3600)) $(($start%3600/60)) $(($start%60)))
    echo 'Generating preview part ' $i $formattedstart
    # Generate the snippet at calculated time
    ffmpeg -i $sourcefile -vf scale=$dimensions -preset fast -qmin 1 -qmax 1 -ss $formattedstart -t $snippetlengthinseconds $tempdir/$i.$extension &>/dev/null
done

# Concat videos
echo 'Generating final preview file'

# Generate a text file with one snippet video location per line
# (https://trac.ffmpeg.org/wiki/Concatenate)
for f in $tempdir/*; do echo "file '$f'" >> $listfile; done

# Concatenate the files based on the generated list
ffmpeg -f concat -safe 0 -i $listfile -c copy $destfile &>/dev/null

echo 'Done!  Check ' $destfile '!'

# Cleanup
rm -rf $tempdir $listfile