iOS開發之仿微博視頻邊下邊播之自定義AVPlayer播放器, 邊下邊播解剖。視頻處理流程,建立連接-請求數據-統籌數據-解碼數據-視頻呈現

Tips:這次的內容分爲兩篇文章講述
01、[iOS]仿微博視頻邊下邊播之封裝播放器 講述如何封裝一個實現了邊下邊播並且緩存的視頻播放器。
02、[iOS]仿微博視頻邊下邊播之滑動TableView自動播放 講述如何實現在tableView中滑動播放視頻,並且是流暢,不阻塞線程,沒有任何卡頓的實現滑動播放視頻。同時也將講述當tableView滾動時,以什麼樣的策略,來確定究竟哪一個cell應該播放視頻。


微博視頻的特點:

  • 秒拍團隊主要致力於視頻處理,微博的視頻播放功能是由秒拍提供技術支持的。微博的視頻一般都是不限時長的,所以它的特點是邊下邊播。
  • 說到視頻播放就不能不提微信的短視頻,微信的短視頻限制時長爲15秒,經過微信團隊處理後,一個短視頻的體積能控制在2MB以內。所以微信的視頻是先下載,再讀取下載好的視頻文件進行播放,也就是所謂的先下後播。這個功能,微信的同行已經把源碼分享出來了,在這裏

我找了很多資料,沒有找到完全意義上,實現了微博首頁列表視頻邊下邊播功能的資料。但是我自己項目中又有這個需求,所以只能自己動手。最後實現的效果如下:


這個列表視頻邊下邊播包含以下主要的功能點:

  • 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框架下,我上面列舉的那些類自動幫我們完成的。


系統處理.png

要實現邊下邊播,並實現緩存功能,就必須拿到播放器的數據,也就是必須手動干預數據加載的過程。我們需要在網絡層和解碼層中間,插入一個我們自己需要的功能塊,也就是我下圖中的紅色模塊。


手動干預.png

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;
  • 在正式進入數據干預之前,我們先看一個很重要的東西。我們知道視頻數據都是容量巨大的連續媒體數據,所以請求數據的時候,我們要將請求策略置爲streaming。這個策略的含義是,將容量巨大的連續媒體數據進行分段,分割爲數量衆多的小文件進行傳遞。

    - (NSURL *)getSchemeVideoURL:(NSURL *)url{
      // NSURLComponents用來替代NSMutableURL,可以readwrite修改URL。這裏通過更改請求策略,將容量巨大的連續媒體數據進行分段
      // 分割爲數量衆多的小文件進行傳遞。採用了一個不斷更新的輕量級索引文件來控制分割後小媒體文件的下載和播放,可同時支持直播和點播
      NSURLComponents *components = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO];
      components.scheme = @"streaming";
      return [components URL];
    }

第二、手動干預系統播放器加載數據?

01、如何使用NSURLSession來下載大文件?

在NSURLSession之前,大家都是使用NSURLConnection。如今在Xcode7中,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;
  }

至此,手動干預播放視頻的流程就走完了。已經可以正常播放視頻了。


JPVideoPlayer.png

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

我的文章集合

下面這個鏈接是我所有文章的一個集合目錄。這些文章凡是涉及實現的,每篇文章中都有GIT地址,GIT上都有源碼。如果某篇文章剛好在你的實際開發中幫到你,又或者提供一種不同的實現思路,讓你覺得有用,那就看看這句話 “堅持每天點讚的人,99%都是帥哥美女,再也不用單身了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章