文章目录
- [IdiotAVplayer 实现视频切片缓存](#IdiotAVplayer 实现视频切片缓存)
-
- [一 iOS视频边下边播原理](#一 iOS视频边下边播原理)
- [一 分片下载的实现](#一 分片下载的实现)
-
- [1 分片下载的思路](#1 分片下载的思路)
- [2 IdiotAVplayer 实现架构](#2 IdiotAVplayer 实现架构)
- [三 IdiotAVplayer 代码解析](#三 IdiotAVplayer 代码解析)
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];
}
}