少女祈禱中...
Loading...
图片加载过慢?试试 从源站加载资源 X

ccloli

[废稿] FFmpeg 推流指令小记

这是一篇废弃的文章。由于某些原因,本文未能最终成文,并且由于时间久远,已经没有成文的可能性,还请谨慎阅读。

若您需要了解最终解决方法,解决思路为:使用 Node.js 的 child_process 创建三个 FFmpeg 进程,第一个负责从文件列表循环读取文件并指定 -re 参数,然后使用统一的编码参数封装为 mpegts 格式,并输出到 stdout;第二个负责从一个预先录制好的黑屏视频 null.flv 并指定 -re 参数 copy 封装为 nut 格式(我也忘了为什么),并输出到 stdout;第三个负责从 stdin 中读取流,在指定 -err_detect aggressivecopy 封装为 flv 格式,推流到 RTMP 地址。将第一个进程 pipe 到第三个进程,当第一个进程转码完成一个文件后,立即将第二个进程 pipe 到第三个进程,防止推流中断,待第一个进程开始转码文件后再将第一个进程 pipe 到第三个进程。

本文实际发表于 2018 年 10 月 9 日。

最近在研究如何使用 FFmpeg 进行推流,所以在这里记录一下一些基本的命令行参数。

其实在研究 FFmpeg 的推流功能之前,自己也算稍微研究过一些 FFmpeg 的参数,比如下面这个 FFmpeg 录屏的辅助脚本:

总之 FFmpeg 就是一个非常强大的开源编解码解决方案,基于它可以实现绝大多数你所能想到的编解码功能。


下面的参数均是将视频源推到本地的直播源,源地址为 rtmp://localhost/live/ccloli

如果需要自己搭建本地直播平台进行测试,可以试试功能强大的 MistServer 或者短小精悍的 MonaServer

关于 FFmpeg 的具体指令介绍,此处不会完全详细解释,RTFM

如果你需要将一个文件进行推流,可以使用

其中 -re 参数描述如下,简单来说就是让输入的文件流按照指定的时间比率进行输入,从而保证输出流时间的稳定(如果不指定,很有可能会出现跳帧的情况);-i 即输入的文件;-c copy 代表对视频流和音频流都直接进行复制而不进行二次编码;最后是输出参数,-f flv 即 Flv 格式输出,最后输出到 rtmp 地址上。如果指定为一个文件名,那么它就是输出到一个文件。

-re (input)

Read input at native frame rate. Mainly used to simulate a grab device, or live input stream (e.g. when reading from a file). Should not be used with actual grab devices or live input streams (where it can cause packet loss). By default ffmpeg attempts to read the input(s) as fast as possible. This option will slow down the reading of the input(s) to the native frame rate of the input(s). It is useful for real-time output (e.g. live streaming).

如果你需要让这个源循环播放,可以用 -stream_loop 来循环输入流,指定数值为重复次数,-1 为无限循环


如果这个源是一个图片,为了保证图片源能循环推流,需要给图片的输入源加上 -loop 1,此时也不能使用 copy,必须使用编码器对其进行编码。
由于 B 站这样的源不支持单纯的视频流或单纯的音频流,那么必须给源加上一个音轨,如果需要输入一个音频文件,可以这样输入,并使用 -map 来对输入的源进行混流。

这里我们将输入的流输出为一个文件,而不是类似上面推流的形式,但是你应该能明白如何改成上面的形式。

-loop 1 即对图片流循环输入,-r 60 即按照 60fps 的速率处理输入的图片,可以按需调整。如果不指定 -loop,那么这一帧将只出现在画面的一开始,后面的时间将不会有任何关键帧数据。你可以试试看去掉它,那个文件仍然可以在你的播放器中播放,看起来也不会有任何差别,但是实际上推流后由于后续时间的视频流为空,播放器将找不到视频数据,很有可能会出现只有音轨的情况。至少在 B 站的推流中,这样会导致无限加载。

我们使用了两个 -i 所以有两个输入流,为了保证两者的时间一致(因为图片源是无限循环的,如果音轨播放完后,视频源会依旧存在,音频源会没有,也会出现上述情况),我们需要加上 -shortest 对输入的流进行裁剪,它会让 FFmpeg 使用最短的时间进行裁剪,这样可以减少 loading 的情况。

后面我们终于开始使用编码器进行编码了。其实如果我们只使用 -copy 的话,FFmpeg 就只需要做对文件解流、混流以及推流的工作,CPU 占用是非常小的。但是如果我们需要对文件进行编码,你会发现 CPU 占用会增大数十倍甚至数百倍,因为 libx264 是使用 CPU 进行编码的(GPU 编码?自己编译 QSV 或者 NVIDIA CUDA 之类的吧)。如果你使用过 OBS 进行推流的话,你会发现建议的 H.264 编码预置文件 -presetultrafast。这个预处理参数是计算 CPU 需要花费多少时间进行编码,速度越快那么编码时间也就越快,但质量也会越差;速度越慢那么编码质量也会越好,但是很有可能会丢帧,因为时间很有可能跟不上推流的实时时间。所以为了保证推流时不影响其他应用比如游戏的效率,一般使用的是 ultrafast,但是如果你喜欢或者对设备配置有信心的话,可以稍微调整下,个人认为使用 fast 就可以达到一个很好的平衡了,不过也要看你的设备。

这里的参数是使用 libx264 进行视频流编码,比率为 1000K,使用 aac 进行音频流编码,质量为 -vbr 4 这里就不赘述了。最后我们使用 -map 来指定需要处理哪些流,a:b 这样的形式中 a 代表输入文件的索引值,b 代表该输入文件的流索引,这里就是使用第一个输入的视频流和第二个输入流的音频流进行混流。

如果你不想输入一个音频,可以使用 lavfi 来输入一个空音频流,并使用 -map 进行索引:


如果我们需要在视频流上加上文本,可以使用 drawtext 滤镜。下面的参数就是在画面正中央写入一行 FFmpeg Stream Test 的文本,并使用半透明黑色作为背景

我们使用 -vf 来添加视频滤镜,使用 drawtext 这一滤镜,对该滤镜配置如下:文本为 ‘FFmpeg Stream Test’,使用字体文件 1.otf,文本为白色,并使用文本框,文本框边框为 5 像素,使用 0.5 透明度的黑色作为背景,文本坐标为 (视频宽度 - 文本宽度) / 2, (视频高度 - 文本高度) / 2


如果你需要在视频流上硬编码当前编码的时间,可以用 timecode,这也是网上很多回答的方法。但是需要注意,这个并不是文件实际的时间,而是编码的时间(按照帧率计,也就是说这个时间并不是编码的实际时间,而是根据 drawtext 参数中的 r= 来计算的,比如 r=24 那么就相当于每 24 帧计一秒,即便这 24 帧可能不是一秒的时间)。其最后一位也不是毫秒值,而是当前的帧数。为了尽可能贴近毫秒的效果,我们必须指定输入和输出的 fps 来达到看起来像毫秒的效果。下面是网上大多数回答所采用的方法,这也是他们的回答的一个改进版,只是增加了帧率的设置而已。注意这里我们没有使用 -re(因为他们也没有使用 -re),毕竟我们只需要输出一个文件,使用 -re 反而会导致输出速度变得非常慢。

yuv444p10 是个非常可怕的色彩空间,实时推流还是乖乖用 yuv420p

如前所述我们使用了大量的 -r 来强行固定 fps,以尽可能保证每 60 帧为 1 秒,这样它看起来就像是这样:
20170504123740

Hmmmm……之前的 bug 无法复现了!是这样,如果你发现这样和实际播放的时间偏差越来越大的话,如果排除了播放器本身的问题仍有这样的情况,有可能是输入的流本身 fps 还是没法固定,这个时候就真的需要 -re 了。如果嫌 -re 太慢,可以在 -re 前指定输入帧率,类似 ffmpeg -framerate 120 -re ...,虽然我不确定为什么这样能解决问题,或许是真的是固定帧比率吧,有时输入图片的 -r 会有偏差。

但是按照帧计算还是按照帧计算,如果帧的输入出现了偏差,时间就会乱掉。那么有没有其他参数呢?有的,drawtext 有一个函数是 pts,它是真正的编码流时间,这个时间是真正精确到毫秒级的,大可以放心使用。这样的话我们就不需要 timecoder 了,只需要在 text 里指定就行了。

你可以使用多个 drawtext 滤镜来添加不同的文本。


另外 drawtext 允许我们使用 textfile 读入一个文本文件,它还有另一个参数 reload,它可以在编码的每一帧前读取该文件而动态改变内容。所以你可以把上面的文本写进一个 txt 然后用 textfile 的形式载入,这样在串流的时候可以动态更改显示的文本。

而在 text.txt 里我们可以自由定义要显示的内容,而显示的内容会实时更改到输出上。

我们甚至还可以写一个程序,通过虚拟声卡捕获扬声器,然后把我们在听的歌给串流上去,视频界面再写上歌曲名和实时歌词,这样不就是一个音乐电台了?

获取和写入歌词的脚本这里就算了(

是不是感觉很 6?
……并不。如果你频繁更改文本很有可能会导致 FFmpeg 输出这样一段文本,然后……FFmpeg 就退出了?????

原来这是因为在你写入文本的时候 FFmpeg 正好在读取文本,结果 FFmpeg 就因为文件的锁机制而炸掉了?????

FFmpeg 的文档里是这样描述 reload 的:

If set to 1, the textfile will be reloaded before each frame. Be sure to update it atomically, or it may be read partially, or even fail.
这就非常坑爹了,读不了就读不了嘛,你居然还退出了。虽然我们可以写个 bat 自递归调用,但是如果是串流文件的话

网上有这样一个解决方法,因为要求在更改文本时达到原子级的操作,可以使用 mv 而不是使用 cp 来尽可能减少操作时间。也就是说,先在另一个地方改好文本,然后采用剪切的方式来更改。

这样确实很高效,在我对正在播放的歌词动态展示的时候,使用 Node.js 来动态接收歌词文本并写入 drawtext 的文本中,相比较于直接写入,采用写入一个临时文件再使用 fs.rename() 确实没有出现退出的问题了。

但是……这只是治标不治本啊,如果我们来个压力测试……

……还是会 gg 嘛……

那么最终还是该怎么解决呢?这样就需要治本了。通过研究 FFmpeg 中 drawtext 滤镜的源代码 vf_drawtext.c,我们在 573 行左右找到了那个可恶的字符串。

我们尝试将第 577 行的 return err; 给注释掉,然后在 MSYS 下进行编译(需先编译 freetype)

然后将编译得到的 libavfilter.dll 覆盖 FFmpeg Shared 编译二进制程序的同名文件,虽然在启动时有参数不一致的警告,但总算是解决了这么一个大难题。


上面提到的都是对一个文件进行编码,或者对当前设备的视频与音频进行编码。如果我们需要对多个文件进行合并编码并进行推流……这样不就是一个电影直播间了?

FFmpeg 有一个输入混流器叫做 concat,使用它就能进行合并文件编码操作啦。我们只需要写一个文本配置文件就能很方便的合并文件。至少 cc 曾经合并 B 站的分段 flv 时就用到了 concat:

而 input.txt 的内容为

文件结构是不是特别简单?如果你能获取到文件的时间的话,还可以加上 duration,虽然并不是必须的,但是这样可以放 FFmpeg 快速地在编码时预分配资源。