IOS音视频(二)AVFoundation视频捕捉android
IOS音视频(三)AVFoundation 播放和录音ios
IOS音视频(四十三)AVFoundation 之 Audio Sessiongit
IOS音视频(四十四)AVFoundation 之 Audio Queue Servicesgithub
IOS音视频(四十五)HTTPS 自签名证书 实现边下边播算法
因为JimuPro相册里面获取视频,须要将视频所有下载到本地后才能播放,若是视频文件很大,则用户须要等待很长时间才能看到视频,这种体验效果不太友好,针对这个问题,须要IOS app端实现边下边播功能,使用一份数据流,完成观看视频的同事将视频保存到本地,等视频播放完成后,视频也就下载到了本地。下载完成后的视频格式是.mp4格式,导出来能够直接播放。当用户第二次观看次视频时,将不从机器人端获取视频,直接读取本地缓存的视频,也就是离线也能够观看。swift
实现边下边播的方式,能够节省数据流量,实时观看到机器人端录制的视频,能够拖拽的方式观看。api
这个功能知足如下需求:数组
IOS客户端实现边下边播的方案有不少,目前我研究的找到3种解决方案。下面将详细介绍3种方案的实现原理。因为JimuPro里面已经用到了开源的播放器:VGPlayer。这个播放器里面基本上实现了方案三的细节问题。只是没有实现HTTPS 自签名证书认证的问题。
IOS项目中我推荐使用第三种方案实现边下边播功能。
此方案是先下载视频到本地文件,而后把本地视频文件地址传给播放器,播放器实际播放的是本地文件。当播放器的播放进度大于当前的可播放的下载缓存进度,则暂停播放,等缓存到足够播放时间以后,再让播放器开始播放。这种方案的下载方式是与播放器彻底没有关系的,只是顺序的将服务器下发的视频数据写入本地文件,而后让播放器来读取数据。
目前的已有解决方案是,当缓存到500kb才把缓存的地址传给播放器,视频文件小于500kb则下载完以后再播放,起播慢(须要改进)。当下载进度比播放进度多5秒的数据量才让播放器播放,否则的话就暂停。若是seek到没有缓存的地方就切换到网络上中止当前的下载,浪费一些流量。每次下载都会保存一份配置文件,来保存是否下载完成,没下载完成则第二次根据当前缓存文件大小,从新开始顺序下载。
总的来讲第一种方案有以下缺点:
- 用户播放视频的时候可能等待的时间较长(起播
- 流量浪费(seek以后会播网络流,中止下载)
- 须要太多控制视频播放的逻辑来进行辅助,与播放器代码耦合严重。
- seek以后切源会耗时,每次seek比较慢
这个代理服务器也能够作在机器人端,一个接口用于播放,一个接口用于下载。
使用 HTTPServer,在本地开启一个 http 服务器,把须要缓存的请求地址指向本地服务器,并带上真正的 url 地址。HTTPServer 无论咱们有没有使用缓存功能,都要在应用打开的时候默默开启,对APP性能是一大损耗。而且咱们引入 HTTPServer 库也会增长一些包体积。
此方案的特色以下:
- 经过代理服务器,从socket截取播放器请求数据;
- 根据截取的range信息,从网络服务器请求视频数据;
- 视频数据写入本地文件,seek后能够从seek位置继续写入并播放;
- 边下边播,加快播放速度;
- 与播放器逻辑彻底解耦,对于播放器只是一个地址
本方案是在播放器与视频源服务器之间加一层代理服务器,截取视频播放器发送的请求,根据截取的请求,向网络服务器请求数据,而后写到本地。本地代理服务器从文件中读取数据并发送给播放器进行播放. 以下图所示:
如上图,具体流程细节以下:
- 启动本地代理服务器。
- 视频源地址传给本地代理服务器。
- 将视频源地址转换成本地代理服务器的地址做为播放器的视频源地址。
- 播放器向本地代理服务器发送请求。
- 本地代理服务器截取这个请求,再根据解析出来请求的信息向真正的服务器发起请求。
- 本地代理服务器开始接受数据,写入文件并将文件数据再返回到播放器。
- 播放器接收到这些数据以后播放。
- seek以后从新进行以上步骤。
上面流程主要描述了代理服务器实现的实时播放流程,下面重点探讨一下代理服务器的下载流程。
考虑到播放视频的时候,用户会拖动进度条进行seek,而此时须要从用户拖动的位置进行下载,这样会让视频文件产生许多的空洞,以下图所示:
fragment = [start,end]; array = [fragment 0,fragment 1,fragment 2,fragment 3]; 复制代码
- 其中
fragment
指的是下载的片断,start
指的是片断开始的位置,end
为片断的结束位置。array
指的是存储fragment
的数组,数组中的fragment
是依靠start
从小到大来来插入到数组中的,保证了数组的有序性。- 下载的片断是记录在一个数组中:
array = [fragment0 ,fragment 1,fragment 2,fragment 3];
下载共分为两个阶段:seek阶段和补洞阶段。
根据seek到的位置分为两种状况:
状况一:若是seek
到的位置是在已有的片断中(例如图中的seek1
的位置,该处有数据),就从该片断(fragment1
)的末尾请求数据(end1
),直到下个片断的开始位置处(fragment2
的start
),也就是向服务器请求的range
为:rang1 = (end1 ) —— start2;
这个片断下载完成后,假如把下载的片断记为fragment1.1
,则会把fragment1
、fragment1.1
、fragment2
合为一个片断为fragment1-2
,则array = [fragment 0,fragement1-2,frament3]
;此次下载后的状态图2所示:
array = [fragment 0,fragement1-3];
以后会判断fragement1-3
有没有到文件末尾,若是到了就下载结束,若是没到就从从fragement3
的(end3
)开始下载直到文件末尾。 状况二:若是seek
到的位置没有在已有的片断中,(例如说是在图1中的seek2
的位置),就从seek
到的位置开始下载数据直到下一个片断的start
(fragment2
的start2
),假如这个片断记为fragment1.1
,则会把fragment1.1
和fragment2
合并即数组为:array= [fragment 0,fragment1,fagment1.1-2,fragment3];
合并后的状况以下图3所示:接下来的操做就是继续下载,直到下载到文件末尾;
fragment1
的大小只有1kb,想要补充fragment0
与fragment1.1-2
之间的数据,就须要发送两次请求,这样频繁的发送请求,比较浪费资源。所以当fragment
过小,就不存在配置数组中。这样会少发一次请求,也不会浪费很大的流量。当下载片断过小(例如说下载的长度<20KB
),就不保存在片断数组中(为了控制片断的粒度)。这样会产生一个问题,当视频文件中间有一个空洞小于20KB,这个片断永远补不上。这个时候就须要用到第二阶段-补洞阶段。 {0,length}
,length
为视频总长度的时候,表示文件已所有下载完成。对于IOS平台来讲,还有一种更好的方案:使用IOS原生API ,使用 AVAssetResourceLoader,在不改变 AVPlayer API 的状况下,对播放的音视频进行缓存。
方案三跟方案二原理差很少,只不过是借助IOS原始API来实现的。
这里的边下边播不是单独开一个子线程去下载,而是把视频播放的数据给保存到本地。简而言之,就是使用一遍的流量,既播放了视频,也保存了视频。
具体实现方案以下:
- 须要在视频播放器和服务器之间添加一层相似代理的机制,视频播放器再也不直接访问服务器,而是访问代理对象,代理对象去访问服务器得到数据,以后返回给视频播放器,同时代理对象根据必定的策略缓存数据。
- AVURLAsset中的resourceLoader能够实现这个机制,resourceLoader的delegate就是上述的代理对象。
- 视频播放器在开始播放以前首先检测是本地cache中是否有此视频,若是没有才经过代理得到数据,若是有,则直接播放本地cache中的视频便可。
- 若是是用HTTP的方式,上述3步能够实现边下边播功能,若是是HTTPS,服务器证书使用的是证书颁发机构签名的证书,则也能够直接跟HTTP方式同样处理。可是,若是是HTTPS+自签名证书的方式,则须要在resourceLoader每次方式请求前,先校验证书,也就是下面的第5步
咱们先来参考网上播放QQ音乐边下边播流程图以下:
先观察并猜想企鹅音乐的缓存策略(固然它不是用AVPlayer播放): 一、开始播放,同时开始下载完整的文件,当文件下载完成时,保存到缓存文件夹中; 二、当seek时 (1)若是seek到已下载到的部分,直接seek成功;(以下载进度60%,seek进度50%) (2)若是seek到未下载到的部分,则开始新的下载(以下载进度60%,seek进度70%) PS1:此时文件下载的范围是70%-100% PS2:以前已下载的部分就被删除了 PS3:若是有别的seek操做则重复步骤2,若是此时再seek到进度40%,则会开始新的下载(范围40%-100%) 三、当开始新的下载以后,因为文件不完整,下载完成以后不会保存到缓存文件夹中; 四、下次再播放同一歌曲时,若是在缓存文件夹中存在,则直接播放缓存文件;
咱们使用AVPlayer 来实现边下边播的大体流程跟上面QQ音乐的缓存机制差很少,就是依赖于AVAssetResourceLoader. 大体流程以下:
如上图所示,咱们简单描述一下AVPlayer实现边下边播的流程:
- 当开始播放视频时,经过视频url判断本地cache中是否已经缓存当前视频,若是有,则直接播放本地cache中视频
- 若是本地cache中没有视频,则视频播放器向代理请求数据
- 加载视频时展现正在加载的提示(菊花转)
- 若是能够正常播放视频,则去掉加载提示,播放视频,若是加载失败,去掉加载提示并显示失败提示
- 在播放过程当中若是因为网络过慢或拖拽缘由致使没有播放数据时,要展现加载提示,跳转到第4步
缓存代理策略:
- 当视频播放器向代理请求dataRequest时,判断代理是否已经向服务器发起了请求,若是没有,则发起下载整个视频文件的请求 2.若是代理已经和服务器创建连接,则判断当前的dataRequest请求的offset是否大于当前已经缓存的文件的offset,若是大于则取消当前与服务器的请求,并从offset开始到文件尾向服务器发起请求(此时应该是因为播放器向后拖拽,而且超过了已缓存的数据时才会出现)
- 若是当前的dataRequest请求的offset小于已经缓存的文件的offset,同时大于代理向服务器请求的range的offset,说明有一部分已经缓存的数据能够传给播放器,则将这部分数据返回给播放器(此时应该是因为播放器向前拖拽,请求的数据已经缓存过才会出现)
- 若是当前的dataRequest请求的offset小于代理向服务器请求的range的offset,则取消当前与服务器的请求,并从offset开始到文件尾向服务器发起请求(此时应该是因为播放器向前拖拽,而且超过了已缓存的数据时才会出现)
- 只要代理从新向服务器发起请求,就会致使缓存的数据不连续,则加载结束后不用将缓存的数据放入本地cache
- 若是代理和服务器的连接超时,重试一次,若是仍是错误则通知播放器网络错误
- 若是服务器返回其余错误,则代理通知播放器网络错误
IOS 播放网络视频咱们通常使用AVFoundation框架里面的AVPlayer去实现自定义播放器,可是AVPlayer的相关API都是高度封装的,这样咱们播放网络视频时,每每不能控制其内部播放逻辑,好比咱们会发现播放时seek会失败,数据加载完毕后不能获取到数据文件进行其余操做,所以咱们须要寻找弥补其不足之处的方法,这里咱们选择了AVAssetResourceLoader。咱们这里实现边下边播功能也是依赖于它。
先来了解一下AVAssetResourceLoader的做用:让咱们自行掌握AVPlayer数据的加载,包括获取AVPlayer须要的数据的信息,以及能够决定传递多少数据给AVPlayer。
咱们大体了解一下AVPlayer的组件图:
AVAssetResourceLoader:一个 iOS 6 就被开放出来,专门用来处理 AVAsset 加载的工具。这个彻底知足JimuPro运行在IOS10以上的要求。
AVAssetResourceLoader 有一个AVAssetResourceLoaderDelegate代理,这个代理有两个重要的接口:
- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest; 复制代码
- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest; 复制代码
咱们只要找一个对象实现了 AVAssetResourceLoaderDelegate 这个协议的方法,丢给 asset,再把 asset 丢给 AVPlayer,AVPlayer 在执行播放的时候就会去问这个 delegate:喂,你能不能播放这个 url 啊?而后会触发下面这个方法:- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest
咱们在这个方法中看看 request 里面的 url 是否是咱们支持的,若是能支持就返回 YES!而后就能够开心的一边下视频数据,一边塞数据给 AVPlayer 让它显示视频画面。
AVUrlAsset
在请求自定义的URLScheme
资源的时候会经过AVAssetResourceLoader
实例来进行资源请求。它是AVUrlAsset
的属性,声明以下:var resourceLoader: AVAssetResourceLoader { get }
而AVAssetResourceLoader
请求的时候会把相关请求(AVAssetResourceLoadingRequest
)传递给AVAssetResourceLoaderDelegate
(若是有实现的话),咱们能够保存这些请求,而后构造本身的NSUrlRequset
来发送请求,当收到响应的时候,把响应的数据设置给AVAssetResourceLoadingRequest
,而且对数据进行缓存,就完成了边下边播,整个流程大致以下图:
AVAssetResourceLoadingDataRequest
,须要控制好
currentOffset
。
下面咱们未来详细的介绍使用AVPlayer和AVAssetResourceLoaderDelegate来实现边下边播的具体实现。
目前网上有好多关于IOS边下边播的代码,其实原理都是同样的,只是实现方式,细节不同,这里推荐两个比较好的开源代码:
边下边播的原理已经在上面的3种方案介绍中详细描述了,这里主要是基于第三种方案用AVPlayer 来实现边下边播。这里先抛开HTTPS字签证书的签名认证问题,先讲解基于HTTP方式的边下边播,主流程图以下:
整个过程就是分为两大块,一块是实时播放视频,一块就是缓存策略下载视频。
咱们先来看第一块,实时播放视频(先无论下载和缓存),实现上,咱们能够分为两步:
在上面的回调方法中,会获得一个 AVAssetResourceLoadingRequest 对象,它里面的属性和方法很少,为了减小干扰,我精简了一下这个类的头文件,只留下咱们会用到以及须要解释的属性和方法:
@interface AVAssetResourceLoadingRequest : NSObject @property (nonatomic, readonly) NSURLRequest *request; @property (nonatomic, readonly, nullable) AVAssetResourceLoadingContentInformationRequest *contentInformationRequest NS_AVAILABLE(10_9, 7_0); @property (nonatomic, readonly, nullable) AVAssetResourceLoadingDataRequest *dataRequest NS_AVAILABLE(10_9, 7_0); - (void)finishLoading NS_AVAILABLE(10_9, 7_0); - (void)finishLoadingWithError:(nullable NSError *)error; @end 复制代码
在 AVAssetResourceLoadingRequest
里面,request
表明原始的请求,因为 AVPlayer
是会触发分片下载的策略,还须要从dataRequest
中获得请求范围的信息。有了请求地址和请求范围,咱们就能够从新建立一个设置了请求 Range
头的 NSURLRequest
对象,让下载器去下载这个文件的 Range
范围内的数据。
当 AVPlayer
触发下载时,老是会先发起一个 Range
为 0-2
的数据请求,这个请求的做用实际上是用来确认视频数据的信息,如文件类型、文件数据长度。当下载器发起这个请求,收到服务端返回的 response
后,咱们要把视频的信息填充到 AVAssetResourceLoadingRequest
的 contentInformationRequest
属性中,告知下载的视频格式以及视频长度。
AVAssetResourceLoadingRequest
在 - (void)finishLoading
的时候,会根据 contentInformationRequest
中的信息,去判断接下去要怎么处理。例如:下载 AVURLAsset
中 URL 指向的文件,获取到的文件的 contentType
是系统不支持的类型,这个 AVURLAsset
将没法正常播放。
获取完视频信息后,会收到刚才指定的 2 Byte
的 data
数据,下载到的数据怎么办? 能够塞给 AVAssetResourceLoadingRequest
里的 dataRequest
。 dataRequest
里面用 - (void)respondWithData:(NSData *)data;
专门用来接收下载的数据,这个方法能够调用屡次,接收增量连续的 data
数据。
当 AVAssetResourceLoadingRequest
要求的全部数据都下载完毕,调用 - (void)finishLoading
完成下载,AVAssetResourceLoader
会继续发起以后的数据片断的请求。若是本次请求失败,能够直接调用 - (void)finishLoadingWithError:(nullable NSError *)error;
结束下载。
在实际的测试中,发现AVAssetResourceLoader
在执行加载的时候,会时不时的触发取消下载调用 - (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest,
而后从新发起加载请求的策略。若是下载了部分,那么从新发起的下载请求会从尚未下载的部分开始。
AVAssetResourceLoaderDelegate
中还有 3 个方法能够针对特殊场景作处理,不过在目前的环境中都用不到因此能够选择不实现这些方法。
经过上面实时播放原理的介绍,咱们已经知道 AVAssetResourceLoaderDelegate
的实现机制,当 AVAsset
须要加载数据时会经过 delegate
告诉外部,外部接管整个视频下载过程。
当咱们接管了视频下载,即可以对视频数据作任何事情。好比:缓存、记录下载速度、得到下载进度等等。
实现一个下载器,就是用 URLSession
开启一个 DataTask
请求数据,把接收到的数据塞给 DataRequest
并写入本地磁盘。在实现下载器时主要有三个注意的点:1. Range 请求 2. 可取消下载 3. 分片缓存
每次获得的 LoadingRequest
带有请求数据范围的信息,好比指望请求第 100 字节到 500 字节,在建立 URLRequest
时须要设置 HTTPHeader
的 Range
值。
NSString *range = [NSString stringWithFormat:@"bytes=%lld-%lld", fromOffset, endOffset]; [request setValue:range forHTTPHeaderField:@"Range"]; 复制代码
引入分块下载最大的复杂点在于对响应数据的contentOffset的处理上,好在AVAssetResourceLoader帮咱们处理了大量工做,咱们只须要用好AVAssetResourceLoadingRequest就能够了。
例如,下面是代码部分,首先是获取原始请求和发送新的请求
func resourceLoader(_ resourceLoader: AVAssetResourceLoader, shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool { if self.session == nil { //构造Session let configuration = URLSessionConfiguration.default configuration.requestCachePolicy = .reloadIgnoringLocalAndRemoteCacheData configuration.networkServiceType = .video configuration.allowsCellularAccess = true self.session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil) } //构造 保存请求 var urlRequst = URLRequest.init(url: self.initalUrl!, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 20) // 20s超时 urlRequst.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type") urlRequst.httpMethod = "GET" //设置请求头 guard let wrappedDataRequest = loadingRequest.dataRequest else{ //本次请求没有数据请求 return true } let range:NSRange = NSMakeRange(Int.init(truncatingBitPattern: wrappedDataRequest.requestedOffset), wrappedDataRequest.requestedLength) let rangeHeaderStr = "byes=\(range.location)-\(range.location+range.length)" urlRequst.setValue(rangeHeaderStr, forHTTPHeaderField: "Range") urlRequst.setValue(self.initalUrl?.host, forHTTPHeaderField: "Referer") guard let task = session?.dataTask(with: urlRequst) else{ fatalError("cant create task for url") } task.resume() self.tasks[task] = loadingRequest return true } 复制代码
收到响应请求后,抓包查看响应的请求头,下图是2个响应的请求头:
start-end/total
,所以就有
Content-Length = end - start + 1
。
AVAsset
在加载视频时,常常会在某次数据请求尚未完成时触发取消下载,而后发起一个新的 LoadingReqeust
。这个机制是 AVAsset
里的黑盒,具体逻辑没法得知,比较像是 AVAsset
的一种重试机制。 做为下载器,在收到取消通知时,须要马上中止下载。因为 DataRequest
的 cancel
操做是异步的,就有可能在 cancel
还未完成时,下一个 LoadingRequest
就已经到来,因此还须要须要保证同一个 URL 只能同时存在一个下载器在下载,不然会出现数据混乱的问题。
若是只是单纯的下载视频,数据单调递增,缓存处理仍是比较容易。然而现实是用户对 player 的 seek
操做给视频的缓存管理带来了巨大的挑战,一旦涉及到用户操做,可能性就越多,复杂度也会越高。
没有 seek
的状况:网速正常时缓存数据比播放时间走得开,正常播放;网速慢时,播放器 loading
,直到有足够的数据量进行播放,若是网速一直很慢就会播几秒卡一下。
当加入 seek
后会有三种可能:
第一种状况,视频彻底下载好,这时 seek 只需读取相应缓存便可,这种状况最简单,就直接从缓存读数据便可。
第二种状况,视频下载一半,用户 seek
到未下载部分,LoadingRequest
请求的部分所有都是未下载的数据。这时须要取消正在下载的数据,而后从 seek
的点开始下载数据。为了支持 seek
操做,下载器就须要支持分片缓存。目前使用的解决方案是下载的视频数据会根据请求的 Range
值,把数据存储到文件中对应的偏移值位置,而且每一个视频文件都会另外再保存一个与之对应的下载信息文件。这个信息文件会记录当前下载了多少数据,总共有多少数据,下载了哪些片断的数据等信息,以后的缓存管理会很是依赖这个配置文件。
第三种状况,视频被 seek
了屡次,用户 seek
到一个时间点,LoadingRequest
请求的部分包含了已下载和未下载的部分。这种状况是最复杂的!简单的作法是,当成上面的状况来处理,所有都从新下载,虽然逻辑简单,但这个方案会下载屡次一样的数据,不是最最优解。个人目标固然是作最优的解决方案,但也是复杂高不少的解决方案。
LoadingRequest
的请求范围后,下载器会先获取已经下载的数据信息,把已下载的分片信息分别建立一个 action
,再把须要远程下载的分片数据分别建立一个 action
。最终组合就多是 LocalAction(50-100 bytes) + RemoteAction(101-200 bytes) + LocalAction(201-300 bytes) + RemoteAction(300-400 bytes)
。每个 action
会按顺序获取数据再返回给 LoadingRequest
。以下图:
在下载视频时,出现错误没法正常下载是比较容易出现的。咱们本身实现了 AVAssetResourceLoaderDelegate 在第一次请求就抛出错误的话,播放器会立刻提示错误状态,而若是是已经响应了部分数据,再抛错误,AVAssetResourceLoader 会忽略错误而一直处于 loading,直到超时。这种状况就比较尴尬,在上面给出的VIMediaCache 实现中, VIResourceLoaderManager 提供了 delegate,若是内部出现错误,就会抛出错误,再又外部业务决定是如何处理。
同一时间同一个 url 不能有屡次下载: 因为缓存内部实现是对每个 url 都共用同一个下载配置文件,若是同时有屡次对同一个 url 进行下载,这个文件下载信息会被同时修改,下载信息会变得混乱。VIMediaCache 里的 MediaCache 内部作了简单的处理,若是正在下载某 url,这时再想尝试下载一样的 url 会直接抛出错误,提示没法开始下载。
实际上VGPlayer只是参考VIMediaCache 方式的Swift版本实现,VIMediaCache 是真的大牛编写的OC版本,值得好好研究。
鉴于咱们JimuPro工程师纯swift项目,里面处了第三方库没有使用OC代码,因此我优先选择VGPlayer来实现机器人端到IOS app端的边下边播功能。
因为VGPlayer没有实现HTTPS的证书验证,这里我只须要简单实现证书验证代码便可。咱们将在下面讲解HTTPS的证书认证明现。这里我简单说一下个人实现, 在VGPlayerDownloadURLSessionManager.swift文件的VGPlayerDownloadURLSessionManager类里面增长一个URLSession的一个代理实现:
即便你参考上面的源码实现了边下载边播放,仍是有些细节地方须要注意的: 例如要实现mp4文件的边下边播功能,不只依赖于上面讲解的边下边播实现方案,还依赖于mp4的文件格式。若是遇到这种mp4文件的元数据放在文件末尾的,咱们须要在服务器端将mp4文件作一下转换才能够实现边下边播功能。
接下来详细讲解一下mp4格式处理问题。
咱们要明确一点就是即便你用上面的缓存方式实现了边下边播的功能,并非全部mp4都支持的,这个须要你理解边下边播的原理。
mp4视频文件头中,包含一些元数据。元数据包含:视频的宽度高度、视频时长、编码格式等。mp4元数据一般在视频文件的头部,这样播放器在读取文件时会最早读取视频的元数据,而后开始播放视频。
固然也存在这样一种状况:mp4视频的元数据处于视频文件最后,这样播放器在加载视频文件时,一直读取到最后,才读取到视频信息,而后开始播放。若是缺乏元数据,也是这样的状况。这就出现了mp4视频不支持边加载、边播放的问题。
在请求头里有一个Range:byte字段来告诉媒体服务器须要请求的是哪一段特定长度的文件内容,对于MP4文件来讲,全部数据都封装在一个个的box或者atom中,其中有两个atom尤其重要,分别是moov atom和mdat atom。
moov atom
:包含媒体的元数据的数据结构,包括媒体的块(box)信息,格式说明等等。mdat atom
: 包含媒体的媒体信息,对于视屏来讲就是视频画面了。在IOS中发送一个请求,利用NSUrlSession直接请求视频资源,针对元信息在视频文件头部的视频能够实现边下边播,而元信息在视频尾部的视频则会下载完才播放,为啥会这样呢?
答案就是:虽然moov和mdat都只有一个,可是因为MP4文件是由若干个这样的box或者atom组成的,所以这两个atom在不一样媒体文件中出现的顺序可能会不同,为了加快流媒体的播放,咱们能够作的优化之一就是手动把moov提到mdat以前。 对于AVPlayer来讲,只有到AVPlayerItemStatusReadyToPlay状态时,才能够开始播放视频,而进入AVPlayerItemStatusReadyToPlay状态的必要条件就是播放器读到了媒体的moov块。
若是mdat位于moov以后,那么这样的mp4视频文件是没法实现边下边播放的。要支持边下边播的mp4视频须要知足moov和mdat都位于文件头部,且moov位于mdat以前。以下图所示:
那么,若是遇到这种mp4文件的元数据放在文件末尾的,咱们须要在服务器端将mp4文件作一下转换才能够实现边下边播功能。
可行的方法是使用的是qt-faststart
工具。 qt-faststart
可以将处于MP4文件末尾的moov atom
元数据转移到最前面,不过因为qt-faststart工具只能处理moov atom
元数据位于MP4末尾的文件。 若是咱们想要将全部文件统一处理:总体思路是将MP4文件经过ffmpeg处理,将moov atom
元数据转移至末尾,而后使用qt-faststart
工具转移至最前面。
tar -jxvf ffmpeg-3.3.3.tar.bz2
./configure --enable-shared --prefix=/usr/local/ffmpeg
prefix就是设置安装位置,通常都默认usr/local下。make
make install
复制代码
编译安装时间会很长,10分钟左右吧,装完之后能够去安装目录下查看。 这时尚未结束,如今使用的话通常会报以下错误:
ffmpeg: error while loading shared libraries: libavfilter.so.1: cannot open shared object file: No such file or directory
复制代码
/etc/ld.so.conf
文件加入以下内容:/usr/local/lib
,保存退出后执行ldconfig
命令。echo "/usr/local/ffmpeg/lib" >> /etc/ld.so.conf #注意这里是你前面安装ffmpeg的路径 ldconfig 复制代码
qt-faststart
工具其实就在ffmpeg的源码中有,由于在ffmpeg解压完的文件中存在qt-faststart的源码,因此直接使用,位置在解压路径/tools/qt-faststart.c
若是你想单独下载点击这里: qt-faststart下载 6. 进入ffmpeg解压路径执行命令:make tools/qt-faststart
,会看到在tools中会出现一个qt-faststart文件(还有一个.c文件) 7. ffmpeg
将元数据转移至文件末尾:
cd ffmpeg安装路径/bin;./ffmpeg -i /opt/mp4test.mp4 -acodec copy -vcodec copy /opt/1.mp4 # /opt/mp4test.mp4为原始MP4文件路径,/opt/1.mp4为生成文件的存放路径 复制代码
qt-faststart
将元数据转移到文件开头:cd ffmpeg压缩包解压路径/tools;
./qt-faststart /opt/1.mp4 /opt/2.mp4
复制代码
以下图:
过程详解: ![]()
- ①客户端的浏览器向服务器发送请求,并传送客户端SSL 协议的版本号,加密算法的种类,产生的随机数,以及其余服务器和客户端之间通信所须要的各类信息。
- ②服务器向客户端传送SSL 协议的版本号,加密算法的种类,随机数以及其余相关信息,同时服务器还将向客户端传送本身的证书。
- ③客户端利用服务器传过来的信息验证服务器的合法性,服务器的合法性包括:证书是否过时,发行服务器证书的CA 是否可靠,发行者证书的公钥可否正确解开服务器证书的“发行者的数字签名”,服务器证书上的域名是否和服务器的实际域名相匹配。若是合法性验证没有经过,通信将断开;若是合法性验证经过,将继续进行第四步。
- ④用户端随机产生一个用于通信的“对称密码”,而后用服务器的公钥(服务器的公钥从步骤②中的服务器的证书中得到)对其加密,而后将加密后的“预主密码”传给服务器。
- ⑤若是服务器要求客户的身份认证(在握手过程当中为可选),用户能够创建一个随机数而后对其进行数据签名,将这个含有签名的随机数和客户本身的证书以及加密过的“预主密码”一块儿传给服务器。
- ⑥若是服务器要求客户的身份认证,服务器必须检验客户证书和签名随机数的合法性,具体的合法性验证过程包括:客户的证书使用日期是否有效,为客户提供证书的CA 是否可靠,发行CA 的公钥可否正确解开客户证书的发行CA 的数字签名,检查客户的证书是否在证书废止列表(CRL)中。检验若是没有经过,通信马上中断;若是验证经过,服务器将用本身的私钥解开加密的“预主密码”,而后执行一系列步骤来产生主通信密码(客户端也将经过一样的方法产生相同的主通信密码)。
- ⑦服务器和客户端用相同的主密码即“通话密码”,一个对称密钥用于SSL 协议的安全数据通信的加解密通信。同时在SSL 通信过程当中还要完成数据通信的完整性,防止数据通信中的任何变化。
- ⑧客户端向服务器端发出信息,指明后面的数据通信将使用的步骤. ⑦中的主密码为对称密钥,同时通知服务器客户端的握手过程结束。
- ⑨服务器向客户端发出信息,指明后面的数据通信将使用的步骤⑦中的主密码为对称密钥,同时通知客户端服务器端的握手过程结束。
- ⑩SSL 的握手部分结束,SSL 安全通道的数据通信开始,客户和服务器开始使用相同的对称密钥进行数据通信,同时进行通信完整性的检验。
public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { let method = challenge.protectionSpace.authenticationMethod if method == NSURLAuthenticationMethodServerTrust { //验证服务器,直接信任或者验证证书二选一,推荐验证证书,更安全 completionHandler( HTTPSManager.trustServerWithCer(challenge: challenge).0, HTTPSManager.trustServerWithCer(challenge: challenge).1) } else if method == NSURLAuthenticationMethodClientCertificate { //认证客户端证书 completionHandler( HTTPSManager.sendClientCer().0, HTTPSManager.sendClientCer().1) } else { //其余状况,不经过验证 completionHandler(.cancelAuthenticationChallenge, nil) } } 复制代码
// // HTTPSManager.swift // JimuPro // // Created by yulu kong on 2019/10/28. // Copyright © 2019 UBTech. All rights reserved. // import UIKit class HTTPSManager: NSObject { // // MARK: - sll证书处理 // static func setKingfisherHTTPS() { // //取出downloader单例 // let downloader = KingfisherManager.shared.downloader // //信任Server的ip // downloader.trustedHosts = Set([ServerTrustHost.fileTransportIP]) // } // // static func setAlamofireHttps() { // // SessionManager.default.delegate.sessionDidReceiveChallenge = { (session: URLSession, challenge: URLAuthenticationChallenge) in // // let method = challenge.protectionSpace.authenticationMethod // if method == NSURLAuthenticationMethodServerTrust { // //验证服务器,直接信任或者验证证书二选一,推荐验证证书,更安全 // return HTTPSManager.trustServerWithCer(challenge: challenge) //// return HTTPSManager.trustServer(challenge: challenge) // // } else if method == NSURLAuthenticationMethodClientCertificate { // //认证客户端证书 // return HTTPSManager.sendClientCer() // // } else { // //其余状况,不经过验证 // return (.cancelAuthenticationChallenge, nil) // } // } // } //不作任何验证,直接信任服务器 static private func trustServer(challenge: URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?) { let disposition = URLSession.AuthChallengeDisposition.useCredential let credential = URLCredential.init(trust: challenge.protectionSpace.serverTrust!) return (disposition, credential) } //验证服务器证书 static func trustServerWithCer(challenge: URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?) { var disposition: URLSession.AuthChallengeDisposition = .performDefaultHandling var credential: URLCredential? //获取服务器发送过来的证书 let serverTrust:SecTrust = challenge.protectionSpace.serverTrust! let certificate = SecTrustGetCertificateAtIndex(serverTrust, 0)! let remoteCertificateData = CFBridgingRetain(SecCertificateCopyData(certificate))! //加载本地CA证书 // let cerPath = Bundle.main.path(forResource: "oooo", ofType: "cer")! // let cerUrl = URL(fileURLWithPath:cerPath) let cerUrl = Bundle.main.url(forResource: "server", withExtension: "cer")! let localCertificateData = try! Data(contentsOf: cerUrl) if (remoteCertificateData.isEqual(localCertificateData) == true) { //服务器证书验证经过 disposition = URLSession.AuthChallengeDisposition.useCredential credential = URLCredential(trust: serverTrust) } else { //服务器证书验证失败 //disposition = URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge disposition = URLSession.AuthChallengeDisposition.useCredential credential = URLCredential(trust: serverTrust) } return (disposition, credential) } //发送客户端证书交由服务器验证 static func sendClientCer() -> (URLSession.AuthChallengeDisposition, URLCredential?) { let disposition = URLSession.AuthChallengeDisposition.useCredential var credential: URLCredential? //获取项目中P12证书文件的路径 let path: String = Bundle.main.path(forResource: "clientp12", ofType: "p12")! let PKCS12Data = NSData(contentsOfFile:path)! let key : NSString = kSecImportExportPassphrase as NSString let options : NSDictionary = [key : "123456"] //客户端证书密码 var items: CFArray? let error = SecPKCS12Import(PKCS12Data, options, &items) if error == errSecSuccess { let itemArr = items! as Array let item = itemArr.first! let identityPointer = item["identity"]; let secIdentityRef = identityPointer as! SecIdentity let chainPointer = item["chain"] let chainRef = chainPointer as? [Any] credential = URLCredential.init(identity: secIdentityRef, certificates: chainRef, persistence: URLCredential.Persistence.forSession) } return (disposition, credential) } } 复制代码
- MP4是一套用于音频、视频信息的压缩编码标准,由国际标准化组织(ISO)和国际电工委员会(IEC)下属的“动态图像专家组”(Moving Picture Experts Group,即MPEG)制定,初版在1998年10月经过,第二版在1999年12月经过。MPEG-4格式的主要用途在于网上流、光盘、语音发送(视频电话),以及电视广播。
- MPEG-4包含了MPEG-1及MPEG-2的绝大部份功能及其余格式的长处,并加入及扩充对虚拟现实模型语言(VRML , VirtualReality Modeling Language)的支持,面向对象的合成档案(包括音效,视讯及VRML对象),以及数字版权管理(DRM)及其余互动功能。而MPEG-4比MPEG-2更先进的其中一个特色,就是再也不使用宏区块作影像分析,而是以影像上个体为变化记录,所以尽管影像变化速度很快、码率不足时,也不会出现方块画面。
MP4标准 MPEG-4码流主要包括基本码流和系统流,基本码流包括音视频和场景描述的编码流表示,每一个基本码流只包含一种数据类型,并经过各自的解码器解码。系统流则指定了根据编码视听信息和相关场景描述信息产生交互方式的方法,并描述其交互通讯系统。
MP4也能够理解成一种视频的封装格式 视频封装格式,简称视频格式,至关于一种储存视频信息的容器,它里面包含了封装视频文件所须要的视频信息、音频信息和相关的配置信息(好比:视频和音频的关联信息、如何解码等等)。一种视频封装格式的直接反映就是对应着相应的视频文件格式。
常见的封装格式有以下:
封装格式:就是将已经编码压缩好的视频数据 和音频数据按照必定的格式放到一个文件中.这个文件能够称为容器. 固然能够理解为这只是一个外壳.
一般咱们不只仅只存放音频数据和视频数据,还会存放 一下视频同步的元数据.例如字幕.这多种数据会不一样的程序来处理,可是它们在传输和存储的时候,这多种数据都是被绑定在一块儿的.
- AVI: 是当时为对抗quicktime格式(mov)而推出的,只能支持固定CBR恒定定比特率编码的声音文件
- MOV:是Quicktime封装
- WMV:微软推出的,做为市场竞争
- mkv:万能封装器,有良好的兼容和跨平台性、纠错性,可带外挂字幕
- flv: 这种封装方式能够很好的保护原始地址,不容易被下载到,目前一些视频分享网站都采用这种封装方式
- MP4:主要应用于mpeg4的封装,主要在手机上使用。
视频编解码的过程是指对数字视频进行压缩或解压缩的一个过程. 在作视频编解码时,须要考虑如下这些因素的平衡:视频的质量、用来表示视频所须要的数据量(一般称之为码率)、编码算法和解码算法的复杂度、针对数据丢失和错误的鲁棒性(Robustness)、编辑的方便性、随机访问、编码算法设计的完美性、端到端的延时以及其它一些因素。
- H.261,主要用于老的视频会议和视频电话系统。是第一个使用的数字视频压缩标准。实质上说,以后的全部的标准视频编解码器都是基于它设计的。
- H.262,等同于 MPEG-2 第二部分,使用在 DVD、SVCD 和大多数数字视频广播系统和有线分布系统中。
- H.263,主要用于视频会议、视频电话和网络视频相关产品。在对逐行扫描的视频源进行压缩的方面,H.263 比它以前的视频编码标准在性能上有了较大的提高。尤为是在低码率端,它能够在保证必定质量的前提下大大的节约码率。
- H.264,等同于 MPEG-4 第十部分,也被称为高级视频编码(Advanced Video Coding,简称 AVC),是一种视频压缩标准,一种被普遍使用的高精度视频的录制、压缩和发布格式。该标准引入了一系列新的可以大大提升压缩性能的技术,并可以同时在高码率端和低码率端大大超越之前的诸标准。
- H.265,被称为高效率视频编码(High Efficiency Video Coding,简称 HEVC)是一种视频压缩标准,是 H.264 的继任者。HEVC 被认为不只提高图像质量,同时也能达到 H.264 两倍的压缩率(等同于一样画面质量下比特率减小了 50%),可支持 4K 分辨率甚至到超高画质电视,最高分辨率可达到 8192×4320(8K 分辨率),这是目前发展的趋势。
- MPEG-1 第二部分,主要使用在 VCD 上,有些在线视频也使用这种格式。该编解码器的质量大体上和原有的 VHS 录像带至关。
- MPEG-2 第二部分,等同于 H.262,使用在 DVD、SVCD 和大多数数字视频广播系统和有线分布系统中。
- MPEG-4 第二部分,可使用在网络传输、广播和媒体存储上。比起 MPEG-2 第二部分和初版的 H.263,它的压缩性能有所提升。
- MPEG-4 第十部分,等同于 H.264,是这两个编码组织合做诞生的标准。
能够把「视频封装格式」看作是一个装着视频、音频、「视频编解码方式」等信息的容器。一种「视频封装格式」能够支持多种「视频编解码方式」,好比:QuickTime File Format(.MOV) 支持几乎全部的「视频编解码方式」,MPEG(.MP4) 也支持至关广的「视频编解码方式」。当咱们看到一个视频文件名为 test.mov 时,咱们能够知道它的「视频文件格式」是 .mov,也能够知道它的视频封装格式是 QuickTime File Format,
可是没法知道它的「视频编解码方式」。那比较专业的说法多是以 A/B 这种方式,A 是「视频编解码方式」,B 是「视频封装格式」。好比:一个 H.264/MOV 的视频文件,它的封装方式就是 QuickTime File Format,编码方式是 H.264
在这里机器人里面录制视频时采用H.264/mp4,因此这里我这边实现的边下边播方案里面也是针对的这种H.264视频编解码方式的mp4容器格式的视频文件。
H264最大的优点,具备很高的数据压缩比率,在同等图像质量下,H264的压缩比是MPEG-2的2倍以上,MPEG-4的1.5~2倍.
原始文件的大小若是为88GB,采用MPEG-2压缩标准压缩后变成3.5GB,压缩比为25∶1,而采用H.264压缩标准压缩后变为879MB,从88GB到879MB,H.264的压缩比达到惊人的102∶1
为了更好的理解播放视频的原理,我这里还简单介绍一下H264编解码的相关知识
H264的码流结构:H264视频压缩后会成为一个序列帧.帧里包含图像,图像分为不少片.每一个片能够分为宏块.每一个宏块由许多子块组成,以下图:
场和帧:视频的一场或一帧可用来产生一个编码图像。在电视中,为减小大面积闪烁现象,把一帧分红两个隔行的场。
片:每一个图象中,若干宏块被排列成片的形式。片分为I片、B片、P片和其余一些片。
- I片只包含I宏块,P片可包含P和I宏块,而B片可包含B和I宏块。
- I宏块利用从当前片中已解码的像素做为参考进行帧内预测。
- P宏块利用前面已编码图象做为参考图象进行帧内预测。
- B宏块则利用双向的参考图象(前一帧和后一帧)进行帧内预测。
- 片的目的是为了限制误码的扩散和传输,使编码片相互间是独立的。
A Annex格式数据,就是起始码+Nal Unit 数据 NAL Unit: NALU 头+NALU数据 NALU 主体,是由切片组成.切片包括切片头+切片数据 Slice数据: 宏块组成 PCM类: 宏块类型+pcm数据,或者宏块类型+宏块模式+残差数据 Residual: 残差块.
NAL 单元是由一个NALU头部+一个切片.切片又能够细分红"切片头+切片数据".咱们之间了解过一个H254的帧是由多个切片构成的.由于一帧数据一次有可能传不完. 以下图:
切片与宏块的关系(Slice & MacroBlock) 每一个切片都包括切片头+切片数据. 那每一个切片数据包括了不少宏块.每一个宏块包括了宏块的类型,宏块的预测,残差数据. 以下图:
而咱们在一副压缩的H264的帧里,能够包含多个切片.至少有一个切片,以下图:
了解了上面关于H264码流的一些基本概念后,咱们就能更好的理解H264编码解码的原理,以及图像渲染,视频播放器的实现原理。
在H264解码的过程当中会涉及到一帧帧的数据,这里有I帧,P帧,B帧,三个概念。
举个例子,若是摄像头对着你拍摄,1秒以内,实际你发生的变化是很是少的.1秒钟以内实际少不多有大幅度的变化.摄像机通常一秒钟会抓取几十帧的数据.好比像动画,就是25帧/s,通常视频文件都是在30帧/s左右.对于一些要求比较高的,对动做的精细度有要求,想要捕捉到完整的动做的,高级的摄像机通常是60帧/s.那些对于一组帧的它的变化很小.为了便于压缩数据,那怎么办了?将第一帧完整的保存下来.若是没有这个关键帧后面解码数据,是完成不了的.因此I帧特别关键.
视频的第一帧会被做为关键帧完整保存下来.然后面的帧会向前依赖.也就是第二帧依赖于第一个帧.后面全部的帧只存储于前一帧的差别.这样就能将数据大大的减小.从而达到一个高压缩率的效果.
- B帧,即参考前一帧,也参考后一帧.这样就使得它的压缩率更高.存储的数据量更小.若是B帧的数量越多,你的压缩率就越高.这是B帧的优势,可是B帧最大的缺点是,若是是实时互动的直播,那时与B帧就要参考后面的帧才能解码,那在网络中就要等待后面的帧传输过来.这就与网络有关了.若是网络状态很好的话,解码会比较快,若是网络很差时解码会稍微慢一些.丢包时还须要重传.对实时互动的直播,通常不会使用B帧.
咱们实时播放视频时,每次从服务器请求一个Range范围的视频帧,实际上服务器是返回一组组的H264帧数据,一组帧数据又称为GOF(Group of Frame),GOF 表示:一个I帧到下一个I帧.这一组的数据.包括B帧/P帧. 以下图所示:
在H264码流中,咱们使用SPS/PPS来存储GOP的参数。
SPS 序列参数集 :全称是Sequence Parameter Set,序列参数集存放帧数,参考帧数目,解码图像尺寸,帧场编码模式选择标识等.
PPS 图像参数集:全称是Picture Parameter Set,图像参数集.存放编码模式选择标识,片组数目,初始量化参数和去方块滤波系数调整标识等.(与图像相关的信息)
在一组帧以前咱们首先收到的是SPS/PPS数据.若是没有这组参数的话,咱们是没法解码. 以前WebRTC视频的时候遇到的一个问题就是:IOS端有时候图传的时候黑屏,这个缘由就是由于I帧缺乏SPS/PPS信息,致使解码失败,致使的黑屏。
若是咱们在解码时发生错误,首先要检查是否有SPS/PPS.若是没有,是由于对端没有发送过来仍是由于对端在发送过程当中丢失了. SPS/PPS
数据,咱们也把其归类到I帧.这2组数据是绝对不能丢的.
视频花屏,卡顿的缘由分析: 咱们在观看视频时,会遇到花屏或者卡顿现象.那这个与咱们刚刚所讲的GOF就息息相关了
- 若是GOP分组中的P帧丢失就会形成解码端的图像发生错误.解码错误时,咱们把解码失败的图片用来展现了,就致使咱们看到的花屏现象
- 为了不花屏问题的发生,通常若是发现P帧或者I帧丢失.就不显示本GOP内的全部帧.只到下一个I帧来后从新刷新图像.
- 当这时由于没有刷新屏幕.丢包的这一组帧所有扔掉了.图像就会卡在哪里不动.这就是卡顿的缘由.
- 因此总结起来,花屏是由于你丢了P帧或者I帧.致使解码错误. 而卡顿是由于为了怕花屏,将整组错误的GOP数据扔掉了.直达下一组正确的GOP再从新刷屏.而这中间的时间差,就是咱们所感觉的卡顿.
- 性能高,低码率下一般质量低于硬编码器,但部分产品在GPU硬件平台移植了优秀的软编码算法(如X264)的,质量基本等同于软编码。
- 硬编码,就是使用GPU计算,获取数据结果,优势速度快,效率高.
- 在IOS平台针对视频硬编码使用
VideoToolBox
框架,针对音频硬编码使用AudioToolBox
框架
- 实现直接、简单,参数调整方便,升级易,但CPU负载重,性能较硬编码低,低码率下质量一般比硬编码要好一点。
- 软编码,就是经过CPU来计算,获取数据结果.
- 在IOS平台针对视频软编码通常使用
FFmpeg,X264
算法把视频原数据YUV/RGB编码成H264。针对音频使用fdk_aac
将音频数据PCM转换成AAC。
若是想更加深刻的探索播放器的底层原理,能够参考这两款开源的播放器: ijkplayer,kxmovie 他们都是基于FFmpeg框架封装的
MP4(MPEG-4 Part 14)
是一种常见的多媒体容器格式,它是在“ISO/IEC 14496-14”标准文件中定义的,属于MPEG-4的一部分,是“ISO/IEC 14496-12(MPEG-4 Part 12 ISO base media file format)”标准中所定义的媒体格式的一种实现,后者定义了一种通用的媒体文件结构标准。MP4是一种描述较为全面的容器格式,被认为能够在其中嵌入任何形式的数据,各类编码的视频、音频等都不在话下,不过咱们常见的大部分的MP4文件存放的AVC(H.264)
或MPEG-4(Part 2)编码的视频和AAC编码的音频。MP4格式的官方文件后缀名是“.mp4”,还有其余的以mp4为基础进行的扩展或者是缩水版本的格式,包括:M4V,3GP,F4V等。
首先看一下软件对于mp4文件的解析以下图所示:
box
,分别为:
ftype,moov,free,mdat
。其实
mp4
文件是有许多的
box
组成的。以下图6.3.2 所示:
box
的基本结构以下图6.3.3所示,其中,size
指明了整个box所占用的大小,包括header
部分,type
指明了box
的类型。若是box
很大(例如存放具体视频数据的mdat box
),超过了uint32的最大数值,size
就被设置为1,并用接下来的8位uint64来存放大小。
一个mp4
文件有可能包含很是多的box
,在很大程度上增长了解析的复杂性,这个网页上http://mp4ra.org/atoms.html记录了一些当前注册过的box
类型。看到这么多box
,若是要所有支持,一个个解析,怕是头都要爆了。还好,大部分mp4文件没有那么多的box类型,下图就是一个简化了的,常见的mp4文件结构以下图6.3.4所示
stbl box
下属的几个
box
中的,须要解析
stbl
下面全部的
box
,来还原媒体信息。下表是对于以上几个重要的
box
存放信息的说明:
上面已经讲解过使用FFmpeg里面的 qt-faststart下载工具能够实现将mp4文件的 moov的box移到前面,从而让mp4文件支持边下边播功能。下面将介绍一种经过IOS原始代码的方式实现将mp4文件的moov的box从文件最后面移到前面。
不过这种方式通常用不到,一是由于效率问题,而是通常实现边下边播,都是由服务器端去完成这种事情。
具体代码以下:
- (NSData*)exchangestco:(NSMutableData*) moovdata{ int i, atom_size, offset_count, current_offset; NSString*atom_type; longlongmoov_atom_size = moovdata.length; Byte*buffer = (Byte*)malloc(5); buffer[4] =0; Byte*buffer01 = (Byte*)malloc(moov_atom_size); [moovdatagetBytes:buffer01 length:moov_atom_size]; for(i =4; i < moov_atom_size -4; i++) { NSRangerange; range.location= I; range.length=4; [moovdatagetBytes:buffer range:range]; atom_type = [selftosType:buffer]; if([atom_typeisEqualToString:@"stco"]) { range.location= i-4; range.length =4; [moovdatagetBytes:bufferrange:range]; atom_size = [selftoSize:buffer]; if(i + atom_size -4> moov_atom_size) { WBLog(LOG_ERROR,@"error i + atom_size - 4 > moov_atom_size"); returnnil; } range.location= I+8; range.length=4; [moovdatagetBytes:bufferrange:range]; offset_count = [selftoSize:buffer]; for(intj =0; j < offset_count; j++) { range.location= i +12+ j *4; range.length=4; [moovdatagetBytes:bufferrange:range]; current_offset= [selftoSize:buffer]; current_offset += moov_atom_size; buffer01[i +12+ j *4+0] = (Byte) ((current_offset >>24) &0xFF); buffer01[i +12+ j *4+1] = (Byte) ((current_offset >>16) &0xFF); buffer01[i +12+ j *4+2] = (Byte) ((current_offset >>8) &0xFF); buffer01[i +12+ j *4+3] = (Byte) ((current_offset >>0) &0xFF); } i += atom_size -4; } elseif([atom_typeisEqualToString:@"co64"]) { range.location= i-4; range.length=4; [moovdatagetBytes:bufferrange:range]; atom_size = [selftoSize:buffer]; if(i + atom_size -4> moov_atom_size) { WBLog(LOG_ERROR,@"error i + atom_size - 4 > moov_atom_size"); returnnil; } range.location= I+8; range.length=4; [moovdatagetBytes:bufferrange:range]; offset_count = [selftoSize:buffer]; for(intj =0; j < offset_count; j++) { range.location= i +12+ j *8; range.length=4; [moovdatagetBytes:bufferrange:range]; current_offset = [selftoSize:buffer]; current_offset += moov_atom_size; buffer01[i +12+ j *8+0] = (Byte)((current_offset >>56) &0xFF); buffer01[i +12+ j *8+1] = (Byte)((current_offset >>48) &0xFF); buffer01[i +12+ j *8+2] = (Byte)((current_offset >>40) &0xFF); buffer01[i +12+ j *8+3] = (Byte)((current_offset >>32) &0xFF); buffer01[i +12+ j *8+4] = (Byte)((current_offset >>24) &0xFF); buffer01[i +12+ j *8+5] = (Byte)((current_offset >>16) &0xFF); buffer01[i +12+ j *8+6] = (Byte)((current_offset >>8) &0xFF); buffer01[i +12+ j *8+7] = (Byte)((current_offset >>0) &0xFF); } i += atom_size -4; } } NSData*moov = [NSDatadataWithBytes:buffer01length:moov_atom_size]; free(buffer); free(buffer01); returnmoov; } 复制代码
参考:www.jianshu.com/p/0188ab038… www.jianshu.com/p/bb925a4a9… www.cnblogs.com/ios4app/p/6… www.jianshu.com/p/990ee3db0…