Bash Script – Get Random Screenshots from Multiple Video Files

📅 August 1, 2019
Do you have a large set of video files and wish to get a random screenshot from each?

Here is a small Bash script that will grab a single screenshot at a random time point from each video file and store the screenshot as an individual JPEG image.

 

The Goal

I encountered a situation where it would be handy to have a screenshot collection of all video files in a given directory. Each screenshot would need to be a JPEG image having the same name as the full name of the video file (including the file extension), and all image files would need to be stored in the same directory, a separate screenshot directory.

The Process

As good practice, let’s think about how the script will perform the steps before writing any code. This is a Bash script. To achieve this, the script will need to,

  1. Search through all directories recursively from a single parent directory
  2. Filter by given file extensions
  3. For each video file found,
    1. Get a screenshot at a random time point within the video file
    2. Convert the screenshot into a smaller thumbnail
    3. Save the screenshot to a dedicated screenshot directory apart from the video files

The Script

I named this script makethumbs.sh and gave it execute permissions on the owner.

#!/bin/bash

# Generate random thumbnails from 

function makeThumb()
{
    DEST='~/screenshots'
    fullpath="$1"
    fname=$(basename "$1")

    # Calculate a random time in seconds to take screenshot
    PERCENT=20
    duration=$(( $(mediainfo --Inform="General;%Duration%" "${fullpath}") / 1000 ))
    min_time=$(( $duration * $PERCENT / 100))
    max_time=$(( $duration - $min_time ))
    screenshot_time=$(date -u --date=@$(shuf -i ${min_time}-${max_time} -n 1) +'%T')

    echo "$fname: $duration frames, min: $min_time, max: $max_time, ss: $screenshot_time"
    ffmpeg -loglevel panic -y -ss $screenshot_time -i "$fullpath" -vframes 1 -q:v 2 "pic.jpg"

    # Resize while maintaining aspect ratio
    convert -resize 640x pic.jpg -quality 90 "${DEST}/${fname}.jpg"

    # Clean up
    rm pic.jpg
}

SRC='/media/videos'

export -f makeThumb
find "$SRC" -type f -iregex "^.*\.\(mkv\|mp4\|flv\|avi\|mpg\)$" -exec bash -c 'makeThumb "{}"' \;

How It Works

There are many ways to achieve the same result, but I wanted to use find to avoid the use of a loop and perform recursive searching in a single statement. Also, there are a number of interesting points about this script that make it worth looking into for future ideas and reference.

The idea is to allow find to find each video file of a matching file extension, and then execute a function, named makeThumb(), on it. The makeThumb() function handles the details of generating a random time and taking the screenshot.

Prerequisites

This script depends upon ffmpeg and Imagemagick’s convert, so be sure that ffmpeg and Imagemagick are installed.

Outside the Function

SRC='/media/videos'

The source directory containing all video files. Contains multiple subdirectories of other video files. Nested deeply.

export -f makeThumb

Since this script uses a function, we need to make this function visible to subprocesses. find uses the -exec option that runs the function in a forked Bash subprocess. Exporting the function makes it available for the subprocess to use. If we omit the export, the function will not run because the subprocess cannot access it. This step is very important.

find "$SRC" -type f -iregex "^.*\.\(mkv\|mp4\|flv\|avi\|mpg\)$" -exec bash -c 'makeThumb "{}"' \;

Much of this is the typical find syntax.

  • “$SRC” – Parent directory to start searching from. Enclose in double quotes to handle names containing spaces.
  • -type f – Look for files only
  • -iregex “^.*\.\(mkv\|mp4\|flv\|avi\|mpg\)$” – This is a case-insensitive regular expression that says, “Find files that end with following file extensions.” Think of the \((mkv\|mp4\|flv\|avi\|mpg\) expression as an OR statement that matches any one of the text separated by the vertical bar (|). All other file extensions will be skipped. If you want to search for additional file formats, such as webm, included them in the list. Remember to escape the parentheses as \(\) to avoid shell interpretation, and end it with the dollar sign ($) to match the end of the string.
  • -exec bash -c ‘makeThumb “{}”‘ \; – This is the magic behind find. At this point, we have found a valid video file, and this tells us what to do to it. The full path to the video file is represented by the curly braces ({ }) also enclosed within double quotes. We call the name of the makeThumb function without parentheses. Use makeThumb, not makeThumb(). The full path of the currently found file is passed to the function as argument the first argument (represented as $1 within the function).

The Function

DEST='~/screenshots'
fullpath="$1"
fname=$(basename "$1")
  • DEST=’~/screenshots’ – Where to store the screenshot.
  • fullpath=”$1″ – Not necessary, but I find a that variable named $fullpath is more meaningful than $1 since it makes it clear what the purpose of this variable is for. Enclose in double quotes to handle spaces in paths.
  • fname=$(basename “$1”) – Store only the filename, not the full path. Remember, the function receives the full path including the filename with its extension, and there will be points in the script where we want only the filename. This makes it simpler.

 

PERCENT=20
duration=$(($(mediainfo --Inform="General;%Duration%" "${fullpath}" ) / 1000 ))
  • PERCENT=20 – Sets a percentage of time into the video to start the minimum time at which to search. Some video files have black frames or introductions at the beginning, and those would not make suitable thumbnails. Changing the value here makes it easier to grab a screenshot from more meaningful parts of the video.
  • duration=$(($(mediainfo –Inform=”General;%Duration%” “${fullpath}” ) / 1000 )) – We need to know how long the video lasts. Video lengths vary wildly, so a fixed frame number or time is out of the question. The result is stored in the appropriately named duration variable. The key behind this statement is the mediainfo command, which lists a large amount of information for a video file. We only need the duration so we use –Inform as shown. grep will not work here as simply because multiple Duration fields exist. This grabs the duration we need. The result is divided by 1000 to get the video’s duration in seconds. Bash only handles integers, so fractional parts are discarded, which we do not need anyway.

 

min_time=$(( $duration * $PERCENT / 100))
max_time=$(( $duration - $min_time ))

This specifies a range from which to get a screenshot. Doing this helps eliminate unimportant frames at the beginning and at the end of the video file. Results are in seconds. The video files in question have variable durations, so this allows the grab range to scale appropriately.

 

screenshot_time=$(date -u --date=@$(shuf -i ${min_time}-${max_time} -n 1) +'%T')

This is the time in seconds at which to get a full frame screenshot from the video. We are doing a number of things with here.

  • shuf -i ${min_time}-${max_time} -n 1 – Generates a random set of values from min_time to max_time and returns only the first value. All values are seconds, and this gets one at random. Try running shuf -i 10-20 repeatedly to see how this works. Then, add -n 1 to see the result. The number returned is the time in seconds at which to get a screenshot.
  • date -u –date=@$(shuf -i ${min_time}-${max_time} -n 1) +’%T’ – ffmpeg wants a time in the HH:MM:SS format to get a screenshot. We cannot use a second value, like 1200, directly. We must convert it into something like 00:15:23. This is what the date command is doing here. The result is stored in the variable screenshot_time.

 

echo "$fname: $duration frames, min: $min_time, max: $max_time, ss: $screenshot_time"

Just a header to show the name of the video file and the generated values. It gives us something to watch while the script runs.

 

ffmpeg -loglevel panic -y -ss $screenshot_time -i "$fullpath" -vframes 1 -q:v 2 "pic.jpg"

We did all of that so we could do this: get a full frame screenshot from the video file. ffmpeg is quick and efficient for this task from a command line.

  • -loglevel panic – Do not prompt to overwrite a file. The prompt can become annoying, so let’s disable it.
  • -vframes 1 -q:v 2 “pic.jpg” – Get only one frame at the given time point (specified by $screenshot_time), and save it as a JPEG image with a quality setting of 2 (ffmpeg’s setting value) stored in the file pic.jpg.

 

convert -resize 640x pic.jpg -quality 90 "${DEST}/${fname}.jpg"

This runs Imagemagick’s convert to change the full frame screenshot into a thumbnail whose width is always 640 pixels. The aspect ratio is maintained by default, so heights might vary. If you want different dimensions, then change convert’s options here.

The output filename is stored as filename.ext.jpg. For example:

  • Video filename: videofile.mp4
  • Thumbnail filename: videofile.mp4.jpg

The file extension is preserved in the filename, not removed. While this feature was necessary for this task, you can remove the file extension if you wish by using something like basename, ${filename##*.}, or ${filename%.*} depending upon your needs. There are multiple ways to remove the file extension.

 

rm pic.jpg

Deletes the temporary pic.jpg file. Not necessary because it gets overwritten each time the function is called, but it automatically removes the last pic.jpg and avoids the need to delete it manually.

Known Issue

There is one issue with this script that needs correction: the first video file found in a directory will have a random time, but after a random time is generated for the second video file, all other video files in the same directory will share that time.

This could be corrected, but the results were so good with the screenshots generated, that I did not bother fixing it.

 

Conclusion

This script does everything I needed to do and does it well. Are there other ways to achieve the same result? Yes, definitely. Python 3 could be used for cleaner code. Bash loops could be used in place of find. However, there were a number of interesting elements comprising this script, that it makes for a good tutorial/reference.

Experimentation is key, so be sure to use test directories when writing a script like this that generates and deletes files in order to avoid the risk of removing important files. When satisfied with the script’s operation, run it on the main source files.

But most of all, have fun!

, ,

  1. Leave a comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: