@[TOC](IOS音視頻(四十五)HTTPS 自簽名證書 實(shí)現(xiàn)邊下邊播)
1. 邊下邊播概述
由于JimuPro相冊(cè)里面獲取視頻号枕,需要將視頻全部下載到本地后才能播放,如果視頻文件很大眨业,則用戶需要等待很長時(shí)間才能看到視頻,這種體驗(yàn)效果不太友好沮协,針對(duì)這個(gè)問題龄捡,需要IOS app端實(shí)現(xiàn)邊下邊播功能,使用一份數(shù)據(jù)流慷暂,完成觀看視頻的同事將視頻保存到本地聘殖,等視頻播放完成后,視頻也就下載到了本地。下載完成后的視頻格式是.mp4格式奸腺,導(dǎo)出來可以直接播放餐禁。當(dāng)用戶第二次觀看次視頻時(shí),將不從機(jī)器人端獲取視頻突照,直接讀取本地緩存的視頻帮非,也就是離線也可以觀看。
實(shí)現(xiàn)邊下邊播的方式讹蘑,可以節(jié)省數(shù)據(jù)流量末盔,實(shí)時(shí)觀看到機(jī)器人端錄制的視頻,可以拖拽的方式觀看座慰。
這個(gè)功能滿足以下需求:
- 支持正常播放器的一切功能庄岖,包括暫停、播放和拖拽角骤∮绶蓿可以播放本地緩存的視頻,也可以實(shí)時(shí)播放機(jī)器人端錄制的視頻邦尊。
- 如果視頻加載完成且完整背桐,將視頻文件保存到本地cache,下一次播放本地cache中的視頻蝉揍,不再請(qǐng)求網(wǎng)絡(luò)數(shù)據(jù)链峭。
- 如果視頻沒有加載完(半路關(guān)閉或者拖拽),下次播放時(shí)又沾,先從緩存中播放已經(jīng)緩存的視頻弊仪,并同時(shí)開啟下載功能,從上次的視頻末尾繼續(xù)下載剩下的部分杖刷。
- 由于機(jī)器人端采用HTTPS + 自簽名證書的方式励饵,實(shí)時(shí)播放視頻需要解決證書信任問題。
2. 邊下邊播實(shí)現(xiàn)方案
IOS客戶端實(shí)現(xiàn)邊下邊播的方案有很多滑燃,目前我研究的找到3種解決方案役听。下面將詳細(xì)介紹3種方案的實(shí)現(xiàn)原理。由于JimuPro里面已經(jīng)用到了開源的播放器:VGPlayer表窘。這個(gè)播放器里面基本上實(shí)現(xiàn)了方案三的細(xì)節(jié)問題。只是沒有實(shí)現(xiàn)HTTPS 自簽名證書認(rèn)證的問題乐严。
IOS項(xiàng)目中我推薦使用第三種方案實(shí)現(xiàn)邊下邊播功能瘤袖。
2.1 方案一
- 通過解析mp4的格式,將mp4的數(shù)據(jù)直接下載并寫入文件昂验,然后讓播放器直接播放的是本地的視頻文件捂敌;
此方案是先下載視頻到本地文件昭娩,然后把本地視頻文件地址傳給播放器,播放器實(shí)際播放的是本地文件黍匾。當(dāng)播放器的播放進(jìn)度大于當(dāng)前的可播放的下載緩存進(jìn)度栏渺,則暫停播放,等緩存到足夠播放時(shí)間之后锐涯,再讓播放器開始播放磕诊。這種方案的下載方式是與播放器完全沒有關(guān)系的,只是順序的將服務(wù)器下發(fā)的視頻數(shù)據(jù)寫入本地文件纹腌,然后讓播放器來讀取數(shù)據(jù)霎终。
以mp4文件為例,通過解析mp4的格式升薯,將mp4的數(shù)據(jù)直接下載并寫入文件莱褒,然后讓播放器直接播放的是本地的視頻文件;如下圖:
這種方式雖然能夠滿足緩存播放這個(gè)需求涎劈,但是會(huì)產(chǎn)生很多問題广凸,例如視頻下載到本地,下載多少才可以把本地文件作為視頻源傳給播放器即視頻開啟播放速度蛛枚;播放的速度大于下載速度的話谅海,該怎么辦?如果播放器seek到文件沒有緩存的位置蹦浦,應(yīng)該怎么處理扭吁?對(duì)于視頻關(guān)閉之后,第二次進(jìn)入如何知道已經(jīng)下載了多少盲镶?等等問題侥袜。
目前的已有解決方案是,當(dāng)緩存到500kb才把緩存的地址傳給播放器溉贿,視頻文件小于500kb則下載完之后再播放枫吧,起播慢(需要改進(jìn))。當(dāng)下載進(jìn)度比播放進(jìn)度多5秒的數(shù)據(jù)量才讓播放器播放顽照,不然的話就暫停由蘑。如果seek到?jīng)]有緩存的地方就切換到網(wǎng)絡(luò)上停止當(dāng)前的下載,浪費(fèi)一些流量代兵。每次下載都會(huì)保存一份配置文件,來保存是否下載完成爷狈,沒下載完成則第二次根據(jù)當(dāng)前緩存文件大小植影,重新開始順序下載。
總的來說第一種方案有如下缺點(diǎn):
- 用戶播放視頻的時(shí)候可能等待的時(shí)間較長(起播
- 流量浪費(fèi)(seek之后會(huì)播網(wǎng)絡(luò)流涎永,停止下載)
- 需要太多控制視頻播放的邏輯來進(jìn)行輔助思币,與播放器代碼耦合嚴(yán)重鹿响。
- seek之后切源會(huì)耗時(shí),每次seek比較慢
2.2 方案二
- 使用的本地代理服務(wù)器的方式:
在服務(wù)器端(機(jī)器人端)支持分片下載的方式下谷饿,APP內(nèi)置一個(gè)HTTPServer代理服務(wù)器惶我,代理服務(wù)器實(shí)現(xiàn)將數(shù)據(jù)緩存到本地,同時(shí)App的播放器之間重代理服務(wù)器獲取播放數(shù)據(jù)博投。這種實(shí)現(xiàn)方式比較復(fù)雜一點(diǎn)绸贡,如果處理不好,容易導(dǎo)致crash的問題毅哗。
這個(gè)代理服務(wù)器也可以做在機(jī)器人端听怕,一個(gè)接口用于播放,一個(gè)接口用于下載虑绵。
使用 HTTPServer尿瞭,在本地開啟一個(gè) http 服務(wù)器,把需要緩存的請(qǐng)求地址指向本地服務(wù)器翅睛,并帶上真正的 url 地址声搁。HTTPServer 不管我們有沒有使用緩存功能,都要在應(yīng)用打開的時(shí)候默默開啟捕发,對(duì)APP性能是一大損耗酥艳。并且我們引入 HTTPServer 庫也會(huì)增加一些包體積。
2.2.1 技術(shù)要點(diǎn)
此方案的特點(diǎn)如下:
- 通過代理服務(wù)器爬骤,從socket截取播放器請(qǐng)求數(shù)據(jù)充石;
- 根據(jù)截取的range信息,從網(wǎng)絡(luò)服務(wù)器請(qǐng)求視頻數(shù)據(jù)霞玄;
- 視頻數(shù)據(jù)寫入本地文件骤铃,seek后可以從seek位置繼續(xù)寫入并播放;
- 邊下邊播坷剧,加快播放速度惰爬;
- 與播放器邏輯完全解耦,對(duì)于播放器只是一個(gè)地址
本方案是在播放器與視頻源服務(wù)器之間加一層代理服務(wù)器惫企,截取視頻播放器發(fā)送的請(qǐng)求撕瞧,根據(jù)截取的請(qǐng)求,向網(wǎng)絡(luò)服務(wù)器請(qǐng)求數(shù)據(jù)狞尔,然后寫到本地丛版。本地代理服務(wù)器從文件中讀取數(shù)據(jù)并發(fā)送給播放器進(jìn)行播放. 如下圖所示:
如上圖,具體流程細(xì)節(jié)如下:
- 啟動(dòng)本地代理服務(wù)器偏序。
- 視頻源地址傳給本地代理服務(wù)器页畦。
- 將視頻源地址轉(zhuǎn)換成本地代理服務(wù)器的地址作為播放器的視頻源地址。
- 播放器向本地代理服務(wù)器發(fā)送請(qǐng)求研儒。
- 本地代理服務(wù)器截取這個(gè)請(qǐng)求豫缨,再根據(jù)解析出來請(qǐng)求的信息向真正的服務(wù)器發(fā)起請(qǐng)求独令。
- 本地代理服務(wù)器開始接受數(shù)據(jù),寫入文件并將文件數(shù)據(jù)再返回到播放器好芭。
- 播放器接收到這些數(shù)據(jù)之后播放燃箭。
- seek之后重新進(jìn)行以上步驟。
上面流程主要描述了代理服務(wù)器實(shí)現(xiàn)的實(shí)時(shí)播放流程舍败,下面重點(diǎn)探討一下代理服務(wù)器的下載流程招狸。
- 下載流程實(shí)現(xiàn)
考慮到播放視頻的時(shí)候,用戶會(huì)拖動(dòng)進(jìn)度條進(jìn)行seek瓤湘,而此時(shí)需要從用戶拖動(dòng)的位置進(jìn)行下載瓢颅,這樣會(huì)讓視頻文件產(chǎn)生許多的空洞,如下圖所示:
為了節(jié)省流量弛说,只會(huì)下載文件中沒有數(shù)據(jù)的部分挽懦,也就是上圖1藍(lán)色的部分。因此需要存儲(chǔ)下載的片段信息木人。目前采用的數(shù)據(jù)結(jié)構(gòu)如下所示:
fragment = [start信柿,end];
array = [fragment 0醒第,fragment 1,fragment 2渔嚷,fragment 3];
- 其中
fragment
指的是下載的片段,start
指的是片段開始的位置稠曼,end
為片段的結(jié)束位置形病。array
指的是存儲(chǔ)fragment
的數(shù)組,數(shù)組中的fragment
是依靠start
從小到大來來插入到數(shù)組中的霞幅,保證了數(shù)組的有序性漠吻。- 下載的片段是記錄在一個(gè)數(shù)組中:
array = [fragment0 ,fragment 1司恳,fragment 2途乃,fragment 3];
下載共分為兩個(gè)階段:seek階段和補(bǔ)洞階段。
- seek階段:即為在播放的時(shí)候扔傅,根據(jù)用戶seek的位置來進(jìn)行下載耍共。
根據(jù)seek到的位置分為兩種情況:
-
情況一:如果
seek
到的位置是在已有的片段中(例如圖中的seek1
的位置,該處有數(shù)據(jù))猎塞,就從該片段(fragment1
)的末尾請(qǐng)求數(shù)據(jù)(end1
)试读,直到下個(gè)片段的開始位置處(fragment2
的start
)邢享,也就是向服務(wù)器請(qǐng)求的range
為:rang1 = (end1 ) —— start2;
這個(gè)片段下載完成后鹏往,假如把下載的片段記為fragment1.1
,則會(huì)把fragment1
骇塘、fragment1.1
伊履、fragment2
合為一個(gè)片段為fragment1-2
,則array = [fragment 0款违,fragement1-2唐瀑,frament3]
;這次下載后的狀態(tài)圖2所示:
圖2--情況一
接下來一直下載直到array = [fragment 0插爹,fragement1-3];
之后會(huì)判斷fragement1-3
有沒有到文件末尾哄辣,如果到了就下載結(jié)束,如果沒到就從從fragement3
的(end3
)開始下載直到文件末尾赠尾。 -
情況二:如果
seek
到的位置沒有在已有的片段中力穗,(例如說是在圖1中的seek2
的位置),就從seek
到的位置開始下載數(shù)據(jù)直到下一個(gè)片段的start
(fragment2
的start2
)气嫁,假如這個(gè)片段記為fragment1.1
当窗,則會(huì)把fragment1.1
和fragment2
合并即數(shù)組為:array= [fragment 0,fragment1寸宵,fagment1.1-2崖面,fragment3];
合并后的情況如下圖3所示:接下來的操作就是繼續(xù)下載梯影,直到下載到文件末尾巫员;圖3--情況二
如果片段太小保存起來就會(huì)讓播放器下次播放的時(shí)候多發(fā)送一次請(qǐng)求,這樣是很耗費(fèi)資源甲棍。例如:如上圖3所示简识,如果fragment1
的大小只有1kb,想要補(bǔ)充fragment0
與fragment1.1-2
之間的數(shù)據(jù)感猛,就需要發(fā)送兩次請(qǐng)求七扰,這樣頻繁的發(fā)送請(qǐng)求,比較浪費(fèi)資源唱遭。因此當(dāng)fragment
太小戳寸,就不存在配置數(shù)組中。這樣會(huì)少發(fā)一次請(qǐng)求拷泽,也不會(huì)浪費(fèi)很大的流量疫鹊。當(dāng)下載片段太小(例如說下載的長度<20KB
),就不保存在片段數(shù)組中(為了控制片段的粒度)。這樣會(huì)產(chǎn)生一個(gè)問題太惠,當(dāng)視頻文件中間有一個(gè)空洞小于20KB如暖,這個(gè)片段永遠(yuǎn)補(bǔ)不上。這個(gè)時(shí)候就需要用到第二階段-補(bǔ)洞階段障涯。
-
補(bǔ)洞階段:
第二階段補(bǔ)洞階段,就是第二次播放的時(shí)候雷客,如果文件中有空洞捞奕,這個(gè)時(shí)候不論片段再小牺堰,也會(huì)存到片段中。
最后當(dāng)配置數(shù)組中存的數(shù)據(jù)只剩下最后的{0颅围,length}
伟葫,length
為視頻總長度的時(shí)候,表示文件已全部下載完成院促。
2.3 方案三
對(duì)于IOS平臺(tái)來說筏养,還有一種更好的方案:使用IOS原生API ,使用 AVAssetResourceLoader常拓,在不改變 AVPlayer API 的情況下渐溶,對(duì)播放的音視頻進(jìn)行緩存。
方案三跟方案二原理差不多弄抬,只不過是借助IOS原始API來實(shí)現(xiàn)的茎辐。
- 使用IOS系統(tǒng)自動(dòng)API 實(shí)現(xiàn)視頻邊下邊播功能:
這里的邊下邊播不是單獨(dú)開一個(gè)子線程去下載,而是把視頻播放的數(shù)據(jù)給保存到本地眉睹。簡而言之荔茬,就是使用一遍的流量,既播放了視頻竹海,也保存了視頻慕蔚。
具體實(shí)現(xiàn)方案如下:
- 需要在視頻播放器和服務(wù)器之間添加一層類似代理的機(jī)制,視頻播放器不再直接訪問服務(wù)器斋配,而是訪問代理對(duì)象孔飒,代理對(duì)象去訪問服務(wù)器獲得數(shù)據(jù),之后返回給視頻播放器艰争,同時(shí)代理對(duì)象根據(jù)一定的策略緩存數(shù)據(jù)坏瞄。
- AVURLAsset中的resourceLoader可以實(shí)現(xiàn)這個(gè)機(jī)制,resourceLoader的delegate就是上述的代理對(duì)象甩卓。
- 視頻播放器在開始播放之前首先檢測是本地cache中是否有此視頻鸠匀,如果沒有才通過代理獲得數(shù)據(jù),如果有逾柿,則直接播放本地cache中的視頻即可缀棍。
- 如果是用HTTP的方式,上述3步可以實(shí)現(xiàn)邊下邊播功能机错,如果是HTTPS,服務(wù)器證書使用的是證書頒發(fā)機(jī)構(gòu)簽名的證書爬范,則也可以直接跟HTTP方式一樣處理。但是弱匪,如果是HTTPS+自簽名證書的方式青瀑,則需要在resourceLoader每次方式請(qǐng)求前,先校驗(yàn)證書,也就是下面的第5步
2.3.1 AVPlayer實(shí)現(xiàn)邊下邊播流程
我們先來參考網(wǎng)上播放QQ音樂邊下邊播流程圖如下:
QQ 音樂實(shí)現(xiàn)的緩存策略大致如下:
先觀察并猜測企鵝音樂的緩存策略(當(dāng)然它不是用AVPlayer播放):
1斥难、開始播放枝嘶,同時(shí)開始下載完整的文件,當(dāng)文件下載完成時(shí)蘸炸,保存到緩存文件夾中躬络;
2尖奔、當(dāng)seek時(shí)
〈钊濉(1)如果seek到已下載到的部分,直接seek成功提茁;(如下載進(jìn)度60%淹禾,seek進(jìn)度50%)
(2)如果seek到未下載到的部分茴扁,則開始新的下載(如下載進(jìn)度60%铃岔,seek進(jìn)度70%)
PS1:此時(shí)文件下載的范圍是70%-100%
PS2:之前已下載的部分就被刪除了
PS3:如果有別的seek操作則重復(fù)步驟2,如果此時(shí)再seek到進(jìn)度40%峭火,則會(huì)開始新的下載(范圍40%-100%)
3毁习、當(dāng)開始新的下載之后,由于文件不完整卖丸,下載完成之后不會(huì)保存到緩存文件夾中纺且;
4、下次再播放同一歌曲時(shí)稍浆,如果在緩存文件夾中存在载碌,則直接播放緩存文件;
我們使用AVPlayer 來實(shí)現(xiàn)邊下邊播的大致流程跟上面QQ音樂的緩存機(jī)制差不多衅枫,就是依賴于AVAssetResourceLoader. 大致流程如下:
如上圖所示嫁艇,我們簡單描述一下AVPlayer實(shí)現(xiàn)邊下邊播的流程:
- 當(dāng)開始播放視頻時(shí),通過視頻url判斷本地cache中是否已經(jīng)緩存當(dāng)前視頻弦撩,如果有步咪,則直接播放本地cache中視頻
- 如果本地cache中沒有視頻,則視頻播放器向代理請(qǐng)求數(shù)據(jù)
- 加載視頻時(shí)展示正在加載的提示(菊花轉(zhuǎn))
- 如果可以正常播放視頻益楼,則去掉加載提示猾漫,播放視頻,如果加載失敗偏形,去掉加載提示并顯示失敗提示
- 在播放過程中如果由于網(wǎng)絡(luò)過慢或拖拽原因?qū)е聸]有播放數(shù)據(jù)時(shí)静袖,要展示加載提示,跳轉(zhuǎn)到第4步
緩存代理策略:
- 當(dāng)視頻播放器向代理請(qǐng)求dataRequest時(shí)俊扭,判斷代理是否已經(jīng)向服務(wù)器發(fā)起了請(qǐng)求队橙,如果沒有,則發(fā)起下載整個(gè)視頻文件的請(qǐng)求
2.如果代理已經(jīng)和服務(wù)器建立鏈接,則判斷當(dāng)前的dataRequest請(qǐng)求的offset是否大于當(dāng)前已經(jīng)緩存的文件的offset捐康,如果大于則取消當(dāng)前與服務(wù)器的請(qǐng)求仇矾,并從offset開始到文件尾向服務(wù)器發(fā)起請(qǐng)求(此時(shí)應(yīng)該是由于播放器向后拖拽,并且超過了已緩存的數(shù)據(jù)時(shí)才會(huì)出現(xiàn))- 如果當(dāng)前的dataRequest請(qǐng)求的offset小于已經(jīng)緩存的文件的offset解总,同時(shí)大于代理向服務(wù)器請(qǐng)求的range的offset贮匕,說明有一部分已經(jīng)緩存的數(shù)據(jù)可以傳給播放器,則將這部分?jǐn)?shù)據(jù)返回給播放器(此時(shí)應(yīng)該是由于播放器向前拖拽花枫,請(qǐng)求的數(shù)據(jù)已經(jīng)緩存過才會(huì)出現(xiàn))
- 如果當(dāng)前的dataRequest請(qǐng)求的offset小于代理向服務(wù)器請(qǐng)求的range的offset刻盐,則取消當(dāng)前與服務(wù)器的請(qǐng)求,并從offset開始到文件尾向服務(wù)器發(fā)起請(qǐng)求(此時(shí)應(yīng)該是由于播放器向前拖拽劳翰,并且超過了已緩存的數(shù)據(jù)時(shí)才會(huì)出現(xiàn))
- 只要代理重新向服務(wù)器發(fā)起請(qǐng)求敦锌,就會(huì)導(dǎo)致緩存的數(shù)據(jù)不連續(xù),則加載結(jié)束后不用將緩存的數(shù)據(jù)放入本地cache
- 如果代理和服務(wù)器的鏈接超時(shí)佳簸,重試一次乙墙,如果還是錯(cuò)誤則通知播放器網(wǎng)絡(luò)錯(cuò)誤
- 如果服務(wù)器返回其他錯(cuò)誤,則代理通知播放器網(wǎng)絡(luò)錯(cuò)誤
2.3.2 AVPlayer相關(guān)API簡介
IOS 播放網(wǎng)絡(luò)視頻我們一般使用AVFoundation框架里面的AVPlayer去實(shí)現(xiàn)自定義播放器生均,但是AVPlayer的相關(guān)API都是高度封裝的听想,這樣我們播放網(wǎng)絡(luò)視頻時(shí),往往不能控制其內(nèi)部播放邏輯马胧,比如我們會(huì)發(fā)現(xiàn)播放時(shí)seek會(huì)失敗汉买,數(shù)據(jù)加載完畢后不能獲取到數(shù)據(jù)文件進(jìn)行其他操作,因此我們需要尋找彌補(bǔ)其不足之處的方法漓雅,這里我們選擇了AVAssetResourceLoader录别。我們這里實(shí)現(xiàn)邊下邊播功能也是依賴于它。
先來了解一下AVAssetResourceLoader的作用:讓我們自行掌握AVPlayer數(shù)據(jù)的加載邻吞,包括獲取AVPlayer需要的數(shù)據(jù)的信息组题,以及可以決定傳遞多少數(shù)據(jù)給AVPlayer。
我們大致了解一下AVPlayer的組件圖:
AVAssetResourceLoader:一個(gè) iOS 6 就被開放出來抱冷,專門用來處理 AVAsset 加載的工具崔列。這個(gè)完全滿足JimuPro運(yùn)行在IOS10以上的要求。
AVAssetResourceLoader 有一個(gè)AVAssetResourceLoaderDelegate代理旺遮,這個(gè)代理有兩個(gè)重要的接口:
- 要求加載資源的代理方法赵讯,這時(shí)我們需要保存loadingRequest并對(duì)其所指定的數(shù)據(jù)進(jìn)行讀取或下載操作,當(dāng)數(shù)據(jù)讀取或下載完成耿眉,我們可以對(duì)loadingRequest進(jìn)行完成操作边翼。
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader
shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest;
- 取消加載資源的代理方法,這時(shí)我們需要取消loadingRequest所指定的數(shù)據(jù)的讀取或下載操作组底。
- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader
didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest;
我們只要找一個(gè)對(duì)象實(shí)現(xiàn)了 AVAssetResourceLoaderDelegate 這個(gè)協(xié)議的方法晶密,丟給 asset夏伊,再把 asset 丟給 AVPlayer骑晶,AVPlayer 在執(zhí)行播放的時(shí)候就會(huì)去問這個(gè) delegate:喂斩郎,你能不能播放這個(gè) url 拔媪洹?然后會(huì)觸發(fā)下面這個(gè)方法:- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest
我們?cè)谶@個(gè)方法中看看 request 里面的 url 是不是我們支持的它匕,如果能支持就返回 YES展融!然后就可以開心的一邊下視頻數(shù)據(jù),一邊塞數(shù)據(jù)給 AVPlayer 讓它顯示視頻畫面豫柬。
AVUrlAsset
在請(qǐng)求自定義的URLScheme
資源的時(shí)候會(huì)通過AVAssetResourceLoader
實(shí)例來進(jìn)行資源請(qǐng)求告希。它是AVUrlAsset
的屬性,聲明如下:var resourceLoader: AVAssetResourceLoader { get }
而AVAssetResourceLoader
請(qǐng)求的時(shí)候會(huì)把相關(guān)請(qǐng)求(AVAssetResourceLoadingRequest
)傳遞給AVAssetResourceLoaderDelegate
(如果有實(shí)現(xiàn)的話),我們可以保存這些請(qǐng)求烧给,然后構(gòu)造自己的NSUrlRequset
來發(fā)送請(qǐng)求燕偶,當(dāng)收到響應(yīng)的時(shí)候,把響應(yīng)的數(shù)據(jù)設(shè)置給AVAssetResourceLoadingRequest
,并且對(duì)數(shù)據(jù)進(jìn)行緩存创夜,就完成了邊下邊播杭跪,整個(gè)流程大體如下圖:
其中最為復(fù)雜的部分是數(shù)據(jù)偏移處理,因?yàn)閿?shù)據(jù)是分塊下載和分塊填充的驰吓,我們的需要填充的對(duì)象是
AVAssetResourceLoadingDataRequest
涧尿,需要控制好currentOffset
。
下面我們將來詳細(xì)的介紹使用AVPlayer和AVAssetResourceLoaderDelegate來實(shí)現(xiàn)邊下邊播的具體實(shí)現(xiàn)檬贰。
3 HTTP邊下邊播 mp4文件 實(shí)現(xiàn)細(xì)節(jié)
目前網(wǎng)上有好多關(guān)于IOS邊下邊播的代碼姑廉,其實(shí)原理都是一樣的,只是實(shí)現(xiàn)方式翁涤,細(xì)節(jié)不一樣桥言,這里推薦兩個(gè)比較好的開源代碼:
- OC版本:VIMediaCache 目前git上面有642顆星星萌踱,相當(dāng)不錯(cuò)。
- Swift版本:VGPlayer 目前git上面有363顆星星号阿,功能也比較完善并鸵,這是我比較推薦的。
3.1 邊下邊播原理
邊下邊播的原理已經(jīng)在上面的3種方案介紹中詳細(xì)描述了扔涧,這里主要是基于第三種方案用AVPlayer 來實(shí)現(xiàn)邊下邊播园担。這里先拋開HTTPS字簽證書的簽名認(rèn)證問題,先講解基于HTTP方式的邊下邊播枯夜,主流程圖如下:
整個(gè)過程就是分為兩大塊弯汰,一塊是實(shí)時(shí)播放視頻,一塊就是緩存策略下載視頻湖雹。
3.1.1 實(shí)時(shí)播放原理
我們先來看第一塊咏闪,實(shí)時(shí)播放視頻(先不管下載和緩存),實(shí)現(xiàn)上摔吏,我們可以分為兩步:
- 需要知道如何請(qǐng)求數(shù)據(jù)鸽嫂,url 是什么,下載多少數(shù)據(jù)舔腾。
- 下載好的數(shù)據(jù)怎么塞給 AVPlayer
3.1.1.1 請(qǐng)求數(shù)據(jù)
在上面的回調(diào)方法中溪胶,會(huì)得到一個(gè) AVAssetResourceLoadingRequest 對(duì)象,它里面的屬性和方法不多稳诚,為了減少干擾,我精簡了一下這個(gè)類的頭文件瀑踢,只留下我們會(huì)用到以及需要解釋的屬性和方法:
@interface AVAssetResourceLoadingRequest : NSObject
@property (nonatomic, readonly) NSURLRequest *request;
@property (nonatomic, readonly, nullable) AVAssetResourceLoadingContentInformationRequest *contentInformationRequest NS_AVAILABLE(10_9, 7_0);
@property (nonatomic, readonly, nullable) AVAssetResourceLoadingDataRequest *dataRequest NS_AVAILABLE(10_9, 7_0);
- (void)finishLoading NS_AVAILABLE(10_9, 7_0);
- (void)finishLoadingWithError:(nullable NSError *)error;
@end
在 AVAssetResourceLoadingRequest
里面扳还,request
代表原始的請(qǐng)求,由于 AVPlayer
是會(huì)觸發(fā)分片下載的策略橱夭,還需要從dataRequest
中得到請(qǐng)求范圍的信息氨距。有了請(qǐng)求地址和請(qǐng)求范圍,我們就可以重新創(chuàng)建一個(gè)設(shè)置了請(qǐng)求 Range
頭的 NSURLRequest
對(duì)象棘劣,讓下載器去下載這個(gè)文件的 Range
范圍內(nèi)的數(shù)據(jù)俏让。
3.1.1.2 賽數(shù)據(jù)給AVPlayer
當(dāng) AVPlayer
觸發(fā)下載時(shí),總是會(huì)先發(fā)起一個(gè) Range
為 0-2
的數(shù)據(jù)請(qǐng)求茬暇,這個(gè)請(qǐng)求的作用其實(shí)是用來確認(rèn)視頻數(shù)據(jù)的信息首昔,如文件類型、文件數(shù)據(jù)長度糙俗。當(dāng)下載器發(fā)起這個(gè)請(qǐng)求勒奇,收到服務(wù)端返回的 response
后,我們要把視頻的信息填充到 AVAssetResourceLoadingRequest
的 contentInformationRequest
屬性中巧骚,告知下載的視頻格式以及視頻長度赊颠。
AVAssetResourceLoadingRequest
在 - (void)finishLoading
的時(shí)候格二,會(huì)根據(jù) contentInformationRequest
中的信息,去判斷接下去要怎么處理竣蹦。例如:下載 AVURLAsset
中 URL 指向的文件顶猜,獲取到的文件的 contentType
是系統(tǒng)不支持的類型,這個(gè) AVURLAsset
將無法正常播放痘括。
獲取完視頻信息后长窄,會(huì)收到剛才指定的 2 Byte
的 data
數(shù)據(jù),下載到的數(shù)據(jù)怎么辦远寸? 可以塞給 AVAssetResourceLoadingRequest
里的 dataRequest
抄淑。 dataRequest
里面用 - (void)respondWithData:(NSData *)data;
專門用來接收下載的數(shù)據(jù),這個(gè)方法可以調(diào)用多次驰后,接收增量連續(xù)的 data
數(shù)據(jù)肆资。
當(dāng) AVAssetResourceLoadingRequest
要求的所有數(shù)據(jù)都下載完畢,調(diào)用 - (void)finishLoading
完成下載灶芝,AVAssetResourceLoader
會(huì)繼續(xù)發(fā)起之后的數(shù)據(jù)片段的請(qǐng)求郑原。如果本次請(qǐng)求失敗,可以直接調(diào)用 - (void)finishLoadingWithError:(nullable NSError *)error;
結(jié)束下載夜涕。
3.1.1.3 重試機(jī)制
在實(shí)際的測試中犯犁,發(fā)現(xiàn)AVAssetResourceLoader
在執(zhí)行加載的時(shí)候,會(huì)時(shí)不時(shí)的觸發(fā)取消下載調(diào)用 - (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest女器,
然后重新發(fā)起加載請(qǐng)求的策略酸役。如果下載了部分,那么重新發(fā)起的下載請(qǐng)求會(huì)從還沒有下載的部分開始驾胆。
AVAssetResourceLoaderDelegate
中還有 3 個(gè)方法可以針對(duì)特殊場景做處理涣澡,不過在目前的環(huán)境中都用不到所以可以選擇不實(shí)現(xiàn)這些方法。
3.1.2 下載緩存原理
通過上面實(shí)時(shí)播放原理的介紹丧诺,我們已經(jīng)知道 AVAssetResourceLoaderDelegate
的實(shí)現(xiàn)機(jī)制入桂,當(dāng) AVAsset
需要加載數(shù)據(jù)時(shí)會(huì)通過 delegate
告訴外部,外部接管整個(gè)視頻下載過程驳阎。
當(dāng)我們接管了視頻下載抗愁,便可以對(duì)視頻數(shù)據(jù)做任何事情。比如:緩存呵晚、記錄下載速度蜘腌、獲得下載進(jìn)度等等。
實(shí)現(xiàn)一個(gè)下載器劣纲,就是用 URLSession
開啟一個(gè) DataTask
請(qǐng)求數(shù)據(jù)逢捺,把接收到的數(shù)據(jù)塞給 DataRequest
并寫入本地磁盤。在實(shí)現(xiàn)下載器時(shí)主要有三個(gè)注意的點(diǎn):1. Range 請(qǐng)求 2. 可取消下載 3. 分片緩存
3.1.2.1 Range 請(qǐng)求
- 能夠通過Range分片請(qǐng)求癞季,是實(shí)現(xiàn)實(shí)時(shí)播放劫瞳,邊下邊播的關(guān)鍵倘潜。
每次得到的 LoadingRequest
帶有請(qǐng)求數(shù)據(jù)范圍的信息,比如期望請(qǐng)求第 100 字節(jié)到 500 字節(jié)志于,在創(chuàng)建 URLRequest
時(shí)需要設(shè)置 HTTPHeader
的 Range
值涮因。
NSString *range = [NSString stringWithFormat:@"bytes=%lld-%lld", fromOffset, endOffset];
[request setValue:range forHTTPHeaderField:@"Range"];
引入分塊下載最大的復(fù)雜點(diǎn)在于對(duì)響應(yīng)數(shù)據(jù)的contentOffset的處理上,好在AVAssetResourceLoader幫我們處理了大量工作伺绽,我們只需要用好AVAssetResourceLoadingRequest就可以了养泡。
例如,下面是代碼部分奈应,首先是獲取原始請(qǐng)求和發(fā)送新的請(qǐng)求
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
if self.session == nil {
//構(gòu)造Session
let configuration = URLSessionConfiguration.default
configuration.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData
configuration.networkServiceType = .video
configuration.allowsCellularAccess = true
self.session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}
//構(gòu)造 保存請(qǐng)求
var urlRequst = URLRequest.init(url: self.initalUrl!, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 20) // 20s超時(shí)
urlRequst.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
urlRequst.httpMethod = "GET"
//設(shè)置請(qǐng)求頭
guard let wrappedDataRequest = loadingRequest.dataRequest else{
//本次請(qǐng)求沒有數(shù)據(jù)請(qǐng)求
return true
}
let range:NSRange = NSMakeRange(Int.init(truncatingBitPattern: wrappedDataRequest.requestedOffset), wrappedDataRequest.requestedLength)
let rangeHeaderStr = "byes=\(range.location)-\(range.location+range.length)"
urlRequst.setValue(rangeHeaderStr, forHTTPHeaderField: "Range")
urlRequst.setValue(self.initalUrl?.host, forHTTPHeaderField: "Referer")
guard let task = session?.dataTask(with: urlRequst) else{
fatalError("cant create task for url")
}
task.resume()
self.tasks[task] = loadingRequest
return true
}
收到響應(yīng)請(qǐng)求后澜掩,抓包查看響應(yīng)的請(qǐng)求頭,下圖是2個(gè)響應(yīng)的請(qǐng)求頭:
Content-Length表示本次請(qǐng)求的數(shù)據(jù)長度
Content-Range表示本次請(qǐng)求的數(shù)據(jù)在總媒體文件中的位置杖挣,格式是
start-end/total
肩榕,因此就有Content-Length = end - start + 1
。
3.1.2.2 可取消下載
AVAsset
在加載視頻時(shí)惩妇,經(jīng)常會(huì)在某次數(shù)據(jù)請(qǐng)求還沒有完成時(shí)觸發(fā)取消下載株汉,然后發(fā)起一個(gè)新的 LoadingReqeust
。這個(gè)機(jī)制是 AVAsset
里的黑盒歌殃,具體邏輯無法得知乔妈,比較像是 AVAsset
的一種重試機(jī)制。 作為下載器氓皱,在收到取消通知時(shí)路召,需要立刻停止下載。由于 DataRequest
的 cancel
操作是異步的波材,就有可能在 cancel
還未完成時(shí)优训,下一個(gè) LoadingRequest
就已經(jīng)到來,所以還需要需要保證同一個(gè) URL 只能同時(shí)存在一個(gè)下載器在下載各聘,否則會(huì)出現(xiàn)數(shù)據(jù)混亂的問題。
3.1.2.3 分片緩存
如果只是單純的下載視頻抡医,數(shù)據(jù)單調(diào)遞增躲因,緩存處理還是比較容易。然而現(xiàn)實(shí)是用戶對(duì) player 的 seek
操作給視頻的緩存管理帶來了巨大的挑戰(zhàn)忌傻,一旦涉及到用戶操作大脉,可能性就越多,復(fù)雜度也會(huì)越高水孩。
沒有 seek
的情況:網(wǎng)速正常時(shí)緩存數(shù)據(jù)比播放時(shí)間走得開镰矿,正常播放;網(wǎng)速慢時(shí)俘种,播放器 loading
秤标,直到有足夠的數(shù)據(jù)量進(jìn)行播放绝淡,如果網(wǎng)速一直很慢就會(huì)播幾秒卡一下。
當(dāng)加入 seek
后會(huì)有三種可能:
-
第一種情況苍姜,視頻完全下載好牢酵,這時(shí) seek 只需讀取相應(yīng)緩存即可,這種情況最簡單衙猪,就直接從緩存讀數(shù)據(jù)即可馍乙。
圖3.1.2.3.1 - seek時(shí)視頻完成下載了 - 第二種情況,視頻下載一半垫释,用戶
seek
到未下載部分丝格,LoadingRequest
請(qǐng)求的部分全部都是未下載的數(shù)據(jù)。這時(shí)需要取消正在下載的數(shù)據(jù)棵譬,然后從seek
的點(diǎn)開始下載數(shù)據(jù)显蝌。為了支持seek
操作仑荐,下載器就需要支持分片緩存麦撵。目前使用的解決方案是下載的視頻數(shù)據(jù)會(huì)根據(jù)請(qǐng)求的Range
值,把數(shù)據(jù)存儲(chǔ)到文件中對(duì)應(yīng)的偏移值位置低缩,并且每個(gè)視頻文件都會(huì)另外再保存一個(gè)與之對(duì)應(yīng)的下載信息文件算谈。這個(gè)信息文件會(huì)記錄當(dāng)前下載了多少數(shù)據(jù)涩禀,總共有多少數(shù)據(jù),下載了哪些片段的數(shù)據(jù)等信息然眼,之后的緩存管理會(huì)非常依賴這個(gè)配置文件艾船。
圖3.1.2.3.2 - seek時(shí)都是未下載的部分
- 第三種情況,視頻被
seek
了多次高每,用戶seek
到一個(gè)時(shí)間點(diǎn)屿岂,LoadingRequest
請(qǐng)求的部分包含了已下載和未下載的部分。這種情況是最復(fù)雜的鲸匿!簡單的做法是爷怀,當(dāng)成上面的情況來處理,全部都重新下載带欢,雖然邏輯簡單运授,但這個(gè)方案會(huì)下載多次同樣的數(shù)據(jù),不是最最優(yōu)解乔煞。我的目標(biāo)當(dāng)然是做最優(yōu)的解決方案吁朦,但也是復(fù)雜高很多的解決方案。圖3.1.2.3.3 - seek時(shí)既有下載完的部分渡贾,又有未下載的部分
在收到LoadingRequest
的請(qǐng)求范圍后逗宜,下載器會(huì)先獲取已經(jīng)下載的數(shù)據(jù)信息,把已下載的分片信息分別創(chuàng)建一個(gè)action
,再把需要遠(yuǎn)程下載的分片數(shù)據(jù)分別創(chuàng)建一個(gè)action
纺讲。最終組合就可能是LocalAction(50-100 bytes) + RemoteAction(101-200 bytes) + LocalAction(201-300 bytes) + RemoteAction(300-400 bytes)
擂仍。每一個(gè)action
會(huì)按順序獲取數(shù)據(jù)再返回給LoadingRequest
。如下圖:
圖3.1.2.3.4 - seek時(shí)既有下載完的部分刻诊,又有未下載的部分防楷,創(chuàng)建action
3.2 邊下邊播實(shí)現(xiàn)細(xì)節(jié)
在下載視頻時(shí),出現(xiàn)錯(cuò)誤無法正常下載是比較容易出現(xiàn)的则涯。我們自己實(shí)現(xiàn)了 AVAssetResourceLoaderDelegate 在第一次請(qǐng)求就拋出錯(cuò)誤的話复局,播放器會(huì)馬上提示錯(cuò)誤狀態(tài),而如果是已經(jīng)響應(yīng)了部分?jǐn)?shù)據(jù)粟判,再拋錯(cuò)誤亿昏,AVAssetResourceLoader 會(huì)忽略錯(cuò)誤而一直處于 loading,直到超時(shí)档礁。這種情況就比較尷尬角钩,在上面給出的VIMediaCache 實(shí)現(xiàn)中, VIResourceLoaderManager 提供了 delegate呻澜,如果內(nèi)部出現(xiàn)錯(cuò)誤递礼,就會(huì)拋出錯(cuò)誤,再又外部業(yè)務(wù)決定是如何處理羹幸。
同一時(shí)間同一個(gè) url 不能有多次下載: 由于緩存內(nèi)部實(shí)現(xiàn)是對(duì)每一個(gè) url 都共用同一個(gè)下載配置文件脊髓,如果同時(shí)有多次對(duì)同一個(gè) url 進(jìn)行下載,這個(gè)文件下載信息會(huì)被同時(shí)修改栅受,下載信息會(huì)變得混亂将硝。VIMediaCache 里的 MediaCache 內(nèi)部做了簡單的處理,如果正在下載某 url屏镊,這時(shí)再想嘗試下載同樣的 url 會(huì)直接拋出錯(cuò)誤依疼,提示無法開始下載。
實(shí)際上VGPlayer只是參考VIMediaCache 方式的Swift版本實(shí)現(xiàn)而芥,VIMediaCache 是真的大牛編寫的OC版本律罢,值得好好研究。
鑒于我們JimuPro工程師純swift項(xiàng)目棍丐,里面處了第三方庫沒有使用OC代碼弟翘,所以我優(yōu)先選擇VGPlayer來實(shí)現(xiàn)機(jī)器人端到IOS app端的邊下邊播功能。
-
由于VGPlayer沒有實(shí)現(xiàn)HTTPS的證書驗(yàn)證骄酗,這里我只需要簡單實(shí)現(xiàn)證書驗(yàn)證代碼即可。我們將在下面講解HTTPS的證書認(rèn)證實(shí)現(xiàn)悦冀。這里我簡單說一下我的實(shí)現(xiàn)趋翻,
在VGPlayerDownloadURLSessionManager.swift文件的VGPlayerDownloadURLSessionManager類里面增加一個(gè)URLSession的一個(gè)代理實(shí)現(xiàn):
增加一個(gè)URLSession的一個(gè)代理實(shí)現(xiàn)
- 即使你參考上面的源碼實(shí)現(xiàn)了邊下載邊播放,還是有些細(xì)節(jié)地方需要注意的:
例如要實(shí)現(xiàn)mp4文件的邊下邊播功能盒蟆,不僅依賴于上面講解的邊下邊播實(shí)現(xiàn)方案踏烙,還依賴于mp4的文件格式师骗。如果遇到這種mp4文件的元數(shù)據(jù)放在文件末尾的,我們需要在服務(wù)器端將mp4文件做一下轉(zhuǎn)換才可以實(shí)現(xiàn)邊下邊播功能讨惩。
接下來詳細(xì)講解一下mp4格式處理問題辟癌。
3.3 邊下邊播mp4文件格式需要注意
我們要明確一點(diǎn)就是即使你用上面的緩存方式實(shí)現(xiàn)了邊下邊播的功能,并不是所有mp4都支持的荐捻,這個(gè)需要你理解邊下邊播的原理黍少。
mp4視頻文件頭中,包含一些元數(shù)據(jù)处面。元數(shù)據(jù)包含:視頻的寬度高度厂置、視頻時(shí)長、編碼格式等魂角。mp4元數(shù)據(jù)通常在視頻文件的頭部昵济,這樣播放器在讀取文件時(shí)會(huì)最先讀取視頻的元數(shù)據(jù),然后開始播放視頻野揪。
當(dāng)然也存在這樣一種情況:mp4視頻的元數(shù)據(jù)處于視頻文件最后访忿,這樣播放器在加載視頻文件時(shí),一直讀取到最后斯稳,才讀取到視頻信息海铆,然后開始播放。如果缺少元數(shù)據(jù)平挑,也是這樣的情況游添。這就出現(xiàn)了mp4視頻不支持邊加載、邊播放的問題通熄。
- 為啥會(huì)出現(xiàn)上面說的這種情況呢唆涝,下面我們簡單分析一下原理:
在請(qǐng)求頭里有一個(gè)Range:byte字段來告訴媒體服務(wù)器需要請(qǐng)求的是哪一段特定長度的文件內(nèi)容,對(duì)于MP4文件來說唇辨,所有數(shù)據(jù)都封裝在一個(gè)個(gè)的box或者atom中廊酣,其中有兩個(gè)atom尤為重要,分別是moov atom和mdat atom赏枚。
-
moov atom
:包含媒體的元數(shù)據(jù)的數(shù)據(jù)結(jié)構(gòu)亡驰,包括媒體的塊(box)信息,格式說明等等饿幅。 -
mdat atom
: 包含媒體的媒體信息凡辱,對(duì)于視屏來說就是視頻畫面了。
在IOS中發(fā)送一個(gè)請(qǐng)求栗恩,利用NSUrlSession直接請(qǐng)求視頻資源透乾,針對(duì)元信息在視頻文件頭部的視頻可以實(shí)現(xiàn)邊下邊播,而元信息在視頻尾部的視頻則會(huì)下載完才播放,為啥會(huì)這樣呢乳乌?
答案就是:雖然moov和mdat都只有一個(gè)捧韵,但是由于MP4文件是由若干個(gè)這樣的box或者atom組成的,因此這兩個(gè)atom在不同媒體文件中出現(xiàn)的順序可能會(huì)不一樣汉操,為了加快流媒體的播放再来,我們可以做的優(yōu)化之一就是手動(dòng)把moov提到mdat之前。 對(duì)于AVPlayer來說磷瘤,只有到AVPlayerItemStatusReadyToPlay狀態(tài)時(shí)芒篷,才可以開始播放視頻,而進(jìn)入AVPlayerItemStatusReadyToPlay狀態(tài)的必要條件就是播放器讀到了媒體的moov塊膀斋。
如果mdat位于moov之后梭伐,那么這樣的mp4視頻文件是無法實(shí)現(xiàn)邊下邊播放的。要支持邊下邊播的mp4視頻需要滿足moov和mdat都位于文件頭部仰担,且moov位于mdat之前糊识。如下圖所示:
當(dāng)moov和mdat都位于文件頭部,且moov位于mdat之前摔蓝。我們理論上一個(gè)請(qǐng)求就可以播放所有的moov位于mdat之前的視頻的赂苗。但是,當(dāng)我們seek拖拽播放的話贮尉,情況就變很復(fù)雜了拌滋,需要借助分塊下載。
那么猜谚,如果遇到這種mp4文件的元數(shù)據(jù)放在文件末尾的败砂,我們需要在服務(wù)器端將mp4文件做一下轉(zhuǎn)換才可以實(shí)現(xiàn)邊下邊播功能。
可行的方法是使用的是qt-faststart
工具魏铅。
qt-faststart
能夠?qū)⑻幱贛P4文件末尾的moov atom
元數(shù)據(jù)轉(zhuǎn)移到最前面昌犹,不過由于qt-faststart工具只能處理moov atom
元數(shù)據(jù)位于MP4末尾的文件。
如果我們想要將所有文件統(tǒng)一處理:整體思路是將MP4文件通過ffmpeg處理览芳,將moov atom
元數(shù)據(jù)轉(zhuǎn)移至末尾斜姥,然后使用qt-faststart
工具轉(zhuǎn)移至最前面。
3.3.1 mp4 元數(shù)據(jù)特殊處理
- FFmpeg下載編譯
ffmpeg下載點(diǎn)擊這里
- 先將下載的FFmpeg包解壓:
tar -jxvf ffmpeg-3.3.3.tar.bz2
- 配置:
./configure --enable-shared --prefix=/usr/local/ffmpeg
prefix就是設(shè)置安裝位置沧竟,一般都默認(rèn)usr/local下铸敏。 - 安裝:
make
make install
編譯安裝時(shí)間會(huì)很長,10分鐘左右吧悟泵,裝完以后可以去安裝目錄下查看杈笔。
這時(shí)還沒有結(jié)束,現(xiàn)在使用的話一般會(huì)報(bào)如下錯(cuò)誤:
ffmpeg: error while loading shared libraries: libavfilter.so.1: cannot open shared object file: No such file or directory
- 需要編輯
/etc/ld.so.conf
文件加入如下內(nèi)容:/usr/local/lib
,保存退出后執(zhí)行ldconfig
命令糕非。
echo "/usr/local/ffmpeg/lib" >> /etc/ld.so.conf
#注意這里是你前面安裝ffmpeg的路徑
ldconfig
- qt-faststart 安裝
上面講到的qt-faststart
工具其實(shí)就在ffmpeg的源碼中有桩撮,因?yàn)樵趂fmpeg解壓完的文件中存在qt-faststart的源碼敦第,所以直接使用,位置在解壓路徑/tools/qt-faststart.c
如果你想單獨(dú)下載點(diǎn)擊這里: qt-faststart下載
- 進(jìn)入ffmpeg解壓路徑執(zhí)行命令:
make tools/qt-faststart
,會(huì)看到在tools中會(huì)出現(xiàn)一個(gè)qt-faststart文件(還有一個(gè).c文件) -
ffmpeg
將元數(shù)據(jù)轉(zhuǎn)移至文件末尾:
cd ffmpeg安裝路徑/bin;./ffmpeg -i /opt/mp4test.mp4 -acodec copy -vcodec copy /opt/1.mp4
# /opt/mp4test.mp4為原始MP4文件路徑店量,/opt/1.mp4為生成文件的存放路徑
-
qt-faststart
將元數(shù)據(jù)轉(zhuǎn)移到文件開頭:
cd ffmpeg壓縮包解壓路徑/tools;
./qt-faststart /opt/1.mp4 /opt/2.mp4
4 HTTPS 邊下邊播 自簽名證書認(rèn)證
- 具體HTTPS自簽名證書的原理,可以參考之前寫的一篇博客:IOS 使用自簽名證書開發(fā)HTTPS文件傳輸
- HTTPS SSL加密建立連接過程
如下圖:
HTTPS SSL加密建立連接過程
過程詳解:
- ①客戶端的瀏覽器向服務(wù)器發(fā)送請(qǐng)求鞠呈,并傳送客戶端SSL 協(xié)議的版本號(hào)融师,加密算法的種類,產(chǎn)生的隨機(jī)數(shù)蚁吝,以及其他服務(wù)器和客戶端之間通訊所需要的各種信息旱爆。
- ②服務(wù)器向客戶端傳送SSL 協(xié)議的版本號(hào),加密算法的種類窘茁,隨機(jī)數(shù)以及其他相關(guān)信息怀伦,同時(shí)服務(wù)器還將向客戶端傳送自己的證書。
- ③客戶端利用服務(wù)器傳過來的信息驗(yàn)證服務(wù)器的合法性山林,服務(wù)器的合法性包括:證書是否過期房待,發(fā)行服務(wù)器證書的CA 是否可靠,發(fā)行者證書的公鑰能否正確解開服務(wù)器證書的“發(fā)行者的數(shù)字簽名”驼抹,服務(wù)器證書上的域名是否和服務(wù)器的實(shí)際域名相匹配。如果合法性驗(yàn)證沒有通過框冀,通訊將斷開绣硝;如果合法性驗(yàn)證通過,將繼續(xù)進(jìn)行第四步帆吻。
- ④用戶端隨機(jī)產(chǎn)生一個(gè)用于通訊的“對(duì)稱密碼”域那,然后用服務(wù)器的公鑰(服務(wù)器的公鑰從步驟②中的服務(wù)器的證書中獲得)對(duì)其加密,然后將加密后的“預(yù)主密碼”傳給服務(wù)器猜煮。
- ⑤如果服務(wù)器要求客戶的身份認(rèn)證(在握手過程中為可選)次员,用戶可以建立一個(gè)隨機(jī)數(shù)然后對(duì)其進(jìn)行數(shù)據(jù)簽名,將這個(gè)含有簽名的隨機(jī)數(shù)和客戶自己的證書以及加密過的“預(yù)主密碼”一起傳給服務(wù)器王带。
- ⑥如果服務(wù)器要求客戶的身份認(rèn)證淑蔚,服務(wù)器必須檢驗(yàn)客戶證書和簽名隨機(jī)數(shù)的合法性,具體的合法性驗(yàn)證過程包括:客戶的證書使用日期是否有效愕撰,為客戶提供證書的CA 是否可靠刹衫,發(fā)行CA 的公鑰能否正確解開客戶證書的發(fā)行CA 的數(shù)字簽名醋寝,檢查客戶的證書是否在證書廢止列表(CRL)中。檢驗(yàn)如果沒有通過带迟,通訊立刻中斷音羞;如果驗(yàn)證通過,服務(wù)器將用自己的私鑰解開加密的“預(yù)主密碼”仓犬,然后執(zhí)行一系列步驟來產(chǎn)生主通訊密碼(客戶端也將通過同樣的方法產(chǎn)生相同的主通訊密碼)嗅绰。
- ⑦服務(wù)器和客戶端用相同的主密碼即“通話密碼”,一個(gè)對(duì)稱密鑰用于SSL 協(xié)議的安全數(shù)據(jù)通訊的加解密通訊搀继。同時(shí)在SSL 通訊過程中還要完成數(shù)據(jù)通訊的完整性窘面,防止數(shù)據(jù)通訊中的任何變化。
- ⑧客戶端向服務(wù)器端發(fā)出信息叽躯,指明后面的數(shù)據(jù)通訊將使用的步驟. ⑦中的主密碼為對(duì)稱密鑰财边,同時(shí)通知服務(wù)器客戶端的握手過程結(jié)束。
- ⑨服務(wù)器向客戶端發(fā)出信息点骑,指明后面的數(shù)據(jù)通訊將使用的步驟⑦中的主密碼為對(duì)稱密鑰酣难,同時(shí)通知客戶端服務(wù)器端的握手過程結(jié)束。
- ⑩SSL 的握手部分結(jié)束畔况,SSL 安全通道的數(shù)據(jù)通訊開始鲸鹦,客戶和服務(wù)器開始使用相同的對(duì)稱密鑰進(jìn)行數(shù)據(jù)通訊,同時(shí)進(jìn)行通訊完整性的檢驗(yàn)跷跪。
- 我這里只給出我項(xiàng)目里面使用VGPlayer播放器里的HTTPS證書認(rèn)證方式實(shí)現(xiàn)代碼馋嗜,只需要簡單的兩部即可實(shí)現(xiàn):
-
先將服務(wù)器給你自簽名證書添加到工程里面:
導(dǎo)入自簽名證書 -
在VGPlayerDownloadURLSessionManager.swift文件的VGPlayerDownloadURLSessionManager類里面增加一個(gè)URLSession的一個(gè)代理實(shí)現(xiàn):
增加一個(gè)URLSession的一個(gè)代理實(shí)現(xiàn)
public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
let method = challenge.protectionSpace.authenticationMethod
if method == NSURLAuthenticationMethodServerTrust {
//驗(yàn)證服務(wù)器,直接信任或者驗(yàn)證證書二選一吵瞻,推薦驗(yàn)證證書葛菇,更安全
completionHandler( HTTPSManager.trustServerWithCer(challenge: challenge).0, HTTPSManager.trustServerWithCer(challenge: challenge).1)
} else if method == NSURLAuthenticationMethodClientCertificate {
//認(rèn)證客戶端證書
completionHandler( HTTPSManager.sendClientCer().0, HTTPSManager.sendClientCer().1)
} else {
//其他情況,不通過驗(yàn)證
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
- 認(rèn)證類HTTPSManager的實(shí)現(xiàn)如下:
//
// HTTPSManager.swift
// JimuPro
//
// Created by yulu kong on 2019/10/28.
// Copyright ? 2019 UBTech. All rights reserved.
//
import UIKit
class HTTPSManager: NSObject {
// // MARK: - sll證書處理
// static func setKingfisherHTTPS() {
// //取出downloader單例
// let downloader = KingfisherManager.shared.downloader
// //信任Server的ip
// downloader.trustedHosts = Set([ServerTrustHost.fileTransportIP])
// }
//
// static func setAlamofireHttps() {
//
// SessionManager.default.delegate.sessionDidReceiveChallenge = { (session: URLSession, challenge: URLAuthenticationChallenge) in
//
// let method = challenge.protectionSpace.authenticationMethod
// if method == NSURLAuthenticationMethodServerTrust {
// //驗(yàn)證服務(wù)器橡羞,直接信任或者驗(yàn)證證書二選一眯停,推薦驗(yàn)證證書,更安全
// return HTTPSManager.trustServerWithCer(challenge: challenge)
//// return HTTPSManager.trustServer(challenge: challenge)
//
// } else if method == NSURLAuthenticationMethodClientCertificate {
// //認(rèn)證客戶端證書
// return HTTPSManager.sendClientCer()
//
// } else {
// //其他情況卿泽,不通過驗(yàn)證
// return (.cancelAuthenticationChallenge, nil)
// }
// }
// }
//不做任何驗(yàn)證莺债,直接信任服務(wù)器
static private func trustServer(challenge: URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?) {
let disposition = URLSession.AuthChallengeDisposition.useCredential
let credential = URLCredential.init(trust: challenge.protectionSpace.serverTrust!)
return (disposition, credential)
}
//驗(yàn)證服務(wù)器證書
static func trustServerWithCer(challenge: URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?) {
var disposition: URLSession.AuthChallengeDisposition = .performDefaultHandling
var credential: URLCredential?
//獲取服務(wù)器發(fā)送過來的證書
let serverTrust:SecTrust = challenge.protectionSpace.serverTrust!
let certificate = SecTrustGetCertificateAtIndex(serverTrust, 0)!
let remoteCertificateData = CFBridgingRetain(SecCertificateCopyData(certificate))!
//加載本地CA證書
// let cerPath = Bundle.main.path(forResource: "oooo", ofType: "cer")!
// let cerUrl = URL(fileURLWithPath:cerPath)
let cerUrl = Bundle.main.url(forResource: "server", withExtension: "cer")!
let localCertificateData = try! Data(contentsOf: cerUrl)
if (remoteCertificateData.isEqual(localCertificateData) == true) {
//服務(wù)器證書驗(yàn)證通過
disposition = URLSession.AuthChallengeDisposition.useCredential
credential = URLCredential(trust: serverTrust)
} else {
//服務(wù)器證書驗(yàn)證失敗
//disposition = URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge
disposition = URLSession.AuthChallengeDisposition.useCredential
credential = URLCredential(trust: serverTrust)
}
return (disposition, credential)
}
//發(fā)送客戶端證書交由服務(wù)器驗(yàn)證
static func sendClientCer() -> (URLSession.AuthChallengeDisposition, URLCredential?) {
let disposition = URLSession.AuthChallengeDisposition.useCredential
var credential: URLCredential?
//獲取項(xiàng)目中P12證書文件的路徑
let path: String = Bundle.main.path(forResource: "clientp12", ofType: "p12")!
let PKCS12Data = NSData(contentsOfFile:path)!
let key : NSString = kSecImportExportPassphrase as NSString
let options : NSDictionary = [key : "123456"] //客戶端證書密碼
var items: CFArray?
let error = SecPKCS12Import(PKCS12Data, options, &items)
if error == errSecSuccess {
let itemArr = items! as Array
let item = itemArr.first!
let identityPointer = item["identity"];
let secIdentityRef = identityPointer as! SecIdentity
let chainPointer = item["chain"]
let chainRef = chainPointer as? [Any]
credential = URLCredential.init(identity: secIdentityRef, certificates: chainRef, persistence: URLCredential.Persistence.forSession)
}
return (disposition, credential)
}
}
6 播放器底層原理
6.1 視頻格式簡介
- mp4 也叫做MPEG-4 官方介紹如下:
- MP4是一套用于音頻、視頻信息的壓縮編碼標(biāo)準(zhǔn)签夭,由國際標(biāo)準(zhǔn)化組織(ISO)和國際電工委員會(huì)(IEC)下屬的“動(dòng)態(tài)圖像專家組”(Moving Picture Experts Group齐邦,即MPEG)制定,第一版在1998年10月通過第租,第二版在1999年12月通過措拇。MPEG-4格式的主要用途在于網(wǎng)上流、光盤慎宾、語音發(fā)送(視頻電話)丐吓,以及電視廣播浅悉。
- MPEG-4包含了MPEG-1及MPEG-2的絕大部份功能及其他格式的長處,并加入及擴(kuò)充對(duì)虛擬現(xiàn)實(shí)模型語言(VRML 券犁, VirtualReality Modeling Language)的支持术健,面向?qū)ο蟮暮铣蓹n案(包括音效,視訊及VRML對(duì)象)粘衬,以及數(shù)字版權(quán)管理(DRM)及其他互動(dòng)功能苛坚。而MPEG-4比MPEG-2更先進(jìn)的其中一個(gè)特點(diǎn),就是不再使用宏區(qū)塊做影像分析色难,而是以影像上個(gè)體為變化記錄,因此盡管影像變化速度很快等缀、碼率不足時(shí)枷莉,也不會(huì)出現(xiàn)方塊畫面。
MP4標(biāo)準(zhǔn)
MPEG-4碼流主要包括基本碼流和系統(tǒng)流尺迂,基本碼流包括音視頻和場景描述的編碼流表示笤妙,每個(gè)基本碼流只包含一種數(shù)據(jù)類型,并通過各自的解碼器解碼噪裕。系統(tǒng)流則指定了根據(jù)編碼視聽信息和相關(guān)場景描述信息產(chǎn)生交互方式的方法蹲盘,并描述其交互通信系統(tǒng)。MP4也可以理解成一種視頻的封裝格式
視頻封裝格式膳音,簡稱視頻格式召衔,相當(dāng)于一種儲(chǔ)存視頻信息的容器,它里面包含了封裝視頻文件所需要的視頻信息祭陷、音頻信息和相關(guān)的配置信息(比如:視頻和音頻的關(guān)聯(lián)信息苍凛、如何解碼等等)。一種視頻封裝格式的直接反映就是對(duì)應(yīng)著相應(yīng)的視頻文件格式兵志。
常見的封裝格式有如下:
封裝格式:就是將已經(jīng)編碼壓縮好的視頻數(shù)據(jù) 和音頻數(shù)據(jù)按照一定的格式放到一個(gè)文件中.這個(gè)文件可以稱為容器. 當(dāng)然可以理解為這只是一個(gè)外殼.
通常我們不僅僅只存放音頻數(shù)據(jù)和視頻數(shù)據(jù),還會(huì)存放 一下視頻同步的元數(shù)據(jù).例如字幕.這多種數(shù)據(jù)會(huì)不同的程序來處理,但是它們?cè)趥鬏敽痛鎯?chǔ)的時(shí)候,這多種數(shù)據(jù)都是被綁定在一起的.
- 常見的視頻容器格式:
- AVI: 是當(dāng)時(shí)為對(duì)抗quicktime格式(mov)而推出的醇蝴,只能支持固定CBR恒定定比特率編碼的聲音文件
- MOV:是Quicktime封裝
- WMV:微軟推出的,作為市場競爭
- mkv:萬能封裝器想罕,有良好的兼容和跨平臺(tái)性悠栓、糾錯(cuò)性,可帶外掛字幕
- flv: 這種封裝方式可以很好的保護(hù)原始地址按价,不容易被下載到惭适,目前一些視頻分享網(wǎng)站都采用這種封裝方式
- MP4:主要應(yīng)用于mpeg4的封裝,主要在手機(jī)上使用俘枫。
- 視頻編解碼方式
視頻編解碼的過程是指對(duì)數(shù)字視頻進(jìn)行壓縮或解壓縮的一個(gè)過程.
在做視頻編解碼時(shí)腥沽,需要考慮以下這些因素的平衡:視頻的質(zhì)量、用來表示視頻所需要的數(shù)據(jù)量(通常稱之為碼率)鸠蚪、編碼算法和解碼算法的復(fù)雜度今阳、針對(duì)數(shù)據(jù)丟失和錯(cuò)誤的魯棒性(Robustness)师溅、編輯的方便性、隨機(jī)訪問盾舌、編碼算法設(shè)計(jì)的完美性墓臭、端到端的延時(shí)以及其它一些因素。
- 常見視頻編碼方式:
- H.26X 系列妖谴,由國際電傳視訊聯(lián)盟遠(yuǎn)程通信標(biāo)準(zhǔn)化組織(ITU-T)主導(dǎo)窿锉,包括 H.261、H.262膝舅、H.263嗡载、H.264、H.265
- H.261仍稀,主要用于老的視頻會(huì)議和視頻電話系統(tǒng)洼滚。是第一個(gè)使用的數(shù)字視頻壓縮標(biāo)準(zhǔn)。實(shí)質(zhì)上說技潘,之后的所有的標(biāo)準(zhǔn)視頻編解碼器都是基于它設(shè)計(jì)的遥巴。
- H.262,等同于 MPEG-2 第二部分享幽,使用在 DVD铲掐、SVCD 和大多數(shù)數(shù)字視頻廣播系統(tǒng)和有線分布系統(tǒng)中。
- H.263值桩,主要用于視頻會(huì)議摆霉、視頻電話和網(wǎng)絡(luò)視頻相關(guān)產(chǎn)品。在對(duì)逐行掃描的視頻源進(jìn)行壓縮的方面颠毙,H.263 比它之前的視頻編碼標(biāo)準(zhǔn)在性能上有了較大的提升斯入。尤其是在低碼率端,它可以在保證一定質(zhì)量的前提下大大的節(jié)約碼率蛀蜜。
- H.264刻两,等同于 MPEG-4 第十部分,也被稱為高級(jí)視頻編碼(Advanced Video Coding滴某,簡稱 AVC)磅摹,是一種視頻壓縮標(biāo)準(zhǔn),一種被廣泛使用的高精度視頻的錄制霎奢、壓縮和發(fā)布格式户誓。該標(biāo)準(zhǔn)引入了一系列新的能夠大大提高壓縮性能的技術(shù),并能夠同時(shí)在高碼率端和低碼率端大大超越以前的諸標(biāo)準(zhǔn)幕侠。
- H.265帝美,被稱為高效率視頻編碼(High Efficiency Video Coding,簡稱 HEVC)是一種視頻壓縮標(biāo)準(zhǔn)晤硕,是 H.264 的繼任者悼潭。HEVC 被認(rèn)為不僅提升圖像質(zhì)量庇忌,同時(shí)也能達(dá)到 H.264 兩倍的壓縮率(等同于同樣畫面質(zhì)量下比特率減少了 50%),可支持 4K 分辨率甚至到超高畫質(zhì)電視舰褪,最高分辨率可達(dá)到 8192×4320(8K 分辨率)皆疹,這是目前發(fā)展的趨勢。
- MPEG 系列占拍,由國際標(biāo)準(zhǔn)組織機(jī)構(gòu)(ISO)下屬的運(yùn)動(dòng)圖象專家組(MPEG)開發(fā)略就。
- MPEG-1 第二部分,主要使用在 VCD 上晃酒,有些在線視頻也使用這種格式表牢。該編解碼器的質(zhì)量大致上和原有的 VHS 錄像帶相當(dāng)。
- MPEG-2 第二部分贝次,等同于 H.262初茶,使用在 DVD、SVCD 和大多數(shù)數(shù)字視頻廣播系統(tǒng)和有線分布系統(tǒng)中浊闪。
- MPEG-4 第二部分,可以使用在網(wǎng)絡(luò)傳輸螺戳、廣播和媒體存儲(chǔ)上搁宾。比起 MPEG-2 第二部分和第一版的 H.263,它的壓縮性能有所提高倔幼。
- MPEG-4 第十部分盖腿,等同于 H.264,是這兩個(gè)編碼組織合作誕生的標(biāo)準(zhǔn)损同。
可以把「視頻封裝格式」看做是一個(gè)裝著視頻翩腐、音頻、「視頻編解碼方式」等信息的容器趟章。一種「視頻封裝格式」可以支持多種「視頻編解碼方式」摹察,比如:QuickTime File Format(.MOV) 支持幾乎所有的「視頻編解碼方式」酸纲,MPEG(.MP4) 也支持相當(dāng)廣的「視頻編解碼方式」。當(dāng)我們看到一個(gè)視頻文件名為 test.mov 時(shí)等龙,我們可以知道它的「視頻文件格式」是 .mov,也可以知道它的視頻封裝格式是 QuickTime File Format伶贰,
但是無法知道它的「視頻編解碼方式」蛛砰。那比較專業(yè)的說法可能是以 A/B 這種方式,A 是「視頻編解碼方式」黍衙,B 是「視頻封裝格式」泥畅。比如:一個(gè) H.264/MOV 的視頻文件,它的封裝方式就是 QuickTime File Format琅翻,編碼方式是 H.264
在這里機(jī)器人里面錄制視頻時(shí)采用H.264/mp4位仁,所以這里我這邊實(shí)現(xiàn)的邊下邊播方案里面也是針對(duì)的這種H.264視頻編解碼方式的mp4容器格式的視頻文件柑贞。
H264最大的優(yōu)勢,具有很高的數(shù)據(jù)壓縮比率,在同等圖像質(zhì)量下,H264的壓縮比是MPEG-2的2倍以上,MPEG-4的1.5~2倍.
原始文件的大小如果為88GB,采用MPEG-2壓縮標(biāo)準(zhǔn)壓縮后變成3.5GB障癌,壓縮比為25∶1凌外,而采用H.264壓縮標(biāo)準(zhǔn)壓縮后變?yōu)?79MB,從88GB到879MB涛浙,H.264的壓縮比達(dá)到驚人的102∶1
- 正常我們機(jī)器人采集到視頻流數(shù)據(jù)后康辑,經(jīng)過H264硬編碼,或者FFmpeg處理的H264軟編碼方式,將YUV4:2:0的數(shù)據(jù)進(jìn)行H264編碼后得到編碼后的H264流數(shù)據(jù)轿亮。
- 我們?cè)贗OS播放時(shí)疮薇,其實(shí)也是拿到這種一幀一幀的H264流數(shù)據(jù),然后進(jìn)行硬解碼或FFmpeg軟解碼我注。(硬解碼在IOS里面是有VideoToolBox框架里面的API可以實(shí)現(xiàn)按咒,軟解碼需要使用FFmpeg里的H264解碼器)。解碼后我們得到原始裸數(shù)據(jù)YUV數(shù)據(jù)但骨,然后我們將YUV數(shù)據(jù)轉(zhuǎn)換為RGB數(shù)據(jù)励七,借助OpenGL ES或Metal 以紋理渲染的方式,將圖像顯示在View的 Layer上奔缠。
- 其實(shí)這些解碼掠抬,播放相關(guān)底層代碼都被我們的AVFoundation框架里面的AVPlayer 封裝了,沒有暴露這些細(xì)節(jié)給我校哎,我們只需要傳遞一個(gè)URL 就可以實(shí)現(xiàn)視頻播放功能两波。
為了更好的理解播放視頻的原理,我這里還簡單介紹一下H264編解碼的相關(guān)知識(shí)
6.2 H264簡介
-
H264的碼流結(jié)構(gòu):H264視頻壓縮后會(huì)成為一個(gè)序列幀.幀里包含圖像,圖像分為很多片.每個(gè)片可以分為宏塊.每個(gè)宏塊由許多子塊組成闷哆,如下圖:
H264的碼流結(jié)構(gòu)
H264結(jié)構(gòu)中腰奋,一個(gè)視頻圖像編碼后的數(shù)據(jù)叫做一幀,一幀由一個(gè)片(slice)或多個(gè)片組成抱怔,一個(gè)片由一個(gè)或多個(gè)宏塊(MB)組成劣坊,一個(gè)宏塊由16x16的yuv數(shù)據(jù)組成。宏塊作為H264編碼的基本單位屈留。
場和幀:視頻的一場或一幀可用來產(chǎn)生一個(gè)編碼圖像讼稚。在電視中,為減少大面積閃爍現(xiàn)象绕沈,把一幀分成兩個(gè)隔行的場锐想。
片:每個(gè)圖象中,若干宏塊被排列成片的形式乍狐。片分為I片赠摇、B片、P片和其他一些片。
- I片只包含I宏塊藕帜,P片可包含P和I宏塊烫罩,而B片可包含B和I宏塊。
- I宏塊利用從當(dāng)前片中已解碼的像素作為參考進(jìn)行幀內(nèi)預(yù)測洽故。
- P宏塊利用前面已編碼圖象作為參考圖象進(jìn)行幀內(nèi)預(yù)測贝攒。
- B宏塊則利用雙向的參考圖象(前一幀和后一幀)進(jìn)行幀內(nèi)預(yù)測。
- 片的目的是為了限制誤碼的擴(kuò)散和傳輸时甚,使編碼片相互間是獨(dú)立的隘弊。
-
H264碼流分層結(jié)構(gòu)圖:
H264碼流分層結(jié)構(gòu)圖
A Annex格式數(shù)據(jù),就是起始碼+Nal Unit 數(shù)據(jù)
NAL Unit: NALU 頭+NALU數(shù)據(jù)
NALU 主體,是由切片組成.切片包括切片頭+切片數(shù)據(jù)
Slice數(shù)據(jù): 宏塊組成
PCM類: 宏塊類型+pcm數(shù)據(jù),或者宏塊類型+宏塊模式+殘差數(shù)據(jù)
Residual: 殘差塊.
-
NAL 單元是由一個(gè)NALU頭部+一個(gè)切片.切片又可以細(xì)分成"切片頭+切片數(shù)據(jù)".我們之間了解過一個(gè)H254的幀是由多個(gè)切片構(gòu)成的.因?yàn)橐粠瑪?shù)據(jù)一次有可能傳不完. 如下圖:
NAL 單元 -
切片與宏塊的關(guān)系(Slice & MacroBlock)
每個(gè)切片都包括切片頭+切片數(shù)據(jù). 那每個(gè)切片數(shù)據(jù)包括了很多宏塊.每個(gè)宏塊包括了宏塊的類型,宏塊的預(yù)測,殘差數(shù)據(jù). 如下圖:
切片與宏塊的關(guān)系
而我們?cè)谝桓眽嚎s的H264的幀里,可以包含多個(gè)切片.至少有一個(gè)切片,如下圖:
了解了上面關(guān)于H264碼流的一些基本概念后荒适,我們就能更好的理解H264編碼解碼的原理梨熙,以及圖像渲染,視頻播放器的實(shí)現(xiàn)原理刀诬。
在H264解碼的過程中會(huì)涉及到一幀幀的數(shù)據(jù)咽扇,這里有I幀,P幀陕壹,B幀质欲,三個(gè)概念。
- I幀: 關(guān)鍵幀,采用幀內(nèi)壓縮技術(shù).
舉個(gè)例子,如果攝像頭對(duì)著你拍攝,1秒之內(nèi),實(shí)際你發(fā)生的變化是非常少的.1秒鐘之內(nèi)實(shí)際少很少有大幅度的變化.攝像機(jī)一般一秒鐘會(huì)抓取幾十幀的數(shù)據(jù).比如像動(dòng)畫,就是25幀/s,一般視頻文件都是在30幀/s左右.對(duì)于一些要求比較高的,對(duì)動(dòng)作的精細(xì)度有要求,想要捕捉到完整的動(dòng)作的,高級(jí)的攝像機(jī)一般是60幀/s.那些對(duì)于一組幀的它的變化很小.為了便于壓縮數(shù)據(jù),那怎么辦了?將第一幀完整的保存下來.如果沒有這個(gè)關(guān)鍵幀后面解碼數(shù)據(jù),是完成不了的.所以I幀特別關(guān)鍵.
- P幀: 向前參考幀.壓縮時(shí)只參考前一個(gè)幀.屬于幀間壓縮技術(shù).
視頻的第一幀會(huì)被作為關(guān)鍵幀完整保存下來.而后面的幀會(huì)向前依賴.也就是第二幀依賴于第一個(gè)幀.后面所有的幀只存儲(chǔ)于前一幀的差異.這樣就能將數(shù)據(jù)大大的減少.從而達(dá)到一個(gè)高壓縮率的效果.
- B幀: 雙向參考幀,壓縮時(shí)即參考前一幀也參考后一幀.幀間壓縮技術(shù).
- B幀,即參考前一幀,也參考后一幀.這樣就使得它的壓縮率更高.存儲(chǔ)的數(shù)據(jù)量更小.如果B幀的數(shù)量越多,你的壓縮率就越高.這是B幀的優(yōu)點(diǎn),但是B幀最大的缺點(diǎn)是,如果是實(shí)時(shí)互動(dòng)的直播,那時(shí)與B幀就要參考后面的幀才能解碼,那在網(wǎng)絡(luò)中就要等待后面的幀傳輸過來.這就與網(wǎng)絡(luò)有關(guān)了.如果網(wǎng)絡(luò)狀態(tài)很好的話,解碼會(huì)比較快,如果網(wǎng)絡(luò)不好時(shí)解碼會(huì)稍微慢一些.丟包時(shí)還需要重傳.對(duì)實(shí)時(shí)互動(dòng)的直播,一般不會(huì)使用B幀.
我們實(shí)時(shí)播放視頻時(shí)糠馆,每次從服務(wù)器請(qǐng)求一個(gè)Range范圍的視頻幀把敞,實(shí)際上服務(wù)器是返回一組組的H264幀數(shù)據(jù),一組幀數(shù)據(jù)又稱為GOF(Group of Frame),GOF 表示:一個(gè)I幀到下一個(gè)I幀.這一組的數(shù)據(jù).包括B幀/P幀. 如下圖所示:
在H264碼流中榨惠,我們使用SPS/PPS來存儲(chǔ)GOP的參數(shù)。
SPS 序列參數(shù)集 :全稱是Sequence Parameter Set,序列參數(shù)集存放幀數(shù),參考幀數(shù)目,解碼圖像尺寸,幀場編碼模式選擇標(biāo)識(shí)等.
PPS 圖像參數(shù)集:全稱是Picture Parameter Set,圖像參數(shù)集.存放編碼模式選擇標(biāo)識(shí),片組數(shù)目,初始量化參數(shù)和去方塊濾波系數(shù)調(diào)整標(biāo)識(shí)等.(與圖像相關(guān)的信息)
在一組幀之前我們首先收到的是SPS/PPS數(shù)據(jù).如果沒有這組參數(shù)的話,我們是無法解碼. 之前WebRTC視頻的時(shí)候遇到的一個(gè)問題就是:IOS端有時(shí)候圖傳的時(shí)候黑屏盛霎,這個(gè)原因就是因?yàn)镮幀缺少SPS/PPS信息赠橙,導(dǎo)致解碼失敗,導(dǎo)致的黑屏愤炸。
如果我們?cè)诮獯a時(shí)發(fā)生錯(cuò)誤,首先要檢查是否有SPS/PPS.如果沒有,是因?yàn)閷?duì)端沒有發(fā)送過來還是因?yàn)閷?duì)端在發(fā)送過程中丟失了.
SPS/PPS
數(shù)據(jù),我們也把其歸類到I幀.這2組數(shù)據(jù)是絕對(duì)不能丟的.視頻花屏期揪,卡頓的原因分析:
我們?cè)谟^看視頻時(shí),會(huì)遇到花屏或者卡頓現(xiàn)象.那這個(gè)與我們剛剛所講的GOF就息息相關(guān)了
- 如果GOP分組中的P幀丟失就會(huì)造成解碼端的圖像發(fā)生錯(cuò)誤.解碼錯(cuò)誤時(shí),我們把解碼失敗的圖片用來展示了规个,就導(dǎo)致我們看到的花屏現(xiàn)象
- 為了避免花屏問題的發(fā)生,一般如果發(fā)現(xiàn)P幀或者I幀丟失.就不顯示本GOP內(nèi)的所有幀.只到下一個(gè)I幀來后重新刷新圖像.
- 當(dāng)這時(shí)因?yàn)闆]有刷新屏幕.丟包的這一組幀全部扔掉了.圖像就會(huì)卡在哪里不動(dòng).這就是卡頓的原因.
- 所以總結(jié)起來,花屏是因?yàn)槟銇G了P幀或者I幀.導(dǎo)致解碼錯(cuò)誤. 而卡頓是因?yàn)闉榱伺禄ㄆ?將整組錯(cuò)誤的GOP數(shù)據(jù)扔掉了.直達(dá)下一組正確的GOP再重新刷屏.而這中間的時(shí)間差,就是我們所感受的卡頓.
- 軟編碼與硬編碼
- 硬編碼: 使用非CPU進(jìn)行編碼,例如使用GPU芯片處理
- 性能高凤薛,低碼率下通常質(zhì)量低于硬編碼器,但部分產(chǎn)品在GPU硬件平臺(tái)移植了優(yōu)秀的軟編碼算法(如X264)的诞仓,質(zhì)量基本等同于軟編碼缤苫。
- 硬編碼,就是使用GPU計(jì)算,獲取數(shù)據(jù)結(jié)果,優(yōu)點(diǎn)速度快,效率高.
- 在IOS平臺(tái)針對(duì)視頻硬編碼使用
VideoToolBox
框架,針對(duì)音頻硬編碼使用AudioToolBox
框架
- 軟編碼: 使用CPU來進(jìn)行編碼計(jì)算.
- 實(shí)現(xiàn)直接墅拭、簡單活玲,參數(shù)調(diào)整方便,升級(jí)易,但CPU負(fù)載重舒憾,性能較硬編碼低镀钓,低碼率下質(zhì)量通常比硬編碼要好一點(diǎn)。
- 軟編碼,就是通過CPU來計(jì)算,獲取數(shù)據(jù)結(jié)果.
- 在IOS平臺(tái)針對(duì)視頻軟編碼一般使用
FFmpeg,X264
算法把視頻原數(shù)據(jù)YUV/RGB編碼成H264镀迂。針對(duì)音頻使用fdk_aac
將音頻數(shù)據(jù)PCM轉(zhuǎn)換成AAC丁溅。
如果想更加深入的探索播放器的底層原理,可以參考這兩款開源的播放器:
ijkplayer,kxmovie 他們都是基于FFmpeg框架封裝的
-
ijkplayer是bilibili出品的一款基于FFmpeg的視頻播放器探遵,在git上面已經(jīng)有25.7k的星星了窟赏,非常強(qiáng)大,值得深入研究别凤,這個(gè)包含ios,和android端的饰序。
ijkplayer -
kxmovie 在git上面也有2.7k的星星,這是實(shí)力的認(rèn)證规哪,值得學(xué)習(xí)求豫,研究。
kxmovie
6.3 MP4 格式
MP4(MPEG-4 Part 14)
是一種常見的多媒體容器格式诉稍,它是在“ISO/IEC 14496-14”標(biāo)準(zhǔn)文件中定義的蝠嘉,屬于MPEG-4的一部分,是“ISO/IEC 14496-12(MPEG-4 Part 12 ISO base media file format)”標(biāo)準(zhǔn)中所定義的媒體格式的一種實(shí)現(xiàn)杯巨,后者定義了一種通用的媒體文件結(jié)構(gòu)標(biāo)準(zhǔn)蚤告。MP4是一種描述較為全面的容器格式,被認(rèn)為可以在其中嵌入任何形式的數(shù)據(jù)服爷,各種編碼的視頻杜恰、音頻等都不在話下,不過我們常見的大部分的MP4文件存放的AVC(H.264)
或MPEG-4(Part 2)編碼的視頻和AAC編碼的音頻仍源。MP4格式的官方文件后綴名是“.mp4”心褐,還有其他的以mp4為基礎(chǔ)進(jìn)行的擴(kuò)展或者是縮水版本的格式,包括:M4V,3GP,F4V等笼踩。
首先看一下軟件對(duì)于mp4文件的解析如下圖所示:
從上圖圖6.3.1 中可以看出這個(gè)視頻文件第一層有4部分逗爹,每一部分都是一個(gè)
box
,分別為:ftype嚎于,moov掘而,free,mdat
于购。其實(shí)mp4
文件是有許多的box
組成的袍睡。如下圖6.3.2 所示:box
的基本結(jié)構(gòu)如下圖6.3.3所示,其中肋僧,size
指明了整個(gè)box所占用的大小女蜈,包括header
部分持舆,type
指明了box
的類型。如果box
很大(例如存放具體視頻數(shù)據(jù)的mdat box
)伪窖,超過了uint32的最大數(shù)值逸寓,size
就被設(shè)置為1,并用接下來的8位uint64來存放大小覆山。
一個(gè)mp4
文件有可能包含非常多的box
竹伸,在很大程度上增加了解析的復(fù)雜性,這個(gè)網(wǎng)頁上http://mp4ra.org/atoms.html記錄了一些當(dāng)前注冊(cè)過的box
類型簇宽⊙ǎ看到這么多box
,如果要全部支持魏割,一個(gè)個(gè)解析譬嚣,怕是頭都要爆了。還好钞它,大部分mp4文件沒有那么多的box類型拜银,下圖就是一個(gè)簡化了的,常見的mp4文件結(jié)構(gòu)如下圖6.3.4所示
一般來說遭垛,解析媒體文件尼桶,最關(guān)心的部分是視頻文件的寬高、時(shí)長锯仪、碼率泵督、編碼格式、幀列表庶喜、關(guān)鍵幀列表小腊,以及所對(duì)應(yīng)的時(shí)戳和在文件中的位置,這些信息久窟,在mp4中秩冈,是以特定的算法分開存放在
stbl box
下屬的幾個(gè)box
中的,需要解析stbl
下面所有的box
瘸羡,來還原媒體信息。下表是對(duì)于以上幾個(gè)重要的box
存放信息的說明:6.4 IOS 原始API實(shí)現(xiàn) 將mp4文件的 moov的box移到前面
上面已經(jīng)講解過使用FFmpeg里面的 qt-faststart下載工具可以實(shí)現(xiàn)將mp4文件的 moov的box移到前面搓茬,從而讓mp4文件支持邊下邊播功能犹赖。下面將介紹一種通過IOS原始代碼的方式實(shí)現(xiàn)將mp4文件的moov的box從文件最后面移到前面。
不過這種方式一般用不到卷仑,一是因?yàn)樾蕟栴}峻村,而是一般實(shí)現(xiàn)邊下邊播,都是由服務(wù)器端去完成這種事情锡凝。
具體代碼如下:
- (NSData*)exchangestco:(NSMutableData*) moovdata{
int i, atom_size, offset_count, current_offset;
NSString*atom_type;
longlongmoov_atom_size = moovdata.length;
Byte*buffer = (Byte*)malloc(5);
buffer[4] =0;
Byte*buffer01 = (Byte*)malloc(moov_atom_size);
[moovdatagetBytes:buffer01 length:moov_atom_size];
for(i =4; i < moov_atom_size -4; i++) {
NSRangerange;
range.location= I;
range.length=4;
[moovdatagetBytes:buffer range:range];
atom_type = [selftosType:buffer];
if([atom_typeisEqualToString:@"stco"]) {
range.location= i-4;
range.length =4;
[moovdatagetBytes:bufferrange:range];
atom_size = [selftoSize:buffer];
if(i + atom_size -4> moov_atom_size) {
WBLog(LOG_ERROR,@"error i + atom_size - 4 > moov_atom_size");
returnnil;
}
range.location= I+8;
range.length=4;
[moovdatagetBytes:bufferrange:range];
offset_count = [selftoSize:buffer];
for(intj =0; j < offset_count; j++) {
range.location= i +12+ j *4;
range.length=4;
[moovdatagetBytes:bufferrange:range];
current_offset= [selftoSize:buffer];
current_offset += moov_atom_size;
buffer01[i +12+ j *4+0] = (Byte) ((current_offset >>24) &0xFF);
buffer01[i +12+ j *4+1] = (Byte) ((current_offset >>16) &0xFF);
buffer01[i +12+ j *4+2] = (Byte) ((current_offset >>8) &0xFF);
buffer01[i +12+ j *4+3] = (Byte) ((current_offset >>0) &0xFF);
}
i += atom_size -4;
}
elseif([atom_typeisEqualToString:@"co64"]) {
range.location= i-4;
range.length=4;
[moovdatagetBytes:bufferrange:range];
atom_size = [selftoSize:buffer];
if(i + atom_size -4> moov_atom_size) {
WBLog(LOG_ERROR,@"error i + atom_size - 4 > moov_atom_size");
returnnil;
}
range.location= I+8;
range.length=4;
[moovdatagetBytes:bufferrange:range];
offset_count = [selftoSize:buffer];
for(intj =0; j < offset_count; j++) {
range.location= i +12+ j *8;
range.length=4;
[moovdatagetBytes:bufferrange:range];
current_offset = [selftoSize:buffer];
current_offset += moov_atom_size;
buffer01[i +12+ j *8+0] = (Byte)((current_offset >>56) &0xFF);
buffer01[i +12+ j *8+1] = (Byte)((current_offset >>48) &0xFF);
buffer01[i +12+ j *8+2] = (Byte)((current_offset >>40) &0xFF);
buffer01[i +12+ j *8+3] = (Byte)((current_offset >>32) &0xFF);
buffer01[i +12+ j *8+4] = (Byte)((current_offset >>24) &0xFF);
buffer01[i +12+ j *8+5] = (Byte)((current_offset >>16) &0xFF);
buffer01[i +12+ j *8+6] = (Byte)((current_offset >>8) &0xFF);
buffer01[i +12+ j *8+7] = (Byte)((current_offset >>0) &0xFF);
}
i += atom_size -4;
}
}
NSData*moov = [NSDatadataWithBytes:buffer01length:moov_atom_size];
free(buffer);
free(buffer01);
returnmoov;
}
參考:http://www.reibang.com/p/0188ab0381ba
http://www.reibang.com/p/bb925a4a9180
https://www.cnblogs.com/ios4app/p/6928806.html
http://www.reibang.com/p/990ee3db0563