为什么有poc_order_cnt_type?poc_order_cnt_type是为了推断出POC,POC表示了播放的顺序,frame_num在传递参考帧后加1,这里涉及到一个播放顺序和解码顺序的问题。
poc_order_cnt_type分为三种,取值为0~2,先从最简单的poc_order_cnt_type为2说起。
poc_order_cnt_type为2
poc_order_cnt_type为2时,显示顺序与解码顺序一致,这种情况下不存在B帧,在这种情况下不存在连续的非参考帧。
协议中定义在8.2.1.3一节。对FrameNumOffset的推导过程如下
以下代码为JM中对poc_order_cnt_type=2的情况的解析,JM代码和协议规范完全一致。
if(pSlice->idr_flag) // IDR picture
{
p_Vid->FrameNumOffset=0; // first pix of IDRGOP,
pSlice->ThisPOC = pSlice->framepoc = pSlice->toppoc = pSlice->bottompoc = 0;
if(pSlice->frame_num){
error("frame_num not equal to zero in IDR picture", -1020);
}
}
else{
if (p_Vid->last_has_mmco_5){
p_Vid->PreviousFrameNum = 0;
p_Vid->PreviousFrameNumOffset = 0;
}
if (pSlice->frame_num<p_Vid->PreviousFrameNum){
p_Vid->FrameNumOffset = p_Vid->PreviousFrameNumOffset + p_Vid->max_frame_num;
}
else{
p_Vid->FrameNumOffset = p_Vid->PreviousFrameNumOffset;
}
pSlice->AbsFrameNum = p_Vid->FrameNumOffset+pSlice->frame_num;
.....
p_Vid->PreviousFrameNum=pSlice->frame_num;
p_Vid->PreviousFrameNumOffset=p_Vid->FrameNumOffset;
}
下面先解释下代码中出现的变量的含义,然后再结合协议理解。
- pSlice->AbsFrameNum
实际的frameNum,取值范围是0 ~ GOP_SIZE - 1。
- p_Vid->max_frame_num
和协议文档中的MaxFrameNum对应,pSlice->AbsFrameNum中的低log2_max_frame_num bits值对应,因为如果我们码流中用pSlice->AbsFrameNum标识帧号,则随着帧号的增加,所用的bit也越来越多,为了节省码流,在码流中只传输低log2_max_frame_num bits作为帧号。
如下图,在SPS中,log2_max_frame_num_minus4=0,则log2_max_frame_num = 4,所以最多有16个取值,
即p_Vid->max_frame_num = 16。
- pSlice->frame_num
为实际帧号的低log2_max_frame_num的值 ,取值范围是 0 - p_Vid->max_frame_num - 1。
- p_Vid->PreviousFrameNum
为上一帧帧号的低log2_max_frame_num的值,取值范围是0 - p_Vid->max_frame_num - 1。
- p_Vid->FrameNumOffset
为实际帧号的高bit的值,有代码看出
pSlice->AbsFrameNum = p_Vid->FrameNumOffset+pSlice->frame_num;
p_Vid->FrameNumOffset必为p_Vid->max_frame_num的整数倍。
- p_Vid->PreviousFrameNumOffset
为上一帧帧号的高bit的值,同样也为p_Vid->max_frame_num的整数倍。
了解了代码中这些参数的含义,也就理解了协议的含义,本质上就是frame num的更新,而POC值一般为
pSlice->AbsFrameNum的两倍,这里也不再赘述,详细可以看协议8.2.1.3
pic_order_cnt_type为0
定义在H264协议8.2.1.1中。pic_order_cnt_type为0时,可以有B帧也可以没有B帧,但是大多数情况下都是含有B帧,因为如果没有B帧,可以设置pic_order_cnt_type为2,这样还更省码流,具体可以参考协议,这里以JM代码为例来说明,JM代码和协议也是完全吻合的。
先来介绍代码中的相关参数说明
- MaxPicOrderCntLsb
对应码流结构中的Log2MaxPOCLsbMinus4,含义和log2_max_frame_num_minus4类似,标识POC的低Log2MaxPocLsb bit能标识的POC个数,对pic_order_cnt_type为2来说,因为解码顺序和播放顺序完全一致,所以Log2MaxPOCLsb没有存在的必要,但是如果播放顺序和解码顺序不同,所以码流中有必要有标识POC的字段。
如下图log2_max_pic_order_cnt_lsb_minus4为2,所以Log2_max_pic_order_cnt_lsb为6,所以MaxPicOrderCntLsb为64。
- p_Vid->PrevPicOrderCntLsb
含义类似p_Vid->PreviousFrameNum,取值范围是0 - MaxPicOrderCntLsb -1,表明前一个参考帧picture的POC的低log2_max_pic_order_cnt_lsb的值。
- p_Vid->PrevPicOrderCntMsb
含义类似p_Vid->PrevPicOrderCntLsb,表明前一个picture的POC的高bit的值。
- pSlice->pic_order_cnt_lsb
当前slice的实际POC低log2_max_pic_order_cnt_lsb bit的值。
- pSlice->PicOrderCntMsb
当前slice的实际POC高bit的值。
从代码来看,主要工作分为三部分,一个是PrevPicOrderCnt赋值,一个是PicOrderCnt赋值,还有一个是POC的赋值,PrevPicOrderCnt赋值比较简单,代码如下,这里不再赘述。
if(pSlice->idr_flag){
p_Vid->PrevPicOrderCntMsb = 0;
p_Vid->PrevPicOrderCntLsb = 0;
}
else{
if (p_Vid->last_has_mmco_5){
if (p_Vid->last_pic_bottom_field){
p_Vid->PrevPicOrderCntMsb = 0;
p_Vid->PrevPicOrderCntLsb = 0;
}
else{
p_Vid->PrevPicOrderCntMsb = 0;
p_Vid->PrevPicOrderCntLsb = pSlice->toppoc;
}
}
}
......
if(pSlice->nal_reference_idc){ // B帧不会进此分支
p_Vid->PrevPicOrderCntLsb = pSlice->pic_order_cnt_lsb;
p_Vid->PrevPicOrderCntMsb = pSlice->PicOrderCntMsb;
}
PicOrderCnt赋值的代码如下:
// Calculate the MSBs of current picture
if( pSlice->pic_order_cnt_lsb < p_Vid->PrevPicOrderCntLsb &&
( p_Vid->PrevPicOrderCntLsb - pSlice->pic_order_cnt_lsb ) >= ( MaxPicOrderCntLsb / 2 ) ){
// condition 1
pSlice->PicOrderCntMsb = p_Vid->PrevPicOrderCntMsb + MaxPicOrderCntLsb;
}
else if ( pSlice->pic_order_cnt_lsb > p_Vid->PrevPicOrderCntLsb &&
( pSlice->pic_order_cnt_lsb - p_Vid->PrevPicOrderCntLsb ) > ( MaxPicOrderCntLsb / 2 ) ){
// condition 2
pSlice->PicOrderCntMsb = p_Vid->PrevPicOrderCntMsb - MaxPicOrderCntLsb;
}
else{
// condition 3
pSlice->PicOrderCntMsb = p_Vid->PrevPicOrderCntMsb;
}
下面分别看没有B帧的情况和B帧存在的情况
- 不存在B帧
这种情况下播放顺序和解码顺序一致,假设Log2MaxPOCLsbMinus4=2,则MaxPicOrderCntLsb=64,我们编码50帧。然后分析码流和代码。
前32帧都会进入condtion3分支, pSlice->pic_order_cnt_lsb每次增加2,p_Vid->PrevPicOrderCntLsb记录上一帧的pSlice->pic_order_cnt_lsb的值,PrevPicOrderCntMsb为0。
当到了第32帧时,pSlice->pic_order_cnt_lsb是62,p_Vid->PrevPicOrderCntLsb是60,
pSlice->frame_num是15,PrevPicOrderCntMsb是0。
下一帧到来时,pSlice->pic_order_cnt_lsb需要回绕,此时pSlice->pic_order_cnt_lsb是0,
p_Vid->PrevPicOrderCntLsb是62,pSlice->frame_num是0,PrevPicOrderCntMsb是0,根据条件会进入condition1分支, pSlice->PicOrderCntMsb为赋值为64。
接下来每帧都会进condition3分支,直至再次出现pSlice->pic_order_cnt_lsb回绕,再进入condition1分支,周期此过程。
- 存在B帧
假设Log2MaxPOCLsbMinus4=2,则MaxPicOrderCntLsb=64,NumberBFrames=1,B帧不作为参考帧,我们编码50帧。然后分析码流和代码。
如下所示,frame_num对应代码中的pSlice->frame_num;pic_order_cnt_lsb对应pSlice->pic_order_cnt_lsb;PrevPicOrderCntLsb对应p_Vid->PrevPicOrderCntLsb;当有参考帧传输时,frame_num加1。
Slice | Type | Used for reference | frame_num | pic_order_cnt_lsb | PrevPicOrderCntLsb | Display order |
1st | I | Yes | 0 | 0 | 0 | 0 |
2nd | P | Yes | 1 | 4 | 0 | 2 |
3rd | B | No | 2 | 2 | 4 | 1 |
4th | P | Yes | 2 | 8 | 4 | 4 |
5th | B | No | 3 | 6 | 8 | 3 |
6th | P | Yes | 3 | 12 | 8 | 6 |
7th | B | No | 4 | 10 | 12 | 5 |
8th | P | Yes | 4 | 16 | 12 | 8 |
9th | B | No | 5 | 14 | 16 | 7 |
只有参考帧,p_Vid->PrevPicOrderCntLsb和p_Vid->PrevPicOrderCntMsb的才会赋值,所以PrevPicOrderCntLsb如上。
当pSlice->pic_order_cnt_lsb没有发生回绕时,进入condtion 3分支;当 pSlice->pic_order_cnt_lsb发生回绕时,先进入conditon1,发生回绕的时候是P帧,发生回绕后下一帧又会进入condition2分支。
POC赋值的代码如下,delta_pic_order_cnt_bottom 表示一个编码帧的底场和顶场的图像顺序数之间的差
if(pSlice->field_pic_flag==0){ // 不存在场
pSlice->toppoc = pSlice->PicOrderCntMsb + pSlice->pic_order_cnt_lsb;
pSlice->bottompoc = pSlice->toppoc + pSlice->delta_pic_order_cnt_bottom;
pSlice->ThisPOC = pSlice->framepoc = (pSlice->toppoc < pSlice->bottompoc)? pSlice->toppoc : pSlice->bottompoc; // POC200301
}
else if (pSlice->bottom_field_flag == FALSE){ // 顶场
pSlice->ThisPOC= pSlice->toppoc = pSlice->PicOrderCntMsb + pSlice->pic_order_cnt_lsb;
}
else{ // 底场
pSlice->ThisPOC= pSlice->bottompoc = pSlice->PicOrderCntMsb + pSlice->pic_order_cnt_lsb;
}
pSlice->framepoc = pSlice->ThisPOC;
p_Vid->ThisPOC = pSlice->ThisPOC;
pic_order_cnt_type为1
定义在H264协议8.2.1.2中,Log2MaxPOCLsbMinus4=2,则MaxPicOrderCntLsb=64,NumberBFrames=2,B帧不作为参考帧,我们编码50帧。然后分析码流和代码。
frame Type的顺序依次为I P B B P B B P B ......。
P B B视为一个cycle,sps中num_ref_frames_in_pic_order_cnt_cycle为1,表示一个循环中的参考帧数目。
Slice | Type | Used for reference | frame_num | POC | Display order |
1st | I | Yes | 0 | 0 | 0 |
2nd | P | Yes | 1 | 6 | 3 |
3rd | B | No | 2 | 2 | 1 |
4th | B | No | 2 | 4 | 2 |
5th | P | Yes | 2 | 12 | 6 |
6th | B | No | 3 | 8 | 4 |
7th | B | No | 3 | 10 | 5 |
8th | P | Yes | 3 | 18 | 9 |
9th | B | No | 4 | 14 | 7 |
10th | B | No | 4 | 16 | 8 |
11th | P | Yes | 4 | 24 | 12 |
12th | B | No | 5 | 20 | 10 |
13th | B | No | 5 | 22 | 11 |
14th | P | Yes | 5 | 30 | 15 |
15th | B | No | 6 | 26 | 13 |
16th | B | No | 6 | 28 | 14 |
对于pic_order_cnt_type为0和1的情况,最重要的是要明确Display order,所以就要分析下代码中POC的计算方法。先看几个重要的参数,下图是SPS中的部分参数。
- offset_for_ref_frame
对应代码中的active_sps->offset_for_ref_frame,在本例中一个cycle只有一个参考帧,所以offset_for_ref_frame只有一个元素。它表示的含义是上一个参考帧到下一个参考帧的picture offset。举个例子,从上面表格看,Slice 2 对应的是P帧,Display order为3,下一个P帧对应的是Slice 3,对应的Display order为6,Display order后者减前者为3,这个差值3对应frame的差值,对应的picture为6,即offset_for_ref_frame[0]。
- offset_for_non_ref_pic
对应代码中的active_sps->offset_for_non_ref_pic,它的含义是上一个参考帧到下一个非参考帧之间的picture offset。还用上面的表格为例,Slice 2对应的是P帧,Display Order为3,下一个非P帧是Slice 3,对应的Display Order为1,后者减前者差值为-2,对应picture offset为-4,即offset_for_non_ref_pic。
- offset_for_top_to_bottom_field
对应代码中的active_sps->offset_for_top_to_bottom_field,用于计算底场的POC,它含义是bottom filed expected poc和top filed poc的差异。
再看Slice header中的字段
- delta_pic_order_cnt[0]
对应代码中的pSlice->delta_pic_order_cnt[0],它的含义是一个编码帧的top field或一个编码场的POC与ExpectedPicOrderCnt之间的差异。
- delta_pic_order_cnt[1]
对应代码中的pSlice->delta_pic_order_cnt[1],它的含义是一个编码帧的bottom field POC 与预期的bottom filed POC之间的差异,而预期的bottom field POC为top field poc + offset_for_top_to_bottom_field;
如果码流中没有,则默认为0,本例中码流中不存在delta_pic_order_cnt[1],它恒为0,如下所示句法,在满足一定条件下delta_pic_order_cnt[1]才存在。
先看ExpectedDeltaPerPicOrderCntCycle的计算,它的含义是一个cycle中有多少个picture。
p_Vid->ExpectedDeltaPerPicOrderCntCycle=0;
if(active_sps->num_ref_frames_in_pic_order_cnt_cycle){
for(i=0;i<(int) active_sps->num_ref_frames_in_pic_order_cnt_cycle;i++){
p_Vid->ExpectedDeltaPerPicOrderCntCycle += active_sps->offset_for_ref_frame[i];
}
}
由上面的分析易知,p_Vid->ExpectedDeltaPerPicOrderCntCycle为6。
再看p_Vid->ExpectedPicOrderCnt的计算,ExpectedPicOrderCnt为预期的POC,代码比较简单,不再赘述,主要代码如下,pSlice->AbsFrameNum对应P帧的个数,通过AbsFrameNum可以推断出当前经过了多少个cycle。
if(pSlice->AbsFrameNum)
{
// 计算经过了多少次cycle
p_Vid->PicOrderCntCycleCnt = (pSlice->AbsFrameNum-1)/active_sps->num_ref_frames_in_pic_order_cnt_cycle;
// 这个cycle中多余了多少个P帧
p_Vid->FrameNumInPicOrderCntCycle = (pSlice->AbsFrameNum-1)%active_sps->num_ref_frames_in_pic_order_cnt_cycle;
// 经过了整cycle的picture 数目
p_Vid->ExpectedPicOrderCnt = p_Vid->PicOrderCntCycleCnt*p_Vid->ExpectedDeltaPerPicOrderCntCycle;
for(i=0;i<=(int)p_Vid->FrameNumInPicOrderCntCycle;i++){ // 加上多余的P帧的offset
p_Vid->ExpectedPicOrderCnt += active_sps->offset_for_ref_frame[i];
}
}
else{
p_Vid->ExpectedPicOrderCnt=0;
}
if(!pSlice->nal_reference_idc){ // 如果当前是B帧
p_Vid->ExpectedPicOrderCnt += active_sps->offset_for_non_ref_pic;
}
主要注意的是:如果当前帧是P帧,上面计算的ExpectedPicOrderCnt的很好理解;如果是B帧,因为显示是先出B帧再出P帧,所以要加上active_sps->offset_for_non_ref_pic。但是这里有个问题,就是如果连续多个B帧,这些B帧的ExpectedPicOrderCnt值是一样的,需要再做进一步调整。计算POC的代码如下。
需要注意delta_pic_order_cnt[0]在帧编码的情况下的意义是top field poc和ExpectedPicOrderCnt的差异,如果是场编码,delta_pic_order_cnt[0]的意义是当前picture POC和ExpetedPicOrderCnt的差异,这也是在顶场和底场的情况下,计算POC用的都是pSlice->delta_pic_order_cnt[0]。计算底场的POC还需要加上offset_for_top_to_bottom_field。
如果当前是帧编码,先通过ExpectedPicOrderCnt和delta_pic_order_cnt[0]计算toppoc;然后通过toppoc计算bottompoc,slice poc为toppoc和bottompoc的最小者。
if(pSlice->field_pic_flag==0){ // frame
pSlice->toppoc = p_Vid->ExpectedPicOrderCnt + pSlice->delta_pic_order_cnt[0];
pSlice->bottompoc = pSlice->toppoc + active_sps->offset_for_top_to_bottom_field + pSlice->delta_pic_order_cnt[1];
pSlice->ThisPOC = pSlice->framepoc = (pSlice->toppoc < pSlice->bottompoc)? pSlice->toppoc : pSlice->bottompoc; // POC200301
}
else if (pSlice->bottom_field_flag == FALSE){ // 顶场
pSlice->ThisPOC = pSlice->toppoc = p_Vid->ExpectedPicOrderCnt + pSlice->delta_pic_order_cnt[0];
}
else{ // 底场
pSlice->ThisPOC = pSlice->bottompoc = p_Vid->ExpectedPicOrderCnt + active_sps->offset_for_top_to_bottom_field + pSlice->delta_pic_order_cnt[0];
}
总结
- 如果解码顺序和播放顺序一致(不存在B帧),最好用poc_order_type=2,这样最省码流,推导也方便。
- 如果解码顺序和播放顺序不一致(存在B帧),poc_order_cnt_type为0最灵活,也好推导,但是码流大些,如果poc_order_cnt_type为1,码流会小些,但是灵活度不够,解码帧必须呈周期性的关系才可以。
Reference
《THE H.264 ADVANCED VIDEO COMPRESSION STANDARD》
《T-REC-H.264-202108》