不再碎片化学习,快速掌握 H5 直播技术

2,723 阅读23分钟
原文链接: segmentfault.com

现在,大多数已工作的前端工作者的学习方式,要么直接到 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 SourceSource 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 提供两个最基本的操作 appendBufferremove。之后,我们就可以通过 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 内存大小。此时,它里面只是空的内存,这时候你需要借用其他两个对象 TypedArrayDataView 来帮助你完成写入和修改的操作。不过,AB 提供了一个非常重要的方法:slice()

slice() 和 Array 对象上的 slice 方法一样也是将数组中的一部分新创建一个副本返回。这个方法为啥有用呢?

因为,通过 TypedArrayDataView 创建的对象底层的 AB 都是不能改变的,所以,如果你想对一个 Buffer 进行不同的操作,比如,对 AB 的 4-8B 全部置 0,并且后面又置为 1。如果你想保留两者的话,就需要手动创建一个副本才行,这就需要用到 slice 方法了。

AB 具体的属性和方法我这里就不多说了,有兴趣的同学可以参考 MDN ArrayBuffer

接下来,我们就来看一下和 AB 实际对接最紧密的两个对象 TypedArrayDataView

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 值,用来表示写入字节序的方式,默认是使用大字节序。参考

It is big-endian by default and can be set to little-endian in the getter/setter methods.

所以,如果你想使用小字节序的话,则需要手动传入 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

他可以直接通过 allocfrom 方法来直接创建指定的大小的 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 音频大小大很多,这就是压缩格式起的主要作用。具体流程图如下:

首先,由原始数码设备提供相关的数字信号流,然后经由视频压缩算法,大幅度的减少流的大小,然后交给视频盒子,打上相应的 dtspts 字段,最终生成可用的视频文件。常用的视频格式和压缩格式如下:

视频格式主要是参考 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 直播的大致基本点差不多就介绍完了。后面如果还有机会,我们可以来进行一下 音视频解码的实战操练。