轉(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)行提示
具體流程圖
視頻播放器處理流程
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一個岸啡,也算是對我的支持和鼓勵原叮。