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