作者:张凌柱(以绳)
本文为《优酷播放黑科技》系列文章第二篇,第一篇文章可点击 优酷播放黑科技 | 自由视角技术体验优化实践进行查看。点关注不迷路 ~
直播内容区别于点播,因其实时性、强互动性的特点吸引着用户,在观看的同时还可以同步进行点赞、评论、打赏等互动行为。此类互动主要集中在用户的社交行为,而内容型的互动也随着点播中的“互动剧集”的出现而映入大众的眼帘,用户可以在剧情进展的关键节点选择角色执行不同的行为继而进入到不同的分支剧情中。同样直播方向上的内容型互动也在被不停的探索和应用中,其中一个例子就是同时进行多个视角的直播,允许用户切换选择某个特定的视角观看直播内容。尤其在中大型的直播活动中(例如街舞、欧冠等),粉丝向比较明显(明星、球星),如果能提供多个观看视角或者提供某个聚焦视角供切换选择,在用户体验上无疑能有很大的提升。
基于此,经过研究与探索,最终基于WebRTC落地实现了低延迟高清晰度的多视角直播能力,并在《这!就是街舞》的年度总决赛正式上线应用。效果视频如下:
视频可点击查看:优酷播放黑科技 | 基于WebRTC实现的直播"云多视角"技术解析
方案选型
能否实现低延迟切换、高清晰度、全机型覆盖的用户体验是最终决定采用哪种技术方案的重要衡量标准。业内常用的应用层协议包括:
- HLS(含LHLS)
- RTMP
- RTP(严格来说RTP介于应用层和传输层之间)
其中,RTMP因其优缺点(低延迟但易被防火墙拦截)被确定用于直播推流,本文暂不多作介绍,下面我们主要针对拉流过程进行分析。根据端上拉流个数可以将实现方案分为:
- 单路流伪多视角直播:基于HLS协议,端上同一时间仅拉取一路流,切视角需要重新切流地址起播;
- 多路流多视角直播:基于HLS协议,端上同时拉取多路流进行解码渲染,切视角不需要切流地址起播;
- 单路流多视角直播:基于RTP协议,端上同一时间仅拉取一路流,切视角不需要重新切流地址起播。
横向对比
对处于劣势的特性作标红处理后得到的表格:
方案 | 协议 | 同时预览 | 无缝切换 | 码率 | 端性能负担 | 增量成本 |
---|---|---|---|---|---|---|
单路流伪多视角直播 | HLS | 否 | 否 | 普通 | 普通 | 无 |
多路流多视角直播 | HLS | 是 | 是 | 高 | 高 | CDN |
单路流多视角直播 | RTP | 是 | 是 | 普通 | 普通 | 边缘服务与流量带宽 |
通过对比,我们最终决定采用基于RTP的单路流多视角直播,即边缘方案。
WebRTC 概述
WebRTC,名称源自网页即时通信(英语:Web Real-Time Communication)的缩写,是一个支持网页浏览器进行实时语音对话或视频对话的 API。它于 2011 年 6 月 1 日开源并在 Google、Mozilla、Opera 支持下被纳入万维网联盟的 W3C 推荐标准。WebRTC默认使用UDP协议(实际上使用的是RTP/RTCP协议)进行音视频数据的传输,但是也可以通过TCP传输。目前主要应用于视频会议和连麦中。
Voice Engine 音频引擎负责音频采集和传输,具有降噪、回声消除等功能。Video Engine视频引擎负责网络抖动优化,互联网传输编解码优化。音视频引擎之上是 一套 C++ API。
ICE、STUN、TURN 用于内网穿透, 解决了获取与绑定外网映射地址的问题。DTLS (UDP 版的 TLS)用于对传输内容进行加密 。RTP/SRTP主要用来传输对实时性要求比较高的音视频数据,RTCP/SRTCP用来控制RTP传输质量。SCTP用于自定义应用数据的传输。
系统设计
多视角直播整体链路涉及流生产、域名调度服务、边缘服务、合流服务、直播播控服务等多个环节,整体结构框架如下:
- 流生产:演播现场摄像机采集流上传到直播中心,直播中心对流进行编码及优化处理;
- 合流服务:多路输入流通过时间戳进行音视频对齐后,按照模版输出多路混合流;
- 边缘服务:预缓存合流服务多路输出流, 对输出流进行编码, 通过RTP连接传输给端侧;
- 域名调度服务:进行IP、端口配置,供端侧与边缘服务通信;
- 播控服务:提供合流服务请求参数配置、及多视角业务能力配置等播放控制服务。
详细设计
端侧为了尽可能的复用播控主链路,减少对主播放链路的侵入, 增加了多视角播放器。多视角播放器与其他播放器保持相同的调用接口,在播控处根据业务类型决定是否创建多视角播放器。在业务层增加多视角插件,与其他业务解耦,可以很方便的挂载与移除。
端侧结构设计如下:
核心流程
用户进入多视角模式主要起播流程如下:
目前支持3种展示形态,分别为mix模式、cover模式及source模式, 下图为具体形态及切换过程。
边端指令
端侧与边缘结点主要交互指令,通过RTP协议进行数据传输。通用头部传输格式如下,PT=96 代表H264媒体数据。
- 建连指令:阻塞式指令,端侧与边缘结点建立RTP连接,需要等待边缘结点返回响应,如果在一定时间内无返回,则认为建连失败;
- 断连指令:非阻塞式指令,端侧与边缘结点断开连接,无需等待边缘结点返回;
- 播放指令:非阻塞式指令,端侧下发播放指令,需要传递播放流ID及OSS流配置信息,供边缘结点查找到正确流;
- 切流指令:非阻塞式指令,端侧下发切换视角指令,为了与原视角保持同步,需要传递原视角帧时间戳到边缘服务。
项目落地
播放能力调整
- 音频采样率调整
WebRTC目前默认不支持48K音频采样率,而当前大型直播的采样率较高,如果不经过调整下发到端上会导致音频解码模块的崩溃;
- AAC音频解码能力扩展
WebRTC里音频编码默认用的是Opus,但当前直播现状大部分都是AAC格式,所以需要客户端在offer SDP 中添加AAC编码信息并实现AAC解码器,边缘服务配合下发RTP打包数据;
- H.264编码支持
为了尽可能地降低延时,WebRTC视频编码采用的是VP8和VP9,需要迁移至更通用的H.264编码;
- 在传输方式上,WebRTC使用P2P方式来进行媒体中转,它只是解决端到端的问题,对于大型PGC、OGC的直播内容来说明显不适合,因此网络传输层并没有打洞逻辑,利用RTP连接传输流媒体数据,RTCP进行传输质量控制。
接入已有播放体系
为了尽可能小的减少对主播放器影响,与主播放器保持了一致的数据请求流程、一样的播放能力接口、一致的数据埋点时机等:
- 多视角播放器:封装WebRTC, 扩展出多视角播放器, 专用于多视角视频播放;
- 播控逻辑调整:调整直播旁路数据获取流程,将合路服务、AMDC等服务返回数据合并到数据流中;
- 播放能力&统计调整:保持原有播放能力及回调接口, 通过扩展接口参数来予以区分;
- 扩展错误码:基于主播放器错误规则, 扩展出多视角播放器错误码。
端侧问题解决
最终需要实现下图所示的主播放窗口附带几个子播放窗口同时渲染:
右侧可滑动列表采用UITableView(RecyclerView)实现,为每一个子窗口添加渲染实例GLKView(被RTCEAGLVideoView包裹管理),通过在合适的时机创建、移除、更新子窗口的渲染frame实现一路流多个视角同步直播的效果。
期间主要解决了切视角闪烁的体验问题以及内存泄漏导致的渲染黑屏稳定性问题。这里作简要介绍。
播放闪烁
【问题描述】假如一共有N个视角,那么我们的实现方案则共有N*3(参见系统设计中的3种展示形态)路流,每次点击视角切换,实际的操作流程是播放器发出RTP切流指令,边缘节点换流ID并发送Sei回传信息,播放器接收到SEI信息后进行各个窗口的裁剪与更新操作。这就存在一个问题,切视角成功后窗口更新需要一定的时间,会导致实际接收到了新流的帧数据最开始的一帧或几帧采用的裁剪方式依旧是旧流的模板,所以用户会看到一瞬间的视觉残留。
【解决方案】在播放内核收到切流指令之后,设置短时间的丢帧窗口期,待收到Sei信息表明切流成功后恢复渲染,这期间因为没有新的帧数据所以用户看到的是静态帧,这样可以屏蔽导致子窗口内容错乱的渲染数据,上层UI在此期间也看不到残留帧闪烁了,用户体感上基本是平滑切换的。
内存泄漏
【问题描述】当用户不停地切换视角,列表刷新,cell重新创建的过程中,内存占用不停增长,达到上限后导致黑屏窗口的出现。
【解决方案】内存泄漏的原因在于cell重新创建过程中,内核中的OpenGL 渲染实例发生了批量新的创建,而旧的实例又未能及时销毁。第一步明确过上层业务代码对UIView对象通过removeFromSuperview有释放操作,而lldb打印引用计数后仍旧不为0,那么问题就出在了内核的实例管理上。__bridge_retained代表OC转CF对象后,内存管理需要手动释放,因此切视角时需要调用相关Filter实现C++层面的销毁与释放。解决后的内存表现:
服务并发优化
优化前版本曾经支持CUBA相关赛事直播,但是存在不少问题:成本高、切换延时高,且不具备支持大规模并发能力。本次优化目标是能够支持大型活动直播,大幅降低成本,同时保证体验。
编码前置
下图为优化前链路,可以看到,每个客户端需要重新布局和编码,这两个操作耗费大量资源,即使是用T4 GPU也只能支持20路同时观看。
经过分析可以发现,在多视角的应用场景下用户观看的组合是有限的,在街舞模式下,观看视角为3*N,N为原始采集视角,那么我们如果把这3N个流预先生产,然后在边缘服务直接复制编码后的H.264数据,则可以把CPU的消耗大大降低,同时承载的用户数就是提高至少一个数量级。
图中合流服务只需生产一次,生产的内容就可以为所有的边缘服务节点所共享。
但是这种简单的切流有个问题,无法做到帧级的对齐,换句话说,就是用户在选择不同视角时会发现新切的视角跟之前的视角时间上不连续。为了解决这个问题,我们请阿里云导播的同学在合流时以绝对时间戳为依据,进行对齐,同时将这个时间戳信息透传给边缘服务。
解决的PTS对齐的问题,我们仍然无法做到真正的帧级对齐,因为还有GOP对齐的问题,熟悉H,264编码的同学都知道,视频的编码解码都是以GOP为单位的,GOP的长度可能有1s到10s甚至更长,在直播中典型应用长度为2s。如果用户切换时,恰好在一个新的GOP的开始,则简单切流即可实现,但是我们不能要求用户在哪些时间能切,哪些时间不能切,用户要的是想切就切。我们的解决方式如果用户切换时没有可以正好切换的GOP,边缘服务就生产一个。
通过上图,我们可以看到,当用户从视角1切换到视角2时,我们在切换点产生一个新的GOP,这样推流到客户端后,端上可以无缝解码渲染到新的视角。
通过上面的步骤,我们大大减少了编码消耗,但是为了能够快速响应用户切视角,我们必须把所有源视角的原始画面Frame(YUV420)准备好,当需要产生新的GOP时,可以直接使用。但是需求总是变化的,4个视角时,我们可以对43=12路流都进行预先解码,当业务方要93=27路流时,同时解码27路1080p视频数据使得整个32核机器不堪重负,更何况以后还可能要求更多路源流。
按需解码
用户想要更多的视角,我们需要满足。那么之前全部预先解码的方式必须改变,所以我们实现了按需解码,也就是只有真的需要解码时,才去为它准备那路流的Frame(YUV420)。这里最大的问题就是实时性的问题,因为当用户切换时,画面可能处于一个GOP的任意位置,而我们只能从GOP的开始帧进行解码。不过通过多次优化,做到了呈现给用户的延迟不会被感知的效果。
客户端动态缓存
做过音视频的同学都应该对卡顿率很熟悉,年年治理总是不够理想,多视角直播则面临更麻烦的问题,因为如果降低卡顿率,最基本的做法是增大播放缓存,但是我们又希望用户能够迅速看到新的视角,就要求缓存不能太高。所以我们引入了动态缓存,简单来讲就是当用户切换时,我们采用极低水位缓存,保证切换体验,而当用户不切换时,缓存会慢慢恢复到一定水位,保证一定程度对抗网络抖动。
总结与展望
多视角能力在街舞总决赛正式上线,作为优酷独创新玩法,收到了来自于微博、朋友圈等广大用户的诸多正向反馈。后续优酷计划将通过交互优化,时延优化等方式,进一步提升用户体验。
关注【阿里巴巴移动技术】微信公众号,每周 3 篇移动技术实践&干货给你思考!