你們好,我是 NewPan,很久沒冒泡了,去年下半年不加班的時間裏,我一直在研究如何實現基於 AVPlayer
實現視頻支持拖拽進度的邊下載邊播放。這個過程緩慢又辛酸,中途數次看不到但願,差點放棄,可是最後仍是堅持了下來,因而就有了如今全新的 3.0 版本。此次會分兩篇文章講解,第一篇是 3.0 的使用介紹,是寫給那些只需知道如何使用的同窗,接下來按照慣例,我會介紹源碼的實現。git
首先,咱們來看一下全新 3.0 版本的新特性。對了,GitHub 地址在這裏。github
這些特性基本涵蓋了作視頻播放的各方面,其中最重要的,也是這個框架價值所在,就是基於 AVPlayer
實現了邊下邊播,同時支持斷點續傳。緩存
因爲這個框架最開始的時候就是爲列表播放視頻設計的,3.0 版本中這一點也獲得了延續。框架對外提供了 3 類 UIView
的分類方法,保證不侵入你的項目。markdown
這個狀況適合在列表中跟隨用戶的滑動,對應的播放某個 cell 上的視頻,就像微博列表頁視頻播放同樣。這種狀況沒有任何對視頻的控制界面,只有一個緩衝進度條和播放進度條,就像下面這樣:app
要實現這個功能,只須要調用下面這個方法就能夠了:框架
NSURL *url = [NSURL URLWithString:@"http://p11s9kqxf.bkt.clouddn.com/bianche.mp4"]; [aview jp_playVideoMuteWithURL:url bufferingIndicator:nil progressView:nil configurationCompletion:nil]; 複製代碼
這個方法有四個參數,第一個不用說了,第二個是視頻緩衝指示器,第三個是緩衝和播放進度條,第四個是配置完視頻之後的一些操做回調。ide
可是在這個接口,除了第一個必選參數外,其餘三個你均可以傳空,由於框架爲你實現了默認的視圖,同時你也能夠繼承我提供的模板類進行快速的自定義。關於這點,我在下面會提到。oop
配套的,還有下面這個方法。佈局
NSURL *url = [NSURL URLWithString:@"http://p11s9kqxf.bkt.clouddn.com/bianche.mp4"]; [aview jp_resumeMutePlayWithURL:url bufferingIndicator:nil progressView:nil configurationCompletion:nil]; 複製代碼
這個方法是什麼意思呢?咱們在視頻列表頁播放,當用戶選中了某一個 cell 的時候會跳轉到對應的視頻詳情頁,這個時候就輪到這個方法上場了。由於若是你直接使用上面那個方法來播放的話,視頻會重頭播,這樣破壞了用戶體驗,而你調用這個方法,就能夠連貫的開始播放。fetch
同時這個方法中,你仍然能夠定製本身的界面,而不是必須和上個界面的控制界面同樣,小棉襖貼心吧?
這個功能在視頻詳情頁是必須的。這個時候除了視頻圖像通常還配套的有緩衝動畫、播放進度以及控制視頻界面。就像下面這樣。
這個功能的接口是:
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]; 複製代碼
這種也是比較常見的,比方說懸停播放,在視頻詳情頁,除了視頻,還有評論什麼的,這時用戶滑動列表頁,有些就會使用懸停播放,此時視頻不須要任何進度或者控制界面。
這個功能的接口是:
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]; 複製代碼
有了這些之後,咱們就能夠實現下面的懸停播放功能。
JPVideoPlayer
快速搭建流行視頻 APP下面我用 demo 來示範如何基於 JPVideoPlayer
快速搭建抖音、微博等流行 APP 的視頻播放界面。
博主也中了抖音的毒,且毒入骨髓,已無藥可救,「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
佈局則沒有要求必定要將佈局寫在這個方法裏。
上個版本不支持不等高 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;
這個代理方法告訴外界,你能夠在這個方法裏選擇想要的方式進行視頻播放。
定製 view 很是簡單。你只須要繼承對應的模板類進行一系列界面的自定義就能夠快速實現。下面是這些模板類的類名。
緩衝動畫指示器:JPVideoPlayerBufferingIndicator
播放和緩衝進度指示器:JPVideoPlayerProgressView
控制界面:JPVideoPlayerControlView
固然,若是你不想使用這些模板類,想要本身從頭搭建,也是很方便的,並且能徹底和播放邏輯解耦。你只須要實現對應的協議便可。
緩衝動畫指示器:<JPVideoPlayerBufferingProtocol>
播放和緩衝進度指示器:<JPVideoPlayerProtocol>
控制界面:<JPVideoPlayerProtocol>
須要注意的是,對視頻的橫屏並無真正的將窗口橫過來,這是對國內 APP 現狀的平衡,國內大多數 APP 都只支持豎屏,優酷 APP、騰訊視頻 APP、嗶哩嗶哩 APP 等都是採用這種方式進行橫屏。若是你關心這內部的實現,請你去看一下源碼,這篇文章不進行講解。
很是感謝有些同窗是從 2.0 版本一路支持過來的,因爲 3.0 對緩存的管理徹底重構,緩存路徑改了,以前的緩存用不了了。因此我提供了 -clearVideoCacheOnVersion2OnCompletion:
方法來清理掉舊的緩存。
緩存內部實現改了,可是對外查詢管理的接口沒有改變,具體請查看接口文檔。對了,GitHub 地址在這裏。
好,這篇文章就到這裏,咱們下篇文章見, see you!
下面這個連接是我全部文章的一個集合目錄。這些文章凡是涉及實現的,每篇文章中都有 Github 地址,Github 上都有源碼。