Video Thumbnails

# Up until now my artist recommendation page was mostly pictures with one animated Gif that I improvised for one of the artists. But that approach was going to get very bloated very fast. I knew I needed actual videos for animated thumbnails.

# I figured this would be trivial. Just use FFmpeg! Normally I throw together a script and drop a video file on it to turn it into a MP4.

#Scripting Challenges

# If I want a thumbnail, surely I can just resize and crop a video just as easily right? … right? Well resizing is theoretically easy if I just want to target a specific height, but yesterday I didn’t know FFmpeg had that option, so I thought I needed to figure out the exact width and height myself. That sent me down a rabbit-hole for the rest of the day.

# To figure out a new width and height I need to know the video’s original size. So the first question is how do I read that? It turns out there’s a program called FFprobe that can do it.

how_to_read_width_height

# But uh… how do you store those numbers in a script? Variables of course! That should be eas- a FOR LOOP? To store just one variable? Really?? I checked. This really is the simplest way out of all the answers. Needless to say after over an hour I never once managed to make it work.

store_program_output

#PowerShell

# When I searched for scripts specifically for FFmpeg and FFprobe, people recommend just using PowerShell instead. Now I see why. But it’s been awhile since I used PowerShell so it’s time to roll up my sleeves and re-learn the basics again. Let’s see… I’ll search for a “hello world” program. How do I even run it? Oh that’s right. ExecutionPolicy. PowerShell is the only program on the planet where you have to tell it to run and also give it “permission” to run, all in the very same command. You know… because that little bit of extra text is somehow more secure or something.

running_powershell

# Okay. Next I need to send it the file that I dragged-and-dropped onto the script. How do I do that? It’s actually two parts. You add "%~1" at the end of the CMD. And then put this… incantation… on the first line of your PowerShell script.

# Param([string]$inFilePath='savdalhv01');

# To be honest I kinda cheated. I couldn’t find this information online anymore, so I just looked at an old PowerShell script I wrote years ago. It’s a miracle I still had it. Can you see why I’m writing this? Nobody will be able to just guess this stuff. So if I figure out how to do something tricky, I should write it down. For your sake and mine.

drag_and_drop_to_powershell

# At least variables are easy to use in PowerShell.

powershell_reading_a_dropped_path

# Now to figure out how to run a program and store its output. The entire reason I’m using PowerShell. Fortunately it’s easy to do. Unfortunately it’s almost impossible to find this information too. Searching for “powershell storing program output” just gets me a bunch of bullshit about launching asynchronous processes, yadda yadda… no thanks. The trick is to search for “ffprobe powershell”. Anyway here’s the answer.

# $width = & "ffprobe.exe" -v error -select_streams v:0 -show_entries stream=width -of default=noprint_wrappers=1:nokey=1 "myVideo.mp4";

# You put a variable on the left, type &, and then type the DOS command.

powershell_reading_width_and_height

# Not that anyone cares, but FFprobe can read and output multiple values at the same time, which is probably like… a nanosecond faster or something. But learning how to juggle text is useful in any programming language, so here’s how to split a string in PowerShell.

$info_ary = $info -split "`n";
$inWidth = $info_ary[0];
$inHeight = $info_ary[1];
powershell_string_split

# I guess it might be faster to run this script in PowerShell’s console instead of dropping that video file onto my CMD script over and over, so let’s just pretend I’m doing that by manually setting that variable to the video’s path. I’ll delete this line later after I have all of this working.

powershell_debugger_output

# Now to do what I came here to do. I wanna scale this video and then crop it. So I want to shrink it down so that its shortest side matches my thumbnail size. Then later I’ll crop away the rest so that it “fills” the thumbnail area. So with a tall video I’ll shrink it so its narrow width matches my thumbnail width, and then later I’ll chop-off the extra stuff at the top and bottom. To do this I’ll use the good old proportion formula I learned in high school… it’s probably the only thing I remember from math class.

scale_to_shortest_side

# I’m using math floor to chop off the decimals because pixels don’t need those. In PowerShell that looks like [math]::floor( $myNumber ). I also need to make the scaled size a multiple of 2 because FFmpeg throws up when it sees odd numbers. Hmm… how DO I check if a number is odd or even? Seems like that should be simple but I’m drawing a blank right now. I guess I’ll just look up an answer. Do a modulus of 2 and see if it’s zero? Yeah that’ll work.

even_or_odd

# Okay now I’ll just tell FFmpeg to scale the video and- Error!? Apparently PowerShell chokes on colons for some reason. I guess I gotta figure out how to escape this “magical” character.

powershell_colon_error

# Apparently you can escape a colon by putting ‘single quotes’ around it. I didn’t actually look that up. It was just a lucky guess. But who cares? Now I have a resized video!

powershell_colon_escaped

# And finally we’ll tell FFmpeg to crop around the center of the video, and tell PowerShell to delete the temporary file afterwards. I always check if a file exists before telling something to delete it. I almost lost an entire hard drive one time when I forgot to do this, and I had to spend a week un-deleting and recovering files. In PowerShell you can put this inside an IF statement [System.IO.File]::Exists( $myFilePath )

powershell_ffmpeg_cropping_video

# And we’re basically done. Now I’ll just delete that part at the top where I manually set the input variable to a file path, and let the incantation do it instead.

# And I can also modify my CMD script so I can drop multiple files and run PowerShell over and over for each one. You just add the top and bottom parts.

batch_loop_multiple_files

#Website

# Well… we’re done converting videos. Now I just need to add videos to the website. I’ll drag the full-size MP4 files into a markdown file…

video_in_markdown

# … Tell HUGO to treat them as shortcodes by manually loading the text in the file, adding curly braces around the video tags using string replacement, and then manually sending this text to the markdown converter using .RenderString

hugo_replace_xml_with_shortcode

# … Then tell HUGO to replace the video tags with new ones that display the thumbnail videos and wrap a link tag around them that points to the full-size video files. That way my website will display the cropped thumbnail videos, and my imageZoom JavaScript can display a full-size video when you click on it.

hugo_wrap_video_with_link

# And the result is that I have animated thumbnails and convenient full-screen videos, just like I do with pictures. Now I can recommend the really badass artists!

#A Simpler Script

# I later learned that FFmpeg is able to scale videos proportionally if you give it only a width or height, and set the missing side to a value of -1 or -2. And since my picture thumbnails have varying widths I ended up just doing this with the video thumbnails as well. This creates a much simpler script since FFmpeg is doing all the math.

thumbnails_with_varied_widths

# Download this code


# Read input:  The drag-and-dropped file
Param([string]$inFilePath='savdalhv01');



# Settings
$outHeight = 290;



# Where are we?
$scriptpath = $MyInvocation.MyCommand.Path;
$scriptFolder = Split-Path $scriptpath;



# Scale the video
$endAt = $inFilePath.lastIndexOf('\');
$droppedFolder = $inFilePath.Substring(0, $endAt);
$extAt = $inFilePath.lastIndexOf('.');
$fileName = $inFilePath.Substring($endAt+1, $extAt-$endAt-1 );
$outputPath = $droppedFolder + '\' + $fileName + '_t.mp4';
# Delete existing thumb file
if ( [System.IO.File]::Exists($outputPath) ) {
    Remove-Item $outputPath;
}
$result = & "$scriptFolder\ffmpeg.exe" -i "$inFilePath" -vf "scale=-2':'$outHeight" -an -pix_fmt yuv420p -preset medium -movflags +faststart "$outputPath";


# Save a poster image
$posterPath = $droppedFolder + '\' + $fileName + '_t.jpg';
if ( -not( [System.IO.File]::Exists($posterPath) ) ) {
    $result = & "$scriptFolder\ffmpeg.exe" -y -i "$outputPath" -ss 1 -vframes 1 -r 1 -f image2 "$posterPath";
}

#Why Bother?

# The reason I’m doing this may be obvious to some people, but there are also grown adults today who have never experienced the internet when it was slow, so they might wonder why I just went to all this trouble. Video files are huge. Lots of video files are freakin’ massive. Even the fastest internet connection will slow down if you load a bunch of giant files all at the same time. If displaying a full-size video is a 1.5 Megabyte download but displaying the thumbnail video only downloads 160 Kilobytes, then doing this with everything makes my entire website literally load 10 times faster!

whats_compression