随着手机性能的不断升级,人们对于播放器的需求也在爆发式增长。对此,LiveVideoStackCon 2022 北京站大会邀请到了来自七牛云的资深开发工程师、播放器负责人,陈军奇老师,从七牛云播放器的历史出发,为大家阐述Qplayer2播放器的具体应用。
文/陈军奇
编辑/LiveVideoStack
大家下午好,我是来自七牛云的陈军奇,5 年前,我进入音视频开发领域,一开始是做音视频上层的播放页相关业务,之后进入播放器底层开发领域。今天我给大家带来的演讲主题是 QPlayer2 播放器——用扩展性支撑起未来需求。
1.七牛云为什么要从 QPlayer1 升级到 QPlayer2
先简单介绍一下七牛云播放器的发展历史。2016 年的时候,我们就推出了 QPlayer1 播放器,但 16 年时,手机安卓端大多数都是 4.4、5.0 版本,iOS 是 iPhone6、iPhone7 的水平。那个时候播放器主要解决兼容性的问题和尽可能多地支持主流协议和封装格式。但是从 17、18 年到现在,特别是前两年需求一下子爆发起来,不管从手机的性能,还是用户对播放器的需求,都呈现猛烈增长。
那个时候 QPlayer1 有这样一些问题:随着硬件设备的提升,更多高级能力需要被支持,比如说 HDR、VR、多码率的无缝切换,特殊场景和垂直领域的业务有一些特殊功能要做。这是16 年的播放器不会去想的,经过多年的迭代,接口的统一性和应用性会被破坏,重写是比重构更好的选择。
除了播放的核心能力以外,贴合核心的附加能力成为不可或缺的一部分。从播放器 SDK 到真正客户的产品,这个中间过程其实就是用户接了我们的 SDK,要去做的一些开发工作。其实客户希望我们能够提供一些支持让他们在这一段路上走得更好。
2.当前市场对播放器的诉求以及在 QPlayer2 上的实现
我们现在来看看,这几年客户和市场给我们提了哪些需求,我们 QPlayer2 是如何去做规划来满足这些需求的。
第一个是丰富的协议/封装格式/媒体类型支持。
协议这块 https 不用说,RTMP 也是比较常用的直播。SRT 其实是一个跟 WebRTC 有点类似的,用在低延迟直播场景下的一个协议,这个协议也是基于 UDP 的。我们的 SRT 云服务跟 QPlayer2 配合起来的话,SRT 端到端的测试延迟是能够达到 700-900 毫秒的水平。
WebRTC 这块,我们公司还有一款快直播的 SDK 是专门适用 WebRTC 协议的播放器。之后我们想把 WebRTC 协议接到 QPlayer2 里面来,这样的话我们其实一个 SDK 就可以解决 WebRTC 的播放和直播的播放。
当然也包括一些常用的封装格式,比如 flv、hls、mp4。也有一些垂直场景用的封装格式,例如 flac、wav, 这两种格式对音质要求比较高,因为它是无损的,音质要求比较高的一些播客或者音乐类客户需要这样的封装格式。
媒体支持类型方面,纯视频、纯音频和音视频的媒体类型都支持,纯视频的话,底层的音画同步逻辑其实跟纯音频和音视频是不太一样的,我们都能完美支持。
在比较基础的协议之外,还有一些特化的需求,比如对残障人士的关怀功能——色盲模式,色盲模式其实有很多种类,像色觉障碍有红绿的,还有蓝黄的等等。比如红绿优化下,我们把原来红色、绿色色块变成红绿障碍的用户能够区分的颜色,让他们能区分开这两个颜色;再比如倍速功能,通过对声音的变速处理,改变播放速度,这块我们也是做到了变速不变调。
VR 视频其实就是一个全景 360 度的视频,一般情况下我们要戴着 VR 眼镜看,现在我们在手机上就可以通过手势或陀螺仪去改变视角,看到 360 度的画面。这个功能是我们在移动端通过底层 OpenGL 去渲染实现的,这样的话,在效率上来说是非常的高。
Seek 分为精准模式和关键帧模式。相对来说,关键帧模式 Seek 的速度会比较快,精准模式会慢一点,但是它会 Seek到指定位置上。因为播放器在 Seek 时会暂停,但是对于 QPlayer2 来说他其实不是暂停状态,它是 Seeking 状态,在 Seeking 状态的时候,它一旦有流的话马上会恢复,不会等很久。如果网络快的话,Seek 之后的耗时跟首帧的耗时其实是差不多的,可能比首帧还快一点,我们的点播的测试首帧数据能达到500-700ms。
然后是清晰度的切换,清晰度的切换分为立即切换和无缝切换。点播的情况下推荐用无缝切换,清晰度切换对流是没有要求的,只要时间轴对齐。比如 720P 跟 1080P,两个流的 GOP 可以是不对齐的,并且两个流的协议、封装格式也可以是不一样的。比如 720 是 flv,1080P 是 mp4,只要时间轴一致,就能做到无缝切换。立即切换一般是用在直播的场景下,因为直播在考虑端到端延迟比较小的情况下,它的 buffer 其实是比较小的,那这个时候用无缝切换,它会损耗一些端到端的延迟,所以推荐用立即切换的方式去做。
起播方式的话,起播以后可以直接播放或者暂停在第一帧的直播位置,起播位置是可以指定某个位置开始播,比如上次看到第 10 分钟了,下一次再看到我也想从第 10 分钟开始看,那这个时候在 play 的方法里面带上一个播放位置的参数就能做到。画面比例就是说在手机上的时候,可以用16:9、4:3、铺满或者拉伸的方式进行展示。这个底层也是通过渲染引擎去进行变换,得到最终画面比例。
截屏功能中,SDK 回调上来的图片数据不是 RGB 格式,我们给的是一个JPEG。这样用户如果需要展示在 UI 上,JPEG 图片本身也能展示,存本地也比较方便,就不用做二次的 JPEG 编码了。
SEI 数据的回调,第一软解硬解都支持 SEI 数据回调;第二就是 SEI 数据一般是带在关键帧上,那我们渲染关键帧的时候才会把 SEI 数据回调上去,保证SEI数据使用时机和视频时间轴对齐。
刚才说了那么多关于功能上的东西,再说一下性能吧。
第一个性能是首帧耗时,我们怎么去缩短首帧耗时呢?其实首帧耗时从优化上来讲,全链路的环节都得把它考虑进去,包括服务端跟播放器。我们今天主要讲一下播放器的优化。
首先是预加载,一般是用在点播情况下,而且它的场景比较局限,一般短视频滑动的场景是比较适合去做预加载的,预加载是跟播放器剥离的,就是说预加载可以一下子加载后面 5 个视频,但是不需要创建 5 个播放器。
这 5 个预加载实例其实只有下载的能力,没有播放的能力,真正需要播放下个视频的时候,把预加载实例投给播放器,播放器的 play 方法除了一个带 URL 的数据单元可以播放,同时我可以支持把预加载实例投进去,让他播放。
在这个过程中,它其实是可以直接拿到里面预加载的音视频数据进行解码播放,省了前面的一些网络的耗时,同时我们的预加载也只会加载一个视频的前一秒钟左右的数据,预加载情况下我们首帧耗时的测试数据大概在 200-300 毫秒的样子。
其次是加速首帧解码。解码这块 QPlayer2 除了软解硬解之外,还有一个叫首帧加速的解码方式也称作混解,比传统的硬解能高出大概 80-90 毫秒的时间的提升。
最后是 DNS 缓存,一般客户的所有视频都是在同一个域名下的,所以会在 DNS 解析层做一个缓存。如果说下一个域名是相同的话,就可以用上一份存的 IP 地址直接去请求,减少了 DNS 解析的时间。首帧耗时基本上通过这三个方面去提升。
第二个性能是弱网下的播放体验。比如在商场或地铁站等人多的地方,播放会出现卡顿,但是出了地铁站以后就好了。那么短时间网络不好的情况下我们该怎么去处理?
可以适当增加端上的缓存大小,让它在遇到弱网之前就缓存了一部分的数据,那这个时候它进入网络差的环境下的时候可以先消耗 buffer 里面的一些数据,出了地铁站以后,可能 buffer 刚好消耗完,这个时候网络也好了,那其实就是无感知卡顿。
其次是清晰度自适应。在点播场景下清晰度是可以无缝切换的,感知到网络不好的情况下我们是可以去请求低清晰度的,保证播放的流畅性。
最后,可以利用 RTC 或者 SRT 协议去做弱网下的流畅优先还是清晰度优先的策略。
第三个性能是端到端的耗时。一个是减小整个链路的缓存。这是跟弱网背道而驰的方式,就看用户到底是要端到端耗时小,还是需要弱网流畅度好,这是鱼和熊掌不可兼得的情况,要看用户的需求。缓存小的话就相当于 buffer 小了,相当于端到端的时间延迟小了。二个其实还是 UDP 的协议, WebRTC 或者 SRT 去做端到端的耗时优化。
刚才讲了性能上的一些关注点,那接下来我们说说接入成本。这也是各个客户、厂商比较关心的一部分,我们从以下几个维度去展开讲接入成本。
第一个是最小化播放闭环代码量。
如果要用我们的 QPlayer2 写一个 demo,以安卓为例,第一步我们先在 xml 文件里面布局一个 SurfacePlayerView,第二步是在 onCreate 方法里面把 view 拿出来,然后调用 init 方法,把 this 传进去,第三步构建一个 media model,它其实就是一个播放元素,里面包含了一些播放元素的信息,然后调用 play media model。这个时候播放器就已经开始播放了,等播完以后在 onDestroy 方法里面把它给释放掉。
那我们看一下,加上 xml 总共是 13 行代码,不加上 xml 总共只需 8 行代码。如果是其他播放器,比如 Media player、EXO Player 里面都有 prepare 这样的操作。用户其实不关心 prepare ,第一次接触音视频播放器的人其实也不太清楚 prepare 到底是干什么的,这点上 QPlayer2 是做到了接口极致的精简。所以简单来说四步走,布局 → init → play → release ,这是非常精简的一个闭环。
第二点是统一的播放器接口。
这个涉及到学习成本,刚开始拿到这个 sdk 的时候,用户可能会去看一下 demo 是怎么写的,当要去调用一些 demo 代码里没有的方法,对于我来说,我会去点一下看看提示里面有些哪些接口。如果说是统一的播放接口,那调过几个以后我就知道 API 的套路了,减少了一个学习成本。虽然说有 API 文档,但是很多情况下,如果能看懂接口的意义,其实大家是不太愿意去翻 API 文档的。
那我们来看一下 QPlayer2 的播放器接口是怎么样的。第一类是控制类别,控制类就是给播放器发送一个指令让他干什么,init / playMediaModel / release / pauseRender / resumeRender / stop / switchQuality / Seek 都是控制类。第二类获取属性类别,就是播放器要提供给我什么,duration/当前的播放进度 / 当前的下载速度 / 当前的播放状态,这些都是获取类别,这是客户主动获取的属性。第三步是监听回调类别,比如当前进度变了 SDK 底层就回调,通过监听的方式去做。
这边我举了个例子是 PlayerStateChangeListener,就是播放状态改变的回调,包括刚才的 position 也是有类似的回调,DownloadSpeed 也是有对应回调的。播放接口其实就分这几类。
第三个是贴近业务的附加能力,是由 QPlayer2-ext 提供的。
其实刚才已经说到了从 SDK 到用户真正的产品中间还有很长的一段路要走,那我们怎么给客户提供支持呢?我们写了一个 QPlayer2-ext 这样的一个库。刚才的播放器其实是 QPlayer2-core,那个 core 是不依赖 ext 的,就是说可以不用 ext,用 core 刚才那些东西都能实现。但如果需要更贴合业务的附加能力的话,可以用 ext 来赋能。
我说一下它有哪些能力,一个是选集控制,比如说一个电视剧场景、多集的场景或者播放器逻辑场景,比如说在一个播放页里面既有点播又有直播,但点播跟直播上面的 UI 都是不一样的,需要有两套业务逻辑,那这个时候播放器逻辑场景管理就设置两个场景去分别处理点播跟直播。播放器控制面板的自定义就是说在面板上我们是有一套可以自己定义的 UI 系统。包括浮窗、控制面板、Toast。
3.QPlayer2 架构的扩展性是如何实现的?
要支持的功能越来越多,架构如何设计才能满足这些要求呢?
首先来讲下 QPlayer2-core 的功能点扩展槽。
我们从播放器的数据流向来看。第一个就是拉流/解封装这块。解封装方面我们支持协议的扩展,封装格式的扩展。比如之后又出了一个其他的协议,那我们可以直接在这里进行解封装,因为解出来以后它其实已经是 NALU 单元和流信息了,出了 InputStream 以后就跟协议、封装都没关系了。
第二个是解码。解码这块我们有两个方向的扩展:一个叫解码格式的扩展,比如说 H264/H265 要去找对应的解码器。还有解码方式的扩展,比如软解、硬解、首帧加速的混解。如果之后又出了一个新的解码方式的话,也可以在这里扩展。
那解码完以后的音频数据就往上走了,有个 PerTransformer,它其实是做音频的预处理。就像刚才我们看到的那个音频的变速不变调,就是在这里做处理。比如说我要加个变声,也会在这里处理。处理完以后我就投给音频处理去渲染出来。
再之后视频的数据就会给到 CanvasRender。画布渲染里面其实包含很多东西,第一个是视频渲染,视频渲染里面其实也有一个预处理的模块,其实功能上和 PerTransformer 是差不多的,它是对解码出来的 YUV 数据进行一个处理。我们刚才看到的色盲模式、VR、HDR、护眼模式都会在这里做处理,处理完以后进行一个最终的渲染。
画布渲染为什么叫画布渲染不叫视频渲染?因为这个上面其实除了视频渲染以外,还有字幕渲染等其他数据在视觉上的渲染,都会放在画布渲染上。这一整套渲染体系里面其实是有个 RenderEngine 去做最终渲染的事情。
除了音频跟视频之外,之后的发展可能新出一个数据类型,那我们也可以在解码那边去把它扩展出来,解码可能不是说视频的解码器了,可能是新的数据解码器去解出来,这个数据出来以后是另外一个渲染方式,也可能是渲染到画布,那就还是去画布渲染。那这样的话就做到了几个关键环节的可扩展性。
下一个是平台扩展性。
因为我们现在已经支持了安卓跟 iOS,后面还要扩展到其他平台, 那么如果要支持扩展到多个平台的话,需要去扩展哪几个点呢?
第一个是音频渲染,因为音频设备是每个系统都不一样的。第二个是解码,这块软解不用特化处理,因为都是同一套代码,但是硬解的话需要特化去实现。第三块是渲染环境。画布渲染里面渲染环境 iOS 跟安卓是用 OpenGL 去渲染的。他们唯一不一样的就是渲染目标的 view 是不一样的,安卓的话就是 surface view 或者 texture view,iOS 的话就 layer,那这一块需要去特化的。但是 shader 和 OpenGL 的处理其实都是一样的。如果说我在 Windows 上要去实现渲染环境,那就跟安卓和 iOS 都不一样,因为要用 Direct X 去特化整套渲染逻辑。
QPlayer2-ext 的能力,有 UI 定制和管理,业务逻辑定制与管理,播放器选集控制。小窗投屏是我们后续要做的,打算放到 ext 里面。
播放器 UI 层级管理,最外面的相当于是一个外层容器,里面是平行的 Layer。最底下是画布渲染层,其实就是 OpenGL 或者 Direct X 渲染层,往上是手势的监听层,再往上是播放器控制面板层, Seek 条、选择按钮、倍速按钮等都在播放控制面板上。它有个逻辑,就是如果没有操作的话,几秒钟之后它是会消失的。
然后是播放器浮层、选集浮层、倍速浮层都在浮窗层上。再上面是提示层,会有很多 toast 弹上来。然后在 UI 层级之外还有一个转屏控制,因为特别是异形屏手机转屏以后要去适配异形屏、凹口屏等,可能每个 APP 的逻辑都不一样,这块就会有回调,回上来然后由上层实现适配。还有一个就是 UI 的生命周期通知和重力感应。
在图上我们来看下可定制的点有哪些,第一个是控制面板可定制,图中上方的 13KB/s、FPS、下载速度、码率,和下方的 Seek、暂停按钮、时间这些都在控制面板上,拿安卓系统举例,可以通过xml布局文件任意修改面板上的控件和样式。第二块是提供通用的手势监听,其实 VR 视频的转向还有缩放都用了 ext 的手势监听。转屏可定制就是凹口屏的适配问题。提示可定制并且支持 Toast 上加按钮,还有浮窗可定制。
播放器业务逻辑定制与管理,我们还是从数据的流向来说。第一个,触发播放器去做一些行为的时候,行为其实都是从播放数据切入的,就是数据驱动的。我们的播放数据,除了一些基础的字段以外,其实我们是通过泛型让客户自己去定义字段,有了自定义以后,整个环节里面都可以拿到客户自己定义的字段了。每个播放数据可以指定不同的播放场景,比如说有两个播放源,第一个是点播,第二个是直播,分别给他们配置对应的播放场景,那当点播视频播放时自动切到点播场景,直播也一样。
那场景切换以后什么会跟着换呢?第一个是面板,面板可以定制两套——点播跟直播的。业务服务也可以定制多个,然后它是可配置的,比如说我有一个跳过片头片尾的业务逻辑,那这个时候我点播场景适用,直播场景不适用,我只要在点播场景里面配一个跳过片头片尾的 Service,但是直播场景不配置。在点播场景下对应的 Service 起播的时候就会 Start 这个 Service 。这些 Service 业务服务内可以调用 Toast 、Widget和手势逻辑,形成一个闭环。
在 ext 中每个定制类都可以获得到 PlayerCore,PlayerCore是集所有能力于一身的类,选集控制、获取播放数据、播放场景管理、转屏控制、播放器控制、所有的 UI 管理都在这里面。大家在开发播放器应用的时候其实会有一点问题,比如逻辑业务非常复杂,播放器实例传来传去,有时候都不知道该怎么管理这个实例了。通过 ext ,任何地方都可以拿到 PlayerCore 实例,但是无需管理 PlayerCore 的生命周期,只需要关心上层的业务逻辑就行了。
4.QPlayer2 未来的方向
接下来说下 QPlayer2 未来的方向,其实 QPlayer2 时间不长,才一年半多一点,也只是刚开始,还是有很多的不足。我们接下来,先把一些该有的功能都加上,其次平台层面,我们有计划支持 Windows、MacOS、Linux。
未来如果有一个颠覆性的创新 QPlayer2 是否是做好准备了呢?
比如说基于区块链的数字化版权和权益,版权在中国市场的地位越来越重,区块链里面有非同质化代币,那这个代币怎么跟数字化版权(视频版权)做一个绑定,在播放器支持这类视频的鉴权和播放,是我们需要去考虑的。
再比如 AR 播放器的实现方面,到 AR 时代、元宇宙时代了,我曾不切实际地想过这个场景到底是怎么样的,以后不用屏幕了而是激光投影,未来我跟某个人打电话,可能通过激光投影投下来对方的形象,类似虚拟偶像演唱会的形式,我跟他的投影进行对话和交互,那这个时候我们的设备就不是屏幕了,QPlayer2 需要去扩展支持这种场景。
谢谢大家,我的分享到此结束。
LiveVideoStackCon 2023上海讲师招募中
LiveVideoStackCon是每个人的舞台,如果你在团队、公司中独当一面,在某一领域或技术拥有多年实践,并热衷于技术交流,欢迎申请成为LiveVideoStackCon的讲师。请提交演讲内容至邮箱:speaker@livevideostack.com。