HEXA开发日志目录
上一篇 HEXA娱乐开发日志技术点002——下位机成功获取弹幕
成果
目前,我手中的HEXA机器人可以用RTMP协议推流flv文件了,即用flv作为视频源做直播,并且此功能已在奥点云和B站试验成功。如果要推实时视频,还需要一些后续工作,这次只说flv文件推流的事。
源码版本812171d9eb56768c18d525b6e3c61b1ae7c315ee
目前的控制端网页长这样,在推流测试的地址中填入rtmp服务器地址后,点击“推”即可启动demo,并开始推流。
这是目前这个skill的目录结构,本次主要添加了一个rtmp.go和deps目录下的一堆依赖文件。
DanmuDriveMe
├── manifest.json
├── remote
│ └── index.html==>控制端网页
├── robot
│ ├── assets
│ │ └── test.flv==>用于测试的flv文件
│ ├── deps
│ │ ├── include==>C文件,包括librtmp库头文件和我对librtmp库的封装
│ │ │ ├── amf.h
│ │ │ ├── log.h
│ │ │ ├── rtmp.h
│ │ │ └── rtmp_sample_api.c==>我对librtmp库的简单封装
│ │ └── lib==>依赖库,包括librtmp所依赖的openssl、zlib库
│ │ ├── libcrypto.so -> libcrypto.so.1.0.0
│ │ ├── libcrypto.so.1.0.0
│ │ ├── librtmp.so -> librtmp.so.1
│ │ ├── librtmp.so.1
│ │ ├── libssl.so -> libssl.so.1.0.0
│ │ ├── libssl.so.1.0.0
│ │ ├── libz.so -> libz.so.1
│ │ ├── libz.so.1 -> libz.so.1.2.11
│ │ └── libz.so.1.2.11
│ └── src==>go代码
│ ├── Danmu.go==>弹幕相关
│ ├── DanmuDriveMe.go
│ └── rtmp.go==>推流相关
从文件角度,目前的依赖或调用关系大概是这样
index.html ==> DanmuDriveMe.go ==> rtmp.go ==> rtmp_sample_api.c ==> librtmp.so ==> libssl.so+libcrypto.so+libz.so
How To
在做之前,先理清一下关于步骤的逻辑。
- 要先在电脑上跑通
推流要使用开源C工程rtmpdump中的librtmp库,在不熟悉它的情况下,最好先在电脑上demo成功,同时在此过程中了解rtmpdump的使用。 - 要封装librtmp库
封装是为了方便给Go程序调用,如果不封装的话,就要在Go程序中使用很多复杂的C数据类型,这会带来很多麻烦,因此要对librtmp库进行简单封装以保持与Go对接的C接口和数据类型都足够简单。 - 用Go程序测试这个封装
这点不必多说,前面的简单封装就是为了方便Go程序调用的。 - 本次最终目标——下位机推流
这里有个大前提,即这是一个嵌入式平台,所以不管搞什么,都要针对平台来做,所有C库,都必须交叉编译才能用。包括librtmp库和它所依赖的库,统统要交叉编译。
前3步的工作体现在我的rtmpdump工程源码的以下文件中
- demo.c(要看这个文件的前一个版本,此版本已经使用封装的接口了)是第1步的成果,参考的是雷霄骅,张晖的例子源码
- rtmp_sample_api.c和rtmp_sample_api.h是第2步的工作成果
- demo.go是第3步的工作成果,使用Go程序调用了第2步的封装接口
在上位机demo rtmpdump
这项内容在网上资料很多,这里只讲一些值的一说的点。
- 雷霄骅前辈引用的是librtmp/rtmp_sys.h,至少在我使用的rtmpdump版本中,这不是一个public头文件,使用自己编的库还好,要是使用安装到系统的librtmp库,会找不到这个头文件。
- librtmp提供了两个推流API,它们分别是RTMP_Write和RTMP_SendPacket,雷前辈的demo中都有展示,我选择了RTMP_Write的方式,因为我的参考依据是OBS Studio的方式,参考一个成熟的开源应用会靠谱一点,并且如果出了什么问题相当于有个官方可以参考,可以借助OBS开发者们的智慧,这也是开源软件的魅力。
- 编译问题要仔细读rtmpdump的README,针对自己平台指定make参数,不要上来就一个make。
上位机封装与Go程序
如果熟悉了librtmp库的使用,封装就很简单,这里就不多说了,只说一些稍微值得一提的点吧。
- 关于CGO
据我所知,Go语言调用C至少有两个工具可用,它们分别是CGO和SWIG,SWIG是mind SDK官方例子中OpenCVSkill中的opencv采用的封装方法,具体不太了解。
我用的是CGO,用法也很简单,和gcc基本编译的那些知识很容易对应上。下面是一个典型的Go代码文件结构,如果经常用gcc的话,应该很容易理解下面注释的意思。
package <包名>
/*
#cgo LDFLAGS: ld参数,比如-L...和-l...之类的,和Makefile常见的写法一样的
#cgo CFLAGS: gcc参数,比如-I...之类的,也合Makefile常见写法一样的
//此处写C代码完全是C语法
void c_fun(){}
*///此处和下一句import "C"之间不可以有空行
import "C"
import(
<其他包>
...
)
func go_func_name() {
C.c_fun() //调用C函数
}
- 数据类型转换
网上有很多对Go和C基本数据类型转换的文章,我不想展开讨论。我这里就做个记录,下面这段[]byte转char *的代码写得让我不舒服,希望将来可以优化。
//buf 是[]byte类型
cc := C.CString(string(buf))
//rtmp_sample_add_data第一个参数要char *类型
C.rtmp_sample_add_data(cc, C.int(datalength)+15)
C.free(unsafe.Pointer(cc))
下位机推流
有了上位机的demo之后,下位机代码就很好写了,所以这个步骤的难点不在于代码,而在于如何交叉编译动态链接库并正确引用他们。
如何建立交叉编译环境?
如果想效仿官方做法,一个方法是读mind SDK的源码,我这么做得到了以下认知:
- 官方使用了Docker
这当然是废话,因为一开始就要求开发者安装了Docker,关键是怎么使用。我对它不是很了解,我只知道它可以帮我建立类似虚拟机的环境。当然,在自己平台上建立一个交叉编译环境也不是不可能,只是相当麻烦而已。
官方的mind SDK的mind x
命令实际上就是运行了一个Docker容器(类似虚拟机),并在这个容器环境里执行mind x
后面跟的命令。如果可以用mind x搞定很多事,但是也有一些不便之处。总之,经过研究,我误入歧途地得到了构造并进入一个官方Docker容器的命令,即
docker run -it -v <主机绝对路径>:/go/src/skill vincross/mindcli bash
上述命令中-v
后面跟的分别是主机(电脑)和容器(类似虚拟机)的路径,-v
实际上类似于mount命令,这样一来,在容器中,我们可以直接对主机文件进行操作;vincross/mindcli
大概是容器的类型,这里指定使用官方容器类型;bash
应该是在容器中运行的程序,意思应该是要启动bash shell。 - 交叉编译器
我绕了些弯路,终于在mind-sdk/xcompile/Dockerfile中看到了这一句ENV CROSS arm-linux-gnueabihf
,虽然我不了解Dockerfile的机制,但是猜得出这句话的意思,然后就好办了,在Docker容器中安装它就行了,安装命令是
apt-get install gcc-arm-linux-gnueabihf
要交叉编译啥?
当然是rtmpdump啦,先来个make SYS=posix
尝尝,立刻发现<openssl/ssl.h>
找不到,搞定openssl之后,再来,会发现<zlib.h>
找不到。如果先读了README就明白了,rtmpdump依赖openssl和zlib库,所以我得把rtmpdump
、openssl
和zlib
都交叉编译成动态库。
zlib编译
下载源码,然后执行
CC=arm-linux-gnueabihf-gcc ./configure --prefix=<安装目录的绝对路径>
make
make install
openssl编译
下载源码,下载后要注意两点
- 选择版本,很可能由于API变动,最新版不和rtmpdump不能匹配,所以要预先知道版本,切换到那个版本的tag上再编译。一个确定版本的方法是,自己的上位机openssl版本或者Docker容器中的openssl版本,因为在前面的第一个步骤中,已经证明了系统中的openssl是可以和rtmpdump匹配的,用它准没错。
- 如果build了错误版本,再build rtmpdump时发现一些奇怪的问题,要换openssl版本的话,一定要先删除.gitignore之后再
git clean -fd
,因为有些ignore的文件是会影响编译的。
我在这吃了一些亏,最后发现了openssl API变动的修改,原本公开的结构体不再公开了,那么rtmpdump中使用这个结构体的地方就编不过了。
确定好版本后,编译命令如下:
AR="arm-linux-gnueabihf-ar" RANLIB=arm-linux-gnueabihf-ranlib CC=arm-linux-gnueabihf-gcc /usr/bin/perl ./Configure shared linux-armv4 --prefix=<安装目录的绝对路径>
make
make install
rtmpdump编译
下载源码,完整的make命令我先不给出来,因为要引用自己编的openssl和zlib库,makefile和make命令改得有点乱,一是目前处于demo阶段,二是预计很长时间内,我没有必要再交叉编译librtmp库,所以这里偷个懒,只是强调一下,认真读README,都能搞定的!
导入库到skill
这里看一下rtmp.go中开头部分对CGO的配置,再结合库文件和C文件的位置就明白了。
一个特别的地方是#include "../deps/include/rtmp_sample_api.c"
这一句,这里直接弃用了.h文件,也只是偷个懒,因为在前面混乱的研究中,只引用头文件的话,好像会找不到.c文件,索性就直接引用.c文件了,也可能只是个误会,不过我最近感冒,暂时懒得研究了。
总结和计划
- 上次说的“调通OpenCV,来验证C/C++库的调用”太简单了,没什么好讲的
- 完成了rtmp推流的上位机和下位机的demo
- 交叉编译和Go调用C的套路基本清楚了
- 官方framework中,可以分别获得视频的YCbCr raw data和音频的raw data,这样的数据不能直接交给librtmp库推流,需要经过压缩编码,所以下一步要研究的是如何用ffmpeg的库将raw data压缩成flv格式的包,并交给librtmp库推流
- 还有些事情可以开始种草了,包括弹幕命令集、机器人命名等