HEXA娱乐开发日志技术点004——一步到位的推流

HEXA开发日志目录
上一篇 HEXA娱乐开发日志技术点003——下位机成功推流


颠覆

本篇的成果是,实现了基于raw data的推流。
前一篇是基于flv文件的推流,flv文件的内容是对raw data进行压缩编码的产物,而我们从mind SDK获得的是raw data,是不能用RTMPDUMP直接推流的,所以才有了本篇内容,并对前一篇的内容进行了颠覆,这个颠覆要从下图说起。

技术需求与技术点

这张图相对于之前的版本有些变化,左边技术需求多了个压缩编码,右边技术点中的RTMPDUMPffmpeg取代了,右边这个变化就是本篇所指的颠覆了。

需求的变化

当初对RTMPDUMP不了解,不清楚它要的是压缩编码后的数据,所以没考虑到压缩编码这个必要的数据转换步骤。

技术点的颠覆

前一篇的部分成果被颠覆的原因是,我只知道ffmpeg可以压缩编码,但不知道它还可以直接推流,当我看到雷前辈的例子时,我惊呆了,因为里面没有一点RTMPDUMP的影子,却说它可以推流。
在验证了雷前辈的例子之后,我当时认为,大概是ffmpeg使用了RTMPDUMP的库吧,因为它们的git地址都出现了ffmpeg.org,既然它们是整整齐齐的一家人,那至少应该是相互有关联的一群人做出来的吧。对于rtmp推流的功能不会单独做出两份相同功能的东西吧。因为我的系统里装了RTMPDUMP的库,所以还侥幸地认为多半ffmpegRTMPDUMP之间有依赖关系,直到我把skill中的RTMPDUMP库删除,并确认机器人身体里确实没有了这个库,还依旧能正常推流之后,我才放弃了侥幸。
这彻底宣告了,我对RTMPDUMP的相关研究,对于目标来说是无用功。

这就是积极前进的人生的常态吧,在对昨天的自己的嘲笑中前进,明知道今天的自己也会被明天的自己嘲笑,但若不做今天的无用功,也无法获得明天嘲笑今天的资格。

干货

回顾了昨天的无用功,再来看看今天又搞出了什么幺蛾子。
首先明确一下本篇目标,就是把从raw data到推流这条路打通,这里的raw data还不是从mind SDK获得的,只是用官方的算法制造的。步骤还是经典三板斧,即研究、上位机demo、移植到下位机

研究ffmpeg

其实算不上什么研究,主要是搞清楚上位机demo的步骤,在考虑如何入手之前,先列一下已知条件:

  • ffmpeg有官方例子可以参考
  • 我在之前的工作中研究并改造过一个官方例子
  • 雷前辈有例子可以参考

下面对已知逐一分析:

  1. 首先,先看一下我研究过的那个例子,这个例子只能编码视频,没有音频,我并不能拿来用。只是在这个例子的研究中,我知道了如何用新ffmpeg接口替换掉过时的接口。
  2. 然后,我确定了我需要参考的官方例子,这个例子把raw data压成了flv文件,而前一篇正好是以flv文件作为输入的,我当时想,一定可以在这两者之间建立数据连接,而不必生成flv文件,这样输入是raw data,输出是推流,本篇的目标就达到了。这时的产出是61dd400e0323bd242e5242ec01608c594373cde0。在这个修改中,我把这个例子的过时接口都换成了新的,并且编译OK了,然后待用。
  3. 最后,我看到了雷前辈的例子,从这个例子可以看出,我根本不用考虑如何与RTMPDUMP建立数据连接,只要对例子改一下输出方式,就可以直接推流啦。

至此,第一板斧砍完。

上位机demo

这个阶段就三步,先验证例子的有效性,再验证改造的例子的推流功能的有效性,最后用GO语言调用测试。
第一步很顺利,没有单独提交。
第二步,即C语言部分的提交是f51f070eeb2b3a0a7827519d58d6a3c27bb7ce23,可以看到修改很简单,如果真去嫁接RTMPDUMP,工作量得比这个大得多得多得多。。。
第三步,加入GO语言部分的提交是cf0d3e304a5a824c6912a8e3b2b13975c990e8c8,其中的demo_ffmpeg.c是基于test.c修改的,主要是把生产raw data的函数改成了call back,这样就可以在GO语言里实现raw data的生成,至少从C部分来看,整个流程就和最后实际的使用方式完全一样了。而对于GO语言部分,后面再把生产raw data的方法改成从摄像头和话筒获取就行了,当然这是后话了。

至此,第二板斧砍完。

移植到下位机

移植也是之前的套路,先编译C依赖库,然后GO程序与mind SDK对接。

ffmpeg的交叉编译

这里先感谢某位哥们儿的帮助,把他以一般Ubuntu为目标平台的编译方法提供给我做参考。
可惜的是,后面还是有东西要探索,最关键的就是交叉编译所带来的麻烦,最后的configure命令长这样:

./configure --prefix="/go/src/skill/FFmpeg" \
--extra-cflags="-I/go/src/skill/FFmpeg/include" \
--extra-ldflags="-L/go/src/skill/FFmpeg/lib" \
--bindir="/go/src/skill/FFmpeg/bin" \
--enable-cross-compile \
--cross-prefix=arm-linux-gnueabihf- \
--cc=arm-linux-gnueabihf-gcc \
--disable-x86asm --arch=armv4 --target-os=linux \
--disable-static --enable-shared \
--disable-ffmpeg --disable-ffplay --disable-ffprobe
make
make install
  • 带有cross字样的都与交叉编译有关,还有cc指定了交叉编译器
  • 要生成动态库,--enable-shared是要加的,我不需要静态库就加了--disable-static
  • -disable-ffmpeg -disable-ffprobe -disable-ffplay是说不需要生成这些可执行程序,因为我只需要依赖库

相对于对接mind SDK,编译ffmpeg还是麻烦一点,以为它的编译选项比较复杂,有些选项好不好用、该不该用,都变成了很随缘的事情,这些选项也是我慢慢摸索出来的。

GO程序对接mind SDK

关于对接mind SDK,修改是ca44359aebc9d2729cd9213f6c126a9be1722c34,虽然文件多,但是并不复杂。

  • 增加ffmpeg的头文件(DanmuDriveMe/robot/deps/include下面多个文件)
  • 增加ffmpeg的库文件(DanmuDriveMe/robot/deps/lib下面多个文件)
  • 删除RTMPDUMP相关的文件,包括头文件、库文件和GO文件
  • ffmpeg demo对接mind SDK,这些是核心修改,只有5个文件

逻辑和上位机demo大同小异,关于对接mind SDK不做详细解释了,只说一个GO与C混合编程的小坑,一个坑了我若干小时的小坑,具体看下面注释吧。

func stream_start(url string) {
    //这个cc是C语言部分的buffer
    cc := C.CString(url)
    //defer是说在即将退出这个函数时执行后面语句,即在退出stream_start函数之前会把cc free掉
    defer C.free(unsafe.Pointer(cc))
    if stream_control(true) {
        log.Debug.Println("url:["+url+"]")
        //setup是C函数,里面会使用cc
        go C.setup(cc)
        //go语句是非阻塞的,如果没有这个sleep,stream_start函数就会很快返回,这意味着cc很快会free
        // avoid to free cc too early
        time.Sleep(time.Second)
    }
}

要注意上面代码中的2件事的顺序,即cc的释放和使用的顺序,我被坑的原因就是,cc先被释放了,然后才被使用,并且我还不知道怎么才能看到C部分的log,对C部分的debug手段很笨拙,所以花很长时间才怀疑到这。
cc被释放的原因是stream_start函数很快返回了,那么基于defer语句的机制,cc就很快被释放了,而go语句是非阻塞的,从cc传入setup到被使用,需要一段时间,所以函数最后要加一个sleep,保证在这段时间内,函数不会返回,cc也就不会在使用前被释放。
当然,还应该有其他解决方法,比如不在GO这边释放cc,在C部分释放也许也能行。

至此,第三板斧砍完。

总结

  • ffmpeg替代了RTMPDUMP,一步到位地完成了压缩编码和推流
  • 下位机推流还有点问题,可以在播放端看到,似乎是推流的速度不够,出现了视频网站那种放一会儿就要缓冲一下的现象,具体原因有待分析
  • 下面计划是对机器人采集的音视频raw data进行推流,以及用OpenCV处理图像
  • 这些事情的草还没发芽,包括弹幕命令集、机器人命名等

下一篇 HEXA娱乐开发日志技术点005——死而复生之Gstreamer推流

推荐阅读更多精彩内容