<?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
*/
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 = [];
$this->additionalParameters = [];
$this->mapCommands = [];
$this->listeners = [];
}
/**
* Returns the available filters.
*
* @return ComplexFilters
*/
public function filters()
{
return new ComplexFilters($this);
}
/**
* Add complex filter.
*
* @param string $in
* @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 = [];
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 bool $forceDisableAudio
* @param bool $forceDisableVideo
*
* @return array
*/
private function applyFormatParams(
FormatInterface $format,
$forceDisableAudio = false,
$forceDisableVideo = false
) {
// Set format params.
$commands = [];
if (!$forceDisableVideo && $format instanceof VideoInterface) {
if (null !== $format->getVideoCodec()) {
$commands[] = '-vcodec';
$commands[] = $format->getVideoCodec();
}
// If the user passed some additional format parameters.
if (null !== $format->getAdditionalParameters()) {
$commands = array_merge($commands, $format->getAdditionalParameters());
}
}
if (!$forceDisableAudio && $format instanceof AudioInterface) {
if (null !== $format->getAudioCodec()) {
$commands[] = '-acodec';
$commands[] = $format->getAudioCodec();
}
if (null !== $format->getAudioKiloBitrate()) {
$commands[] = '-b:a';
$commands[] = $format->getAudioKiloBitrate().'k';
}
if (null !== $format->getAudioChannels()) {
$commands[] = '-ac';
$commands[] = $format->getAudioChannels();
}
}
// If the user passed some extra parameters.
if ($format->getExtraParams()) {
$commands = array_merge($commands, $format->getExtraParams());
}
return $commands;
}
/**
* @return string
*/
private function applyComplexFilter(ComplexFilterInterface $filter)
{
/** @var $format VideoInterface */
$filterCommands = $filter->applyComplex($this);
foreach ($filterCommands as $index => $command) {
if ('-vf' === $command || '-filter:v' === $command || '-filter_complex' === $command) {
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 (false !== stripos($strCommand, '[in]')) {
$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 (false !== stripos($strCommand, '[out]')) {
$strCommand = str_replace('[out]', $filter->getOutLabels(), $strCommand);
$out = '';
} else {
$out = $filter->getOutLabels();
}
return $in.$strCommand.$out;
}
/**
* @return void
*
* @throws RuntimeException
*/
protected function assertFiltersAreCompatibleToCurrentFFMpegVersion()
{
$messages = [];
$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 = ['threads', 'filter_threads', 'filter_complex_threads'];
return array_merge(
['-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 = [];
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 = [];
foreach ($inputs as $input) {
$commands[] = '-i';
$commands[] = $input;
}
return $commands;
}
/**
* Build "-filter_complex" part of the ffmpeg command.
*
* @return array
*/
private function buildComplexFilterPart(FiltersCollection $complexFilters)
{
$commands = [];
/** @var ComplexFilterInterface $filter */
foreach ($complexFilters as $filter) {
$filterCommand = $this->applyComplexFilter($filter);
$commands[] = $filterCommand;
}
if (empty($commands)) {
return [];
}
return ['-filter_complex', implode(';', $commands)];
}
}