04.视频播放器通用架构实践

2,596 阅读26分钟

04.视频播放器通用架构实践

目录介绍

  • 01.视频播放器的痛点
  • 02.业务需求的目标
  • 03.该播放器框架特点
  • 04.播放器内核封装
  • 05.播放器UI层封装
  • 06.如何简单使用
  • 07.如何自定义播放器
  • 08.该案例的拓展性分享
  • 09.关于视频缓存方案
  • 10.如何监控视频埋点
  • 11.待实现的需求分析
  • 12.一些细节上优化
  • 13.参考案例和博客记录

00.视频播放器通用框架

  • 基础封装视频播放器player,可以在ExoPlayer、IjkPlayer、MediaPlayer,声网RTC视频播放器内核,原生MediaPlayer可以自由切换
  • 对于视图状态切换和后期维护拓展,避免功能和业务出现耦合。比如需要支持播放器UI高度定制,而不是该lib库中UI代码
  • 针对视频播放,音频播放,播放回放,以及视频直播的功能。使用简单,代码拓展性强,封装性好,主要是和业务彻底解耦,暴露接口监听给开发者处理业务具体逻辑
  • 该播放器整体架构:播放器内核(自由切换) + 视频播放器 + 边播边缓存 + 高度定制播放器UI视图层
  • 项目地址:github.com/yangchong21…
  • 关于视频播放器整体功能介绍文档:juejin.cn/post/688345…
  • 关于视频播放器通用架构实践:juejin.cn/post/688417…
  • image

01.视频播放器的痛点

  • 播放器内核难以切换
    • 不同的视频播放器内核,由于api不一样,所以难以切换操作。要是想兼容内核切换,就必须自己制定一个视频接口+实现类的播放器
  • 播放器内核和UI层耦合
    • 也就是说视频player和ui操作柔和到了一起,尤其是两者之间的交互。比如播放中需要更新UI进度条,播放异常需要显示异常UI,都比较难处理播放器状态变化更新UI操作
  • UI难以自定义或者修改麻烦
    • 比如常见的视频播放器,会把视频各种视图写到xml中,这种方式在后期代码会很大,而且改动一个小的布局,则会影响大。这样到后期往往只敢加代码,而不敢删除代码……
    • 有时候难以适应新的场景,比如添加一个播放广告,老师开课,或者视频引导业务需求,则需要到播放器中写一堆业务代码。迭代到后期,违背了开闭原则,视频播放器需要做到和业务分离
  • 视频播放器结构不清晰
    • 这个是指该视频播放器能否看了文档后快速上手,知道封装的大概流程。方便后期他人修改和维护,因此需要将视频播放器功能分离。比如切换内核+视频播放器(player+controller+view)
  • 播放器播放和业务耦合
    • 比如多个app共用一个视频播放器组件,一个播放业务播放器状态发生变化,其他播放业务必须同步更新播放状态,各个播放业务之间互相交叉,随着播放业务的增多,开发和维护成本会急剧增加, 导致后续开发不可持续。

02.业务需求的目标

  • 常见的业务需求
    • 基础封装视频播放器player,可以在ExoPlayer、MediaPlayer,声网RTC视频播放器内核,原生MediaPlayer可以自由切换
    • 对于视图状态切换和后期维护拓展,避免功能和业务出现耦合。比如需要支持播放器UI高度定制,而不是该lib库中UI代码
    • 针对视频播放,音频播放,播放回放,以及视频直播的功能。使用简单,代码拓展性强,封装性好,主要是和业务彻底解耦,暴露接口监听给开发者处理业务具体逻辑
  • 音视频播放框架
    • 视频播放等于MediaPlayer和SurfaceView,MediaPlayer主要用于播放音频,没有提供图像输出界面,所以我们需要借助其他的组件来显示MediaPlayer播放的图像输出,我们可以使用SurfaceView来显示
    • 能否实践开发出一套音视频播放的通用架构,能支持音频播放场景,也能播放视频场景,还可以无缝切换。比如视频切换音频操作,增强库的功能性
    • 视频窗口、音频窗口、视频浮窗、音频浮窗、短视频窗口、短视频浮窗、音频控制台等多种场景播放,需要灵活切换,这个也是一个大的难点

03.该播放器框架特点

  • 一定要解耦合
    • 播放器内核与播放器解耦: 支持更多的播放场景、以及新的播放业务快速接入,并且不影响其他播放业务,比如后期添加阿里云播放器内核,或者腾讯播放器内核
    • 播放器player与视频UI解耦:支持添加自定义视频视图,比如支持添加自定义广告,新手引导,或者视频播放异常等视图,这个需要较强的拓展性
  • 适合多种业务场景
    • 比如适合播放单个视频,多个视频,以及列表视频,或者类似抖音那种一个页面一个视频,还有小窗口播放视频。也就是适合大多数业务场景
  • 播放器的整体层级图
    • image
  • 播放器架构的介绍
    • 基础内核播放库:提供基础的播放功能,可以自由切换内核,也方便拓展添加其他sdk内核播放器
    • 统一播放器:屏蔽底层内核播放器播放差异,根据协议为上层提供统一的播放能力接口,供上层调用
    • 播放视图层:负责播放器视图层的UI控制和调度,彻底解除播放业务与播放器的耦合
    • 播放场景业务:负责向用户展示音视频播放能力和交互的业务
    • 播放关联业务: 为播放器提供增值或支撑的业务,比如视频埋点统计,后期添加投屏,后期添加下载功能
    • demo:提供各种播放场景案例代码,基本上有大多数常用播放器的使用场景,建议直接看demo拿来即用

04.播放器内核封装

4.0 遇到的问题

  • 播放器内核拓展难
    • 不同的播放SDK提供的API都不一样,如果业务层对每个合作方都进行业务开发,就会导致业务量非常庞大,并且不同合作的方的播放SDK会产生交叉,不利于播放业务的维护和拓展。
  • 播放器内核难以切换
    • 不同的视频播放器内核,由于api不一样,所以难以切换操作。要是想兼容内核切换,就必须自己制定一个视频接口+实现类的播放器

4.1 视频播放器内核封装需求

  • 一定要解耦合
    • 播放器内核与播放器解耦: 支持更多的播放场景、以及新的播放业务快速接入,并且不影响其他播放业务,比如后期添加阿里云播放器内核,或者腾讯播放器内核
  • 传入不同类型方便创建不同内核
    • 隐藏内核播放器创建具体细节,开发者只需要关心所需产品对应的工厂,无须关心创建细节,甚至无须知道具体播放器类的类名。需要符合开闭原则
  • 具体设计方案
    • 设计统一播放协议,对于上层播放业务,只调用按照统一协议设计接口,不必关心底层播放器的设计逻辑。保证上层播放业务不随新的接入播放SDK发生变化。

4.2 播放器内核架构图

  • 播放器内核架构图
    • image
  • 播放器内核代码说明
    • image

4.3 如何兼容不同内核播放器

  • 提问:针对不同内核播放器,比如谷歌的ExoPlayer,B站的IjkPlayer,还有原生的MediaPlayer,有些api不一样,那使用的时候如何统一api呢?
    • 比如说,ijk和exo的视频播放listener监听api就完全不同,这个时候需要做兼容处理
    • 定义接口,然后各个不同内核播放器实现接口,重写抽象方法。调用的时候,获取接口对象调用api,这样就可以统一Api
  • 播放器内核
    • 可以切换ExoPlayer、MediaPlayer,IjkPlayer,声网视频播放器,这里使用工厂模式Factory + AbstractVideoPlayer + 各个实现AbstractVideoPlayer抽象类的播放器类
    • 定义抽象的播放器,主要包含视频初始化,设置,状态设置,以及播放监听。由于每个内核播放器api可能不一样,所以这里需要实现AbstractVideoPlayer抽象类的播放器类,方便后期统一调用
    • 为了方便创建不同内核player,所以需要创建一个PlayerFactory,定义一个createPlayer创建播放器的抽象方法,然后各个内核都实现它,各自创建自己的播放器
  • 关于AbstractVideoPlayer接口详细说明。这个接口定义通用视频播放器方法,比如常见的有:视频初始化,设置url,加载,以及播放状态,简单来说可以分为三个部分。
    • 第一部分:视频初始化实例对象方法,主要包括:initPlayer初始化视频,setDataSource设置视频播放器地址,setSurface设置视频播放器渲染view,prepareAsync开始准备播放操作
    • 第二部分:视频播放器状态方法,主要包括:播放,暂停,恢复,重制,设置进度,释放资源,获取进度,设置速度,设置音量
    • 第三部分:player绑定view后,需要监听播放状态,比如播放异常,播放完成,播放准备,播放size变化,还有播放准备
  • 播放器的核心实现要点
    • 针对上层播放器业务,该内核库提供统一的播放暂停,设置播放状态的接口,由于播放器内核和播放器业务解耦合,所以非常方便快速添加其他sdk播放器,具体可以看这篇文章:05.视频播放器内核切换封装

05.播放器UI层封装

5.1 实际开发遇到问题

  • 发展中遇到的问题
    • 播放器可支持多种场景下的播放,多个产品会用到同一个播放器,这样就会带来一个问题,一个播放业务播放器状态发生变化,其他播放业务必须同步更新播放状态,各个播放业务之间互相交叉,随着播放业务的增多,开发和维护成本会急剧增加, 导致后续开发不可持续。
  • 不太好适合多种业务场景
    • 比如适合播放单个视频,多个视频,以及列表视频,或者类似抖音那种一个页面一个视频,还有小窗口播放视频。也就是适合大多数业务场景,视频通用性需要尽可能完善

5.2 如何分离播放和UI分离

  • VideoPlayer播放器
    • 可以自由切换视频内核,Player+Controller。player负责播放的逻辑,Controller负责视图相关的逻辑,两者之间用接口进行通信
    • 针对Controller,需要定义一个接口,主要负责视图UI处理逻辑,支持添加各种自定义视图View【统一实现自定义接口Control】,每个view尽量保证功能单一性,最后通过addView形式添加进来
    • 针对Player,需要定义一个接口,主要负责视频播放处理逻辑,比如视频播放,暂停,设置播放进度,设置视频链接,切换播放模式等操作。需要注意把Controller设置到Player里面,两者之间通过接口交互
  • UI控制器视图
    • 定义一个BaseVideoController类,这个主要是集成各种事件的处理逻辑,比如播放器状态改变,控制视图隐藏和显示,播放进度改变,锁定状态改变,设备方向监听等等操作
    • 定义一个view的接口InterControlView,在这里类里定义绑定视图,视图隐藏和显示,播放状态,播放模式,播放进度,锁屏等操作。这个每个实现类则都可以拿到这些属性呢
    • 在BaseVideoController中使用LinkedHashMap保存每个自定义view视图,添加则put进来后然后通过addView将视图添加到该控制器中,这样非常方便添加自定义视图
    • 播放器切换状态需要改变Controller视图,比如视频异常则需要显示异常视图view,则它们之间的交互是通过ControlWrapper(同时实现Controller接口和Player接口)实现
  • 具体如何实现呢

5.3 关于优先级视图展示

  • 视频播放器为了拓展性,需要暴露view接口供外部开发者自定义视频播放器视图,通过addView的形式添加到播放器的控制器中。
    • 这就涉及view视图的层级性。控制view视图的显示和隐藏是特别重要的,这个时候在自定义view中就需要拿到播放器的状态
  • 举一个简单的例子,基础视频播放器
    • 添加了基础播放功能的几个播放视图。有播放完成,播放异常,播放加载,顶部标题栏,底部控制条栏,锁屏,以及手势滑动栏。如何控制它们的显示隐藏切换呢?
    • 在addView这些视图时,大多数的view都是默认GONE隐藏的。比如当视频初始化时,先缓冲则显示缓冲view而隐藏其他视图,接着播放则显示顶部/底部视图而隐藏其他视图
  • 比如有时候需要显示两种不同的自定义视图如何处理
    • 举个例子,播放的时候,点击一下视频,会显示顶部title视图和底部控制条视图,那么这样会同时显示两个视图。
    • 点击顶部title视图的返回键可以关闭播放器,点击底部控制条视图的播放暂停可以控制播放条件。这个时候底部控制条视图FrameLayout的ChildView在整个视频的底部,顶部title视图FrameLayout的ChildView在整个视频的顶部,这样可以达到上下层都可以相应事件。
  • 那么FrameLayout层层重叠,如何让下层不响应事件
    • 在最上方显示的层加上: android:clickable="true" 可以避免点击上层触发底层。或者直接给控制设置一个background颜色也可以。

5.4 视频播放器重力感应监听

  • 区别视频几种不同的播放模式
    • 正常播放时,设置检查系统是否开启自动旋转,打开监听;全屏模式播放视频的时候,强制监听设备方向;在小窗口模式播放视频的时候,取消重力感应监听
    • 注意一点。关于是否开启自动旋转的重力感应监听,可以给外部开发者暴露一个方法设置的开关。让用户选择是否开启该功能
  • 具体怎么操作
    • 写一个类,然后继承OrientationEventListener类,注意视频播放器重力感应监听不要那么频繁。表示500毫秒才检测一次……
    • mOrientationHelper.enable();表示检查系统是否开启自动旋转。mOrientationHelper.disable();表示取消监听
    • 具体可以看这篇博客:06.播放器UI抽取封装

06.如何简单使用

6.1 播放单个视频

  • 必须需要的四步骤代码如下所示
    //创建基础视频播放器,一般播放器的功能
    BasisVideoController controller = new BasisVideoController(this);
    //设置控制器
    mVideoPlayer.setVideoController(controller);
    //设置视频播放链接地址
    mVideoPlayer.setUrl(url);
    //开始播放
    mVideoPlayer.start();
    
  • 只需要四步操作即可,非常简单。这样就可以满足一个基础的视频播放器
    • 具体逻辑可以看:BasisVideoController
  • 如何添加只定义视图,非常方便。AdControlView需要实现InterControlView接口才可以
    AdControlView adControlView = new AdControlView(this);
    controller.addControlComponent(adControlView);
    
  • 要是一个页面播放多个视频怎么办
    • 直接创建两个VideoPlayer,实现代码和播放单个视频一样,只是需要注意:不要开启音频焦点监听。
    • 如果是开启的音频焦点改变监听,那么播放该视频的时候,就会停止其他音视频的播放操作。类似,你听音乐,这个时候去看视频,那么音乐就暂停呢

6.2 列表播放视频

  • 关于列表播放视频,该案例支持
    • 列表页面有多个item
      • 第一种:点击item播放,当item滑动到不可见时暂停播放;点击其他可见item播放视频,则会暂停其他正在播放的视频,也就是说一次只能播放一个视频
      • 第二种:滑动item,用户不用点击,让其自动进行播放,这种业务场景在玩手机碰到过。大概思路时,进入列表自动播放第一个,然后在RecyclerView滑动监听的方法中,判断如果页面滑动停止了,则遍历RecyclerView子控件找到第一个完全可见的item,然后拿到该item的索引即可播放该位置的视频
    • 列表页面是一个页面一个item
      • 第一种操作使用ViewPager,是垂直方向可以滚动的VerticalViewPager + PagerAdapter,这种方式在item创建上可以设置预加载加载布局视图
      • 第二种操作使用RecyclerView,是用ScrollPageHelper + RecyclerView,这种方式也可以实现一个页面一个item,一次滑动一个
  • 如何保证在列表中只播放一个视频。两种方案
    • 第一种:每个item放一个VideoPlayer,但是要注意需要用一个单例VideoPlayerManager来保证只有一个VideoPlayer对象,这样就可以保证一次播放一个视频。当ViewHolder中的视图被回收时需要销毁视频资源
    • 第二种:只创建一个VideoPlayer,那个播放就添加到具体的item布局中。比如播放第一个视频就把player对象添加到视图中,点击播放第三个时需要把player从它的父布局中移除后然后再添加到该item的布局中,这样就可以实现
  • list条目中滑动item不可见就停止视频播放
    • 在列表中播放,可以监听RecyclerView中的item生命周期,有一个AttachedToWindow是绑定item视图,还有一个DetachedFromWindow方法是item离开窗口时调用,在这个里面可以做视频销毁的逻辑。

07.如何自定义播放器

  • BasisVideoController已经满足基础视频播放器功能
    • 在该控制器中,已经做了相关的初始化操作,比如设置视频可以拖动,根据屏幕方向自动进入/退出全屏,设置滑动调节亮度,音量,进度,默认开启等操作。
    • 快速添加基础视频播放器的模块,包括视频播放完成view,播放异常view,播放top视图view,播放底部控制蓝view,手势滑动视图view等。同时在每一个视图view中可以拿到视频播放器的状态,便于设置UI的操作。
  • 比如在此播放器基础上,添加广告视图view
    • 现在有个业务需求,需要在视频播放器刚开始添加一个广告视图,等待广告倒计时120秒后,直接进入播放视频逻辑。相信这个业务场景很常见,大家都碰到过,使用该播放器就特别简单。
    • 首先创建一个自定义view,需要实现InterControlView接口,重写该接口中所有抽象方法,这里省略了很多代码,具体看demo。最后在调用controller.addControlComponent(adControlView);添加到基础视频播放器,这种方式满足大多数的场景……
  • 那要是基础播放器不满足UI该怎么办?
    • 好办,直接仿照BasisVideoController创建一个你自己的控制器,ui想怎么定制你自己决定。比如说你要实现一个小窗口播放视频,那这个时候肯定需要定制,照葫芦画瓢,具体可以看CustomFloatController类。

08.该案例的拓展性分享

  • 可以配置多个内核切换
    • 只需要你在配置的时候,传入不同的类型即可创建不同的播放器内核,十分方便。如果后期你要拓展其他的内核播放器,只需要按照exo的代码案例弄一套即可,十分方便,加入其他内核播放器不会影响到你的业务。
    PlayerFactory player = PlayerFactoryUtils.getPlayer(PlayerConstant.PlayerType.TYPE_IJK);
    
  • 可以配置统一视频埋点监听
    • 避免在每个带有视频的页面activity或者Fragment中添加埋点,而是有播放器框架内部提供一个埋点的接口,外部开发者只需要实现这个接口即可全局埋点视频播放器,非常方便和管理维护,针对接口增加或者删除都是不影响你其他的业务。
  • 开发者可以自由添加自定义视频视图
    • 在封装BaseVideoController控制器的时候,考虑到后期的拓展性,把视频各个视频都是以addView的形式添加进来,使用LinkedHashMap存储这样可以保证顺序。
    • 需要注意的是在这个Controller中,需要把播放器的播放状态,播放模式,播放进度,锁屏等操作给绑定到开发者自定义实现的播放器视图View中。
  • 暴露众多视频操作的方法给开发者
    • 比如给视频设置封面图片,这个时候总不能在播放器内部引入一个Glide,然后加载图片,这样和业务耦合呢。可以把这个设置封面view暴露给开发者,然后设置,这样更好一些。
    • 比如外部开发者想要知道视频播放器的状态,做一些业务上操作,这个时候完全可以通过接口的形式暴露出来,该播放器把视频的播放模式监听,播放状态监听,还有各种视频操作都暴露了方法出来,方便开发者调用。

09.关于视频缓存方案

  • 网络上比较好的项目:github.com/danikula/An…
    • 网络用的HttpURLConnection,文件缓存处理,文件最大限度策略,回调监听处理,断点续传,代理服务等。
  • 但是存在一些问题,比如如下所示
    • 文件的缓存超过限制后没有按照lru算法删除,
    • 处理返回给播放器的http响应头消息,响应头消息的获取处理改为head请求(需服务器支持)
    • 替换网络库为okHttp(因为大部分的项目都是以okHttp为网络请求库的),但是这个改动性比较大
  • 然后看一下怎么使用,超级简单。传入视频url链接,返回一个代理链接,然后就可以呢
    HttpProxyCacheServer server = new HttpProxyCacheServer(this);
    String proxyVideoUrl = server.getProxyUrl(URL_AD);
    
  • 大概的原理
    • 原始的方式是直接塞播放地址给播放器,它就可以直接播放。现在我们要在中间加一层本地代理,播放器播放的时候(获取数据)是通过我们的本地代理的地址来播放的,这样我们就可以很好的在中间层(本地代理层)做一些处理,比如:文件缓存,预缓存(秒开处理),监控等。
  • 原理详细一点来说
  • 如何实现预加载
    • 其实预加载的思路很简单,在进行一个播放视频后,再返回接下来需要预加载的视频url,启用线程去请求下载数据
    • 开启一个线程去请求并预加载一部分的数据,可能需要预加载的数据大于>1,利用队列先进入的先进行加载,因此可以采用LinkedHashMap保存正在预加载的task。
    • 在开始预加载的时候,判断该播放地址是否已经预加载,如果不是那么创建一个线程task,并且把它放到map集合中。然后执行预加载逻辑,也就是执行HttpURLConnection请求
    • 提供取消对应url加载的任务,因为有可能该url不需要再进行预加载了,比如参考抖音,当用户瞬间下滑几个视频,那么很多视频就需要跳过了不需要再进行预加载。这个后期在做

10.如何监控视频埋点

  • 传统一点的做法
    • 比如用友盟或者百度统计,或者用其他的统计。之前的做法是,在每个有视频的页面比如说Activity,Fragment等开启时视频播放时埋点一次,页面退出时埋点一次。
    • 如果app中有多个activity或者fragment页面,那么就每个页面都要进行埋点。比如如果你的app是付费视频,你想知道有多少人试看了,该怎么操作。那么你需要在每一个有视频的activity页面挨个添加埋点,那还有没有更好的办法?
  • 解决方案
    • 举个例子:例如,你需要来让外部开发者手动去埋点,可是在类中怎么埋点又是由其他人来设计的,你只是需要对外暴露监听的方法。那么该如何做呢?采用接口 + 实现类方式即可实现。
  • 该案例中怎么操作
    • 定义一个接口,规定其他人设计类,必须继承这个接口。在这个接口中,定义进入视频播放,退出视频播放器,记录播放进度,视频播放完成,播放异常,点击广告,点击试看等操作的抽象方法。具体可以看BuriedPointEvent类代码……
  • 外部开发者如何使用
    • 定义一个类实现该视频埋点接口,重写里面方法。然后需要在初始化配置视频播放器的时候,将这个实现类的对象传递进来即可。通过这个配置类传进来的对象,播放器就可以处理监听设置逻辑呢。
    • 这种操作最大的好处就是:在这个类中统一处理视频的埋点,修改快捷,而不用在每一个有视频播放器的页面埋点,方便维护。比如如何处理视频播放完成监听,代码如下所示:
    @Override
    public void onCompletion() {
        VideoPlayerConfig config = VideoViewManager.getConfig();
        if (config!=null && config.mBuriedPointEvent!=null){
            //视频播放完成
            config.mBuriedPointEvent.playerCompletion(mUrl);
        }
    }
    

11.待实现的需求分析

  • 音视频无缝切换
    • 比如在豆神教育中,有视频播放,也有音频播放,这两块都是写到了业务代码中,能否将两者糅合起来。但音频相比视频,多了一个可以在后台播放的功能,一般用在service中,这一相互切换需求待完善。以满足后期可能出现的需求功能。
  • 优化播放器持续平滑播放
    • 画中画方案:虽然Android8.0及其以上版本已提供了画中画方案,但是Android8.0以下版本仍然保有大量用户,其缺点就是无法满足Android8.0以下用户需;
    • 采用系统浮层:采用系统浮层需要系统浮层权限,Android厂商对系统浮层的授权越来越严格,导致用户授权过程的体验比较差;需要权限,可能有些手机不太好适配;
    • 在每个展示页面单独添加播放器浮窗:优点是不受Android系统版本限制,并且用户无需系统浮层权限授权,适合所有手机用户,体验较好

12.一些细节上优化

  • 多使用注解限定符
    • 对于一些关于类型的方法参数,可以多用注解限定符,暴露给外部开发者调用的方法,可以防止传入正确的类型。比如:PlayerFactoryUtils.getPlayer(PlayerConstant.PlayerType.TYPE_IJK)
  • 完善的api文档
    • api文档充分完善到每一个细节,以及配套demo,方便快速上手。完善的代码注释,以及项目的类结构图,方便快速了解视频播放器的整体轮廓
  • 丰富的demo案例
    • 提供绝大多数场景的视频播放器功能,完全可以套用demo中的案例,甚至你还可以在案例基础上大幅度优化
  • 日志做到过滤
    • 比如,视频内核,日志过滤则是:VideoKernel;比如,视频player,日志过滤则是:VideoPlayer;比如,缓存模块,日志过滤则是:VideoCache
  • 该库异常code说明
    • 针对视频封装库,统一处理抛出的异常,为了方便开发者快速知道异常的来由,则可以查询约定的code码。这个在sdk中特别常见,因此该库一定程度是借鉴腾讯播放器……

13.参考案例和博客记录

项目地址:github.com/yangchong21…