02.視頻播放器整體結(jié)構(gòu)

02.視頻播放器整體結(jié)構(gòu)

目錄介紹

  • 01.視頻常見的布局視圖
  • 02.后期可能涉及的視圖
  • 03.需要達(dá)到的目的和效果
  • 04.視頻視圖層級(jí)示意圖
  • 05.整體架構(gòu)思路分析流程
  • 06.如何創(chuàng)建不同播放器
  • 07.如何友好處理播放器UI
  • 08.交互交給外部開發(fā)者
  • 09.關(guān)于優(yōu)先級(jí)視圖展示
  • 10.代碼項(xiàng)目lib代碼介紹

00.視頻播放器通用框架

  • 基礎(chǔ)封裝視頻播放器player,可以在ExoPlayer、MediaPlayer,聲網(wǎng)RTC視頻播放器內(nèi)核,原生MediaPlayer可以自由切換
  • 對(duì)于視圖狀態(tài)切換和后期維護(hù)拓展,避免功能和業(yè)務(wù)出現(xiàn)耦合。比如需要支持播放器UI高度定制,而不是該lib庫中UI代碼
  • 針對(duì)視頻播放,音頻播放,播放回放,以及視頻直播的功能。使用簡(jiǎn)單,代碼拓展性強(qiáng),封裝性好,主要是和業(yè)務(wù)徹底解耦,暴露接口監(jiān)聽給開發(fā)者處理業(yè)務(wù)具體邏輯
  • 該播放器整體架構(gòu):播放器內(nèi)核(自由切換) + 視頻播放器 + 邊播邊緩存 + 高度定制播放器UI視圖層
  • 項(xiàng)目地址:https://github.com/yangchong211/YCVideoPlayer
  • 關(guān)于視頻播放器整體功能介紹文檔:https://juejin.im/post/6883457444752654343

01.視頻常見的布局視圖

  • 視頻底圖(用于顯示初始化視頻時(shí)的封面圖),視頻狀態(tài)視圖【加載loading,播放異常,加載視頻失敗,播放完成等】
  • 改變亮度和聲音【改變聲音視圖,改變亮度視圖】,改變視頻快進(jìn)和快退,左右滑動(dòng)快進(jìn)和快退視圖(手勢(shì)滑動(dòng)的快進(jìn)快退提示框)
  • 頂部控制區(qū)視圖(包含返回健,title等),底部控制區(qū)視圖(包含進(jìn)度條,播放暫停,時(shí)間,切換全屏等)
  • 鎖屏布局視圖(全屏?xí)r展示,其他隱藏),底部播放進(jìn)度條視圖(很多播放器都有這個(gè)),清晰度列表視圖(切換清晰度彈窗)
  • 底部播放進(jìn)度條視圖(很多播放器都有這個(gè)),當(dāng)bottom視圖顯示時(shí)底部進(jìn)度條隱藏,反之則顯示

02.后期可能涉及的視圖

  • 手勢(shì)指導(dǎo)頁面(有些播放器有新手指導(dǎo)功能),離線下載的界面(該界面中包含下載列表, 列表的item編輯(全選, 刪除))
  • 用戶從wifi切換到4g網(wǎng)絡(luò),提示網(wǎng)絡(luò)切換彈窗界面(當(dāng)網(wǎng)絡(luò)由wifi變?yōu)?g的時(shí)候會(huì)顯示)
  • 圖片廣告視圖(帶有倒計(jì)時(shí)消失),開始視頻廣告視圖,非會(huì)員試看視圖
  • 彈幕視圖(這個(gè)很重要),水印顯示視圖,倍速播放界面(用于控制倍速),底部視頻列表縮略圖視圖
  • 投屏視頻視圖界面,視頻直播間刷禮物界面,老師開課界面,展示更多視圖(下載,分享,切換音頻等)

03.需要達(dá)到的目的和效果

  • 基礎(chǔ)封裝視頻播放器player,可以在ExoPlayer、MediaPlayer,聲網(wǎng)RTC視頻播放器內(nèi)核,原生MediaPlayer可以自由切換
  • 對(duì)于視圖狀態(tài)切換和后期維護(hù)拓展,避免功能和業(yè)務(wù)出現(xiàn)耦合。比如需要支持播放器UI高度定制,而不是該lib庫中UI代碼
  • 針對(duì)視頻播放,音頻播放,播放回放,以及視頻直播的功能。使用簡(jiǎn)單,代碼拓展性強(qiáng),封裝性好,主要是和業(yè)務(wù)徹底解耦,暴露接口監(jiān)聽給開發(fā)者處理業(yè)務(wù)具體邏輯

04.視頻視圖層級(jí)示意圖

image

05.整體架構(gòu)思路分析流程

  • 播放器內(nèi)核
    • 可以切換ExoPlayer、MediaPlayer,IjkPlayer,聲網(wǎng)視頻播放器,這里使用工廠模式Factory + AbstractVideoPlayer + 各個(gè)實(shí)現(xiàn)AbstractVideoPlayer抽象類的播放器類
    • 定義抽象的播放器,主要包含視頻初始化,設(shè)置,狀態(tài)設(shè)置,以及播放監(jiān)聽。由于每個(gè)內(nèi)核播放器api可能不一樣,所以這里需要實(shí)現(xiàn)AbstractVideoPlayer抽象類的播放器類,方便后期統(tǒng)一調(diào)用
    • 為了方便創(chuàng)建不同內(nèi)核player,所以需要?jiǎng)?chuàng)建一個(gè)PlayerFactory,定義一個(gè)createPlayer創(chuàng)建播放器的抽象方法,然后各個(gè)內(nèi)核都實(shí)現(xiàn)它,各自創(chuàng)建自己的播放器
  • VideoPlayer播放器
    • 可以自由切換視頻內(nèi)核,Player+Controller。player負(fù)責(zé)播放的邏輯,Controller負(fù)責(zé)視圖相關(guān)的邏輯,兩者之間用接口進(jìn)行通信
    • 針對(duì)Controller,需要定義一個(gè)接口,主要負(fù)責(zé)視圖UI處理邏輯,支持添加各種自定義視圖View【統(tǒng)一實(shí)現(xiàn)自定義接口Control】,每個(gè)view盡量保證功能單一性,最后通過addView形式添加進(jìn)來
    • 針對(duì)Player,需要定義一個(gè)接口,主要負(fù)責(zé)視頻播放處理邏輯,比如視頻播放,暫停,設(shè)置播放進(jìn)度,設(shè)置視頻鏈接,切換播放模式等操作。需要注意把Controller設(shè)置到Player里面,兩者之間通過接口交互
  • UI控制器視圖
    • 定義一個(gè)BaseVideoController類,這個(gè)主要是集成各種事件的處理邏輯,比如播放器狀態(tài)改變,控制視圖隱藏和顯示,播放進(jìn)度改變,鎖定狀態(tài)改變,設(shè)備方向監(jiān)聽等等操作
    • 定義一個(gè)view的接口InterControlView,在這里類里定義綁定視圖,視圖隱藏和顯示,播放狀態(tài),播放模式,播放進(jìn)度,鎖屏等操作。這個(gè)每個(gè)實(shí)現(xiàn)類則都可以拿到這些屬性呢
    • 在BaseVideoController中使用LinkedHashMap保存每個(gè)自定義view視圖,添加則put進(jìn)來后然后通過addView將視圖添加到該控制器中,這樣非常方便添加自定義視圖
    • 播放器切換狀態(tài)需要改變Controller視圖,比如視頻異常則需要顯示異常視圖view,則它們之間的交互是通過ControlWrapper(同時(shí)實(shí)現(xiàn)Controller接口和Player接口)實(shí)現(xiàn)

06.如何創(chuàng)建不同播放器

  • 目標(biāo)要求
    • 基礎(chǔ)播放器封裝了包含ExoPlayer、MediaPlayer,ijkPlayer,聲網(wǎng)視頻播放器等
    • 可以自由切換初始化任何一種視頻播放器,比如通過構(gòu)造傳入類型參數(shù)來創(chuàng)建不同的視頻播放器
    PlayerFactory playerFactory = IjkPlayerFactory.create();
    IjkVideoPlayer ijkVideoPlayer = (IjkVideoPlayer) playerFactory.createPlayer(this);
    PlayerFactory playerFactory = ExoPlayerFactory.create();
    ExoMediaPlayer exoMediaPlayer = (ExoMediaPlayer) playerFactory.createPlayer(this);
    PlayerFactory playerFactory = MediaPlayerFactory.create();
    AndroidMediaPlayer androidMediaPlayer = (AndroidMediaPlayer) playerFactory.createPlayer(this);
    
  • 使用那種形式創(chuàng)建播放器
    • 工廠模式
      • 隱藏內(nèi)核播放器創(chuàng)建具體細(xì)節(jié),開發(fā)者只需要關(guān)心所需產(chǎn)品對(duì)應(yīng)的工廠,無須關(guān)心創(chuàng)建細(xì)節(jié)即可創(chuàng)建播放器。符合開閉原則
    • 適配器模式
    • 如何做到內(nèi)核無縫切換?
      • 具體的代碼案例,以及具體做法,在下一篇博客中會(huì)介紹到?;蛘咧苯涌创a:視頻播放器
  • 播放器內(nèi)核的架構(gòu)圖如下所示
    • image

07.如何友好處理播放器UI

  • 發(fā)展中遇到的問題
    • 播放器可支持多種場(chǎng)景下的播放,多個(gè)產(chǎn)品會(huì)用到同一個(gè)播放器,這樣就會(huì)帶來一個(gè)問題,一個(gè)播放業(yè)務(wù)播放器狀態(tài)發(fā)生變化,其他播放業(yè)務(wù)必須同步更新播放狀態(tài),各個(gè)播放業(yè)務(wù)之間互相交叉,隨著播放業(yè)務(wù)的增多,開發(fā)和維護(hù)成本會(huì)急劇增加, 導(dǎo)致后續(xù)開發(fā)不可持續(xù)。
  • 播放器內(nèi)核和UI層耦合
    • 也就是說視頻player和ui操作柔和到了一起,尤其是兩者之間的交互。比如播放中需要更新UI進(jìn)度條,播放異常需要顯示異常UI,都比較難處理播放器狀態(tài)變化更新UI操作
  • UI難以自定義或者修改麻煩
    • 比如常見的視頻播放器,會(huì)把視頻各種視圖寫到xml中,這種方式在后期代碼會(huì)很大,而且改動(dòng)一個(gè)小的布局,則會(huì)影響大。這樣到后期往往只敢加代碼,而不敢刪除代碼……
    • 有時(shí)候難以適應(yīng)新的場(chǎng)景,比如添加一個(gè)播放廣告,老師開課,或者視頻引導(dǎo)業(yè)務(wù)需求,則需要到播放器中寫一堆業(yè)務(wù)代碼。迭代到后期,違背了開閉原則,視頻播放器需要做到和業(yè)務(wù)分離
  • 視頻播放器結(jié)構(gòu)需要清晰
    • 這個(gè)是指該視頻播放器能否看了文檔后快速上手,知道封裝的大概流程。方便后期他人修改和維護(hù),因此需要將視頻播放器功能分離。比如切換內(nèi)核+視頻播放器(player+controller+view)
  • 一定要解耦合
    • 播放器player與視頻UI解耦:支持添加自定義視頻視圖,比如支持添加自定義廣告,新手引導(dǎo),或者視頻播放異常等視圖,這個(gè)需要較強(qiáng)的拓展性
  • 適合多種業(yè)務(wù)場(chǎng)景
    • 比如適合播放單個(gè)視頻,多個(gè)視頻,以及列表視頻,或者類似抖音那種一個(gè)頁面一個(gè)視頻,還有小窗口播放視頻。也就是適合大多數(shù)業(yè)務(wù)場(chǎng)景
  • 具體操作
    • 播放狀態(tài)變化是導(dǎo)致不同播放業(yè)務(wù)場(chǎng)景之間交叉同步,解除播放業(yè)務(wù)對(duì)播放器的直接操控,采用接口監(jiān)聽進(jìn)行解耦。比如:player+controller+interface
    • 具體的代碼案例,以及具體做法,在下一篇博客中會(huì)介紹到。或者直接看代碼:視頻播放器

08.交互交給外部開發(fā)者

  • 在播放器中,很重要一個(gè)就是需要把播放器player的播放模式(小屏幕,正常,全屏模式),以及播放狀態(tài)(播放,暫停,異常,完成,加載,緩沖等多種狀態(tài))暴露給控制層view,方便做UI更新。
  • 比如外部開發(fā)者想加一個(gè)廣告視圖,這個(gè)時(shí)候肯定需要給它播放器的狀態(tài)
    • 添加了自定義播放器視圖,比如添加視頻廣告,可以選擇跳過,選擇播放暫停。那這個(gè)視圖view,肯定是需要操作player或者獲取player的狀態(tài)的。這個(gè)時(shí)候就需要暴露監(jiān)聽視頻播放的狀態(tài)接口監(jiān)聽
    • 首先定義一個(gè)InterControlView接口,也就是說所有自定義視頻視圖view需要實(shí)現(xiàn)這個(gè)接口,該接口中的核心方法有:綁定視圖到播放器,視圖顯示隱藏變化監(jiān)聽,播放狀態(tài)監(jiān)聽,播放模式監(jiān)聽,進(jìn)度監(jiān)聽,鎖屏監(jiān)聽等
    • 在BaseVideoController中的狀態(tài)監(jiān)聽中,通過InterControlView接口對(duì)象就可以把播放器的狀態(tài)傳遞到子類中
  • 舉一個(gè)代碼的例子
    • 比如,現(xiàn)在有個(gè)業(yè)務(wù)需求,需要在視頻播放器剛開始添加一個(gè)廣告視圖,等待廣告倒計(jì)時(shí)120秒后,直接進(jìn)入播放視頻邏輯。相信這個(gè)業(yè)務(wù)場(chǎng)景很常見,大家都碰到過,使用該播放器就特別簡(jiǎn)單,代碼如下所示:
    • 首先創(chuàng)建一個(gè)自定義view,需要實(shí)現(xiàn)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);
        }
       
        /**
         * 播放狀態(tài)
         * -1               播放錯(cuò)誤
         * 0                播放未開始
         * 1                播放準(zhǔn)備中
         * 2                播放準(zhǔn)備就緒
         * 3                正在播放
         * 4                暫停播放
         * 5                正在緩沖(播放器正在播放時(shí),緩沖區(qū)數(shù)據(jù)不足,進(jìn)行緩沖,緩沖區(qū)數(shù)據(jù)足夠后恢復(fù)播放)
         * 6                暫停緩沖(播放器正在播放時(shí),緩沖區(qū)數(shù)據(jù)不足,進(jìn)行緩沖,此時(shí)暫停播放器,繼續(xù)緩沖,緩沖區(qū)數(shù)據(jù)足夠后恢復(fù)暫停
         * 7                播放完成
         * 8                開始播放中止
         * @param playState                     播放狀態(tài),主要是指播放器的各種狀態(tài)
         */
        @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;
            }
            //暫未實(shí)現(xiàn)全面屏適配邏輯,需要你自己補(bǔ)全
        }
    }
    
    • 然后該怎么使用這個(gè)自定義view呢?很簡(jiǎn)單,在之前基礎(chǔ)上,通過控制器對(duì)象add進(jìn)來即可,代碼如下所示
    controller = new BasisVideoController(this);
    AdControlView adControlView = new AdControlView(this);
    adControlView.setListener(new AdControlView.AdControlListener() {
        @Override
        public void onAdClick() {
            BaseToast.showRoundRectToast( "廣告點(diǎn)擊跳轉(zhuǎn)");
        }
    
        @Override
        public void onSkipAd() {
            playVideo();
        }
    });
    controller.addControlComponent(adControlView);
    //設(shè)置控制器
    mVideoPlayer.setController(controller);
    mVideoPlayer.setUrl(proxyUrl);
    mVideoPlayer.start();
    

09.關(guān)于優(yōu)先級(jí)視圖展示

  • 視頻播放器為了拓展性,需要暴露view接口供外部開發(fā)者自定義視頻播放器視圖,通過addView的形式添加到播放器的控制器中。
    • 這就涉及view視圖的層級(jí)性??刂苬iew視圖的顯示和隱藏是特別重要的,這個(gè)時(shí)候在自定義view中就需要拿到播放器的狀態(tài)
  • 舉一個(gè)簡(jiǎn)單的例子,基礎(chǔ)視頻播放器
    • 添加了基礎(chǔ)播放功能的幾個(gè)播放視圖。有播放完成,播放異常,播放加載,頂部標(biāo)題欄,底部控制條欄,鎖屏,以及手勢(shì)滑動(dòng)欄。如何控制它們的顯示隱藏切換呢?
    • 在addView這些視圖時(shí),大多數(shù)的view都是默認(rèn)GONE隱藏的。比如當(dāng)視頻初始化時(shí),先緩沖則顯示緩沖view而隱藏其他視圖,接著播放則顯示頂部/底部視圖而隱藏其他視圖
  • 比如有時(shí)候需要顯示兩種不同的自定義視圖如何處理
    • 舉個(gè)例子,播放的時(shí)候,點(diǎn)擊一下視頻,會(huì)顯示頂部title視圖和底部控制條視圖,那么這樣會(huì)同時(shí)顯示兩個(gè)視圖。
    • 點(diǎn)擊頂部title視圖的返回鍵可以關(guān)閉播放器,點(diǎn)擊底部控制條視圖的播放暫停可以控制播放條件。這個(gè)時(shí)候底部控制條視圖FrameLayout的ChildView在整個(gè)視頻的底部,頂部title視圖FrameLayout的ChildView在整個(gè)視頻的頂部,這樣可以達(dá)到上下層都可以相應(yīng)事件。
  • 那么FrameLayout層層重疊,如何讓下層不響應(yīng)事件
    • 在最上方顯示的層加上: android:clickable="true" 可以避免點(diǎn)擊上層觸發(fā)底層。或者直接給控制設(shè)置一個(gè)background顏色也可以。

10.代碼項(xiàng)目lib代碼介紹

image

image

image

image
?著作權(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ù)。

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