Asterisk 现有版本不支持播放视频文件(支持视频通话),无法满足发送视频通知、视频 IVR 等场景。本系列文章,通过学习音视频的相关知识和工具,尝试实现一个通过 Asterisk 播放 mp4 视频文件的应用。
- Asterisk播放mp4(1)——音频和PCM编码
- Asterisk播放mp4(2)——音频封装
- Asterisk播放mp4(3)——搭建开发环境
- Asterisk播放mp4(4)——H264&AAC
- Asterisk播放mp4(5)——MP4文件解析
- Asterisk播放mp4(6)——音视频同步
- Asterisk播放mp4(7)——DTMF
MP4文件中的视频流采用H264编码,音频流采用AAC编码,所以在处理MP4文件前,我们先了解一下H264和AAC。
H264/MPEG-4 AVC
H264 是一种视频编码格式,分为视频编码层(VCL,Video Coding Layer)和网络提取层 (NAL,Network Abstraction Layer),其中 VCL 负责处理图像的压缩编码,NAL 负责数据的打包。
H.264 原始码流是由连续的 NALU 组成。NALU 之间通过 startcode(起始码)进行分隔,起始码有两种值:0x000001(3字节)或者 0x00000001(4字节)。如果 NALU 对应的分片(Slice)为一帧的开始就用 0x00000001,否则就用 0x000001。每个 NALU 由头(header)和原始字节序列负荷(RBSP,Raw Byte Sequence Payload)两部分组成。这种用开始码分割NALU的方式称之为Annex B
。
另一个存储H.264流的方式是AVCC
格式,在这种格式中,每一个NALU包都加上了一个指定其长度(NALU包大小)的前缀(大字节序),这种格式的包非常容易解析,但是它去掉了Annex B
格式中的字节对齐特性,其前缀可以是1、2或4字节。MP4文件中采用这种格式,后续讲MP4文件时会涉及。
NALU
- NALU Header
字段 | 比特位数 | 说明 |
---|---|---|
forbidden_zero_bit | 1 | 必须等于 0 |
nal_ref_idc | 2 | 标识 NALU 是否为参考帧,等 0为非参考帧。 |
nal_unit_type | 5 | NALU 类型 |
NALU 类型 | 说明 |
---|---|
1 | 非IDR帧,不采用数据划分的片数据。 |
2-4 | 分别对应片数据A,B,C分区。 |
5 | IDR 帧。I 帧的一种,告诉解码器,之前依赖的解码参数集合(接下来要出现的 SPS\PPS 等)可以被刷新了。 |
6 | 补充增强信息(SEI,Supplemental Enhancement Information),提供了向视频码流中加入额外信息的方法。 |
7 | 序列参数集(SPS,全称 Sequence Paramater Set)。SPS 中保存了一组编码视频序列(Coded Video Sequence)的全局参数,如标识符 seq_parameter_set_id、帧数及 POC 的约束、参考帧数目、解码图像尺寸和帧场编码模式选择标识等等。 |
8 | 图像参数集(PPS,全称 Picture Paramater Set)。该类型保存了整体图像相关的参数。 |
9-23 | 本文不涉及 |
24-31 | 后面会提到 |
- SPS
SPS 中有很多参数,需要特别的关注的有如下几个:
参数 | 说明 |
---|---|
profile_idc | 在 H.264 的 SPS 中,第一个字节表示 profile_idc,根据 profile_idc 的值可以确定码流符合哪一种档次。 |
constraint_set0_flag ~ constraint_set5_flag | 每个占 1 位是在编码的档次方面对码流增加的其他一些额外限制性条件。 |
level_idc | 编码的 Level 定义了某种条件下的最大视频分辨率、最大视频帧率等参数,码流所遵从的 level 由 level_idc 指定。 |
pic_width_in_mbs_minus1 | 用于计算图像的宽度。 frame_width = 16 × (pic_width_in_mbs_minus1 + 1) |
pic_height_in_map_units_minus1 | 使用 PicHeightInMapUnits 来度量视频中一帧图像的高度。PicHeightInMapUnits 并非图像明确的以像素或宏块为单位的高度,而需要考虑该宏块是帧编码或场编码。PicHeightInMapUnits 的计算方式为:PicHeightInMapUnits = pic_height_in_map_units_minus1 + 1。 |
frame_mbs_only_flag | 标识位,说明宏块的编码方式。当该标识位为 0 时,宏块可能为帧编码或场编码;该标识位为 1 时,所有宏块都采用帧编码。根据该标识位取值不同,pic_height_in_map_units 的含义也不同,为 0 时表示一场数据按宏块计算的高度,为 1 时表示一帧数据按宏块计算的高度。按照宏块计算的图像实际高度 frame_height_in_mbs 的计算方法为:frame_height_in_mbs = ( 2 − frame_mbs_only_flag ) * pic_height_in_map_units。 |
timing_info_present_flag | 等于1表示num_units_in_tick ,time_scale 和fixed_frame_rate_flag 3个参数存在,可以计算fps,否则这些参数不存在。 |
num_units_in_tick | |
time_scale | 时间基数,1秒多少次。 |
fixed_frame_rate_flag | |
log2_max_frame_num_minus4 | 用于解析slice 中的frame_num 参数。 |
pic_order_cnt_type | 指定了计算图片顺序(POC)的方式,取值:0,1,2。 |
log2_max_pic_order_cnt_lsb_minus4 | 用于计算图片顺序,取值:0-12。 |
Profile 是用来描述视频压缩特性的,profile 越高,就说明采用了越高级的压缩特性。Baseline Profile(66)
提供 I/P 帧,仅支持 progressive(逐行扫描)和 CAVLC;Main Profile(77)
提供 I/P/B 帧,支持 progressive(逐行扫描)和 interlaced(隔行扫描),提供 CAVLC 或 CABAC;Extended profile(88)
提供 I/P/B/SP/SI 帧,提供 CAVLC,仅支持 progressive(逐行扫描);High Profile(100)
在 Main Profile 基础上新增:8x8 intra prediction(8x8 帧内预测),custom quant(自定义量化),lossless video coding(无损视频编码), 更多的 yuv 格式(4:4:4...)。
Baseline profile、Extended profile 和 Main profile 都是针对 8 位样本数据、4:2:0 格式(YUV)的视频序列。在相同配置情况下,High profile(HP)可以比 Main profile(MP)降低 10%的码率。 根据应用领域的不同,Baseline profile 多应用于实时通信领域,Main profile 多应用于流媒体领域,High profile 则多应用于广电和存储领域。
Level 是对视频本身特性的描述(码率、分辨率、fps),Level 越高,视频的码率、分辨率、fps 越高,而 level 主要是对码流的关键参数的取值范围作了限定,与解码器的处理能力和存储能力相关联。
H264 Level这里存在一个疑问:Profile和Level具有强制性吗?是否会因为它们的值和实际数据的值不相符导致无法解码?
视频帧率信息在 SPS 中,需要根据 time_scale、fixed_frame_rate_flag 计算得到:fps = time_scale /( num_units_in_tick*2)。
由于H264中包括B帧,所以帧编码顺序和显示顺序会不一致,sps
中很多参数都与解析和计算帧顺序相关,例如:log2_max_frame_num_minus4
,pic_order_cnt_type
,log2_max_pic_order_cnt_lsb_minus4
等。
图像数据
image.png-
宏块(Macro Block):把视频的每一帧(相当于一张图片)划分成 1616 的小块,一块一块的依次压缩,而不是对整张图片一起压缩。这样降低了计算的复杂度,比较节省时间。一个宏块又可以分成 1616,168,816,88,84,48,44等大小不等的块。
-
片(Slice):包含一帧图像的部分或全部数据。一个片最少包含一个宏块,最多包含整帧图像的数据。片共有 5 种类型:I 片(只包含 I 宏块)、P 片(P 和 I 宏块)、B 片(B 和 I 宏块)、SP 片(用于不同编码流之间的切换)和 SI 片(特殊类型的编码宏块)。在不同的编码实现中,同一帧图像中所构成的片数目不一定相同。在H.264中设计片的目的主要在于防止误码的扩散。因为不同的片之间,其解码操作是独立的。某一个片的解码过程所参考的数据(例如预测编码)不能越过片的边界。
-
I 帧:帧内编码帧又称 intra picture,表示关键帧,I 帧通常是每个 GOP(MPEG 所使用的一种视频压缩技术)的第一个帧,经过适度地压缩,做为随机访问的参考点,可以当成图象。I 帧可以看成是一个图像经过压缩后的产物。
-
P 帧: 前向预测编码帧又称 predictive-frame,通过充分将低于图像序列中前面已编码帧的时间冗余信息来压缩传输数据量的编码图像,也叫预测帧;表示的是这一帧跟之前的一个关键帧(或 P 帧)的差别,解码时需要用之前缓存的画面(I 帧)叠加上本帧定义的差别(它只参考前面最靠近它的I帧或P帧),生成最终画面。
-
B 帧: 双向预测内插编码帧(双向差别帧、双向预测帧)又称 bi-directional interpolated prediction frame,既考虑与源图像序列前面已编码帧,也顾及源图像序列后面已编码帧之间的时间冗余信息来压缩传输数据量的编码图像;也就是 B 帧记录的是本帧与前后帧的差别(具体比较复杂,有 4 种情况),换言之,要解码 B 帧,不仅要取得之前的缓存画面,还要解码之后的画面,通过前后画面的与本帧数据的叠加取得最终的画面。
-
GOP:两个 I 帧之间是一个图像序列,在一个图像序列中只有一个 I 帧。(图像组)主要用作形容一个 i 帧 到下一个 i 帧之间的间隔了多少个帧,增大图片组能有效的减少编码后的视频体积,但是也会降低视频质量。
-
场:隔行扫描的图像,偶数行成为顶场行。奇数行成为底场行。所有顶场行称为顶场。所有底场行称为底场。
需要注意的是对原始视频编码时,存在B帧的情况下,因为P帧会先于B帧编码,所以帧编码顺序(存储顺序)和帧的播放顺序不一致。
片数据
片(slice)作为NALU中的负荷部分,由头(header)和体(body)两部分组成。
slice slice头前几个字段slice header
中保存了当前片的一些全局的信息,slice body
中的宏块在进行解码时需依赖这些信息。片头包含的字段非常多,我们着重关心帧类型和帧顺序。片头中使用指数哥伦布编码(Exp-Golomb-coded),参数类型为ue(v)
,所以每个参数的起始位置不是固定的(相对位置固定)。具体的算法就不展开了,可以先记住如下编码对照关系。
片头的第1个部分(ue(v))是first_mb_in_slice
,指明当前片中包含的第一个宏块在整帧中的位置(一般为0,一个slice为一帧图像,所以为0);第2个部分(ue(v))是slice_type
,指明帧类型,其定义如下表。
frame_num
代表参考帧(nal_ref_idc
指定)的编号,IDR帧的编号为0,后面依次加1,SPS中的log2_max_frame_num_minus4
参数决定了frame_num
的最大值,求余作为frame_num
的值,非参考帧的值无意义。(这个只是最简单的情况,实际上还存在很多变化。这个值单独看没有什么意思,应该是在解码过程中有用。)
pic_order_cnt_lsb
参数用于计算帧的显示顺序。它代表了显示顺序的低位,高位(PicOrderCntMsb)根据SPS中的参数pic_order_cnt_type
对应的算法计算获得,最终的显示顺序为poc = PicOrderCntMsb + pic_order_cnt_lsb
。SPS中的参数log2_max_pic_order_cnt_lsb_minus4
决定了pic_order_cnt_lsb
的最大值。(这个完整算法比较复杂,涉及到多种情况,我理解这个设计一个好处是解决了B帧导致的乱序问题。)
通过frame_num
和pic_order_cnt_lsb
就可以计算出帧的编码顺序和现实顺序,也就是说sps
+slice header
中的信息决定了最终的播放顺序。
如果需要了解全部片头字段信息参考规范的7.3.3 Slice header syntax
章节。
RTP 打包
nal_unit_type | NAL 类型 | 名称 |
---|---|---|
24 | STAP-A | 单一时间的组合包 |
25 | STAP-B | 单一时间的组合包 |
26 | MTAP16 | 多时间的组合包 |
27 | MTAP24 | 多时间的组合包 |
28 | FU-A | 分片的单元 |
29 | FU-B | 分片的单元 |
NALU 的长度小于 MTU 大小的包,一般采用单一 NAL 单元模式:RTP Header + NALU。
当 NALU 的长度特别小时,可以把几个 NALU 单元封在一个 RTP 包中:RTP Header(12 字节) + 0x18(STAP-A,1 字节)+ 第 1 个 NALU 长度 (2 字节,大字节序) + NALU + 第 2 个 NALU 长度 + ...)。
当 NALU 的长度超过 MTU 时,就必须对 NALU 单元进行分片封包,也称为 Fragmentation Units(FUs)。
FU Indicator(1 字节),就是 type 为 FU-A 的 nalu header。
FU 头(1 字节)的格式如下:
+---------------+
|0|1|2|3|4|5|6|7|
+-+-+-+-+-+-+-+-+
|S|E|R| Type |
+---------------+
S:当设置成 1,开始位指示分片 NAL 单元的开始。当跟随的 FU 荷载不是分片 NAL 单元荷载的开始,开始位设为 0。E:当设置成 1,结束位指示分片 NAL 单元的结束,即,荷载的最后字节也是分片 NAL 单元的最后一个字节。当跟随的 FU 荷载不是分片 NAL 单元的最后分片,结束位设置为 0。R:必须设置为 0。Type 和 nalu_type 对应。
AAC
高级音频编码(AAC,Advanced Audio Coding),目标是取代MP3,在压缩比更高的情况下提供更好的音质。AAC共有9种场景,对应不同场景的需要,例如:MPEG-4 AAC LC(主流),MPEG-4 AAC Main等。AAC有两种封装格式:ADIF和ADTS。音频数据交换格式(ADIF,Audio Data Interchange Format) 。这种格式的特征是可以确定的找到这个音频数据的开始,不需进行在音频数据流中间开始的解码,即它的解码必须在明确定义的开始处进行。故这种格式常用在磁盘文件中。音频数据传输流(ADTS,Audio Data Transport Stream)。这种格式的特征是它是一个有同步字的比特流,解码可以在这个流中任何位置开始。它的特征类似于mp3数据流格式。我们主要关注ADTS格式。
image.png- 固定头
位数 | 头字段 | 说明 |
---|---|---|
12 | syncword | 总是0xFFF,代表一个ADTS帧的开始,用于同步。解码器可通过0xFFF确定每个ADTS的开始位置。 |
1 | ID | MPEG 版本: 0 代表 MPEG-4,1 代表 MPEG-2。 |
2 | layer | 总是00 |
1 | protection_absent | 设置0,进行crc校验,1不进行crc校验,进行crc校验导致固定部分边长成28+16 = 44bits。 |
2 | profile | 表示使用哪个级别的AAC,需要加1后查对照表。 |
4 | sampling_frequency_index | 采样率,需要查对照表,0x3是48000,0x4是44100,0xb是8000。 |
1 | private_bit | 私有位置,编码时候设置0,解码忽略。 |
3 | channel_configuration | 声道数。 |
1 | original_copy | |
1 | home |
- 可变头
位数 | 头字段 | 说明 |
---|---|---|
1 | copyright_identification_bit | |
1 | copyright_identification_start | |
13 | aac_frame_length | 该帧的总长度,包括ADTS头和AAC原始流。 |
11 | adts_buffer_fullness | 0x7FF 说明是码率可变的码流。 |
2 | number_of_raw_data_blocks_in_frame | 表示ADTS帧中有number_of_raw_data_blocks_in_frame + 1个AAC原始帧。number_of_raw_data_blocks_in_frame == 0 表示说ADTS帧中有一个AAC数据块。 |
制作样本数据
H264
FFmpeg命令行输出ffmpeg -lavfi color=red -t 10 -c:v libx264 -profile:v main -level 3.1 red.h264
生成的red.h264
文件共有250帧(25fps*10s),有1个I帧,63个P帧,186个B帧。
如果将命令行中profile:v
参数由main
改为baseline
,那么编码的结果也将变化,有1个I帧,249个P帧。
FFmpeg命令行输出ffmpeg -lavfi color=red -t 10 -c:v libx264 -profile:v baseline -level 3.1 red-baseline.h264
ffmpeg除了控制是否包含B帧,还可以控制gop
的大小。将gop设置为10,生成的250个帧中,有25个I帧,有225个P帧。
设置gopffmpeg -t 10 -lavfi testsrc2 -c:v libx264 -profile:v baseline -level 3.1 -g 10 testsrc2-gop10-10s.h264
下面打开文件,看看文件内容。
red.h264打开red.h264
文件,可以看到前4个字节为0000 0001
,是起始码,后面是第一个NALU。NALU的首字节是NALU头,值为0x67
,对应的二进制为0110 0111
,后5位是nalu_type,值为7,是sps
帧。NALU负荷的首字节是profile_idc
,值为0x4d(77)
,对应的是main profile
。接着的一个字节是constraint_set_flag
,值为0x40(0100 00)
,constraint_set1_flag=1
,其他等于0。再后面一个字节是level_idc
,值为0x1f(31)
。后面的其它参数就不逐一分析了。后面可以看到分别有用0000 0001
和00 0001
分隔的NALU,一个是PPS
,一个是SEI
。
地址0x2d9
开始了第一个视频帧,帧头的值为0x65(0110 0101)
,后5位是nalu_type
,值为5,是IDR
。下一个帧的帧头为0x41(0100 0001)
,非IDR
帧。
下面看看通过RTP发送是什么情况。
sdpffmpeg -re -f h264 -i red.h264 -c:v copy -f rtp rtp://127.0.0.1:5005
执行FFmpeg命令行生成的sdp中有profile-level-id=4D401F
,实际上它就是SPS
帧的前3个字节,分别对应profile_idc
,constraint_set_flag
和level_idc
。
第一个RTP包的NALU类型是STAP-A
,单一时间的组合包,其中包括了SPS
,PPS
,SEI
和IDR
四个帧,也就是把4个帧打包成了一个帧。
从SPS帧中我们可以获取一下媒体流的基本参数。
参数 | 取值 |
---|---|
profile_idc | 77 |
level_idc | 31 |
frame_mbs_only_flag | 1 |
log2_max_frame_num_minus4 | 0 |
pic_order_cnt_type | 0 |
log2_max_pic_order_cnt_lsb_minus4 | 2 |
根据这些参数我们就可以解析出帧的顺序(具体的算法还是要参照规范,这里只是作为示例)。
解析red-default.h264下面试试传一个大的帧,需要进行拆包的情况。
FU-A开始 FU-A结束AAC
FFmpeg命令行输出ffmpeg -lavfi sine -t 10 -ar 8000 -b:a 8k -c:a aac sine-10s-8k.aac
生成的文件大小为10764个字节,同样规格的mp3文件大小为10413,两者差别不大。(同样也尝试了其它采样率,文件的空间并没有减小,看来压缩比和MP3相比没有明显不同。)
sine-10s-8k.accADTS头7个字节fff1 6c40 11df fc
。
位数 | 头字段 | 取值 | 说明 |
---|---|---|---|
12 | syncword | fff | |
1 | ID | 0 | 代表MPEG-4 |
2 | layer | 00 | 总是00 |
1 | protection_absent | 1 | 1不进行crc校验 |
2 | profile | 01 | 加1,对应的是AAC LC。 |
4 | sampling_frequency_index | 1011 | 采样率,需要查对照表,0xb8000。 |
1 | private_bit | 私有位置,编码时候设置0,解码忽略。 | |
3 | channel_configuration | 001 | 单声道数。 |
1 | original_copy | 0 | |
1 | home | 0 | |
1 | copyright_identification_bit | 0 | |
1 | copyright_identification_start | 0 | |
13 | aac_frame_length | 1 0001 110 | 0x8e,帧长度142。 |
11 | adts_buffer_fullness | 1 1111 1111 11 | 0x7FF 说明是码率可变的码流。 |
2 | number_of_raw_data_blocks_in_frame | 00 | 有一个AAC数据块。 |
通常在网络上不会直接传递aac编码的音频,而是转换为pcm编码,或者opus编码,因此就不尝试通过rtp发送aac了。
网友评论