1. 程式人生 > >仿微博視訊邊下邊播之滑動 TableView 自動播放

仿微博視訊邊下邊播之滑動 TableView 自動播放

112122663-37d536741b4ec304

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

上篇文章講述了封裝一個邊下邊播,並且帶有快取功能的播放器。如果你還沒有看,請點選跳轉[iOS]仿微博視訊邊下邊播之封裝播放器 。接下來,講述如何將這個播放器應用到tableView裡。並且達到如下效果。

122122663-98e851e16014a23d

01、dispatch_semaphore訊號量?

dispatch_semaphore 訊號量基於計數器的一種多執行緒同步機制。在多個執行緒訪問共有資源時候,會因為多執行緒的特性而引發資料出錯的問題。

Objective-C
123456789101112131415161718192021 dispatch_queue_t queue=dispatch_get_global_queue(0,0);// “建立方法裡會傳入一個long型的引數,這個東西你可以想象是一個庫存”dispatch_semaphore_t semaphore=dispatch_semaphore_create(1);NSMutableArray*array=[NSMutableArrayarray];for(intindex=0;index<10000;index++)
{dispatch_async(queue,^(){// “每執行一次,會先清一個庫存,如果庫存為0,那麼根據傳入的等待時間,決定等待增加庫存的時間//如果設定為DISPATCH_TIME_FOREVER,那麼意思就是永久等待增加庫存,否則就永遠不往下面走”dispatch_semaphore_wait(semaphore,DISPATCH_TIME_FOREVER);NSLog(@"addd :%d",index);[array addObject:[NSNumber numberWithInt:index]];// 每執行一次,增加一個庫存dispatch_semaphore_signal(semaphore);});}

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
如果semaphore計數大於等於1.計數-1,返回,程式繼續執行。
如果計數為0,則等待。
這裡設定的等待時間是一直等待。

dispatch_semaphore_signal(semaphore);
計數+1.
在這兩句程式碼中間的執行程式碼,每次只會允許一個執行緒進入,這樣就有效的保證了在多執行緒環境下,只能有一個執行緒進入。

  • AVPlayer底層是有訊號量等待的特性的。具體表現在,“AVPlayer的replaceCurrentItemWithPlayerItem(用來切換視訊的)方法在切換視訊時底層會呼叫訊號量等待,然後導致當前執行緒卡頓,如果在UITableViewCell中切換視訊播放使用這個方法,會導致當前執行緒凍結幾秒鐘。” 這裡說的執行緒是UI執行緒,即主執行緒,主執行緒凍結的結果就是主執行緒阻塞,帶來卡頓和不流暢。
  • 你可能會想,那就不要在主執行緒切換視訊,不就不卡頓主執行緒了嗎?如果你這麼做,那你就不能保證視訊播放是及時響應的。同時,因為子執行緒你不知道什麼時候呼叫,你也不能保證你能及時關閉不需要播放的視訊。也就是說,如果基於以上思路,當你滑動tableView時,可能會出現多個cell同時播放視訊,而且會出現你要播的播不了,你想掐死的掐不死。

02、切換視訊解決方案?

在tableView裡播放視訊,當用戶滑動時,肯定是頻繁切換視訊的。上面講了使用AVPlayer自帶的replaceCurrentItemWithPlayerItem來切換視訊帶來的問題,我們得出的結論是:

  • 不能使用replaceCurrentItemWithPlayerItem方法切換視訊。
  • 不能在子執行緒切換視訊。

解決方案一

當出現這種問題的時候,我只能跑到官方文件去找答案了。

1 @interfaceAVQueuePlayer:AVPlayer

我在官方文件裡找到AVQueuePlayer,他是AVPlayer的一個子類,他會自己維護一個播放佇列。並且提供方法,可以插入播放條目,也可以移除播放條目,然後切換視訊。

123456789101112 // 建立AVQueuePlayerNSArray *items=;AVQueuePlayer *queuePlayer=[[AVQueuePlayer alloc]initWithItems:items];// 插入item-(void)insertItem:(AVPlayerItem *)item afterItem:(nullable AVPlayerItem *)afterItem;// 移除item-(void)removeItem:(AVPlayerItem *)item;// 切換視訊-(void)advanceToNextItem;

這個類還是很好使的,可以在不卡頓主執行緒的情況下流暢切換視訊。但是他有他的坑,是我多次試驗以後發現的,就是重播視訊的時候,如果你放在那裡不動,他大概會重複播放十次左右,然後播放器就莫名其妙的死掉了,這個時候你沒有辦法重新喚醒他。至於什麼原因導致的,我暫時還沒有找到。如果你知道,請你務必在下面留言,讓更多人看到。

解決方案二

上面的那個方案被我否了,接下來,我採取的是嘗試每次切換視訊的時候都重新建立播放器,重新建立網路請求。總之,就是所有的配置都重新建立。剛開始的時候,我擔心這樣會造成處理器負擔,但是實際使用起來,發現並沒有任何效能問題。但是前提是,在重新建立之前,把所有的請求釋放掉,同時把之前的播放器也釋放,還有預覽圖層也釋放。

03、重播?

先來看一下AVFoundation下用來表示時間的結構體CMTime,AVFoundation下的時間刻度是以最精準的分數形式來表示的。他有兩個重要的值,value表示分子,timescale表示分母。

1234567 typedefstruct{CMTimeValue value;// 分子CMTimeScale timescale;// 分母CMTimeFlags flags;CMTimeEpoch epoch;}CMTime;

比如說我們要表示視訊的起點,就是0秒,那就可以寫成CMTimeMake(0, 1)。

我們的播放器是支援自動重播的,所以我們要在系統的通知中心監聽播放器播放完成的通知,在接收到通知後,進行對應的處理。

123456789101112 // 監聽播放結束通知[[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(playerItemDidPlayToEnd:)name:AVPlayerItemDidPlayToEndTimeNotification object:nil];-(void)playerItemDidPlayToEnd:(NSNotification *)notification{// 重複播放, 從起點開始重播, 沒有記憶體暴漲__weak typeof(self)weak_self=self;[self.player seekToTime:CMTimeMake(0,1)completionHandler:^(BOOLfinished){__strong typeof(weak_self)strong_self=weak_self;if(!strong_self)return;[strong_self.player play];}];}

04、滑動tableView自動播放?

首先是一啟動,應該自動去可見cell中查詢第一個需要播放視訊的cell,如果找到就開始播放。

12345678910111213141516171819202122 -(void)playVideoInVisiableCells{NSArray *visiableCells=[self.tableView visibleCells];// 在可見cell中找到第一個有視訊的cellJPVideoPlayerCell *videoCell=nil;for(JPVideoPlayerCell *cell invisiableCells){if(cell.videoPath.length>0){videoCell=cell;break;}}// 如果找到了, 就開始播放視訊if(videoCell){self.playingCell=videoCell;self.currentVideoPath=videoCell.videoPath;JPVideoPlayer *player=[JPVideoPlayer sharedInstance];[player playWithUrl:[NSURL URLWithString:videoCell.videoPath]showView:videoCell.containerView];player.mute=YES;}}

接下來就是滾動tableView的時候,播放視訊。在做之前,首先,我們要先制定一個規則來確定,滾動的時候究竟哪一個cell應該播放視訊。我畫了一張圖,一起來看一下。

132122663-850677694e9abc02 播放規則.png

我的規則是:當tableView滑動的時候,我會播放可見cell中,最靠近螢幕中心的那個cell的視訊。如果最靠近螢幕中心的那個cell沒有視訊,就會按照這個規則去其他可見cell中找,如果都沒有找到,就不播放視訊。規則有了,接下來就是去實現。其實,實現的時候,我們應該換一個思路,就是,只有那個cell需要播放視訊,才會參與篩選是否是最靠近螢幕中心的cell。這樣就避免了遞迴查詢。

Objective-C
1234567891011121314151617181920212223242526272829303132333435363738394041 -(void)handleScroll{// 找到下一個要播放的cell(最在螢幕中心的)JPVideoPlayerCell*finnalCell=nil;NSArray*visiableCells=[self.tableView visibleCells];NSMutableArray*indexPaths=[NSMutableArrayarray];CGFloatgap=MAXFLOAT;for(JPVideoPlayerCell*cellinvisiableCells){[indexPaths addObject:cell.indexPath];if(cell.videoPath.length>0){// 如果這個cell有視訊CGPointcoorCentre=[cell.superview convertPoint:cell.center toView:nil];CGFloatdelta=fabs(coorCentre.y-[UIScreenmainScreen].bounds.size.height*0.5);if(delta<gap){gap=delta;finnalCell=cell;}}}// 注意, 如果正在播放的cell和finnalCell是同一個cell, 不應該在播放if(finnalCell!=nil&&self.playingCell!=finnalCell){[[JPVideoPlayersharedInstance]stop];[[JPVideoPlayersharedInstance]playWithUrl:[NSURL URLWithString:finnalCell.videoPath] showView:finnalCell.containerView];self.playingCell=finnalCell;self.currentVideoPath=finnalCell.videoPath;[JPVideoPlayersharedInstance].mute=YES;return;}// 再看正在播放視訊的那個cell移出視野, 則停止播放BOOLisPlayingCellVisiable=YES;if(![indexPaths containsObject:self.playingCell.indexPath]){isPlayingCellVisiable=NO;}if(!isPlayingCellVisiable&&self.playingCell){[selfstopPlay];}}

這裡沒有難點,唯一可以講一下的就是座標之間的轉換。我們拿到的cell的中心點的座標是tableView的座標,但是我們計算各個中心點離螢幕中心點之間的距離,用的是螢幕Window的座標,所以要先將這個中心點的座標轉換為螢幕的座標,再進行計算。但是,你也可以不轉換座標,因為他們是同一個座標系的(tableView的座標系)。可是,我個人的程式設計習慣是先轉換,再計算。因為我覺得會比較清晰一點。尤其是當我們做複雜的過渡動畫的時候,有這個意識,你會發現條理會很清晰。

05、什麼時候播?

你肯定告訴我,滑動的時候播。這個回答是正確的,但是也是不正確的,因為我們嘗試用程式設計的思想來思考這個問題。tableView的滾動過程分為兩種情況:

  • 將要開始拖動 –> 開始拖動 –> 滾動 –> 鬆手 –> 靜止 –> 結束
  • 將要開始拖動 –> 開始拖動 –> 滾動 –> 鬆手 –> 開始減速 –> 減速完成 –> 靜止 –> 結束

首先要肯定的是,不能在滾動的時候呼叫視訊播放的邏輯。這一點應該沒有異議。原因是,第一,這個方法會來很多很多次,而且從實現上來說,不可能一呼叫滾動的代理就實現播放。第二從使用者角度來說,在滑動的時候,他自己也沒有決定要看哪一個cell。所以在滾動時,我們不做反應。

其次是開始拖動時,也不要作反應,因為,這個時候作反應沒有意義。鬆手的時候,因為有鬆手時靜止和鬆手時滾動兩種情況,所以我不做處理。

逐漸排除下來,最後,適合呼叫播放邏輯的,只剩下“靜止”這個動作了。我們來看一下靜止對應的代理方法:

123456789101112 // 鬆手時已經靜止,只會呼叫scrollViewDidEndDragging-(void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{if(decelerate==NO){// scrollView已經完全靜止[selfhandleScroll];}}// 鬆手時還在運動, 先呼叫scrollViewDidEndDragging,在呼叫scrollViewDidEndDecelerating-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{// scrollView已經完全靜止