H264 - 码流结构详解

无论是解析视频文件或者通过网络传输,其实都是一串字节序列。H264 码流就是按照一定的规则组织排列的字节串。

一、码流的组织形式

在 H264 中完全没有 I 帧、P 帧、B 帧、IDR 帧的概念,之所以沿用这些说法是为了表明数据的编码模式。H264 码流的组织形式从大到小排序是:视频序列(video sequence)、图像(frame/field-picture)、片组(slice group)、片(slice)、宏块(macroblock)、子块(sub-block)、像素(pixel)。
H264\_Struct\_A.png

二、码流功能的角度

从码流功能的角度可以分为两层:视频编码层(VCL)和网络提取层(NAL)

  • VCL:进行视频编解码,包括预测(帧内预测和帧间预测),DCT 变化和量化,熵编码和切分数据等功能,是为了实现更高的视频压缩比。
  • NAL:负责以网络所要求的恰当的方式对 VCL 数据进行打包和传送。

VCL 是管理 H264 的视频数据层,是为了实现更高的视频压缩比,那 VCL 究竟是怎么管理 H264 视频数据的呢?抛开 H264 压缩算法细节来看就 3 步:

  1. 压缩:预测(帧内预测和帧间预测)-> DCT 变化和量化 -> 比特流编码;
  2. 切分数据,主要为了第三步。这里一点,网上看到的“切片(slice)”、“宏块(macroblock)”是在VCL 中的概念,一方面提高编码效率和降低误码率、另一方面提高网络传输的灵活性。
  3. 压缩切分后的 VCL 数据会包装成为 NAL 中的一部分。

下面要重点讲解下 NAL。

三、网络提取层(NAL)

NAL,英文全称为 Network Abstraction Layer,这块和 H264 压缩算法无关,涉设计出 NAL 的目的就是为了获得 “network-friendly”,即为了实现良好的网络亲和性,即可适用于各种传输网络。

终于要讲 NAL 了,但是,我们需要先看 NAL的组成单元 - NALU

3.1 NAL单元 - NALU

NALU 的格式如下图(引用H264 PDF)所示:

FFmpeg\_H264\_NALU.png

很明显,NALU 由身体两个部分组成:

  • 头:一般存储标志信息,譬如 NALU 的类型。前面讲过 NAL 会打包 VCL 数据,但是这并不意味着所有的 NALU 负载的都是 VCL,也有一些 NALU 仅仅存储了和编解码信息相关的数据;
  • 身体:存储了真正的数据。但实际上,这块也会相对比较复杂,前面其实也提到过 H264 的一个目的是“网络友好性”,说白了就是能够很好地适配各种传输格式。所以根据实际采用数据传输流格式,也会对这部分数据格式再进行处理。

3.2 NALU Header

首先,NALU Header只占 1 个字节,即 8 位,其组成如下图所示:

H264\_Struct\_B.jpg

  • forbidden_zero_bit

    在网络传输中发生错误时,会被置为 1,告诉接收方丢掉该单元;否则为 0。

  • nal_ref_idc
    用于表示当前NALU的重要性,值越大,越重要。
    解码器在解码处理不过来的时候,可以丢掉重要性为 0 的 NALU。

  • nal_unit_type
    表示 NALU 数据的类型,有以下几种:

H264\_Struct\_C.png

其中比较注意的应该是以下几个:

  • 1-4:I/P/B帧,如果 nal_ref_idc 为 0,则表示 I 帧,不为 0 则为 P/B 帧。
  • 5:IDR帧,I 帧的一种,告诉解码器,之前依赖的解码参数集合(接下来要出现的 SPS\PPS 等)可以被刷新了。
  • 6:SEI,英文全称 Supplemental Enhancement Information,翻译为“补充增强信息”,提供了向视频码流中加入额外信息的方法。
  • 7:SPS,全称 Sequence Paramater Set,翻译为“序列参数集”。SPS 中保存了一组编码视频序列(Coded Video Sequence)的全局参数。因此该类型保存的是和编码序列相关的参数。
  • 8: PPS,全称 Picture Paramater Set,翻译为“图像参数集”。该类型保存了整体图像相关的参数。
  • 9:AU 分隔符,AU 全称 Access Unit,它是一个或者多个 NALU 的集合,代表了一个完整的帧,有时候用于解码中的帧边界识别。

特殊的 NALU 类型:SPS和PPS

SPSPPS 存储了编解码需要一些图像参数,SPS,PPS 需要在 I 帧前出现,不然解码器没法解码。而 SPS,PPS 出现的频率也跟不同应用场景有关,对于一个本地 h264 流,可能只要在第一个 I 帧前面出现一次就可以,但对于直播流,每个 I 帧前面都应该插入 sps 或 pps,因为直播时客户端进入的时间是不确定的。

3.3 NALU Payload

很少有资料会称身体部分为 Payload,绝大部分资料对 NALU 组成的定义是这样子的:

1
2
3
4
NALU = NALU Header + SODB // 定义1
NALU = NALU Header + RBSP // 定义2
NALU = NALU Header + EBSP // 定义3

于是新的问题来了:SODB,RBSP和EBSP都是什么东西呢?这块概念,在博客NALU详解二(EBSP、RBSP与SODB)中介绍得非常清楚,总结来说就是:

SODB

英文全称 String Of Data Bits,称原始数据比特流,就是最原始的编码/压缩得到的数据。

RBSP

英文全称 Raw Byte Sequence Payload,又称原始字节序列载荷。和 SODB 关系如下:

1
2
RBSP = SODB + RBSP Trailing Bits(RBSP尾部补齐字节)

引入 RBSP Trailing Bits 做 8 位字节补齐。

EBSP

英文全称 Encapsulated Byte Sequence Payload,称为扩展字节序列载荷。和 RBSP 关系如下:

1
2
EBSP :RBSP插入防竞争字节(`0x03`)

这里说明下防止竞争字节(0x03):读者可以先认为 H264 会插入一个叫做 StartCode 的字节串来分割 NALU,于是问题来了,如果 RBSP 中也包括了 StartCode(0x000001 或 0x00000001)怎么办呢?所以,就有了防止竞争字节(0x03)

1
2
编码时,扫描 RBSP,如果遇到连续两个 0x00 字节,就在后面添加 防止竞争字节(0x03);解码时,同样扫描 EBSP,进行逆向操作即可。

最后,以一幅图总结 NALU 这段内容:

H264\_Struct\_D.jpg

四、码流解析的角度

H264 码流实际可以理解为由一个一个的 NALU 单元组成。(下图中的 RBSP 类似 NALU Payload)

H264\_Struct\_E.png

前面提到的一帧图像(I 帧, P 帧, B 帧)就是一个 NALU 单元,NALU 单元除了代表图像外还能包含其他类型的数据,如 PPS 和 SPS。

五、H264更详细的分层结构

H264\_Struct\_F.png

  • 第一层:比特流。该层有两种格式:Annexb 格式和 RTP 格式。
  • 第二层:NAL Unit 层。包含了 NAL Header 和 NAL Body 信息。
  • 第三层:Slice 层。一帧视频图像可编码成一个或者多个片,每片包含整数个宏块,即每片至少 一个宏块,最多时包含整个图像的宏块。
  • 第四层:Slice data 层。Slice 由宏块(macro block, MB)组成。宏块是编码处理的基本单元。
  • 第五层:PCM 类。
  • 第六层:残差层。

片的目的:

为了限制误码的扩散和传输,使编码片相互间保持独立。片共有 5 种类型: I 片(只包含 I 宏块)、P 片(P 和 I 宏块)、B 片(B 和 I 宏块)、SP 片(用于不同编码流之 间的切换)和 SI 片(特殊类型的编码宏块)。

六、扩展:怎么区分 NALU 的边界?

了解了 NALU 之后,关于 H264 格式,还有一个问题:解码器怎么知道一个 NALU 要结束了?或者说它怎么区分 NALU 的边界?

要回答这个问题,就必须了解 H264 的打包方式,通俗来说是H264 如何组织一连串的 NALU 为完整的 H264 码流。目前 H264 主流的两种格式:

  • Annex-B:本文关于 NALU 的很多细节介绍都是 Annex-B,它依靠前文提到的 Start Code 来分隔 NALU,打包方式如下:
1
2
[start code]--[NALU]--[start code]--[NALU]...

  • AVCC:笔者对这个格式了解的不多,从网上找到很多资料知道以下几点:
    • NALUextradata/sequence header 组成,由于在 extradata/sequence header 中存储了 NALU 的长度,因此 NALU Payload 不需要做字节对齐,不过防竞争字节还是有的;
    • SPSPPS 被放在了 extradata/sequence header
    • 打包方式如下:
1
2
[SIZE (4 bytes)]--[NAL]--[SIZE (4 bytes)]--[NAL]... // 请注意,SIZE一般为4字节,但是具体以实际为准

至于为什么要有这两类格式,还需要查阅更多的资料。不过 StackOverflow 上关于Possible Locations for Sequence/Picture Parameter Set(s) for H.264 Stream的回答可以帮助深入了解这两种格式,推荐阅读。

下面是一个 H264 码流,可以看到每个 NALU 前有一个 StartCode(0x000001 或 0x00000001),作为 NALU 的分割符:

H264\_Struct\_G.jpg

分析其中比较有代表性的3帧:

  • 00 00 00 01 67
    00 00 00 01 是一个 NALU 开始,67 是Header, 二进制为 0110 0111, nal_unit_type 为00111,即7为 SPS 帧。
  • 00 00 00 01 68
    68 二进制为 0110 1000,nal_unit_type 为00111,即 8 为 SPS 帧。
  • 00 00 00 01 65
    65 二进制为 0110 0101,nal_unit_type 为 00101, 即 5 为 IDR 帧。

参考:

H264系列–码流组成和分层结构

知乎 - 视频和视频帧:H264编码格式整理

(十三) h264标准解析

PCM原始音频数据格式介绍

什么是PCM

PCM全称Pulse-Code Modulation,翻译一下是脉冲调制编码。

其实大可以不用关心英文释义,之所以这么命名是因为一些历史原因。

在音视频中,PCM是一种用数字表示采样模拟信号的方法。

要将一段音频模拟信号转换为数字表示,包含如下三个步骤:

  • Sampling(采样)
  • Quantization(量化)
  • Coding(编码)

通常,我们可以通过一条曲线在坐标中显示连续的模拟信号,如下图所示:
IMAGE

为了更容易理解PCM,取其中一段来说明。
IMAGE
假设这表示一段一秒的音频模拟信号。
IMAGE

Sampling(采样)

Sampling(采样)处理,实际上就是让采样数据能够完全表示原始信号,且采样数据能够通过重构还原成原始信号的过程,如上图。

将采样后的图拿出来单独解释:
IMAGE

红色曲线:表示原始信号。
蓝色垂直线段:表示当前时间点对原始信号的一次采样。采样是一系列基于振幅(amplitude和相同时间间隔的样本。这也是为什么采样过程被称为PAM的原因。
PAM:(Pulse Amplitude Modulation)是一系列离散样本之的结果。
采样率(Sample rate)

每秒钟的样本数也被称之为采样率(Sample rate)。在Sampling图示案例中,采样率为每秒34次。意味着在一秒的时间内,原始信号被采样了34次(也就是蓝色垂直线段的数量)。

通常,采样率的单位用Hz表示,例如1Hz表示每秒钟对原始信号采样一次,1KHz表示每秒钟采样1000次。1MHz表示每秒钟采样1百万次。

根据场景的不同,采样率也有所不同,采样率越高,声音的还原程度越高,质量就越好,同时占用空间会变大。

例如:通话时的采样率为8KHz,常用的媒体采样率有44KHz,对于一些蓝光影片采样率高达1MHz。

#Quantization(量化)

原始信号采样后,需要通过量化来描述采样数据的大小。如图:
IMAGE

量化处理过程,就是将时间连续的信号,处理成时间离散的信号,并用实数表示。这些实数将被转换为二进制数用于模拟信号的存储和传输。

在图例中,如果说采样是画垂直线段的话,那么量化就是画水平线,用于衡量每次采样的数字指标。如图:
IMAGE
图中,每条横线表示一个等级(level)。

为了更好的描述量化过程,先来介绍一下bit-depth(位深):用来描述存储数字信号值的bit数。较常用的模拟信号位深有:

8-bit:2^8 = 256 levels,有256个等级可以用于衡量真实的模拟信号。
16-bit:2^16 = 65,536 levels,有65,536个等级可以用于衡量真实的模拟信号。
24-bit:2^24 = 16,666,216 levels,有16,666,216个等级可以用于衡量真实的模拟信号。
显而易见,位深越大,对模拟信号的描述将越真实,对声音的描述更加准确。

在当前例子中,如果用为8-bit位深来描述的话,就如下图所示:
IMAGE
量化的过程就是将一个平顶样本四舍五入到一个可用最近level描述的过程。如图中黑色加粗梯形折线。量化过程中,我们将尽量让每个采样和一个level匹配,因为每个level都是表示一个bit值。

图中,第9次采样的平顶样本对应的level用十进制表示为255,也就是二进制的1111 1111。

Encoding(编码)

IMAGE

在编码这一步,我们会将时间线上的每个sample数据转化为对应的二进制数据。

采样数据经过编码后产生的二进制数据,就是PCM数据。PCM数据可以直接存储在介质上,也可以在经过编解码处理后进行存储或传输。

PCM数据常用量化指标

采样率(Sample rate):每秒钟采样多少次,以Hz为单位。详见:**采样率(Sample rate)**一节。

位深度(Bit-depth):表示用多少个二进制位来描述采样数据,一般为16bit。详见:**Quantization(量化)**一节。

字节序:表示音频PCM数据存储的字节序是大端存储(big-endian)还是小端存储(little-endian),为了数据处理效率的高效,通常为小端存储。

声道数(channel number):当前PCM文件中包含的声道数,是单声道(mono)、双声道(stereo)?此外还有5.1声道等。

采样数据是否有符号(Sign):要表达的就是字面上的意思,需要注意的是,使用有符号的采样数据不能用无符号的方式播放。

以FFmpeg中常见的PCM数据格式s16le为例:它描述的是有符号16位小端PCM数据。

s表示有符号,16表示位深,le表示小端存储。

PCM数据流

对于PCM数据都是一些图像化的描述,那么一段PCM格式的数据流怎么表示的呢?

以8-bit有符号为例,长得像这样:

+———+———–+———–+—-
binary | 0010 0000 | 1010 0000 | …
decimal | 32 | -96 | …
+———+———–+———–+—-

每个分割符”|”分割字节。因为是8-bit有符号表示的采样数据,所以采样的范围为-128~128。

图示中表示的是两个连续采样数据的二进制和十进制表示的值。

如果我们有一个PCM文件,在代码中,我们可以通过以下方式来读取这样的PCM数据流(Stream)。

FILE *file
int8_t *buffer;
file = fopen(“PCM file path”);
buffer = malloc(fileSize);
fread(buffer, sizeof(int8_t), fileSize / sizeof(int8_t), file);

伪代码仅仅表示一种加载方式。但在代码中,一开始就将整个文件加载到了内存中,这是不对的。因为我们的音频数据量往往会比较大,一次性全部加载增加了内存负担,而且并不必要。

通常我们会为buffer分配一个固定的长度,例如2048字节,通过循环的方式一边从文件中加载PCM数据,一边播放。

加载好PCM数据后,需要送到音频设备驱动程序中播放,这时我们应该能听到声音。与PCM数数据一同到达驱动程序的通常还有采样率(sample rate),用来告诉驱动每秒钟应该播放多少个采样数据。如果传递给驱动程序的采样率大于PCM实际采样率,那么声音的播放速度将比实际速度快,反之亦然。

OK,对于PCM数据流的存储而言,上面仅仅只是单声道。对于多声道的PCM数据而言,通常会交错排列,就像这样:

+———+———–+———–+———–+———–+—-
FL | FR | FL | FR | FL |
+———+———–+———–+———–+———–+—-

对于8-bit有符号的PCM数据而言,上图表示第一个字节存放第一个左声道数据(FL),第二个字节放第一个右声道数据(FL),第三个字节放第二个左声道数据(FL)…

不同的驱动程序对于多声道数据的排列方式可能稍有区别,下面是常用的声道排列地图:

2: FL FR (stereo)
3: FL FR LFE (2.1 surround)
4: FL FR BL BR (quad)
5: FL FR FC BL BR (quad + center)
6: FL FR FC LFE SL SR (5.1 surround - last two can also be BL BR)
7: FL FR FC LFE BC SL SR (6.1 surround)
8: FL FR FC LFE BL BR SL SR (7.1 surround)

音量控制

音量的表示实际上就是量化过程中每个采样数据的level值,只要适当的增大或者缩小采样的level就可以达到更改音量的目的。

但需要说明的是,并是不将level值*2就能得到两倍于原声音的音量。

因为如下两个原因:

数据溢出:我们都知道每个采样数据的取值范围是有限制的,例如一个signed 8-bit样本,取值范围为-128~128,值为125时,放大两倍后的值为250,超过了可描述的范围,此时发生了数据溢出。这个时候就需要我们做策略性的裁剪处理,使放大后的值符合当前格式的取值区间。

如下伪代码描述了signed 8-bit格式的声音放大两倍的裁剪处理:

1
2
3
4
5
6
7
8
9
10
11
12
int16_t pcm[1024] = read in some pcm data;
int32_t pcmval;
for (ctr = 0; ctr < 1024; ctr++) {
pcmval = pcm[ctr] * 2;
if (pcmval < 128 && pcmval > -128) {
pcm[ctr] = pcmval
} else if (pcmval > 128) {
pcm[ctr] = 128;
} else if (pcmval < -128) {
pcm[ctr] = -128;
}
}

对数描述:平时表示声音强度我们都是用分贝(db)作单位的,声学领域中,分贝的定义是声源功率与基准声功率比值的对数乘以10的数值。根据人耳的心理声学模型,人耳对声音感知程度是对数关系,而不是线性关系。人类的听觉反应是基于声音的相对变化而非绝对的变化。对数标度正好能模仿人类耳朵对声音的反应。所以用分贝作单位描述声音强度更符合人类对声音强度的感知。前面我们直接将声音乘以某个值,也就是线性调节,调节音量时会感觉到刚开始音量变化很快,后面调的话好像都没啥变化,使用对数关系调节音量的话声音听起来就会均匀增大。

如下图所示,横轴表示音量调节滑块,纵坐标表示人耳感知到的音量,图中取了两块横轴变化相同的区域,音量滑块滑动变化一样,但是人耳感觉到的音量变化是不一样的,在左侧也就是较安静的地方,感觉到音量变化大,在右侧声音较大区域人耳感觉到的音量变化较小。
IMAGE

这就需要对音量值的乘数系数合理取值。具体如何取值,请参考非常专业的一篇文章:PCM音量控制

采样率调整

采样率的定义为:每秒钟采样次数。而降低增加采样率只需要以固定的频率复制或者丢弃采样数据即可。

如10Hz表示每秒钟采样10次,我们只需要将2*n(n为从0开始的值)处的采样数据舍弃,就可以得到10/2 = 5Hz的采样数据。

PCM数据格式

音频参数

经常见到这样的描述: 44100HZ 16bit stereo 或者 22050HZ 8bit mono 等等.

  • 44100HZ 16bit stereo:
    每秒钟有 44100 次采样, 采样数据用 16 位(2字节)记录, 双声道(立体声);
  • 22050HZ 8bit mono:
    每秒钟有 22050 次采样, 采样数据用 8 位(1字节)记录, 单声道;

什么是PCM音频数据

PCM(Pulse Code Modulation)也被称为脉冲编码调制。
PCM音频数据是未经压缩的音频采样数据裸流,它是由模拟信号经过采样、量化、编码转换成的标准的数字音频数据。

PCM数据存储格式

IMAGE

PCM数据存储格式

本文中声音样值的采样频率一律是44100Hz,采样格式一律为16LE。
“16”代表采样位数是16bit。由于1Byte=8bit,所以一个声道的一个采样值占用2Byte。
“LE”代表Little Endian,代表2 Byte采样值的存储方式为高位存在高地址中。

立体声的左右声道的数据是一样的吗?

原来一直理解立体声的左右声道是一样的,但是通过查看实际的数据,发现并不一样。比如:

IMAGE

PCM文件的16进制数据

从以上可以看出,刚开始的4个字节是同一个采样点,其中0x04e8是左声道,0x01c9是右声道,发现是不一样的。
通过cool edit pro查看波形:
IMAGE

左右声道对比

可以看出左右声道,虽然波形相似,但是的确存在不同。

PCM Examples

以下代码可读取pcm文件,并演示了打印采样点的值和将立体声分离成左声道和右声道。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#include "stdafx.h"
#include <fstream>
#include <iostream>
using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
// Read file to buffer
char szFileName[256] = {0};
sprintf_s(szFileName, ".\\44100_stereo_s16le.pcm");

ifstream inFile(szFileName, ios::in | ios::binary | ios::ate);
long nFileSizeInBytes = inFile.tellg();

char *pszPCMBuffer = new char[nFileSizeInBytes];

inFile.seekg(0, ios::beg);
inFile.read(pszPCMBuffer, nFileSizeInBytes);
inFile.close();

cout<<"FileName: " << szFileName <<", nFileSizeInBytes: " << nFileSizeInBytes <<endl;
cout << "Read file to a buffer Done!!!\n\n";

// 从文件中读取采样点并打印
short szItem[1024] = {0};
memcpy(szItem, pszPCMBuffer, 256);

for (int i = 0; i < 128; i++)
{
printf("%6d ", szItem[i]);

if ((i + 1) % 8 == 0)
{
cout<<endl;
}
}

// 将双声道分离成左声道和右声道
FILE *fpLeft = fopen("44100_mono_s16le_l.pcm","wb+");
FILE *fpRight = fopen("44100_mono_s16le_r.pcm","wb+");

printf("\n Split stereo to left and right ...\n");

for (int nPos = 0; nPos < nFileSizeInBytes; )
{
if ((nPos / 2) % 2 == 0) // 偶数
{
if (fpLeft)
{
fwrite(pszPCMBuffer + nPos, 1, 2, fpLeft);
}
}
else
{
if (fpRight)
{
fwrite(pszPCMBuffer + nPos, 1, 2, fpRight);
}
}

nPos += 2;
}

if (fpLeft)
{
fclose(fpLeft);
fpLeft = NULL;
}

if (fpRight)
{
fclose(fpRight);
fpRight = NULL;
}

printf("\n Split stereo to left and right Done. \n");

if (pszPCMBuffer)
{
delete [] pszPCMBuffer;
pszPCMBuffer = NULL;
}

getchar();

return 0;
}

References:

http://blog.csdn.net/ownwell/article/details/8114121
http://blog.csdn.net/leixiaohua1020/article/details/50534316
https://www.cnblogs.com/CoderTian/p/6657844.html

作者:FlyingPenguin
链接:https://www.jianshu.com/p/e568f94cdf6a
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

FreeSWITCH视频会议“标准”解决方案

本文由FreeSWITCH 中文社区创始人杜金房在LiveVideoStack线上分享的演讲内容整理而成,详细介绍了FreeSWITCH做为一种开源的视频会议解决方案如何在开源、开放的基础上,对接各种无法修改的“标准”视频会议终端、WebRTC浏览器以及微信小程序等,迎接各种挑战。

文 / 杜金房

整理 / LiveVideoStack

我们所谓的“标准”解决方案,并非是指这个解决方案是标准的。而是在做视频会议的过程中,FreeSWITCH作为一个服务器,会面对不同类型的客户端以及各种硬件的终端。由于它们使用了各种各样的标准协议,是我们没办法修改的,所以称它们为标准的客户端。而FreeSWITCH视频会议“标准”解决方案就是指针对这些不可修改的标准客户端所做的一种解决方案。

视频会议类型

视频会议大体上可以分为三种类型。一是传统视频会议,传统的视频会议是“标准”的,因为它们之间需要互通。早期的视频会议协议一般是H323,即使现在我们也还会经常遇到H323的设备,但后来大部分被SIP协议的设备所取代。SIP协议是一个文本协议,整体更灵活一些。

近几年开始出现一些云视频会议,今年其实也可以算作云视频会议的元年,由于疫情的原因,大家开始更多地使用视频会议。例如Zoom,腾讯会议、小鱼易连等,据说腾讯会议一周之内上线了10万台服务器,进行紧急扩容,这在传统的视频会议时代是不可能实现的,只有在云计算时代才能快速实现扩容,这也体现了云计算的优势。据了解这些云视频会议厂商,基本都是使用的私有协议,其好处是可以无限制的优化。但私有协议无法进行很好的互联互通,不过各个视频会议厂商为了实现与其它设备互通,自己也会提供一些SDK。

开源领域的视频会议,有FreeSWITCH、Jitsi、Kurento、Janus、Medooze等,这些视频会议也有许多年的历史了,目前大多已经开始支持WebRTC。有的以支持WebRTC为主,例如Kurento和Janus;Janus和Medooze最初是支持SIP的,最近几年我没有太关注;Jitsi对WebRTC的支持非常好。

对于FreeSWITCH,大家可能会有些误解。FreeSWITCH其实最早是用于音频通信的,即PBX 程控交换机,但实际上FreeSWITCH的视频会议功能也非常强。开源的视频会议因为是开源、开放的,使用的是开放的API,因此更多的是使用开放协议如SIP协议。

目前WebRTC比较火,所有的视频会议设备基本都在支持WebRTC,在浏览器里就可以打电话。WebRTC是一个媒体协议,没有规定信令,在信令层面没有标准,因此大家实现起来会比较灵活。FreeSWITCH在信令层实现了两种协议,一种是SIP,它承载在WebSocket上,因为在浏览器里只有WebSocket可以实现双向通信。另外还有自己实现的私有协议Verto。

互联互通

随着我们对于视频会议软件的使用越来越多,会发现一个问题:手机和电脑上的视频会议客户端越来越多。

其实我们也非常希望能解决这个问题,方法就是互联互通。我们希望所有的终端都能互联互通,难道只是因为使用的视频会议客户端不同,就不能在一起开会了吗?

理想很丰满,但现实执行起来还是很困难的。

其实更多的是由于商业的原因,没有人会选择这么做。当然从技术层面来说,全部使用私有的协议、服务器和终端,能更好地优化、更好的保证安全等等。总之,实现互联互通任重道远。

虽然任重道远,但其实我们一直想做这方面的事情,FreeSWITCH也连接了很多不同的客户端,希望能跟更多的设备进行互联互通。

FreeSWITCH的成长史

说到FreeSWITCH,简单了解一下它的历史。

2006年FreeSWITCH发布了第1个版本。FreeSWITCH本身最开始是作为一个PBX进行的。PBX就是我们所说的企业里的交换机,打电话用的。

2008年发布了1.0版本—凤凰版,像凤凰涅磐一样,经历了无数次的崩溃、优化,所以称为凤凰版。

2012年发布FreeSWITCH1.2版本(FreeSWITCH的版本号都是偶数),1.2版本非常稳定,音频方面也已经非常成熟,在电话方面基本上没什么可做的了。但随着的WebRTC的出现,FreeSWITCH决定接下来要支持WebRTC。

2014年推出1.4版本,开始支持WebRTC。早期的WebRTC还不是很稳定,但WebRTC的标准变得非常快,所以FreeSWITCH也一直在跟着改。

其实早在2008年我就开始做FreeSWITCH了,那时主要做在线教育,早期的在线教育没有视频,只有音频,教师利用音频做英语的对话教学。后来又做了一些其它的项目,要求有视频,然后就增加了一些视频的功能,包括视频的MCU。由于FreeSWITCH早期在视频方面还不是很成熟,做了几个项目不是特别满意,所以后来我们就把视频部分开源了。

2014年-2015年,我们耗费了一年的时间,将自己做的部分合并到FreeSWITCH的主分支中,用一整年的时间将自己写的代码规范化、同时适配Windows、Linux、Unix等各种平台,实现FreeSWITCH在各种平台上的编译和支持,发布了1.6版。这时FreeSWITCH开始支持视频通话和视频会议,之后从2017年开始到2020年,这几年一直在不停的修bug,将FreeSWITCH做得越来越完善。

在视频会议开源的这个分支上,我们也做了很多内容,有的已经合并到1.8版本中,有的合并在1.10版本中。其实我们自己还维护了一个分支没有合并进去,由于将自己的代码合并到开放的分支中需要很多劳动力,所以会在后续逐步完成。

FreeSWITCH支持的标准协议

说到标准协议,FreeSWITCH支持上图中的这些标准协议。

首先FreeSWITCH支持SIP信令,就是音频和视频通话标准的协议,支持各种客户端、终端,目前市面上很多的会议设备都是支持SIP的,可以直接实现互通。

其次我们还做了一个H323的模块,FreeSWITCH本身有两个H323的模块,但都不支持视频,因为有客户需求,就又做了支持视频的模块,这样也就能跟H323的终端进行互通。随着移动互联网的发展,目前各种移动端设备上的APP大多都是支持SIP信令的,可以直接实现互通。

随着WebRTC的发展,很多人开始将其移植到手机客户端上。WebRTC的优点在于不需要自己写媒体层,随着WebRTC的开源带来了很多特性,比如说JitterBuffer、回声消除、降噪等等之类的内容在WebRTC中已经包含,不需要再自己写,虽然不如各种私有化的厂家做的好,因为私有化的厂家可以进行更加极致的优化。但对于一个开源项目来说,WebRTC做的已经足够好了,由于WebRTC只有媒体层没有信令层,所以大家都开始往WebRTC上套各种信令。

值得一提的是RTMP,其实最开始我做的就是RTMP的视频。尽管目前 Flash基本上已经没有人用了,但RTMP协议还是非常好的,目前更广泛应用于直播和推流等。

视频会议的几种实现方式:Mesh、MCU、SFU

视频会议简单来讲有三种方式:Mesh、MCU、SFU。

Mesh是单纯的点对点连接形成的网状结构且不需要服务器,但是这种方式使用的非常少,因为不好控制。

目前比较主流的两种方式,MCU和SFU。

MCU之所以说它比较主流,是因为最开始的视频会议设备基本上都是MCU的。MCU中间有一个服务器,视频客户端与服务器直接通讯,实际上收发都是一路流。视频服务器把所有的流合成一路,即视频融屏。当然音频也会融合,简单起见,我们这里只说视频。视频服务器将视频流融合到一起形成一个画面,分发给所有的终端,所有的终端看到的画面都是一样的,这种情况叫MCU(Multipoint Control Unit),即多点控制单元。

随着WebRTC的出现,很多人开始用SFU(Selective Forward in Unit)。SFU不解码、不融屏,前面说到MCU会对各种画面拼接、融屏,也就需要对视频进行编解码。而SFU只需要把收到的各个客户端发来的视频和音频,有选择的发给不同的人。其好处是不需要占用过多的CPU,但缺点是比较浪费带宽。比如5个人进行通话,其中一方只需要发一路流,转发单元会进行布置,将这一路流复制成多份进行分发,每个人都会收到很多路流,终端所承受的压力会比较大,因为一方的终端需要对另外的4路流进行解码。好处是终端可以自由排列所收到的其它客户端的显示样式,每个人看到的画面都可能是不一样的。

上图是MCU的基本原理图,如图有4个摄像头,各个摄像头把自己的画面上传到MCU,MCU进行缩放、拼接,拼接成一个画面,然后下发。每个终端显示器上显示出来的都是一样的画面。FreeSWITCH内部也是这样实现的,FreeSWITCH内部实现了诸如1、2、3、4等的多个层,以及一个画布(canvas),我们将收到的不同视频解码,再缩放,拼接到画布上,形成一路流,最后分发出去。

画布的样式有多种类型,FreeSWITCH除了支持标准的画布:3×3、4×4、8×8以外,还支持培训班模式:演讲者画面(大)+听众画面(小),以及更多不同的排列形式。

这其中我们做了一个比较关键的优化,就是RTP。众所周知视频流是靠RTP传输,RTP还有一个姊妹协议叫做RTCP,是一个控制协议用来控制RTP,这个协议里有一个东西叫做TMMBR(Temporary Maximum Media Stream Bit Rate Request)。在视频会议中,一般来说大家看到的高清画面有720p或1080p的,而在演讲者模式中,观众的画面通常是比较小的,没有必要上传1080P或720P的画面,浪费1兆或2兆的带宽。这时,FreeSWITCH会发送一条指令-TMMBR,提示不需要高清视频上传,降低带宽上传,这样观众就可以通过低带宽消耗的方式上线,减轻服务器的带宽压力、同时也减轻解码的压力。

我们已经将其应用在我们的视频会议里,效果非常明显。但前提是终端要支持这个协议,在WebRTC中是支持的,这个协议的标准叫RFC 5104。RFC 5104里除了规范TMMBR以外,还规定了一些其它的协议,例如FIR、NACK、PLI,都是与关键帧相关的。在有丢包产生的情况下,FIR是请求一个关键帧,因为解码失败将要出马赛克,请求对方发送一个关键帧,刷新画面。NACK是丢包,其实丢包就涉及到了缓存,就是我所说的Jitter Buffer,Jitter Buffer是在两个通信终端之间,不管是发送端还是接收端,都会有一个Buffer,这个缓冲区发出去的东西,会放到缓冲区里接收,当发生丢包的情况下,发现有一个丢包,就向对方请求重发。这时,对方就看到这个包还在缓冲区,即可从冲缓冲区中取出重发,这种方式叫NACK。PLI是Packet Lost Indication,告诉这一端我丢包了,这个协议负责音视频传输的质量,因为视频传输大多数用的UDP的协议是不可靠的,所以在发生丢包的情况下再做比较多一些补偿。

我们还做了一些其它的优化,在大规模视频会议中,几十个人甚至上百人参与,对于服务器的解码压力会比较大。而且同时在一个屏上显示几百个人是没意义的,因为太小根本看不清观众表情,所以一般在参会人数比较多的情况下,就直接展示几个或者几十个人,太多了就不展示,不展示的部分也就没有必要解码,不需要浪费服务器的处理能力。但观众还是需要轮番展示一下的,所以我们还做了一个多人轮循展示,例如现在开始看这10个人,下一次我再看另外10个人,然后做一个定时器进行轮循,让每个人都有出镜的机会。当然由于关键帧的原因,展示出来的时候,就需要解码,这时不一定正好有个关键帧过来,所以需要提前两秒开启解码,然后等展示到它的时候,一般这个关键帧就到了,因为每次展示的同时也会向对方要关键帧,这样的话正好能展示出来,不至于黑屏。还有一些我们在视频会议的过程中遇到很多坑,例如NACK请求太多,有时候由于好多终端的网络都不好,然后都过来请求NACk丢包,如果是发现这个NACK请求太多,我们就直接发关键帧,不理会丢包情况。

还有就是限制FIR请求的频率,FIR就是我们说的关键帧请求,刷新关键帧,当终端在网络条件不好的情况下请求一个关键帧,若有10个终端都请求关键帧,若一秒钟之内产生10个关键帧,带宽就会被撑爆或者视频画面会非常的模糊,因为满足不了这么多关键帧的请求,所以我们做了一些算法,其实这也是一些基本的算法,限制关键帧的请求。例如可以设置两秒或者三秒,那在这两秒或者三秒之内,只产生两个关键帧,即第一个请求和最后一个请求会产生关键帧,其他的都忽略,这样的情况下才会保护带宽,当然对方可能会有短暂的花屏,但是没关系,然后两秒钟之内就已经清晰了,因为毕竟其网络状态不好自身是可以忍受的,当然如果是网络状态都好的终端呢,他也不会有影响。

另外,不同的编码有不同的编码器, FreeSWITCH支持不同的编码,由于历史原因,Chrome支持vp8,苹果的浏览器只支持H.264,不能实现互通,然后最开始的WebRTC也不能互通,当然最近几年可以了,Chrome也支持H.264了,其他的浏览器也支持vp8了,所以说FreeSWITCH从最开始就支持多种编码,然后在同一个会议里,不同编码的会议,用不同的编码器即可。

还有情况不同的终端,可能这些做私有终端、私有通信的没有这种困扰,因为终端都是他自己的,规定用什么编码就用什么编码,但是在开源领域,需要面对各种各样的客户端,例如军队的项目,他们好多设备还是H.263的,不能被替换,我们就只能去适配H.263,H.263不支持720p的分辨率,它只支持CIF分辨率的,CIF即不是16:9也不是4:3,所以需要我们单独分一个编码器。等等之类的情况,想要支持的终端的型号越多需要做的就越多。

早期的我们也做了一些优化,降分辨率、降帧率,发现有终端使用flash是我们处理不了的,那么我们就降分辨率,把分辨率降低一半,然后降帧,原先30帧降成15帧。现在有了更好的技术叫SVC,SVC就是在一个编码器里出多个分辨率和多个帧率,当然对CPU还是有代价的,它编出来之后可以有选择的去分出去,其实FreeSWITCH有一个模块叫mod_openh264,使用的是思科开源的编解码器,支持SVC,但目前我们还没有使用,只是用了它比较简单的编解码的功能,我们在FreeSWITCH内部使用的,另外一个就是内部的libx264,它是在FFmpeg自带的。

在视频会议里边我们经常遇到的还有一个就是双流,传统的视频会议设备,H.323的设备,一般支持的协议叫H.239,它可以在一个通话里支持两个流,一个流是演讲者的视频,另外一个就是PPT,两个流可以上到MCU,大家可以看到,甚至对方也可以放两个电视或者投影仪,一个投影仪展示演讲者的视频,另一个投影仪展示PPT。

SIP设备也支持双流,SIP设备的双流叫BFCP,全称是Binary Floor Control Protocol。演讲者会做一些控制,H.323的双流我们没有做,只做了简单的对接,由于对接的设备比较少,能通即可。对于SIP协议的双流,我们现在还没有开源,也是BFCP的。我们直接在SIP的模块中挟持了SDP,因为在SDP里边会有两个视频流,挟持到以后处理生成一路新的呼叫(一个假的呼叫),FreeSWITCH在收到一路呼叫时,就看到他是一个双流的呼叫,然后就生出两个呼叫,这样的话两个呼叫会同时调到会议里边,会议的代码不需要改。对会议来讲是来了两个呼叫,但是对FreeSWITCH来讲是一路呼叫,这样就可以支持双流了。

另外在WebRTC中,双流有一个叫Simlcast,它也可以在一个SDP里边看多个流,由于Simlcast早期不稳定,有很多问题,现在我们利用Simlcast只是做了一些实验,还没有具体详细的代码,我们只是比较简单粗暴的,直接在浏览器里发起两路呼叫,一个呼叫是演讲者的这个视频,另外一个呼叫是共享桌面,因为在浏览器里发起WebRTC呼叫时,可以直接选视频源是摄像头还是屏幕或者是共享某个应用程序,形成了这种双流。同样到了FreeSWITCH,它还是作为两路流,作为两个呼叫进到会议中。

对于上面提到的SFU,FreeSWITCH是可以实现SFU的,其实我们也做了很多的尝试,但是SFU比较复杂,需要控制很多路呼叫,未来FreeSWITCH的核心是要支持多路流,但是目前实现SFU的计划还未提上日程,另外一个原因也是由于市面上很多做SFU的产品,并且都比较成熟了,例如Jitsi、Kurento、Janus都有SFU的实现,如果需要的话,可以搭配FreeSWITCH进行实现。

当视频会议的规模比较大时,就需要级联,因为一台服务器支撑不下。FreeSWITCH的视频会议在实验室测试一台服务器可以支撑400路720p的视频流,根据具体的应用场景选择服务器的规格是32核或64核,当然我们在开大会的场景下,不会把所有的人都显示出来,只把展示出来的人编解码,就是上面提到的优化不解码,这样可以只编码一路流下发,所以对CPU的压力不大。其实我们在实验室里做到400路流,但是给客户上线的是一台服务器支撑100路终端,最大应该能上到200路。但是应用场景是需要800路客户终端,那我们就做了级联。

级联存在一个问题,我们称之为“看对眼”,就是当两个MCU级联时,例如1、2、3在一个MCU上,5、6、7在另外一个MCU上,当MCU合成出的画面进入到另外一个画面的时候,它就会把你的画面返回来,这样这中间就有一个无限循环的画面,当然下图的动图就体现的比较直观。

在做桌面共享的时候肯定对上图这个界面比较熟悉了。

其实Google上能搜出很多类似的情况,这就是我们收到的视频又作为视频源发出去了,这种情况就称为看对眼。视频会议在两个MCU进行级联的情况下就会出现这种情况。那么如何解决呢?

FreeSWITCH实现了一个功能叫做多画布,如上图的应用场景,当一个人开始演讲时,就将其当做主会场,放在画布1上。所有的与会者都需要看到主会场,同时演讲者需要监督其它与会者学习,就需要看到其它与会者的画面,我们把与会者的画面都放在第2个画布上,主会场人员就可以看到分会场的与会者情况。

甚至FreeSWITCH还有一个Super Canvas——超级画布,主要用来做监控的,无论会议里有多少个画布,都可以一目了然的看到。这样,后台管理人员做会议控制的时候,就可以很方便地看到整个会议的场景。通过这种方式,我们就解决了这个“看对眼”的问题,这样两个MCU就可以级联了,例如上行MCU永远放在画布2上,下行MCU永远在画布1上,这样就可以它们区分开。

上图是一个级联的示意图,Master是主服务器,下边有很多FreeSWITCH的从服务器,下行画面永远是在第1张画布上,上行画面的永远是在第2张画布上,反之也可以。这样的话就可以随时切换,查看上行的情况,各个与会者也都可以看到主会场。有时在开会的过程中,为了防止与会者只看到主会场而感到孤独,这时我们就会把第2张画布的画面也推下去,这样所有人都看到其它的与会者。甚至在开会的过程中要交流,例如举手发言,这时也可以替换掉主会场的画面。级联的基本原理就是这样,但实际控制还是比较复杂的。

值得一提的是,FreeSWITCH与现在的一些视频会议不同,比如某些会议可以简单的支持几百人规模的会议,将演讲者画面推到一个流媒体服务器上,但是演讲者是看不到与会者的。这是实际上是一种直播的模式,与会者收到的流都是单向流,只有下行的流。

在一些直播场景中,交流互动即直播连麦。由于参与用户规模比较大,例如十万人的直播场景,当主播跟观众想要进行互动的时候,就需要把这个观众的层级提升,提升到主播的阶层,这时我们才可以把他的画面广播出去。这种应用场景与视频会议在实现原理上基本是一样的,不过FreeSWITCH的会议自始至终都是双向流的。

有了这种方式以后, FreeSWITCH就可以跟其它的MCU进行级联。另外有的单位已经有MCU了, FreeSWITCH又有多个画布,就可以把上行和下行的区分在不同的画布上。例如图中左边两个1、2是FreeSWITCH的用户,右边两个手机客户端上3、4是其它MCU的用户,上行以后在其它MCU上进行融屏后下发即可。

与其它会议系统对接

FreeSWITCH除了与其它MCU对接、级联外,还做了一些优化。

因为FreeSWITCH是开源的,如果要与其它的视频会议系统对接,就需要集成其它视频会议的SDK。比如我们可以跟腾讯会议对接(目前已经接通了音频,视频还没有接),TRTC提供多平台的SDK,我们写了一个模块将其放到FreeSWITCH里,FreeSWITCH就可以与它进行通信。通过PSTN我们可使用电话拨号接入到FreeSWITCH中,也就可以直接接入到腾讯会议中,FreeSWITCH可以当做网关一样使用。当然PSTN现在还不支持视频,只支持音频。

另外一个是Agora的SDK,我们早在很多年前就集成了Agora的SDK,音频和视频都可以接通。

以Agora为例,我们在FreeSWITCH中写了一个模块叫mod_agrtc,这样就可以实现与Agora的互通。Agora与WebRTC类似,只有媒体层的SDK没有信令层,因此就需要自己实现一个信令的服务。当然一般公司会做自己的APP,需要进行注册、鉴权等,已经有信令服务,那么只需要调用FreeSWITCH的API,就可以控制发起呼叫、录音等,实现互联互通。但有的客户不想自己写信令服务,或是自己的信令服务难以扩展。所以我们也写了一个基于WebRTC的信令服务,在移动端集成Agora的SDK,FreeSWITCH里集成了Linux的SDK,即可实现互通。

这其中存在一个问题,无论是Agora还是 TRTC,由于早期的SDK是针对客户端的,所以只支持一个客户端,也就是一个SDK只支持一路通话。于是我们又做了一个服务 — IPC,有几个通话就在FreeSWITCH中创建几个进程,这样就可以实现与FreeSWITCH的互通。

微信小程序解决方案

说到微信小程序,我们再讲一下Flash。其实对于Flash的支持很早就已经在FreeSWITCH中实现,FreeSWITCH有个模块叫mod_rtmp,RTMP协议已经实现了。因为微信小程序使用了RTMP的协议,我们最开始做微信小程序的时候,本来就是想在RTMP的基础上进行扩展实现微信小程序的支持,但实际上并非如此,原来的Flash只需要一个socket和信令,就可以进行音视频的通信,但是微信小程序不行。

微信小程序中提供了两个组件,Live Publisher以及Live Player,是推流与拉流的概念,用RTMP的流可以收和发。于是我们就引入了SRS的服务(SRS也是一个开源软件,主要用于直播平台),起了个名叫mod_weixin。FreeSWITCH可以推流到SRS上,通过小程序来拉取,反之微信小程序可以推流到SRS上,FreeSWITCH再拉取。这样就实现了一个双向的通信。

由于RTMP没有信令,所以我们又实现了Websocket的信令,小程序还是通过Websocket连到FreeSWITCH,这样才能控制通话的建立和连接。

ASR/TTS

FreeSWITCH还支持ASR/TTS,当然并不是原生的支持,而是通过调用一些第三方的SDK实现,这样就可以实现智能控制,甚至做自动会议记录、自动翻译等等。

NAT穿越

在公网上实现视频会议,不可避免的涉及到NAT穿越问题。对于NAT穿越,WebRTC已经做得很好了,比如ICE方式。FreeSWITCH当前已经支持ICE,在ICE打不通的时候,也可以用Stun/TURN服务器进行打通。

还有一些应用如银行,由于情况特殊不能开太多端口。而基于FreeSWITCH的通信每一路通话都要求多个RTP的端口。所以可以采用VPN的方式,连接到公网的服务器上,这样只需要一个UDP的端口即可实现。

针对大规模的视频会议,我们使用了iptables。例如我们在北京和上海都有服务器,就可以在上海的服务器上做一个iptables,然后将所有的流量全部转化到北京的服务器,这样客户端就可以实现就近接入。当然对于一些比较灵活的应用,就需要通过设计相应的应用程序来控制如何调用这些iptables做转发。

除此之外,我们也尝试了腾讯云的CLB(负载均衡)。实际上腾讯云的负载均衡,可以直接提供一个云的负载均衡IP,然后转发到后端的服务上。

下一步我们计划尝试使用阿里的LVS。

在目前情况下,加上前面提及的会议室级联技术,其实已经可以实现相对比较大规模的视频会议。

4G/5G

下一步就是4G/5G,我们其实已经做了很多的尝试。目前直接用手机的4G发视频呼叫的情况可能还比较少,但在业界一些客服系统中已经开始使用,部分客户可以直接通过电话的方式,使用4G视频呼叫到呼叫中心,进行信息交互。相信随着未来5G的发展,通信技术与能力也会愈加强大。

FreeSWITCH社区官网:https://www.freeswitch.org.cn欢迎大家关注、一起交流,未来我们也会将开源进行到底。另外,考虑到FreeSWITCH涉及面可能会太窄,我们就做了一个RTS的社区—Real-Time Solutions,未来也会把对FreeSWITCH扩展的一些代码开源,放入其中。当然最终的目标是直接放到上游的FreeSWITCH当中,但因为需要经过各种的代码审查,接收可能会比较慢,所以会先存放在这个仓库中,等待时机成熟再开源到FreeSWITCH上游。如果大家有条件、有能力的话也非常欢迎一起参与进来。

视频会议项目实施方案

文档下载地址: http://blog.jeestack.com/doc/视频会议项目实施方案.docx
视频会议解决方案,融合多类型会议系统平台和终端,提供一站式会议数字化服务,实现简洁、高效、一致的会议体验。统一认证、统一调度、统一管理,面向企业工作人员,提供订、管、维一站式会议管家服务;实现随时随地全场景连接,大幅提升协同办公效率,释放组织效能。

第1章项目实施流程

1.1. 概况

首先,视频会议系统提供了多种与会方式,包括会议室终端、PC客户端、手机APP等多种使用方式,用户可以根据自己的需求和场景选择最适合的方式进行会议。同时,视频会议系统还支持多方会议,可以容纳数十个与会者同时参与,极大地提高了会议效率和参与度。
其次,视频会议系统具有高清的视频和音频质量,能够提供清晰流畅的会议体验。提供独有的超清画质技术和音频全双工技术,能够准确还原与会者的面部表情和语音细节,使得远程会议的沟通效果更加真实和准确。

1.2. 工程服务

工程服务由提供专业的工程实施服务,主要提供项目中的硬件安装、软件调试以及全局项目管理。实施服务由作为交付主体,依托平台以及丰富的工程经验快速安全的完成工程项目。
工程服务包含项目管理、工程勘测与设计、硬件安装、软件调测、系统联调、验收测试等六大服务项。

1.3. 详细描述

1.3.1. 项目管理

服务定义
以满足视频会议系统需求为导向,依托PMP、EPC流程以及TL9000标准,对整个工程实施过程进行监控管理,以保证项目质量,确保项目按照计划完成。
服务内容

  • 质量管理:实施服务质量进行质量规划,确保质量以及控制,完成对项目的质量评估以及工程实施管理。
  • 进度管理: 实施服务进行工期估算,活动排序以及编制进度计划、更新进度计划,控制项目实施的进度计划。
  • 沟通管理:实施服务的以及合作方进行管理。进行项目工程实施交流以及经验总结。
  • 成本管理: 实施服务涉及采购、实施进行合理的估算并监控项目成本核算。合理的实施服务成本。
  • 范围管理:依托丰富的工程经验进行区域范围管理。
  • 技术管理:依托技术平台以及远程专家对项目技术管理。
  • 风险管理:对项目整体进行风险预警以及管理。
  • 客户满意度管理:定期进行交流,对项目实施过程中的各项事务进行沟通、协调,提升客户满意度。

1.3.2. 成立项目组

为保障项目的顺利实施,项目立项后,公司将成立高效的项目组织,组织结构包括项目管理、技术、质量、协调等职责。

1.3.3. 工程勘测与设计

服务定义
工程勘测与设计是指根据《视讯工程勘测监控操作指导书》,项目经理安排服务工程师到各会场进行工勘,对各会场平面布局的环境、供电条件、走线环境等进行勘测。输出《视讯工程终端产品勘测数据表》等,以指导设备的安装。
服务内容
工程勘测与设计提供工程勘测和工程设计服务。对于不同的产品,提供的服务内容略有不同,如下表所示:

  • 设备布局勘测:勘测设备会议室内环境、会议室的结构尺寸、会议室内已有设备分布及客户的配套设施(例如:电源、接地排、空调、走线架或活动地板等是否已经可用),以确保设备可以正常安装到会议室。
  • 设备传输及走线勘测:勘测设备的线缆路由、中继线缆及接口类型、线缆长度。
  • 设备安装布局设计:绘制设备所在的会议室平面图、电源端口分配图,确保工程按照规范的设计施工。
  • 设备线缆走线设计:绘制会议室走线布置图和接地排端子分配图、制作内部线缆连接表和外部线缆连接表,确保工程按照规范的设计施工。
  • 设备功耗计算:提供设备的功耗表,主要是为了对会议室的供电系统是否可以满足要求提供参考。

1.3.4. 发货

公司严格遵照合同要求,将采购货物及其附件发运到合同指定地点。以签收单为准,交货时应一并交付货物的有关单证。货物运输过程中的一切责任由承担。
公司在成交后按合同中规定的到货时间和地点完成所有设备的生产及设备检验/检测工作。

1.3.5. 工程技术协调会

公司将现场检验之前,举行一次工程技术协调会。工程技术协调会应包括但不限于以如下内容:
(1)讨论交货的具体事宜。
(2)公司提供现场检验的具体实施文件,公司还应提供与工程实际有关的技术资料以及与工程系统相关的图纸和资料,并由双方共同确认。
(3)对供货合同清单中的项目和内容作进一步确认,并根据工程需要对供货合同清单中的项目和内容进行调整。
(4)讨论解决合同实施中具体相关技术问题。
(5)讨论确定本工程的各个系统及与周边系统对接的具体实施方案。
(6)确定培训的具体时间安排。
(7)讨论公司提供的技术服务的具体实施方案。
(8)施工进度及设备安装的安排。
(9)双方认为需要讨论的其他问题。

1.3.6. 组织验货

到货时,提供的货物将在客户和公司代表均在场的情况下进行开箱验货。合同产品的数量与外观将对照装箱单检查。有权派代表参加开箱验货。若由客户与共同组织的检查发现缺货、损伤或与装箱单中列出的质量标准不一致,将由双方拟订一份详细报告并签字,应对该不一致负责补发或者更换。检查要点为:

  • 设备包装外观完好性检查;
  • 拆箱后设备及板卡外观检查;
  • 各箱设备及配件数量清点。

在上述情形中签字的检查报告将作为客户用于要求换货、维修或补足的证据。若检查由客户单独完成,并且未及时通知,则将不对检查结果负责,视同装箱单所列产品完整、完好的完成交接。
设备检验/检测通过后,由客户和公司代表共同在装箱单上签字确认。

1.3.7. 硬件安装

服务定义
硬件安装服务是指对设备和工程配套辅料进行安装并对设备加电的过程,包括主设备及随主设备发货的设备及工程配套物料的安装。
服务内容
硬件安装主要提供各种场景下的硬件安装服务,如下表所示:

* 机柜安装:按照工程安装规范和客户要求,对机柜进行安装。
* 视讯终端及配套外设安装:按照视讯安装规范和客户要求,对视讯终端及其配套外设进行安装。
* 单板安装和调整:硬件安装时按照事先规划好的板位图,核查单板位置并做相应安装或调整。
* 线缆安装:连接与主设备及其配套设备相关的互联电缆、光纤等。
* 线缆调整:根据安装场景进行主设备及其配套设备的线缆调整。
* 线缆走线及布线:按照标准布线工艺,对设备的之间的互联线缆进行布放、绑扎、打标等,使得用户体验良好的视音频效果,并方便设备的后期维护。
* 硬件自检及上电:对照质量检查表,对所完成的设备进行质量检查;对不符合要求的进行整改。确认硬件安装正确无误,上电,检查各指示灯正常。

1.3.8. 软件调测

服务定义
网络工程实施软件调测服务是指对设备进行软件适配、配置、调测,使设备实现功能的过程。软件调测服务主要涉及终端侧配置、MCU侧配置、服务平台侧配置等内容。
服务内容

  • 终端侧调测:终端软件版本加载升级,终端侧相关参数配置;
  • MCU侧调测:MCU软件版本加载升级,MCU侧相关参数配置,MCU license加载配置;
  • 服务平台侧调测:GK、RM关键点配置,相关license加载配置,业务管理平台相关参数配置;

1.3.9. 系统联调

服务定义
对设备及外设进行系统联调,对各会场进行系统联调,包括视音频环回测试、点对点互通测试、主叫呼集测试、多点会议测试等内容。
服务内容

  • 视频环回测试:在终端侧进行视频环回测试,保证本端会场视频的互联互通,保证本端与远端会场视频的互联互通;
  • 音频环回测试:在终端侧进行音频环回测试,保证本端会场音频的互联互通,保证本端与远端会场音频的互联互通;
  • 点对点互通测试:检验终端的通讯录号码点对点呼叫功能是否正常,视音频功能是否正常;
  • 主叫呼集测试:检验终端能否通过主叫呼集方式正常建立多点会议;
  • 云平台测试:检测云平台召集和会议控制功能是否正常,终端多点会议的基本视音频是否正常。

1.3.10. 验收测试

服务定义
验收测试是指对本期销售的设备进行硬件和软件的功能性测试和验收, 确保所提供的设备能够满足客户对设备运行和维护的最基本的需求,并且已经具备承载客户业务和在客户环境中长期稳定运行的能力。验收服务包括系统联调测试、初验测试、试运行测试、工程转维、终验测试等内容。
服务内容

  • 系统联调测试:本测试主要是指工程人员在安装完成后对系统进行的测试。系统测试时,公司负责将测试项目明细表、测试方法、使用到的仪器、测得的各项技术指标做好记录,输出测试报告。公司定期将系统测试的计划进度安排、进展情况、遇到的问题及解决的措施通告客户。
  • 试运行测试:初验测试通过,系统投入试运行,试运行期为3个月。试运行期间由客户按正常维护规范要求进行例行测试,观察并做好维护记录和用户申告故障处理记录。对运行中的告警和各种软硬件故障应进行详细的记录和原因分析,对故障修复、更换坏件、修改软件程序等要有详细的记录。并适时派出经验丰富的技术人员对网络运行情况进行巡视和检查,以确保设备的正常工作。
  • 验收测试:是指进行设备移交时,由双方共同进行的系统测试。公司负责提供详细的初验测试计划(含测试项目),作为客户验收参考,经双方确认后作为初验测试的依据。初验测试检查的基本内容包括硬件安装、设备运行、技术资料及备品备件等。工具准备: 公司提供系统测试内容、终端测试内容、测试方法、测试指标和必要的测试仪器。
  • 系统测试内容:
  1. 软件和硬件终端同时加入同一个会议中,实现视频、音频、辅流互联互通。
  2. 云管理平台支持批量导入软件账号和硬件账号。
  3. 会议中主流和辅流同时支持1080P30fps。
  4. 管理平台支持查看所有的当前会议、预约会议、历史会议和录制的文件。
  5. 管理平台支持全局会议配置,包括入会静音,自动呼叫,会议提醒,语音,带宽限制等。
  6. 支持AVC和SVC混合会议,软终端能够通过双击任意小画面进行大画面观看。
  7. 支持16分屏,默认支持VIP N+1多画面。
  8. 支持自动多画面功能,根据会场入会数量自动增加删除多画面布局。
  9. 支持通过云web界面或者软硬终端通过地址本自主发起多方会议。
  10. Web界面可以预定会议模板,通过会议模板召集会议。
  11. web界面和软终端支持发起会议支持立即会议和预约会议,预约会议可选择日期和时间。
  12. Web界面和软终端可选择发起视频会议或语音数据会议。
  13. web界面和软终端可以选择会议是否进行录制。
  14. 召集会议后,云平台自动通过短信和邮件方式进行会议通知。
  15. 支持会议链接(不限制浏览器)加入会议,或者通过会议号码和密码加入会议。
  16. 软终端上支持共享会议信息给其他人员加入会议。
  17. 软终端支持不登录账号,通过点击链接匿名加入会议。
  18. 软终端加入会议可选择关闭摄像机和麦克风。
  19. 预约的会议,自动推送会议信息给与会者会议日程(软件或硬件终端),到达预定时间自动加入会议或者通过会议日程一键加入会议。
  20. 云web界面和软硬终端界面进行会议控制。
  21. Web界面支持添加与会者、呼叫未入会终端、全场静音、多画面设置、延长会议、结束会议、声控模式、自由讨论模式和轮训模式。
  22. 软硬终端支持打开关闭本地麦克风、扬声器、摄像机。
  23. 软硬终端支持进行主席会控,邀请与会者、静音会场、广播会场和多画面、延长会议等。
  24. 软硬终端支持共享辅流。

1.4. 会议室布置连线

1.4.1. 主会场设备布置连线

主会场使用Box700,连接会议室的视频、音频系统。如下图部署:
Box700背板连接图
1. 视频输入:高清摄像机通过HD-VI三合一线连接终端;用户电脑数据通过HDMI接口连接中断。
2. 视频输出:终端通过HDMI接口输出视频。
3. 音频输入:终端通过RCA接口连接调音台,获取麦克风声音信号。
4. 音频输出:终端通过RCA接口连接调音台,发出声音到音响设备。
5. 网络连接:通过6类网线连接用户网络。
6. 电源连接:通过标准三叉接口连接设备。
会议室显示效果如下:

1.4.2. 分会场设备布置连线

分会场使用Board-65,自带65寸触摸显示屏,内置麦克风、音箱、5K摄像机。仅需要连接电源和网络,如下图部署:

1. 电源连接:通过标准三叉接口连接设备。
2. 网络连接:通过6类网线连接用户网络。
会议室显示效果如下:

1.5. 项目组织构架

一个高效运作的项目组织能够有效的控制项目的质量、成本、进度,给客户带来最大的效益。针对视讯实施服务,公司将组建两级的项目管理结构,配备经验丰富的工程管理人员和技术人员,与客户一起组成项目实施组,确保视讯实施服务高质量、高效率地完成。

  • 项目经理
    负责资源管理、进度控制、质量控制、风险防范、重大问题处理、进展汇报等工作,是本项目公司的总接口人。
  • 技术负责人
    负责设计方案审核、技术交流、培训等工作,是项目现场第一技术责任人。另外协助项目经理进行项目管理工作,对项目小组人员进行技术指导。
  • 远程技术支持专家小组
    由企业视讯实施专家组成,协助项目组进行勘测、设计方案审核,疑难问题处理指导,共享整个技术支持团队的经验。
  • 勘测工程师
    负责设备布局、安装方式以及会议室安装环境的勘测,输出勘测报告。
  • 硬件安装工程师
    组织客户对货物开箱验收,进行设备硬件、电力、传输等实施安装服务,完成上电自检报告。
  • 软件调试工程师
    完成软件的安装、数据配置、版本号、License、软件自检以及软件调试,确保设备上电正常运行。
  • 视讯实施专家
    复杂视讯产品组网方案确定,指导复杂项目的网络构建。现场疑难问题处理指导,通过多年相关经验解决实施中疑难问题。

1.6. 交付件清单

H264 - 码流中SPS PPS详解

  • 1 SPS和PPS从何处而来?
  • 2 SPS和PPS中的每个参数起什么作用?
  • 3 如何解析SDP中包含的H.264的SPS和PPS串?

1 客户端抓包

在做客户端视频解码时,一般都会使用Wireshark抓包工具对接收的H264码流进行分析,如下所示:

在这里我们可以看到对解码视频起关键作用的SPS和PPS。

双击SPS内容如下:

双击PPS内容如下:

那么从上面的sps中我们知道图像的宽,高。

宽=(19+1 )*16=320

高=(14+1)*16=240

为什么?参考下面

2 SPS PPS详解

2.1 SPS语法元素及其含义

在H.264标准协议中规定了多种不同的NAL
Unit类型,其中类型7表示该NAL Unit内保存的数据为Sequence Paramater
Set。在H.264的各种语法元素中,SPS中的信息至关重要。如果其中的数据丢失或出现错误,那么解码过程很可能会失败。SPS及后续将要讲述的图像参数集PPS在某些平台的视频处理框架(比如iOS的VideoToolBox等)还通常作为解码器实例的初始化信息使用。

SPS即Sequence Paramater Set,又称作序列参数集。SPS中保存了一组编码视频序列(Coded
video
sequence)的全局参数。所谓的编码视频序列即原始视频的一帧一帧的像素数据经过编码之后的结构组成的序列。而每一帧的编码后数据所依赖的参数保存于图像参数集中。一般情况SPS和PPS的NAL
Unit通常位于整个码流的起始位置。但在某些特殊情况下,在码流中间也可能出现这两种结构,主要原因可能为:

  • 解码器需要在码流中间开始解码;
  • 编码器在编码的过程中改变了码流的参数(如图像分辨率等);

在做视频播放器时,为了让后续的解码过程可以使用SPS中包含的参数,必须对其中的数据进行解析。其中H.264标准协议中规定的SPS格式位于文档的7.3.2.1.1部分,如下图所示:

其中的每一个语法元素及其含义如下:

(1) profile_idc:

标识当前H.264码流的profile。我们知道,H.264中定义了三种常用的档次profile:

基准档次:baseline profile;

主要档次:main profile;

扩展档次:extended profile;

在H.264的SPS中,第一个字节表示profile_idc,根据profile_idc的值可以确定码流符合哪一种档次。判断规律为:

profile_idc = 66 → baseline profile;

profile_idc = 77 → main profile;

profile_idc = 88 → extended profile;

在新版的标准中,还包括了High、High 10、High 4:2:2、High 4:4:4、High 10 Intra、High
4:2:2 Intra、High 4:4:4 Intra、CAVLC 4:4:4 Intra等,每一种都由不同的profile_idc表示。

另外,constraint_set0_flag ~ constraint_set5_flag是在编码的档次方面对码流增加的其他一些额外限制性条件。

在我们实验码流中,profile_idc = 0x42 = 66,因此码流的档次为baseline profile。

(2) level_idc

标识当前码流的Level。编码的Level定义了某种条件下的最大视频分辨率、最大视频帧率等参数,码流所遵从的level由level_idc指定。

当前码流中,level_idc = 0x1e = 30,因此码流的级别为3。

(3) seq_parameter_set_id

表示当前的序列参数集的id。通过该id值,图像参数集pps可以引用其代表的sps中的参数。

(4) log2_max_frame_num_minus4

用于计算MaxFrameNum的值。计算公式为MaxFrameNum = 2^(log2_max_frame_num_minus4 +
4)。MaxFrameNum是frame_num的上限值,frame_num是图像序号的一种表示方法,在帧间编码中常用作一种参考帧标记的手段。

(5) pic_order_cnt_type

表示解码picture order count(POC)的方法。POC是另一种计量图像序号的方式,与frame_num有着不同的计算方法。该语法元素的取值为0、1或2。

(6) log2_max_pic_order_cnt_lsb_minus4

用于计算MaxPicOrderCntLsb的值,该值表示POC的上限。计算方法为MaxPicOrderCntLsb = 2^(log2_max_pic_order_cnt_lsb_minus4 + 4)。

(7) max_num_ref_frames

用于表示参考帧的最大数目。

(8) gaps_in_frame_num_value_allowed_flag

标识位,说明frame_num中是否允许不连续的值。

(9) pic_width_in_mbs_minus1

用于计算图像的宽度。单位为宏块个数,因此图像的实际宽度为:

frame_width = 16 × (pic\_width\_in\_mbs_minus1 + 1);

(10) pic_height_in_map_units_minus1

使用PicHeightInMapUnits来度量视频中一帧图像的高度。PicHeightInMapUnits并非图像明确的以像素或宏块为单位的高度,而需要考虑该宏块是帧编码或场编码。PicHeightInMapUnits的计算方式为:

PicHeightInMapUnits = pic\_height\_in\_map\_units\_minus1 + 1;

(11) frame_mbs_only_flag

标识位,说明宏块的编码方式。当该标识位为0时,宏块可能为帧编码或场编码;该标识位为1时,所有宏块都采用帧编码。根据该标识位取值不同,PicHeightInMapUnits的含义也不同,为0时表示一场数据按宏块计算的高度,为1时表示一帧数据按宏块计算的高度。

按照宏块计算的图像实际高度FrameHeightInMbs的计算方法为:

FrameHeightInMbs = ( 2 − frame_mbs_only_flag ) * PicHeightInMapUnits

(12) mb_adaptive_frame_field_flag

标识位,说明是否采用了宏块级的帧场自适应编码。当该标识位为0时,不存在帧编码和场编码之间的切换;当标识位为1时,宏块可能在帧编码和场编码模式之间进行选择。

(13) direct_8x8_inference_flag

标识位,用于B_Skip、B_Direct模式运动矢量的推导计算。

(14) frame_cropping_flag

标识位,说明是否需要对输出的图像帧进行裁剪。

(15) vui_parameters_present_flag

标识位,说明SPS中是否存在VUI信息。

2.2 PPS语法元素及其含义

除了序列参数集SPS之外,H.264中另一重要的参数集合为图像参数集Picture Paramater
Set(PPS)。通常情况下,PPS类似于SPS,在H.264的裸码流中单独保存在一个NAL Unit中,只是PPS NAL
Unit的nal_unit_type值为8;而在封装格式中,PPS通常与SPS一起,保存在视频文件的文件头中。

在H.264的协议文档中,PPS的结构定义在7.3.2.2节中,具体的结构如下表所示:

其中的每一个语法元素及其含义如下:

(1) pic_parameter_set_id

表示当前PPS的id。某个PPS在码流中会被相应的slice引用,slice引用PPS的方式就是在Slice header中保存PPS的id值。该值的取值范围为[0,255]。

(2) seq_parameter_set_id

表示当前PPS所引用的激活的SPS的id。通过这种方式,PPS中也可以取到对应SPS中的参数。该值的取值范围为[0,31]。

(3) entropy_coding_mode_flag

熵编码模式标识,该标识位表示码流中熵编码/解码选择的算法。对于部分语法元素,在不同的编码配置下,选择的熵编码方式不同。例如在一个宏块语法元素中,宏块类型mb_type的语法元素描述符为“ue(v)
| ae(v)”,在baseline profile等设置下采用指数哥伦布编码,在main profile等设置下采用CABAC编码。

标识位entropy_coding_mode_flag的作用就是控制这种算法选择。当该值为0时,选择左边的算法,通常为指数哥伦布编码或者CAVLC;当该值为1时,选择右边的算法,通常为CABAC。

(4) bottom_field_pic_order_in_frame_present_flag

标识位,用于表示另外条带头中的两个语法元素delta_pic_order_cnt_bottom和delta_pic_order_cn是否存在的标识。这两个语法元素表示了某一帧的底场的POC的计算方法。

(5) num_slice_groups_minus1

表示某一帧中slice group的个数。当该值为0时,一帧中所有的slice都属于一个slice group。slice group是一帧中宏块的组合方式,定义在协议文档的3.141部分。

(6) num_ref_idx_l0_default_active_minus1、num_ref_idx_l0_default_active_minus1

表示当Slice Header中的num_ref_idx_active_override_flag标识位为0时,P/SP/B
slice的语法元素num_ref_idx_l0_active_minus1和num_ref_idx_l1_active_minus1的默认值。

(7) weighted_pred_flag

标识位,表示在P/SP slice中是否开启加权预测。

(8) weighted_bipred_idc

表示在B Slice中加权预测的方法,取值范围为[0,2]。0表示默认加权预测,1表示显式加权预测,2表示隐式加权预测。

(9) pic_init_qp_minus26和pic_init_qs_minus26

表示初始的量化参数。实际的量化参数由该参数、slice header中的slice_qp_delta/slice_qs_delta计算得到。

(10) chroma_qp_index_offset

用于计算色度分量的量化参数,取值范围为[-12,12]。

(11) deblocking_filter_control_present_flag

标识位,用于表示Slice header中是否存在用于去块滤波器控制的信息。当该标志位为1时,slice header中包含去块滤波相应的信息;当该标识位为0时,slice header中没有相应的信息。

(12) constrained_intra_pred_flag

若该标识为1,表示I宏块在进行帧内预测时只能使用来自I和SI类型宏块的信息;若该标识位0,表示I宏块可以使用来自Inter类型宏块的信息。

(13) redundant_pic_cnt_present_flag

标识位,用于表示Slice header中是否存在redundant_pic_cnt语法元素。当该标志位为1时,slice header中包含redundant_pic_cnt;当该标识位为0时,slice header中没有相应的信息。

3 解析SDP中包含的H.264的SPS和PPS串

用RTP传输H264的时候,需要用到sdp协议描述,其中有两项:Sequence Parameter Sets (SPS)
和Picture Parameter Set (PPS)需要用到,那么这两项从哪里获取呢?答案是从H264码流中获取.在H264码流中,都是以”0x00 0x00 0x01”或者”0x00 0x00 0x00 0x01”为开始码的,找到开始码之后,使用开始码之后的第一个字节的低5位判断是否为7(sps)或者8(pps), 及data[4] & 0x1f == 7 || data[4] & 0x1f == 8
然后对获取的nal去掉开始码之后进行base64编码,得到的信息就可以用于sdp.sps和pps需要用逗号分隔开来.

SDP中的H.264的SPS和PPS串,包含了初始化H.264解码器所需要的信息参数,包括编码所用的profile,level,图像的宽和高,deblock滤波器等。

由于SDP中的SPS和PPS都是BASE64编码形式的,不容易理解,有一个工具软件可以对SDP中的SPS和PPS进行解析,下载地址:http://download.csdn.net/download/davebobo/9898045

用法是在命令行中输入:

spsparser sps.txt pps.txt output.txt

例如sps.txt中的内容为:

Z0LgFNoFglE=

pps.txt中的内容为:

aM4wpIA=

最终解析得到的结果为:

这里需要特别提一下这两个参数

pic_width_in_mbs_minus1 = 21

pic_height_in_mbs_minus1 = 17

分别表示图像的宽和高,以宏块(16x16)为单位的值减1

因此,实际的宽为 (21+1)*16 = 352 高为 (17+1)*16 = 288

到这里应该知道第一部分客户端抓包计算图像宽高遗留下来的问题了吧。

Reference:

http://www.cnblogs.com/lidabo/p/6553305.html

http://blog.csdn.net/heanyu/article/details/6205390

http://blog.csdn.net/shaqoneal/article/details/52771030

http://blog.csdn.net/shaqoneal/article/details/52877689

如何设计一套RTC PaaS服务?

目前实时音视频通信RTC(Real-Time Communication) 服务常见的有两种形态:SaaS(Software as a Service)和PaaS(Platform as Service)[1]

SaaS服务往往是一个大的集合。如在线教育场景,不仅包含RTC服务,还包含一系列线上上课流程所需的基础服务保障和提升上课效率、体验的一些列教具,如即时通信IM服务、课程管理系统、互动课件和电子白板、点播回放等。

PaaS服务则聚焦音视频实时通信的能力,依托音频3A前处理(回声消除、噪声抑制和自动增益控制算法)、音视频编解码、信道传输、弱网对抗、网络调度等技术,为用户提供高可用、高品质、低延时的音视频通信服务。用户可通过集成不同平台的SDK,快速搭建多端实时应用,在项目中实现语音通话、视频通话、音频互动直播、视频互动直播等,适用于在线教育、视频会议、互动娱乐、音视频社交等场景。

1、面临的问题和挑战

首先从需求和问题出发,对于公有云RTC PaaS服务来说,先抛开不同平台音视频设备采集、编码、网络发送、网络接收、解码、渲染的差异不谈,主要围绕的问题就是如何在当前复杂、不稳定的公有云环境下提供高可用、高质量、低延时的音视频通话服务,而其中几个主要影响音视频QOS(Quality of Service)的因素[2]主要包含以下几个方面:

  • 网络延时(Latency/Delay)
  • 网络丢包(Packect Loss)
  • 网络乱序(Out-of-order delivery)
  • 网络抖动(Jitter)
  • 网络拥塞(Bandwidth Overuse/network congestion)

所以如何设计服务的架构,提供一张优质、高效的网络传输链路和一系列保证弱网情形下音视频QOS的手段或策略,则显得尤为重要。对于网络这一层,往往是优先解决用户最后一公里的问题,这也是跟传统的边缘节点或者CDN的思路一脉传承。

2、RTC服务架构

按照功能划分,设计一套完整的RTC PaaS服务[3],需要完成:

  • 调度服务器(Coordinator)

    因为无论如何优化压缩算法和代码,网络仍然是贡献大部分延时至关重要的因素,服务架构需提供一种机制,保证用户就近接入负载最低的最优媒体节点,来减少RTT(Round Trip Time)和尽可能保证用户最后一公里的网络质量,这就是调度服务器的价值所在。

    如上图steps 1、2所示,当新用户接入RTC服务时,首先请求调度服务器获取可用媒体服务器的列表,调度服务器通过请求中携带的用户的外网IP获取用户的地理位置和运营商信息,按照一定的策略返回媒体服务器列表,然后在step 3,客户端通过ping消息探测与所有候选媒体服务器之间的实时网络状况,从而选择最佳的媒体服务器进行推拉流。这些探测获取到的网络状况列表,特别是延时信息,将稍后被反馈发送至调度服务器,以致于它能够更加准确地分配最合适的媒体服务器,服务于后续新的用户。

  • 信令服务器(Signaling Server)

    设计一个视频会议系统,信令协商和消息交互是一个必不可少的部分,不过我一直主张:「轻信令重媒体」「在保证必要消息交互的前提下尽可能减少消息交互的次数和消息体的大小,怎么简单怎么来」。对于信令服务器的设计,建议简洁、够用即可。如上图所示,建议一种思路,将**「轻薄」**的信令服务也放在媒体服务器这一侧,这样不仅维护起来比较方便,而且媒体服务器能够独立于其他服务对外提供服务,即使其他服务出现异常也不受影响。另外测试和排查问题也更加高效便捷。缺点就是对于调度服务器来说,获取到的用户媒体信息有限,对于负载消耗的预估、控制不能很精细。

    在这里,想讨论一个问题,「RTC PaaS服务一定需要会议、Channel或者房间的概念吗?」

    对于这个问题,我认为可以抽象为**「长连接vs短连接」**的问题, 因为对于同一个会议或者房间这种概念,听的更多的是来自于IM即时通讯,在房间里的人能够立马收到或者消费其他人发送的消息,而且能感知其他人在线或者离线的状态。对于RTC PaaS服务来说,目前有两种方式供用户使用:

    1. 「无会议、房间的概念,不打包长连接服务」。使用方在使用SDK进行推拉流时,不用先JoinChannel或者JoinRoom,只需通过调度服务器拿到媒体服务列表之后选择一个最优节点进行推拉流即可。而此时RTC PaaS服务本身无法感知与会者的加入和离开,对于拉流动作只能依赖SDK的定时不断重试去保证,不能提供消息的发布(Publish)和订阅(Subscribe)。好处当然是不用去新开发一个类似IM的服务,要做一个稳定高效的长连接IM服务同样不是很容易,而且开发逻辑和调用流程都很简单;相反,就必须牺牲一些东西,如SDK侧拉流动作需要不断重试,这个就显得不是很优雅,另外由于缺乏长连接,服务端无法感知端上的实际在线状态,如客户端Crash,很多消息无法正常触发。对于某些任务session或记录的销毁严重依赖端上消息的抵达,否则的话就只能依赖定时超时去清理。

    适合的场景:很多业务使用方往往有自己的IM即时通信服务,因为现在的app在线长连接和直播间内发消息、点赞、送礼物等基本上已成为标配,所以可以依赖偏上层业务的信道消息去实现消息的发布和订阅,从而决定推拉流的时机。
    2. 「有会议、房间的概念,打包长连接服务」。与第一种情况相反,这种情况下就需要为使用方提供稳定高效的IM服务,一般情况下,可使用websocket+消息队列(如RabbitMQ)的方式来实现,使用方在使用SDK进行推拉流前,必须先JoinChannel或者JoinRoom,然后才能订阅或者消费消息,从而在其他人加入、离开房间或者推流时收到通知后去处理相应的回调。对应的由于长连接的存在,服务端能够准确感知客户端的状态,当用户离线时,及时清理任务session或记录。

    适合的场景:有些小公司没有能力开发自己的IM服务,从他们的角度来说,给我提供一整完整的解决方案即可,我不需要care其他的技术细节,只需要知道什么样的事件回调执行什么调用即可。

    在信令层面上,还想讨论的是在抗弱网的表现上,tcp明显不如udp,所以往往会存在媒体传输还可以信令却无法建立的情形,所以建议在信令层面上可以尝试udp的可靠传输,如QUIC等。

  • 媒体服务器(Media Server或者Streaming Server)

    所有媒体服务器是完全独立的,当一个服务器加入或者离开RTC网络时,系统能自动调整它的分配,不让用户有感知和影响。在高负载的情况下,可以通过新增机器部署服务来进行**「水平扩展」**(horizontal scaling)。

    媒体服务器一般按照网络拓扑架构可以划分为两大类[4]:「SFU(Selected Forward Unit)「和」MCU(Multipoint Control Unit)」

如图所示,SFU只负责媒体流的转发,不做太多复杂的媒体处理,瓶颈在IO,并发能力强;劣势是下行转发路数多,带宽占用高,对用户侧带宽和设备的要求高。MCU除了媒体流的接收/发送,还要进行转码和混流,对服务器性能要求高,服务部署成本高,瓶颈在CPU,并发能力差,转发流数少,而且实时传输系统中,转码会带来额外的延时,实时性稍差;优势就是下行带宽占用少,对用户侧带宽和设备的要求低。

对于实时性和高并发有强要求的会议场景,更多的是选择SFU架构。随着未来5G技术的推广,可以预见带宽越来越不是问题,SFU的优势会更加明显。

然而在实际业务场景中,服务架构往往是**「SFU+MCU混合」**的方式去满足业务的需求。那么问题来了,是将MCU和SFU做在一起呢?即该服务既可以转发也可以混流,还是SFU和MCU完全独立,转发路径上的媒体节点都是单纯的SFU呢?

因为从媒体处理流程和功能层面上,SFU可视为MCU的子集,MCU支持一定的转发能力,比如混流完成后可选择不同的媒体封装输出方式,可直接被客户端拉流(这个就是MCU也具有SFU的功能了)。

如图所示,建议将不同能力分由不同的节点来实现[5],如转发服务SFU节点负责媒体的转发和跨国级联,混流服务器MCU节点负载转码、混音和混流,因为这样服务的设计能尽量简单,耦合性低,同时对于不同类型的服务调度策略也能简单很多,降低整体系统的复杂度和提高效率。

对于一套完整的RTC PaaS服务来说,往往需要将各种能力补齐,除了上述的媒体服务类型以外,还需要:

「云录制服务器(RTP to flv or mp4)「和」云转推服务器(RTP to RTMP)」,云录制主要用于回放点播,云转推主要用于旁路直播,在1vsN的场景下减少转发节点的压力,降低成本。如上图所示,SFU和MCU都可作为云录制和云转推的输入端,类比选择不同的媒体封装输出方式。另外也可使用CDN RTMP直播的录制来做回放点播,不过链路增加了,相应出现问题的概率也增大了,需结合着业务形态和研发阶段进行合理的选择。

如果是走的私有协议,为了支持Web端,还需要设计开发**「WebRTC网关」**,进行信令和媒体的转换。

3、网络架构演进

历史总是惊人的相似,可以参考RTMP直播架构的发展和演进,因为从本质上网络这一层面上RTMP直播和RTC要解决的问题是一样的。

  • 第一阶段,「单区域源站模式」,各个区域都从源站上进行推拉流。如上图所示,很显然跨地域跨运营商时推拉流效果是无法保证的。

  • 第二阶段,「多区域多线BGP+专线级联回源模式」,如上图所示,能有效改善用户与服务器之间的网络状况。阿里云ECS为多线BGP机房,不同区域间通过拉专线保持级联互通,相比运营商的主干网网络状况会稳定很多;但这种模式成本很高,多线BGP机房按照流量收费单价很贵,同时专线成本更高;另外对于阿里云ECS来说,并不是每个地区都有相应的VPC,存在一些区域无法正常覆盖,而且每引入新的节点,可能就需要重新购买专线,节点与节点之间的专线有的还受物理因素的限制,如国内到国外的出海光缆只有从华东上海出。

    这一阶段就需要根据实际的需求进行**「合理的区划」「选点部署」「定制一定的调度策略」**,如同一区域一个room尽可能分配到同一台服务器上来避免级联;根据实际布点的网络拓扑动态生成路由,用户拉流时,通过调度服务器选择最优路径,动态回源;在一定情形下避免专线带宽的消耗,同时监控专线带宽的消耗,当超过一定阈值的情况下报警和使用外网通信,避免雪崩。

  • 第三阶段,「多区域边缘节点(ENS)接入+多线BGP(ECS)跨区域跨运营商中转+云企业网(CEN)模式」。如上图所示(偷了个懒~😊),同一区域同一运营商就通过边缘节点进行中转;不同区域或者不同运营商的情况下通过ECS进行级联中转,不同ECS通过云企业网进行彼此互通,专线连接。相比ECS节点,边缘节点ENS覆盖范围更广,成本更低,网络拓扑可扩展性更强;云企业网能够帮助用户在引入新的VPC节点时省去两两拉专线的麻烦,而且能共享专线之间的带宽资源,同时不用关心实际专线网络拓扑的结构和物理限制,将VPC之间的路由交由阿里云来保障,这样使用起来更加地便捷,节点扩展起来更加地方便高效。不得不说,阿里云确实是在想着解决用户的痛点,成就客户的同时就是成就自己。
  • 第四阶段,「边缘节点CDN+WebRTC标准化模式」。这个也是参考RTMP直播发展的历史规律来看的,还处于理想未实现的状态。据我所知,各大云厂商也在纷纷发力。云厂商在边缘节点CDN服务支持标准的WebRTC协议,用户不需要维护自己的源站和服务器,只需要就近接入最优的CDN进行推拉流即可,网络链路这一层由云厂商来保证。不过真正要实现仍需很长时间的等待。未来对于小厂来说,只需要按照标准进行信令协商和媒体推拉流,就可以进行实时音视频互动,不仅在开发和维护成本上大大降低,服务的稳定性上也有更大的保障,不过对于那些有特殊的定制化需求和场景的,则相应的灵活性会差一些。

4、音视频行业发展的趋势和归宿

互联网的高速发展,加快了信息的传播,改变了人们的生活,同样促进着技术的不断更新与迭代。伴随着人们孜孜不倦地对于音频、视频感官体验的不断追求,各种形态的音视频应用应运而生,冲击着人们的生活,改变着人们的习惯。而价值驱动技术的发展,各种技术由兴起到盛行,再到大行其道,门槛由高转低,小作坊成本高昂变成统一白菜价。互联网不断革着世俗的命,技术的发展也在不断革着程序猿的命,抱着一门技术坐吃山空的时代早已不复存在,留下的都是大浪淘沙的不断前行者和35岁的伤痛。音视频行业相对于其他领域专业化程度相对较高一些,但依然逃不过社会化大生产的规律。

前前后后,国内音视频行业的人才积累大致来自于三个时代:

  • 「播放器时代。「对应的应用形态是」点播和短视频」,点播代表app有腾讯视频、爱奇艺、优酷、芒果TV,没落的搜狐视频,乐视、土豆等,短视频代表app有抖音和快手。需要的技术栈就是播放器 + ffmpeg,致敬ffmpeg,真是养活了不少人。

  • 「RTMP直播的时代。「对应的应用形态是」**娱乐直播、体育直播、直播秀场、直播带货、在线教育大班课」**等,直播代表app有虎牙、斗鱼、映客,倒下的熊猫等,今年疫情以来,直播带货很火,各家电商似乎不做都不太好意思。直播兴起的早期有自建源站和CDN的,到有专门做CDN的厂商,如网宿、蓝汛、帝联、白云山等,渐渐地腾讯云、阿里云、金山云、华为云等巨头入局CDN,直播研发的人力、维护成本越来越低。

    直播客户端比较成熟的开源项目首推OBS,服务端常用的是nginx-rtmp-moduleSRS

  • 「RTC的时代。「对应的应用形态是」**连麦、音视频通话、视频会议、在线教育1vs1和小组课」**等,老牌传统的视频会议厂商有思科Cisco、宝利通Polycom、华为HuaWei、新晋新秀Zoom,还有低调的Vidyo等。视频通话软件有Apple FaceTime、Microsoft Skype、Google Hangouts & Duo等。国内比较有名的RTC PaaS厂商有声网Agora、即构科技Zego、腾讯云和阿里云等。

    虽然VOIP和视频会议在国外已发展多年,但在2017以前仍然是小众市场,技术人才积累有限。不得不提的就是Google 2011年开源的WebRTC,在WebRTC出现之前,音视频实时通信是高不可攀的领域,需要大量的专业积累才能入门,而现在,越来越多的开发者通过WebRTC来深入了解RTC技术。我也有幸在2014年与她相遇,期待着WebRTC标准大一统的时刻到来,跟着Google老铁混有肉吃。

    参考上面“网络架构演进”中第四阶段阐述的那样,如直播的发展套路一样,渐渐地RTC只剩下两种形态:

    1. 云厂商的WebRTC CDN,用户只需要使用云厂商的SDK或者自己定制SDK,就近推到最优的CDN节点,网络链路由云厂商去保证,既支持私有云也支持公有云部署,用户可选择附加的云录制、云转推、转码混流服务等。
    2. 不触及底层,结合RTC技术app前端业务形态的不断创新和小厂特殊化或者安全性的需求,选择自建。

在当前这个时代,永远不变的就是变化。从一个技术开发者的角度,我们阻挡不了历史的洪流,能做的唯有顺应和适应,打铁还需自身硬,我们能做的就是打好基础,不断地迎接挑战,力争精通和专业化。

欢迎大家留言交流,欢迎大家关注我个人公众号!

Reference

[1]

IaaS,PaaS,SaaS 的区别:

http://www.ruanyifeng.com/blog/2017/07/iaas-paas-saas.html

[2]

preparing-ip-network-video-conferencing:

https://support.polycom.com/content/dam/polycom-support/products/uc-infrastructure-support/management-scheduling/dma/other-documents/en/preparing-ip-network-video-conferencing.pdf

[3]

Open Source Cloud Gaming with WebRTC:

https://webrtchacks.com/open-source-cloud-gaming-with-webrtc/

[4]

从入门到进阶|如何基于WebRTC搭建一个视频会议:

https://zhuanlan.zhihu.com/p/130406233

[5]

即构实时网络调度系统如何应对跨国场景挑战:

https://zhuanlan.zhihu.com/p/47951276

本文使用 mdnice 排版

作者:angrychuan
链接:https://juejin.cn/post/6881188316943908872
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

M3U8格式解析

HLS流可以用于直播,也可以用于点播

M3U8 文件实质是一个播放列表(playlist),其可能是一个媒体播放列表(Media Playlist),或者是一个主列表(Master Playlist)。

1.M3U8类型

当 M3U8 文件作为媒体播放列表(Media Playlist)时,其内部信息记录的是一系列媒体片段资源,顺序播放该片段资源,即可完整展示多媒体资源。其格式如下所示:

1
2
3
4
5
6
7
8
9
10
#EXTM3U
#EXT-X-TARGETDURATION:10

#EXTINF:9.009,
http://media.example.com/first.ts
#EXTINF:9.009,
http://media.example.com/second.ts
#EXTINF:3.003,
http://media.example.com/third.ts
#EXT-X-ENDLIST

复制

当 M3U8 作为主播放列表(Master Playlist)时,其内部提供的是同一份媒体资源的多份流列表资源。其格式如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=150000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
http://example.com/low/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=240000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
http://example.com/lo_mid/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=440000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
http://example.com/hi_mid/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=640000,RESOLUTION=640x360,CODECS="avc1.42e00a,mp4a.40.2"
http://example.com/high/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS="mp4a.40.5"
http://example.com/audio/index.m3u8
#EXT-X-ENDLIST

复制

2.M3U8基本字段

1
2
3
4
5
6
7
8
9
10
#EXTM3U                    M3U8文件头,必须放在第一行;
#EXT-X-MEDIA-SEQUENCE 第一个TS分片的序列号,一般情况下是0,但是在直播场景下,这个序列号标识直播段的起始位置; #EXT-X-MEDIA-SEQUENCE:0
#EXT-X-TARGETDURATION 每个分片TS的最大的时长; #EXT-X-TARGETDURATION:10 每个分片的最大时长是 10s
#EXT-X-ALLOW-CACHE 是否允许cache; #EXT-X-ALLOW-CACHE:YES #EXT-X-ALLOW-CACHE:NO 默认情况下是YES
#EXT-X-ENDLIST M3U8文件结束符;
#EXTINF extra info,分片TS的信息,如时长,带宽等;一般情况下是 #EXTINF:\<duration\>,[\<title\>] 后面可以跟着其他的信息,逗号之前是当前分片的ts时长,分片时长 移动要小于 #EXT-X-TARGETDURATION 定义的值;
#EXT-X-VERSION M3U8版本号
#EXT-X-DISCONTINUITY 该标签表明其前一个切片与下一个切片之间存在中断。下面会详解
#EXT-X-PLAYLIST-TYPE 表明流媒体类型;
#EXT-X-KEY 是否加密解析, #EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52" 加密方式是AES-128,秘钥需要请求 https://priv.example.com/key.php?r=52 ,请求回来存储在本地;

复制

3.如何判断M3U8是否直播

  • 1.判断是否存在 #EXT-X-ENDLIST

对于一个M3U8文件,如果结尾不存在 #EXT-X-ENDLIST,那么一定是 直播,不是点播;

  • 2.判断 #EXT-X-PLAYLIST-TYPE 类型

‘#EXT-X-PLAYLIST-TYPE’ 有两种类型,

VOD 即 Video on Demand,表示该视频流为点播源,因此服务器不能更改该 M3U8 文件;

EVENT 表示该视频流为直播源,因此服务器不能更改或删除该文件任意部分内容(但是可以在文件末尾添加新内容)(注:VOD 文件通常带有 EXT-X-ENDLIST 标签,因为其为点播片源,不会改变;而 EVEVT 文件初始化时一般不会有 EXT-X-ENDLIST 标签,暗示有新的文件会添加到播放列表末尾,因此也需要客户端定时获取该 M3U8 文件,以获取新的媒体片段资源,直到访问到 EXT-X-ENDLIST 标签才停止)。

4.M3U8多码率

上面的Master Playlist 就是会提供 多码率的列表资源,如下:

1
2
3
4
5
6
7
8
9
10
11
12
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=150000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
http://example.com/low/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=240000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
http://example.com/lo_mid/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=440000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
http://example.com/hi_mid/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=640000,RESOLUTION=640x360,CODECS="avc1.42e00a,mp4a.40.2"
http://example.com/high/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS="mp4a.40.5"
http://example.com/audio/index.m3u8
#EXT-X-ENDLIST

复制

‘#EXT-X-STREAM-INF’ 字段后面有:

BANDWIDTH 指定码率

RESOLUTION 分辨率

PROGRAM-ID 唯一ID

CODECS 指定流的编码类型

码率、码流是同一个概念,是数据传输时单位时间传送的数据量,一般用单位kbps表示。

视频码率就是指视频文件在单位时间内使用的数据量。简单理解就是要播放一秒的视频需要多少数据,从这个角度就不难理解通常码率越高视频质量也越好,相应的文件体积也会越大。码率、视频质量、文件体积是正相关的。但当码率超过一定数值后,对图像的质量影响就不大了。几乎所有的编码算法都在追求用最低的码率达到最少的失真(最好的清晰度)。

5.如何在M3U8中插入广告

M3U8文件中插入广告,要想灵活的控制广告,则广告可以插入任何视频中,那么无法保证广告的编码格式和码率等信息和原视频的编码格式等信息保持一致,就必须告知播放器,在插入广告的地方,ts片段发生的信息变更,需要播放器适配处理。

‘#EXT-X-DISCONTINUITY’ 该标签表明其前一个切片与下一个切片之间存在中断。说明有不连续的视频出现,这个视频绝大多数情况下就是广告; ‘#EXT-X-DISCONTINUITY’ 这个字段就是来做这个事情的;

下面展示一个插入广告的例子:

1
2
3
4
5
6
7
8
9
10
#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-VERSION:4
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:10.0,
movieA.ts
#EXTINF:10.0,
movieB.ts
...
#EXT-X-ENDLIST

复制

想在开头插入广告:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-VERSION:4
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:10.0,
ad0.ts
#EXTINF:8.0,
ad1.ts
#EXT-X-DISCONTINUITY
#EXTINF:10.0,
movieA.ts
#EXTINF:10.0,
movieB.ts
...
#EXT-X-ENDLIST

复制

当然你可以在任意位置插入广告。

HLS协议草案:HLS协议中还有很多字段,但是有些字段其实就是协议,在实际应用中并不大;大家可以参考看看;https://tools.ietf.org/html/rfc8216

M3U8实战

将一个mp4视频转化为m3u8视频:

1
ffmpeg -re -i test.mp4 -c copy -f hls -bsf:v h264_mp4toannexb test.m3u8

复制

生成了一个m3u8和很多ts分片:

但是生成的test.m3u8发现了问题,如下,才有5个分片。这是因为ffmpeg 默认的list size 为5,所以只获得最后的5个片段。

要想解决这个问题,需要指定参数-hls_list_size 0,这样就能包含所有的片段。加上 -hls_list_size 0

1
ffmpeg -re -i test.mp4 -c copy -f hls -hls_list_size 0  -bsf:v h264_mp4toannexb test.m3u8

复制

为什么还要加上参数-bsf:v h264_mp4toannexb ?

这个参数的作用是将MP4中的H.264数据转换成为H.264 AnnexB标准的编码,AnnexB标准的编码常见于实时传输流中。如果源文件为FLV、TS等可以作为直播传输流的视频,则不需要这个参数。

这也非常强烈地说明了MP4不是流式文件,不能作为直播使用。

学习思考

1.视频广告

目前M3U8视频占我们线上视频的比例是近60%,量非常大,我们可以在M3U8视频中任意位置插入一些广告,为探索商业化开辟新的路。

2.为什么M3U8中分片使用TS不用MP4

这是因为两个 TS 片段可以无缝拼接,播放器能连续播放,而 MP4 文件由于编码方式的原因,两段 MP4 不能无缝拼接,播放器连续播放两个 MP4 文件会出现破音和画面间断,影响用户体验。而且如果要在一段长达一小时的视频中跳转,如果使用单个 MP4 格式的视频文件,如果也用 HTTP 协议,那么需要代理服务器支持 HTTP range request 获取大文件中的一部分。这样的话,对于代理服务器的性能来说要求较高。而 HTTP Live Streaming 则只需要根据列表文件中的时间轴找出对应的 TS 片段下载即可,不需要 range request,对代理服务器的要求小很多。所有代理服务器都支持小文件的高效缓存。

G711编解码原理及代码实现

G711也称为PCM(脉冲编码调制),是国际电信联盟制定出来的一套语音压缩标准,主要用于电话。G711编码的声音清晰度好,语音自然度高,但压缩效率低,数据量大常在32Kbps以上(推荐使用64Kbps)。它主要用脉冲编码调制对音频采样,采样率为8KHz。它利用一个64Kbps未压缩通道传输语音讯号。其压缩率为1:2,即把16位数据压缩成8位。G.711是主流的波形声音编解码器。

G.711 标准下主要有两种压缩算法。一种是µ-law algorithm (又称often u-law, ulaw, mu-law),主要运用于北美和日本;另一种是a-law algorithm,主要运用于欧洲和世界其他地区。其中,后者是特别设计用来方便计算机处理的。这两种算法都使用一个采样率为8kHz的输入来创建64Kbps的数字输出。

1、a-law

a-law也叫g711a,输入的是13位(其实是S16的高13位),使用在欧洲和其他地区,这种格式是经过特别设计的,便于数字设备进行快速运算。在WAV文件中的识别标志是 WAVE_FORMAT_ALAW。

目前最主要的用途是将13bit的数据转化成为8bit的数据,13bit的最高位是符号位,转化成为的8bit并不是线性的,而是每几位都有其意义。

第7位: 代表符号位,1表示正数,0表示负数,这点和一般的计算机系统正好相反

第4-6位: 这实际上是一个表示一种幂级数的位

第0-3位: 是具体的量化数值

运算过程如下:

(1) 取符号位并取反得到s,

(2) 获取强度位eee,获取方法如图所示

(3) 获取高位样本位wxyz

(4) 组合为seeewxyz,将seeewxyz逢偶数位取反,编码完毕

示例:

输入pcm数据为3210,二进制对应为(0000 1100 1000 1010)

二进制变换下排列组合方式(0 0001 1001 0001010)

(1) 获取符号位最高位为0,取反,s=1

(2) 获取强度位0001,查表,编码制应该是eee=100

(3) 获取高位样本wxyz=1001

(4) 组合为11001001,逢偶数位取反为10011100

编码完毕。

编码代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#define MAX  (32635)   
void encode(unsigned char *dst, short *src, size_t len)
{
for(int i = 0; i < len ; i++)
{

// *dst++ = *src++;
short pcm = *src++;
int sign = (pcm & 0x8000) >> 8;
if(sign != 0)
pcm = -pcm;
if(pcm > MAX) pcm = MAX;
int exponent = 7;
int expMask;
for(expMask = 0x4000; (pcm & expMask) == 0 && exponent >0; exponent--,expMask >>= 1){}
int mantissa = (pcm >> ((exponent == 0) ? 4 : (exponent + 3))) & 0x0f;
unsigned char alaw = (unsigned char)(sign | exponent << 4 | mantissa);
*dst++ = (unsigned char)(alaw ^0xD5);
}
}

译码代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

void decode(short *dst, unsigned char *src, size_t len)
{
for(size_t i=0; i < len ; i++)
{
unsigned char alaw = *src++;
alaw ^= 0xD5;
int sign = alaw & 0x80;
int exponent = (alaw & 0x70) >> 4;
int data = alaw & 0x0f;
data <<= 4;
data += 8; //丢失的a 写1
if(exponent != 0) //将wxyz前面的1补上
data += 0x100;
if(exponent > 1)
data <<= (exponent - 1);

*dst++ = (short)(sign == 0 ? data : -data);

}
}

从编解码的过程中,不难看出这种编码方式的思路:

首先,我们确定目的,原始的码流是13bit,除去符号位,12bit;我们需要将其转化成为8bit,除去符号位,还有7bit。

基本是思想就是:记录符号位,记录下最有效数据位(也就是除去符号位第一个1)后的4位,记录下这4位移动到最低位所需的次数(放在0-3位中),解码的时候根据这些信息还原,也就是最高的5位会是准确的,后面紧跟一个1是用来补偿舍弃的数据,这就是误差的来源。

但是在实际的操作中,是会直接将最后4位舍弃,然后在开始保留数据操作,也就是要是有效数据小于4位,最后全舍弃了,就会补偿为8。若大于4位小于8位,最后准确的数据可能就不会有5位了。

2、µ-law

µ-law也叫g711µ,使用在北美和日本,输入的是14位,编码算法就是查表,没啥复杂算法,就是基础值+平均偏移值,具体示例如下:

pcm=2345

(1)取得范围值

+4062 to +2015 in 16 intervals of 128

1

(2)得到基础值0x90,

(3)间隔数128,

(4)区间基本值4062,

(5)当前值2345和区间基本值差异4062-2345=1717,

(6)偏移值=1717/间隔数=1717/128,取整得到13,

(7)输出为0x90+13=0x9D

为了简化编码过程,原始的线性幅度增加了33,使得编码范围从(0 - 8158)变为(33 - 8191)。结果如下表所示:

Biased Linear Input Code Compressed Code
00000001wxyza 000wxyz
0000001wxyzab 001wxyz
000001wxyzabc 010wxyz
00001wxyzabcd 011wxyz
0001wxyzabcde 100wxyz
001wxyzabcdef 101wxyz
01wxyzabcdefg 110wxyz
1wxyzabcdefgh 111wxyz

每个偏置线性输入码都有一个前导1来标识段号。段号的值等于7减去前导0的个数。量化间隔的个数是直接可用的四个位wxyz。后面的位(a - h)被忽略。

由上可见,这种编码方式和a-Law是极为类似的,是将14bit的数据编码成为8bit的数据(最高位都是符号位),8bit的每位数据的意义和a-Law编码是一致的。但是和a-Law不同的是,这是对14bit的数据进行压缩,并且压缩完后的数据不仅仅是偶数位取反,而是每一位都需要做取反操作。

并且,注意到表中,这里的最高位都是从1开始,和a-Law相比,这里加了一位,但是seg还是3bit,所以对照两表的第一个数是有明显差异的,于是每个数加33是很有必要的,然后在解码的时候减去,相当于移位就直接减少了一种情况。

注意:在g711.c代码中,BIAS的值是0x84,该值是由33<<2得来的。

g711.c代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
/*
* This source code is a product of Sun Microsystems, Inc. and is provided
* for unrestricted use. Users may copy or modify this source code without
* charge.
*
* SUN SOURCE CODE IS PROVIDED AS IS WITH NO WARRANTIES OF ANY KIND INCLUDING
* THE WARRANTIES OF DESIGN, MERCHANTIBILITY AND FITNESS FOR A PARTICULAR
* PURPOSE, OR ARISING FROM A COURSE OF DEALING, USAGE OR TRADE PRACTICE.
*
* Sun source code is provided with no support and without any obligation on
* the part of Sun Microsystems, Inc. to assist in its use, correction,
* modification or enhancement.
*
* SUN MICROSYSTEMS, INC. SHALL HAVE NO LIABILITY WITH RESPECT TO THE
* INFRINGEMENT OF COPYRIGHTS, TRADE SECRETS OR ANY PATENTS BY THIS SOFTWARE
* OR ANY PART THEREOF.
*
* In no event will Sun Microsystems, Inc. be liable for any lost revenue
* or profits or other special, indirect and consequential damages, even if
* Sun has been advised of the possibility of such damages.
*
* Sun Microsystems, Inc.
* 2550 Garcia Avenue
* Mountain View, California 94043
*/

/*
* g711.c
*
* u-law, A-law and linear PCM conversions.
*/
#define SIGN_BIT (0x80) /* Sign bit for a A-law byte. */
#define QUANT_MASK (0xf) /* Quantization field mask. */
#define NSEGS (8) /* Number of A-law segments. */
#define SEG_SHIFT (4) /* Left shift for segment number. */
#define SEG_MASK (0x70) /* Segment field mask. */

static short seg_end[8] = {0xFF, 0x1FF, 0x3FF, 0x7FF,
0xFFF, 0x1FFF, 0x3FFF, 0x7FFF};

/* copy from CCITT G.711 specifications */
unsigned char _u2a[128] = { /* u- to A-law conversions */
1, 1, 2, 2, 3, 3, 4, 4,
5, 5, 6, 6, 7, 7, 8, 8,
9, 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24,
25, 27, 29, 31, 33, 34, 35, 36,
37, 38, 39, 40, 41, 42, 43, 44,
46, 48, 49, 50, 51, 52, 53, 54,
55, 56, 57, 58, 59, 60, 61, 62,
64, 65, 66, 67, 68, 69, 70, 71,
72, 73, 74, 75, 76, 77, 78, 79,
81, 82, 83, 84, 85, 86, 87, 88,
89, 90, 91, 92, 93, 94, 95, 96,
97, 98, 99, 100, 101, 102, 103, 104,
105, 106, 107, 108, 109, 110, 111, 112,
113, 114, 115, 116, 117, 118, 119, 120,
121, 122, 123, 124, 125, 126, 127, 128};

unsigned char _a2u[128] = { /* A- to u-law conversions */
1, 3, 5, 7, 9, 11, 13, 15,
16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31,
32, 32, 33, 33, 34, 34, 35, 35,
36, 37, 38, 39, 40, 41, 42, 43,
44, 45, 46, 47, 48, 48, 49, 49,
50, 51, 52, 53, 54, 55, 56, 57,
58, 59, 60, 61, 62, 63, 64, 64,
65, 66, 67, 68, 69, 70, 71, 72,
73, 74, 75, 76, 77, 78, 79, 79,
80, 81, 82, 83, 84, 85, 86, 87,
88, 89, 90, 91, 92, 93, 94, 95,
96, 97, 98, 99, 100, 101, 102, 103,
104, 105, 106, 107, 108, 109, 110, 111,
112, 113, 114, 115, 116, 117, 118, 119,
120, 121, 122, 123, 124, 125, 126, 127};

static int
search(
int val,
short *table,
int size)
{
int i;

for (i = 0; i < size; i++) {
if (val <= *table++)
return (i);
}
return (size);
}

/*
* linear2alaw() - Convert a 16-bit linear PCM value to 8-bit A-law
*
* linear2alaw() accepts an 16-bit integer and encodes it as A-law data.
*
* Linear Input Code Compressed Code
* ------------------------ ---------------
* 0000000wxyza 000wxyz
* 0000001wxyza 001wxyz
* 000001wxyzab 010wxyz
* 00001wxyzabc 011wxyz
* 0001wxyzabcd 100wxyz
* 001wxyzabcde 101wxyz
* 01wxyzabcdef 110wxyz
* 1wxyzabcdefg 111wxyz
*
* For further information see John C. Bellamy's Digital Telephony, 1982,
* John Wiley & Sons, pps 98-111 and 472-476.
*/
unsigned char
linear2alaw(
int pcm_val) /* 2's complement (16-bit range) */
{
int mask;
int seg;
unsigned char aval;

if (pcm_val >= 0) {
mask = 0xD5; /* sign (7th) bit = 1 */
} else {
mask = 0x55; /* sign bit = 0 */
pcm_val = -pcm_val - 8;
}

/* Convert the scaled magnitude to segment number. */
seg = search(pcm_val, seg_end, 8);

/* Combine the sign, segment, and quantization bits. */

if (seg >= 8) /* out of range, return maximum value. */
return (0x7F ^ mask);
else {
aval = seg << SEG_SHIFT;
if (seg < 2)
aval |= (pcm_val >> 4) & QUANT_MASK;
else
aval |= (pcm_val >> (seg + 3)) & QUANT_MASK;
return (aval ^ mask);
}
}

/*
* alaw2linear() - Convert an A-law value to 16-bit linear PCM
*
*/
int
alaw2linear(
unsigned char a_val)
{
int t;
int seg;

a_val ^= 0x55;

t = (a_val & QUANT_MASK) << 4;
seg = ((unsigned)a_val & SEG_MASK) >> SEG_SHIFT;
switch (seg) {
case 0:
t += 8;
break;
case 1:
t += 0x108;
break;
default:
t += 0x108;
t <<= seg - 1;
}
return ((a_val & SIGN_BIT) ? t : -t);
}

#define BIAS (0x84) /* Bias for linear code. */

/*
* linear2ulaw() - Convert a linear PCM value to u-law
*
* In order to simplify the encoding process, the original linear magnitude
* is biased by adding 33 which shifts the encoding range from (0 - 8158) to
* (33 - 8191). The result can be seen in the following encoding table:
*
* Biased Linear Input Code Compressed Code
* ------------------------ ---------------
* 00000001wxyza 000wxyz
* 0000001wxyzab 001wxyz
* 000001wxyzabc 010wxyz
* 00001wxyzabcd 011wxyz
* 0001wxyzabcde 100wxyz
* 001wxyzabcdef 101wxyz
* 01wxyzabcdefg 110wxyz
* 1wxyzabcdefgh 111wxyz
*
* Each biased linear code has a leading 1 which identifies the segment
* number. The value of the segment number is equal to 7 minus the number
* of leading 0's. The quantization interval is directly available as the
* four bits wxyz. * The trailing bits (a - h) are ignored.
*
* Ordinarily the complement of the resulting code word is used for
* transmission, and so the code word is complemented before it is returned.
*
* For further information see John C. Bellamy's Digital Telephony, 1982,
* John Wiley & Sons, pps 98-111 and 472-476.
*/
unsigned char
linear2ulaw(
int pcm_val) /* 2's complement (16-bit range) */
{
int mask;
int seg;
unsigned char uval;

/* Get the sign and the magnitude of the value. */
if (pcm_val < 0) {
pcm_val = BIAS - pcm_val;
mask = 0x7F;
} else {
pcm_val += BIAS;
mask = 0xFF;
}

/* Convert the scaled magnitude to segment number. */
seg = search(pcm_val, seg_end, 8);

/*
* Combine the sign, segment, quantization bits;
* and complement the code word.
*/
if (seg >= 8) /* out of range, return maximum value. */
return (0x7F ^ mask);
else {
uval = (seg << 4) | ((pcm_val >> (seg + 3)) & 0xF);
return (uval ^ mask);
}

}

/*
* ulaw2linear() - Convert a u-law value to 16-bit linear PCM
*
* First, a biased linear code is derived from the code word. An unbiased
* output can then be obtained by subtracting 33 from the biased code.
*
* Note that this function expects to be passed the complement of the
* original code word. This is in keeping with ISDN conventions.
*/
int
ulaw2linear(
unsigned char u_val)
{
int t;

/* Complement to obtain normal u-law value. */
u_val = ~u_val;

/*
* Extract and bias the quantization bits. Then
* shift up by the segment number and subtract out the bias.
*/
t = ((u_val & QUANT_MASK) << 3) + BIAS;
t <<= ((unsigned)u_val & SEG_MASK) >> SEG_SHIFT;

return ((u_val & SIGN_BIT) ? (BIAS - t) : (t - BIAS));
}

/* A-law to u-law conversion */
unsigned char
alaw2ulaw(
unsigned char aval)
{
aval &= 0xff;
return ((aval & 0x80) ? (0xFF ^ _a2u[aval ^ 0xD5]) :
(0x7F ^ _a2u[aval ^ 0x55]));
}

/* u-law to A-law conversion */
unsigned char
ulaw2alaw(
unsigned char uval)
{
uval &= 0xff;
return ((uval & 0x80) ? (0xD5 ^ (_u2a[0xFF ^ uval] - 1)) :
(0x55 ^ (_u2a[0x7F ^ uval] - 1)));
}

//java版 g711a 转换 seg_end 错误
https://github.com/glaciall/acodec/blob/master/src/main/java/cn/org/hentai/acodec/G711UCodec.java

g711a/g711u/pcm 转码 带音频资源
https://github.com/dreamisdream/audioTransfromTools
————————————————

版权声明:本文为CSDN博主「houxiaoni01」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/houxiaoni01/article/details/104692606

WebRTC 协议介绍

本文介绍了基于WebRTC API构建的协议。

ICE

交互式连接设施Interactive Connectivity Establishment (ICE) 是一个允许你的浏览器和对端浏览器建立连接的协议框架。在实际的网络当中,有很多原因能导致简单的从A端到B端直连不能如愿完成。这需要绕过阻止建立连接的防火墙,给你的设备分配一个唯一可见的地址(通常情况下我们的大部分设备没有一个固定的公网地址),如果路由器不允许主机直连,还得通过一台服务器转发数据。ICE通过使用以下几种技术完成上述工作。

STUN

NAT的会话穿越功能Session Traversal Utilities for NAT (STUN) (缩略语的最后一个字母是NAT的首字母)是一个允许位于NAT后的客户端找出自己的公网地址,判断出路由器阻止直连的限制方法的协议。

客户端通过给公网的STUN服务器发送请求获得自己的公网地址信息,以及是否能够被(穿过路由器)访问。

An interaction between two users of a WebRTC application involving a STUN server.

NAT

网络地址转换协议Network Address Translation (NAT) 用来给你的(私网)设备映射一个公网的IP地址的协议。一般情况下,路由器的WAN口有一个公网IP,所有连接这个路由器LAN口的设备会分配一个私有网段的IP地址(例如192.168.1.3)。私网设备的IP被映射成路由器的公网IP和唯一的端口,通过这种方式不需要为每一个私网设备分配不同的公网IP,但是依然能被外网设备发现。

一些路由器严格地限定了部分私网设备的对外连接。这种情况下,即使STUN服务器识别了该私网设备的公网IP和端口的映射,依然无法和这个私网设备建立连接。这种情况下就需要转向TURN协议。

TURN

一些路由器使用一种“对称型NAT”的NAT模型。这意味着路由器只接受和对端先前建立的连接(就是下一次请求建立新的连接映射)。

NAT的中继穿越方式Traversal Using Relays around NAT (TURN) 通过TURN服务器中继所有数据的方式来绕过“对称型NAT”。你需要在TURN服务器上创建一个连接,然后告诉所有对端设备发包到服务器上,TURN服务器再把包转发给你。很显然这种方式是开销很大的,所以只有在没得选择的情况下采用。

An interaction between two users of a WebRTC application involving STUN and TURN servers.

SDP

会话描述协议Session Description Protocol (SDP) 是一个描述多媒体连接内容的协议,例如分辨率,格式,编码,加密算法等。所以在数据传输时两端都能够理解彼此的数据。本质上,这些描述内容的元数据并不是媒体流本身。

从技术上讲,SDP并不是一个真正的协议,而是一种数据格式,用于描述在设备之间共享媒体的连接。

记录SDP远远超出了本文档的范围。但是,这里有几件事值得注意。

结构体

SDP由一行或多行UTF-8文本组成,每行以一个字符的类型开头,后跟等号(“ =”),然后是包含值或描述的结构化文本,其格式取决于类型。以给定字母开头的文本行通常称为“字母行”。例如,提供媒体描述的行的类型为“ m”,因此这些行称为“ m行”。

获取更多信息

要了解有关SDP的更多信息,请参见以下有用的资源: