Skip to content

Commit

Permalink
PS-670-rendition-video_enhance3 (#494)
Browse files Browse the repository at this point in the history
* more "twigable" options
* almost every "time" options supports float (2.5) or timecode ("00.00.02.50")
* video-summary module  can generate "phraseanet" animated gif
    - period sets the gap between frames from the _input video_, e.g. `"{{ input.duration/10 }}"` -> 10 frames extracted.
    - duration sets the duration in seconds of each frame into the _output animated gif_, e.g. `0.1` -> 10 frames / seconds.
* video-summary module supports `start`
* ffmpeg module can extract audio: set `format: "audio-wav"` or `audio-mp3`, `audio-aac` ; todo: audio options
* ffmpeg module / filter "watermark" supports `path: <url>`
* bypass ffmpeg bugs (timecode / seconds)
* fix attributes access in twig : use `"{{ attr.myfield }}"` where `myfield` is the *slugified* attribute name
* new cli `app:documentation:dump` to generate doc
* documentation & config validator as services, used by databox.
* allow empty definition
* move video output "formats" documentation to each related transformer module.
  • Loading branch information
jygaulier authored Dec 9, 2024
1 parent 944c51c commit bb8482a
Show file tree
Hide file tree
Showing 44 changed files with 1,662 additions and 214 deletions.
2 changes: 2 additions & 0 deletions databox/api/config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,5 @@ services:
$decorated: '@.inner'

Alchemy\RenditionFactory\Templating\TemplateResolverInterface: '@App\Asset\Attribute\TemplateResolver'

App\Validator\ValidRenditionDefinitionConstraintValidator: ~
31 changes: 31 additions & 0 deletions databox/api/src/Command/DocumentationDumperCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace App\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Alchemy\RenditionFactory\RenditionBuilderConfigurationDocumentation;


#[AsCommand('app:documentation:dump')]
class DocumentationDumperCommand extends Command
{
public function __construct(
private readonly RenditionBuilderConfigurationDocumentation $renditionBuilderConfigurationDocumentation,
)
{
parent::__construct();
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('# ' . $this->renditionBuilderConfigurationDocumentation::getName());
$output->writeln($this->renditionBuilderConfigurationDocumentation->generate());

return Command::SUCCESS;
}
}
7 changes: 4 additions & 3 deletions databox/api/src/Entity/Core/RenditionDefinition.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
namespace App\Entity\Core;

use Alchemy\CoreBundle\Entity\AbstractUuidEntity;
use Alchemy\CoreBundle\Entity\Traits\CreatedAtTrait;
use Alchemy\CoreBundle\Entity\Traits\UpdatedAtTrait;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
Expand All @@ -16,10 +18,8 @@
use App\Api\Model\Input\RenditionDefinitionInput;
use App\Api\Provider\RenditionDefinitionCollectionProvider;
use App\Controller\Core\RenditionDefinitionSortAction;

use Alchemy\CoreBundle\Entity\Traits\CreatedAtTrait;
use Alchemy\CoreBundle\Entity\Traits\UpdatedAtTrait;
use App\Entity\Traits\WorkspaceTrait;
use App\Validator as CustomAssert;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection as DoctrineCollection;
use Doctrine\DBAL\Types\Types;
Expand Down Expand Up @@ -153,6 +153,7 @@ class RenditionDefinition extends AbstractUuidEntity implements \Stringable
#[Groups([RenditionDefinition::GROUP_LIST, RenditionDefinition::GROUP_READ, RenditionDefinition::GROUP_WRITE])]
#[ORM\Column(type: Types::TEXT, nullable: true)]
#[ApiProperty(security: self::GRANT_ADMIN_PROP)]
#[CustomAssert\ValidRenditionDefinitionConstraint]
private ?string $definition = null;

#[Groups([RenditionDefinition::GROUP_READ])]
Expand Down
17 changes: 17 additions & 0 deletions databox/api/src/Validator/ValidRenditionDefinitionConstraint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace App\Validator;

use Symfony\Component\Validator\Constraint;

/** @uses ValidRenditionDefinitionConstraintValidator */
#[\Attribute]
class ValidRenditionDefinitionConstraint extends Constraint
{
public function getTargets(): string|array
{
return self::PROPERTY_CONSTRAINT;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace App\Validator;

use Alchemy\RenditionFactory\Config\BuildConfigValidator;
use Alchemy\RenditionFactory\Config\YamlLoader;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

class ValidRenditionDefinitionConstraintValidator extends ConstraintValidator
{
/** @uses BuildConfigValidator */
public function __construct(private readonly YamlLoader $yamlLoader, private readonly BuildConfigValidator $validator)
{
}

/**
* @param string $value
* @param ValidRenditionDefinitionConstraint $constraint
*/
public function validate($value, Constraint $constraint): void
{
if(!$value) {
return;
}
try {
$config = $this->yamlLoader->parse($value);
$this->validator->validate($config);
} catch (\Exception $e) {
$this->context
->buildViolation($e->getMessage())
->addViolation();
}
}
}
56 changes: 37 additions & 19 deletions lib/php/rendition-factory-bundle/Resources/config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ services:
autoconfigure: true

Alchemy\RenditionFactory\Command\CreateCommand: ~
Alchemy\RenditionFactory\Command\ConfigurationValidateCommand: ~

Alchemy\RenditionFactory\Context\TransformationContextFactory: ~
Alchemy\RenditionFactory\FileFamilyGuesser: ~
Expand Down Expand Up @@ -44,43 +45,56 @@ services:
tags:
- { name: !php/const Alchemy\RenditionFactory\Transformer\TransformerModuleInterface::TAG }

# FFMpeg "formats"
Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\JpegFormat:
# Output "formats"
Alchemy\RenditionFactory\Transformer\Video\Format\JpegFormat:
tags:
- { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG }
- { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG }

Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\MkvFormat:
Alchemy\RenditionFactory\Transformer\Video\Format\MkvFormat:
tags:
- { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG }
- { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG }

Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\Mpeg4Format:
Alchemy\RenditionFactory\Transformer\Video\Format\Mpeg4Format:
tags:
- { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG }
- { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG }

Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\MpegFormat:
Alchemy\RenditionFactory\Transformer\Video\Format\MpegFormat:
tags:
- { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG }
- { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG }

Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\QuicktimeFormat:
Alchemy\RenditionFactory\Transformer\Video\Format\QuicktimeFormat:
tags:
- { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG }
- { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG }

Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\WebmFormat:
Alchemy\RenditionFactory\Transformer\Video\Format\WebmFormat:
tags:
- { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG }
- { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG }

Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\AnimatedGifFormat:
Alchemy\RenditionFactory\Transformer\Video\Format\AnimatedGifFormat:
tags:
- { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG }
- { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG }

Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\AnimatedPngFormat:
Alchemy\RenditionFactory\Transformer\Video\Format\AnimatedPngFormat:
tags:
- { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG }
- { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG }

Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\AnimatedWebpFormat:
Alchemy\RenditionFactory\Transformer\Video\Format\AnimatedWebpFormat:
tags:
- { name: !php/const Alchemy\RenditionFactory\Transformer\Video\FFMpeg\Format\FormatInterface::TAG }
- { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG }

Alchemy\RenditionFactory\Transformer\Video\Format\WavFormat:
tags:
- { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG }

Alchemy\RenditionFactory\Transformer\Video\Format\AacFormat:
tags:
- { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG }

Alchemy\RenditionFactory\Transformer\Video\Format\Mp3Format:
tags:
- { name: !php/const Alchemy\RenditionFactory\Transformer\Video\Format\FormatInterface::TAG }

Alchemy\RenditionFactory\Transformer\Video\Format\OutputFormatsDocumentation: ~

Imagine\Imagick\Imagine: ~
Imagine\Image\ImagineInterface: '@Imagine\Imagick\Imagine'
Expand All @@ -89,3 +103,7 @@ services:
Alchemy\RenditionFactory\Format\FormatGuesser: ~
Alchemy\RenditionFactory\Format\FormatFactory: ~
Alchemy\RenditionFactory\Config\ModuleOptionsResolver: ~

Alchemy\RenditionFactory\RenditionBuilderConfigurationDocumentation: ~

Alchemy\RenditionFactory\Config\BuildConfigValidator: ~
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace Alchemy\RenditionFactory\Command;

use Alchemy\RenditionFactory\Config\BuildConfigValidator;
use Alchemy\RenditionFactory\Config\YamlLoader;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

#[AsCommand('alchemy:rendition-factory:conf:validate')]
class ConfigurationValidateCommand extends Command
{
public function __construct(
private readonly YamlLoader $yamlLoader,
private readonly BuildConfigValidator $validator,
) {
parent::__construct();
}

protected function configure(): void
{
parent::configure();

$this->addArgument('config', InputArgument::REQUIRED, 'A build config YAML file to validate')
->setHelp('Validate a config file.')
;
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$config = $this->yamlLoader->load($input->getArgument('config'));
$this->validator->validate($config);

$output->writeln('Configuration is valid.');

return Command::SUCCESS;
}
}
22 changes: 13 additions & 9 deletions lib/php/rendition-factory/src/Command/CreateCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,34 +45,38 @@ protected function configure(): void

protected function execute(InputInterface $input, OutputInterface $output): int
{
$ret = 0;
$ret = Command::SUCCESS;
$src = $input->getArgument('src');
if (is_dir($src)) {
if (false === ($od = opendir($src))) {
$output->writeln(sprintf('Directory "%s" could not be opened.', $src));

return 1;
return Command::FAILURE;
}
while ($f = readdir($od)) {
if ('.' === $f || '..' === $f) {
continue;
}
$ret |= $this->doFile($input, $output, $src.'/'.$f);
if(false === $this->doFile($input, $output, $src.'/'.$f)) {
$ret = Command::FAILURE;
}
}
closedir($od);
} else {
$ret = $this->doFile($input, $output, $src);
if(false === $this->doFile($input, $output, $src)) {
$ret = Command::FAILURE;
}
}

return $ret;
}

protected function doFile(InputInterface $input, OutputInterface $output, string $src): int
protected function doFile(InputInterface $input, OutputInterface $output, string $src): bool
{
if (!file_exists($src)) {
$output->writeln(sprintf('File "%s" does not exist.', $src));

return 1;
return false;
}

$time = microtime(true);
Expand Down Expand Up @@ -104,7 +108,7 @@ protected function doFile(InputInterface $input, OutputInterface $output, string
} catch (\InvalidArgumentException $e) {
$output->writeln(sprintf('<error>%s</error>', $e->getMessage()));

return 1;
return false;
}

if ($outputPath = $input->getOption('output')) {
Expand All @@ -121,7 +125,7 @@ protected function doFile(InputInterface $input, OutputInterface $output, string
if ($src === $outputFile->getPath()) {
$output->writeln('No transformation needed');

return 1;
return false;
}

if (!$input->getOption('debug')) {
Expand All @@ -130,6 +134,6 @@ protected function doFile(InputInterface $input, OutputInterface $output, string

$output->writeln(sprintf('Execution time: %0.2f', microtime(true) - $time));

return 0;
return true;
}
}
57 changes: 57 additions & 0 deletions lib/php/rendition-factory/src/Config/BuildConfigValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

namespace Alchemy\RenditionFactory\Config;

use Alchemy\RenditionFactory\DTO\BuildConfig\BuildConfig;
use Alchemy\RenditionFactory\DTO\FamilyEnum;
use Alchemy\RenditionFactory\Transformer\TransformerModuleInterface;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Config\Definition\Processor;
use Symfony\Component\DependencyInjection\Attribute\TaggedLocator;
use Symfony\Component\DependencyInjection\ServiceLocator;

readonly class BuildConfigValidator
{
public function __construct(
#[TaggedLocator(TransformerModuleInterface::TAG, defaultIndexMethod: 'getName')]
private ServiceLocator $transformers,
) {
}

public function getTransformers(): ServiceLocator
{
return $this->transformers;
}

public function validate(BuildConfig $config): void
{
foreach (FamilyEnum::cases() as $family) {
$familyConfig = $config->getFamily($family);
if (null === $familyConfig) {
continue;
}
foreach ($familyConfig->getTransformations() as $transformation) {
$transformerName = $transformation->getModule();

/** @var TransformerModuleInterface $transformer */
$transformer = $this->transformers->get($transformerName);

try {
$this->checkTransformerConfiguration($transformer, $transformation->toArray());
} catch (\Throwable $e) {
$msg = sprintf("Error in module \"%s\"\n%s", $transformerName, $e->getMessage());
throw new InvalidConfigurationException($msg);
}
}
}
}

private function checkTransformerConfiguration(TransformerModuleInterface $transformer, array $options): void
{
$documentation = $transformer->getDocumentation();
$treeBuilder = $documentation->getTreeBuilder();

$processor = new Processor();
$processor->process($treeBuilder->buildTree(), ['root' => $options]);
}
}
Loading

0 comments on commit bb8482a

Please sign in to comment.