Initial commit

This commit is contained in:
Chris 2016-12-22 03:15:02 +01:00
commit 58f27830f4
20 changed files with 654 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/vendor

35
README.md Normal file
View File

@ -0,0 +1,35 @@
VfxApply: Video Effect Toolkit
==============================
VfxApply is a tool to apply effects and filters using presets and plugins. A preset
can for example apply audio normalization, visualize audio as video, or apply a
Natron pipeline.
## Usage
vfxapply [-i <input>] [-o <output>|-O] [-p <preset>] [-c <key>=<value>]
-i Set the input file (if omitted, a file picker will be displayed)
-o Set the output file (if omitted, a file picker will be displayed)
-O Automatically assign output filename based on input filename
-p Select the preset to apply
-c Specify parameters for the preset
Examples:
vfxapply
Ask for input, output and preset
vfxapply -i in.mp4 -o out.m4
Ask for preset to apply and process in.mp4 into out.mp4
vfxapply -O -p ffmpeg-audio-normalize
Normalize audio after picking input, output name will be generated.
## Plugins
### ffmpeg
### natron

9
bin/vfxapply Executable file
View File

@ -0,0 +1,9 @@
#!/usr/bin/env php
<?php
if (!trim(exec("which zenity"))) {
fprintf(STDERR, "You need to install zenity to use this tool.\n");
exit(1);
}
require_once __DIR__."/../src/app.php";

20
composer.json Normal file
View File

@ -0,0 +1,20 @@
{
"name": "noccylabs/vfxapply",
"description": "Apply effects to video and audio",
"type": "application",
"license": "GPL-3.0",
"authors": [
{
"name": "Christopher Vagnetoft",
"email": "cvagnetoft@gmail.com"
}
],
"autoload": {
"psr-4": {
"VfxApply\\": "src/"
}
},
"require": {
"symfony/yaml": "^3.2"
}
}

89
plugins/ffmpeg/plugin.php Normal file
View File

@ -0,0 +1,89 @@
<?php
namespace VfxApply\Plugin\Ffmpeg;
use VfxApply\Plugin;
use VfxApply\Input;
use VfxApply\Output;
use VfxApply\Preset;
class FfmpegPlugin extends Plugin
{
public function getName()
{
return "ffmpeg";
}
private function appendFilterOption($filter, $key, $value=null)
{
if (strpos($filter,"=")===false) {
$filter.= "=";
} else {
$filter.= ":";
}
$filter.= $key . ($value?"={$value}":"");
return $filter;
}
public function applyPreset(Preset $preset, Input $input, Output $output)
{
// Get filter string from preset
$filter = $preset->get("filter");
$type = $preset->get("type")?:"complex";
$extra = $preset->get("extra");
switch ($type) {
case 'audio':
$filter_type = "-af";
$filter_extra = "";
break;
case 'video':
$filter_type = "-vf";
$filter_extra = "";
break;
case 'complex':
default:
$filter_type = "-filter_complex";
$filter_extra = "";
break;
}
/*if (($size = $preset->getParam("size"))) {
$filter = $this->appendFilterOption($filter, "s", $size);
}*/
// Create a progress dialog
$dialog = $this->createDialog(DIALOG_PROGRESS, "Applying filter");
$dialog->show();
// Create command line to run ffmpeg
$cmdline = sprintf(
"ffmpeg -loglevel error -y -stats -i %s %s %s %s %s 2>&1",
escapeshellarg($input->getFilename()),
$filter_type,
escapeshellarg($filter),
trim($extra." ".$filter_extra),
escapeshellarg($output->getFilename())
);
// Create a coprocess for ffmpeg
$frames = $input->getVideoDuration()->frames;
$ffmpeg = $this->createProcess($cmdline, function ($read) use ($dialog,$frames) {
$dialog->setText(trim($read));
if (preg_match('/^frame=[\s]*([0-9]+)\s/', trim($read), $match)) {
$frame = intval($match[1]);
$pc = round($frame/$frames*100);
$dialog->setProgress($pc);
}
if ($dialog->isCancelled()) return false;
// frame= 4140 fps=102 q=-1.0 Lsize= 4559kB time=00:00:48.04 bitrate= 777.3kbits/s speed=1.18x
});
$ffmpeg->run();
unset($dialog);
}
}
return new FfmpegPlugin();

24
plugins/natron/plugin.php Normal file
View File

@ -0,0 +1,24 @@
<?php
namespace VfxApply\Plugin\Natron;
use VfxApply\Plugin;
use VfxApply\Input;
use VfxApply\Output;
use VfxApply\Preset;
class NatronPlugin extends Plugin
{
public function getName()
{
return "natron";
}
public function applyPreset(Preset $preset, Input $input, Output $output)
{
}
}
return new NatronPlugin();

View File

@ -0,0 +1,6 @@
preset:
name: Create histogram from audio
group: ffmpeg/a2v
plugin: ffmpeg
props:
filter: "ahistogram=dmode=separate:slide=1:scale=log"

View File

@ -0,0 +1,6 @@
preset:
name: Create phase meter from audio
group: ffmpeg/a2v
plugin: ffmpeg
props:
filter: "aphasemeter=mpc=#ff8800"

View File

@ -0,0 +1,7 @@
preset:
name: Create spectrum from audio
group: ffmpeg/a2v
plugin: ffmpeg
props:
filter: "showspectrum=mode=separate:color=intensity:slide=1:scale=log"

View File

@ -0,0 +1,8 @@
preset:
name: Compress/expand dynamic range
group: ffmpeg/audio
plugin: ffmpeg
props:
filter: "[0:a]compand=.3|.3:1|1:-90/-60|-60/-40|-40/-30|-20/-20:6:0:-90:0.2"
extra: "-c:v copy"
type: complex

View File

@ -0,0 +1,12 @@
preset:
name: Normalize audio level
group: ffmpeg/audio
plugin: ffmpeg
props:
filter: "dynaudnorm=m=100:s=12"
#filter: "dynaudnorm=m={maxgain}:s={compress}"
extra: "-c:v copy"
type: complex
params:
compress: { label:"Compression factor", type:float, min:0.0, max:30.0, default:0.0 }
maxgain: { label:"Max gain factor", type:float, min:0.0, max:100.0, default:10.0 }

110
src/Application.php Normal file
View File

@ -0,0 +1,110 @@
<?php
namespace VfxApply;
class Application
{
protected $plugins = [];
protected $presets = [];
public function readPlugins()
{
$iter = new \DirectoryIterator(
__DIR__."/../plugins"
);
foreach ($iter as $dir) {
if (!$dir->isDir())
continue;
if (!file_exists($dir->getPathname()."/plugin.php"))
continue;
$this->loadPlugin($dir->getPathname());
}
}
public function readPresets()
{
$iter = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator(
__DIR__."/../presets"
));
foreach ($iter as $file) {
if ($file->isDir() || ($file->getExtension()!='yml'))
continue;
$this->loadPreset($file->getPathname());
}
}
private function loadPlugin($path)
{
$plugin = require_once $path."/plugin.php";
$this->plugins[$plugin->getName()] = $plugin;
}
private function loadPreset($file)
{
$preset = Preset::createFromFile($file);
$id = basename($file,".yml");
$this->presets[$id] = $preset;
}
private function selectPreset()
{
$cmdl = "zenity --list --column=Group --column=Preset --column=Description ".
"--title=\"Apply VFX\" ".
"--width=400 --height=400 ".
"--text=\"Select the preset to apply\" ".
"--mid-search --print-column=2 ";
foreach ($this->presets as $id=>$preset) {
$cmdl.=sprintf(
"%s %s %s ",
escapeshellarg($preset->getGroup()),
escapeshellarg($id),
escapeshellarg($preset->getName())
);
}
exec($cmdl." 2>/dev/null", $output, $retval);
if ($retval == 0) {
$name = trim($output[0]);
return $this->presets[$name];
} else {
return false;
}
}
public function run()
{
$this->readPlugins();
$this->readPresets();
$opts = getopt("i:o:");
$input = new Input();
if (!empty($opts['i'])) {
$input->setFilename($opts['i']);
} else {
if (!$input->selectFile()) {
return 1;
}
}
$input->analyze();
$dur = $input->getVideoDuration();
printf("Input: %s %.2fs (%d frames)\n", $input->getFilename(), $dur->seconds, $dur->frames);
if (!($preset = $this->selectPreset())) {
return 1;
}
$plugin_name = $preset->getPlugin();
$plugin = $this->plugins[$plugin_name];
$output = new Output();
$output->setFilename("/tmp/out.mp4");
$plugin->applyPreset($preset, $input, $output);
}
}

29
src/Dialog.php Normal file
View File

@ -0,0 +1,29 @@
<?php
namespace VfxApply;
class Dialog
{
protected $title;
public function __construct($title=null)
{
$this->title = $title;
}
public function __destruct()
{
$this->onDestroy();
}
public function show()
{
$this->onCreate();
}
protected function onCreate()
{}
protected function onDestroy()
{}
}

View File

@ -0,0 +1,57 @@
<?php
namespace VfxApply\Dialog;
use VfxApply\Dialog;
class ProgressDialog extends Dialog
{
protected $ps;
protected $pipe;
protected $out;
protected function onCreate()
{
$this->out = fopen("php://temp", "wb");
$cmdl = sprintf("zenity --progress --title=%s --auto-close --text='Running' --width=400 --progress=0 2>/dev/null", escapeshellarg($this->title));
$ds = [
0 => [ "pipe", "r" ],
1 => [ "pipe", "w" ],
2 => [ "pipe", "w" ]
];
$this->ps = proc_open($cmdl, $ds, $pipes);
$this->pipe = $pipes[0];
}
protected function onDestroy()
{
/*
$status = proc_get_status($this->ps);
$pid = $status['pid'];
posix_kill($pid,15);
pcntl_waitpid($pid,$status);
print_r($status);
*/
proc_terminate($this->ps);
proc_close($this->ps);
}
public function setProgress($pc)
{
fprintf($this->pipe, "%d\n", $pc);
}
public function setText($text)
{
fprintf($this->pipe, "# %s\r\n", addslashes($text));
}
public function isCancelled()
{
$status = proc_get_status($this->ps);
return ($status['running']==false);
}
}

69
src/Input.php Normal file
View File

@ -0,0 +1,69 @@
<?php
namespace VfxApply;
class Input
{
protected $filename;
protected $audiostreams = [];
protected $videostreams = [];
public function setFilename($filename)
{
$this->filename = $filename;
}
public function getFilename()
{
return $this->filename;
}
public function selectFile()
{
$cmdl = "zenity --file-selection --title \"Select input file\" ";
$cmdl.= "--file-filter=\"Video files (mp4,m4v,avi,ogv,mkv)|*.mp4 *.m4v *.avi *.ogv *.mkv\" ";
$cmdl.= "--file-filter=\"Audio files (mp3,m4a,wav,ogg)|*.mp3 *.m4a *.wav *.ogg\" ";
$cmdl.= "--file-filter=\"All files|*\" 2>/dev/null";
exec($cmdl, $out, $ret);
if ($ret == 0) {
$this->filename = trim(array_shift($out));
return true;
}
return false;
}
public function analyze()
{
$cmdl = "ffprobe -loglevel error -show_streams -print_format json ".escapeshellarg($this->filename);
exec($cmdl, $out, $ret);
if ($ret != 0) {
throw new \Exception("Unable to analyze file {$this->filename}");
}
$info = json_decode(join("\n",$out));
foreach ($info->streams as $stream) {
if ($stream->codec_type == 'video') {
$this->videostreams[] = $stream;
} elseif ($stream->codec_type == 'audio') {
$this->audiostreams[] = $stream;
}
}
}
public function getVideoDuration()
{
$duration = $this->videostreams[0]->duration;
$frames = $this->videostreams[0]->nb_frames;
return (object)[
"seconds" => floatval($duration),
"frames" => intval($frames)
];
}
}

18
src/Output.php Normal file
View File

@ -0,0 +1,18 @@
<?php
namespace VfxApply;
class Output
{
protected $filename;
public function setFilename($filename)
{
$this->filename = $filename;
}
public function getFilename()
{
return $this->filename;
}
}

22
src/Plugin.php Normal file
View File

@ -0,0 +1,22 @@
<?php
namespace VfxApply;
class Plugin
{
protected function createDialog($type, $title=null)
{
switch ($type) {
case DIALOG_PROGRESS:
return new Dialog\ProgressDialog($title);
default:
throw new \Exception("Error, undefined dialog type {$type}");
}
}
protected function createProcess($command, callable $callback)
{
return new Process($command, $callback);
}
}

70
src/Preset.php Normal file
View File

@ -0,0 +1,70 @@
<?php
namespace VfxApply;
use Symfony\Component\Yaml\Yaml;
class Preset
{
/** @var string Preset name */
protected $name;
/** @var string The group this preset belong to, for organizing */
protected $group;
/** @var string The plugin this preset uses */
protected $plugin;
/** @var array The properties are used by the plugin */
protected $props = [];
/** @var array The params can be assigned from user data */
protected $params = [];
public static function createFromFile($filename)
{
$body = file_get_contents($filename);
$conf = Yaml::parse($body);
if (!array_key_exists('preset',$conf)) {
throw new \Exception("File does not appear to be a valid preset");
}
return new Preset($conf['preset']);
}
public function __construct(array $preset)
{
$this->name = $preset['name'];
$this->group = empty($preset['group'])?null:$preset['group'];
$this->plugin = $preset['plugin'];
$this->props = $preset['props'];
$this->params = empty($preset['params'])?null:$preset['params'];
}
public function getName()
{
return $this->name;
}
public function getGroup()
{
return $this->group;
}
public function getPlugin()
{
return $this->plugin;
}
public function get($prop)
{
if (!array_key_exists($prop, $this->props)) {
return null;
}
return $this->props[$prop];
}
public function getParam($param)
{
if (!array_key_exists($param, $this->params)) {
throw new \Exception("No such param defined in preset: {$param}");
}
return $this->params[$param];
}
}

47
src/Process.php Normal file
View File

@ -0,0 +1,47 @@
<?php
namespace VfxApply;
class Process
{
public function __construct($cmdline, callable $callback)
{
$this->cmdline = $cmdline;
$this->callback = $callback;
}
public function __destruct()
{
}
public function run()
{
$ds = [
0 => [ "pipe", "r" ],
1 => [ "pipe", "w" ],
2 => [ "pipe", "w" ],
];
echo "Exec: {$this->cmdline}\n";
$ps = proc_open($this->cmdline, $ds, $pipes);
list($stdin,$stdout,$stderr) = $pipes;
stream_set_blocking($stdout,false);
$buf = null;
while (true) {
$str = fgets($stdout, 4192);
$status = proc_get_status($ps);
if ($str) {
$ret = call_user_func($this->callback, $str);
if ($ret === false) {
proc_terminate($ps);
}
}
if ($status['running']==false) break;
usleep(10000);
}
proc_close($ps);
return $status['exitcode'];
}
}

15
src/app.php Normal file
View File

@ -0,0 +1,15 @@
<?php
define("DIALOG_PROGRESS", "progress");
define("DIALOG_CONFIRM", "confirm");
define("DIALOG_WARNING", "warning");
define("DIALOG_MESSAGE", "message");
define("DIALOG_ERROR", "error");
define("DIALOG_OPENFILE", "openfile");
define("DIALOG_SAVEFILE", "savefile");
require_once __DIR__."/../vendor/autoload.php";
use VfxApply\Application;
$app = new Application();
$app->run();