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.<?php002./**003.* This class uses ffmpeg to extract frames from a video file004.*005.* @author Lorenzo Alberton <lorenzo@ibuildings.com>006.* @copyright 2008-2009 Lorenzo Alberton007.* @license http://www.debian.org/misc/bsd.license ; BSD License (3 Clause)008.*/009.classThumbnail_ExtractorimplementsIterator010.{011./**012.* @var string path to ffmpeg binary013.*/014.protected$ffmpeg='ffmpeg';015.016./**017.* @var string path to video018.*/019.protected$video;020.021./**022.* @var array frames extracted from video023.*/024.protected$frames=array();025.026./**027.* @var string thumbnail size028.*/029.protected$size='';030.031./**032.* @var integer video length033.*/034.protected$duration= 0;035.036./**037.* @var boolean A switch to keep track of the end of the array038.*/039.private$valid= false;040.041./**042.* Constructor043.*044.* @param string $video path to source video045.* @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 binary048.*/049.publicfunction__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 ffmpeg059.*060.* @param string|array $size frame size [format: '320x260' or array(320,260)]061.*062.* @return void063.*/064.privatefunction_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 array074.*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.privatefunction_setFrames($frames) {081.if(empty($frames)) {082.// throw exception?083.return;084.}085.if(is_integer($frames)) {086.// take a frame every X seconds087.$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 array098.foreach($framesas$frame) {099.$this->frames[$frame] = null;100.}101.}102.103./**104.* Get the video duration105.*106.* @return integer107.*/108.privatefunction_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.return0;114.}115.116./**117.* Get a video frame from a certain point in time118.*119.* @param integer $second seconds from start120.*121.* @return string binary image contents122.*/123.privatefunctiongetFrame($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 second135.*136.* @param mixed $second if integer, it's taken as absolute time in seconds137.* from the start, otherwise it's supposed to be a percentual138.*139.* @return integer140.*/141.privatefunctiongetSecond($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 element151.* PHP's reset() returns false if the array has no elements152.*153.* @return void154.*/155.publicfunctionrewind() {156.$this->valid = (false !== reset($this->frames));157.}158.159./**160.* Return the current array element161.*162.* @return string binary image contents163.*/164.publicfunctioncurrent() {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.returncurrent($this->frames);171.}172.173./**174.* Return the key of the current array element175.*176.* @return mixed177.*/178.publicfunctionkey() {179.returnkey($this->frames);180.}181.182./**183.* Move forward by one184.* PHP's next() returns false if there are no more elements185.*186.* @return void187.*/188.publicfunctionnext() {189.$this->valid = (false !== next($this->frames));190.}191.192./**193.* Is the current element valid?194.*195.* @return boolean196.*/197.publicfunctionvalid() {198.return$this->valid;199.}200.}
Thumbnail_Joiner
01.<?php02./**03.* This class uses Imagick to join some images into an animated gif04.*05.* @author Lorenzo Alberton <lorenzo@ibuildings.com>06.* @copyright 2008-2009 Lorenzo Alberton07.* @license http://www.debian.org/misc/bsd.license ; BSD License (3 Clause)08.*/09.classThumbnail_Joiner10.{11./**12.* @var integer delay between images (in milliseconds)13.*/14.protected$delay= 50;15.16./**17.* @var array18.*/19.protected$images=array();20.21./**22.* @param integer $delay between images23.*/24.publicfunction__construct($delay= 50) {25.$this->delay =$delay;26.}27.28./**29.* Load an image from file30.*31.* @param string $filename32.*33.* @return void34.*/35.publicfunctionaddFile($image) {36.$this->images[] =file_get_contents($image);37.}38.39./**40.* Load an image41.*42.* @param string $image binary image data43.*44.* @return void45.*/46.publicfunctionadd($image) {47.$this->images[] =$image;48.}49.50./**51.* Generate the animated gif52.*53.* @return string binary image data54.*/55.publicfunctionget() {56.$animation=newImagick();57.$animation->setFormat('gif');58.foreach($this->imagesas$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 file69.*70.* @param string $outfile output file name71.*72.* @return void73.*/74.publicfunctionsave($outfile) {75.file_put_contents($outfile,$this->get());76.}77.}
Example usage
01.<?php02.require'Thumbnail_Extractor.php';03.require'Thumbnail_Joiner.php';04.05.// where ffmpeg is located, such as /usr/sbin/ffmpeg06.$ffmpeg='/usr/bin/ffmpeg';07.08.// the input video file09.$video= dirname(__FILE__) .'/sample.avi';10.11.// extract one frame at 10% of the length, one at 30% and so on12.$frames=array('10%','30%','50%','70%','90%');13.14.// set the delay between frames in the output GIF15.$joiner=newThumbnail_Joiner(50);16.// loop through the extracted frames and add them to the joiner object17.foreach(newThumbnail_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

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