简述
视频播放是我们开发中比较常见的场景。这两年关于视频方面的热度不断提升,可以说前两年是直播年,今年是小视频年,各种短视频应用铺天盖地。对于视频的业务场景也越来越丰富,功能也越来越多。对于我们开发来说播放相关组件的代码变得也越来越复杂,管理维护成本也越来越高,面对不断迭代的业务,我们需要一种有效的方案来应对这种频繁的业务变化。
这几年一直在做视频相关的业务,手机端和TV端均做过适配开发。MediaPlayer、exoplayer、ijkplayer、VLC、FFmpeg等都摸索使用过。这一路遇到很多问题……说多了都是泪,为了适应多变的产品需求,中间重构了N多个版本。最终PlayerBase也就诞生了。PlayerBase3 版本进行了完整重构设计,目前大致框架基本已稳定下来。对于大部分应用视频播放组件场景都能轻松处理。
^_^ star传送门--->项目地址:github.com/jiajunhui/P…
框架简介
请注意! 请注意! 请注意! PlayerBase区别于大部分播放器封装库。
PlayerBase是一种将解码器和播放视图组件化处理的解决方案框架。您需要什么解码器实现框架定义的抽象引入即可,对于视图,无论是播放器内的控制视图还是业务视图,均可以做到组件化处理。将播放器的开发变得清晰简单,更利于产品的迭代。
PlayerBase不会为您做任何多余的功能业务组件,有别于大部分播放器封装库的通过配置或者继承然后重写然后定制你需要的功能组件和屏蔽你不需要的功能组件(这种之前我也经历过,上层可能需要经常改动,感觉很low!!!)。正确的方向应该是需要什么组件就拓展添加什么组件,不需要时移除即可,而不是已经提供了该组件去选择用不用。
功能特色
- 视图的组件化处理
- 视图组件的高复用、低耦合
- 解码方案的组件化、配置化管理
- 视图组件的完全定制
- 视图组件的热插拔,用时添加不用时移除
- 自定义接入各种解码方案
- 解码方案的切换
- 支持倍速播放
- 支持Window模式播放
- 支持Window模式的无缝续播
- 支持列表模式的无缝续播
- 支持跨页面无缝续播
- 支持调整画面显示比例
- 支持动态调整渲染视图类型
- 支持VideoView切角处理,边缘阴影效果
- 提供自定义数据提供者
- 统一的事件下发机制
- 扩展事件的添加
- 等功能……
部分使用示例
使用
需要的权限,如果targetSDK版本在Android M以上的,请注意运行时权限的处理。
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
添加如下依赖
dependencies { compile 'com.kk.taurus.playerbase:playerbase:3.2.8.5' }
代码混淆时,请在proguard中添加如下保护
-keep public class * extends android.view.View{*;} -keep public class * implements com.kk.taurus.playerbase.player.IPlayer{*;}
初始化
public class App extends Application { @Override public void onCreate() { //... //如果您想使用默认的网络状态事件生产者,请添加此行配置。 //并需要添加权限 android.permission.ACCESS_NETWORK_STATE PlayerConfig.setUseDefaultNetworkEventProducer(true); //初始化库 PlayerLibrary.init(this); } }
- 解码配置和框架初始化
public class App extends Application { @Override public void onCreate() { //... //如果您想使用默认的网络状态事件生产者,请添加此行配置。 //并需要添加权限 android.permission.ACCESS_NETWORK_STATE PlayerConfig.setUseDefaultNetworkEventProducer(true); //设置默认解码器 int defaultPlanId = 1; PlayerConfig.addDecoderPlan(new DecoderPlan(defaultPlanId, IjkPlayer.class.getName(), "IjkPlayer")); PlayerConfig.setDefaultPlanId(defaultPlanId); //初始化库 PlayerLibrary.init(this); } }
- 组装组件(添加您需要的组件【组件来自用户自定义,框架不提供任何视图组件】)
ReceiverGroup receiverGroup = new ReceiverGroup(); //Loading组件 receiverGroup.addReceiver(KEY_LOADING_COVER, new LoadingCover(context)); //Controller组件 receiverGroup.addReceiver(KEY_CONTROLLER_COVER, new ControllerCover(context)); //CompleteCover组件 receiverGroup.addReceiver(KEY_COMPLETE_COVER, new CompleteCover(context)); //Error组件 receiverGroup.addReceiver(KEY_ERROR_COVER, new ErrorCover(context));
- 设置组件启动播放
BaseVideoView videoView = findViewById(R.id.videoView); videoView.setReceiverGroup(receiverGroup); DataSource data = new DataSource("http://url..."); videoView.setDataSource(data); videoView.start();
- 事件的监听
//player event videoView.setOnPlayerEventListener(new OnPlayerEventListener(){ @Override public void onPlayerEvent(int eventCode, Bundle bundle){ //... } }); //receiver event videoView.setOnReceiverEventListener(new OnReceiverEventListener(){ @Override public void onReceiverEvent(int eventCode, Bundle bundle) { //... } });
详细使用示例请参阅github项目主页及wiki介绍
框架的设计
视图的处理
别小看一个小小的播放器,里面真的是别有洞天。有时视图组件复杂到你怀疑人生。
我们先看下播放器开发时常见的一些视图场景:
以上是我们最常见到的一些视图(其实还有很多,比如清晰度切换、视频列表、播放完成提示页等等),这些视图如果没有一个行之有效的方案来进行管理,将逐渐会乱到失控。
上面只是列出了控制器视图、加载视图、手势视图、错误视图、弹幕视图和广告视图,这一股脑的视图都是和播放紧密相连的,完全由播放状态驱动,视图之间可能共存、可能制约。
那么这些视图如何进行统一的管理呢?光布局文件就够喝一壶了吧,即便用include来管理依然摆脱不了显示层级的管理问题。要是一股脑全写到一个xml中,想想都可怕……, 改进型的一般都是把每个组件封装成View了,然后再分别写到布局中,显然比前一种要轻松一些。但是,但是播放器和组件间的通信、组件与组件间的通信是个问题。依然有问题存在:
- 组件布局的层级完全由布局文件决定了,想调整只能去修改布局文件。并不友好。
- 组件和播放器完全捆绑了,耦合度相当高,播放器和组件,组件和组件间的通信完全直接使用引用去操作,如果产品说某个组件要去掉或者大改,你就哭吧,改不好手一哆嗦就有可能带来一堆bug。组件耦合度高,并不支持插拔。这是最大阻碍。
- 组件的复用困难。
接下来,且看PlayerBase如何做。
接收者Receiver与覆盖层Cover的概念
做过播放器开发的应该都很清楚一点,所有视图的工作都是由状态事件来驱动的,这是一条主线。有可能是来自播放器的事件(比如解码器出错了),也有可能是来自某个视图的事件(比如手势调节播放进度),还有可能是外部事件(比如网络状态变化)。
这些信息我们可以归结为
- 视图是事件接收者,也是事件的生产者
- 解码器是事件生产者
- 可能有外来的事件生产者
也就是说我们把视图当做事件接收者,同时视图具备发送事件的能力。
解码器不断发出自己工作状态的事件要传递给视图。
外部的某些事件也需要传递给视图
至此,框架内部定义了事件接收者的概念,接收者作为事件消费者的同时也能生产事件,而覆盖层继承自接收者引入了视图View。
public abstract class BaseReceiver implements IReceiver { //... protected final void notifyReceiverEvent(int eventCode, Bundle bundle){ //.. } /** * all player event dispatch by this method. */ void onPlayerEvent(int eventCode, Bundle bundle); /** * error event. */ void onErrorEvent(int eventCode, Bundle bundle); /** * receivers event. */ void onReceiverEvent(int eventCode, Bundle bundle); /** * you can call this method dispatch private event for a receiver. * * @return Bundle Return value after the receiver's response, nullable. */ @Nullable Bundle onPrivateEvent(int eventCode, Bundle bundle); }
public abstract class BaseCover extends BaseReceiver{ //... public abstract View onCreateCoverView(Context context); //... }
且看代码,有播放器的事件、有错误事件、有组件(Receiver)间的事件。这众多事件如何下发呢,如果有N多个接收者呢,如何破?
接收者组管理(ReceiverGroup)
ReceiverGroup的出现目的就是对众多接收者进行统一的管理,统一的事件下发,当然还有下面的数据共享问题。来张图:
在ReceiverGroup中包含Cover(其实也是Receiver)和Receiver,提供了Receiver的添加、移除、遍历、销毁等操作。当有事件需要下发时,便可通过ReceiverGroup进行统一的遍历下发。
public interface IReceiverGroup { void setOnReceiverGroupChangeListener(OnReceiverGroupChangeListener onReceiverGroupChangeListener); /** * add a receiver, you need put a unique key for this receiver. * @param key * @param receiver */ void addReceiver(String key, IReceiver receiver); /** * remove a receiver by key. * @param key */ void removeReceiver(String key); /** * loop all receivers * @param onLoopListener */ void forEach(OnLoopListener onLoopListener); /** * loop all receivers by a receiver filter. * @param filter * @param onLoopListener */ void forEach(OnReceiverFilter filter, OnLoopListener onLoopListener); /** * get receiver by key. * @param key * @param <T> * @return */ <T extends IReceiver> T getReceiver(String key); /** * get the ReceiverGroup group value. * @return */ GroupValue getGroupValue(); /** * clean receivers. */ void clearReceivers(); }
组件间数据共享(GroupValue)
播放器开发中很多时候我们需要依据某个视图的状态来限制另外视图的功能或状态,比如当处于加载中时禁止拖动进度条或者播放出错显示error后禁止其他视图操作等等。这些都属于状态上的相互制约。
GroupValue就相当于提供了一个共享的数据池,当某个数据被刷新时,监听该数据的回调接口能及时收到通知,当然也可以直接去主动获取数据状态。你可以指定你要监听那些数据的更新事件,如果您注册了您要监听的数据的key值,其对应的value被更新时,您就会收到回调。然后您可以在回调中进行UI视图的控制。
public class CustomCover extends BaseCover{ //... @Override public void onReceiverBind() { super.onReceiverBind(); getGroupValue().registerOnGroupValueUpdateListener(mOnGroupValueUpdateListener); } //... private IReceiverGroup.OnGroupValueUpdateListener mOnGroupValueUpdateListener = new IReceiverGroup.OnGroupValueUpdateListener() { @Override public String[] filterKeys() { return new String[]{ DataInter.Key.KEY_COMPLETE_SHOW }; } @Override public void onValueUpdate(String key, Object value) { //... } }; //... @Override public void onReceiverUnBind() { super.onReceiverUnBind(); getGroupValue().unregisterOnGroupValueUpdateListener(mOnGroupValueUpdateListener); } }
视图的布局管理(分级填充)
上文中常见的视图组件,我们在使用中肯定会遇到覆盖优先级的问题。举个栗子,比如Error视图出现后其他的视图一概不可见,也就是说Error视图的优先级是最高的,谁都不能挡着它,我们创建了一个个的Cover视图,对于视图的放置就需要一个视图的优先级标量(CoverLevel)来进行控制,不同的Level的Cover视图会被放置于不同级别的容器内。
总结为以下:
- 指定Cover的优先级CoverLevel
- Cover组件被添加时自动根据Level值进行分别放置
示意图
代码示例public class CustomCover extends BaseCover{ //... @Override public int getCoverLevel() { return ICover.COVER_LEVEL_LOW; } //... }
默认的视图容器管理器
public class DefaultLevelCoverContainer extends BaseLevelCoverContainer { //... @Override protected void onAvailableCoverAdd(BaseCover cover) { super.onAvailableCoverAdd(cover); switch (cover.getCoverLevel()){ case ICover.COVER_LEVEL_LOW: mLevelLowCoverContainer.addView(cover.getView(),getNewMatchLayoutParams()); break; case ICover.COVER_LEVEL_MEDIUM: mLevelMediumCoverContainer.addView(cover.getView(),getNewMatchLayoutParams()); break; case ICover.COVER_LEVEL_HIGH: mLevelHighCoverContainer.addView(cover.getView(),getNewMatchLayoutParams()); break; } } //... }
组件的层级关系
如图:
事件生产者(EventProducer)
顾名思义,就是它是产生事件的源。比如系统网络状态发生了变化,发出了通知,然后各个应用根据自己的情况来调整显示或设置等。又或者电池电量的变化和低电量预警通知事件等。
再比如,我们上文中的弹幕视图中需要显示弹幕数据,弹幕数据来自服务器,我们需要源源不断的从服务器上取数据,然后显示在弹幕视图。取回数据传给视图的这个过程我们可以将其看作是一个事件生产者在不断生产弹幕数据更新事件,弹幕数据更新时不断将事件发送给弹幕视图来刷新显示。
框架内自带了一个网络变化事件生产者的示例:
public class NetworkEventProducer extends BaseEventProducer { //... private Handler mHandler = new Handler(Looper.getMainLooper()){ @Override public void handleMessage(Message msg) { super.handleMessage(msg); switch (msg.what){ case MSG_CODE_NETWORK_CHANGE: int state = (int) msg.obj; //...将网络状态发送出去 getSender().sendInt(InterKey.KEY_NETWORK_STATE, state); PLog.d(TAG,"onNetworkChange : " + state);