大家好,我是 NewPan,好久没冒泡了,去年下半年不加班的时间里,我一直在研究如何实现基于AVPlayer实现视频支持拖拽进度的边下载边播放。这个过程缓慢又辛酸,中途数次看不到希望,差点放弃,但是最后还是坚持了下来,于是就有了现在全新的 3.0 版本。这次会分两篇文章讲解,第一篇是 3.0 的使用介绍,是写给那些只需知道如何使用的同学,接下来按照惯例,我会介绍源码的实现。
首先,我们来看一下全新 3.0 版本的新特性。对了,GitHub 地址在这里。
这些特性基本涵盖了做视频播放的各方面,其中最重要的,也是这个框架价值所在,就是基于AVPlayer实现了边下边播,同时支持断点续传。
边下边播支持
拖拽进度支持(new)
断点续传支持(new)
假横屏 auto-layout 布局支持(new)
继承协议自定义播放控制界面支持(new)
同一 URL 不重复下载支持
保证不阻塞主线程
本地视频播放播放支持
Swift 支持
由于这个框架最开始的时候就是为列表播放视频设计的,3.0 版本中这一点也得到了延续。框架对外提供了 3 类UIView的分类方法,保证不侵入你的项目。
这个情况适合在列表中跟随用户的滑动,对应的播放某个 cell 上的视频,就像微博列表页视频播放一样。这种情况没有任何对视频的控制界面,只有一个缓冲进度条和播放进度条,就像下面这样:
要实现这个功能,只需要调用下面这个方法就可以了:
NSURL *url = [NSURL URLWithString:@"http://p11s9kqxf.bkt.clouddn.com/bianche.mp4"]; [aview jp_playVideoMuteWithURL:url bufferingIndicator:nil progressView:nil configurationCompletion:nil];
这个方法有四个参数,第一个不用说了,第二个是视频缓冲指示器,第三个是缓冲和播放进度条,第四个是配置完视频以后的一些操作回调。
但是在这个接口,除了第一个必选参数外,其他三个你都可以传空,因为框架为你实现了默认的视图,同时你也可以继承我提供的模板类进行快速的自定义。关于这点,我在下面会提到。
配套的,还有下面这个方法。
NSURL *url = [NSURL URLWithString:@"http://p11s9kqxf.bkt.clouddn.com/bianche.mp4"]; [aview jp_resumeMutePlayWithURL:url bufferingIndicator:nil progressView:nil configurationCompletion:nil];
这个方法是什么意思呢?我们在视频列表页播放,当用户选中了某一个 cell 的时候会跳转到对应的视频详情页,这个时候就轮到这个方法上场了。因为如果你直接使用上面那个方法来播放的话,视频会重头播,这样破坏了用户体验,而你调用这个方法,就可以连贯的开始播放。
同时这个方法中,你仍然可以定制自己的界面,而不是必须和上个界面的控制界面一样,小棉袄贴心吧?
这个功能在视频详情页是必须的。这个时候除了视频图像一般还配套的有缓冲动画、播放进度以及控制视频界面。就像下面这样。
这个功能的接口是:
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];
有了这些以后,我们就可以实现下面的悬停播放功能。
下面我用 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
当然,如果你不想使用这些模板类,想要自己从头搭建,也是很方便的,而且能完全和播放逻辑解耦。你只需要实现对应的协议即可。
缓冲动画指示器:
播放和缓冲进度指示器:
控制界面:
需要注意的是,对视频的横屏并没有真正的将窗口横过来,这是对国内 APP 现状的平衡,国内大多数 APP 都只支持竖屏,优酷 APP、腾讯视频 APP、哔哩哔哩 APP 等都是采用这种方式进行横屏。如果你关心这内部的实现,请你去看一下源码,这篇文章不进行讲解。
非常感谢有些同学是从 2.0 版本一路支持过来的,由于 3.0 对缓存的管理完全重构,缓存路径改了,之前的缓存用不了了。所以我提供了-clearVideoCacheOnVersion2OnCompletion:方法来清理掉旧的缓存。
缓存内部实现改了,但是对外查询管理的接口没有改变,具体请查看接口文档。对了,GitHub 地址在这里。
下篇文章在这里 [iOS]JPVideoPlayer 3.0 源码解析。
下面这个链接是我所有文章的一个集合目录。这些文章凡是涉及实现的,每篇文章中都有 Github 地址,Github 上都有源码。
作者:NewPan
链接:https://www.jianshu.com/p/282e3e0cfe78#comments