diff --git a/README.md b/README.md index 825afeb..a7000cf 100644 --- a/README.md +++ b/README.md @@ -403,6 +403,22 @@ $frame->save('target.jpg'); This method has a second optional boolean parameter. Set it to true to get accurate images ; it takes more time to execute. +#### Gif + +A gif is an animated image extracted from a sequence of the video ;. + +You can save gif files using the `FFMpeg\Media\Gif::save` method. + +```php +$video = $ffmpeg->open( '/path/to/video' ); +$video + ->gif(FFMpeg\Coordinate\TimeCode::fromSeconds(2), new FFMpeg\Coordinate\Dimension(640, 480), 3) + ->save($new_file); +``` + +This method has a third optional boolean parameter, which is the duration of the animation. +If you don't set it, you will get a fixed gif image. + #### Formats A format implements `FFMpeg\Format\FormatInterface`. To save to a video file, diff --git a/src/FFMpeg/Filters/Gif/GifFilterInterface.php b/src/FFMpeg/Filters/Gif/GifFilterInterface.php new file mode 100644 index 0000000..141bbad --- /dev/null +++ b/src/FFMpeg/Filters/Gif/GifFilterInterface.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FFMpeg\Filters\Gif; + +use FFMpeg\Filters\FilterInterface; +use FFMpeg\Media\Gif; + +interface GifFilterInterface extends FilterInterface +{ + public function apply(Gif $gif); +} diff --git a/src/FFMpeg/Filters/Gif/GifFilters.php b/src/FFMpeg/Filters/Gif/GifFilters.php new file mode 100644 index 0000000..9834c91 --- /dev/null +++ b/src/FFMpeg/Filters/Gif/GifFilters.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FFMpeg\Filters\Gif; + +use FFMpeg\Media\Gif; + +class GifFilters +{ + private $gif; + + public function __construct(Gif $gif) + { + $this->gif = $gif; + } +} diff --git a/src/FFMpeg/Media/Gif.php b/src/FFMpeg/Media/Gif.php new file mode 100644 index 0000000..67c640b --- /dev/null +++ b/src/FFMpeg/Media/Gif.php @@ -0,0 +1,137 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FFMpeg\Media; + +use Alchemy\BinaryDriver\Exception\ExecutionFailureException; +use FFMpeg\Filters\Gif\GifFilterInterface; +use FFMpeg\Filters\Gif\GifFilters; +use FFMpeg\Driver\FFMpegDriver; +use FFMpeg\FFProbe; +use FFMpeg\Exception\RuntimeException; +use FFMpeg\Coordinate\TimeCode; +use FFMpeg\Coordinate\Dimension; + +class Gif extends AbstractMediaType +{ + /** @var TimeCode */ + private $timecode; + /** @var Dimension */ + private $dimension; + /** @var integer */ + private $duration; + /** @var Video */ + private $video; + + public function __construct(Video $video, FFMpegDriver $driver, FFProbe $ffprobe, TimeCode $timecode, Dimension $dimension, $duration = null) + { + parent::__construct($video->getPathfile(), $driver, $ffprobe); + $this->timecode = $timecode; + $this->dimension = $dimension; + $this->duration = $duration; + $this->video = $video; + } + + /** + * Returns the video related to the gif. + * + * @return Video + */ + public function getVideo() + { + return $this->video; + } + + /** + * {@inheritdoc} + * + * @return GifFilters + */ + public function filters() + { + return new GifFilters($this); + } + + /** + * {@inheritdoc} + * + * @return Gif + */ + public function addFilter(GifFilterInterface $filter) + { + $this->filters->add($filter); + + return $this; + } + + /** + * @return TimeCode + */ + public function getTimeCode() + { + return $this->timecode; + } + + /** + * @return Dimension + */ + public function getDimension() + { + return $this->dimension; + } + + /** + * Saves the gif in the given filename. + * + * @param string $pathfile + * + * @return Gif + * + * @throws RuntimeException + */ + public function save($pathfile) + { + /** + * @see http://ffmpeg.org/ffmpeg.html#Main-options + */ + $commands = array( + '-ss', (string)$this->timecode + ); + + if(null !== $this->duration) { + $commands[] = '-t'; + $commands[] = (string)$this->duration; + } + + $commands[] = '-i'; + $commands[] = $this->pathfile; + $commands[] = '-vf'; + $commands[] = 'scale=' . $this->dimension->getWidth() . ':-1'; + $commands[] = '-gifflags'; + $commands[] = '+transdiff'; + $commands[] = '-y'; + + foreach ($this->filters as $filter) { + $commands = array_merge($commands, $filter->apply($this)); + } + + $commands = array_merge($commands, array($pathfile)); + + try { + $this->driver->command($commands); + } catch (ExecutionFailureException $e) { + $this->cleanupTemporaryFile($pathfile); + throw new RuntimeException('Unable to save gif', $e->getCode(), $e); + } + + return $this; + } +} diff --git a/src/FFMpeg/Media/Video.php b/src/FFMpeg/Media/Video.php index 9200f49..bafe2bd 100644 --- a/src/FFMpeg/Media/Video.php +++ b/src/FFMpeg/Media/Video.php @@ -13,6 +13,7 @@ namespace FFMpeg\Media; use Alchemy\BinaryDriver\Exception\ExecutionFailureException; use FFMpeg\Coordinate\TimeCode; +use FFMpeg\Coordinate\Dimension; use FFMpeg\Filters\Audio\SimpleFilter; use FFMpeg\Exception\InvalidArgumentException; use FFMpeg\Exception\RuntimeException; @@ -181,4 +182,17 @@ class Video extends Audio { return new Frame($this, $this->driver, $this->ffprobe, $at); } + + /** + * Extracts a gif from a sequence of the video. + * + * @param TimeCode $at + * @param Dimension $dimension + * @param integer $duration + * @return Gif + */ + public function gif(TimeCode $at, Dimension $dimension, $duration = null) + { + return new Gif($this, $this->driver, $this->ffprobe, $at, $dimension, $duration); + } } diff --git a/tests/Unit/Media/GifTest.php b/tests/Unit/Media/GifTest.php new file mode 100644 index 0000000..4925953 --- /dev/null +++ b/tests/Unit/Media/GifTest.php @@ -0,0 +1,116 @@ +getFFMpegDriverMock(); + $ffprobe = $this->getFFProbeMock(); + $timecode = $this->getTimeCodeMock(); + $dimension = $this->getDimensionMock(); + + $gif = new Gif($this->getVideoMock(__FILE__), $driver, $ffprobe, $timecode, $dimension); + $this->assertSame($timecode, $gif->getTimeCode()); + } + + public function testGetDimension() + { + $driver = $this->getFFMpegDriverMock(); + $ffprobe = $this->getFFProbeMock(); + $timecode = $this->getTimeCodeMock(); + $dimension = $this->getDimensionMock(); + + $gif = new Gif($this->getVideoMock(__FILE__), $driver, $ffprobe, $timecode, $dimension); + $this->assertSame($dimension, $gif->getDimension()); + } + + public function testFiltersReturnFilters() + { + $driver = $this->getFFMpegDriverMock(); + $ffprobe = $this->getFFProbeMock(); + $timecode = $this->getTimeCodeMock(); + $dimension = $this->getDimensionMock(); + + $gif = new Gif($this->getVideoMock(__FILE__), $driver, $ffprobe, $timecode, $dimension); + $this->assertInstanceOf('FFMpeg\Filters\Gif\GifFilters', $gif->filters()); + } + + public function testAddFiltersAddsAFilter() + { + $driver = $this->getFFMpegDriverMock(); + $ffprobe = $this->getFFProbeMock(); + $timecode = $this->getTimeCodeMock(); + $dimension = $this->getDimensionMock(); + + $filters = $this->getMockBuilder('FFMpeg\Filters\FiltersCollection') + ->disableOriginalConstructor() + ->getMock(); + + $filter = $this->getMock('FFMpeg\Filters\Gif\GifFilterInterface'); + + $filters->expects($this->once()) + ->method('add') + ->with($filter); + + $gif = new Gif($this->getVideoMock(__FILE__), $driver, $ffprobe, $timecode, $dimension); + $gif->setFiltersCollection($filters); + $gif->addFilter($filter); + } + + /** + * @dataProvider provideSaveOptions + */ + public function testSave($dimension, $duration, $commands) + { + $driver = $this->getFFMpegDriverMock(); + $ffprobe = $this->getFFProbeMock(); + $timecode = $this->getTimeCodeMock(); + + $timecode->expects($this->once()) + ->method('__toString') + ->will($this->returnValue('timecode')); + + $pathfile = '/target/destination'; + + array_push($commands, $pathfile); + + $driver->expects($this->once()) + ->method('command') + ->with($commands); + + $gif = new Gif($this->getVideoMock(__FILE__), $driver, $ffprobe, $timecode, $dimension, $duration); + $this->assertSame($gif, $gif->save($pathfile)); + } + + public function provideSaveOptions() + { + return array( + array( + new Dimension(320, 240), 3, + array( + '-ss', 'timecode', + '-t', '3', + '-i', __FILE__, + '-vf', + 'scale=320:-1', '-gifflags', + '+transdiff', '-y' + ), + ), + array( + new Dimension(320, 240), null, + array( + '-ss', 'timecode', + '-i', __FILE__, + '-vf', + 'scale=320:-1', '-gifflags', + '+transdiff', '-y' + ) + ), + ); + } +}