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