现在,大多数已工作的前端工作者的学习方式,要么直接到 Stackoverflow
上搜代码,要么直接看看相关博文。这样是快,但是零零碎碎只是一个一个孤立的知识点而已。有可能一下午都忘记了,唯一可能记住的收藏一下那个文章,然后就彻底躺尸了。那有没有啥更好的办法能解决呢?
当然有,第一,有时间,第二,有人指导,第三,找对资料。
这其实和看书是一样的,一本书,最有价值的地方不在它的内容或者作者,而在于它的目录
,是否真正的打动你。如果只是出现一些模糊而没有落地技术的目录的书籍,还是别再上面浪费时间了。
所以,本文主要给大家介绍一下当下 HTML5 直播所涵盖的技术范围,如果要深度学习每一个技术,我们后续可以继续讨论。
直播协议
直播是 16 年搭着短视频热火起来的。它的业务场景有很多,有游戏主播,才艺主播,网上教学,群体实验(前段时间,有人直播让观众来炒股)等等。不过,根据技术需求的划分,还可以分为低延迟和高延迟的直播,这里就主要是协议选择的问题。
现在,常用的直播协议有很多种,比如 RTMP,HLS,HTTP-FLV。不过,最常用的还是 HLS 协议,因为支持度高,技术简单,但是延迟非常严重。这对一些对实时性比较高的场景,比如运动赛事直播来说非常蛋疼。这里,我们来细分的看一下每个协议。
HLS
HLS 全称是 HTTP Live Streaming。这是 Apple 提出的直播流协议。(其实,Adobe 公司 FLV 播放器的没落,苹果也是幕后黑手之一。)
HLS 由两部分构成,一个是 .m3u8
文件,一个是 .ts
视频文件(TS 是视频文件格式的一种)。整个过程是,浏览器会首先去请求 .m3u8
的索引文件,然后解析 m3u8
,找出对应的 .ts
文件链接,并开始下载。更加详细的说明可以参考这幅图:
他的使用方式为:
<video controls autoplay>
<source src="http://devimages.apple.com/iphone/samples/bipbop/masterplaylist.m3u8" type="application/vnd.apple.mpegurl" />
<p class="warning">Your browser does not support HTML5 video.</p>
</video>
直接可以将 m3u8
写进 src
中,然后交由浏览器自己去解析。当然,我们也可以采取 fetch
来手动解析并获取相关文件。HLS 详细版的内容比上面的简版多了一个 playlist
,也可以叫做 master
。在 master
中,会根据网络段实现设置好不同的 m3u8 文件,比如,3G/4G/wifi 网速等。比如,一个 master 文件中为:
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2855600,CODECS="avc1.4d001f,mp4a.40.2",RESOLUTION=960x540
live/medium.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=5605600,CODECS="avc1.640028,mp4a.40.2",RESOLUTION=1280x720
live/high.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1755600,CODECS="avc1.42001f,mp4a.40.2",RESOLUTION=640x360
live/low.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=545600,CODECS="avc1.42001e,mp4a.40.2",RESOLUTION=416x234
live/cellular.m3u8
大家只要关注 BANDWIDTH
(带宽)字段,其他的看一下字段内容大致就清楚了。假如这里选择 high.m3u8
文件,那么,里面内容为:
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:26
#EXTINF:9.901,
http://media.example.com/wifi/segment26.ts
#EXTINF:9.901,
http://media.example.com/wifi/segment27.ts
#EXTINF:9.501,
http://media.example.com/wifi/segment28.ts
注意,其中以 ts
结尾的链接就是我们在直播中真正需要播放的视频文件。该第二级的 m3u8
文件也可以叫做 media
文件。该文件,其实有三种类型:
live playlist: 动态列表。顾名思义,该列表是动态变化的,里面的 ts 文件会实时更新,并且过期的 ts 索引会被删除。默认,情况下都是使用动态列表。
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:26
#EXTINF:9.901,
http://media.example.com/wifi/segment26.ts
#EXTINF:9.901,
http://media.example.com/wifi/segment27.ts
#EXTINF:9.501,
http://media.example.com/wifi/segment28.ts
event playlist: 静态列表。它和动态列表主要区别就是,原来的 ts 文件索引不会被删除,该列表是不断更新,而且文件大小会逐渐增大。它会在文件中,直接添加 #EXT-X-PLAYLIST-TYPE:EVENT 作为标识。
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:EVENT
#EXTINF:9.9001,
http://media.example.com/wifi/segment0.ts
#EXTINF:9.9001,
http://media.example.com/wifi/segment1.ts
#EXTINF:9.9001,
http://media.example.com/wifi/segment2.ts
VOD playlist: 全量列表。它就是将所有的 ts 文件都列在 list 当中。如果,使用该列表,就和播放一整个视频没有啥区别了。它是使用 #EXT-X-ENDLIST 表示文件结尾。
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:9.9001,
http://media.example.com/wifi/segment0.ts
#EXTINF:9.9001,
http://media.example.com/wifi/segment1.ts
#EXTINF:9.9001,
http://media.example.com/wifi/segment2.ts
#EXT-X-ENDLIST
里面相关字段解释可以参考: Apple HLS
HLS 缺陷
HLS 啥都好,就是延迟性太大了,估计苹果一开始设计的时候,并不在乎它的延时性。HLS 中的延时包括:
TCP 握手
m3u8 文件下载
m3u8 文件下所有 ts 文件下载
这里,我们先假设每个 ts 文件播放时长为 5s,每个 m3u8 最多可携带的 ts 文件数为 3~8。那么最大的延迟则为 40s。注意,只有当一个 m3u8
文件下所有的 ts 文件下载完后,才能开始播放。这里还不包括 TCP 握手,DNS 解析,m3u8 文件下载。所以,HLS 总的延时是非常令人绝望的。那解决办法有吗? 有,很简单,要么减少每个 ts 文件播放时长,要么减少 m3u8
的中包含 ts 的数量。如果超过平衡点,那么每次请求新的 m3u8 文件时,都会加上一定的延时,所以,这里需要根据业务指定合适的策略。当然,现在由于 mediaSource
的普及,自定义一个播放器也没有多大的难度,这样就可以保证直播延迟性的同时,完成直播的顺利进行。
RTMP
RTMP 全称为:Real-Time Messaging Protocol
。它是基于 FLV
格式进行开发的,所以,第一反应就是,卧槽,又不能用了!!!
是的,在现在设备中,由于 FLV 的不支持,基本上 RTMP 协议在 Web 中,根本用不到。不过,由于 MSE
(MediaSource Extensions)的出现,在 Web 上直接接入 RTMP 也不是不可能的。基本思路是根据 WebSocket 直接建立长连接进行数据的交流和监听。这里,我们就先不细说了。我们主要目的是讲概念,讲框架。RTMP 协议根据不同的套层,也可以分为:
纯 RTMP: 直接通过 TCP 连接,端口为 1935
RTMPS: RTMP + TLS/SSL,用于安全性的交流。
RTMPE: RTMP + encryption。在 RTMP 原始协议上使用,Adobe 自身的加密方法
RTMPT: RTMP + HTTP。使用 HTTP 的方式来包裹 RTMP 流,这样能直接通过防火墙。不过,延迟性比较大。
RTMFP: RMPT + UDP。该协议常常用于 P2P 的场景中,针对延时有变态的要求。
RTMP 内部是借由 TCP 长连接协议传输相关数据,所以,它的延时性非常低。并且,该协议灵活性非常好(所以,也很复杂),它可以根据 message stream ID 传输数据,也可以根据 chunk stream ID 传递数据。两者都可以起到流的划分作用。流的内容也主要分为:视频,音频,相关协议包等。
详细传输过程如图:
如果后期要使用到 RTMP 协议,可以直接参考
HTTP-FLV
该协议和 RTMP 比起来其实差别不大,只是落地部分有些不同:
RTMP 是直接将流的传输架在 RTMP 协议之上,而 HTTP-FLV 是在 RTMP 和客户端之间套了一层转码的过程,即:
由于,每个 FLV 文件是通过 HTTP 的方式获取的,所以,它通过抓包得出的协议头需要使用 chunked
编码。
Content-Type:video/x-flv
Expires:Fri, 10 Feb 2017 05:24:03 GMT
Pragma:no-cache
Transfer-Encoding:chunked
它用起来比较方便,不过后端实现的难度和直接使用 RTMP 来说还是比较大的。
上面简单介绍了一下三种协议,具体选择哪种协议,还是需要和具体的业务进行强相关,否则的话吃亏的还是自己(自己挖坑)。。。
这里,简单的做个对比
协议对比
HLS | 支持性广 | 延时巨高 | 10s 以上 |
RTMP | 延时性好,灵活 | 量大的话,负载较高 | 1s 以上 |
HTTP-FLV | 延时性好,游戏直播常用 | 只能在手机 APP 播放 | 2s 以上 |
前端音视频流
由于各大浏览器的对 FLV 的围追堵截,导致 FLV 在浏览器的生存状况堪忧,但是,FLV 凭借其格式简单,处理效率高的特点,使各大视频后台的开发者都舍不得启用,如果一旦更改的话,就需要对现有视频进行转码,比如变为 MP4,这样不仅在播放,而且在流处理来说都有点重的让人无法接受。而 MSE 的出现,彻底解决了这个尴尬点,能够让前端能够自定义来实现一个 Web 播放器,确实完美。(不过,苹果老大爷觉得没这必要,所以,在 IOS 上无法实现。)
MSE
MSE 全称就是 Media Source Extensions
。它是一套处理视频流技术的简称,里面包括了一系列 API:Media Source
,Source Buffer
等。在没有 MSE 出现之前,前端对 video 的操作,仅仅局限在对视频文件的操作,而并不能对视频流做任何相关的操作。现在 MSE 提供了一系列的接口,使开发者可以直接提供 media stream。
我们来看一下 MSE 是如何完成基本流的处理的。
var vidElement = document.querySelector('video');
if (window.MediaSource) {
var mediaSource = new MediaSource();
vidElement.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', sourceOpen);
} else {
console.log("The Media Source Extensions API is not supported.")
}
function sourceOpen(e) {
URL.revokeObjectURL(vidElement.src);
var mime = 'video/webm; codecs="opus, vp9"';
var mediaSource = e.target;
var sourceBuffer = mediaSource.addSourceBuffer(mime);
var videoUrl = 'droid.webm';
fetch(videoUrl)
.then(function(response) {
return response.arrayBuffer();
})
.then(function(arrayBuffer) {
sourceBuffer.addEventListener('updateend', function(e) {
if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
mediaSource.endOfStream();
}
});
sourceBuffer.appendBuffer(arrayBuffer);
});
}
上面的代码完成了相关的获取流和处理流的两个部分。其中,主要利用的是 MS 和 Source Buffer 来完成的。接下来,我们来具体涉及一下详细内容:
MediaSource
MS(MediaSource) 只是一系列视频流的管理工具,它可以将音视频流完整的暴露给 Web 开发者来进行相关的操作和处理。所以,它本身不会造成过度的复杂性。
MS 整个只挂载了 4 个属性,3 个方法和 1 个静态测试方法。有:
4 个属性:
sourceBuffers: 获得当前创建出来的 SourceBuffer
activeSourceBuffers: 获得当前正处于激活状态的 SourceBuffer
readyState: 返回当前 MS 的状态,比如:
closed
,open
,ended
.duration: 设置当前 MS 的播放时长。
3 个方法:
addSourceBuffer(): 根据给定的 MIME 创建指定类型的 SourceBuffer
removeSourceBuffer(): 将 MS 上指定的 SourceBuffer 移除。
endOfStream(): 直接终止该流
1 个静态测试方法:
isTypeSupported(): 主要用来判断指定的音频的 MIME 是否支持。
最基本的就是使用 addSourceBuffer
该方法来获得指定的 SourceBuffer。
var sourceBuffer = mediaSource.addSourceBuffer('video/mp4; codecs="avc1.42E01E, mp4a.40.2"');
Source Buffer
一旦利用 MS 创建好 SourceBuffer 之后,后续的工作就是将额外获得的流放进 Buffer 里面进行播放即可。所以,SourceBuffer 提供两个最基本的操作 appendBuffer
,remove
。之后,我们就可以通过 appendBuffer
直接将 ArrayBuffer 放进去即可。
其中,SourceBuffer 还提供了一个应急的方法 abort()
如果该流发生问题的话可以直接将指定的流给废弃掉。
所以,整个流程图为:
音视频的 ArrayBuffer 通过 MediaSource 和 SourceBuffer 的处理直接将 <audio>
&& <video>
接入。然后,就可以实现正常播放的效果。
当然,上面介绍的仅仅只是一些概念,如果要实际进行编码的话,还得继续深入下去学习。有兴趣的同学,可以继续深入了解,我的另外一篇博客:全面进阶 H5 直播。
当然,如果后期有机会,可以继续来实现以下如何进行实际的编码。本文,主要是给大家介绍直播所需的必要技术和知识点,只有完备之后,我们才能没有障碍的完成实际编码的介绍。
流的处理
上面我们已经讲解了在直播中,我们怎样通过 MSE 接触到实际播放的流,那么,接下来,我们就要开始脚踏实地的了解具体的流的操作处理。因为,视频流格式解协议中,最常涉及的就是拼包,修改字段,切包等操作。
在正式介绍之前,我们需要先了解一下关于流的一些具体概念:
二进制
二进制没啥说的就是 比特流。但是,在 Web 中,有几个简写的进制方式:二进制,八进制,十六进制。
二进制(binary):使用 0b 字面上表示二进制。每一位代表 1bit(2^1)
八进制(octet): 使用 0o 字面上表示八进制。每一位代表 3bit(2^3)
十六进制(hexadecimal): 使用 0x 字面上表示十六进制。每一位代表 4bit(2^4)
上面说的每一位代表着,实际简写的位数,比如 0xff31; 这个就代表 2B 的长度。
位运算
位运算在处理流操作中,是非常重要的,不过由于前端 Buffer 提供的操作集合不多,所以,有些轮子我们还得需要自己构造一下。
这里,我就不细致介绍,在 Web 中常用的位运算符有:
&
|
~
^
<<
>>
>>>
详细介绍可以参考 Web 位运算。
整个优先级为:
~ >> << >>> & ^ |
字节序
字节序说白了就是 bit 放置的顺序,因为历史遗漏原因,字节的放置有两种顺序:
大字节序(BigEndian): 将数据从大到小放置,认为第一个字节是最高位(正常思维)。
小字节序(LittleEndian):将数据从小到达防止,认为第一个字节是最低位。
这个概念在我们后面写入过程中,经常用到。当然,我们如何了解到某台电脑使用的是大字节还是小字节呢?(其实大部分都是小字节)。可以使用 IIFE 进行简单的判断:
const LE = (function () {
let buf = new ArrayBuffer(2);
(new DataView(buf)).setInt16(0, 256, true); // little-endian write
return (new Int16Array(buf))[0] === 256; // platform-spec read, if equal then LE
})();
而在前端,我们归根结底的就是操作 ArrayBuffer
。它也是我们直接和 Buffer 交流的通道。
ArrayBuffer
AB(ArrayBuffer) 不是像 NodeJS 的 Buffer 对象一样是一个纯粹的集合流处理的工具。它只是一个流的容器,这也是底层 V8 实现的内容。基本用法就是给实例化一个固定的内存区:
new ArrayBuffer(length)
创建指定的 length Byte 内存大小。此时,它里面只是空的内存,这时候你需要借用其他两个对象 TypedArray
和 DataView
来帮助你完成写入和修改的操作。不过,AB 提供了一个非常重要的方法:slice()
slice()
和 Array 对象上的 slice 方法一样也是将数组中的一部分新创建一个副本返回。这个方法为啥有用呢?
因为,通过 TypedArray
和 DataView
创建的对象底层的 AB 都是不能改变的,所以,如果你想对一个 Buffer 进行不同的操作,比如,对 AB 的 4-8B 全部置 0,并且后面又置为 1。如果你想保留两者的话,就需要手动创建一个副本才行,这就需要用到 slice
方法了。
AB 具体的属性和方法我这里就不多说了,有兴趣的同学可以参考 MDN ArrayBuffer
接下来,我们就来看一下和 AB 实际对接最紧密的两个对象 TypedArray
和 DataView
。
TypedArray
TA(TypedArray) 是一套 ArrayBuffer 处理的集合。怎么说呢?它里面还可以细分为
Int8Array();
Uint8Array();
Uint8ClampedArray();
Int16Array();
Uint16Array();
Int32Array();
Uint32Array();
Float32Array();
Float64Array();
为什么会有这么多呢?
因为 TA 是将 Buffer 根据指定长度分隔为指定大小的数组。比如:
var buf = new Uint8Array(arrayBuffer);
buf[0]
buf[1]
...
像这样具体通过 index
来获取指定的 Buffer 中的比特值。比如像上面 Uint8Array
每一位就是 1B。
出来分隔的长度不同,剩下的内容,基本上就可以用 TA 来进行整体概括。实际上,大家也可以把它理解为 Array 即可。为啥呢?你可以看一下它有哪些方法后,就彻底明白了:
reverse()
set()
slice()
some()
sort()
subarray()
...
不过,由于兼容性的原因,对于某些方法来说,我们需要加上相关的 polyfill 才行。不过,这也不影响我们的研究性学习,并且,因为 MSE 是针对现代手机浏览器开发的,所以,我们在做 Web 播放器的时候,也并不需要过度关注浏览器的兼容。
TypedArray 最常用的操作方式,是直接根据 index 进行相关的写入操作:
buf[0] = fmt << 6 | 1;
buf[1] = chunkID % 256 - 64;
buf[2] = Math.floor(chunkID / 256);
需要注意在 TypedArray 中的字节序,是根据平台默认的字节序来读取 Buffer 的,比如 UintArray32()
。不过,大部分平台默认都是 little-endian
来进行读取。
DataView
DV(DataView) 和 TypedArray 很类似,也是用来修改底层的 Buffer 的。说白了,它俩就是 NodeJS Buffer 的两部分,可能由于某些原因,将两者给分开创建。DataView 提供的 API 很简单,就是一些 get/set 之类的方法。基本用法为:
new DataView(Arraybuffer [, byteOffset [, byteLength]])
注意,前面那个参数只能是 ArrayBuffer ,你不能把 TypedArray 也给我算进去,不然的话...你可以试试。
同样需要提醒的是 DataView 的修改是和对象一样的,是进行引用类型的修改,即,如果对一个 Buffer 创建多个 DataView 那么,多次修改只会在一个 Buffer 显现出来。
DV 最大的用处就可可以很方便的写入不同字节序的值,这相比于使用 TypedArray 来做 swap()
(交换) 是很方便的事。当然,字节序相关也只能是大于 8 bit 以上的操作才有效。
这里以 setUInt32
为例子,其基本格式为:
setInt32(byteOffset, value [, littleEndian])
其中,littleEndian 是 boolean 值,用来表示写入字节序的方式,默认是使用大字节序。参考:
所以,如果你想使用小字节序的话,则需要手动传入 true
才行!
比如:
let view = new DataView(buffer);
view.setUint32(0, arr[0] || 1, BE);
// 使用 TypedArray 手动构造 swap
buf = new Uint8Array(11);
buf[3] = byteLength >>> 16 & 0xFF;
buf[4] = byteLength >>> 8 & 0xFF;
buf[5] = byteLength & 0xFF;
当然,如果你觉得不放心,可以直接使用,一个 IIFE 进行相关判断:
const LE = (function () {
let buf = new ArrayBuffer(2);
(new DataView(buf)).setInt16(0, 256, true); // little-endian write
return (new Int16Array(buf))[0] === 256; // platform-spec read, if equal then LE
})();
上面是前端 Buffer 的部分,为了让大家更好的了解到 JS 开发工作者从前端到后端操作 Buffer 的区别,这里一并提一下在 NodeJS 中如何处理 Buffer。
Node Buffer
Node Buffer 实际上才是前端最好用的 Buffer 操作,因为它是整合的 ArrayBuffer , TypedArray ,Dataview 一起的一个集合,该对象上挂载了所有处理的方式。详情可以参考一下:Node Buffer。
他可以直接通过 alloc
和 from
方法来直接创建指定的大小的 Buffer。以前那种通过 new Buffer
的方法官方已经不推荐使用了,具体原因可以 stackoverflow 搜一搜,这里我就不多说了。这里想特别提醒的是,NodeJS 已经可以和前端的 ArrayBuffer 直接转换了。通过 from
方法,可以直接将 ArrayBuffer 转换为 NodeJS 的 Buffer。
格式为:
Buffer.from(arrayBuffer[, byteOffset[, length]])
参考 NodeJS 提供的 demo:
const arr = new Uint16Array(2);
arr[0] = 5000;
arr[1] = 4000;
// 共享 arr 的缓存
const buf = Buffer.from(arr.buffer);
// 打印结果: <Buffer 88 13 a0 0f>
console.log(buf);
// 直接改变原始的 Buffer 值
arr[1] = 6000;
// 打印: <Buffer 88 13 70 17>
console.log(buf);
在 Node Buffer 对象上,还挂载了比如:
buf.readInt16BE(offset[, noAssert])
buf.readInt16LE(offset[, noAssert])
buf.readInt32BE(offset[, noAssert])
buf.readInt32LE(offset[, noAssert])
有点不同的是,它是直接根据名字的不同而决定使用哪种字节序。
BE 代表 BigEndian
LE 代表 LittleEndian
之后,我们就可以使用指定的方法进行写入和读取操作。
const buf = Buffer.from([0, 5]);
// Prints: 5
console.log(buf.readInt16BE());
// Prints: 1280
console.log(buf.readInt16LE());
在实际使用中,我们一般对照着 Node 官方文档使用即可,里面文档很详尽。
音视频基本概念
为了大家能够在学习中减少一定的不适感,这里先给大家介绍一下音视频的基本概念,以防止以后别人在吹逼,你可以在旁边微微一笑。首先,基本的就是视频格式和视频压缩格式。
视频格式应该不用多说,就是我们通常所说的 .mp4
,.flv
,.ogv
,.webm
等。简单来说,它其实就是一个盒子,用来将实际的视频流以一定的顺序放入,确保播放的有序和完整性。
视频压缩格式和视频格式具体的区别就是,它是将原始的视频码流变为可用的数字编码。因为,原始的视频流非常大,打个比方就是,你直接使用手机录音,你会发现你几分钟的音频会比市面上出现的 MP3 音频大小大很多,这就是压缩格式起的主要作用。具体流程图如下:
首先,由原始数码设备提供相关的数字信号流,然后经由视频压缩算法,大幅度的减少流的大小,然后交给视频盒子,打上相应的 dts
,pts
字段,最终生成可用的视频文件。常用的视频格式和压缩格式如下:
视频格式主要是参考 ISO 提供的格式文件进行学习,然后参照进行编解码即可。
这里,主要想介绍一下压缩算法,因为这个在你实际解码用理解相关概念很重要。
首先来看一下,什么叫做视频编码。
视频编码
视频实际上就是一帧一帧的图片,拼接起来进行播放而已。而图片本身也可以进行相关的压缩,比如去除重复像素,合并像素块等等。不过,还有另外一种压缩方法就是,运动估计和运动补偿压缩,因为相邻图片一定会有一大块是相似的,所以,为了解决这个问题,可以在不同图片之间进行去重。
所以,总的来说,常用的编码方式分为三种:
变换编码:消除图像的帧内冗余
运动估计和运动补偿:消除帧间冗余
熵编码:提高压缩效率
变换编码
这里就涉及到图像学里面的两个概念:空域和频域。空域就是我们物理的图片,频域就是将物理图片根据其颜色值等映射为数字大小。而变换编码的目的是利用频域实现去相关和能量集中。常用的正交变换有离散傅里叶变换,离散余弦变换等等。
熵编码
熵编码主要是针对码节长度优化实现的。原理是针对信源中出现概率大的符号赋予短码,对于概率小的符号赋予长码,然后总的来说实现平均码长的最小值。编码方式(可变字长编码)有:霍夫曼编码、算术编码、游程编码等。
运动估计和运动补偿
上面那两种办法主要是为了解决图像内的关联性。另外,视频压缩还存在时间上的关联性。例如,针对一些视频变化,背景图不变而只是图片中部分物体的移动,针对这种方式,可以只对相邻视频帧中变化的部分进行编码。
接下来,再来进行说明一下,运动估计和运动补偿压缩相关的,I,B,P 帧。
I,B,P 帧
I,B,P 实际上是从运动补偿中引出来的,这里为了后面的方便先介绍一下。
I 帧(I-frame): 学名叫做:
Intra-coded picture
。也可以叫做独立帧。该帧是编码器随机挑选的参考图像,换句话说,一个 I 帧本身就是一个静态图像。它是作为 B,P 帧的参考点。对于它的压缩,只能使用熵
和变化编码
这两种方式进行帧内压缩。所以,它的运动学补偿基本没有。P 帧(P‑frame): 又叫做
Predicted picture
--前向预测帧。即,他会根据前面一张图像,来进行图片间的动态压缩,它的压缩率和 I 帧比起来要高一些。B 帧(B‑frame): 又叫做
Bi-predictive picture
-- 双向预测。它比 P 帧来说,还多了后一张图像的预测,所以它的压缩率更高。
可以参考一下雷博士的图:
不过,这样理解 OK,如果一旦涉及实际编码的话,那么就不是那么一回事了。设想一下,视频中的 IBP 三帧,很有可能会遇到 I B B P
的情形。这样其实也还好,不过在对数据解码的时候,就会遇到一个问题,B 帧是相对于前后两帧的,但是只有前一帧是固定帧,后面又相对于前面,即,B 帧只能相对于 I/P。但是,这时候 P 帧又还没有被解析。所以,为了解决这个问题,在解码的时候,就需要将他们换一个位置,即 I P B B。这样就可以保证解码的正确性。
那怎么进行保证呢?这就需要 DTS 和 PTS 来完成。这两个是我们在进行视频帧编解码中最终要的两个属性(当然还有一个 CTS)。
解释一下:
pts(presentation time stamps):显示时间戳,显示器从接受到解码到显示的时间。
dts(decoder timestamps): 解码时间戳。也表示该 sample 在整个流中的顺序
所以视频帧的顺序简单的来表示一下就是:
PTS: 1 4 2 3
DTS: 1 2 3 4
Stream: I P B B
可以看到,我们使用 DTS 来解码,PTS 来进行播放。OK,关于 Web 直播的大致基本点差不多就介绍完了。后面如果还有机会,我们可以来进行一下 音视频解码的实战操练。