diff --git a/README.md b/README.md index 5232627..825afeb 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,18 @@ $frame = $video->frame(FFMpeg\Coordinate\TimeCode::fromSeconds(42)); $frame->save('image.jpg'); ``` +If you want to extract multiple images from your video, you can use the following filter: + +```php +$video + ->filters() + ->extractMultipleFrames(FFMpeg\Filters\Video\ExtractMultipleFramesFilter::FRAMERATE_EVERY_10SEC, . '/path/to/destination/folder/') + ->synchronize(); + +$video + ->save(new FFMpeg\Format\Video\X264(), '/path/to/new/file'); +``` + ##### Generate a waveform You can generate a waveform of an audio file using the `FFMpeg\Media\Audio::waveform` diff --git a/src/FFMpeg/Filters/Video/ExtractMultipleFramesFilter.php b/src/FFMpeg/Filters/Video/ExtractMultipleFramesFilter.php new file mode 100644 index 0000000..18b8225 --- /dev/null +++ b/src/FFMpeg/Filters/Video/ExtractMultipleFramesFilter.php @@ -0,0 +1,127 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FFMpeg\Filters\Video; + +use FFMpeg\FFProbe; +use FFMpeg\Exception\InvalidArgumentException; +use FFMpeg\Media\Video; +use FFMpeg\Format\VideoInterface; + +class ExtractMultipleFramesFilter implements VideoFilterInterface +{ + /** will extract a frame every second */ + const FRAMERATE_EVERY_SEC = '1/1'; + /** will extract a frame every 2 seconds */ + const FRAMERATE_EVERY_2SEC = '1/2'; + /** will extract a frame every 5 seconds */ + const FRAMERATE_EVERY_5SEC = '1/5'; + /** will extract a frame every 10 seconds */ + const FRAMERATE_EVERY_10SEC = '1/10'; + /** will extract a frame every 30 seconds */ + const FRAMERATE_EVERY_30SEC = '1/30'; + /** will extract a frame every minute */ + const FRAMERATE_EVERY_60SEC = '1/60'; + + /** @var integer */ + private $priority; + private $frameRate; + private $destinationFolder; + + public function __construct($frameRate = self::FRAMERATE_EVERY_SEC, $destinationFolder = __DIR__, $priority = 0) + { + $this->priority = $priority; + $this->frameRate = $frameRate; + + // Make sure that the destination folder has a trailing slash + if(strcmp( substr($destinationFolder, -1), "/") != 0) + $destinationFolder .= "/"; + + // Set the destination folder + $this->destinationFolder = $destinationFolder; + } + + /** + * {@inheritdoc} + */ + public function getPriority() + { + return $this->priority; + } + + /** + * {@inheritdoc} + */ + public function getFrameRate() + { + return $this->frameRate; + } + + /** + * {@inheritdoc} + */ + public function getDestinationFolder() + { + return $this->destinationFolder; + } + + /** + * {@inheritdoc} + */ + public function apply(Video $video, VideoInterface $format) + { + $commands = array(); + + try { + // Get the duration of the video + foreach ($video->getStreams()->videos() as $stream) { + if ($stream->has('duration')) { + $duration = $stream->get('duration'); + } + } + + // Get the number of frames per second we have to extract. + if(preg_match('/(\d+)(?:\s*)([\+\-\*\/])(?:\s*)(\d+)/', $this->frameRate, $matches) !== FALSE){ + $operator = $matches[2]; + + switch($operator){ + case '/': + $nbFramesPerSecond = $matches[1] / $matches[3]; + break; + + default: + throw new InvalidArgumentException('The frame rate is not a proper division: ' . $this->frameRate); + break; + } + } + + // Set the number of digits to use in the exported filenames + $nbImages = ceil( $duration * $nbFramesPerSecond ); + + if($nbImages < 100) + $nbDigitsInFileNames = "02"; + elseif($nbImages < 1000) + $nbDigitsInFileNames = "03"; + else + $nbDigitsInFileNames = "06"; + + // Set the parameters + $commands[] = '-vf'; + $commands[] = 'fps=' . $this->frameRate; + $commands[] = $this->destinationFolder . 'frame-%'.$nbDigitsInFileNames.'d.jpg'; + } + catch (RuntimeException $e) { + throw new RuntimeException('An error occured while extracting the frames: ' . $e->getMessage() . '. The code: ' . $e->getCode()); + } + + return $commands; + } +} diff --git a/src/FFMpeg/Filters/Video/VideoFilters.php b/src/FFMpeg/Filters/Video/VideoFilters.php index 0eac3f3..c00243d 100644 --- a/src/FFMpeg/Filters/Video/VideoFilters.php +++ b/src/FFMpeg/Filters/Video/VideoFilters.php @@ -57,6 +57,21 @@ class VideoFilters extends AudioFilters return $this; } + /** + * Extract multiple frames from the video + * + * @param string $frameRate + * @param string $destinationFolder + * + * @return $this + */ + public function extractMultipleFrames($frameRate = ExtractMultipleFramesFilter::FRAMERATE_EVERY_2SEC, $destinationFolder = __DIR__) + { + $this->media->addFilter(new ExtractMultipleFramesFilter($frameRate, $destinationFolder)); + + return $this; + } + /** * Synchronizes audio and video. * diff --git a/tests/Unit/Filters/Video/ExtractMultipleFramesFilterTest.php b/tests/Unit/Filters/Video/ExtractMultipleFramesFilterTest.php new file mode 100644 index 0000000..6619df7 --- /dev/null +++ b/tests/Unit/Filters/Video/ExtractMultipleFramesFilterTest.php @@ -0,0 +1,51 @@ +getVideoMock(); + $pathfile = '/path/to/file'.mt_rand(); + + $format = $this->getMock('FFMpeg\Format\VideoInterface'); + $format->expects($this->any()) + ->method('getModulus') + ->will($this->returnValue($modulus)); + + $streams = new StreamCollection(array( + new Stream(array( + 'codec_type' => 'video', + 'duration' => $duration, + )) + )); + + $video->expects($this->once()) + ->method('getStreams') + ->will($this->returnValue($streams)); + + $filter = new ExtractMultipleFramesFilter($frameRate, $destinationFolder); + $this->assertEquals($expected, $filter->apply($video, $format)); + } + + public function provideFrameRates() + { + return array( + array(ExtractMultipleFramesFilter::FRAMERATE_EVERY_SEC, '/', 100, 2, array('-vf', 'fps=1/1', '/frame-%03d.jpg')), + array(ExtractMultipleFramesFilter::FRAMERATE_EVERY_2SEC, '/', 100, 2, array('-vf', 'fps=1/2', '/frame-%02d.jpg')), + array(ExtractMultipleFramesFilter::FRAMERATE_EVERY_5SEC, '/', 100, 2, array('-vf', 'fps=1/5', '/frame-%02d.jpg')), + array(ExtractMultipleFramesFilter::FRAMERATE_EVERY_10SEC, '/', 100, 2, array('-vf', 'fps=1/10', '/frame-%02d.jpg')), + array(ExtractMultipleFramesFilter::FRAMERATE_EVERY_30SEC, '/', 100, 2, array('-vf', 'fps=1/30', '/frame-%02d.jpg')), + array(ExtractMultipleFramesFilter::FRAMERATE_EVERY_60SEC, '/', 100, 2, array('-vf', 'fps=1/60', '/frame-%02d.jpg')), + ); + } +}