iOS邊下邊播總結

Sunxb發表於2021-12-10

邊下邊播總結(一)

概述

最近修改了專案中的視訊播放功能, 由之前的全量下載完再播, 改為了邊下邊播的方式. 由於我們專案中的視訊在發出時都進行了加密, 所以整個過程其實就是邊下載邊解密邊播放.

邊下邊播的技術方案, 網上的部落格很容易搜到, 不外乎兩種方式, 內建本地代理伺服器AVAssetResourceLoader. 我們採取了系統提供的AVAssetResourceLoader這一方案.

方案原理

具體的AVAssetResourceLoader實現原理網上可以找到很多邏輯圖, 如下圖(來自網路)所示.

原理圖.png

這裡結合我們的實際程式碼簡單的介紹一個這個圖片.

在平時使用AVPlayer播放url時, 我們會這樣建立一個播放器(簡略)

let videoAsset = AVURLAsset(url: "http://resource_url/xxxxx")
let item = AVPlayerItem(asset: videoAsset)
let player = AVPlayer(playerItem: item)

如果我們這樣設定播放, 整個播放的內部流程其實都我們都是不可見的, 視訊的下載和快取等, 我們只能通過已知的一些方法,來控制播放器的播放暫停等.

如果想要實現我們專案中想要的效果, 邊下載邊播放, 同時, 我們可能需要接手視訊的快取這一模組, 所以我們就必須得能進入到整個播放流程中, AVAssetResourceLoader其實就算是蘋果給我們留的一個小口子, 然後通過設定遵守AVAssetResourceLoaderDelegate這一協議的代理物件, 接手資料處理的這一過程(包括獲取資料和向播放器填充資料).

videoAsset.resourceLoader.setDelegate(self, queue: queue)

注意事項

  1. 要進入到 AVAssetResourceLoader的代理回撥, 除了要給videoAsset.resourceLoader設定delegate之外, 還需要把我們的url改為不能識別的scheme. 我們一遍的資源路徑都是http或者https, 我們需要把url的scheme改為不能識別的(私有的), 比如http://resource/xxx/xxx.mp4改為http-prefix://reource/xxxx/xxx.mp4
  2. url路徑的最後必須要有視訊的字尾, 類似.mp4, 我之前使用的資源路徑是沒有字尾的, 導致了播放器無法起播.

AVAssetResourceLoaderDelegate

AVAssetResourceLoaderDelegate有兩個常用的回撥方法如下

// MARK: - AVAssetResourceLoaderDelegate
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {}

func resourceLoader(_ resourceLoader: AVAssetResourceLoader, didCancel loadingRequest: AVAssetResourceLoadingRequest) {}

當播放器開始播放的時候, 會通過shouldWaitForLoadingOfRequestedResource這個回撥方法向我們索要資料, 具體所要資料的資訊細節都封裝在loadingRequest裡面.
因為這個回撥會走很多次, 上圖中表示的是要儲存起來每一次的loadingRequest, 但在實際專案中, 我使用了不太一樣的策略, 我把每一次loadingRequest都對應一個worker物件來處理, 這樣每次索要資料, 都有一個單獨的worker來處理相對應的網路請求(暫不考慮快取), 這樣比較條理. 同時我們也需要儲存起我們的worker, 因為如果播放器需要支援進度條拖動時, 需要手動seek到某一個位置, 這樣會觸發didCancel這個回撥, 所以我們也需要把我們對應的worker內部停掉.

原理圖mark.png

回撥處理

當我們收到一個回撥時, 我們主要關注這個AVAssetResourceLoadingRequest型別的loadingRequest.

他內部有一個dataRequest屬性, dataRequest中有requestedOffset, requestedLength等一些有用資訊. 我們通過requestedOffset和requestedLength構建出我們的Range, 塞到請求頭裡面去, 獲取相應range的資料.

當我們的player開始播放時, 收到的第一個回撥, requestedOffset=0,requestedLength=2, 也就是索要0-1這兩個位元組, 這次請求其實可以理解為一個嗅探請求, 目的是為了得到視訊的相關資訊, 檔案大小, 型別等.

guard request.contentInformationRequest == nil else {
    if request.dataRequest?.requestsAllDataToEndOfResource == false {
        request.contentInformationRequest?.contentLength = totalLen
    } else {
        request.contentInformationRequest?.contentLength = Int64(data.count)
   }
   request.contentInformationRequest?.isByteRangeAccessSupported = true
   request.contentInformationRequest?.contentType = "video/mp4"
   request.finishLoading()
   return
}

上述程式碼就是第一個嗅探請求的處理方式, 通過request.contentInformationRequest==nil, 判斷出是第一個嗅探請求, 然後我們需要填充request的contentInformationRequest, 然後填充資訊結束呼叫finishLoading(), 當前的loadingRequest就結束了.

第一個嗅探請求結束後, 如果我們返回的沒有問題, 那播放器會立刻進行下一個回撥, 開始所要視訊資料, 我在專案中測試時, 第二個請求一般都是0-xxx(檔案大小-1), 索要整個檔案, 這時我們dataTask型別的請求,等待伺服器一片一片的返回資料, 沒收到一部分資料後呼叫dataRequest.respond(with: data), 全部收取完畢之後呼叫request.finishLoading().

其實這就是最基本的資料填充的邏輯, 除了第一個嗅探請求特殊處理一下, 後面的就是收到資料, 就填充回dataRequest, 索要的資料全部填充完畢, 呼叫finishLoading.

在我們請求整個檔案的過程中, 有時候會發現一種現象, 就是respond一部分資料之後, loadingRequest被cancel了, 然後又開始索要很後面的range的資料, 其實這可以理解為一個尋找檔案的moov的過程, 檔案的moov可能在檔案頭, 也可能在檔案尾部.moov裡面定義視訊的時間尺度,時長,顯示特性以及每個軌道資訊等, 這一部分可以通過了解mp4檔案頭格式來多做一下了解.

我們不管他索要的是那一部分資料, 只要我們請求到對應的資料, respond回去就沒問題.

補充

那這麼簡單的邏輯對於我們自己的專案來說難點是什麼呢,這裡簡單描述一下.

前面有說到我們專案中的資源都是經過加密的, 使用了AES的加密演算法, 這樣我們在接受到資料之後, 是不能直接返回給dataRequest的, 需要我們先解密, 然後簡單的說我們使用的加密策略是每16位元組是一個加密片段, 但請求返回的資料並不能保證每次都是16倍數, 所以我們處理16的倍數才能進行解密這一個問題, 然後還有一個range的修正問題, 打比方我們需要1-10這10個位元組的資料, 但是我請求頭的range是不能直接寫1-10的, 因為按照我們每16個位元組是一個加密片段, 我們需要的1-10, 在0-15這個片段中, 所以我們必須要先請求下來0-15這一個片段, 然後解密, 再從中拿出1-10, 填充回去. 當然了還有一些細節就不展開敘述了, 等有機會結合專案單獨聊一聊AES這個解密方法.

總結

上面就是在實現邊下邊播過程中總結到的一些小點, 當然每個人在實際專案可能會遇到不一樣的問題. 同時本文沒有涉及到資料的快取, github上也有很多不錯的快取方案, 大家可以看看.

感謝閱讀.

相關文章