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

轉(zhuǎn)載鏈接:http://www.reibang.com/p/990ee3db0563

google搜索“iOS視頻變下邊播”,有好幾篇博客寫到了實(shí)現(xiàn)方法,其實(shí)只有一篇吕嘀,其他都是copy的弯院,不過他們都是使用的本地代理服務(wù)器的方式,原理很簡單纵潦,但是缺點(diǎn)也很明顯徐鹤,需要自己寫一個本地代理服務(wù)器或者使用第三方庫httpSever。如果使用httpSever作為本地代理服務(wù)器邀层,如果只緩存一個視頻是沒有問題的返敬,如果緩存多個視頻互相切換,本地代理服務(wù)器提供的數(shù)據(jù)很不穩(wěn)定寥院,crash概率非常大劲赠。

這里我采用ios7以后系統(tǒng)自帶的方法實(shí)現(xiàn)視頻邊下邊播,這里的邊下邊播不是單獨(dú)開一個子線程去下載秸谢,而是把視頻播放的數(shù)據(jù)給保存到本地凛澎。簡而言之,就是使用一遍的流量估蹄,既播放了視頻塑煎,也保存了視頻。

用到的框架:用到的播放器:AVplayer

先說一下avplayer自身的播放原理元媚,當(dāng)我們給播放器設(shè)置好url等一些參數(shù)后轧叽,播放器就會向url所在的服務(wù)器發(fā)送請求(請求參數(shù)有兩個值苗沧,一個是offset偏移量,另一個是length長度炭晒,其實(shí)就相當(dāng)于NSRange一樣)待逞,服務(wù)器就根據(jù)range參數(shù)給播放器返回數(shù)據(jù)。這就是大致的原理网严,當(dāng)然實(shí)際的過程還是略微比較復(fù)雜识樱。

下面進(jìn)入主題

產(chǎn)品需求:

1.支持正常播放器的一切功能,包括暫停震束、播放和拖拽

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

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

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

1.需要在視頻播放器和服務(wù)器之間添加一層類似代理的機(jī)制割疾,視頻播放器不再直接訪問服務(wù)器,而是訪問代理對象嘉栓,代理對象去訪問服務(wù)器獲得數(shù)據(jù)宏榕,之后返回給視頻播放器,同時代理對象根據(jù)一定的策略緩存數(shù)據(jù)侵佃。

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

3.視頻播放器在開始播放之前首先檢測是本地cache中是否有此視頻馋辈,如果沒有才通過代理獲得數(shù)據(jù)抚芦,如果有,則直接播放本地cache中的視頻即可迈螟。

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

1.有開始暫停按鈕

2.顯示播放進(jìn)度及總時長

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

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

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

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

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

3.如果向服務(wù)器的請求出現(xiàn)錯誤,需要通知給視頻播放器答毫,以便視頻播放器對用戶進(jìn)行提示

具體流程圖


971366-0a9b11be2df75aaa.png

視頻播放器處理流程

1.當(dāng)開始播放視頻時卜壕,通過視頻url判斷本地cache中是否已經(jīng)緩存當(dāng)前視頻,如果有烙常,則直接播放本地cache中視頻

2.如果本地cache中沒有視頻轴捎,則視頻播放器向代理請求數(shù)據(jù)

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

4.如果可以正常播放視頻,則去掉加載提示蚕脏,播放視頻侦副,如果加載失敗,去掉加載提示并顯示失敗提示

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

代理對象處理流程

1.當(dāng)視頻播放器向代理請求dataRequest時,判斷代理是否已經(jīng)向服務(wù)器發(fā)起了請求挣棕,如果沒有译隘,則發(fā)起下載整個視頻文件的請求

2.如果代理已經(jīng)和服務(wù)器建立鏈接亲桥,則判斷當(dāng)前的dataRequest請求的offset是否大于當(dāng)前已經(jīng)緩存的文件的offset,如果大于則取消當(dāng)前與服務(wù)器的請求固耘,并從offset開始到文件尾向服務(wù)器發(fā)起請求(此時應(yīng)該是由于播放器向后拖拽题篷,并且超過了已緩存的數(shù)據(jù)時才會出現(xiàn))

3.如果當(dāng)前的dataRequest請求的offset小于已經(jīng)緩存的文件的offset,同時大于代理向服務(wù)器請求的range的offset厅目,說明有一部分已經(jīng)緩存的數(shù)據(jù)可以傳給播放器番枚,則將這部分?jǐn)?shù)據(jù)返回給播放器(此時應(yīng)該是由于播放器向前拖拽,請求的數(shù)據(jù)已經(jīng)緩存過才會出現(xiàn))

4.如果當(dāng)前的dataRequest請求的offset小于代理向服務(wù)器請求的range的offset损敷,則取消當(dāng)前與服務(wù)器的請求葫笼,并從offset開始到文件尾向服務(wù)器發(fā)起請求(此時應(yīng)該是由于播放器向前拖拽,并且超過了已緩存的數(shù)據(jù)時才會出現(xiàn))

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

6.如果代理和服務(wù)器的鏈接超時,重試一次诱桂,如果還是錯誤則通知播放器網(wǎng)絡(luò)錯誤

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

resourceLoader的難點(diǎn)處理


- (BOOL)resourceLoader:(AVAssetResourceLoader*)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest*)loadingRequest

{   

 [self.pendingRequests addObject:loadingRequest];    [selfdealWithLoadingRequest:loadingRequest];returnYES;

}

播放器發(fā)出的數(shù)據(jù)請求從這里開始,我們保存從這里發(fā)出的所有請求存放到數(shù)組访诱,自己來處理這些請求,當(dāng)一個請求完成后韩肝,對請求發(fā)出finishLoading消息触菜,并從數(shù)組中移除。正常狀態(tài)下哀峻,當(dāng)播放器發(fā)出下一個請求的時候涡相,會把上一個請求給finish。

下面這個方法發(fā)出的請求說明播放器自己關(guān)閉了這個請求剩蟀,我們不需要再對這個請求進(jìn)行處理催蝗,系統(tǒng)每次結(jié)束一個舊的請求,便必然會發(fā)出一個或多個新的請求育特,除了播放器已經(jīng)獲得整個視頻完整的數(shù)據(jù)丙号,這時候就不會再發(fā)起請求。


- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoaderdidCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest

{    

[self.pendingRequestsremoveObject:loadingRequest];

}

下面這個方法是對播放器發(fā)出的請求進(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;

}

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


- (void)processPendingRequests

{

NSMutableArray*requestsCompleted = [NSMutableArrayarray];

//請求完成的數(shù)組//每次下載一塊數(shù)據(jù)都是一次請求缰冤,把這些請求放到數(shù)組犬缨,遍歷數(shù)組for(AVAssetResourceLoadingRequest *loadingRequestin self.pendingRequests)    {       

 [self fillInContentInformation:loadingRequest.contentInformationRequest];

//對每次請求加上長度,文件類型等信息

BOOL didRespondCompletely =[self respondWithDataForRequest:loadingRequest.dataRequest];

//判斷此次請求的數(shù)據(jù)是否處理完全

if(didRespondCompletely) {           

 [requestsCompleted addObject:loadingRequest];

//如果完整棉浸,把此次請求放進(jìn) 請求完成的數(shù)組

[loadingRequest finishLoading];       

 }    

}    

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

}

resourceLoader的難點(diǎn)基本上就是上面這點(diǎn)了怀薛,說到播放器,下面便順便講下AVPlayer的難點(diǎn)迷郑。

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

舉個簡單的例子枝恋,視頻總長度60分创倔,現(xiàn)在緩沖的數(shù)據(jù)才10分鐘,然后拖動到20分鐘的位置進(jìn)行播放焚碌,在網(wǎng)速較慢的時候畦攘,視頻從當(dāng)前位置開始播放,必然會出現(xiàn)一段時間的卡頓呐能,為了有一個更好的用戶體驗(yàn)念搬,在卡頓的時候,我們需要加一個菊花轉(zhuǎn)的狀態(tài)摆出,現(xiàn)在問題就來了朗徊。

在拖動到未緩沖區(qū)域內(nèi),是否需要加菊花轉(zhuǎn)偎漫,如果加爷恳,要顯示多久再消失,而且如果在網(wǎng)速很慢的時候象踊,播放器如果等了太久温亲,哪怕最后有數(shù)據(jù)了,播放器也已經(jīng)“死”了杯矩,它自己無法恢復(fù)播放栈虚,這個時候需要我們?nèi)藶榈娜セ謴?fù)播放,如果恢復(fù)播放不成功史隆,那么過一段時間需要再次恢復(fù)播放魂务,是否恢復(fù)播放成功暂论,這里也需要捕獲其狀態(tài)亲族。所以,如果要有一個好的用戶體驗(yàn)乞榨,我們需要時時知道播放器的狀態(tài)熔酷。

有兩個狀態(tài)需要捕獲孤紧,一個是正在緩沖,一個是正在播放拒秘,監(jiān)聽播放的“playbackBufferEmpty”屬性就可以捕獲正在緩沖狀態(tài)号显,播放器的時間監(jiān)聽器則可以捕獲正在播放狀態(tài),我的demo中一共有4個狀態(tài):


typedef NS_ENUM(NSInteger,TBPlayerState) {

TBPlayerStateBuffering= 1,

TBPlayerStatePlaying= 2,

TBPlayerStateStopped= 3,

TBPlayerStatePause= 4

};

這樣可以對播放器更好的把握和處理了躺酒。

然后說一說在緩沖時候的處理咙轩,以及緩沖后多久去播放,處理方法:

進(jìn)入緩沖狀態(tài)后阴颖,緩沖2秒后去手動播放活喊,如果播放不成功(緩沖的數(shù)據(jù)太少,還不足以播放),那就再緩沖2秒再次播放钾菊,如此循環(huán)帅矗,看詳細(xì)代碼:


- (void)bufferingSomeSecond

{

// playbackBufferEmpty會反復(fù)進(jìn)入,因此在bufferingOneSecond延時播放執(zhí)行完之前再調(diào)用bufferingSomeSecond都忽略

static BOOL isBuffering = NO;

if (isBuffering) {

return;

}

isBuffering = YES;

// 需要先暫停一小會之后再播放煞烫,否則網(wǎng)絡(luò)狀況不好的時候時間在走浑此,聲音播放不出來

[self.player pause];

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

// 如果此時用戶已經(jīng)暫停了,則不再需要開啟播放了

if (self.isPauseByUser) {

isBuffering = NO;

return;

}

[self.player play];

// 如果執(zhí)行了play還是沒有播放則說明還沒有緩存好滞详,則再次緩存一段時間

isBuffering = NO;

if (!self.currentPlayerItem.isPlaybackLikelyToKeepUp) {

[self bufferingSomeSecond];

}

});

}

這個demo花了我很長的時間凛俱,實(shí)現(xiàn)這個demo我也遇到了很多坑最后才完成的,現(xiàn)在我奉獻(xiàn)出來料饥,也許對你會有所幫助蒲犬。如果你覺得不錯,還請為我Star一個岸啡,也算是對我的支持和鼓勵原叮。

參考文章

demo下載地址

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市巡蘸,隨后出現(xiàn)的幾起案子奋隶,更是在濱河造成了極大的恐慌,老刑警劉巖悦荒,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件唯欣,死亡現(xiàn)場離奇詭異,居然都是意外死亡搬味,警方通過查閱死者的電腦和手機(jī)境氢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來身腻,“玉大人,你說我怎么就攤上這事匹厘∴痔耍” “怎么了?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵愈诚,是天一觀的道長她按。 經(jīng)常有香客問我,道長炕柔,這世上最難降的妖魔是什么酌泰? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮匕累,結(jié)果婚禮上陵刹,老公的妹妹穿的比我還像新娘。我一直安慰自己欢嘿,他們只是感情好衰琐,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布也糊。 她就那樣靜靜地躺著,像睡著了一般羡宙。 火紅的嫁衣襯著肌膚如雪狸剃。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天狗热,我揣著相機(jī)與錄音钞馁,去河邊找鬼。 笑死匿刮,一個胖子當(dāng)著我的面吹牛僧凰,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播僻焚,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼允悦,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了虑啤?” 一聲冷哼從身側(cè)響起隙弛,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎狞山,沒想到半個月后全闷,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡萍启,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年总珠,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片勘纯。...
    茶點(diǎn)故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡局服,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出驳遵,到底是詐尸還是另有隱情淫奔,我是刑警寧澤,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布堤结,位于F島的核電站唆迁,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏竞穷。R本人自食惡果不足惜唐责,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望瘾带。 院中可真熱鬧鼠哥,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至菜皂,卻和暖如春贞绵,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背恍飘。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工榨崩, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人章母。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓母蛛,卻偏偏與公主長得像,于是被迫代替她去往敵國和親乳怎。 傳聞我的和親對象是個殘疾皇子彩郊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評論 2 353

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