授权转载,作者:明仔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的缓存实现的更多相关文章

  1. iOS音频篇:使用AVPlayer播放网络音乐

    http://www.cocoachina.com/ios/20160324/15767.html 引言 假如你现在打算做一个类似百度音乐.豆瓣电台的在线音乐类APP,你会怎样做? 首先了解一下音频播 ...

  2. 一篇对iOS音频比较完善的文章

    转自:http://www.cnblogs.com/iOS-mt/p/4268532.html 感谢作者:梦想通 前言 从事音乐相关的app开发也已经有一段时日了,在这过程中app的播放器几经修改我也 ...

  3. iOS开发:AVPlayer实现流音频边播边存

    1. AVPlayer简介 AVPlayer存在于AVFoundation中,可以播放视频和音频,可以理解为一个随身听 AVPlayer的关联类: AVAsset:一个抽象类,不能直接使用,代表一个要 ...

  4. IOS 音频播放

    iOS音频播放 (一):概述 前言 从事音乐相关的app开发也已经有一段时日了,在这过程中app的播放器几经修改我也因此对于iOS下的音频播放实现有了一定的研究.写这个系列的博客目的一方面希望能够抛砖 ...

  5. iOS音频播放(一):概述

    (本文转自码农人生) 前言 从事音乐相关的app开发也已经有一段时日了,在这过程中app的播放器几经修改,我也因此对于iOS下的音频播放实现有了一定的研究.写这个 系列的博客目的一方面希望能够抛砖引玉 ...

  6. iOS音频播放 (二):AudioSession 转

    原文出处 :http://msching.github.io/blog/2014/07/08/audio-in-ios-2/ 前言 本篇为<iOS音频播放>系列的第二篇. 在实施前一篇中所 ...

  7. 一步一步教你实现iOS音频频谱动画(二)

    如果你想先看看最终效果再决定看不看文章 -> bilibili 示例代码下载 第一篇:一步一步教你实现iOS音频频谱动画(一) 本文是系列文章中的第二篇,上篇讲述了音频播放和频谱数据计算,本篇讲 ...

  8. iOS音频播放 (五):AudioQueue

    码农人生 ChengYin's coding life 主页 Blog 分类 Categories 归档 Archives 关于 About Weibo GitHub RSS Where there ...

  9. 一步一步教你实现iOS音频频谱动画(一)

    如果你想先看看最终效果再决定看不看文章 -> bilibili 示例代码下载 第二篇:一步一步教你实现iOS音频频谱动画(二) 基于篇幅考虑,本次教程分为两篇文章,本篇文章主要讲述音频播放和频谱 ...

随机推荐

  1. UVa 11971 (概率) Polygon

    题意: 有一根绳子,在上面随机选取k个切点,将其切成k+1段,求这些线段能够成k+1边形的概率. 分析: 要构成k+1边形,必须最长的线段小于其他k个线段之和才行. 紫书上给出了一种解法,但是感觉理解 ...

  2. 微软 Virtual studion Code

    在 Build 2015 大会上,微软除了发布了 Microsoft Edge 浏览器和新的 Windows 10 预览版外,最大的惊喜莫过于宣布推出免费跨平台的 Visual Studio Code ...

  3. 理解java reference

    Java世界泰山北斗级大作<Thinking In Java>切入Java就提出“Everything is Object”.在Java这个充满Object的世界中,reference是一 ...

  4. SharePoint 2010 master page 控件介绍(3) :页面主体内容

    转:http://blog.csdn.net/lgm97/article/details/6409217 <!-- =====  页面滚动区域开始  ====================== ...

  5. Parallel for loops in .NET C# z

    The start index: this is inclusive, i.e. this will be the first index value in the loop The end inde ...

  6. 中文Win7下成功安装calabash-android步骤

    Calabash-android是支持android的UI自动化测试框架,网上看见很多同学说,安装calabash比较费劲,特别是Windows下安装,也没有一个详细的安装手册可供参考.正好,今天在W ...

  7. Js 与 TextArea

    当给一个js变量赋值一个有换行的值得时候,就会报错: <!DOCTYPE HTML> <html> <head> <script src="http ...

  8. C++空类以及没有成员变量的类的大小

    关于C++中空类的大小为1,我们大家都有所了解,但是除了空类之外的其他一些没有成员变量的类的大小,还是有很多不明之处的. 我们来看如下一个例子: #include<iostream> us ...

  9. ThinkPHP中SQL调试方法

    $admin = D('Admin'); $info = $admin->field('`lastlogintime`,`lastloginip`')->where(array('user ...

  10. JVM 必备指南

    简介 Java虚拟机(JVM)是Java应用的运行环境,从一般意义上来讲,JVM是通过规范来定义的一个虚拟的计算机,被设计用来解释执行从Java源码编译而来的字节码.更通俗地说,JVM是指对这个规范的 ...