参考文章
音视频 H264 编解码协议-知乎
视频H264编码详解(上)-知乎
H.264媒体流AnnexB和AVCC格式分析-CSDN
H264之NALU解析-知乎
H264帧,SPS,PPS概念-知乎
H.264流媒体协议格式中的Annex B格式和AVCC格式深度解析-CSDN
H264简介
H.264从1999年开始,到2003年形成草案,最后在2007年定稿有待核实。在ITU的标准⾥称为H.264,在MPEG的标准⾥是MPEG-4的⼀个组成部分--MPEG-4 Part 10,⼜叫Advanced Video Codec,因此常常称为MPEG-4 AVC或直接叫AVC。
带宽
Mbps(Million bits per second)即"传输速率",也叫"带宽"。Mbps和MB/s是怎么换算的呢?
8Mbps=1MB/s;8Mbps换算成下载速度就是1MB/s,不过由于种种限制,实际情况中8M的宽带往往达不到1MB/s的下载速度,能达到800KB/s以上算是正常情况。
NALU(Network Abstract Layer Unit)网络抽象层单元
H.264原始码流(裸流)由⼀个接⼀个NALU组成.
SPS :序列参数集,包含解码配置,如profile level分辨率和帧率等。编码后的第一帧。
PPS :图像参数集,包含有关熵编码模式、分片组、运动预测和去块滤波器等信息。编码后的第二帧。
I帧 :帧内编码,可独立解码生成完整的图片。
P帧 :前向预测编码帧,需要参考其前⾯的⼀个I 或者B 来⽣成⼀张完整的图⽚。
B帧 : 双向预测内插编码帧,则要参考其前⼀个I或者P帧及其后⾯的⼀个P帧来⽣成⼀张完整的图⽚。
NALU = StartCode + ⼀组对应于视频编码的NALU头部信息 + ⼀个原始字节序列负荷(RBSP, Raw Byte Sequence Payload)
其中 Start Code⽤于标示这是⼀个NALU单元的开始,必须是"00 00 00 01" 或"00 00 01",除此之外基本相当于⼀个NAL header + RBSP:
值得注意的是,一个NALU即使是VCL NALU 也并不一定表示一个视频帧。因为一个帧的数据可能比较多,可以分片为多个NALU来储存。一个或者多个NALU组成一个访问单元AU,一个AU包含一个完整的帧。
H264编码分层
- NAL层:(Network Abstraction Layer,视频数据网络抽象层)
它的作用是H264只要在网络上传输,在传输的过程每个包以太网是1500字节. 而H264的帧往往会大于1500字节的.所以就要进行拆包. 将一个帧拆成多个包进行传输.所有的拆包或者组包都是通过NAL层去处理的. - VCL层:(Video Coding Layer,视频数据编码层)
它的作用就是对视频原始数据进行压缩.
H264 码流格式
H264有两种封装:
-
⼀种是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) | ...
FLV/MP4/MKV中H264采用的是AVCC模式,也叫AVC1格式。
没有startcode,每⼀个frame前⾯4个字节是frame⻓度。SPS和PPS等参数信息被封装在extradata中。
比如ffmpeg中解析mp4文件后sps pps存在
streams[index]->codecpar->extradata
中。也就是说这种码流通常只包含VCL类型NALU。此时将前4个字节替换为0x00000001才能得到AnnexB模式的NALU数据。
很多解码器只⽀持AnnexB这种模式,因此需要将MP4做转换。在ffmpeg中⽤ h264_mp4toannexb_filter接口可以做转换。
FFmpeg解析mp4中H264码流
MP4文件中编码信息是存储在文件开始或者文件末尾的,详细结构这里不详述了。就知道不是和图像数据放在一起的就可以了。
FFmpeg使用av_read_frame(AVFormatContext *s, AVPacket *pkt)
函数读mp4文件,读到packet里面仅仅是VCL编码数据NAL,并且这个编码数据是AVCC格式组织的码流,直接保存成.264文件没法播放。
- 从
avFmtCtx->streams[_videoStreamIndex]->codecpar->extradata
中解析SPS和PPS数据,解析出SPS PPS数据加上4字节的0001的起始码拼装成AnnexB格式的NALU,先写入文件. - 通过
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的h264_mp4toannexb
进行格式转换
参考链接:
FFmpeg简单使用:过滤器 ---- h264_mp4toannexb-CSDN
ffmpeg 新接口 :从flv文件中提取h264码流-3ms