记录和 ffmpeg 与 LLM 搏斗的两天
要做的
最近在写一个制作视频的功能,就是把 N 个视频合并,然后把对应的 N 张图片,在视频开始的前 5 秒叠加显示出来。
第一口 - diffusion studio
本来我用的是 diffusion studio,这是一个 JS 库,但这玩意性能太差了,因为他要把视频每一帧都读到 canvas 里,数据一多页面就卡住了(为啥要折腾 DOM 呢?)
而且他的 API 十分不好用,作为浏览器脚本你无法读本地数据也就算了,你起码给一个接受纯数据的参数吧,比如 HTML 类型接受源代码,Image 类型接受 Uint8Array/Image,他就只支持 File、url、Request 等参数。
那你只有本地图片咋办呢?你只能先把他读成 File 了,反正在浏览器端弄,要么用 input,要么用比较新的 File API,这些都离不开弹窗。
我后面就想了个办法,用 Tauri 后端的 Rust 去批量读,然后把数据传回给前端,前端再转换成 File,之后就遇到了慎用 Tauri 读取文件的问题,吃了一口大的。
第二口 - ffmpeg(主要是 LLM)
于是我就想,干脆使用知名的 ffmpeg 吧,这个应该不会出啥大问题。
把他整合进项目不难,但写他的命令很难,因为我没用过,所以我就拜托 LLM 来帮我,让他帮我设计两种函数:
get_videos_duration
:计算每个视频的长度merge_video_with_overlay
:拼接视频叠加图片
函数中会生成 ffmpeg 命令,之后调用 ffmpeg 来进行处理。
这里本来我是打算用远程视频的,但由于网络不稳定,很多回视频都无法完整下载出来,所以我就改成了用本地视频(视频我自己下载到本地)。
这里就遇到了第一个问题,LLM 修改代码,他只改了 merge_video_with_overlay
,他没改 get_videos_duration
,导致获取长度的代码用的依然是远程视频的数据,获取出来的长度居然和本地的不一样,所以图片叠加到了奇怪的时间点,这个问题我找了一天,最后才定位到的。
修复后遇到了第二个问题,因为我图片本身有透明度,我发现叠加出来的效果是这样的:
首先透明度没了,其次阴影全变成实心的了,后面经过排查,发现是 fade 的问题,LLM 给我加了一段渐入渐出的效果:
fade=t=in:st=0:d=0.5:alpha=1,fade=t=out:st=4.5:d=0.5:alpha=1
大概是这样,我不知道 ffmpeg 内部怎么实现的,我猜测因为图片会作为一帧的视频,所以在渐隐的时候,相当于每一帧显示一个“动态透明度”的图片,但前一帧的图片还没有消失,所以叠起来就成了这样。
总之,把这段参数删除后就正常了,但也没了渐变效果,这个还可以继续尝试微调,就这个问题,我又找了半天,中途问了 LLM 无数次,都没啥效果。
好事
搞了两天,其实也学到了一些东西,比如现在会看(一点)ffmpeg 的命令了,我这个需求的完整命令是这样的:
ffmpeg.exe
-i video_0.mp4
# ... 到 video_8
-i overlay_0.png
# ... 到 overlay_8
-filter_complex "
[0:v]scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2:color=black[v0];
[1:v]scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2:color=black[v1];
[2:v]scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2:color=black[v2];
[3:v]scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2:color=black[v3];
[4:v]scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2:color=black[v4];
[5:v]scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2:color=black[v5];
[6:v]scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2:color=black[v6];
[7:v]scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2:color=black[v7];
[8:v]scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2:color=black[v8];
[v0][v1][v2][v3][v4][v5][v6][v7][v8]concat=n=9:v=1:a=0[vbase];
[0:a][1:a][2:a][3:a][4:a][5:a][6:a][7:a][8:a]concat=n=9:v=0:a=1[abase];
[9:v]format=rgba,scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[img0];
[10:v]format=rgba,scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[img1];
[11:v]format=rgba,scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[img2];
[12:v]format=rgba,scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[img3];
[13:v]format=rgba,scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[img4];
[14:v]format=rgba,scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[img5];
[15:v]format=rgba,scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[img6];
[16:v]format=rgba,scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[img7];
[17:v]format=rgba,scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[img8];
[vbase][img0]overlay=0:0:format=auto:enable='between(t,0,5)'[v9];
[v9][img1]overlay=0:0:format=auto:enable='between(t,41.666667,46.666667)'[v10];
[v10][img2]overlay=0:0:format=auto:enable='between(t,134.9,139.9)'[v11];
[v11][img3]overlay=0:0:format=auto:enable='between(t,209.766667,214.766667)'[v12];
[v12][img4]overlay=0:0:format=auto:enable='between(t,269.766667,274.766667)'[v13];
[v13][img5]overlay=0:0:format=auto:enable='between(t,336.641667,341.641667)'[v14];
[v14][img6]overlay=0:0:format=auto:enable='between(t,396.641667,401.641667)'[v15];
[v15][img7]overlay=0:0:format=auto:enable='between(t,472.975,477.975)'[v16];
[v16][img8]overlay=0:0:format=auto:enable='between(t,566.558333,571.558333)'[out]"
-map [out]
-map [abase]
-c:v libx264
-c:a aac
-preset ultrafast
-threads 0
-crf 23
-max_muxing_queue_size 1024
-y merged_20241116_162132.mp4
-i
后面的就是输入源,写进命令之后先是按顺序命名的,比如要取第一个输入源的视频流,就可以写成 [0:v]
,取音频流就是 [0:a]
。
[0:v]scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2:color=black[v0];
这一段是用来规范视频尺寸的,将他们都规范成 1920x1080,有不符合的就等比缩放,(ow-iw)/2:(oh-ih)/2
用于设置居中,对于剩余的区域填充黑色,然后将输出流命名为 v0
。
[v0][v1][v2][v3][v4][v5][v6][v7][v8]concat=n=9:v=1:a=0[vbase];
[0:a][1:a][2:a][3:a][4:a][5:a][6:a][7:a][8:a]concat=n=9:v=0:a=1[abase];
这两个是用来合并流的,将规范尺寸后的视频流合并成 vbase
,再将每段视频的音频流合并成 abase
。
[9:v]format=rgba,scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2[img0];
这一段用来规范图片,图片会被视作一帧的视频,所以还是用 [n:v]
来选中,设置像素格式为 rgba,保留 alpha 通道,然后将图片缩放到 1920x1080 居中和视频对应,最后将输出流命名为 img0
。
[vbase][img0]overlay=0:0:format=auto:enable='between(t,0,5)'[v9];
[v9][img1]overlay=0:0:format=auto:enable='between(t,41.666667,46.666667)'[v10];
这一段的功能是“视频”合并,[vbase]
和 [img0]
都作为输入流,后续的参数是设置给 [img0]
的,将叠加位置设置做左上角 (0, 0),format=auto
自动选择叠加模式,enable='between(t,0,5)
控制叠加时间在 0 到 5 秒,t
表示当前时间点,最后把输出流命名为 v9
,之后以此类推。
-map [out]
-map [abase]
-c:v libx264
-c:a aac
-preset ultrafast
-threads 0
-crf 23
-y merged_20241116_162132.mp4
-map
用于选择输出流,比如这里选了 out
和 abase
作为输出,out
是合并后的视频流,abase
是音频流。
-c
用于为流选择编码器,冒号后的 v
和 c
分别表示视频类型和音频类型。
-preset
用于设置编码速度,从最快到最慢有:
ultrafast
superfast
veryfast
faster
fast
medium
slow
slower
veryslow
ultrafast
速度最快但文件较大,压缩效率较低,所以输出的文件比较大。
-threads
设置处理的线程数,0 表示自动选用最优的,比单线程处理得要快。
-crf
用于控制视频质量,范围 0-51,0 表示无损,23 是默认值,数值越小,文件越大。
-max_muxing_queue_size
用于设置 ffmpeg 的复用队列大小,当处理大量视频流或者复杂操作的时候,ffmpeg 需要保留一定量的帧用于复用,设置得大一点能让更多的帧被缓存下来。
-y
表示无需确认,自动覆盖已存在的同名文件。
Subscribe to my newsletter
Read articles from Erioifpud directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by