本章主要是关于RTMP直播系统中音视频编解码模块的介绍。
static ngx_rtmp_module_t ngx_rtmp_codec_module_ctx = {
NULL, /* preconfiguration */
ngx_rtmp_codec_postconfiguration, /* postconfiguration */
NULL, /* create main configuration */
NULL, /* init main configuration */
NULL, /* create server configuration */
NULL, /* merge server configuration */
ngx_rtmp_codec_create_app_conf, /* create app configuration */
ngx_rtmp_codec_merge_app_conf /* merge app configuration */
};
首先我们来看postconfiguration阶段:
static ngx_int_t
ngx_rtmp_codec_postconfiguration(ngx_conf_t *cf)
{
ngx_rtmp_core_main_conf_t *cmcf;
ngx_rtmp_handler_pt *h;
ngx_rtmp_amf_handler_t *ch;
cmcf = ngx_rtmp_conf_get_module_main_conf(cf, ngx_rtmp_core_module);
h = ngx_array_push(&cmcf->events[NGX_RTMP_MSG_AUDIO]);
*h = ngx_rtmp_codec_av;
h = ngx_array_push(&cmcf->events[NGX_RTMP_MSG_VIDEO]);
*h = ngx_rtmp_codec_av;
h = ngx_array_push(&cmcf->events[NGX_RTMP_DISCONNECT]);
*h = ngx_rtmp_codec_disconnect;
/* register metadata handler */
ch = ngx_array_push(&cmcf->amf);
if (ch == NULL) {
return NGX_ERROR;
}
ngx_str_set(&ch->name, "@setDataFrame");
ch->handler = ngx_rtmp_codec_meta_data;
ch = ngx_array_push(&cmcf->amf);
if (ch == NULL) {
return NGX_ERROR;
}
ngx_str_set(&ch->name, "onMetaData");
ch->handler = ngx_rtmp_codec_meta_data;
return NGX_OK;
}
1、ngx_rtmp_codec_av:同在ngx_rtmp_live_module中类似,也是在接收到音频或视频消息时的处理函数。
if (h->type != NGX_RTMP_MSG_AUDIO && h->type != NGX_RTMP_MSG_VIDEO) {
return NGX_OK;
}
ngx_rtmp_codec_av函数中只处理type类型是音频或视频的数据,对于非音视频数据,直接返回NGX_OK。
ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module);
if (ctx == NULL) {
ctx = ngx_pcalloc(s->connection->pool, sizeof(ngx_rtmp_codec_ctx_t));
ngx_rtmp_set_ctx(s, ctx, ngx_rtmp_codec_module);
}
获取该RTMP会话对应的ngx_rtmp_codec_ctx_t结构,如果当前会话不存在ngx_rtmp_codec_ctx_t结构,我们就创建一个ngx_rtmp_codec_ctx_t,并将它设置给该RTMP会话。
if (h->type == NGX_RTMP_MSG_AUDIO) {
ctx->audio_codec_id = (fmt & 0xf0) >> 4;
ctx->audio_channels = (fmt & 0x01) + 1;
ctx->sample_size = (fmt & 0x02) ? 2 : 1;
if (ctx->sample_rate == 0) {
ctx->sample_rate = sample_rates[(fmt & 0x0c) >> 2];
}
} else {
ctx->video_codec_id = (fmt & 0x0f);
}
解析音频数据和视频数据头,判断音频audio_codec_id和视频video_codec_id。
Filed | Type | Comment |
---|---|---|
Sound Format | UB[4] | 音频编码格式 0 = Linear PCM platform endian 1 = ADPCM 2 = MP3 3 = Linear PCM little endian 4 = Nellymoser 16-kHz mono 5 = Nellymoser 8-kHz mono 6 = Nellymoser 7 = G.711 A-law logarithmic PCM 8 = G.711 mu-law logarithmic PCM 9 = reserved 10 = AAc 11 = Speex 14 = MP3 8-kHz 15 = Device-specific sound |
Sound Rate | UB[2] | 音频频率 0 = 5.5-kHz 1 = 11-kHz 2 = 22-kHz 3 = 44-kHz |
Sound Size | UB[1] | 每个音频样本的大小。 此参数仅适用于未压缩的格式。压缩格式总是解码内部为16位。 0 = snd8Bit 1 = snd16Bit |
Sound Type | UB[1] | 0 = sndMono 1 = sndStereo |
如果sound type = 10,那么音频数据就是AACAUDIODATA。
AACAUDIODATA格式如下:
Filed | Type | Comment |
---|---|---|
AACPacketType | UI8 | 0 = AAC sequence header 1 = AAC raw |
Data | UI8[n] | 如果AACPacketType==0,Data = AudioSpecificConfig 如果AACPacketType=1,Data = Raw AAC frame data |
Filed | Type | Comment |
---|---|---|
FrameType | UB[4] | 1 = keyframe 2 = inter frame 3 = disposable inter frame 4 = generated keyframe 5 = video info /command frame |
CodecID | UB[4] | 1 = JPEG(currently unused) 2 = Sorenson H.263 3 = Screen video 4 = On2 VP6 5 = On2 VP6 with alpha channel 6 = Screen video version 2 7 = AVC |
对于H.264数据来说,CodecID = 7。
当CodecID = 7时,视频数据就是AVCVIDEOPACKET格式。
下面讲解一下AVCVIDEOPACKET。
Filed | Type | Comment |
---|---|---|
AVCPacketType | UI8 | 0 = AVC Sequence Header 1 = AVC NALU 2 = AVC end of sequence |
CompositionTime | SI24 | AVCPacketType = 1,存储Composition Time offset 否则就是0. |
Data | UI8[n] | AVCPacketType = 0,存储AVCDecoderConfigurationRecord AVCPacketType = 1,存储一个或多个NALUs AVCPacketType = 2,就是空。 |
if (!ngx_rtmp_is_codec_header(in)) {
return NGX_OK;
}
判断该音视频数据是否携带codec,即是判断AACPacketType == 0或者AVCPacketType == 0。只有携带codec信息的音视频数据才会进行下一步运算,否则直接返回NGX_OK。
if (h->type == NGX_RTMP_MSG_AUDIO) {
if (ctx->audio_codec_id == NGX_RTMP_AUDIO_AAC) {
header = &ctx->aac_header;
ngx_rtmp_codec_parse_aac_header(s, in);
}
} else {
if (ctx->video_codec_id == NGX_RTMP_VIDEO_H264) {
header = &ctx->avc_header;
ngx_rtmp_codec_parse_avc_header(s, in);
}
}
对于AAC音频和H264的视频,进行分别处理。
2、ngx_rtmp_codec_disconnect:是注册在关闭RTMP会话时的函数调用数组中。
3、ngx_rtmp_codec_meta_data:当接收到推流端发送的meta信息时的处理函数。
本节我们重点介绍codec模块中的音频和视频消息处理函数,即ngx_rtmp_codec_av。
获取该RTMP会话对应的ngx_rtmp_codec_ctx_t结构
ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module);
初始化Nginx中按位读取数据的读者
ngx_rtmp_bit_init_reader(&br, in->buf->pos, in->buf->last);
跳过前面的16位,因为前面有4位的Sound Format,2位的Sound Rate,1位的Sound Size,1位的Sound Type和8位的AACPacketType。
ngx_rtmp_bit_read(&br, 16);
解析出aac_profile,sample_rate等音频解码器必须的参数。
AAC sequence header也就是包含了AudioSpecificConfig,AudioSpecificConfig包含着一些更加详细音频的信息,AudioSpecificConfig的定义在ISO14496-3中1.6.2.1 AudioSpecificConfig,这里就不详细贴了。而且在ffmpeg中有对AudioSpecificConfig解析的函数,ff_mpeg4audio_get_config(),可以对比的看一下,理解更深刻。
AAC raw 这种包含的就是音频ES流了,也就是audio payload.
在FLV的文件中,一般情况下 AAC sequence header 这种包只出现1次,而且是第一个audio tag,为什么要提到这种tag,因为当时在做FLVdemux的时候,如果是AAC的音频,需要在每帧AAC ES流前边添加7个字节ADST头,ADST在音频的格式中会详细解读,这是解码器通用的格式,就是AAC的纯ES流要打包成ADST格式的AAC文件,解码器才能正常播放.就是在打包ADST的时候,需要samplingFrequencyIndex这个信息,samplingFrequencyIndex最准确的信息是在AudioSpecificConfig中,所以就对AudioSpecificConfig进行解析并得到了samplingFrequencyIndex。
到这步你就完全可以把FLV 文件中的音频信息及数据提取出来,送给音频解码器正常播放了。
ctx->aac_profile = (ngx_uint_t) ngx_rtmp_bit_read(&br, 5);
if (ctx->aac_profile == 31) {
ctx->aac_profile = (ngx_uint_t) ngx_rtmp_bit_read(&br, 6) + 32;
}
idx = (ngx_uint_t) ngx_rtmp_bit_read(&br, 4);
if (idx == 15) {
ctx->sample_rate = (ngx_uint_t) ngx_rtmp_bit_read(&br, 24);
} else {
ctx->sample_rate = aac_sample_rates[idx];
}
ctx->aac_chan_conf = (ngx_uint_t) ngx_rtmp_bit_read(&br, 4);
if (ctx->aac_profile == 5 || ctx->aac_profile == 29) {
if (ctx->aac_profile == 29) {
ctx->aac_ps = 1;
}
ctx->aac_sbr = 1;
idx = (ngx_uint_t) ngx_rtmp_bit_read(&br, 4);
if (idx == 15) {
ctx->sample_rate = (ngx_uint_t) ngx_rtmp_bit_read(&br, 24);
} else {
ctx->sample_rate = aac_sample_rates[idx];
}
ctx->aac_profile = (ngx_uint_t) ngx_rtmp_bit_read(&br, 5);
if (ctx->aac_profile == 31) {
ctx->aac_profile = (ngx_uint_t) ngx_rtmp_bit_read(&br, 6) + 32;
}
}
AVCDecoderConfigurationRecord.包含着是H.264解码相关比较重要的sps和pps信息,再给AVC解码器送数据流之前 一定要把sps和pps信息送出,否则的话解码器不能正常解码。而且在解码器stop之后再次start之前,如seek、快进快退状态切换等,都需要重新送一遍sps和pps的信息。AVCDecoderConfigurationRecord在FLV文件中一般情况也是出现1次,也就是第一个 video tag.
SPS的全称是Sequence Paramater Set,即是序列参数集。SPS中保存了一组编码视频序列(Coded video sequence)的全局参数。编码视频序列即原始视频的一帧一帧的像素数据经过编码之后的结构组成的序列。而每一帧的编码后数据所依赖的参数保存于图像参数集中。一般情况SPS和PPS的NAL
Unit通常位于整个码流的起始位置。但在某些特殊情况下,在码流中间也可能出现这两种结构,主要原因可能为:1、解码器需要在码流中间开始解码;2、编码器在编码的过程中改变了码流的参数(如图像分辨率等)。
H.264标准协议中规定的SPS格式如下:
seq_parameter_set_data() { | C | Descriptor |
---|---|---|
profile_idc | 0 | u(8) |
constraint_set0_flag | 0 | u(1) |
constraint_set1_flag | 0 | u(1) |
constraint_set2_flag | 0 | u(1) |
constraint_set3_flag | 0 | u(1) |
constraint_set4_flag | 0 | u(1) |
constraint_set5_flag | 0 | u(1) |
reserved_zero_2bits /* equal to 0 */ | 0 | u(2) |
level_idc | 0 | u(8) |
seqparameter_set_id | 0 | u(v) |
if(profile_idc == 100 || profile_idc == 110 || profile_idc == 122 || profile_idc == 244 || profile_idc == 44 || profile_idc == 83 || profile_idc == 86 || profile_idc == 118 || profile_idc == 128) { |
||
chroma_format_idc | 0 | ue(v) |
if(chroma_format_idc == 3) | ||
separate_colour_plane_flag | 0 | u(1) |
bit_depth_luma_minus8 | 0 | ue(v) |
bit_depth_chroma_minus8 | 0 | ue(v) |
qpprime_y_zero_transform_bypass_flag | 0 | u(1) |
seq_scaling_matrix_present_flag | 0 | u(1) |
if(seq_scaling_matrix_present_flag) | ||
for(i=0; i<(chroma_format_idc != 3)?8:12); i++) { | ||
seq_scaling_list_present_flag[i] | 0 | u(1) |
if(seq_scaling_list_present_flag[i]) | ||
if(i<6) | ||
scaling_list(ScalingList44[i],16,UseDefaultScalingMatrix44Flag[i]) | 0 | |
else | ||
scaling_list(ScalingList88[i - 6],64,UseDefaultScalingMatrix88Flag[i-6]) | 0 | |
} | ||
} | ||
log2_max_frame_num_minus4 | 0 | ue(v) |
pic_order_cnt_type | 0 | ue(v) |
if(pic_order_cnt_type == 0) | ||
log2_max_pic_order_cnt_lsb_minus4 | 0 | ue(v) |
if(pic_order_cnt_type ==1 ) | ||
delta_pic_order_always_zero_flag | 0 | ue(1) |
offset_for_non_ref_pic | 0 | se(v) |
offset_for_top_to_bottom_field | 0 | se(v) |
num_ref_frames_in_pic_order_cnt_cycle | 0 | ue(v) |
for(i=0; i< num_ref_frames_in_pic_order_cnt_cycle; i++) | ||
offset_for_ref_frame[i] | 0 | se(v) |
} | ||
max_num_ref_frames | 0 | ue(v) |
gaps_in_frame_num_value_allowed_flag | 0 | u(1) |
pic_width_in_mbs_minus1 | 0 | ue(v) |
pic_height_in_mbs_minus1 | 0 | ue(v) |
frame_mbs_only_flag | 0 | ue(1) |
if(!frame_mbs_only_flag) | ||
mb_adaptive_frame_field_flag | 0 | u(1) |
direct_8*8_inference_flag | 0 | u(1) |
frame_cropping_flag | 0 | u(1) |
if(frame_cropping_flag) { | ||
frame_crop_left_offset | 0 | ue(v) |
frame_crop_right_offset | 0 | ue(v) |
frame_crop_top_offset | 0 | ue(v) |
frame_crop_bottom_offset | 0 | ue(v) |
} | ||
vui_parameters_present_flag | 0 | u(1 |
if(vui_parameters_present_flag) | ||
vui_parameters() | 0 | |
} |
其中的每一个语法元素及其含义如下:
标识当前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。\
标识当前码流的Level。编码的Level定义了某种条件下的最大视频分辨率、最大视频帧率等参数,码流所遵从的level由level_idc指定。
当前码流中,level_idc = 0x1e = 30,因此码流的级别为3。
表示当前的序列参数集的id。通过该id值,图像参数集pps可以引用其代表的sps中的参数。
用于计算MaxFrameNum的值。计算公式为MaxFrameNum = 2^(log2_max_frame_num_minus4 +
4)。MaxFrameNum是frame_num的上限值,frame_num是图像序号的一种表示方法,在帧间编码中常用作一种参考帧标记的手段。
表示解码picture order count(POC)的方法。POC是另一种计量图像序号的方式,与frame_num有着不同的计算方法。该语法元素的取值为0、1或2。
用于计算MaxPicOrderCntLsb的值,该值表示POC的上限。计算方法为MaxPicOrderCntLsb = 2^(log2_max_pic_order_cnt_lsb_minus4 + 4)。
用于表示参考帧的最大数目。
标识位,说明frame_num中是否允许不连续的值。
用于计算图像的宽度。单位为宏块个数,因此图像的实际宽度为:
frame_width = 16 × (pic_width_in_mbs_minus1 + 1);
使用PicHeightInMapUnits来度量视频中一帧图像的高度。PicHeightInMapUnits并非图像明确的以像素或宏块为单位的高度,而需要考虑该宏块是帧编码或场编码。PicHeightInMapUnits的计算方式为:
PicHeightInMapUnits = pic_height_in_map_units_minus1 + 1;
标识位,说明宏块的编码方式。当该标识位为0时,宏块可能为帧编码或场编码;该标识位为1时,所有宏块都采用帧编码。根据该标识位取值不同,PicHeightInMapUnits的含义也不同,为0时表示一场数据按宏块计算的高度,为1时表示一帧数据按宏块计算的高度。
按照宏块计算的图像实际高度FrameHeightInMbs的计算方法为:
FrameHeightInMbs = ( 2 − frame_mbs_only_flag ) * PicHeightInMapUnits
标识位,说明是否采用了宏块级的帧场自适应编码。当该标识位为0时,不存在帧编码和场编码之间的切换;当标识位为1时,宏块可能在帧编码和场编码模式之间进行选择。
标识位,用于B_Skip、B_Direct模式运动矢量的推导计算。
标识位,说明是否需要对输出的图像帧进行裁剪。
标识位,说明SPS中是否存在VUI信息。
除了序列参数集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节中,具体的结构如下表所示:
pic_parameter_set_rbsp() { | C | Descriptor |
---|---|---|
pic_parameter_set_id | 1 | ue(v) |
seq_parameter_set_id | 1 | ue(v) |
entropy_coding_mode_flag | 1 | u(1) |
bottom_filed_pic_order_in_frame_present_flag | 1 | u(1) |
num_slice_groups_minus1 | 1 | ue(v) |
if(num_slice_groups_minus1 > 0) { | ||
slice_group_map_type | 1 | ue(v) |
if(slice_group_map_type == 0) | ||
for(iGroup = 0; iGroup <= num_slice_groups_minus1; iGroup++) | ||
run_length_munus1[iGroup] | 1 | ue(v) |
else if(slice_group_map_type == 2) | ||
  for(iGroup = 0; iGroup <= num_slice_groups_minus1; iGroup++) { | ||
top_left[iGroup] | 1 | ue(v) |
bottom_right[iGroup] | 1 | ue(v) |
} | ||
else if(slice_group_map_type == 3 slice_group_map_type == 4 slice_group_map_type == 5) { |
||
slice_group_change_direction_flag | 1 | u(1) |
slice_group_change_rate_minus1 | 1 | ue(v) |
} else if(slice_group_map_type == 6) { | ||
pic_size_in_map_units_minus1 | 1 | ue(v) |
  for(i = 0; i < pic_size_in_map_units_minus1; i++) | ||
slice_group_id[i] | 1 | u(v) |
} | ||
} | ||
num_ref_idx_10_default_active_minus1 | 1 | ue(v) |
num_ref_idx_11_default_active_minus1 | 1 | ue(v) |
weighted_pred_flag | 1 | ue(1) |
weighted_bipred_idc | 1 | ue(2) |
pic_init_qp_minus26 relative to 26 | 1 | se(v) |
pic_init_qs_minus26 relative to 26 | 1 | se(v) |
chroma_qp_index_offset | 1 | se(v) |
deblocking_filter_control_present_flag | 1 | u(1) |
constrained_intra_pred_flag | 1 | u(1) |
redundant_pic_cnt_present_flag | 1 | u(1) |
if(more_rbsp_data()) { | ||
transform_8*8_mode_flag | 1 | u(1) |
pic_scaling_matrix_present_flag | 1 | u(1) |
if(pic_scaling_matrix_present_flag) | ||
for(i = 0; i< 6 + ((chroma_format_idc != 3)?2:6)transform_88_mode_flag; i++) { | ||
pic_scaling_list_present_flag[i] | 1 | u(1) |
if(pic_scaling_list_present_flag[i]) | ||
if(i < 6) | ||
scaling_list(ScalingList44[i], 16, UseDafaultScalingMatrix44Flag[i]) | 1 | |
else | ||
scaling_list(ScalingList88[i-6], 64, UseDafaultScalingMatrix88Flag[i-6]) | 1 | |
} | ||
second_chroma_qp_index_offset | 1 | se(v) |
} | ||
rbsp_trailing_bits() | 1 | |
} |
其中的每一个语法元素及其含义如下:
表示当前PPS的id。某个PPS在码流中会被相应的slice引用,slice引用PPS的方式就是在Slice header中保存PPS的id值。该值的取值范围为[0,255]。
表示当前PPS所引用的激活的SPS的id。通过这种方式,PPS中也可以取到对应SPS中的参数。该值的取值范围为[0,31]。
熵编码模式标识,该标识位表示码流中熵编码/解码选择的算法。对于部分语法元素,在不同的编码配置下,选择的熵编码方式不同。例如在一个宏块语法元素中,宏块类型mb_type的语法元素描述符为“ue(v)
| ae(v)”,在baseline profile等设置下采用指数哥伦布编码,在main profile等设置下采用CABAC编码。
标识位entropy_coding_mode_flag的作用就是控制这种算法选择。当该值为0时,选择左边的算法,通常为指数哥伦布编码或者CAVLC;当该值为1时,选择右边的算法,通常为CABAC。
标识位,用于表示另外条带头中的两个语法元素delta_pic_order_cnt_bottom和delta_pic_order_cn是否存在的标识。这两个语法元素表示了某一帧的底场的POC的计算方法。
表示某一帧中slice group的个数。当该值为0时,一帧中所有的slice都属于一个slice group。slice group是一帧中宏块的组合方式,定义在协议文档的3.141部分。
表示当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的默认值。
标识位,表示在P/SP slice中是否开启加权预测。
表示在B Slice中加权预测的方法,取值范围为[0,2]。0表示默认加权预测,1表示显式加权预测,2表示隐式加权预测。
表示初始的量化参数。实际的量化参数由该参数、slice header中的slice_qp_delta/slice_qs_delta计算得到。
用于计算色度分量的量化参数,取值范围为[-12,12]。
标识位,用于表示Slice header中是否存在用于去块滤波器控制的信息。当该标志位为1时,slice header中包含去块滤波相应的信息;当该标识位为0时,slice header中没有相应的信息。
若该标识为1,表示I宏块在进行帧内预测时只能使用来自I和SI类型宏块的信息;若该标识位0,表示I宏块可以使用来自Inter类型宏块的信息。
标识位,用于表示Slice header中是否存在redundant_pic_cnt语法元素。当该标志位为1时,slice header中包含redundant_pic_cnt;当该标识位为0时,slice header中没有相应的信息。
在H264码流中,都是以"0x00 0x00 0x01"或者"0x00
0x00 0x00 0x01"为起始码的,在寻找到起始码之后,使用起始码之后的第一个字节的低5位判断是否为7(sps)或者8(pps), 即data[4] & 0x1f == 7 ||data[4] & 0x1f == 8。
H.264的SPS和PPS串,包含了初始化H.264解码器所需要的信息参数,包括编码所用的profile,level,图像的宽和高,deblock滤波器等。
获取rtmp会话对应的codec模块结构体
ctx = ngx_rtmp_get_module_ctx(s, ngx_rtmp_codec_module);
初始化位读信息
ngx_rtmp_bit_init_reader(&br, in->buf->pos, in->buf->last);
跳过前面的48位
ngx_rtmp_bit_read(&br, 48);
进入到seq_parameter_set_data()解析,可以根据前面的图表,在buf读取对应位置的信息并赋值给ctx。s
ctx->avc_profile = (ngx_uint_t) ngx_rtmp_bit_read_8(&br);
ctx->avc_compat = (ngx_uint_t) ngx_rtmp_bit_read_8(&br);
ctx->avc_level = (ngx_uint_t) ngx_rtmp_bit_read_8(&br);