ios AVPlayer 边下边播音频、视频详解

同志们我怀着激动而又沉重的心情写这篇文章,这篇文章我主要是介绍我怎么一点点一步步把边下边播弄懂的,而且我觉得难点还有重点我都会重点说明,保证没有弄过变下边播的人看了这篇文章都能明白并且自己会做一些修改。

注意点或者建议:

1.先大致看一下我的文章,然后估计你在没有看代码的时候可能还是有些不明白。然后去看代码,代码看的差不多了在来看我的文章你应该就明白了
2.放心 代码什么的都在git上给大家放好了,在文章结尾,这样做的目的是为了大家先看完文章在读代码如果先读代码可能效果不明显。
3.文章中的图片有可能会跟你们看到的其他文章相同,放心本人并不是抄袭他们,我只是觉得他们写的对的会拿到我的文章中,但是他们文章中没有说到的点或者他们比较模糊的点我都会说明
4.看这个文章的时候希望大家不要浮躁,过于浮躁可能会使你只知道大概但是要问你细节你可能就不明白了。
5.安装缓存的音频或者视频的sdk的时候需要到入库


image.png

封装的结构

image.png

解释:其中我封装的东西在这个四个文件中。第一个文件中是非缓存的音频,也就是说不能缓存音乐的,第二个文件是非缓存的视频,第三个是缓存的音频(也支持非缓存的音频),第四个大家应该都知道了。为什么我第三个 第四个文件都包括了所有的功能 我还要写第一个文件和第二个文件。因为:假如你非常确定你的app是不用缓存的并且产品经理也非常的好那么干嘛写那么多没用的代码。

  • 拿一个视频缓存的文件介绍


    image.png

    其中framework我已经帮你打好了,但是他是debug,因为我如果给你打了release的framework一些断言的信息什么的你都看不到了,这样不利于你调试。下面我来介绍一下怎么使用:

@protocol DGCacheVideoPlayerDelegate <NSObject>
/**
 播放失败了
 
 @param error error
 */
- (void)DGCacheVideoPlayFailed:(NSError *)error;

/**
 一首歌曲播放完成了,会把下一首需要播放的歌曲返回来 会自动播放下一首,不要再这里播放下一首
 
 @param nextModel 下一首歌曲的模型
 */
- (void)DGCacheVideoPlayFinish:(DGCacheVideoModel *)nextModel;
/**
 播放状态发生了改变
 
 @param status 改变后的状态
 */
- (void)DGCacheVideoPlayStatusChanged:(DGCacheVideoState)status;
/**
 缓存的进度 注意:当需要缓存的时候是下载的进度 不需要缓存的时候是监听player loadedTimeRanges的进度
 
 @param cacheProgress 播放的缓存的进度
 */
- (void)DGCacheVideoCacheProgress:(CGFloat )cacheProgress;

/**
 当前时间 总的时间 缓冲的进度的播放代理回调
 
 @param currentTime 当前的时间
 @param durationTime 总的时间
 @param playProgress 播放的进度 (0-1)之间
 */
- (void)DGCacheVideoPlayerCurrentTime:(CGFloat)currentTime
                             duration:(CGFloat)durationTime
                         playProgress:(CGFloat)playProgress;



@end
@interface DGCacheVideoPlayer : NSObject
#pragma mark - 初始化
@property (weak, nonatomic) id <DGCacheVideoPlayerDelegate> DGCacheVideoDelegate;
+(instancetype)shareInstance;
#pragma mark - 设置相关的方法

/**
 设置播放列表没有设置播放列表播放器没有播放地址
 
 @param playList 需要播放的模型数组
 @param offset 偏移量
 @param videoGravity 视频的显示类型
 @param addViewLayer 需要添加的layer
 @param cache 是否缓存 YES: 缓存 NO:不缓存
 @param frame 视频的frame
 */
- (void)setPlayList:(NSArray<DGCacheVideoModel *> *)playList
             offset:(NSUInteger)offset
       videoGravity:(AVLayerVideoGravity)videoGravity
            addViewLayer:(CALayer *)addViewLayer
            isCache:(BOOL)cache
         layerFrame:(CGRect)frame;

/**
 点击下一个播放
 */
- (void)playNextVideo;
/**
 点击上一个播放
 */
- (void)playPreviousVideo;
/**
 设置当前的播放动作
 
 @param operate 动作: 播放、暂停、停止
 停止:清空播放列表,如果在要播放需要重新设置播放列表
 */
- (void)playOperate:(DGCacheVideoOperate)operate;
/**
 清空播放列表
 
 @param isStopPlay YES:停止播放 NO:不停止播放
 */
- (void)clearPlayList:(BOOL)isStopPlay;
/**
 删除一个播放列表
 
 @param deleteList 要删除的播放列表
 */
- (void)deletePlayList:(NSArray<DGCacheVideoModel *>*)deleteList;
/**
 添加一个新的歌单到播放列表
 
 @param addList 新的歌曲的数组
 */
- (void)addPlayList:(NSArray<DGCacheVideoModel *>*)addList;
/**
 快进或者快退
 
 @param time 要播放的那个时间点
 */
- (void)seekTime:(NSUInteger)time;
/**
 设置播放器的音量 非系统的  (不是点击手机音量加减键的音量)
 
 @param value 【0-10】大于10 等于10  小于0 等于0
 */
- (void)setVolumeValue:(CGFloat)value;

#pragma mark - 可以获得的方法
/**
 当前的播放状态,方便用户随时拿到
 
 @return 对应的播放状态
 */
- (DGCacheVideoState)currentPlayeStatus;
/**
 当前的播放的模型
 
 @return 当前的播放模型
 */
- (DGCacheVideoModel *)currentMusicModel;
/**
 当前播放歌曲的下标
 
 @return 为了你更加省心 我给你提供出来
 */
- (NSUInteger)currentIndex;
/**
 获得播放列表
 
 @return 播放列表
 */
- (NSArray<DGCacheVideoModel *> *)getPlayList;
/**
 获得当前播放器的总时间
 
 @return 时间
 */
- (CGFloat )durationTime;
/**
 获得播放器的音量
 */
- (CGFloat)getVolueValue;

解释:我们要做的首先就是设置播放列表和代理其他的就看你自己的需求了。然后播放器外部是看不到的。
比如我这样使用

NSArray *temArray = @[@"https://weiboshipin.cmvideo.cn/depository_sp/fsv/trans/2018/11/25/649656072/25/5bfaa1eb6633d9b67e3369fe.mp4"];
    NSMutableArray *infoArray = [NSMutableArray array];
    for (NSInteger index = 0; index < temArray.count; index ++) {
        DGVideoInfo *videoInfo = [[DGVideoInfo alloc] init];
        videoInfo.videoId = [NSString stringWithFormat:@"%zd",index];
        videoInfo.playUrl = temArray[index];
        [infoArray addObject:videoInfo];
    }
    
    [DGVideoManager shareInstance].DGDelegate = self;
    [[DGVideoManager shareInstance] setPlayList:infoArray offset:0 videoGravity:AVLayerVideoGravityResizeAspect addViewLayer:self.view.layer layerFrame:CGRectMake(0, 64, 375, 300)];

至于其他的我就不介绍怎么使用了,要想看怎么使用直接看demo就行了


image.png

边下边播的流程

  • 思路:
    我们正常情况下是AVPlayer 调用AVPlayerItem ,item在调用url 就可以实现播放了,但是现在是边下边播,我们就需要自己实现底层的那一套代理的方法,自己下载缓存,确定缓存好了的在告诉播放器进行播放。
  • 对AVPlayer的理解:
    AVPlayer他的主要的功能是对视频、音频的解码,他可以告诉播放速度、状态等等。他管理AVPlayerItem.
  • AVPlayerItem的理解:
    他的作用主要是承接AVPlyer和AVAsset的,主要是用来统筹数据的,判断各种状态,监听信息的(比如是否播放完成、当前时间等等)
  • AVAsset 的理解:
    他的作用主要是处理一些流信息(比如我们的视频包括:音频流、视频流等等)并且他还要和服务器打交道获取一些流信息。我们一般不使用他。一般用它的子类
    AVURLAsset.
    他们三个的关系我用一张图来表示:


    image.png
  • AVPlayerLayer 理解:
    这个东西在视频播放的时候才会用到,他的作用主要是用来显示画面用的
    疑问:但是我们为什么平时写非缓存的用不到AVURLAsset?
    其实我们也是用到了,比如我们平时可能这样写:
        NSURL *url = [NSURL URLWithString:VideoInfo.playUrl];
        AVPlayerItem *item = [AVPlayerItem playerItemWithURL:url];
        AVPlayer *player = [[AVPlayer alloc] initWithPlayerItem:item];
        self.playerItem = item;
        self.player = player;
        
        [self.layer removeFromSuperlayer];
        self.layer = [AVPlayerLayer playerLayerWithPlayer:self.player];
        self.layer.frame = self.videoFrame;
        self.layer.videoGravity = self.currentVideoGravity;
        [self.needAddLayer addSublayer:self.layer];
        
        [self.player play];
        [self addMyObserver];

这也没有用到啊,实际上这句话:

 AVPlayerItem *item = [AVPlayerItem playerItemWithURL:url];

苹果在内部已经给你处理好了,并且苹果是闭元的所以我们感觉没有用到罢了。

AVPlayer 对 视频、音频处理方式

其实给我们一个url(我拿视频做例子),其实他是一段一段进行处理的。我们都知道一个视频可以用ffmpeg对他进行分割 我们可以分割成很多段,其实这个也是一样的道理,拿一个图来举例子:


image.png

解释:假如我们分成8段(至于分成多少段、每段大小多大都有系统决定),开始时候我们下载第一段,下载完了配置信息确定是已经下载好了并且是完整的,就会交给播放器进行播放,然后下载队列还在下载其他的段。注意下载队列和我们返回给播放器的每段并没有直接的关系。他只是负责下载而我们会读取数据确定是能够播放的进行播放。

一般边下边播的产品需求

1.支持正常播放器的一切功能,包括暂停、播放和拖拽
2.如果视频加载完成且完整,将视频文件保存到本地cache,下一次播放本地cache中的视频,不再请求网络数据
3.如果视频没有加载完(半路关闭或者拖拽)就不用保存到本地cache,因为数据不完整,但是如果拖拽在缓存区的那么还是要缓存下来。一句话话只要缓存连续就缓存下来。不连续不缓存

根据产品需求我们做出如下的判断:

1.开始的时候请求下载队列正常下载,在这之前需要判断时候有临时的缓存文件如果有进行删除。是否有下载队列如果有停止下载并且取消任务。如果有缓存文件不进行下载直接本地播放音频、视频。
2.开启一个新的请求任务,并且往临时文件中写入数据。(因为我们只有确认是完成的数据才能将临时文件中的数据拷贝到永久缓存文件中)
3.读取临时文件中的数据,并且在在resourceloader代理中进行判断是否是完成的数据或者是否能够把这个数据传递给播放器,ok的直接传递给播放器进行播放。
4.如果出现拖拽,或前或后判断拖拽的是否大于缓存区,如果大于则说明缓存不能连续了,则继续开始一个新的下载队列并且从当前的请求位置开始进行请求下载。
5.如果缓存是完整的则对下载链接进行md5 加密作为保存路径从临时文件中copy数据到永久缓存文件中。

实现方案

我们正常的情况下 这样写代码

        self.needAddLayer = addViewLayer;
        self.videoFrame = frame;
        self.currentVideoGravity = videoGravity;
        
        self.resourceLoader = [[DGVideoResourceLoader alloc] init];
        self.resourceLoader.loaderDelegate = self;
        AVURLAsset *urlAset = [AVURLAsset URLAssetWithURL:self.currentModel.playUrl options:nil];
        [urlAset.resourceLoader setDelegate:self.resourceLoader queue:dispatch_get_main_queue()];
        self.playerItem = [AVPlayerItem playerItemWithAsset:urlAset];
        self.player = [AVPlayer playerWithPlayerItem:self.playerItem];
        
        [self.layer removeFromSuperlayer];
        self.layer = [AVPlayerLayer playerLayerWithPlayer:self.player];
        self.layer.frame = self.videoFrame;
        self.layer.videoGravity = self.currentVideoGravity;
        [self.needAddLayer addSublayer:self.layer];
        
        [self.player play];
        [self addMyObserver];

这样的话是不会调用delegate的,必须我们将我们的下载链接的url的scheme改了 随便换成一个(不能使http之类的)才会进我们的代理。所以我们的方案是(这个图是我从别的地方拿过来的因为他说的很对):


image.png

其中resourceLoader有两个delegate使我们经常使用的
第一个:

/**
 avasert 每次都会进这个方法,他会返回每次的loadingRequest
 
 @param resourceLoader resourceLoader
 @param loadingRequest loadingRequest
 @return 如果为YES:继续返回 NO:终止返回不在返回loadingRequest
 */
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest 

这个方法就是我上面所说的苹果给我们返回来一段一段的流数据,也就是每一个loadingRequest,他们在整个期间调用多次。并且我们会在这个方法进行判断是否是已经配置好的信息
第二个:

/**
 处理完成的请求取消
 
 @param resourceLoader resourceLoader
 @param loadingRequest loadingRequest
 */
- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest

这个方法就是我们配置完信息告诉完播放器进行播放了之后,我们要把处理完的数据移除 就会调用这个方法。这个放的触发是在这个方法之后:

[loadingRequest finishLoading];

联系起来代码是这样的:

#pragma mark - avassetResourceLoaderDelegate
/**
 avasert 每次都会进这个方法,他会返回每次的loadingRequest
 
 @param resourceLoader resourceLoader
 @param loadingRequest loadingRequest
 @return 如果为YES:继续返回 NO:终止返回不在返回loadingRequest
 */
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest {
    dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
    [self handleLoadingRequest:loadingRequest];
    return YES;
}

/**
 处理完成的请求取消
 
 @param resourceLoader resourceLoader
 @param loadingRequest loadingRequest
 */
- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest{
    // 已经取消的 从数组中移除
    [self.requestList removeObject:loadingRequest];
}
#pragma mark - 自己事件的处理
/**
 处理delegatee给的loadingRequest
 
 @param loadingRequest loadingRequest
 */
- (void)handleLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest{
    [self.requestList addObject:loadingRequest];
    if (self.downloadManager) {
        if (loadingRequest.dataRequest.requestedOffset >= self.downloadManager.requestOffset &&
            loadingRequest.dataRequest.requestedOffset <= self.downloadManager.requestOffset + self.downloadManager.cacheLength) {
            //数据已经缓存,则直接完成
           
            [self haveCacheProcessRequestList];
        }else {
            //数据还没缓存,则等待数据下载;如果是Seek操作,则重新请求
            if (self.isSeek) {
               
                [self startNewLoadrequest:loadingRequest cache:NO];
            }
        }
    }else {
        [self startNewLoadrequest:loadingRequest cache:YES];
    }
    // 完事就要发送信号
    dispatch_semaphore_signal(self.semaphore);
}
/**
 处理缓存好的的请求
 */
- (void)haveCacheProcessRequestList{
    
    NSMutableArray * finishRequestList = [NSMutableArray array];
    for (AVAssetResourceLoadingRequest * loadingRequest in self.requestList) {
        if ([self configFinishLoadingRequest:loadingRequest]) {
            [finishRequestList addObject:loadingRequest];
        }
    }
    if (finishRequestList.count) {
      [self.requestList removeObjectsInArray:finishRequestList];
    }
}

/**
 配置相关的信息,并且判断完成了没
 
 @param loadingRequest loadingRequest
 @return 是否完成了
 */
- (BOOL)configFinishLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest{
    
    NSString *mineType = [self.downloadManager getMyMimeType];
    if (mineType.length == 0) {
        mineType = @"video/mp4";
    }
    //填充信息
    CFStringRef contentType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, (__bridge CFStringRef)(mineType), NULL);
    loadingRequest.contentInformationRequest.contentType = CFBridgingRelease(contentType);
    loadingRequest.contentInformationRequest.byteRangeAccessSupported = YES;
    loadingRequest.contentInformationRequest.contentLength = self.downloadManager.fileLenth;
    
    //读文件,填充数据
    NSUInteger cacheLength = self.downloadManager.cacheLength;
    NSUInteger requestedOffset = loadingRequest.dataRequest.requestedOffset;
    if (loadingRequest.dataRequest.currentOffset != 0) {
        requestedOffset = loadingRequest.dataRequest.currentOffset;
    }
    NSUInteger canReadLength = cacheLength - (requestedOffset - self.downloadManager.requestOffset);
    NSUInteger respondLength = MIN(canReadLength, loadingRequest.dataRequest.requestedLength);
    NSUInteger offset = requestedOffset - self.downloadManager.requestOffset;
    if (requestedOffset < self.downloadManager.requestOffset) {
        offset = 0;
    }
    [loadingRequest.dataRequest respondWithData:[DGVideoStrFileHandle readTempFileDataWithOffset:offset length:respondLength]];
    //如果完全响应了所需要的数据,则完成
    NSUInteger nowendOffset = requestedOffset + canReadLength;
    NSUInteger reqEndOffset = loadingRequest.dataRequest.requestedOffset + loadingRequest.dataRequest.requestedLength;
    if (nowendOffset >= reqEndOffset) {
        [loadingRequest finishLoading];
        return YES;
    }
    return NO;
}
/**
 开始发送新的请求
 
 @param loadingRequest loadingRequest
 @param isCache isCache
 */
- (void)startNewLoadrequest:(AVAssetResourceLoadingRequest *)loadingRequest cache:(BOOL)isCache{
    
    NSUInteger fileLength = 0;
    if (self.downloadManager) {
        fileLength = self.downloadManager.fileLenth;
        self.downloadManager.cancel = YES;
    }
    self.downloadManager = [[DGVideoDownloadManager alloc] init];
    self.downloadManager.requestURL = loadingRequest.request.URL;
    self.downloadManager.requestOffset = loadingRequest.dataRequest.requestedOffset;
    self.downloadManager.isCache = isCache;
    if (fileLength > 0) {
        self.downloadManager.fileLenth = fileLength;
    }
    self.downloadManager.downloadManagerDelegate = self;
    [self.downloadManager startRequest];
    self.isSeek = NO;
}

流程解释:
1.开始请求 并且创建一个下载队列。
2.在delegate回调之后我们要判断是否是已经下载好的或者是否有拖拽的,如果是无拖拽的正常下载我们会对他配置信息并且交给播放器播放。
3.如果有拖拽并且大于缓存区了那么会重新开始一个下载队列,并且删除之前的临时缓存文件
4.配置完信息并且确定是加完的loadingRequest 从数组中移除。因为我们开始会创建一个数组保存所有的loadingRequest。
5.回调信息等等会回调(进度、当前时间、总时间、播放状态等等自己定义的delegate)

难于理解的点或者比较疏忽的点或者自己遇到的坑(个人认为)

1.怎样触发resourceloader 那就更改他的scheme
我的代码是这样写的:

   NSURL *myUrl = [NSURL URLWithString:str];
    
    NSString *schemeName = myUrl.scheme;
    [[NSUserDefaults standardUserDefaults] setObject:schemeName forKey:DGVideoSchemeKey];
    [[NSUserDefaults standardUserDefaults] synchronize];
    
    NSURLComponents * components = [[NSURLComponents alloc] initWithURL:myUrl resolvingAgainstBaseURL:NO];
    components.scheme = @"streaming";
    return [components URL];
}

然后我获取之前的scheme是这样写的:

    NSString *schemeName = [[NSUserDefaults standardUserDefaults] objectForKey:DGVideoSchemeKey];
    NSURLComponents * components = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO];
    components.scheme = schemeName.length > 0 ? schemeName: @"http";
    return [components URL];

这样写的好处 就是之前是啥拿到之后就是之前那个,我看网上很多人直接返回返回 http的,那如果是https的不就不行了吗。这是一个注意点
2.我们在点击下一首或者上一首等等的需要把之前的临时文件删除。如果不删除那么缓存进相当于原来的数据相加 那样肯定是有问题的,比如我是这样处理的

        self.resourceLoader.downloadManager.cancel = YES;
        self.resourceLoader = nil;
        [DGVideoStrFileHandle deleleTempFile];

3.我们在获取minetype的时候尽量用服务器返回给我们的那个

self.innerMyMimeType = response.MIMEType;

引用的地方这样引用的,我拿视频作为例子:

    NSString *mineType = [self.downloadManager getMyMimeType];
    if (mineType.length == 0) {
        mineType = @"video/mp4";
    }

我看网上的人直接写死的 video/mp4,那一但不是呢?那不就出问题了(因为本人不知道视频的mineType是不是都是video/mp4,个人理解不一定都是)
4.对下面这个方法详细解释:

if (loadingRequest.dataRequest.requestedOffset >= self.downloadManager.requestOffset &&
            loadingRequest.dataRequest.requestedOffset <= self.downloadManager.requestOffset + self.downloadManager.cacheLength) {
            //数据已经缓存,则直接完成
           
            [self haveCacheProcessRequestList];
        }else {
            //数据还没缓存,则等待数据下载;如果是Seek操作,则重新请求
            if (self.isSeek) {
               
                [self startNewLoadrequest:loadingRequest cache:NO];
            }
        }

起初我当时不明白的是为什么第一个判断中&&后面的还要加上:self.downloadManager.requestOffset 就因为这个我纠结了好久问了同事 说实话他也是支支吾吾后来我们一起研究出来了。他是这样的:
因为正常情况下:self.downloadManager.requestOffset都是等于0的,因为他是在这个方法中给他赋值的:

- (void)startNewLoadrequest:(AVAssetResourceLoadingRequest *)loadingRequest cache:(BOOL)isCache

刚开始请求的时候这个肯定是0 后来正常的请求他们也不会开始新的请求 所以就相当于小于缓存文件的长度这个判断是正确的。假如现在出现了拖拽:1.在缓存文件范围之内 那么self.downloadManager.requestOffset还是0 所以还是对的,2.假如不在缓存文件之内 那么他会重新开始一个新的请求 这时候self.downloadManager.requestOffset是上一个请求的的requestOffset,但是这时候的cacheLength也就不是之前的cacheLength ,个人理解就是当前段的缓存 并不是之前段的那些缓存相加。所以这个判断还是对的。这个判断写的很神奇也很巧妙不知道我这样解释你懂了吗。
5.关于配置信息的这个方法

   NSString *mineType = [self.downloadManager getMyMimeType];
    if (mineType.length == 0) {
        mineType = @"video/mp4";
    }
    //填充信息
    CFStringRef contentType = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, (__bridge CFStringRef)(mineType), NULL);
    loadingRequest.contentInformationRequest.contentType = CFBridgingRelease(contentType);
    loadingRequest.contentInformationRequest.byteRangeAccessSupported = YES;
    loadingRequest.contentInformationRequest.contentLength = self.downloadManager.fileLenth;
    
    //读文件,填充数据
    NSUInteger cacheLength = self.downloadManager.cacheLength;
    NSUInteger requestedOffset = loadingRequest.dataRequest.requestedOffset;
    if (loadingRequest.dataRequest.currentOffset != 0) {
        requestedOffset = loadingRequest.dataRequest.currentOffset;
    }
    NSUInteger canReadLength = cacheLength - (requestedOffset - self.downloadManager.requestOffset);
    NSUInteger respondLength = MIN(canReadLength, loadingRequest.dataRequest.requestedLength);
    NSUInteger offset = requestedOffset - self.downloadManager.requestOffset;
    if (requestedOffset < self.downloadManager.requestOffset) {
        offset = 0;
    }
    [loadingRequest.dataRequest respondWithData:[DGVideoStrFileHandle readTempFileDataWithOffset:offset length:respondLength]];
    //如果完全响应了所需要的数据,则完成
    NSUInteger nowendOffset = requestedOffset + canReadLength;
    NSUInteger reqEndOffset = loadingRequest.dataRequest.requestedOffset + loadingRequest.dataRequest.requestedLength;
    if (nowendOffset >= reqEndOffset) {
        [loadingRequest finishLoading];
        return YES;
    }
    return NO;

其他的我写的注释 很详细 我主要是解释这段代码


image.png

为什么要

cacheLength - (requestedOffset - self.downloadManager.requestOffset);

还是和刚才差不多的self.downloadManager.requestOffset在这个方法里基本都是0,相当于cacheLength - requestedOffset ,这不正好就是那一段的可以读的requestedOffset,其中这个方法:

[loadingRequest.dataRequest respondWithData:[DGVideoStrFileHandle readTempFileDataWithOffset:offset length:respondLength]];

就是告诉播放器流数据播放器好进行播放。当调用了这个方法

[loadingRequest finishLoading];

也就是loadingRequest处理完了 就开始调用删除方法的delegate了。
6.注意我们在创建临时文件的时候 尽量不要创建文件夹。
什么意思 比如我的临时文件路径是:

#define DGMyTempPath [[NSHomeDirectory() stringByAppendingPathComponent:@"tmp"] stringByAppendingPathComponent:@"videoTemp.mp4"]

我的意思是尽量不要这样写:

#define DGMyTempPath [[NSHomeDirectory() stringByAppendingPathComponent:@"tmp/MediaCahce"] stringByAppendingPathComponent:@"videoTemp.mp4"]

在加一个文件夹因为这样写 创建时间有点稍微长,如果你立刻写入数据可能会写入失败。
7.关于AVPlayer 和 AVPlayeritem这个几个观察属性的解释

#define DGPlayerRateKey @"rate"
#define DGPlayerLoadTimeKey @"loadedTimeRanges"
#define DGPlayerBufferEmty @"playbackBufferEmpty"
#define DGPlayerLikelyToKeepUp @"playbackLikelyToKeepUp"

其中:
1.rate = 1.0 并不一定就是播放,怎么才是真正的播放我在代码中有处理
2.loadedTimeRanges 进度 当进度 = 1 之后不再调用了,本地播放也不调用
3.playbackBufferEmpty 说明缓存中呢,但是下载缓存的视频也不准,具体看我的代码处理
4.playbackLikelyToKeepUp 一般没有什么用,我没有处理
5.开始播放的时候并不会进rate的这个观察者中 当暂停或者继续才会调用。

8.关于下载队列中某点说明
其中下载队列中的这段代码:

  if (self.requestOffset > 0) {
        [request addValue:[NSString stringWithFormat:@"bytes=%ld-%ld", self.requestOffset, self.fileLenth - 1] forHTTPHeaderField:@"Range"];
    }

其实这个代码在做断点续传的时候有用到 他的的意思是告诉请求头从那块开始下载到哪块结束
9.小小的注意点
AVPlayer 并没有直接停止的方法 只有暂停的方法

代码的存放位置

实话说 我这样给你讲了 你可能有的点还是不明白 所以我开始就是建议先大致读一下然后读代码,读完代码在来读我的文章。我的代码为在:https://github.com/liudiange/DGCachePlayer/tree/master

封装的sdk的特点

1.外部是看不到avplayer 的,这样写的好处是外部不能随便改播放器
2.功能齐全 ,有如下的功能:


image.png

并且像音频播放中的单曲循环 当你点击下一曲的时候我会通过你传递属性来判断是否是真正的要切换到下一曲
3.有缓存的时候 缓存进度是下载的进度,非缓存的情况下是监听loadedTimeRanges的进度
4.视频有无缓存、音频有无缓存以及单独的sdk 我都给你封装好了
5.会通过代理方式告诉当前的状态等等,条件不符合会断言告诉你哪里错了,判断充足,比如我当前的状态判断有这么多:

typedef NS_ENUM(NSUInteger,DGCacheVideoState) {
    DGCacheVideoStatePlay        = 1, // 播放
    DGCacheVideoStatePause       = 2, // 暂停
    DGCacheVideoStateBuffer      = 3, // 缓冲
    DGCacheVideoStateStop        = 4, // 停止
    DGCacheVideoStateError       = 5, // 错误
};

6.当前在播放音频或者视频的时候 当你控制器引用完成之后如果你不让他继续播放需要做停止的操作,我会自动帮你把所有无用的信息清空
7.添加歌曲列表或者删除列表我会做去重判断,根据id来进行判断的。我每个sdk都有模型 包括歌曲或者视频的基本信息,你如果要使用可以继承我的model
8.假如发现bug或者不足的点可以在我的基础上修改并且重新打包自己使用。
9.没有网络什么的并没有在sdk中写,因为一般来说一个app都会有检查网络情况的方法
10.在播放缓存音乐的时候你会发现下载完了再播放 ,其实我仔细看了没有问题,系统的也是缓存完了才播放。并且那个delegate值回调一次 并不是多次。这个可能是根据音频还有视频有关系 或者跟当前的音乐资源有关系。
11.视频中可能画面有点歪, 那个是约束还有你选的播放模式有关系 。不是sdk的问题。
12.demo中我没有对那些控件做约束,建议你在6s的手机或者模拟器中查看,我就是在此基础上随便做的,因为界面并不是我们的重点。

题外话

今天废话有点多了,这个东西 耗费我20多天的时间,当然是工作之余。我一步一步弄明白的,如果你觉得我说的不对或者封装的sdk不好 欢迎issues me

推荐阅读更多精彩内容