06.播放器UI抽取封装

4,267 阅读19分钟

06.播放器UI抽取封装

目录介绍

  • 01.视频播放器UI封装需求
  • 02.播放器UI架构图
  • 03.如何分离播放和UI分离
  • 04.VideoPlayer如何实现
  • 05.VideoController实现
  • 06.播放Player和UI通信
  • 07.如何添加自定义播放视图
  • 08.关于播放器视图层级
  • 09.视频播放器重力感应监听

00.视频播放器通用框架

  • 基础封装视频播放器player,可以在ExoPlayer、MediaPlayer,声网RTC视频播放器内核,原生MediaPlayer可以自由切换
  • 对于视图状态切换和后期维护拓展,避免功能和业务出现耦合。比如需要支持播放器UI高度定制,而不是该lib库中UI代码
  • 针对视频播放,音频播放,播放回放,以及视频直播的功能。使用简单,代码拓展性强,封装性好,主要是和业务彻底解耦,暴露接口监听给开发者处理业务具体逻辑
  • 该播放器整体架构:播放器内核(自由切换) + 视频播放器 + 边播边缓存 + 高度定制播放器UI视图层
  • 项目地址:github.com/yangchong21…
  • 关于视频播放器整体功能介绍文档:juejin.cn/post/688345…

01.视频播放器UI封装需求

  • 发展中遇到的问题
    • 播放器可支持多种场景下的播放,多个产品会用到同一个播放器,这样就会带来一个问题,一个播放业务播放器状态发生变化,其他播放业务必须同步更新播放状态,各个播放业务之间互相交叉,随着播放业务的增多,开发和维护成本会急剧增加, 导致后续开发不可持续。
  • 播放器内核和UI层耦合
    • 也就是说视频player和ui操作柔和到了一起,尤其是两者之间的交互。比如播放中需要更新UI进度条,播放异常需要显示异常UI,都比较难处理播放器状态变化更新UI操作
  • UI难以自定义或者修改麻烦
    • 比如常见的视频播放器,会把视频各种视图写到xml中,这种方式在后期代码会很大,而且改动一个小的布局,则会影响大。这样到后期往往只敢加代码,而不敢删除代码……
    • 有时候难以适应新的场景,比如添加一个播放广告,老师开课,或者视频引导业务需求,则需要到播放器中写一堆业务代码。迭代到后期,违背了开闭原则,视频播放器需要做到和业务分离
  • 视频播放器结构需要清晰
    • 这个是指该视频播放器能否看了文档后快速上手,知道封装的大概流程。方便后期他人修改和维护,因此需要将视频播放器功能分离。比如切换内核+视频播放器(player+controller+view)
  • 一定要解耦合
    • 播放器player与视频UI解耦:支持添加自定义视频视图,比如支持添加自定义广告,新手引导,或者视频播放异常等视图,这个需要较强的拓展性
  • 适合多种业务场景
    • 比如适合播放单个视频,多个视频,以及列表视频,或者类似抖音那种一个页面一个视频,还有小窗口播放视频。也就是适合大多数业务场景

02.播放器UI架构图

image image

03.如何分离播放和UI分离

  • 方便播放业务发生变化
    • 播放状态变化是导致不同播放业务场景之间交叉同步,解除播放业务对播放器的直接操控,采用接口监听进行解耦。比如:player+controller+interface
  • 关于视频播放器
    • 定义一个视频播放器InterVideoPlayer接口,操作视频播放,暂停,缓冲,进度设置,设置播放模式等多种操作。
    • 然后写一个播放器接口的具体实现类,在这个里面拿到内核播放器player,然后做相关的实现操作。
  • 关于视频视图View
    • 定义一个视图InterVideoController接口,主要负责视图显示/隐藏,播放进度,锁屏,状态栏等操作。
    • 然后写一个播放器视图接口的具体实现类,在这里里面inflate视图操作,然后接口方法实现,为了方便后期开发者自定义view,因此需要addView操作,将添加进来的视图用map集合装起来。
  • 播放器player和controller交互
    • 在player中创建BaseVideoController对象,这个时候需要把controller添加到播放器中,这个时候有两个要点特别重要,需要把播放器状态监听,和播放模式监听传递给控制器
    • setPlayState设置视频播放器播放逻辑状态,主要是播放缓冲,加载,播放中,暂停,错误,完成,异常,播放进度等多个状态,方便控制器做UI更新操作
    • setPlayerState设置视频播放切换模式状态,主要是普通模式,小窗口模式,正常模式三种其中一种,方便控制器做UI更新
  • 播放器player和view交互
    • 这块非常关键,举个例子,视频播放失败需要显示控制层的异常视图View;播放视频初始化需要显示loading,然后更新UI播放进度条等。都是播放器和视图层交互
    • 可以定义一个类,同时实现InterVideoPlayer接口和InterVideoController接口,这个时候会重新这两个接口所有的方法。此类的目的是为了在InterControlView接口实现类中既能调用VideoPlayer的api又能调用BaseVideoController的api
  • 如何添加自定义播放器视图
    • 添加了自定义播放器视图,比如添加视频广告,可以选择跳过,选择播放暂停。那这个视图view,肯定是需要操作player或者获取player的状态的。这个时候就需要暴露监听视频播放的状态接口监听
    • 首先定义一个InterControlView接口,也就是说所有自定义视频视图view需要实现这个接口,该接口中的核心方法有:绑定视图到播放器,视图显示隐藏变化监听,播放状态监听,播放模式监听,进度监听,锁屏监听等
    • 在BaseVideoController中的状态监听中,通过InterControlView接口对象就可以把播放器的状态传递到子类中

04.VideoPlayer如何实现

  • 代码如下所示,省略了部分代码,具体看demo
    public class VideoPlayer<P extends AbstractVideoPlayer> extends FrameLayout
            implements InterVideoPlayer, VideoPlayerListener {
    
        private Context mContext;
        /**
         * 播放器
         */
        protected P mMediaPlayer;
        /**
         * 实例化播放核心
         */
        protected PlayerFactory<P> mPlayerFactory;
        /**
         * 控制器
         */
        @Nullable
        protected BaseVideoController mVideoController;
        /**
         * 真正承载播放器视图的容器
         */
        protected FrameLayout mPlayerContainer;
    
        public VideoPlayer(@NonNull Context context) {
            this(context, null);
        }
    
        public VideoPlayer(@NonNull Context context, @Nullable AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public VideoPlayer(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            mContext = context;
            init(attrs);
        }
    
        private void init(AttributeSet attrs) {
            BaseToast.init(mContext.getApplicationContext());
            //读取全局配置
            initConfig();
            //读取xml中的配置,并综合全局配置
            initAttrs(attrs);
            initView();
        }
    
        private void initConfig() {
            VideoPlayerConfig config = VideoViewManager.getConfig();
            mEnableAudioFocus = config.mEnableAudioFocus;
            mProgressManager = config.mProgressManager;
            mPlayerFactory = config.mPlayerFactory;
            mCurrentScreenScaleType = config.mScreenScaleType;
            mRenderViewFactory = config.mRenderViewFactory;
            //设置是否打印日志
            VideoLogUtils.setIsLog(config.mIsEnableLog);
        }
    
        @Override
        protected Parcelable onSaveInstanceState() {
            VideoLogUtils.d("onSaveInstanceState: " + mCurrentPosition);
            //activity切到后台后可能被系统回收,故在此处进行进度保存
            saveProgress();
            return super.onSaveInstanceState();
        }
    
        private void initAttrs(AttributeSet attrs) {
            TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.VideoPlayer);
            mEnableAudioFocus = a.getBoolean(R.styleable.VideoPlayer_enableAudioFocus, mEnableAudioFocus);
            mIsLooping = a.getBoolean(R.styleable.VideoPlayer_looping, false);
            mCurrentScreenScaleType = a.getInt(R.styleable.VideoPlayer_screenScaleType, mCurrentScreenScaleType);
            mPlayerBackgroundColor = a.getColor(R.styleable.VideoPlayer_playerBackgroundColor, Color.BLACK);
            a.recycle();
        }
    
        /**
         * 初始化播放器视图
         */
        protected void initView() {
            mPlayerContainer = new FrameLayout(getContext());
            //设置背景颜色,目前设置为纯黑色
            mPlayerContainer.setBackgroundColor(mPlayerBackgroundColor);
            LayoutParams params = new LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.MATCH_PARENT);
            //将布局添加到该视图中
            this.addView(mPlayerContainer, params);
        }
    
        /**
         * 设置控制器,传null表示移除控制器
         * @param mediaController                           controller
         */
        public void setController(@Nullable BaseVideoController mediaController) {
            mPlayerContainer.removeView(mVideoController);
            mVideoController = mediaController;
            if (mediaController != null) {
                mediaController.setMediaPlayer(this);
                LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                        ViewGroup.LayoutParams.MATCH_PARENT);
                mPlayerContainer.addView(mVideoController, params);
            }
        }
    
        /**
         * 开始播放,注意:调用此方法后必须调用{@link #release()}释放播放器,否则会导致内存泄漏
         */
        @Override
        public void start() {
            if (mVideoController==null){
                //在调用start方法前,请先初始化视频控制器,调用setController方法
                throw new VideoException(VideoException.CODE_NOT_SET_CONTROLLER,
                        "Controller must not be null , please setController first");
            }
            boolean isStarted = false;
            if (isInIdleState() || isInStartAbortState()) {
                isStarted = startPlay();
            } else if (isInPlaybackState()) {
                startInPlaybackState();
                isStarted = true;
            }
            if (isStarted) {
                mPlayerContainer.setKeepScreenOn(true);
                if (mAudioFocusHelper != null){
                    mAudioFocusHelper.requestFocus();
                }
            }
        }
    
        /**
         * 第一次播放
         * @return 是否成功开始播放
         */
        protected boolean startPlay() {
            //如果要显示移动网络提示则不继续播放
            if (showNetWarning()) {
                //中止播放
                setPlayState(ConstantKeys.CurrentState.STATE_START_ABORT);
                return false;
            }
            //监听音频焦点改变
            if (mEnableAudioFocus) {
                mAudioFocusHelper = new AudioFocusHelper(this);
            }
            //读取播放进度
            if (mProgressManager != null) {
                mCurrentPosition = mProgressManager.getSavedProgress(mUrl);
            }
            initPlayer();
            addDisplay();
            startPrepare(false);
            return true;
        }
    
    
        /**
         * 初始化播放器
         */
        protected void initPlayer() {
            //通过工厂模式创建对象
            mMediaPlayer = mPlayerFactory.createPlayer(mContext);
            mMediaPlayer.setPlayerEventListener(this);
            setInitOptions();
            mMediaPlayer.initPlayer();
            setOptions();
        }
    
        /**
         * 是否显示移动网络提示,可在Controller中配置
         */
        protected boolean showNetWarning() {
            //播放本地数据源时不检测网络
            if (VideoPlayerHelper.instance().isLocalDataSource(mUrl,mAssetFileDescriptor)){
                return false;
            }
            return mVideoController != null && mVideoController.showNetWarning();
        }
    
    
        /**
         * 初始化之前的配置项
         */
        protected void setInitOptions() {
    
        }
    
        /**
         * 初始化之后的配置项
         */
        protected void setOptions() {
            //设置是否循环播放
            mMediaPlayer.setLooping(mIsLooping);
        }
    
        /**
         * 初始化视频渲染View
         */
        protected void addDisplay() {
            if (mRenderView != null) {
                mPlayerContainer.removeView(mRenderView.getView());
                mRenderView.release();
            }
            //创建TextureView对象
            mRenderView = mRenderViewFactory.createRenderView(mContext);
            //绑定mMediaPlayer对象
            mRenderView.attachToPlayer(mMediaPlayer);
            LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER);
            mPlayerContainer.addView(mRenderView.getView(), 0, params);
        }
    
        /**
         * 开始准备播放(直接播放)
         */
        protected void startPrepare(boolean reset) {
            if (reset) {
                mMediaPlayer.reset();
                //重新设置option,media player reset之后,option会失效
                setOptions();
            }
            if (prepareDataSource()) {
                mMediaPlayer.prepareAsync();
                setPlayState(ConstantKeys.CurrentState.STATE_PREPARING);
                setPlayerState(isFullScreen() ? ConstantKeys.PlayMode.MODE_FULL_SCREEN :
                        isTinyScreen() ? ConstantKeys.PlayMode.MODE_TINY_WINDOW : ConstantKeys.PlayMode.MODE_NORMAL);
            }
        }
    
        /**
         * 设置播放数据
         * @return 播放数据是否设置成功
         */
        protected boolean prepareDataSource() {
            if (mAssetFileDescriptor != null) {
                mMediaPlayer.setDataSource(mAssetFileDescriptor);
                return true;
            } else if (!TextUtils.isEmpty(mUrl)) {
                mMediaPlayer.setDataSource(mUrl, mHeaders);
                return true;
            }
            return false;
        }
    
        /**
         * 视频播放出错回调
         */
        @Override
        public void onError() {
            mPlayerContainer.setKeepScreenOn(false);
            setPlayState(ConstantKeys.CurrentState.STATE_ERROR);
            VideoPlayerConfig config = VideoViewManager.getConfig();
            if (config!=null && config.mBuriedPointEvent!=null){
                //相当于进入了视频页面
                if (PlayerUtils.isConnected(mContext)){
                    config.mBuriedPointEvent.onError(mUrl,false);
                } else {
                    config.mBuriedPointEvent.onError(mUrl,true);
                }
            }
        }
    
        /**
         * 视频播放完成回调
         */
        @Override
        public void onCompletion() {
            mPlayerContainer.setKeepScreenOn(false);
            mCurrentPosition = 0;
            if (mProgressManager != null) {
                //播放完成,清除进度
                mProgressManager.saveProgress(mUrl, 0);
            }
            setPlayState(ConstantKeys.CurrentState.STATE_BUFFERING_PLAYING);
            VideoPlayerConfig config = VideoViewManager.getConfig();
            if (config!=null && config.mBuriedPointEvent!=null){
                //视频播放完成
                config.mBuriedPointEvent.playerCompletion(mUrl);
            }
        }
    
        @Override
        public void onInfo(int what, int extra) {
            switch (what) {
                case PlayerConstant.MEDIA_INFO_BUFFERING_START:
                    setPlayState(ConstantKeys.CurrentState.STATE_BUFFERING_PAUSED);
                    break;
                case PlayerConstant.MEDIA_INFO_BUFFERING_END:
                    setPlayState(ConstantKeys.CurrentState.STATE_COMPLETED);
                    break;
                case PlayerConstant.MEDIA_INFO_VIDEO_RENDERING_START: // 视频开始渲染
                    setPlayState(ConstantKeys.CurrentState.STATE_PLAYING);
                    if (mPlayerContainer.getWindowVisibility() != VISIBLE) {
                        pause();
                    }
                    break;
                case PlayerConstant.MEDIA_INFO_VIDEO_ROTATION_CHANGED:
                    if (mRenderView != null)
                        mRenderView.setVideoRotation(extra);
                    break;
            }
        }
    
        /**
         * 视频缓冲完毕,准备开始播放时回调
         */
        @Override
        public void onPrepared() {
            setPlayState(ConstantKeys.CurrentState.STATE_PREPARED);
            if (mCurrentPosition > 0) {
                seekTo(mCurrentPosition);
            }
        }
    
        /**
         * 获取当前播放器的状态
         */
        public int getCurrentPlayerState() {
            return mCurrentPlayerState;
        }
    
    
        /**
         * 向Controller设置播放状态,用于控制Controller的ui展示
         * 这里使用注解限定符,不要使用1,2这种直观数字,不方便知道意思
         * 播放状态,主要是指播放器的各种状态
         * -1               播放错误
         * 0                播放未开始
         * 1                播放准备中
         * 2                播放准备就绪
         * 3                正在播放
         * 4                暂停播放
         * 5                正在缓冲(播放器正在播放时,缓冲区数据不足,进行缓冲,缓冲区数据足够后恢复播放)
         * 6                暂停缓冲(播放器正在播放时,缓冲区数据不足,进行缓冲,此时暂停播放器,继续缓冲,缓冲区数据足够后恢复暂停
         * 7                播放完成
         * 8                开始播放中止
         */
        protected void setPlayState(@ConstantKeys.CurrentStateType int playState) {
            mCurrentPlayState = playState;
            if (mVideoController != null) {
                mVideoController.setPlayState(playState);
            }
            if (mOnStateChangeListeners != null) {
                for (OnVideoStateListener l : PlayerUtils.getSnapshot(mOnStateChangeListeners)) {
                    if (l != null) {
                        l.onPlayStateChanged(playState);
                    }
                }
            }
        }
    
        /**
         * 向Controller设置播放器状态,包含全屏状态和非全屏状态
         * 播放模式
         * 普通模式,小窗口模式,正常模式三种其中一种
         * MODE_NORMAL              普通模式
         * MODE_FULL_SCREEN         全屏模式
         * MODE_TINY_WINDOW         小屏模式
         */
        protected void setPlayerState(@ConstantKeys.PlayModeType int playerState) {
            mCurrentPlayerState = playerState;
            if (mVideoController != null) {
                mVideoController.setPlayerState(playerState);
            }
            if (mOnStateChangeListeners != null) {
                for (OnVideoStateListener l : PlayerUtils.getSnapshot(mOnStateChangeListeners)) {
                    if (l != null) {
                        l.onPlayerStateChanged(playerState);
                    }
                }
            }
        }
    
        /**
         * OnStateChangeListener的空实现。用的时候只需要重写需要的方法
         */
        public static class SimpleOnStateChangeListener implements OnVideoStateListener {
            @Override
            public void onPlayerStateChanged(@ConstantKeys.PlayModeType int playerState) {}
            @Override
            public void onPlayStateChanged(int playState) {}
        }
    
        /**
         * 添加一个播放状态监听器,播放状态发生变化时将会调用。
         */
        public void addOnStateChangeListener(@NonNull OnVideoStateListener listener) {
            if (mOnStateChangeListeners == null) {
                mOnStateChangeListeners = new ArrayList<>();
            }
            mOnStateChangeListeners.add(listener);
        }
    
        /**
         * 移除某个播放状态监听
         */
        public void removeOnStateChangeListener(@NonNull OnVideoStateListener listener) {
            if (mOnStateChangeListeners != null) {
                mOnStateChangeListeners.remove(listener);
            }
        }
    
        /**
         * 设置一个播放状态监听器,播放状态发生变化时将会调用,
         * 如果你想同时设置多个监听器,推荐 {@link #addOnStateChangeListener(OnVideoStateListener)}。
         */
        public void setOnStateChangeListener(@NonNull OnVideoStateListener listener) {
            if (mOnStateChangeListeners == null) {
                mOnStateChangeListeners = new ArrayList<>();
            } else {
                mOnStateChangeListeners.clear();
            }
            mOnStateChangeListeners.add(listener);
        }
    
        /**
         * 移除所有播放状态监听
         */
        public void clearOnStateChangeListeners() {
            if (mOnStateChangeListeners != null) {
                mOnStateChangeListeners.clear();
            }
        }
    
        /**
         * 改变返回键逻辑,用于activity
         */
        public boolean onBackPressed() {
            return mVideoController != null && mVideoController.onBackPressed();
        }
    
    
        /**-----------------------------暴露api方法--------------------------------------**/
        /**-----------------------------暴露api方法--------------------------------------**/
    
    
        public void setVideoBuilder(VideoPlayerBuilder videoBuilder){
            if (mPlayerContainer==null || videoBuilder==null){
                return;
            }
            //设置视频播放器的背景色
            mPlayerContainer.setBackgroundColor(videoBuilder.mColor);
            //设置小屏的宽高
            if (videoBuilder.mTinyScreenSize!=null && videoBuilder.mTinyScreenSize.length>0){
                mTinyScreenSize = videoBuilder.mTinyScreenSize;
            }
            //一开始播放就seek到预先设置好的位置
            if (videoBuilder.mCurrentPosition>0){
                this.mCurrentPosition = videoBuilder.mCurrentPosition;
            }
            //是否开启AudioFocus监听, 默认开启
            this.mEnableAudioFocus = videoBuilder.mEnableAudioFocus;
        }
    }
    

05.VideoController实现

  • 代码如下所示,代码太长,省略部分代码,具体看demo
    public abstract class BaseVideoController extends FrameLayout implements InterVideoController,
            OrientationHelper.OnOrientationChangeListener {
    
        //播放器包装类,集合了MediaPlayerControl的api和IVideoController的api
        protected ControlWrapper mControlWrapper;
    
        public BaseVideoController(@NonNull Context context) {
            //创建
            this(context, null);
        }
    
        public BaseVideoController(@NonNull Context context, @Nullable AttributeSet attrs) {
            //创建
            this(context, attrs, 0);
        }
    
        public BaseVideoController(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            initView(context);
        }
    
        @Override
        protected void onDetachedFromWindow() {
            super.onDetachedFromWindow();
            if (mShowAnim != null){
                mShowAnim.cancel();
                mShowAnim = null;
            }
            if (mHideAnim != null){
                mHideAnim.cancel();
                mHideAnim = null;
            }
        }
    
        @Override
        public void onWindowFocusChanged(boolean hasWindowFocus) {
            super.onWindowFocusChanged(hasWindowFocus);
            if (mControlWrapper.isPlaying() && (mEnableOrientation || mControlWrapper.isFullScreen())) {
                if (hasWindowFocus) {
                    postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            mOrientationHelper.enable();
                        }
                    }, 800);
                } else {
                    mOrientationHelper.disable();
                }
            }
        }
    
        protected void initView(Context context) {
            if (getLayoutId() != 0) {
                LayoutInflater.from(getContext()).inflate(getLayoutId(), this, true);
            }
            mOrientationHelper = new OrientationHelper(context.getApplicationContext());
            mEnableOrientation = VideoViewManager.getConfig().mEnableOrientation;
            mAdaptCutout = VideoViewManager.getConfig().mAdaptCutout;
            mShowAnim = new AlphaAnimation(0f, 1f);
            mShowAnim.setDuration(300);
            mHideAnim = new AlphaAnimation(1f, 0f);
            mHideAnim.setDuration(300);
            mActivity = PlayerUtils.scanForActivity(context);
        }
    
        /**
         * 设置控制器布局文件,子类必须实现
         */
        protected abstract int getLayoutId();
    
        /**
         * 重要:此方法用于将{@link VideoPlayer} 和控制器绑定
         */
        @CallSuper
        public void setMediaPlayer(InterVideoPlayer mediaPlayer) {
            mControlWrapper = new ControlWrapper(mediaPlayer, this);
            //绑定ControlComponent和Controller
            for (Map.Entry<InterControlView, Boolean> next : mControlComponents.entrySet()) {
                InterControlView component = next.getKey();
                component.attach(mControlWrapper);
            }
            //开始监听设备方向
            mOrientationHelper.setOnOrientationChangeListener(this);
        }
    
        /**
         * 添加控制组件,最后面添加的在最下面,合理组织添加顺序,可让ControlComponent位于不同的层级
         */
        public void addControlComponent(InterControlView... component) {
            for (InterControlView item : component) {
                addControlComponent(item, false);
            }
        }
    
        /**
         * 添加控制组件,最后面添加的在最下面,合理组织添加顺序,可让ControlComponent位于不同的层级
         *
         * @param isPrivate 是否为独有的组件,如果是就不添加到控制器中
         */
        public void addControlComponent(InterControlView component, boolean isPrivate) {
            mControlComponents.put(component, isPrivate);
            if (mControlWrapper != null) {
                component.attach(mControlWrapper);
            }
            View view = component.getView();
            if (view != null && !isPrivate) {
                addView(view, 0);
            }
        }
    
        /**
         * 移除控制组件
         */
        public void removeControlComponent(InterControlView component) {
            removeView(component.getView());
            mControlComponents.remove(component);
        }
    
        /**
         * 移除所有的组件
         */
        public void removeAllControlComponent() {
            for (Map.Entry<InterControlView, Boolean> next : mControlComponents.entrySet()) {
                removeView(next.getKey().getView());
            }
            mControlComponents.clear();
        }
    
        public void removeAllPrivateComponents() {
            Iterator<Map.Entry<InterControlView, Boolean>> it = mControlComponents.entrySet().iterator();
            while (it.hasNext()) {
                Map.Entry<InterControlView, Boolean> next = it.next();
                if (next.getValue()) {
                    it.remove();
                }
            }
        }
    
        /**
         * {@link VideoPlayer}调用此方法向控制器设置播放状态。
         * 这里使用注解限定符,不要使用1,2这种直观数字,不方便知道意思
         * 播放状态,主要是指播放器的各种状态
         * -1               播放错误
         * 0                播放未开始
         * 1                播放准备中
         * 2                播放准备就绪
         * 3                正在播放
         * 4                暂停播放
         * 5                正在缓冲(播放器正在播放时,缓冲区数据不足,进行缓冲,缓冲区数据足够后恢复播放)
         * 6                暂停缓冲(播放器正在播放时,缓冲区数据不足,进行缓冲,此时暂停播放器,继续缓冲,缓冲区数据足够后恢复暂停
         * 7                播放完成
         * 8                开始播放中止
         */
        @CallSuper
        public void setPlayState(@ConstantKeys.CurrentStateType int playState) {
            //设置播放器的状态
            handlePlayStateChanged(playState);
        }
    
        /**
         * {@link VideoPlayer}调用此方法向控制器设置播放器状态
         * 播放模式
         * 普通模式,小窗口模式,正常模式三种其中一种
         * MODE_NORMAL              普通模式
         * MODE_FULL_SCREEN         全屏模式
         * MODE_TINY_WINDOW         小屏模式
         */
        @CallSuper
        public void setPlayerState(@ConstantKeys.PlayModeType final int playerState) {
            //调用此方法向控制器设置播放器状态
            handlePlayerStateChanged(playerState);
        }
    
        /**
         * 播放和暂停
         */
        protected void togglePlay() {
            mControlWrapper.togglePlay();
        }
    
        /**
         * 横竖屏切换
         */
        protected void toggleFullScreen() {
            if (PlayerUtils.isActivityLiving(mActivity)){
                mControlWrapper.toggleFullScreen(mActivity);
            }
        }
    
        /**
         * 子类中请使用此方法来进入全屏
         *
         * @return 是否成功进入全屏
         */
        protected boolean startFullScreen() {
            if (!PlayerUtils.isActivityLiving(mActivity)) {
                return false;
            }
            mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
            mControlWrapper.startFullScreen();
            return true;
        }
    
        /**
         * 子类中请使用此方法来退出全屏
         *
         * @return 是否成功退出全屏
         */
        protected boolean stopFullScreen() {
            if (!PlayerUtils.isActivityLiving(mActivity)) {
                return false;
            }
            mActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
            mControlWrapper.stopFullScreen();
            return true;
        }
    
        /**
         * 改变返回键逻辑,用于activity
         */
        public boolean onBackPressed() {
            return false;
        }
    
        /**
         * 是否自动旋转, 默认不自动旋转
         */
        public void setEnableOrientation(boolean enableOrientation) {
            mEnableOrientation = enableOrientation;
        }
    
        private void handleVisibilityChanged(boolean isVisible, Animation anim) {
            if (!mIsLocked) {
                //没锁住时才向ControlComponent下发此事件
                for (Map.Entry<InterControlView, Boolean> next : mControlComponents.entrySet()) {
                    InterControlView component = next.getKey();
                    component.onVisibilityChanged(isVisible, anim);
                }
            }
            onVisibilityChanged(isVisible, anim);
        }
    
        /**
         * 子类重写此方法监听控制的显示和隐藏
         *
         * @param isVisible 是否可见
         * @param anim      显示/隐藏动画
         */
        protected void onVisibilityChanged(boolean isVisible, Animation anim) {
    
        }
    
        private void handlePlayStateChanged(int playState) {
            for (Map.Entry<InterControlView, Boolean> next : mControlComponents.entrySet()) {
                InterControlView component = next.getKey();
                component.onPlayStateChanged(playState);
            }
            onPlayStateChanged(playState);
        }
    
        /**
         * 子类重写此方法并在其中更新控制器在不同播放状态下的ui
         */
        @CallSuper
        protected void onPlayStateChanged(int playState) {
            switch (playState) {
                case ConstantKeys.CurrentState.STATE_IDLE:
                    mOrientationHelper.disable();
                    mOrientation = 0;
                    mIsLocked = false;
                    mShowing = false;
                    removeAllPrivateComponents();
                    break;
                case ConstantKeys.CurrentState.STATE_BUFFERING_PLAYING:
                    mIsLocked = false;
                    mShowing = false;
                    break;
                case ConstantKeys.CurrentState.STATE_ERROR:
                    mShowing = false;
                    break;
            }
        }
    
        /**
         * 播放器状态改变
         * @param playerState                       播放器状态
         */
        private void handlePlayerStateChanged(int playerState) {
            for (Map.Entry<InterControlView, Boolean> next : mControlComponents.entrySet()) {
                InterControlView component = next.getKey();
                component.onPlayerStateChanged(playerState);
            }
            onPlayerStateChanged(playerState);
        }
    
        /**
         * 子类重写此方法并在其中更新控制器在不同播放器状态下的ui
         * 普通模式,小窗口模式,正常模式三种其中一种
         * MODE_NORMAL              普通模式
         * MODE_FULL_SCREEN         全屏模式
         * MODE_TINY_WINDOW         小屏模式
         */
        @CallSuper
        protected void onPlayerStateChanged(@ConstantKeys.PlayMode int playerState) {
            switch (playerState) {
                case ConstantKeys.PlayMode.MODE_NORMAL:
                    //视频正常播放是设置监听
                    if (mEnableOrientation) {
                        //检查系统是否开启自动旋转
                        mOrientationHelper.enable();
                    } else {
                        //取消监听
                        mOrientationHelper.disable();
                    }
                    if (hasCutout()) {
                        StatesCutoutUtils.adaptCutoutAboveAndroidP(getContext(), false);
                    }
                    break;
                case ConstantKeys.PlayMode.MODE_FULL_SCREEN:
                    //在全屏时强制监听设备方向
                    mOrientationHelper.enable();
                    if (hasCutout()) {
                        StatesCutoutUtils.adaptCutoutAboveAndroidP(getContext(), true);
                    }
                    break;
                case ConstantKeys.PlayMode.MODE_TINY_WINDOW:
                    //小窗口取消重力感应监听
                    mOrientationHelper.disable();
                    break;
            }
        }
    
        private void handleSetProgress(int duration, int position) {
            for (Map.Entry<InterControlView, Boolean> next : mControlComponents.entrySet()) {
                InterControlView component = next.getKey();
                component.setProgress(duration, position);
            }
            setProgress(duration, position);
        }
    
        /**
         * 刷新进度回调,子类可在此方法监听进度刷新,然后更新ui
         *
         * @param duration 视频总时长
         * @param position 视频当前时长
         */
        protected void setProgress(int duration, int position) {
    
        }
    
        private void handleLockStateChanged(boolean isLocked) {
            for (Map.Entry<InterControlView, Boolean> next : mControlComponents.entrySet()) {
                InterControlView component = next.getKey();
                component.onLockStateChanged(isLocked);
            }
            onLockStateChanged(isLocked);
        }
    
    }
    

06.播放Player和UI通信

  • 比如,在自定义view视图中,我想调用VideoPlayer的api又能调用BaseVideoController的api,该如何实现呢?
    • 当创建了下面的对象,就可以同时拿到player和controller中的api方法呢,这里面省略一部分代码,具体看demo案例
    public class ControlWrapper implements InterVideoPlayer, InterVideoController {
        
        private InterVideoPlayer mVideoPlayer;
        private InterVideoController mController;
        
        public ControlWrapper(@NonNull InterVideoPlayer videoPlayer, @NonNull InterVideoController controller) {
            mVideoPlayer = videoPlayer;
            mController = controller;
        }
        
        @Override
        public void start() {
            mVideoPlayer.start();
        }
    
        @Override
        public void pause() {
            mVideoPlayer.pause();
        }
    
        @Override
        public long getDuration() {
            return mVideoPlayer.getDuration();
        }
    
        @Override
        public boolean isShowing() {
            return mController.isShowing();
        }
    
        @Override
        public void setLocked(boolean locked) {
            mController.setLocked(locked);
        }
    
    }
    

07.如何添加自定义播放视图

  • 比如,现在有个业务需求,需要在视频播放器刚开始添加一个广告视图,等待广告倒计时120秒后,直接进入播放视频逻辑。相信这个业务场景很常见,大家都碰到过,使用该播放器就特别简单,代码如下所示:
  • 首先创建一个自定义view,需要实现InterControlView接口,重写该接口中所有抽象方法,这里省略了很多代码,具体看demo。
    public class AdControlView extends FrameLayout implements InterControlView, View.OnClickListener {
    
        private ControlWrapper mControlWrapper;
        public AdControlView(@NonNull Context context) {
            super(context);
            init(context);
        }
    
        private void init(Context context){
            LayoutInflater.from(getContext()).inflate(R.layout.layout_ad_control_view, this, true);
        }
       
        /**
         * 播放状态
         * -1               播放错误
         * 0                播放未开始
         * 1                播放准备中
         * 2                播放准备就绪
         * 3                正在播放
         * 4                暂停播放
         * 5                正在缓冲(播放器正在播放时,缓冲区数据不足,进行缓冲,缓冲区数据足够后恢复播放)
         * 6                暂停缓冲(播放器正在播放时,缓冲区数据不足,进行缓冲,此时暂停播放器,继续缓冲,缓冲区数据足够后恢复暂停
         * 7                播放完成
         * 8                开始播放中止
         * @param playState                     播放状态,主要是指播放器的各种状态
         */
        @Override
        public void onPlayStateChanged(int playState) {
            switch (playState) {
                case ConstantKeys.CurrentState.STATE_PLAYING:
                    mControlWrapper.startProgress();
                    mPlayButton.setSelected(true);
                    break;
                case ConstantKeys.CurrentState.STATE_PAUSED:
                    mPlayButton.setSelected(false);
                    break;
            }
        }
    
        /**
         * 播放模式
         * 普通模式,小窗口模式,正常模式三种其中一种
         * MODE_NORMAL              普通模式
         * MODE_FULL_SCREEN         全屏模式
         * MODE_TINY_WINDOW         小屏模式
         * @param playerState                   播放模式
         */
        @Override
        public void onPlayerStateChanged(int playerState) {
            switch (playerState) {
                case ConstantKeys.PlayMode.MODE_NORMAL:
                    mBack.setVisibility(GONE);
                    mFullScreen.setSelected(false);
                    break;
                case ConstantKeys.PlayMode.MODE_FULL_SCREEN:
                    mBack.setVisibility(VISIBLE);
                    mFullScreen.setSelected(true);
                    break;
            }
            //暂未实现全面屏适配逻辑,需要你自己补全
        }
    }
    
  • 然后该怎么使用这个自定义view呢?很简单,在之前基础上,通过控制器对象add进来即可,代码如下所示
    controller = new BasisVideoController(this);
    AdControlView adControlView = new AdControlView(this);
    adControlView.setListener(new AdControlView.AdControlListener() {
        @Override
        public void onAdClick() {
            BaseToast.showRoundRectToast( "广告点击跳转");
        }
    
        @Override
        public void onSkipAd() {
            playVideo();
        }
    });
    controller.addControlComponent(adControlView);
    //设置控制器
    mVideoPlayer.setController(controller);
    mVideoPlayer.setUrl(proxyUrl);
    mVideoPlayer.start();
    

08.关于播放器视图层级

  • 视频播放器为了拓展性,需要暴露view接口供外部开发者自定义视频播放器视图,通过addView的形式添加到播放器的控制器中。
    • 这就涉及view视图的层级性。控制view视图的显示和隐藏是特别重要的,这个时候在自定义view中就需要拿到播放器的状态
  • 举一个简单的例子,基础视频播放器
    • 添加了基础播放功能的几个播放视图。有播放完成,播放异常,播放加载,顶部标题栏,底部控制条栏,锁屏,以及手势滑动栏。如何控制它们的显示隐藏切换呢?
    • 在addView这些视图时,大多数的view都是默认GONE隐藏的。比如当视频初始化时,先缓冲则显示缓冲view而隐藏其他视图,接着播放则显示顶部/底部视图而隐藏其他视图
  • 比如有时候需要显示两种不同的自定义视图如何处理
    • 举个例子,播放的时候,点击一下视频,会显示顶部title视图和底部控制条视图,那么这样会同时显示两个视图。
    • 点击顶部title视图的返回键可以关闭播放器,点击底部控制条视图的播放暂停可以控制播放条件。这个时候底部控制条视图FrameLayout的ChildView在整个视频的底部,顶部title视图FrameLayout的ChildView在整个视频的顶部,这样可以达到上下层都可以相应事件。
  • 那么FrameLayout层层重叠,如何让下层不响应事件
    • 在最上方显示的层加上: android:clickable="true" 可以避免点击上层触发底层。或者直接给控制设置一个background颜色也可以。
  • 比如基础播放器的视图层级是这样的
    //添加自动完成播放界面view
    CustomCompleteView completeView = new CustomCompleteView(mContext);
    completeView.setVisibility(GONE);
    this.addControlComponent(completeView);
    
    //添加错误界面view
    CustomErrorView errorView = new CustomErrorView(mContext);
    errorView.setVisibility(GONE);
    this.addControlComponent(errorView);
    
    //添加与加载视图界面view,准备播放界面
    CustomPrepareView prepareView = new CustomPrepareView(mContext);
    thumb = prepareView.getThumb();
    prepareView.setClickStart();
    this.addControlComponent(prepareView);
    
    //添加标题栏
    titleView = new CustomTitleView(mContext);
    titleView.setTitle(title);
    titleView.setVisibility(VISIBLE);
    this.addControlComponent(titleView);
    
    if (isLive) {
        //添加底部播放控制条
        CustomLiveControlView liveControlView = new CustomLiveControlView(mContext);
        this.addControlComponent(liveControlView);
    } else {
        //添加底部播放控制条
        CustomBottomView vodControlView = new CustomBottomView(mContext);
        //是否显示底部进度条。默认显示
        vodControlView.showBottomProgress(true);
        this.addControlComponent(vodControlView);
    }
    //添加滑动控制视图
    CustomGestureView gestureControlView = new CustomGestureView(mContext);
    this.addControlComponent(gestureControlView);
    

09.视频播放器重力感应监听

  • 区别视频几种不同的播放模式
    • 正常播放时,设置检查系统是否开启自动旋转,打开监听
    • 全屏模式播放视频的时候,强制监听设备方向
    • 在小窗口模式播放视频的时候,取消重力感应监听
    • 注意一点。关于是否开启自动旋转的重力感应监听,可以给外部开发者暴露一个方法设置的开关。让用户选择是否开启该功能
  • 首先写一个类,然后继承OrientationEventListener类,注意视频播放器重力感应监听不要那么频繁。表示500毫秒才检测一次……
    public class OrientationHelper extends OrientationEventListener {
    
        private long mLastTime;
    
        private OnOrientationChangeListener mOnOrientationChangeListener;
    
        public OrientationHelper(Context context) {
            super(context);
        }
    
        @Override
        public void onOrientationChanged(int orientation) {
            long currentTime = System.currentTimeMillis();
            if (currentTime - mLastTime < 500) {
                return;
            }
            //500毫秒检测一次
            if (mOnOrientationChangeListener != null) {
                mOnOrientationChangeListener.onOrientationChanged(orientation);
            }
            mLastTime = currentTime;
        }
    
    
        public interface OnOrientationChangeListener {
            void onOrientationChanged(int orientation);
        }
    
        public void setOnOrientationChangeListener(OnOrientationChangeListener onOrientationChangeListener) {
            mOnOrientationChangeListener = onOrientationChangeListener;
        }
    }
    
  • 关于播放器播放模式状态发生变化时,需要更新是开启重力感应监听,还是关闭重力感应监听。代码如下所示
    /**
     * 子类重写此方法并在其中更新控制器在不同播放器状态下的ui
     * 普通模式,小窗口模式,正常模式三种其中一种
     * MODE_NORMAL              普通模式
     * MODE_FULL_SCREEN         全屏模式
     * MODE_TINY_WINDOW         小屏模式
     */
    @CallSuper
    protected void onPlayerStateChanged(@ConstantKeys.PlayMode int playerState) {
        switch (playerState) {
            case ConstantKeys.PlayMode.MODE_NORMAL:
                //视频正常播放是设置监听
                if (mEnableOrientation) {
                    //检查系统是否开启自动旋转
                    mOrientationHelper.enable();
                } else {
                    //取消监听
                    mOrientationHelper.disable();
                }
                break;
            case ConstantKeys.PlayMode.MODE_FULL_SCREEN:
                //在全屏时强制监听设备方向
                mOrientationHelper.enable();
                break;
            case ConstantKeys.PlayMode.MODE_TINY_WINDOW:
                //小窗口取消重力感应监听
                mOrientationHelper.disable();
                break;
        }
    }