IOS音視頻(四十五)HTTPS 自簽名證書 實現邊下邊播

IOS音視頻(一)AVFoundation核心類html

IOS音視頻(二)AVFoundation視頻捕捉android

IOS音視頻(三)AVFoundation 播放和錄音ios

IOS音視頻(四十三)AVFoundation 之 Audio Sessiongit

IOS音視頻(四十四)AVFoundation 之 Audio Queue Servicesgithub

IOS音視頻(四十五)HTTPS 自簽名證書 實現邊下邊播算法

IOS音視頻(四十六)離線在線語音識別方案shell

1. 邊下邊播概述

因爲JimuPro相冊裏面獲取視頻,須要將視頻所有下載到本地後才能播放,若是視頻文件很大,則用戶須要等待很長時間才能看到視頻,這種體驗效果不太友好,針對這個問題,須要IOS app端實現邊下邊播功能,使用一份數據流,完成觀看視頻的同事將視頻保存到本地,等視頻播放完成後,視頻也就下載到了本地。下載完成後的視頻格式是.mp4格式,導出來能夠直接播放。當用戶第二次觀看次視頻時,將不從機器人端獲取視頻,直接讀取本地緩存的視頻,也就是離線也能夠觀看。swift

實現邊下邊播的方式,能夠節省數據流量,實時觀看到機器人端錄製的視頻,能夠拖拽的方式觀看。api

這個功能知足如下需求:數組

  • 支持正常播放器的一切功能,包括暫停、播放和拖拽。能夠播放本地緩存的視頻,也能夠實時播放機器人端錄製的視頻。
  • 若是視頻加載完成且完整,將視頻文件保存到本地cache,下一次播放本地cache中的視頻,再也不請求網絡數據。
  • 若是視頻沒有加載完(半路關閉或者拖拽),下次播放時,先從緩存中播放已經緩存的視頻,並同時開啓下載功能,從上次的視頻末尾繼續下載剩下的部分。
  • 因爲機器人端採用HTTPS + 自簽名證書的方式,實時播放視頻須要解決證書信任問題。

2. 邊下邊播實現方案

  • IOS客戶端實現邊下邊播的方案有不少,目前我研究的找到3種解決方案。下面將詳細介紹3種方案的實現原理。因爲JimuPro裏面已經用到了開源的播放器:VGPlayer。這個播放器裏面基本上實現了方案三的細節問題。只是沒有實現HTTPS 自簽名證書認證的問題。

  • IOS項目中我推薦使用第三種方案實現邊下邊播功能。

2.1 方案一

  • 經過解析mp4的格式,將mp4的數據直接下載並寫入文件,而後讓播放器直接播放的是本地的視頻文件;

此方案是先下載視頻到本地文件,而後把本地視頻文件地址傳給播放器,播放器實際播放的是本地文件。當播放器的播放進度大於當前的可播放的下載緩存進度,則暫停播放,等緩存到足夠播放時間以後,再讓播放器開始播放。這種方案的下載方式是與播放器徹底沒有關係的,只是順序的將服務器下發的視頻數據寫入本地文件,而後讓播放器來讀取數據。

先下載而後直接播放本地文件
以mp4文件爲例,經過解析mp4的格式,將mp4的數據直接下載並寫入文件,而後讓播放器直接播放的是本地的視頻文件;以下圖:
將mp4的數據直接下載並寫入文件
這種方式雖然可以知足緩存播放這個需求,可是會產生不少問題,例如視頻下載到本地,下載多少才能夠把本地文件做爲視頻源傳給播放器即視頻開啓播放速度;播放的速度大於下載速度的話,該怎麼辦?若是播放器seek到文件沒有緩存的位置,應該怎麼處理?對於視頻關閉以後,第二次進入如何知道已經下載了多少?等等問題。

目前的已有解決方案是,當緩存到500kb才把緩存的地址傳給播放器,視頻文件小於500kb則下載完以後再播放,起播慢(須要改進)。當下載進度比播放進度多5秒的數據量才讓播放器播放,否則的話就暫停。若是seek到沒有緩存的地方就切換到網絡上中止當前的下載,浪費一些流量。每次下載都會保存一份配置文件,來保存是否下載完成,沒下載完成則第二次根據當前緩存文件大小,從新開始順序下載。

總的來講第一種方案有以下缺點:

  1. 用戶播放視頻的時候可能等待的時間較長(起播
  2. 流量浪費(seek以後會播網絡流,中止下載)
  3. 須要太多控制視頻播放的邏輯來進行輔助,與播放器代碼耦合嚴重。
  4. seek以後切源會耗時,每次seek比較慢

2.2 方案二

  • 使用的本地代理服務器的方式: 在服務器端(機器人端)支持分片下載的方式下,APP內置一個HTTPServer代理服務器,代理服務器實現將數據緩存到本地,同時App的播放器之間重代理服務器獲取播放數據。這種實現方式比較複雜一點,若是處理很差,容易致使crash的問題。

這個代理服務器也能夠作在機器人端,一個接口用於播放,一個接口用於下載。

代理服務器方式實現邊下邊播

使用 HTTPServer,在本地開啓一個 http 服務器,把須要緩存的請求地址指向本地服務器,並帶上真正的 url 地址。HTTPServer 無論咱們有沒有使用緩存功能,都要在應用打開的時候默默開啓,對APP性能是一大損耗。而且咱們引入 HTTPServer 庫也會增長一些包體積。

2.2.1 技術要點

此方案的特色以下:

  1. 經過代理服務器,從socket截取播放器請求數據;
  2. 根據截取的range信息,從網絡服務器請求視頻數據;
  3. 視頻數據寫入本地文件,seek後能夠從seek位置繼續寫入並播放;
  4. 邊下邊播,加快播放速度;
  5. 與播放器邏輯徹底解耦,對於播放器只是一個地址

本方案是在播放器與視頻源服務器之間加一層代理服務器,截取視頻播放器發送的請求,根據截取的請求,向網絡服務器請求數據,而後寫到本地。本地代理服務器從文件中讀取數據併發送給播放器進行播放. 以下圖所示:

邊下邊播-代理服務器模式流程

如上圖,具體流程細節以下:

  1. 啓動本地代理服務器。
  2. 視頻源地址傳給本地代理服務器。
  3. 將視頻源地址轉換成本地代理服務器的地址做爲播放器的視頻源地址。
  4. 播放器向本地代理服務器發送請求。
  5. 本地代理服務器截取這個請求,再根據解析出來請求的信息向真正的服務器發起請求。
  6. 本地代理服務器開始接受數據,寫入文件並將文件數據再返回到播放器。
  7. 播放器接收到這些數據以後播放。
  8. seek以後從新進行以上步驟。

上面流程主要描述了代理服務器實現的實時播放流程,下面重點探討一下代理服務器的下載流程。

  • 下載流程實現

考慮到播放視頻的時候,用戶會拖動進度條進行seek,而此時須要從用戶拖動的位置進行下載,這樣會讓視頻文件產生許多的空洞,以下圖所示:

圖1-seek文件
爲了節省流量,只會下載文件中沒有數據的部分,也就是上圖1藍色的部分。所以須要存儲下載的片斷信息。目前採用的數據結構以下所示:

fragment = [start,end];
array = [fragment 0,fragment 1,fragment 2,fragment 3];
複製代碼
  1. 其中fragment指的是下載的片斷,start指的是片斷開始的位置,end爲片斷的結束位置。
  2. array指的是存儲fragment的數組,數組中的fragment是依靠start從小到大來來插入到數組中的,保證了數組的有序性。
  3. 下載的片斷是記錄在一個數組中:array = [fragment0 ,fragment 1,fragment 2,fragment 3];

下載共分爲兩個階段:seek階段補洞階段

  • seek階段:即爲在播放的時候,根據用戶seek的位置來進行下載。

根據seek到的位置分爲兩種狀況:

  • 狀況一:若是seek到的位置是在已有的片斷中(例如圖中的seek1的位置,該處有數據),就從該片斷(fragment1)的末尾請求數據(end1),直到下個片斷的開始位置處(fragment2start),也就是向服務器請求的range爲:rang1 = (end1 ) —— start2; 這個片斷下載完成後,假如把下載的片斷記爲fragment1.1,則會把fragment1fragment1.1fragment2合爲一個片斷爲fragment1-2,則array = [fragment 0,fragement1-2,frament3];此次下載後的狀態圖2所示:

    圖2--狀況一
    接下來一直下載直到array = [fragment 0,fragement1-3];以後會判斷fragement1-3有沒有到文件末尾,若是到了就下載結束,若是沒到就從從fragement3的(end3)開始下載直到文件末尾。

  • 狀況二:若是seek到的位置沒有在已有的片斷中,(例如說是在圖1中的seek2的位置),就從seek到的位置開始下載數據直到下一個片斷的startfragment2start2),假如這個片斷記爲fragment1.1,則會把fragment1.1fragment2合併即數組爲:array= [fragment 0,fragment1,fagment1.1-2,fragment3]; 合併後的狀況以下圖3所示:接下來的操做就是繼續下載,直到下載到文件末尾;

    圖3--狀況二
    若是片斷過小保存起來就會讓播放器下次播放的時候多發送一次請求,這樣是很耗費資源。例如:如上圖3所示,若是fragment1的大小隻有1kb,想要補充fragment0fragment1.1-2之間的數據,就須要發送兩次請求,這樣頻繁的發送請求,比較浪費資源。所以當fragment過小,就不存在配置數組中。這樣會少發一次請求,也不會浪費很大的流量。當下載片斷過小(例如說下載的長度<20KB),就不保存在片斷數組中(爲了控制片斷的粒度)。這樣會產生一個問題,當視頻文件中間有一個空洞小於20KB,這個片斷永遠補不上。這個時候就須要用到第二階段-補洞階段

  • 補洞階段: 第二階段補洞階段,就是第二次播放的時候,若是文件中有空洞,這個時候不論片斷再小,也會存到片斷中。 最後當配置數組中存的數據只剩下最後的{0,length}length爲視頻總長度的時候,表示文件已所有下載完成。

2.3 方案三

對於IOS平臺來講,還有一種更好的方案:使用IOS原生API ,使用 AVAssetResourceLoader,在不改變 AVPlayer API 的狀況下,對播放的音視頻進行緩存。

方案三跟方案二原理差很少,只不過是藉助IOS原始API來實現的。

  • 使用IOS系統自動API 實現視頻邊下邊播功能:

這裏的邊下邊播不是單獨開一個子線程去下載,而是把視頻播放的數據給保存到本地。簡而言之,就是使用一遍的流量,既播放了視頻,也保存了視頻。

具體實現方案以下:

  1. 須要在視頻播放器和服務器之間添加一層相似代理的機制,視頻播放器再也不直接訪問服務器,而是訪問代理對象,代理對象去訪問服務器得到數據,以後返回給視頻播放器,同時代理對象根據必定的策略緩存數據。
  2. AVURLAsset中的resourceLoader能夠實現這個機制,resourceLoader的delegate就是上述的代理對象。
  3. 視頻播放器在開始播放以前首先檢測是本地cache中是否有此視頻,若是沒有才經過代理得到數據,若是有,則直接播放本地cache中的視頻便可。
  4. 若是是用HTTP的方式,上述3步能夠實現邊下邊播功能,若是是HTTPS,服務器證書使用的是證書頒發機構簽名的證書,則也能夠直接跟HTTP方式同樣處理。可是,若是是HTTPS+自簽名證書的方式,則須要在resourceLoader每次方式請求前,先校驗證書,也就是下面的第5步

2.3.1 AVPlayer實現邊下邊播流程

咱們先來參考網上播放QQ音樂邊下邊播流程圖以下:

整個邊下邊播流程圖以下
QQ 音樂實現的緩存策略大體以下:

先觀察並猜想企鵝音樂的緩存策略(固然它不是用AVPlayer播放):   一、開始播放,同時開始下載完整的文件,當文件下載完成時,保存到緩存文件夾中;   二、當seek時    (1)若是seek到已下載到的部分,直接seek成功;(以下載進度60%,seek進度50%)    (2)若是seek到未下載到的部分,則開始新的下載(以下載進度60%,seek進度70%)       PS1:此時文件下載的範圍是70%-100%       PS2:以前已下載的部分就被刪除了       PS3:若是有別的seek操做則重複步驟2,若是此時再seek到進度40%,則會開始新的下載(範圍40%-100%)   三、當開始新的下載以後,因爲文件不完整,下載完成以後不會保存到緩存文件夾中;   四、下次再播放同一歌曲時,若是在緩存文件夾中存在,則直接播放緩存文件;

咱們使用AVPlayer 來實現邊下邊播的大體流程跟上面QQ音樂的緩存機制差很少,就是依賴於AVAssetResourceLoader. 大體流程以下:

AVPlayer播放流程

如上圖所示,咱們簡單描述一下AVPlayer實現邊下邊播的流程:

  1. 當開始播放視頻時,經過視頻url判斷本地cache中是否已經緩存當前視頻,若是有,則直接播放本地cache中視頻
  2. 若是本地cache中沒有視頻,則視頻播放器向代理請求數據
  3. 加載視頻時展現正在加載的提示(菊花轉)
  4. 若是能夠正常播放視頻,則去掉加載提示,播放視頻,若是加載失敗,去掉加載提示並顯示失敗提示
  5. 在播放過程當中若是因爲網絡過慢或拖拽緣由致使沒有播放數據時,要展現加載提示,跳轉到第4步

緩存代理策略:

  1. 當視頻播放器向代理請求dataRequest時,判斷代理是否已經向服務器發起了請求,若是沒有,則發起下載整個視頻文件的請求 2.若是代理已經和服務器創建連接,則判斷當前的dataRequest請求的offset是否大於當前已經緩存的文件的offset,若是大於則取消當前與服務器的請求,並從offset開始到文件尾向服務器發起請求(此時應該是因爲播放器向後拖拽,而且超過了已緩存的數據時纔會出現)
  2. 若是當前的dataRequest請求的offset小於已經緩存的文件的offset,同時大於代理向服務器請求的range的offset,說明有一部分已經緩存的數據能夠傳給播放器,則將這部分數據返回給播放器(此時應該是因爲播放器向前拖拽,請求的數據已經緩存過纔會出現)
  3. 若是當前的dataRequest請求的offset小於代理向服務器請求的range的offset,則取消當前與服務器的請求,並從offset開始到文件尾向服務器發起請求(此時應該是因爲播放器向前拖拽,而且超過了已緩存的數據時纔會出現)
  4. 只要代理從新向服務器發起請求,就會致使緩存的數據不連續,則加載結束後不用將緩存的數據放入本地cache
  5. 若是代理和服務器的連接超時,重試一次,若是仍是錯誤則通知播放器網絡錯誤
  6. 若是服務器返回其餘錯誤,則代理通知播放器網絡錯誤

2.3.2 AVPlayer相關API簡介

IOS 播放網絡視頻咱們通常使用AVFoundation框架裏面的AVPlayer去實現自定義播放器,可是AVPlayer的相關API都是高度封裝的,這樣咱們播放網絡視頻時,每每不能控制其內部播放邏輯,好比咱們會發現播放時seek會失敗,數據加載完畢後不能獲取到數據文件進行其餘操做,所以咱們須要尋找彌補其不足之處的方法,這裏咱們選擇了AVAssetResourceLoader。咱們這裏實現邊下邊播功能也是依賴於它。

先來了解一下AVAssetResourceLoader的做用:讓咱們自行掌握AVPlayer數據的加載,包括獲取AVPlayer須要的數據的信息,以及能夠決定傳遞多少數據給AVPlayer。

咱們大體瞭解一下AVPlayer的組件圖:

AVPlayer組件圖

AVAssetResourceLoader:一個 iOS 6 就被開放出來,專門用來處理 AVAsset 加載的工具。這個徹底知足JimuPro運行在IOS10以上的要求。

AVAssetResourceLoader 有一個AVAssetResourceLoaderDelegate代理,這個代理有兩個重要的接口:

  • 要求加載資源的代理方法,這時咱們須要保存loadingRequest並對其所指定的數據進行讀取或下載操做,當數據讀取或下載完成,咱們能夠對loadingRequest進行完成操做。
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader 
shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest;
複製代碼
  • 取消加載資源的代理方法,這時咱們須要取消loadingRequest所指定的數據的讀取或下載操做。
- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader 
didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest;
複製代碼

咱們只要找一個對象實現了 AVAssetResourceLoaderDelegate 這個協議的方法,丟給 asset,再把 asset 丟給 AVPlayer,AVPlayer 在執行播放的時候就會去問這個 delegate:喂,你能不能播放這個 url 啊?而後會觸發下面這個方法:- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest

咱們在這個方法中看看 request 裏面的 url 是否是咱們支持的,若是能支持就返回 YES!而後就能夠開心的一邊下視頻數據,一邊塞數據給 AVPlayer 讓它顯示視頻畫面。

AVUrlAsset在請求自定義的URLScheme資源的時候會經過AVAssetResourceLoader實例來進行資源請求。它是AVUrlAsset的屬性,聲明以下:var resourceLoader: AVAssetResourceLoader { get }

AVAssetResourceLoader請求的時候會把相關請求(AVAssetResourceLoadingRequest)傳遞給AVAssetResourceLoaderDelegate(若是有實現的話),咱們能夠保存這些請求,而後構造本身的NSUrlRequset來發送請求,當收到響應的時候,把響應的數據設置給AVAssetResourceLoadingRequest,而且對數據進行緩存,就完成了邊下邊播,整個流程大致以下圖:

AVAssetResourceLoaderDelegate實現邊下邊播流程
其中最爲複雜的部分是數據偏移處理,由於數據是分塊下載和分塊填充的,咱們的須要填充的對象是 AVAssetResourceLoadingDataRequest,須要控制好 currentOffset

下面咱們未來詳細的介紹使用AVPlayer和AVAssetResourceLoaderDelegate來實現邊下邊播的具體實現。

3 HTTP邊下邊播 mp4文件 實現細節

目前網上有好多關於IOS邊下邊播的代碼,其實原理都是同樣的,只是實現方式,細節不同,這裏推薦兩個比較好的開源代碼:

  • OC版本:VIMediaCache 目前git上面有642顆星星,至關不錯。
  • Swift版本:VGPlayer 目前git上面有363顆星星,功能也比較完善,這是我比較推薦的。

3.1 邊下邊播原理

邊下邊播的原理已經在上面的3種方案介紹中詳細描述了,這裏主要是基於第三種方案用AVPlayer 來實現邊下邊播。這裏先拋開HTTPS字簽證書的簽名認證問題,先講解基於HTTP方式的邊下邊播,主流程圖以下:

AVPlayer邊下邊播主流程

整個過程就是分爲兩大塊,一塊是實時播放視頻,一塊就是緩存策略下載視頻。

3.1.1 實時播放原理

咱們先來看第一塊,實時播放視頻(先無論下載和緩存),實現上,咱們能夠分爲兩步:

  1. 須要知道如何請求數據,url 是什麼,下載多少數據。
  2. 下載好的數據怎麼塞給 AVPlayer

3.1.1.1 請求數據

在上面的回調方法中,會獲得一個 AVAssetResourceLoadingRequest 對象,它裏面的屬性和方法很少,爲了減小干擾,我精簡了一下這個類的頭文件,只留下咱們會用到以及須要解釋的屬性和方法:

@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 表明原始的請求,因爲 AVPlayer 是會觸發分片下載的策略,還須要從dataRequest 中獲得請求範圍的信息。有了請求地址和請求範圍,咱們就能夠從新建立一個設置了請求 Range 頭的 NSURLRequest 對象,讓下載器去下載這個文件的 Range 範圍內的數據。

3.1.1.2 賽數據給AVPlayer

AVPlayer 觸發下載時,老是會先發起一個 Range0-2 的數據請求,這個請求的做用實際上是用來確認視頻數據的信息,如文件類型、文件數據長度。當下載器發起這個請求,收到服務端返回的 response 後,咱們要把視頻的信息填充到 AVAssetResourceLoadingRequestcontentInformationRequest 屬性中,告知下載的視頻格式以及視頻長度。

AVAssetResourceLoadingRequest- (void)finishLoading 的時候,會根據 contentInformationRequest 中的信息,去判斷接下去要怎麼處理。例如:下載 AVURLAsset 中 URL 指向的文件,獲取到的文件的 contentType 是系統不支持的類型,這個 AVURLAsset 將沒法正常播放。

獲取完視頻信息後,會收到剛纔指定的 2 Bytedata 數據,下載到的數據怎麼辦? 能夠塞給 AVAssetResourceLoadingRequest 裏的 dataRequestdataRequest 裏面用 - (void)respondWithData:(NSData *)data; 專門用來接收下載的數據,這個方法能夠調用屢次,接收增量連續的 data 數據。

AVAssetResourceLoadingRequest 要求的全部數據都下載完畢,調用 - (void)finishLoading 完成下載,AVAssetResourceLoader 會繼續發起以後的數據片斷的請求。若是本次請求失敗,能夠直接調用 - (void)finishLoadingWithError:(nullable NSError *)error; 結束下載。

3.1.1.3 重試機制

在實際的測試中,發現AVAssetResourceLoader 在執行加載的時候,會時不時的觸發取消下載調用 - (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest,而後從新發起加載請求的策略。若是下載了部分,那麼從新發起的下載請求會從尚未下載的部分開始。

AVAssetResourceLoaderDelegate 中還有 3 個方法能夠針對特殊場景作處理,不過在目前的環境中都用不到因此能夠選擇不實現這些方法。

3.1.2 下載緩存原理

經過上面實時播放原理的介紹,咱們已經知道 AVAssetResourceLoaderDelegate 的實現機制,當 AVAsset 須要加載數據時會經過 delegate 告訴外部,外部接管整個視頻下載過程。

當咱們接管了視頻下載,即可以對視頻數據作任何事情。好比:緩存、記錄下載速度、得到下載進度等等。

實現一個下載器,就是用 URLSession 開啓一個 DataTask 請求數據,把接收到的數據塞給 DataRequest 並寫入本地磁盤。在實現下載器時主要有三個注意的點:1. Range 請求 2. 可取消下載 3. 分片緩存

3.1.2.1 Range 請求

  • 可以經過Range分片請求,是實現實時播放,邊下邊播的關鍵。

每次獲得的 LoadingRequest 帶有請求數據範圍的信息,好比指望請求第 100 字節到 500 字節,在建立 URLRequest 時須要設置 HTTPHeaderRange 值。

NSString *range = [NSString stringWithFormat:@"bytes=%lld-%lld", fromOffset, endOffset];
[request setValue:range forHTTPHeaderField:@"Range"];
複製代碼

引入分塊下載最大的複雜點在於對響應數據的contentOffset的處理上,好在AVAssetResourceLoader幫咱們處理了大量工做,咱們只須要用好AVAssetResourceLoadingRequest就能夠了。

例如,下面是代碼部分,首先是獲取原始請求和發送新的請求

func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
    if self.session == nil {
        //構造Session
        let configuration = URLSessionConfiguration.default
        configuration.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData
        configuration.networkServiceType = .video
        configuration.allowsCellularAccess = true
        self.session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
    }
    //構造 保存請求
    var urlRequst = URLRequest.init(url: self.initalUrl!, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 20) // 20s超時
    urlRequst.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
    urlRequst.httpMethod = "GET"
    //設置請求頭
    guard let wrappedDataRequest = loadingRequest.dataRequest else{
        //本次請求沒有數據請求
        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
}
複製代碼

收到響應請求後,抓包查看響應的請求頭,下圖是2個響應的請求頭:

range請求響應
Content-Length表示本次請求的數據長度 Content-Range表示本次請求的數據在總媒體文件中的位置,格式是 start-end/total,所以就有 Content-Length = end - start + 1

3.1.2.2 可取消下載

AVAsset 在加載視頻時,常常會在某次數據請求尚未完成時觸發取消下載,而後發起一個新的 LoadingReqeust。這個機制是 AVAsset 裏的黑盒,具體邏輯沒法得知,比較像是 AVAsset 的一種重試機制。 做爲下載器,在收到取消通知時,須要馬上中止下載。因爲 DataRequestcancel 操做是異步的,就有可能在 cancel 還未完成時,下一個 LoadingRequest 就已經到來,因此還須要須要保證同一個 URL 只能同時存在一個下載器在下載,不然會出現數據混亂的問題。

3.1.2.3 分片緩存

若是隻是單純的下載視頻,數據單調遞增,緩存處理仍是比較容易。然而現實是用戶對 player 的 seek 操做給視頻的緩存管理帶來了巨大的挑戰,一旦涉及到用戶操做,可能性就越多,複雜度也會越高。

沒有 seek 的狀況:網速正常時緩存數據比播放時間走得開,正常播放;網速慢時,播放器 loading,直到有足夠的數據量進行播放,若是網速一直很慢就會播幾秒卡一下。

當加入 seek 後會有三種可能:

  • 第一種狀況,視頻徹底下載好,這時 seek 只需讀取相應緩存便可,這種狀況最簡單,就直接從緩存讀數據便可。

    圖3.1.2.3.1 - seek時視頻完成下載了

  • 第二種狀況,視頻下載一半,用戶 seek 到未下載部分,LoadingRequest 請求的部分所有都是未下載的數據。這時須要取消正在下載的數據,而後從 seek 的點開始下載數據。爲了支持 seek 操做,下載器就須要支持分片緩存。目前使用的解決方案是下載的視頻數據會根據請求的 Range 值,把數據存儲到文件中對應的偏移值位置,而且每一個視頻文件都會另外再保存一個與之對應的下載信息文件。這個信息文件會記錄當前下載了多少數據,總共有多少數據,下載了哪些片斷的數據等信息,以後的緩存管理會很是依賴這個配置文件。

    圖3.1.2.3.2 - seek時都是未下載的部分

  • 第三種狀況,視頻被 seek 了屢次,用戶 seek 到一個時間點,LoadingRequest 請求的部分包含了已下載和未下載的部分。這種狀況是最複雜的!簡單的作法是,當成上面的狀況來處理,所有都從新下載,雖然邏輯簡單,但這個方案會下載屢次一樣的數據,不是最最優解。個人目標固然是作最優的解決方案,但也是複雜高不少的解決方案。

    圖3.1.2.3.3 - seek時既有下載完的部分,又有未下載的部分
    在收到 LoadingRequest 的請求範圍後,下載器會先獲取已經下載的數據信息,把已下載的分片信息分別建立一個 action,再把須要遠程下載的分片數據分別建立一個 action。最終組合就多是 LocalAction(50-100 bytes) + RemoteAction(101-200 bytes) + LocalAction(201-300 bytes) + RemoteAction(300-400 bytes)。每個 action 會按順序獲取數據再返回給 LoadingRequest。以下圖:
    圖3.1.2.3.4 - seek時既有下載完的部分,又有未下載的部分,建立action

3.2 邊下邊播實現細節

  • 在下載視頻時,出現錯誤沒法正常下載是比較容易出現的。咱們本身實現了 AVAssetResourceLoaderDelegate 在第一次請求就拋出錯誤的話,播放器會立刻提示錯誤狀態,而若是是已經響應了部分數據,再拋錯誤,AVAssetResourceLoader 會忽略錯誤而一直處於 loading,直到超時。這種狀況就比較尷尬,在上面給出的VIMediaCache 實現中, VIResourceLoaderManager 提供了 delegate,若是內部出現錯誤,就會拋出錯誤,再又外部業務決定是如何處理。

  • 同一時間同一個 url 不能有屢次下載: 因爲緩存內部實現是對每個 url 都共用同一個下載配置文件,若是同時有屢次對同一個 url 進行下載,這個文件下載信息會被同時修改,下載信息會變得混亂。VIMediaCache 裏的 MediaCache 內部作了簡單的處理,若是正在下載某 url,這時再想嘗試下載一樣的 url 會直接拋出錯誤,提示沒法開始下載。

  • 實際上VGPlayer只是參考VIMediaCache 方式的Swift版本實現,VIMediaCache 是真的大牛編寫的OC版本,值得好好研究。

  • 鑑於咱們JimuPro工程師純swift項目,裏面處了第三方庫沒有使用OC代碼,因此我優先選擇VGPlayer來實現機器人端到IOS app端的邊下邊播功能。

  • 因爲VGPlayer沒有實現HTTPS的證書驗證,這裏我只須要簡單實現證書驗證代碼便可。咱們將在下面講解HTTPS的證書認證明現。這裏我簡單說一下個人實現, 在VGPlayerDownloadURLSessionManager.swift文件的VGPlayerDownloadURLSessionManager類裏面增長一個URLSession的一個代理實現:

    增長一個URLSession的一個代理實現

  • 即便你參考上面的源碼實現了邊下載邊播放,仍是有些細節地方須要注意的: 例如要實現mp4文件的邊下邊播功能,不只依賴於上面講解的邊下邊播實現方案,還依賴於mp4的文件格式。若是遇到這種mp4文件的元數據放在文件末尾的,咱們須要在服務器端將mp4文件作一下轉換才能夠實現邊下邊播功能。

接下來詳細講解一下mp4格式處理問題。

3.3 邊下邊播mp4文件格式須要注意

咱們要明確一點就是即便你用上面的緩存方式實現了邊下邊播的功能,並非全部mp4都支持的,這個須要你理解邊下邊播的原理。

mp4視頻文件頭中,包含一些元數據。元數據包含:視頻的寬度高度、視頻時長、編碼格式等。mp4元數據一般在視頻文件的頭部,這樣播放器在讀取文件時會最早讀取視頻的元數據,而後開始播放視頻。

固然也存在這樣一種狀況:mp4視頻的元數據處於視頻文件最後,這樣播放器在加載視頻文件時,一直讀取到最後,纔讀取到視頻信息,而後開始播放。若是缺乏元數據,也是這樣的狀況。這就出現了mp4視頻不支持邊加載、邊播放的問題。

  • 爲啥會出現上面說的這種狀況呢,下面咱們簡單分析一下原理:

在請求頭裏有一個Range:byte字段來告訴媒體服務器須要請求的是哪一段特定長度的文件內容,對於MP4文件來講,全部數據都封裝在一個個的box或者atom中,其中有兩個atom尤其重要,分別是moov atom和mdat atom。

  • moov atom:包含媒體的元數據的數據結構,包括媒體的塊(box)信息,格式說明等等。
  • mdat atom: 包含媒體的媒體信息,對於視屏來講就是視頻畫面了。

在IOS中發送一個請求,利用NSUrlSession直接請求視頻資源,針對元信息在視頻文件頭部的視頻能夠實現邊下邊播,而元信息在視頻尾部的視頻則會下載完才播放,爲啥會這樣呢?

答案就是:雖然moov和mdat都只有一個,可是因爲MP4文件是由若干個這樣的box或者atom組成的,所以這兩個atom在不一樣媒體文件中出現的順序可能會不同,爲了加快流媒體的播放,咱們能夠作的優化之一就是手動把moov提到mdat以前。 對於AVPlayer來講,只有到AVPlayerItemStatusReadyToPlay狀態時,才能夠開始播放視頻,而進入AVPlayerItemStatusReadyToPlay狀態的必要條件就是播放器讀到了媒體的moov塊。

若是mdat位於moov以後,那麼這樣的mp4視頻文件是沒法實現邊下邊播放的。要支持邊下邊播的mp4視頻須要知足moov和mdat都位於文件頭部,且moov位於mdat以前。以下圖所示:

moov位於mdat以前
當moov和mdat都位於文件頭部,且moov位於mdat以前。咱們理論上一個請求就能夠播放全部的moov位於mdat以前的視頻的。可是,當咱們seek拖拽播放的話,狀況就變很複雜了,須要藉助分塊下載。

那麼,若是遇到這種mp4文件的元數據放在文件末尾的,咱們須要在服務器端將mp4文件作一下轉換才能夠實現邊下邊播功能。

可行的方法是使用的是qt-faststart工具。 qt-faststart可以將處於MP4文件末尾的moov atom元數據轉移到最前面,不過因爲qt-faststart工具只能處理moov atom元數據位於MP4末尾的文件。 若是咱們想要將全部文件統一處理:總體思路是將MP4文件經過ffmpeg處理,將moov atom元數據轉移至末尾,而後使用qt-faststart工具轉移至最前面。

3.3.1 mp4 元數據特殊處理

  1. 先將下載的FFmpeg包解壓:tar -jxvf ffmpeg-3.3.3.tar.bz2
  2. 配置:./configure --enable-shared --prefix=/usr/local/ffmpeg prefix就是設置安裝位置,通常都默認usr/local下。
  3. 安裝:
make
make install
複製代碼

編譯安裝時間會很長,10分鐘左右吧,裝完之後能夠去安裝目錄下查看。 這時尚未結束,如今使用的話通常會報以下錯誤:

ffmpeg: error while loading shared libraries: libavfilter.so.1: cannot open shared object file: No such file or directory
複製代碼
  1. 須要編輯/etc/ld.so.conf文件加入以下內容:/usr/local/lib,保存退出後執行ldconfig命令。
echo "/usr/local/ffmpeg/lib" >> /etc/ld.so.conf
#注意這裏是你前面安裝ffmpeg的路徑
ldconfig
複製代碼
  • qt-faststart 安裝 上面講到的qt-faststart工具其實就在ffmpeg的源碼中有,由於在ffmpeg解壓完的文件中存在qt-faststart的源碼,因此直接使用,位置在解壓路徑/tools/qt-faststart.c

若是你想單獨下載點擊這裏: qt-faststart下載 6. 進入ffmpeg解壓路徑執行命令:make tools/qt-faststart,會看到在tools中會出現一個qt-faststart文件(還有一個.c文件) 7. ffmpeg將元數據轉移至文件末尾:

cd ffmpeg安裝路徑/bin;./ffmpeg -i /opt/mp4test.mp4 -acodec copy -vcodec copy /opt/1.mp4
# /opt/mp4test.mp4爲原始MP4文件路徑,/opt/1.mp4爲生成文件的存放路徑

複製代碼
  1. qt-faststart 將元數據轉移到文件開頭:
cd ffmpeg壓縮包解壓路徑/tools;
./qt-faststart /opt/1.mp4 /opt/2.mp4
複製代碼

4 HTTPS 邊下邊播 自簽名證書認證

  • HTTPS SSL加密創建鏈接過程

以下圖:

HTTPS SSL加密創建鏈接過程
過程詳解:

  1. ①客戶端的瀏覽器向服務器發送請求,並傳送客戶端SSL 協議的版本號,加密算法的種類,產生的隨機數,以及其餘服務器和客戶端之間通信所須要的各類信息。
  2. ②服務器向客戶端傳送SSL 協議的版本號,加密算法的種類,隨機數以及其餘相關信息,同時服務器還將向客戶端傳送本身的證書。
  3. ③客戶端利用服務器傳過來的信息驗證服務器的合法性,服務器的合法性包括:證書是否過時,發行服務器證書的CA 是否可靠,發行者證書的公鑰可否正確解開服務器證書的「發行者的數字簽名」,服務器證書上的域名是否和服務器的實際域名相匹配。若是合法性驗證沒有經過,通信將斷開;若是合法性驗證經過,將繼續進行第四步。
  4. ④用戶端隨機產生一個用於通信的「對稱密碼」,而後用服務器的公鑰(服務器的公鑰從步驟②中的服務器的證書中得到)對其加密,而後將加密後的「預主密碼」傳給服務器。
  5. ⑤若是服務器要求客戶的身份認證(在握手過程當中爲可選),用戶能夠創建一個隨機數而後對其進行數據簽名,將這個含有簽名的隨機數和客戶本身的證書以及加密過的「預主密碼」一塊兒傳給服務器。
  6. ⑥若是服務器要求客戶的身份認證,服務器必須檢驗客戶證書和簽名隨機數的合法性,具體的合法性驗證過程包括:客戶的證書使用日期是否有效,爲客戶提供證書的CA 是否可靠,發行CA 的公鑰可否正確解開客戶證書的發行CA 的數字簽名,檢查客戶的證書是否在證書廢止列表(CRL)中。檢驗若是沒有經過,通信馬上中斷;若是驗證經過,服務器將用本身的私鑰解開加密的「預主密碼」,而後執行一系列步驟來產生主通信密碼(客戶端也將經過一樣的方法產生相同的主通信密碼)。
  7. ⑦服務器和客戶端用相同的主密碼即「通話密碼」,一個對稱密鑰用於SSL 協議的安全數據通信的加解密通信。同時在SSL 通信過程當中還要完成數據通信的完整性,防止數據通信中的任何變化。
  8. ⑧客戶端向服務器端發出信息,指明後面的數據通信將使用的步驟. ⑦中的主密碼爲對稱密鑰,同時通知服務器客戶端的握手過程結束。
  9. ⑨服務器向客戶端發出信息,指明後面的數據通信將使用的步驟⑦中的主密碼爲對稱密鑰,同時通知客戶端服務器端的握手過程結束。
  10. ⑩SSL 的握手部分結束,SSL 安全通道的數據通信開始,客戶和服務器開始使用相同的對稱密鑰進行數據通信,同時進行通信完整性的檢驗。
  • 我這裏只給出我項目裏面使用VGPlayer播放器裏的HTTPS證書認證方式實現代碼,只須要簡單的兩部便可實現:
  1. 先將服務器給你自簽名證書添加到工程裏面:
    導入自簽名證書
  2. 在VGPlayerDownloadURLSessionManager.swift文件的VGPlayerDownloadURLSessionManager類裏面增長一個URLSession的一個代理實現:
    增長一個URLSession的一個代理實現
public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        let method = challenge.protectionSpace.authenticationMethod
        if method == NSURLAuthenticationMethodServerTrust {
            //驗證服務器,直接信任或者驗證證書二選一,推薦驗證證書,更安全
            completionHandler( HTTPSManager.trustServerWithCer(challenge: challenge).0, HTTPSManager.trustServerWithCer(challenge: challenge).1)
            
        } else if method == NSURLAuthenticationMethodClientCertificate {
            //認證客戶端證書
             
            completionHandler( HTTPSManager.sendClientCer().0, HTTPSManager.sendClientCer().1)
            
        } else {
            //其餘狀況,不經過驗證
            completionHandler(.cancelAuthenticationChallenge, nil)
        }
    }
複製代碼
  1. 認證類HTTPSManager的實現以下:
//
// 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 {
// //驗證服務器,直接信任或者驗證證書二選一,推薦驗證證書,更安全
// return HTTPSManager.trustServerWithCer(challenge: challenge)
//// return HTTPSManager.trustServer(challenge: challenge)
// 
// } else if method == NSURLAuthenticationMethodClientCertificate {
// //認證客戶端證書
// return HTTPSManager.sendClientCer()
// 
// } else {
// //其餘狀況,不經過驗證
// return (.cancelAuthenticationChallenge, nil)
// }
// }
// }
    
    //不作任何驗證,直接信任服務器
    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)
        
    }
    
    //驗證服務器證書
    static  func trustServerWithCer(challenge: URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?) {
        
        var disposition: URLSession.AuthChallengeDisposition = .performDefaultHandling
        var credential: URLCredential?
        
        //獲取服務器發送過來的證書
        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) {
            //服務器證書驗證經過
            disposition = URLSession.AuthChallengeDisposition.useCredential
            credential = URLCredential(trust: serverTrust)
            
        } else {
            //服務器證書驗證失敗
            //disposition = URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge
            disposition = URLSession.AuthChallengeDisposition.useCredential
            credential = URLCredential(trust: serverTrust)
        }
        
        return (disposition, credential)
        
    }
    
    //發送客戶端證書交由服務器驗證
    static  func sendClientCer() -> (URLSession.AuthChallengeDisposition, URLCredential?) {
        
        let disposition = URLSession.AuthChallengeDisposition.useCredential
        var credential: URLCredential?
        
        //獲取項目中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 官方介紹以下:
  1. MP4是一套用於音頻、視頻信息的壓縮編碼標準,由國際標準化組織(ISO)和國際電工委員會(IEC)下屬的「動態圖像專家組」(Moving Picture Experts Group,即MPEG)制定,初版在1998年10月經過,第二版在1999年12月經過。MPEG-4格式的主要用途在於網上流、光盤、語音發送(視頻電話),以及電視廣播。
  2. MPEG-4包含了MPEG-1及MPEG-2的絕大部份功能及其餘格式的長處,並加入及擴充對虛擬現實模型語言(VRML , VirtualReality Modeling Language)的支持,面向對象的合成檔案(包括音效,視訊及VRML對象),以及數字版權管理(DRM)及其餘互動功能。而MPEG-4比MPEG-2更先進的其中一個特色,就是再也不使用宏區塊作影像分析,而是以影像上個體爲變化記錄,所以儘管影像變化速度很快、碼率不足時,也不會出現方塊畫面。
  • MP4標準 MPEG-4碼流主要包括基本碼流和系統流,基本碼流包括音視頻和場景描述的編碼流表示,每一個基本碼流只包含一種數據類型,並經過各自的解碼器解碼。系統流則指定了根據編碼視聽信息和相關場景描述信息產生交互方式的方法,並描述其交互通訊系統。

  • MP4也能夠理解成一種視頻的封裝格式 視頻封裝格式,簡稱視頻格式,至關於一種儲存視頻信息的容器,它裏面包含了封裝視頻文件所須要的視頻信息、音頻信息和相關的配置信息(好比:視頻和音頻的關聯信息、如何解碼等等)。一種視頻封裝格式的直接反映就是對應着相應的視頻文件格式。

常見的封裝格式有以下:

常見的封裝格式有以下

封裝格式:就是將已經編碼壓縮好的視頻數據 和音頻數據按照必定的格式放到一個文件中.這個文件能夠稱爲容器. 固然能夠理解爲這只是一個外殼.

一般咱們不只僅只存放音頻數據和視頻數據,還會存放 一下視頻同步的元數據.例如字幕.這多種數據會不一樣的程序來處理,可是它們在傳輸和存儲的時候,這多種數據都是被綁定在一塊兒的.

  • 常見的視頻容器格式:
  1. AVI: 是當時爲對抗quicktime格式(mov)而推出的,只能支持固定CBR恆定定比特率編碼的聲音文件
  2. MOV:是Quicktime封裝
  3. WMV:微軟推出的,做爲市場競爭
  4. mkv:萬能封裝器,有良好的兼容和跨平臺性、糾錯性,可帶外掛字幕
  5. flv: 這種封裝方式能夠很好的保護原始地址,不容易被下載到,目前一些視頻分享網站都採用這種封裝方式
  6. MP4:主要應用於mpeg4的封裝,主要在手機上使用。
  • 視頻編解碼方式

視頻編解碼的過程是指對數字視頻進行壓縮或解壓縮的一個過程. 在作視頻編解碼時,須要考慮如下這些因素的平衡:視頻的質量、用來表示視頻所須要的數據量(一般稱之爲碼率)、編碼算法和解碼算法的複雜度、針對數據丟失和錯誤的魯棒性(Robustness)、編輯的方便性、隨機訪問、編碼算法設計的完美性、端到端的延時以及其它一些因素。

  • 常見視頻編碼方式:
  • H.26X 系列,由國際電傳視訊聯盟遠程通訊標準化組織(ITU-T)主導,包括 H.26一、H.26二、H.26三、H.26四、H.265
  1. H.261,主要用於老的視頻會議和視頻電話系統。是第一個使用的數字視頻壓縮標準。實質上說,以後的全部的標準視頻編解碼器都是基於它設計的。
  2. H.262,等同於 MPEG-2 第二部分,使用在 DVD、SVCD 和大多數數字視頻廣播系統和有線分佈系統中。
  3. H.263,主要用於視頻會議、視頻電話和網絡視頻相關產品。在對逐行掃描的視頻源進行壓縮的方面,H.263 比它以前的視頻編碼標準在性能上有了較大的提高。尤爲是在低碼率端,它能夠在保證必定質量的前提下大大的節約碼率。
  4. H.264,等同於 MPEG-4 第十部分,也被稱爲高級視頻編碼(Advanced Video Coding,簡稱 AVC),是一種視頻壓縮標準,一種被普遍使用的高精度視頻的錄製、壓縮和發佈格式。該標準引入了一系列新的可以大大提升壓縮性能的技術,並可以同時在高碼率端和低碼率端大大超越之前的諸標準。
  5. H.265,被稱爲高效率視頻編碼(High Efficiency Video Coding,簡稱 HEVC)是一種視頻壓縮標準,是 H.264 的繼任者。HEVC 被認爲不只提高圖像質量,同時也能達到 H.264 兩倍的壓縮率(等同於一樣畫面質量下比特率減小了 50%),可支持 4K 分辨率甚至到超高畫質電視,最高分辨率可達到 8192×4320(8K 分辨率),這是目前發展的趨勢。
  • MPEG 系列,由國際標準組織機構(ISO)下屬的運動圖象專家組(MPEG)開發。
  1. MPEG-1 第二部分,主要使用在 VCD 上,有些在線視頻也使用這種格式。該編解碼器的質量大體上和原有的 VHS 錄像帶至關。
  2. MPEG-2 第二部分,等同於 H.262,使用在 DVD、SVCD 和大多數數字視頻廣播系統和有線分佈系統中。
  3. MPEG-4 第二部分,可使用在網絡傳輸、廣播和媒體存儲上。比起 MPEG-2 第二部分和初版的 H.263,它的壓縮性能有所提升。
  4. MPEG-4 第十部分,等同於 H.264,是這兩個編碼組織合做誕生的標準。

能夠把「視頻封裝格式」看作是一個裝着視頻、音頻、「視頻編解碼方式」等信息的容器。一種「視頻封裝格式」能夠支持多種「視頻編解碼方式」,好比:QuickTime File Format(.MOV) 支持幾乎全部的「視頻編解碼方式」,MPEG(.MP4) 也支持至關廣的「視頻編解碼方式」。當咱們看到一個視頻文件名爲 test.mov 時,咱們能夠知道它的「視頻文件格式」是 .mov,也能夠知道它的視頻封裝格式是 QuickTime File Format,

可是沒法知道它的「視頻編解碼方式」。那比較專業的說法多是以 A/B 這種方式,A 是「視頻編解碼方式」,B 是「視頻封裝格式」。好比:一個 H.264/MOV 的視頻文件,它的封裝方式就是 QuickTime File Format,編碼方式是 H.264

在這裏機器人裏面錄製視頻時採用H.264/mp4,因此這裏我這邊實現的邊下邊播方案裏面也是針對的這種H.264視頻編解碼方式的mp4容器格式的視頻文件。

H264最大的優點,具備很高的數據壓縮比率,在同等圖像質量下,H264的壓縮比是MPEG-2的2倍以上,MPEG-4的1.5~2倍.

原始文件的大小若是爲88GB,採用MPEG-2壓縮標準壓縮後變成3.5GB,壓縮比爲25∶1,而採用H.264壓縮標準壓縮後變爲879MB,從88GB到879MB,H.264的壓縮比達到驚人的102∶1

  • 正常咱們機器人採集到視頻流數據後,通過H264硬編碼,或者FFmpeg處理的H264軟編碼方式,將YUV4:2:0的數據進行H264編碼後獲得編碼後的H264流數據。
  • 咱們在IOS播放時,其實也是拿到這種一幀一幀的H264流數據,而後進行硬解碼或FFmpeg軟解碼。(硬解碼在IOS裏面是有VideoToolBox框架裏面的API能夠實現,軟解碼須要使用FFmpeg裏的H264解碼器)。解碼後咱們獲得原始裸數據YUV數據,而後咱們將YUV數據轉換爲RGB數據,藉助OpenGL ES或Metal 以紋理渲染的方式,將圖像顯示在View的 Layer上。
  • 其實這些解碼,播放相關底層代碼都被咱們的AVFoundation框架裏面的AVPlayer 封裝了,沒有暴露這些細節給我,咱們只須要傳遞一個URL 就能夠實現視頻播放功能。

爲了更好的理解播放視頻的原理,我這裏還簡單介紹一下H264編解碼的相關知識

6.2 H264簡介

  • H264的碼流結構:H264視頻壓縮後會成爲一個序列幀.幀裏包含圖像,圖像分爲不少片.每一個片能夠分爲宏塊.每一個宏塊由許多子塊組成,以下圖:

    H264的碼流結構
    H264結構中,一個視頻圖像編碼後的數據叫作一幀,一幀由一個片(slice)或多個片組成,一個片由一個或多個宏塊(MB)組成,一個宏塊由16x16的yuv數據組成。宏塊做爲H264編碼的基本單位。

  • 場和幀:視頻的一場或一幀可用來產生一個編碼圖像。在電視中,爲減小大面積閃爍現象,把一幀分紅兩個隔行的場。

  • :每一個圖象中,若干宏塊被排列成片的形式。片分爲I片、B片、P片和其餘一些片。

  1. I片只包含I宏塊,P片可包含P和I宏塊,而B片可包含B和I宏塊。
  2. I宏塊利用從當前片中已解碼的像素做爲參考進行幀內預測。
  3. P宏塊利用前面已編碼圖象做爲參考圖象進行幀內預測。
  4. B宏塊則利用雙向的參考圖象(前一幀和後一幀)進行幀內預測。
  5. 的目的是爲了限制誤碼的擴散和傳輸,使編碼片相互間是獨立的。
  • H264碼流分層結構圖:
    H264碼流分層結構圖

A Annex格式數據,就是起始碼+Nal Unit 數據 NAL Unit: NALU 頭+NALU數據 NALU 主體,是由切片組成.切片包括切片頭+切片數據 Slice數據: 宏塊組成 PCM類: 宏塊類型+pcm數據,或者宏塊類型+宏塊模式+殘差數據 Residual: 殘差塊.

  • NAL 單元是由一個NALU頭部+一個切片.切片又能夠細分紅"切片頭+切片數據".咱們之間瞭解過一個H254的幀是由多個切片構成的.由於一幀數據一次有可能傳不完. 以下圖:

    NAL 單元

  • 切片與宏塊的關係(Slice & MacroBlock) 每一個切片都包括切片頭+切片數據. 那每一個切片數據包括了不少宏塊.每一個宏塊包括了宏塊的類型,宏塊的預測,殘差數據. 以下圖:

    切片與宏塊的關係

而咱們在一副壓縮的H264的幀裏,能夠包含多個切片.至少有一個切片,以下圖:

H264切片

瞭解了上面關於H264碼流的一些基本概念後,咱們就能更好的理解H264編碼解碼的原理,以及圖像渲染,視頻播放器的實現原理。

在H264解碼的過程當中會涉及到一幀幀的數據,這裏有I幀,P幀,B幀,三個概念。

  • I幀: 關鍵幀,採用幀內壓縮技術.

舉個例子,若是攝像頭對着你拍攝,1秒以內,實際你發生的變化是很是少的.1秒鐘以內實際少不多有大幅度的變化.攝像機通常一秒鐘會抓取幾十幀的數據.好比像動畫,就是25幀/s,通常視頻文件都是在30幀/s左右.對於一些要求比較高的,對動做的精細度有要求,想要捕捉到完整的動做的,高級的攝像機通常是60幀/s.那些對於一組幀的它的變化很小.爲了便於壓縮數據,那怎麼辦了?將第一幀完整的保存下來.若是沒有這個關鍵幀後面解碼數據,是完成不了的.因此I幀特別關鍵.

  • P幀: 向前參考幀.壓縮時只參考前一個幀.屬於幀間壓縮技術.

視頻的第一幀會被做爲關鍵幀完整保存下來.然後面的幀會向前依賴.也就是第二幀依賴於第一個幀.後面全部的幀只存儲於前一幀的差別.這樣就能將數據大大的減小.從而達到一個高壓縮率的效果.

  • B幀: 雙向參考幀,壓縮時即參考前一幀也參考後一幀.幀間壓縮技術.
  1. B幀,即參考前一幀,也參考後一幀.這樣就使得它的壓縮率更高.存儲的數據量更小.若是B幀的數量越多,你的壓縮率就越高.這是B幀的優勢,可是B幀最大的缺點是,若是是實時互動的直播,那時與B幀就要參考後面的幀才能解碼,那在網絡中就要等待後面的幀傳輸過來.這就與網絡有關了.若是網絡狀態很好的話,解碼會比較快,若是網絡很差時解碼會稍微慢一些.丟包時還須要重傳.對實時互動的直播,通常不會使用B幀.

咱們實時播放視頻時,每次從服務器請求一個Range範圍的視頻幀,實際上服務器是返回一組組的H264幀數據,一組幀數據又稱爲GOF(Group of Frame),GOF 表示:一個I幀到下一個I幀.這一組的數據.包括B幀/P幀. 以下圖所示:

Group of Frame

  • 在H264碼流中,咱們使用SPS/PPS來存儲GOP的參數。

  • SPS 序列參數集 :全稱是Sequence Parameter Set,序列參數集存放幀數,參考幀數目,解碼圖像尺寸,幀場編碼模式選擇標識等.

  • PPS 圖像參數集:全稱是Picture Parameter Set,圖像參數集.存放編碼模式選擇標識,片組數目,初始量化參數和去方塊濾波係數調整標識等.(與圖像相關的信息)

在一組幀以前咱們首先收到的是SPS/PPS數據.若是沒有這組參數的話,咱們是沒法解碼. 以前WebRTC視頻的時候遇到的一個問題就是:IOS端有時候圖傳的時候黑屏,這個緣由就是由於I幀缺乏SPS/PPS信息,致使解碼失敗,致使的黑屏。

  • 若是咱們在解碼時發生錯誤,首先要檢查是否有SPS/PPS.若是沒有,是由於對端沒有發送過來仍是由於對端在發送過程當中丟失了. SPS/PPS數據,咱們也把其歸類到I幀.這2組數據是絕對不能丟的.

  • 視頻花屏,卡頓的緣由分析: 咱們在觀看視頻時,會遇到花屏或者卡頓現象.那這個與咱們剛剛所講的GOF就息息相關了

  1. 若是GOP分組中的P幀丟失就會形成解碼端的圖像發生錯誤.解碼錯誤時,咱們把解碼失敗的圖片用來展現了,就致使咱們看到的花屏現象
  2. 爲了不花屏問題的發生,通常若是發現P幀或者I幀丟失.就不顯示本GOP內的全部幀.只到下一個I幀來後從新刷新圖像.
  3. 當這時由於沒有刷新屏幕.丟包的這一組幀所有扔掉了.圖像就會卡在哪裏不動.這就是卡頓的緣由.
  4. 因此總結起來,花屏是由於你丟了P幀或者I幀.致使解碼錯誤. 而卡頓是由於爲了怕花屏,將整組錯誤的GOP數據扔掉了.直達下一組正確的GOP再從新刷屏.而這中間的時間差,就是咱們所感覺的卡頓.
  • 軟編碼與硬編碼
  • 硬編碼: 使用非CPU進行編碼,例如使用GPU芯片處理
  1. 性能高,低碼率下一般質量低於硬編碼器,但部分產品在GPU硬件平臺移植了優秀的軟編碼算法(如X264)的,質量基本等同於軟編碼。
  2. 硬編碼,就是使用GPU計算,獲取數據結果,優勢速度快,效率高.
  3. 在IOS平臺針對視頻硬編碼使用VideoToolBox框架,針對音頻硬編碼使用AudioToolBox 框架
  • 軟編碼: 使用CPU來進行編碼計算.
  1. 實現直接、簡單,參數調整方便,升級易,但CPU負載重,性能較硬編碼低,低碼率下質量一般比硬編碼要好一點。
  2. 軟編碼,就是經過CPU來計算,獲取數據結果.
  3. 在IOS平臺針對視頻軟編碼通常使用FFmpeg,X264算法把視頻原數據YUV/RGB編碼成H264。針對音頻使用fdk_aac 將音頻數據PCM轉換成AAC。

若是想更加深刻的探索播放器的底層原理,能夠參考這兩款開源的播放器: ijkplayer,kxmovie 他們都是基於FFmpeg框架封裝的

  • ijkplayer是bilibili出品的一款基於FFmpeg的視頻播放器,在git上面已經有25.7k的星星了,很是強大,值得深刻研究,這個包含ios,和android端的。
    ijkplayer
  • kxmovie 在git上面也有2.7k的星星,這是實力的認證,值得學習,研究。
    kxmovie

6.3 MP4 格式

MP4(MPEG-4 Part 14)是一種常見的多媒體容器格式,它是在「ISO/IEC 14496-14」標準文件中定義的,屬於MPEG-4的一部分,是「ISO/IEC 14496-12(MPEG-4 Part 12 ISO base media file format)」標準中所定義的媒體格式的一種實現,後者定義了一種通用的媒體文件結構標準。MP4是一種描述較爲全面的容器格式,被認爲能夠在其中嵌入任何形式的數據,各類編碼的視頻、音頻等都不在話下,不過咱們常見的大部分的MP4文件存放的AVC(H.264)或MPEG-4(Part 2)編碼的視頻和AAC編碼的音頻。MP4格式的官方文件後綴名是「.mp4」,還有其餘的以mp4爲基礎進行的擴展或者是縮水版本的格式,包括:M4V,3GP,F4V等。

首先看一下軟件對於mp4文件的解析以下圖所示:

圖6.3.1 mp4文件格式
從上圖圖6.3.1 中能夠看出這個視頻文件第一層有4部分,每一部分都是一個 box,分別爲: ftype,moov,free,mdat。其實 mp4文件是有許多的 box組成的。以下圖6.3.2 所示:
圖6.3.2 - mp4文件格式

box的基本結構以下圖6.3.3所示,其中,size指明瞭整個box所佔用的大小,包括header部分,type指明瞭box的類型。若是box很大(例如存放具體視頻數據的mdat box),超過了uint32的最大數值,size就被設置爲1,並用接下來的8位uint64來存放大小。

圖6.3.3 - Box結構

一個mp4文件有可能包含很是多的box,在很大程度上增長了解析的複雜性,這個網頁上http://mp4ra.org/atoms.html記錄了一些當前註冊過的box類型。看到這麼多box,若是要所有支持,一個個解析,怕是頭都要爆了。還好,大部分mp4文件沒有那麼多的box類型,下圖就是一個簡化了的,常見的mp4文件結構以下圖6.3.4所示

圖6.3.4 - mp4包含的box結構
通常來講,解析媒體文件,最關心的部分是視頻文件的寬高、時長、碼率、編碼格式、幀列表、關鍵幀列表,以及所對應的時戳和在文件中的位置,這些信息,在mp4中,是以特定的算法分開存放在 stbl box下屬的幾個 box中的,須要解析 stbl下面全部的 box,來還原媒體信息。下表是對於以上幾個重要的 box存放信息的說明:
圖6.3.5 - mp4 box類型說明

6.4 IOS 原始API實現 將mp4文件的 moov的box移到前面

上面已經講解過使用FFmpeg裏面的 qt-faststart下載工具能夠實現將mp4文件的 moov的box移到前面,從而讓mp4文件支持邊下邊播功能。下面將介紹一種經過IOS原始代碼的方式實現將mp4文件的moov的box從文件最後面移到前面。

不過這種方式通常用不到,一是由於效率問題,而是通常實現邊下邊播,都是由服務器端去完成這種事情。

具體代碼以下:

- (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;

}

複製代碼

參考:www.jianshu.com/p/0188ab038… www.jianshu.com/p/bb925a4a9… www.cnblogs.com/ios4app/p/6… www.jianshu.com/p/990ee3db0…

相關文章
相關標籤/搜索