Create Short Preview from Video

By  on  

I reference an adult video (porn) site in this post.  We're all grown ups, please do not be offended.

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

Improvements

You can bet that PornHub and every other video site that employs this technique has a script much more secure and powerful than this script.  A few ways this script could be improved:

  • Validation:  I check that the file exists but there's no check to ensure it's a valid media file.
  • Command Line Arguments:  Settings like video size should probably be an optional command line argument.
  • Encoding:  I know that additional command line arguments need to be passed in depending on video file type (and there are a billion out there).  A serious script would really beef of that logic.
  • Optimization:  I'm not even close to a video or media expert of any kind.  There has to be a way to speed up the snippet generation process as it's quite slow within the script now.  I usually add -c copy when I slice files but ffmpeg yelled at me so I passed on that argument.  Please share if you know how to improve the slicing or concatenation process!
  • Dependencies:  Not necessarily and improvement but it's important to call out that a simple brew install ffmpeg wont get you many of the codec and encoding support your need.  I needed to do the following to work with webm's, for example:
    brew reinstall ffmpeg --with-fdk-aac --with-ffplay --with-freetype --with-frei0r --with-libass --with-libvo-aacenc --with-libvorbis --with-libvpx --with-opencore-amr --with-openjpeg --with-opus --with-rtmpdump --with-speex --with-theora --with-tools --with-libvorbis
    

Taking the time to piece together this script was exhilarating; a mix of media handling, bash scripting, and seeing the end product for something that's used on scores of video sites makes for a real feeling of achievement.  Please let me know how the script above can be improved!

Recent Features

  • By
    5 Awesome New Mozilla Technologies You&#8217;ve Never Heard Of

    My trip to Mozilla Summit 2013 was incredible.  I've spent so much time focusing on my project that I had lost sight of all of the great work Mozillians were putting out.  MozSummit provided the perfect reminder of how brilliant my colleagues are and how much...

  • By
    Write Better JavaScript with Promises

    You've probably heard the talk around the water cooler about how promises are the future. All of the cool kids are using them, but you don't see what makes them so special. Can't you just use a callback? What's the big deal? In this article, we'll...

Incredible Demos

  • By
    jQuery Wookmark

    The first thing that hits you when you visit Pinterest is "Whoa, the columns are the same width and the photos are cut to fit just the way they should."  Basic web users probably think nothing of it but as a developer, I can appreciate the...

  • By
    Translate Content with the Google Translate API and JavaScript

    Note:  For this tutorial, I'm using version1 of the Google Translate API.  A newer REST-based version is available. In an ideal world, all websites would have a feature that allowed the user to translate a website into their native language (or even more ideally, translation would be...

Discussion

  1. hatoro

    As always thanks for sharing your thoughts. I am interested in the details of said and element swap. Surely it involves async loading of video onhover? I would use webpack, what about you?

  2. John Brown

    “We’re all grown ups” Absolutely wrong! I’m in my early 17’s. Still visiting this site. Moreover if we grown up so it means we seen porn or gave reference from those nasty sites? ridiculus logic.

    • Tom

      He was referring to personal mindset not physical age. Get over it.

  3. Awesome! Thanks for the post + script. :) I’m interested in writing a script to pull a poster image out of a webm or mp4 file in order to automate the process a little. Just started scripting with ffmpeg. I’ll checkout your “Create an Image Preview from a Video” post now.

  4. Sander

    To calculate the correct duration I had to change the ‘interval’ line to:

    interval=$((($length-$starttimeseconds)/$desiredsnippets))
  5. Sander

    If you want more than 9 snippets, the sorting of the snippets is out of order (1, 10, 11, 12, 2, 3, 4…). I’ve changed this part of the code to work with leading zeros (as many as needed) to have the snippets in the correct order when they are concatenated:

    for i in $(seq -w 1 $desiredsnippets)
      do
        # Format the second marks into hh:mm:ss format
        start=$(($(($(echo $i | sed 's/^0*//')*$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
    

Wrap your code in <pre class="{language}"></pre> tags, link to a GitHub gist, JSFiddle fiddle, or CodePen pen to embed!