本文将继续介绍秀品中视频模块的原理以及实现过程,在上一篇中分析了在RecyclerView中滑动过程的手势处理,接下来会详细的介绍视频控制和状态更新部分,在整理视频播放相关文章时,发现了github上一个关于视频列表播放的开源项目,代码上写的很简洁,而且设计的很合理。通过学习,在原有的秀品视频播放类进行了优化,精简代码,重构了一个版本。
秀品中的视频控制和状态更新
上一篇说到ViewHolder中对视频相关的View做了封装,其实是通过实例化一个ViewVideoPlayContent类完成的,该类中会持有一个VsMediaPlayerPool类,VsMediaPlayerPool为单例模式,通过Application类获取,Pool类会通过一个Map来管理NEMediaPlayer,NEMediaPlayer类是网易提供的一款第三方播放器,相较于Android原生的播放器能支持更多的格式。各个类之间的关系可以通过下图来理解:
从图中可以看出,最核心部分是VsMediaPlayerPool类,这里分析一下该类的实现思路,在Pool类中,主要由4部分组成:
- 视频缓存区Map
- 状态相关的缓存区,由多个Map构成
- 一个HandlerThread提供单独线程来管理视频的相关操作
- 回调接口相关
视频列表需要实例化的NEMediaPlayer,但是视频相关的类是很耗资源的,不能无节制的实例化视频类,所以在Pool类中,使用了一个Map表,其键值为视频唯一的key值,在缓存区的MediaPlayer,会在列表滑出屏幕时被回收,因此NEMeidaPlayer最多不会超过3个,从而很好的保证资源不被过多的占用。NEMediaPlayer类被回收的时候,需要保存好视频的进度,大小以及其它状态。Pool类为特定的状态创建一个以视频key为键值的Map表,因此,在Pool类中,你会看到很多Map表,这种设计导致了状态的难以维护上,同时过多的把状态暴露给其它类设置,会导致状态的混乱,因为很多状态的设定是与视频操作紧紧关联的。一下是相关成员变量:
public class VsMediaPlayerPool implements Handler.Callback {
public static final int PLAYER_S_PAUSED = 1;
public static final int PLAYER_S_FINISHED = 2;
public static final int PLAYER_S_PLAYING = 3;
//handler线程,维护视频播放类NEMediaPlayer,该类存放在map表中
private static HandlerThread mPlayerThread = new HandlerThread("vs_video_player_thread");
private static final int MSG_OPEN_VIDEO = 531;
private static final int MSG_RELEASE = 709;
private static final int MSG_SEEK = 829;
private static final int MSG_RESUME = 534;
private static final int MSG_PAUSE = 707;
//HandlerThread下的handler
private Handler mPoolHandler;
//播放器线程池
private Map mPool;
//继续播放的位置
private Map mResumePositions;
//视频长宽
private Map mVideoSizeMap;
//视频状态
private Map mPlayerViewResumeStatus;
//回调相关
private Map> mPlayCompleteCallbacks;
private Map> mPrepareDoneCallbacks;
private Map> mBufferingCallbacks;
private Map> mInfoCallbacks;
/**
* 错误回调接口,map类型,视频的key为键值,回调接口List为对应值
* @description: Created by Zhangfeng on 2016/10/11 18:06
*/
private Map> mErrorCallbacks;
private Map> mSizeGotCallbacks;
//一些操作记录
private Set mReleasedSet;
private Set mCompleteSet;
//不自动播放标志位,一般在一次播放完整后标记,下次再拉到该item不自动播放
private Set mCompleteNoAutoPlaySet;
//不自动播放标志位,在全屏模式下暂停后跳转到小窗口停止自动播放
private Set mNoAutoPlaySet;
//播放标志位
private Set mPlayingStartedKeys;
static {
mPlayerThread.start();
}
}
在对视频进行播放,停止等操作时,需要注意的是,这些直接操作是需要调用和硬件相关的本地方法,所以是一个耗时的过程,直接再UI线程里进行操作会导致明显的卡顿现象,如上代码所示,Pool类中使用了一个HandlerThread,使用该线程实例化一个Handler,通过信息队列来管理视频相关的操作。在NEMediaPlayer中,每一个操作都其对应的回调接口,通过这些接口可以实现一系列操作动作,和状态的关系,同样通过一个Map来维护。
接下来通过一个播放流程来理解这个Pool类是如何工作的。
视频的播放是通过对视频封装View类ViewVideoPlayContent中activate方法来激活视频的
大致流程是通过setKey方法设置视频的key值,同时为该key的视频类设置了回调接口,保存到Map表中。刷新Surface,在其可用的状态下,通过各种标志位的判断,最后确定是继续上一播放位置还是重新初始化播放,然后通过initializePlayerAndPlay方法里的handler去发起播放请求,即调用Pool类的openVideo方法:
public void openVideo(String key, String path, Surface surface, boolean isMute) {
Message message = mPoolHandler.obtainMessage(MSG_OPEN_VIDEO);
Bundle data = new Bundle();
data.putString("key", key);
data.putString("path", path);
data.putParcelable("surface", surface);
data.putBoolean("isMute", isMute);
message.setData(data);
message.sendToTarget();
}
其他操作如pause,resume等视频相关操作都是类似的通过一个handler方法一个message,看一下Handler中对应的Message处理:
@Override
public boolean handleMessage(Message msg) {
synchronized (VsMediaPlayerPool.class) {
Bundle data = msg.getData();
switch (msg.what) {
case MSG_OPEN_VIDEO:
internalOpenVideo(data.getString("key"), data.getString("path"), (Surface) data.getParcelable("surface"), data.getBoolean("isMute"));
break;
case MSG_RELEASE:
threadRelease(data.getString("key"));
break;
case MSG_SEEK:
threadSeek(data.getString("key"), data.getLong("pos"));
break;
case MSG_PAUSE:
threadPause(data.getString("key"));
break;
case MSG_RESUME:
threadResume(data.getString("key"));
break;
}
}
return true;
}
可以看到对应的操作都交由HandlerThread中的Handler来管理了,播放对应着internalOpenVideo方法:
private void internalOpenVideo(String key, String path, Surface surface, boolean isMute) {
if (mPool != null) {
mCompleteSet.remove(key);
NEMediaPlayer player = new NEMediaPlayer();
……
try {
if (!TextUtils.isEmpty(path)) {
player.setDataSource(path);
player.prepareAsync(PlayerApp.getAppContext());
}
} catch (Exception e) {
……
}
}
}
方法中实例化了NEMediaPlayer,然后设置一些必须的配置,以及视频对应的回调接口,最后通过player.prepareAsync(PlayerApp.getAppContext())方法异步去做播放前的缓冲,当准备状态结束后会调用对应的回调接口:
private NELivePlayer.OnPreparedListener mPreparedListener = new NELivePlayer.OnPreparedListener() {
@Override
public void onPrepared(NELivePlayer neLivePlayer) {
String key = getKeyFromPool(neLivePlayer);
//取出保存在map中对应的key的接口,即在View封装类里设置的接口
List mDoneListenerList = mPrepareDoneCallbacks.get(key);
……
neLivePlayer.start();//播放视频
if (mDoneListenerList != null) {
for (int i = 0; i < mDoneListenerList.size(); i++) {
if (mDoneListenerList.get(i) != null) {
//调用对应接口
mDoneListenerList.get(i).onPrepareDone(key);
}
}
}
//更新视频状态,保存到map表中
addPlayerViewResumeStatus(key, PLAYER_S_PLAYING);
threadCheckIfOthersStopped(getKeyFromPool(neLivePlayer));
}
};
在Prepared回调方法中会播放该视频,同时会调用Map表中保存的对应接口,更新视频状态。其它操作与播放类似。
在VsMediaPlayerPool类中主要有两个问题:
- 过多的Map表
- 视频的操作以及其状态的关系太离散
如何解决该问题,首先是Map表过多,这个很简单,通过一个类将同一个Key对应一个视频状态类,该类中保存着状态,进度条位置,回调接口等这样就仅需要两个Map对象,精简后的成员变量:
public class NEMediaPlayerManager implements Handler.Callback {
public static final int PLAYER_S_PAUSED = 1;
public static final int PLAYER_S_FINISHED = 2;
public static final int PLAYER_S_PLAYING = 3;
//handler线程,维护视频播放类NEMediaPlayer,该类存放在map表中
private static HandlerThread mPlayerThread = new HandlerThread("vs_video_player_thread");
private Handler mPoolHandler;
private Map mMediaStatusMap;
//播放器线程池
private Map mPool;
static {
mPlayerThread.start();
}
}
对于视频的操作进行了重新封装,参考github上开源项目VideoPlayerManager,在对视频进行解码相关操作时,添加了前置和后置状态的更新,该作者通过自己实现了一个MessagesHandlerThread来维护这些操作,特定是可以在每一次操作时保证响应状态的更新,到达低耦合高聚态的目的,同时使这些耗时操作在一个单独的线程下同步执行。而在秀品中,视频播放的已经交由系统的HandlerThread来维护,实现方便,性能稳定,结合两者的优点,重构了视频操作的相关代码。
对于所有视频的操作,以一个Message为单位,而每一个Message继承与IPlayerMessage抽象类,该类为一个模板类,整个设计成一个模方法板模式(),代码如下,其中主要介绍下构造器:
public abstract class IPlayerMessage {
public static final int MSG_START = 532;
public static final int MSG_PERPARE = 533;
public static final int MSG_RELEASE = 709;
public static final int MSG_SEEK = 829;
public static final int MSG_RESUME = 534;
public static final int MSG_PAUSE = 707;
protected Message mMessage;
/**
* 根据message的类型获取对应的操作Message
* @param message handleMessage中传入的Message
* @description: Created by Boqin on 2016/11/7 9:36
*/
public static IPlayerMessage createByMessage(Message message){
IPlayerMessage playerMessage = null;
switch (message.what) {
case MSG_START:
playerMessage = new StartMessage(message);
break;
case MSG_PERPARE:
playerMessage = new PrepareMessage(message);
break;
case MSG_RELEASE:
playerMessage = new ReleaseMessage(message);
break;
case MSG_SEEK:
playerMessage = new SeekMessage(message);
break;
case MSG_PAUSE:
playerMessage = new PauseMessage(message);
break;
case MSG_RESUME:
playerMessage = new ResumeMessage(message);
break;
}
return playerMessage;
}
/**
* 执行操作
* @param pool 视频类Map表
* @description: Created by Boqin on 2016/11/7 9:37
*/
public abstract void performAction(Map pool);
/**
* 执行之前的操作,比如更新状态
* @param statusMap 视频状态Map
* @description: Created by Boqin on 2016/11/7 9:38
*/
public abstract void statusBefore(Map statusMap);
/**
* 执行之后的操作,比如更新状态
* @param statusMap 视频状态Map
* @description: Created by Boqin on 2016/11/7 9:38
*/
public abstract void statusAfter(Map statusMap);
/**
* 发生到Handler处理
* @description: Created by Boqin on 2016/11/7 9:40
*/
public void sendToTarget(){
if (mMessage!=null) {
mMessage.sendToTarget();
}
}
/**
* 执行模板方法
* @param pool 视频类Map表
* @param statusMap 视频状态Map
* @description: Created by Boqin on 2016/11/7 9:40
*/
final public void perform(@NonNull Map pool, @NonNull Map statusMap){
statusBefore(statusMap);
performAction(pool);
statusAfter(statusMap);
}
}
该构造器交由handlerMessage使用,即Pool类中的一个单独维护视频线程的handler,在handlerMessage完成调用,只需要调用perform方法即可:
@Override
public boolean handleMessage(Message msg) {
synchronized (NEMediaPlayerManager.class) {
IPlayerMessage playerMessage = IPlayerMessage.createByMessage(msg);
playerMessage.perform(mPool, mMediaStatusMap);
}
return true;
}
perform为模板方法,在IPlayerMessage类中实现,约定执行顺序:
final public void perform(@NonNull Map pool, @NonNull Map statusMap){
statusBefore(statusMap);
performAction(pool);
statusAfter(statusMap);
}
而具体的操作,交由子类来完成,根据现有的视频操作,实现了以下几个Message类:
还是以播放为例,这里对应着StartMessage:
public class StartMessage extends IPlayerMessage{
private String mKey;
/**
* @param handler 对应处理的handler
* @param key map键值
* @description: Created by Boqin on 2016/11/7 9:43
*/
public StartMessage(Handler handler, String key){
mMessage = handler.obtainMessage();
mMessage.what = IPlayerMessage.MSG_START;
Bundle data = new Bundle();
data.putString("key", key);
mMessage.setData(data);
}
/**
* handleMessage中生成
* @description: Created by Boqin on 2016/11/7 9:44
*/
protected StartMessage(Message message){
mKey = message.getData().getString("key");
}
@Override
public void performAction(Map pool) {
if (mKey==null) {
return;
}
NEMediaPlayer mediaPlayer = pool.get(mKey);
if (mediaPlayer==null){
return;
}
mediaPlayer.start();
}
@Override
public void statusBefore(@NonNull Map statusMap) {
MediaStatusBean mediaStatusBean = statusMap.get(mKey);
if (mediaStatusBean == null) {
return;
}
mediaStatusBean.setPlayerViewResumeStatus(MediaStatusBean.PLAYER_S_PLAYING);
}
@Override
public void statusAfter(@NonNull Map statusMap) {
}
}
两个构造器,第一个用于外部调用发起请求,第二个用于handler中进行改造,三个重写的方法,分别对应于执行前,执行,以及执行后的操作。这样解耦后,如果播放器有先的功能要添加,只需要实现一个新的Message以及在其父类IPlayerMessage中增加对应的createByMessage方法就行。
而在使用的时候,仅需要new一个message实例即可:
private void start(String key){
StartMessage startMessage = new StartMessage(mPoolHandler, key);
startMessage.sendToTarget();
}