04.视频播放器通用架构实践
2020-10-16
1270
简介:
04.视频播放器通用架构实践
04.视频播放器通用架构实践
目录介绍
- 01.视频播放器的痛点
- 02.业务需求的目标
- 03.该播放器框架特点
- 04.播放器内核封装
- 05.播放器UI层封装
- 06.如何简单使用
- 07.如何自定义播放器
- 08.该案例的拓展性分享
- 09.关于视频缓存方案
- 10.如何监控视频埋点
- 11.待实现的需求分析
- 12.一些细节上优化
- 13.参考案例和博客记录
00.视频播放器通用框架
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 如何兼容不同内核播放器
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 关于优先级视图展示
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.如何自定义播放器
08.该案例的拓展性分享
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.如何监控视频埋点
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播放器
-
饺子播放器
版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。