iOS視頻邊下邊播--緩存播放數(shù)據(jù)流

轉(zhuǎn)自博客:iOS視頻邊下邊播--緩存播放數(shù)據(jù)流

google搜索“iOS視頻變下邊播”,有好幾篇博客寫到了實(shí)現(xiàn)方法固额,其實(shí)只有一篇头谜,其他都是copy的狐蜕,不過他們都是使用的本地代理服務(wù)器的方式宠纯,原理很簡(jiǎn)單,但是缺點(diǎn)也很明顯层释,需要自己寫一個(gè)本地代理服務(wù)器或者使用第三方庫httpSever婆瓜。如果使用httpSever作為本地代理服務(wù)器,如果只緩存一個(gè)視頻是沒有問題的贡羔,如果緩存多個(gè)視頻互相切換廉白,本地代理服務(wù)器提供的數(shù)據(jù)很不穩(wěn)定,crash概率非常大乖寒。


這里我采用ios7以后系統(tǒng)自帶的方法實(shí)現(xiàn)視頻邊下邊播猴蹂,這里的邊下邊播不是單獨(dú)開一個(gè)子線程去下載,而是把視頻播放的數(shù)據(jù)給保存到本地楣嘁。簡(jiǎn)而言之磅轻,就是使用一遍的流量,既播放了視頻逐虚,也保存了視頻聋溜。

用到的框架:<AVFoundation/AVFoundation.h>

用到的播放器:AVplayer

先說一下avplayer自身的播放原理,當(dāng)我們給播放器設(shè)置好url等一些參數(shù)后叭爱,播放器就會(huì)向url所在的服務(wù)器發(fā)送請(qǐng)求(請(qǐng)求參數(shù)有兩個(gè)值撮躁,一個(gè)是offset偏移量,另一個(gè)是length長(zhǎng)度涤伐,其實(shí)就相當(dāng)于NSRange一樣)馒胆,服務(wù)器就根據(jù)range參數(shù)給播放器返回?cái)?shù)據(jù)缨称。這就是大致的原理凝果,當(dāng)然實(shí)際的過程還是略微比較復(fù)雜。


下面進(jìn)入主題

產(chǎn)品需求:

  • 1.支持正常播放器的一切功能睦尽,包括暫停器净、播放和拖拽

  • 2.如果視頻加載完成且完整,將視頻文件保存到本地cache当凡,下一次播放本地cache中的視頻山害,不再請(qǐng)求網(wǎng)絡(luò)數(shù)據(jù)

  • 3.如果視頻沒有加載完(半路關(guān)閉或者拖拽)就不用保存到本地cache

實(shí)現(xiàn)方案:

  • 1.需要在視頻播放器和服務(wù)器之間添加一層類似代理的機(jī)制,視頻播放器不再直接訪問服務(wù)器沿量,而是訪問代理對(duì)象浪慌,代理對(duì)象去訪問服務(wù)器獲得數(shù)據(jù),之后返回給視頻播放器朴则,同時(shí)代理對(duì)象根據(jù)一定的策略緩存數(shù)據(jù)权纤。

  • 2.AVURLAsset中的resourceLoader可以實(shí)現(xiàn)這個(gè)機(jī)制,resourceLoader的delegate就是上述的代理對(duì)象。

  • 3.視頻播放器在開始播放之前首先檢測(cè)是本地cache中是否有此視頻汹想,如果沒有才通過代理獲得數(shù)據(jù)外邓,如果有,則直接播放本地cache中的視頻即可古掏。

視頻播放器需要實(shí)現(xiàn)的功能

  • 1.有開始暫停按鈕

  • 2.顯示播放進(jìn)度及總時(shí)長(zhǎng)

  • 3.可以通過拖拽從任意位置開始播放視頻

  • 4.視頻加載中的過程和加載失敗需要有相應(yīng)的提示

代理對(duì)象需要實(shí)現(xiàn)的功能

  • 1.接收視頻播放器的請(qǐng)求损话,并根據(jù)請(qǐng)求的range向服務(wù)器請(qǐng)求本地沒有獲得的數(shù)據(jù)

  • 2.緩存向服務(wù)器請(qǐng)求回的數(shù)據(jù)到本地

  • 3.如果向服務(wù)器的請(qǐng)求出現(xiàn)錯(cuò)誤,需要通知給視頻播放器槽唾,以便視頻播放器對(duì)用戶進(jìn)行提示

具體流程圖

971366-0a9b11be2df75aaa.png

視頻播放器處理流程

  • 1.當(dāng)開始播放視頻時(shí)丧枪,通過視頻url判斷本地cache中是否已經(jīng)緩存當(dāng)前視頻,如果有夏漱,則直接播放本地cache中視頻

  • 2.如果本地cache中沒有視頻豪诲,則視頻播放器向代理請(qǐng)求數(shù)據(jù)

  • 3.加載視頻時(shí)展示正在加載的提示(菊花轉(zhuǎn))

  • 4.如果可以正常播放視頻,則去掉加載提示挂绰,播放視頻屎篱,如果加載失敗,去掉加載提示并顯示失敗提示

  • 5.在播放過程中如果由于網(wǎng)絡(luò)過慢或拖拽原因?qū)е聸]有播放數(shù)據(jù)時(shí)葵蒂,要展示加載提示交播,跳轉(zhuǎn)到第4步

代理對(duì)象處理流程

  • 1.當(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))

  • 3.如果當(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))

  • 4.如果當(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))

  • 5.只要代理重新向服務(wù)器發(fā)起請(qǐng)求幕庐,就會(huì)導(dǎo)致緩存的數(shù)據(jù)不連續(xù),則加載結(jié)束后不用將緩存的數(shù)據(jù)放入本地cache

  • 6.如果代理和服務(wù)器的鏈接超時(shí)家淤,重試一次异剥,如果還是錯(cuò)誤則通知播放器網(wǎng)絡(luò)錯(cuò)誤

  • 7.如果服務(wù)器返回其他錯(cuò)誤,則代理通知播放器網(wǎng)絡(luò)錯(cuò)誤


resourceLoader的難點(diǎn)處理

- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest
{
    [self.pendingRequests addObject:loadingRequest];
    [self dealWithLoadingRequest:loadingRequest];

    return YES;
}

播放器發(fā)出的數(shù)據(jù)請(qǐng)求從這里開始絮重,我們保存從這里發(fā)出的所有請(qǐng)求存放到數(shù)組冤寿,自己來處理這些請(qǐng)求错妖,當(dāng)一個(gè)請(qǐng)求完成后,對(duì)請(qǐng)求發(fā)出finishLoading消息疚沐,并從數(shù)組中移除暂氯。正常狀態(tài)下,當(dāng)播放器發(fā)出下一個(gè)請(qǐng)求的時(shí)候亮蛔,會(huì)把上一個(gè)請(qǐng)求給finish痴施。

下面這個(gè)方法發(fā)出的請(qǐng)求說明播放器自己關(guān)閉了這個(gè)請(qǐng)求,我們不需要再對(duì)這個(gè)請(qǐng)求進(jìn)行處理究流,系統(tǒng)每次結(jié)束一個(gè)舊的請(qǐng)求辣吃,便必然會(huì)發(fā)出一個(gè)或多個(gè)新的請(qǐng)求,除了播放器已經(jīng)獲得整個(gè)視頻完整的數(shù)據(jù)芬探,這時(shí)候就不會(huì)再發(fā)起請(qǐng)求神得。

- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest
{
    [self.pendingRequests removeObject:loadingRequest];

}

下面這個(gè)方法是對(duì)播放器發(fā)出的請(qǐng)求進(jìn)行填充數(shù)據(jù)

- (BOOL)respondWithDataForRequest:(AVAssetResourceLoadingDataRequest *)dataRequest
{
    long long startOffset = dataRequest.requestedOffset;

    if (dataRequest.currentOffset != 0) {
        startOffset = dataRequest.currentOffset;
    }

    if ((self.task.offset +self.task.downLoadingOffset) < startOffset)
    {
        //NSLog(@"NO DATA FOR REQUEST");
        return NO;
    }

    if (startOffset < self.task.offset) {
        return NO;
    }

    NSData *filedata = [NSData dataWithContentsOfURL:[NSURL fileURLWithPath:_videoPath] options:NSDataReadingMappedIfSafe error:nil];

    // This is the total data we have from startOffset to whatever has been downloaded so far
    NSUInteger unreadBytes = self.task.downLoadingOffset - ((NSInteger)startOffset - self.task.offset);

    // Respond with whatever is available if we can't satisfy the request fully yet
    NSUInteger numberOfBytesToRespondWith = MIN((NSUInteger)dataRequest.requestedLength, unreadBytes);

    [dataRequest respondWithData:[filedata subdataWithRange:NSMakeRange((NSUInteger)startOffset- self.task.offset, (NSUInteger)numberOfBytesToRespondWith)]];

    long long endOffset = startOffset + dataRequest.requestedLength;
    BOOL didRespondFully = (self.task.offset + self.task.downLoadingOffset) >= endOffset;

    return didRespondFully;

}

這是對(duì)存放所有的請(qǐng)求的數(shù)組進(jìn)行處理

- (void)processPendingRequests
{
    NSMutableArray *requestsCompleted = [NSMutableArray array];  //請(qǐng)求完成的數(shù)組
    //每次下載一塊數(shù)據(jù)都是一次請(qǐng)求,把這些請(qǐng)求放到數(shù)組偷仿,遍歷數(shù)組
    for (AVAssetResourceLoadingRequest *loadingRequest in self.pendingRequests)
    {
        [self fillInContentInformation:loadingRequest.contentInformationRequest]; //對(duì)每次請(qǐng)求加上長(zhǎng)度哩簿,文件類型等信息

        BOOL didRespondCompletely = [self respondWithDataForRequest:loadingRequest.dataRequest]; //判斷此次請(qǐng)求的數(shù)據(jù)是否處理完全

        if (didRespondCompletely) {

            [requestsCompleted addObject:loadingRequest];  //如果完整,把此次請(qǐng)求放進(jìn) 請(qǐng)求完成的數(shù)組
            [loadingRequest finishLoading];

        }
    }

    [self.pendingRequests removeObjectsInArray:requestsCompleted];   //在所有請(qǐng)求的數(shù)組中移除已經(jīng)完成的

}

resourceLoader的難點(diǎn)基本上就是上面這點(diǎn)了酝静,說到播放器节榜,下面便順便講下AVPlayer的難點(diǎn)。

難點(diǎn):對(duì)播放器狀態(tài)的捕獲

舉個(gè)簡(jiǎn)單的例子别智,視頻總長(zhǎng)度60分宗苍,現(xiàn)在緩沖的數(shù)據(jù)才10分鐘,然后拖動(dòng)到20分鐘的位置進(jìn)行播放薄榛,在網(wǎng)速較慢的時(shí)候讳窟,視頻從當(dāng)前位置開始播放,必然會(huì)出現(xiàn)一段時(shí)間的卡頓敞恋,為了有一個(gè)更好的用戶體驗(yàn)丽啡,在卡頓的時(shí)候,我們需要加一個(gè)菊花轉(zhuǎn)的狀態(tài)耳舅,現(xiàn)在問題就來了碌上。

在拖動(dòng)到未緩沖區(qū)域內(nèi)倚评,是否需要加菊花轉(zhuǎn)浦徊,如果加,要顯示多久再消失天梧,而且如果在網(wǎng)速很慢的時(shí)候盔性,播放器如果等了太久,哪怕最后有數(shù)據(jù)了呢岗,播放器也已經(jīng)“死”了冕香,它自己無法恢復(fù)播放蛹尝,這個(gè)時(shí)候需要我們?nèi)藶榈娜セ謴?fù)播放,如果恢復(fù)播放不成功悉尾,那么過一段時(shí)間需要再次恢復(fù)播放突那,是否恢復(fù)播放成功,這里也需要捕獲其狀態(tài)构眯。所以愕难,如果要有一個(gè)好的用戶體驗(yàn),我們需要時(shí)時(shí)知道播放器的狀態(tài)惫霸。

有兩個(gè)狀態(tài)需要捕獲猫缭,一個(gè)是正在緩沖,一個(gè)是正在播放壹店,監(jiān)聽播放的“playbackBufferEmpty”屬性就可以捕獲正在緩沖狀態(tài)猜丹,播放器的時(shí)間監(jiān)聽器則可以捕獲正在播放狀態(tài),我的demo中一共有4個(gè)狀態(tài):

typedef NS_ENUM(NSInteger, TBPlayerState) {
    TBPlayerStateBuffering = 1,
    TBPlayerStatePlaying   = 2,
    TBPlayerStateStopped   = 3,
    TBPlayerStatePause     = 4
};

這樣可以對(duì)播放器更好的把握和處理了硅卢。
然后說一說在緩沖時(shí)候的處理射窒,以及緩沖后多久去播放,處理方法:
進(jìn)入緩沖狀態(tài)后将塑,緩沖2秒后去手動(dòng)播放轮洋,如果播放不成功(緩沖的數(shù)據(jù)太少,還不足以播放)抬旺,那就再緩沖2秒再次播放弊予,如此循環(huán),看詳細(xì)代碼:

- (void)bufferingSomeSecond
{
    // playbackBufferEmpty會(huì)反復(fù)進(jìn)入开财,因此在bufferingOneSecond延時(shí)播放執(zhí)行完之前再調(diào)用bufferingSomeSecond都忽略
    static BOOL isBuffering = NO;
    if (isBuffering) {
        return;
    }
    isBuffering = YES;

    // 需要先暫停一小會(huì)之后再播放汉柒,否則網(wǎng)絡(luò)狀況不好的時(shí)候時(shí)間在走,聲音播放不出來
    [self.player pause];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

        // 如果此時(shí)用戶已經(jīng)暫停了责鳍,則不再需要開啟播放了
        if (self.isPauseByUser) {
            isBuffering = NO;
            return;
        }

        [self.player play];
        // 如果執(zhí)行了play還是沒有播放則說明還沒有緩存好碾褂,則再次緩存一段時(shí)間
        isBuffering = NO;
        if (!self.currentPlayerItem.isPlaybackLikelyToKeepUp) {
            [self bufferingSomeSecond];
        }
    });
}

這個(gè)demo花了我很長(zhǎng)的時(shí)間,實(shí)現(xiàn)這個(gè)demo我也遇到了很多坑最后才完成的历葛,現(xiàn)在我奉獻(xiàn)出來正塌,也許對(duì)你會(huì)有所幫助。如果你覺得不錯(cuò)恤溶,還請(qǐng)為我Star一個(gè)乓诽,也算是對(duì)我的支持和鼓勵(lì)。
參考文章

demo下載地址

作者:夜千尋墨
鏈接:http://www.reibang.com/p/990ee3db0563
來源:簡(jiǎn)書
著作權(quán)歸作者所有咒程。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán)鸠天,非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末帐姻,一起剝皮案震驚了整個(gè)濱河市稠集,隨后出現(xiàn)的幾起案子奶段,更是在濱河造成了極大的恐慌,老刑警劉巖剥纷,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件痹籍,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡晦鞋,警方通過查閱死者的電腦和手機(jī)词裤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鳖宾,“玉大人吼砂,你說我怎么就攤上這事《ξ模” “怎么了渔肩?”我有些...
    開封第一講書人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)拇惋。 經(jīng)常有香客問我周偎,道長(zhǎng),這世上最難降的妖魔是什么撑帖? 我笑而不...
    開封第一講書人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任蓉坎,我火速辦了婚禮,結(jié)果婚禮上胡嘿,老公的妹妹穿的比我還像新娘蛉艾。我一直安慰自己,他們只是感情好衷敌,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開白布勿侯。 她就那樣靜靜地躺著,像睡著了一般缴罗。 火紅的嫁衣襯著肌膚如雪助琐。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評(píng)論 1 305
  • 那天面氓,我揣著相機(jī)與錄音兵钮,去河邊找鬼。 笑死舌界,一個(gè)胖子當(dāng)著我的面吹牛掘譬,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播禀横,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼屁药,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼粥血!你這毒婦竟也來了柏锄?” 一聲冷哼從身側(cè)響起酿箭,我...
    開封第一講書人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎趾娃,沒想到半個(gè)月后缭嫡,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡抬闷,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年妇蛀,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片笤成。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡评架,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出炕泳,到底是詐尸還是另有隱情纵诞,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布培遵,位于F島的核電站浙芙,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏籽腕。R本人自食惡果不足惜嗡呼,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望皇耗。 院中可真熱鬧南窗,春花似錦、人聲如沸郎楼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽箭启。三九已至壕翩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間傅寡,已是汗流浹背放妈。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留荐操,地道東北人芜抒。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像托启,于是被迫代替她去往敵國和親宅倒。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容