原文:http://sky-weihao.github.io/2015/10/06/Video-streaming-and-caching-in-iOS/
近段时间制作视频播放社区的功能,期间查找了不少资料,做过很多尝试,现在来整理一下其中遇到的一些坑.由于考虑到AVPlayer对视频有更高自由度的控制,而且能够使用它自定义视频播放界面,iOS中所使用的视频播放控件为AVPlayer,而抛弃了高层次的MediaPlayer框架,现在想想挺庆幸当初使用了AVPlayer。
AVPlayer的基本知识
AVPlayer本身并不能显示视频,而且它也不像MPMoviePlayerController有一个view属性。如果AVPlayer要显示必须创建一个播放器层AVPlayerLayer用于展示,播放器层继承于CALayer,有了AVPlayerLayer之添加到控制器视图的layer中即可。要使用AVPlayer首先了解一下几个常用的类:
AVAsset:主要用于获取多媒体信息,是一个抽象类,不能直接使用。
AVURLAsset:AVAsset的子类,可以根据一个URL路径创建一个包含媒体信息的AVURLAsset对象。
AVPlayerItem:一个媒体资源管理对象,管理者视频的一些基本信息和状态,一个AVPlayerItem对应着一个视频资源。
iOS视频实现边下载边播放的几种实现
1.本地实现http server
在iOS本地开启Local Server服务,然后使用播放控件请求本地Local Server服务,本地的服务再不断请求视频地址获取视频流,本地服务请求的过程中把视频缓存到本地,这种方法在网上有很多例子 ,有兴趣了解的人可自己下载例子查看。
2.使用AVPlayer的方法开启下载服务
1 2 3 4 1. AVURLAsset *urlAsset = [[AVURLAsset alloc]initWithURL:url options:nil ];2. AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:urlAsset];3. [self .avPlayer replaceCurrentItemWithPlayerItem:item];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实例才能输出。代码例子如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 NSString *documentDirectory = NSSearchPathForDirectoriesInDomains (NSDocumentDirectory , NSUserDomainMask , YES )[0 ];NSString *myPathDocument = [documentDirectory stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.mp4" ,[_source.videoUrl MD5]]];NSURL *fileUrl = [NSURL fileURLWithPath:myPathDocument];if (asset != nil ) {AVMutableComposition *mixComposition = [[AVMutableComposition alloc]init];AVMutableCompositionTrack *firstTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid ];[firstTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, asset.duration ) ofTrack:[[asset tracksWithMediaType:AVMediaTypeVideo ]objectAtIndex:0 ] atTime:kCMTimeZero error:nil ]; AVMutableCompositionTrack *audioTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid ];[audioTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, asset.duration ) ofTrack:[[asset tracksWithMediaType:AVMediaTypeAudio ]objectAtIndex:0 ] atTime:kCMTimeZero error:nil ]; AVAssetExportSession *exporter = [[AVAssetExportSession alloc]initWithAsset:mixComposition presetName:AVAssetExportPresetHighestQuality ];exporter.outputURL = fileUrl; if (exporter.supportedFileTypes ) {exporter.outputFileType = [exporter.supportedFileTypes objectAtIndex:0 ] ; exporter.shouldOptimizeForNetworkUse = YES ; [exporter exportAsynchronouslyWithCompletionHandler:^{ }]; } }
3.使用AVAssetResourceLoader回调下载,也是最终决定使用的技术
AVAssetResourceLoader通过你提供的委托对象去调节AVURLAsset所需要的加载资源。而很重要的一点是,AVAssetResourceLoader仅在AVURLAsset不知道如何去加载这个URL资源时才会被调用,就是说你提供的委托对象在AVURLAsset不知道如何加载资源时才会得到调用 。所以我们又要通过一些方法来曲线解决这个问题,把我们目标视频URL地址的scheme替换为系统不能识别的scheme,然后在我们调用网络请求去处理这个URL时把scheme切换为原来的scheme。
实现边下边播功能AVResourceLoader的委托对象必须要实现AVAssetResourceLoaderDelegate下五个协议的其中两个:
1 2 3 4 1 - (BOOL )resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest NS_AVAILABLE (10 _9, 6 _0); 2 - (void )resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest NS_AVAILABLE (10 _9, 7 _0);
以下来说说具体要怎么做处理
第一步,创建一个AVURLAsset,并且用它来初始化一个AVPlayerItem
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #define kCustomVideoScheme @"yourScheme" NSURL *currentURL = [NSURL URLWithString:@"http://***.***.***" ];NSURLComponents *components = [[NSURLComponents alloc]initWithURL:currentURL resolvingAgainstBaseURL:NO ];1 components.scheme = kCustomVideoScheme; AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:components.URL options:nil ]; 2 [urlAsset.resourceLoader setDelegate:_resourceManager queue:dispatch_get_main_queue()]; AVPlayerItem *item = [AVPlayerItem playerItemWithAsset:urlAsset];_playerItem = item; if (IOS9_OR_LATER) {item.canUseNetworkResourcesForLiveStreamingWhilePaused = YES ; } [self .avPlayer replaceCurrentItemWithPlayerItem:item]; self .playerLayer .player = self .avPlayer ;[self addObserverToPlayerItem:item];**
第二步,创建AVResourceManager实现AVResourceLoader协议
1 @interface AVAResourceLoaderManager : NSObject < AVAssetResourceLoaderDelegate >
第三步,实现两个必须的回调协议,实现中有几件需要做的事情
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 - (BOOL )resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest { 1 NSURL *resourceURL = [loadingRequest.request URL];2 if ([self checkIsLegalURL:resourceURL] && [resourceURL.scheme isEqualToString:kCustomVideoScheme]){3 AVResourceLoaderForASI *loader = [self asiresourceLoaderForRequest:loadingRequest];if (loader == nil ){loader = [[AVResourceLoaderForASI alloc] initWithResourceURL:resourceURL]; loader.delegate = self ; 4 [self .resourceLoaders setObject:loader forKey:[self keyForResourceLoaderWithURL:resourceURL]]; } 5 [loader addRequest:loadingRequest]; 6 return YES ;}else { return NO ;} }
1 2 3 4 5 6 7 8 - (void )resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest { NSURL *resourceURL = [loadingRequest.request URL];NSString *actualURLString = [self actualURLStringWithURL:resourceURL];AVResourceLoaderForASI *loader = [_resourceLoaders objectForKey:actualURLString];[loader removeRequest:loadingRequest]; }
第四步,判断缓存中是否已下载完视频
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 - (void )addRequest:(AVAssetResourceLoadingRequest *)loadingRequest { if (self .isCancelled ==NO ){AVAResourceFile *resourceFile = [self .resourceFileManager resourceFileWithURL:self .resourceURL ];if (resourceFile) {loadingRequest.contentInformationRequest .byteRangeAccessSupported = YES ; loadingRequest.contentInformationRequest .contentType = resourceFile.contentType ; loadingRequest.contentInformationRequest .contentLength = resourceFile.contentLength ; long long requestedOffset = loadingRequest.dataRequest .requestedOffset ;NSInteger requestedLength = loadingRequest.dataRequest .requestedLength ;NSData *subData = [resourceFile.data subdataWithRange:NSMakeRange (@(requestedOffset).unsignedIntegerValue , requestedLength)];[loadingRequest.dataRequest respondWithData:subData]; [loadingRequest finishLoading]; }else { [self startWithRequest:loadingRequest]; } } else {if (loadingRequest.isFinished ==NO ){[loadingRequest finishLoadingWithError:[self loaderCancelledError]]; } } }
第五步,添加loadingRequest到网络文件加载器,这部分的操作比较长
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 - (void )startWithRequest:(AVAssetResourceLoadingRequest *)loadingRequest { 1 if (self .dataTask == nil ){2 NSURLRequest *request = [self requestWithLoadingRequest:loadingRequest];__weak __typeof (self )weakSelf = self ; 3 NSString *urlString = request.URL .absoluteString ;self .dataTask = [self GET:urlString requestBlock:^(Request *req) {NSLog (@"### %s %@ ###" , __func__, req);4 if (req.recvingHeader NSLog (@"### %s recvingHeader ###" , __func__);__strong __typeof (weakSelf)strongSelf = weakSelf; if ([urlString isEqualToString:req.originalURL .absoluteString ]) {4.1 strongSelf.tempData = [NSMutableData data]; } 4.2 [strongSelf processPendingRequests]; } else if (req.recving NSLog (@"### %s recving ###" , __func__);__strong __typeof (weakSelf)strongSelf = weakSelf; 5 if (urlString == req.originalURL .absoluteString ) {5.1 if (!_contentInformation && req.responseHeaders ) {if ([req.responseHeaders objectForKey:@"Location" ] ) {NSLog (@" ### %s redirection URL ###" , __func__);}else { _contentInformation = [[RLContentInformationForASI alloc]init]; long long numer = [[req.responseHeaders objectForKey:@"Content-Length" ]longLongValue];_contentInformation.contentLength = numer; _contentInformation.byteRangeAccessSupported = YES ; _contentInformation.contentType = [req.responseHeaders objectForKey:@"Content-type" ]; } } NSLog (@"### %s before tempData length = %lu ###" , __FUNCTION__, (unsigned long )self .tempData .length );strongSelf.tempData = [NSMutableData dataWithData:req.rawResponseData ]; NSLog (@"### %s after tempData length = %lu ###" ,__FUNCTION__, (unsigned long )self .tempData .length );[strongSelf processPendingRequests]; } }else if (req.succeed ){ 6 NSLog (@"### %s succeed ###" , __func__);NSLog (@"### %s tempData length = %lu ###" , __func__, (unsigned long )self .tempData .length );__strong __typeof (weakSelf)strongSelf = weakSelf; if (strongSelf) {[strongSelf processPendingRequests]; 7 AVAResourceFile *resourceFile = [[AVAResourceFile alloc]initWithContentType:strongSelf.contentInformation .contentType date:strongSelf.tempData ];[strongSelf.resourceFileManager saveResourceFile:resourceFile withURL:self .resourceURL ]; 8 [strongSelf complete]; if (strongSelf.delegate && [strongSelf.delegate respondsToSelector:@selector (resourceLoader:didLoadResource:)]) {[strongSelf.delegate resourceLoader:strongSelf didLoadResource:strongSelf.resourceURL ]; } } }else if (req.failed ){ NSLog (@"### %s failed ###" , __func__);[self completeWithError:req.error ]; } }]; } [self .pendingRequests addObject:loadingRequest]; }
第六步,把请求返回数据输出到loadingRequest的操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 - (void )processPendingRequests { __weak __typeof (self )weakSelf = self ; dispatch_async (dispatch_get_main_queue(), ^{__strong __typeof (weakSelf)strongSelf = weakSelf; NSMutableArray *requestsCompleted = [NSMutableArray array];1 for (AVAssetResourceLoadingRequest *loadingRequest in strongSelf.pendingRequests ){2 [strongSelf fillInContentInformation:loadingRequest.contentInformationRequest ]; 3 BOOL didRespondCompletely = [strongSelf respondWithDataForRequest:loadingRequest.dataRequest ];4 if (didRespondCompletely){[requestsCompleted addObject:loadingRequest]; [loadingRequest finishLoading]; } } 5 [strongSelf.pendingRequests removeObjectsInArray:requestsCompleted]; }); } 、 - (void )fillInContentInformation:(AVAssetResourceLoadingContentInformationRequest *)contentInformationRequest { if (contentInformationRequest == nil || self .contentInformation == nil ){return ;} contentInformationRequest.byteRangeAccessSupported = self .contentInformation .byteRangeAccessSupported ; contentInformationRequest.contentType = self .contentInformation .contentType ; contentInformationRequest.contentLength = self .contentInformation .contentLength ; } - (BOOL )respondWithDataForRequest:(AVAssetResourceLoadingDataRequest *)dataRequest { long long startOffset = dataRequest.requestedOffset ;if (dataRequest.currentOffset != 0 ){startOffset = dataRequest.currentOffset ; } if (self .tempData .length < startOffset){return NO ;} NSUInteger unreadBytes = self .tempData .length - (NSUInteger )startOffset;NSUInteger numberOfBytesToRespondWith = MIN((NSUInteger )dataRequest.requestedLength , unreadBytes);[dataRequest respondWithData:[self .tempData subdataWithRange:NSMakeRange ((NSUInteger )startOffset, numberOfBytesToRespondWith)]]; long long endOffset = startOffset + dataRequest.requestedLength ;BOOL didRespondFully = self .tempData .length >= endOffset;return didRespondFully;}
视频边下边播的流程大致上已经描述完毕,本博文中没有说到的代码有错误处理方式、缓存文件的读写和保存格式、部分内存缓存使用说明、
参考链接: 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对视频进行重构后保存,经过检验会对视频源数据产生变化,对于程序开发人员来说,需要保证各端存在的视频数据一致。第三种边下边播的方法其实是对第二种方法的扩展,能够解决上面所说的三种问题,可操控的自由度更高。