秀品中视频播放模块的解析(下)

本文将继续介绍秀品中视频模块的原理以及实现过程,在上一篇中分析了在RecyclerView中滑动过程的手势处理,接下来会详细的介绍视频控制和状态更新部分,在整理视频播放相关文章时,发现了github上一个关于视频列表播放的开源项目,代码上写的很简洁,而且设计的很合理。通过学习,在原有的秀品视频播放类进行了优化,精简代码,重构了一个版本。

秀品中的视频控制和状态更新

上一篇说到ViewHolder中对视频相关的View做了封装,其实是通过实例化一个ViewVideoPlayContent类完成的,该类中会持有一个VsMediaPlayerPool类,VsMediaPlayerPool为单例模式,通过Application类获取,Pool类会通过一个Map来管理NEMediaPlayer,NEMediaPlayer类是网易提供的一款第三方播放器,相较于Android原生的播放器能支持更多的格式。各个类之间的关系可以通过下图来理解:


秀品中视频播放模块的解析(下)_第1张图片
player

  从图中可以看出,最核心部分是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类:


秀品中视频播放模块的解析(下)_第2张图片
messgae

  还是以播放为例,这里对应着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();
    }

你可能感兴趣的:(秀品中视频播放模块的解析(下))