Added the ComplexMedia type and the possibility to use -filter_complex and -map features. Added some built-in complex filters. Added the unit and functional tests of the new features. README.md has been updated.
This commit is contained in:
parent
89b0c2b4d0
commit
f20ad8a82e
17 changed files with 1583 additions and 7 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).
|
||||
|
||||
### ComplexMedia
|
||||
ComplexMedia 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.
|
||||
ComplexMedia already contains some built-in filters.
|
||||
|
||||
#### Base usage
|
||||
For example:
|
||||
|
||||
```php
|
||||
$complexMedia = $ffmpeg->openComplex(array ('video_1.mp4', 'video_2.mp4'));
|
||||
$complexMedia->filters()
|
||||
->custom('[0:v][1:v]', 'hstack', '[v]');
|
||||
$complexMedia
|
||||
->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 ComplexMedia. 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',
|
||||
);
|
||||
|
||||
$complexMedia = $ffmpeg->openComplex($inputs);
|
||||
$complexMedia->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]');
|
||||
$complexMedia
|
||||
->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 map!
|
||||
You do not have to use `-filter_complex`. For example, just extract the audio from the video:
|
||||
|
||||
```php
|
||||
$complexMedia = $ffmpeg->openComplex(array ('video.mp4'));
|
||||
$complexMedia
|
||||
->map(array('0:a'), new Mp3(), 'output.mp3')
|
||||
->save();
|
||||
```
|
||||
|
||||
#### Customisation
|
||||
If you need you can extra customize the result ffmpeg command of the ComplexMedia:
|
||||
|
||||
```php
|
||||
$complexMedia = $ffmpeg->openComplex($inputs);
|
||||
$complexMedia
|
||||
->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 FFMpeg\Driver\FFMpegDriver;
|
|||
use FFMpeg\Exception\InvalidArgumentException;
|
||||
use FFMpeg\Exception\RuntimeException;
|
||||
use FFMpeg\Media\Audio;
|
||||
use FFMpeg\Media\ComplexMedia;
|
||||
use FFMpeg\Media\Video;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
|
|
@ -102,6 +103,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 ComplexMedia
|
||||
*/
|
||||
public function openComplex($inputs)
|
||||
{
|
||||
return new ComplexMedia($inputs, $this->driver, $this->ffprobe);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new FFMpeg instance.
|
||||
*
|
||||
|
|
|
|||
61
src/FFMpeg/Filters/ComplexMedia/ANullSrcFilter.php
Normal file
61
src/FFMpeg/Filters/ComplexMedia/ANullSrcFilter.php
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
namespace FFMpeg\Filters\ComplexMedia;
|
||||
|
||||
use FFMpeg\Media\ComplexMedia;
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function applyComplex(ComplexMedia $media)
|
||||
{
|
||||
return array(
|
||||
'-filter_complex',
|
||||
'anullsrc' . $this->buildFilterOptions(array(
|
||||
'channel_layout' => $this->channelLayout,
|
||||
'sample_rate' => $this->sampleRate,
|
||||
'nb_samples' => $this->nbSamples,
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
52
src/FFMpeg/Filters/ComplexMedia/AbstractComplexFilter.php
Normal file
52
src/FFMpeg/Filters/ComplexMedia/AbstractComplexFilter.php
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
namespace FFMpeg\Filters\ComplexMedia;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 '';
|
||||
}
|
||||
}
|
||||
18
src/FFMpeg/Filters/ComplexMedia/ComplexCompatibleFilter.php
Normal file
18
src/FFMpeg/Filters/ComplexMedia/ComplexCompatibleFilter.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace FFMpeg\Filters\ComplexMedia;
|
||||
|
||||
use FFMpeg\Filters\FilterInterface;
|
||||
use FFMpeg\Media\ComplexMedia;
|
||||
|
||||
interface ComplexCompatibleFilter extends FilterInterface
|
||||
{
|
||||
/**
|
||||
* Apply the complex filter to the given media.
|
||||
*
|
||||
* @param ComplexMedia $media
|
||||
*
|
||||
* @return string[] An array of arguments.
|
||||
*/
|
||||
public function applyComplex(ComplexMedia $media);
|
||||
}
|
||||
80
src/FFMpeg/Filters/ComplexMedia/ComplexFilterContainer.php
Normal file
80
src/FFMpeg/Filters/ComplexMedia/ComplexFilterContainer.php
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
namespace FFMpeg\Filters\ComplexMedia;
|
||||
|
||||
use FFMpeg\Media\ComplexMedia;
|
||||
|
||||
/**
|
||||
* Container for the complex 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function applyComplex(ComplexMedia $media)
|
||||
{
|
||||
return $this->baseFilter->applyComplex($media);
|
||||
}
|
||||
}
|
||||
16
src/FFMpeg/Filters/ComplexMedia/ComplexFilterInterface.php
Normal file
16
src/FFMpeg/Filters/ComplexMedia/ComplexFilterInterface.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace FFMpeg\Filters\ComplexMedia;
|
||||
|
||||
interface ComplexFilterInterface extends ComplexCompatibleFilter
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getInLabels();
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getOutLabels();
|
||||
}
|
||||
169
src/FFMpeg/Filters/ComplexMedia/ComplexFilters.php
Normal file
169
src/FFMpeg/Filters/ComplexMedia/ComplexFilters.php
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
<?php
|
||||
|
||||
namespace FFMpeg\Filters\ComplexMedia;
|
||||
|
||||
use FFMpeg\Coordinate\Dimension;
|
||||
use FFMpeg\Filters\Video\PadFilter;
|
||||
use FFMpeg\Filters\Video\WatermarkFilter;
|
||||
use FFMpeg\Media\ComplexMedia;
|
||||
|
||||
class ComplexFilters
|
||||
{
|
||||
/**
|
||||
* @var ComplexMedia
|
||||
*/
|
||||
protected $media;
|
||||
|
||||
/**
|
||||
* ComplexFilters constructor.
|
||||
*
|
||||
* @param ComplexMedia $media
|
||||
*/
|
||||
public function __construct(ComplexMedia $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;
|
||||
}
|
||||
}
|
||||
33
src/FFMpeg/Filters/ComplexMedia/CustomComplexFilter.php
Normal file
33
src/FFMpeg/Filters/ComplexMedia/CustomComplexFilter.php
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace FFMpeg\Filters\ComplexMedia;
|
||||
|
||||
use FFMpeg\Media\ComplexMedia;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function applyComplex(ComplexMedia $media)
|
||||
{
|
||||
return array('-filter_complex', $this->filter);
|
||||
}
|
||||
}
|
||||
77
src/FFMpeg/Filters/ComplexMedia/SineFilter.php
Normal file
77
src/FFMpeg/Filters/ComplexMedia/SineFilter.php
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
|
||||
namespace FFMpeg\Filters\ComplexMedia;
|
||||
|
||||
use FFMpeg\Media\ComplexMedia;
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the complex filter to the given media.
|
||||
*
|
||||
* @param ComplexMedia $media
|
||||
*
|
||||
* @return string[] An array of arguments.
|
||||
*/
|
||||
public function applyComplex(ComplexMedia $media)
|
||||
{
|
||||
return array(
|
||||
'-filter_complex',
|
||||
'sine' . $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,
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
211
src/FFMpeg/Filters/ComplexMedia/TestSrcFilter.php
Normal file
211
src/FFMpeg/Filters/ComplexMedia/TestSrcFilter.php
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
<?php
|
||||
|
||||
namespace FFMpeg\Filters\ComplexMedia;
|
||||
|
||||
use FFMpeg\Media\ComplexMedia;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function applyComplex(ComplexMedia $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
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
71
src/FFMpeg/Filters/ComplexMedia/XStackFilter.php
Normal file
71
src/FFMpeg/Filters/ComplexMedia/XStackFilter.php
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
namespace FFMpeg\Filters\ComplexMedia;
|
||||
|
||||
use FFMpeg\Media\ComplexMedia;
|
||||
|
||||
/**
|
||||
* "xstack" filter.
|
||||
* Warning: this filter is supported starting from 4.1 ffmpeg version.
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function applyComplex(ComplexMedia $media)
|
||||
{
|
||||
return array(
|
||||
'-filter_complex',
|
||||
'xstack' . $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\ComplexMedia\ComplexCompatibleFilter;
|
||||
use FFMpeg\Format\VideoInterface;
|
||||
use FFMpeg\Media\ComplexMedia;
|
||||
use FFMpeg\Media\Video;
|
||||
|
||||
class PadFilter implements VideoFilterInterface
|
||||
class PadFilter implements VideoFilterInterface, ComplexCompatibleFilter
|
||||
{
|
||||
/** @var Dimension */
|
||||
private $dimension;
|
||||
|
|
@ -48,11 +50,30 @@ class PadFilter implements VideoFilterInterface
|
|||
* {@inheritdoc}
|
||||
*/
|
||||
public function apply(Video $video, VideoInterface $format)
|
||||
{
|
||||
return $this->getCommands();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function applyComplex(ComplexMedia $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\ComplexMedia\ComplexCompatibleFilter;
|
||||
use FFMpeg\Format\VideoInterface;
|
||||
use FFMpeg\Media\ComplexMedia;
|
||||
use FFMpeg\Media\Video;
|
||||
|
||||
class WatermarkFilter implements VideoFilterInterface
|
||||
class WatermarkFilter implements VideoFilterInterface, ComplexCompatibleFilter
|
||||
{
|
||||
/** @var string */
|
||||
private $watermarkPath;
|
||||
|
|
@ -47,6 +49,22 @@ class WatermarkFilter implements VideoFilterInterface
|
|||
* {@inheritdoc}
|
||||
*/
|
||||
public function apply(Video $video, VideoInterface $format)
|
||||
{
|
||||
return $this->getCommands();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function applyComplex(ComplexMedia $media)
|
||||
{
|
||||
return $this->getCommands();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function getCommands()
|
||||
{
|
||||
$position = isset($this->coordinates['position']) ? $this->coordinates['position'] : 'absolute';
|
||||
|
||||
|
|
@ -55,7 +73,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 +81,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 +93,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]',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
381
src/FFMpeg/Media/ComplexMedia.php
Normal file
381
src/FFMpeg/Media/ComplexMedia.php
Normal file
|
|
@ -0,0 +1,381 @@
|
|||
<?php
|
||||
|
||||
namespace FFMpeg\Media;
|
||||
|
||||
use Alchemy\BinaryDriver\Exception\ExecutionFailureException;
|
||||
use FFMpeg\Driver\FFMpegDriver;
|
||||
use FFMpeg\Exception\RuntimeException;
|
||||
use FFMpeg\FFProbe;
|
||||
use FFMpeg\Filters\ComplexMedia\ComplexCompatibleFilter;
|
||||
use FFMpeg\Filters\ComplexMedia\ComplexFilterContainer;
|
||||
use FFMpeg\Filters\ComplexMedia\ComplexFilterInterface;
|
||||
use FFMpeg\Filters\ComplexMedia\ComplexFilters;
|
||||
use FFMpeg\Filters\FiltersCollection;
|
||||
use FFMpeg\Format\AudioInterface;
|
||||
use FFMpeg\Format\FormatInterface;
|
||||
use FFMpeg\Format\ProgressableInterface;
|
||||
use FFMpeg\Format\VideoInterface;
|
||||
|
||||
/**
|
||||
* Complex media may have multiple inputs and multiple outputs.
|
||||
* This class accepts only filters for -filter_complex.
|
||||
* But you can set initial and additional parameters of the ffmpeg command.
|
||||
*
|
||||
* @see http://trac.ffmpeg.org/wiki/Creating%20multiple%20outputs
|
||||
*/
|
||||
class ComplexMedia extends AbstractMediaType
|
||||
{
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private $inputs;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private $initialParameters;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private $additionalParameters;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private $mapCommands;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $listeners;
|
||||
|
||||
/**
|
||||
* ComplexMedia 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 ComplexMedia you can set filters collection'
|
||||
. ' contains only objects that implement ComplexFilterInterface!');
|
||||
}
|
||||
}
|
||||
|
||||
return parent::setFiltersCollection($filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
* @return void
|
||||
*/
|
||||
public function getInitialParameters()
|
||||
{
|
||||
return $this->initialParameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $initialParameters
|
||||
*
|
||||
* @return ComplexMedia
|
||||
*/
|
||||
public function setInitialParameters(array $initialParameters)
|
||||
{
|
||||
$this->initialParameters = $initialParameters;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
* @return void
|
||||
*/
|
||||
public function getAdditionalParameters()
|
||||
{
|
||||
return $this->additionalParameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $additionalParameters
|
||||
*
|
||||
* @return ComplexMedia
|
||||
*/
|
||||
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());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $outs Output labels of the -filter_complex part.
|
||||
* @param FormatInterface $format
|
||||
* @param string $outputPathfile
|
||||
* @param bool $forceDisableAudio
|
||||
* @param bool $forceDisableVideo
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function map(
|
||||
array $outs,
|
||||
FormatInterface $format,
|
||||
$outputPathfile,
|
||||
$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[] = $outputPathfile;
|
||||
|
||||
// Create 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 ComplexMedia
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
$command = $this->buildCommand();
|
||||
|
||||
try {
|
||||
$this->driver->command($command, false, $this->listeners);
|
||||
} catch (ExecutionFailureException $e) {
|
||||
throw new RuntimeException('Encoding failed', $e->getCode(), $e);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 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 array
|
||||
*/
|
||||
protected function buildCommand()
|
||||
{
|
||||
$commands = array('-y');
|
||||
if ($this->driver->getConfiguration()->has('ffmpeg.threads')) {
|
||||
$commands[] = '-threads';
|
||||
$commands[] = $this->driver->getConfiguration()->get('ffmpeg.threads');
|
||||
}
|
||||
|
||||
return array_merge($commands,
|
||||
$this->getInitialParameters(),
|
||||
$this->buildInputsPart($this->inputs),
|
||||
$this->buildComplexFilterPart($this->filters),
|
||||
$this->mapCommands,
|
||||
$this->getAdditionalParameters()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
}
|
||||
}
|
||||
241
tests/Functional/ComplexMediaTest.php
Normal file
241
tests/Functional/ComplexMediaTest.php
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\FFMpeg\Functional;
|
||||
|
||||
use FFMpeg\Coordinate\Dimension;
|
||||
use FFMpeg\Filters\ComplexMedia\TestSrcFilter;
|
||||
use FFMpeg\Filters\ComplexMedia\XStackFilter;
|
||||
use FFMpeg\Format\Audio\Mp3;
|
||||
use FFMpeg\Format\Video\X264;
|
||||
|
||||
class ComplexMediaTest extends FunctionalTestCase
|
||||
{
|
||||
/**
|
||||
* Path prefix to avoid conflicts with another tests.
|
||||
*/
|
||||
const PATH_PREFIX = 'complex_media_';
|
||||
|
||||
public function testRunWithoutComplexFilterTestExtractAudio()
|
||||
{
|
||||
$ffmpeg = $this->getFFMpeg();
|
||||
$inputs = array(realpath(__DIR__ . '/../files/Test.ogv'));
|
||||
$format = new Mp3();
|
||||
$output = __DIR__ . '/output/' . self::PATH_PREFIX . 'extracted_with_map.mp3';
|
||||
|
||||
// You can run it without -filter_complex, just using -map.
|
||||
$complexMedia = $ffmpeg->openComplex($inputs);
|
||||
$complexMedia
|
||||
->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__ . '/output/' . self::PATH_PREFIX . 'audio_test.mp3';
|
||||
|
||||
$complexMedia = $ffmpeg->openComplex($inputs);
|
||||
$complexMedia
|
||||
->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__ . '/output/' . self::PATH_PREFIX . 'multiple_inputs_test.mp4';
|
||||
|
||||
$complexMedia = $ffmpeg->openComplex($inputs);
|
||||
$complexMedia->filters()
|
||||
->custom('[0:v][1:v]', 'hstack', '[v]');
|
||||
$complexMedia
|
||||
->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\ComplexMedia::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__ . '/output/' . self::PATH_PREFIX . 'test_multiple_outputs.mp3';
|
||||
$outputVideo1 = __DIR__ . '/output/' . self::PATH_PREFIX . 'test_multiple_outputs_v1.mp4';
|
||||
$outputVideo2 = __DIR__ . '/output/' . self::PATH_PREFIX . 'test_multiple_outputs_v2.mp4';
|
||||
|
||||
$complexMedia = $ffmpeg->openComplex($inputs);
|
||||
$complexMedia->filters()
|
||||
->sine('[a]', 5)
|
||||
->testSrc('[v1]', TestSrcFilter::TESTSRC, '160x120', 5)
|
||||
->testSrc('[v2]', TestSrcFilter::TESTSRC, '160x120', 5)
|
||||
->custom('[v1]', 'negate', '[v1negate]')
|
||||
->custom('[v2]', 'edgedetect', '[v2edgedetect]');
|
||||
$complexMedia
|
||||
->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\ComplexMedia\TestSrcFilter
|
||||
* @covers \FFMpeg\Filters\ComplexMedia\SineFilter
|
||||
*/
|
||||
public function testTestSrcFilterTestSineFilter()
|
||||
{
|
||||
$ffmpeg = $this->getFFMpeg();
|
||||
$inputs = array(realpath(__DIR__ . '/../files/Test.ogv'));
|
||||
$format = new X264('aac', 'libx264');
|
||||
$output = __DIR__ . '/output/' . self::PATH_PREFIX . 'testsrc.mp4';
|
||||
|
||||
$complexMedia = $ffmpeg->openComplex($inputs);
|
||||
$complexMedia->filters()
|
||||
->sine('[a]', 10)
|
||||
->testSrc('[v]', TestSrcFilter::TESTSRC, '160x120', 10);
|
||||
$complexMedia
|
||||
->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\ComplexMedia\XStackFilter
|
||||
* @covers \FFMpeg\Filters\ComplexMedia\SineFilter
|
||||
*/
|
||||
public function testXStackFilter()
|
||||
{
|
||||
$ffmpeg = $this->getFFMpeg();
|
||||
$inputs = array(realpath(__DIR__ . '/../files/Test.ogv'));
|
||||
$format = new X264('aac', 'libx264');
|
||||
$output = __DIR__ . '/output/' . self::PATH_PREFIX . 'xstack_test.mp4';
|
||||
|
||||
$complexMedia = $ffmpeg->openComplex($inputs);
|
||||
$complexMedia->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]');
|
||||
$complexMedia
|
||||
->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__ . '/output/' . self::PATH_PREFIX . 'test_of_compatibility_with_existed_filters.mp4';
|
||||
|
||||
$complexMedia = $ffmpeg->openComplex($inputs);
|
||||
$complexMedia->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]');
|
||||
$complexMedia
|
||||
->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();
|
||||
|
||||
$complexMedia1 = $ffmpeg->openComplex(array(__FILE__));
|
||||
$complexMedia1
|
||||
->map(array('test'), $format, 'outputFile.mp4', false);
|
||||
$this->assertContains('acodec', $complexMedia1->getFinalCommand());
|
||||
|
||||
$complexMedia2 = $ffmpeg->openComplex(array(__FILE__));
|
||||
$complexMedia2
|
||||
->map(array('test'), $format, 'outputFile.mp4', true);
|
||||
$this->assertNotContains('acodec', $complexMedia2->getFinalCommand());
|
||||
}
|
||||
|
||||
public function testForceDisableVideo()
|
||||
{
|
||||
$ffmpeg = $this->getFFMpeg();
|
||||
$format = new X264();
|
||||
|
||||
$complexMedia1 = $ffmpeg->openComplex(array(__FILE__));
|
||||
$complexMedia1->map(array('test'), $format,
|
||||
'outputFile.mp4', false, false);
|
||||
$this->assertContains('vcodec', $complexMedia1->getFinalCommand());
|
||||
|
||||
$complexMedia2 = $ffmpeg->openComplex(array(__FILE__));
|
||||
$complexMedia2->map(array('test'), $format,
|
||||
'outputFile.mp4', false, true);
|
||||
$this->assertNotContains('vcodec', $complexMedia2->getFinalCommand());
|
||||
}
|
||||
}
|
||||
35
tests/Unit/Media/ComplexMediaTest.php
Normal file
35
tests/Unit/Media/ComplexMediaTest.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\FFMpeg\Unit\Media;
|
||||
|
||||
use FFMpeg\Media\ComplexMedia;
|
||||
|
||||
class ComplexMediaTest extends AbstractMediaTestCase
|
||||
{
|
||||
public function testGetInputs()
|
||||
{
|
||||
$driver = $this->getFFMpegDriverMock();
|
||||
$ffprobe = $this->getFFProbeMock();
|
||||
|
||||
$complexMedia = new ComplexMedia(array(__FILE__, __FILE__), $driver, $ffprobe);
|
||||
$this->assertSame(array(__FILE__, __FILE__), $complexMedia->getInputs());
|
||||
}
|
||||
|
||||
public function testGetInputsCount()
|
||||
{
|
||||
$driver = $this->getFFMpegDriverMock();
|
||||
$ffprobe = $this->getFFProbeMock();
|
||||
|
||||
$complexMedia = new ComplexMedia(array(__FILE__, __FILE__), $driver, $ffprobe);
|
||||
$this->assertEquals(2, $complexMedia->getInputsCount());
|
||||
}
|
||||
|
||||
public function testFiltersReturnFilters()
|
||||
{
|
||||
$driver = $this->getFFMpegDriverMock();
|
||||
$ffprobe = $this->getFFProbeMock();
|
||||
|
||||
$complexMedia = new ComplexMedia(array(__FILE__, __FILE__), $driver, $ffprobe);
|
||||
$this->assertInstanceOf('FFMpeg\Filters\ComplexMedia\ComplexFilters', $complexMedia->filters());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue