[iOS]仿微博视频边下边播之封装播放器

注意:框架已经迭代到2.0版本,我重新架构了整个框架,API 也得到了更好的设计,我为 2.0 版本的实现写了一篇文章 [iOS]如何重新架构 JPVideoPlayer ?。此文中的实现思路仍然是一致的,但是实现细节已经不能沿用了,具体细节请前往我的 GitHub 查看。

Tips:这次的内容分为两篇文章讲述

01、[iOS]仿微博视频边下边播之封装播放器 讲述如何封装一个实现了边下边播并且缓存的视频播放器。

02、[iOS]仿微博视频边下边播之滑动TableView自动播放 讲述如何实现在tableView中滑动播放视频,并且是流畅,不阻塞线程,没有任何卡顿的实现滑动播放视频。同时也将讲述当tableView滚动时,以什么样的策略,来确定究竟哪一个cell应该播放视频。

微博视频的特点:

  • 秒拍团队主要致力于视频处理,微博的视频播放功能是由秒拍提供技术支持的。微博的视频一般都是不限时长的,所以它的特点是边下边播。
  • 说到视频播放就不能不提微信的短视频,微信的短视频限制时长为15秒,经过微信团队处理后,一个短视频的体积能控制在2MB以内。所以微信的视频是先下载,再读取下载好的视频文件进行播放,也就是所谓的先下后播。这个功能,微信的同行已经把源码分享出来了,在这里

我找了很多资料,没有找到完全意义上,实现了微博首页列表视频边下边播功能的资料。但是我自己项目中又有这个需求,所以只能自己动手。最后实现的效果如下:

2.x 版本效果如下:

这个列表视频边下边播包含以下主要的功能点:

  • 01.必须是边下边播。
  • 02.如果缓存好的视频是完整的,就要把这个视频保存起来,下次再次加载这个视频的时候,就先检查本地有没有缓存好的视频。这一点对于节省用户流量,提升用户体验很重要。要实现这一点,也就是说,我们要手动干预系统播放器加载数据的内部实现,这个细节后面再讲。
  • 03.不阻塞线程,不卡顿,滑动如丝顺滑,这是保证用户体验最重要的一点。
  • 04.当tableView滚动时,以什么样的策略,来确定究竟哪一个cell应该播放视频。

可能你着急赶项目,只想尽快的把这个功能集成到你的项目,那么请你直接去 Github 上下载源码。需要说明的是,我上面说的功能点的第一和第二点,不用你关心,我已经帮你处理封装好了。但是,第三和第四点,需要你自己结合你自己的项目来定制,我只提供了模板和巨细无比的注释。

接下来就来看看我是怎么实现这些功能的。

第一、AVPlayer 基本使用?

首先从最基本的封装播放器开始。

01、AVPlayer?

AVPlayer 播放视频需要涉及以下几个类:

  • AVURLAsset,是 AVAsset 的子类,负责网络连接,请求数据。
  • AVPlayerItem ,会建立媒体资源动态视角的数据模型并保存 AVPlayer 播放资源的状态。说白了,就是数据管家。
  • AVPlayer,播放器,将数据解码处理成为图像和声音。
  • AVPlayerLayer,图像层,AVPlayer 的图像要通过 AVPlayerLayer 呈现。

需要注意的是,AVPlayer 的模式是,你不要主动调用 play 方法播放视频,而是等待 AVPlayerItem 告诉你,我已经准备好播放了,你现在可以播放了,所以我们要监听 AVPlayerItem 的状态,通过添加监听者的方式获取 AVPlayerItem 的状态:

// 添加监听
[_currentPlayerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];
复制代码

在监听结果中处理播放逻辑。当监听到播放器已经准备好播放的时候,就可以调用 play 方法。 注意点:如果视频还没准备好播放,你就把 AVPlayerLayer 图层添加到 cell 上,那么在播放器还没有准备好播放之前,负责显示的图像的图层会变成黑色,直到准备好播放,拿到数据,才会出现画面。这在列表中自动播放是应该极力避免的。所以,要等待播放器有图像输出的时候再添加显示的预览图层到 cell 上。

    -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
      if ([keyPath isEqualToString:@"status"]) {
          AVPlayerItem *playerItem = (AVPlayerItem *)object;
          AVPlayerItemStatus status = playerItem.status;
          switch (status) {
              case AVPlayerItemStatusUnknown:{
                  
              }
                  break;
              case AVPlayerItemStatusReadyToPlay:{
                  [self.player play];
                  self.player.muted = self.mute;
                  // 显示图像逻辑
                  [self handleShowViewSublayers];
                
              }
                  break;
              case AVPlayerItemStatusFailed:{
                
              }
                  break;
              default:
                  break;
          }
      }
    }
复制代码

到这里就可以播放一个网络或者本地视频了。但是,在播放过程中:建立连接 --> 请求数据 --> 统筹数据 --> 数据解码 --> 输出图像和声音,这些过程都是 AVFoundation 框架下,我上面列举的那些类自动帮我们完成的。

要实现边下边播,并实现缓存功能,就必须拿到播放器的数据,也就是必须手动干预数据加载的过程。我们需要在网络层和解码层中间,插入一个我们自己需要的功能块,也就是我下图中的红色模块。

02、AVAssetResourceLoaderDelegate?
  • 要实现在播放器请求中插入自己的模块的功能,我们需要借助于 AVAssetResourceLoaderDelegate。我们用到的 AVURLAsset 下有一个 AVAssetResourceLoader 属性。
@property (nonatomic, readonly) AVAssetResourceLoader *resourceLoader;
复制代码
  • 这个 AVAssetResourceLoader 是负责数据加载的,最最重要的是我们只要遵守了 AVAssetResourceLoaderDelegate,就可以成为它的代理,成为它的代理以后,数据加载可能会通过代理方法询问我们。
-(BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest;
-(void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest;
复制代码
  • AVAssetResourceLoader 通过你提供的委托对象去调节 AVURLAsset 所需要的加载资源。而很重要的一点是,AVAssetResourceLoader 仅在 AVURLAsset 不知道如何去加载这个 URL 资源时才会被调用,就是说你提供的委托对象在 AVURLAsset 不知道如何加载资源时才会得到调用。所以我们又要通过一些方法来曲线解决这个问题,把我们目标视频 URL 地址的 scheme 替换为系统不能识别的 scheme
- (NSURL *)getSchemeVideoURL:(NSURL *)url{
    // NSURLComponents用来替代NSMutableURL,可以readwrite修改URL。这里通过更改请求策略,将容量巨大的连续媒体数据进行分段
    // AVAssetResourceLoader通过你提供的委托对象去调节AVURLAsset所需要的加载资源。
    // 而很重要的一点是,AVAssetResourceLoader仅在AVURLAsset不知道如何去加载这个URL资源时才会被调用
    // 就是说你提供的委托对象在AVURLAsset不知道如何加载资源时才会得到调用。
    // 所以我们又要通过一些方法来曲线解决这个问题,把我们目标视频URL地址的scheme替换为系统不能识别的scheme
    NSURLComponents *components = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO];
    components.scheme = @"systemCannotRecognition";
    return [components URL];
}
复制代码

第二、手动干预系统播放器加载数据?

01、如何使用 NSURLSessionc来下载大文件?

NSURLSession 之前,大家都是使用 NSURLConnection。如今在 Xcode 7 中,NSURLConnection 已经成为过期的类目了,我们常用的 AFNNetwork 也彻底抛弃了NSURLConnection,转向 NSURLSession。现在看一下怎么使用 NSURLSession

// 替代NSMutableURL, 可以动态修改scheme
NSURLComponents *actualURLComponents = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO];
actualURLComponents.scheme = @"http";

// 创建请求
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[actualURLComponents URL] cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:20.0];

// 修改请求数据范围
if (offset > 0 && self.videoLength > 0) {
    [request addValue:[NSString stringWithFormat:@"bytes=%ld-%ld",(unsigned long)offset, (unsigned long)self.videoLength - 1] forHTTPHeaderField:@"Range"];
}

// 重置
[self.session invalidateAndCancel];

// 创建Session,并设置代理
self.session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];

// 创建会话对象
NSURLSessionDataTask *dataTask = [self.session dataTaskWithRequest:request];

// 开始下载
[dataTask resume];
复制代码

我们可以在 NSURLSession 的代理方法中获得下载的数据,拿到下载的数据以后,我们使用 NSOutputStream,将数据写入到硬盘中存放临时文件的文件夹。在请求结束的时候,我们判断是否成功下载好文件,如果下载成功,就把这个文件转移到我们的存储成功文件的文件夹。如果下载失败,就把临时数据删除。

// 1.接收到服务器响应的时候
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler;

// 2.接收到服务器返回数据的时候调用,会调用多次
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data;

// 3.请求结束的时候调用(成功|失败),如果失败那么error有值
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error;
复制代码
02、AVAssetResourceLoader的代理?

为了更好的封装性和可维护性,新建一个文件,让这个文件负责和系统播放器对接数据。上面说到,只要这个文件遵守了 AVAssetResourceLoaderDelegate 协议,他就有资格代理系统播放器请求数据。并且系统会通过

-(BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest;
复制代码

这个代理方法,把下载请求 loadingRequest 传给我们。拿到请求以后,首先把请求用一个数组保存起来。为什么要用数组保存起来?因为,当我们拿到请求去下载数据,到数据下载好,这个过程需要的时间是不确定的。

拿到请求以后,我们就需要调用上面封装的 NSURLSession 下载器来下载文件。

- (void)dealLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest{
    NSURL *interceptedURL = [loadingRequest.request URL];
    NSRange range = NSMakeRange(loadingRequest.dataRequest.currentOffset, MAXFLOAT);

    if (self.manager) {
        if (self.manager.downLoadingOffset > 0)
        [self processPendingRequests];

        // 如果新的rang的起始位置比当前缓存的位置还大300k,则重新按照range请求数据
        if (self.manager.offset + self.manager.downLoadingOffset + 1024*300 < range.location
        // 如果往回拖也重新请求
        || self.manager.offset > range.location) {
            [self.manager setUrl:interceptedURL offset:range.location];
        }
    }
    else{
            self.manager = [JPDownloadManager new];
            self.manager.delegate = self;
            [self.manager setUrl:interceptedURL offset:0];
    }
}
复制代码

如果文件有下载好,就去检查下载好的数据长度有没有满足请求数据需要的长度,如果满足,就从硬盘的临时文件中取出对应的数据,并把这段数据填充给请求,然后把这个请求从请求列表数组中移除。播放器拿到了这段数据,就可以开始解码播放了。

// 判断此次请求的数据是否处理完全, 和填充数据
- (BOOL)respondWithDataForRequest:(AVAssetResourceLoadingDataRequest *)dataRequest{
    // 请求起始点
    long long startOffset = dataRequest.requestedOffset;

    // 当前请求点
    if (dataRequest.currentOffset != 0)
        startOffset = dataRequest.currentOffset;

    // 播放器拖拽后大于已经缓存的数据
    if (startOffset > (self.manager.offset + self.manager.downLoadingOffset))
            return NO;

    // 播放器拖拽后小于已经缓存的数据
    if (startOffset < self.manager.offset)
        return NO;

    NSData *fileData = [NSData dataWithContentsOfFile:_videoPath options:NSDataReadingMappedIfSafe error:nil];

    NSInteger unreadBytes = self.manager.downLoadingOffset - self.manager.offset - (NSInteger)startOffset;
    NSUInteger numberOfBytesToRespondWith = MIN((NSUInteger)dataRequest.requestedLength, unreadBytes);

    [dataRequest respondWithData:[fileData subdataWithRange:NSMakeRange((NSUInteger)startOffset- self.manager.offset, (NSUInteger)numberOfBytesToRespondWith)]];

    long long endOffset = startOffset + dataRequest.requestedOffset;

    BOOL didRespondFully = (self.manager.offset + self.manager.downLoadingOffset) >= endOffset;

    return didRespondFully;
}
复制代码

至此,手动干预播放视频的流程就走完了。已经可以正常播放视频了。

03、加载缓存数据逻辑?

接下来要做的就是实现,当下次播放同一个视频的时候,先去检查硬盘里有没有这个文件的缓存。借助于 NSFileManager,我们可以查找指定的路径有没有存在指定的文件,从而判断有没有缓存可以启用。

NSFileManager *manager = [NSFileManager defaultManager];
NSString *savePath = [self fileSavePath];
savePath = [savePath stringByAppendingPathComponent:self.suggestFileName];
if ([manager fileExistsAtPath:savePath]) { 
    // 已经存在这个下载好的文件了
    return;
}
复制代码

至此,播放器封装完毕。

我将在下一篇文章 [iOS]仿微博视频边下边播之滑动 TableView 自动播放 ,讲述如何实现在 tableView 中滑动播放视频,并且是流畅,不阻塞线程,没有任何卡顿的实现滑动播放视频。同时也将讲述当 tableView 滚动时,以什么样的策略,来确定究竟哪一个 cell 应该播放视频。

03、更新

  • 2016.10.09 : 处理在切换视频的短暂时间内, 当前播放视频的 cell 吸收了滑动事件, 如果滑动当前播放视频的 cell, 会导致 tableView 无法接收到滑动事件, 造成 tableView 假死。 感谢提供 bug 的朋友@大墙66370 具体见我的 Github JPVideoPlayer

  • 2016.11.04: 简书朋友@菜先生提交了一个关于单例里重复添加监听的问题, 具体是播放工具单例在每次调用 init 方法时总会重复添加监听播放完成等的通知, 会导致通知方法重复调用, 这个问题可能带来卡顿. 最新的版本已经修复了这个问题, 具体见我的 Github JPVideoPlayer

  • 2016.11.08 感谢简书作者 @老孟(http://www.jianshu.com/users/9f6960a40be6/timeline), 他帮我测试了多数的真机设备, 包括iPhone 5s 国行 系统9.3.5  iPhone 6plus 港行 系统10.0.2 iPhone 6s 国行 系统9.3.2  iPhone 6s plus 港行 系统10.0.0 iPhone 7plus 国行 系统10.1.1, 我之前由于手上设备有限, 只测试了 iPhone 6s 和 iPhone 6s plus, 但是 @老孟发 现在较旧设备上有卡顿的现象, 具体表现为播放本地已经缓存的视频的时候会出现2-3秒的假死, 其实是阻塞了主线程. 现在经过修改过后的版本修复了这个问题, 并且以上设备都测试通过, 没有出现卡顿情况.

  • 2016.11.10 关闭播放器以后, 视频还在后台播放的bug已经修复提交, 详见 JPVideoPlayer。感谢简书朋友@花无缺_提交的 bug.

  • 2016.11.18 1.修复了可能出现, 当播放一些特别小的视频文件时, 会出现播放不了的情况. 2.添加了缓存管理的工具类, 你可以调用 -getSize: 方法异步获取缓存大小. 也可以使用 -clearVideoCacheForUrl: 或者 -clearAllVideoCache 方法清除缓存.

  • 2017.05.02 更新. 有些朋友反应有些视频无法边下边播, 具体解决思路请参考 这篇博文

注意:框架已经迭代到2.0版本,我重新架构了整个框架,API 也得到了更好的设计,我为 2.0 版本的实现写了一篇文章 [iOS]如何重新架构 JPVideoPlayer ?。此文中的实现思路仍然是一致的,但是实现细节已经不能沿用了,具体细节请前往我的 GitHub 查看。

NewPan 的文章集合

下面这个链接是我所有文章的一个集合目录。这些文章凡是涉及实现的,每篇文章中都有 Github 地址,Github 上都有源码。

NewPan 的文章集合索引

如果你有问题,除了在文章最后留言,还可以在微博 @盼盼_HKbuy 上给我留言,以及访问我的 Github
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值