04.视频播放器通用架构实践
目录介绍
- 01.视频播放器的痛点
- 02.业务需求的目标
- 03.该播放器框架特点
- 04.播放器内核封装
- 05.播放器UI层封装
- 06.如何简单使用
- 07.如何自定义播放器
- 08.该案例的拓展性分享
- 09.关于视频缓存方案
- 10.如何监控视频埋点
- 11.待实现的需求分析
- 12.一些细节上优化
- 13.参考案例和博客记录
00.视频播放器通用框架
- 基础封装视频播放器player,可以在ExoPlayer、MediaPlayer,声网RTC视频播放器内核,原生MediaPlayer可以自由切换
- 对于视图状态切换和后期维护拓展,避免功能和业务出现耦合。比如需要支持播放器UI高度定制,而不是该lib库中UI代码
- 针对视频播放,音频播放,播放回放,以及视频直播的功能。使用简单,代码拓展性强,封装性好,主要是和业务彻底解耦,暴露接口监听给开发者处理业务具体逻辑
- 该播放器整体架构:播放器内核(自由切换) + 视频播放器 + 边播边缓存 + 高度定制播放器UI视图层
- 项目地址:https://github.com/yangchong211/YCVideoPlayer
- 关于视频播放器整体功能介绍文档:https://juejin.im/post/6883457444752654343
- 关于视频播放器通用架构实践:
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解耦:支持添加自定义视频视图,比如支持添加自定义广告,新手引导,或者视频播放异常等视图,这个需要较强的拓展性
- 适合多种业务场景
- 比如适合播放单个视频,多个视频,以及列表视频,或者类似抖音那种一个页面一个视频,还有小窗口播放视频。也就是适合大多数业务场景
- 播放器的整体层级图
- 播放器架构的介绍
- 基础内核播放库:提供基础的播放功能,可以自由切换内核,也方便拓展添加其他sdk内核播放器
- 统一播放器:屏蔽底层内核播放器播放差异,根据协议为上层提供统一的播放能力接口,供上层调用
- 播放视图层:负责播放器视图层的UI控制和调度,彻底解除播放业务与播放器的耦合
- 播放场景业务:负责向用户展示音视频播放能力和交互的业务
- 播放关联业务: 为播放器提供增值或支撑的业务,比如视频埋点统计,后期添加投屏,后期添加下载功能
- demo:提供各种播放场景案例代码,基本上有大多数常用播放器的使用场景,建议直接看demo拿来即用
04.播放器内核封装
4.0 遇到的问题
- 播放器内核拓展难
- 不同的播放SDK提供的API都不一样,如果业务层对每个合作方都进行业务开发,就会导致业务量非常庞大,并且不同合作的方的播放SDK会产生交叉,不利于播放业务的维护和拓展。
- 播放器内核难以切换
- 不同的视频播放器内核,由于api不一样,所以难以切换操作。要是想兼容内核切换,就必须自己制定一个视频接口+实现类的播放器
4.1 视频播放器内核封装需求
- 一定要解耦合
- 播放器内核与播放器解耦: 支持更多的播放场景、以及新的播放业务快速接入,并且不影响其他播放业务,比如后期添加阿里云播放器内核,或者腾讯播放器内核
- 传入不同类型方便创建不同内核
- 隐藏内核播放器创建具体细节,开发者只需要关心所需产品对应的工厂,无须关心创建细节,甚至无须知道具体播放器类的类名。需要符合开闭原则
- 具体设计方案
- 设计统一播放协议,对于上层播放业务,只调用按照统一协议设计接口,不必关心底层播放器的设计逻辑。保证上层播放业务不随新的接入播放SDK发生变化。
4.2 播放器内核架构图
- 播放器内核架构图
- 播放器内核代码说明
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接口)实现
- 具体如何实现呢
- 可以看这篇博客:06.播放器UI抽取封装
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
- 如何保证在列表中只播放一个视频。两种方案
- 第一种:每个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.关于视频缓存方案
网络上比较好的项目:https://github.com/danikula/AndroidVideoCache
- 网络用的HttpURLConnection,文件缓存处理,文件最大限度策略,回调监听处理,断点续传,代理服务等。
但是存在一些问题,比如如下所示
- 文件的缓存超过限制后没有按照lru算法删除,
- 处理返回给播放器的http响应头消息,响应头消息的获取处理改为head请求(需服务器支持)
- 替换网络库为okHttp(因为大部分的项目都是以okHttp为网络请求库的),但是这个改动性比较大
然后看一下怎么使用,超级简单。传入视频url链接,返回一个代理链接,然后就可以呢
HttpProxyCacheServer server = new HttpProxyCacheServer(this); String proxyVideoUrl = server.getProxyUrl(URL_AD);
大概的原理
- 原始的方式是直接塞播放地址给播放器,它就可以直接播放。现在我们要在中间加一层本地代理,播放器播放的时候(获取数据)是通过我们的本地代理的地址来播放的,这样我们就可以很好的在中间层(本地代理层)做一些处理,比如:文件缓存,预缓存(秒开处理),监控等。
原理详细一点来说
- 1.采用了本地代理服务的方式,通过原始url给播放器返回一个本地代理的一个url ,代理URL类似:http://127.0.0.1:port/视频url;(port端口为系统随机分配的有效端口,真实url是为了真正的下载),然后播放器播放的时候请求到了你本地的代理上了。
- 2.本地代理采用ServerSocket监听127.0.0.1的有效端口,这个时候手机就是一个服务器了,客户端就是socket,也就是播放器。
- 3.读取客户端就是socket来读取数据(http协议请求)解析http协议。
- 4.根据url检查视频文件是否存在,读取文件数据给播放器,也就是往socket里写入数据(socket通信)。同时如果没有下载完成会进行断点下载,当然弱网的话数据需要生产消费同步处理。
如何实现预加载
- 其实预加载的思路很简单,在进行一个播放视频后,再返回接下来需要预加载的视频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中的案例,甚至你还可以在案例基础上大幅度优化
13.参考案例和博客记录
- exo播放器
- ijk播放器
- 阿里云播放器
- GSY播放器
- 饺子播放器