iOS音频篇:AVPlayer的缓存实现

在上一篇文章《使用AVPlayer播放网络音乐》介绍了AVPlayer的基本使用,下面介绍如何通过AVAssetResourceLoader实现AVPlayer的缓存

需求梳理

没有任何工具能适用于所有的场景,在使用AVPlayer的过程中,我们会发现它有很多局限性,比如播放网络音乐时,往往不能控制其内部播放逻辑,比如我们会发现播放时seek会失败,数据加载完毕后不能获取到数据文件进行其他操作,因此我们需要寻找弥补其不足之处的方法,这里我们选择了AVAssetResourceLoader。

AVAssetResourceLoader的作用:让我们自行掌握AVPlayer数据的加载,包括获取AVPlayer需要的数据的信息,以及可以决定传递多少数据给AVPlayer。

AVAssetResourceLoader在AVPlayer中的位置如下:*

Location.jpeg

实现核心

使用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、下次再播放同一歌曲时,如果在缓存文件夹中存在,则直接播放缓存文件;

实现流程

流程示意图:
Process.png
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 播放本地文件、网络文件、缓存实现的讲解

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容