你们好,我是 NewPan,此次咱们来说解 JPVideoPlayer 3.0 实现上的细节。git
若是你没有了解实现原理的需求,请直接看另一篇介绍如何使用的文章:[iOS]JPVideoPlayer 3.0 使用介绍。github
从去年发了 2.0 版本之后,愈来愈多的同窗使用这个框架, issue 也愈来愈多,一度有 90 多个,可是绝大多数是说使用这个框架并不能实现变下边播,而是要下载完才能播。当时我也是头大,我看了整个 AVFoundation
关于视频播放的文档,苹果除了留出了一个拦截 AVPlayer
的请求的接口,另外没有任何关于对请求的处理的介绍。数组
文档没有结果,就去 Google 上搜,网上的结果大体有三类,第一类就是说不可能基 AVPlayer
实现变下边播;第二类是就是 2.0 版本时候的样子,只能支持某些视频的边下边播;第三类是说使用本地代理实现对端口的请求的拦截。我本身考虑还可使用 ijkPlayer
实现边下边播。缓存
我最早研究的是 ijkPlayer
,由于 FFmpeg
是开源的,只要从底层开始将播放器拿到请求数据回调到上层,就能实现视频数据的缓存。当时看了差很少一周的 ijkPlayer
源码,整个 ijkPlayer
大概有四层封装,最后才能看到 OC 的接口,我从最上层往下看,依次是 OC 层,iOS 平台层,iOS和安卓共用层,最后才是 FFmepg,我看到第二层,到后面愈来愈难,并且随着调试的深刻,发如今平台特性上,内存、启动时间、优化、性能真的不如 AVPlayer
,并且还有一点,如今不少 APP 都有直播功能,直播 SDK 都是使用 FFmpeg
,若是直接基于ijkPlayer
,会出现标识符重复,不少人都没办法使用。因而选择研究其余方案。服务器
接下来开始研究使用本地代理实现对端口的请求的拦截,要实现对端口的拦截,GitHub 上有一个颇有名的基于 GCD 的框架能够实现 GCDWebServer。这个框架的做者当时是为了作局网内实现 iPad 本地数据投屏到电视仍是什么鬼的作了这样一个框架。这个框架的原理是对指定的端口进行拦截,而后让 AVPlayer
往这个端口请求,而后就能拦截到 AVPlayer
的全部请求,而后把这个请求转发给用户,用户能够响应本地的视频数据,而后 AVPlayer
就能够开始播放了。这个很典型的使用场景就是,在局网内把 iPhone 或是 iPad 的本地数据分享给别的终端。这个想法挺棒的,我看到安卓有一个很棒的开源项目就是基于这个思路给安卓官方的播放器作的本地缓存。因此去年过年那几天都在研究这个框架。微信
可是这个GCDWebserver
的做者只作了本地数据的响应,也就是说,我本地有一个数据,其余地方来请求,我把这个本地数据一片一片的读出来写到 socket 里,而后请求者就能拿到数据了,等这个数据写完之后,这个 socket 就断开了。可是咱们如今要作的事情,咱们的视频数据不在本地,咱们拿到请求之后还要去网络上请求数据才能响应数据给 socket,这个框架不符合咱们的使用场景。因此我要作的就是,本身基于这个框架写一个咱们要用的功能。而后吭哧吭哧写了好几天,发现这些底层的写起来真的很费劲,并且调试也不容易。写高级语言习惯了,已经不会写底层了。网络
一次偶然逛 GitHub,看到了 AVPlayerCacheSupport 这个框架,发现这位做者实现了支持 seek 的缓存,赶忙下载了源码下来看了一下,发现原来 JPVideoPlayer
2.0 有些视频播不了是由于我对请求队列的管理出了问题,因此我后来联系了这个框架的做者,请他受权我在个人框架中使用他部分源码,他慷慨答应,可是要加他微信,他就没回我了。框架
到了这里,我把以前基于端口拦截的方案给停了,由于从底层开始写,真的效率过低了。并且既然 AVPlayer
提供了请求拦截的入口,我就不必本身再基于端口进行拦截了。socket
因而方案终于敲定,也就到了年后开工的时候了,如今想一想,这个方案真的花了差很少半年的时间,也是挺不容易的。ide
接下来咱们就把下面这张结构图讲清楚就能够了。
如今框架支持下面三种类型的视频路径的播放:
local file, play video
。这个是最简单的,检查一下 URL,若是是本地 URL,初始化一个JPVideoPlayer
,把路径塞给它,立马就开始播放了,没什么好讲的。network result, play video
。这个就复杂点,初始化一个播放器之后,拦截它的请求,而后把这个请求封装成为本身内部的请求,而后去网络上下载视频,下载下来响应播放器,同时也缓存到本地。disk result, play video
,和上面同样要初始化播放器,而后拦截播放器请求,而后要先去缓存中查一下这个请求,有哪些数据已经缓存到本地了,那些已经缓存到本地的数据就不用再去下载了,直接从磁盘中读出来就能够了,那些没缓存的就按照第二点的思路去下载。而后整个过程就串起来了。接下来熟悉一下整个类目结构:
我如今按照我当时写的循序,从最低层开始,一点一点往上封装,直到最后用户看到的只有一个简单的接口。
AVPlayer
请求的截获AVPlayer
请求队列的管理JPResourceLoadingRequestTask
封装本地和网络请求JPVideoPlayerCacheFile
如何管理断点续传JPResourceLoadingRequestTask
和JPVideoPlayerCacheFile
UIView+WebVideoCache
接口如何封装JPVideoPlayerControlViews
怎么和播放业务彻底解耦AVPlayer
请求的截获// 获取到新的请求
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader
shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest;
// 取消请求
- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader
didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest;
复制代码
一切都从这里开始,咱们从这里拿到 AVPlayer
想要获取的数据的请求,而后将这个请求保存到数组中,而后发起网络请求,拿回这个请求的数据,而后将这个数据传回给播放器,视频播放就开始了。
同时,播放器也可能会调用取消请求的回调,告诉咱们某个请求已经取消掉了,不用在请求数据了,此时咱们就应该将这个请求从请求数组中移除掉。
AVPlayer
请求队列的管理上面有说过,2.0 版本时对这个请求队列的管理有问题致使有些视频播不了。以前的处理方式是,一旦AVPlayer
有新的请求过来,就立马将以前内部请求中止掉,而后发起新的请求。这样致使的问题是,若是这个视频的metadata
在视频数据的最前面,那么立马能够拿到这个元数据,就能够一个请求从头播到尾。可是并非全部的视频在编码的时候都把metadata
放在最前面,metadata
可能编码在视频数据的任何位置,就像下面这张图同样。这就是 2.0 为何有些视频能播,有些视频却要下载完再播的缘由。
在生产环境,大多都不是metadata
在视频数据的最前面,因此AVPlayer
会不断地调整请求的 range 来得到视频的metadata
。由于要播放一个视频,必需要有metadata
,metadata
就是这个视频的身份证信息。下面我列出了使用青花瓷拦截一次视频播放的请求。
Range: bytes=0-1 // 获取请求的 contenttype,只响应视频或音频
Range: bytes=0-34611998 // 尝试从头至尾请求
Range: bytes=34537472-34611998 // 上次请求没有拿到 metadata, 调整请求range
Range: bytes=34548960-34611998 // 上次请求没有拿到 metadata, 调整请求range
Range: bytes=34603008-34611998 // 上次请求没有拿到 metadata, 调整请求range
Range: bytes=34601692-34603007 // 上次请求没有拿到 metadata, 调整请求range
Range: bytes=1388-34537471 // 获取 metadata 成功,视频开始播放
...
复制代码
能够看到,AVPlayer
的请求有必定的套路。第一次请求是拿到服务器的响应,看这个 URL 是否是一个视频或者音频,若是不是视频或者音频,播放器就直接抛出播放失败的错误。若是是一个可播放的 URL,那么接下来第一步,会假设这个视频的metadata
在头部,若是不在头部,再一次,会假设在尾部,尾部尚未就可能编码在任何位置了,只能经过不断地尝试来获取到这个metadata
。因此若是你要在手机端录制视频,最能快速播放这个视频的方式就是把这个metadata
编码在视频头部,这样,别人播放时的时候就能一个请求百步穿杨,大大减小了这个视频看到首帧的时间,进而提升用户体验。而这个视频的数据编码在Range: bytes=1388-34537471
的范围内,因此要多请求不少次。
知道了这些之后咱们的请求队列就应该更改为为,不要进来一个请求就将以前的请求取消掉,而是应该将这些请求编队,让它们遵行 FIFO(先进先出),若是播放器明确要求将某个请求取消的时候,在将对应的请求 cancel 掉,而后移出队列。
上面两点都是JPVideoPlayerResourceLoader
所作的事情的一部分,简单来讲就是拦截请求,而后管理这些拦截到的请求。
JPResourceLoadingRequestTask
封装本地和网络请求因为咱们要作断点续传,因此不可能直接截获AVPlayer
就拿这个请求的 range 进行请求。由于有些数据可能已经缓存在磁盘里了,不须要再次从网络上重复下载,而有些确实须要经过网络请求获取,就像下面这样。
因此当咱们拿到一个AVPlayer
请求的时候,先要和本地已有的缓存进行比对,而后按照规则:没有的从网络上下载,有的直接取本地。这样之后,每一个AVPlayer
请求就会拆分为多个内部的本地和网络请求,而JPResourceLoadingRequestTask
就是内部请求。
JPResourceLoadingRequestTask
是一个抽象模板类,不能直接被使用,须要继承而且实现它定义的方法,才可使用。由于咱们的使用场景就是本地和网络请求,因此框架中实现了网络和本地请求两个子类,分别对应的负责对应的请求,并在得到数据之后回调给它的代理。
到此为止,咱们拦截到的请求就已经所有封装成为框架内部的请求了。
JPVideoPlayerCacheFile
如何管理断点续传对视频数据文件的增删改查绝对是这个框架的核心,这个文件是 AVPlayerCacheSupport的做者写的,我只是在他文件的基础上改了 bug,让这个类能正常工做。
这个文件持有两个文件句柄NSFileHandle
,一个负责写文件,一个负责读文件。每当一个视频第一次播放的时候,播放器确定会先请求前两个字节的数据,其实就是为了拿到这个 URL 的contentType
,当拿到这个响应信息的时候,当前这个类也会把contentType
信息缓存到本地。而后每次视频数据一片一片回来的时候,这个类拿到数据,就会使用文件句柄写到本地,而后每次写完数据也会将当前这一片数据的 range 保存起来。同时也会将这个 range 和已有的 range 进行比对,当这个 range 和已有的 range 有交集,或者先后衔接的时候,就将这两个 range 合成一个 range。
想象一下,按照这个规则一直循环下去,最后当这个文件缓存彻底的时候,这些 range 最后会合并成一个 range,而这个 range 就是文件的长度。这样,咱们就实现了文件的断点续传。
而读文件就相对简单了。可是读文件有一点须要注意,咱们不该该将视频文件一次性所有读出来,假如一个视频有 1 GB,那内存会忽然爆掉。因此咱们应该采起的策略是一点一点读,比方说,每次读出 32 Kb 写给播放器,写完之后再读 32 Kb,这样循环,直到数据读完。
对先后台的管理可能在不一样的产品中有不一样的形式,比方说用户将 APP 推入后台和用户滑出通知中心可能有不一样的处理。而在 iPhone 设备上先后台总共分为“通知中心,控制中心,全局警告,双击 home 键,跳去其余 APP 分享,进入后台,锁屏”。而这些,都不用你操心,框架中有一个JPApplicationStateMonitor
类,专门负责监听 APP 状态。你只须要成为代理就能轻松应对这些状态。
- (BOOL)shouldPausePlaybackWhenApplicationWillResignActiveForURL:(NSURL *)videoURL;
- (BOOL)shouldPausePlaybackWhenApplicationDidEnterBackgroundForURL:(NSURL *)videoURL;
- (BOOL)shouldResumePlaybackWhenApplicationDidBecomeActiveFromResignActiveForURL:(NSURL *)videoURL;
- (BOOL)shouldResumePlaybackWhenApplicationDidBecomeActiveFromBackgroundForURL:(NSURL *)videoURL;
复制代码
UIView+WebVideoCache
接口如何封装考虑到列表播放视频的场景,一个是在列表中播放视频,还有就是从视频列表页跳转视频详情页面,另一个就是详情页悬停的界面。框架为这三个场景封装了专门的 API,若是在使用中还有其余的场景,能够基于最基础的视频播放 API 进行封装。
列表中播放视频,像新浪微博、Facebook、Twitter 这样的 APP,都只有一个缓冲动画和播放&缓冲进度指示器,框架也使用了同样的思路进行了相似的封装。
在详情页播放视频,上面的缓冲动画和播放&缓冲进度指示器都得有,并且还须要一套和用户交互控制视频播放的界面。
悬停的时候比较简单,就是单独一个视频窗口。
基于以上三个场景,封装了如下三个方法:
- (void)jp_playVideoMuteWithURL:url
bufferingIndicator:nil
progressView:nil
configurationCompletion:nil;
- (void)jp_playVideoWithURL:url
bufferingIndicator:nil
controlView:nil
progressView:nil
configurationCompletion:nil;
- (void)jp_playVideoWithURL:url
options:kNilOption
configurationCompletion:nil;
复制代码
固然还有一种必不可少的场景就是,比方说用户从列表页跳转到详情页,这个时候若是使用以上的接口,就会出现到了详情页之后视频从新播放,这样很影响用户体验。因此框架里对这种状况也进行了封装,就是使用包含resume
的接口,这样就能实现连贯的播放了。就像下面这样:
JPVideoPlayerControlViews
怎么和播放业务彻底解耦考虑到用户后期须要定制本身的界面,因此业务层和界面层必须彻底解耦。框架里使用了面向协议的方式进行解耦。抽取了三个不一样的协议,要定制不一样的界面只须要实现指定的协议方法就能够根据播放状态更新 UI。
<JPVideoPlayerBufferingProtocol>
<JPVideoPlayerProtocol>
<JPVideoPlayerProtocol>
同时框架还根据对应的协议实现了对应的模板类,若是没有定制 UI 的需求,能够直接使用模板类,就能快速实现对应的界面。同时也能够继承模板类替换 UI 素材快速定制 UI。
包括腾讯视频、优酷视频、哔哩哔哩等 APP 都是采用假横屏来实现视频横屏,那究竟什么是假横屏?下面这张图演示了什么是假横屏。将视频添加到 window 上,而后将视频顺时针旋转 90°,这样就是假横屏。
横屏代码以下:
- (void)executeLandscape {
UIView *videoPlayerView = ...;
CGRect screenBounds = [[UIScreen mainScreen] bounds];
CGRect bounds = CGRectMake(0, 0, CGRectGetHeight(screenBounds), CGRectGetWidth(screenBounds));
CGPoint center = CGPointMake(CGRectGetMidX(screenBounds), CGRectGetMidY(screenBounds));
videoPlayerView.bounds = bounds;
videoPlayerView.center = center;
videoPlayerView.transform = CGAffineTransformMakeRotation(M_PI_2);
}
复制代码
这样横是横过来了,可是这个videoPlayerView
的子view
都没有横过来,并且就算是这些子view
是使用 autolayout 布局的,也没有对应的更改约束。
下面是 frame 的文档说明:
The frame rectangle is position and size of the layer specified in the superlayer’s coordinate space. For layers, the frame rectangle is a computed property that is derived from the values in thebounds, anchorPoint and position properties. When you assign a new value to this property, the layer changes its position and bounds properties to match the rectangle you specified. The values of each coordinate in the rectangle are measured in points.
复制代码
意思就是子view
是相对父view
进行布局的,如今咱们直接更改videoPlayerView
的bounds
和center
属性,而没有更改frame
属性,这样就会致使子view
布局出现问题,因此咱们在更改完bounds
和center
之后,也要讲对应的frame
属性也进行更正。这样更正之后,使用 autolayout 布局的子view
布局就正常了。可是直接使用frame
布局的子view
仍是会有横竖屏兼容的问题,因此框架里专门抽取了一个布局的方法给子类复写。
- (void)layoutThatFits:(CGRect)constrainedRect
nearestViewControllerInViewTree:(UIViewController *_Nullable)nearestViewController
interfaceOrientation:(JPVideoPlayViewInterfaceOrientation)interfaceOrientation;
复制代码
这个方法把父视图的大小传了过来,同时也把当前视图对应的控制器也传了过来,同时还把当前的视频的方向也传了过来,这样,就能够根据不一样的屏幕方向进行不一样的布局了。
不一样的产品中可能同时存在等高和不等高的 cell 来播放视频,上个版本就只支持等高 cell 的滑动播放,其实滑动播放的策略是不分等高和不等高的,只要稍加修改就能够了。
此次不只支持不等高 cell 的滑动播放,还支持在计算离 tableView 可见区域中心最近时,可使用 cell 进行计算,也可使用播放视频的 view 来进行计算。为此我专门画了一幅图来讲明这个区别:
下面这个连接是我全部文章的一个集合目录。这些文章凡是涉及实现的,每篇文章中都有 Github 地址,Github 上都有源码。