iOS音频篇:AVPlayer的缓存实现

授权转载,作者:明仔Su(简书)
在上一篇文章《使用AVPlayer播放网络音乐》介绍了AVPlayer的基本使用,下面介绍如何通过AVAssetResourceLoader实现AVPlayer的缓存。
需求梳理
没有任何工具能适用于所有的场景,在使用AVPlayer的过程中,我们会发现它有很多局限性,比如播放网络音乐时,往往不能控制其内部播放逻辑,比如我们会发现播放时seek会失败,数据加载完毕后不能获取到数据文件进行其他操作,因此我们需要寻找弥补其不足之处的方法,这里我们选择了AVAssetResourceLoader。
AVAssetResourceLoader的作用:让我们自行掌握AVPlayer数据的加载,包括获取AVPlayer需要的数据的信息,以及可以决定传递多少数据给AVPlayer。
AVAssetResourceLoader在AVPlayer中的位置如下:

实现核心
使用AVAssetResourceLoader需要实现AVAssetResourceLoaderDelegate的方法:
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader
shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest;
要求加载资源的代理方法,这时我们需要保存loadingRequest并对其所指定的数据进行读取或下载操作,当数据读取或下载完成,我们可以对loadingRequest进行完成操作。
- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader
didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest;
取消加载资源的代理方法,这时我们需要取消loadingRequest所指定的数据的读取或下载操作。
实现策略
通过AVAssetResourceLoader实现缓存的策略有多种,没有绝对的优与劣,只要符合我们的实际需求就可以了。
下面我们以模仿企鹅音乐的来演示AVAssetResourceLoader实现缓存的过程为例子。
先观察并猜测企鹅音乐的缓存策略(当然它不是用AVPlayer播放):
1、开始播放,同时开始下载完整的文件,当文件下载完成时,保存到缓存文件夹中;
2、当seek时
(1)如果seek到已下载到的部分,直接seek成功;(如下载进度60%,seek进度50%)
(2)如果seek到未下载到的部分,则开始新的下载(如下载进度60%,seek进度70%)
PS1:此时文件下载的范围是70%-100%
PS2:之前已下载的部分就被删除了
PS3:如果有别的seek操作则重复步骤2,如果此时再seek到进度40%,则会开始新的下载(范围40%-100%)
3、当开始新的下载之后,由于文件不完整,下载完成之后不会保存到缓存文件夹中;
4、下次再播放同一歌曲时,如果在缓存文件夹中存在,则直接播放缓存文件;
实现流程
流程示意图:

1、通过自定义scheme来创建avplayer,并给AVURLAsset指定代理(SUPlayer对象)
AVURLAsset * asset = [AVURLAsset URLAssetWithURL:[self.url customSchemeURL] options:nil];
[asset.resourceLoader setDelegate:self.resourceLoader queue:dispatch_get_main_queue()];
self.currentItem = [AVPlayerItem playerItemWithAsset:asset];
self.player = [AVPlayer playerWithPlayerItem:self.currentItem];
2、代理实现AVAssetResourceLoader的代理方法(SUResourceLoader对象)
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest {
[self addLoadingRequest:loadingRequest];
return YES;
}
- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest {
[self removeLoadingRequest:loadingRequest];
}
3、对loadingRequest的处理(addLoadingRequest方法)
(1)将其加入到requestList中
[self.requestList addObject:loadingRequest];
(2)如果还没开始下载,则开始请求数据,否则静待数据的下载
[self newTaskWithLoadingRequest:loadingRequest cache:YES];
(3)如果是seek之后的loadingRequest,判断请求开始的位置,如果已经缓冲到,则直接读取数据
if (loadingRequest.dataRequest.requestedOffset >= self.requestTask.requestOffset &&
loadingRequest.dataRequest.requestedOffset <= self.requestTask.requestOffset + self.requestTask.cacheLength) {
[self processRequestList];
}
3.4如果还没缓冲到,则重新请求
if (self.seekRequired) {
[self newTaskWithLoadingRequest:loadingRequest cache:NO];
}
4、数据请求的处理(newTaskWithLoadingRequest方法)
(1)先判断是否已经有下载任务,如果有,则先取消该任务
if (self.requestTask) {
fileLength = self.requestTask.fileLength;
self.requestTask.cancel = YES;
}
(2)建立新的请求,设置代理
self.requestTask = [[SURequestTask alloc]init];
self.requestTask.requestURL = loadingRequest.request.URL;
self.requestTask.requestOffset = loadingRequest.dataRequest.requestedOffset;
self.requestTask.cache = cache;
if (fileLength > 0) {
self.requestTask.fileLength = fileLength;
}
self.requestTask.delegate = self;
[self.requestTask start];
self.seekRequired = NO;
5、数据响应的处理(processRequestList方法)
对requestList里面的loadingRequest填充响应数据,如果已完全响应,则将其从requestList中移除
- (void)processRequestList {
NSMutableArray * finishRequestList = [NSMutableArray array];
for (AVAssetResourceLoadingRequest * loadingRequest in self.requestList) {
if ([self finishLoadingWithLoadingRequest:loadingRequest]) {
[finishRequestList addObject:loadingRequest];
}
}
[self.requestList removeObjectsInArray:finishRequestList];
}
填充响应数据的过程如下:
(1)填写 contentInformationRequest的信息,注意contentLength需要填写下载的文件的总长度,contentType需要转换
CFStringRef contentType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, (__bridge CFStringRef)(MimeType), NULL);
loadingRequest.contentInformationRequest.contentType = CFBridgingRelease(contentType);
loadingRequest.contentInformationRequest.byteRangeAccessSupported = YES;
loadingRequest.contentInformationRequest.contentLength = self.requestTask.fileLength;
(2)计算可以响应的数据长度,注意数据读取的起始位置是当前avplayer当前播放的位置,结束位置是loadingRequest的结束位置或者目前文件下载到的位置
NSUInteger cacheLength = self.requestTask.cacheLength;
NSUInteger requestedOffset = loadingRequest.dataRequest.requestedOffset;
if (loadingRequest.dataRequest.currentOffset != 0) {
requestedOffset = loadingRequest.dataRequest.currentOffset;
}
NSUInteger canReadLength = cacheLength - (requestedOffset - self.requestTask.requestOffset);
NSUInteger respondLength = MIN(canReadLength, loadingRequest.dataRequest.requestedLength);
(3)读取数据并填充到loadingRequest
[loadingRequest.dataRequest respondWithData:[SUFileHandle readTempFileDataWithOffset:requestedOffset - self.requestTask.requestOffset length:respondLength]];
(4) 如果完全响应了所需要的数据,则完成loadingRequest,注意判断的依据是 响应数据结束的位置 >= loadingRequest结束的位置
NSUInteger nowendOffset = requestedOffset + canReadLength;
NSUInteger reqEndOffset = loadingRequest.dataRequest.requestedOffset + loadingRequest.dataRequest.requestedLength;
if (nowendOffset >= reqEndOffset) {
[loadingRequest finishLoading];
return YES;
}
return NO;
6、处理requestList的时机
当有新的loadingRequest或者文件下载进度更新时,都需要处理requestList
7、新的请求任务实现的过程(SURequestTask对象)
(1)初始化时,需要删除旧的临时文件,并创建新的空白临时文件
- (instancetype)init {
if (self = [super init]) {
[SUFileHandle createTempFile];
}
return self;
}
(2)建立新的连接,如果是seek后的请求,则指定其请求内容的范围
- (void)start {
NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[self.requestURL originalSchemeURL] cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:RequestTimeout];
if (self.requestOffset > 0) {
[request addValue:[NSString stringWithFormat:@"bytes=%ld-%ld", self.requestOffset, self.fileLength - 1] forHTTPHeaderField:@"Range"];
}
self.session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
self.task = [self.session dataTaskWithRequest:request];
[self.task resume];
}
(3)当收到数据时,将数据写入临时文件,更新下载进度,同时通知代理处理requestList
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
if (self.cancel) return;
[SUFileHandle writeTempFileData:data];
self.cacheLength += data.length;
if (self.delegate && [self.delegate respondsToSelector:@selector(requestTaskDidUpdateCache)]) {
[self.delegate requestTaskDidUpdateCache];
}
}
(4)当下载完成时,如果满足缓存的条件,则将临时文件拷贝到缓存文件夹中
if (self.cache) {
[SUFileHandle cacheTempFileWithFileName:[NSString fileNameWithURL:self.requestURL]];
}
if (self.delegate && [self.delegate respondsToSelector:@selector(requestTaskDidFinishLoadingWithCache:)]) {
[self.delegate requestTaskDidFinishLoadingWithCache:self.cache];
}
示例Demo
以上就是总体的实现流程,当然每个人的思路都不同,你可以在对其理解得足够深刻之后使用更高效更安全的方式去实现。
本文的demo在我的github上可以下载:GitHub : SUCacheLoader
本demo是以缓存豆瓣FM的歌曲(MP4格式)为例写的,如果你追求更完美的效果,可以从以下几方面入手:
1、对缓存格式支持的处理:并不是所有文件格式都支持的哦,对于不支持的格式,你应该不使用缓存功能;
2、对缓存过程中各种错误的处理:比如下载超时、连接失败、读取数据错误等等的处理;
3、缓存文件的命名处理,如果缓存文件没有后缀(如.mp4),可能会导致播放失败;
4、AVPlayer播放状态的处理,要做到完美的播放体验,在这方面要下点功夫;
Next:
接下来将带来AudioFileStream + AudioQueue 播放本地文件、网络文件、缓存实现的讲解。
iOS音频篇:AVPlayer的缓存实现的更多相关文章
- iOS音频篇:使用AVPlayer播放网络音乐
http://www.cocoachina.com/ios/20160324/15767.html 引言 假如你现在打算做一个类似百度音乐.豆瓣电台的在线音乐类APP,你会怎样做? 首先了解一下音频播 ...
- 一篇对iOS音频比较完善的文章
转自:http://www.cnblogs.com/iOS-mt/p/4268532.html 感谢作者:梦想通 前言 从事音乐相关的app开发也已经有一段时日了,在这过程中app的播放器几经修改我也 ...
- iOS开发:AVPlayer实现流音频边播边存
1. AVPlayer简介 AVPlayer存在于AVFoundation中,可以播放视频和音频,可以理解为一个随身听 AVPlayer的关联类: AVAsset:一个抽象类,不能直接使用,代表一个要 ...
- IOS 音频播放
iOS音频播放 (一):概述 前言 从事音乐相关的app开发也已经有一段时日了,在这过程中app的播放器几经修改我也因此对于iOS下的音频播放实现有了一定的研究.写这个系列的博客目的一方面希望能够抛砖 ...
- iOS音频播放(一):概述
(本文转自码农人生) 前言 从事音乐相关的app开发也已经有一段时日了,在这过程中app的播放器几经修改,我也因此对于iOS下的音频播放实现有了一定的研究.写这个 系列的博客目的一方面希望能够抛砖引玉 ...
- iOS音频播放 (二):AudioSession 转
原文出处 :http://msching.github.io/blog/2014/07/08/audio-in-ios-2/ 前言 本篇为<iOS音频播放>系列的第二篇. 在实施前一篇中所 ...
- 一步一步教你实现iOS音频频谱动画(二)
如果你想先看看最终效果再决定看不看文章 -> bilibili 示例代码下载 第一篇:一步一步教你实现iOS音频频谱动画(一) 本文是系列文章中的第二篇,上篇讲述了音频播放和频谱数据计算,本篇讲 ...
- iOS音频播放 (五):AudioQueue
码农人生 ChengYin's coding life 主页 Blog 分类 Categories 归档 Archives 关于 About Weibo GitHub RSS Where there ...
- 一步一步教你实现iOS音频频谱动画(一)
如果你想先看看最终效果再决定看不看文章 -> bilibili 示例代码下载 第二篇:一步一步教你实现iOS音频频谱动画(二) 基于篇幅考虑,本次教程分为两篇文章,本篇文章主要讲述音频播放和频谱 ...
随机推荐
- Json时间格式转换问题
很多时候在数据库中取出数据,需要用Json来接收,但是接受出来的数据竟然是:/Date(1386040883000+0800)/ 这种格式. 这个时候就需要将Json格式,转换成Javascript格 ...
- vijos1049送给圣诞夜的礼品
这题犯了两个sb错误,写下来,为以后做个警告 一.mul过程中将k作为了循环变量 二.看错了题…… 题目中说是数到k行,而我却以为数k遍…… 做矩阵乘法,只要记住一句话:置换一定可以写成矩阵的形式! ...
- jqGrid如何实现单选。
设置如下:multiselect:true // multi-select checkboxes appear multiboxonly:true // checkboxes act like rad ...
- SQL中的CASE WHEN用法
其语法如下: 1)case vlaue when [compare-value]then reslut [when[compare-value]] then result ...] [else res ...
- 树莓PI安装jdk1.8,ant,maven【转】
http://the.taoofmac.com/space/hw/RaspberryPi/JDK%20Installation jdk--------------------------------- ...
- LeetCode题解——Longest Substring Without Repeating Characters
题目: 给定一个字符串,返回其中不包含重复字符的最长子串长度. 解法: 维持两个指针,第一个指向子串开始,第二个负责遍历,当遍历到的字符出现在子串内时,应计算当前子串长度,并更新最长值:然后第一个指针 ...
- bzoj 3551 [ONTAK2010]Peaks加强版(kruskal,主席树,dfs序)
Description [题目描述]同3545 Input 第一行三个数N,M,Q. 第二行N个数,第i个数为h_i 接下来M行,每行3个数a b c,表示从a到b有一条困难值为c的双向路径. 接下来 ...
- CentOS无损调整home,root磁盘的大小
调整硬盘分区大小想增加root空间,减少home空间. 需要说明的是整个操作需要使用root用户. 1.查看硬盘使用情况. [root@Slave1 /]# df -h Filesystem Size ...
- mapreduce学习指导及疑难解惑汇总
原文链接http://www.aboutyun.com/thread-7091-1-1.html 1.思想起源: 我们在学习mapreduce,首先我们从思想上来认识.其实任何的奇思妙想,抽象的,好的 ...
- 可以使用Markdown了?
园子果然领先 1.标题类 一级标题 二级标题 三级标题 四级 六级 怎么可以用#号?上传上去看看 2.换行 第一行 换一行 在换一行 3.多个下划线 the_odd_egg odd 斜体用星号 4.删 ...