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.
class
Thumbnail_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.
public
function
__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.
public
function
addFile(
$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.
public
function
add(
$image
) {
47.
$this
->images[] =
$image
;
48.
}
49.
50.
/**
51.
* Generate the animated gif
52.
*
53.
* @return string binary image data
54.
*/
55.
public
function
get() {
56.
$animation
=
new
Imagick();
57.
$animation
->setFormat(
'gif'
);
58.
foreach
(
$this
->images
as
$image
) {
59.
$frame
=
new
Imagick();
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.
public
function
save(
$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
http://www.alberton.info/video_preview_as_animated_gif_with_ffmpeg_and_spl.html