仿抖音在線播放器設計

仿抖音在線播放器設計

一. 文檔大綱

  • 效果預覽
  • 功能說明
  • 工程說明
  • 無限滑動技術實現
  • 邊播邊下載技術實現
  • 參考資料

Demo地址:MBVideoPlayer

二. 效果預覽

工程效果預覽如下圖所示

preview.gif

三. 功能說明

工程實現了三方面的功能

  • 基于UIScrollView的無限滑動功能

在下拉過程中,到底部的時候,加載新的數據,若數據無限,則頁面可以無限滑動下去。

  • 在線視頻的邊播放邊下載功能

視頻播放過程中,會自動下載到本地沙盒中。支持斷點續傳功能。

  • 離線播放功能

如果本地存在播放的視頻數據,則優先播放本地的數據,所以在離線狀態下也可以進行視頻播放。

四. 工程說明

工程結構如下所示:

project.png

說明如下:


MBVideoPlayer
   └── Other
   │   ├── MBAVAssetResourceLoader (h/m)      #播放器數據代理類,用于攔截播放器請求并返回數據
   |   └── MBNetworkManager (h/m)             #網絡訪問類,用于向服務器請求播放數據
   ├── View 
   |   ├── MBScrollView (h/m)                 #ScrollerView類,用來顯示PlayerView
   |   └── MBPlayerView (h/m)                 #視頻播放視圖
   |   └── MBToastLabelView (h/m)             #用于顯示使用過程的提示
   ├── Model 
   |   ├── MBVideoModel (h/m)                 #videoModel類,包含了視頻下載鏈接,視頻描述等字段
   |   └── MBURLTaskModel (h/m)               #視頻下載model,包含了當前下載進度,視頻大小等信息   
   ├── Controller
       ├── ViewController (h/m)               #主VC,顯示視頻流
       |── MBSettingViewController (h/m)      #設置界面VC,用于清除緩存數據      
       

五. 無限滑動技術實現

使用UIScrollView實現無限滑動,基于具體的應用場景,有不同的實現方式。這里列舉兩種應用場景的使用方式。

1.顯示的View的數目固定的情況下(這種方式不在本工程中)

當顯示View的數目固定的時候,其實UIScollView上面只需要添加三個View就能顯示所有的View,無需為所有的View都添加一個獨立的View。實現的原理如下:

  • 初始化scrollView的時候,設置3個View,添加到scrollView中,并通過設置contentoffset的屬性,讓中間的view顯示到界面上。

  • 每次滑動的時候,在scrollViewDidScroll:方法種,判斷是否滑動的下一個View,如果滑動到下一個View的話,則繼續設置scrollView的contentOffset,讓scrollView復位,始終讓中間那個View顯示。

  • 復位ScrollView之后,設置3個View的位置,如果是UIImageView的話,其實直接修改它們的image的值就可以了。

代碼如下所示:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
 CGFloat offset = scrollView.contentOffset.y;
 if (self.lives.count) {
     if (offset >= 2*self.frame.size.height) //向下滑
     {
         // 對ScollView進行復位處理
         scrollView.contentOffset = CGPointMake(0, self.frame.size.height);
         _currentIndex++;
         self.upperImageView.image = self.middleImageView.image;
         self.middleImageView.image = self.downImageView.image;
         
         
         if (_currentIndex == self.lives.count - 1)//獲取最后一張顯示的是什么內容
         {
             _downLive = [self.lives firstObject];
         } else if (_currentIndex == self.lives.count)
         {
             _downLive = self.lives[1];
             _currentIndex = 0;
             
         } else
         {
             _downLive = self.lives[_currentIndex+1];
         }
         [self prepareForImageView:self.downImageView withLive:_downLive];
     }
     else if (offset <= 0) //向上滑
     {
         // slides to the upper player
         scrollView.contentOffset = CGPointMake(0, self.frame.size.height);
         _currentIndex--;
         self.downImageView.image = self.middleImageView.image;
         self.middleImageView.image = self.upperImageView.image;
         
         
         if (_currentIndex == 0)
         {
             _upperLive = [self.lives lastObject];
             
         } else if (_currentIndex == -1)
         {
             _upperLive = self.lives[self.lives.count - 2];
             _currentIndex = self.lives.count-1;
             
         } else
         {
             _upperLive = self.lives[_currentIndex - 1];
         }
         [self prepareForImageView:self.upperImageView withLive:_upperLive];
     }
 }
}

2.顯示的view的數目不固定,通過上拉到底進行數據加載的場景

當要顯示的View的數目不固定的時候,使用上面那種方式對于數據加載時機的判斷就會相對比較復雜,所以考慮使用一個比較簡單的方式,第一種方式可以把UIScrollView想成一個閉環,3個View無限循環。為了實現上滑到底加載,我們可以考慮把這個閉環打開,還是3個View。但是3個view的整體位置,從滑動第二個view開始到倒數第二個view之間,相對位置不變,整體隨著滑動向下滑。整體流程,如下草圖所示:

unlimitscroll.png

示例代碼如下所示


- (void)reveiveNewData:(NSArray *)data {
   ......
         
   if (data.count > 0) {//如果獲取到新的數據,則自動上滑顯示
         self.contentSize = CGSizeMake(self.frame.size.width, self.frame.size.height * self.dataArray.count);
         self.contentOffset = CGPointMake(0, self.frame.size.height * self.currentIndexOfImageView);
     }
   ......
}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
 CGFloat offset_y = scrollView.contentOffset.y;
 
 CGPoint translatePoint = [scrollView.panGestureRecognizer translationInView:scrollView];
 if (self.dataArray.count == 0) {
     return;
 }
 
 if (offset_y > (self.frame.size.height * (self.dataArray.count - 1))) {
     if (self.isLoading) {
         return;
     }
     NSLog(@"拉到底部了");
     
     self.isLoading = YES;
     [self.dataDelegate pullNewData]; //如果拉到了底部,則去拉取新數據
     return;
 }

 if (self.currentIndexOfImageView > self.dataArray.count - 1) {
     return;
 }

 //向下滑動。
 if (offset_y > (self.frame.size.height * self.currentIndexOfImageView) && translatePoint.y < 0) {
     self.currentIndexOfImageView++;
     NSLog(@"lalalalalal");

     if (self.currentIndexOfImageView == self.dataArray.count) {
         return;
     }

     self.firstImageView.frame = self.secondImageView.frame;
     self.firstImageView.image = self.secondImageView.image;
     self.secondImageView.frame = self.thirdImageView.frame;
     self.firstImageView.image = self.secondImageView.image;
     self.secondImageView.image = self.thirdImageView.image;

     CGRect frame = self.thirdImageView.frame;
     frame.origin.y += self.frame.size.height;
     self.thirdImageView.frame = frame;
     self.thirdVideoModel = [self.dataArray objectAtIndex:self.currentIndexOfImageView];
     [self.thirdImageView sd_setImageWithURL:self.thirdVideoModel.imageURL];
 }
 
 if (offset_y < 0) {
     NSLog(@"已經到頂部了");
     return;
 }
 
 //向上滑動
 if (translatePoint.y > 0 && offset_y < self.secondImageView.frame.origin.y) {
     if (self.currentIndexOfImageView >= 3) {
         self.thirdImageView.frame = self.secondImageView.frame;
         self.thirdImageView.image = self.secondImageView.image;
         self.secondImageView.frame = self.firstImageView.frame;
         self.secondImageView.image = self.firstImageView.image;
         
         CGRect frame = self.firstImageView.frame;
         frame.origin.y -= self.frame.size.height;
         self.firstImageView.frame = frame;
         self.firstVideoModel = [self.dataArray objectAtIndex:self.currentIndexOfImageView - IMAGEVIEW_COUNT];
         [self.firstImageView sd_setImageWithURL:self.firstVideoModel.imageURL];
         
         self.currentIndexOfImageView--;
     }
 }
}

六. 邊播邊下載技術實現

工程中使用AVPlayer來實現視頻的播放,在視頻播放過程中,會經歷如下過程:

avplayerprocess.png

這些過程都是系統的類幫我們完成的。如果我們要實現邊下邊播,就需要在數據請求的過程中,設置一個代理,截取請求,然后轉發,收到服務器數據返回后,代理保存數據到本地,然后再把數據返回到播放器那邊,如下所示:

avplayerprocess.png

AVURLAsset中的AVAssetResourceLoader就是負責數據加載的,我們只要遵守它的AVAssetResourceLoaderDelegate協議,就能設置一個代理。AVURLAsset加載數據的時候,都會調用到協議shouldWaitForLoadingOfRequestedResource方法,我們通過這個方法取獲取到請求,并轉發到網絡訪問模塊。具體流程可以分為以下步驟:

1.修改視頻的URL的scheme為系統無法識別的scheme,這樣AVURLAsset發出的請求才會跑到我們的代理。

- (NSURL *)getSchemeVideoURL:(NSURL *)url
{
  NSURLComponents *components = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO];
  components.scheme = @"streaming";
  return [components URL];
}

2.設置我們的代理。

[urlAsset.resourceLoader setDelegate:self.resourceLoader queue:dispatch_get_main_queue()];

3.轉發請求

- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest {
  NSLog(@"dk----%@", loadingRequest);
  [self.loadingRequests addObject:loadingRequest];
  [self dealLoadingRequest:loadingRequest];
  
  return YES;
}

4.把請求返回給播放器


- (BOOL)respondWithDataForRequest:(AVAssetResourceLoadingDataRequest *)dataRequest {
  long long startOffset = dataRequest.requestedOffset;
  ......
  
  [dataRequest respondWithData:[filedata subdataWithRange:NSMakeRange((NSUInteger)startOffset, (NSUInteger)numberOfBytesToRespondWith)]]; //把本地存在的數據返回到播放器
  
  ....
 }

七. 參考資料

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373