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
    How to Create a RetroPie on Raspberry Pi &#8211; Graphical Guide

    Today we get to play amazing games on our super powered game consoles, PCs, VR headsets, and even mobile devices.  While I enjoy playing new games these days, I do long for the retro gaming systems I had when I was a kid: the original Nintendo...

  • By
    CSS @supports

    Feature detection via JavaScript is a client side best practice and for all the right reasons, but unfortunately that same functionality hasn't been available within CSS.  What we end up doing is repeating the same properties multiple times with each browser prefix.  Yuck.  Another thing we...

Incredible Demos

  • By
    Fancy FAQs with jQuery Sliders

    Frequently asked questions can be super boring, right? They don't have to be! I've already shown you how to create fancy FAQs with MooTools -- here's how to create the same effect using jQuery. The HTML Simply a series of H3s and DIVs wrapper...

  • By
    GitHub-Style Sliding Links

    GitHub seems to change a lot but not really change at all, if that makes any sense; the updates come often but are always fairly small. I spotted one of the most recent updates on the pull request page. Links to long branch...

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
    
  6. user135711

    I would change:

    extension=${destfile#*.}

    to

    extension=${destfile##*.}

    in case the file name has periods.

  7. user

    i keep getting

    list.txt: No such file or directory

    even if i create the list.txt file it gets removed when i run the script and the same error appears ?

  8. Fatih Akgun

    i get this error when i run

    $ ./preview.sh input.mp4 output.mp4
    
    list.txt: No such file or directory
    Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'input.mp4':
      Metadata:
        major_brand     : isom
        minor_version   : 512
        compatible_brands: isomiso2avc1mp41
        encoder         : Lavf56.40.101
      Duration: 00:42:01.20, start: 0.000000, bitrate: 255 kb/s
        Stream #0:0(und): Video: h264 (Constrained Baseline) (avc1 / 0x31637661), yuv420p, 480x270, 219 kb/s, 15 fps, 15 tbr, 15360 tbn, 30 tbc (default)
        Metadata:
          handler_name    : VideoHandler
        Stream #0:1(und): Audio: mp3 (mp4a / 0x6134706D), 22050 Hz, mono, s16p, 32 kb/s (default)
        Metadata:
          handler_name    : SoundHandler
    snippets/1.mp4: No such file or directory
    
  9. Fatih Akgun

    removing

    &>/dev/null

    fixed it but why ?

  10. Fatih Akgun

    btw could this script be converted in php

  11. Fatih Akgun

    ok i finnaly made it in php

    here is the file https://gist.github.com/AkgunFatih/88c2241865c25e40a50edc4e24679a94

    i only need some checks and refactor but it works :)

  12. Armin

    To speed things up you should put the -ss switch before the -i switch. Doing it your way is slower. You’re doing output filtering, so it takes a frame, translates it and then checks if it is in the legal range. The other way around it checks the legal range before translating the frame.
    You can also use the -thread switch with $(nproc) as argument. This spawns as many threads as there are cores in the CPU.

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