技术名词
- H.264:国际标准化组织(ISO)和国际电信联盟(ITU)共同提出的继MPEG4之后的新一代数字视频压缩格式
- FMP4:Fragmented mp4,是基于 MPEG-4 Part 12 的流媒体格式,通常我们使用的mp4文件是嵌套结构的,客户端必须要从头加载一个 MP4 文件,才能够完整播放,不能从中间一段开始播放。而Fragmented MP4(简称fmp4),就如它的名字碎片mp4,是由一系列的片段组成,这些片段可以独立的进行请求到客户端进行播放,而不需要加载整个文件。
- MediaSource :video缺少了诸如视频分段加载、视频码率切换、部分加载等等现代播放器应该有的功能,所以绝大部分的浏览器视频播放器过去都是基于 Flash 开发的。MediaSource 允许JavaScript 生成媒体流。这可以用于自适应流及随时间变化的视频直播流(live streaming)等应用场景。
关键技术详解
Blob和ArrayBuffer
在我们的项目中,如果查看video标签的src属性,会发现值是这样的:
所以先了解一下blob是个什么东西:
- Blob最早是数据库直接用来存储二进制数据对象,这样就不用关注存储数据的格式了。在web领域,Blob对象表示一个只读原始数据的类文件对象,File 接口基于Blob,继承了 blob 的功能并将其扩展使其支持用户系统上的文件,因此可以像操作文件对象一样操作Blob对象。
- ArrayBuffer对象表示原始的二进制数据缓冲区,即在内存中分配指定大小的二进制缓冲区(容器),用于存储各种类型化数组的数据,是最基础的原始数据容器,无法直接读取或写入, 需要通过具体视图来读取或写入,即TypedArray对象或DataView对象对内存大小进行读取或写入
- Blob与ArrayBuffer的区别是,除了原始字节以外它还提供了mime type作为元数据
- Blob和ArrayBuffer可以互相转换
URL.createObjectURL
video标签,audio标签还是img标签的src属性,不管是相对路径,绝对路径,或者一个网络地址,归根结底都是指向一个文件资源的地址。既然我们知道了Blob其实是一个可以当作文件用的二进制数据,那么只要我们可以生成一个指向Blob的地址,是不是就可以用在这些标签的src属性上,答案肯定是可以的,这里我们要用到的就是URL.createObjectURL()。
举个例子🌰
// 在bigfish或者umi中
request('maosong.mp4', {
// "blob" Blob对象
// "arraybuffer" ArrayBuffer对象
responseType: 'blob',
}).then(res => {
const src = URL.createObjectURL(res);
video.src = src;
})
MediaSource
video标签src指向一个视频地址,视频播完了再将src修改为下一段的视频地址然后播放,这显然不符合我们无缝播放的要求。我们可能会想到一个思路,用Blob URL指向一个视频二进制数据,然后不断将下一段视频的二进制数据添加拼接进去。这样就可以在不影响播放的情况下,不断的更新视频内容并播放下去。这就是MediaSource所要做的事情:
const mediaSource = new MediaSource();
const video = document.querySelector('video');
video.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', sourceOpen);
function sourceOpen () {
//视频格式和编码信息,主要为判断浏览器是否支持视频格式,但如果信息和视频不符可能会报错
var mime = 'video/mp4; codecs="avc1.42E01E,mp4a.40.2"'
// 新建一个 sourceBuffer
var sourceBuffer = mediaSource.addSourceBuffer(mime);
// 加载一段视频流,然后append到sourceBuffer中
request('maosong.mp4', {
// "blob" Blob对象
// "arraybuffer" ArrayBuffer对象
responseType: 'arraybuffer',
}).then(res => {
sourceBuffer.appendBuffer(res)
})
}
编码
- 编码原因:视频数据原始体积是巨大的,以720P 30fps的视频为例,一个像素大约3个字节,如下所得,每秒钟产生79MB(1280 * 720 * 30 * 3 / 1024 / 1024),显然是过大的。因此我们需要压缩编码
- 编码种类
-
软件编码:实现直接、简单,参数调整方便,升级易,但CPU负载重,性能较硬编码低,低码率下质量通常比硬编码要好一点。
-
硬硬件编码:性能高,低码率下通常质量低于硬编码器,但部分产品在GPU硬件平台移植了优秀的软编码算法(如X264)的,质量基本等同于软编码。
-
- 编码原理
对视频执行编码操作后,原始视频数据会被压缩成三种不同类型的视频帧: I帧,P帧,B帧.
- I帧:关键帧.完整编码的帧.可以理解成是一张完整画面,不依赖其他帧
- P帧:参考前面的I帧或P帧,即通过前面的I帧与自己记录的不同的部分可以形成完整的画面.因此,单独的P帧无法形成画面.
- B帧:参考前面的I帧或P帧以及后面的P帧
- 核心算法:
- 帧内压缩:
当压缩一帧图像时,仅考虑本帧的数据而不考虑相邻帧之间的冗余信息,这实际上与静态图像压缩类似 帧内一般采用有损压缩算法,由于帧内压缩是编码一个完整的图像,所以可以独立的解码、显示。帧内压缩一般达不到很高的压缩,跟编码jpeg差不多。 如下图:我们可以通过第前面的编码来推测和计算第 6 块的编码,因此就不需要对第 6 块进行编码
- 帧间压缩
相邻几帧的数据有很大的相关性,或者说前后两帧信息变化很小的特点。也即连续的视频其相邻帧之间具有冗余信息,根据这一特性,压缩相邻帧之间的冗余量就可以进一步提高压缩量,减小压缩比。 如下图:可以看到前后两帧的差异其实是很小的,这时候用帧间压缩就很有意义。
- DTS和PTS
- DTS:主要用于视频的解码,在解码阶段使用.
- PTS:主要用于视频的同步和输出.在渲染的时候使用.在没有B frame的情况下.DTS和PTS的输出顺序是一样的。

编码数据流
- IDR:一个序列的第一个图像叫做 IDR 图像(立即刷新图像),IDR 图像都是 I 帧图像。引入 IDR 图像是为了解码的重同步,当解码器解码到 IDR 图像时,立即将参考帧队列清空,将已解码的数据全部输出或抛弃,重新查找参数集,开始一个新的序列。这样,如果前一个序列出现重大错误,在这里可以获得重新同步的机会。IDR图像之后的图像永远不会使用IDR之前的图像的数据来解码。
- 宏块(Macroblock)宏块是视频信息的主要承载者,因为它包含着每一个像素的亮度和色度信息。
- 片(slice):片的主要作用是用作宏块的载体。设置片的目的是为了限制误码的扩散和传输,编码片是项目独立的,一个片的预测不能以其他片中的宏块为参考图像。保证了某一片的预测误差不会传播到别的片。 我们可以理解为一张图片可以包含一个或多个片,而每一个片包含整数个宏块,即每片至少一个宏块,最多时每片包整个图像的宏块。
- 编码数据组成:由一个个的 NALU 组成,而它的功能分为两层,VCL(视频编码层)和 NAL(网络提取层).
- VCL:核心压缩引擎,设计目标是尽可能地独立于网络进行高效的编码。
- NAL:网络抽象层(Network Abstraction Layer,简称NAL)。在以太网每个包大小是 1500 字节,而一帧往往会大于这个值,所以就需要用于按照一定格式,对 VCL 视像编码层输出的数据拆成多个包传输,并提供包头(header)等信息,以在不同速率的网络上传输或进行存储,所有的拆包和组包都是 NAL 层去处理的。
- VCL 数据传输或者存储之前,会被映射到一个 NALU 中,H264 数据包含一个个 NALU。如下图:

- PPS:图像参数集(Picture Parameter Set),包含一幅图像所用的公共参数,即一幅图像中所有片段SS(Slice Segment)引用同一个PPS。
- SPS: 序列参数集(Sequence Parameter Sets),存储的是一个序列的信息,包括有多少帧等。
H.264码流
一个原始的H.264 NALU 单元常由 [StartCode] [NALU Header] [NALU Payload] 三部分组成

-
StartCode : Start Code 用于标示这是一个NALU 单元的开始,必须是”00 00 00 01” 或”00 00 01”
-
NALU Header 下表为 NAL Header Type

举个例子🌰
下面幅图分别代表IDR与非IDR帧具体的码流信息:
在一个NALU中,第一个字节(即NALU header)用以表示其包含数据的类型及其他信息。我们假定一个头信息字节为0x67作为例子:
| 十六进制 | 二进制 |
|---|---|
| 0x27 | 0 01 00111 |
如表所示,头字节可以被解析成3个部分,其中:
- forbidden_zero_bit = 0:占1个bit,禁止位,用以检查传输过程中是否发生错误,0表示正常,1表示违反语法;
- nal_ref_idc = 1:占2个bit,用来表示当前NAL单元的优先级。非0值表示参考字段/帧/图片数据,其他不那么重要的数据则为0。对于非0值,值越大表示NALU重要性越高
- nal_unit_type = 7:最后5位用以指定NALU类型,NALU类型定义如上表 在解码过程中,我们只需要取出NALU头字节的后5位,即将NALU头字节和0x1F进行与计算即可得知NALU类型,即:
NALU类型 = NALU头字节 & 0x1F

