02.視頻播放器整體結構
目錄介紹
- 01.視頻常見的佈局視圖
- 02.後期可能涉及的視圖
- 03.需要達到的目的和效果
- 04.視頻視圖層級示意圖
- 05.整體架構思路分析流程
- 06.如何創建不同播放器
- 07.如何友好處理播放器UI
- 08.交互交給外部開發者
- 09.關於優先級視圖展示
- 10.代碼項目lib代碼介紹
00.視頻播放器通用框架
- 基礎封裝視頻播放器player,可以在ExoPlayer、MediaPlayer,聲網RTC視頻播放器內核,原生MediaPlayer可以自由切換
- 對於視圖狀態切換和後期維護拓展,避免功能和業務出現耦合。比如需要支持播放器UI高度定製,而不是該lib庫中UI代碼
- 針對視頻播放,音頻播放,播放回放,以及視頻直播的功能。使用簡單,代碼拓展性強,封裝性好,主要是和業務徹底解耦,暴露接口監聽給開發者處理業務具體邏輯
- 該播放器整體架構:播放器內核(自由切換) + 視頻播放器 + 邊播邊緩存 + 高度定製播放器UI視圖層
- 項目地址:https://github.com/yangchong211/YCVideoPlayer
- 關於視頻播放器整體功能介紹文檔:https://juejin.im/post/6883457444752654343
01.視頻常見的佈局視圖
- 視頻底圖(用於顯示初始化視頻時的封面圖),視頻狀態視圖【加載loading,播放異常,加載視頻失敗,播放完成等】
- 改變亮度和聲音【改變聲音視圖,改變亮度視圖】,改變視頻快進和快退,左右滑動快進和快退視圖(手勢滑動的快進快退提示框)
- 頂部控制區視圖(包含返回健,title等),底部控制區視圖(包含進度條,播放暫停,時間,切換全屏等)
- 鎖屏佈局視圖(全屏時展示,其他隱藏),底部播放進度條視圖(很多播放器都有這個),清晰度列表視圖(切換清晰度彈窗)
- 底部播放進度條視圖(很多播放器都有這個),當bottom視圖顯示時底部進度條隱藏,反之則顯示
02.後期可能涉及的視圖
- 手勢指導頁面(有些播放器有新手指導功能),離線下載的界面(該界面中包含下載列表, 列表的item編輯(全選, 刪除))
- 用戶從wifi切換到4g網絡,提示網絡切換彈窗界面(當網絡由wifi變為4g的時候會顯示)
- 圖片廣告視圖(帶有倒計時消失),開始視頻廣告視圖,非會員試看視圖
- 彈幕視圖(這個很重要),水印顯示視圖,倍速播放界面(用於控制倍速),底部視頻列表縮略圖視圖
- 投屏視頻視圖界面,視頻直播間刷禮物界面,老師開課界面,展示更多視圖(下載,分享,切換音頻等)
03.需要達到的目的和效果
- 基礎封裝視頻播放器player,可以在ExoPlayer、MediaPlayer,聲網RTC視頻播放器內核,原生MediaPlayer可以自由切換
- 對於視圖狀態切換和後期維護拓展,避免功能和業務出現耦合。比如需要支持播放器UI高度定製,而不是該lib庫中UI代碼
- 針對視頻播放,音頻播放,播放回放,以及視頻直播的功能。使用簡單,代碼拓展性強,封裝性好,主要是和業務徹底解耦,暴露接口監聽給開發者處理業務具體邏輯
04.視頻視圖層級示意圖
05.整體架構思路分析流程
-
播放器內核
- 可以切換ExoPlayer、MediaPlayer,IjkPlayer,聲網視頻播放器,這裡使用工廠模式Factory + AbstractVideoPlayer + 各個實現AbstractVideoPlayer抽象類的播放器類
- 定義抽象的播放器,主要包含視頻初始化,設置,狀態設置,以及播放監聽。由於每個內核播放器api可能不一樣,所以這裡需要實現AbstractVideoPlayer抽象類的播放器類,方便後期統一調用
- 為了方便創建不同內核player,所以需要創建一個PlayerFactory,定義一個createPlayer創建播放器的抽象方法,然後各個內核都實現它,各自創建自己的播放器
-
VideoPlayer播放器
- 可以自由切換視頻內核,Player+Controller。player負責播放的邏輯,Controller負責視圖相關的邏輯,兩者之間用接口進行通信
- 針對Controller,需要定義一個接口,主要負責視圖UI處理邏輯,支持添加各種自定義視圖View【統一實現自定義接口Control】,每個view儘量保證功能單一性,最後通過addView形式添加進來
- 針對Player,需要定義一個接口,主要負責視頻播放處理邏輯,比如視頻播放,暫停,設置播放進度,設置視頻鏈接,切換播放模式等操作。需要注意把Controller設置到Player裡面,兩者之間通過接口交互
-
UI控制器視圖
- 定義一個BaseVideoController類,這個主要是集成各種事件的處理邏輯,比如播放器狀態改變,控制視圖隱藏和顯示,播放進度改變,鎖定狀態改變,設備方向監聽等等操作
- 定義一個view的接口InterControlView,在這裡類裡定義綁定視圖,視圖隱藏和顯示,播放狀態,播放模式,播放進度,鎖屏等操作。這個每個實現類則都可以拿到這些屬性呢
- 在BaseVideoController中使用LinkedHashMap保存每個自定義view視圖,添加則put進來後然後通過addView將視圖添加到該控制器中,這樣非常方便添加自定義視圖
- 播放器切換狀態需要改變Controller視圖,比如視頻異常則需要顯示異常視圖view,則它們之間的交互是通過ControlWrapper(同時實現Controller接口和Player接口)實現
06.如何創建不同播放器
-
目標要求
- 基礎播放器封裝了包含ExoPlayer、MediaPlayer,ijkPlayer,聲網視頻播放器等
- 可以自由切換初始化任何一種視頻播放器,比如通過構造傳入類型參數來創建不同的視頻播放器
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);
-
使用那種形式創建播放器
-
工廠模式
- 隱藏內核播放器創建具體細節,開發者只需要關心所需產品對應的工廠,無須關心創建細節即可創建播放器。符合開閉原則
-
適配器模式
- 這個也是事後補救模式,但是在該庫中,沒有嘗試這種方式。https://www.runoob.com/design-pattern/adapter-pattern.html
-
如何做到內核無縫切換?
- 具體的代碼案例,以及具體做法,在下一篇博客中會介紹到。或者直接看代碼:視頻播放器
-
-
播放器內核的架構圖如下所示
07.如何友好處理播放器UI
-
發展中遇到的問題
- 播放器可支持多種場景下的播放,多個產品會用到同一個播放器,這樣就會帶來一個問題,一個播放業務播放器狀態發生變化,其他播放業務必須同步更新播放狀態,各個播放業務之間互相交叉,隨著播放業務的增多,開發和維護成本會急劇增加, 導致後續開發不可持續。
-
播放器內核和UI層耦合
- 也就是說視頻player和ui操作柔和到了一起,尤其是兩者之間的交互。比如播放中需要更新UI進度條,播放異常需要顯示異常UI,都比較難處理播放器狀態變化更新UI操作
-
UI難以自定義或者修改麻煩
- 比如常見的視頻播放器,會把視頻各種視圖寫到xml中,這種方式在後期代碼會很大,而且改動一個小的佈局,則會影響大。這樣到後期往往只敢加代碼,而不敢刪除代碼……
- 有時候難以適應新的場景,比如添加一個播放廣告,老師開課,或者視頻引導業務需求,則需要到播放器中寫一堆業務代碼。迭代到後期,違背了開閉原則,視頻播放器需要做到和業務分離
-
視頻播放器結構需要清晰
- 這個是指該視頻播放器能否看了文檔後快速上手,知道封裝的大概流程。方便後期他人修改和維護,因此需要將視頻播放器功能分離。比如切換內核+視頻播放器(player+controller+view)
-
一定要解耦合
- 播放器player與視頻UI解耦:支持添加自定義視頻視圖,比如支持添加自定義廣告,新手引導,或者視頻播放異常等視圖,這個需要較強的拓展性
-
適合多種業務場景
- 比如適合播放單個視頻,多個視頻,以及列表視頻,或者類似抖音那種一個頁面一個視頻,還有小窗口播放視頻。也就是適合大多數業務場景
-
具體操作
- 播放狀態變化是導致不同播放業務場景之間交叉同步,解除播放業務對播放器的直接操控,採用接口監聽進行解耦。比如:player+controller+interface
- 具體的代碼案例,以及具體做法,在下一篇博客中會介紹到。或者直接看代碼:視頻播放器
08.交互交給外部開發者
- 在播放器中,很重要一個就是需要把播放器player的播放模式(小屏幕,正常,全屏模式),以及播放狀態(播放,暫停,異常,完成,加載,緩衝等多種狀態)暴露給控制層view,方便做UI更新。
-
比如外部開發者想加一個廣告視圖,這個時候肯定需要給它播放器的狀態
- 添加了自定義播放器視圖,比如添加視頻廣告,可以選擇跳過,選擇播放暫停。那這個視圖view,肯定是需要操作player或者獲取player的狀態的。這個時候就需要暴露監聽視頻播放的狀態接口監聽
- 首先定義一個InterControlView接口,也就是說所有自定義視頻視圖view需要實現這個接口,該接口中的核心方法有:綁定視圖到播放器,視圖顯示隱藏變化監聽,播放狀態監聽,播放模式監聽,進度監聽,鎖屏監聽等
- 在BaseVideoController中的狀態監聽中,通過InterControlView接口對象就可以把播放器的狀態傳遞到子類中
-
舉一個代碼的例子
- 比如,現在有個業務需求,需要在視頻播放器剛開始添加一個廣告視圖,等待廣告倒計時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();
09.關於優先級視圖展示
-
視頻播放器為了拓展性,需要暴露view接口供外部開發者自定義視頻播放器視圖,通過addView的形式添加到播放器的控制器中。
- 這就涉及view視圖的層級性。控制view視圖的顯示和隱藏是特別重要的,這個時候在自定義view中就需要拿到播放器的狀態
-
舉一個簡單的例子,基礎視頻播放器
- 添加了基礎播放功能的幾個播放視圖。有播放完成,播放異常,播放加載,頂部標題欄,底部控制條欄,鎖屏,以及手勢滑動欄。如何控制它們的顯示隱藏切換呢?
- 在addView這些視圖時,大多數的view都是默認GONE隱藏的。比如當視頻初始化時,先緩衝則顯示緩衝view而隱藏其他視圖,接著播放則顯示頂部/底部視圖而隱藏其他視圖
-
比如有時候需要顯示兩種不同的自定義視圖如何處理
- 舉個例子,播放的時候,點擊一下視頻,會顯示頂部title視圖和底部控制條視圖,那麼這樣會同時顯示兩個視圖。
- 點擊頂部title視圖的返回鍵可以關閉播放器,點擊底部控制條視圖的播放暫停可以控制播放條件。這個時候底部控制條視圖FrameLayout的ChildView在整個視頻的底部,頂部title視圖FrameLayout的ChildView在整個視頻的頂部,這樣可以達到上下層都可以相應事件。
-
那麼FrameLayout層層重疊,如何讓下層不響應事件
- 在最上方顯示的層加上: android:clickable="true" 可以避免點擊上層觸發底層。或者直接給控制設置一個background顏色也可以。