本片为转载内容,主要是之后本身看起来方便一些html
原文地址: iOS音视频实现边下载边播放ios
其实音视频本地缓存的思想都差很少,都须要一个中间对象来链接播放器和服务器。git
近段时间制做视频播放社区的功能,期间查找了很多资料,作过不少尝试,如今来整理一下其中遇到的一些坑.因为考虑到AVPlayer对视频有更高自由度的控制,并且可以使用它自定义视频播放界面,iOS中所使用的视频播放控件为AVPlayer,而抛弃了高层次的MediaPlayer框架,如今想一想挺庆幸当初使用了AVPlayer。github
AVPlayer自己并不能显示视频,并且它也不像MPMoviePlayerController有一个view属性。若是AVPlayer要显示必须建立一个播放器层AVPlayerLayer用于展现,播放器层继承于CALayer,有了AVPlayerLayer之添加到控制器视图的layer中便可。要使用AVPlayer首先了解一下几个经常使用的类:缓存
AVAsset:主要用于获取多媒体信息,是一个抽象类,不能直接使用。服务器
AVURLAsset:AVAsset的子类,能够根据一个URL路径建立一个包含媒体信息的AVURLAsset对象。网络
AVPlayerItem:一个媒体资源管理对象,管理者视频的一些基本信息和状态,一个AVPlayerItem对应着一个视频资源。app
在iOS本地开启Local Server服务,而后使用播放控件请求本地Local Server服务,本地的服务再不断请求视频地址获取视频流,本地服务请求的过程当中把视频缓存到本地,这种方法在网上有不少例子,有兴趣了解的人可本身下载例子查看。框架
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 }
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);
如下来讲说具体要怎么作处理
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];**
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 }
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 }
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对视频进行重构后保存,通过检验会对视频源数据产生变化,对于程序开发人员来讲,须要保证各端存在的视频数据一致。第三种边下边播的方法实际上是对第二种方法的扩展,可以解决上面所说的三种问题,可操控的自由度更高。