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

现在,大多数已工作的前端工作者的学习方式,要么直接到 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-TimeMessagingProtocol。它是基于 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 全称就是 MediaSourceExtensions。它是一套处理视频流技术的简称,里面包括了一系列 API: MediaSourceSourceBuffer 等。在没有 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。以前那种通过 newBuffer 的方法官方已经不推荐使用了,具体原因可以 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): 又叫做 Predictedpicture--前向预测帧。即,他会根据前面一张图像,来进行图片间的动态压缩,它的压缩率和 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 直播的大致基本点差不多就介绍完了。后面如果还有机会,我们可以来进行一下 音视频解码的实战操练。

原文发布于微信公众号 - 前端小吉米(villainThr)

原文发表时间:2017-09-28

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏我和未来有约会

Silverlight第三方控件专题

这里我收集整理了目前网上silverlight第三方控件的专题,若果有所遗漏请告知我一下。 名称 简介 截图 telerik 商 RadC...

3985
来自专栏跟着阿笨一起玩NET

c#实现打印功能

2702
来自专栏ASP.NETCore

ASP.NET Core 整合Autofac和Castle实现自动AOP拦截

除了ASP.NETCore自带的IOC容器外,我们还可以使用其他成熟的DI框架,如Autofac,StructureMap等(笔者只用过Unity,Ninjec...

674
来自专栏落花落雨不落叶

canvas画简单电路图

60811
来自专栏飞扬的花生

jsencrypt参数前端加密c#解密

      写程序时一般是通过form表单或者ajax方式将参数提交到服务器进行验证,如何防止提交的请求不被抓包后串改,虽然无法说绝对安全却给非法提交提高了难度...

3859
来自专栏转载gongluck的CSDN博客

cocos2dx 打灰机

#include "GamePlane.h" #include "PlaneSprite.h" #include "BulletNode.h" #include...

5406
来自专栏张善友的专栏

LINQ via C# 系列文章

LINQ via C# Recently I am giving a series of talk on LINQ. the name “LINQ via C...

2625
来自专栏张善友的专栏

Miguel de Icaza 细说 Mix 07大会上的Silverlight和DLR

Mono之父Miguel de Icaza 详细报道微软Mix 07大会上的Silverlight和DLR ,上面还谈到了Mono and Silverligh...

2707
来自专栏pangguoming

Spring Boot集成JasperReports生成PDF文档

由于工作需要,要实现后端根据模板动态填充数据生成PDF文档,通过技术选型,使用Ireport5.6来设计模板,结合JasperReports5.6工具库来调用渲...

1.2K7
来自专栏一个爱瞎折腾的程序猿

sqlserver使用存储过程跟踪SQL

USE [master] GO /****** Object: StoredProcedure [dbo].[sp_perfworkload_trace_s...

2040

扫码关注云+社区