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

dbcn7230 8年前
   <p>微博视频的特点:</p>    <ul>     <li> <p>秒拍团队主要致力于视频处理,微博的视频播放功能是由秒拍提供技术支持的。微博的视频一般都是不限时长的,所以它的特点是边下边播。</p> </li>     <li> <p>说到视频播放就不能不提微信的短视频,微信的短视频限制时长为15秒,经过微信团队处理后,一个短视频的体积能控制在2MB以内。所以微信的视频是先下载,再读取下载好的视频文件进行播放,也就是所谓的先下后播。这个功能,微信的同行已经把源码分享出来了,在这里。</p> </li>    </ul>    <p>我找了很多资料,没有找到完全意义上,实现了微博首页列表视频边下边播功能的资料。但是我自己项目中又有这个需求,所以只能自己动手。最后实现的效果如下:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/99410bd64ea14bab99998de5bdc1963c.gif"></p>    <p style="text-align:center">JPVideoPlayer.gif</p>    <p>这个列表视频边下边播包含以下主要的功能点:</p>    <ul>     <li> <p>01.必须是边下边播。</p> </li>     <li> <p>02.如果缓存好的视频是完整的,就要把这个视频保存起来,下次再次加载这个视频的时候,就先检查本地有没有缓存好的视频。这一点对于节省用户流量,提升用户体验很重要。要实现这一点,也就是说,我们要手动干预系统播放器加载数据的内部实现,这个细节后面再讲。</p> </li>     <li> <p>03.不阻塞线程,不卡顿,滑动如丝顺滑,这是保证用户体验最重要的一点。</p> </li>     <li> <p>04.当tableView滚动时,以什么样的策略,来确定究竟哪一个cell应该播放视频。</p> </li>    </ul>    <p>来看看我是怎么实现这些功能的。</p>    <h2><strong>第一、AVPlayer基本使用?</strong></h2>    <p>首先从最基本的封装播放器开始。</p>    <p><strong>01、AVPlayer?</strong></p>    <p>AVPlayer播放视频需要涉及以下几个类:</p>    <ul>     <li> <p><strong>AVURLAsset</strong>,是AVAsset的子类,负责网络连接,请求数据。</p> </li>     <li> <p><strong>AVPlayerItem,</strong>会建立媒体资源动态视角的数据模型并保存AVPlayer播放资源的状态。说白了,就是数据管家。</p> </li>     <li> <p><strong>AVPlayer</strong>,播放器,将数据解码处理成为图像和声音。</p> </li>     <li> <p><strong>AVPlayerLayer</strong>,图像层,AVPlayer的图像要通过AVPlayerLayer呈现。</p> </li>    </ul>    <p>需要注意的是,AVPlayer的模式是,你不要主动调用play方法播放视频,而是等待AVPlayerItem告诉你,我已经准备好播放了,你现在可以播放了,所以我们要监听AVPlayerItem的状态,通过添加监听者的方式获取AVPlayerItem的状态:</p>    <pre>  <code class="language-objectivec">// 添加监听  [_currentPlayerItem addObserver:self forKeyPath:@"status" options:NSKeyValueObservingOptionNew context:nil];</code></pre>    <p>在监听结果中处理播放逻辑。当监听到播放器已经准备好播放的时候,就可以调用play方法。</p>    <p>注意点:如果视频还没准备好播放,你就把AVPlayerLayer图层添加到cell上,那么在播放器还没有准备好播放之前,负责显示的图像的图层会变成黑色,直到准备好播放,拿到数据,才会出现画面。这在列表中自动播放是应该极力避免的。所以,要等待播放器有图像输出的时候再添加显示的预览图层到cell上。</p>    <pre>  <code class="language-objectivec">-(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;        }    }  }</code></pre>    <p>到这里就可以播放一个网络或者本地视频了。但是,在播放过程中:建立连接-->请求数据-->统筹数据-->数据解码-->输出图像和声音,这些过程都是AVFoundation框架下,我上面列举的那些类自动帮我们完成的。</p>    <p><img src="https://simg.open-open.com/show/5a87500fb9f7ad7f94bf5710c8dce389.png"></p>    <p style="text-align:center">系统处理.png</p>    <p>要实现边下边播,并实现缓存功能,就必须拿到播放器的数据,也就是必须手动干预数据加载的过程。我们需要在网络层和解码层中间,插入一个我们自己需要的功能块,也就是我下图中的红色模块。</p>    <p><img src="https://simg.open-open.com/show/6aca7909fd0d4b08b5910e55e9d75005.png"></p>    <p style="text-align:center">手动干预.png</p>    <p><strong>02、AVAssetResourceLoaderDelegate?</strong></p>    <ul>     <li> <p>要实现在播放器请求中插入自己的模块的功能,我们需要借助于AVAssetResourceLoaderDelegate。我们用到的AVURLAsset下有一个AVAssetResourceLoader属性。</p> <pre>  <code class="language-objectivec">@property (nonatomic, readonly) AVAssetResourceLoader *resourceLoader;</code></pre> </li>     <li> <p>这个AVAssetResourceLoader是负责数据加载的,最最重要的是我们只要遵守了AVAssetResourceLoaderDelegate,就可以成为它的代理,成为它的代理以后,数据加载都会通过代理方法询问我们。这样,我们就找到切入口干预数据的加载了。</p> <pre>  <code class="language-objectivec">-(BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest;  -(void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest;</code></pre> </li>     <li> <p>在正式进入数据干预之前,我们先看一个很重要的东西。我们知道视频数据都是容量巨大的连续媒体数据,所以请求数据的时候,我们要将请求策略置为streaming。这个策略的含义是,将容量巨大的连续媒体数据进行分段,分割为数量众多的小文件进行传递。</p> <pre>  <code class="language-objectivec">- (NSURL *)getSchemeVideoURL:(NSURL *)url{    // NSURLComponents用来替代NSMutableURL,可以readwrite修改URL。这里通过更改请求策略,将容量巨大的连续媒体数据进行分段    // 分割为数量众多的小文件进行传递。采用了一个不断更新的轻量级索引文件来控制分割后小媒体文件的下载和播放,可同时支持直播和点播    NSURLComponents *components = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO];    components.scheme = @"streaming";    return [components URL];  }</code></pre> </li>    </ul>    <h2><strong>第二、手动干预系统播放器加载数据?</strong></h2>    <p><strong>01、如何使用NSURLSession来下载大文件?</strong></p>    <p>在NSURLSession之前,大家都是使用NSURLConnection。如今在Xcode7中,NSURLConnection已经成为过期的类目了,我们常用的AFNNetwork也彻底抛弃了NSURLConnection,转向NSURLSession。现在看一下怎么使用NSURLSession:</p>    <pre>  <code class="language-objectivec">// 替代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];</code></pre>    <p>我们可以在NSURLSession的代理方法中获得下载的数据,拿到下载的数据以后,我们使用NSOutputStream,将数据写入到硬盘中存放临时文件的文件夹。在请求结束的时候,我们判断是否成功下载好文件,如果下载成功,就把这个文件转移到我们的存储成功文件的文件夹。如果下载失败,就把临时数据删除。</p>    <pre>  <code class="language-objectivec">// 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;</code></pre>    <p><strong>02、AVAssetResourceLoader的代理?</strong></p>    <p>为了更好的封装性和可维护性,新建一个文件,让这个文件负责和系统播放器对接数据。上面说到,只要这个文件遵守了AVAssetResourceLoaderDelegate协议,他就有资格代理系统播放器请求数据。并且系统会通过</p>    <pre>  <code class="language-objectivec">-(BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest;</code></pre>    <p>这个代理方法,把下载请求loadingRequest传给我们。拿到请求以后,首先把请求用一个数组保存起来。为什么要用数组保存起来?因为,当我们拿到请求去下载数据,到数据下载好,这个过程需要的时间是不确定的。</p>    <p>拿到请求以后,我们就需要调用上面封装的NSURLSession下载器来下载文件。</p>    <pre>  <code class="language-objectivec">- (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];      }  }</code></pre>    <p>如果文件有下载好,就去检查下载好的数据长度有没有满足请求数据需要的长度,如果满足,就从硬盘的临时文件中取出对应的数据,并把这段数据填充给请求,然后把这个请求从请求列表数组中移除。播放器拿到了这段数据,就可以开始解码播放了。</p>    <pre>  <code class="language-objectivec">// 判断此次请求的数据是否处理完全, 和填充数据  - (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;    }</code></pre>    <p>至此,手动干预播放视频的流程就走完了。已经可以正常播放视频了。</p>    <p><img src="https://simg.open-open.com/show/e851b37eb6a13395f9610f4615723d8f.png"></p>    <p style="text-align:center">JPVideoPlayer.png</p>    <p><strong>03、加载缓存数据逻辑?</strong></p>    <p>接下来要做的就是实现,当下次播放同一个视频的时候,先去检查硬盘里有没有这个文件的缓存。借助于NSFileManager,我们可以查找指定的路径有没有存在指定的文件,从而判断有没有缓存可以启用。</p>    <pre>  <code class="language-objectivec">NSFileManager *manager = [NSFileManager defaultManager];  NSString *savePath = [self fileSavePath];  savePath = [savePath stringByAppendingPathComponent:self.suggestFileName];  if ([manager fileExistsAtPath:savePath]) {       // 已经存在这个下载好的文件了      return;  }</code></pre>    <p>至此,播放器封装完毕。</p>    <p> </p>    <p> </p>    <p> </p>    <p>来自:http://www.jianshu.com/p/0d4588a7540f</p>    <p> </p>