本期作者
朱俊炜
哔哩哔哩流媒体高级开发工程师
前言
随着高速网络和5G的普及,目前市面上开始有越来越多的云游戏产品。通过云游戏,玩家不需要性能强劲的设备,也不需要下载如今包体越来越大的游戏,云游戏使玩家接触游戏的门槛变得越来越低。
目前业界云游戏最大的难题是如何降低延迟和解决串流卡顿,其次市面上的云游戏解决方案几乎被几家提供商垄断,第三方解决方案在功能定制和优化上自由度低、租用成本高。
为了解决第三方解决方案的瓶颈,于是B站自研云游戏由此诞生。
B站云游戏从设计上允许全平台使用、游戏触感反馈、本地多人合作、远程多人合作甚至可以让用户私有化部署到自己的PC上。
WebRTC
为了可以在Web端直接使用,在技术选型上采用了WebRTC作为底层协议,虽然WebTransport作为新起之秀也已经被浏览器支持,但是目前支持覆盖不高。
WebRTC协议本身使用ICE(交互式连接建立)进行端口穿透,同时支持基于SRTP(安全实时传输协议)的音视频传输能力和基于SCTP(流控制传输协议)的数据传输通道。
WebRTC协议结构
 Jitter Buffer
WebRTC的SRTP音视频模块中Jitter Buffer(抖动缓冲区)是必不可少的部分,Jitter Buffer类似与普通视频播放的缓冲区,但是在Jitter Buffer中会将接收到的RTP包进行重新排序,随后将排序后RTP包内的音视频帧进行排序,然后进行GOP重排序,最终数据由音视频解码器进行解码。
低延迟音视频
虽然WebRTC的音视频传输部分使用的SRTP在业界广泛应用在云游戏上,但是WebRTC在上层做了一层封装后很难在Web端对RTP本身进行修改。WebRTC最初是为Google Talk用于点对点音视频通话开发的,由于云游戏存在用户的操作即时音画反馈,所以对延迟的要求更高。
而WebRTC为了降低卡顿,会根据网络抖动情况,在播放端为音视频设置较长的Jitter Buffer。较长的Jitter Buffer会导致音视频会在接收到数据后延迟解码和展示,在真实无线网络+公网传输环境下会比网络延迟增加500ms左右的展示延迟,这对于云游戏来说是不可接受的。
降低Jitter Buffer造成的延迟
在Safari中,当AudioTrack和VideoTrack附加到Video标签播放时,视频和音频不是强同步的,音视频会分别计算Jitter Buffer并总是消耗、播放最新数据。
而在Chrome中,使用相同代码会发现音频和视频的Jitter Buffer总是最小的去适配最大的(比如音频500ms,视频50ms时,视频Jitter Buffer会慢慢增大到500ms),这样虽然展示有延迟,但是保证了音画同步。如果想要在Chrome里实现Safari相同效果,可以将AudioTrack和VideoTrack分别附加到Audio和Video标签,但是会像Safari一样有轻微的音画不同步出现。
通过让浏览器不进行音画同步来降低延迟治标不治本,在阅读Chromium、WebRTC源码后发现Chrome有私有导出了WebRTC的三个私有的设置项,这意味着可以在基于Chromium的浏览器上在一定程度上自定义Jitter Buffer策略。通过对上述私有设置项进行调参,将音视频本地展示延迟控制在了50ms左右,在跨主干网+2.4G无线网络情况下可以不高于100ms。
上述使用x264编码器,关闭B-Frame,开启zerolatency调参。如果使用WebRTC内置的libvpx和openh264编码器编码,则需要更大的Jitter Buffer。
解决低延迟导致的卡顿
在降低了Jitter Buffer长度后,在网络抖动时由于没有足够的缓冲重排、拼装完整音视频帧,会导致更容易出现音视频的卡顿。
这个时候需要用到RTP对应的RTCP Transport Feedback来进行拥塞控制,通过动态控制音视频码率来避免网络抖动所带来的卡顿。在WebRTC中目前常用的是GCC(Google Congestion Control)控制算法(RFC草案:https://datatracker.ietf.org/doc/draft-ietf-rmcat-gcc/)。
GCC算法有一个抵达时间算法,发送方发送两组数据包,两组数据包的发送时间差为,接收时间差为。理想网络下接收时间差应等于发送时间差,但是实际上网络的延迟抖动、拥塞、丢包会导致这两个值是不一样的,其中的差异定义为。GCC算法通过持续测量,持续更新当前网络负载状态(OverUse、Normal、UnderUse)。
随后GCC通过RTCP Transport Feedback计算接收方的ACK字节数,并且统计接收速率,然后将上述通过抵达时间算法计算的状态分类,然后使用滤波器进行滤波得到接近准确的网络负载状态,最后根据网络负载状态进行码率预估。
使用了动态码率后,当网络抖动时就可以快速自动降低码率,避免卡顿,当网络恢复时慢慢调整至原始码率。在取舍方面,因为玩家对帧率更敏感,比如从高帧率突然降低帧率,会明显感觉到卡顿感,但是分辨率的动态降低要更难察觉到。所以在降低码率时,同时降低分辨率可以在让画面不出现大量欠码色块的同时保障了云游戏的画面流畅度。
超低延迟控制协议
上面介绍了云游戏的低延迟视频传输,云游戏的控制传输借助了WebRTC的DataChannel,DataChannel默认使用了可靠传输模式,在可靠模式下DataChannel类似与WebSockets,但是游戏控制信号如果需要确保低延迟,必须要允许在网络异常时允许丢包。
B站自研云游戏在控制协议上设计在非可靠传输中使用,最理想的情况下是将传输数据协议使用类似UDP一样的底层协议传输,因此将DataChannel设置为“允许乱序、不允许传输重试”。在设计初期,控制协议设计想要参考音视频的Jitter Buffer设计一个很短的数据重排缓冲区,在研究了国内外各种云游戏后,决定采用只接收最新控制数据的激进策略。
自适应硬件回报率
在调研中发现市面上一些已有的云游戏产品基本上设计为操作时按照硬件回报率进行控制数据上传,当无操作或者手柄处于Dead Zone内时停止控制数据上传。
使用固定硬件回报率进行上传,这样在实际游玩过程中如果硬件回报率过高会导致上行网络拥塞,造成操作卡顿或延迟变高。
当进入Dead Zone时停止数据上传,如果游戏是由三方厂商制作,可能因为对死区的定义不同而出现一些问题,比如在某云游戏平台游玩万代南梦宫的《绯红结系》时,将手柄右摇杆缓缓置中时,由于云游戏平台定义的Dead Zone大于游戏的Dead Zone,导致摇杆完全回中后游戏依然会一直缓慢旋转游戏镜头。
动图:市面上采用Dead Zone停止数据上传的方案
B站自研云游戏使用了120Hz作为最大回报率,在每次硬件回报时,根据手柄的各种模拟信号和数字信号状态计算动态回报率。
首先,定义了各种数字信号按键(只有0和1两种状态的按键,如手柄的大部分按键)为高频率按键,当两次回报检测发现状态变更时,直接将本次操作帧作为120Hz的最高回报率等待发送。
其次,根据实验将模拟信号按键(通过编程器、电位器或者霍尔传感器实现的按键,如线性扳机或者摇杆)分为不同等级:
摇杆分级(示意图)
线性扳机分级(示意图)
在正常游戏过程中一般最多使用的输入级别定义为高频区,同时因为这部分输入在游戏内的表现也是最为明显的(比如快速行走、视角快速移动、射击、快速打方向盘、猛踩油门等),所以给予最高的回报率使游戏可以快速响应。
中频区域的采用了动态分级,这部分虽然游戏过程中玩家也经常用到,但是对于游戏内部的表现小于高频区(比如控制玩家缓慢行走),所以可以使用较低的频率上报。这部分的回报率定义采用了一个非线性的对应函数,整体会偏向于较高频率进行发送。
而摇杆低频区的定义参考了Xbox开发的Dead Zone建议值,这部分数据虽然大部分来自模拟电路的噪声,但是这部分数据有可能是有用的,另外线性扳机的精度一般要低于摇杆,很少出现数值上的噪声(所以在分区时线性扳机的低频区要小于摇杆的低频区),但是这部分数据在游戏内的反映一般不明显,且在用户操作上也很难准确控制在这一区间,所以这部分数据统一以10Hz的最低频率进行回报。
动态回报率演示(左侧“间隔”为实时回报率)
如下图所示场景中使用10-120Hz动态回报率下,发送响应延迟仅为0.667ms,而传统的120Hz固定回报率下,发送响应延迟则为4.834ms:
发送时间线示意图
 数据拼装
每一个数据包包含包序号、会话ID、包类型、数据体。包序号是由于将DataChannel视为原生UDP进行传输,所以需要序号自行确保数据顺序。会话ID定义的是一个手柄声明周期内的会话ID,如果客户端插入了多个手柄,那么就需要多个会话ID,通过会话ID可以实现一台电脑多个手柄、多个电脑多个手柄。包类型定义了该数据是什么样的数据类型(比如控制数据、灯光数据、振动数据等)。数据体是一个结构体(小端优先的二进制内存块),比如控制数据类型的数据体使用了微软XInput接口定义的XINPUT_GAMEPAD结构体:
XINPUT_GAMEPAD
无论是客户端还是游戏运行端(服务端),使用统一的收发逻辑,各端实现不同的数据类型。比如在游戏运行端实现了控制数据类型,接入具体控制游戏的模块,而客户端实现手柄振动等回馈数据的处理。
 游戏控制
B站自研云游戏允许UP主在直播时运行私有化部署的服务端,然后邀请观众一起玩一款游戏,所以对于游戏控制方面尽可能减少对游戏本体的侵入(游戏大部分是主播自行购买的,而不是经过云游戏化改造的特殊版本)。
最初的方案是通过Hook游戏的XInput接口来实现游戏控制,但是在开发了简易的注入模块后发现实际游戏可能会用RawInput或者DirectInput实现输入数据读取,甚至一些新的Windows游戏可能会用Windows Runtime API的Windows.Gaming.Input进行游戏输入。最初的方案在基于Unreal Engine的游戏上可以使用,但是基于Unity的游戏不能直接控制。对所有的API进行Hook成本太高,且一些具有反作弊的游戏不能直接注入。
最终采用了驱动级输入方案,通过Windows内核驱动模拟Xbox手柄输入,解决了各种游戏的适配问题。控制管理上允许在游戏运行端上插入空手柄进行玩家占位、在玩家操作时插入手柄、更换玩家保留玩家位等。
 游戏触感反馈
目前市面部分云游戏支持了手柄的触感反馈,通过Web端的的HapticActuator API可以轻松实现。但是目前很多云游戏用户是在手机端使用的,这部分用户可能不使用外接游戏手柄,而是通过触摸控制。
在iOS端App内可以通过Core Haptics利用手机内置的Taptic Engine实现各种触觉效果,但是在Android和Web端虽然有Vibrate API,但是无法控制振动效果和强度。通过BiliHaptic API,使云游戏支持在各种平台上都可以支持游戏触感反馈。
 BiliHaptic API
在此之前,曾经为哔哩哔哩触感直播开发了跨平台的BiliHaptic API,这套API支持在PC端Web、移动端Web、Android、iOS使用。
PC端Web和iOS APP的实现因为是标准API所以非常简单,所以在此不做描述。
在手机端的Web和Android APP上,因为没有可以控制强弱的API(Android的新API虽然提供了振动强度调节,但是不同ROM上效果不一样,比如在OnePlus、OPPO、VIVO手机上设置强度无效),所以将重回馈和轻回馈分为不同的基础振动持续时间,在调节两种不同类型强度的时候,将基础振动持续时间和强度进行计算获得实际振动持续时间。然后将计算出的振动时间和根据不同强度计算出的间隔时间放入“时间线”(此处是一个抽象概念)。有一个使用条件变量编写的持续异步任务会判定当前时间应有的状态(振动或闲置),当此异步任务运行时,相当于持续播放“时间线”上的振动数据。通过播放“时间线”上的振动和闲置状态,可以通过PWM调制不同强度和振感的触觉反馈效果。
总结
最终B站自研云游戏通过将上述开发的不同功能模块进行封装,为业务方提供了高灵活度的服务端与客户端SDK,做到了跨平台兼容与低延迟的体验。在服务端有用户自行部署时,通过SFU服务器进行音视频数据转发,可让观众在直播间内和主播一起玩本地合作游戏。当部署在服务器上时,可以提供更高的稳定性和更低的延迟。
以上是今天的分享内容,如果你有什么想法或疑问,欢迎大家在留言区与我们互动,如果喜欢本期内容的话,欢迎点个“在看”吧!
继续阅读
阅读原文