前言
近期一点C++基础都没有的我被莫名其妙调去做基于OWT Native Client的二次开发,遇到了一些奇奇怪怪的问题,搜寻的时候发现不管是中文网络还是其他社群下的经验分享和issues大多是基于Chromium WebRTC,而对OWT的讨论相对较少(原生Linux Client就更甚)。遇到问题的时候有时候可以从WebRTC相关的文章中找到思路(毕竟OWT也不过是一层Wrapper),但也有时候没有那么简单,因此把遇到的几个有代表性的问题罗列在下面,有些找到了解决方法,但是大多数还没有找到合适的work-around,如果后面的业务解决了这方面问题,会尝试更新在本文。
这里格外说明下我们的业务场景,大概涉及在Linux Client下调用自定义解码器、通过自定义的Audio\Video设备输出数据到Socket,而且客户要求的运行环境是CentOS 7.5 with(glibc 2.17)
另外,这篇博文所基于的OWT Native的CodeBase大约为 2022.10月,当时仓库没有新提交已经月余,但是在随后的3个月中,突然有了巨量的提交,甚至包括将Chromium由M86换成M102这种Breaking Changes,大概是年底冲业绩所以把内部私有Repo的提交同步出来了。因此本文的内容可能不再适用,请读者自行鉴别。
编译流程
OWT在编译的时候会拉下来Chromium WebRTC组件的Codebase,具体的流程无非是挂代理,缺啥依赖补啥依赖,注意同时安装Py2和Py3(存疑,我在10月拉下来的Codebase不需要多少Py2依赖,但是1月份更新之后发现至少需要在Py2环境下安装较新版本SetupTools,Chromium是可以自行甄别runtime是Py2还是Py3并执行相关命令,大概是OWT搞坏了),以下是几个小Tips:
- 拉取的时候可以 在g
client sync
命令加入--no-history,
不拉取Git的历史来减少下载量。但是可能会导致日后更新的困难。 关于编译时加入的参数问题。(仅针对Linux编译的情形)
—-gn_gen
可以在输出目录重新生成args.gn
文件,里面包含一些针对Chromium WebRTC的参数配置,注意修改之后需要编译时不加--gn_gen
才能起效(要不然就被重新覆盖了)—-tests
决定是否包含Chromium项目中的测试模块,在10月份的版本中,加入会导致编译失败,但是后来OWT也加入了一些依赖于测试模块的End2End的功能测试,因此成了必须的依赖项。(没错,在此之前OWT连个靠谱的功能测试都没有)—-fake_audio
在文档里表明可以去除对PulseAudio的依赖,但是经过我的测试,似乎没有用,可能出现编译不出来或者引发ADM=0
的断言,猜测可能是没有实现。Update: 关于剥离依赖的问题已经解决,见下文.
—-scheme
可以指定编译Release或Debug版本,Debug版本会附带调试符号,但是会输出巨量的网络流量日志,可以在args.gn
中写入rtc_disable_logging=true
干掉所有日志,或者在logging.h
里手动修改干掉Debug日志。- 注意下编译环境的版本,我在10月份的时候拉取的Codebase大概还是C17的标准,12月就已经突飞猛进到C20了,太过古老的运行和编译环境都可能出现奇奇怪怪的问题,大概要求gcc在7(但是CentOS SCL中的GCC7的版本会精准命中某些Bug,因此最好用8编译)或10(C++20的标准)
开发流程
整个代码库太大了,因此我干脆用VSCode Remote
功能远程到服务器上查看代码,得益于Chromium优秀的生态,整个Flow还是很流畅的。
- Clangd LSP: 在VSCode中,我用了Clangd来支持C++语言功能,众所周知Clangd要想正常工作,必须有针对项目生成的
compile_commands.json
.可以通过如下命令生成:
gn gen out/Default --args='is_clang=false' #指定编译args,可在args.gn查看
tools/clang/scripts/generate_compdb.py -p out/Default > compile_commands.json #输出compile_commands.json
- gdb: WebRTC项目自带了一份GDBinit配置,位于
tools/gdb/gdbinit
,在自己的.gdbinit文件中source即可。 - 有时候Clangd会提示
too many errors
,可能是由于版本兼容性问题导致加入了不支持的clang arg,在项目根目录中创建如下内容的.clangd
配置,去掉clang的最大错误限制.
CompileFlags:
Add: -ferror-limit=0
几点问题
Observer OnStreamAdded 事件触发时机问题
项目中使用了AttachAudioDevice/AttachVideoRenderer的方式来将我们自定义的模块附到Sink链路上,从而允许我们从旁路
获取音视频流的内容.遵循OWT官方提供的Demo中的实践,我们在StreamObservor中的OnStreamAdded
回调中执行Attach,但是在测试中我们发现:很多时候AttachAudio执行成功,但AttachVideo会报错称"流中不包含VideoTrack".翻了一遍代码+调试之后发现:由于视频和音频是分轨的且音频的编码简单,因此音频轨先到达,Chromium WebRTC为其创建RemoteStream并触发OnStreamAdded
回调,但是视频轨的到达时间较晚,因此在OnStreamAdded
回调中AttachVideo时,RemoteStream中还没有VideoTrack,因此AttachVideo失败,而当视频轨到达时,WebRTC不会再次触发OnStreamAdded
回调,而是修改之前的RemoteStream流对象,并触发OnTrack
和OnAddTrack
回调,而OWT并没有将这两个回调暴露到上层.这里我们修改了OWT的库代码,依次按照OWTOnStreamAdded
回调的逻辑补全OnTrack回调,最后在OnTrack中重新Attach即可.OnTrack
的回调和OnStreamAdded
的参数类型不同,但是WebRTC接口的注释中特别说明了如何获取Stream对象.(这里我怀疑是由于WebRTC版本更迭导致,因此在WebRTC相关Observor接口中OnTrack并不是纯虚函数,因此可能底层WebRTC Observor逻辑更迭,但是OWT没有更新,也没有做E2E的Test,因此没有发现这个问题.)
CustomDecoder解码问题
另一个由于OWT功能实现不全导致的坑.如果你尝试去实现解码器的逻辑,会惊奇的发现,只有获取EncodedFrame的接口,没有返回DecodedImage的接口.打开 talk/owt/sdk/base/customizedvideodecoderproxy.cc
文件,你会看到如下眼前一黑的注释,大意就是说 没有做这个功能,鸽了
.
// TODO(chunbo): Fetch VideoFrame from the result of the decoder
// Obtain the |video_frame| containing the decoded image.
// decoded_image_callback_->Decoded(video_frame);
因此唯一选择就是自己去把这个功能补上去,我们自己修改了CustomDecoderInterface,加入了一个注册解码完成回调的方法,并在解码完成之后调用这个回调返回解码后的数据.但是因为OWT自己又包了一层数据结构,callback中的参数类型webrtc::VideoFrame
是没有暴露给上层的,这就要发挥自己的才智了...
我们另外自定义了一个类似的数据结构作为回调参数,用lambda函数把下层提供的回调函数包装了一层再抛给上层注册,不是一个很优雅的解决方案,只能是期待Intel更新了.
剥离PulseAudio依赖
又一个OWT没实现的坑.
简单来讲,见talk/owt/sdk/base/peerconnectiondependencyfactory.cc
.如果你没有在全局设置中创建CustomizedAudioFrameGenerator,那adm
变量就是nullptr
,不会被初始化.我们的业务场景不牵扯到虚拟音频输入的情况,因此随便写了个FrameGenerator塞了进去.
if (GlobalConfiguration::GetCustomizedAudioInputEnabled()) {
// Create ADM on worker thred as RegisterAudioCallback is invoked there.
adm = worker_thread->Invoke<rtc::scoped_refptr<AudioDeviceModule>>(
RTC_FROM_HERE,
Bind(&PeerConnectionDependencyFactory::
CreateCustomizedAudioDeviceModuleOnCurrentThread,
this));
} else {
}
但是,要注意到WebRTC的音频流的驱动方式和视频流是完全不同的,音频数据包在收到后会暂存在NetEQ缓冲区中,而下一步的流动是由输出侧的音频设备的播放线程向AudioBuffer请求数据,然后NetEQ的数据包才会进行解码等一系列操作最终通过Buffer流入播放线程.而OWT的 CustomAudioDevice
没有实现这一请求部分,因此数据包完全不会流动.这一块也需要自己补全.我们的业务不需要对接下层的真实音频驱动,我就干脆写了个while循环,每10ms request 一次数据,数据的获取靠之前旁路的AudioPlayer来完成.
一点小吐槽
技术选型中什么前提下应当考虑OWT?什么时候都不应当!
(我承认Star数目不能代表项目的质量,但是OWT Server项目不到1K的Star比起 Pion(Implemented in Go,10.5K Stars)或者WebRTC-RS(2.6K Stars)多少有点寒碜,更别提Issue区不到50%的解决率了\摊手)
除非:
- 你不想做啥修改,就想要一个iOS/Android/Web 都几乎开箱即用,包含硬件解码的WebRTC App.
- 你喜欢(服务端的)JS的开发环境
- 你爱死(在客户端)写C++了,而且尤其热爱Chromium团队想出来的那一套独树一帜的构建和依赖系统,非常享受在浩如烟海的Codebase里翻找宝藏的感觉。