How to Generate PNG Screenshots Using FFMPEG

4 minute read

After setting up my Server and Home Entertainment Center (I should write up a post on that...) I needed an easy way to automatically generate screenshots of my movies so that my hacked Apple TV could preview the movie, just like iTunes does. My Apple TV uses the ATVFiles plugin and all that is necessary to display the screenshot is to have a file named exactly like the video file, but with an image extension instead. So I set out to write a script that would scan my folders, looking for supported vids that did not have a screenshot, generate one, and rename it.
I use FFMPEG to generate the screenshot at the bash prompt:

        
bash-$ ffmpeg -ss 00:10:20 -t 1 -s 400x300 -i <INPUT_FILE> -f mjpeg <OUTPUT_FILE>
Input file is the movie to preview and output file is the name of the new screenshot. -ss and the time argument that follows tells ffmpeg at what point you want the screenshot snapped. In this example, ffmpeg will take a shot at the 10 minute and 20 second point. -t tells ffmpeg that you want only 1 shot, -s is the size of the pic, and -f tells it to make a photo (but not limited to jpg).
For example, to generate a png screenshot for Batman.avi at the 1 hour, 12 minute, and 30 second point:

        
bash-$ ffmpeg -ss 01:12:30 -t 1 -s 400x300 -i Batman.avi -f mjpeg Batman.png
Now that I have the command, I just wrap it in a perl script that finds all preview-less movies, gets the length, randomly chooses a time in that length, snaps a photo at that time and saves it under the right name.
generate_pics.pl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#! /usr/bin/perl

use strict;
use File::Find;
use File::Basename;

my $root = @ARGV[0] || '/Content';

find( \&wanted, $root );
sub wanted {
    return unless -f && /avi$|mov$|mp4$|mkv$|mpeg$|mpg$/i;
    my ($name, $path, $ext) =
      fileparse($_, qr/\.avi|\.mov|\.mp4|\.mkv|\.wmv|\.mpeg|\.mpg/i);
    my $new_pic = $File::Find::dir . "/" . $name . ".png";

    if (!( -e $new_pic )) {

        my ($hrs_range, $min_range, $sec_range);
        my $ffmpeg = `ffmpeg -i '$File::Find::dir/$_' 2>&1`;
        if ($ffmpeg =~ /Duration: (\d{2}):(\d{2}):(\d{2})/) {
            ($hrs_range, $min_range, $sec_range) = ($1, $2, $3);
        }
        my $hrs = int(rand($hrs_range));
        my $min = ($hrs == $hrs_range) ? int(rand($min_range)) : int(rand(60));
        my $sec = ($hrs == $hrs_range && $min == $min_range) ?
          int(rand($sec_range)) : int(rand(60));

        print " ---> Creating Cover: $new_pic\n";
        `ffmpeg -ss $hrs:$min:$sec -t 1 -s 400x300 -i "$_" -f mjpeg "$new_pic"`;

        # if it didnt work, try mplayer
        if ($! || !( -e $new_pic )) {
            #print " ---> ffmpeg failed, using mplayer\n";
            my $secs = ($hrs == 0) ?
              ($min * 60) + $sec : (($hrs * 3600) + ($min * 60) + $sec);
            `mplayer -really-quiet -ss $secs -frames 1 -vf scale=400:-2 -vo png:z=1 "$_"`;
            `mv 00000001.png "$new_pic"`;
        }
    }

}
Perl always looks complicated but isn't too hard to explain:
  • Lines 1-5: Use the right Perl Modules to make our lives easier
  • Line 7: Accept an argument that will be the base dir for searching. Default on my machine is /Content
  • Lines 9-10: Use the Perl find module and define the "wanted" function used to determine if a file should be processed or not.
  • Line 11: Return if the file is not a movie file with the correct extension.
  • Lines 12-14: Split the filename into its name and its extension, also get the directory path to the file. Then create a new string that represents the new pic based on those parameters.
  • Line 16: Only process the movie if it doesn't already have a preview pic.
  • Lines 18-26: If you call ffmpeg by itself with a movie file as the only argument, it will print out info about the movie including the length. I parse this output and generate a random time within this movie length to generate the screenshot.
  • Line 27: Make the ffmpeg call using the random times, $_ (which is the input file found by "wanted") and the output filename that we made on line 13
  • Lines 32-37: Some codecs aren't read by ffmpeg so if there is an error in the ffmpeg call... we try using a method with mplayer instead. This hardly ever happens so feel free to omit this section, but I leave it in just in case.
So I have this script called as a cronjob and will generate screenshots of the videos I put on my server automatically. If it generates a blurry picture, or a picture I am unsatisfied with, I simply delete it and a new random pic will be generated.

Was this page helpful for you? Buy me a slice of 🍕 to say thanks!

Updated:

Comments