Building Meaningful Video Thumbnails Using FFMPEG and PHP

This is an old post!

This post is over 2 years old. Solutions referenced in this article may no longer be valid. Please consider this when utilizing any information referenced here.

Working on doing some upgrades for one of my clients and I hit on an idea. He has a lot of videos available, but each one only has a static image as a thumbnail, taken at a set point in the video (by default; the owner or and admin can go in and recreate the thumbnail at a different time point if they want.) But what if, instead, we could create an animated GIF composed of several frames from the video?

From a user’s perspective, a single frame might not tell you a lot about a video. But ten frames taken over the course of the whole video can tell you a lot more about the video than the single frame would. How would we implement something like that?

What we actually did was generate two thumbnails, a static one and an animated one. On mobile devices, only the static one is displayed. On desktop devices, the static one is the default and, when you mouse over the thumbnail, we swap the static one for the animated one.

Turns out, this is rather easy to implement, if not very straightforward. My first thought was to increate the frame rate of the video and use PHP-FFMPEG’s built-in GIF generator, but this didn’t work. You can’t do it in a single step that I was able to find.

Doing this consists of three steps:

  1. Extracting the frames from given points in a video.
  2. Resizing each frame down to thumbnail size.
  3. Combining them all into an animated GIF.

Because this is modern PHP in the 21st century, we will be using composer and the following packages to save us a lot of time:

  • php-ffmpeg/php-ffmpeg
  • intervention/image
  • sybio/gif-creator

As a note, php-ffmpeg will require that you have ffmpeg installed on your system or in your container. gif-creator requires ImageMagick support in PHP. image will need either GD or ImageMagick as well.

The final function is something like this:

<?php

use Intervention\Image\ImageManagerStatic as Image;
use GifCreator\GifCreator;
use FFMpeg\FFprobe;
use FFMpeg\FFMpeg;
use FFMpeg\Coordinate\TimeCode;

public function build_video_thumbnail($video_path, $thumb_path) {

    // Create a temp directory for building.
    $temp = sys_get_temp_dir() . "/build";

    // Use FFProbe to get the duration of the video.
    $ffprobe = FFprobe::create();
    $duration = floor($ffprobe
        ->format($video_path)
        ->get('duration'));

    // If we couldn't get the direction or it was zero, exit.
    if (empty($duration)) {
        return;
    }

    // Create an FFMpeg instance and open the video.
    $ffmpeg = FFMpeg::create();
    $video = $ffmpeg->open($video_path);

    // This array holds our "points" that we are going to extract from the
    // video. Each one represents a percentage into the video we will go in
    // extracitng a frame. 0%, 10%, 20% ..
    $points = range(0, 100, 10);

    // This will hold our finished frames.
    $frames = [];

    foreach ($points as $point) {

        // Point is a percent, so get the actual seconds into the video.
        $time_secs = floor($duration * ($point / 100));

        // Created a var to hold the point filename.
        $point_file = "$temp/$point.jpg";

        // Extract the frame.
        $frame = $video->frame(TimeCode::fromSeconds($time_secs));
        $frame->save($point_file);

        // If the frame was successfully extracted, resize it down to
        // 320x200 keeping aspect ratio.
        if (file_exists($point_file)) {
            $img = Image::make($point_file)->resize(300, 200, function ($constraint) {
                $constraint->aspectRatio();
                $constraint->upsize();
            });

            $img->save($point_file, 40);
            $img->destroy();
        }

        // If the resize was successful, add it to the frames array.
        if (file_exists($point_file)) {
            $frames[] = $point_file;
        }
    }

    // If we have frames that were successfully extracted.
    if (!empty($frames)) {

        // We show each frame for 100 ms.
        $durations = array_fill(0, count($frames), 100);

        // Create a new GIF and save it.
        $gc = new GifCreator();
        $gc->create($frames, $durations, 0);
        file_put_contents($thumb_path, $gc->getGif());

        // Remove all the temporary frames.
        foreach ($frames as $file) {
            unlink($file);
        }
    }
}

As you can see from the above, we’re taking a video and extracting a frame every 10% of the video until we reach the end. We shrink them down and mash them together into an animated GIF and store that on the filesystem, removing the intermediate files at the end.

As this takes some time to run, it would be a good idea to run this code using some type of out-of-band processing. Laravel’s Queue’s are a good example that I have used before, as are Gearman, Redis, Beanstalk, etc. Basically, don’t run this in mod_php or php-fpm because you want those tasks to complete quickly.

Comments (0)

Interested in why you can't leave comments on my blog? Read the article about why comments are uniquely terrible and need to die. If you are still interested in commenting on this article, feel free to reach out to me directly and/or share it on social media.

Contact Me
Share It
PHP
Phinx is a really cool database migration package that allows you to write changes to your database as code. It keeps track of which changes have been applied and allows you the option of rolling back if you hit an issue. All the documentation on Phinx describes a typical setup where you would run the phinx command to do your migrations. And that is all fine and good in most projects. But what happens if you are integrating Phinx into an existing project that already has a lot of the usual scaffolding in place?
Read More
PHP
Let’s say you have a Laravel application that does some data processing, and you want to monitor a directory for incoming changes, that you can then process using queued jobs. There are a couple of ways you could do something like this. You could scan those directories on a schedule using a cronjob. It’s doable. But what happens if you want to monitor a few thousand directories for changes? You can use tools like incron. Also doable, but another dependency. But what if I told you you could do it all with PHP. And within Laravel, no less?
Read More
Release Announcements
Launched two new pieces of open source code in the last couple of months. PlayerControls PlayerControls is a macOS Cocoa framework that creates a View containing playback controls for media like videos or sounds. It is written in pure Swift 4 and has no dependencies. SearchParser SearchParser is a parser that converts a freeform query into an intermediate object, that can then be converted to query many backends (SQL, ElasticSearch, etc). It includes translators for SQL (using PDO) and Laravel Eloquent ORM. It supports a faceted language search as commonly found on many sites across the web. It is written in modern PHP. Both are licensed under the MIT license. Go check them out on Github.
Read More