玩一下 FFmpeg

因为最近有两件事情,所以要研究一下这玩意:把一整个 capoo 动画分割出几个小片段然后做成表情包(capoo 好萌),还有就是 vps 上下的动画,直接用 filebroswer 播不了,需要稍微转一下码才行。

总参数参考:ffmpeg Documentation

视频精确分割

关键词:帧内编码(intra)

参考文章:FFMPEG 视频分割和合并

不过这篇文章没有谈一个参数,to,to 是裁剪到时间节点而 t 是裁剪自开始以来的一段时间长度,这里为了方便我很明显是要用 to 的。

1
ffmpeg -ss 00:00.00 -to 00:00.01 -i capoo.mp4 -vcodec copy -acodec copy "C:\Users\zbttl\OneDrive - go.Stockton.edu\Desktop\capoo\ capoo1.mp4"

不过发到 telegram 上的 gif 的 mp4 不能带音频,所以索性再改一下

1
ffmpeg -ss 00:00.00 -to 00:00.01 -i capoo.mp4 -vcodec copy -an "C:\Users\zbttl\OneDrive - go.Stockton.edu\Desktop\capoo\ capoo1.mp4"

顺便附带一个文件夹内批量转换为无声 mp4 的例子:

1
for i in *.mp4; do ffmpeg -i $i -vcodec copy -an "/home/zbttl/capoo/${i%%.*}.mp4";done

参数里可以加上 -avoid_negative_ts 移动关键帧使其与要剪辑的位置相符。

使用 gui(不成熟)

也可以使用 gui 工具 LosslessCut。gui 可以通过视频中的 Intra(I 帧,关键帧)识别转场,操作上便捷许多。不过很多视频由于参数原因(I 帧过多会增大视频体积),所以 I 帧和真正的转场不一定完全符合,可能还要通过 ffmpeg 转一下码。

参考:

加入以下参数:

1
2
3
-keyint_min #Intra最小间隔时间,可设置为 0。
-g #group of picture,Intra最大间隔时间。设置为 1 就全部都是关键帧。全部设置为关键帧有助于手动切割。
-sc_threshold #(scenecut)设置场景更改检测的阈值。可设置为 0-无限。

比较有用的就是这三个参数。我还实验了 -profile-preset 两个参数。profile 在文档中有 extended 这个选项可能对关键帧切换有帮助,但实际使用起来选项无法使用;preset 只要不设置成比较快的那些选项,使用 slow 和什么都不用出来的 I 帧数量和分布没有区别。

ffmpeg 使用非 copy 模式转码会显示 I/P/B 帧的数量和占比。也可以使用 elecard streameye tools 查看 I/P/B 的数量和分布(但要花钱,破解找不到,要不就只能试用)。

转码的时候也不要加入 -c:a copy 参数,可能导致时间轴误差。

例子:

1
ffmpeg -y -i '.\capoo.mp4'  -preset slow -keyint_min 1 -sc_threshold 60 './capoo_1.mp4'

再用得到的新视频文件在 gui 内裁剪。

然而实际使用时发现在每个关键帧处还要往上倒三帧否则就会包含下个场景的画面。原因是这个 gui 命令里面用了 -c copy 参数。。。目前还无解。而且除了这个,有时候导出的某些视频还会出现只剪了后面没剪前面的情况,貌似是因为放在桌面,桌面的路径里面有中文(onedrive 的锅)。。。。

还有其他的 gui 工具,比如 ffmpegyag。但这就没有根据 Intra 帧在时间轴上快捷指向的功能了(虽然还是能识别处 I/P/B 帧)。而且不能直接使用(点 OK 就卡住),生成脚本后运行倒是没问题。

配合 opencv

门槛有点高。给两篇参考文章,先挖个坑。

vps 动画转码

为什么播不了?我想估计是位深太大了,使用 ffmpeg x264 默认参数转一下就好了。

1
ffmpeg  -i "[Sakurato.sub][Nande Koko ni Sensei ga!][01][GB][1080P].mp4" -vcodec libx264 -acodec copy  test.mp4

不过转出来感觉略微有点不太对,windows potplayer 缩略图显示不出东西来。。。原因不明,排除了 10bit 不兼容原因(原案是 10bit,可显示)和 hevc(h265)与 avc(h264)原因(用哪个编码器都转不出来)。

另外,x264 编译器还有很多奇奇怪怪的参数,参考这里:H.264 Video Encoding Guide

显卡加速

参考文章:使用GPU硬件加速FFmpeg视频转码

ffmpeg 还支持显卡加速,不过嘛。。。参数很麻烦,没什么可靠的参考(因为 ffmpeg 的参数经常有顺序限制的,上面那篇文章的参考我试了一下,失败),下面这个,我转起来速度比较快(不过也有是用 8bit 的原因),而且 potplayer 识别了缩略图,另外显卡也工作了(不过占用只有百分之8。。。。)(未加 -hwaccel cuda,虽然是硬解但仍然经过内存。但下面的命令仅硬解码,且仅 h265)

1
ffmpeg  -hwaccel cuda  -c:v h265_cuvid -i "[Sakurato.sub][Nande Koko ni Sensei ga!][01][GB][1080P].mp4" -pix_fmt yuv420p test.mp4

参考:NVIDIA FFmpeg 转码指南

拿来转部落战视频用的,因为 ipad 录的视频码率高,而且带了旋转属性(很诡异,是写在 ffmpeg 参数里面的,也就是说对于支持的播放器打开后会自动转正变成横屏,但其实视频硬属性是竖着的),因为有这个自动旋转所以网上写的大部分硬件转码无法使用(不支持关掉自动旋转并摆正视频),但是我试了一个新的,还凑合,而且 gpu 打满。原理是硬解硬编码,下面的方框部分是指定使用最广泛的 h264 硬解,记得如果原来就是 h265 视频需要把这个参数换掉(hevc_cuvid)或关掉。硬编码的部分也可以换成 h264_nvenc

1
ffmpeg -y -vsync 0 -hwaccel cuda [-c:v h264_cuvid]  -i xxx.MP4 -vcodec hevc_nvenc -b:v 3000k xxx.mp4

hwaccel 也有被称为 cuvid 的参数。但我用 cuvid 代替 cuda 时会报错

1
Pixel format 'yuvj420p' is not supported

原因未知。cuvid 和 cuda 的区别我也没发现有参考资料能解释。

对于其他硬解方式,可以参考这篇文章:(三+1)用显卡加速视频转码压制之ffmpeg、media coder、shana encoder

查看解码方法:

1
ffmpeg -decoders

查看编码方法:

1
ffmpeg -encoders

上面提到的 cudah264_cuvid 就在解码方法里面,而 hevc_nvenc 就在编码方法里面。对于 intel 系来说,硬解应该是 qsv 后缀一类的方法;而 amd 是 amf 后缀一类的方法。

因为一般是转为 x264/265 编码的视频,可以借助 h264/hevc 快速筛选出当前 ffmpeg 转换时支持的编解码方法,类似于:

1
2
ffmpeg -decoders|findstr h264
ffmpeg -encoders|ffmpeg -hevc

我仔细看了看最新版 ffmpeg,发现起码对于 amd,只找到了编码方法(比如 h264_amf)而没有找到解码方法。。。不过实测,仅使用编码方法也能有效的加速视频的转换:

1
ffmpeg -c:v h264_amf -i "[Sakurato.sub][Nande Koko ni Sensei ga!][01][GB][1080P].mp4" test.mp4

好在我平常操作的视频都是解码不怎么费劲的视频,解码费劲的 4k 编码起来必然更慢,暂时不属于我手上硬件能触及的范围了。。。

另外,文章提到

1
ffmpeg -hwaccels

能查看当前硬件和 ffmpeg 支持的硬解,但我看结果感觉扯淡。。。我用 amd 的机子能查出来 intel 和 cuda,却没有 amf,就 tm 离谱(当然文章里面也提到了这个方法不准就是了)。另外实际转换时使用qsv -hwaccels qsv 时需保证没有独立显卡(特别是N卡),否则会报错,是bug,来自#6996(尝试在 Windows 10 上使用 NVidia 主 GPU 支持的 Intel 系统上使用 QSV 会导致崩溃)– FFmpeg

多线程

参考文章:ffmpeg 多线程转码

通过写在 -i 参数前的 -threads [线程数] 可以指定 ffmpeg 使用的线程。不过经过测试比较新的 ffmpeg 都会用完 cpu 的所有线程,所以除非要限制 cpu 功率否则这个参数没必要动。

mkv 内挂字幕嵌入 mp4

参考文章:題 FFMPEG mkv到mp4轉換失去了字幕

内挂字幕的 mkv 在 filebroswer 里面看不到字幕啊。。。于是要想办法提取字幕出来,再把字幕直接内嵌进去。

有两种方法:

  1. 不靠谱方法,很快,但是不一定能识别

    1
    ffmpeg -i input.mkv -c copy -c:s mov_text output.mp4
  2. 重新编码的方法。一定能识别,但是贼慢

    1
    ffmpeg -i input.mkv -vf subtitles=input.mkv output.mp4

mp4 批量转换 gif

参考文章:High quality GIF with FFmpeg

以 centos 为例,先编辑个小脚本:

change.sh

1
2
3
4
5
6
7
#!/bin/sh
palette="./palette.png"

filters="fps=15,scale=-1:-1:flags=lanczos"

ffmpeg -v warning -i $1 -vf "$filters,palettegen" -y $palette
ffmpeg -v warning -i $1 -i $palette -lavfi "$filters [x]; [x][1:v] paletteuse" -y $2

运行

1
./change.sh './[要转换的mp4]' '[转换成的gif名字]'

或者把需要转换的 mp4 文件放到和上面这个脚本相同目录中,执行

1
for i in *.mp4; do ./change.sh $i "${i%%.*}.gif";done

这条命令会把所有的 mp4 的后缀名去掉,换成 gif。

另外,windows 端也可以通过修改成两个 bat 文件做到类似的效果(不过第二部我不知道怎么把原 mp4 文件夹的所有后缀替换成 gif,只能直接加 gif),要批量转换的时候运行第二个 bat 就 ok 了

change.bat

1
2
3
4
5
6
7
8
@echo off
cd /d %~dp0
set palette="palette.png"

set filters="fps=15,scale=-1:-1:flags=lanczos"

ffmpeg -v warning -i %1 -vf "%filters%,palettegen" -y %palette%
ffmpeg -v warning -i %1 -i %palette% -lavfi "%filters% [x]; [x][1:v] paletteuse" -y %2

change_all.bat

1
2
@echo off
for %%i in (*.mp4) do change.bat "%%i" "%%i.gif"

嫌转换成的 gif 太大?改点参数就成,比如:

change_slim.bat

1
2
3
4
5
6
7
8
@echo off
cd /d %~dp0
set palette="palette.png"

set filters="fps=10,scale=350:-1"

ffmpeg -v warning -i %1 -vf "%filters%,palettegen" -y %palette%
ffmpeg -v warning -i %1 -i %palette% -b:v 1000k -lavfi "%filters% [x]; [x][1:v] paletteuse" -y %2

合并音视频

1
ffmpeg -i xxx -i xxx -c:v copy -c:a copy output.mp4

mp4 转音频

对于大部分 mp4,可以直接提取其中的音频,速度很快:

1
ffmpeg -i .\test.mp4 -acodec copy -vn test.mp3

不过,这条命令是直接把 mp4 封装中的音频部分提取出来,如果音频部分不是 mp3 格式就会出错:

从日志里面,我们可以观察到原来封装里面是什么格式的,比如这里就是 aac 的:

这时候就需要把命令中的格式从 mp3 改成 aac:

1
ffmpeg -i .\test.mp4 -acodec copy -vn test.aac

如果需要转换其他格式,可以使用 c:a 参数替代 acodec,比如转换为当前较先进的 opus 格式(当然速度就慢多了):

1
ffmpeg -i .\test.mp4 -c:a libopus -vn test.opus

下载 m3u8

参考文章:Why does ffmpeg ignore protocol_whitelist flag when converting https m3u8 stream?

某些视频网站和 ios 软件用的视频地址抓出来是 m3u8 的。比如机核的视频。可以用 chrome 插件猫抓获取 m3u8 文件。然后,加入相关参数,注意需要紧跟在 ffmpeg 命令后面:

1
ffmpeg -protocol_whitelist file,http,https,tcp,tls,crypto -i xxxx.m3u8 xxxx.xxx

xxxx.xxx 指的是你需要输出的视频名字和格式,因为 m3u8 流切下来一般是 h264 的,封装格式需要你自己来确定,ffmpeg 会帮你把所有切片合并。

自动裁切黑边

参考文章:CROPDETECT AND FFPLAY - 2020

先检测黑边:

1
ffmpeg -i .\test.mp4 -vcodec copy -acodec copy cropdetect=24:16:0 test1.mp4

24:16:0 是默认参数,一般如果要调也只调第一个参数(黑边阈值)。

输出的日志中会有类似于

1
[Parsed_cropdetect_0 @ 0x3704360] x1:0 x2:639 y1:43 y2:317 w:640 h:272 x:0 y:46 pts:181320 t:181.320000 crop=640:272:0:46

其中有用的就是 w、h、x、y 四个参数,分别放入新命令的相应位置

1
ffmpeg -i .\test.mp4 -vcodec copy -acodec copy -vf crop=640:272:0:46 test1.mp4

即可。(虽然经过我测试默认参数检测还是有点偏差,但稍微手动调一下 w 和 h 的值效果就令人满意了)

其他

参考文章:给新手的 20 多个 FFmpeg 命令示例

比较有用的几个地方:

转换格式时不压缩视频

使用-qscale 0

1
$ ffmpeg -i input.webm -qscale 0 output.mp4

(21.6.26 更新)批量将视频大小限制到一定范围内(试做)

参考文章:

众所周知媒体文件(图片,视频,音频等)要降低体积不难,但要降低到一个大小范围内,就很折腾了。

ffmpeg 缩小视频体积一般有那么几种途径:

  1. 降低码率,这种方法用的比较多。降低码率比较简单的一共有三种方法
    • 动态码率(vbr)下,直接使用参数 -b:v [目标码率]
    • 固定码率(cbr)下,使用参数 -cbr [cbr参数] -pass 1,cbr 参数默认为 23,增大至 28 左右,可有效减小体积。
    • 参数 -qp
  2. 降低分辨率。
  3. 降低帧数。

对于视频文件,通常我们不采用降低帧率的方法,而是配合使用降低分辨率和降低码率的方法,因为在固定分辨率的情况下,码率的下降是有极限的,比方说在 1080p 分辨率下,即使我们使用参数 -b:v 1000,最后转换出来的视频平均码率可能也有 1500 左右;并且后续再使用 -b:v 500 也会发现转出来的视频和上一个视频大小几乎没有区别。另一方面,高分辨率低码率的视频有可能比同体积的稍低分辨率但高码率的视频还要胡上许多。

我们的思路是,第一次先对码率进行下调,当体积大于目标值时,再对视频的码率和分辨率同时进行缩小,直到体积符合要求。

change.bat

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
::调试时改为echo on
::set local可参考[请问批处理setlocal命令一般如何使用? - 知乎](https://www.zhihu.com/question/279379047/answer/847986676)
@echo off&setlocal enabledelayedexpansion

::nv硬解,无硬解能力的话可设为libx264
set self_codec=hevc_nvenc
::码率
set bitrate=2000
::目标大小
set destiny_space=30

for %%i in (*) do (
rem bat 在 do 中 echo[批处理文件 - 如何在/ F的循环内设置变量 - VoidCC](https://stackoverflow.com/questions/13805187/how-to-set-a-variable-inside-a-loop-for-f)
for /f "tokens=2 delims==" %%j in ('ffprobe -v error -show_entries stream^=bit_rate -select_streams v:0 "%%i"') do call :Foo %%j bit_ratenow
for /f "tokens=2 delims==" %%k in ('ffprobe -v error -show_entries stream^=width -select_streams v:0 "%%i"') do set widthnow=%%k
echo 初次转换视频码率为!bit_ratenow!,宽度为!widthnow!
::这里没法用双冒号注释,只能用rem,原因不明
ffmpeg -y -i "%%i" -vcodec %self_codec% -b:v !bit_ratenow!k "change.%%i"
call :NextReduce "%%i" !bit_ratenow! !widthnow!
)

goto End

:Foo
::/a参数指对语句进行计算
set /a z=%1/1024
::echo %2
if %z% geq %bitrate% (
set z=%bitrate%
) else (
::bat不支持浮点数
::set /a z=%z%*4/5
set /a z=%z%
)
set /a %2=%z%
::echo %z%
goto :eof

:NextReduce
setlocal
set bitrate=%2
set width=%3
:lessthen100m
set space=0
rem 使用~符号忽略双引号
::echo change.%~1
::echo %~1
for /f "tokens=3 delims= " %%l in ('dir /a /c "change.%~1"^| find /i "change.%~1"') do set /a space=%%l/1024/1024
echo %space%
if !space! geq %destiny_space% (
set /a bitrate=!bitrate!*4/5
set /a width=!width!*4/5
echo 目标视频码率为!bitrate!,宽度为!width!
ffmpeg -y -i "%~1" -vcodec %self_codec% -b:v !bitrate!k -vf "scale=!width!:-1" "change.%~1"
goto :lessthen100m
)
endlocal
:End

然后将需要处理的视频放到和本 bat 同一个文件夹下即可。处理过的视频文件将被命名为 change+源文件名.mp4

当然这个脚本还是有不少的改进空间:

  1. 不同分辨率的目标码率应该不太一样,我现在把目标码率统一设置为一个数字了。如果设置的太小就会牺牲高分辨率的视频文件;设置得过大低分辨率的视频文件就可能需要多次转换。
  2. 直接使用参数 -b:v [目标码率]进行码率降低实际上是一个简单而糟糕的方法。

第一个问题,如果分辨率和码率有一个合适的对应函数曲线可能就能解决,我没找到;有个大概的表格

但用分辨率中的宽度/码率得到的结果在 0.2-0.3 直接浮动,差距太大,可能凑合用都有点勉强。

第二个问题,推荐的方法是使用 Two-Pass ABR 转换法:

用于限制输出文件的大小,比如预期视频文件有 10min(600s),200 MB:

1
2
3
>200 * 8192 / 600 = ~2730 Kb
>2730 - 128(音频常用的比特率) = 2602 kb
>12

那么:

1
2
>ffmpeg -y -i input -c:v libx264 -b:v 2600k -pass 1 -c:a aac -b:a 128k -f mp4 /dev/null && \
>ffmpeg -i input -c:v libx264 -b:v 2600k -pass 2 -c:a aac -b:a 128k output.mp4

但要在 bat 下这样处理是有点麻烦。。。如果下一次我再改这个脚本,我可能会选择用 python 写吧,现在这个,先凑合着用吧2333

(22.1.19 更新)批量将视频大小限制到一定范围内-python 版(试做)

用 bash 写脚本真是吃力。。。还不能在 linux 上用。想要改进一下脚本,让子目录下的文件也能被一键转换,发现用 bash 写实在是太复杂了,所以干脆把上面的脚本用 python 重构了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import os
from pymediainfo import MediaInfo

filetuple = os.walk(r'./')

#走显存编码,可选qsv/cuda/nvenc/amf,使用qsv时需保证没有独立显卡(特别是N卡),否则会报错,是bug,来自[#6996(尝试在 Windows 10 上使用 NVidia 主 GPU 支持的 Intel 系统上使用 QSV 会导致崩溃)– FFmpeg](https://trac.ffmpeg.org/ticket/6996),不用可置空。
hwaccel=''
#hwaccel=r' -hwaccel qsv '
#解码方法,可用h264_cuvid/h264_amf/h264_qsv/libx264,不用可置空。
#self_decodec=''
self_decodec=r' -c:v h264_qsv '
#解码方法,可用h264_nvenc/h264_amf/h264_qsv/libx264
#self_encodec=''
self_encodec=r' -c:v h264_qsv '

#码率(单位kbps)
destiny_bitrate=4000
#目标大小(MB)
destiny_space=200
#目标帧数
fps=r'23'
#目标格式
format=r'.mp4'

#转换命令
def change_bat(file_name,extension,bit_rate,height):
command=r'ffmpeg'+hwaccel+self_decodec+r'-i "'+file_name+extension+r'"'+self_encodec+r' -b:v '+str(bit_rate)+r' -vf scale=-1:'+str(height)+r' -r '+fps+r' -y "'+file_name+r'_convert'+format+r'"'
os.system(command)

#获取码率
def detect_bit_rate(file_name):
command=r'ffprobe -i "'+file_name+r'" -show_entries format=bit_rate -v quiet -of csv="p=0"'
bit_rate=os.popen(command).read().split()[0]
return bit_rate

#获取视频高度
def detect_height(file_name):
command=r'ffprobe -i "'+file_name+r'" -show_entries stream=height -v quiet -of csv="p=0"'
height=os.popen(command).read().split()[0]
return height


for path,dir_list,files in filetuple:
for file in files:
try:
path=path.strip('./')
if(path!=''):
file=os.path.join(path,file)
fullfilename=file
#排除非视频文件
fileInfo = MediaInfo.parse(file)
for track in fileInfo.tracks:
if track.track_type == 'Video':
#获取拓展名
(file, extension) = os.path.splitext(file)
#已转换/直接更名的视频直接跳过
if(not fullfilename.endswith(r'convert'+format) and not os.path.exists(file+r'_convert'+format) and not fullfilename.endswith(r'convert'+extension) and not os.path.exists(file+r'_noconvert'+extension)):
if(os.path.getsize(file+extension)>destiny_space*1024*1024):
#第一次转换,大于目标大小的,码率缩到目标码率,高度缩到1080,若码率和高度均一低于目标码率,则取源文件码率/高度,然后缩减帧率,转换
bit_rate= int(detect_bit_rate(file+extension))
height=detect_height(file+extension)
if(bit_rate>destiny_bitrate*1000):
bit_rate=destiny_bitrate*1000
if(int(height)>1080):
height='1080'
print("初次转换视频码率为:"+str(bit_rate/1000)+"kbps")
change_bat(file,extension,bit_rate,height)
#第一次转换后文件仍大于目标大小的,则进入循环转换流程,每次转换码率和高度会同时缩减到上次转换的80%,直到大小低于目标大小为止
while(os.path.getsize(file+r'_convert'+format)>destiny_space*1024*1024):
bit_rate=int(bit_rate)*4/5;
height=int(height)*4/5;
print("本次转换视频码率为:"+str(int(bit_rate/1000))+"kbps,视频宽度为:"+str(height)+"px")
change_bat(file,extension,bit_rate,height)
else:
#未转换,直接复制更名,便于后续筛选
all_path=r'copy /y "'+file+extension+r'" "' +file+r'_noconvert'+extension+r'"'
os.system(all_path)
except:
continue

新脚本在实现了上一个一键限制视频到对应体积功能的基础上,增加了以下功能:

  1. 通过修改参数使编解码均使用硬解,默认使用支持最为广泛 qsv 硬解(intel 家的硬解标准)。
  2. 对子目录中的视频文件同样进行转换。
  3. 默认的封装格式为 mp4,可更改。
  4. 同时对帧数进行了更改。
  5. 转换时对修改/无需进行修改的文件均进行备份,多次运行时不会对已转换的项目进行多次转换。

版本

ffmpeg windows 端出了新的编译版:BtbN/FFmpeg-Builds。release 中有许多不同版本。lgbl 和 gbl 应该是许可证之间的不同,但大小也有区别,我个人认为是为了 lgbl 剔除了一些东西;shared 和不带 shared 的版本,区别是后者将运行库文件合并进了 ffmpeg 二进制文件中(但不带 shared 的版本体积大了很多)。最关键的是 vulkan 版本,带有 vulkan 版本需要比较新的独显和新的驱动才能使用,我的 965m 运行都会报错 Lossless encoding not supported