來源:NewPan(@盼盼_HKbuy) git
連接:http://www.jianshu.com/p/0d4588a7540fgithub
Tips:此次的內容分爲兩篇文章講述數組
0一、[iOS]仿微博視頻邊下邊播之封裝播放器 講述如何封裝一個實現了邊下邊播而且緩存的視頻播放器。緩存
0二、[iOS]仿微博視頻邊下邊播之滑動TableView自動播放 講述如何實如今tableView中滑動播放視頻,而且是流暢,不阻塞線程,沒有任何卡頓的實現滑動播放視頻。同時也將講述當tableView滾動時,以什麼樣的策略,來肯定究竟哪個cell應該播放視頻。服務器
微博視頻的特色:微信
秒拍團隊主要致力於視頻處理,微博的視頻播放功能是由秒拍提供技術支持的。微博的視頻通常都是不限時長的,因此它的特色是邊下邊播。網絡
說到視頻播放就不能不提微信的短視頻,微信的短視頻限制時長爲15秒,通過微信團隊處理後,一個短視頻的體積能控制在2MB之內。因此微信的視頻是先下載,再讀取下載好的視頻文件進行播放,也就是所謂的先下後播。這個功能,微信的同行已經把源碼分享出來了,(http://www.jianshu.com/p/3d5ccbde0de1)。session
我找了不少資料,沒有找到徹底意義上,實現了微博首頁列表視頻邊下邊播功能的資料。可是我本身項目中又有這個需求,因此只能本身動手。最後實現的效果以下:框架
這個列表視頻邊下邊播包含如下主要的功能點:ide
01.必須是邊下邊播。
02.若是緩存好的視頻是完整的,就要把這個視頻保存起來,下次再次加載這個視頻的時候,就先檢查本地有沒有緩存好的視頻。這一點對於節省用戶流量,提高用戶體驗很重要。要實現這一點,也就是說,咱們要手動干預系統播放器加載數據的內部實現,這個細節後面再講。
03.不阻塞線程,不卡頓,滑動如絲順滑,這是保證用戶體驗最重要的一點。
04.當tableView滾動時,以什麼樣的策略,來肯定究竟哪個cell應該播放視頻。
可能你着急趕項目,只想儘快的把這個功能集成到你的項目,那麼請你直接去(https://github.com/Chris-Pan/JPVideoPlayer)上下載源碼。須要說明的是,我上面說的功能點的第一和第二點,不用你關心,我已經幫你處理封裝好了。可是,第三和第四點,須要你本身結合你本身的項目來定製,我只提供了模板和鉅細無比的註釋。
接下來就來看看我是怎麼實現這些功能的。
第1、AVPlayer基本使用?
首先從最基本的封裝播放器開始。
0一、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 *)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
0二、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];
}
第2、手動干預系統播放器加載數據?
0一、如何使用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;
0二、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 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 = endOffset;
return didRespondFully;
}
至此,手動干預播放視頻的流程就走完了。已經能夠正常播放視頻了。
JPVideoPlayer.png
0三、加載緩存數據邏輯?
接下來要作的就是實現,當下次播放同一個視頻的時候,先去檢查硬盤裏有沒有這個文件的緩存。藉助於NSFileManager,咱們能夠查找指定的路徑有沒有存在指定的文件,從而判斷有沒有緩存能夠啓用。
NSFileManager *manager = [NSFileManager defaultManager];
NSString *savePath = [self fileSavePath];
savePath = [savePath stringByAppendingPathComponent:self.suggestFileName];
if ([manager fileExistsAtPath:savePath]) {
// 已經存在這個下載好的文件了
return;
}
至此,播放器封裝完畢。
我將在下一篇文章 [iOS]仿微博視頻邊下邊播之滑動TableView自動播放 ,講述如何實如今tableView中滑動播放視頻,而且是流暢,不阻塞線程,沒有任何卡頓的實現滑動播放視頻。同時也將講述當tableView滾動時,以什麼樣的策略,來肯定究竟哪個cell應該播放視頻。
0三、更新
2016.10.09 :
處理在切換視頻的短暫時間內, 當前播放視頻的cell吸取了滑動事件, 若是滑動當前播放視頻的cell, 會致使tableView沒法接收到滑動事件, 形成tableView假死。 感謝提供bug的朋友@大牆66370 具體見個人Github JPVideoPlayer(https://github.com/Chris-Pan/JPVideoPlayer)