Concatenation feature (#287)
* Creation of a feature to concatenate files into a new one. * Update of the README * Creation of the tests for the concatenation * We use an array of videos instead of a path to a text files * We use the bundle Temporary File System instead of getcwd
This commit is contained in:
		
					parent
					
						
							
								a6f6bbcb1e
							
						
					
				
			
			
				commit
				
					
						21c28dea25
					
				
			
		
					 8 changed files with 541 additions and 1 deletions
				
			
		
							
								
								
									
										20
									
								
								src/FFMpeg/Filters/Concat/ConcatFilterInterface.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/FFMpeg/Filters/Concat/ConcatFilterInterface.php
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,20 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * This file is part of PHP-FFmpeg.
 | 
			
		||||
 *
 | 
			
		||||
 * (c) Strime <contact@strime.io>
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view the LICENSE
 | 
			
		||||
 * file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace FFMpeg\Filters\Concat;
 | 
			
		||||
 | 
			
		||||
use FFMpeg\Filters\FilterInterface;
 | 
			
		||||
use FFMpeg\Media\Concat;
 | 
			
		||||
 | 
			
		||||
interface ConcatFilterInterface extends FilterInterface
 | 
			
		||||
{
 | 
			
		||||
    public function apply(Concat $concat);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										24
									
								
								src/FFMpeg/Filters/Concat/ConcatFilters.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/FFMpeg/Filters/Concat/ConcatFilters.php
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,24 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * This file is part of PHP-FFmpeg.
 | 
			
		||||
 *
 | 
			
		||||
 * (c) Strime <contact@strime.io>
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view the LICENSE
 | 
			
		||||
 * file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace FFMpeg\Filters\Concat;
 | 
			
		||||
 | 
			
		||||
use FFMpeg\Media\Concat;
 | 
			
		||||
 | 
			
		||||
class ConcatFilters
 | 
			
		||||
{
 | 
			
		||||
    private $concat;
 | 
			
		||||
 | 
			
		||||
    public function __construct(Concat $concat)
 | 
			
		||||
    {
 | 
			
		||||
        $this->concat = $concat;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										259
									
								
								src/FFMpeg/Media/Concat.php
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										259
									
								
								src/FFMpeg/Media/Concat.php
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,259 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * This file is part of PHP-FFmpeg.
 | 
			
		||||
 *
 | 
			
		||||
 * (c) Strime <contact@strime.io>
 | 
			
		||||
 *
 | 
			
		||||
 * 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 Alchemy\BinaryDriver\Exception\InvalidArgumentException;
 | 
			
		||||
use FFMpeg\Filters\Concat\ConcatFilterInterface;
 | 
			
		||||
use FFMpeg\Filters\Concat\ConcatFilters;
 | 
			
		||||
use FFMpeg\Driver\FFMpegDriver;
 | 
			
		||||
use FFMpeg\FFProbe;
 | 
			
		||||
use FFMpeg\Filters\Audio\SimpleFilter;
 | 
			
		||||
use FFMpeg\Exception\RuntimeException;
 | 
			
		||||
use FFMpeg\Format\FormatInterface;
 | 
			
		||||
use FFMpeg\Filters\FilterInterface;
 | 
			
		||||
use FFMpeg\Format\ProgressableInterface;
 | 
			
		||||
use FFMpeg\Format\AudioInterface;
 | 
			
		||||
use FFMpeg\Format\VideoInterface;
 | 
			
		||||
use Neutron\TemporaryFilesystem\Manager as FsManager;
 | 
			
		||||
 | 
			
		||||
class Concat extends AbstractMediaType
 | 
			
		||||
{
 | 
			
		||||
    /** @var array */
 | 
			
		||||
    private $sources;
 | 
			
		||||
 | 
			
		||||
    public function __construct($sources, FFMpegDriver $driver, FFProbe $ffprobe)
 | 
			
		||||
    {
 | 
			
		||||
        parent::__construct($sources, $driver, $ffprobe);
 | 
			
		||||
        $this->sources = $sources;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Returns the path to the sources.
 | 
			
		||||
     *
 | 
			
		||||
     * @return string
 | 
			
		||||
     */
 | 
			
		||||
    public function getSources()
 | 
			
		||||
    {
 | 
			
		||||
        return $this->sources;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * {@inheritdoc}
 | 
			
		||||
     *
 | 
			
		||||
     * @return ConcatFilters
 | 
			
		||||
     */
 | 
			
		||||
    public function filters()
 | 
			
		||||
    {
 | 
			
		||||
        return new ConcatFilters($this);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * {@inheritdoc}
 | 
			
		||||
     *
 | 
			
		||||
     * @return Concat
 | 
			
		||||
     */
 | 
			
		||||
    public function addFilter(ConcatFilterInterface $filter)
 | 
			
		||||
    {
 | 
			
		||||
        $this->filters->add($filter);
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Saves the concatenated video in the given array, considering that the sources videos are all encoded with the same codec.
 | 
			
		||||
     *
 | 
			
		||||
     * @param array   $outputPathfile
 | 
			
		||||
     * @param string  $streamCopy
 | 
			
		||||
     *
 | 
			
		||||
     * @return Concat
 | 
			
		||||
     *
 | 
			
		||||
     * @throws RuntimeException
 | 
			
		||||
     */
 | 
			
		||||
    public function saveFromSameCodecs($outputPathfile, $streamCopy = TRUE)
 | 
			
		||||
    {
 | 
			
		||||
        /**
 | 
			
		||||
         * @see https://ffmpeg.org/ffmpeg-formats.html#concat
 | 
			
		||||
         * @see https://trac.ffmpeg.org/wiki/Concatenate
 | 
			
		||||
         */
 | 
			
		||||
 | 
			
		||||
        // Create the file which will contain the list of videos
 | 
			
		||||
        $fs = FsManager::create();
 | 
			
		||||
        $sourcesFile = $fs->createTemporaryFile('ffmpeg-concat');
 | 
			
		||||
 | 
			
		||||
        // Set the content of this file
 | 
			
		||||
        $fileStream = fopen($sourcesFile, 'w') or die("Cannot open file.");
 | 
			
		||||
        $count_videos = 0;
 | 
			
		||||
        if(is_array($this->sources) && (count($this->sources) > 0)) {
 | 
			
		||||
            foreach ($this->sources as $videoPath) {
 | 
			
		||||
                $line = "";
 | 
			
		||||
                
 | 
			
		||||
                if($count_videos != 0)
 | 
			
		||||
                    $line .= "\n";
 | 
			
		||||
                
 | 
			
		||||
                $line .= "file ".$videoPath;
 | 
			
		||||
                
 | 
			
		||||
                fwrite($fileStream, $line);
 | 
			
		||||
                
 | 
			
		||||
                $count_videos++;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            throw new InvalidArgumentException('The list of videos is not a valid array.');
 | 
			
		||||
        }
 | 
			
		||||
        fclose($fileStream);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        $commands = array(
 | 
			
		||||
            '-f', 'concat', '-safe', '0',
 | 
			
		||||
            '-i', $sourcesFile
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        // Check if stream copy is activated
 | 
			
		||||
        if($streamCopy === TRUE) {
 | 
			
		||||
            $commands[] = '-c';
 | 
			
		||||
            $commands[] = 'copy';
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // If not, we can apply filters
 | 
			
		||||
        else {
 | 
			
		||||
            foreach ($this->filters as $filter) {
 | 
			
		||||
                $commands = array_merge($commands, $filter->apply($this));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Set the output file in the command
 | 
			
		||||
        $commands = array_merge($commands, array($outputPathfile));
 | 
			
		||||
 | 
			
		||||
        // Execute the command
 | 
			
		||||
        try {
 | 
			
		||||
            $this->driver->command($commands);
 | 
			
		||||
        } catch (ExecutionFailureException $e) {
 | 
			
		||||
            $this->cleanupTemporaryFile($outputPathfile);
 | 
			
		||||
            $this->cleanupTemporaryFile($sourcesFile);
 | 
			
		||||
            throw new RuntimeException('Unable to save concatenated video', $e->getCode(), $e);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $this->cleanupTemporaryFile($sourcesFile);
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Saves the concatenated video in the given filename, considering that the sources videos are all encoded with the same codec.
 | 
			
		||||
     *
 | 
			
		||||
     * @param string  $outputPathfile
 | 
			
		||||
     *
 | 
			
		||||
     * @return Concat
 | 
			
		||||
     *
 | 
			
		||||
     * @throws RuntimeException
 | 
			
		||||
     */
 | 
			
		||||
    public function saveFromDifferentCodecs(FormatInterface $format, $outputPathfile)
 | 
			
		||||
    {
 | 
			
		||||
        /**
 | 
			
		||||
         * @see https://ffmpeg.org/ffmpeg-formats.html#concat
 | 
			
		||||
         * @see https://trac.ffmpeg.org/wiki/Concatenate
 | 
			
		||||
         */
 | 
			
		||||
 | 
			
		||||
        // Check the validity of the parameter
 | 
			
		||||
        if(!is_array($this->sources) || (count($this->sources) == 0)) {
 | 
			
		||||
            throw new InvalidArgumentException('The list of videos is not a valid array.');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Create the commands variable
 | 
			
		||||
        $commands = array();
 | 
			
		||||
 | 
			
		||||
        // Prepare the parameters
 | 
			
		||||
        $nbSources = 0;
 | 
			
		||||
        $files = array();
 | 
			
		||||
 | 
			
		||||
        // For each source, check if this is a legit file
 | 
			
		||||
        // and prepare the parameters
 | 
			
		||||
        foreach ($this->sources as $videoPath) {
 | 
			
		||||
            $files[] = '-i';
 | 
			
		||||
            $files[] = $videoPath;
 | 
			
		||||
            $nbSources++;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $commands = array_merge($commands, $files);
 | 
			
		||||
 | 
			
		||||
        // Set the parameters of the request
 | 
			
		||||
        $commands[] = '-filter_complex';
 | 
			
		||||
 | 
			
		||||
        $complex_filter = '';
 | 
			
		||||
        for($i=0; $i<$nbSources; $i++) {
 | 
			
		||||
            $complex_filter .= '['.$i.':v:0] ['.$i.':a:0] ';
 | 
			
		||||
        }
 | 
			
		||||
        $complex_filter .= 'concat=n='.$nbSources.':v=1:a=1 [v] [a]';
 | 
			
		||||
        
 | 
			
		||||
        $commands[] = $complex_filter;
 | 
			
		||||
        $commands[] = '-map';
 | 
			
		||||
        $commands[] = '[v]';
 | 
			
		||||
        $commands[] = '-map';
 | 
			
		||||
        $commands[] = '[a]';
 | 
			
		||||
 | 
			
		||||
        // Prepare the filters
 | 
			
		||||
        $filters = clone $this->filters;
 | 
			
		||||
        $filters->add(new SimpleFilter($format->getExtraParams(), 10));
 | 
			
		||||
 | 
			
		||||
        if ($this->driver->getConfiguration()->has('ffmpeg.threads')) {
 | 
			
		||||
            $filters->add(new SimpleFilter(array('-threads', $this->driver->getConfiguration()->get('ffmpeg.threads'))));
 | 
			
		||||
        }
 | 
			
		||||
        if ($format instanceof VideoInterface) {
 | 
			
		||||
            if (null !== $format->getVideoCodec()) {
 | 
			
		||||
                $filters->add(new SimpleFilter(array('-vcodec', $format->getVideoCodec())));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if ($format instanceof AudioInterface) {
 | 
			
		||||
            if (null !== $format->getAudioCodec()) {
 | 
			
		||||
                $filters->add(new SimpleFilter(array('-acodec', $format->getAudioCodec())));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Add the filters
 | 
			
		||||
        foreach ($this->filters as $filter) {
 | 
			
		||||
            $commands = array_merge($commands, $filter->apply($this));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ($format instanceof AudioInterface) {
 | 
			
		||||
            if (null !== $format->getAudioKiloBitrate()) {
 | 
			
		||||
                $commands[] = '-b:a';
 | 
			
		||||
                $commands[] = $format->getAudioKiloBitrate() . 'k';
 | 
			
		||||
            }
 | 
			
		||||
            if (null !== $format->getAudioChannels()) {
 | 
			
		||||
                $commands[] = '-ac';
 | 
			
		||||
                $commands[] = $format->getAudioChannels();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // If the user passed some additional parameters
 | 
			
		||||
        if ($format instanceof VideoInterface) {
 | 
			
		||||
            if (null !== $format->getAdditionalParameters()) {
 | 
			
		||||
                foreach ($format->getAdditionalParameters() as $additionalParameter) {
 | 
			
		||||
                    $commands[] = $additionalParameter;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Set the output file in the command
 | 
			
		||||
        $commands[] = $outputPathfile;
 | 
			
		||||
 | 
			
		||||
        $failure = null;
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            $this->driver->command($commands);
 | 
			
		||||
        } catch (ExecutionFailureException $e) {
 | 
			
		||||
            throw new RuntimeException('Encoding failed', $e->getCode(), $e);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -204,4 +204,15 @@ class Video extends Audio
 | 
			
		|||
    {
 | 
			
		||||
        return new Gif($this, $this->driver, $this->ffprobe, $at, $dimension, $duration);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Concatenates a list of videos into one unique video.
 | 
			
		||||
     *
 | 
			
		||||
     * @param  string $sources
 | 
			
		||||
     * @return Concat
 | 
			
		||||
     */
 | 
			
		||||
    public function concat($sources)
 | 
			
		||||
    {
 | 
			
		||||
        return new Concat($sources, $this->driver, $this->ffprobe);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue