From 1b3fe186ae70dcdbd5a00a7b621aabe2a929d42d Mon Sep 17 00:00:00 2001 From: jygaulier Date: Thu, 17 Oct 2024 12:04:33 +0200 Subject: [PATCH 1/3] add animation module and animated formats --- .../Resources/config/services.yaml | 48 +++++ .../Video/FFMpeg/Format/AnimatedGifFormat.php | 29 +++ .../Video/FFMpeg/Format/AnimatedPngFormat.php | 29 +++ .../FFMpeg/Format/AnimatedWebpFormat.php | 29 +++ .../Video/FFMpegTransformerModule.php | 155 +++++++++------- .../Video/VideoSummaryTransformerModule.php | 133 +++++++------- .../VideoToAnimationTransformerModule.php | 165 ++++++++++++++++++ .../Video/VideoToFrameTransformerModule.php | 72 ++++++++ 8 files changed, 525 insertions(+), 135 deletions(-) create mode 100644 lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/AnimatedGifFormat.php create mode 100644 lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/AnimatedPngFormat.php create mode 100644 lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/AnimatedWebpFormat.php create mode 100644 lib/php/rendition-factory/src/Transformer/Video/VideoToAnimationTransformerModule.php create mode 100644 lib/php/rendition-factory/src/Transformer/Video/VideoToFrameTransformerModule.php diff --git a/lib/php/rendition-factory-bundle/Resources/config/services.yaml b/lib/php/rendition-factory-bundle/Resources/config/services.yaml index 2c43c10d4..68ca31ddc 100644 --- a/lib/php/rendition-factory-bundle/Resources/config/services.yaml +++ b/lib/php/rendition-factory-bundle/Resources/config/services.yaml @@ -28,6 +28,14 @@ services: tags: - { name: !php/const Alchemy\RenditionFactory\Transformer\TransformerModuleInterface::TAG } + Alchemy\RenditionFactory\Transformer\Video\VideoToFrameTransformerModule: + tags: + - { name: !php/const Alchemy\RenditionFactory\Transformer\TransformerModuleInterface::TAG } + + Alchemy\RenditionFactory\Transformer\Video\VideoToAnimationTransformerModule: + tags: + - { name: !php/const Alchemy\RenditionFactory\Transformer\TransformerModuleInterface::TAG } + Alchemy\RenditionFactory\Transformer\Document\DocumentToPdfTransformerModule: tags: - { name: !php/const Alchemy\RenditionFactory\Transformer\TransformerModuleInterface::TAG } @@ -36,7 +44,47 @@ services: tags: - { name: !php/const Alchemy\RenditionFactory\Transformer\TransformerModuleInterface::TAG } + # FFMpeg "formats" + Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\JpegFormat: + tags: + - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG } + + Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\MkvFormat: + tags: + - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG } + + Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\Mpeg4Format: + tags: + - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG } + + Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\MpegFormat: + tags: + - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG } + + Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\QuicktimeFormat: + tags: + - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG } + + Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\WebmFormat: + tags: + - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG } + + Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\AnimatedGifFormat: + tags: + - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG } + + Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\AnimatedPngFormat: + tags: + - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG } + + Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\AnimatedWebpFormat: + tags: + - { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG } + + Imagine\Imagick\Imagine: ~ Imagine\Image\ImagineInterface: '@Imagine\Imagick\Imagine' Alchemy\RenditionFactory\MimeType\MimeTypeGuesser: ~ + Alchemy\RenditionFactory\Format\FormatGuesser: ~ + Alchemy\RenditionFactory\Format\FormatFactory: ~ diff --git a/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/AnimatedGifFormat.php b/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/AnimatedGifFormat.php new file mode 100644 index 000000000..7e3e126a4 --- /dev/null +++ b/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/AnimatedGifFormat.php @@ -0,0 +1,29 @@ +formats->has($format)) { + throw new InvalidArgumentException(sprintf('Invalid format %s', $format)); + } + /** @var FormatInterface $outputFormat */ + $outputFormat = $this->formats->get($format); + + if (null != ($extension = $options['extension'] ?? null)) { + if(!in_array($extension, $outputFormat->getAllowedExtensions())) { + throw new InvalidArgumentException(sprintf('Invalid extension %s for format %s', $extension, $format)); + } + } + else { + $extension = ($outputFormat->getAllowedExtensions())[0]; } - if (!($extension = $options['extension'])) { - throw new \InvalidArgumentException('Missing extension'); + + if($outputFormat->getFamily() !== FamilyEnum::Video) { + throw new InvalidArgumentException(sprintf('Invalid format %s, only video formats supported', $format)); } - $fqcnFormat = 'FFMpeg\\Format\\Video\\'.$format; - if (class_exists($fqcnFormat)) { - return $this->doVideo($format, $extension, $inputFile, $options, $context); + if($outputFormat->getFamily() === FamilyEnum::Video) { + return $this->doVideo($outputFormat, $extension, $inputFile, $options, $context); } - $fqcnFormat = 'FFMpeg\\Format\\Audio\\'.$format; - if (class_exists($fqcnFormat)) { - return $this->doAudio($format, $extension, $inputFile, $options, $context); + + if ($outputFormat->getFamily() === FamilyEnum::Audio) { + return $this->doAudio($outputFormat, $extension, $inputFile, $options, $context); } - throw new \InvalidArgumentException(sprintf('Invalid format %s', $format)); + throw new InvalidArgumentException(sprintf('Invalid format %s, only video or audio format supported', $format)); } - private function doVideo(string $format, string $extension, InputFileInterface $inputFile, array $options, TransformationContextInterface $context): OutputFileInterface + private function doVideo(FormatInterface $ouputFormat, string $extension, InputFileInterface $inputFile, array $options, TransformationContextInterface $context): OutputFileInterface { - $fqcnFormat = 'FFMpeg\\Format\\Video\\'.$format; - /** @var FormatInterface $ouputFormat */ - $ouputFormat = new $fqcnFormat(); + $format = $ouputFormat->getFormat(); + if(!method_exists($ouputFormat, 'getFFMpegFormat')) { + throw new InvalidArgumentException('format %s does not declare FFMpeg format', $format); + } + /** @var FFMpegFormatInterface $FFMpegFormat */ + $FFMpegFormat = $ouputFormat->getFFMpegFormat(); if ($videoCodec = $options['video_codec'] ?? null) { - if (!in_array($videoCodec, $ouputFormat->getAvailableVideoCodecs())) { - throw new \InvalidArgumentException(sprintf('Invalid video codec %s for format %s', $videoCodec, $format)); + if (!in_array($videoCodec, $FFMpegFormat->getAvailableVideoCodecs())) { + throw new InvalidArgumentException(sprintf('Invalid video codec %s for format %s', $videoCodec, $format)); } - $ouputFormat->setVideoCodec($videoCodec); + $FFMpegFormat->setVideoCodec($videoCodec); } if ($audioCodec = $options['audio_codec'] ?? null) { - if (!in_array($audioCodec, $ouputFormat->getAvailableAudioCodecs())) { - throw new \InvalidArgumentException(sprintf('Invalid audio codec %s for format %s', $audioCodec, $format)); + if (!in_array($audioCodec, $FFMpegFormat->getAvailableAudioCodecs())) { + throw new InvalidArgumentException(sprintf('Invalid audio codec %s for format %s', $audioCodec, $format)); } - $ouputFormat->setAudioCodec($audioCodec); + $FFMpegFormat->setAudioCodec($audioCodec); } if (null !== ($videoKilobitrate = $options['video_kilobitrate'] ?? null)) { - if (!method_exists($ouputFormat, 'setKiloBitrate')) { - throw new \InvalidArgumentException(sprintf('format %s does not support video_kilobitrate', $format)); + if (!method_exists($FFMpegFormat, 'setKiloBitrate')) { + throw new InvalidArgumentException(sprintf('format %s does not support video_kilobitrate', $format)); } if (!is_int($videoKilobitrate)) { - throw new \InvalidArgumentException('Invalid video kilobitrate'); + throw new InvalidArgumentException('Invalid video kilobitrate'); } - $ouputFormat->setKiloBitrate($videoKilobitrate); + $FFMpegFormat->setKiloBitrate($videoKilobitrate); } if (null !== ($audioKilobitrate = $options['audio_kilobitrate'] ?? null)) { if (!method_exists($ouputFormat, 'setAudioKiloBitrate')) { - throw new \InvalidArgumentException(sprintf('format %s does not support audio_kilobitrate', $format)); + throw new InvalidArgumentException(sprintf('format %s does not support audio_kilobitrate', $format)); } if (!is_int($audioKilobitrate)) { - throw new \InvalidArgumentException('Invalid audio kilobitrate'); + throw new InvalidArgumentException('Invalid audio kilobitrate'); } - $ouputFormat->setAudioKiloBitrate($audioKilobitrate); + $FFMpegFormat->setAudioKiloBitrate($audioKilobitrate); } if (null !== ($passes = $options['passes'] ?? null)) { if (!method_exists($ouputFormat, 'setPasses')) { - throw new \InvalidArgumentException(sprintf('format %s does not support passes', $format)); + throw new InvalidArgumentException(sprintf('format %s does not support passes', $format)); } if (!is_int($passes) || $passes < 1) { - throw new \InvalidArgumentException('Invalid passes count'); + throw new InvalidArgumentException('Invalid passes count'); } if (0 === $videoKilobitrate) { - throw new \InvalidArgumentException('passes must not be set if video_kilobitrate is 0'); + throw new InvalidArgumentException('passes must not be set if video_kilobitrate is 0'); } - $ouputFormat->setPasses($passes); + $FFMpegFormat->setPasses($passes); } $ffmpegOptions = []; if ($timeout = $options['timeout'] ?? null) { if (!is_int($timeout)) { - throw new \InvalidArgumentException('Invalid timeout'); + throw new InvalidArgumentException('Invalid timeout'); } $ffmpegOptions['timeout'] = $timeout; } if ($threads = $options['threads'] ?? null) { if (!is_int($threads) || $threads < 1) { - throw new \InvalidArgumentException('Invalid threads count'); + throw new InvalidArgumentException('Invalid threads count'); } $ffmpegOptions['ffmpeg.threads'] = $threads; } @@ -121,10 +147,10 @@ private function doVideo(string $format, string $extension, InputFileInterface $ foreach ($filters as $filter) { if ('pre_clip' === $filter['name']) { - throw new \InvalidArgumentException('"pre_clip" filter must be the first filter'); + throw new InvalidArgumentException('"pre_clip" filter must be the first filter'); } if (!method_exists($this, $filter['name'])) { - throw new \InvalidArgumentException(sprintf('Invalid filter: %s', $filter['name'])); + throw new InvalidArgumentException(sprintf('Invalid filter: %s', $filter['name'])); } $context->log(sprintf('Applying filter: %s', $filter['name'])); @@ -136,39 +162,38 @@ private function doVideo(string $format, string $extension, InputFileInterface $ $outputPath = $context->createTmpFilePath($extension); - $clip->save($ouputFormat, $outputPath); - - unset($clip); - unset($video); + $clip->save($FFMpegFormat, $outputPath); - $mimeType = $context->guessMimeTypeFromPath($outputPath); - $fileFamilyGuesser = new FileFamilyGuesser(); - $family = $fileFamilyGuesser->getFamily($outputPath, $mimeType); + unset($clip, $video, $ffmpeg); + gc_collect_cycles(); return new OutputFile( $outputPath, - $mimeType, - $family + $ouputFormat->getMimeType(), + $ouputFormat->getFamily(), ); } /** * todo: implement audio filters. */ - private function doAudio(string $format, string $extension, InputFileInterface $inputFile, array $options, TransformationContextInterface $context): OutputFileInterface + private function doAudio(FormatInterface $ouputFormat, string $extension, InputFileInterface $inputFile, array $options, TransformationContextInterface $context): OutputFileInterface { - $fqcnFormat = 'FFMpeg\\Format\\Audio\\'.$format; - /** @var FormatInterface $ouputFormat */ - $ouputFormat = new $fqcnFormat(); + $format = $ouputFormat->getFormat(); + if(!method_exists($ouputFormat, 'getFFMpegFormat')) { + throw new InvalidArgumentException('format %s does not declare FFMpeg format', $format); + } + /** @var FFMpegFormatInterface $FFMpegFormat */ + $FFMpegFormat = $ouputFormat->getFFMpegFormat(); if ($audioCodec = $options['audio_codec'] ?? null) { - if (!in_array($audioCodec, $ouputFormat->getAvailableAudioCodecs())) { - throw new \InvalidArgumentException(sprintf('Invalid audio codec %s for format %s', $audioCodec, $format)); + if (!in_array($audioCodec, $FFMpegFormat->getAvailableAudioCodecs())) { + throw new InvalidArgumentException(sprintf('Invalid audio codec %s for format %s', $audioCodec, $format)); } - $ouputFormat->setAudioCodec($audioCodec); + $FFMpegFormat->setAudioCodec($audioCodec); } - throw new \InvalidArgumentException('Audio transformation not implemented'); + throw new InvalidArgumentException('Audio transformation not implemented'); } private function preClip(Video $video, array $options, TransformationContextInterface $context): Clip @@ -183,7 +208,7 @@ private function preClip(Video $video, array $options, TransformationContextInte $startAsTimecode = TimeCode::fromSeconds($start); } if (false === $startAsTimecode) { - throw new \InvalidArgumentException('Invalid start for filter "clip"'); + throw new InvalidArgumentException('Invalid start for filter "clip"'); } if (is_string($duration)) { @@ -192,7 +217,7 @@ private function preClip(Video $video, array $options, TransformationContextInte $durationAsTimecode = TimeCode::fromSeconds($duration); } if (false === $durationAsTimecode) { - throw new \InvalidArgumentException('Invalid duration for filter "clip"'); + throw new InvalidArgumentException('Invalid duration for filter "clip"'); } return $video->clip($startAsTimecode, $durationAsTimecode); @@ -217,7 +242,7 @@ private function resize(Clip $clip, array $options, TransformationContextInterfa FFMpeg\Filters\Video\ResizeFilter::RESIZEMODE_SCALE_HEIGHT, ] )) { - throw new \InvalidArgumentException('Invalid mode for filter "resize"'); + throw new InvalidArgumentException('Invalid mode for filter "resize"'); } $clip->filters()->resize( @@ -235,7 +260,7 @@ private function rotate(Clip $clip, array $options, TransformationContextInterfa ]; $angle = $options['angle'] ?? 0; if (!array_key_exists($angle, $rotations)) { - throw new \InvalidArgumentException('Invalid rotation, must be 90, 180 or 270 for filter "rotate"'); + throw new InvalidArgumentException('Invalid rotation, must be 90, 180 or 270 for filter "rotate"'); } $clip->filters()->rotate($rotations[$angle]); @@ -268,7 +293,7 @@ private function clip(Clip $clip, array $options, TransformationContextInterface $startAsTimecode = TimeCode::fromSeconds($start); } if (false === $startAsTimecode) { - throw new \InvalidArgumentException('Invalid start for filter "clip"'); + throw new InvalidArgumentException('Invalid start for filter "clip"'); } if (is_string($duration)) { @@ -277,7 +302,7 @@ private function clip(Clip $clip, array $options, TransformationContextInterface $durationAsTimecode = TimeCode::fromSeconds($duration); } if (false === $durationAsTimecode) { - throw new \InvalidArgumentException('Invalid duration for filter "clip"'); + throw new InvalidArgumentException('Invalid duration for filter "clip"'); } $clip->filters()->clip($startAsTimecode, $durationAsTimecode); @@ -292,20 +317,20 @@ private function watermark(Clip $clip, array $options, TransformationContextInte { $path = $options['path'] ?? null; if (!file_exists($path)) { - throw new \InvalidArgumentException('Watermark file for filter "watermark" not found'); + throw new InvalidArgumentException('Watermark file for filter "watermark" not found'); } $position = $options['position'] ?? 'absolute'; if ('relative' == $position) { $coord = array_filter($options, fn ($k) => in_array($k, ['bottom', 'right', 'top', 'left']), ARRAY_FILTER_USE_KEY); if (array_key_exists('bottom', $coord) && array_key_exists('top', $coord) || array_key_exists('right', $coord) && array_key_exists('left', $coord)) { - throw new \InvalidArgumentException('Invalid relative coordinates for filter "watermark", only one of top/bottom or left/right can be set'); + throw new InvalidArgumentException('Invalid relative coordinates for filter "watermark", only one of top/bottom or left/right can be set'); } // in wm filter, missing coord are set to 0 } elseif ('absolute' == $position) { $coord = array_filter($options, fn ($k) => in_array($k, ['x', 'y']), ARRAY_FILTER_USE_KEY); } else { - throw new \InvalidArgumentException('Invalid position for filter "watermark"'); + throw new InvalidArgumentException('Invalid position for filter "watermark"'); } $clip->filters()->watermark($path, $coord); @@ -315,7 +340,7 @@ private function framerate(Clip $clip, array $options, TransformationContextInte { $framerate = $options['framerate'] ?? 0; if ($framerate <= 0) { - throw new \InvalidArgumentException('Invalid framerate for filter "framerate"'); + throw new InvalidArgumentException('Invalid framerate for filter "framerate"'); } $gop = $options['gop'] ?? 0; @@ -327,7 +352,7 @@ private function getDimension(array $options, string $filterName): FFMpeg\Coordi $width = $options['width'] ?? 0; $height = $options['height'] ?? 0; if ($width <= 0 || $height <= 0) { - throw new \InvalidArgumentException(sprintf('Invalid width/height for filter "%s"', $filterName)); + throw new InvalidArgumentException(sprintf('Invalid width/height for filter "%s"', $filterName)); } return new FFMpeg\Coordinate\Dimension($width, $height); diff --git a/lib/php/rendition-factory/src/Transformer/Video/VideoSummaryTransformerModule.php b/lib/php/rendition-factory/src/Transformer/Video/VideoSummaryTransformerModule.php index 8bf12e06e..5e87d190c 100644 --- a/lib/php/rendition-factory/src/Transformer/Video/VideoSummaryTransformerModule.php +++ b/lib/php/rendition-factory/src/Transformer/Video/VideoSummaryTransformerModule.php @@ -8,95 +8,84 @@ use Alchemy\RenditionFactory\DTO\OutputFile; use Alchemy\RenditionFactory\DTO\OutputFileInterface; use Alchemy\RenditionFactory\Transformer\TransformerModuleInterface; +use Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface; use FFMpeg; use FFMpeg\Coordinate\TimeCode; -use FFMpeg\Format\FormatInterface; +use FFMpeg\Format\VideoInterface; +use InvalidArgumentException; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\NotFoundExceptionInterface; +use RuntimeException; +use Symfony\Component\DependencyInjection\Attribute\AutowireLocator; +use Symfony\Component\DependencyInjection\ServiceLocator; + final readonly class VideoSummaryTransformerModule implements TransformerModuleInterface { + public function __construct(#[AutowireLocator(FormatInterface::TAG, defaultIndexMethod: 'getFormat')] private ServiceLocator $formats) + { + } + public static function getName(): string { return 'video_summary'; } + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ public function transform(InputFileInterface $inputFile, array $options, TransformationContextInterface $context): OutputFileInterface { - if (!($format = $options['format'])) { - throw new \InvalidArgumentException('Missing format'); + if (!($format = $options['format'] ?? null)) { + throw new InvalidArgumentException('Missing format'); } - if (!($extension = $options['extension'])) { - throw new \InvalidArgumentException('Missing extension'); + + if(!$this->formats->has($format)) { + throw new InvalidArgumentException(sprintf('Invalid format %s', $format)); } + /** @var FormatInterface $outputFormat */ + $outputFormat = $this->formats->get($format); - $fqcnFormat = 'FFMpeg\\Format\\Video\\'.$format; - if (class_exists($fqcnFormat)) { - return $this->processVideo($format, $extension, $inputFile, $options, $context); + if($outputFormat->getFamily() !== FamilyEnum::Video) { + throw new InvalidArgumentException(sprintf('Invalid format %s, only video formats supported', $format)); } - throw new \InvalidArgumentException(sprintf('Invalid format %s', $format)); - } + if (null != ($extension = $options['extension'] ?? null)) { + if(!in_array($extension, $outputFormat->getAllowedExtensions())) { + throw new InvalidArgumentException(sprintf('Invalid extension %s for format %s', $extension, $format)); + } + } + else { + $extension = ($outputFormat->getAllowedExtensions())[0]; + } - private function processVideo(string $format, string $extension, InputFileInterface $inputFile, array $options, TransformationContextInterface $context): OutputFileInterface - { $period = $options['period'] ?? 0; if ($period <= 0) { - throw new \InvalidArgumentException(sprintf('Invalid period for module "%s"', self::getName())); + throw new InvalidArgumentException(sprintf('Invalid period for module "%s"', self::getName())); } $clipDuration = $options['duration'] ?? 0; if ($clipDuration <= 0 || $clipDuration >= $period) { - throw new \InvalidArgumentException(sprintf('Invalid duration for module "%s"', self::getName())); + throw new InvalidArgumentException(sprintf('Invalid duration for module "%s"', self::getName())); } - $fqcnFormat = 'FFMpeg\\Format\\Video\\'.$format; - $outpuFormat = new $fqcnFormat(); + /** @var VideoInterface $FFMpegOutputFormat */ + $FFMpegOutputFormat = $outputFormat->getFFMpegFormat(); if ($videoCodec = $options['video_codec'] ?? null) { - if (!in_array($videoCodec, $outpuFormat->getAvailableVideoCodecs())) { - throw new \InvalidArgumentException(sprintf('Invalid video codec %s for format %s', $videoCodec, $format)); + if (!in_array($videoCodec, $FFMpegOutputFormat->getAvailableVideoCodecs())) { + throw new InvalidArgumentException(sprintf('Invalid video codec %s for format %s', $videoCodec, $format)); } - $outpuFormat->setVideoCodec($videoCodec); + $FFMpegOutputFormat->setVideoCodec($videoCodec); } if ($audioCodec = $options['audio_codec'] ?? null) { - if (!in_array($audioCodec, $outpuFormat->getAvailableAudioCodecs())) { - throw new \InvalidArgumentException(sprintf('Invalid audio codec %s for format %s', $audioCodec, $format)); + if (!in_array($audioCodec, $FFMpegOutputFormat->getAvailableAudioCodecs())) { + throw new InvalidArgumentException(sprintf('Invalid audio codec %s for format %s', $audioCodec, $format)); } - $outpuFormat->setAudioCodec($audioCodec); + $FFMpegOutputFormat->setAudioCodec($audioCodec); } - // try to find an "enhanced by alchemy" format (supports "copy" video codec) for clips - - switch ($inputFile->getType()) { - case 'video/mp4': - case 'video/mov': - $clipsFormatName = 'X264'; - break; - case 'video/webm': - $clipsFormatName = 'WebM'; - break; - case 'video/ogg': - $clipsFormatName = 'Ogg'; - break; - default: - $clipsFormatName = '?'; - break; - } + $clipsExtension = ($outputFormat->getAllowedExtensions())[0]; - /** @var FormatInterface $clipsFormat */ - $fqcnFormat = 'Alchemy\\RenditionFactory\\Transformer\\Video\\FFMpeg\\Format\\Video\\'.$clipsFormatName; - if (class_exists($fqcnFormat)) { - $clipsFormat = new $fqcnFormat(); - if (in_array('copy', $clipsFormat->getAvailableVideoCodecs())) { - $clipsFormat->setVideoCodec('copy'); - } - if (in_array('copy', $clipsFormat->getAvailableAudioCodecs())) { - $clipsFormat->setAudioCodec('copy'); - } - $clipsExtension = $inputFile->getExtension(); - } else { - $clipsFormat = $outpuFormat; - $clipsExtension = $extension; - } - - $outputPath = $context->createTmpFilePath($extension); $clipsFiles = []; try { $ffmpeg = FFMpeg\FFMpeg::create([], $context->getLogger()); @@ -108,25 +97,30 @@ private function processVideo(string $format, string $extension, InputFileInterf $context->log(sprintf('Duration duration: %s, extracting %d clips of %d seconds', $inputDuration, $nClips, $clipDuration)); $clipDuration = TimeCode::fromSeconds($clipDuration); + $removeAudioFilter = new FFMpeg\Filters\Audio\SimpleFilter(['-an']); for ($i = 0; $i < $nClips; ++$i) { $start = $i * $period; $clip = $video->clip(TimeCode::fromSeconds($start), $clipDuration); + $clip->addFilter($removeAudioFilter); $clipPath = $context->createTmpFilePath($clipsExtension); - $clip->save($clipsFormat, $clipPath); + $clip->save($FFMpegOutputFormat, $clipPath); + unset($clip); + gc_collect_cycles(); $clipsFiles[] = realpath($clipPath); } + unset($removeAudioFilter, $video); + gc_collect_cycles(); $outVideo = $ffmpeg->open($clipsFiles[0]); - if ($format == $clipsFormatName) { - $outVideo - ->concat($clipsFiles) - ->saveFromSameCodecs($outputPath, true); - } else { - $outVideo - ->concat($clipsFiles) - ->saveFromDifferentCodecs($outpuFormat, $outputPath); - } + $outputPath = $context->createTmpFilePath($extension); + + $outVideo + ->concat($clipsFiles) + ->saveFromSameCodecs($outputPath, true); + + unset($outVideo, $ffmpeg); + gc_collect_cycles(); } finally { foreach ($clipsFiles as $clipFile) { @unlink($clipFile); @@ -134,14 +128,13 @@ private function processVideo(string $format, string $extension, InputFileInterf } if (!file_exists($outputPath)) { - throw new \RuntimeException(sprintf('Failed to create summary video')); + throw new RuntimeException('Failed to create summary video'); } - // TODO return the correct family and MIME type return new OutputFile( $outputPath, - 'application/octet-stream', - FamilyEnum::Unknown + $outputFormat->getMimeType(), + $outputFormat->getFamily(), ); } } diff --git a/lib/php/rendition-factory/src/Transformer/Video/VideoToAnimationTransformerModule.php b/lib/php/rendition-factory/src/Transformer/Video/VideoToAnimationTransformerModule.php new file mode 100644 index 000000000..dc4497a0f --- /dev/null +++ b/lib/php/rendition-factory/src/Transformer/Video/VideoToAnimationTransformerModule.php @@ -0,0 +1,165 @@ +formats->has($format)) { + throw new InvalidArgumentException(sprintf('Invalid format %s', $format)); + } + /** @var FormatInterface $outputFormat */ + $outputFormat = $this->formats->get($format); + if($outputFormat->getFamily() !== FamilyEnum::Animation) { + throw new InvalidArgumentException(sprintf('Invalid format %s, only animation formats supported', $format)); + } + + if (null != ($extension = $options['extension'] ?? null)) { + if(!in_array($extension, $outputFormat->getAllowedExtensions())) { + throw new InvalidArgumentException(sprintf('Invalid extension %s for format %s', $extension, $format)); + } + } + else { + $extension = ($outputFormat->getAllowedExtensions())[0]; + } + + $fromSeconds = FFMpeg\Coordinate\TimeCode::fromSeconds($options['from_seconds'] ?? 0); + + $duration = $options['duration'] ?? null; + if(null !== $duration && ($duration = (int)$duration) <= 0) { + throw new InvalidArgumentException('Invalid duration'); + } + + if( ($fps = (int)($options['fps'] ?? 1)) <= 0) { + throw new InvalidArgumentException('Invalid fps'); + } + + $width = $options['width'] ?? null; + $height = $options['height'] ?? null; + if ( (null !== $width && ($width = (int)$width) <= 0) || (null !== $height && ($height = (int)$height) <= 0)) { + throw new InvalidArgumentException('Invalid width or height'); + } + + $mode = $options['mode'] ?? FFMpeg\Filters\Video\ResizeFilter::RESIZEMODE_INSET; + if (!in_array( + $mode, + [ + FFMpeg\Filters\Video\ResizeFilter::RESIZEMODE_INSET, + FFMpeg\Filters\Video\ResizeFilter::RESIZEMODE_FIT, + FFMpeg\Filters\Video\ResizeFilter::RESIZEMODE_SCALE_WIDTH, + FFMpeg\Filters\Video\ResizeFilter::RESIZEMODE_SCALE_HEIGHT, + ] + )) { + throw new InvalidArgumentException('Invalid resize mode'); + } + switch($mode) { + case FFMpeg\Filters\Video\ResizeFilter::RESIZEMODE_INSET: + list($width, $height) = $this->getDimensionsInset($inputFile->getPath(), $width, $height); + break; + // other modes not implemented + default: + throw new InvalidArgumentException('Invalid resize mode'); + } + + $commands = [ + '-i', + $inputFile->getPath(), + '-ss', + $fromSeconds, + ]; + if(null !== $duration) { + $commands[] = '-t'; + $commands[] = $duration; + } + $commands[] = '-vf'; + $commands[] = 'fps='.$fps.',scale='.$width.':'.$height.':flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse'; + + $commands[] = '-loop'; + $commands[] = '0'; + + $outputPath = $context->createTmpFilePath($extension); + $commands[] = $outputPath; + + $ffmpeg = FFMpeg\FFMpeg::create(); + $ffmpeg->getFFMpegDriver()->command($commands); + + if(!file_exists($outputPath)) { + throw new RuntimeException('Failed to create animated gif'); + } + + unset($ffmpeg); + gc_collect_cycles(); + + return new OutputFile( + $outputPath, + $outputFormat->getMimeType(), + $outputFormat->getFamily() + ); + } + + private function getDimensionsInset($path, $width, $height): array + { + if(null === $width && null === $height) { + return [-1, -1]; + } + if(null === $width) { + return [-1, $height]; + } + if(null === $height) { + return [$width, -1]; + } + $ffmpeg = FFMpeg\FFMpeg::create(); + $video = $ffmpeg->open($path); + $dimensions = null; + foreach ($video->getStreams() as $stream) { + if ($stream->isVideo()) { + try { + $dimensions = $stream->getDimensions(); + break; + } catch (Exception $e) { + // no-op + } + } + } + unset($video, $ffmpeg); + if($dimensions) { + $wRatio = $width ? ($dimensions->getWidth() / $width) : 0; + $hRatio = $height ? ($dimensions->getHeight() / $height) : 0; + if($wRatio > $hRatio) { + return [(int)floor($dimensions->getWidth() / $wRatio), -1]; + } + else { + return [-1, (int)floor($dimensions->getHeight() / $hRatio)]; + } + } + return [$width, $height]; // fallback : exact fit (maybe not homothetic) + } +} diff --git a/lib/php/rendition-factory/src/Transformer/Video/VideoToFrameTransformerModule.php b/lib/php/rendition-factory/src/Transformer/Video/VideoToFrameTransformerModule.php new file mode 100644 index 000000000..99f094668 --- /dev/null +++ b/lib/php/rendition-factory/src/Transformer/Video/VideoToFrameTransformerModule.php @@ -0,0 +1,72 @@ +formats->has($format)) { + throw new InvalidArgumentException(sprintf('Invalid format %s', $format)); + } + /** @var FormatInterface $outputFormat */ + $outputFormat = $this->formats->get($format); + + if($outputFormat->getFamily() !== FamilyEnum::Image) { + throw new InvalidArgumentException(sprintf('Invalid format %s, only image formats supported', $format)); + } + + if (null != ($extension = $options['extension'] ?? null)) { + if(!in_array($extension, $outputFormat->getAllowedExtensions())) { + throw new InvalidArgumentException(sprintf('Invalid extension %s for format %s', $extension, $format)); + } + } + else { + $extension = ($outputFormat->getAllowedExtensions())[0]; + } + + $ffmpeg = FFMpeg\FFMpeg::create(); // (new FFMpeg\FFMpeg)->open('/path/to/video'); + + $from_seconds = $options['from_seconds'] ?? 0; + + $video = $ffmpeg->open($inputFile->getPath()); + $frame = $video->frame(FFMpeg\Coordinate\TimeCode::fromSeconds($from_seconds)); + $outputPath = $context->createTmpFilePath($extension); + + $frame->save($outputPath); + + unset($frame, $video, $ffmpeg); + gc_collect_cycles(); + + return new OutputFile( + $outputPath, + $outputFormat->getMimeType(), + $outputFormat->getFamily(), + ); + } +} From 228c78ae9c4251cab54f5b8a9202d9bbeb3fe215 Mon Sep 17 00:00:00 2001 From: jygaulier Date: Thu, 17 Oct 2024 12:34:10 +0200 Subject: [PATCH 2/3] add animation module and animated formats --- .../src/Command/CreateCommand.php | 6 +-- .../src/Config/YamlLoader.php | 1 + .../Video/FFMpeg/Format/FormatInterface.php | 18 ++++++++ .../Video/FFMpeg/Format/JpegFormat.php | 29 +++++++++++++ .../Video/FFMpeg/Format/MkvFormat.php | 42 +++++++++++++++++++ .../Video/FFMpeg/Format/Mpeg4Format.php | 42 +++++++++++++++++++ .../Video/FFMpeg/Format/MpegFormat.php | 42 +++++++++++++++++++ .../Video/FFMpeg/Format/QuicktimeFormat.php | 42 +++++++++++++++++++ .../Video/FFMpeg/Format/Video/WebM.php | 22 ---------- .../Video/FFMpeg/Format/Video/X264.php | 22 ---------- .../Video/FFMpeg/Format/WebmFormat.php | 42 +++++++++++++++++++ 11 files changed, 261 insertions(+), 47 deletions(-) create mode 100644 lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/FormatInterface.php create mode 100644 lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/JpegFormat.php create mode 100644 lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/MkvFormat.php create mode 100644 lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/Mpeg4Format.php create mode 100644 lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/MpegFormat.php create mode 100644 lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/QuicktimeFormat.php delete mode 100644 lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/Video/WebM.php delete mode 100644 lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/Video/X264.php create mode 100644 lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/WebmFormat.php diff --git a/lib/php/rendition-factory/src/Command/CreateCommand.php b/lib/php/rendition-factory/src/Command/CreateCommand.php index f1821e0ad..9ee330de9 100644 --- a/lib/php/rendition-factory/src/Command/CreateCommand.php +++ b/lib/php/rendition-factory/src/Command/CreateCommand.php @@ -32,7 +32,7 @@ protected function configure(): void $this->addArgument('src', InputArgument::REQUIRED, 'The source file'); $this->addArgument('build-config', InputArgument::REQUIRED, 'The build config YAML file'); - $this->addOption('type', 't', InputOption::VALUE_OPTIONAL, 'Force the MIME type of file'); + $this->addOption('type', 't', InputOption::VALUE_REQUIRED, 'Force the MIME type of file'); $this->addOption('working-dir', 'w', InputOption::VALUE_REQUIRED, 'The working directory. Defaults to system temp directory'); $this->addOption('output', 'o', InputOption::VALUE_REQUIRED, 'The output file name WITHOUT extension'); $this->addOption('debug', 'd', InputOption::VALUE_NONE, 'set to debug mode (keep files in working directory)'); @@ -72,6 +72,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $buildConfig, $options ); + $output->writeln(sprintf('Rendition created: %s', $outputFile->getPath())); + } catch (\InvalidArgumentException $e) { $output->writeln(sprintf('%s', $e->getMessage())); @@ -91,8 +93,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 1; } - $output->writeln(sprintf('Rendition created: %s', $outputFile->getPath())); - if (!$input->getOption('debug')) { $this->renditionCreator->cleanUp(); } diff --git a/lib/php/rendition-factory/src/Config/YamlLoader.php b/lib/php/rendition-factory/src/Config/YamlLoader.php index 2dac57c54..bed285f97 100644 --- a/lib/php/rendition-factory/src/Config/YamlLoader.php +++ b/lib/php/rendition-factory/src/Config/YamlLoader.php @@ -70,6 +70,7 @@ private function parseTransformation(array $transformation): Transformation { return new Transformation( $transformation['module'], + $transformation['enabled'] ?? true, $transformation['options'] ?? [], $transformation['description'] ?? null ); diff --git a/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/FormatInterface.php b/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/FormatInterface.php new file mode 100644 index 000000000..493ac99d3 --- /dev/null +++ b/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/FormatInterface.php @@ -0,0 +1,18 @@ +format = new X264(); + } + + public static function getAllowedExtensions(): array + { + return ['mkv']; + } + + public static function getMimeType(): string + { + return 'video/x-matroska'; + } + + public static function getFormat(): string + { + return 'video-mkv'; + } + + public static function getFamily(): FamilyEnum + { + return FamilyEnum::Video; + } + + public function getFFMpegFormat(): VideoInterface + { + return $this->format; + } +} diff --git a/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/Mpeg4Format.php b/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/Mpeg4Format.php new file mode 100644 index 000000000..6280581d3 --- /dev/null +++ b/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/Mpeg4Format.php @@ -0,0 +1,42 @@ +format = new X264(); + } + + public static function getAllowedExtensions(): array + { + return ['mp4']; + } + + public static function getMimeType(): string + { + return 'video/mp4'; + } + + public static function getFormat(): string + { + return 'video-mpeg4'; + } + + public static function getFamily(): FamilyEnum + { + return FamilyEnum::Video; + } + + public function getFFMpegFormat(): VideoInterface + { + return $this->format; + } +} diff --git a/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/MpegFormat.php b/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/MpegFormat.php new file mode 100644 index 000000000..914fcb3e5 --- /dev/null +++ b/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/MpegFormat.php @@ -0,0 +1,42 @@ +format = new X264(); + } + + public static function getAllowedExtensions(): array + { + return ['mpeg']; + } + + public static function getMimeType(): string + { + return 'video/mpeg'; + } + + public static function getFormat(): string + { + return 'video-mpeg'; + } + + public static function getFamily(): FamilyEnum + { + return FamilyEnum::Video; + } + + public function getFFMpegFormat(): VideoInterface + { + return $this->format; + } +} diff --git a/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/QuicktimeFormat.php b/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/QuicktimeFormat.php new file mode 100644 index 000000000..951e10bed --- /dev/null +++ b/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/QuicktimeFormat.php @@ -0,0 +1,42 @@ +format = new X264(); + } + + public static function getAllowedExtensions(): array + { + return ['mov']; + } + + public static function getMimeType(): string + { + return 'video/quicktime'; + } + + public static function getFormat(): string + { + return 'video-quicktime'; + } + + public static function getFamily(): FamilyEnum + { + return FamilyEnum::Video; + } + + public function getFFMpegFormat(): VideoInterface + { + return $this->format; + } +} diff --git a/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/Video/WebM.php b/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/Video/WebM.php deleted file mode 100644 index 63f2b5e6b..000000000 --- a/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/Video/WebM.php +++ /dev/null @@ -1,22 +0,0 @@ -videoCodecs = parent::getAvailableVideoCodecs(); - if (!in_array('copy', $this->videoCodecs)) { - $this->videoCodecs[] = 'copy'; - } - parent::__construct($audioCodec, $videoCodec); - } - - public function getAvailableVideoCodecs() - { - return $this->videoCodecs; - } -} diff --git a/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/Video/X264.php b/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/Video/X264.php deleted file mode 100644 index c303ab82d..000000000 --- a/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/Video/X264.php +++ /dev/null @@ -1,22 +0,0 @@ -videoCodecs = parent::getAvailableVideoCodecs(); - if (!in_array('copy', $this->videoCodecs)) { - $this->videoCodecs[] = 'copy'; - } - parent::__construct($audioCodec, $videoCodec); - } - - public function getAvailableVideoCodecs() - { - return $this->videoCodecs; - } -} diff --git a/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/WebmFormat.php b/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/WebmFormat.php new file mode 100644 index 000000000..e9f98cd6a --- /dev/null +++ b/lib/php/rendition-factory/src/Transformer/Video/FFMpeg/Format/WebmFormat.php @@ -0,0 +1,42 @@ +format = new WebM(); + } + + public static function getAllowedExtensions(): array + { + return ['webm']; + } + + public static function getMimeType(): string + { + return 'video/webm'; + } + + public static function getFormat(): string + { + return 'video-webm'; + } + + public static function getFamily(): FamilyEnum + { + return FamilyEnum::Video; + } + + public function getFFMpegFormat(): VideoInterface + { + return $this->format; + } +} From 2e5e04c0913538cc0575898321b747e5c8461203 Mon Sep 17 00:00:00 2001 From: jygaulier Date: Thu, 17 Oct 2024 16:04:37 +0200 Subject: [PATCH 3/3] doc ; factorization of common options : timeout, threads) --- lib/php/rendition-factory/README.md | 173 ++++++++++++++++++ .../src/Transformer/Video/FFMpegHelper.php | 29 +++ .../Video/FFMpegTransformerModule.php | 15 +- .../Video/VideoSummaryTransformerModule.php | 3 +- .../VideoToAnimationTransformerModule.php | 3 +- .../Video/VideoToFrameTransformerModule.php | 2 +- 6 files changed, 208 insertions(+), 17 deletions(-) create mode 100644 lib/php/rendition-factory/README.md create mode 100644 lib/php/rendition-factory/src/Transformer/Video/FFMpegHelper.php diff --git a/lib/php/rendition-factory/README.md b/lib/php/rendition-factory/README.md new file mode 100644 index 000000000..3dd2347c6 --- /dev/null +++ b/lib/php/rendition-factory/README.md @@ -0,0 +1,173 @@ +# Rendition Factory for video-input modules (wip) + +## Common options + +### `enabled` (optional) + +Used to disable a whole module from the build chain. + +__default__: true + +### `format` (mandatory) + +A format defines the output file : +- family (image, video, audio, animation, document, unknown) +- mime type (unique mime type for this type of file) +- extension (possible extenstion(s) for this type of file) + +For a specific module, only a subset of formats may be available, e.g.: +Since `video_to_frame` extracts one image from the video, the only supported output format(s) +are ones of family=image. + +see below "Output formats" for the list of available formats. + +### `extension` (optional) + +For the file formats that support multiple extensions, e.g.: +`image/jpeg` : [`jpg`, `jpeg`], the prefered extension can be set to override the default (first) one. + +__default__: first value in the list of extensions. + +### `timeout` (optional) + +maximum duration of the ffmpeg command in seconds. + +__default__: 3600 seconds + +### `threads` (optional) + +set the number of threads used by ffmpeg. + +__default__: depends on cpu (usually high), so the setting in most usefull to limit the cpu usage + +## Common options for video output formats + +### `video_kilobitrate`, `audio_kilobitrate` (optionals - advanced -) + +For video and audio output formats, change bitrate. + +__default__: depends on the output format. + +### `video_codec`, `audio_codec`, `passes` (optionals - advanced -) + +Video output formats use internaly a "ffmpeg-format" which itself may support multiple codecs. + +e.g. `video-mpeg4` uses ffmpeg-format `X264`, which supports many audio codecs like `aac`, `libmp3lame`, ... + +One can change the default ffmpeg codec(s) by setting `video_codec` and/or `audio_codec`. + +__default__: depends on the output format, if it uses internally a "ffmpeg-format" like X264, Ogg, ... + + + +-------------------------------------------- + +# Modules + +## video_to_frame +Extracts a frame (image) from a video. + +- `from_seconds` time in the video where the frame is extracted. + +```yaml +# example +video: + normalization: ~ + transformations: + - + module: video_to_frame + enabled: true + options: + timeout: 3600 + threads: 4 + format: image-jpeg + from_seconds: 4 + extension: jpeg +``` + +## video_to_animation +Build an animation from a video. + +- `from_seconds` time in the video where the animation begins. +- `duration` duration of the animation in seconds. +- `fps` frames per second of the animation. +- `width`, `height` size of the animation (see below "resize modes"). +- `mode` default to `inset` (see below "resize modes"). + +```yaml + module: video_to_animation + options: + format: animated-gif + from_seconds: 25 + duration: 5 + fps: 5 + width: 200 + height: 100 + mode: inset +``` + +## video_summary +Build a video made from extracts of the input video. + +- `period` period in seconds between each extract. +- `duration` duration of each extract in seconds. + +```yaml + module: video_summary + options: + format: video-quicktime + period: 30 + duration: 2 +``` + +## ffmpeg +Generic module to chain ffmpeg "filters" in a single command. + +- `filters` list of ffmpeg filters to apply. + +Each "filter" has a name and a list of specific options. + +```yaml + module: ffmpeg + options: + format: video-quicktime + filters: + - + name: resize + width: 320 + height: 240 + mode: inset + - + name: watermark + # only local files are supported for now + path: "/var/workspace/my_watermarks/google_PNG.png" + position: relative + bottom: 50 + right: 50 +``` + + +-------------------------------------------- + +## Output formats + +| format | family | mime type | extension(s) | +|-----------------|-----------|------------------|--------------| +| animated-gif | Animation | image/gif | gif | +| animated-png | Animation | image/png | apng, png | +| animated-webp | Animation | image/webp | webp | +| image-jpeg | Image | image/jpeg | jpg, jpeg | +| video-mkv | Video | video/x-matroska | mkv | +| video-mpeg4 | Video | video/mp4 | mp4 | +| video-mpeg | Video | video/mpeg | mpeg | +| video-quicktime | Video | video/quicktime | mov | +| video-webm | Video | video/webm | webm | + +-------------------------------------------- + +## Resize modes +### `inset` +The output is garanteed to fit in the requested size (width, height) and the aspect ratio is kept. +- If only one dimension is provided, the other is computed. +- If both dimensions are provided, the output is resize so the biggest dimension fits into the rectangle. +- If no dimension is provided, the output is the same size as the input. diff --git a/lib/php/rendition-factory/src/Transformer/Video/FFMpegHelper.php b/lib/php/rendition-factory/src/Transformer/Video/FFMpegHelper.php new file mode 100644 index 000000000..029ecd1b9 --- /dev/null +++ b/lib/php/rendition-factory/src/Transformer/Video/FFMpegHelper.php @@ -0,0 +1,29 @@ +getLogger()); + } +} diff --git a/lib/php/rendition-factory/src/Transformer/Video/FFMpegTransformerModule.php b/lib/php/rendition-factory/src/Transformer/Video/FFMpegTransformerModule.php index fa53886d1..48b1f0aeb 100644 --- a/lib/php/rendition-factory/src/Transformer/Video/FFMpegTransformerModule.php +++ b/lib/php/rendition-factory/src/Transformer/Video/FFMpegTransformerModule.php @@ -117,20 +117,7 @@ private function doVideo(FormatInterface $ouputFormat, string $extension, InputF $FFMpegFormat->setPasses($passes); } - $ffmpegOptions = []; - if ($timeout = $options['timeout'] ?? null) { - if (!is_int($timeout)) { - throw new InvalidArgumentException('Invalid timeout'); - } - $ffmpegOptions['timeout'] = $timeout; - } - if ($threads = $options['threads'] ?? null) { - if (!is_int($threads) || $threads < 1) { - throw new InvalidArgumentException('Invalid threads count'); - } - $ffmpegOptions['ffmpeg.threads'] = $threads; - } - $ffmpeg = FFMpeg\FFMpeg::create($ffmpegOptions, $context->getLogger()); + $ffmpeg = FFMpegHelper::createFFMpeg($options, $context); /** @var Video $video */ $video = $ffmpeg->open($inputFile->getPath()); diff --git a/lib/php/rendition-factory/src/Transformer/Video/VideoSummaryTransformerModule.php b/lib/php/rendition-factory/src/Transformer/Video/VideoSummaryTransformerModule.php index 5e87d190c..a09d8930f 100644 --- a/lib/php/rendition-factory/src/Transformer/Video/VideoSummaryTransformerModule.php +++ b/lib/php/rendition-factory/src/Transformer/Video/VideoSummaryTransformerModule.php @@ -86,9 +86,10 @@ public function transform(InputFileInterface $inputFile, array $options, Transfo $clipsExtension = ($outputFormat->getAllowedExtensions())[0]; + $ffmpeg = FFMpegHelper::createFFMpeg($options, $context); + $clipsFiles = []; try { - $ffmpeg = FFMpeg\FFMpeg::create([], $context->getLogger()); /** @var FFMpeg\Media\Video $video */ $video = $ffmpeg->open($inputFile->getPath()); diff --git a/lib/php/rendition-factory/src/Transformer/Video/VideoToAnimationTransformerModule.php b/lib/php/rendition-factory/src/Transformer/Video/VideoToAnimationTransformerModule.php index dc4497a0f..e61b6701a 100644 --- a/lib/php/rendition-factory/src/Transformer/Video/VideoToAnimationTransformerModule.php +++ b/lib/php/rendition-factory/src/Transformer/Video/VideoToAnimationTransformerModule.php @@ -108,7 +108,8 @@ public function transform(InputFileInterface $inputFile, array $options, Transfo $outputPath = $context->createTmpFilePath($extension); $commands[] = $outputPath; - $ffmpeg = FFMpeg\FFMpeg::create(); + $ffmpeg = FFMpegHelper::createFFMpeg($options, $context); + $ffmpeg->getFFMpegDriver()->command($commands); if(!file_exists($outputPath)) { diff --git a/lib/php/rendition-factory/src/Transformer/Video/VideoToFrameTransformerModule.php b/lib/php/rendition-factory/src/Transformer/Video/VideoToFrameTransformerModule.php index 99f094668..706b8072e 100644 --- a/lib/php/rendition-factory/src/Transformer/Video/VideoToFrameTransformerModule.php +++ b/lib/php/rendition-factory/src/Transformer/Video/VideoToFrameTransformerModule.php @@ -50,7 +50,7 @@ public function transform(InputFileInterface $inputFile, array $options, Transfo $extension = ($outputFormat->getAllowedExtensions())[0]; } - $ffmpeg = FFMpeg\FFMpeg::create(); // (new FFMpeg\FFMpeg)->open('/path/to/video'); + $ffmpeg = FFMpegHelper::createFFMpeg($options, $context); $from_seconds = $options['from_seconds'] ?? 0;