iOS m3u8本地緩存播放(控制下載并發、暫停恢復)

目錄
一、m3u8緩存播放的整個流程
二、控制媒體下載的并發數
三、控制單個媒體的切片下載并發數
四、下載的中斷和恢復
五、注意的問題與思路延伸
更新(相關demo會繼續完善,使用operation實現了并發控制的版本)

一、m3u8緩存播放的整個流程

1.下載m3u8文件
2.解析m3u8文件獲取視頻切片單元的信息。
3.根據2.獲取的視頻切片信息中的切片鏈接下載切片并保持到本地。
3.根據獲取的切片信息與本地服務器的配置信息,拼接出切片的本地地址、生成新的m3u8文件并保存到本地。
4.開啟本地服務器,使用本地url播放本地m3u8文件。

附上:時序圖,具體可看demo

image.png

二、控制媒體下載的并發數

   這里使用信號量來控制并發數
- (void)downloadVideoWithUrlString:(NSString *)urlStr downloadProgressHandler:(ZBLM3u8ManagerDownloadProgressHandler)downloadProgressHandler downloadSuccessBlock:(ZBLM3u8ManagerDownloadSuccessBlock) downloadSuccessBlock
{
    dispatch_async(_downloadQueue, ^{
        dispatch_semaphore_wait(_movieSemaphore, DISPATCH_TIME_FOREVER);
        __weak __typeof(self) weakself = self;
        [[self downloadContainerWithUrlString:urlStr] startDownloadWithUrlString:urlStr  downloadProgressHandler:^(float progress) {
            downloadProgressHandler(progress);
        } completaionHandler:^(NSString *locaLUrl, NSError *error) {
            if (!error) {
                [weakself.downloadContainerDictionary removeObjectForKey:[ZBLM3u8Setting uuidWithUrl:urlStr]];
                NSLog(@"下載完成:%@",urlStr);
                downloadSuccessBlock(locaLUrl);
            }
            else
            {
                NSLog(@"下載失敗:%@",error);
                [self resumeDownload];
            }
            NSLog(@"%@",weakself.downloadContainerDictionary.allKeys);
            dispatch_semaphore_signal(_movieSemaphore);
        }];
    });
}

這里可以設置_movieSemaphore的的初始值為具體的可同時下載數。
Example:_movieSemaphore = dispatch_semaphore_create(1),意味著同一時間只允許下載一個視頻,等同于視頻的串行下載。

三、控制單個媒體的切片下載并發數

   開始的時候,考慮使用AFURLSessionManager中的operationQueue.maxConcurrentOperationCount來控制并發。但這是行不通的。因為這個queue是用于回調而不是用于下載隊列。
/**
 The operation queue on which delegate callbacks are run.
 */
@property (readonly, nonatomic, strong) NSOperationQueue *operationQueue;

再看AF中初始化

- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration {
    self = [super init];
    if (!self) {
        return nil;
    }

    if (!configuration) {
        configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
    }

    self.sessionConfiguration = configuration;

    self.operationQueue = [[NSOperationQueue alloc] init];
    self.operationQueue.maxConcurrentOperationCount = 1;

    self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];
...
   這個queue確實是用于回調,而我是需要控制下載并發。這似乎不滿足。而且實測中也發現確實不行。那么,只能在任務發起哪里做并發控制,同樣,這里還是采用信號量。這里的控制相對復雜一點、因為后面的任務恢復、失敗任務重新創建也要做控制。
- (void)startDownload
{
    //因為這是外部調用的方法,操作的執行要放到異步線程中。避免因為并發控制中的等待而堵塞外部線程
    dispatch_async(self.downloadQueue, ^{
        if (!_fileDownloadInfos.count) {
            _completaionHandler(nil);
            return;
        }
        NSLog(@"downloadInfoCount:%ld",(long)_fileDownloadInfos.count);
        
        [_fileDownloadInfos enumerateObjectsUsingBlock:^(ZBLM3u8FileDownloadInfo * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            //控制切片下載并發
            dispatch_semaphore_wait(self.tsSemaphore, DISPATCH_TIME_FOREVER);
            if ([ZBLM3u8FileManager exitItemWithPath:obj.filePath]) {
                obj.success = YES;
                [self verifyDownloadCountAndCallbackByDownloadSuccess:YES];
            }
            else
            {
                //如果收到中斷信號,中斷下載流程釋放信號量并返回
                if (self.suspend) {
                    obj.beStopCreateTask = YES;
                    dispatch_semaphore_signal(self.tsSemaphore);
                    NSLog(@"suspend and return! don not createDownloadTask!");
                    return ;
                }
                else
                {
                    //真正的創建下載任務
                    [self createDownloadTaskWithIndex:idx];
                }
            }
        }];
    });
}

//信號量在每個任務的回調后都會釋放一次
- (void)verifyDownloadCountAndCallbackByDownloadSuccess:(BOOL) isSuccess
{
    dispatch_semaphore_signal(self.tsSemaphore);
...
  • 這里也看到信號量的控制問題,必須理清信號量的獲得和釋放時機,一次獲得必須有一次釋放。不釋放或者重復釋放,都會導致并發的控制不準。如果這樣,那這里的并發控制就沒有意義了。

  • 要做到準確獲取和釋放,重點在于理清程序的執行路徑。在每一條執行路徑中都必須釋放信號量。這個跟鎖的使用也是一樣的。

  • 調試現象:如果程序不像預料中運行,又沒有什么錯誤,那很有可能就是堵塞了。鎖沒有釋放或者信號量的處理有問題。
    處理步驟:點擊xcode調試欄的暫停按鈕,查看程序的調用棧,分析每個線程的運行情況,找到堵塞具體執行代碼。根據具體的邏輯修正問題。

四、下載的中斷和恢復

   這里有幾個小問題:根據判斷NSURLSessionTask 提供的3個方法可以做一些中斷和恢復處理
  • (void)suspend;
    掛起任務,但只能掛起執行中的任務。對于已經創建而且執行resum方法但并沒真正執行的任務無效(這里非??樱?。
    通常我們使用這個方法的時候會判斷下任務的具體狀態,如果是task.state == NSURLSessionTaskStateRunning采取執行 [task suspend]。但這個判斷是不準確的。如果一個任務創建并執行resume但并沒真正執行,它的狀態也是為NSURLSessionTaskStateRunning。如果這個時候程序收到中斷消息,對狀態為NSURLSessionTaskStateRunning 的任務全部執行suspend操作,你會發現有些任務不聽話,繼續執行。到底什么搞鬼...
    我的理解是這樣的,這些不聽話的任務正是那些添加到下載隊列中等待執行的任務,而在等待狀態下收到suspend消息是不管用的。但它接收cannel消息是管用的。那么問題的解決就是找出這些等待的任務。
    處理辦法:通過判斷接收字節數來區分狀態?,F在我只面向你接收的字節數,而不管你真開啟還是假開啟了。
switch (obj.downloadTask.state) {
                case NSURLSessionTaskStateRunning:
                {
                    //等待中,假開啟狀態,
                    if (obj.downloadTask.countOfBytesReceived <= 0) {
                        [obj.downloadTask cancel];
                    }
                    else
                    {
                    //正在下載,真開啟狀態,
                        [obj.downloadTask suspend];
                    }
                }
                    break;
  • (void)resume;
    官方文檔是這么說明的:Resumes the task, if it is suspended.意思是指只能發起被掛起的任務。
    存在兩種情況:
    1.新創建的任務并沒有執行resume,此時狀態為:NSURLSessionTaskStateSuspended
    2.執行suspend方法后被手動掛起的任務,狀態同為NSURLSessionTaskStateSuspended。
    同時這里提供了額外信息:狀態為NSURLSessionTaskStateCompleted的任務是不能通過resume重新發起的。而在某些情況下我們需要對這種狀態的任務重新發起,包括手動cannel的、執行失敗的。這種情況下只能根據具體的情況,重新創建任務并發起。
if (obj.downloadTask.error && 
    obj.downloadTask.state == NSURLSessionTaskStateCompleted)
{
              //下載失敗的任務重新創建
                [self createDownloadTaskWithIndex:idx];
 }
  • (void)cancel;
    官方文檔說明:

This method returns immediately, marking the task as being canceled. Once a task is marked as being canceled, URLSession:task:didCompleteWithError: will be sent to the task delegate, passing an error in the domain NSURLErrorDomain with the code NSURLErrorCancelled. A task may, under some circumstances, send messages to its delegate before the cancelation is acknowledged.
This method may be called on a task that is suspended.

簡而言之:調用這個方法可以cannel任意狀態的任務包括掛起的任務。并執行didCompleteWithError回調(AF中會執行回調并返回錯誤NSURLErrorCancelled),狀態被標記為NSURLSessionTaskStateCompleted。故上面恢復失敗任務的時候,通過task.state和task.error共同判斷。

總結下任務生命周期中的任務狀態變化:

1.創建成功:...Suspended
2.執行resume:...Running(可以通過判斷countOfBytesReceived來區分任務處于等待還是下載中)
3.執行suspend:...Suspended
4.執行cannel:(中間狀態...Canceling)->...Completed(可以結合task.error判斷任務執行結果)

回歸正題:
視頻單元的中斷和恢復,中斷就是調用suspend方法掛起正在執行的任務,cannel掉等待的任務,代碼跟上面說明方法的時候非常雷同。這里著重說明下恢復。如果單單恢復其實很簡單,恢復掛起的任務和重新創建錯誤的任務。這只是從程序的角度看待,要使一個程序有更高的可用性,應在功能實現的同時做的更加的合理。應優先恢復掛起的任務、然后重新創建錯誤的任務。這樣做都是為了承前啟后更快的把一個下載任務完成。而且這個視頻切片是講究有序的,所以我們恢復的時候也要遵從FIFO的原則。

五、注意的問題與思路延伸

1.解析m3u8注意的問題
meu8的解析格式太多,很容易出現問題,應使用try/catch來保證程序的健壯性。

2.根據url獲取原始m3u8文件信息,這個操作太耗時了。為了提高程序的效率,獲取到原始m3u8文件后應做本地持久化并用于二次下載。如果整個流程下載成功,可以選擇刪除該文件,或者不操作。由于是純文本文件,少量的文件冗余是允許的。

3.文件的操作通過開啟一個同步隊列來處??梢栽O定low優先級避免占用太高的cpu資源。其實高cpu占用會伴隨著另外一個問題,手機的發熱量。

4.key的處理問題
如果存在key的下載,需要把key下載到本地,約定好key的名稱和新建m3u8文件中的key鏈接。這樣本地播放就能正常加解密。
例如下載到本地的key保存為.../key。那么鏈接應該是http:localhost:port/.../key

5.中斷的優先級
程序的中斷操作擁有最高優先級的,因為要任何狀態下都能中斷下載。無論是為了程序的流暢性、網絡變為移動信號避免使用用戶的移動流量等發出中斷命令,都必須立即響應。

6.切片數量的全局分配
多個視頻同時下載,多個切片同時并發。如果要做到控制全局的切片并發數而不是單個視頻的切片并發數。這就要設計一個算法在全局Manger哪里做分配和回收。

7.保證app流程,監控網速開啟和中斷下載
下載視頻的功能應該要保證app本身網絡請求的正常運行。
app如何獲取到網絡的帶寬,好像只能通過下載文件方式來推算??稍趹谜埱罂臻e時通過短時間下載一個可用源來計算帶寬,同時監控app 實時網絡吞吐,適當的開關下載。雖然很難做到實時,但是在切換網絡的時候進行帶寬重測、又或者地理位置變化一定距離后進行帶寬重測、又或者定時作帶寬重測。還有就是考慮wifi狀態下才進行下載。

8.切換網絡后請求失去連接,恢復下載的問題
因為網絡切換本地ip變化,發起的請求會失去連接。如果通過downloadTask是沒辦法做到恢復下載的。雖然可以用resumeData來恢復下載,但是這個只能在cannel操作的時候獲取,至于失去連接的情況下是沒辦法獲取到的(系統提供的api中沒有在失敗回調哪里返回resumeData的)。(思路是這樣,不一定能實現)這個時候需要自己創建文件句柄,使用dataTask做到文件續下。初始化dataTask的時候設定請求頭'Accept-Ranges'參數為文件的已下載字節數(需要服務器支持),就可以獲取到未下載的部分數據。

9.并發中鎖的處理
要理清那些代碼可能存在并發,那些操作要保證原子性。難就難在一個方法中會存在部分代碼塊是并發執行的,這有利于效率的提高;部分些代碼要原子操作。最優的做法就是對原子操作用鎖來保證,沒有任何多余的代碼加入到同步操作中,這樣也是效率最高的。而拿捏不準的情況下,可以鎖定更多的代碼,至少這樣不會因為并發而導致問題,但這樣就犧牲了效率和及時性。當一個簡單的系統,要做到最優好像并不難,但是一個復雜的系統做到最優就非常難了或者是要花費非常大的精力。基于這個demo的實現多線程流了不少坑,總結下多線程還是復雜。開發中優先考慮線程安全,再提高性能吧。

10.是否需要全部切片下載完成才能播放
其實并不需要全部下載完成就能播放的。保證key 先下載下來,而且要保證有序下載,然后下載一定量的切片文件,這個時候就可以組裝m3u8文件到本地,發起播放。只要后面下載的切片能滿足播放器的播放,就不會出現問題。但如果供應不足視頻就會停了,播放不了,盡管后面文件下載下來了,還是不能自動恢復,仿佛失去了緩沖功能。這里就是跟直接請求服務器的差別了,直接請求服務器,因為文件本身是存在的,發起的請求是存在的,如果網速慢,播放器的反應是緩沖;而本地服務播放就不同了,如果文件在播放前沒有下載下來,發起的請求立馬就掛了,這個請求不存在,當然就不存在緩沖。

11.線程多開占用資源,每個線程占用512K到1M空間。建議使用單線程下載,且穩定性高。

dome雖然實現了多線程下載,偶發死鎖的問題會存在,就是有坑?。。?。但m3u8文本文件跟數據解析部分處理是穩定的。(已更新修正部分問題。)
鏈接:https://github.com/zmubai/ZBLM3U8DownLoadTest

更新:
  1. 之前的demo問題不少,就抽空改了下,問題有所改善,但還不能達到穩定。就又弄了一個簡單的demo,把主要功能實現,做到簡單點穩定點。
    地址:https://github.com/zmubai/m3u8DownloadSimpleDemo[2019-4-7]
  2. 要實現并發控制,使用信號量的方式是可以實現的,但有一個很大的缺點,就是暫停控制變得麻煩。假設并發控制數為2,同時發起10個,那么有8個在等待,如果執行暫停,那么已經執行的2個可以暫停,但等待的8個取消不了,需要讓他們發起,然后再取消,操作變得很麻煩。衡量了一下,可以使用NSOperationQueue去控制并發,繼承NSOperation,創建子類,并通過設置finish變量為true,讓任務完成。只有當finish為true,任務才會在隊列中移除,這樣就能控制并發,并且更易于執行cannel等操作。[2019-4-7]

3.由于之前版本的實現不是特別滿意,就使用operation實現的版本。(支持使用cocoaPods 安裝)
支持媒體并發控制,支持單個媒體文件并發控制。支持任務取消,支持任務掛起和恢復。

地址:https://github.com/zmubai/BNM3u8Cache[2019-12]

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,835評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,676評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,730評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,118評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,873評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,266評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,330評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,482評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,036評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,846評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,025評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,575評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,279評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,684評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,953評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,751評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,016評論 2 375

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,669評論 25 708
  • 發現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,154評論 4 61
  • 強烈推薦snapseed+spring! 快點上手學起來吧! 人人都是“新手大師”!
    菩葉生閱讀 180評論 0 0
  • (在家訪工作推進會上的發言) 各位領導、老師: 大家下午好!今天能與各位交流我的家訪故事和心得感...
    常星慧閱讀 1,383評論 0 2
  • 《我的前半生》經典語錄,句句是毒藥 我的前半生,就像一場夢。你的后半生,愿君多珍重。結婚為什么?不就是人生不易,要...
    張錦全閱讀 277評論 0 0