视频播放是咱们开发中比较常见的场景。这两年关于视频方面的热度不断提高,能够说前两年是直播年,今年是小视频年,各类短视频应用铺天盖地。对于视频的业务场景也愈来愈丰富,功能也愈来愈多。对于咱们开发来讲播放相关组件的代码变得也愈来愈复杂,管理维护成本也愈来愈高,面对不断迭代的业务,咱们须要一种有效的方案来应对这种频繁的业务变化。java
这几年一直在作视频相关的业务,手机端和TV端均作过适配开发。MediaPlayer、exoplayer、ijkplayer、VLC、FFmpeg等都摸索使用过。这一路遇到不少问题……说多了都是泪,为了适应多变的产品需求,中间重构了N多个版本。最终PlayerBase也就诞生了。PlayerBase3 版本进行了完整重构设计,目前大体框架基本已稳定下来。对于大部分应用视频播放组件场景都能轻松处理。android
^_^ star传送门--->项目地址:github.com/jiajunhui/P…git
QQ交流群:600201778 ,有问题群里直接提出,看到后会一一解答。github
P图技术有限,文中图片就凑合着看吧!服务器
请注意! 请注意! 请注意! PlayerBase区别于大部分播放器封装库。网络
PlayerBase是一种将解码器和播放视图组件化处理的解决方案框架。您须要什么解码器实现框架定义的抽象引入便可,对于视图,不管是播放器内的控制视图仍是业务视图,都可以作到组件化处理。将播放器的开发变得清晰简单,更利于产品的迭代。框架
PlayerBase不会为您作任何多余的功能业务组件,有别于大部分播放器封装库的经过配置或者继承而后重写而后定制你须要的功能组件和屏蔽你不须要的功能组件(这种以前我也经历过,上层可能须要常常改动,感受很low!!!)。正确的方向应该是须要什么组件就拓展添加什么组件,不须要时移除便可,而不是已经提供了该组件去选择用不用。ide
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介绍oop
别小看一个小小的播放器,里面真的是别有洞天。有时视图组件复杂到你怀疑人生。组件化
咱们先看下播放器开发时常见的一些视图场景:
以上是咱们最多见到的一些视图(其实还有不少,好比清晰度切换、视频列表、播放完成提示页等等),这些视图若是没有一个行之有效的方案来进行管理,将逐渐会乱到失控。
上面只是列出了控制器视图、加载视图、手势视图、错误视图、弹幕视图和广告视图,这一股脑的视图都是和播放紧密相连的,彻底由播放状态驱动,视图之间可能共存、可能制约。
那么这些视图如何进行统一的管理呢?光布局文件就够喝一壶了吧,即使用include来管理依然摆脱不了显示层级的管理问题。要是一股脑全写到一个xml中,想一想均可怕……, 改进型的通常都是把每一个组件封装成View了,而后再分别写到布局中,显然比前一种要轻松一些。可是,可是播放器和组件间的通讯、组件与组件间的通讯是个问题。依然有问题存在:
接下来,且看PlayerBase如何作。
作过播放器开发的应该都很清楚一点,全部视图的工做都是由状态事件来驱动的,这是一条主线。有多是来自播放器的事件(好比解码器出错了),也有多是来自某个视图的事件(好比手势调节播放进度),还有多是外部事件(好比网络状态变化)。
这些信息咱们能够归结为
也就是说咱们把视图当作事件接收者,同时视图具有发送事件的能力。
解码器不断发出本身工做状态的事件要传递给视图。
外部的某些事件也须要传递给视图
至此,框架内部定义了事件接收者的概念,接收者做为事件消费者的同时也能生产事件,而覆盖层继承自接收者引入了视图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中包含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();
}
复制代码
播放器开发中不少时候咱们须要依据某个视图的状态来限制另外视图的功能或状态,好比当处于加载中时禁止拖动进度条或者播放出错显示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视图会被放置于不一样级别的容器内。
总结为如下:
示意图
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;
}
}
//...
}
复制代码
如图:
顾名思义,就是它是产生事件的源。好比系统网络状态发生了变化,发出了通知,而后各个应用根据本身的状况来调整显示或设置等。又或者电池电量的变化和低电量预警通知事件等。
再好比,咱们上文中的弹幕视图中须要显示弹幕数据,弹幕数据来自服务器,咱们须要源源不断的从服务器上取数据,而后显示在弹幕视图。取回数据传给视图的这个过程咱们能够将其看做是一个事件生产者在不断生产弹幕数据更新事件,弹幕数据更新时不断将事件发送给弹幕视图来刷新显示。
框架内自带了一个网络变化事件生产者的示例:
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);
break;
}
}
};
//...
public NetworkEventProducer(Context context){
//...
}
//...
public static class NetChangeBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
//...
//post state message
}
//...
}
}
复制代码
因为事件生产者所发出的事件是针对Receiver的,因此会被回调到onReceiverEvent()中,若是发送的是key-value的数据,会被放置于GroupValue中。以下代码:
public class CustomCover extends BaseCover{
//...
@Override
public void onReceiverEvent(int eventCode, Bundle bundle) {
//...
}
//...
}
复制代码
DataProvider是为了播控的统一以及使用上的优雅而设计的。
在开发中,咱们可能会遇到以下场景:你拿到的数据源可能只是个id之类的标识,并非能直接播放的uri或者url,须要你再用这个id去请求一个接口才能拿到播放的源地址。一般咱们都是先去请求接口,而后在成功回调中用拿到的源数据再设置给播放器去播放。
public class MonitorDataProvider extends BaseDataProvider {
//...
public MonitorDataProvider(){
//...
}
private Handler mHandler = new Handler(Looper.getMainLooper());
@Override
public void handleSourceData(DataSource sourceData) {
this.mDataSource = sourceData;
//...provider start
onProviderDataStart();
//...
//...将数据回调出去
onProviderMediaDataSuccess(bundle);
//...
//...异常时
onProviderError(-1, null)
}
//...
@Override
public void cancel() {
//...cancel something
}
@Override
public void destroy() {
//...destroy something
}
}
复制代码
注意: 数据提供者必需要设置在启动播放前。
大体归结为如下步骤:
public class VideoViewActivity extends AppCompatActivity implements OnPlayerEventListener{
//...
BaseVideoView mVideoView;
@Override
public void onCreate(Bundle saveInstance){
super.onCreate(saveInstance);
mVideoView = findViewById(R.id.videoView);
mVideoView.setOnPlayerEventListener(this);
//设置数据提供者 MonitorDataProvider
MonitorDataProvider dataProvider = new MonitorDataProvider();
mVideoView.setDataProvider(dataProvider);
//...
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));
//...
DataSource data = new DataSource("monitor_id");
videoView.setDataSource(data);
videoView.start();
}
//...
public void onPlayerEvent(int eventCode, Bundle bundle){
switch (eventCode){
case OnPlayerEventListener.PLAYER_EVENT_ON_VIDEO_RENDER_START:
//...
break;
case OnPlayerEventListener.PLAYER_EVENT_ON_PLAY_COMPLETE:
//...
break;
}
}
//...
@Override
public void onPause(){
super.onPause();
mVideoView.pause();
//...
}
@Override
public void onResume(){
super.onResume();
mVideoView.onResume();
//...
}
@Override
public void onDestroy(){
super.onDestroy();
mVideoView.stopPlayback();
//...
}
}
复制代码
若是您想直接使用AVPlayer本身进行处理播放,那么大体步骤以下:
SuperContainer mSuperContainer = new SuperContainer(context);
ReceiverGroup receiverGroup = new ReceiverGroup();
//...add some covers
receiverGroup.addReceiver(KEY_LOADING_COVER, new LoadingCover(context));
mSuperContainer.setReceiverGroup(receiverGroup);
//...
final RenderTextureView render = new RenderTextureView(mAppContext);
render.setTakeOverSurfaceTexture(true);
//....
mPlayer.setOnPlayerEventListener(new OnPlayerEventListener() {
@Override
public void onPlayerEvent(int eventCode, Bundle bundle) {
//...此处须要根据事件自行实现一些特定的设置
//...好比视频的尺寸须要传递Render刷新测量或者视频的角度等等
//将事件分发给子视图
mSuperContainer.dispatchPlayEvent(eventCode, bundle);
}
});
mPlayer.setOnErrorEventListener(new OnErrorEventListener() {
@Override
public void onErrorEvent(int eventCode, Bundle bundle) {
//将事件分发给子视图
mSuperContainer.dispatchErrorEvent(eventCode, bundle);
}
});
//...
render.setRenderCallback(new IRender.IRenderCallback() {
@Override
public void onSurfaceCreated(IRender.IRenderHolder renderHolder, int width, int height) {
mRenderHolder = renderHolder;
bindRenderHolder(mRenderHolder);
}
@Override
public void onSurfaceChanged(IRender.IRenderHolder renderHolder, int format, int width, int height) {
}
@Override
public void onSurfaceDestroy(IRender.IRenderHolder renderHolder) {
mRenderHolder = null;
}
});
mSuperContainer.setRenderView(render.getRenderView());
mPlayer.setDataSource(dataSource);
mPlayer.start();
复制代码
若是非必须,请尽可能使用框架封装好的BaseVideoView进行播放,框架相对来讲处理的比较完善且提供了丰富的回调和定制性。
如今的短视频应用都有这样的场景:
对于第一条在列表中播放,理论上VideoView就能完成,可是VideoView用在列表中量级较重,不太适合。须要一个轻量化处理的方案。
而对于第二条,VideoView就不行了,VideoView是对解码器进行了包装,当跳到下一个页面时,是一个新的页面天然有新的视图,没法使用前一个页面的播放器实例去渲染当前页面播放。
其实对于这种无缝的续播,原理很简单。就是不一样的渲染视图使用同一个解码实例便可。能够简单比做一个MediaPlayer去不断设置不一样的surface呈现播放。若是本身处理这个过程的话想对比较繁琐,你须要处理Render的回调并关联给解码器,还须要本身处理Render的测量以及显示比例、角度等等问题。
RelationAssist 就是为了简化这个过程而设计的。在不一样的页面或视图切换播放时,您只须要提供并传入对应位置的视图容器(ViewGroup类型)便可。内部复杂的设置项和关联由RelationAssist完成。
public class TestActivity extends AppcompatActivity{
//...
RelationAssist mAssist;
ViewGroup view2;
public void onCreate(Bundle saveInstance){
super.onCreate(saveInstance);
//...
mAssist = new RelationAssist(this);
mAssist.setEventAssistHandler(eventHandler);
mReceiverGroup = ReceiverGroupManager.get().getLiteReceiverGroup(this);
mAssist.setReceiverGroup(mReceiverGroup);
DataSource dataSource = new DataSource();
dataSource.setData("http://...");
dataSource.setTitle("xxx");
mAssist.setDataSource(dataSource);
mAssist.attachContainer(mVideoContainer);
mAssist.play();
//...
switchPlay(view2);
}
//...
private void switchPlay(ViewGroup container){
mAssist.attachContainer(container);
}
}
复制代码
若是您想跨页面进行关联,只须要本身将RelationAssist包装为一个单例便可。此处不作代码展现,详细代码可参见github项目demo代码。
视图中的一些基本操做,好比暂停播放、重播、重试、恢复播放等等,这些事件最终都要传递给解码器进行相关操做。可能还有用户自定义的事件好比播放下一个或上一个等。
对于基本的操做事件(暂停、恢复、重播等),框架内部可自动完成,而用户自定的事件须要让用户自行处理。框架内部BaseVideoView和RelationAssist均作了EventAssistHandler的对接,使用时须要传入一个可用的事件处理器对象,可根据不一样的事件参数进行相应处理。以下代码:
mVideoView.setOnVideoViewEventHandler(new OnVideoViewEventHandler(){
@Override
public void onAssistHandle(BaseVideoView assist, int eventCode, Bundle bundle) {
//基本的事件处理已在父类super中完成,若是须要重写,重写相应方法便可。
super.onAssistHandle(assist, eventCode, bundle);
switch (eventCode){
case DataInter.Event.EVENT_CODE_REQUEST_NEXT:
//...播放下一个
break;
}
}
});
复制代码
咱们有时可能为了避免打断用户的浏览须要小窗播放。框架特地设计了window播放的使用。框架提供了两种window相关的组件。
WindowVideoView使用上几乎和VideoView是同样的,只不过WindowVideoView是以window的形式呈现的。window默认是能够拖动的,若是您不须要,能够禁止,window的每一个设置项都有默认值,window的设置示例代码:
FloatWindowParams windowParams = new FloatWindowParams();
windowParams.setWindowType(WindowManager.LayoutParams.TYPE_TOAST)
.setFormat(PixelFormat.RGBA_8888)
.setFlag(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE)
.setDefaultAnimation(true)
.setX(100)
.setY(100)
.setWidth(width)
.setHeight(height)
.setGravity(Gravity.TOP | Gravity.LEFT));
mWindowVideoView = new WindowVideoView(this,windowParams);
//...
复制代码
而FloatWindow只是一个悬浮窗View,您能够传入您要显示的布局View。能够用于窗口切换播放时的无缝续播。此处不作代码示例展现。
样式的设置是针对 VideoView、WindowVideoView 和 FloatWindow 的。固然框架提供的StyleSetter您也能够用于别处。提供了以下的样式设置:
public interface IStyleSetter {
//设置圆角
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
void setRoundRectShape(float radius);
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
void setRoundRectShape(Rect rect, float radius);
//设置为圆形
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
void setOvalRectShape();
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
void setOvalRectShape(Rect rect);
//清除样式设置
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
void clearShapeStyle();
//设置阴影
//注意阴影的设置要求对应的View对象必需要有背景色(不能是TRANSPARENT)
//若是您没设置,框架内部会自定设置为黑色
void setElevationShadow(float elevation);
void setElevationShadow(int backgroundColor, float elevation);
}
复制代码
框架自带了系统的MediaPlayer的解码实现,项目demo中示例接入了ijkplayer和exoplayer,若是您想接入其余的解码器,请参见示例代码,如下为简单示例,更详细的请参见项目源码。
接入步骤
public class XXXPlayer extends BaseInternalPlayer{
public XXXPlayer() {
//...
}
//...
//implements some abstract methods.
}
复制代码
经过配置设置使用该解码器。
int planId = 2;
PlayerConfig.addDecoderPlan(new DecoderPlan(planId, XXXPlayer.class.getName(), "XXXPlayer"));
PlayerConfig.setDefaultPlanId(planId);
复制代码
以上对于PlayerBase的讲解基本完成。码字好累!!!
主要的模块差很少就这么多了,更详细的可参见项目源码。
若有问题联系:junhui_jia@163.com
QQ交流群:600201778
最后再附上项目地址:PlayerBase