01. 引言
大家好,我是 NewPan,好久沒冒泡了,去年下半年不加班的時間裡,我一直在研究如何實現基於 AVPlayer
實現視訊支援拖拽進度的邊下載邊播放。這個過程緩慢又辛酸,中途數次看不到希望,差點放棄,但是最後還是堅持了下來,於是就有了現在全新的 3.0 版本。這次會分兩篇文章講解,第一篇是 3.0 的使用介紹,是寫給那些只需知道如何使用的同學,接下來按照慣例,我會介紹原始碼的實現。
首先,我們來看一下全新 3.0 版本的新特性。對了,GitHub 地址在這裡。
02. 新版本新特性
這些特性基本涵蓋了做視訊播放的各方面,其中最重要的,也是這個框架價值所在,就是基於 AVPlayer
實現了邊下邊播,同時支援斷點續傳。
- 邊下邊播支援
- 拖拽進度支援(new)
- 斷點續傳支援(new)
- 假橫屏 auto-layout 佈局支援(new)
- 繼承協議自定義播放控制介面支援(new)
- 同一 URL 不重複下載支援
- 保證不阻塞主執行緒
- 本地視訊播放播放支援
- Swift 支援
03. 新版本使用介紹
由於這個框架最開始的時候就是為列表播放視訊設計的,3.0 版本中這一點也得到了延續。框架對外提供了 3 類 UIView
的分類方法,保證不侵入你的專案。
03.1 靜音播放
這個情況適合在列表中跟隨使用者的滑動,對應的播放某個 cell 上的視訊,就像微博列表頁視訊播放一樣。這種情況沒有任何對視訊的控制介面,只有一個緩衝進度條和播放進度條,就像下面這樣:
要實現這個功能,只需要呼叫下面這個方法就可以了:
NSURL *url = [NSURL URLWithString:@"http://p11s9kqxf.bkt.clouddn.com/bianche.mp4"];
[aview jp_playVideoMuteWithURL:url
bufferingIndicator:nil
progressView:nil
configurationCompletion:nil];
複製程式碼
這個方法有四個引數,第一個不用說了,第二個是視訊緩衝指示器,第三個是緩衝和播放進度條,第四個是配置完視訊以後的一些操作回撥。
但是在這個介面,除了第一個必選引數外,其他三個你都可以傳空,因為框架為你實現了預設的檢視,同時你也可以繼承我提供的模板類進行快速的自定義。關於這點,我在下面會提到。
配套的,還有下面這個方法。
NSURL *url = [NSURL URLWithString:@"http://p11s9kqxf.bkt.clouddn.com/bianche.mp4"];
[aview jp_resumeMutePlayWithURL:url
bufferingIndicator:nil
progressView:nil
configurationCompletion:nil];
複製程式碼
這個方法是什麼意思呢?我們在視訊列表頁播放,當使用者選中了某一個 cell 的時候會跳轉到對應的視訊詳情頁,這個時候就輪到這個方法上場了。因為如果你直接使用上面那個方法來播放的話,視訊會重頭播,這樣破壞了使用者體驗,而你呼叫這個方法,就可以連貫的開始播放。
同時這個方法中,你仍然可以定製自己的介面,而不是必須和上個介面的控制介面一樣,小棉襖貼心吧?
03.2 帶控制介面的播放
這個功能在視訊詳情頁是必須的。這個時候除了視訊影像一般還配套的有緩衝動畫、播放進度以及控制視訊介面。就像下面這樣。
這個功能的介面是:
NSURL *url = [NSURL URLWithString:@"http://p11s9kqxf.bkt.clouddn.com/bianche.mp4"];
[aview jp_playVideoWithURL:url
bufferingIndicator:nil
controlView:nil
progressView:nil
configurationCompletion:nil];
複製程式碼
和上一個型別的方法沒有太多不同,就是多了一個引數,多了一個 controlView
這個是和使用者互動的那個介面。
配套的,還有一個恢復播放的方法,比方上面說的從視訊列表進入到視訊詳情,在視訊列表使用的是靜音帶快取和播放進度的方法進行播放,當使用者點選某個視訊的時候,進入到視訊詳情頁就是開始恢復播放,這個介面帶有使用者控制 controlView
介面,而且還有橫屏按鈕。就像下面這樣。
這個 API 是:
NSURL *url = [NSURL URLWithString:@"http://p11s9kqxf.bkt.clouddn.com/bianche.mp4"];
[aview jp_resumePlayWithURL:url
bufferingIndicator:nil
controlView:nil
progressView:nil
configurationCompletion:nil];
複製程式碼
03.3 只有視訊
這種也是比較常見的,比方說懸停播放,在視訊詳情頁,除了視訊,還有評論什麼的,這時使用者滑動列表頁,有些就會使用懸停播放,此時視訊不需要任何進度或者控制介面。
這個功能的介面是:
NSURL *url = [NSURL URLWithString:@"http://p11s9kqxf.bkt.clouddn.com/bianche.mp4"];
[aview jp_playVideoWithURL:url
options:kNilOptions
configurationCompletion:nil];
複製程式碼
配套的恢復播放也有一個介面:
NSURL *url = [NSURL URLWithString:@"http://p11s9kqxf.bkt.clouddn.com/bianche.mp4"];
[aview jp_resumePlayWithURL:url
options:kNilOptions
configurationCompletion:nil];
複製程式碼
有了這些以後,我們就可以實現下面的懸停播放功能。
04. 基於 JPVideoPlayer
快速搭建流行視訊 APP
下面我用 demo 來示範如何基於 JPVideoPlayer
快速搭建抖音、微博等流行 APP 的視訊播放介面。
04.1 抖音
博主也中了抖音的毒,且毒入骨髓,已無藥可救,“c哩c哩”,“海草舞”來一發。下面的 demo 的結構是這樣的,一個 scrollView
上面新增三個 imageView
,開始的時候設定 scrollView 滾到中間那個 imageView,以後每次使用者滑動完螢幕,將 scrollView 復位到這個狀態。
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
self.scrollViewOffsetYOnStartDrag = -100;
[self scrollViewDidEndScrolling];
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
[self.secondImageView jp_stopPlay];
}
#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView
willDecelerate:(BOOL)decelerate {
if (decelerate == NO) {
[self scrollViewDidEndScrolling];
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
[self scrollViewDidEndScrolling];
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
self.scrollViewOffsetYOnStartDrag = scrollView.contentOffset.y;
}
#pragma mark - JPVideoPlayerDelegate
- (BOOL)shouldShowBlackBackgroundBeforePlaybackStart {
return YES;
}
#pragma mark - Private
- (void)scrollViewDidEndScrolling {
if(self.scrollViewOffsetYOnStartDrag == self.scrollView.contentOffset.y){
return;
}
CGSize referenceSize = UIScreen.mainScreen.bounds.size;
[self.scrollView setContentOffset:CGPointMake(0, referenceSize.height) animated:NO];
[self.secondImageView jp_stopPlay];
[self.secondImageView jp_playVideoMuteWithURL:[self fetchDouyinURL]
bufferingIndicator:nil
progressView:[JPDouyinProgressView new]
configurationCompletion:^(UIView *view, JPVideoPlayerModel *playerModel) {
view.jp_muted = NO;
}];
}
- (NSURL *)fetchDouyinURL {
if(self.currentVideoIndex == (self.douyinVideoStrings.count - 1)){
self.currentVideoIndex = 0;
}
NSURL *url = [NSURL URLWithString:self.douyinVideoStrings[self.currentVideoIndex]];
self.currentVideoIndex++;
return url;
}
複製程式碼
初始化的程式碼我沒拷過來,這些程式碼裡還有百分之七十是使用者滾動的判斷操作,其實播放視訊就只有一行程式碼。
[self.secondImageView jp_playVideoMuteWithURL:[self fetchDouyinURL]
bufferingIndicator:nil
progressView:[JPDouyinProgressView new]
configurationCompletion:^(UIView *view, JPVideoPlayerModel *playerModel) {
view.jp_muted = NO;
}];
複製程式碼
這裡使用了靜音播放,為什麼呢?因為這個介面預設不顯示視訊控制介面。注意,這裡在 configurationCompletion
裡設定了視訊不要靜音播放,為什麼呢?因為播放視訊的初始化並非是同步操作,內部還需要在子執行緒查視訊資料等一系列操作以後才會切回主執行緒,所以要等播放視訊初始化以後再去操作播放器,這樣才有效。
這裡還有一個自定義的 progressView
,這個是啥呢,因為預設 JPVideoPlayerProgressView
的快取和播放進度條是載入 view 的最下方,而抖音是顯示在 tabBar
上方,所以我們要繼承 JPVideoPlayerProgressView
重新佈局。
@interface JPDouyinProgressView: JPVideoPlayerProgressView
@end
@implementation JPDouyinProgressView
- (void)layoutThatFits:(CGRect)constrainedRect
nearestViewControllerInViewTree:(UIViewController *_Nullable)nearestViewController
interfaceOrientation:(JPVideoPlayViewInterfaceOrientation)interfaceOrientation {
[super layoutThatFits:constrainedRect
nearestViewControllerInViewTree:nearestViewController
interfaceOrientation:interfaceOrientation];
self.trackProgressView.frame = CGRectMake(0,
constrainedRect.size.height - JPVideoPlayerProgressViewElementHeight - nearestViewController.tabBarController.tabBar.bounds.size.height,
constrainedRect.size.width,
JPVideoPlayerProgressViewElementHeight);
self.cachedProgressView.frame = self.trackProgressView.bounds;
self.elapsedProgressView.frame = self.trackProgressView.frame;
}
@end
複製程式碼
注意,如果使用 frame 佈局,那麼佈局程式碼一定要寫在框架提供的佈局方法裡,因為如果使用橫屏的時候,view 要重新佈局,只有寫在這個方法裡,佈局程式碼才會被執行到。
注意,這裡有三個引數。第一個是佈局的約束大小,一般是父控制元件的 bounds
。第二個引數是當前這個 view 所在的控制器,可能為空。第三個引數是當前 view 的螢幕方向,可能會是橫屏,也有可能是豎屏,你可能拿到這個狀態值進行對應的佈局。
- (void)layoutThatFits:(CGRect)constrainedRect
nearestViewControllerInViewTree:(UIViewController *_Nullable)nearestViewController
interfaceOrientation:(JPVideoPlayViewInterfaceOrientation)interfaceOrientation;
複製程式碼
如果使用 autoLayout
佈局則沒有要求一定要將佈局寫在這個方法裡。
04.2 微博
上個版本不支援不等高 cell 的滑動播放,其實大多數場景都是不等高 cell。也不支援恢復播放,進度詳情介面以後就需要重頭開始播,使用者體驗不是很好。
這個版本不僅解決了這兩個大問題,還同時帶來了拖拽進度和兩種滑動判斷策略。一起來看下。
要實現上面的功能,大致需要這些程式碼。
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
CGRect tableViewFrame = self.tableView.frame;
tableViewFrame.size.height -= self.tabBarController.tabBar.bounds.size.height;
self.tableView.jp_tableViewVisibleFrame = tableViewFrame;
}
- (void)viewDidAppear:(BOOL)animated{
[super viewDidAppear:animated];
[self.tableView jp_handleCellUnreachableTypeInVisibleCellsAfterReloadData];
[self.tableView jp_playVideoInVisibleCellsIfNeed];
// 用來防止選中 cell push 到下個控制器時, tableView 再次呼叫 scrollViewDidScroll 方法, 造成 playingVideoCell 被置空.
self.tableView.delegate = self;
}
- (void)viewWillDisappear:(BOOL)animated{
[super viewWillDisappear:animated];
// 用來防止選中 cell push 到下個控制器時, tableView 再次呼叫 scrollViewDidScroll 方法, 造成 playingVideoCell 被置空.
self.tableView.delegate = nil;
}
#pragma mark - Data Srouce
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
JPVideoPlayerWeiBoEqualHeightCell *cell = ...;
cell.jp_videoURL = [NSURL URLWithString:self.pathStrings[indexPath.row]];
cell.jp_videoPlayView = cell.videoPlayView;
[tableView jp_handleCellUnreachableTypeForCell:cell
atIndexPath:indexPath];
return cell;
}
#pragma mark - TableView Delegate
/**
* Called on finger up if the user dragged. decelerate is true if it will continue moving afterwards
* 鬆手時已經靜止, 只會呼叫scrollViewDidEndDragging
*/
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
[self.tableView jp_scrollViewDidEndDraggingWillDecelerate:decelerate];
}
/**
* Called on tableView is static after finger up if the user dragged and tableView is scrolling.
* 鬆手時還在運動, 先呼叫scrollViewDidEndDragging, 再呼叫scrollViewDidEndDecelerating
*/
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
[self.tableView jp_scrollViewDidEndDecelerating];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
[self.tableView jp_scrollViewDidScroll];
}
#pragma mark - JPTableViewPlayVideoDelegate
- (void)tableView:(UITableView *)tableView willPlayVideoOnCell:(UITableViewCell *)cell {
[cell.jp_videoPlayView jp_resumeMutePlayWithURL:cell.jp_videoURL
bufferingIndicator:nil
progressView:nil
configurationCompletion:nil];
}
複製程式碼
框架給 UITableView
新增了分類方法,使用者處理滑動列表滑動播放視訊,但凡是這個分類中標註了必須呼叫的方法,就需要在正確的位置正確的呼叫,否則滑動播放的邏輯就不能正常工作。
這個是告訴框架,當前這個 tableView 可見區域的屬性,這個屬性是決定當使用者滑動停止的時候這個 tableView 的中心在哪裡,必須要正確的賦值。
CGRect tableViewFrame = self.tableView.frame;
tableViewFrame.size.height -= self.tabBarController.tabBar.bounds.size.height;
self.tableView.jp_tableViewVisibleFrame = tableViewFrame;
複製程式碼
每次對 tableView 進行 reloadData
操作以後,都需要呼叫這個方法。這個方法是對 tableView 的 cell 進行是否是滑動不可及的判斷的,如果 [self.tableView jp_playVideoInVisibleCellsIfNeed];
這樣程式碼沒有生效,那肯定是你忘記呼叫下面這個方法了。
[self.tableView jp_handleCellUnreachableTypeInVisibleCellsAfterReloadData];
複製程式碼
下面這些屬性也必須賦值。
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
JPVideoPlayerWeiBoEqualHeightCell *cell = ...;
cell.jp_videoURL = [NSURL URLWithString:self.pathStrings[indexPath.row]];
cell.jp_videoPlayView = cell.videoPlayView;
[tableView jp_handleCellUnreachableTypeForCell:cell
atIndexPath:indexPath];
return cell;
}
複製程式碼
然後就是在 scrollView 的代理方法中告訴框架對應的代理行為,當確定需要播放視訊的時候,框架會通過 - (void)tableView:(UITableView *)tableView willPlayVideoOnCell:(UITableViewCell *)cell;
這個代理方法告訴外界,你可以在這個方法裡選擇想要的方式進行視訊播放。
05. 自定義進度和控制 view
定製 view 非常簡單。你只需要繼承對應的模板類進行一系列介面的自定義就可以快速實現。下面是這些模板類的類名。
緩衝動畫指示器:JPVideoPlayerBufferingIndicator
播放和緩衝進度指示器:JPVideoPlayerProgressView
控制介面:JPVideoPlayerControlView
當然,如果你不想使用這些模板類,想要自己從頭搭建,也是很方便的,而且能完全和播放邏輯解耦。你只需要實現對應的協議即可。
緩衝動畫指示器:<JPVideoPlayerBufferingProtocol>
播放和緩衝進度指示器:<JPVideoPlayerProtocol>
控制介面:<JPVideoPlayerProtocol>
需要注意的是,對視訊的橫屏並沒有真正的將視窗橫過來,這是對國內 APP 現狀的平衡,國內大多數 APP 都只支援豎屏,優酷 APP、騰訊視訊 APP、嗶哩嗶哩 APP 等都是採用這種方式進行橫屏。如果你關心這內部的實現,請你去看一下原始碼,這篇文章不進行講解。
06. 快取管理
非常感謝有些同學是從 2.0 版本一路支援過來的,由於 3.0 對快取的管理完全重構,快取路徑改了,之前的快取用不了了。所以我提供了 -clearVideoCacheOnVersion2OnCompletion:
方法來清理掉舊的快取。
快取內部實現改了,但是對外查詢管理的介面沒有改變,具體請檢視介面文件。對了,GitHub 地址在這裡。
好,這篇文章就到這裡,我們下篇文章見, see you!
我的文章集合
下面這個連結是我所有文章的一個集合目錄。這些文章凡是涉及實現的,每篇文章中都有 Github 地址,Github 上都有原始碼。