Skip to main content

Command Palette

Search for a command to run...

记录和 ffmpeg 与 LLM 搏斗的两天

Updated
2 min read
记录和 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 来帮我,让他帮我设计两种函数:

  1. get_videos_duration:计算每个视频的长度

  2. 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 用于选择输出流,比如这里选了 outabase 作为输出,out 是合并后的视频流,abase 是音频流。

-c 用于为流选择编码器,冒号后的 vc 分别表示视频类型和音频类型。

-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 表示无需确认,自动覆盖已存在的同名文件。

More from this blog

12 月装机行动记录

(Banner 图文无关) 这几天买了新的装备回来升级配置,除了显卡和散热器,其他都更新了,这两个不更新的原因是太贵了。 由于我不太懂选配置,所以还是让朋友给推荐,我说我的预算在 5000 左右,他就给我转了一个整机,配置大概是这样的: CPU:AMD 9700X 主板:微星 B650M GAMING PLUS WIFI 或者 微星 B650M GAMING WIFI 显卡:木有 内存:英睿达/宇瞻 DDR5 6000 32G 硬盘:1T NVMe PCIe4 SSD 读速 3500M...

Dec 7, 20241 min read
12 月装机行动记录

Homekit + cozylife 插座连接 HA

近期在淘宝上买了个 Homekit + cozylife 的插座,就这种: 一开始只通过 iOS 访问,就是只连接 Homekit,长按开关重置插座,iOS 一扫码就连上了,后来我嫌在外面访问不了,又不想掏钱买苹果的 HomePod,于是就装上了 Home Assistant,打算让设备们都连上 HA,这样就不用交苹果税了。 连接方式还是通过 Homekit,一般来说支持 Homekit 设备都能这样连接,先连上 iOS,然后在 Home App 中移除设备,这时候就能在 HA 中找到设备了:...

Nov 30, 20241 min read
Homekit + cozylife 插座连接 HA

找到了一台祖传的 Ccd 相机

开个玩笑,这台相机其实是我们家在 05 年的时候买的,发票都还在呢,当时花了 4000 块钱!搁现在我都受不了,更别说当年了,看到价格我都震惊了。 相机的型号是索尼的 Cybershot DSC-N1,属于小红书时尚单品 CCD 相机,由于一直放在包装盒里,现在还有 99 新呢。 机子还是正常的,能开机,能拍照,其中一个问题是日期,这款没有 WiFi 功能,所以时间只能保存在本地,不知道是不是 BUG,每次开机都让我重新设置,默认就定在 2005 年 1 月 1 日。 第二个问题是电池,电池应该...

Nov 28, 20241 min read
找到了一台祖传的 Ccd 相机
V

void mian

39 posts