PHP animated GIF with FFmpeg and PHP

The tools

To create the video preview, we need PHP >= 5.1 with the Imagick extension and FFmpeg. FFmpeg supports most of the existing video formats, has a command line interface, and is available under the LGPL license.

The code

I created two classes: one to extract some video frames, and one to join them back into an animated GIF. The frame extractor class implements the Iterator interface, so we can specify which frames we want, and then loop through the object to get them. The thumbnail joiner class uses Imagick to read the frames (either from disk or as binary content) and join them into a .gif file.

Thumbnail_Extractor

001.<?php
002./**
003.* This class uses ffmpeg to extract frames from a video file
004.*
005.* @author    Lorenzo Alberton <lorenzo@ibuildings.com>
006.* @copyright 2008-2009 Lorenzo Alberton
007.* @license   http://www.debian.org/misc/bsd.license ; BSD License (3 Clause)
008.*/
009.class Thumbnail_Extractor implements Iterator
010.{
011./**
012.* @var string path to ffmpeg binary
013.*/
014.protected $ffmpeg = 'ffmpeg';
015.
016./**
017.* @var string path to video
018.*/
019.protected $video;
020.
021./**
022.* @var array frames extracted from video
023.*/
024.protected $frames = array();
025.
026./**
027.* @var string thumbnail size
028.*/
029.protected $size = '';
030.
031./**
032.* @var integer video length
033.*/
034.protected $duration = 0;
035.
036./**
037.* @var boolean A switch to keep track of the end of the array
038.*/
039.private $valid = false;
040.
041./**
042.* Constructor
043.*
044.* @param string $video  path to source video
045.* @param array  $frames array of frames extracted [array('10%', '30%', '50%', '70%', '90%')]
046.* @param string $size   frame size [format: '320x260' or array(320,260)]
047.* @param string $ffmpeg path to ffmpeg binary
048.*/
049.public function __construct($video, $frames = array(), $size ='', $ffmpeg = 'ffmpeg') {
050.$this->video  = escapeshellarg($video);
051.$this->ffmpeg = escapeshellcmd($ffmpeg);
052.$this->duration = $this->_getDuration();
053.$this->_setSizeParam($size);
054.$this->_setFrames($frames);
055.}
056.
057./**
058.* Parse and set the frame size args to pass to ffmpeg
059.*
060.* @param string|array $size frame size [format: '320x260' or array(320,260)]
061.*
062.* @return void
063.*/
064.private function _setSizeParam($size) {
065.if (is_array($size) && 2 == count($size)) {
066.$this->size = '-s '.(int)array_shift($size).'x'.(int)array_shift($size);
067.} elseif (is_string($size) && preg_match('/^\d+x\d+$/',$size)) {
068.$this->size = '-s '.$size;
069.}
070.}
071.
072./**
073.* Init the frames array
074.*
075.* @param mixed $frames If integer, take a frame every X seconds;
076.*                      If array, take a frame for each array value,
077.*                      which can be an integer (seconds from start)
078.*                      or a string (percent)
079.*/
080.private function _setFrames($frames) {
081.if (empty($frames)) {
082.// throw exception?
083.return;
084.}
085.if (is_integer($frames)) {
086.// take a frame every X seconds
087.$interval = $frames;
088.$frames = array();
089.for ($pos=0; $pos < $this->duration; $pos += $interval) {
090.$frames[] = $pos;
091.}
092.}
093.if (!is_array($frames)) {
094.// throw exception?
095.return;
096.}
097.// init the frames array
098.foreach ($frames as $frame) {
099.$this->frames[$frame] = null;
100.}
101.}
102.
103./**
104.* Get the video duration
105.*
106.* @return integer
107.*/
108.private function _getDuration() {
109.$cmd = "{$this->ffmpeg} -i {$this->video} 2>&1";
110.if (preg_match('/Duration: ((\d+):(\d+):(\d+))/s', `$cmd`,$time)) {
111.return ($time[2] * 3600) + ($time[3] * 60) + $time[4];
112.}
113.return 0;
114.}
115.
116./**
117.* Get a video frame from a certain point in time
118.*
119.* @param integer $second seconds from start
120.*
121.* @return string binary image contents
122.*/
123.private function getFrame($second) {
124.$image = tempnam('/tmp', 'FRAME_');
125.$out = escapeshellarg($image);
126.$cmd = "{$this->ffmpeg} -i {$this->video} -deinterlace -an -ss {$second} -t 00:00:01 -r 1 -y {$this->size} -vcodec mjpeg -f mjpeg {$out} 2>&1";
127.`$cmd`;
128.$frame = file_get_contents($image);
129.@unlink($image);
130.return $frame;
131.}
132.
133./**
134.* Get the second
135.*
136.* @param mixed $second if integer, it's taken as absolute time in seconds
137.*                      from the start, otherwise it's supposed to be a percentual
138.*
139.* @return integer
140.*/
141.private function getSecond($second) {
142.if (false !== strpos($second, '%')) {
143.$percent = (int)str_replace('%', '', $second);
144.return (int)($percent * $this->duration / 100);
145.}
146.return (int)$second;
147.}
148.
149./**
150.* Return the array "pointer" to the first element
151.* PHP's reset() returns false if the array has no elements
152.*
153.* @return void
154.*/
155.public function rewind() {
156.$this->valid = (false !== reset($this->frames));
157.}
158.
159./**
160.* Return the current array element
161.*
162.* @return string binary image contents
163.*/
164.public function current() {
165.if (is_null(current($this->frames))) {
166.$k = $this->key();
167.$second = $this->getSecond($k);
168.$this->frames[$k] = $this->getFrame($second + 1);
169.}
170.return current($this->frames);
171.}
172.
173./**
174.* Return the key of the current array element
175.*
176.* @return mixed
177.*/
178.public function key() {
179.return key($this->frames);
180.}
181.
182./**
183.* Move forward by one
184.* PHP's next() returns false if there are no more elements
185.*
186.* @return void
187.*/
188.public function next() {
189.$this->valid = (false !== next($this->frames));
190.}
191.
192./**
193.* Is the current element valid?
194.*
195.* @return boolean
196.*/
197.public function valid() {
198.return $this->valid;
199.}
200.}

Thumbnail_Joiner

01.<?php
02./**
03.* This class uses Imagick to join some images into an animated gif
04.*
05.* @author    Lorenzo Alberton <lorenzo@ibuildings.com>
06.* @copyright 2008-2009 Lorenzo Alberton
07.* @license   http://www.debian.org/misc/bsd.license ; BSD License (3 Clause)
08.*/
09.classThumbnail_Joiner
10.{
11./**
12.* @var integer delay between images (in milliseconds)
13.*/
14.protected$delay= 50;
15.
16./**
17.* @var array
18.*/
19.protected$images= array();
20.
21./**
22.* @param integer $delay between images
23.*/
24.publicfunction__construct($delay= 50) {
25.$this->delay = $delay;
26.}
27.
28./**
29.* Load an image from file
30.*
31.* @param string $filename
32.*
33.* @return void
34.*/
35.publicfunctionaddFile($image) {
36.$this->images[] = file_get_contents($image);
37.}
38.
39./**
40.* Load an image
41.*
42.* @param string $image binary image data
43.*
44.* @return void
45.*/
46.publicfunctionadd($image) {
47.$this->images[] = $image;
48.}
49.
50./**
51.* Generate the animated gif
52.*
53.* @return string binary image data
54.*/
55.publicfunctionget() {
56.$animation= newImagick();
57.$animation->setFormat('gif');
58.foreach($this->images as$image) {
59.$frame= newImagick();
60.$frame->readImageBlob($image);
61.$animation->addImage($frame);
62.$animation->setImageDelay($this->delay);
63.}
64.return$animation->getImagesBlob();
65.}
66.
67./**
68.* Save the animated gif to file
69.*
70.* @param string $outfile output file name
71.*
72.* @return void
73.*/
74.publicfunctionsave($outfile) {
75.file_put_contents($outfile, $this->get());
76.}
77.}

Example usage

01.<?php
02.require 'Thumbnail_Extractor.php';
03.require 'Thumbnail_Joiner.php';
04.
05.// where ffmpeg is located, such as /usr/sbin/ffmpeg
06.$ffmpeg = '/usr/bin/ffmpeg';
07.
08.// the input video file
09.$video = dirname(__FILE__) . '/sample.avi';
10.
11.// extract one frame at 10% of the length, one at 30% and so on
12.$frames = array('10%', '30%', '50%', '70%', '90%');
13.
14.// set the delay between frames in the output GIF
15.$joiner = new Thumbnail_Joiner(50);
16.// loop through the extracted frames and add them to the joiner object
17.foreach (new Thumbnail_Extractor($video, $frames, '200x120', $ffmpeg)as $key => $frame) {
18.$joiner->add($frame);
19.}
20.$joiner->save(dirname(__FILE__).'/'.'out.gif');

As you can see, the usage is pretty easy and self-explanatory. You can select which frames to extract (or how to extract them) specifying: – an array of seconds – an array of percentages – an integer (interval in seconds between frames).
Also the output image dimensions are customisable.

Example

Source movie:

Source: www.archive.org.

Output

Extract frames from a movie and create a preview as animated GIF, using PHP, SPL, Imagick and FFmpeg - sample output

 

http://www.alberton.info/video_preview_as_animated_gif_with_ffmpeg_and_spl.html

 

Leave a comment