文章目录
- IdiotAVplayer 实现视频切片缓存
- 一 iOS视频边下边播原理
- 一 分片下载的实现
- 1 分片下载的思路
- 2 IdiotAVplayer 实现架构
- 三 IdiotAVplayer 代码解析
- IdiotPlayer
- IdiotResourceLoader
- IdiotDownLoader
IdiotAVplayer 实现视频切片缓存
一 iOS视频边下边播原理
初始化AVURLAsset 的时候,将资源链接中的http替换成其他字符串,并且将AVURLAsset的resourceLoader 设置代理对象,然后该代理对象实现AVAssetResourceLoaderDelegate 的代理方法
#pragma mark - AVAssetResourceLoaderDelegate
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest {return YES;
}- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest {
}
在代理方法中实现资源的下载,保存, 并将下载好的资源塞给 loadingRequest, 实现视频的播放
一 分片下载的实现
简单的实现方案,就是将一个视频从头开始下载,或者从当前下载到的位置开始下载,然后下载到结束 这种方案对于短视频是可以的,因为短视频总共也没有多大,即使我们快进,从头下载开始到快进的地方也没有多少流量,用户体验影响不大,但是仍然浪费了中间的流量。
如果一个视频比较大,用户进行快进操作的话,从开头下载到用户快进的地方需要的时间很长,这时候,如果能根据用户快进的进度,根据用户的需要进行资源下载,那就是一个好的方案了。
1 分片下载的思路
步骤
1 首先根据链接获取本地资源
2 根据获取到的本地资源和视频请求request对比,计算需要新下载的资源 片段。
3 将本地的资源或者下载好的资源分片塞给请求对象request
2 IdiotAVplayer 实现架构
IdiotAVPlayer 负责实现视频播放功能
IdiotResourceLoader
负责实现
AVAssetResourceLoaderDelegate代理 方法,
负责将数据塞给AVAssetResourceLoadingRequest 请求,并管理AVAssetResourceLoadingRequest 请求,添加,移除,塞数据,快进的处理
IdiotDownLoader 负责 资源片段的获取,需要下载的片段的计算
NSURLSessionDelegate 代理方法的实现,并将下载好的数据传给IdiotResourceLoader, 还负责在读取本地数据的时候,将占用内存较大的视频资源分片读取到内存中传给 IdiotResourceLoader,避免造成因为资源较大而产生的内存撑爆问题
IdiotFileManager 负责管理下载的资源
三 IdiotAVplayer 代码解析
创建播放器,并设置resouceLoader代理
IdiotPlayer
_resourceLoader = [[IdiotResourceLoader alloc] init];_resourceLoader.delegate = self;AVURLAsset * playerAsset = [AVURLAsset URLAssetWithURL:[_currentUrl idiotSchemeURL] options:nil];[playerAsset.resourceLoader setDelegate:_resourceLoader queue:_queue];_playerItem = [AVPlayerItem playerItemWithAsset:playerAsset];
IdiotResourceLoader
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest {[self addLoadingRequest:loadingRequest];DLogDebug(@"loadingRequest == %@",loadingRequest)return YES;
}- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest {[self removeLoadingRequest:loadingRequest];
}- (void)removeLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest {dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);NSArray * temptaskList = [NSArray arrayWithArray:self.taskList];dispatch_semaphore_signal(semaphore);IdiotResourceTask * deleteTask = nil;for (IdiotResourceTask * task in temptaskList) {if ([task.loadingRequest isEqual:loadingRequest]) {deleteTask = task;break;}}if (deleteTask) {dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);[self.taskList removeObject:deleteTask];dispatch_semaphore_signal(semaphore);}}- (void)addLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest {if (self.currentResource) {if (loadingRequest.dataRequest.requestedOffset >= self.currentResource.requestOffset &&loadingRequest.dataRequest.requestedOffset <= self.currentResource.requestOffset + self.currentResource.cacheLength) {IdiotResourceTask * task = [[IdiotResourceTask alloc] init];task.loadingRequest = loadingRequest;task.resource = self.currentResource;dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);[self.taskList addObject:task];dispatch_semaphore_signal(semaphore);[self processRequestList];}else{if (self.seek) {[self newTaskWithLoadingRequest:loadingRequest];}else{IdiotResourceTask * task = [[IdiotResourceTask alloc] init];task.loadingRequest = loadingRequest;task.resource = self.currentResource;NSLog(@"哈哈哈哈哈啊哈哈这里这里这里添加22222 %lld %lld %p\n", loadingRequest.dataRequest.requestedOffset, loadingRequest.dataRequest.currentOffset, task);dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);[self.taskList addObject:task];dispatch_semaphore_signal(semaphore);}}}else {[self newTaskWithLoadingRequest:loadingRequest];}
}- (void)newTaskWithLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest {long long fileLength = 0;if (self.currentResource) {fileLength = self.currentResource.fileLength;self.currentResource.cancel = YES;}IdiotResource * resource = [[IdiotResource alloc] init];resource.requestURL = loadingRequest.request.URL;resource.requestOffset = loadingRequest.dataRequest.requestedOffset;resource.resourceType = IdiotResourceTypeTask;if (fileLength > 0) {resource.fileLength = fileLength;}IdiotResourceTask * task = [[IdiotResourceTask alloc] init];task.loadingRequest = loadingRequest;task.resource = resource;self.currentResource = resource;dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);[self.taskList addObject:task];dispatch_semaphore_signal(semaphore);printf("哈哈哈这里事创建的这里事创建的%lld %lld %lld %p %p\n", resource.requestOffset, loadingRequest.dataRequest.requestedOffset, loadingRequest.dataRequest.currentOffset, loadingRequest, task);[IdiotDownLoader share].delegate = self;[[IdiotDownLoader share] start:self.currentResource];self.seek = NO;
}- (void)stopResourceLoader{[[IdiotDownLoader share] cancel];
}- (void)processRequestList {@synchronized (self) {dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);NSArray * temptaskList = [NSArray arrayWithArray:self.taskList];dispatch_semaphore_signal(semaphore);for (IdiotResourceTask * task in temptaskList) {NSInvocationOperation * invoke = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(finishLoadingWithLoadingRequest:) object:task];[_playQueue addOperation:invoke];}}
}- (void)finishLoadingWithLoadingRequest:(IdiotResourceTask *)task {//填充信息task.loadingRequest.contentInformationRequest.contentType = @"video/mp4";task.loadingRequest.contentInformationRequest.byteRangeAccessSupported = YES;task.loadingRequest.contentInformationRequest.contentLength = task.resource.fileLength;if (task.resource.fileLength <= 0) {DLogDebug(@"requestTask.fileLength <= 0");}//读文件,填充数据long long cacheLength = task.resource.cacheLength;long long requestedOffset = task.loadingRequest.dataRequest.requestedOffset;if (task.loadingRequest.dataRequest.currentOffset != 0) {requestedOffset = task.loadingRequest.dataRequest.currentOffset;}printf("哈哈哈1111执行执行执行%lld点 %lld 一 %lld %p %p\n", task.loadingRequest.dataRequest.requestedOffset,task.loadingRequest.dataRequest.currentOffset, task.resource.requestOffset, task.loadingRequest, task);printf("哈哈哈数量数量%ld\n", self.taskList.count);for (IdiotResourceTask *task1 in self.taskList) {printf("哈哈哈啦啊啦这里这里数组里的%p %lld\n",task1, task.resource.requestOffset);}if (requestedOffset < task.resource.requestOffset) {printf("哈哈哈1111返回%lld点 %lld 一 %lld %p %p\n", task.loadingRequest.dataRequest.requestedOffset,task.loadingRequest.dataRequest.currentOffset, task.resource.requestOffset, task.loadingRequest, task);return;}long long paddingOffset = requestedOffset - task.resource.requestOffset;long long canReadLength = cacheLength - paddingOffset;printf("哈哈哈能获取到的能获取到的%lld \n", canReadLength);if (canReadLength <= 0) {printf("哈哈哈返回222222 %lld\n", canReadLength);return;}long long respondLength = MIN(canReadLength, task.loadingRequest.dataRequest.requestedLength);NSFileHandle * handle = [IdiotFileManager fileHandleForReadingAtPath:task.resource.cachePath];[handle seekToFileOffset:paddingOffset];[task.loadingRequest.dataRequest respondWithData:[handle readDataOfLength:[[NSNumber numberWithLongLong:respondLength] unsignedIntegerValue]]];printf("哈哈哈匹配到匹配到%lld \n",respondLength);[handle closeFile];//如果完全响应了所需要的数据,则完成long long nowendOffset = requestedOffset + canReadLength;long long reqEndOffset = task.loadingRequest.dataRequest.requestedOffset + task.loadingRequest.dataRequest.requestedLength;printf("哈哈哈差别差别%lld\n",reqEndOffset - nowendOffset);if (nowendOffset >= reqEndOffset) {[task.loadingRequest finishLoading];printf("哈哈哈移除移除移除%lld %lld\n", nowendOffset, reqEndOffset);[self removeLoadingRequest:task.loadingRequest];return;}}#pragma mark - DownLoaderDataDelegate
- (void)didReceiveData:(IdiotDownLoader *__weak)downLoader{[self processRequestList];if (self.delegate&&[self.delegate respondsToSelector:@selector(didCacheProgressChange:)]) {__weak typeof(self) weakself = self;dispatch_async(dispatch_get_main_queue(), ^{__strong typeof(weakself) strongself = weakself;NSMutableArray * caches = [downLoader.resources mutableCopy];[caches addObject:self.currentResource];[strongself.delegate didCacheProgressChange:caches];});}}
下面单独介绍各个方法的实现
if (self.currentResource) {if (loadingRequest.dataRequest.requestedOffset >= self.currentResource.requestOffset &&loadingRequest.dataRequest.requestedOffset <= self.currentResource.requestOffset + self.currentResource.cacheLength) {IdiotResourceTask * task = [[IdiotResourceTask alloc] init];task.loadingRequest = loadingRequest;task.resource = self.currentResource;dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);[self.taskList addObject:task];dispatch_semaphore_signal(semaphore);[self processRequestList];}else{if (self.seek) {[self newTaskWithLoadingRequest:loadingRequest];}else{IdiotResourceTask * task = [[IdiotResourceTask alloc] init];task.loadingRequest = loadingRequest;task.resource = self.currentResource;dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);[self.taskList addObject:task];dispatch_semaphore_signal(semaphore);}}}else {[self newTaskWithLoadingRequest:loadingRequest];}
}
上面方法中,的判断条件 self.currentResource 说明执行过newTaskWithLoadingRequest 方法了,因为在该方法中设置了self.currentResource,说明就不是第一次执行addLoadingRequest 添加request了,loadingRequest.dataRequest.requestedOffset >= self.currentResource.requestOffset &&
loadingRequest.dataRequest.requestedOffset <= self.currentResource.requestOffset + self.currentResource.cacheLength 该判断条件说明
新请求的offset 是大于当前的offset, 但是小于当前的offset + cachelength ,说明
当前的的本地资源是有一部分是可以塞给当前的 request的 ,所以在创建了新的任务task的同时,还执行了 [self processRequestList];
方法。下面的 else中 if (self.seek) 说明当前的request是因为用户拖拽进度条触发的,所以要重新创建一个source ,因为一个拖拽就会引起一个不连续的下载片段,而在IdiotAvplayer的设计中,每一个资源片段都要有一个resouce,
所以要执行newTaskWithLoadingRequest 方法
else说明不是拖拽的,则直接添加新的任务即可,等到新的下载好的资源到来,就会去塞给新添加的请求,而新的下载是不会停止的,直到到达资源的最后。
- (void)finishLoadingWithLoadingRequest:(IdiotResourceTask *)task {//填充信息task.loadingRequest.contentInformationRequest.contentType = @"video/mp4";task.loadingRequest.contentInformationRequest.byteRangeAccessSupported = YES;task.loadingRequest.contentInformationRequest.contentLength = task.resource.fileLength;if (task.resource.fileLength <= 0) {DLogDebug(@"requestTask.fileLength <= 0");}//读文件,填充数据long long cacheLength = task.resource.cacheLength;long long requestedOffset = task.loadingRequest.dataRequest.requestedOffset;if (task.loadingRequest.dataRequest.currentOffset != 0) {requestedOffset = task.loadingRequest.dataRequest.currentOffset;}if (requestedOffset < task.resource.requestOffset) {/*task.resource 是第一次播放或者拖拽才会创建的对象,其 requestOffset就是对应的那次请求的offset,这里的判断条件 requestedOffset < task.resource.requestOffset 说明 该request是 创建 resouce 之前的request,那么该resouce 对应的资源中满足该request,所以就返回*/ return;}long long paddingOffset = requestedOffset - task.resource.requestOffset;long long canReadLength = cacheLength - paddingOffset;if (canReadLength <= 0) {如果该resouce offset+ resouce的资源长度,仍然小与request 的offset,说明该资源完全在request的前面,无法满足该request,返回return;}long long respondLength = MIN(canReadLength, task.loadingRequest.dataRequest.requestedLength);NSFileHandle * handle = [IdiotFileManager fileHandleForReadingAtPath:task.resource.cachePath];[handle seekToFileOffset:paddingOffset];[task.loadingRequest.dataRequest respondWithData:[handle readDataOfLength:[[NSNumber numberWithLongLong:respondLength] unsignedIntegerValue]]];[handle closeFile];//如果完全响应了所需要的数据,则完成long long nowendOffset = requestedOffset + canReadLength;long long reqEndOffset = task.loadingRequest.dataRequest.requestedOffset + task.loadingRequest.dataRequest.requestedLength;if (nowendOffset >= reqEndOffset) {[task.loadingRequest finishLoading];[self removeLoadingRequest:task.loadingRequest];return;}}
如下图,分片缓存的资源在沙盒中的保存形式,是根据offset 分别保存的
IdiotDownLoader
- (void)start:(IdiotResource *)task {if (self.currentDataTask) {[self.currentDataTask cancel];}[self.taskDic setObject:task forKey:[NSString stringWithFormat:@"%zd",task.requestOffset]];//获取本地资源BOOL refresh = NO;while (!self.writing&&!refresh) {self.resources = [IdiotFileManager getResourceWithUrl:task.requestURL];refresh = YES;}IdiotResource * resource = nil;//找出对应的资源if (!self.resources.count) {//本地无资源resource = [[IdiotResource alloc] init];resource.requestURL = task.requestURL;resource.requestOffset = task.requestOffset;resource.fileLength = task.fileLength;resource.cachePath = task.cachePath;resource.cacheLength = 0;resource.resourceType = IdiotResourceTypeNet;//网络资源[self.resources addObject:resource];}else{//本地有资源for (IdiotResource * obj in self.resources) {if (task.requestOffset >= obj.requestOffset&&task.requestOffset < obj.requestOffset+obj.cacheLength) {/*该判断条件说明当前任务offset比获取的本地分片资源offset大,比本地分片资源offset+cachelength小,在本地资源中间,有重合的地方*/resource = obj;break;}}if (task.requestOffset > resource.requestOffset&&resource.resourceType == IdiotResourceTypeNet) {/*该resouce 是从上面的判断条件中获取的该判断说明当前任务比获取到的本地resouce offset大,并且是网路请求资源,说明本地没有资源,需要重新下载,这里新建一个IdiotResource,并且设置offset=task.offset就是为了从当前任务的offset开始下载,否则会中本得resouce 的offset开始下载,这样就会导致下载的比我们需要的多,并且用户会有一个卡住的体验,因为下载的不是用户需要的offset,这里这样写,保证下载的offset就是用户需要的,并且避免流量浪费 */long long adjustCacheLength = task.requestOffset - resource.requestOffset;IdiotResource * net = [[IdiotResource alloc] init];net.requestURL = task.requestURL;net.requestOffset = task.requestOffset;net.fileLength = task.fileLength;net.cachePath = task.cachePath;net.cacheLength = resource.cacheLength - adjustCacheLength;net.resourceType = IdiotResourceTypeNet;//网络资源resource.cacheLength = adjustCacheLength;NSInteger index = [self.resources indexOfObject:resource]+1;[self.resources insertObject:net atIndex:index];resource = net;}}self.currentResource = resource;[self fetchDataWith:task Resource:self.currentResource];}
- (void)fetchFromLocal:(IdiotResource *)sliceRequest withResource:(IdiotResource *)resource{if (sliceRequest.requestOffset == resource.requestOffset) {sliceRequest.cachePath = resource.cachePath;sliceRequest.fileLength = resource.fileLength;sliceRequest.cacheLength = resource.cacheLength;//直接开始下一个资源获取if (self.delegate && [self.delegate respondsToSelector:@selector(didReceiveData:)]) {[self.delegate didReceiveData:self];}[self willNextResource:sliceRequest];return;}NSFileHandle * readHandle = [IdiotFileManager fileHandleForReadingAtPath:resource.cachePath];unsigned long long seekOffset = sliceRequest.requestOffset < resource.requestOffset?0:sliceRequest.requestOffset-resource.requestOffset;[readHandle seekToFileOffset:seekOffset];//文件过大可分次读取long long canReadLength = resource.cacheLength-seekOffset;NSUInteger bufferLength = 5242880; //长度大于5M分次返回数据/*如果本地资源比较大,就分片塞数据,如果一下将整个资源读取到内存中,就会造成内存撑爆,导致严重的卡顿*/while (canReadLength >= bufferLength) {//长度大于1M分次返回数据canReadLength -= bufferLength;NSData * responseData = [readHandle readDataOfLength:bufferLength];[self didReceiveLocalData:responseData requestTask:sliceRequest complete:canReadLength==0?YES:NO];}if (canReadLength != 0) {NSData * responseData = [readHandle readDataOfLength:[[NSNumber numberWithLongLong:canReadLength] unsignedIntegerValue]];[readHandle closeFile];[self didReceiveLocalData:responseData requestTask:sliceRequest complete:YES];}else{[readHandle closeFile];}}