Android视频播放器封装

写在前面:
  • 因项目需要,需要使用到视频播放相关技术,虽然系统提供了播放器VideoView,但由于各种原因无法满足项目需要,特将播放器封装成库,方便日后项目使用及自定义拓展。 此文章适合未接触过视频播放相关、没有时间来研究视频播放相关、不想写UI交互直接用现成的成熟播放器 的开发者阅读,大神大牛请绕路。
给大家推荐视频播放器iPlayer,支持的特性包括但不限于:
  • 支持网络地址、直播流、本地Assets和Raw音视频资源文件播放
  • 支持IJKPlayer、ExoPlayer、MediaPlayer和其它更多自定义解码器
  • 支持自定义视频解码器、控制器、UI交互组件、视频画面渲染器
  • 支持播放倍速、缩放模式、静音、镜像等功能设置
  • 支持多播放器同时播放、跳转到详情无缝衔接播放
  • 支持重力感应横竖屏旋转及开关设置
  • 支持无权限开启Activity级别窗口播放及全局悬浮窗窗口播放
  • 窗口播放器支持自动吸附、悬停
  • Demo仿抖音播放示例,支持视频缓存、秒播、弹幕交互等
    如Github无法访问可访问码云项目地址

一、播放器框架设计

iPlayer架构关系图

二、播放器功能实现

1、画面渲染(TextureView)
1.1、TextureView创建及设置Surface监听
        TextureView textureView =new TextureView(context);
        textureView .setSurfaceTextureListener(this);
1.2、在TextureView初始化完成的onSurfaceTextureAvailable回调里将SurfaceTexture与MediaPlayer绑定
    private MediaTextureView mTextureView;//画面渲染
    private Surface mSurface;
    private SurfaceTexture mSurfaceTexture;

    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
//        ILogger.d(TAG,"onSurfaceTextureAvailable-->width:"+width+",height:"+height);
        if(null==mTextureView||null==mMediaPlayer) return;
        if(null!=mSurfaceTexture){
            mTextureView.setSurfaceTexture(mSurfaceTexture);
        }else{
            mSurfaceTexture = surfaceTexture;
            mSurface =new Surface(surfaceTexture);
            mMediaPlayer.setSurface(mSurface);
        }
    }
2、全屏播放
2.1、开启全屏播放
  • 全屏分三个步骤:1、保存播放器父容器ViewGroup。2、改变屏幕方向为横屏。3、将播放器添加到Window中。
    /**
     * 全屏播放
     * @param bgColor 开启全屏模式播放:横屏时播放器的背景颜色,内部默认用黑色#000000
     */
    @Override
    public void startFullScreen(int bgColor) {
//        ILogger.d(TAG,"startFullScreen");
        if(mScreenOrientation==IMediaPlayer.ORIENTATION_LANDSCAPE) return;
        Activity activity = PlayerUtils.getInstance().getActivity(getTargetContext());
        if (null != activity&& !activity.isFinishing()) {
            ViewGroup viewGroup = (ViewGroup) activity.getWindow().getDecorView();
            if(null==viewGroup){
                return;
            }
            //1.保存播放器在父布局中的宽、高、index层级等属性(如果存在的话)
            mPlayerParams = new int[3];
            mPlayerParams[0]=this.getMeasuredWidth();
            mPlayerParams[1]=this.getMeasuredHeight();
            if(null!=getParent()&& getParent() instanceof ViewGroup){
                mParent = (ViewGroup) getParent();
                mPlayerParams[2]=mParent.indexOfChild(this);//保存播放器本身的宽高和位于父容器的索引位置,恢复正常模式时需准确的还原到父容器index
            }
            PlayerUtils.getInstance().removeViewFromParent(this);//从原宿主中移除自己
            //2.改变屏幕方向为横屏状态,播放器所在的Activity需要添加属性:android:configChanges="orientation|screenSize"
            activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);//改变屏幕方向
            setScreenOrientation(IMediaPlayer.ORIENTATION_LANDSCAPE);//更新控制器方向状态
            findViewById(R.id.player_surface).setBackgroundColor(bgColor!=0?bgColor:Color.parseColor("#000000"));//设置一个背景颜色
            //3.隐藏NavigationBar和StatusBar
            hideSystemBar(viewGroup);
            //4.添加到此播放器宿主context的window中
            viewGroup.addView(this, new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER));
        }
    }
2.2、退出全屏播放
  • 退出全屏分四个步骤:1、Window窗口中移除自己。2、改变屏幕方向为竖屏。3、还原全屏设置为正常设置。4、将自己交给此前的宿主ViewGroup(还需要注意:还原播放器在原宿主的宽、高、index位置)

    /**
     * 退出全屏播放
     */
    @Override
    public void quitFullScreen() {
//        ILogger.d(TAG,"quitLandscapeScreen");
        Activity activity = PlayerUtils.getInstance().getActivity(getTargetContext());
        if(null!=activity&&!activity.isFinishing()){
            ViewGroup viewGroup = (ViewGroup) activity.getWindow().getDecorView();
            if(null==viewGroup){
                return;
            }
            //1:从Window窗口中移除自己
            PlayerUtils.getInstance().removeViewFromParent(this);
            //2.改变屏幕方向为竖屏
            activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);//改变屏幕方向
            setScreenOrientation(IMediaPlayer.ORIENTATION_PORTRAIT);
            findViewById(R.id.player_surface).setBackgroundColor(Color.parseColor("#00000000"));//设置纯透明背景
            //3.还原全屏设置为正常设置
            showSysBar(viewGroup);
            //3.将自己交给此前的宿主ViewGroup,并还原播放器在原宿主的宽、高、index位置
            if(null!=mParent){
                if(null!=mPlayerParams&&mPlayerParams.length>0){
//                    ILogger.d(TAG,"index:"+mPlayerParams[2]);
                    mParent.addView(this, mPlayerParams[2],new LayoutParams(mPlayerParams[0], mPlayerParams[1]));//将自己还原到父容器的index位置,取消了Gravity.CENTER属性
                }else{
                    mParent.addView(this, new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
                }
//                ILogger.d(TAG,"quitLandscapeScreen-->已退出全屏");
            }else{
                //通知宿主监听器触发返回事件
//                ILogger.d(TAG,"quitLandscapeScreen-->退出全屏无宿主接收,销毁播放器");
                //无宿主接收时直接停止播放并销毁播放器
                onDestroy();
            }
        }
    }
3、自定义控制器及UI交互组件
3.1、自定义控制器
  • BasePlayer提供了setController(BaseController controller)方法给有需要UI交互的场景设置UI控制器
    /**
     * 设置视图控制器
     * @param controller 继承VideoBaseController的控制器
     */
    @Override
    public void setController(BaseController controller) {}
3.2、自定义UI交互组件
  • 为什么有自定义Controller还整个自定义UI交互组件?因为Controller不适合处理所有UI交互,比如播放器的场景不同,UI也不尽相同,此时若把所有UI交互全写在Controller会显得臃肿、耦合性过高、开发者无法根据自己的需要来选择和自定义部分UI交互。
  • 自定义交互组件的使用
        //播放器的准备
        VideoPlayer videoPlayer = new VideoPlayer(this);
        videoPlayer.setBackgroundColor(Color.parseColor("#000000"));
        VideoController controller=new VideoController(videoPlayer.getContext());
        /**
         * 给播放器设置控制器
         */
        videoPlayer.setController(controller);
        /**
         * 给播放器控制器绑定需要的自定义UI交互组件
         */
        ControlToolBarView toolBarView=new ControlToolBarView(this);//标题栏,返回按钮、视频标题、功能按钮、系统时间、电池电量等组件
        ControlFunctionBarView functionBarView=new ControlFunctionBarView(this);//底部时间、seek、静音、全屏功能栏
        functionBarView.showSoundMute(true,false);//启用静音功能交互\默认不静音
        ControlStatusView statusView=new ControlStatusView(this);//移动网络播放提示、播放失败、试看完成
        ControlGestureView gestureView=new ControlGestureView(this);//手势控制屏幕亮度、系统音量、快进、快退UI交互
        ControlCompletionView completionView=new ControlCompletionView(this);//播放完成、重试
        ControlLoadingView loadingView=new ControlLoadingView(this);//加载中、开始播放
        //将自定义UI交互组件设置到控制器
        controller.addControllerWidget(toolBarView,functionBarView,statusView,gestureView,completionView,loadingView);
4、自定义解码器
  • SDK内部封装时,为了方便开发者切换解码器,将切换解码器的入口封装在播放器的监听器内,开发者可在回调方法返回自己的解码器。
    private int MEDIA_CORE=2;//这里默认用ExoPlayer解码器

        //自定义解码器
        mVideoPlayer.setOnPlayerActionListener(new OnPlayerEventListener() {
            @Override
            public AbstractMediaPlayer createMediaPlayer() {
                if (1 == MEDIA_CORE) {
                    return new JkMediaPlayer(LivePlayerActivity.this);
                } else if (2 == MEDIA_CORE) {
                    return new ExoMediaPlayer(LivePlayerActivity.this);
                } else {
                    return null;
                }
            }
        });
  • 自定义解码器请参考Demo中的JkMediaPlayer和ExoMediaPlayer类。
5、转场无缝衔接播放实现
5.1、列表转场衔接继续播放原理:
    1、点击跳转到新的界面时将播放器从父容器中移除,并保存到全局变量
    2、将全局变量播放器对象添加到新的ViewGroup容器
    3、回到列表界面时如果播放的视频源没有被切换,关闭当前Activity不要销毁播放器,将播放器从当前父容器中移除
    4、重新添加到列表界面的此前正在播放的item中的ViewGroup中
5.2、列表转场衔接继续播放实现:主要参考Demo中的ListPlayerChangedFragment、ListPlayerFragment、VideoDetailsActivity类
    1、开始播放:参考ListPlayerFragment类的startPlayer()方法,注意标记当前mCurrentPosition和mPlayerContainer
    2、点击item跳转:参考ListPlayerChangedFragment类的onItemClick()方法,跳转到新的Activity
    3、新的Activity接收播放器继续播放:参考VideoDetailsActivity类的initPlayer方法,根据mIsChange变量来确认是否处理转场播放。
    4、新的Activity销毁:新的Activity在关闭时如果播放器视频地址未被切换,则在onDestroy中不要销毁播放器,参考:VideoDetailsActivity类的onDestroy
    5、回到列表界面:如果处理了第4步,在回到列表界面时接收并处理播放器,参考:ListPlayerChangedFragment类的onActivityResult方法和ListPlayerFragment类的recoverPlayerParent方法
6、Window窗口播放实现
    /**
     * 开启Activity级别的小窗口播放
     * @param width 窗口播放器的宽,当小于=0时用默认
     * 开启可拖拽的窗口播放
     * 默认宽为屏幕1/2+30dp,高为1/2+30dp的16:9比例,X起始位置为:播放器原宿主的右下方,距离原宿主View顶部15dp,右边15dp(如果原宿主不存在,则位于屏幕右上角距离顶部60dp位置)
     * 全局悬浮窗口和局部小窗口不能同时开启
     * 横屏下不允许开启
     * @param height 窗口播放器的高,当小于=0时用默认
     * @param startX 窗口位于屏幕中的X轴起始位置,当小于=0时用默认
     * @param startY 窗口位于屏幕中的Y轴起始位置
     * @param radius 窗口的圆角 单位:像素
     * @param bgColor 窗口的背景颜色
     */
    @Override
    public void startWindow(int width, int height, float startX, float startY, float radius, int bgColor) {
        ILogger.d(TAG,"startWindow-->width:"+width+",height:"+height+",startX:"+startX+",startY:"+startY+",radius:"+radius+",bgColor:"+bgColor+",windowProperty:"+ mIsActivityWindow +",screenOrientation:"+mScreenOrientation);
        if(mIsActivityWindow ||mScreenOrientation==IMediaPlayer.ORIENTATION_LANDSCAPE) return;//已开启窗口模式或者横屏情况下不允许开启小窗口
        Activity activity = PlayerUtils.getInstance().getActivity(getTargetContext());
        if (null != activity&& !activity.isFinishing()) {
            ViewGroup viewGroup = (ViewGroup) activity.getWindow().getDecorView();
            if(null==viewGroup){
                return;
            }
            int[] screenLocation=new int[2];
            //保存播放器本身的宽高和位于父容器的索引位置,恢复正常模式时需准确的还原到父容器index
            mPlayerParams = new int[3];
            mPlayerParams[0]=this.getMeasuredWidth();
            mPlayerParams[1]=this.getMeasuredHeight();
            //1.从原有竖屏窗口移除自己前保存自己的Parent,直接开启全屏是不存在宿主ViewGroup的,可直接窗口转场
            if(null!=getParent()&& getParent() instanceof ViewGroup){
                mParent = (ViewGroup) getParent();
                mParent.getLocationInWindow(screenLocation);
                mPlayerParams[2]=mParent.indexOfChild(this);
//                ILogger.d(TAG,"startWindow-->parent_id:"+getId()+",parentX:"+screenLocation[0]+",parentY:"+screenLocation[1]+",parentWidth:"+mParent.getWidth()+",parentHeight:"+mParent.getHeight());
            }
            PlayerUtils.getInstance().removeViewFromParent(this);//从原宿主中移除自己
            //2.改变播放器横屏或窗口播放状态
            setWindowPropertyPlayer(true,false);
            //3.获取宿主的View属性和startX、Y轴
            //如果传入的宽高不存在,则使用默认的16:9的比例创建Window View
            if(width<=0){
                width = PlayerUtils.getInstance().getScreenWidth(getContext())/2+PlayerUtils.getInstance().dpToPxInt(30f);
                height = width*9/16;
//                ILogger.d(TAG,"startWindow-->未传入宽高,width:"+width+",height:"+height);
            }
            //如果传入的startX不存在,则startX起点位于屏幕宽度1/2-距离右侧15dp位置,startY起点位于宿主View的下方15dp处
            if(startX<=0&&null!=mParent){
                startX=(PlayerUtils.getInstance().getScreenWidth(getContext())/2-PlayerUtils.getInstance().dpToPxInt(30f))-PlayerUtils.getInstance().dpToPxInt(15f);
                startY=screenLocation[1]+mParent.getHeight()+PlayerUtils.getInstance().dpToPxInt(15f);
//                ILogger.d(TAG,"startWindow-->未传入X,Y轴,取父容器位置,startX:"+startX+",startY:"+startY);
            }
            //如果宿主也不存在,则startX起点位于屏幕宽度1/2-距离右侧15dp位置,startY起点位于屏幕高度-Window View 高度+15dp位置处
            if(startX<=0){
                startX=(PlayerUtils.getInstance().getScreenWidth(getContext())/2-PlayerUtils.getInstance().dpToPxInt(30f))-PlayerUtils.getInstance().dpToPxInt(15f);
                startY=PlayerUtils.getInstance().dpToPxInt(60f);
//                ILogger.d(TAG,"startWindow-->未传入X,Y轴或取父容器位置失败,startX:"+startX+",startY:"+startY);
            }
            ILogger.d(TAG,"startWindow-->final:width:"+width+",height:"+height+",startX:"+startX+",startY:"+startY);
            //4.转场到window中,并指定宽高和x,y轴
            WindowPlayerFloatView container=new WindowPlayerFloatView(viewGroup.getContext());
            container.setOnWindowActionListener(new OnWindowActionListener() {
                @Override
                public void onMovie(float x, float y) {

                }

                @Override
                public void onClick(BasePlayer basePlayer, Object coustomParams) {

                }

                @Override
                public void onClose() {
//                    ILogger.d(TAG,"startWindow-->onClose");
                    quitWindow();//退出小窗口
                }
            });
            container.setId(R.id.player_window);
            container.addPlayerView(this,width,height,startX,startY,radius,bgColor);//先将播放器包装到可托拽的容器中
            viewGroup.addView(container, new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER));
        }
    }

三、更多功能和全部源码请移步至iPlayer

  • 全部功能请阅读接入文档

你可能感兴趣的:(Android视频播放器封装)