ffmpeg-mappable-media/src/MappableMedia.php
Dan Jones de5cf914da 🚧 Add stream to map
Still need to handle the codecs. But almost there.
2022-08-23 15:25:27 -05:00

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;
}
}