iOS音视频实现边下载边播放
2016-07-24 22:21
1708人阅读
收藏
举报
分类:
iOS_音视频(18)
目录(?)[+]
http://sky-weihao.github.io/2015/10/06/Video-streaming-and-caching-in-iOS/
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的方法开启下载服务
1234 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实例才能输出。代码例子如下:
12345678910111213141516171819202122232425 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下五个协议的其中两个:
1234 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
123456789101112131415161718 #define kCustomVideoScheme @"yourScheme"NSURL *currentURL = [NSURL URLWithString:@"http://***.***.***"];NSURLComponents *components = [[NSURLComponents alloc]initWithURL:currentURL resolvingAgainstBaseURL:NO];1components.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 >
第三步,实现两个必须的回调协议,实现中有几件需要做的事情
1234567891011121314151617181920212223 - (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest{1NSURL *resourceURL = [loadingRequest.request URL];2if ([self checkIsLegalURL:resourceURL] && [resourceURL.scheme isEqualToString:kCustomVideoScheme]){3AVResourceLoaderForASI *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];6return YES;}else{return NO;}}
12345678 - (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];}
第四步,判断缓存中是否已下载完视频
12345678910111213141516171819202122232425262728293031323334 - (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到网络文件加载器,这部分的操作比较长
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374 - (void)startWithRequest:(AVAssetResourceLoadingRequest *)loadingRequest{1if (self.dataTask == nil){2NSURLRequest *request = [self requestWithLoadingRequest:loadingRequest];__weak __typeof(self)weakSelf = self;3NSString *urlString = request.URL.absoluteString;self.dataTask = [self GET:urlString requestBlock:^(Request *req) {NSLog(@"### %s %@ ###", __func__, req);4if (req.recvingHeaderNSLog(@"### %s recvingHeader ###", __func__);__strong __typeof(weakSelf)strongSelf = weakSelf;if ([urlString isEqualToString:req.originalURL.absoluteString]) {4.1strongSelf.tempData = [NSMutableData data];}4.2[strongSelf processPendingRequests];}else if (req.recvingNSLog(@"### %s recving ###", __func__);__strong __typeof(weakSelf)strongSelf = weakSelf;5if (urlString == req.originalURL.absoluteString) {5.1if (!_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){6NSLog(@"### %s succeed ###", __func__);NSLog(@"### %s tempData length = %lu ###", __func__, (unsigned long)self.tempData.length);__strong __typeof(weakSelf)strongSelf = weakSelf;if (strongSelf) {[strongSelf processPendingRequests];7AVAResourceFile *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的操作
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061 - (void)processPendingRequests{__weak __typeof(self)weakSelf = self;dispatch_async(dispatch_get_main_queue(), ^{__strong __typeof(weakSelf)strongSelf = weakSelf;NSMutableArray *requestsCompleted = [NSMutableArray array];1for (AVAssetResourceLoadingRequest *loadingRequest in strongSelf.pendingRequests){2[strongSelf fillInContentInformation:loadingRequest.contentInformationRequest]; 3BOOL didRespondCompletely = [strongSelf respondWithDataForRequest:loadingRequest.dataRequest];4if (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对视频进行重构后保存,经过检验会对视频源数据产生变化,对于程序开发人员来说,需要保证各端存在的视频数据一致。第三种边下边播的方法其实是对第二种方法的扩展,能够解决上面所说的三种问题,可操控的自由度更高。
转载请注明原文地址: https://ju.6miu.com/read-33017.html