仿抖音在線播放器設(shè)計(jì)
一. 文檔大綱
- 效果預(yù)覽
- 功能說明
- 工程說明
- 無限滑動(dòng)技術(shù)實(shí)現(xiàn)
- 邊播邊下載技術(shù)實(shí)現(xiàn)
- 參考資料
Demo地址:MBVideoPlayer
二. 效果預(yù)覽
工程效果預(yù)覽如下圖所示
三. 功能說明
工程實(shí)現(xiàn)了三方面的功能
- 基于UIScrollView的無限滑動(dòng)功能
在下拉過程中撤嫩,到底部的時(shí)候,加載新的數(shù)據(jù)赃阀,若數(shù)據(jù)無限,則頁(yè)面可以無限滑動(dòng)下去焰檩。
- 在線視頻的邊播放邊下載功能
視頻播放過程中多柑,會(huì)自動(dòng)下載到本地沙盒中遥椿。支持?jǐn)帱c(diǎn)續(xù)傳功能。
- 離線播放功能
如果本地存在播放的視頻數(shù)據(jù)掏父,則優(yōu)先播放本地的數(shù)據(jù)笋轨,所以在離線狀態(tài)下也可以進(jìn)行視頻播放。
四. 工程說明
工程結(jié)構(gòu)如下所示:
說明如下:
MBVideoPlayer
└── Other
│ ├── MBAVAssetResourceLoader (h/m) #播放器數(shù)據(jù)代理類赊淑,用于攔截播放器請(qǐng)求并返回?cái)?shù)據(jù)
| └── MBNetworkManager (h/m) #網(wǎng)絡(luò)訪問類爵政,用于向服務(wù)器請(qǐng)求播放數(shù)據(jù)
├── View
| ├── MBScrollView (h/m) #ScrollerView類,用來顯示PlayerView
| └── MBPlayerView (h/m) #視頻播放視圖
| └── MBToastLabelView (h/m) #用于顯示使用過程的提示
├── Model
| ├── MBVideoModel (h/m) #videoModel類陶缺,包含了視頻下載鏈接钾挟,視頻描述等字段
| └── MBURLTaskModel (h/m) #視頻下載model,包含了當(dāng)前下載進(jìn)度饱岸,視頻大小等信息
├── Controller
├── ViewController (h/m) #主VC掺出,顯示視頻流
|── MBSettingViewController (h/m) #設(shè)置界面VC徽千,用于清除緩存數(shù)據(jù)
五. 無限滑動(dòng)技術(shù)實(shí)現(xiàn)
使用UIScrollView
實(shí)現(xiàn)無限滑動(dòng),基于具體的應(yīng)用場(chǎng)景汤锨,有不同的實(shí)現(xiàn)方式双抽。這里列舉兩種應(yīng)用場(chǎng)景的使用方式。
1.顯示的View的數(shù)目固定的情況下(這種方式不在本工程中)
當(dāng)顯示View的數(shù)目固定的時(shí)候闲礼,其實(shí)UIScollView上面只需要添加三個(gè)View就能顯示所有的View牍汹,無需為所有的View都添加一個(gè)獨(dú)立的View。實(shí)現(xiàn)的原理如下:
初始化scrollView的時(shí)候柬泽,設(shè)置3個(gè)View慎菲,添加到scrollView中,并通過設(shè)置contentoffset的屬性锨并,讓中間的view顯示到界面上钧嘶。
每次滑動(dòng)的時(shí)候,在
scrollViewDidScroll:
方法種琳疏,判斷是否滑動(dòng)的下一個(gè)View,如果滑動(dòng)到下一個(gè)View的話闸拿,則繼續(xù)設(shè)置scrollView的contentOffset空盼,讓scrollView復(fù)位,始終讓中間那個(gè)View顯示新荤。復(fù)位ScrollView之后揽趾,設(shè)置3個(gè)View的位置,如果是UIImageView的話苛骨,其實(shí)直接修改它們的image的值就可以了篱瞎。
代碼如下所示:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat offset = scrollView.contentOffset.y;
if (self.lives.count) {
if (offset >= 2*self.frame.size.height) //向下滑
{
// 對(duì)ScollView進(jìn)行復(fù)位處理
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)//獲取最后一張顯示的是什么內(nèi)容
{
_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的數(shù)目不固定,通過上拉到底進(jìn)行數(shù)據(jù)加載的場(chǎng)景
當(dāng)要顯示的View的數(shù)目不固定的時(shí)候痒芝,使用上面那種方式對(duì)于數(shù)據(jù)加載時(shí)機(jī)的判斷就會(huì)相對(duì)比較復(fù)雜俐筋,所以考慮使用一個(gè)比較簡(jiǎn)單的方式,第一種方式可以把UIScrollView想成一個(gè)閉環(huán)严衬,3個(gè)View無限循環(huán)澄者。為了實(shí)現(xiàn)上滑到底加載,我們可以考慮把這個(gè)閉環(huán)打開请琳,還是3個(gè)View粱挡。但是3個(gè)view的整體位置,從滑動(dòng)第二個(gè)view開始到倒數(shù)第二個(gè)view之間俄精,相對(duì)位置不變询筏,整體隨著滑動(dòng)向下滑。整體流程竖慧,如下草圖所示:
示例代碼如下所示
- (void)reveiveNewData:(NSArray *)data {
......
if (data.count > 0) {//如果獲取到新的數(shù)據(jù)嫌套,則自動(dòng)上滑顯示
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]; //如果拉到了底部逆屡,則去拉取新數(shù)據(jù)
return;
}
if (self.currentIndexOfImageView > self.dataArray.count - 1) {
return;
}
//向下滑動(dòng)。
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(@"已經(jīng)到頂部了");
return;
}
//向上滑動(dòng)
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--;
}
}
}
六. 邊播邊下載技術(shù)實(shí)現(xiàn)
工程中使用AVPlayer
來實(shí)現(xiàn)視頻的播放灌危,在視頻播放過程中康二,會(huì)經(jīng)歷如下過程:
這些過程都是系統(tǒng)的類幫我們完成的。如果我們要實(shí)現(xiàn)邊下邊播勇蝙,就需要在數(shù)據(jù)請(qǐng)求的過程中沫勿,設(shè)置一個(gè)代理,截取請(qǐng)求味混,然后轉(zhuǎn)發(fā)产雹,收到服務(wù)器數(shù)據(jù)返回后,代理保存數(shù)據(jù)到本地翁锡,然后再把數(shù)據(jù)返回到播放器那邊蔓挖,如下所示:
而AVURLAsset
中的AVAssetResourceLoader
就是負(fù)責(zé)數(shù)據(jù)加載的,我們只要遵守它的AVAssetResourceLoaderDelegate
協(xié)議馆衔,就能設(shè)置一個(gè)代理瘟判。AVURLAsset
加載數(shù)據(jù)的時(shí)候,都會(huì)調(diào)用到協(xié)議shouldWaitForLoadingOfRequestedResource
方法角溃,我們通過這個(gè)方法取獲取到請(qǐng)求拷获,并轉(zhuǎn)發(fā)到網(wǎng)絡(luò)訪問模塊。具體流程可以分為以下步驟:
1.修改視頻的URL的scheme為系統(tǒng)無法識(shí)別的scheme减细,這樣AVURLAsset
發(fā)出的請(qǐng)求才會(huì)跑到我們的代理匆瓜。
- (NSURL *)getSchemeVideoURL:(NSURL *)url
{
NSURLComponents *components = [[NSURLComponents alloc] initWithURL:url resolvingAgainstBaseURL:NO];
components.scheme = @"streaming";
return [components URL];
}
2.設(shè)置我們的代理。
[urlAsset.resourceLoader setDelegate:self.resourceLoader queue:dispatch_get_main_queue()];
3.轉(zhuǎn)發(fā)請(qǐng)求
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest {
NSLog(@"dk----%@", loadingRequest);
[self.loadingRequests addObject:loadingRequest];
[self dealLoadingRequest:loadingRequest];
return YES;
}
4.把請(qǐng)求返回給播放器
- (BOOL)respondWithDataForRequest:(AVAssetResourceLoadingDataRequest *)dataRequest {
long long startOffset = dataRequest.requestedOffset;
......
[dataRequest respondWithData:[filedata subdataWithRange:NSMakeRange((NSUInteger)startOffset, (NSUInteger)numberOfBytesToRespondWith)]]; //把本地存在的數(shù)據(jù)返回到播放器
....
}