From 18c3cbb2840e3d157b53518ba01c71cc4700a039 Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Mon, 5 Sep 2022 21:17:44 -0500 Subject: [PATCH 1/7] =?UTF-8?q?=E2=9C=A8=20Listeners?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + src/Map.php | 19 ++++++++++ src/MappableMedia.php | 86 +++++++++++++------------------------------ 3 files changed, 47 insertions(+), 60 deletions(-) diff --git a/.gitignore b/.gitignore index ce4942f..9ea8c84 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ vendor .php-cs-fixer.cache *.mkv +tags +*.mp3 diff --git a/src/Map.php b/src/Map.php index d2d638f..d18196a 100644 --- a/src/Map.php +++ b/src/Map.php @@ -2,6 +2,9 @@ namespace Danjones\FFMpeg; +use FFMpeg\Format\ProgressableInterface; +use FFMpeg\Format\ProgressListener\AbstractProgressListener; + class Map { use Traits\HasMetadata; @@ -10,6 +13,8 @@ class Map protected string $path; /** @var Stream[] */ protected array $streams = []; + /** @var AbstractProgressListener[] */ + protected array $listeners = []; public function __construct(MappableMedia $media) { @@ -46,10 +51,24 @@ class Map public function saveStream(Stream $stream): static { $this->streams[] = $stream; + $format = $stream->getCodec(); + if ($format instanceof ProgressableInterface) { + $listener = $format->createProgressListener( + $this->media, + $this->media->getFFProbe(), + 1, 1, 0 + ); + $this->listeners = array_merge($this->listeners, $listener); + } return $this; } + public function getListeners(): array + { + return $this->listeners; + } + public function buildCommand(): array { $commands = []; diff --git a/src/MappableMedia.php b/src/MappableMedia.php index 6bede7c..095bab3 100644 --- a/src/MappableMedia.php +++ b/src/MappableMedia.php @@ -3,6 +3,8 @@ namespace Danjones\FFMpeg; use Alchemy\BinaryDriver\Exception\ExecutionFailureException; +use Evenement\EventEmitterInterface; +use Evenement\EventEmitterTrait; use FFMpeg\Driver\FFMpegDriver; use FFMpeg\Exception\RuntimeException; use FFMpeg\FFMpeg; @@ -14,20 +16,23 @@ 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\ProgressListener\VideoProgressListener; +use FFMpeg\Format\ProgressableInterface; 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. + * MappableMedia may have multiple inputs and multiple outputs. + * This class does not accept filters. * 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 +class MappableMedia extends AbstractMediaType implements EventEmitterInterface { + use EventEmitterTrait; + /** * @var string[] */ @@ -46,10 +51,8 @@ class MappableMedia extends AbstractMediaType */ protected array $additionalParameters = []; - /** - * @var AbstractProgressListener[] - */ - protected array $listeners = []; + /** @var AbstractProgressListener[] */ + protected array $progressListeners = []; public function __construct(FFMpegDriver $driver, FFProbe $ffprobe) { @@ -152,72 +155,35 @@ class MappableMedia extends AbstractMediaType public function saveMap(Map $map): static { $this->maps[] = $map; + $this->progressListeners = array_merge($this->progressListeners, $map->getListeners()); 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(); + $this->addListener(); try { - $this->driver->command($command, false, $this->listeners); + $this->driver->command($command, false, $this->progressListeners); } catch (ExecutionFailureException $e) { throw new RuntimeException('Encoding failed', $e->getCode(), $e); } } + protected function addListener(): void + { + $self = $this; + $listener = new VideoProgressListener($this->ffprobe, $this->getPathfile(), 1, 1, 0); + + $listener->on('progress', function (...$args) use ($self) { + $self->emit('progress', array_merge([$self, null], $args)); + }); + + $this->progressListeners[] = $listener; + } + /** * @param bool $forceDisableAudio * @param bool $forceDisableVideo From 7baacb86a55b3af7004ed17479adc3955942293c Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Tue, 6 Sep 2022 12:03:22 -0500 Subject: [PATCH 2/7] =?UTF-8?q?=F0=9F=94=A5=20Delete=20unused=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/MappableMedia.php | 61 ++----------------------------------------- 1 file changed, 2 insertions(+), 59 deletions(-) diff --git a/src/MappableMedia.php b/src/MappableMedia.php index 095bab3..c2fc9c9 100644 --- a/src/MappableMedia.php +++ b/src/MappableMedia.php @@ -9,17 +9,8 @@ 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\ProgressListener\AbstractProgressListener; use FFMpeg\Format\ProgressListener\VideoProgressListener; -use FFMpeg\Format\ProgressableInterface; -use FFMpeg\Format\VideoInterface; use FFMpeg\Media\AbstractMediaType; /** @@ -57,8 +48,6 @@ class MappableMedia extends AbstractMediaType implements EventEmitterInterface 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); @@ -71,7 +60,7 @@ class MappableMedia extends AbstractMediaType implements EventEmitterInterface public function filters() { - return $this->filters; + return null; } public function addInput(string $path): static @@ -178,58 +167,12 @@ class MappableMedia extends AbstractMediaType implements EventEmitterInterface $listener = new VideoProgressListener($this->ffprobe, $this->getPathfile(), 1, 1, 0); $listener->on('progress', function (...$args) use ($self) { - $self->emit('progress', array_merge([$self, null], $args)); + $self->emit('progress', array_merge([$self], $args)); }); $this->progressListeners[] = $listener; } - /** - * @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 */ From e07743625cf0b1b0cb7e17ccdc6b1224292742e7 Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Tue, 6 Sep 2022 12:44:56 -0500 Subject: [PATCH 3/7] =?UTF-8?q?=F0=9F=93=9D=20Document=20listeners?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit And other stuff --- README.md | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c4e378a..38e0585 100644 --- a/README.md +++ b/README.md @@ -88,11 +88,62 @@ ffmpeg -y -i video.mp4 -i audio.opus -i sub.srt -map 0:0 -c:0 libvp9 -map 1:0 -c This will result in two files. The first will be a webm file (suitable for play in a browser) with a `title` field on the file set to "The Greatest Story Ever Hulaed". The second file will copy the first stream from each file directly, and set the language of the subtitle stream to English. -## What's not yet done? +### Using FormatInterface -Mostly documentation. There are a lot more ways to use this, including using the `Format` objects from PHP-FFMpeg to set the codecs, and the ability to use callbacks to set additional data on both maps and individual streams. +It is also possible to set the codec from a stream using the [Formats](https://github.com/PHP-FFMpeg/PHP-FFMpeg/#formats) from PHP-FFMPEG. -I also need to figure out how to set the listeners on the codecs to get realtime progress updates (see [Formats](https://github.com/PHP-FFMpeg/PHP-FFMpeg/#formats)). Once that's figured out, I need to document it properly here. +```php +use FFMpeg\Format\Video\X264; + +MappableMedia::make($ffmpeg) + ->addInput('input.mkv') + ->map() + ->saveAs('output.mk') + ->stream()->setCodec(new X264())->saveStream() + ->saveMap() + ->save() +``` + +### Using callbacks + +The `map` and `stream` methods can take an optional callback, allowing you to set the properties on those individual objects. When a callback is passed the object itself is returned, allowing you to continue the chain. + +```php +MappableMedia::make($ffmpeg) + ->addInput('input.mkv') + ->map(function (Map $map) { + $map->saveAs('output.mk') + ->stream(function (Stream $stream) { + $stream->copy()->setInput('0:0'); + }); + })->save() +``` + +Note that when using callbacks, you also don't need to call `saveMap()` or `saveStream()`. + +### Getting progress updates + +It is possible to set a listener on the individual streams, using the Format class. However, this don't reliably report the progress of a particular stream. So, this package adds a listener on the `MappableMedia` object itself, which represents the progress of the entire job. + +```php +MappableMedia::make($ffmpeg) + ->addInput('input.mkv') + ->map() + ->saveAs('output.mk') + ->stream()->copy()->saveStream() + ->saveMap() + ->on('progress', function (MappableMedia $media, int $percent, int $remaining, int $rate) { + echo "Finished {$percent}% of ", $media->getPathfile(), "\n"; + }) + ->save() +``` + +## Future Plans + +- [ ] Add listeners that return all the stdin/stderr +- [ ] Support itsoffset for inputs +- [ ] Support -t and -ss + + I need to figure out how this will affect the progress listener. ## Contributing From 4a833b4ca9e74452eb43db548441b378006ae08e Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Tue, 6 Sep 2022 12:49:54 -0500 Subject: [PATCH 4/7] =?UTF-8?q?=F0=9F=93=9D=20Update=20changelog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e5a4cc..915529f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to `ffmpeg-mappable-media` will be documented in this file. +## 0.1.3 + +- ✨ Support progress listeners +- 📝 Improve documentation + ## 0.1.0 🎉 Initial release. Basic functionality works. From 77cf08260aee96d734d3b7e2f4be8ce32ca03d99 Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Sat, 10 Sep 2022 22:52:40 -0500 Subject: [PATCH 5/7] =?UTF-8?q?=E2=9C=A8=20Support=20attachments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Attachment.php | 46 ++++++++++++++++++++++++++++++++++++++++++++++ src/Map.php | 25 ++++++++++++++++++++++--- 2 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 src/Attachment.php diff --git a/src/Attachment.php b/src/Attachment.php new file mode 100644 index 0000000..5db4fd8 --- /dev/null +++ b/src/Attachment.php @@ -0,0 +1,46 @@ +map = $map; + $this->input = $file; + // Shouldn't be necessary, but just in case + $this->codec = new Format\Copy(); + } + + public function mime(string $mime): static + { + return $this->addMetadata('mimetype', $mime); + } + + public function setCodec(FormatInterface $codec): static + { + return $this; + } + + protected function parseCodec(): static + { + return $this; + } + + public function saveAttachment(): Map + { + return $this->saveStream();; + } + + public function buildCommand(int $idx = 0): array + { + $commands = parent::buildCommand($idx); + $commands[0] = '-attach'; + + return $commands; + } +} diff --git a/src/Map.php b/src/Map.php index d18196a..c6ef98c 100644 --- a/src/Map.php +++ b/src/Map.php @@ -13,6 +13,8 @@ class Map protected string $path; /** @var Stream[] */ protected array $streams = []; + /** @var Attachment[] */ + protected array $attachments = []; /** @var AbstractProgressListener[] */ protected array $listeners = []; @@ -35,9 +37,8 @@ class Map return $this; } - public function stream(callable $callback = null): Stream|static + protected function doStream(Stream $stream, callable $callback = null): Stream|static { - $stream = new Stream($this); if (!$callback) { return $stream; } @@ -48,8 +49,24 @@ class Map return $this; } + public function stream(callable $callback = null): Stream|static + { + return $this->doStream(new Stream($this), $callback); + } + + public function attach(string $file = '', callable $callback = null): Attachment|static + { + return $this->doStream(new Attachment($this, $file), $callback); + } + public function saveStream(Stream $stream): static { + if ($stream instanceof Attachment){ + $this->attachments[] = $stream; + + return $this; + } + $this->streams[] = $stream; $format = $stream->getCodec(); if ($format instanceof ProgressableInterface) { @@ -72,7 +89,9 @@ class Map public function buildCommand(): array { $commands = []; - foreach ($this->streams as $idx => $stream) { + $streams = $this->streams; + array_push($streams, ...$this->attachments); + foreach ($streams as $idx => $stream) { array_push($commands, ...$stream->buildCommand($idx)); } foreach ($this->metadata as $k => $v) { From cb892134ca0866ef7e7fc223abb15f011886e1a7 Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Sat, 10 Sep 2022 23:19:18 -0500 Subject: [PATCH 6/7] =?UTF-8?q?=F0=9F=9A=B8=20Get=20full=20fully-escaped?= =?UTF-8?q?=20command?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/MappableMedia.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/MappableMedia.php b/src/MappableMedia.php index c2fc9c9..b3f81c2 100644 --- a/src/MappableMedia.php +++ b/src/MappableMedia.php @@ -125,7 +125,9 @@ class MappableMedia extends AbstractMediaType implements EventEmitterInterface public function getFinalCommand(): string { - return implode(' ', $this->buildCommand()); + $proc = $this->driver->getProcessBuilderFactory()->create($this->buildCommand()); + + return $proc->getCommandLine(); } public function map(callable $callback = null): Map|static From 249a559c1277440e16671cdaab6fc87d1d2c6211 Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Sat, 10 Sep 2022 23:45:52 -0500 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=93=9D=20Document=20attachments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 4 ++++ README.md | 47 ++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 915529f..2ec1abe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to `ffmpeg-mappable-media` will be documented in this file. +## 0.2.0 + +- ✨ Support attachments + ## 0.1.3 - ✨ Support progress listeners diff --git a/README.md b/README.md index 38e0585..bd97fa0 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ use FFMpeg\Format\Video\X264; MappableMedia::make($ffmpeg) ->addInput('input.mkv') ->map() - ->saveAs('output.mk') + ->saveAs('output.mkv') ->stream()->setCodec(new X264())->saveStream() ->saveMap() ->save() @@ -112,7 +112,7 @@ The `map` and `stream` methods can take an optional callback, allowing you to se MappableMedia::make($ffmpeg) ->addInput('input.mkv') ->map(function (Map $map) { - $map->saveAs('output.mk') + $map->saveAs('output.mkv') ->stream(function (Stream $stream) { $stream->copy()->setInput('0:0'); }); @@ -129,7 +129,7 @@ It is possible to set a listener on the individual streams, using the Format cla MappableMedia::make($ffmpeg) ->addInput('input.mkv') ->map() - ->saveAs('output.mk') + ->saveAs('output.mkv') ->stream()->copy()->saveStream() ->saveMap() ->on('progress', function (MappableMedia $media, int $percent, int $remaining, int $rate) { @@ -138,6 +138,47 @@ MappableMedia::make($ffmpeg) ->save() ``` +### Attachments + +Some formats (mkv, for example) support arbitrary data attachments. These can be used as cover art, fonts for subtitles, or any arbitrary data. + +FFMpeg does support attachments as an additional input. This works well for images, but can be finicky for other file types. Because of this, FFMpeg also supports an `-attach` flag which can be used to explicitly attach a new stream. + +Due to the way FFMpeg handles `-attach` differently than `-i`, these need to be added as streams to a specific map, rather than the whole media. Here are a few examples. + +```php +MappableMedia::make($ffmpeg) + ->addInput('input.mkv') + ->map() + ->saveAs('output.mkv') + ->stream()->copy()->saveStream() + ->attach('image.jpg') + ->mime('image/jpeg') + ->addMetadata('filename', 'cover.jpg') + ->saveAttachment() + ->saveMap() + ->save(); +``` + +In this example, we added cover art to the file. Notice the use of the `mime` method to specify the mime-type. **This must always be done**. Note that we also specified a different filename so that the media player would recognize it as cover art. If you don't specify a filename, the original will be used. + +```php +MappableMedia::make($ffmpeg) + ->addInput('input.mkv') + ->addInput('subs.ass') + ->map() + ->saveAs('output.mkv') + ->stream()->copy()->saveStream() + ->stream()->setInput('1:0')->copy()->saveStream() + ->attach(verdana.ttf') + ->mime('application/x-truetype-font') + ->saveAttachment() + ->saveMap() + ->save(); +``` + +In this example, we've added a font, which is likely referenced in the subtitle file, `subs.ass`. + ## Future Plans - [ ] Add listeners that return all the stdin/stderr