Android視頻播放器封裝

寫在前面:
  • 因項(xiàng)目需要,需要使用到視頻播放相關(guān)技術(shù),雖然系統(tǒng)提供了播放器VideoView,但由于各種原因無(wú)法滿足項(xiàng)目需要,特將播放器封裝成庫(kù),方便日后項(xiàng)目使用及自定義拓展。 此文章適合未接觸過(guò)視頻播放相關(guān)、沒(méi)有時(shí)間來(lái)研究視頻播放相關(guān)、不想寫UI交互直接用現(xiàn)成的成熟播放器 的開發(fā)者閱讀,大神大牛請(qǐng)繞路。
給大家推薦視頻播放器iPlayer,支持的特性包括但不限于:
  • 支持網(wǎng)絡(luò)地址、直播流、本地Assets和Raw音視頻資源文件播放
  • 支持IJKPlayer、ExoPlayer、MediaPlayer和其它更多自定義解碼器
  • 支持自定義視頻解碼器、控制器、UI交互組件、視頻畫面渲染器
  • 支持播放倍速、縮放模式、靜音、鏡像等功能設(shè)置
  • 支持多播放器同時(shí)播放、跳轉(zhuǎn)到詳情無(wú)縫銜接播放
  • 支持重力感應(yīng)橫豎屏旋轉(zhuǎn)及開關(guān)設(shè)置
  • 支持無(wú)權(quán)限開啟Activity級(jí)別窗口播放及全局懸浮窗窗口播放
  • 窗口播放器支持自動(dòng)吸附、懸停
  • Demo仿抖音播放示例,支持視頻緩存、秒播、彈幕交互等
    Github無(wú)法訪問(wèn)可訪問(wèn)碼云項(xiàng)目地址

一、播放器框架設(shè)計(jì)

iPlayer架構(gòu)關(guān)系圖

二、播放器功能實(shí)現(xiàn)

1、畫面渲染(TextureView)
1.1、TextureView創(chuàng)建及設(shè)置Surface監(jiān)聽
        TextureView textureView =new TextureView(context);
        textureView .setSurfaceTextureListener(this);
1.2、在TextureView初始化完成的onSurfaceTextureAvailable回調(diào)里將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、開啟全屏播放
  • 全屏分三個(gè)步驟:1、保存播放器父容器ViewGroup。2、改變屏幕方向?yàn)闄M屏。3、將播放器添加到Window中。
    /**
     * 全屏播放
     * @param bgColor 開啟全屏模式播放:橫屏?xí)r播放器的背景顏色,內(nèi)部默認(rèn)用黑色#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層級(jí)等屬性(如果存在的話)
            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);//保存播放器本身的寬高和位于父容器的索引位置,恢復(fù)正常模式時(shí)需準(zhǔn)確的還原到父容器index
            }
            PlayerUtils.getInstance().removeViewFromParent(this);//從原宿主中移除自己
            //2.改變屏幕方向?yàn)闄M屏狀態(tài),播放器所在的Activity需要添加屬性:android:configChanges="orientation|screenSize"
            activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);//改變屏幕方向
            setScreenOrientation(IMediaPlayer.ORIENTATION_LANDSCAPE);//更新控制器方向狀態(tài)
            findViewById(R.id.player_surface).setBackgroundColor(bgColor!=0?bgColor:Color.parseColor("#000000"));//設(shè)置一個(gè)背景顏色
            //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、退出全屏播放
  • 退出全屏分四個(gè)步驟:1、Window窗口中移除自己。2、改變屏幕方向?yàn)樨Q屏。3、還原全屏設(shè)置為正常設(shè)置。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.改變屏幕方向?yàn)樨Q屏
            activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);//改變屏幕方向
            setScreenOrientation(IMediaPlayer.ORIENTATION_PORTRAIT);
            findViewById(R.id.player_surface).setBackgroundColor(Color.parseColor("#00000000"));//設(shè)置純透明背景
            //3.還原全屏設(shè)置為正常設(shè)置
            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{
                //通知宿主監(jiān)聽器觸發(fā)返回事件
//                ILogger.d(TAG,"quitLandscapeScreen-->退出全屏無(wú)宿主接收,銷毀播放器");
                //無(wú)宿主接收時(shí)直接停止播放并銷毀播放器
                onDestroy();
            }
        }
    }
3、自定義控制器及UI交互組件
3.1、自定義控制器
  • BasePlayer提供了setController(BaseController controller)方法給有需要UI交互的場(chǎng)景設(shè)置UI控制器
    /**
     * 設(shè)置視圖控制器
     * @param controller 繼承VideoBaseController的控制器
     */
    @Override
    public void setController(BaseController controller) {}
3.2、自定義UI交互組件
  • 為什么有自定義Controller還整個(gè)自定義UI交互組件?因?yàn)镃ontroller不適合處理所有UI交互,比如播放器的場(chǎng)景不同,UI也不盡相同,此時(shí)若把所有UI交互全寫在Controller會(huì)顯得臃腫、耦合性過(guò)高、開發(fā)者無(wú)法根據(jù)自己的需要來(lái)選擇和自定義部分UI交互。
  • 自定義交互組件的使用
        //播放器的準(zhǔn)備
        VideoPlayer videoPlayer = new VideoPlayer(this);
        videoPlayer.setBackgroundColor(Color.parseColor("#000000"));
        VideoController controller=new VideoController(videoPlayer.getContext());
        /**
         * 給播放器設(shè)置控制器
         */
        videoPlayer.setController(controller);
        /**
         * 給播放器控制器綁定需要的自定義UI交互組件
         */
        ControlToolBarView toolBarView=new ControlToolBarView(this);//標(biāo)題欄,返回按鈕、視頻標(biāo)題、功能按鈕、系統(tǒng)時(shí)間、電池電量等組件
        ControlFunctionBarView functionBarView=new ControlFunctionBarView(this);//底部時(shí)間、seek、靜音、全屏功能欄
        functionBarView.showSoundMute(true,false);//啟用靜音功能交互\默認(rèn)不靜音
        ControlStatusView statusView=new ControlStatusView(this);//移動(dòng)網(wǎng)絡(luò)播放提示、播放失敗、試看完成
        ControlGestureView gestureView=new ControlGestureView(this);//手勢(shì)控制屏幕亮度、系統(tǒng)音量、快進(jìn)、快退UI交互
        ControlCompletionView completionView=new ControlCompletionView(this);//播放完成、重試
        ControlLoadingView loadingView=new ControlLoadingView(this);//加載中、開始播放
        //將自定義UI交互組件設(shè)置到控制器
        controller.addControllerWidget(toolBarView,functionBarView,statusView,gestureView,completionView,loadingView);
4、自定義解碼器
  • SDK內(nèi)部封裝時(shí),為了方便開發(fā)者切換解碼器,將切換解碼器的入口封裝在播放器的監(jiān)聽器內(nèi),開發(fā)者可在回調(diào)方法返回自己的解碼器。
    private int MEDIA_CORE=2;//這里默認(rèn)用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;
                }
            }
        });
  • 自定義解碼器請(qǐng)參考Demo中的JkMediaPlayer和ExoMediaPlayer類。
5、轉(zhuǎn)場(chǎng)無(wú)縫銜接播放實(shí)現(xiàn)
5.1、列表轉(zhuǎn)場(chǎng)銜接繼續(xù)播放原理:
    1、點(diǎn)擊跳轉(zhuǎn)到新的界面時(shí)將播放器從父容器中移除,并保存到全局變量
    2、將全局變量播放器對(duì)象添加到新的ViewGroup容器
    3、回到列表界面時(shí)如果播放的視頻源沒(méi)有被切換,關(guān)閉當(dāng)前Activity不要銷毀播放器,將播放器從當(dāng)前父容器中移除
    4、重新添加到列表界面的此前正在播放的item中的ViewGroup中
5.2、列表轉(zhuǎn)場(chǎng)銜接繼續(xù)播放實(shí)現(xiàn):主要參考Demo中的ListPlayerChangedFragment、ListPlayerFragmentVideoDetailsActivity
    1、開始播放:參考ListPlayerFragment類的startPlayer()方法,注意標(biāo)記當(dāng)前mCurrentPosition和mPlayerContainer
    2、點(diǎn)擊item跳轉(zhuǎn):參考ListPlayerChangedFragment類的onItemClick()方法,跳轉(zhuǎn)到新的Activity
    3、新的Activity接收播放器繼續(xù)播放:參考VideoDetailsActivity類的initPlayer方法,根據(jù)mIsChange變量來(lái)確認(rèn)是否處理轉(zhuǎn)場(chǎng)播放。
    4、新的Activity銷毀:新的Activity在關(guān)閉時(shí)如果播放器視頻地址未被切換,則在onDestroy中不要銷毀播放器,參考:VideoDetailsActivity類的onDestroy
    5、回到列表界面:如果處理了第4步,在回到列表界面時(shí)接收并處理播放器,參考:ListPlayerChangedFragment類的onActivityResult方法和ListPlayerFragment類的recoverPlayerParent方法
6、Window窗口播放實(shí)現(xiàn)
    /**
     * 開啟Activity級(jí)別的小窗口播放
     * @param width 窗口播放器的寬,當(dāng)小于=0時(shí)用默認(rèn)
     * 開啟可拖拽的窗口播放
     * 默認(rèn)寬為屏幕1/2+30dp,高為1/2+30dp的16:9比例,X起始位置為:播放器原宿主的右下方,距離原宿主View頂部15dp,右邊15dp(如果原宿主不存在,則位于屏幕右上角距離頂部60dp位置)
     * 全局懸浮窗口和局部小窗口不能同時(shí)開啟
     * 橫屏下不允許開啟
     * @param height 窗口播放器的高,當(dāng)小于=0時(shí)用默認(rèn)
     * @param startX 窗口位于屏幕中的X軸起始位置,當(dāng)小于=0時(shí)用默認(rèn)
     * @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];
            //保存播放器本身的寬高和位于父容器的索引位置,恢復(fù)正常模式時(shí)需準(zhǔn)確的還原到父容器index
            mPlayerParams = new int[3];
            mPlayerParams[0]=this.getMeasuredWidth();
            mPlayerParams[1]=this.getMeasuredHeight();
            //1.從原有豎屏窗口移除自己前保存自己的Parent,直接開啟全屏是不存在宿主ViewGroup的,可直接窗口轉(zhuǎn)場(chǎng)
            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.改變播放器橫屏或窗口播放狀態(tài)
            setWindowPropertyPlayer(true,false);
            //3.獲取宿主的View屬性和startX、Y軸
            //如果傳入的寬高不存在,則使用默認(rèn)的16:9的比例創(chuàng)建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起點(diǎn)位于屏幕寬度1/2-距離右側(cè)15dp位置,startY起點(diǎn)位于宿主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起點(diǎn)位于屏幕寬度1/2-距離右側(cè)15dp位置,startY起點(diǎn)位于屏幕高度-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.轉(zhuǎn)場(chǎng)到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));
        }
    }

三、更多功能和全部源碼請(qǐng)移步至iPlayer

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容