Merge pull request #700 from CaliforniaMountainSnake/master
ComplexMedia, -filter_complex and -map features
This commit is contained in:
commit
4175c02b7d
20 changed files with 1875 additions and 10 deletions
76
README.md
76
README.md
|
|
@ -520,6 +520,82 @@ $video
|
|||
|
||||
More details about concatenation in FFMPEG can be found [here](https://trac.ffmpeg.org/wiki/Concatenate), [here](https://ffmpeg.org/ffmpeg-formats.html#concat-1) and [here](https://ffmpeg.org/ffmpeg.html#Stream-copy).
|
||||
|
||||
### AdvancedMedia
|
||||
AdvancedMedia may have multiple inputs and multiple outputs.
|
||||
|
||||
This class has been developed primarily to use with `-filter_complex`.
|
||||
|
||||
So, its `filters()` method accepts only filters that can be used inside `-filter_complex` command.
|
||||
AdvancedMedia already contains some built-in filters.
|
||||
|
||||
#### Base usage
|
||||
For example:
|
||||
|
||||
```php
|
||||
$advancedMedia = $ffmpeg->openAdvanced(array('video_1.mp4', 'video_2.mp4'));
|
||||
$advancedMedia->filters()
|
||||
->custom('[0:v][1:v]', 'hstack', '[v]');
|
||||
$advancedMedia
|
||||
->map(array('0:a', '[v]'), new X264('aac', 'libx264'), 'output.mp4')
|
||||
->save();
|
||||
```
|
||||
|
||||
This code takes 2 input videos, stacks they horizontally in 1 output video and adds to this new video the audio from the first video.
|
||||
(It is impossible with simple filtergraph that has only 1 input and only 1 output).
|
||||
|
||||
|
||||
#### Complicated example
|
||||
A more difficult example of possibilities of the AdvancedMedia. Consider all input videos already have the same resolution and duration. ("xstack" filter has been added in the 4.1 version of the ffmpeg).
|
||||
|
||||
```php
|
||||
$inputs = array(
|
||||
'video_1.mp4',
|
||||
'video_2.mp4',
|
||||
'video_3.mp4',
|
||||
'video_4.mp4',
|
||||
);
|
||||
|
||||
$advancedMedia = $ffmpeg->openAdvanced($inputs);
|
||||
$advancedMedia->filters()
|
||||
->custom('[0:v]', 'negate', '[v0negate]')
|
||||
->custom('[1:v]', 'edgedetect', '[v1edgedetect]')
|
||||
->custom('[2:v]', 'hflip', '[v2hflip]')
|
||||
->custom('[3:v]', 'vflip', '[v3vflip]')
|
||||
->xStack('[v0negate][v1edgedetect][v2hflip][v3vflip]', XStackFilter::LAYOUT_2X2, 4, '[resultv]');
|
||||
$advancedMedia
|
||||
->map(array('0:a'), new Mp3(), 'video_1.mp3')
|
||||
->map(array('1:a'), new Flac(), 'video_2.flac')
|
||||
->map(array('2:a'), new Wav(), 'video_3.wav')
|
||||
->map(array('3:a'), new Aac(), 'video_4.aac')
|
||||
->map(array('[resultv]'), new X264('aac', 'libx264'), 'output.mp4')
|
||||
->save();
|
||||
```
|
||||
|
||||
This code takes 4 input videos, then the negates the first video, stores result in `[v0negate]` stream, detects edges in the second video, stores result in `[v1edgedetect]` stream, horizontally flips the third video, stores result in `[v2hflip]` stream, vertically flips the fourth video, stores result in `[v3vflip]` stream, then takes this 4 generated streams ans combine them in one 2x2 collage video.
|
||||
Then saves audios from the original videos into the 4 different formats and saves the generated collage video into the separate file.
|
||||
|
||||
As you can see, you can take multiple input sources, perform the complicated processing for them and produce multiple output files in the same time, in the one ffmpeg command.
|
||||
|
||||
#### Just give me a map!
|
||||
You do not have to use `-filter_complex`. You can use only `-map` options. For example, just extract the audio from the video:
|
||||
|
||||
```php
|
||||
$advancedMedia = $ffmpeg->openAdvanced(array('video.mp4'));
|
||||
$advancedMedia
|
||||
->map(array('0:a'), new Mp3(), 'output.mp3')
|
||||
->save();
|
||||
```
|
||||
|
||||
#### Customisation
|
||||
If you need you can extra customize the result ffmpeg command of the AdvancedMedia:
|
||||
|
||||
```php
|
||||
$advancedMedia = $ffmpeg->openAdvanced($inputs);
|
||||
$advancedMedia
|
||||
->setInitialParameters(array('the', 'params', 'that', 'will', 'be', 'added', 'before', '-i', 'part', 'of', 'the', 'command'))
|
||||
->setAdditionalParameters(array('the', 'params', 'that', 'will', 'be', 'added', 'at', 'the', 'end', 'of', 'the', 'command'));
|
||||
```
|
||||
|
||||
#### Formats
|
||||
|
||||
A format implements `FFMpeg\Format\FormatInterface`. To save to a video file,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ use Alchemy\BinaryDriver\Configuration;
|
|||
use Alchemy\BinaryDriver\ConfigurationInterface;
|
||||
use Alchemy\BinaryDriver\Exception\ExecutableNotFoundException as BinaryDriverExecutableNotFound;
|
||||
use FFMpeg\Exception\ExecutableNotFoundException;
|
||||
use FFMpeg\Exception\RuntimeException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class FFMpegDriver extends AbstractBinary
|
||||
|
|
@ -54,4 +55,20 @@ class FFMpegDriver extends AbstractBinary
|
|||
throw new ExecutableNotFoundException('Unable to load FFMpeg', $e->getCode(), $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ffmpeg version.
|
||||
*
|
||||
* @return string
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function getVersion()
|
||||
{
|
||||
preg_match('#version\s(\S+)#', $this->command('-version'), $version);
|
||||
if (!isset($version[1])) {
|
||||
throw new RuntimeException('Cannot to parse the ffmpeg version!');
|
||||
}
|
||||
|
||||
return $version[1];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ use FFMpeg\Driver\FFMpegDriver;
|
|||
use FFMpeg\Exception\InvalidArgumentException;
|
||||
use FFMpeg\Exception\RuntimeException;
|
||||
use FFMpeg\Media\Audio;
|
||||
use FFMpeg\Media\AdvancedMedia;
|
||||
use FFMpeg\Media\Video;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
|
|
@ -59,6 +60,8 @@ class FFMpeg
|
|||
/**
|
||||
* Sets the ffmpeg driver.
|
||||
*
|
||||
* @param FFMpegDriver $ffmpeg
|
||||
*
|
||||
* @return FFMpeg
|
||||
*/
|
||||
public function setFFMpegDriver(FFMpegDriver $ffmpeg)
|
||||
|
|
@ -102,6 +105,18 @@ class FFMpeg
|
|||
throw new InvalidArgumentException('Unable to detect file format, only audio and video supported');
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens multiple input sources.
|
||||
*
|
||||
* @param string[] $inputs Array of files to be opened.
|
||||
*
|
||||
* @return AdvancedMedia
|
||||
*/
|
||||
public function openAdvanced($inputs)
|
||||
{
|
||||
return new AdvancedMedia($inputs, $this->driver, $this->ffprobe);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new FFMpeg instance.
|
||||
*
|
||||
|
|
|
|||
71
src/FFMpeg/Filters/AdvancedMedia/ANullSrcFilter.php
Normal file
71
src/FFMpeg/Filters/AdvancedMedia/ANullSrcFilter.php
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
namespace FFMpeg\Filters\AdvancedMedia;
|
||||
|
||||
use FFMpeg\Media\AdvancedMedia;
|
||||
|
||||
/**
|
||||
* @see https://ffmpeg.org/ffmpeg-filters.html#anullsrc
|
||||
*/
|
||||
class ANullSrcFilter extends AbstractComplexFilter
|
||||
{
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $channelLayout;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $sampleRate;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $nbSamples;
|
||||
|
||||
/**
|
||||
* ANullSrcComplexFilter constructor.
|
||||
*
|
||||
* @param string|null $channelLayout
|
||||
* @param int|null $sampleRate
|
||||
* @param int|null $nbSamples
|
||||
* @param int $priority
|
||||
*/
|
||||
public function __construct(
|
||||
$channelLayout = null,
|
||||
$sampleRate = null,
|
||||
$nbSamples = null,
|
||||
$priority = 0
|
||||
) {
|
||||
parent::__construct($priority);
|
||||
$this->channelLayout = $channelLayout;
|
||||
$this->sampleRate = $sampleRate;
|
||||
$this->nbSamples = $nbSamples;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name of the filter.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return 'anullsrc';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function applyComplex(AdvancedMedia $media)
|
||||
{
|
||||
return array(
|
||||
'-filter_complex',
|
||||
$this->getName() . $this->buildFilterOptions(array(
|
||||
'channel_layout' => $this->channelLayout,
|
||||
'sample_rate' => $this->sampleRate,
|
||||
'nb_samples' => $this->nbSamples,
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
62
src/FFMpeg/Filters/AdvancedMedia/AbstractComplexFilter.php
Normal file
62
src/FFMpeg/Filters/AdvancedMedia/AbstractComplexFilter.php
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
namespace FFMpeg\Filters\AdvancedMedia;
|
||||
|
||||
abstract class AbstractComplexFilter implements ComplexCompatibleFilter
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $priority;
|
||||
|
||||
/**
|
||||
* AbstractComplexFilter constructor.
|
||||
*
|
||||
* @param int $priority
|
||||
*/
|
||||
public function __construct($priority = 0)
|
||||
{
|
||||
$this->priority = $priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getPriority()
|
||||
{
|
||||
return $this->priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get minimal version of ffmpeg starting with which this filter is supported.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getMinimalFFMpegVersion()
|
||||
{
|
||||
return '0.3';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the config of the filter.
|
||||
*
|
||||
* @param array $params Associative array of filter options. The options may be null.
|
||||
*
|
||||
* @return string The string of the form "=name1=value1:name2=value2" or empty string.
|
||||
*/
|
||||
protected function buildFilterOptions(array $params)
|
||||
{
|
||||
$config = array();
|
||||
foreach ($params as $paramName => $paramValue) {
|
||||
if ($paramValue !== null) {
|
||||
$config[] = $paramName . '=' . $paramValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($config)) {
|
||||
return '=' . implode(':', $config);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
35
src/FFMpeg/Filters/AdvancedMedia/ComplexCompatibleFilter.php
Normal file
35
src/FFMpeg/Filters/AdvancedMedia/ComplexCompatibleFilter.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
namespace FFMpeg\Filters\AdvancedMedia;
|
||||
|
||||
use FFMpeg\Filters\FilterInterface;
|
||||
use FFMpeg\Media\AdvancedMedia;
|
||||
|
||||
/**
|
||||
* A filter that can be used inside "-filter_complex" option.
|
||||
*/
|
||||
interface ComplexCompatibleFilter extends FilterInterface
|
||||
{
|
||||
/**
|
||||
* Get name of the filter.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName();
|
||||
|
||||
/**
|
||||
* Get minimal version of ffmpeg starting with which this filter is supported.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getMinimalFFMpegVersion();
|
||||
|
||||
/**
|
||||
* Apply the complex filter to the given media.
|
||||
*
|
||||
* @param AdvancedMedia $media
|
||||
*
|
||||
* @return string[] An array of arguments.
|
||||
*/
|
||||
public function applyComplex(AdvancedMedia $media);
|
||||
}
|
||||
100
src/FFMpeg/Filters/AdvancedMedia/ComplexFilterContainer.php
Normal file
100
src/FFMpeg/Filters/AdvancedMedia/ComplexFilterContainer.php
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<?php
|
||||
|
||||
namespace FFMpeg\Filters\AdvancedMedia;
|
||||
|
||||
use FFMpeg\Media\AdvancedMedia;
|
||||
|
||||
/**
|
||||
* Container for the complex compatible filter.
|
||||
*/
|
||||
class ComplexFilterContainer implements ComplexFilterInterface
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $priority;
|
||||
|
||||
/**
|
||||
* @var ComplexCompatibleFilter
|
||||
*/
|
||||
private $baseFilter;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $inLabels;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $outLabels;
|
||||
|
||||
/**
|
||||
* ComplexFilter constructor.
|
||||
*
|
||||
* @param string $inLabels
|
||||
* @param ComplexCompatibleFilter $baseFilter
|
||||
* @param string $outLabels
|
||||
*/
|
||||
public function __construct($inLabels, ComplexCompatibleFilter $baseFilter, $outLabels)
|
||||
{
|
||||
$this->priority = $baseFilter->getPriority();
|
||||
$this->inLabels = $inLabels;
|
||||
$this->baseFilter = $baseFilter;
|
||||
$this->outLabels = $outLabels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the priority of the filter.
|
||||
*
|
||||
* @return integer
|
||||
*/
|
||||
public function getPriority()
|
||||
{
|
||||
return $this->priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getInLabels()
|
||||
{
|
||||
return $this->inLabels;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getOutLabels()
|
||||
{
|
||||
return $this->outLabels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name of the filter.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->baseFilter->getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get minimal version of ffmpeg starting with which this filter is supported.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getMinimalFFMpegVersion()
|
||||
{
|
||||
return $this->baseFilter->getMinimalFFMpegVersion();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function applyComplex(AdvancedMedia $media)
|
||||
{
|
||||
return $this->baseFilter->applyComplex($media);
|
||||
}
|
||||
}
|
||||
19
src/FFMpeg/Filters/AdvancedMedia/ComplexFilterInterface.php
Normal file
19
src/FFMpeg/Filters/AdvancedMedia/ComplexFilterInterface.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace FFMpeg\Filters\AdvancedMedia;
|
||||
|
||||
/**
|
||||
* A filter that is completely ready to use inside "-filter_complex" option.
|
||||
*/
|
||||
interface ComplexFilterInterface extends ComplexCompatibleFilter
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getInLabels();
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getOutLabels();
|
||||
}
|
||||
169
src/FFMpeg/Filters/AdvancedMedia/ComplexFilters.php
Normal file
169
src/FFMpeg/Filters/AdvancedMedia/ComplexFilters.php
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
<?php
|
||||
|
||||
namespace FFMpeg\Filters\AdvancedMedia;
|
||||
|
||||
use FFMpeg\Coordinate\Dimension;
|
||||
use FFMpeg\Filters\Video\PadFilter;
|
||||
use FFMpeg\Filters\Video\WatermarkFilter;
|
||||
use FFMpeg\Media\AdvancedMedia;
|
||||
|
||||
class ComplexFilters
|
||||
{
|
||||
/**
|
||||
* @var AdvancedMedia
|
||||
*/
|
||||
protected $media;
|
||||
|
||||
/**
|
||||
* ComplexFilters constructor.
|
||||
*
|
||||
* @param AdvancedMedia $media
|
||||
*/
|
||||
public function __construct(AdvancedMedia $media)
|
||||
{
|
||||
$this->media = $media;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $in
|
||||
* @param string $parameters
|
||||
* @param string $out
|
||||
*
|
||||
* @return ComplexFilters
|
||||
*/
|
||||
public function custom($in, $parameters, $out)
|
||||
{
|
||||
$this->media->addFilter($in, new CustomComplexFilter($parameters), $out);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds padding (black bars) to a video.
|
||||
*
|
||||
* @param string $in
|
||||
* @param Dimension $dimension
|
||||
* @param string $out
|
||||
*
|
||||
* @return ComplexFilters
|
||||
*/
|
||||
public function pad($in, Dimension $dimension, $out)
|
||||
{
|
||||
$this->media->addFilter($in, new PadFilter($dimension), $out);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a watermark image to a video.
|
||||
*
|
||||
* @param string $in
|
||||
* @param string $imagePath
|
||||
* @param string $out
|
||||
* @param array $coordinates
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function watermark($in, $imagePath, $out, array $coordinates = array())
|
||||
{
|
||||
$this->media->addFilter($in, new WatermarkFilter($imagePath, $coordinates), $out);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply "xstack" filter.
|
||||
* Warning: this filter is supported starting from 4.1 ffmpeg version.
|
||||
*
|
||||
* @param string $in
|
||||
* @param string $layout
|
||||
* @param int $inputsCount
|
||||
* @param string $out
|
||||
*
|
||||
* @return ComplexFilters
|
||||
* @see https://ffmpeg.org/ffmpeg-filters.html#xstack
|
||||
*/
|
||||
public function xStack($in, $layout, $inputsCount, $out)
|
||||
{
|
||||
$this->media->addFilter($in, new XStackFilter($layout, $inputsCount), $out);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* This filter build various types of computed inputs.
|
||||
*
|
||||
* @param string $out
|
||||
* @param string|null $type
|
||||
* @param string|null $size
|
||||
* @param string|null $duration
|
||||
* @param string|null $sar
|
||||
* @param string|null $rate
|
||||
* @param string|null $level
|
||||
* @param string|null $color
|
||||
* @param int|null $alpha
|
||||
* @param float|null $decimals
|
||||
*
|
||||
* @return ComplexFilters
|
||||
* @see https://ffmpeg.org/ffmpeg-filters.html#allrgb_002c-allyuv_002c-color_002c-haldclutsrc_002c-nullsrc_002c-pal75bars_002c-pal100bars_002c-rgbtestsrc_002c-smptebars_002c-smptehdbars_002c-testsrc_002c-testsrc2_002c-yuvtestsrc
|
||||
*/
|
||||
public function testSrc(
|
||||
$out,
|
||||
$type = TestSrcFilter::TESTSRC,
|
||||
$size = '320x240',
|
||||
$duration = null,
|
||||
$sar = null,
|
||||
$rate = null,
|
||||
$level = null,
|
||||
$color = null,
|
||||
$alpha = null,
|
||||
$decimals = null
|
||||
) {
|
||||
$this->media->addFilter('',
|
||||
new TestSrcFilter($type, $size, $duration, $sar, $rate, $level, $color, $alpha, $decimals), $out);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply "anullsrc" filter.
|
||||
*
|
||||
* @param string $out
|
||||
* @param string|null $channelLayout
|
||||
* @param int|null $sampleRate
|
||||
* @param int|null $nbSamples
|
||||
*
|
||||
* @return ComplexFilters
|
||||
* @see https://ffmpeg.org/ffmpeg-filters.html#anullsrc
|
||||
*/
|
||||
public function aNullSrc(
|
||||
$out,
|
||||
$channelLayout = null,
|
||||
$sampleRate = null,
|
||||
$nbSamples = null
|
||||
) {
|
||||
$this->media->addFilter('', new ANullSrcFilter($channelLayout, $sampleRate, $nbSamples), $out);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply "sine" filter.
|
||||
*
|
||||
* @param $out
|
||||
* @param string $duration
|
||||
* @param int|null $frequency
|
||||
* @param string|null $beep_factor
|
||||
* @param int|null $sample_rate
|
||||
* @param string|null $samples_per_frame
|
||||
*
|
||||
* @return $this
|
||||
* @see https://ffmpeg.org/ffmpeg-filters.html#sine
|
||||
*/
|
||||
public function sine(
|
||||
$out,
|
||||
$duration,
|
||||
$frequency = null,
|
||||
$beep_factor = null,
|
||||
$sample_rate = null,
|
||||
$samples_per_frame = null
|
||||
) {
|
||||
$this->media->addFilter('',
|
||||
new SineFilter($duration, $frequency, $beep_factor, $sample_rate, $samples_per_frame), $out);
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
43
src/FFMpeg/Filters/AdvancedMedia/CustomComplexFilter.php
Normal file
43
src/FFMpeg/Filters/AdvancedMedia/CustomComplexFilter.php
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
namespace FFMpeg\Filters\AdvancedMedia;
|
||||
|
||||
use FFMpeg\Media\AdvancedMedia;
|
||||
|
||||
class CustomComplexFilter extends AbstractComplexFilter
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $filter;
|
||||
|
||||
/**
|
||||
* CustomComplexFilter constructor.
|
||||
*
|
||||
* @param string $filter
|
||||
* @param int $priority
|
||||
*/
|
||||
public function __construct($filter, $priority = 0)
|
||||
{
|
||||
parent::__construct($priority);
|
||||
$this->filter = $filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name of the filter.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return 'custom_filter';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function applyComplex(AdvancedMedia $media)
|
||||
{
|
||||
return array('-filter_complex', $this->filter);
|
||||
}
|
||||
}
|
||||
97
src/FFMpeg/Filters/AdvancedMedia/SineFilter.php
Normal file
97
src/FFMpeg/Filters/AdvancedMedia/SineFilter.php
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
<?php
|
||||
|
||||
namespace FFMpeg\Filters\AdvancedMedia;
|
||||
|
||||
use FFMpeg\Media\AdvancedMedia;
|
||||
|
||||
/**
|
||||
* @see https://ffmpeg.org/ffmpeg-filters.html#sine
|
||||
*/
|
||||
class SineFilter extends AbstractComplexFilter
|
||||
{
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $frequency;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $beep_factor;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $sample_rate;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $duration;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $samples_per_frame;
|
||||
|
||||
/**
|
||||
* SineComplexFilter constructor.
|
||||
*
|
||||
* @param string $duration
|
||||
* @param int|null $frequency
|
||||
* @param string|null $beep_factor
|
||||
* @param int|null $sample_rate
|
||||
* @param string|null $samples_per_frame
|
||||
* @param int $priority
|
||||
*/
|
||||
public function __construct($duration, $frequency, $beep_factor, $sample_rate, $samples_per_frame, $priority = 0)
|
||||
{
|
||||
parent::__construct($priority);
|
||||
$this->duration = $duration;
|
||||
$this->frequency = $frequency;
|
||||
$this->beep_factor = $beep_factor;
|
||||
$this->sample_rate = $sample_rate;
|
||||
$this->samples_per_frame = $samples_per_frame;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name of the filter.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return 'sine';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get minimal version of ffmpeg starting with which this filter is supported.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getMinimalFFMpegVersion()
|
||||
{
|
||||
return '2.0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the complex filter to the given media.
|
||||
*
|
||||
* @param AdvancedMedia $media
|
||||
*
|
||||
* @return string[] An array of arguments.
|
||||
*/
|
||||
public function applyComplex(AdvancedMedia $media)
|
||||
{
|
||||
return array(
|
||||
'-filter_complex',
|
||||
$this->getName() . $this->buildFilterOptions(array(
|
||||
'frequency' => $this->frequency,
|
||||
'beep_factor' => $this->beep_factor,
|
||||
'sample_rate' => $this->sample_rate,
|
||||
'duration' => $this->duration,
|
||||
'samples_per_frame' => $this->samples_per_frame,
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
246
src/FFMpeg/Filters/AdvancedMedia/TestSrcFilter.php
Normal file
246
src/FFMpeg/Filters/AdvancedMedia/TestSrcFilter.php
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
<?php
|
||||
|
||||
namespace FFMpeg\Filters\AdvancedMedia;
|
||||
|
||||
use FFMpeg\Media\AdvancedMedia;
|
||||
|
||||
/**
|
||||
* This filter builds various types of computed inputs.
|
||||
*
|
||||
* @see https://ffmpeg.org/ffmpeg-filters.html#allrgb_002c-allyuv_002c-color_002c-haldclutsrc_002c-nullsrc_002c-pal75bars_002c-pal100bars_002c-rgbtestsrc_002c-smptebars_002c-smptehdbars_002c-testsrc_002c-testsrc2_002c-yuvtestsrc
|
||||
*/
|
||||
class TestSrcFilter extends AbstractComplexFilter
|
||||
{
|
||||
/**
|
||||
* Source returns frames of size 4096x4096 of all rgb colors.
|
||||
*/
|
||||
const ALLRGB = 'allrgb';
|
||||
|
||||
/**
|
||||
* Source returns frames of size 4096x4096 of all yuv colors.
|
||||
*/
|
||||
const ALLYUV = 'allyuv';
|
||||
|
||||
/**
|
||||
* Source provides an uniformly colored input.
|
||||
*/
|
||||
const COLOR = 'color';
|
||||
|
||||
/**
|
||||
* Source provides an identity Hald CLUT.
|
||||
*/
|
||||
const HALDCLUTSRC = 'haldclutsrc';
|
||||
|
||||
/**
|
||||
* Source returns unprocessed video frames.
|
||||
* It is mainly useful to be employed in analysis / debugging tools,
|
||||
* or as the source for filters which ignore the input data.
|
||||
*/
|
||||
const NULLSRC = 'nullsrc';
|
||||
|
||||
/**
|
||||
* Source generates a color bars pattern, based on EBU PAL recommendations with 75% color levels.
|
||||
*/
|
||||
const PAL75BARS = 'pal75bars';
|
||||
|
||||
/**
|
||||
* Source generates a color bars pattern, based on EBU PAL recommendations with 100% color levels.
|
||||
*/
|
||||
const PAL100BARS = 'pal100bars';
|
||||
|
||||
/**
|
||||
* Source generates an RGB test pattern useful for detecting RGB vs BGR issues.
|
||||
* You should see a red, green and blue stripe from top to bottom.
|
||||
*/
|
||||
const RGBTESTSRC = 'rgbtestsrc';
|
||||
|
||||
/**
|
||||
* Source generates a color bars pattern, based on the SMPTE Engineering Guideline EG 1-1990.
|
||||
*/
|
||||
const SMPTEBARS = 'smptebars';
|
||||
|
||||
/**
|
||||
* Source generates a color bars pattern, based on the SMPTE RP 219-2002.
|
||||
*/
|
||||
const SMPTEHDBARS = 'smptehdbars';
|
||||
|
||||
/**
|
||||
* Source generates a test video pattern, showing a color pattern, a scrolling gradient and a timestamp.
|
||||
* This is mainly intended for testing purposes.
|
||||
*/
|
||||
const TESTSRC = 'testsrc';
|
||||
|
||||
/**
|
||||
* Source is similar to testsrc, but supports more pixel formats instead of just rgb24.
|
||||
* This allows using it as an input for other tests without requiring a format conversion.
|
||||
*/
|
||||
const TESTSRC2 = 'testsrc2';
|
||||
|
||||
/**
|
||||
* Source generates an YUV test pattern. You should see a y, cb and cr stripe from top to bottom.
|
||||
*/
|
||||
const YUVTESTSRC = 'yuvtestsrc';
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $type;
|
||||
|
||||
/**
|
||||
* Specify the level of the Hald CLUT, only available in the haldclutsrc source.
|
||||
* A level of N generates a picture of N*N*N by N*N*N pixels to be used as identity matrix for 3D lookup tables.
|
||||
* Each component is coded on a 1/(N*N) scale.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
private $level;
|
||||
|
||||
/**
|
||||
* Specify the color of the source, only available in the color source.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
private $color;
|
||||
|
||||
/**
|
||||
* Specify the size of the sourced video.
|
||||
* This option is not available with the allrgb, allyuv, and haldclutsrc filters.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
private $size;
|
||||
|
||||
/**
|
||||
* Specify the frame rate of the sourced video, as the number of frames generated per second.
|
||||
* It has to be a string in the format frame_rate_num/frame_rate_den, an integer number,
|
||||
* a floating point number or a valid video frame rate abbreviation. The default value is "25".
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
private $rate;
|
||||
|
||||
/**
|
||||
* Set the duration of the sourced video.
|
||||
* If not specified, or the expressed duration is negative, the video is supposed to be generated forever.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
private $duration;
|
||||
|
||||
/**
|
||||
* Set the sample aspect ratio of the sourced video.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
private $sar;
|
||||
|
||||
/**
|
||||
* Specify the alpha (opacity) of the background, only available in the testsrc2 source.
|
||||
* The value must be between 0 (fully transparent) and 255 (fully opaque, the default).
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
private $alpha;
|
||||
|
||||
/**
|
||||
* Set the number of decimals to show in the timestamp, only available in the testsrc source.
|
||||
* The displayed timestamp value will correspond to the original timestamp value multiplied
|
||||
* by the power of 10 of the specified value. Default value is 0.
|
||||
*
|
||||
* @var float|null
|
||||
*/
|
||||
private $decimals;
|
||||
|
||||
/**
|
||||
* TestSrcComplexFilter constructor.
|
||||
*
|
||||
* @param string|null $type
|
||||
* @param string|null $size
|
||||
* @param string|null $duration
|
||||
* @param string|null $sar
|
||||
* @param string|null $rate
|
||||
* @param string|null $level
|
||||
* @param string|null $color
|
||||
* @param int|null $alpha
|
||||
* @param float|null $decimals
|
||||
* @param int|null $priority
|
||||
*/
|
||||
public function __construct(
|
||||
$type = self::TESTSRC,
|
||||
$size = '320x240',
|
||||
$duration = null,
|
||||
$sar = null,
|
||||
$rate = null,
|
||||
$level = null,
|
||||
$color = null,
|
||||
$alpha = null,
|
||||
$decimals = null,
|
||||
$priority = 0
|
||||
) {
|
||||
parent::__construct($priority);
|
||||
$this->type = $type;
|
||||
$this->level = $level;
|
||||
$this->color = $color;
|
||||
$this->size = $size;
|
||||
$this->rate = $rate;
|
||||
$this->duration = $duration;
|
||||
$this->sar = $sar;
|
||||
$this->alpha = $alpha;
|
||||
$this->decimals = $decimals;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name of the filter.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get minimal version of ffmpeg starting with which this filter is supported.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getMinimalFFMpegVersion()
|
||||
{
|
||||
switch ($this->type) {
|
||||
case self::PAL75BARS:
|
||||
case self::PAL100BARS:
|
||||
return '4.1';
|
||||
case self::YUVTESTSRC:
|
||||
return '3.2';
|
||||
case self::ALLRGB:
|
||||
case self::ALLYUV:
|
||||
return '2.8';
|
||||
case self::SMPTEHDBARS:
|
||||
return '2.0';
|
||||
case self::SMPTEBARS:
|
||||
return '1.0';
|
||||
default:
|
||||
return '0.3';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function applyComplex(AdvancedMedia $media)
|
||||
{
|
||||
return array(
|
||||
'-filter_complex',
|
||||
$this->type . $this->buildFilterOptions(array(
|
||||
'level' => $this->level,
|
||||
'color' => $this->color,
|
||||
'size' => $this->size,
|
||||
'rate' => $this->rate,
|
||||
'duration' => $this->duration,
|
||||
'sar' => $this->sar,
|
||||
'alpha' => $this->alpha,
|
||||
'decimals' => $this->decimals
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
93
src/FFMpeg/Filters/AdvancedMedia/XStackFilter.php
Normal file
93
src/FFMpeg/Filters/AdvancedMedia/XStackFilter.php
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
|
||||
namespace FFMpeg\Filters\AdvancedMedia;
|
||||
|
||||
use FFMpeg\Media\AdvancedMedia;
|
||||
|
||||
/**
|
||||
* "xstack" filter.
|
||||
* This filter helps you to create a collage from the given videos.
|
||||
* This filter is supported starting from 4.1 ffmpeg version.
|
||||
* (On early versions you can use combinations of hstack and vstack filters).
|
||||
*
|
||||
* @see https://ffmpeg.org/ffmpeg-filters.html#xstack
|
||||
*/
|
||||
class XStackFilter extends AbstractComplexFilter
|
||||
{
|
||||
const LAYOUT_2X2 = '0_0|0_h0|w0_0|w0_h0';
|
||||
const LAYOUT_1X4 = '0_0|0_h0|0_h0+h1|0_h0+h1+h2';
|
||||
const LAYOUT_3X3 = '0_0|0_h0|0_h0+h1|w0_0|w0_h0|w0_h0+h1|w0+w3_0|w0+w3_h0|w0+w3_h0+h1';
|
||||
const LAYOUT_4X4 = '0_0|0_h0|0_h0+h1|0_h0+h1+h2|w0_0|w0_h0|w0_h0+h1|w0_h0+h1+h2|w0+w4_0|w0+w4_h0|w0+w4_h0+h1|w0+w4_h0+h1+h2|w0+w4+w8_0|w0+w4+w8_h0|w0+w4+w8_h0+h1|w0+w4+w8_h0+h1+h2';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $layout;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $inputsCount;
|
||||
|
||||
/**
|
||||
* CustomComplexFilter constructor.
|
||||
*
|
||||
* @param string $layout
|
||||
* @param int $inputsCount
|
||||
* @param int $priority
|
||||
*/
|
||||
public function __construct($layout, $inputsCount, $priority = 0)
|
||||
{
|
||||
parent::__construct($priority);
|
||||
$this->layout = $layout;
|
||||
$this->inputsCount = $inputsCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $count
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getInputByCount($count)
|
||||
{
|
||||
$result = '';
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$result .= '[' . $i . ':v]';
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name of the filter.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return 'xstack';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get minimal version of ffmpeg starting with which this filter is supported.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getMinimalFFMpegVersion()
|
||||
{
|
||||
return '4.1';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function applyComplex(AdvancedMedia $media)
|
||||
{
|
||||
return array(
|
||||
'-filter_complex',
|
||||
$this->getName() . $this->buildFilterOptions(array(
|
||||
'inputs' => $this->inputsCount,
|
||||
'layout' => $this->layout
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -12,10 +12,12 @@
|
|||
namespace FFMpeg\Filters\Video;
|
||||
|
||||
use FFMpeg\Coordinate\Dimension;
|
||||
use FFMpeg\Media\Video;
|
||||
use FFMpeg\Filters\AdvancedMedia\ComplexCompatibleFilter;
|
||||
use FFMpeg\Format\VideoInterface;
|
||||
use FFMpeg\Media\AdvancedMedia;
|
||||
use FFMpeg\Media\Video;
|
||||
|
||||
class PadFilter implements VideoFilterInterface
|
||||
class PadFilter implements VideoFilterInterface, ComplexCompatibleFilter
|
||||
{
|
||||
/** @var Dimension */
|
||||
private $dimension;
|
||||
|
|
@ -44,15 +46,54 @@ class PadFilter implements VideoFilterInterface
|
|||
return $this->dimension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name of the filter.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return 'pad';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get minimal version of ffmpeg starting with which this filter is supported.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getMinimalFFMpegVersion()
|
||||
{
|
||||
return '0.4.9';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function apply(Video $video, VideoInterface $format)
|
||||
{
|
||||
return $this->getCommands();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function applyComplex(AdvancedMedia $media)
|
||||
{
|
||||
return $this->getCommands();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function getCommands()
|
||||
{
|
||||
$commands = array();
|
||||
|
||||
$commands[] = '-vf';
|
||||
$commands[] = 'scale=iw*min(' . $this->dimension->getWidth() . '/iw\,' . $this->dimension->getHeight() .'/ih):ih*min(' . $this->dimension->getWidth() . '/iw\,' . $this->dimension->getHeight() .'/ih),pad=' . $this->dimension->getWidth() . ':' . $this->dimension->getHeight() . ':(' . $this->dimension->getWidth() . '-iw)/2:(' . $this->dimension->getHeight() .'-ih)/2';
|
||||
$commands[] = 'scale=iw*min(' . $this->dimension->getWidth() . '/iw\,' . $this->dimension->getHeight()
|
||||
. '/ih):ih*min(' . $this->dimension->getWidth() . '/iw\,' . $this->dimension->getHeight() . '/ih),pad='
|
||||
. $this->dimension->getWidth() . ':' . $this->dimension->getHeight() . ':(' . $this->dimension->getWidth()
|
||||
. '-iw)/2:(' . $this->dimension->getHeight() . '-ih)/2';
|
||||
|
||||
return $commands;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,10 +12,12 @@
|
|||
namespace FFMpeg\Filters\Video;
|
||||
|
||||
use FFMpeg\Exception\InvalidArgumentException;
|
||||
use FFMpeg\Filters\AdvancedMedia\ComplexCompatibleFilter;
|
||||
use FFMpeg\Format\VideoInterface;
|
||||
use FFMpeg\Media\AdvancedMedia;
|
||||
use FFMpeg\Media\Video;
|
||||
|
||||
class WatermarkFilter implements VideoFilterInterface
|
||||
class WatermarkFilter implements VideoFilterInterface, ComplexCompatibleFilter
|
||||
{
|
||||
/** @var string */
|
||||
private $watermarkPath;
|
||||
|
|
@ -43,10 +45,46 @@ class WatermarkFilter implements VideoFilterInterface
|
|||
return $this->priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name of the filter.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return 'watermark';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get minimal version of ffmpeg starting with which this filter is supported.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getMinimalFFMpegVersion()
|
||||
{
|
||||
return '0.8';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function apply(Video $video, VideoInterface $format)
|
||||
{
|
||||
return $this->getCommands();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function applyComplex(AdvancedMedia $media)
|
||||
{
|
||||
return $this->getCommands();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function getCommands()
|
||||
{
|
||||
$position = isset($this->coordinates['position']) ? $this->coordinates['position'] : 'absolute';
|
||||
|
||||
|
|
@ -55,7 +93,7 @@ class WatermarkFilter implements VideoFilterInterface
|
|||
if (isset($this->coordinates['top'])) {
|
||||
$y = $this->coordinates['top'];
|
||||
} elseif (isset($this->coordinates['bottom'])) {
|
||||
$y = sprintf('main_h - %d - overlay_h', $this->coordinates['bottom']);
|
||||
$y = 'main_h - ' . $this->coordinates['bottom'] . ' - overlay_h';
|
||||
} else {
|
||||
$y = 0;
|
||||
}
|
||||
|
|
@ -63,7 +101,7 @@ class WatermarkFilter implements VideoFilterInterface
|
|||
if (isset($this->coordinates['left'])) {
|
||||
$x = $this->coordinates['left'];
|
||||
} elseif (isset($this->coordinates['right'])) {
|
||||
$x = sprintf('main_w - %d - overlay_w', $this->coordinates['right']);
|
||||
$x = 'main_w - ' . $this->coordinates['right'] . ' - overlay_w';
|
||||
} else {
|
||||
$x = 0;
|
||||
}
|
||||
|
|
@ -75,6 +113,9 @@ class WatermarkFilter implements VideoFilterInterface
|
|||
break;
|
||||
}
|
||||
|
||||
return array('-vf', sprintf('movie=%s [watermark]; [in][watermark] overlay=%s:%s [out]', $this->watermarkPath, $x, $y));
|
||||
return array(
|
||||
'-vf',
|
||||
'movie=' . $this->watermarkPath . ' [watermark]; [in][watermark] overlay=' . $x . ':' . $y . ' [out]',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
424
src/FFMpeg/Media/AdvancedMedia.php
Normal file
424
src/FFMpeg/Media/AdvancedMedia.php
Normal file
|
|
@ -0,0 +1,424 @@
|
|||
<?php
|
||||
|
||||
namespace FFMpeg\Media;
|
||||
|
||||
use Alchemy\BinaryDriver\Exception\ExecutionFailureException;
|
||||
use FFMpeg\Driver\FFMpegDriver;
|
||||
use FFMpeg\Exception\RuntimeException;
|
||||
use FFMpeg\FFProbe;
|
||||
use FFMpeg\Filters\AdvancedMedia\ComplexCompatibleFilter;
|
||||
use FFMpeg\Filters\AdvancedMedia\ComplexFilterContainer;
|
||||
use FFMpeg\Filters\AdvancedMedia\ComplexFilterInterface;
|
||||
use FFMpeg\Filters\AdvancedMedia\ComplexFilters;
|
||||
use FFMpeg\Filters\FiltersCollection;
|
||||
use FFMpeg\Format\AudioInterface;
|
||||
use FFMpeg\Format\FormatInterface;
|
||||
use FFMpeg\Format\ProgressableInterface;
|
||||
use FFMpeg\Format\ProgressListener\AbstractProgressListener;
|
||||
use FFMpeg\Format\VideoInterface;
|
||||
|
||||
/**
|
||||
* AdvancedMedia may have multiple inputs and multiple outputs.
|
||||
* This class accepts only filters for -filter_complex option.
|
||||
* But you can set initial and additional parameters of the ffmpeg command.
|
||||
*
|
||||
* @see http://trac.ffmpeg.org/wiki/Creating%20multiple%20outputs
|
||||
*/
|
||||
class AdvancedMedia extends AbstractMediaType
|
||||
{
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private $inputs;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private $initialParameters;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private $additionalParameters;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private $mapCommands;
|
||||
|
||||
/**
|
||||
* @var AbstractProgressListener[]
|
||||
*/
|
||||
private $listeners;
|
||||
|
||||
/**
|
||||
* AdvancedMedia constructor.
|
||||
*
|
||||
* @param string[] $inputs Array of files to be opened.
|
||||
* @param FFMpegDriver $driver
|
||||
* @param FFProbe $ffprobe
|
||||
*/
|
||||
public function __construct($inputs, FFMpegDriver $driver, FFProbe $ffprobe)
|
||||
{
|
||||
// In case of error user will see this text in the error log.
|
||||
// But absence of inputs is a correct situation for some cases.
|
||||
// For example, if the user will use filters such as "testsrc".
|
||||
$pathfile = 'you_can_pass_empty_inputs_array_only_if_you_use_computed_inputs';
|
||||
$inputsKeys = array_keys($inputs);
|
||||
if (count($inputsKeys) > 0) {
|
||||
$pathfile = $inputs[$inputsKeys[0]];
|
||||
}
|
||||
|
||||
parent::__construct($pathfile, $driver, $ffprobe);
|
||||
$this->filters = new FiltersCollection();
|
||||
$this->inputs = $inputs;
|
||||
$this->initialParameters = array();
|
||||
$this->additionalParameters = array();
|
||||
$this->mapCommands = array();
|
||||
$this->listeners = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the available filters.
|
||||
*
|
||||
* @return ComplexFilters
|
||||
*/
|
||||
public function filters()
|
||||
{
|
||||
return new ComplexFilters($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add complex filter.
|
||||
*
|
||||
* @param string $in
|
||||
* @param ComplexCompatibleFilter $filter
|
||||
* @param string $out
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addFilter($in, ComplexCompatibleFilter $filter, $out)
|
||||
{
|
||||
$this->filters->add(new ComplexFilterContainer($in, $filter, $out));
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function setFiltersCollection(FiltersCollection $filters)
|
||||
{
|
||||
foreach ($filters as $filter) {
|
||||
if (!($filter instanceof ComplexFilterInterface)) {
|
||||
throw new RuntimeException ('For AdvancedMedia you can set filters collection'
|
||||
. ' contains only objects that implement ComplexFilterInterface!');
|
||||
}
|
||||
}
|
||||
|
||||
return parent::setFiltersCollection($filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getInitialParameters()
|
||||
{
|
||||
return $this->initialParameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $initialParameters
|
||||
*
|
||||
* @return AdvancedMedia
|
||||
*/
|
||||
public function setInitialParameters(array $initialParameters)
|
||||
{
|
||||
$this->initialParameters = $initialParameters;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getAdditionalParameters()
|
||||
{
|
||||
return $this->additionalParameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $additionalParameters
|
||||
*
|
||||
* @return AdvancedMedia
|
||||
*/
|
||||
public function setAdditionalParameters(array $additionalParameters)
|
||||
{
|
||||
$this->additionalParameters = $additionalParameters;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getInputs()
|
||||
{
|
||||
return $this->inputs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getInputsCount()
|
||||
{
|
||||
return count($this->inputs);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getFinalCommand()
|
||||
{
|
||||
return implode(' ', $this->buildCommand());
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the streams for output.
|
||||
*
|
||||
* @param string[] $outs Output labels of the -filter_complex part.
|
||||
* @param FormatInterface $format Format of the output file.
|
||||
* @param string $outputFilename Output filename.
|
||||
* @param bool $forceDisableAudio
|
||||
* @param bool $forceDisableVideo
|
||||
*
|
||||
* @return $this
|
||||
* @see https://ffmpeg.org/ffmpeg.html#Manual-stream-selection
|
||||
*/
|
||||
public function map(
|
||||
array $outs,
|
||||
FormatInterface $format,
|
||||
$outputFilename,
|
||||
$forceDisableAudio = false,
|
||||
$forceDisableVideo = false
|
||||
) {
|
||||
$commands = array();
|
||||
foreach ($outs as $label) {
|
||||
$commands[] = '-map';
|
||||
$commands[] = $label;
|
||||
}
|
||||
|
||||
// Apply format params.
|
||||
$commands = array_merge($commands,
|
||||
$this->applyFormatParams($format, $forceDisableAudio, $forceDisableVideo));
|
||||
|
||||
// Set output file.
|
||||
$commands[] = $outputFilename;
|
||||
|
||||
// Create a listener.
|
||||
if ($format instanceof ProgressableInterface) {
|
||||
$listener = $format->createProgressListener($this, $this->ffprobe, 1, 1, 0);
|
||||
$this->listeners = array_merge($this->listeners, $listener);
|
||||
}
|
||||
|
||||
$this->mapCommands = array_merge($this->mapCommands, $commands);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply added filters and execute ffmpeg command.
|
||||
*
|
||||
* @return void
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
$this->assertFiltersAreCompatibleToCurrentFFMpegVersion();
|
||||
$command = $this->buildCommand();
|
||||
|
||||
try {
|
||||
$this->driver->command($command, false, $this->listeners);
|
||||
} catch (ExecutionFailureException $e) {
|
||||
throw new RuntimeException('Encoding failed', $e->getCode(), $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormatInterface $format
|
||||
* @param bool $forceDisableAudio
|
||||
* @param bool $forceDisableVideo
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function applyFormatParams(
|
||||
FormatInterface $format,
|
||||
$forceDisableAudio = false,
|
||||
$forceDisableVideo = false
|
||||
) {
|
||||
// Set format params.
|
||||
$commands = array();
|
||||
if (!$forceDisableVideo && $format instanceof VideoInterface) {
|
||||
if ($format->getVideoCodec() !== null) {
|
||||
$commands[] = '-vcodec';
|
||||
$commands[] = $format->getVideoCodec();
|
||||
}
|
||||
// If the user passed some additional format parameters.
|
||||
if ($format->getAdditionalParameters() !== null) {
|
||||
$commands = array_merge($commands, $format->getAdditionalParameters());
|
||||
}
|
||||
}
|
||||
if (!$forceDisableAudio && $format instanceof AudioInterface) {
|
||||
if ($format->getAudioCodec() !== null) {
|
||||
$commands[] = '-acodec';
|
||||
$commands[] = $format->getAudioCodec();
|
||||
}
|
||||
if ($format->getAudioKiloBitrate() !== null) {
|
||||
$commands[] = '-b:a';
|
||||
$commands[] = $format->getAudioKiloBitrate() . 'k';
|
||||
}
|
||||
if ($format->getAudioChannels() !== null) {
|
||||
$commands[] = '-ac';
|
||||
$commands[] = $format->getAudioChannels();
|
||||
}
|
||||
}
|
||||
|
||||
// If the user passed some extra parameters.
|
||||
if ($format->getExtraParams()) {
|
||||
$commands = array_merge($commands, $format->getExtraParams());
|
||||
}
|
||||
|
||||
return $commands;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ComplexFilterInterface $filter
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function applyComplexFilter(ComplexFilterInterface $filter)
|
||||
{
|
||||
/** @var $format VideoInterface */
|
||||
$filterCommands = $filter->applyComplex($this);
|
||||
foreach ($filterCommands as $index => $command) {
|
||||
if ($command === '-vf' || $command === '-filter:v' || $command === '-filter_complex') {
|
||||
unset ($filterCommands[$index]);
|
||||
}
|
||||
}
|
||||
|
||||
$strCommand = implode(' ', $filterCommands);
|
||||
|
||||
// Compatibility with the some existed filters:
|
||||
// If the command contains [in], just replace it to inLabel. If not - to add it manually.
|
||||
if (stripos($strCommand, '[in]') !== false) {
|
||||
$strCommand = str_replace('[in]', $filter->getInLabels(), $strCommand);
|
||||
$in = '';
|
||||
} else {
|
||||
$in = $filter->getInLabels();
|
||||
}
|
||||
|
||||
// If the command contains [out], just replace it to outLabel. If not - to add it manually.
|
||||
if (stripos($strCommand, '[out]') !== false) {
|
||||
$strCommand = str_replace('[out]', $filter->getOutLabels(), $strCommand);
|
||||
$out = '';
|
||||
} else {
|
||||
$out = $filter->getOutLabels();
|
||||
}
|
||||
|
||||
return $in . $strCommand . $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
protected function assertFiltersAreCompatibleToCurrentFFMpegVersion()
|
||||
{
|
||||
$messages = array();
|
||||
$currentVersion = $this->getFFMpegDriver()->getVersion();
|
||||
/** @var ComplexFilterInterface $filter */
|
||||
foreach ($this->filters as $filter) {
|
||||
if (version_compare($currentVersion, $filter->getMinimalFFMpegVersion(), '<')) {
|
||||
$messages[] = $filter->getName() . ' filter is supported starting from '
|
||||
. $filter->getMinimalFFMpegVersion() . ' ffmpeg version';
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($messages)) {
|
||||
throw new RuntimeException(implode('; ', $messages)
|
||||
. '; your ffmpeg version is ' . $currentVersion);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function buildCommand()
|
||||
{
|
||||
$globalOptions = array('threads', 'filter_threads', 'filter_complex_threads');
|
||||
return array_merge(array('-y'),
|
||||
$this->buildConfiguredGlobalOptions($globalOptions),
|
||||
$this->getInitialParameters(),
|
||||
$this->buildInputsPart($this->inputs),
|
||||
$this->buildComplexFilterPart($this->filters),
|
||||
$this->mapCommands,
|
||||
$this->getAdditionalParameters()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $optionNames
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function buildConfiguredGlobalOptions($optionNames)
|
||||
{
|
||||
$commands = array();
|
||||
foreach ($optionNames as $optionName) {
|
||||
if (!$this->driver->getConfiguration()->has('ffmpeg.' . $optionName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$commands[] = '-' . $optionName;
|
||||
$commands[] = $this->driver->getConfiguration()->get('ffmpeg.' . $optionName);
|
||||
}
|
||||
|
||||
return $commands;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build inputs part of the ffmpeg command.
|
||||
*
|
||||
* @param string[] $inputs
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function buildInputsPart(array $inputs)
|
||||
{
|
||||
$commands = array();
|
||||
foreach ($inputs as $input) {
|
||||
$commands[] = '-i';
|
||||
$commands[] = $input;
|
||||
}
|
||||
|
||||
return $commands;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build "-filter_complex" part of the ffmpeg command.
|
||||
*
|
||||
* @param FiltersCollection $complexFilters
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function buildComplexFilterPart(FiltersCollection $complexFilters)
|
||||
{
|
||||
$commands = array();
|
||||
/** @var ComplexFilterInterface $filter */
|
||||
foreach ($complexFilters as $filter) {
|
||||
$filterCommand = $this->applyComplexFilter($filter);
|
||||
$commands[] = $filterCommand;
|
||||
}
|
||||
|
||||
if (empty($commands)) {
|
||||
return array();
|
||||
}
|
||||
return array('-filter_complex', implode(';', $commands));
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,8 @@ use PHPUnit\Framework\TestCase;
|
|||
* This is a BC Layer to support phpunit 4.8 needed for php <= 5.5.
|
||||
*/
|
||||
if (class_exists('PHPUnit_Runner_Version')
|
||||
&& version_compare(\PHPUnit_Runner_Version::id(), '5', '<')) {
|
||||
&& version_compare(\PHPUnit_Runner_Version::id(), '5', '<')
|
||||
) {
|
||||
class BaseTestCase extends TestCase
|
||||
{
|
||||
public static function assertScalar($value, $message = '')
|
||||
|
|
@ -40,6 +41,16 @@ if (class_exists('PHPUnit_Runner_Version')
|
|||
{
|
||||
$this->setExpectedException($exception, $message);
|
||||
}
|
||||
|
||||
public static function assertStringContainsString($needle, $haystack, $message = '')
|
||||
{
|
||||
self::assertContains($needle, $haystack, $message);
|
||||
}
|
||||
|
||||
public static function assertStringNotContainsString($needle, $haystack, $message = '')
|
||||
{
|
||||
self::assertNotContains($needle, $haystack, $message);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
class BaseTestCase extends TestCase
|
||||
|
|
|
|||
268
tests/Functional/AdvancedMediaTest.php
Normal file
268
tests/Functional/AdvancedMediaTest.php
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\FFMpeg\Functional;
|
||||
|
||||
use FFMpeg\Coordinate\Dimension;
|
||||
use FFMpeg\Filters\AdvancedMedia\TestSrcFilter;
|
||||
use FFMpeg\Filters\AdvancedMedia\XStackFilter;
|
||||
use FFMpeg\Format\Audio\Mp3;
|
||||
use FFMpeg\Format\Video\X264;
|
||||
|
||||
class AdvancedMediaTest extends FunctionalTestCase
|
||||
{
|
||||
/**
|
||||
* Path prefix to avoid conflicts with another tests.
|
||||
*/
|
||||
const OUTPUT_PATH_PREFIX = 'output/advanced_media_';
|
||||
|
||||
public function testRunWithoutComplexFilterTestExtractAudio()
|
||||
{
|
||||
$ffmpeg = $this->getFFMpeg();
|
||||
$inputs = array(realpath(__DIR__ . '/../files/Test.ogv'));
|
||||
$format = new Mp3();
|
||||
$output = __DIR__ . '/' . self::OUTPUT_PATH_PREFIX . 'extracted_with_map.mp3';
|
||||
|
||||
// You can run it without -filter_complex, just using -map.
|
||||
$advancedMedia = $ffmpeg->openAdvanced($inputs);
|
||||
$advancedMedia
|
||||
->map(array('0:a'), $format, $output)
|
||||
->save();
|
||||
|
||||
$this->assertFileExists($output);
|
||||
$this->assertEquals('MP2/3 (MPEG audio layer 2/3)',
|
||||
$ffmpeg->open($output)->getFormat()->get('format_long_name'));
|
||||
unlink($output);
|
||||
}
|
||||
|
||||
public function testAudio()
|
||||
{
|
||||
$ffmpeg = $this->getFFMpeg();
|
||||
$inputs = array(realpath(__DIR__ . '/../files/Audio.mp3'));
|
||||
$format = new Mp3();
|
||||
$format->setAudioKiloBitrate(30);
|
||||
$output = __DIR__ . '/' . self::OUTPUT_PATH_PREFIX . 'audio_test.mp3';
|
||||
|
||||
$advancedMedia = $ffmpeg->openAdvanced($inputs);
|
||||
$advancedMedia
|
||||
->map(array('0:a'), $format, $output)
|
||||
->save();
|
||||
|
||||
$this->assertFileExists($output);
|
||||
$this->assertEquals('MP2/3 (MPEG audio layer 2/3)',
|
||||
$ffmpeg->open($output)->getFormat()->get('format_long_name'));
|
||||
unlink($output);
|
||||
}
|
||||
|
||||
public function testMultipleInputs()
|
||||
{
|
||||
$ffmpeg = $this->getFFMpeg();
|
||||
$inputs = array(
|
||||
realpath(__DIR__ . '/../files/portrait.MOV'),
|
||||
realpath(__DIR__ . '/../files/portrait.MOV')
|
||||
);
|
||||
$format = new X264('aac', 'libx264');
|
||||
$output = __DIR__ . '/' . self::OUTPUT_PATH_PREFIX . 'multiple_inputs_test.mp4';
|
||||
|
||||
$advancedMedia = $ffmpeg->openAdvanced($inputs);
|
||||
$advancedMedia->filters()
|
||||
->custom('[0:v][1:v]', 'hstack', '[v]');
|
||||
$advancedMedia
|
||||
->map(array('0:a', '[v]'), $format, $output)
|
||||
->save();
|
||||
|
||||
$this->assertFileExists($output);
|
||||
$this->assertEquals('QuickTime / MOV',
|
||||
$ffmpeg->open($output)->getFormat()->get('format_long_name'));
|
||||
unlink($output);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \FFMpeg\Media\AdvancedMedia::map
|
||||
*/
|
||||
public function testMultipleOutputsTestAbsenceOfInputs()
|
||||
{
|
||||
$ffmpeg = $this->getFFMpeg();
|
||||
// in this test we use only computed inputs
|
||||
// and can ignore -i part of the command, pass empty inputs array.
|
||||
$inputs = array();
|
||||
$formatX264 = new X264('aac', 'libx264');
|
||||
$formatMp3 = new Mp3();
|
||||
|
||||
$outputMp3 = __DIR__ . '/' . self::OUTPUT_PATH_PREFIX . 'test_multiple_outputs.mp3';
|
||||
$outputVideo1 = __DIR__ . '/' . self::OUTPUT_PATH_PREFIX . 'test_multiple_outputs_v1.mp4';
|
||||
$outputVideo2 = __DIR__ . '/' . self::OUTPUT_PATH_PREFIX . 'test_multiple_outputs_v2.mp4';
|
||||
|
||||
$advancedMedia = $ffmpeg->openAdvanced($inputs);
|
||||
$advancedMedia->filters()
|
||||
->sine('[a]', 5)
|
||||
->testSrc('[v1]', TestSrcFilter::TESTSRC, '160x120', 5)
|
||||
->testSrc('[v2]', TestSrcFilter::TESTSRC, '160x120', 5)
|
||||
->custom('[v1]', 'negate', '[v1negate]')
|
||||
->custom('[v2]', 'edgedetect', '[v2edgedetect]');
|
||||
$advancedMedia
|
||||
->map(array('[a]'), $formatMp3, $outputMp3)
|
||||
->map(array('[v1negate]'), $formatX264, $outputVideo1)
|
||||
->map(array('[v2edgedetect]'), $formatX264, $outputVideo2)
|
||||
->save();
|
||||
|
||||
|
||||
$this->assertFileExists($outputMp3);
|
||||
$this->assertEquals('MP2/3 (MPEG audio layer 2/3)',
|
||||
$ffmpeg->open($outputMp3)->getFormat()->get('format_long_name'));
|
||||
unlink($outputMp3);
|
||||
|
||||
$this->assertFileExists($outputVideo1);
|
||||
$this->assertEquals('QuickTime / MOV',
|
||||
$ffmpeg->open($outputVideo1)->getFormat()->get('format_long_name'));
|
||||
unlink($outputVideo1);
|
||||
|
||||
$this->assertFileExists($outputVideo2);
|
||||
$this->assertEquals('QuickTime / MOV',
|
||||
$ffmpeg->open($outputVideo2)->getFormat()->get('format_long_name'));
|
||||
unlink($outputVideo2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \FFMpeg\Filters\AdvancedMedia\TestSrcFilter
|
||||
* @covers \FFMpeg\Filters\AdvancedMedia\SineFilter
|
||||
*/
|
||||
public function testTestSrcFilterTestSineFilter()
|
||||
{
|
||||
$ffmpeg = $this->getFFMpeg();
|
||||
$inputs = array(realpath(__DIR__ . '/../files/Test.ogv'));
|
||||
$format = new X264('aac', 'libx264');
|
||||
$output = __DIR__ . '/' . self::OUTPUT_PATH_PREFIX . 'testsrc.mp4';
|
||||
|
||||
$advancedMedia = $ffmpeg->openAdvanced($inputs);
|
||||
$advancedMedia->filters()
|
||||
->sine('[a]', 10)
|
||||
->testSrc('[v]', TestSrcFilter::TESTSRC, '160x120', 10);
|
||||
$advancedMedia
|
||||
->map(array('[a]', '[v]'), $format, $output)
|
||||
->save();
|
||||
|
||||
$this->assertFileExists($output);
|
||||
$this->assertEquals('QuickTime / MOV',
|
||||
$ffmpeg->open($output)->getFormat()->get('format_long_name'));
|
||||
unlink($output);
|
||||
}
|
||||
|
||||
/**
|
||||
* XStack filter is supported starting from 4.1 ffmpeg version.
|
||||
*
|
||||
* @covers \FFMpeg\Filters\AdvancedMedia\XStackFilter
|
||||
* @covers \FFMpeg\Filters\AdvancedMedia\SineFilter
|
||||
*/
|
||||
public function testXStackFilter()
|
||||
{
|
||||
$xStack = new XStackFilter('', 0);
|
||||
$ffmpeg = $this->getFFMpeg();
|
||||
$ffmpegVersion = $ffmpeg->getFFMpegDriver()->getVersion();
|
||||
if (version_compare($ffmpegVersion, $xStack->getMinimalFFMpegVersion(), '<')) {
|
||||
$this->markTestSkipped('XStack filter is supported starting from ffmpeg version '
|
||||
. $xStack->getMinimalFFMpegVersion() . ', your version is '
|
||||
. $ffmpegVersion);
|
||||
return;
|
||||
}
|
||||
|
||||
$inputs = array(realpath(__DIR__ . '/../files/Test.ogv'));
|
||||
$format = new X264('aac', 'libx264');
|
||||
$output = __DIR__ . '/' . self::OUTPUT_PATH_PREFIX . 'xstack_test.mp4';
|
||||
|
||||
$advancedMedia = $ffmpeg->openAdvanced($inputs);
|
||||
$advancedMedia->filters()
|
||||
->sine('[a]', 5)
|
||||
->testSrc('[v1]', TestSrcFilter::TESTSRC, '160x120', 5)
|
||||
->testSrc('[v2]', TestSrcFilter::TESTSRC, '160x120', 5)
|
||||
->testSrc('[v3]', TestSrcFilter::TESTSRC, '160x120', 5)
|
||||
->testSrc('[v4]', TestSrcFilter::TESTSRC, '160x120', 5)
|
||||
->xStack('[v1][v2][v3][v4]',
|
||||
XStackFilter::LAYOUT_2X2, 4, '[v]');
|
||||
$advancedMedia
|
||||
->map(array('[a]', '[v]'), $format, $output)
|
||||
->save();
|
||||
|
||||
$this->assertFileExists($output);
|
||||
$this->assertEquals('QuickTime / MOV',
|
||||
$ffmpeg->open($output)->getFormat()->get('format_long_name'));
|
||||
unlink($output);
|
||||
}
|
||||
|
||||
public function testOfCompatibilityWithExistedFilters()
|
||||
{
|
||||
$ffmpeg = $this->getFFMpeg();
|
||||
$inputs = array(realpath(__DIR__ . '/../files/Test.ogv'));
|
||||
$watermark = realpath(__DIR__ . '/../files/watermark.png');
|
||||
$format = new X264('aac', 'libx264');
|
||||
$output = __DIR__ . '/' . self::OUTPUT_PATH_PREFIX . 'test_of_compatibility_with_existed_filters.mp4';
|
||||
|
||||
$advancedMedia = $ffmpeg->openAdvanced($inputs);
|
||||
$advancedMedia->filters()
|
||||
// For unknown reasons WatermarkFilter produce an error on Windows,
|
||||
// because the path to the watermark becomes corrupted.
|
||||
// This behaviour related with Alchemy\BinaryDriver\AbstractBinary::command().
|
||||
// The path inside filter becomes like
|
||||
// "D:ServerswwwPHP-FFMpegtestsfileswatermark.png" (without slashes).
|
||||
// But on Linux systems filter works as expected.
|
||||
//->watermark('[0:v]', $watermark, '[v]')
|
||||
->pad('[0:v]', new Dimension(300, 100), '[v]');
|
||||
$advancedMedia
|
||||
->map(array('0:a', '[v]'), $format, $output)
|
||||
->save();
|
||||
|
||||
$this->assertFileExists($output);
|
||||
$this->assertEquals('QuickTime / MOV',
|
||||
$ffmpeg->open($output)->getFormat()->get('format_long_name'));
|
||||
unlink($output);
|
||||
}
|
||||
|
||||
public function testForceDisableAudio()
|
||||
{
|
||||
$ffmpeg = $this->getFFMpeg();
|
||||
$format = new X264();
|
||||
|
||||
$advancedMedia1 = $ffmpeg->openAdvanced(array(__FILE__));
|
||||
$advancedMedia1
|
||||
->map(array('test'), $format, 'outputFile.mp4', false);
|
||||
$this->assertStringContainsString('acodec', $advancedMedia1->getFinalCommand());
|
||||
|
||||
$advancedMedia2 = $ffmpeg->openAdvanced(array(__FILE__));
|
||||
$advancedMedia2
|
||||
->map(array('test'), $format, 'outputFile.mp4', true);
|
||||
$this->assertStringNotContainsString('acodec', $advancedMedia2->getFinalCommand());
|
||||
}
|
||||
|
||||
public function testForceDisableVideo()
|
||||
{
|
||||
$ffmpeg = $this->getFFMpeg();
|
||||
$format = new X264();
|
||||
|
||||
$advancedMedia1 = $ffmpeg->openAdvanced(array(__FILE__));
|
||||
$advancedMedia1->map(array('test'), $format,
|
||||
'outputFile.mp4', false, false);
|
||||
$this->assertStringContainsString('vcodec', $advancedMedia1->getFinalCommand());
|
||||
|
||||
$advancedMedia2 = $ffmpeg->openAdvanced(array(__FILE__));
|
||||
$advancedMedia2->map(array('test'), $format,
|
||||
'outputFile.mp4', false, true);
|
||||
$this->assertStringNotContainsString('vcodec', $advancedMedia2->getFinalCommand());
|
||||
}
|
||||
|
||||
public function testGlobalOptions()
|
||||
{
|
||||
$configuration = array(
|
||||
'ffmpeg.threads' => 3,
|
||||
'ffmpeg.filter_threads' => 13,
|
||||
'ffmpeg.filter_complex_threads' => 24,
|
||||
);
|
||||
|
||||
$ffmpeg = $this->getFFMpeg($configuration);
|
||||
$advancedMedia = $ffmpeg->openAdvanced(array(__FILE__));
|
||||
$command = $advancedMedia->getFinalCommand();
|
||||
|
||||
foreach ($configuration as $optionName => $optionValue) {
|
||||
$optionName = str_replace('ffmpeg.', '', $optionName);
|
||||
$this->assertStringContainsString('-' . $optionName . ' ' . $optionValue, $command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,10 +8,12 @@ use Tests\FFMpeg\BaseTestCase;
|
|||
abstract class FunctionalTestCase extends BaseTestCase
|
||||
{
|
||||
/**
|
||||
* @param array $configuration
|
||||
*
|
||||
* @return FFMpeg
|
||||
*/
|
||||
public function getFFMpeg()
|
||||
public function getFFMpeg($configuration = array())
|
||||
{
|
||||
return FFMpeg::create(array('timeout' => 300));
|
||||
return FFMpeg::create(array_merge(array('timeout' => 300), $configuration));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
35
tests/Unit/Media/AdvancedMediaTest.php
Normal file
35
tests/Unit/Media/AdvancedMediaTest.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\FFMpeg\Unit\Media;
|
||||
|
||||
use FFMpeg\Media\AdvancedMedia;
|
||||
|
||||
class AdvancedMediaTest extends AbstractMediaTestCase
|
||||
{
|
||||
public function testGetInputs()
|
||||
{
|
||||
$driver = $this->getFFMpegDriverMock();
|
||||
$ffprobe = $this->getFFProbeMock();
|
||||
|
||||
$advancedMedia = new AdvancedMedia(array(__FILE__, __FILE__), $driver, $ffprobe);
|
||||
$this->assertSame(array(__FILE__, __FILE__), $advancedMedia->getInputs());
|
||||
}
|
||||
|
||||
public function testGetInputsCount()
|
||||
{
|
||||
$driver = $this->getFFMpegDriverMock();
|
||||
$ffprobe = $this->getFFProbeMock();
|
||||
|
||||
$advancedMedia = new AdvancedMedia(array(__FILE__, __FILE__), $driver, $ffprobe);
|
||||
$this->assertEquals(2, $advancedMedia->getInputsCount());
|
||||
}
|
||||
|
||||
public function testFiltersReturnFilters()
|
||||
{
|
||||
$driver = $this->getFFMpegDriverMock();
|
||||
$ffprobe = $this->getFFProbeMock();
|
||||
|
||||
$advancedMedia = new AdvancedMedia(array(__FILE__, __FILE__), $driver, $ffprobe);
|
||||
$this->assertInstanceOf('FFMpeg\Filters\AdvancedMedia\ComplexFilters', $advancedMedia->filters());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue