332 lines
8.6 KiB
PHP
332 lines
8.6 KiB
PHP
<?php
|
|
|
|
namespace Danjones\FFMpeg;
|
|
|
|
use Alchemy\BinaryDriver\Exception\ExecutionFailureException;
|
|
use FFMpeg\Driver\FFMpegDriver;
|
|
use FFMpeg\Exception\RuntimeException;
|
|
use FFMpeg\FFMpeg;
|
|
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;
|
|
use FFMpeg\Media\AbstractMediaType;
|
|
|
|
/**
|
|
* 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 MappableMedia extends AbstractMediaType
|
|
{
|
|
/**
|
|
* @var string[]
|
|
*/
|
|
protected array $inputs = [];
|
|
|
|
/** @var Map[] */
|
|
protected array $maps = [];
|
|
|
|
/**
|
|
* @var string[]
|
|
*/
|
|
protected array $initialParameters = [];
|
|
|
|
/**
|
|
* @var string[]
|
|
*/
|
|
protected array $additionalParameters = [];
|
|
|
|
/**
|
|
* @var AbstractProgressListener[]
|
|
*/
|
|
protected array $listeners = [];
|
|
|
|
public function __construct(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';
|
|
|
|
parent::__construct($pathfile, $driver, $ffprobe);
|
|
}
|
|
|
|
public static function make(FFMpeg $ffmpeg): static
|
|
{
|
|
return new static($ffmpeg->getFFMpegDriver(), $ffmpeg->getFFProbe());
|
|
}
|
|
|
|
public function filters()
|
|
{
|
|
return $this->filters;
|
|
}
|
|
|
|
public function addInput(string $path): static
|
|
{
|
|
if (empty($this->inputs)) {
|
|
$this->pathfile = $path;
|
|
}
|
|
|
|
$this->inputs[] = $path;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @return string[]
|
|
*/
|
|
public function getInitialParameters(): array
|
|
{
|
|
return $this->initialParameters;
|
|
}
|
|
|
|
/**
|
|
* @param string[] $initialParameters
|
|
*/
|
|
public function setInitialParameters(array $initialParameters): static
|
|
{
|
|
$this->initialParameters = $initialParameters;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @return string[]
|
|
*/
|
|
public function getAdditionalParameters(): array
|
|
{
|
|
return $this->additionalParameters;
|
|
}
|
|
|
|
/**
|
|
* @param string[] $additionalParameters
|
|
*/
|
|
public function setAdditionalParameters(array $additionalParameters): static
|
|
{
|
|
$this->additionalParameters = $additionalParameters;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @return string[]
|
|
*/
|
|
public function getInputs(): array
|
|
{
|
|
return $this->inputs;
|
|
}
|
|
|
|
public function getInputsCount(): int
|
|
{
|
|
return count($this->inputs);
|
|
}
|
|
|
|
public function getFinalCommand(): string
|
|
{
|
|
return implode(' ', $this->buildCommand());
|
|
}
|
|
|
|
public function map(callable $callback = null): Map|static
|
|
{
|
|
$map = new Map($this);
|
|
if (!$callback) {
|
|
return $map;
|
|
}
|
|
|
|
$callback($map);
|
|
$this->saveMap($map);
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function saveMap(Map $map): static
|
|
{
|
|
$this->maps[] = $map;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* @todo Redo all of this.
|
|
* @see https://ffmpeg.org/ffmpeg.html#Manual-stream-selection
|
|
*/
|
|
private function map2(
|
|
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.
|
|
*
|
|
* @throws RuntimeException
|
|
*/
|
|
public function save(): void
|
|
{
|
|
$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
|
|
* @todo Redo it all
|
|
*/
|
|
protected 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 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->buildMaps($this->maps),
|
|
$this->getAdditionalParameters()
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param string[] $optionNames
|
|
*
|
|
* @return array
|
|
*/
|
|
protected 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
|
|
*/
|
|
protected function buildInputsPart(array $inputs): array
|
|
{
|
|
$commands = [];
|
|
foreach ($inputs as $input) {
|
|
$commands[] = '-i';
|
|
$commands[] = $input;
|
|
}
|
|
|
|
return $commands;
|
|
}
|
|
|
|
/**
|
|
* @param Map[] $maps
|
|
*/
|
|
protected function buildMaps(array $maps): array
|
|
{
|
|
$commands = [];
|
|
foreach($maps as $map) {
|
|
array_push($commands, ...$map->buildCommand());
|
|
}
|
|
|
|
return $commands;
|
|
}
|
|
}
|