做过播放器的朋友一定知道,播放器是一个非常复杂的自定义View,它的复杂主要在于各种元素之间的相互作用,比如说:
- 外部事件作用于播放器(播放器根据开关wifi控制是否自动播放)
- 某个遮罩控件上的元素被点击(点击全屏按钮播放器进入全屏)
- 播放器解码错误(解码错误需要显示错误遮罩,提供重试按钮)
等等。
如果把这些元素、功能统统写在一个类中,会显得非常复杂,难以维护。而PlayerBase 是一个拥有良好架构的播放器组件,它对播放器以及遮罩(Cover)的行为有着良好的抽象,也提供一系列的回调接口。前端时间重构项目中的视频组件我就参考了这个项目。接下来我们分析一下PlayerBase的的结构与实现原理。
一、播放器中View的组成结构
我们平时项目中封装的播放器,由两个主要元素组成——遮罩(Cover)与播放器(Player),他们的View层级一般是这样的:
PlayerBase也是类似的,它的稍显复杂一点(嵌套层级变多了):
根布局是BaseVideoView,这就是封装的播放器。他包含了SuperContainer,这是一个自定义View,这里面包含着播放器中所有的功能View,包括mRenderContainer,它是SurfaceView或者TextureView的容器(为什么定义这个呢,主要是为了实现SurfaceView与TextureView的切换,个人感觉不是一个很重要的功能),还有“遮罩根布局”,这个根布局可以自定义,默认的是一个FrameLayout,里面包含着“Height、Low、Medium”三种等级的遮罩容器,所有的遮罩会按等级放入其中,“Heigh”在最上,“Low”在最下,提供分组管理遮罩的功能。
二、整体结构
在View结构图的基础上,我们再加上一些关键角色:
1、SuperContainer
SuperContainer中首先包含一个FrameLayout——mRenderContainer,上面已有简单介绍,它会通过addView加入SuperContainer中。
其次,有一个ICoverStrategy对象,默认实现是DefaultLevelCoverContainer,这个是可以自定义的,只要实现ICoverStrategy即可,默认实现中,它包含了所有的遮罩——BaseCover,BaseCover拥有Cover的行为,继承了ICover,它还是一个信息接收者,继承了IReceiver。ICoverStrategy有getContainView接口,用于获取第一张图里所说的的“遮罩根布局”,最后会通过addView加入SuperContainer中。
最后,还包含一个IEventDispatcher,它不是View,是一个信息中转站,它持有了一个列表,包含了所有的IReceiver,其中就有所有的BaseCover(BaseCover实现了IReceiver)。所有的信息都会回调到IEventDispatcher中,再传送给所有的IReceiver。
2、AVPlayer
AVPlayer是播放器的一层装饰,它持有一个播放器的实例对象BaseInternalPlayer,PlayerBase框架中适配了三种播放器:SysMediaPlayer(适配了Android自带的MediaPlayer)、ExoMediaPlayer(适配了谷歌封装的SimpleExoPlayer)、IjkPlayer(适配了bilibili的IjkMediaPlayer),它们适配的接口是IPlayer。
AVPlayer在BaseInternalPlayer之上增加了一点功能,使用了装饰器模式;三种实现的播放器,使用了对象适配器模式,从而我们可以做到轻松切换这三种播放器。
3、IRender
IRender接口统一了SurfaceView与TextureView的行为,同样是用了适配器模式,作者这里是为了实现SurfaceView与TextureView的切换。IRender会在BaseVideView的setRenderType方法中创建,此时会创建一个匿名内部类的实例mRenderCallback,它会对SurfaceHolder.Callback
或者SurfaceTextureListener
的事件进行回调,在surfaceCreated
或onSurfaceTextureAvailable
时绑定播放器。最终,会通过IRender的getRenderView方法获取SurfaceView或TextureView,使用addView加入mRenderContainer。
三、BaseVideoView
PlayerBase中有着复杂但有规律的回调关系,要弄懂这些关系,最好的方式就是画图,接下来会展示几张关于方法、对象的调用关系图,他们的作图规则如下:
浅蓝色大方框代表当前类;深蓝色方框代表类中供外界调用的重要公共方法;肉色方框代表对象,紫色文字是对象名,加粗斜体的白色文字是接口名或抽象类名,正常的白色文字是实现类的名字;浅绿色方框与黑色方框都是说明文字。
好,我们来看BaseVideoVIew中的调用关系是怎样的:
(图中箭头上的“包含”意思是被指向的对象会被赋值到发起指向的对象中)
首先我们看三个黑色虚线方框,第一个方框中包括了BaseVideoView中的四个主要角色:mSuperContainer、mStyleSetter(可以忽略)、mPlayer、mRender,前面都有所介绍。往右看是第二个方框——通过匿名内部类实现的接口,这些接口的实现类似下面的代码:
private OnReceiverEventListener mInternalReceiverEventListener = (int eventCode, Bundle bundle) -> {
......
};
他们统一的命名方式是“mInternalXXXListener”,当BaseVideoView内部有事件触发时,会统一先调用对应的“Internal Listener”,然后回调到通过set方法传入的接口或者其他回调接口或者角色,所以上图信息传递方向是这样的:
而我们可以在BaseVideoView之外实现“Set Listener”定义我们自己的事件回调。举个例子,我们看图中最顶端的一个关系链(看白色斜体文字,图中后两个对象属于同一个接口类):
mSuperContainer中设置了mInternalReceiverEventListener,而mInternalReceiverEventListener是BaseVideoView中的匿名内部类,可以调用其中的mOnReceiverEventListener,而mOnReceiverEventListener正是我们在外部定义好后设置进来的回调接口。从而这里实现了将mSuperContainer中的事件回调给外部的功能。
整个PlayerBase框架中经常使用这种多个回调接口“接力”的方式来实现回调。
四、SuperContainer
看图中右侧的两个黑色方框,SuperContainer也拥有着与BaseVideoView同样的结构——“Internal Listener”与“Set Listener”,“接力”回调。
1、View容器
图中顶部的黑色方框——View容器,相信大家都能理解,mRenderContainer直接addView加入SuperContainer中;mCoverStrategy获取ContainerView后,再通过addView加入SuperContainer中,之后会被加入各种遮罩(BaseCover)。
2、EventDispatcher
EventDispatcher是整个播放器的事件分发中心,这是PlayerBase最核心的设计,它实现了播放器中所有角色都可以发起事件,并且所有实现IReceiver的角色都可以接收到事件。
EventDispatcher中包含着一个IReceiverGroup,它存放着一系列的IReceiver,这些IReceiver可以通过setReceiverGroup方法设置,这些IReceiver可以是BaseCover,也可以是纯粹的IReceiver。
在设置ReceiverGroup的时候,mReceiverGroup会通过OnLoopListener遍历每一个IReceiver: 源码如下:
// 传入接口实例,其中调用了attachReceiver
mReceiverGroup.forEach(new IReceiverGroup.OnLoopListener() {
@Override
public void onEach(IReceiver receiver) {
attachReceiver(receiver);
}
});
private List<IReceiver> mReceiverArray;
@Override
public void forEach(OnLoopListener onLoopListener) {
forEach(null, onLoopListener);
}
@Override
public void forEach(OnReceiverFilter filter, OnLoopListener onLoopListener) {
for(IReceiver receiver:mReceiverArray){
if(filter==null || filter.filter(receiver)){
onLoopListener.onEach(receiver);
}
}
}
在attachReceiver方法中,如果IReceiver实例是BaseCover的子类,就会调用mCoverStrategy.addCover,将BaseCover加入ICoverStrategy对象中。
attachReceiver还会调用receiver.bindStateGetter,用于receiver内部获取当前播放器的状态、进度等数据。回顾BaseVideoView中的关系我们发现,mStateGetter实际上是BaseVideoView中的匿名内部类的实例——mInternalStateGetter,然后通过mInternalPlayerStateGetter直接调用mPlayer返回对应数据。
BaseVideoView创建StateGetter的源码:
//Internal StateGetter for SuperContainer
private StateGetter mInternalStateGetter = new StateGetter() {
@Override
public PlayerStateGetter getPlayerStateGetter() {
return mInternalPlayerStateGetter;
}
};
//Internal PlayerStateGetter for StateGetter
private PlayerStateGetter mInternalPlayerStateGetter = new PlayerStateGetter() {
@Override
public int getState() {
// 直接调用player方法
return mPlayer.getState();
}
@Override
public int getCurrentPosition() {
// 直接调用player方法
return mPlayer.getCurrentPosition();
}
......
};
所以,每个receiver都可以实时获取播放器的状态、进度等数据。
最后,attachReceiver还会调用receiver.bindReceiverEventListener,给每个receiver设置一个回调监听——mInternalReceiverEventListener,它会调用mEventDispatcher的方法,将事件传递给所有IReceiver。同时,它也会调用一个(在BaseVideoView中)通过set方法传入的相同类型的接口——mOnReceiverEventListener,这个接口又会回调我们通过set方法设置的两个自定义回调监听:
仔细看完整的大图,你会发现所有的事件都会回调到EventDispatcher中,并传递给每一个IReceiver;每一个IReceiver又可以通过自身内设的接口回调事件给EventDispatcher,从而实现每个Cover都可以主动发起事件。
看,播放器的事件也是会回调给EventDispatcher(并且还可以回调给外部的回调接口):
3、EventProducer
我们看到中间“addEventProducer”的一条线:
EventProducer有什么用?它可以让事件从外部发起,播放器响应。mProducerGroup可以存放多个EventProducer,而BaseEventProducer中包含一个ReceiverEventSender接口,我们在实现它的时候可以通过ReceiverEventSender发送事件,然后会调用mDelegateReceiverEventSender的相关方法,最后调用mEventDispatcher的分发事件方法,发送给播放器中的每一个IReceiver。
五、AVPlayer
与BaseVideoView类似,AVPlayer的回调接口也分为两大部分——“Internal Listener”与“Set Listener”。大家可以根据上面给出的三个大图了解整个PlayerBase的运行原理,核心就是一个词——信息分发。
1、switchDecoder
前面我们知道AVPlayer内包含一个BaseInternalPlayer实例,而我们常用的三种播放器——MediaPlayer、SimpleExoPlayer、IjkMediaPlayer都适配了BaseInternalPlayer(实现IPlayer接口),所以这三种播放器可以在AVPlayer中自由切换,这正是AVPlayer的强大之处。
在使用PalyerBase之前,我们需要配置对应的播放器策略:
PlayerConfig.addDecoderPlan(new DecoderPlan(PLAN_ID_IJK, IjkPlayer.class.getName(), "IjkPlayer"));
PlayerConfig.addDecoderPlan(new DecoderPlan(PLAN_ID_EXO, ExoMediaPlayer.class.getName(), "ExoPlayer"));
PlayerConfig.setDefaultPlanId(PLAN_ID_IJK);
switchDecoder方法会通过PlanId获取对应的DecoderPlan,通过反射创建对应的播放器,然后赋值给mInternalPlayer,完成播放器的切换。
2、DataSource
我们回顾BaseVideoView的大图,AVPlayer的setDataSource方法实际是由BaseVideoView的setDataSource调用的,其中会先调用setRenderType,这个方法会生成对应的IRender(Surface或者Texture),然后绑定播放器与SurfaceView或者TextureView。其次就调用AVPlayer中的setDataSource方法,然后将DataSource传递给播放器,同时生成一系列的回调接口实例。
3、DataProvider
DataProvider提供了异步获取DataSource的功能,最终也会回调到interPlayerSetDataSource方法,给播放器设置DataSource。在BaseVideoView中也有着setDataProvider方法,会调用到AVPlayer中的setDataProvider。
六、结语
其实播放器的难点就是如何传递信息:Cover的事件如何传递给其他Cover以及Player,Player的事件如何通知Cover,以及如何响应外部事件,如何将内部事件回调给外部:
PlayerBase的核心就是EventDispatcher,根据三张大图搞清楚都有哪些组件的事件会回调给EventDispatcher,又有哪些组件会接收到这些信息,那么你对PlayerBase就算是摸透啦~
彩蛋:BaseCover继承于BaseReceiver,每个BaseReceiver都维护着一个相同IReceiverGroup,所以,每个Cover之间其实还实现了单独通讯(Private Event),demo中GestureCover的324行使用了这种方法,有兴趣的朋友可以去研读一下~