iOS IdiotAVplayer实现视频分片缓存

文章目录

  • [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];
    }
    
}
相关推荐
只是有点小怂5 分钟前
受害者缓存(Victim Cache)
缓存
Mr.简锋1 小时前
opencv视频读写
人工智能·opencv·音视频
simpleGq2 小时前
Redis知识点整理 - 脑图
数据库·redis·缓存
春末的南方城市2 小时前
开源音乐分离器Audio Decomposition:可实现盲源音频分离,无需外部乐器分离库,从头开始制作。将音乐转换为五线谱的程序
人工智能·计算机视觉·aigc·音视频
Hali_Botebie2 小时前
采样率22050,那么CHUNK_SIZE 一次传输的音频数据大小设置多少合适?unity接收后出现卡顿的问题的思路
音视频
运维小文3 小时前
服务器硬件介绍
运维·服务器·计算机网络·缓存·硬件架构
风之馨技术录3 小时前
智谱AI清影升级:引领AI视频进入音效新时代
人工智能·音视频
日里安3 小时前
8. 基于 Redis 实现限流
数据库·redis·缓存
晚点吧4 小时前
视频横屏转竖屏播放-使用人脸识别+目标跟踪实现
人工智能·目标跟踪·音视频
EasyCVR4 小时前
ISUP协议视频平台EasyCVR视频设备轨迹回放平台智慧农业视频远程监控管理方案
服务器·网络·数据库·音视频