[iOS]仿微博視頻邊下邊播之封裝播放器

dbcn7230 8年前發布 | 31K 次閱讀 iOS開發 移動開發

微博視頻的特點:

  • 秒拍團隊主要致力于視頻處理,微博的視頻播放功能是由秒拍提供技術支持的。微博的視頻一般都是不限時長的,所以它的特點是邊下邊播。

  • 說到視頻播放就不能不提微信的短視頻,微信的短視頻限制時長為15秒,經過微信團隊處理后,一個短視頻的體積能控制在2MB以內。所以微信的視頻是先下載,再讀取下載好的視頻文件進行播放,也就是所謂的先下后播。這個功能,微信的同行已經把源碼分享出來了,在這里。

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

JPVideoPlayer.gif

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

  • 01.必須是邊下邊播。

  • 02.如果緩存好的視頻是完整的,就要把這個視頻保存起來,下次再次加載這個視頻的時候,就先檢查本地有沒有緩存好的視頻。這一點對于節省用戶流量,提升用戶體驗很重要。要實現這一點,也就是說,我們要手動干預系統播放器加載數據的內部實現,這個細節后面再講。

  • 03.不阻塞線程,不卡頓,滑動如絲順滑,這是保證用戶體驗最重要的一點。

  • 04.當tableView滾動時,以什么樣的策略,來確定究竟哪一個cell應該播放視頻。

來看看我是怎么實現這些功能的。

第一、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;
}

至此,播放器封裝完畢。

 

 

 

來自:http://www.jianshu.com/p/0d4588a7540f

 

 本文由用戶 dbcn7230 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!