H264码流分两种组织方式,一种是AnnexB格式,一种是AVCC格式。
作者:码农小明
来源:https://blog.csdn.net/shaosunrise/article/details/121548065
首先要了解的是H.264编码规范只是规定了如何编码,并没有规定以何种方式来排列编码后的数据
就如同AES算法只是规定如何加密一组数据,并没有强制规定如果分组。H.264规范没有规定如何组织数据,但是在附录B中提供了一种可选方案,即Annex B格式。

H.264 NALU 概念

H.264视频编码后的数据叫 NALU(Network Abstraction Layer Units)
NALU有多种类型,分为两大类:VCL(Video Coding Layer) 和 非VCL。VCL是图像编码数据,非VCL为编码参数信息。
NALU结构头部指明类型,类型字段如下。
SPS:序列参数集,包含解码配置,比如profile level 分辨率和帧率等。
PPS:图像参数集,包含有关熵编码模式、分片组、运动预测和去块滤波器等信息。
IDR: 立即解码刷新单元,这种NALU包含一个完整的图像序列,不依赖其他NALU就可以独立解码和显示,即一种特殊的I帧。
值得注意的是,一个NALU即使是VCL NALU 也并不一定表示一个视频帧。因为一个帧的数据可能比较多,可以分片为多个NALU来储存。一个或者多个NALU组成一个访问单元AU,一个AU包含一个完整的帧。

H.264 码流格式

H264码流分两种组织方式,一种是AnnexB格式,一种是AVCC格式。

AnnexB格式

[start code]NALU | [start code] NALU |...

这种格式比较常见,也就是我们熟悉的每个帧前面都有0x00 00 00 01或者0x00 00 01作为起始码。
h264文件就是采用的这种格式,每个帧前面都要有个起始码。
SPS PPS等也作为一类NALU存储在这个码流中,一般在码流最前面。也就是说这种格式包含VCL 和 非VCL 类型的NALU。

AVCC格式

([extradata]) | ([length] NALU) | ([length] NALU) | ...

这种模式也叫AVC1格式,没有起始码,每个帧最前面几个字节(通常4字节)是帧长度。
这里的NALU一般没有SPS PPS等参数信息,参数信息属于额外数据extradata存在其他地方。
比如ffmpeg中解析mp4文件后sps pps存在streams[index]->codecpar->extradata;中。
也就是说这种码流通常只包含VCL类型NALU。
这些extradata通常有如下格式(可以根据这个规则ffmpeg解析mp4文件的SPS和PPS)
1
字节:version (通常
0x01
)

2
字节:
avc 
profile(值同第1个sps的第2字节)
第3字节:avc
compatibility(值同第1个sps的第3字节)
第4字节:avc
level(值同第1个sps的第3字节)
第5字节前6位:保留全1

第5字节后2位:NALU Length 字段大小减1,通常这个值为3,即NAL码流中使用3+1
=
4
字节表示NALU的长度

6
字节前
3
位:保留,全
1
6
字节后
5
位:SPS NALU的个数,通常为
1
7
字节开始后接
1
个或者多个SPS数据

 SPS结构 [
16
位 SPS长度][SPS NALU data]


SPS数据后

1
字节:PPS的个数,通常为
1
2
字节开始接
1
个或多个PPS数据

 PPS结构 [
16
位 PPS长度][SPS NALU data]

FFmpeg解析mp4中H.264 码流

MP4文件中编码信息是存储在文件开始或者文件末尾的,详细结构这里不详述了。就知道不是和图像数据放在一起的就可以了。FFmpeg使用av_read_frame(AVFormatContext *s, AVPacket *pkt)函数读mp4文件,读到packet里面仅仅是VCL编码数据NAL,并且这个编码数据是AVCC格式组织的码流,直接保存成.264文件没法播放。

先说一下思路:

1 .从avFmtCtx->streams[_videoStreamIndex]->codecpar->extradata中解析SPS和PPS数据,数据格式上一节已经描述了。解析出SPS PPS数据加上4字节的0001的起始码拼装成nnexB格式的NALU,先写入文件。
  1. 通过av_read_frame(AVFormatContext *s, AVPacket *pkt)读取到数据存放在pkt->data中,长度为pkt->size。
注意:这1个pkt->data中的数据可能是多个NALU的数据!!!这些数据按([length] NALU) | ([length] NALU) | ...规则排列。先取前4字节作为长度,读取指定长度的数据加上起始码拼NALU。然后同样的方式读取后面的数据,直到总长度等于pkt->size。

FFmpeg 解析mp4中H264码流 代码示例

这里就只贴关键部分代码。省略前面打开文件和查询流信息等操作。
//...
    AVPacket spsPacket, ppsPacket, tmpPacket;

uint8_t
 startCode[
4
] = {
0x00
0x00
0x00
0x01
};

bool
 sendSpsPps = 
false
;


while
 (av_read_frame(_avFmtCtx, _avPacket) == 
0
) { 
// 能读到数据返回0,循环读取
// 根据pkt->stream_index判断是不是视频流
if
 (_avPacket->stream_index == _videoStreamIndex) {

// 仅1次处理sps pps,也可以拿在while外面
if
 (!sendSpsPps) { 

int
 spsLength = 
0
;

int
 ppsLength = 
0
;

// extradata 数据指针,方便操作取其指针
uint8_t
 *ex = _avFmtCtx->streams[_videoStreamIndex]->codecpar->extradata;


// extradata;第6字节后5位表示SPS个数,通常为1,这里就省略判断处理,严谨期间还是要判断
// 直接 取第7 8 俩字节作为SPS长度
                spsLength = (ex[
6
] << 
8
) | ex[
7
];


// x[8+spsLength]表示PPS个数,通常为1,这里就省略判断处理
// 取接下来两位作为PPS长度
                ppsLength = (ex[
8
 + spsLength + 
1
] << 
8
) | ex[
8
 + spsLength + 
2
];


// 为spsPacket ppsPacket的data分配内存,类似malloc
// 如果只是为了保存文件,可以不使用pkt结构,直接malloc就行
// 分配的空间为sps或pps长度加上4字节的起始码
                av_new_packet(&spsPacket, spsLength + 
4
);

                av_new_packet(&ppsPacket, ppsLength + 
4
);


// 给SPS拼前4字节起始码
memcpy
(spsPacket.data, startCode, 
4
);

// 把SPS数据拼在起始码后面
memcpy
(spsPacket.data + 
4
, ex + 
8
, spsLength);


// TODO: 这里可以把spsPacket.data数据写入文件 

// 给PPS拼前4字节起始码
memcpy
(ppsPacket.data, startCode, 
4
);

// 把PPS数据拼在起始码后面
memcpy
(ppsPacket.data + 
4
, ex + 
8
 + spsLength + 
2
 + 
1
, ppsLength);


// TODO: 这里可以把ppsPacket.data数据写入文件 

                sendSpsPps = 
true
;

            }


// 下面处理读到pkt中的数据
int
 nalLength = 
0
;

uint8_t
 *data = _avPacket->data;

// _avPacket->data中可能有多个NALU,循环处理
while
 (data < _avPacket->data + _avPacket->size) {

// 取前4字节作为nal的长度
                nalLength = (data[
0
] << 
24
) | (data[
1
] << 
16
) | (data[
2
] << 
8
) | data[
3
];

if
 (nalLength > 
0
) {

memcpy
(data, startCode, 
4
);  
// 拼起始码
                    tmpPacket = *_avPacket;      
// 仅为了复制packet的其他信息,保存文件可忽略
                    tmpPacket.data = data;   
// 把tmpPkt指针偏移到实际数据位置
                    tmpPacket.size = nalLength + 
4
// 长度为nal长度+起始码4

//TODO: 处理这个NALU的数据,可以直接把tmpPacket.data写入文件
                }

                data = data + 
4
 + nalLength; 
// 处理data中下一个NALU数据
            }

        }


        av_packet_unref(_avPacket);

    }

上述代码FFmpeg 4.4使用正常。
此外,FFmpeg也提供了h264_mp4toannexb_filter相关的过滤器进行相应转换的操作,这里不示例怎么用了。
最后欢迎大家加入 音视频开发进阶 知识星球,这里有知识干货、编程答疑、开发教程,还有很多精彩分享。
更多内容可以在星球菜单中找到,随着时间推移,干货也会越来越多!!!
给出 10元 优惠券,涨价在即,目前还是白菜价,基本上提几个问题就回本,投资自己就是最好的投资!!!
加我微信 ezglumes ,拉你进技术交流群
推荐阅读:
觉得不错,点个在看呗~
继续阅读
阅读原文