ios 音視頻實現邊播邊緩存的思路和解決方案 (轉)

 

本片爲轉載內容,主要是之後本身看起來方便一些html

原文地址: iOS音視頻實現邊下載邊播放ios

其實音視頻本地緩存的思想都差很少,都須要一箇中間對象來鏈接播放器和服務器。git

近段時間製做視頻播放社區的功能,期間查找了很多資料,作過不少嘗試,如今來整理一下其中遇到的一些坑.因爲考慮到AVPlayer對視頻有更高自由度的控制,並且可以使用它自定義視頻播放界面,iOS中所使用的視頻播放控件爲AVPlayer,而拋棄了高層次的MediaPlayer框架,如今想一想挺慶幸當初使用了AVPlayer。github

AVPlayer的基本知識

AVPlayer自己並不能顯示視頻,並且它也不像MPMoviePlayerController有一個view屬性。若是AVPlayer要顯示必須建立一個播放器層AVPlayerLayer用於展現,播放器層繼承於CALayer,有了AVPlayerLayer之添加到控制器視圖的layer中便可。要使用AVPlayer首先了解一下幾個經常使用的類:緩存

AVAsset:主要用於獲取多媒體信息,是一個抽象類,不能直接使用。服務器

AVURLAsset:AVAsset的子類,能夠根據一個URL路徑建立一個包含媒體信息的AVURLAsset對象。網絡

AVPlayerItem:一個媒體資源管理對象,管理者視頻的一些基本信息和狀態,一個AVPlayerItem對應着一個視頻資源。app

iOS視頻實現邊下載邊播放的幾種實現

1.本地實現http server

在iOS本地開啓Local Server服務,而後使用播放控件請求本地Local Server服務,本地的服務再不斷請求視頻地址獲取視頻流,本地服務請求的過程當中把視頻緩存到本地,這種方法在網上有不少例子,有興趣瞭解的人可本身下載例子查看。框架

2.使用AVPlayer的方法開啓下載服務

1 1.AVURLAsset *urlAsset = [[AVURLAsset alloc]initWithURL:url options:nil];
2 2.AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:urlAsset];
3 3.[self.avPlayer replaceCurrentItemWithPlayerItem:item];
4 4.[self addObserverToPlayerItem:item];

 

但因爲AVPlayer是沒有提供方法給咱們直接獲取它下載下來的數據,因此咱們只能在視頻下載完以後本身去尋找緩存視頻數據的辦法,AVFoundation框架中有一種從多媒體信息類AVAsset中提取視頻數據的類AVMutableComposition和AVAssetExportSession。
其中AVMutableComposition的做用是可以從現有的asset實例中建立出一個新的AVComposition(它也是AVAsset的字類),使用者可以從別的asset中提取他們的音頻軌道或視頻軌道,而且把它們添加到新建的Composition中。
AVAssetExportSession的做用是把現有的本身建立的asset輸出到本地文件中。
爲何須要把原先的AVAsset(AVURLAsset)實現的數據提取出來後拼接成另外一個AVAsset(AVComposition)的數據後輸出呢,因爲經過網絡url下載下來的視頻沒有保存視頻的原始數據(或者蘋果沒有暴露接口給咱們獲取),下載後播放的avasset不能使用AVAssetExportSession輸出到本地文件,要曲線地把下載下來的視頻經過重構成另一個AVAsset實例才能輸出。代碼例子以下:async

 1 NSString *documentDirectory = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
 2 NSString *myPathDocument = [documentDirectory stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.mp4",[_source.videoUrl MD5]]];
 3 
 4 
 5 NSURL *fileUrl = [NSURL fileURLWithPath:myPathDocument];
 6 
 7 if (asset != nil) {
 8 AVMutableComposition *mixComposition = [[AVMutableComposition alloc]init];
 9 AVMutableCompositionTrack *firstTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];
10 [firstTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, asset.duration) ofTrack:[[asset tracksWithMediaType:AVMediaTypeVideo]objectAtIndex:0] atTime:kCMTimeZero error:nil];
11 
12 AVMutableCompositionTrack *audioTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];
13 [audioTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, asset.duration) ofTrack:[[asset tracksWithMediaType:AVMediaTypeAudio]objectAtIndex:0] atTime:kCMTimeZero error:nil];
14 
15 AVAssetExportSession *exporter = [[AVAssetExportSession alloc]initWithAsset:mixComposition presetName:AVAssetExportPresetHighestQuality];
16 exporter.outputURL = fileUrl;
17 if (exporter.supportedFileTypes) {
18 exporter.outputFileType = [exporter.supportedFileTypes objectAtIndex:0] ;
19 exporter.shouldOptimizeForNetworkUse = YES;
20 [exporter exportAsynchronouslyWithCompletionHandler:^{
21 
22 }];
23 
24 }
25 }

3.使用AVAssetResourceLoader回調下載,也是最終決定使用的技術

AVAssetResourceLoader經過你提供的委託對象去調節AVURLAsset所須要的加載資源。而很重要的一點是,AVAssetResourceLoader僅在AVURLAsset不知道如何去加載這個URL資源時纔會被調用,就是說你提供的委託對象在AVURLAsset不知道如何加載資源時纔會獲得調用。因此咱們又要經過一些方法來曲線解決這個問題,把咱們目標視頻URL地址的scheme替換爲系統不能識別的scheme,而後在咱們調用網絡請求去處理這個URL時把scheme切換爲原來的scheme。

實現邊下邊播功能AVResourceLoader的委託對象必需要實現AVAssetResourceLoaderDelegate下五個協議的其中兩個:

1 1//在系統不知道如何處理URLAsset資源時回調
2 - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest NS_AVAILABLE(10_9, 6_0);
3 2//在取消加載資源後回調
4 - (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest NS_AVAILABLE(10_9, 7_0);

如下來講說具體要怎麼作處理

第一步,建立一個AVURLAsset,而且用它來初始化一個AVPlayerItem

 1 #define kCustomVideoScheme @"yourScheme"
 2 NSURL *currentURL = [NSURL URLWithString:@"http://***.***.***"];
 3 NSURLComponents *components = [[NSURLComponents alloc]initWithURL:currentURL resolvingAgainstBaseURL:NO];
 4 1////注意,不加這一句不能執行到回調操做
 5 components.scheme = kCustomVideoScheme;
 6 AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:components.URL  
 7 options:nil];
 8 2//_resourceManager在接下來說述
 9 [urlAsset.resourceLoader setDelegate:_resourceManager queue:dispatch_get_main_queue()];
10 AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:urlAsset];
11 _playerItem = item;
12 
13 if (IOS9_OR_LATER) {
14 item.canUseNetworkResourcesForLiveStreamingWhilePaused = YES;
15 }
16 [self.avPlayer replaceCurrentItemWithPlayerItem:item];
17 self.playerLayer.player = self.avPlayer;
18 [self addObserverToPlayerItem:item];**

第二步,建立AVResourceManager實現AVResourceLoader協議

1 @interface AVAResourceLoaderManager : NSObject < AVAssetResourceLoaderDelegate >

第三步,實現兩個必須的回調協議,實現中有幾件須要作的事情

 1 - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest
 2 {
 3 1//獲取系統中不能處理的URL
 4 NSURL *resourceURL = [loadingRequest.request URL];
 5 2//判斷這個URL是否遵照URL規範和其是不是咱們所設定的URL
 6 if ([self checkIsLegalURL:resourceURL] && [resourceURL.scheme isEqualToString:kCustomVideoScheme]){
 7 3//判斷當前的URL網絡請求是否已經被加載過了,若是緩存中裏面有URL對應的網絡加載器(本身封裝,也能夠直接使用NSURLRequest),則取出來添加請求,每個URL對應一個網絡加載器,loader的實現接下來會說明
 8 AVResourceLoaderForASI *loader = [self asiresourceLoaderForRequest:loadingRequest];
 9 if (loader == nil){
10 loader = [[AVResourceLoaderForASI alloc] initWithResourceURL:resourceURL];
11 loader.delegate = self;
12 4//緩存網絡加載器
13 [self.resourceLoaders setObject:loader forKey:[self keyForResourceLoaderWithURL:resourceURL]];
14 }
15 5//加載器添加請求
16 [loader addRequest:loadingRequest];
17 6//返回YES則代表使用咱們的代碼對AVAsset中請求網絡資源作處理
18 return YES;
19 }else{
20 return NO;
21 }
22 
23 }
1 - (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest
2 {
3 //若是用戶在下載的過程當中調用者取消了獲取視頻,則從緩存中取消這個請求
4 NSURL *resourceURL = [loadingRequest.request URL];
5 NSString *actualURLString = [self actualURLStringWithURL:resourceURL];
6 AVResourceLoaderForASI *loader = [_resourceLoaders objectForKey:actualURLString];
7 [loader removeRequest:loadingRequest];
8 }

第四步,判斷緩存中是否已下載完視頻

 1 - (void)addRequest:(AVAssetResourceLoadingRequest *)loadingRequest
 2 {
 3 //1判斷自身是否已經取消加載
 4 if(self.isCancelled==NO){
 5 //2判斷本地中是否已經有文件的緩存,若是有,則直接從緩存中讀取數據,文件保存和讀取這裏不作詳述,使用者可根據自身狀況建立文件系統
 6 AVAResourceFile *resourceFile = [self.resourceFileManager resourceFileWithURL:self.resourceURL];
 7 if (resourceFile) {
 8 //3若本地文件存在,則從文件中獲取如下屬性  
 9 loadingRequest.contentInformationRequest.byteRangeAccessSupported = YES;
10 //3.1contentType
11 loadingRequest.contentInformationRequest.contentType = resourceFile.contentType;
12 //3.2數據長度                    
13 loadingRequest.contentInformationRequest.contentLength = resourceFile.contentLength;
14 //3.3請求的偏移量
15 long long requestedOffset = loadingRequest.dataRequest.requestedOffset;
16 //3.4請求總長度
17 NSInteger requestedLength = loadingRequest.dataRequest.requestedLength;
18 //3.5取出本地文件中從偏移量到請求長度的數據
19 NSData *subData = [resourceFile.data subdataWithRange:NSMakeRange(@(requestedOffset).unsignedIntegerValue, requestedLength)];
20 //3.6返回數據給請求
21 [loadingRequest.dataRequest respondWithData:subData];
22 [loadingRequest finishLoading];
23 }else{
24 //4若是沒有本地文件,則開啓網絡請求,從網絡中獲取 ,見第五步 
25 [self startWithRequest:loadingRequest];
26 }
27 }
28 else{
29 //5若是已經取消請求,而且請求沒有完成,則封裝錯誤給請求,可本身實現
30 if(loadingRequest.isFinished==NO){
31 [loadingRequest finishLoadingWithError:[self loaderCancelledError]];
32 }
33 }
34 }

第五步,添加loadingRequest到網絡文件加載器,這部分的操做比較長

 1 - (void)startWithRequest:(AVAssetResourceLoadingRequest *)loadingRequest
 2 {
 3 1//判斷當前請求是否已經開啓,因爲蘋果系統緣由,會有兩次回調到AVResourceLoaderDelegate,咱們對其進行判斷,只開啓一次請求
 4 if (self.dataTask == nil){
 5 2//根據loadingRequest中的URL建立NSURLRequest,注意在此把URL中的scheme修改成原先的scheme
 6 NSURLRequest *request = [self requestWithLoadingRequest:loadingRequest];
 7 __weak __typeof(self)weakSelf = self;
 8 3//獲取url的絕對路徑,並使用ASIHttpRequest進行網絡請求,下面的請求方法通過封裝,就不詳說如何對ASI進行封裝了,可是每一步須要作的事情能以block的形式更好說明
 9 NSString *urlString = request.URL.absoluteString;
10 self.dataTask = [self GET:urlString requestBlock:^(Request *req) {
11 NSLog(@"### %s %@ ###", __func__, req);
12 4//在接受到請求頭部信息時,說明連接成功,數據開始傳輸
13 if (req.recvingHeader//意思是請求接受到頭部信息狀態){
14 NSLog(@"### %s recvingHeader ###", __func__);
15 __strong __typeof(weakSelf)strongSelf = weakSelf;
16 if ([urlString isEqualToString:req.originalURL.absoluteString]) {
17 4.1//,建立臨時數據保存網絡下載下來的視頻信息
18 strongSelf.tempData = [NSMutableData data];
19 }
20 4.2//把頭部信息內容寫入到AVAssetResourceLoadingRequest,即loadingRequest中
21 [strongSelf processPendingRequests];
22 }
23 else if (req.recving//請求接受中狀態){
24 NSLog(@"### %s recving ###", __func__);
25 __strong __typeof(weakSelf)strongSelf = weakSelf;
26 5//此處需屢次調用把請求的信息寫入到loadingRequest的步驟,實現下載的過程當中數據能輸出到loadingRequest播放
27 if (urlString == req.originalURL.absoluteString) {
28 5.1//這個處理是判斷此時返回的頭部信息是重定向仍是實際視頻的頭部信息,若是是重定向信息,則不做處理
29 if (!_contentInformation && req.responseHeaders) {
30 if ([req.responseHeaders objectForKey:@"Location"] ) {
31 NSLog(@" ### %s redirection URL ###", __func__);
32 }else{
33 //5.2若是不是重定向信息,則把須要用到的信息提取出來
34 _contentInformation = [[RLContentInformationForASI alloc]init];
35 long long numer = [[req.responseHeaders objectForKey:@"Content-Length"]longLongValue];
36 _contentInformation.contentLength = numer;
37 _contentInformation.byteRangeAccessSupported = YES;
38 _contentInformation.contentType = [req.responseHeaders objectForKey:@"Content-type"];
39 }
40 }
41 
42 //5.3開始從請求中獲取返回數據
43 NSLog(@"### %s before tempData length = %lu ###", __FUNCTION__, (unsigned long)self.tempData.length);
44 strongSelf.tempData = [NSMutableData dataWithData:req.rawResponseData];
45 NSLog(@"### %s after tempData length = %lu ###",__FUNCTION__, (unsigned long)self.tempData.length);
46 //5.4把返回數據輸出到loadingRequest中
47 [strongSelf processPendingRequests];
48 }
49 }else if (req.succeed){
50 6//請求返回成功,在這裏作最後一次把數據輸出到loadingRequest,且作一些成功後的事情
51 NSLog(@"### %s succeed ###", __func__);
52 NSLog(@"### %s tempData length = %lu ###", __func__, (unsigned long)self.tempData.length);
53 __strong __typeof(weakSelf)strongSelf = weakSelf;
54 if (strongSelf) {
55 [strongSelf processPendingRequests];
56 
57 7//保存緩存文件,我在保存文件這裏作了一次偷懶,若是有人蔘考我寫的文件可對保存文件做改進,在每次返回數據時把數據追加寫到文件,而不是下載成功以後才保存,這請求時也可使用這個來實現斷點重輸的功能
58 AVAResourceFile *resourceFile = [[AVAResourceFile alloc]initWithContentType:strongSelf.contentInformation.contentType date:strongSelf.tempData];
59 [strongSelf.resourceFileManager saveResourceFile:resourceFile withURL:self.resourceURL];
60 8//在此作一些清理緩存、釋放對象和回調到上層的操做
61 [strongSelf complete];
62 if (strongSelf.delegate && [strongSelf.delegate respondsToSelector:@selector(resourceLoader:didLoadResource:)]) {
63 [strongSelf.delegate resourceLoader:strongSelf didLoadResource:strongSelf.resourceURL];
64 }
65 }
66 }else if (req.failed){
67 //9若是請求返回失敗,則向上層拋出錯誤,且清理緩存等操做
68 NSLog(@"### %s failed ###" , __func__);
69 [self completeWithError:req.error];
70 }
71 }];
72 }
73 [self.pendingRequests addObject:loadingRequest];
74 }

第六步,把請求返回數據輸出到loadingRequest的操做

 1 - (void)processPendingRequests
 2 {
 3 __weak __typeof(self)weakSelf = self;
 4 dispatch_async(dispatch_get_main_queue(), ^{
 5 __strong __typeof(weakSelf)strongSelf = weakSelf;
 6 NSMutableArray *requestsCompleted = [NSMutableArray array];
 7 1//從緩存信息中找出當前正在請求中的loadingRequest
 8 for (AVAssetResourceLoadingRequest *loadingRequest in strongSelf.pendingRequests){
 9 2//把頭部信息輸出到loadingRequest中
10 [strongSelf fillInContentInformation:loadingRequest.contentInformationRequest];      
11 3//把視頻數據輸出到loadingRequest中
12 BOOL didRespondCompletely = [strongSelf respondWithDataForRequest:loadingRequest.dataRequest];
13 4//在success狀態中作最後一次調用的時候,檢測到請求已經完成,則從緩存信息中清除loadingRequest,而且把loadingRequest標誌爲完成處理狀態
14 if (didRespondCompletely){
15 [requestsCompleted addObject:loadingRequest];
16 [loadingRequest finishLoading];
17 }
18 }
19 5//清理緩存
20 [strongSelf.pendingRequests removeObjectsInArray:requestsCompleted];
21 });
22 }
23 24 
25 //把提取出來的頭部信息輸出到loadingRequest中,能夠優化
26 - (void)fillInContentInformation:(AVAssetResourceLoadingContentInformationRequest *)contentInformationRequest
27 {
28 if (contentInformationRequest == nil || self.contentInformation == nil){
29 return;
30 }
31 contentInformationRequest.byteRangeAccessSupported = self.contentInformation.byteRangeAccessSupported;
32 contentInformationRequest.contentType = self.contentInformation.contentType;
33 contentInformationRequest.contentLength = self.contentInformation.contentLength;
34 }
35 
36 //把緩存數據輸出到loadingRequest中
37 - (BOOL)respondWithDataForRequest:(AVAssetResourceLoadingDataRequest *)dataRequest
38 {
39 long long startOffset = dataRequest.requestedOffset;
40 if (dataRequest.currentOffset != 0){
41 startOffset = dataRequest.currentOffset;
42 }
43 
44 // Don't have any data at all for this request
45 if (self.tempData.length < startOffset){
46 return NO;
47 }
48 
49 // This is the total data we have from startOffset to whatever has been downloaded so far
50 NSUInteger unreadBytes = self.tempData.length - (NSUInteger)startOffset;
51 
52 // Respond with whatever is available if we can't satisfy the request fully yet
53 NSUInteger numberOfBytesToRespondWith = MIN((NSUInteger)dataRequest.requestedLength, unreadBytes);
54 
55 [dataRequest respondWithData:[self.tempData subdataWithRange:NSMakeRange((NSUInteger)startOffset, numberOfBytesToRespondWith)]];
56 
57 long long endOffset = startOffset + dataRequest.requestedLength;
58 BOOL didRespondFully = self.tempData.length >= endOffset;
59 
60 return didRespondFully;
61 }

視頻邊下邊播的流程大體上已經描述完畢,本博文中沒有說到的代碼有錯誤處理方式、緩存文件的讀寫和保存格式、部份內存緩存使用說明、

參考連接:
http://www.codeproject.com/Articles/875105/Audio-streaming-and-caching-in-iOS-using
http://www.cnblogs.com/kenshincui/p/4186022.html#mpMoviePlayerController

補充:在開發過程當中遇到的一些坑在這裏補充一下1.在iOS9後,AVPlayer的replaceCurrentItemWithPlayerItem方法在切換視頻時底層會調用信號量等待而後致使當前線程卡頓,若是在UITableViewCell中切換視頻播放使用這個方法,會致使當前線程凍結幾秒鐘。遇到這個坑還真很差在系統層面對它作什麼,後來找到的解決方法是在每次須要切換視頻時,需從新建立AVPlayer和AVPlayerItem。2.iOS9後,AVFoundation框架還作了幾點修改,若是須要切換視頻播放的時間,或須要控制視頻從頭播放調用seekToDate方法,須要保持視頻的播放rate大於0才能修改,還有canUseNetworkResourcesForLiveStreamingWhilePaused這個屬性,在iOS9前默認爲YES,以後默認爲NO。3.AVPlayer的replaceCurrentItemWithPlayerItem方法正常是會引用住參數AVPlayerItem的,但在某些狀況下致使視頻播放失敗,它會立刻釋放對這個對象的持有,假如你對AVPlayerItem的實例對象添加了監聽,可是本身沒有對item的計數進行管理,不知道何時釋放這個監聽,則會致使程序崩潰。4.爲何我選擇第三種方法實現邊下邊播,第一種方法須要程序引入LocalServer庫,需增長大量app包大小,且須要開啓本地服務,從性能方面考慮也是不合適。第二種方式存在的缺陷不少,一來只能播放網絡上返回格式contentType爲public/mpeg4等視頻格式的url視頻地址,若保存下來以後,文件的格式也須要保存爲.mp4或.mov等格式的本地文件才能從本地中讀取,三來使用AVMutableComposition對視頻進行重構後保存,通過檢驗會對視頻源數據產生變化,對於程序開發人員來講,須要保證各端存在的視頻數據一致。第三種邊下邊播的方法實際上是對第二種方法的擴展,可以解決上面所說的三種問題,可操控的自由度更高。

相關文章
相關標籤/搜索