解决方案:微信小游戏直播在Android端的跨进程渲染推流实践
优采云 发布时间: 2022-11-24 13:40解决方案:微信小游戏直播在Android端的跨进程渲染推流实践
本文由微信开发团队工程师“virwu”分享。
1 简介
近期,微信小游戏支持一键播放视频号。将微信升级到最新版本后,打开腾讯小游戏(如跳转、欢乐斗地主等),在右上角的菜单中可以看到开始直播的按钮。按钮成为游戏主播(如下图所示)。
但出于性能、安全等一系列考虑,微信小游戏运行在独立进程中,视频号直播相关的模块不会在该环境下初始化。这意味着小游戏的音视频数据必须跨进程传输到主进程进行流式传输,这给我们实现小游戏的直播带来了一系列的挑战。
(本文同步发表于:)
2.系列文章
本文是系列文章中的第 5 篇:
《系统聊天技术(一):美拍直播弹幕系统的实时推送技术实践之路》 《系统聊天技术(二):阿里电商IM消息平台,在群聊和直播场景下的技术实践直播系统聊天技术(三):微信直播*敏*感*词*单间1500万在线消息的消息架构演进》 《直播系统聊天技术(四):微信直播*敏*感*词*实时消息系统架构演进实践》百度直播海量用户》《直播系统聊天技术(五):微信小游戏直播在Android端的跨进程渲染与推流实践》(*篇)3.视频采集与推流
3.1 屏幕截图?
小游戏直播的本质是将主播手机屏幕上的内容展示给观众。自然而然,我们可以想到利用系统的录屏接口MediaProjection来采集视频数据。
该方案具有以下优点:
但最终这个方案被否决了,主要是出于以下考虑:
转念一想,既然小游戏的渲染完全由我们来控制,那么为了更好的直播体验,是否可以将小游戏渲染的内容跨进程传输到主进程进行推流呢?
3.2 小游戏渲染架构
为了更好的描述我们采用的方案,这里简单介绍一下小游戏的渲染架构:
可以看到图中左半部分代表前台的小游戏流程,其中MagicBrush是小游戏渲染引擎,从小游戏代码接收渲染指令,将画面渲染到由小游戏提供的Surface上屏幕上的 SurfaceView。主进程在后台不参与整个过程。
3.3 小游戏录屏时的情况
小游戏之前已经支持录制游戏内容,原理上和直播类似,都需要获取当前小游戏的画面内容。
开启录屏后,小游戏会切换到以下渲染模式:
可以看出MagicBrush的输出目标不再是屏幕上的SurfaceView,而是Renderer生成的SurfaceTexture。
这里简单介绍一下Renderer的作用:
Renderer是一个独立的渲染模块,代表了一个独立的GL环境。它可以创建SurfaceTexture作为输入,接收到SurfaceTexture的onFrameAvailable回调后,通过updateTexImage方法将图像数据转换为GL_TEXTURE_EXTERNAL_OES类型的纹理参与后续的渲染过程,并可以将渲染结果输出到另一个Surface。
图中的流程一步步解释如下:
1)MagicBrush接收到小游戏代码调用的渲染指令,将小游戏的内容渲染到第一个Renderer创建的SurfaceTexture上;
2)然后这个Renderer做了两件事:
3)第二个Renderer将第一个Renderer提供的纹理渲染到mp4编码器提供的输入SurfaceTexture中,最终编码器编码生成mp4录屏文件。
3.4 改造录屏解决方案?
可以看出,在录屏方案中,一个Renderer负责将游戏内容上传到屏幕上,另一个Renderer将相同的纹理渲染到编码器,用于录制游戏内容。直播其实也差不多。是不是只要把编码器换成直播的推流模块就够了?
没错,但是还漏掉了一个关键环节:streaming模块运行在主进程中,我们需要跨进程传递图片数据!如何跨进程?
说到跨进程: 可能我们脑海中跳出的第一反应就是Binder、Socket、共享内存等传统的IPC通信方式。但是仔细想想,系统提供的SurfaceView是一个非常特殊的View组件。它不通过传统的View树参与绘制,而是通过系统的SurfaceFlinger直接合成到屏幕上,SurfaceFlinger运行在系统进程上。我们在SurfaceView提供的Surface上绘制的内容必须能够跨进程传递,而Surface跨进程的方法很简单——它本身实现了Parcelable接口,也就是说我们可以使用Binder直接传递Surface对象跨进程。
所以我们有以下初步计划:
可以看出,第三步不再渲染到mp4编码器,而是渲染到从主进程跨进程传过来的Surface。主进程的Surface由一个Renderer创建的SurfaceTexture封装而成。现在小游戏进程将图像渲染到这个 Surface 作为生产者。当一帧渲染完成后,主进程的SurfaceTexture会收到一个onFrameAvailable回调通知图像数据已经准备好,然后通过updateTexImage获取对应的纹理数据。这里,由于直播模块只支持GL_TEXTURE_2D类型的贴图,所以这里主进程Renderer会将GL_TEXTURE_EXTERNAL_OES转换成GL_TEXTURE_2D贴图,然后发送给直播编码器,完成推流过程。
经过一番改造:上述方案已经成功将小游戏渲染到屏幕上,并传递给主进程进行流式传输,但这真的是最优方案吗?
想了想,发现上面的解决方案中Renderer太多了。小游戏进程中有两个,一个是渲染到屏幕上,一个是跨进程渲染到Surface,还有一个是在主进程中转换纹理调用流模块。如果要同时支持录屏,需要在小游戏进程中再启动一个Renderer渲染到mp4编码器。过多的Renderer意味着过多的额外渲染开销,会影响小游戏的性能。
3.5 跨进程渲染方案
" />
纵观整个流程,其实只需要主进程的Renderer。小游戏额外使用的Render无非是为了同时满足同屏渲染和跨进程传输。constraints,那么我们就简单的把小游戏进程的入屏Surface传给主进程进行入屏渲染!
最终,我们将小游戏流程中的两个冗余Renderer大幅砍掉。MagicBrush直接渲染到跨进程传递给Surface,而主进程的Renderer负责将纹理渲染到跨进程以及纹理类型转换。传输的小游戏进程在屏幕Surface上,实现画面在屏幕上的渲染。
最终需要的Renderer数量从原来的3个减少到需要的1个,性能提升的同时架构更加清晰。
以后需要同时支持录屏的时候,稍微改动一下,把mp4编码器的输入SurfaceTexture跨进程传给主进程,然后给它添加一个Renderer渲染纹理(如图下图)。
3.6 兼容性和性能
说到这里,不禁有些担心。Surface方案的跨进程传输和渲染会不会有兼容性问题?
其实虽然不常见,但是官方文档上说是可以跨进程抽取的:
SurfaceView 结合了表面和视图。SurfaceView 的视图组件由 SurfaceFlinger(而不是应用程序)合成,支持从单独的线程/进程进行渲染,并与应用程序 UI 渲染隔离。
而Chrome和Android O之后的系统WebView都有使用跨进程渲染的方案。
我们的兼容性测试覆盖了Android 5.1及之后的所有主流系统版本和机型。除了Android 5.flow跨进程渲染黑屏的问题。
性能方面:我们使用WebGL Aquarium的demo进行性能测试。我们可以看到对平均帧率的影响约为15%。由于渲染和流式处理,主进程的 CPU 有所增加。奇怪的是,小游戏进程的CPU开销却减少了。减少的原因尚未得到证实。疑似与屏幕操作移至主进程有关,不排除统计方法的影响。
3.7 总结
为了不记录宿主端的评论插件,我们先从小游戏的渲染流程说起。借助Surface的跨进程渲染和传输图像的能力,我们将渲染和上传小游戏的过程移到了主进程中,同时生成纹理进行流式传输。兼容性和性能满足要求。
4. 音频采集和流式传输
4.1 方案选择
在音频采集方案中,我们注意到Android 10及以上系统提供了AudioPlaybackCapture方案,可以让我们在一定范围内采集系统音频。当时的一些预研结论如下。
捕获者 - 捕获发生的条件:
被俘方 - 可以被俘的条件:
总的来说:Android 10及以上可以使用AudioPlaybackCapture方案进行音频采集,但考虑到Android 10的系统版本限制太多,我们最终选择了自己对小游戏播放的所有音频进行采集和混音。
4.2 跨进程音频数据传输
现在,老问题又一次出现在我们面前:小游戏中混入的音频数据在小游戏进程中,我们需要将数据传输到主进程中进行流式处理。
它不同于一般的用于方法调用的IPC跨进程通信:在这种场景下,我们需要频繁(每40毫秒)传输大数据块(16毫秒的数据量约为8k)。
同时:由于直播的特点,这个跨进程传输过程的延迟需要尽可能低,否则会导致音视频不同步。
为了实现上述目标:我们测试了Binder、LocalSocket、MMKV、SharedMemory、Pipe等几种IPC方案。在搭建的测试环境中,我们模拟小游戏过程中真实的音频传输过程,每16毫秒发送一次序列化数据对象。数据对象的大小分为三个级别:3k/4M/10M,发送前存储时间戳在对象中;以主进程接收到数据并反序列化成数据对象的时刻为结束时间,计算传输延迟。
最终得到如下结果:
注:其中XIPCInvoker(Binder)和MMKV传输大量数据耗时过长,结果不显示。
各方案分析如下(stall rate表示延迟>2倍平均延迟且>10毫秒的数据占总数的比例):
可以看出,LocalSocket方案在各个情况下的传输延时表现都非常好。造成差异的主要原因是裸二进制数据在跨进程传输到主进程后,仍然需要进行数据复制操作,将其反序列化为数据对象。在使用LocalSocket时,可以使用ObjectStream和Serializeable来实现流式复制。相比其他方案,一次性接收数据,然后复制,节省了很多时间(当然其他方案也可以设计成分块流,同时复制,但是实现起来有一定的成本,它不像 ObjectStream 那样稳定和易于使用)。
我们还测试了LocalSocket的兼容性和性能,没有出现传输失败和断开连接的情况。只有三星S6的平均延迟超过了10毫秒,其他机型的延迟都在1毫秒左右,可以达到我们的预期。
4.3 LocalSocket 的安全性
常用的Binder的跨进程安全是由系统实现的认证机制来保证的。LocalSocket是对Unix域套接字的封装,所以要考虑它的安全性。
《Android Unix Domain Sockets的误用及安全隐患》一文详细分析了在Android中使用LocalSocket带来的安全风险。
PS:论文原文附件下载(请从本链接4.3节下载:)
论文概要:由于LocalSocket本身缺乏认证机制,任何应用程序都可以连接,从而拦截数据或向接收端发送非法数据而导致异常。
针对这个特点,我们可以做两种防御方式:
4.4 总结
为了兼容Android 10以下机型和直播,我们选择自己处理小游戏的音频采集,通过对比评测,我们选择了LocalSocket作为跨进程音频数据传输的方案,满足了直播在时延方面的需求。
同时,通过一些对策,可以有效规避LocalSocket的安全风险。
" />
5.多进程带来的问题
回过头来看,虽然整个解决方案看起来比较顺利,但是由于多进程的原因,在实现过程中还是有很多坑。以下两个是主要的。
5.1 glFinish导致渲染帧率严重下降
在实现跨进程渲染流解决方案后,我们进行了一轮性能和兼容性测试。在测试过程中,我们发现部分中低端机型的帧率下降非常严重(如下图)。
重现后查看小游戏进程渲染的帧率(即小游戏进程绘制到跨进程Surface的帧率),发现可以达到直播时的帧率未启用广播。
我们使用的测试软件PerfDog记录了屏幕Surface的帧率,可以看出性能下降不是因为小游戏代码执行效率低导致直播开销过高,而是主进程效率低下造成的在屏幕渲染器上。
于是我们剖析了直播时主进程的运行效率,发现耗时函数是glFinish。
并且有两个电话:
如果去掉第一个调用,这次Live SDK里面的时间会超过100毫秒。
要理解为什么这个 GL 命令需要这么长时间,让我们看一下它的描述:
直到所有先前调用的 GL 命令的效果完成后,glFinish 才会返回。
描述很简单:它会阻塞,直到所有先前调用的 GL 指令都完成。
所以好像之前的GL指令太多了?但是,GL指令队列是线程隔离的。在主进程的Renderer线程中,在glFinish之前只会执行极少量的纹理类型转换的GL指令。从腾讯云的同学那里了解到,本帖不会使用推流接口。线程执行了很多GL指令,这么少的GL指令怎么会让glFinish阻塞这么久?等等,很多 GL 指令?小游戏进程此时不是在执行大量的GL指令吗?会不会是小游戏进程中GL指令较多导致主进程的glFinsih耗时过长?
这样的猜测不无道理:虽然GL指令队列是线程隔离的,但是处理指令的GPU只有一个。一个进程中过多的 GL 指令会导致另一个进程在需要 glFinish 时阻塞时间过长。谷歌没有找到相关的描述,需要自己验证一下这个猜测。
重*敏*感*词*上面的测试数据:发现直播能到60帧的时候,直播结束后能到60帧左右。这是否意味着在小游戏GPU负载低的情况下,glFinish的耗时也会减少呢??
在性能下降严重的模型上:保持其他变量不变,尝试低负载运行一个小游戏,发现glFinsih的耗时成功下降到10毫秒左右,证实了上面的猜测——确实是小游戏正在执行的游戏进程大量的GL指令阻塞了主进程glFinish的执行。
如何解决?小游戏进程的高负载是改不了的,那小游戏可不可以在一帧渲染完就停止,等主进程的glFinish完成再渲染下一帧呢?
这里做了各种尝试:OpenGL的glFence同步机制不能跨进程使用;由于GL指令是异步执行的,所以通过跨进程通信锁定小游戏的GL线程,并不能保证主进程执行glFinish指令时小游戏进程的进度已经执行完毕,这只能通过以下方式来保证在小游戏进程中加入glFinish,但这会使双缓冲机制失效,导致小游戏渲染帧率大幅下降。
既然避免不了glFinish带来的阻塞,那我们回到问题的开头:为什么需要glFinish?由于双缓冲机制的存在,一般来说不需要glFinish等待前面的绘制完成,否则双缓冲就失去了意义。在两次glFinish调用中,可以直接去掉第一个纹理处理调用。经过沟通,发现引入了第二次腾讯云SDK调用,解决了一个历史问题,可以尝试去掉。在腾讯云同学的帮助下,去掉glFinish后,渲染帧率终于和小游戏的输出帧率一致了。经过兼容性和性能测试,没有发现去掉glFinish导致的问题。
这个问题的最终解决方案很简单:但是在分析问题原因的过程中其实做了很多实验。同一个应用中一个GPU负载高的进程会影响到另一个进程耗时的glFinish这种场景确实是非常困难的。很少见,参考资料也不多。这个过程也让我深刻体会到glFinish使双缓冲机制失效带来的性能影响是巨大的。在使用OpenGL进行渲染和绘图时,我应该非常谨慎地使用glFinish。
5.2 后台进程优先级问题
测试过程中:我们发现无论使用多少帧率向直播SDK发送图片,观众看到的图片帧率始终只有16帧左右。排除背景原因后,我们发现编码器的帧率不够。经腾讯云同学测试,同一进程编码帧率可以达到设定的30帧,所以还是多进程导致的问题。在这里,编码是一个很重的操作,比较消耗CPU资源,所以我们首先怀疑的是后台进程的优先级。
要确认问题:
总结一下:可以确认帧率下降是后台进程(以及它拥有的线程)的优先级低导致的。
在微信中提高线程优先级是很常见的。比如小程序的JS线程和小游戏的渲染线程,在运行时都会通过android.os.Process.setThreadPriority方法设置线程的优先级。腾讯云SDK的同学很快就给我们提供了设置线程优先级的接口,但是实际运行的时候发现编码帧率只从16帧增加到18帧左右。什么地方出了错?
前面提到:我们通过chrt命令设置线程优先级是有效的,但是android.os.Process.setThreadPriority方法设置的线程优先级对应的是renice命令设置的nice值。仔细看了chrt手册,发现之前测试的理解是错误的。我使用命令chrt -p [pid] [priority]直接设置了优先级,但是没有设置调度策略参数,导致线程的调度策略从Linux默认的SCHED_OTHER改为SCHED_RR,即该命令的默认设置,而SCHED_RR是一种“实时策略”,导致线程的调度优先级变得很高。
其实:renice设置的线程优先级(即android.os.Process.setThreadPriority)对于后台进程拥有的线程帮助不大。
事实上,已经有人对此进行了解释:
为了解决这个问题,Android 还以一种简单的方式使用 Linux cgroups 来创建更严格的前台与后台调度。前台/默认 cgroup 允许正常进行线程调度。然而,后台 cgroup 只应用了总 CPU 时间的一小部分的限制,该时间可用于该 cgroup 中的所有线程。因此,如果该百分比为 5%,并且您有 10 个后台线程和一个前台线程都想运行,那么这 10 个后台线程加在一起最多只能占用前台可用 CPU 周期的 5%。(当然,如果没有前台线程想要运行,后台线程可以使用所有可用的 CPU 周期。)
关于线程优先级的设置,有兴趣的同学可以看看另一位大佬的文章:《Android的诡异陷阱——设置线程优先级导致的微信卡顿悲剧》。
最后:为了提高编码帧率,防止后台主进程被kill掉,我们最终决定在直播时在主进程中创建一个前台Service。
六、总结与展望
多进程是一把双刃剑。在给我们带来隔离和性能优势的同时,也给我们带来了跨进程通信的问题。幸运的是,借助系统Surface能力和各种跨进程解决方案,我们可以更好地解决小游戏直播中遇到的问题。
当然:解决跨进程问题最好的办法就是避免跨进程。我们也考虑过在小游戏的过程中运行视频号直播的推流模块的方案,但是考虑到改造成本,我们没有选择这个方案。
同时:本次SurfaceView跨进程渲染的实践对其他业务也有一定的参考价值——对于一些内存压力大或者安全风险高的场景,需要SurfaceView渲染绘制,逻辑可以放在一个独立的进程,然后通过跨进程渲染的方式绘制到主进程的View上,在获得独立进程优势的同时,也避免了进程间跳转带来的体验碎片化。
*敏*感*词*搭建一个简单的直播系统》《淘宝直播技术干货:高清低延迟实时视频直播技术解密》《技术干货:实时视频直播首屏优化实践》 400ms》《新浪微博技术分享:微博实时直播百万级高并发架构实践》《视频直播实时混音技术原理与实践总结》《七牛云技术分享:使用QUIC协议》实现0卡顿的实时视频直播!》《近期火爆的实时直播问答系统实现思路与技术难点分享》》微信朋友圈千亿访问量背后的技术挑战与实践总结》《微信团队分享:微信移动端全文搜索多拼音字符问题解决方案》《微信团队分享:高性能通用iOS版微信key-value组件技术实践》》微信团队分享:iOS版微信如何防止特殊字符导致爆群,APP闪退?》 《微信团队原创分享:iOS版微信的内存监控系统技术实践》 《iOS后台唤醒实践:微信收到和到达语音提醒技术总结》 《微信团队分享:视频图像的超分辨率技术原理及应用场景》 》》微信团队分享:微信团队分享:iOS版微信高性能通用键值组件技术实践》 《微信团队分享:iOS版微信如何防止特殊字符导致的爆群和APP闪退?》 《微信团队原创分享:iOS版微信的内存监控系统技术实践》 《iOS后台唤醒实践:微信收到和到达语音提醒技术总结》 《微信团队分享:视频图像的超分辨率技术原理及应用场景》 》 《微信团队分享:解密微信每天几亿次实时音视频聊天背后的技术》 《微信团队分享:微信安卓版小视频编*敏*感*词*填补的那些坑》
. . . 《来龙去脉》《月活8.89亿的超级IM微信如何做Android端兼容性测试》《一文搞定微信开源移动数据库组件WCDB!》《技术专访微信客户端团队负责人:如何启动客户端性能监控与优化》《基于时序海量数据冷热分层架构的微信后台设计实践》《微信团队原创分享:Android版微信臃肿难点与解决方案》 《模块化实践之路》《微信后台团队:异步消息队列优化升级微信后台分享》《微信团队原创分享:微信客户端SQLite数据库损坏修复实践》《微信Mars:微信内部使用的网络层封装库,即将开源》《如约而至:移动端微信自用End-IM网络层跨平台组件库Mars》已正式开源》 《开源libco库:支撑8亿微信用户单机千万级连接的后台框架基石【源码下载】》 《微信新一代通信安全解决方案:基于在 TLS1 上。《微信团队分享:极致优化,iOS版微信编译速度提升3倍实践总结》《IM“扫一扫”》功能容易做吗?看微信“扫一扫识物”完整技术实现《微信团队分享:微信支付代码重构带来的移动端软件架构思考》《IM开发宝典:史上最全,微信各种功能参数》数据与逻辑规律总结》《微信团队分享:微信*敏*感*词*单间1500万在线*敏*感*词*的消息架构演进》>>更多类似文章...
(本文同步发表于:)返回搜狐查看更多
编辑:
近期发布:中恒电国际融媒体平台,多端传播,全媒体发布
" />
媒体融合技术平台-内容管理系统是顺应全媒体时代和媒体融合发展的创新媒体发布管理平台。平台重构新闻采编生成流程,升级采编系统,真正实现“一次采编、多生成、多终端传播、全媒体发布”。
平台系统系统、实用、可扩展、经济、技术先进、成熟、安全;基于JAVA语言开发,采用自主研发的Enorth StructsX架构设计,具有良好的跨平台性能。可支持Windows、Linux等多种操作系统平台,支持Oracle、MySQL等多种主流数据库管理系统,可部署到Tomcat、Weblogic等多种应用服务器平台。
" />
系统浏览器兼容性好,支持主流浏览器使用,包括Firefox、Chrome、IE、360浏览器等浏览器。可承载亿级数据量,支持高并发、高可用,配备多重安全策略。系统支持中文、英文、俄文、维吾尔文、哈萨克文等多种语言进行管理和发布,支持使用utf-8编码。手机端页面,客户端支持ios6.0及以上系统,Android4.0及以上系统。系统提供标准化接口规范,预留标准化二次开发接口,方便日后平台功能扩展及与第三方软件系统对接。
媒体融合技术平台支持网站、手机网站、APP客户端、微博等主流社交平台内容同步管理,实现内容全面覆盖、多终端覆盖。新闻发布由过去的*敏*感*词*发布转变为一对多发布。