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)建播放器。符合開閉原則
- 適配器模式
- 這個(gè)也是事后補(bǔ)救模式,但是在該庫中,沒有嘗試這種方式。https://www.runoob.com/design-pattern/adapter-pattern.html
- 如何做到內(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