仿抖音在線播放器設計
一. 文檔大綱
- 效果預覽
- 功能說明
- 工程說明
- 無限滑動技術實現
- 邊播邊下載技術實現
- 參考資料
Demo地址:MBVideoPlayer
二. 效果預覽
工程效果預覽如下圖所示
三. 功能說明
工程實現了三方面的功能
- 基于UIScrollView的無限滑動功能
在下拉過程中,到底部的時候,加載新的數據,若數據無限,則頁面可以無限滑動下去。
- 在線視頻的邊播放邊下載功能
視頻播放過程中,會自動下載到本地沙盒中。支持斷點續傳功能。
- 離線播放功能
如果本地存在播放的視頻數據,則優先播放本地的數據,所以在離線狀態下也可以進行視頻播放。
四. 工程說明
工程結構如下所示:
說明如下:
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之間,相對位置不變,整體隨著滑動向下滑。整體流程,如下草圖所示:
示例代碼如下所示
- (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
來實現視頻的播放,在視頻播放過程中,會經歷如下過程:
這些過程都是系統的類幫我們完成的。如果我們要實現邊下邊播,就需要在數據請求的過程中,設置一個代理,截取請求,然后轉發,收到服務器數據返回后,代理保存數據到本地,然后再把數據返回到播放器那邊,如下所示:
而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)]]; //把本地存在的數據返回到播放器
....
}