此章节分析承接上一章分析:
【十五】【vlc-android】vlc-sout流媒体输出端源码实现分析【Part 2】【01】
9、block_SkipBytes实现分析:
// 【vlc/include/vlc_block_helper.h】
static inline int block_SkipBytes( block_bytestream_t *p_bytestream,
size_t i_data )
{
return block_GetBytes( p_bytestream, NULL, i_data );
}
// 【vlc/include/vlc_block_helper.h】
static inline int block_GetBytes( block_bytestream_t *p_bytestream,
uint8_t *p_data, size_t i_data )
{
// 此处为检查块字节流数据剩余未读取数据大小是否小于需要获取【跳过】的字节数数据大小
// 若小于则表示需要从字节流未读取数据中获取的数据不足
// 见下面的分析
if( block_BytestreamRemaining( p_bytestream ) < i_data )
return VLC_EGENERIC;
/* Copy the data */
// i_block_offset表示:块数据内部已读取数据位置偏移量
// i_data表示:待读取的数据大小
size_t i_offset = p_bytestream->i_block_offset;
size_t i_size = i_data;
size_t i_copy = 0;
block_t *p_block;
for( p_block = p_bytestream->p_block;
p_block != NULL; p_block = p_block->p_next )
{
// 计算需要读取数据的大小,并防止当前块数据读取越界处理
i_copy = __MIN( i_size, p_block->i_buffer - i_offset );
// 更新待读取的数据大小
i_size -= i_copy;
if( i_copy && p_data != NULL )
{// 若p_data不为空则将读取的i_copy大小的数据存入p_data指针中
// 【p_block->p_buffer + i_offset】该计算表示为:得到当前block数据的未读取数据的开始位置,
// 然后读取i_copy大小的数据
memcpy( p_data, p_block->p_buffer + i_offset, i_copy );
// 将存放读取数据的指针往后移动读取数据大小位置
p_data += i_copy;
}
// 若p_data为空,则表示仅仅跳过待读取数据的大小位置
// 若所有待读取数据大小都已读取完毕则退出读取
if( i_size == 0 )
break;
// 没有读取完毕,则继续循环读取下一个block块数据处理
p_bytestream->i_base_offset += p_block->i_buffer;
// 新block读取偏移量为0,因为还未读取呢
i_offset = 0;
}
// 更新块字节流中当前字节流读取指针块
p_bytestream->p_block = p_block;
// 更相信当前块已读取数据偏移量位置
p_bytestream->i_block_offset = i_offset + i_copy;
return VLC_SUCCESS;
}
static inline size_t block_BytestreamRemaining( const block_bytestream_t *p_bytestream )
{
// 此处处理为:检查和计算块字节流数据剩余未读取数据大小
// i_total表示:当前块字节流中数据块链的所有数据块总负载数据大小
// i_base_offset表示:此前已读取的所有block块数据已读取总大小位置即块基准偏移量
// i_block_offset表示:块数据内部已读取数据位置偏移量
return ( p_bytestream->i_total > p_bytestream->i_base_offset + p_bytestream->i_block_offset ) ?
p_bytestream->i_total - p_bytestream->i_base_offset - p_bytestream->i_block_offset : 0;
}
10、PacketizeParse实现分析:打包(分组分包)h264码流NALU单元数据
注意:从分析流程中可知,文中的【p_sys.leading.p_head】信息为SEI补充增强信息单元的信息
//【vlc/modules/packetizer/h264.c】
static block_t *PacketizeParse( void *p_private, bool *pb_ts_used, block_t *p_block )
{
decoder_t *p_dec = p_private;
// 注意:此处的block块数据中负载数据已经被处理为只负载一个NALU单元数据了即NAL头 + RBSP【原始字节序列负载】
// 移除掉当前block数据中一个NALU单元的尾部字节对齐添加的0字节数据
// 【条件:负载数据长度需大于5个字节数,若末尾字节值为0对齐字节值,
// 则将i_buffer负载数据大小减1,这样不会读到该数据了】
/* Remove trailing 0 bytes */
while( p_block->i_buffer > 5 && p_block->p_buffer[p_block->i_buffer-1] == 0x00 )
p_block->i_buffer--;
return ParseNALBlock( p_dec, pb_ts_used, p_block );
}
// 解析NAL单元数据块
// 解析AnnexB流格式类型的NAL数据
// 传入的参数【p_frag所有分片】数据块都必须以0 0 0 1 四个字节起始码开始
//【vlc/modules/packetizer/h264.c】
/*****************************************************************************
* ParseNALBlock: parses annexB type NALs
* All p_frag blocks are required to start with 0 0 0 1 4-byte startcode
*****************************************************************************/
static block_t *ParseNALBlock( decoder_t *p_dec, bool *pb_ts_used, block_t *p_frag )
{
decoder_sys_t *p_sys = p_dec->p_sys;
block_t *p_pic = NULL;
// 起始码四字节之后第5个字节就是NAL头信息,该字节后五个比特位表示的值就是NALU单元类型
const int i_nal_type = p_frag->p_buffer[4]&0x1f;
const mtime_t i_frag_dts = p_frag->i_dts;
const mtime_t i_frag_pts = p_frag->i_pts;
if( p_sys->b_slice && (!p_sys->p_active_pps || !p_sys->p_active_sps) )
{// 若需要码流数据分片并且PPS或SPS类型的NAL单元数据不存在时,则进入此处等待SPS或PPS数据
msg_Warn( p_dec, "waiting for SPS/PPS" );
// 重置上下文环境数据
/* Reset context */
// 丢弃已存储的NAL数据单元数据
// 见前面的分析
DropStoredNAL( p_sys );
// 重置输出端参数值
// 见前面的分析
ResetOutputVariables( p_sys );
// 清空字幕数据
cc_storage_reset( p_sys->p_ccs );
}
switch( i_nal_type )
{// NALU类型,1~23为单个NAL单元包,24~31表示需要分包或组合发送,0表示未定义
/*** Slices ***/
// 值为1:不分区,非IDR图像的片
case H264_NAL_SLICE:
// 值为2:片分区A ====》此处的DP表示数据分割的英文意思 【Data Partitioning】
case H264_NAL_SLICE_DPA:
// 值为3:片分区B
case H264_NAL_SLICE_DPB:
// 值为4:片分区C
case H264_NAL_SLICE_DPC:
// 值为5:IDR图像中的片
case H264_NAL_SLICE_IDR:
{// 备注:数据分割DP片层数据,可用于错误恢复解码
// 声明一个h264码流片分区数据结构体对象
h264_slice_t newslice;
if( i_nal_type == H264_NAL_SLICE_IDR )
{// 若为IDR图像中的片类型数据,因此标记可用于错误恢复解码
p_sys->b_recovered = true;
p_sys->i_recovery_frame_cnt = UINT_MAX;
p_sys->i_recoveryfnum = UINT_MAX;
}
// 解析NAL单元中【编码】片的头信息
// 见下面的分析
if( ParseSliceHeader( p_dec, p_frag, &newslice ) )
{// 解析成功
// 注译:只有IDR帧才会携带传输的该图像ID
// 注意此处p_sys->slice和newslice的区别:
// 前者表示上一个片【帧】信息,后者表示当前新到来的片信息。
/* Only IDR carries the id, to be propagated */
if( newslice.i_idr_pic_id == -1 )
newslice.i_idr_pic_id = p_sys->slice.i_idr_pic_id;
// 判断是否是新图像即是否是第一个VCL NAL单元数据
// 见10.3小节分析
bool b_new_picture = IsFirstVCLNALUnit( &p_sys->slice, &newslice );
if( b_new_picture )
{// 当前NALU单元数据为新图像时解析SEI信息
// 解析SEI补充增强信息单元【已匹配了SPS/PPS的视频帧】
/* Parse SEI for that frame now we should have matched SPS/PPS */
for( block_t *p_sei = p_sys->leading.p_head; p_sei; p_sei = p_sei->p_next )
{
if( (p_sei->i_flags & BLOCK_FLAG_PRIVATE_SEI) == 0 )
// 若当前帧数据不为SEI【模块私有信息】,则进行查询下一个数据块
continue;
// 找到SEI信息并解析
// HxxxParse_AnnexB_SEI实现见10.4小节分析
// ParseSeiCallback解析SEI信息的回调方法指针,实现见10.5小节分析
HxxxParse_AnnexB_SEI( p_sei->p_buffer, p_sei->i_buffer,
1 /* nal header */, ParseSeiCallback, p_dec );
}
// 该标识表示是否当前NALU单元数据为上面switch中片类型的信息
if( p_sys->b_slice )
// 若是片类型NALU数据则输出当前图像
// 见10.6小节分析
p_pic = OutputPicture( p_dec );
}
// 缓存当前的新片信息
/* */
p_sys->slice = newslice;
}
else
{// 片头部信息解析失败,则将pps清空
p_sys->p_active_pps = NULL;
// 注译:分片信息稍后将被丢弃
/* Fragment will be discarded later on */
}
// 记录当前NALU类型为片信息标识为true
p_sys->b_slice = true;
// 见上面流程中的相同分析
// // 添加当前片类型图像数据块链数据到frame帧数据块链尾部
block_ChainLastAppend( &p_sys->frame.pp_append, p_frag );
} break;
// 以下为NALU单元序列的前缀信息
/*** Prefix NALs ***/
// NAL访问单元分隔符:NALU类型为0x09
case H264_NAL_AU_DELIMITER:
if( p_sys->b_slice )
// 当前NALU类型为片信息标识时,获取输出图像
p_pic = OutputPicture( p_dec );
// 清空当前打包模块解码对象的帧数据块缓存值等,因为此时的数据块总是第一个NALU单元数据
/* clear junk if no pic, we're always the first nal */
DropStoredNAL( p_sys );
// 标识记录为当前模块私有AUD即访问单元分隔符
p_frag->i_flags |= BLOCK_FLAG_PRIVATE_AUD;
// 添加当前片类型图像数据块链数据到leading数据块链尾部
// leading结构体从[OutputPicture]方法分析中,
// 大致可看出其主要保存的是NALU中附加信息值即非视频帧数据块,
// 而frame结构体保存的就是视频帧数据块。
block_ChainLastAppend( &p_sys->leading.pp_append, p_frag );
break;
// NALU类型为:SPS 0x07 / PPS 0x08
case H264_NAL_SPS:
case H264_NAL_PPS:
if( p_sys->b_slice )
// 当前NALU类型为片信息标识时,获取输出图像
p_pic = OutputPicture( p_dec );
// 注译:存储用于在关键帧上插入【使用这些值】
/* Stored for insert on keyframes */
if( i_nal_type == H264_NAL_SPS )
{// SPS类型的NALU单元数据时
// 见10.7小节分析
PutSPS( p_dec, p_frag );
// 并标记当前SPS数据更新了
p_sys->b_new_sps = true;
}
else
{// PPS类型的NALU单元数据时
// 见10.8小节分析
PutPPS( p_dec, p_frag );
// 并标记当前PPS数据更新了
p_sys->b_new_pps = true;
}
break;
case H264_NAL_SEI:
if( p_sys->b_slice )
p_pic = OutputPicture( p_dec );
p_frag->i_flags |= BLOCK_FLAG_PRIVATE_SEI;
block_ChainLastAppend( &p_sys->leading.pp_append, p_frag );
break;
case H264_NAL_SPS_EXT:
case H264_NAL_PREFIX: /* first slice/VCL associated data */
case H264_NAL_SUBSET_SPS:
case H264_NAL_DEPTH_PS:
case H264_NAL_RESERVED_17:
case H264_NAL_RESERVED_18:
if( p_sys->b_slice )
p_pic = OutputPicture( p_dec );
block_ChainLastAppend( &p_sys->leading.pp_append, p_frag );
break;
/*** Suffix NALs ***/
case H264_NAL_END_OF_SEQ:
case H264_NAL_END_OF_STREAM:
/* Early end of packetization */
block_ChainLastAppend( &p_sys->frame.pp_append, p_frag );
/* important for still pictures/menus */
p_sys->i_next_block_flags |= BLOCK_FLAG_END_OF_SEQUENCE;
if( p_sys->b_slice )
p_pic = OutputPicture( p_dec );
break;
case H264_NAL_SLICE_WP: // post
case H264_NAL_UNKNOWN:
case H264_NAL_FILLER_DATA:
case H264_NAL_SLICE_EXT:
case H264_NAL_SLICE_3D_EXT:
case H264_NAL_RESERVED_22:
case H264_NAL_RESERVED_23:
default: /* others 24..31, including unknown */
block_ChainLastAppend( &p_sys->frame.pp_append, p_frag );
break;
}
*pb_ts_used = false;
if( p_sys->i_frame_dts <= VLC_TS_INVALID &&
p_sys->i_frame_pts <= VLC_TS_INVALID )
{
p_sys->i_frame_dts = i_frag_dts;
p_sys->i_frame_pts = i_frag_pts;
*pb_ts_used = true;
if( i_frag_dts > VLC_TS_INVALID )
date_Set( &p_sys->dts, i_frag_dts );
}
if( p_pic && (p_pic->i_flags & BLOCK_FLAG_DROP) )
{
block_Release( p_pic );
p_pic = NULL;
}
return p_pic;
}
//【vlc/modules/packetizer/h264.c】
static bool ParseSliceHeader( decoder_t *p_dec, const block_t *p_frag, h264_slice_t *p_slice )
{
decoder_sys_t *p_sys = p_dec->p_sys;
// 【脱掉的】NAL单元数据
const uint8_t *p_stripped = p_frag->p_buffer;
size_t i_stripped = p_frag->i_buffer;
// 去掉所有的AnnexB数据流格式数据中的起始码【三字节和四字节的起始码】
// 【其实也不是释放其起始码占用内存,而是将已读取数据指针位置往后移,移除掉起始码即可】
// 见第十五章节【Part 3】部分1.2.1小节分析 TODO
if( !hxxx_strip_AnnexB_startcode( &p_stripped, &i_stripped ) || i_stripped < 2 )
// 失败
return false;
// 解码片【Slice】的头信息和数据
// h264_decode_slice:见10.1小节分析
// GetSPSPPS该方法:获取NAL单元中SPS/PPS数据,见下面的分析
if( !h264_decode_slice( p_stripped, i_stripped, GetSPSPPS, p_sys, p_slice ) )
return false;
// 此处再次获取SPS、PPS数据,主要用于验证数据是否正常,因为上面的方法内部已经绑定处理过一次了
const h264_sequence_parameter_set_t *p_sps;
const h264_picture_parameter_set_t *p_pps;
GetSPSPPS( p_slice->i_pic_parameter_set_id, p_sys, &p_sps, &p_pps );
if( unlikely( !p_sps || !p_pps) )
return false;
// 激活可用的【PPS SPS】参数集数据
// 见10.2小节分析
ActivateSets( p_dec, p_sps, p_pps );
return true;
}
//【vlc/modules/packetizer/h264.c】
static void GetSPSPPS( uint8_t i_pps_id, void *priv,
const h264_sequence_parameter_set_t **pp_sps,
const h264_picture_parameter_set_t **pp_pps )
{
decoder_sys_t *p_sys = priv;
// 通过 pps id获取pps数组中的pps NALU数据,
// 然后通过其数据中的sps id从sps数组中获取sps NALU数据
*pp_pps = p_sys->pps[i_pps_id].p_pps;
if( *pp_pps == NULL )
*pp_sps = NULL;
else
*pp_sps = p_sys->sps[(*pp_pps)->i_sps_id].p_sps;
}
10.1、h264_decode_slice实现分析:
//【vlc/modules/packetizer/h264_slice.c】
bool h264_decode_slice( const uint8_t *p_buffer, size_t i_buffer,
void (* get_sps_pps)(uint8_t, void *,
const h264_sequence_parameter_set_t **,
const h264_picture_parameter_set_t ** ),
void *priv, h264_slice_t *p_slice )
{
// 由传入参数可知【priv】为【decoder_sys_t *p_sys】
// 片类型
int i_slice_type;
// h264分片对象信息初始化
// 见第3小节分析
h264_slice_init( p_slice );
// bit stream 【比特流】码流信息结构体对象,注意此处不是x264库中的结构体,而是vlc中自己定义的
bs_t s;
// 【比特流】码流大小
unsigned i_bitflow = 0;
// 初始化赋值码流信息
// 见10.1.1小节分析
bs_init( &s, p_buffer, i_buffer );
// 【比特流】码流大小
s.p_fwpriv = &i_bitflow;
// 转换功能【是否将模拟3字节转换为RBSP即原始字节序列负载数据】
// 作用其实就是:解码时脱壳操作即去掉NALU RBSP中的防伪0x03字节【避免与起始码竞争】
// 见10.1.2小节分析
s.pf_forward = hxxx_bsfw_ep3b_to_rbsp; /* Does the emulated 3bytes conversion to rbsp */
// NALU header头部信息
// 【1个字节:禁止位(1位)、重要性指示位(2位)、NALU类型(5位)】
/* nal unit header */
// 此处的处理为码流跳过头部信息第一个字节的第一位即修改里面的i_left剩余可用位数为7
// 见10.1.3小节分析
bs_skip( &s, 1 );
// NAL引用重要指示位
// 读取码流当前字节数据中剩余可用位数的2位bits数据值
// 见10.1.4小节分析
const uint8_t i_nal_ref_idc = bs_read( &s, 2 );
// NALU类型
// 读取码流当前字节数据中剩余可用位数的5位bits数据值
const uint8_t i_nal_type = bs_read( &s, 5 );
// 下面接着根据h264 Slice片头部信息进行解码对应h264语法元素信息的
// 获取h264片数据中第一个exp-golomb code 指数哥伦布编码
// 是一种压缩编码算法(视频编码中有用到这个了,h264,avs)
// 其实就是获取h24语法中的first_mb_in_slice即片中的第一个宏块地址,
// 片通过这个句法元素来标定它自己的地址。在帧场自适应模式下,宏块都是成对出现,
// 这时本句法元素表示的是第几个宏块对,
// 对应的第一个宏块的真实地址应该是:2*first_mb_in_slice */
// 见10.1.5小节分析
/* first_mb_in_slice */
/* int i_first_mb = */ bs_read_ue( &s );
// 片类型,其实就是h264帧类型【I P B SP SI帧】
/* slice_type */
i_slice_type = bs_read_ue( &s );
// 片类型:取余即为片类型的小值。
// 关于片类型值和h264帧类型的关联
// 【0:P 1:B 2:I 3:SP 4:SI 5:P 6:B 7:I 8:SP 9:SI】
p_slice->type = i_slice_type % 5;
/* */
p_slice->i_nal_type = i_nal_type;
p_slice->i_nal_ref_idc = i_nal_ref_idc;
// 读取到PPS id值 参考的图像参数集索引
p_slice->i_pic_parameter_set_id = bs_read_ue( &s );
// 最大不能大于255
if( p_slice->i_pic_parameter_set_id > H264_PPS_ID_MAX )
return false;
const h264_sequence_parameter_set_t *p_sps;
const h264_picture_parameter_set_t *p_pps;
// 绑定匹配/参考的PPS和SPS
/* Bind matched/referred PPS and SPS */
// 该方法调用见10小节中【GetSPSPPS】方法的分析
get_sps_pps( p_slice->i_pic_parameter_set_id, priv, &p_sps, &p_pps );
if( !p_sps || !p_pps )
return false;
// 此处先讲一个log2_XXX字段的值的含义:
// 如log2_xxx = y,其实表示的是log2为底真数V的对数等于y,即求V的值
// 即2的y次方 = V。
// 因此下面涉及到的log2命名就知道为啥如此命名并计算它。
// frame_num【解码顺序计算值】:每个参考帧都有一个连续的frame_num作为它们的标识,
// 它指明了各图像的解码顺序。非参考帧也有,但没有意义。
// log2_max_frame_num_minus4 这个句法元素主要是为读取另一个句法元素 frame_num 服务的,
// frame_num 是最重要的句法元素之一,它标识所属图像的解码顺序 。
// 这个句法元素同时也指明了 frame_num 的所能达到的最大值: MaxFrameNum = 2*exp( log2_max_frame_num_minus4 + 4 )
p_slice->i_frame_num = bs_read( &s, p_sps->i_log2_max_frame_num + 4 );
// frame_mbs_only_flag 本句法元素等于 0 时表示本序列中所有图像的编码模式都是帧,
// 没有其他编码模式存在;本句法元素等于 1 时表示本序列中图像的编码模式可能是帧,
// 也可能是场或帧场自适应,某个图像具体是哪一种要由其他句法元素决定。
if( !p_sps->frame_mbs_only_flag )
{// 本序列中所有图像的编码模式都是帧时
/* field_pic_flag */
// 读取当前一个比特位的int值,记录的是图像场的类型【初始化为0】
p_slice->i_field_pic_flag = bs_read( &s, 1 );
if( p_slice->i_field_pic_flag )
// 读取当前一个比特位的int值,记录的是图像底场的类型【初始化为-1】
p_slice->i_bottom_field_flag = bs_read( &s, 1 );
}
// 是否是IDR图像中的片信息 【值为5】
if( p_slice->i_nal_type == H264_NAL_SLICE_IDR )
// IDR图像片数据时,读取并解码哥伦布编码的数据,此为IDR图像ID
p_slice->i_idr_pic_id = bs_read_ue( &s );
// pic_order_cnt_type 指明了 poc (picture order count) 的编码方法,
// poc 标识图像的播放顺序即图像序列号。poc 可以由 frame-num 通过映射关系计算得来,
// 也可以索性由编码器显式地传送。
// 图像显示/播放顺序类型
p_slice->i_pic_order_cnt_type = p_sps->i_pic_order_cnt_type;
if( p_sps->i_pic_order_cnt_type == 0 )
{// 获取到POC底有效位
// log2_max_pic_order_cnt_lsb_minus4指明了变量MaxPicOrderCntLsb的值:
// MaxPicOrderCntLsb = pow(2, (log2_max_pic_order_cnt_lsb_minus4 + 4) )
p_slice->i_pic_order_cnt_lsb = bs_read( &s, p_sps->i_log2_max_pic_order_cnt_lsb + 4 );
if( p_pps->i_pic_order_present_flag && !p_slice->i_field_pic_flag )
// 若pps图像顺序展示标志位存在并且片的图像场标志位不存在时,则获取POC底场偏移值
// 见10.1.6小节分析
p_slice->i_delta_pic_order_cnt_bottom = bs_read_se( &s );
}
else if( (p_sps->i_pic_order_cnt_type == 1) &&
(!p_sps->i_delta_pic_order_always_zero_flag) )
{// delta_pic_order_always_zero_flag等于1时,句法元素delta_pic_order_cnt[0]和 delta_pic_order_cnt[1]不在片头出现,
// 并且它们的值默认为0;本句法元素等于0时,上述的两个句法元素将在片头出现。
// 因此此处需要获取这两个元素值
p_slice->i_delta_pic_order_cnt0 = bs_read_se( &s );
if( p_pps->i_pic_order_present_flag && !p_slice->i_field_pic_flag )
p_slice->i_delta_pic_order_cnt1 = bs_read_se( &s );
}
// 注:pps数据是在【vlc/modules/packetizer/h264_nal.c】的
// 【h264_parse_picture_parameter_set_rbsp】方法中赋值的
// 若有多余的图像展示标志位值,那么需要将其去掉【跳过该数据】
if( p_pps->i_redundant_pic_present_flag )
bs_read_ue( &s ); /* redudant_pic_count */
// num_ref_idx_l0_active_minus1 加1后指明目前参考帧队列的长度,
// 即有多少个参考帧(包括短期和长期)。值得注意的是,当目前解码图像是场模式下,
// 参考帧队列的长度应该是本句法元素再乘以2,因为场模式下各帧必须被分解以场对形式存在。
// (这里所说的场模式包括图像的场及帧场自适应下的处于场模式的宏块对)
// 本句法元素的值有可能在片头被重载。
// num_ref_idx_l0_active_minus1 , num_ref_idx_l1_active_minus1
// 在序列参数集中有句法元素 num_ref_frames 也是跟参考帧队列有关,
// 它们的区别是num_ref_frames指明参考帧队列的最大值,解码器用它的值来分配内存空间;num_ref_idx_l0_active_minus1
// 指明在这个队列中当前实际的、已存在的参考帧数目,这从它的名字“active”中也可以看出来。
// 图像时,并不是直接传送该图像的编号,而是传送该图像在参考帧队列中的序号。
// 这个序号并不是在码流中传送的,这个句法元素是 H.264 中最重要的句法元素之一,
// 编码器要通知解码器某个运动矢量所指向的是哪个参考,而是编码器和解码器同步地、用相同的方法将参考图像放入队列,
// 从而获得一个序号。这个队列在每解一个图像,甚至是每个片后都会动态地更新。
// 维护参考帧队列是编解码器十分重要的工作,而本句法元素是维护参考帧队列的重要依据。
// 参考帧队列的复杂的维护机制是 H.264 重要也是很有特色的组成部分。
unsigned num_ref_idx_l01_active_minus1[2] = {0 , 0};
// 分片【帧】类型【0:P 1:B 2:I 3:SP 4:SI 5:P 6:B 7:I 8:SP 9:SI】
if( i_slice_type == 1 || i_slice_type == 6 ) /* B slices */
// 读取当前字节中当前可读比特数的第一位比特值,此处为跳过该值
// 此值为直接空间运动预测标识位值
bs_read1( &s ); /* direct_spatial_mv_pred_flag */
if( i_slice_type == 0 || i_slice_type == 5 ||
i_slice_type == 3 || i_slice_type == 8 ||
i_slice_type == 1 || i_slice_type == 6 ) /* P SP B slices */
{
// num_ref_idx_active_override_flag为重载PPS中的参考帧队列中实际可用的参考帧的数目
// 若该比特位值为1则进行读取
if( bs_read1( &s ) ) /* num_ref_idx_active_override_flag */
{
// 重载值
num_ref_idx_l01_active_minus1[0] = bs_read_ue( &s );
if( i_slice_type == 1 || i_slice_type == 6 ) /* B slices */
// B帧PPS重载值
num_ref_idx_l01_active_minus1[1] = bs_read_ue( &s );
}
}
// 注译:下面,进一步处理,以确定MMCO 5存在的POC
/* BELOW, Further processing up to assert MMCO 5 presence for POC */
if( p_slice->i_nal_type == 5 || p_slice->i_nal_ref_idc == 0 )
{// P帧的片,P片没有mmco5,因此直接返回,不需要以下mmco5的处理流程
/* Early END, don't waste parsing below */
p_slice->has_mmco5 = false;
return true;
}
// 以下mmco5的处理流程
// 参考【帧】图像列表修正过程处理即参考帧重排序处理
// 每个参考图像都有frame_num【图像解码序列号】,
// 但编码器要指定当前图像的参考图像时使用的ref_id,frame_num->PicNum->ref_id
/* ref_pic_list_[mvc_]modification() */
// 根据h264的NAL类型定义值可知,20和21为保留值
const bool b_mvc = (p_slice->i_nal_type == 20 || p_slice->i_nal_type == 21 );
unsigned i = 0;
// 分片【帧】类型【0:P 1:B 2:I 3:SP 4:SI 5:P 6:B 7:I 8:SP 9:SI】
if( i_slice_type % 5 != 2 && i_slice_type % 5 != 4 )
// 不是I和SI帧时,将i加1,值为1
i++;
if( i_slice_type % 5 == 1 )
// 若为B帧,再加1,值为2
i++;
for( ; i>0; i-- )
{
// 指明List0是否进行重排序
// 获取当前1个比特位值类型为1时,表明需要重排序
if( bs_read1( &s ) ) /* ref_pic_list_modification_flag_l{0,1} */
{
uint32_t mod;
do
{
// mod记录:执行哪种重排序操作
// 0: 短期参考帧重排序,abs_diff_pic_num_minus1会出现在码流中,
// 从当前图像的PicNum减去(abs_diff_pic_num_minus1+1)后指明需要重排序的图像。
// 1: 短期参考帧重排序,abs_diff_pic_num_minus1会出现在码流中,
// 从当前图像的PicNum加上(abs_diff_pic_num_minus1+1)后指明需要重排序的图像
// 2: 长期参考帧重排序,long_term_pic_num会出现在码流中,指明需要重排序的图像。
// 3: 结束循环,退出重排序操作。
mod = bs_read_ue( &s );
if( mod < 3 || ( b_mvc && (mod == 4 || mod == 5) ) )
// 因此此处的处理:是将这两个值读取,但又没保存使用,应该是丢弃了这个数据??TODO 后续分析下原因
bs_read_ue( &s ); /* abs_diff_pic_num_minus1, long_term_pic_num, abs_diff_view_idx_min1 */
}
// bs_remain见本小节下面的分析【处理其实就是计算剩余可读字节中的总可读剩余比特数,防止越界】
while( mod != 3 && bs_remain( &s ) );
}
}
// 分片【帧】类型【0:P 1:B 2:I 3:SP 4:SI 5:P 6:B 7:I 8:SP 9:SI】
// 加权预测的处理
/* pred_weight_table() */
if( ( p_pps->weighted_pred_flag && ( i_slice_type == 0 || i_slice_type == 5 || /* P, SP */
i_slice_type == 3 || i_slice_type == 8 ) ) ||
( p_pps->weighted_bipred_idc == 1 && ( i_slice_type == 1 || i_slice_type == 6 ) /* B */ ) )
{// weighted_pred_flag用以指明是否允许P和SP片的加权预测,如果允许,在片头会出现用以计算加权预测的句法元素。
// weighted_bipred_flag[/idc]用以指明是否允许 B 片的加权预测,本句法元素等于 0 时表示使用默认加权预测模式,
// 等于 1 时表示使用显式加权预测模式,等于 2 时表示使用隐式加权预测模式。
// luma_log2_weight_denom: 给出参考帧列表中参考图像所有亮度的加权系数,[0..7]
bs_read_ue( &s ); /* luma_log2_weight_denom */
// 色度不同颜色平面标志位为0
if( !p_sps->b_separate_colour_planes_flag ) /* ChromaArrayType != 0 */
// chroma_log2_weight_denom: 给出参考帧列表中参考图像所有色度的加权系数,[0..7]
bs_read_ue( &s ); /* chroma_log2_weight_denom */
// 层数值,若B帧则为2,否则为1;即2的意思就是若有B帧则代表B、P帧PPS参考帧都需要重载操作
const unsigned i_num_layers = ( i_slice_type % 5 == 1 ) ? 2 : 1;
for( unsigned j=0; j < i_num_layers; j++ )
{
// num_ref_idx_l01_active_minus1[j]该值为B/P/SP的PPS参考序列重载值
// P SP:j = 0
// B: j = 1
for( unsigned k=0; k<=num_ref_idx_l01_active_minus1[j]; k++ )
{
// 1:在参考帧序列0中的亮度的加权系数存在
if( bs_read1( &s ) ) /* luma_weight_l{0,1}_flag */
{
// 读取luma_weight_l0[k]:用参考序列0预测亮度值时,所用的加权系数。
// 如果luma_weight_l0_flag=0,luma_weight_l0[k]=2exp(luma_log2_weight_denom)。
bs_read_se( &s );
// luma_offset_l0[k]:用参考序列0预测亮度值时,所用的加权系数的偏移,[-128-..127],
// 如果luma_weight_l0_flag=0,该值0
bs_read_se( &s );
}
// 色度不同颜色平面标志位为0
if( !p_sps->b_separate_colour_planes_flag ) /* ChromaArrayType != 0 */
{// 同Luma亮度处理相似,但用于色度
if( bs_read1( &s ) ) /* chroma_weight_l{0,1}_flag */
{// 此处需要如下读取两组【chroma_weight_l0】和【chroma_offset_l0】值
// chroma_weight_l0[k]
bs_read_se( &s );
// chroma_offset_l0[k]
bs_read_se( &s );
// chroma_weight_l0[k]
bs_read_se( &s );
// chroma_offset_l0[k]
bs_read_se( &s );
}
}
}
}
}
// 参考图像序列标记 处理流程
// 前文介绍的重排序操作是对参考帧队列重新排序,
// 而标记操作负责将参考图像移入或移出参考帧队列。
/* dec_ref_pic_marking() */
if( p_slice->i_nal_type != 5 ) /* IdrFlag */
{// 当前片信息不为IDR图像中的片时,则处理
// adaptive_ref_pic_marking_mode_flag值为:
// 0:FIFO,使用滑动窗的机制,先入先出,在这种模式下,无法对长期参考帧进行操作,
// 1:自适应标记,后续码流中会有一系列句法元素显式指明操作的步骤。
if( bs_read1( &s ) ) /* adaptive_ref_pic_marking_mode_flag */
{// 自适应标记时
// MMCO表示【memory_management_control_operation】内存管理控制操作
// 自适应模式下,指明本次操作的具体内容:
// 0:结束循环
// 1:将一个短期参考图像标记为非参考图像,也即将一个短期参考图像移出参考帧队列
// 2:将一个长期参考图像标记为非参考图像,也即将一个长期参考图像移出参考帧队列
// 3:将一个短期参考图像转为长期参考图像
// 4:指明长期参考帧的最大数目
// 5:清空参考帧队列,将所有参考图像移出参考帧队列,并禁用长期参考机制
// 6:将当前图像存存为一个长期参考帧
uint32_t mmco;
do
{
// 获取其值【0..6】
mmco = bs_read_ue( &s );
if( mmco == 1 || mmco == 3 )
// difference_of_pic_nums_minus1: 通过该元素可以计算出需要操作的图像在短期参考队列中的序号
bs_read_ue( &s ); /* diff_pics_minus1 */
if( mmco == 2 )
// long_term_pic_num: 得到所要操作的长期参考图像的序号
bs_read_ue( &s ); /* long_term_pic_num */
if( mmco == 3 || mmco == 6 )
// long_term_frame_idx: 分配一个长期参考帧的序号给一个图像
bs_read_ue( &s ); /* long_term_frame_idx */
if( mmco == 4 )
// max_long_tewrm_frame_idx_plus1: 此元素减1,指明长期参考队列的最大数目,[0..num_ref_frames]
bs_read_ue( &s ); /* max_long_term_frame_idx_plus1 */
if( mmco == 5 )
{// 清空参考帧队列,将所有参考图像移参考帧队列,并禁用长期参考机制
// 即此处就是直接退出mmco的任何处理
p_slice->has_mmco5 = true;
break; /* Early END */
}
}
while( mmco > 0 );
}
}
// 注译:若需要存储除上面MMCO存在相关数据之外的更多数据,则需要注意代码中【"Early END"】情况。
/* If you need to store anything else than MMCO presence above, care of "Early END" cases */
return true;
}
// 梳理一下上面方法中关于bs_read_ue和bs_read_se的处理,
// 很多地方都只读但没保存读到的值仅仅读到后就跳过该数据了,
// 应该是VLC推流打包数据中用不到这些数据,然后进行跳过处理?? TODO 后续分析下原因
// [vlc/include/vlc_bits.h]
static inline int bs_remain( const bs_t *s )
{// 主要的处理就是判断是否数据已读取完
if( s->p >= s->p_end )
// 数据已读取完
return 0;
else
// 此处其实就是计算剩余可读字节中的总可读剩余比特数
return( 8 * ( s->p_end - s->p ) - 8 + s->i_left );
}
10.1.1、bs_init实现分析:【此处为vlc的实现】
// 【vlc/include/vlc_bits.h】
static inline void bs_init( bs_t *s, const void *p_data, size_t i_data )
{
bs_write_init( s, (void*) p_data, i_data );
// 设置为只读 RD only
s->b_read_only = true;
}
// 【vlc/include/vlc_bits.h】
static inline void bs_write_init( bs_t *s, void *p_data, size_t i_data )
{
// 记录NALU数据开始结束指针位置
s->p_start = (uint8_t *)p_data;
s->p = s->p_start;
s->p_end = s->p_start + i_data;
// 可用位数:其实应该为当前存放数据的数据位数宽度值即目前[p_data]为8位指针存储的
s->i_left = 8;
s->b_read_only = false;
s->p_fwpriv = NULL;
s->pf_forward = NULL;
}
// 下面的该分析是x264库中的实现。。。 但不是此处vlc自己的实现,不过已经分析了就先放着吧。。。 不用看
// 【vlc/contrib/contrib-android-arm-lilnux-androideabi/x264/common/bitstream.h】
static inline void bs_init( bs_t *s, void *p_data, int i_data )
{
// 将指针转为int值后与3进行与运算取二进制最后两位值作为偏移量值
// offset值可能为0、1、2、3
int offset = ((intptr_t)p_data & 3);
// 因此此处计算指针为:指针往前移动,指向p_data指针的前0或1或2或3个字节的数据位置,
// 其实作用一个就是指向此前NALU数据前面的起始码开始位置 TODO
s->p = s->p_start = (uint8_t*)p_data - offset;
// 指向数据末尾
s->p_end = (uint8_t*)p_data + i_data;
// 计算计数值:可用位数。 乘以8是将其转换为位单位
// WORD_SIZE宏定义为【sizeof(void*)】即计算的是无类型指针变量本身的大小【如32位系统指针变量永远为32位4字节】
s->i_left = (WORD_SIZE - offset)*8;
// 计算当前数据的位数
// 此处通过分析应该其作用是【大端或小端处理】即计算机的字节存储次序处理
// M32宏定义表示将传入参数(8位存储字节次序)加载和存储为对齐的32位类型数据
// 【会自动字节对齐(备注:memcopy不会自动对齐字节位数)】
s->cur_bits = endian_fix32( M32(s->p) );
// 将当前数据的总位数右移运算
// 由上面可知,offset值可能为0、1、2、3
// 也就是将当前【cur_bits】位数值除以2的1x8、2x8、3x8、4x8次方之后的商即为其值
// 该参数待后续实例分析确认其值含义 TODO
s->cur_bits >>= (4-offset)*8;
}
10.1.2、hxxx_bsfw_ep3b_to_rbsp实现分析:
该方法的处理,简单的说就是将当前码流数据脱壳后将真正的码流真实数据往前移动【i_count】字节,也就是指向后面的指定位置后的新字节位置,便于后续读取码流数据。
//【vlc/modules/packetizer/hxxx_nal.h】
// 注译:vlc_bits的bs_t的forward前向回调函数,用于剥离【移除】模拟预防三个字节
/* vlc_bits's bs_t forward callback for stripping emulation prevention three bytes */
static inline uint8_t *hxxx_bsfw_ep3b_to_rbsp( uint8_t *p, uint8_t *end, void *priv, size_t i_count )
{// 传入参数p为开始位置指针
// 该参数是用于记录当前比特流bit flow【码流】位置,
// 注意此处的【bitflow】作用是:标记循环执行次数有效性【见下分析】
// 由初始化可知该值最开始为0
unsigned *pi_prev = (unsigned *) priv;
for( size_t i=0; i<i_count; i++ )
{
// 注意此处P是先往前移了一个字节即指向了下一个新字节
if( ++p >= end )
// 若读取已到尾部,则返回退出
return p;
// 右移运算,并和0或1进行或运算,若p指针指向值不为空则和0进行或运算,否则为空时和1进行或运算
*pi_prev = (*pi_prev << 1) | (!*p);
// 重要:从Nal提取RBSP时,过滤掉0x3. 就是说在Nal中遇到 00 00 03 时,需要移除掉03
// 扩展:
// SODB 数据比特串-->最原始的编码数据
// RBSP 原始字节序列载荷-->在SODB的后面填加了结尾比特(RBSP trailing bits
// 一个bit“1”)若干比特“0”,以便字节对齐。
// EBSP 扩展字节序列载荷-->在RBSP基础上填加了仿校验字节(0X03)它的原因是:
// 在NALU加到Annexb上时,需要填加每组NALU之前的开始码StartCodePrefix,
// 如果该NALU对应的slice为一帧的开始则用4位字节表示,ox00000001,否则用3位字节表示ox000001.
// 为了使NALU主体中不包括与开始码相冲突的,在编码时,每遇到两个字节连续为0,
// 就插入一个字节的0x03。解码时将0x03去掉。也称为脱壳操作。
if( *p == 0x03 &&
( p + 1 ) != end ) /* Never escape sequence if no next byte */
{// 注译:如果没有下一个字节,永远不要转义序列
// 此处0x06 = 0000 0110 主要是判断pi_prev是否经过了三个字节的运算,若是则进入
if( (*pi_prev & 0x06) == 0x06 )
{
// 解码时脱壳操作即去掉NALU中的防伪0x03字节
++p;
*pi_prev = !*p;
}
}
}
return p;
}
10.1.3、bs_skip实现分析:
该处理为码流开始字节剩余可用位数位置开始跳过i_count位的计算,即修改里面的i_left剩余可用位数
// 【vlc/include/vlc_bits.h】
static inline void bs_skip( bs_t *s, ssize_t i_count )
{
// 由此前分析可知当前字节剩余可用位数该值初始化为8,如减去1后为7
s->i_left -= i_count;
// 小于等于0才能进入,否则不处理,因为当前字节中还有剩余可访问位数
if( s->i_left <= 0 )
{
// i_left大于等于8并且小于16时,i_bytes该值为0,小于8则该值大于0的整数(通常为1),
// 大于等于16则该值小于0的整数
const size_t i_bytes = 1 + s->i_left / -8;
// 此方法为宏定义
// 见下面的分析
bs_forward( s, i_bytes );
if( i_bytes * 8 < i_bytes /* ofw */ )
s->i_left = i_bytes;
else
s->i_left += 8 * i_bytes;
}
}
// 因此最终调用了pf_forward方法,即上面分析过的流程,解码时脱壳操作去掉NALU中防伪0x03字节数据
#define bs_forward( s, i ) \
s->p = s->pf_forward ? s->pf_forward( s->p, s->p_end, s->p_fwpriv, i ) : s->p + i
10.1.4、bs_read实现分析:
// 【vlc/include/vlc_bits.h】
static inline uint32_t bs_read( bs_t *s, int i_count )
{
// 33个32位的十六进制表示的掩码值
static const uint32_t i_mask[33] =
{ 0x00,
0x01, 0x03, 0x07, 0x0f,
0x1f, 0x3f, 0x7f, 0xff,
0x1ff, 0x3ff, 0x7ff, 0xfff,
0x1fff, 0x3fff, 0x7fff, 0xffff,
0x1ffff, 0x3ffff, 0x7ffff, 0xfffff,
0x1fffff, 0x3fffff, 0x7fffff, 0xffffff,
0x1ffffff, 0x3ffffff, 0x7ffffff, 0xfffffff,
0x1fffffff,0x3fffffff,0x7fffffff,0xffffffff};
int i_shr, i_drop = 0;
uint32_t i_result = 0;
// 若码流读取位数大于32位则进行记录剩余位数drop值
// 因此该方法处理读取的值为,一次最多读取32位即4个字节的数据,将其转为int值
if( i_count > 32 )
{
i_drop = i_count - 32;
i_count = 32;
}
while( i_count > 0 )
{
if( s->p >= s->p_end )
{// 开始读取的内存位置和码流数据结尾位置相同,则退出
break;
}
if( ( i_shr = s->i_left - i_count ) >= 0 )
{// 若当前字节数据剩余可用位数足够读取,则进入此处直接读取当前字节中的指定位数值
/* more in the buffer than requested */
// 如当前读取NAL header字节中的第二、三位的值并转换为int值即重要性指示位值
// 则可知i_shr为5即7 - 2获得,然后将当前字节int值进行5位的右移运算,
// 此时得到的值为高3位,然后在于【i_count】值此时为2,
// 和i_mask[i_count]即值为i_mask[2] = 0x03做与运算,
// 即最后是0000 0111 & 0000 0011 计算即可获得当前字节中的第二、三位表示的int值
// 注意:这里还有一个将此前的result值和当前计算的值进行或运算,
// 其目的是为了当读取位数值大于当前剩余的可用位数之后会先进入else中进行分组多次读取,
// 最后一次进入到if中,从而得到完全的所有位数表示的int值。
i_result |= ( *s->p >> i_shr )&i_mask[i_count];
// 更新剩余可用位数
s->i_left -= i_count;
if( s->i_left == 0 )
{// 若刚好读取完当前字节中剩余位数,则进行下一个字节的forward流程处理
// 该方法分析见上面小节相关分析
bs_forward( s, 1 );
// 因为是新字节数据,因此可用位数变为8
s->i_left = 8;
}
break;
}
else
{// 否则当前字节剩余可用位数不足
/* less in the buffer than requested */
// 由上面的分析可知,i_shr此时的值为还需要读取的位数的负值
if( -i_shr == 32 )
// 为i_shr为-32值,表明此前i_left值为0,那么重置i_result值
i_result = 0;
else
// 通过上面的类似分析,可知此处处理为先读取剩余可用位数,并将其进行左运算符计算,
// 即将当前字节【8位】中可用位数表示的int值和【-i_shr】值进行左运算符计算,
// 实际就是将其值置为合适的高位位置中,然后可能再次进入到此处
// 即分组多次读取对应位数位置上表示的int值,将其进行和此前结果或运算,
// 最终得到请求的【i_count】多位数表示的32位int值。
i_result |= (*s->p&i_mask[s->i_left]) << -i_shr;
// 减去本次已读取的位数值
i_count -= s->i_left;
// 进行下一个字节的forward流程处理
// 该方法分析见上面小节相关分析
bs_forward( s, 1);
// 因为是新字节数据,因此可用位数变为8
s->i_left = 8;
}
}
if( i_drop )
// 此处的处理结果:会将当前读取数据指针位置,向后移动i_drop个字节数据,
// 即跳过这些字节数据。
// 目前不知道这么做的理由是啥,后续见到大于32位读取时再结合场景分析吧 TODO
bs_forward( s, i_drop );
// 返回当前请求读取位数的int转换值数据
return( i_result );
}
10.1.5、bs_read_ue实现分析:【请查看下一章分析】
【十五】【vlc-android】vlc-sout流媒体输出端源码实现分析【Part 2】【03】