前言:本文是基于SwarmCloud创始人在2018腾讯Live开发者大会上的演讲整理而成。
项目背景
我们知道,Adobe宣布2020年将停止Flash的更新。实际上,Flash本身就存在很多问题,比如一些安全漏洞以及性能问题。H5相比Flash的话,首先是浏览器原生支持的,不需要安装任何插件,而且它速度更快、更节能、而且更加的安全。
现在直播平台的带宽成本还是很高昂的。据统计,各大直播平台一个月的带宽成本达到了每100万人3000万元,某头部直播平台,一个月的带宽成本能达到了3亿元。
还有一个背景就是,目前运营商已经逐渐放开上行带宽,下面是网上找到的一张图片,以前都是上下行带宽不对等的。现在慢慢的,上下行带宽放开了,而上行带宽有利于我们来做P2P的。
还有一个原因是WebRTC目前已经得到了很多主流浏览器的支持,包括Chrome, Firefox,Edge和Sarafi等,国产基于Chromium内核的浏览器也大部分支持。
目前1.0标准也出来了,创造了很好的条件。
刚刚说的是一些大趋势方面的,与自身业务相关的,比如以前王者荣耀流行的时候。当吃鸡游戏越来越流行的时候,因为大屏幕看直播的体验更好,有一部分比例流回PC端。
我们知道这种电竞直播是具有潮汐现象的。平时工作时间可能没什么人看,而到了晚上主播直播开播了,带宽就上来了。这个时候看直播的人会一下子很多,而P2P技术是可以有效削低峰值带宽的。
企鹅电竞目前,移动端已经接入P2P技术,但Web端才刚刚开始,也算是填补这方面的一些空白吧。
接下来介绍一下我们的技术选型。首先是底层的传输技术,在Web端肯定不用说,做P2P肯定是要用WebRTC的。关于WebRTC的原理还有API使用方法,这里就不做介绍了,相信大家在其他地方已经了解过了。这里简单介绍一下它的几个特点吧。
首先,它是可以实现音视频流或者是其他任意数据的点对点传输,这里要强调的是其他任意的二进制数据。第二,它不需要安装插件,也就是说,用户在接入P2P的过程中他是完全无感知的。第三,它是使用STUN来协助P2P打洞的,所以会用到信令服务器,这个待会会详细介绍。一般情况下,在打洞失败的情况下,会用TURN服务器来中转,但是TURN服务器其实也是要有部署成本的,所以这里采用在P2P和CDN间切换的策略来规避TURN服务器的使用。最后,JS API相对简单,但是对新手来说还是有点复杂,所以大家也可以用开源的第三方封装好的。我们做P2P主要是用到了WebRTC里面的一个叫做RTCDataChannel的一个API。据谷歌官方的介绍,API有几个特点。首先,它的API和WebSokets是很像的。其次,它具有低延迟的特点,而且它可以在有序传输和无序传输之间切换,就有点类似于TCP和UDP这种区别。其次,它也是安全的,类似HTTPS。
这是它API使用的一个示例,这里大家可以看到它也是用onmessage来接收数据,用send来发送数据,跟websocket很像。
在createDataChannel 的时候可以传入一个reliable字段来决定是有序还是无序传输的,默认是有序的。
然后为什么说它是安全的?主要从两个方面来看,第一方面是从信令来看,它这里写的是HTTPS,实际上我们是用Websoket来做信令传输的,也是用WSS来加密的,它的数据通道是基于DTLS的。
然后选完了P2P的底层传输技术,我们还要选择一种P2P的算法,或者说拓扑结构。比较典型的是树型的拓扑结构,还有网状的拓扑结构。树形比较具有代表性的是叫做Fastmesh算法。
Fastmesh算法是基于树型拓扑结构,顾名思义,它的数据是单向流动的,从根节点一直流到叶子节点。树型有一个特点就是当你加入或离开的时候,会造成拓扑节点的频繁变动。此外,还要讲一下Fastmesh的两个优势。
第一,它是分为多个子流的,它从多个父节点获得数据,这样粒度更小,更有利于充分利用上行带宽。第二,它有个爬树的流程,如果子节点的上行带宽比父节点或者祖父节点更高的话,它会有个爬树流程,从而让子节点可以爬到更顶层来覆其他的节点。这种树型它只适用于直播,像刚刚所说的,它的带宽利用率比较高,但是它需要在P2P的过程中测量上行带宽,而且它的实现是比较复杂的。
然后网状的比较典型的是BT算法,它的数据流向是双向的。而且由于是网状的,它的拓扑结构变动相对来说是比较不频繁,比较稳定的。它稍加修改,可以同时适用于直播或者点播。它相对于Fastmesh来说,带宽利用率比较低,但是它的实现相对比较简单。
综合考虑到Web P2P的特点,还有业务方面的一些特点,最后我们选择了网状这种拓扑结构。其他直播平台和企鹅电竞一样,同时有直播和点播这两种业务需求,而不止有直播。
其次,我们知道PC端和移动端,你把它的应用关了,它可能在后台还跑着,所以节点实际上是没有离开的。但是Web端,你把网页关了,这个脚本就没了就什么都没有了,所以它节点的加入和离开是非常频繁的,所以不适合用树型的拓扑结构。所以最终是选择了这种仿BT的算法。
那目前有没有开源项目给我们参考呢?还真是有的,叫做Webtorrent这种开源项目,它也是基于WebRTC还有BT算法的。它算是比较经典的一个项目了,在Github上有大概2万多个Star。
它可以同时运行浏览器和node.js环境。但是它这个项目有几个缺点。
第一个是它的框架比较重,它其实是比较老的项目,很久之前做的,扩展性不强。第二,它采用的是MP4的协议的,不支持直播。最后,它最致命的弱点,它是纯P2P传输的。
我们知道网络的环境很复杂的,这种采用纯P2P来做直播和点播都是不现实的,还要有一个CDN,可以给你及时回源的余地,所以它的实际工程应用价值不太大,但是它也为我们提供了很有价值的启发,可以说我们是站在巨人的肩膀上。
选完了P2P算法之后,我们还要选择一种流媒体协议。目前比较主流的大概就是这几种,MP4,RTMP,FLV,DASH,HLS。首先RTMP,FLV流式传输的肯定不适合用来做P2P,我们要选择一种切片的协议,而MP4只适用于点播,所以就可以把前三种给淘汰掉了,最后就剩下DASH和HLS。这两种算法都同时适合直播和点播,但是DASH兼容性比较一般,HLS同时可以兼容安卓,iOS平台,而且通过一个框架Electron在PC端也是可以兼容的,这是一方面。
另一方面,HLS在国内还有国外相对DASH来说会用的比较多,而且企鹅电竞用的是HLS,所以最终我们选择的是HLS这种流媒体协议。如果采用了HLS协议,就不得不提hls.js。
这个开源项目的架构比较优秀,而且我们可以替换loader为自己的模块(loader是hook网络请求的模块,是实现P2P传输的关键)。
接下来介绍一下我们实践的细节,首先说一下我们插件的一些设计原则。
第一,这个插件是同时适用于直播和点播的,但这里的直播指的是延迟较高的直播,不是指互动直播,互动直播对延迟要求很严格。像电竞直播,赛事直播,对延迟的容忍率较高,一般可以有半分钟到一分钟延迟。
第二,作为hls.js的插件,我们是不改变它的源码的,其本身也一直在更新中。
第三,要求集成简单,可以在现有的项目中快速集成,这也是从产品角度考虑,要接入一个P2P重写的项目播放器是不现实的。还有就是考虑到用户特定的使用环境,做成高可配置化的,让用户自行根据自己的使用环境来配置参数。
我们插件还可以通过有效的调度策略在P2P和CDN之间无缝切换,在保证用户的播放体验的前提下最大化P2P率,这也是我们一直优化的目标。
最后,后台可以通过IP调度提高P2P连接的成功率和稳定性。这是总体的设计原则。
然后来看一下我们的分层模型。
最底层是WebRTC,是原生的,当然我们要对它进行封装,这个封装就是第二层,datachannel这个模块。在应用层还要实现一些私有协议,
还有传输对上层来说是透明的,这些都是由datachannel来负责的。然后第三层是P2P scheduler这个模块,它是负责P2P的调度的。我们一个节点它是可能同时连接多个节点的,那我们怎么来对这些节点打分、排序、淘汰,从哪些节点来获取数据,这都是由P2P scheduler这个模块来负责。然后它的上一层是Fragment Loader,这个其实就是HLS.js暴露的一个loader。这个loader也要实现一个调度,就是在P2P和CDN之间来进行一个无缝的切换。当你从P2P没有办法获得及时数据的,能够及时切到CDN,从而保证用户播方的流畅。
然后最顶层就是HLS.js了,它请求数据,我们负责把数据喂给它。它怎么播方的,怎么封装解码,都是它自己负责,我们只要负责喂给它数据就好。
这是整体的架构,左边是Peer1、Peer2、Peer3、Peer4,这些代表一个个Peer,然后右边是代表CDN。
首先,在初始化之后,P2P scheduler它会首先向Tracker服务器来请求节点,Tracker服务器来做一层调度之后,把适合节点返回给它。它就会通过信令服务器来尝试和这些节点建立P2P连接,成功建立P2P连接之后呢,它们之间就可以开始进行数据的共享了,请求到数据之后,就会返还给Fragment Loader,Fragment loader就会在CDN和P2P之间做一个调度,获取到数据之后就会给HLS.js。
接下来我们对各个模块来进行介绍。首先是信令服务器,信令服务器,顾名思义,就是用来给两个对等节点交换信令的,因为它们之间需要交换公网IP和端口才能进行P2P连接。在理想情况下,两个节点都是位于公网中的,那很容易就建立P2P连接了。
但在实际条件下,两个节点位于各自的NAT后面的,这时流程就有点复杂了。首先要通过STUN服务器获取自己公网的IP端口,通过信令互相交换,然后才会进行打洞的尝试,但不一定会打洞成功,有一定的概率失败。
对于Tracker服务器,当节点向它发起请求的时候,把其他的合适节点返回回去,这里我们也要做一个调度,基本的原则就是同一个ISP,甚至同一个地域的,让它们优先建立P2P连接,还有同一个内网的,同一个内网举一个比较典型的场景,就像高校里面,可能有几万个学生在看电竞直播或者世界杯等,他们处于同一个内网中,内网中带宽理论上是无限的,而且很容易就建立P2P连接,所以如果很多人处于同一个内网中,就可以节省不少的流量成本。其他节点能够通过发送一个心跳包,来向Tracker服务器证明自己目前还在线。如果不在线,就及时把它给剔除掉。
整体流程是,当一个节点发起一个请求的时候,我们先获取它公网的IP,先去查IP数据库,获取了ISP信息之后,就可以和其他节点进行匹配了,首先进行一层过滤,如果某个节点已经连接了很多个Peer,那你就应该把它过滤掉,就是ISP过滤和IP过滤,最后都返回一个合适的节点,这就是Tracker大概的一个调度流程。
然后是P2P Scheduler,它主要实现了一个仿BT的算法,假设有三个节点观看同一个视频,通过Tracker服务器互相建立P2P连接,P2P连接建立成功之后,它会把自己bitmap广播给它的邻居,bitmap就代表它到底下载了哪些数据,哪些数据还没下载,每个Peer都维持着和它连接的其他节点的bitmap,并且及时告诉其他的Peer,它已经有了某个ts文件了,然后其他Peer就把它保存到bitmap里面。
这是它还有一个调度过程:
然后是Fragment Loader,正常的Hls.js它的是这样一个数据请求逻辑,首先它会请求M3U8并解析,顺序请求ts文件,我们自己的Fragment Loader,第一步也是和它一样去请求m3u8,首先去缓存中找,如果缓存命中了,这个ts就直接返回。
Hlsjs Fragment Loader
如果没有命中,就去bitmap中找,如果bitmap找到了,证明和你连接的peer是有这个ts的,那么就去向它请求。如果bitmap中也找不到,那就只能去CDN请求了。这里要设置一个超时时间,如果在超时时间内没有获取到一个完整的ts,就及时去CDN请求,保证用户播放不卡顿。
Modified Fragment Loader
还要讲一下在实践中踩过的坑,也就是需要注意的地方。
首先,你用RTCDataChannel直接发送整个ts,通常情况下是不会成功的,因为它对包的大小是有限制的,所以建议是不要超过64KB,也就是说把数据给切片,切成一片一片的,发送到对等端,再把它给拼接起来。当然你也可以切的更小,以兼容比较旧的浏览器。
分片情况下传输协议的设计会复杂一点,这里有两种可以想到的,第一种是第一个数据块传一个json,里面是接下来要传输的ts的元信息,里面写着接下来会传几个数据块,对方接收后就初始化一个内存空间,把接下来接收到的chunk按顺序拼接起来。第二种是每一个chunk都加一个协议头,协议头里包含这个chunk的序号等信息。
这两种各有优劣势,第一种的话,因为只有一种协议头需要解析,所以效率更高,但是它不能无序传输,也不能和其他的二进制数据同时传输。第二种由于每块都带了一个头,所以可以无序传输也可以和其他一起传输,但这种效率就会低一点。
还有一个是Channel的问题,同一个Channel的节点,它们才会建立一个P2P连接,或者说会处于同一个P2P网络中,所以比较自然的想法就是采用m3u8来作为Channel的ID,这个m3u8代表大家处于同一个流或播放同一个视频。
这是最初的想法。但是在实践过程中,就会出一个问题,比如你的传输协议更新了,那你新旧两个传输协议之间的节点,它们建立连接就会出现一个冲突的问题,所以在后面加一个协议的版本号,代表如果使用不同的传输协议,即使你播的是同一个m3u8,它们之间也不会建立P2P连接,从而解决一个冲突的问题。
还有一个动态url问题,可能是出于视频防盗链的考虑,有一些流媒体服务商提供的流,它是根据你的IP动态生成m3u8的,从而导致不同用户用不同的m3u8播放,但是它们播放的是同一个视频,所以它们进入了不同的频道,从而它们之间就没办法进行P2P连接。
这个时候怎么解决呢?我们插件是不知道用户播的是不是同一个视频的,所以这个我们需要交给插件的使用者来解决。我的想法是在p2pConfig里面加一个channelId函数,用于返回相应视频video Id,让用户来决定哪些节点处于同一个频道中。这样就可以解决因为url不一样导致不能建立P2P连接的问题。
还可以让客户端上传一个Tag,因为我们的插件中有很多的参数是需要调整的,你不可能说调整一个参数就发一个新版本,这是不切实际的。所以我们在里面加了个Tag,这个Tag可能就是个数字,你在文档中记录这些数字是调整哪些参数,然后你在做P2P的时候,它会把Tag上传到服务器,再把它存入数据库,这样你就可以根据这个Tag来查看你调整参数适用P2P的效果。
最后一部分介绍一下后面我们优化的方向,首先是高码率优化问题。
我们知道现在蓝光的ts就达到好几兆,而我们的P2P策略里面,它的一个ts只会从一个节点获取。因为用户的上行带宽比较小,在超时时间内无法下载完整的ts,超时时间内没有下载完成,它可能会把完整的ts 抛弃掉,这样就造成一些P2P效率的下降。那我们可以怎么做呢,首先还是要用P2P来下载数据的,但是我们不一定要下载一个完整的ts,我们可以在超时时间内能下载多少就下载多少,然后剩余部分用CDN来补足,然后把它们给拼接起来就好了。但是这样的话,就要求你的源支持range请求。但有些源,比如像腾讯云,它是不支持range请求的,所以要根据自己的流媒体供应商来决定用不用这个策略。
还有弹性伸缩,这也是我们直播的特点,这种潮汐现象很明显。在高峰时段,可以用弹性伸缩来增加服务器实力。在低峰时段,就减少实例来降低服务器成本。
还有一个是移动端Web P2P策略,当用户使用的是手机蜂窝网络的时候,偷跑用户的数据,虽然说用户是无感知的,如果你比较在意的话也可以做一些改进,比如你可以通过navigator.connection来获取用户的网络类型。
如果用户使用的是手机蜂窝网络,那你就只用P2P下载不上传,这样你有很好地保证用户的流量。
还有其他优化方面,包括上行带宽估计,因为这个上行带宽,如果你能事先估计的话,那么你就知道它能服务多少个Peer,这样将对你的调度是有利的。你可以参照speedtest自己部署一些服务器来测量上行带宽,但是这样成本是比较高的,你也不能直接用speedtest,会有一个跨域问题。我们可以采用动态估算的算法,在它上行的过程中来估算它的带宽。还有一个就是我们可以做一个更精细的调度,如果在线用户节点足够多的话,我们不仅可以做ISP,省份的调度,甚至还可以根据城市经纬度,甚至调度的时候我们还可以把节点的在线时长,上行带宽等等考虑在内。
还有一个优化是现在压缩包后的体积还有100多k,通过去掉一些冗余的依赖,把它缩小到50k左右。如果可能的话,还可以将原生APP和Web端打通,当然前提是要在APP端实现一套WebRTC的协议栈。还有就是结合雾计算,比如在路由器实现WebRTC,然后把一些数据缓存在其中,其实就是一个廉价的CDN。
接下来简单来介绍一下SwarmCloud,创始人之前任职于腾讯,后来出来全职做这个P2P流媒体项目。相信大家在听了之后,想要自己来开发一套这样的项目,还是一头雾水的,但没关系,如果你不想重复造轮子,如果你不想踩我们踩过的坑,可以考虑使用SwarmCloud提供的SaaS服务,在节省CDN成本的同时,也给用户带来一个流畅的播放体验。
那么怎么集成呢,其实很简单,如果你是直接采用hjs.js,只需要把它的script标签直接替换为我们提供的script标签。我们已经把集成方式,做到了非常的傻瓜化。
如果你不是直接使用hjs.js,也没关系,我们的插件同时支持各种第三方播放器。只要是基于hls.js的播放器,都可以!以下仅列出部分支持的播放器:
更多使用姿势请参考官网文档
谢谢大家的耐心阅读,SwarmCloud与你下期再见。