01.視頻播放器框架介紹

視頻播放器介紹文檔

目錄介紹

  • 01.該視頻播放器介紹
  • 02.視頻播放器功能
  • 03.視頻播放器架構(gòu)說(shuō)明
  • 04.視頻播放器如何使用
  • 05.播放器詳細(xì)Api文檔
  • 06.播放器封裝思路
  • 07.播放器示例展示圖
  • 08.添加自定義視圖
  • 09.視頻播放器優(yōu)化處理
  • 10.播放器問(wèn)題記錄說(shuō)明
  • 11.性能優(yōu)化和庫(kù)大小
  • 12.視頻緩存原理介紹
  • 13.查看視頻播放器日志
  • 14.該庫(kù)異常code說(shuō)明
  • 15.該庫(kù)系列wiki文檔
  • 16.版本更新文檔記錄

00.視頻播放器通用框架

  • 基礎(chǔ)封裝視頻播放器player疚察,可以在ExoPlayer、MediaPlayer比驻,聲網(wǎng)RTC視頻播放器內(nèi)核别惦,原生MediaPlayer可以自由切換
  • 對(duì)于視圖狀態(tài)切換和后期維護(hù)拓展夫椭,避免功能和業(yè)務(wù)出現(xiàn)耦合蹭秋。比如需要支持播放器UI高度定制仁讨,而不是該lib庫(kù)中UI代碼
  • 針對(duì)視頻播放陪竿,音頻播放,播放回放闰挡,以及視頻直播的功能长酗。使用簡(jiǎn)單桐绒,代碼拓展性強(qiáng)茉继,封裝性好烁竭,主要是和業(yè)務(wù)徹底解耦,暴露接口監(jiān)聽給開發(fā)者處理業(yè)務(wù)具體邏輯
  • 該播放器整體架構(gòu):播放器內(nèi)核(自由切換) + 視頻播放器 + 邊播邊緩存 + 高度定制播放器UI視圖層

01.該視頻播放器介紹

1.1 該庫(kù)說(shuō)明

播放器功能 MediaPlayer ExoPlayer IjkPlayer RTC TXPlayer
UI/Player/業(yè)務(wù)解耦 支持 支持 支持
切換視頻播放模式 支持 支持 支持
視頻無(wú)縫切換 支持 支持 支持
調(diào)節(jié)播放進(jìn)度 支持 支持 支持
網(wǎng)絡(luò)環(huán)境監(jiān)聽 支持 支持 支持
滑動(dòng)改變亮度/聲音 支持 支持 支持
設(shè)置視頻播放比例 支持 支持 支持
自由切換視頻內(nèi)核 支持 支持 支持
記錄播放位置 支持 支持 支持
清晰度模式切換 支持 支持 支持
重力感應(yīng)自動(dòng)進(jìn)入 支持 支持 支持
鎖定屏幕功能 支持 支持 支持
倍速播放 不支持 支持 支持
視頻小窗口播放 支持 支持 支持
列表小窗口播放 支持 支持 支持
邊播邊緩存 支持 支持 支持
同時(shí)播放多個(gè)視頻 支持 支持 支持
仿快手預(yù)加載 支持 支持 支持
基于內(nèi)核無(wú)UI 支持 支持 支持
添加彈幕 支持 支持 支持
全屏顯示電量 支持 支持 支持

1.2 該庫(kù)功能說(shuō)明

類型 功能說(shuō)明
項(xiàng)目結(jié)構(gòu) VideoCache緩存lib睬魂,VideoKernel視頻內(nèi)核lib氯哮,VideoPlayer視頻UIlib
內(nèi)核 MediaPlayer喉钢、ExoPlayer出牧、IjkPlayer歇盼,后期接入Rtc和TXPlayer
協(xié)議/格式 http/https豹缀、concat邢笙、rtsp、hls叮雳、rtmp帘不、file杨箭、m3u8互婿、mkv慈参、webm、mp3侈净、mp4等
畫面 調(diào)整顯示比例:默認(rèn)畜侦、16:9旋膳、4:3途事、填充尸变;播放時(shí)旋轉(zhuǎn)畫面角度(0,90,180,270)召烂;鏡像旋轉(zhuǎn)
布局 內(nèi)核和UI分離奏夫,和市面GitHub上大多數(shù)播放器不一樣,方便定制廊谓,通過(guò)addView添加
播放 正常播放蒸痹,小窗播放叠荠,列表播放竖共,仿抖音播放
自定義 可以自定義添加視頻UI層公给,可以說(shuō)UI和Player高度分離淌铐,支持自定義渲染層SurfaceView

02.視頻播放器功能

  • A基礎(chǔ)功能
    • A.1.1 能夠自定義視頻加載loading類型腿准,設(shè)置視頻標(biāo)題,設(shè)置視頻底部圖片街望,設(shè)置播放時(shí)長(zhǎng)等基礎(chǔ)功能
    • A.1.2 可以切換播放器的視頻播放狀態(tài)防症,播放錯(cuò)誤炭玫,播放未開始榴鼎,播放開始盗似,播放準(zhǔn)備中接癌,正在播放椭符,暫停播放,正在緩沖等等狀態(tài)
    • A.1.3 可以自由設(shè)置播放器的播放模式,比如丈秩,正常播放,全屏播放瓶籽,和小屏幕播放汤求。其中全屏播放支持旋轉(zhuǎn)屏幕裤唠。
    • A.1.4 可以支持多種視頻播放類型,比如,原生封裝視頻播放器,還有基于ijkPlayer封裝的播放器。
    • A.1.5 可以設(shè)置是否隱藏播放音量,播放進(jìn)度,播放亮度等,可以通過(guò)拖動(dòng)seekBar改變視頻進(jìn)度。還支持設(shè)置n秒后不操作則隱藏頭部和頂部布局功能
    • A.1.6 可以設(shè)置豎屏模式下全屏模式和橫屏模式下的全屏模式,方便多種使用場(chǎng)景
    • A.1.7 top和bottom面版消失和顯示:點(diǎn)擊視頻畫面會(huì)顯示、隱藏操作面板;顯示后不操作會(huì)5秒后自動(dòng)消失【也可以設(shè)置n秒消失時(shí)間】
  • B高級(jí)功能
    • B.1.1 支持一遍播放一遍緩沖的功能,其中緩沖包括兩部分,第一種是播放過(guò)程中緩沖驶俊,第二種是暫停過(guò)程中緩沖
    • B.1.2 基于ijkPlayer,ExoPlayer想鹰,Rtc,原生MediaPlayer等的封裝播放器何缓,支持多種格式視頻播放
    • B.1.3 可以設(shè)置是否記錄播放位置碌廓,設(shè)置播放速度谷婆,設(shè)置屏幕比例
    • B.1.4 支持滑動(dòng)改變音量【屏幕右邊】期贫,改變屏幕亮度【屏幕左邊】唯灵,屏幕底測(cè)左右滑動(dòng)調(diào)節(jié)進(jìn)度
    • B.1.5 支持list頁(yè)面中視頻播放,滾動(dòng)后暫停播放,播放可以自由設(shè)置是否記錄狀態(tài)斑匪。并且還支持刪除視頻播放位置狀態(tài)庶橱。
    • B.1.6 切換橫豎屏:切換全屏?xí)r,隱藏狀態(tài)欄泉孩,顯示自定義top(顯示電量)句喷;豎屏?xí)r恢復(fù)原有狀態(tài)
    • B.1.7 支持切換視頻清晰度模式
    • B.1.8 添加鎖屏功能脏嚷,豎屏不提供鎖屏按鈕父叙,橫屏全屏?xí)r顯示趾唱,并且鎖屏?xí)r甜癞,屏蔽手勢(shì)處理
  • C拓展功能【這塊根據(jù)實(shí)際情況選擇是否需要使用悠咱,一般視頻付費(fèi)App會(huì)有這個(gè)工鞥】
    • C1產(chǎn)品需求:類似優(yōu)酷析既,愛(ài)奇藝視頻播放器部分邏輯眼坏。比如如果用戶沒(méi)有登錄也沒(méi)有看視頻權(quán)限宰译,則提示試看視頻[自定義布局]沿侈;如果用戶沒(méi)有登錄但是有看視頻權(quán)限缀拭,則正常觀看智厌;如果用戶登錄铣鹏,但是沒(méi)有充值會(huì)員,部分需要權(quán)限視頻則進(jìn)入試看模式葵第,試看結(jié)束后彈出充值會(huì)員界面缀台;如果用戶余額不足哮奇,比如余額只有99元哲身,但是視頻觀看要199元勘天,則又有其他提示脯丝。
    • C2自身需求:比如封裝好了視頻播放庫(kù)伏伐,那么點(diǎn)擊視頻上登錄按鈕則跳到登錄頁(yè)面巾钉;點(diǎn)擊充值會(huì)員頁(yè)面也跳到充值頁(yè)面。這個(gè)通過(guò)定義接口秘案,可以讓使用者通過(guò)方法調(diào)用,靈活處理點(diǎn)擊事件潦匈。
    • C.1.1 可以設(shè)置試看模式阱高,設(shè)置試看時(shí)長(zhǎng)。試看結(jié)束后就提示登錄或者充值……
    • C.1.2 對(duì)于設(shè)置視頻的寬高茬缩,建議設(shè)置成4:3或者16:9或者常用比例,如果不是常用比例凰锡,則可能會(huì)有黑邊未舟。其中黑邊的背景可以設(shè)置
    • C.1.3 可以設(shè)置播放有權(quán)限的視頻時(shí)的各種文字描述,而沒(méi)有把它寫在封裝庫(kù)中掂为,使用者自己設(shè)定
    • C.1.4 鎖定屏幕功能裕膀,這個(gè)參考大部分播放器,只有在全屏模式下才會(huì)有

03.視頻播放器架構(gòu)說(shuō)明

  • 視頻常見(jiàn)的布局視圖
    • 視頻底圖(用于顯示初始化視頻時(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è))珍语,清晰度列表視圖(切換清晰度彈窗)
  • 后期可能涉及的布局視圖
    • 手勢(shì)指導(dǎo)頁(yè)面(有些播放器有新手指導(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è)很重要)拳氢,水印顯示視圖募逞,倍速播放界面(用于控制倍速),底部視頻列表縮略圖視圖
    • 投屏視頻視圖界面馋评,視頻直播間刷禮物界面放接,老師開課界面,展示更多視圖(下載留特,分享纠脾,切換音頻等)
  • 視頻播放器的痛點(diǎn)
    • 播放器內(nèi)核難以切換
      • 不同的視頻播放器內(nèi)核,由于api不一樣蜕青,所以難以切換操作苟蹈。要是想兼容內(nèi)核切換,就必須自己制定一個(gè)視頻接口+實(shí)現(xiàn)類的播放器
    • 播放器內(nèi)核和UI層耦合
      • 也就是說(shuō)視頻player和ui操作柔和到了一起右核,尤其是兩者之間的交互慧脱。比如播放中需要更新UI進(jìn)度條,播放異常需要顯示異常UI贺喝,都比較難處理播放器狀態(tài)變化更新UI操作
    • UI難以自定義或者修改麻煩
      • 比如常見(jiàn)的視頻播放器菱鸥,會(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)
  • 需要達(dá)到的目的和效果
    • 基礎(chǔ)封裝視頻播放器player,可以在ExoPlayer艾凯、MediaPlayer献幔,聲網(wǎng)RTC視頻播放器內(nèi)核,原生MediaPlayer可以自由切換
    • 對(duì)于視圖狀態(tài)切換和后期維護(hù)拓展趾诗,避免功能和業(yè)務(wù)出現(xiàn)耦合蜡感。比如需要支持播放器UI高度定制,而不是該lib庫(kù)中UI代碼
    • 針對(duì)視頻播放恃泪,視頻投屏郑兴,音頻播放,播放回放贝乎,以及視頻直播的功能
  • 通用視頻框架特點(diǎn)
    • 一定要解耦合
      • 播放器內(nèi)核與播放器解耦: 支持更多的播放場(chǎng)景情连、以及新的播放業(yè)務(wù)快速接入,并且不影響其他播放業(yè)務(wù)览效,比如后期添加阿里云播放器內(nèi)核却舀,或者騰訊播放器內(nèi)核
      • 播放器player與視頻UI解耦:支持添加自定義視頻視圖,比如支持添加自定義廣告锤灿,新手引導(dǎo)挽拔,或者視頻播放異常等視圖,這個(gè)需要較強(qiáng)的拓展性
    • 適合多種業(yè)務(wù)場(chǎng)景
      • 比如適合播放單個(gè)視頻衡招,多個(gè)視頻,以及列表視頻每强,或者類似抖音那種一個(gè)頁(yè)面一個(gè)視頻始腾,還有小窗口播放視頻。也就是適合大多數(shù)業(yè)務(wù)場(chǎng)景
  • 視頻分層
    • 播放器內(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盡量保證功能單一性甫菠,最后通過(guò)addView形式添加進(jìn)來(lái)
      • 針對(duì)Player,需要定義一個(gè)接口冕屯,主要負(fù)責(zé)視頻播放處理邏輯寂诱,比如視頻播放,暫停安聘,設(shè)置播放進(jìn)度痰洒,設(shè)置視頻鏈接,切換播放模式等操作浴韭。需要注意把Controller設(shè)置到Player里面丘喻,兩者之間通過(guò)接口交互
    • 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)來(lái)后然后通過(guò)addView將視圖添加到該控制器中蜜自,這樣非常方便添加自定義視圖
      • 播放器切換狀態(tài)需要改變Controller視圖菩貌,比如視頻異常則需要顯示異常視圖view,則它們之間的交互是通過(guò)ControlWrapper(同時(shí)實(shí)現(xiàn)Controller接口和Player接口)實(shí)現(xiàn)

04.視頻播放器如何使用

4.1 關(guān)于gradle引用說(shuō)明

  • 如下所示
    //視頻UI層袁辈,必須要有
    implementation 'cn.yc:VideoPlayer:3.0.1'
    //視頻緩存菜谣,如果不需要?jiǎng)t可以不依賴
    implementation 'cn.yc:VideoCache:3.0.0'
    //視頻內(nèi)核層,必須有
    implementation 'cn.yc:VideoKernel:3.0.1'
    

4.2 在xml中添加布局

  • 注意晚缩,在實(shí)際開發(fā)中尾膊,由于Android手機(jī)碎片化比較嚴(yán)重,分辨率太多了荞彼,建議靈活設(shè)置布局的寬高比為4:3或者16:9或者你認(rèn)為合適的冈敛,可以用代碼設(shè)置。
  • 如果寬高比變形鸣皂,則會(huì)有黑邊
    <org.yczbj.ycvideoplayerlib.player.VideoPlayer
        android:id="@+id/video_player"
        android:layout_width="match_parent"
        android:layout_height="240dp"/>
    

4.3 最簡(jiǎn)單的視頻播放器參數(shù)設(shè)定

  • 如下所示
    //創(chuàng)建基礎(chǔ)視頻播放器抓谴,一般播放器的功能
    BasisVideoController controller = new BasisVideoController(this);
    //設(shè)置控制器
    mVideoPlayer.setVideoController(controller);
    //設(shè)置視頻播放鏈接地址
    mVideoPlayer.setUrl(url);
    //開始播放
    mVideoPlayer.start();
    

4.4 注意問(wèn)題

  • 如果是全屏播放浆洗,則需要在清單文件中設(shè)置當(dāng)前activity的屬性值
    • android:configChanges 保證了在全屏的時(shí)候橫豎屏切換不會(huì)執(zhí)行Activity的相關(guān)生命周期吸占,打斷視頻的播放
    • android:screenOrientation 固定了屏幕的初始方向
    • 這兩個(gè)變量控制全屏后和退出全屏的屏幕方向
          <activity android:name=".VideoActivity"
              android:configChanges="orientation|keyboardHidden|screenSize"
              android:screenOrientation="portrait"/>
      
  • 如何一進(jìn)入頁(yè)面就開始播放視頻,稍微延時(shí)一下即可
    • 代碼如下所示撰洗,注意避免直接start()荆陆,因?yàn)橛锌赡芤曨l還沒(méi)有初始化完成……
      mVideoPlayer.postDelayed(new Runnable() {
          @Override
          public void run() {
              mVideoPlayer.start();
          }
      },300);
      

05.播放器詳細(xì)Api文檔

  • 01.最簡(jiǎn)單的播放
  • 02.如何切換視頻內(nèi)核
  • 03.切換視頻模式
  • 04.切換視頻清晰度
  • 05.視頻播放監(jiān)聽
  • 06.列表中播放處理
  • 07.懸浮窗口播放
  • 08.其他重要功能Api
  • 09.播放多個(gè)視頻
  • 10.VideoPlayer相關(guān)Api
  • 11.Controller相關(guān)Api
  • 12.仿快手播放視頻
  • 具體看這篇文檔:視頻播放器Api說(shuō)明

06.播放器封裝思路

6.1視頻層級(jí)示例圖

image

6.2 視頻播放器流程圖

  • 待完善

6.3 視頻播放器lib庫(kù)

image

6.4 視頻內(nèi)核lib庫(kù)介紹

image

image

6.5視頻播放器UI庫(kù)介紹

image

07.播放器示例展示圖

image

image

image

image

image

image

image

image

image

image

08.添加自定義視圖

  • 比如滩届,現(xiàn)在有個(gè)業(yè)務(wù)需求,需要在視頻播放器剛開始添加一個(gè)廣告視圖被啼,等待廣告倒計(jì)時(shí)120秒后帜消,直接進(jìn)入播放視頻邏輯。相信這個(gè)業(yè)務(wù)場(chǎng)景很常見(jiàn)浓体,大家都碰到過(guò)泡挺,使用該播放器就特別簡(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ǔ)上鳄虱,通過(guò)控制器對(duì)象add進(jìn)來(lái)即可,代碼如下所示
    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.視頻播放器優(yōu)化處理

9.1 如何兼容不同內(nèi)核播放器

  • 提問(wèn):針對(duì)不同內(nèi)核播放器凭峡,比如谷歌的ExoPlayer拙已,B站的IjkPlayer,還有原生的MediaPlayer摧冀,有些api不一樣倍踪,那使用的時(shí)候如何統(tǒng)一api呢?
    • 比如說(shuō)索昂,ijk和exo的視頻播放listener監(jiān)聽api就完全不同建车,這個(gè)時(shí)候需要做兼容處理
    • 定義接口,然后各個(gè)不同內(nèi)核播放器實(shí)現(xiàn)接口椒惨,重寫抽象方法缤至。調(diào)用的時(shí)候,獲取接口對(duì)象調(diào)用api框产,這樣就可以統(tǒng)一Api
  • 定義一個(gè)接口凄杯,這個(gè)接口有什么呢?這個(gè)接口定義通用視頻播放器方法秉宿,比如常見(jiàn)的有:視頻初始化戒突,設(shè)置url,加載描睦,以及播放狀態(tài)膊存,簡(jiǎn)單來(lái)說(shuō)可以分為三個(gè)部分。
    • 第一部分:視頻初始化實(shí)例對(duì)象方法忱叭,主要包括:initPlayer初始化視頻隔崎,setDataSource設(shè)置視頻播放器地址,setSurface設(shè)置視頻播放器渲染view韵丑,prepareAsync開始準(zhǔn)備播放操作
    • 第二部分:視頻播放器狀態(tài)方法爵卒,主要包括:播放,暫停撵彻,恢復(fù)钓株,重制实牡,設(shè)置進(jìn)度,釋放資源轴合,獲取進(jìn)度创坞,設(shè)置速度,設(shè)置音量
    • 第三部分:player綁定view后受葛,需要監(jiān)聽播放狀態(tài)题涨,比如播放異常,播放完成总滩,播放準(zhǔn)備纲堵,播放size變化,還有播放準(zhǔn)備
  • 首先定義一個(gè)工廠抽象類闰渔,然后不同的內(nèi)核播放器分別創(chuàng)建其具體的工廠實(shí)現(xiàn)具體類
    • PlayerFactory:抽象工廠婉支,擔(dān)任這個(gè)角色的是工廠方法模式的核心,任何在模式中創(chuàng)建對(duì)象的工廠類必須實(shí)現(xiàn)這個(gè)接口
    • ExoPlayerFactory:具體工廠澜建,具體工廠角色含有與業(yè)務(wù)密切相關(guān)的邏輯向挖,并且受到使用者的調(diào)用以創(chuàng)建具體產(chǎn)品對(duì)象。
  • 如何使用炕舵,分為三步何之,具體操作如下所示
    • 1.先調(diào)用具體工廠對(duì)象中的方法createPlayer方法;2.根據(jù)傳入產(chǎn)品類型參數(shù)獲得具體的產(chǎn)品對(duì)象咽筋;3.返回產(chǎn)品對(duì)象并使用溶推。
    • 簡(jiǎn)而言之,創(chuàng)建對(duì)象的時(shí)候只需要傳遞類型type奸攻,而不需要對(duì)應(yīng)的工廠蒜危,即可創(chuàng)建具體的產(chǎn)品對(duì)象
  • 這種創(chuàng)建對(duì)象最大優(yōu)點(diǎn)
    • 工廠方法用來(lái)創(chuàng)建所需要的產(chǎn)品,同時(shí)隱藏了哪種具體產(chǎn)品類將被實(shí)例化這一細(xì)節(jié)睹耐,用戶只需要關(guān)心所需產(chǎn)品對(duì)應(yīng)的工廠辐赞,無(wú)須關(guān)心創(chuàng)建細(xì)節(jié),甚至無(wú)須知道具體產(chǎn)品類的類名硝训。
    • 加入新的產(chǎn)品時(shí)响委,比如后期新加一個(gè)阿里播放器內(nèi)核,這個(gè)時(shí)候就只需要添加一個(gè)具體工廠和具體產(chǎn)品就可以窖梁。系統(tǒng)的可擴(kuò)展性也就變得非常好赘风,完全符合“開閉原則”

9.2 播放器UI抽取封裝優(yōu)化

  • 發(fā)展中遇到的問(wèn)題
    • 播放器可支持多種場(chǎng)景下的播放,多個(gè)產(chǎn)品會(huì)用到同一個(gè)播放器纵刘,這樣就會(huì)帶來(lái)一個(gè)問(wèn)題邀窃,一個(gè)播放業(yè)務(wù)播放器狀態(tài)發(fā)生變化,其他播放業(yè)務(wù)必須同步更新播放狀態(tài)假哎,各個(gè)播放業(yè)務(wù)之間互相交叉瞬捕,隨著播放業(yè)務(wù)的增多敲茄,開發(fā)和維護(hù)成本會(huì)急劇增加, 導(dǎo)致后續(xù)開發(fā)不可持續(xù)。
  • UI難以自定義或者修改麻煩
    • 比如常見(jiàn)的視頻播放器山析,會(huì)把視頻各種視圖寫到xml中,這種方式在后期代碼會(huì)很大掏父,而且改動(dòng)一個(gè)小的布局笋轨,則會(huì)影響大。這樣到后期往往只敢加代碼赊淑,而不敢刪除代碼……
    • 有時(shí)候難以適應(yīng)新的場(chǎng)景爵政,比如添加一個(gè)播放廣告,老師開課陶缺,或者視頻引導(dǎo)業(yè)務(wù)需求钾挟,則需要到播放器中寫一堆業(yè)務(wù)代碼。迭代到后期饱岸,違背了開閉原則掺出,視頻播放器需要做到和業(yè)務(wù)分離
  • 視頻播放器結(jié)構(gòu)需要清晰
    • 也就是說(shuō)視頻player和ui操作柔和到了一起,尤其是兩者之間的交互苫费。比如播放中需要更新UI進(jìn)度條汤锨,播放異常需要顯示異常UI,都比較難處理播放器狀態(tài)變化更新UI操作
    • 這個(gè)是指該視頻播放器能否看了文檔后快速上手百框,知道封裝的大概流程闲礼。方便后期他人修改和維護(hù),因此需要將視頻播放器功能分離铐维。比如切換內(nèi)核+視頻播放器(player+controller+view)
    • 一定要解耦合柬泽,播放器player與視頻UI解耦:支持添加自定義視頻視圖,比如支持添加自定義廣告嫁蛇,新手引導(dǎo)锨并,或者視頻播放異常等視圖,這個(gè)需要較強(qiáng)的拓展性
  • 適合多種業(yè)務(wù)場(chǎng)景
    • 比如適合播放單個(gè)視頻睬棚,多個(gè)視頻琳疏,以及列表視頻,或者類似抖音那種一個(gè)頁(yè)面一個(gè)視頻闸拿,還有小窗口播放視頻空盼。也就是適合大多數(shù)業(yè)務(wù)場(chǎng)景
  • 方便播放業(yè)務(wù)發(fā)生變化
    • 播放狀態(tài)變化是導(dǎo)致不同播放業(yè)務(wù)場(chǎng)景之間交叉同步,解除播放業(yè)務(wù)對(duì)播放器的直接操控新荤,采用接口監(jiān)聽進(jìn)行解耦揽趾。比如:player+controller+interface
  • 關(guān)于視頻播放器
    • 定義一個(gè)視頻播放器InterVideoPlayer接口,操作視頻播放苛骨,暫停篱瞎,緩沖苟呐,進(jìn)度設(shè)置,設(shè)置播放模式等多種操作俐筋。
    • 然后寫一個(gè)播放器接口的具體實(shí)現(xiàn)類牵素,在這個(gè)里面拿到內(nèi)核播放器player,然后做相關(guān)的實(shí)現(xiàn)操作澄者。
  • 關(guān)于視頻視圖View
    • 定義一個(gè)視圖InterVideoController接口笆呆,主要負(fù)責(zé)視圖顯示/隱藏,播放進(jìn)度粱挡,鎖屏赠幕,狀態(tài)欄等操作。
    • 然后寫一個(gè)播放器視圖接口的具體實(shí)現(xiàn)類询筏,在這里里面inflate視圖操作榕堰,然后接口方法實(shí)現(xiàn),為了方便后期開發(fā)者自定義view嫌套,因此需要addView操作逆屡,將添加進(jìn)來(lái)的視圖用map集合裝起來(lái)。
  • 播放器player和controller交互
    • 在player中創(chuàng)建BaseVideoController對(duì)象踱讨,這個(gè)時(shí)候需要把controller添加到播放器中康二,這個(gè)時(shí)候有兩個(gè)要點(diǎn)特別重要,需要把播放器狀態(tài)監(jiān)聽勇蝙,和播放模式監(jiān)聽傳遞給控制器
    • setPlayState設(shè)置視頻播放器播放邏輯狀態(tài)沫勿,主要是播放緩沖,加載味混,播放中产雹,暫停,錯(cuò)誤翁锡,完成蔓挖,異常,播放進(jìn)度等多個(gè)狀態(tài)馆衔,方便控制器做UI更新操作
    • setPlayerState設(shè)置視頻播放切換模式狀態(tài)瘟判,主要是普通模式,小窗口模式角溃,正常模式三種其中一種拷获,方便控制器做UI更新
  • 播放器player和view交互
    • 這塊非常關(guān)鍵,舉個(gè)例子减细,視頻播放失敗需要顯示控制層的異常視圖View匆瓜;播放視頻初始化需要顯示loading,然后更新UI播放進(jìn)度條等。都是播放器和視圖層交互
    • 可以定義一個(gè)類驮吱,同時(shí)實(shí)現(xiàn)InterVideoPlayer接口和InterVideoController接口茧妒,這個(gè)時(shí)候會(huì)重新這兩個(gè)接口所有的方法。此類的目的是為了在InterControlView接口實(shí)現(xiàn)類中既能調(diào)用VideoPlayer的api又能調(diào)用BaseVideoController的api
  • 如何添加自定義播放器視圖
    • 添加了自定義播放器視圖左冬,比如添加視頻廣告桐筏,可以選擇跳過(guò),選擇播放暫停拇砰。那這個(gè)視圖view梅忌,肯定是需要操作player或者獲取player的狀態(tài)的。這個(gè)時(shí)候就需要暴露監(jiān)聽視頻播放的狀態(tài)接口監(jiān)聽
    • 首先定義一個(gè)InterControlView接口毕匀,也就是說(shuō)所有自定義視頻視圖view需要實(shí)現(xiàn)這個(gè)接口,該接口中的核心方法有:綁定視圖到播放器癌别,視圖顯示隱藏變化監(jiān)聽皂岔,播放狀態(tài)監(jiān)聽展姐,播放模式監(jiān)聽教馆,進(jìn)度監(jiān)聽板鬓,鎖屏監(jiān)聽等
    • 在BaseVideoController中的狀態(tài)監(jiān)聽中,通過(guò)InterControlView接口對(duì)象就可以把播放器的狀態(tài)傳遞到子類中

9.4 代碼方面優(yōu)化措施

  • 如果是在Activity中的話,建議設(shè)置下面這段代碼
    @Override
    protected void onResume() {
        super.onResume();
        if (mVideoPlayer != null) {
            //從后臺(tái)切換到前臺(tái)绵患,當(dāng)視頻暫停時(shí)或者緩沖暫停時(shí)掘殴,調(diào)用該方法重新開啟視頻播放
            mVideoPlayer.resume();
        }
    }
    
    
    @Override
    protected void onPause() {
        super.onPause();
        if (mVideoPlayer != null) {
            //從前臺(tái)切到后臺(tái),當(dāng)視頻正在播放或者正在緩沖時(shí)套菜,調(diào)用該方法暫停視頻
            mVideoPlayer.pause();
        }
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mVideoPlayer != null) {
            //銷毀頁(yè)面戏溺,釋放托享,內(nèi)部的播放器被釋放掉,同時(shí)如果在全屏闰围、小窗口模式下都會(huì)退出
            mVideoPlayer.release();
        }
    }
    
    @Override
    public void onBackPressed() {
        //處理返回鍵邏輯止潘;如果是全屏,則退出全屏辫诅;如果是小窗口凭戴,則退出小窗口
        if (mVideoPlayer == null || !mVideoPlayer.onBackPressed()) {
            super.onBackPressed();
        }
    }
    

10.播放器問(wèn)題記錄說(shuō)明

11.性能優(yōu)化和庫(kù)大小

12.視頻緩存原理介紹

  • 網(wǎng)絡(luò)上比較好的項(xiàng)目:https://github.com/danikula/AndroidVideoCache
    • 網(wǎng)絡(luò)用的HttpURLConnection,文件緩存處理炕矮,文件最大限度策略么夫,回調(diào)監(jiān)聽處理,斷點(diǎn)續(xù)傳,代理服務(wù)等肤视。
  • 但是存在一些問(wèn)題档痪,比如如下所示
    • 文件的緩存超過(guò)限制后沒(méi)有按照l(shuí)ru算法刪除,
    • 處理返回給播放器的http響應(yīng)頭消息邢滑,響應(yīng)頭消息的獲取處理改為head請(qǐng)求(需服務(wù)器支持)
    • 替換網(wǎng)絡(luò)庫(kù)為okHttp(因?yàn)榇蟛糠值捻?xiàng)目都是以okHttp為網(wǎng)絡(luò)請(qǐng)求庫(kù)的)腐螟,但是這個(gè)改動(dòng)性比較大
  • 然后看一下怎么使用,超級(jí)簡(jiǎn)單。傳入視頻url鏈接乐纸,返回一個(gè)代理鏈接衬廷,然后就可以呢
    HttpProxyCacheServer cacheServer = ProxyVideoCacheManager.getProxy(this);
    String proxyUrl = cacheServer.getProxyUrl(URL_AD);
    mVideoPlayer.setUrl(proxyUrl);
    
    
    public static HttpProxyCacheServer getProxy(Context context) {
        return sharedProxy == null ? (sharedProxy = newProxy(context)) : sharedProxy;
    }
    
    private static HttpProxyCacheServer newProxy(Context context) {
        return new HttpProxyCacheServer.Builder(context)
                .maxCacheSize(512 * 1024 * 1024)       // 512MB for cache
                //緩存路徑,不設(shè)置默認(rèn)在sd_card/Android/data/[app_package_name]/cache中
                //.cacheDirectory()
                .build();
    }
    
  • 大概的原理
    • 原始的方式是直接塞播放地址給播放器汽绢,它就可以直接播放÷鸢希現(xiàn)在我們要在中間加一層本地代理,播放器播放的時(shí)候(獲取數(shù)據(jù))是通過(guò)我們的本地代理的地址來(lái)播放的宁昭,這樣我們就可以很好的在中間層(本地代理層)做一些處理跌宛,比如:文件緩存,預(yù)緩存(秒開處理)积仗,監(jiān)控等疆拘。
  • 原理詳細(xì)一點(diǎn)來(lái)說(shuō)
    • 1.采用了本地代理服務(wù)的方式,通過(guò)原始url給播放器返回一個(gè)本地代理的一個(gè)url 寂曹,代理URL類似:http://127.0.0.1:port/視頻url哎迄;(port端口為系統(tǒng)隨機(jī)分配的有效端口,真實(shí)url是為了真正的下載)稀颁,然后播放器播放的時(shí)候請(qǐng)求到了你本地的代理上了芬失。
    • 2.本地代理采用ServerSocket監(jiān)聽127.0.0.1的有效端口楣黍,這個(gè)時(shí)候手機(jī)就是一個(gè)服務(wù)器了匾灶,客戶端就是socket,也就是播放器租漂。
    • 3.讀取客戶端就是socket來(lái)讀取數(shù)據(jù)(http協(xié)議請(qǐng)求)解析http協(xié)議阶女。
    • 4.根據(jù)url檢查視頻文件是否存在,讀取文件數(shù)據(jù)給播放器哩治,也就是往socket里寫入數(shù)據(jù)(socket通信)秃踩。同時(shí)如果沒(méi)有下載完成會(huì)進(jìn)行斷點(diǎn)下載,當(dāng)然弱網(wǎng)的話數(shù)據(jù)需要生產(chǎn)消費(fèi)同步處理。
  • 如何實(shí)現(xiàn)預(yù)加載
    • 其實(shí)預(yù)加載的思路很簡(jiǎn)單业筏,在進(jìn)行一個(gè)播放視頻后憔杨,再返回接下來(lái)需要預(yù)加載的視頻url,啟用線程去請(qǐng)求下載數(shù)據(jù)
    • 開啟一個(gè)線程去請(qǐng)求并預(yù)加載一部分的數(shù)據(jù)蒜胖,可能需要預(yù)加載的數(shù)據(jù)大于>1消别,利用隊(duì)列先進(jìn)入的先進(jìn)行加載,因此可以采用LinkedHashMap保存正在預(yù)加載的task台谢。
    • 在開始預(yù)加載的時(shí)候寻狂,判斷該播放地址是否已經(jīng)預(yù)加載,如果不是那么創(chuàng)建一個(gè)線程task朋沮,并且把它放到map集合中蛇券。然后執(zhí)行預(yù)加載邏輯,也就是執(zhí)行HttpURLConnection請(qǐng)求
    • 提供取消對(duì)應(yīng)url加載的任務(wù),因?yàn)橛锌赡茉搖rl不需要再進(jìn)行預(yù)加載了纠亚,比如參考抖音塘慕,當(dāng)用戶瞬間下滑幾個(gè)視頻,那么很多視頻就需要跳過(guò)了不需要再進(jìn)行預(yù)加載
  • 具體直接看項(xiàng)目代碼:VideoCache緩沖模塊

13.查看視頻播放器日志

  • 統(tǒng)一管理視頻播放器封裝庫(kù)日志菜枷,方便后期排查問(wèn)題
    • 比如苍糠,視頻內(nèi)核,日志過(guò)濾則是:aaa
    • 比如啤誊,視頻player岳瞭,日志過(guò)濾則是:bbb
    • 比如,緩存模塊蚊锹,日志過(guò)濾則是:VideoCache

14.該庫(kù)異常code說(shuō)明

  • 針對(duì)視頻封裝庫(kù)瞳筏,統(tǒng)一處理拋出的異常,為了方便開發(fā)者快速知道異常的來(lái)由牡昆,則可以查詢約定的code碼姚炕。
    • 這個(gè)在sdk中特別常見(jiàn),因此該庫(kù)一定程度是借鑒騰訊播放器……

視頻框架:https://github.com/yangchong211/YCVideoPlayer

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末丢烘,一起剝皮案震驚了整個(gè)濱河市柱宦,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌播瞳,老刑警劉巖掸刊,帶你破解...
    沈念sama閱讀 218,204評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異赢乓,居然都是意外死亡忧侧,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門牌芋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)蚓炬,“玉大人,你說(shuō)我怎么就攤上這事躺屁】舷模” “怎么了?”我有些...
    開封第一講書人閱讀 164,548評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵犀暑,是天一觀的道長(zhǎng)驯击。 經(jīng)常有香客問(wèn)我,道長(zhǎng)母怜,這世上最難降的妖魔是什么余耽? 我笑而不...
    開封第一講書人閱讀 58,657評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮苹熏,結(jié)果婚禮上碟贾,老公的妹妹穿的比我還像新娘币喧。我一直安慰自己,他們只是感情好袱耽,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,689評(píng)論 6 392
  • 文/花漫 我一把揭開白布杀餐。 她就那樣靜靜地躺著,像睡著了一般朱巨。 火紅的嫁衣襯著肌膚如雪史翘。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,554評(píng)論 1 305
  • 那天冀续,我揣著相機(jī)與錄音琼讽,去河邊找鬼。 笑死洪唐,一個(gè)胖子當(dāng)著我的面吹牛钻蹬,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播凭需,決...
    沈念sama閱讀 40,302評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼问欠,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了粒蜈?” 一聲冷哼從身側(cè)響起顺献,我...
    開封第一講書人閱讀 39,216評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎枯怖,沒(méi)想到半個(gè)月后注整,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,661評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡嫁怀,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,851評(píng)論 3 336
  • 正文 我和宋清朗相戀三年设捐,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了借浊。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片塘淑。...
    茶點(diǎn)故事閱讀 39,977評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖蚂斤,靈堂內(nèi)的尸體忽然破棺而出存捺,到底是詐尸還是另有隱情,我是刑警寧澤曙蒸,帶...
    沈念sama閱讀 35,697評(píng)論 5 347
  • 正文 年R本政府宣布捌治,位于F島的核電站,受9級(jí)特大地震影響纽窟,放射性物質(zhì)發(fā)生泄漏肖油。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,306評(píng)論 3 330
  • 文/蒙蒙 一臂港、第九天 我趴在偏房一處隱蔽的房頂上張望森枪。 院中可真熱鬧视搏,春花似錦、人聲如沸县袱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)式散。三九已至筋遭,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間暴拄,已是汗流浹背漓滔。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留乖篷,地道東北人次和。 一個(gè)月前我還...
    沈念sama閱讀 48,138評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像那伐,于是被迫代替她去往敵國(guó)和親踏施。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,927評(píng)論 2 355