闲鱼直播flutter化实践

4014次阅读  |  发布于2年以前

背景

直播带货已成为近年来最热的“风口”,已成为电商升级的新突破口。闲鱼作为国内最大的二手交易平台市场,直播带货也成为推动成交的强烈需求。但是闲鱼直播原先接入外部提供的直播sdk,存在以下几个痛点问题:

  1. 业务定制困难。接外部sdk都存在“改不动,不敢改”困境。目前闲鱼直播和sdk提供方业务特点存在一定差异,产品提出的定制化需求难以得到满足
  2. 双端一致性难以保证。android,ios各提供一套sdk,两端表现并非完全一致,存在bug也不敢轻易升级sdk(直播sdk还是2019年版本)
  3. 排查舆情问题难度大。目前直播sdk里面日志有哪些,以及日志系统在哪里都不清楚,有问题得求助别人排查。

因此闲鱼想基于直播sdk的一些能力,做一套跨平台直播sdk,减少开发维护成本。考虑到闲鱼在flutter技术栈上有一定技术积累,新版直播通过Flutter实现跨平台开发(支持ios,android),再接入闲鱼的全链路日志系统,能解决目前闲鱼直播面临的问题。

整体设计

基本原则

在设计开发之初,我们定下了几条原则:

  1. 复用原生业务无关的核心能力:这里主要考虑到开发成本因素。比如底层视频编解码能力,webview容器等这些,native已经做的相当成熟,如果在flutter重新做,势必会大大延长开发时间。通过插件化复用原生层能力,将加快开发进度
  2. native层尽量做薄,flutter层尽量做厚:我们知道flutter是个移动UI框架,很擅长做UI。上一条原则强调复用原生能力,我们很容易陷入一个误区:业务逻辑作为中间的地带(不属于UI,也不属于底层核心能力),放到哪都可以。

如果放到native层,那么flutter就会做的很薄,仅提供有限的接口能力。这样会带来一个问题:如果我们想修改一个业务逻辑,实际上还得改三处代码(android,ios,flutter),开发成本和原来差不多。

如果放到flutter层,那么修改业务代码只需要改flutter一处即可,真正做到跨平台。

功能划分

对照线上老native闲鱼直播间的很多功能,我们发现核心部分主要有:视频播放,评论,分享,关注,分享,宝贝口袋等等(下图左)。这其中难点涉及到如何划分:哪些是核心能力,必须依赖native能力?哪些能flutter化?首先我们从视觉角度分析,可将直播间拆解为以下三层:播放渲染层,互动容器层,UI层(见下图右)。
播放渲染层:作用是渲染拉流数据,完成视频播放,业务无关。我们可以复用底层拉流和编解码能力,播控控逻辑上移到flutter层。

互动渲染层:基于动态容器完成渲染,完成一些互动玩法(例如直播间红包,拍卖组件等)。这个动态容器是基于native层,但是依赖上层桥能力可以进行上移到flutter,完成动态化。

UI层:UI交互层,比如分享,评论,关注,点赞等以及feed流框架等。这层纯UI交互,完全可以用flutter化。

最终我们得到如下新版flutter直播框架图

关键模块实现

播放器

直播必然离不开视频播放,播放器作为核心能力,方案选型至关重要。目前开源的flutter播放器主要分为以下几类:

  1. 官方提供的videoPlayer,以及基于官方插件开发的chewie,betterplayer,yoyo-player
  2. 基于ijkplayer开发的播放器,如fijkplayer等
播放器类型 介绍 特点
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:

  1. 如果播放器调用load方法立马调用play函数会不生效。这是因为播放器会认为非法状态,直接跳过(下图左)
  2. 频繁调用play,pause容易造成页面卡顿。实际上我们真正生效的是最后一次操作(下图右)

// 视频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组件包括红包组件,拍卖组件,更多直播组件等。

当然我们在实践的过程也遇到很多坑:

  1. PlatfomView手势冲突问题,见[《Flutter PlatformView 在闲鱼直播业务中的实践》]
  2. PlatfromView黑屏问题。在OppoR15 容器出现动态层黑屏现象,定位原因是platformView背景是透明的情况下,容易复现。通过将官方提供AndroidView替换成PlatformViewLink可以解决这个问题,但是会引起画面卡顿掉帧。

无缝小窗播放

直播场景下小窗播放并不是什么新鲜事情。当从直播页面切到后台,屏幕上会显示另外一个小窗显示直播画面。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

京ICP备2023019179号-2