Merge pull request #283 from Romain/multipleframes
Creation of a filter to extract multiple frames in one encoding session
This commit is contained in:
		
				commit
				
					
						40f8edaff9
					
				
			
		
					 4 changed files with 205 additions and 0 deletions
				
			
		
							
								
								
									
										12
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										12
									
								
								README.md
									
										
									
									
									
								
							|  | @ -141,6 +141,18 @@ $frame = $video->frame(FFMpeg\Coordinate\TimeCode::fromSeconds(42)); | ||||||
| $frame->save('image.jpg'); | $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 | ##### Generate a waveform | ||||||
| 
 | 
 | ||||||
| You can generate a waveform of an audio file using the `FFMpeg\Media\Audio::waveform` | You can generate a waveform of an audio file using the `FFMpeg\Media\Audio::waveform` | ||||||
|  |  | ||||||
							
								
								
									
										127
									
								
								src/FFMpeg/Filters/Video/ExtractMultipleFramesFilter.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								src/FFMpeg/Filters/Video/ExtractMultipleFramesFilter.php
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,127 @@ | ||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | /* | ||||||
|  |  * This file is part of PHP-FFmpeg. | ||||||
|  |  * | ||||||
|  |  * (c) Strime <romain@strime.io> | ||||||
|  |  * | ||||||
|  |  * 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; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -57,6 +57,21 @@ class VideoFilters extends AudioFilters | ||||||
|         return $this; |         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. |      * Synchronizes audio and video. | ||||||
|      * |      * | ||||||
|  |  | ||||||
							
								
								
									
										51
									
								
								tests/Unit/Filters/Video/ExtractMultipleFramesFilterTest.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								tests/Unit/Filters/Video/ExtractMultipleFramesFilterTest.php
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,51 @@ | ||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | namespace Tests\FFMpeg\Unit\Filters\Video; | ||||||
|  | 
 | ||||||
|  | use FFMpeg\Filters\Video\ExtractMultipleFramesFilter; | ||||||
|  | use Tests\FFMpeg\Unit\TestCase; | ||||||
|  | use FFMpeg\FFProbe\DataMapping\Stream; | ||||||
|  | use FFMpeg\FFProbe\DataMapping\StreamCollection; | ||||||
|  | 
 | ||||||
|  | class ExtractMultipleFramesFilterTest extends TestCase | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * @dataProvider provideFrameRates | ||||||
|  |      */ | ||||||
|  |     public function testApply($frameRate, $destinationFolder, $duration, $modulus, $expected) | ||||||
|  |     { | ||||||
|  |         $video = $this->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')), | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue