Create Short Preview from Video
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!
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?
“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.
He was referring to personal mindset not physical age. Get over it.
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.
To calculate the correct duration I had to change the ‘interval’ line to:
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:
this fixed order but
60mins video, 30 snippets -> 2mins each snippets
so it will start at 0:02:00 -> 0:04:00 ->
is there have anyway to make it start at 0:00:00 (1st snippet) -> 0:02:00 (2nd snippet) -> ….
on pornhub i see it use 12 snippets, each snippets is 1s i think, and it start from beginning of each snippets
I would change:
to
in case the file name has periods.
i keep getting
even if i create the list.txt file it gets removed when i run the script and the same error appears ?
i get this error when i run
removing
fixed it but why ?
btw could this script be converted in php
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 :)
What happened to the link? Could do with this in PHP if anyone has it.
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.Mr. Walsh, this is exactly what I was looking for! Thank you!
And @Armin, thank you. Using Linux time bringing the -ss to the front brought my real time down from 34 seconds to 4 SECONDS. Wow. Thanks.
I was familiar with trimming clips with ffmpeg and definitely skimmed past a post or two about the -ss placement in the past but this was a real-life example and definitely made me take it more seriously
Here is a working optimized version with faster encoding and optimized mp4 parameters which makes the file size 10 times less in my case.
Unfortunately I can’t edit my comment. Move -ss to the beginning like that for much faster speeds
I’m trying to do the exact same thing but using JavaScript. By chance do you know how this is done?
I’m trying to do the same exact thing but using JavaScript and Python; I’m using Django for my web site. By any chance do you know how to do it this way or can maybe point me in the right direction. Thank you very much.
Does know how to predict which frames are going to be used to let the uploader choose the video thumbnail in pornhub and similar sites? The workaround I found is to upload a video with the same number of frames with only a frame counter, check which frames are selected and then re-render the video with a title thumbnail in one of those frames. Of course, it’s a pain and it would be great if I could do the rendering only once..
Hi guys, you guys are coding gods to me. Thank you very much, I had problems even to make this work but now I finally did.
Is it please possible to use this in batch mode and without need to manually naming input output files ?
Thank you
Vlad
Here is a working PowerShell script for Windows users.