直播带货已成为近年来最热的“风口”,已成为电商升级的新突破口。闲鱼作为国内最大的二手交易平台市场,直播带货也成为推动成交的强烈需求。但是闲鱼直播原先接入外部提供的直播sdk,存在以下几个痛点问题:
因此闲鱼想基于直播sdk的一些能力,做一套跨平台直播sdk,减少开发维护成本。考虑到闲鱼在flutter技术栈上有一定技术积累,新版直播通过Flutter实现跨平台开发(支持ios,android),再接入闲鱼的全链路日志系统,能解决目前闲鱼直播面临的问题。
在设计开发之初,我们定下了几条原则:
如果放到native层,那么flutter就会做的很薄,仅提供有限的接口能力。这样会带来一个问题:如果我们想修改一个业务逻辑,实际上还得改三处代码(android,ios,flutter),开发成本和原来差不多。
如果放到flutter层,那么修改业务代码只需要改flutter一处即可,真正做到跨平台。
对照线上老native闲鱼直播间的很多功能,我们发现核心部分主要有:视频播放,评论,分享,关注,分享,宝贝口袋等等(下图左)。这其中难点涉及到如何划分:哪些是核心能力,必须依赖native能力?哪些能flutter化?首先我们从视觉角度分析,可将直播间拆解为以下三层:播放渲染层,互动容器层,UI层(见下图右)。
播放渲染层:作用是渲染拉流数据,完成视频播放,业务无关。我们可以复用底层拉流和编解码能力,播控控逻辑上移到flutter层。
互动渲染层:基于动态容器完成渲染,完成一些互动玩法(例如直播间红包,拍卖组件等)。这个动态容器是基于native层,但是依赖上层桥能力可以进行上移到flutter,完成动态化。
UI层:UI交互层,比如分享,评论,关注,点赞等以及feed流框架等。这层纯UI交互,完全可以用flutter化。
最终我们得到如下新版flutter直播框架图
直播必然离不开视频播放,播放器作为核心能力,方案选型至关重要。目前开源的flutter播放器主要分为以下几类:
播放器类型 | 介绍 | 特点 |
---|---|---|
video_player | 跨三端播放器(android/ios/web) 底层依赖Exoplayer(android),AVPlayer(ios) | 官方支持,兼容性好,迭代及时。api使用较为复杂;底层依赖难以切换 |
ijkplayer系列播放器 | 基于ijkplayer封装的播放器插件 | 相比官方插件,迭代频率低,点赞人数少些 |
引入第三方插件播放器会带来几个问题:造成包体积增大,维护风险未知;闲鱼底层播放器并非基exoplayer,这样造成播放器管理不统一;引入外来库容易造成库版本冲突。因此我们决定自研一套基于闲鱼环境的播放器。采用方案是基于外接纹理,将native创建的纹理id传递到flutter层进行展示,具体原理自行查阅,不在此展开。
其中,我们遇到一个难题:播放器的状态管理做过播放器业务的同学都知道,播放器内部是个状态机。如果调用时序不合理,很容易发生crash。常见的做法是在播放器上再封装一层sdk层,内部做状态维护和管理,过滤掉不合法的状态操作。这次我们决定将sdk层业务逻辑全部上移到flutter层,但是flutter和native直接是通过methodChannel进行通信的,methodChannel是异步的。这会造成上层播放状态和底层的播放状态是不一致的。考虑两种case:
// 视频prepare
void load() async {
...
}
// 播放
void play async() {
...
}
// 暂停
void pause async() {
...
}
// 情况1:加载中立即调用play
load();
play(); // 不生效
// 情况2:频繁点击暂停/播放
play();
pause();
play();
pause();
....
调用不生效
线程卡顿
针对上述问题存在的问题,我们在flutter层做了更为细致状态管理。对于异步调用加入了另外两种状态:过渡态和期望态。
例如:
当我们有加载动作load,会将播放器状态设置为过渡态(load_pending),处于此状态下将不会响应特定的操作(比如play);如果此时调用了play方法,播放器仅仅会记录下期望态wantedState=playing;
当加载完成后native会通知flutter层状态改变,从load_pending->loaded。此时我们会校验当前状态是否和期望态是否相等。由于当前为loaded 不等于期望态playing,我们会帮用户补上一次play操作(见下图)。
同样当频繁调用play/pause方法时,我们会将播放器状态设置成过渡态(play_pending/pause_pending),此状态不响应播放暂停操作,而仅仅改变用户的期望态;当native通知flutter层操作状态改变完成后,再校验一次期望态是否和当前状态是否相等,如果不相等再做响应操作
过渡态/期望态
对于带货直播来说,互动玩法是个必不可少的重要元素。每逢重点促销节日,各种运营活动接踵而来。一个不需要跟版,全版本覆盖的动态层容器有着非常大的吸引力。我们参考了直播sdk中native实现方案,将动态层容器桥接到原生的h5容器(可以理解成webView)。
PlatformView桥接到动态容器层
事实上,我们将动态层容器桥接到native这是远远不够的。我们最需要解决的问题是:容器依赖的能力注入。
容器依赖能力就是h5互动组件通过jsBridge,获取到原生native运行时环境数据能力。
原先直播sdk中将apiBridge能力设计成全局单例,这在feed流的直播场景下存在一定隐患。例如从第一个直播间将滑到第二个直播间过程中,可能同时存在两个native容器向apiBridge拿数据。如果这个数据是全局公用的,那没问题。但是有个apiBridge方法是获取当前直播间详情数据(getLiveDetailData),这必然造成有个直播间拿到的数据不正确。
因此我们在设计API Bridge的时候,采用多实例方案。每个直播间分配一个bridge实例(并不耗内存),同时为每个直播间建立一个单独的methdChannel,native容器和bridge单独走这个通道进行通信。这样便能彻底解决上面消息的隐患问题(下图左)。
我们将apidBridge逻辑彻底flutter化的同时,也提供了动态注册api Bridge的能力,不需要修改native侧代码(上图右)。目前互动层容器已随flutter直播间上线并且经历了双十一的考验。目前线上运行的h5组件包括红包组件,拍卖组件,更多直播组件等。
当然我们在实践的过程也遇到很多坑:
直播场景下小窗播放并不是什么新鲜事情。当从直播页面切到后台,屏幕上会显示另外一个小窗显示直播画面。android平台下,实现原理是通过windowManager将渲染层view add到window中去。
我们在实现第一版小窗播放的时候,是通过新起一个播放器的方式将画面渲染到一个window 的SurfaceView中,由于加载耗时的影响,这会造成播放不连贯。
我们做了进一步的优化,复用原来的直播播放器,切换渲染层surface。将原来的外接纹理surface进行detach,attach小窗surface。这样视频流并没有中断,只是切换了渲染层,做到了真正的无缝切换效果。
直播间存各种各样消息需要传递,可靠的底层消息通道至关重要。我们需要抽离出单独的消息模块便于业务复用。消息模块flutter化,抹平端差异。考虑到直播有相当成熟的消息中间件,我们讲消息模块做成单独的插件,上层提供统一的消息处理接口。
其中消息传递免不了native和dart层通信。考虑到消息有两大类型:控制类和数据类,我们采用了双管道通信,其中控制消息类采用methodChannel(基于方法粒度);native层产生的数据消息采用eventChannel通知到flutter层(频繁通信,单向)。
设计之初我们想做的是一个“直播场”,任何直播源都能接入进来。比如接入淘宝直播,优酷直播,或者闲鱼自己的直播等等。但是不同来源的直播就会面临数据协议不同,UI样式也无法统一的难题。因此保留组件动态替换能力是非常有必要的。
常见做法是站在组件UI的角度,UI雏形下拥有一定的定制能力,由子类覆写。比如组件种icon图标的定制,图标点击事件处理等等。这种继承方式会牺牲一定的灵活性,定制化能力不够强。
而我们站在直播能力的角度,为每个组件赋予不同能力,通过组合的方式进行组装,这样灵活性会更强。
目前闲鱼直播间大致划分成上面几个组件:LivePage/LiveCell/LiveInteractive/LivePlayer/LiveComment等。每个组件会承担一定能力角色,具体分工如下:
不同组件之间难免会有数据进行共享和通信,这样会造成一定的耦合。为了最大程度的降低这种耦合,这边基于轻量级的Knight框架进行通信,其基本原理仍然采用观察者模式。比如:LivePage作为直播间详情数据提供者,它会将共享数据声明出去(declare),使用方(LiveCell/LiveInteractive)回通过require方法进行订阅。当共享数据源发生变化,会通知到使用方。
目前新版直播间在7.2.40版本已经全量上线,总版本uv占比达到九成以上,暂无线上舆情问题
经历了双十一流量峰值考验,线上红包问题取得超预期效果间,给直播间用户增长带来很大促进作用。
这篇文章系统讲述了我们闲鱼创新小组在flutter直播化所做的一些努力,希望能给大家带来一些启发和收获。未来我们还有许多工作需要进一步探索:1 PlatformView 兼容性问题需要解决,以及相关内存流畅度优化 2 做一款真正意义上的flutter播放器,将编解码数据直接对接到flutter层,实现真正意义上的跨平台。道阻且长,行则将至,行而不辍,则未来可期。
Copyright© 2013-2019