你们好,我是 NewPan,很久没冒泡了,去年下半年不加班的时间里,我一直在研究如何实现基于 AVPlayer
实现视频支持拖拽进度的边下载边播放。这个过程缓慢又辛酸,中途数次看不到但愿,差点放弃,可是最后仍是坚持了下来,因而就有了如今全新的 3.0 版本。此次会分两篇文章讲解,第一篇是 3.0 的使用介绍,是写给那些只需知道如何使用的同窗,接下来按照惯例,我会介绍源码的实现。git
首先,咱们来看一下全新 3.0 版本的新特性。对了,GitHub 地址在这里。github
这些特性基本涵盖了作视频播放的各方面,其中最重要的,也是这个框架价值所在,就是基于 AVPlayer
实现了边下边播,同时支持断点续传。缓存
因为这个框架最开始的时候就是为列表播放视频设计的,3.0 版本中这一点也获得了延续。框架对外提供了 3 类 UIView
的分类方法,保证不侵入你的项目。app
这个状况适合在列表中跟随用户的滑动,对应的播放某个 cell 上的视频,就像微博列表页视频播放同样。这种状况没有任何对视频的控制界面,只有一个缓冲进度条和播放进度条,就像下面这样:框架
要实现这个功能,只须要调用下面这个方法就能够了:ide
NSURL *url = [NSURL URLWithString:@"http://p11s9kqxf.bkt.clouddn.com/bianche.mp4"];
[aview jp_playVideoMuteWithURL:url
bufferingIndicator:nil
progressView:nil
configurationCompletion:nil];
复制代码
这个方法有四个参数,第一个不用说了,第二个是视频缓冲指示器,第三个是缓冲和播放进度条,第四个是配置完视频之后的一些操做回调。布局
可是在这个接口,除了第一个必选参数外,其余三个你均可以传空,由于框架为你实现了默认的视图,同时你也能够继承我提供的模板类进行快速的自定义。关于这点,我在下面会提到。fetch
配套的,还有下面这个方法。动画
NSURL *url = [NSURL URLWithString:@"http://p11s9kqxf.bkt.clouddn.com/bianche.mp4"];
[aview jp_resumeMutePlayWithURL:url
bufferingIndicator:nil
progressView:nil
configurationCompletion:nil];
复制代码
这个方法是什么意思呢?咱们在视频列表页播放,当用户选中了某一个 cell 的时候会跳转到对应的视频详情页,这个时候就轮到这个方法上场了。由于若是你直接使用上面那个方法来播放的话,视频会重头播,这样破坏了用户体验,而你调用这个方法,就能够连贯的开始播放。ui
同时这个方法中,你仍然能够定制本身的界面,而不是必须和上个界面的控制界面同样,小棉袄贴心吧?
这个功能在视频详情页是必须的。这个时候除了视频图像通常还配套的有缓冲动画、播放进度以及控制视频界面。就像下面这样。
这个功能的接口是:
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 上都有源码。