在Laravel框架中的FFMpeg工具包:laravel-ffmpeg-tools

2023-06-01 00:00:00 laravel 框架 工具包

这周我做了一个非常酷的小包,应该会引起@laravelphp和@php开发者的兴趣,他们都是用@ffmpeg工作的。但要理解我为什么对它如此兴奋,我需要解释一下像这样一个简单的视频需要多少工作才能生成。

laravel-ffmpeg-tools1.png

这里是最后的shell命令,将生成该视频:

ffmpeg -y -f lavfi -i "color=c=black:s=256x256:d=1" -filter_complex "[0:v] loop=-1:1 [bg]; [bg] drawtext=text='Motion Tween':fontcolor=white:x=(main_w/2)-(tw/2):y=if(gt(t\,4)\,if(lt(t\,4)\,((main_h/2)-(th/2))\,if(gt(t\,4+2)\,(main_h)\,((main_h/2)-(th/2))+(((main_h)-((main_h/2)-(th/2)))*(if(eq(((t-4)/2)\,0)\,0\,if(eq(((t-4)/2)\,1)\,1\,-pow(2\,10*((t-4)/2)-10)*sin((((t-4)/2)*10-10.75)*2.0943951023932)))))))\,if(lt(t\,1)\,(-th)\,if(gt(t\,1+2)\,((main_h/2)-(th/2))\,(-th)+((((main_h/2)-(th/2))-(-th))*(if(lt(((t-1)/2)\, 1/2.75)\,7.5625*pow(((t-1)/2)\,2)\,if(lt(((t-1)/2)\,2/2.75)\,7.5625*(((t-1)/2)-1.5/2.75)*(((t-1)/2)-1.5/2.75)+0.75\,if(lt(((t-1)/2)\,2.5/2.75)\,7.5625*(((t-1)/2)-2.25/2.75)*(((t-1)/2)-2.25/2.75)+0.9375\,7.5625*(((t-1)/2)-2.65/2.75)*(((t-1)/2)-2.65/2.75)+0.984375))))))))" -codec:a copy -codec:v libx264 -crf 25 -pix_fmt yuv420p -t 8 drawtext_y_enter-OutBounce_exit-InElastic.mp4

如果大家有兴趣,我可以进一步分解,但实际上我们只关心drawtext视频过滤器的y参数。

if(gt(t\,4)\,if(lt(t\,4)\,((main_h/2)-(th/2))\,if(gt(t\,4+2)\,(main_h)\,((main_h/2)-(th/2))+(((main_h)-((main_h/2)-(th/2)))*(if(eq(((t-4)/2)\,0)\,0\,if(eq(((t-4)/2)\,1)\,1\,-pow(2\,10*((t-4)/2)-10)*sin((((t-4)/2)*10-10.75)*2.0943951023932)))))))\,if(lt(t\,1)\,(-th)\,if(gt(t\,1+2)\,((main_h/2)-(th/2))\,(-th)+((((main_h/2)-(th/2))-(-th))*(if(lt(((t-1)/2)\, 1/2.75)\,7.5625*pow(((t-1)/2)\,2)\,if(lt(((t-1)/2)\,2/2.75)\,7.5625*(((t-1)/2)-1.5/2.75)*(((t-1)/2)-1.5/2.75)+0.75\,if(lt(((t-1)/2)\,2.5/2.75)\,7.5625*(((t-1)/2)-2.25/2.75)*(((t-1)/2)-2.25/2.75)+0.9375\,7.5625*(((t-1)/2)-2.65/2.75)*(((t-1)/2)-2.65/2.75)+0.984375))))))))

我们在这里对y位置所做的是初始化它在帧的顶部,等待一秒钟,

用EaseOutBounce的缓和方式在2秒内将它过渡到帧的中心,保持这个位置1秒钟,

然后用EaseInElastic的缓和方式在2秒内过渡到帧的底部,并保持它直到视频结束。


这个描述可能听起来并不难,

但让我们快速看一下这些缓和函数的数学运算,

看看我们需要做什么来把它插入ffmpeg。


下面是这个函数的typescript和EaseOutBounce的结果图(来自Easings.net)。

https://easings.net/#easeOutBounce
function easeOutBounce(x: number): number {
    const n1 = 7.5625;
    const d1 = 2.75;
 
    if (x < 1 / d1) {
        return n1 * x * x;
    } else if (x < 2 / d1) {
        return n1 * (x -= 1.5 / d1) * x + 0.75;
    } else if (x < 2.5 / d1) {
        return n1 * (x -= 2.25 / d1) * x + 0.9375;
    } else {
        return n1 * (x -= 2.625 / d1) * x + 0.984375;
    }
}

> 注意 我们在这些条件语句中重新分配了x。

laravel-ffmpeg-tools2.png


这个函数所做的就是接收一个介于0和1之间的数字,代表我们在动画中的绝对进度,

然后给我们一个介于0和1之间的不同数字,然后用这个数字与我们的delta相乘来计算我们在动画这一点上的值,或者说tween。


首先让我们列出几个常量,我们可以在drawtext过滤器中使用:

t = 正在生成的这一帧的当前时间(秒)。
th = 文本高度
main_h = 视频帧的高度

为了得到x(我们在这个动画中的绝对进度),它将是当前时间(t)减去延迟(1),再加上持续时间(2)。

用FFMpeg的话说,就是(t-1)/2。

这是因为,虽然通常我们会做t/持续时间,但我们必须考虑到延迟,从我们的分数的分子中减去它。


现在是超级严重的部分。

我们必须把那个非常简单的ts函数变成一个FFMpeg函数,用我们的函数替换所有对x的引用。

要做到这一点,我们需要三个FFMpeg函数:

if、lt(小于)和pow

因为我们要在这些条件中重新分配时间值(x),所以我们要事先把它们分开。

这里有一个PHP函数,所有的条件都是预先制作好的,然后串联起来形成缓和字符串:

public static function EaseOutBounce(string $time): string
{
    $n1 = 7.5625;
    $d1 = 2.75;
    $firstExpr = "{$n1}*pow(({$time})\\,2)";
    $secondTime = "(({$time})-1.5/{$d1})";
    $secondExpr = "{$n1}*{$secondTime}*{$secondTime}+0.75";
    $thirdTime = "(({$time})-2.25/{$d1})";
    $thirdExpr = "{$n1}*{$thirdTime}*{$thirdTime}+0.9375";
    $fourthTime = "(({$time})-2.65/{$d1})";
    $fourthExpr = "{$n1}*{$fourthTime}*{$fourthTime}+0.984375";
 
    return "if(lt(({$time})\\, 1/{$d1})\\,{$firstExpr}\\,if(lt(({$time})\\,2/{$d1})\\,{$secondExpr}\\,if(lt(({$time})\\,2.5/{$d1})\\,{$thirdExpr}\\,{$fourthExpr})))";
}

通过这个函数运行我们的x值(t-1)/2,可以得到这样的输出:

if(lt(((t-1)/2)\, 1/2.75)\,7.5625*pow(((t-1)/2)\,2)\,if(lt(((t-1)/2)\,2/2.75)\,7.5625*(((t-1)/2)-1.5/2.75)*(((t-1)/2)-1.5/2.75)+0.75\,if(lt(((t-1)/2)\,2.5/2.75)\,7.5625*(((t-1)/2)-2.25/2.75)*(((t-1)/2)-2.25/2.75)+0.9375\,7.5625*(((t-1)/2)-2.65/2.75)*(((t-1)/2)-2.65/2.75)+0.984375)))

这就是我们的轻松。

为了得到我们的delta,我们使用我们的初始位置(从)-th(就在框架的顶部之外),和我们的最终位置(到)(main_h/2)-(th/2)(框架的垂直中心)。

这给了我们一个

delta((main_h/2)-(th/2))-(-th)


在这个Tween的最后一步,

我们要告诉ffmpeg,如果t小于延迟,就使用我们的from值,

如果t大于我们的延迟加上我们的持续时间,就使用to值,

否则就把我们的from值加上delta乘以我们的ease。

public function build(): string
{
    return "if(lt(t\,{$this->delay})\,{$this->from}\,if(gt(t\,{$this->delay}+{$this->duration})\,{$this->to}\,{$this->from}+({$this->getDelta()}*{$this->ease})))";
}

这给了我们这个动画特定部分的最终字符串:

if(lt(t\,1)\,(-th)\,if(gt(t\,1+2)\,((main_h/2)-(th/2))\,(-th)+((((main_h/2)-(th/2))-(-th))*(if(lt(((t-1)/2)\, 1/2.75)\,7.5625*pow(((t-1)/2)\,2)\,if(lt(((t-1)/2)\,2/2.75)\,7.5625*(((t-1)/2)-1.5/2.75)*(((t-1)/2)-1.5/2.75)+0.75\,if(lt(((t-1)/2)\,2.5/2.75)\,7.5625*(((t-1)/2)-2.25/2.75)*(((t-1)/2)-2.25/2.75)+0.9375\,7.5625*(((t-1)/2)-2.65/2.75)*(((t-1)/2)-2.65/2.75)+0.984375)))))))

一定有更好的方法 好吧,我很高兴你这么问。

我已经迫不及待地想告诉你这个软件包:

https://github.com/ProjektGopher/laravel-ffmpeg-tools

有了这个软件包,生成这个字符串就像下面这样简单了

(new Tween())
    ->from("-th")
    ->to("(main_h/2)-(th/2)")
    ->delay(Timing::seconds(1))
    ->duration(Timing::seconds(2))
    ->ease(Ease::OutBounce);

但我们并不是只有这一个Tween来达到我们在文章开头的drawtext过滤器的最终y值。

这就是时间线和关键帧的作用。

下面是这个包中生成时间线测试脚本的一部分,我用它来制作第一个视频:

use ProjektGopher\FFMpegTools\Timeline;
use ProjektGopher\FFMpegTools\Keyframe;
use ProjektGopher\FFMpegTools\Ease;
use ProjektGopher\FFMpegTools\Timing;
 
echo 'Generating video sample using Timeline...'.PHP_EOL;
 
$timeline = new Timeline();
$timeline->keyframe((new Keyframe())
    ->value('-th')
    ->hold(Timing::seconds(1))
);
$timeline->keyframe((new Keyframe())
    ->value('(main_h/2)-(th/2)')
    ->ease(Ease::OutBounce)
    ->duration(Timing::seconds(2))
    ->hold(Timing::seconds(1))
);
$timeline->keyframe((new Keyframe())
    ->value('main_h')
    ->ease(Ease::InElastic)
    ->duration(Timing::seconds(2))
);
 
$input = "-f lavfi -i \"color=c=black:s=256x256:d=1\"";
$filter = "-filter_complex \"[0:v] loop=-1:1 [bg]; [bg] drawtext=text='Motion Tween':fontcolor=white:x=(main_w/2)-(tw/2):y={$timeline}\"";
$codecs = '-codec:a copy -codec:v libx264 -crf 25 -pix_fmt yuv420p';
$duration = '-t 8'; // in seconds
$out = "tests/Snapshots/Timelines/drawtext_y_enter-OutBounce_exit-InElastic.mp4";
$redirect = '2>&1'; // redirect stderr to stdout
 
$cmd = "ffmpeg -y {$input} {$filter} {$codecs} {$duration} {$out} {$redirect}";

你可以通过composer安装该软件包:

composer require projektgopher/laravel-ffmpeg-tools

在写这篇文章时,目前的版本是v0.5.0:

https://github.com/ProjektGopher/laravel-ffmpeg-tools/tree/v0.5.0

额外的

绘图的shell cmd

这里是generateEasings测试脚本的一个稍加修改的部分,用于生成本帖中间的EaseOutBounce的情节:

// $ease->value = "OutBounce";
echo "Generating snapshot for {$ease->value} easing...".PHP_EOL;
$time = "X/H";
$easeMultiplier = Ease::{$ease->value}($time);
$input = "-f lavfi -i \"color=c=black:s=256x256:d=1\"";
$margin = '28';
$filter = "-vf \"geq=if(eq(round((H-2*{$margin})*({$easeMultiplier}))\,H-Y-{$margin})\,128\,0):128:128\"";
$out = "-frames:v 1 -update 1 tests/Snapshots/Easings/{$ease->value}.png";
$redirect = '2>&1'; // redirect stderr to stdout
 
$cmd = "ffmpeg -y {$input} {$filter} {$out} {$redirect}";

这里的 "有趣 "之处在于,我们的$时间值并不与t挂钩,而是与我们的绘图的X轴挂钩。


转:

https://laravel-news.com/laravel-ffmpeg-tools

相关文章