使用 libav 实现四人同屏直播效果

前几天加的一个学校原神群里想趁原神躲猫猫活动之前在群里举办一个躲猫猫大赛,一开始是想大家简单各自比一比,然后记录分数就好。不过这样就太没意思了,躲猫猫活动里经常有人有骚操作,如果能够直播就好了。而且躲猫猫是四个人玩,观众如果能看到猎手和游侠的位置,就更有意思了。按理来说,如果大家都在校园网内,那么直播是很简单的,我只需要在我的电脑上开一个 OBS,然后让大家推流到我的电脑,我再推流合成后的画面,但是此时已经是寒假,很多人已经回家了,所以这个办法马上就被否决了。大家此时又想到了用会议软件来实现多人同屏,可是目前国内常用的飞书还有腾讯会议都不支持多人同时共享屏幕,而 Zoom 虽然支持,但并不是每个人都能方便地连接上 Zoom。因此,最终只能选择开一个云服务器,然后让云服务器接收大家的推流之后再推到 B 站即可。按理来说,我只需要开一个 Windows 的服务器,在上面安装一个 OBS,再安装一个 RTMP 服务器就大功告成了。可是作为程序员怎么能不用 Linux!但是我之前并没有音视频处理的经验,也想借这个机会学习一下音视频编程的知识,于是,我开始了研究在 Linux 下进行音视频处理的踩坑之路……

最终的效果:https://www.bilibili.com/video/BV1vT4y1y7ve

完整代码:https://github.com/howardlau1999/live/blob/main/live.cpp

总结一下需求就是,需要实现四人直播同屏,且四个人是异地的,都从公网推流,并且希望延迟尽可能低。这个看上去很简单的需求其实有很多细节需要注意处理。

当然,在 Linux 下处理音视频也是有现成的工具的,那就是 ffmpeg。经过一番简单的搜索,发现使用 ffmpeg 命令行工具就可以实现需求,只需要使用一个稍微复杂的滤镜即可。但是经过测试之后发现,这个方法虽然简单,但是稳定性却很差,如果四个流其中一个发生卡顿,会造成整个画面都会卡顿,而且经常出现时空回溯的现象。也就是说,ffmpeg 命令行工具是要等所有的输入都有响应之后才会处理下一帧。考虑到大家都是公网推流,且大家的网络以及设备高度不可控,这样的稳定性是无法接受的。所以,一个很自然的想法就是在拉流解码端和编码推流端加入一个中间层,来缓冲数据流不同步的问题。我开始研究能不能使用一些命令行工具组合来提高稳定性。首先想到的就是 mkfifo 命名管道,先用四个拉流的程序拉取每个人的直播流并解码,然后往四个命名管道写入数据,然后编写一个程序从四个管道读取数据并缓存,然后写入到新的命名管道中,如果某个拉流管道没有新的数据,就不断写入缓存中的数据。负责整合画面的推流程序则不断从管道中读取数据。程序很简单,很快就编写完毕了,但实际测试发现并没有什么用,画面反而卡顿得更厉害了,这个简单的思路行不通。而且,我们无法知道写入管道里的数据是不是刚好是一帧数据。所以,最后不得不采用了重量级的解决办法——自己调用 ffmpeg 的库函数写一个拉流推流程序。

ffmpeg 这个命令行工具实际上是 libavformat 等库函数的封装,所以,我们首先得安装 libavformat 等库。

sudo apt install libavformat-dev libavdevice-dev libavcodec-dev libavutil-dev libavfilter-dev libswscale-dev

平时我们见到的 mp4、flv 等文件实际上是一个“容器”,也就是说它只负责将已经编码好的视频和音频合并起来,而不负责编码,关于封装格式的处理,都是由 libavformat 处理的。而具体到音视频的编解码则是由 libavcodec 来处理,例如,我们网上的直播视频通常采用的是 H.264 编码,而音频则是使用 AAC 进行编码。当视频被解码之后得到的原始数据则可以使用 libavfilter 进行一些处理,比如缩放、旋转、叠加文字等等。

关于音视频部分的编解码,其实没有什么好说的,直接传入自己需要的参数给编码器,然后使用函数从编解码器获取需要的数据即可。

在新版本的 ffmpeg/libav 库中,基本上音视频数据都被抽象成 packetframe 这两个数据结构了。可以认为 packet 对应的是原始数据,也就是编码后的数据,而 frame 则是解码后的数据。无论是将 RAW 的数据用 avcodec 编码成 H.264 或者 AAC,还是将编码好的 H.264 和 AAC 用 avformat 封装成 FLV 格式,都是统一采用的 framepacket 来进行数据的传输。这里需要注意的是,一些函数会帮你释放 framepacket,一些则会拷贝数据,在开发的时候,建议先不要写 freeunref 的函数,也就是任他内存泄漏,等跑通了之后,再去仔细地添加释放函数。

在解码之后,将 frame 传给 avfilter 由它来合并画面之后,将输出的 frame 重新编码为 packet 推给 B 站 RTMP 服务器。

服务器搭建

服务器除了视频合成程序以外,还需要启动一个 RTMP 服务器来接收客户端的推流,然后视频合成程序再从这个服务器进程分别把流拉下来解码处理,然后再推送给 B 站。RTMP 服务器可以使用 nginx,我这里直接使用了 OpenSRS,只需要一行命令就能启动:

docker run --rm -it -p 1935:1935 -p 1985:1985 -p 8080:8080 registry.cn-hangzhou.aliyuncs.com/ossrs/srs:5 ./objs/srs -c conf/realtime.conf

客户端推流,移动端使用的是 Larix Screencaster,体积很小而且没有广告,App Store 和 Google Play 都可以下载,电脑端就还是使用 OBS Studio。

后记

可以看到直接使用 ffmpeg/libav 库来操作音视频编解码还是比较底层的,而且也比较繁琐,如果只是单纯的想合成画面之类的,可能使用 libobs 会更好一些,但是没有时间去研究了。