1.H264简介
H.264从1999年开始,到2003年形成草案,最后在2007年定稿有待核实。在ITU的标准⾥称 为H.264,在MPEG的标准⾥是MPEG-4的⼀个组成部分–MPEG-4 Part 10,⼜叫Advanced Video Codec,因此常常称为MPEG-4 AVC或直接叫AVC。
2.H264编码原理以及H264中的I帧、P帧和B帧
对于视频⽂件来说,视频由单张图⽚帧所组成,⽐如每秒25帧,但是图⽚帧的像素块之间存在 相似性,因此视频帧图像可以进⾏图像压缩;H264采⽤了16*16的分块⼤⼩对,视频帧图像进⾏相似⽐较和压缩编码。
H26使⽤帧内压缩和帧间压缩的⽅式提⾼编码压缩率;H264采⽤了独特的I帧、P帧和B帧策略来实现,连续帧之间的压缩;
-
I帧:帧内编码帧 ,I 帧通常是每个 GOP(MPEG 所使⽤的⼀种视频压缩技术) 的第⼀个帧,经过适度地压缩,做为随机访问的参考点,可以当成图象。I帧可以看成是⼀个图像经过压缩后的产物。 ⾃身可以通过视频解压算法解压成⼀张单独的完整的图⽚。
-
P帧:前向预测编码帧,通过充分将低于图像序列中前⾯已编码帧的时间冗余信息来压缩传输数据量的编码图像,也叫预测帧。
需要参考其前⾯的⼀个I frame 或者P frame来⽣成⼀张完整 的图⽚。
-
B帧:双向预测帧,既考虑与源图像序列前⾯已编码帧,也顾及源图像序列后⾯ 已编码帧之间的时间冗余信息来压缩传输数据量的编码图像, 也叫双向预测帧。要参考其前⼀个I或者P帧及其后⾯的⼀个P帧来⽣成⼀张完 整的图⽚。
一般是说,I帧的压缩率是7,p帧的压缩率是20,b帧的压缩率是50
3.H264编码结构解析
H264除了实现了对视频的压缩处理之外,为了⽅便⽹络传输,提供了对应的视频编码和分⽚策略;在H264中将其称为组(GOP, group of pictures)、⽚ (slice)、宏块(Macroblock)这些⼀起组成了H264的码流分层结构;H264将其组织成为序列(GOP)、图⽚(pictrue)、⽚(Slice)、宏块(Macroblock)、⼦块(subblock)五个层次。
GOP (图像组)主要⽤作形容⼀个IDR帧 到下⼀个IDR帧之间的间隔了多少个帧:
H264将视频分为连续的帧进⾏传输,在连续的帧之间使⽤I帧、P帧和B帧。同时对于帧内⽽ ⾔,将图像分块为⽚、宏块和字块进⾏分⽚传输;通过这个过程实现对视频⽂件的压缩包装。
IDR(Instantaneous Decoding Refresh,即时解码刷新) ⼀个序列的第⼀个图像叫做 IDR 图像(⽴即刷新图像),IDR 图像都是 I 帧图像,I和IDR帧都使⽤帧内预测。I帧不⽤参考任何帧,但是之后的P帧和B帧是有可能参考这个I帧之 前的帧的。IDR就不允许这样。
IDR帧核⼼作⽤是:是为了解码的重同步,当解码器解码到 IDR 图像时,⽴即将参考帧队列清空,将已解码的数据全部输出或抛弃,重新查找参数集,开始⼀个新的序列。这样,如果前⼀ 个序列出现重⼤错误,在这⾥可以获得重新同步的机会。IDR图像之后的图像永远不会使⽤ IDR之前的图像的数据来解码。
4.NALU(Network Abstract Layer Unit)
SPS:序列参数集,SPS中保存了⼀组编码视频序列(Coded video sequence)的全局参数。
PPS:图像参数集,对应的是⼀个序列中某⼀幅图像或者某⼏幅图像的参数。
I帧:帧内编码帧,可独⽴解码⽣成完整的图⽚。
P帧**😗* 前向预测编码帧,需要参考其前⾯的⼀个I 或者B 来⽣成⼀张完整的图⽚。
B帧**😗* 双向预测内插编码帧,则要参考其前⼀个I或者P帧及其后⾯的⼀个P帧来⽣成⼀张完整的 图⽚。
发I帧之前,⾄少要发⼀次SPS和PPS。
5.NALU结构
H.264原始码流(裸流)是由⼀个接⼀个NALU组成,它的功能分为两层,VCL(视频编码层)和 NAL(⽹络提取层):
- VCL:包括核⼼压缩引擎和块,宏块和⽚的语法级别定义,设计⽬标是尽可能地独⽴于⽹络进⾏⾼效的编码
- NAL:负责将VCL产⽣的⽐特字符串适配到各种各样的⽹络和多元环境中,覆盖了所有⽚级以上的语法级别
在VCL进⾏数据传输或存储之前,这些编码的VCL数据,被映射或封装进NAL单元。 ⼀个NALU = ⼀组对应于视频编码的NALU头部信息 + ⼀个原始字节序列负荷(RBSP,Raw Byte Sequence Payload)
NALU结构单元的主体结构如下所示;⼀个原始的H.264 NALU单元通常由**[StartCode] [NALU** **Header] [NALU Payload]**三部分组成,其中 Start Code ⽤于标示这是⼀个NALU 单元的开始,必须是"00 00 00 01" 或"00 00 01",除此之外基本相当于⼀个NAL header + RBSP;
注意:对于FFmpeg解复⽤后,MP4⽂件读取出来的packet是不带startcode,但TS⽂件读取出来 的packet带了startcode
每个NAL单元是⼀个⼀定语法元素的可变⻓字节字符串,包括包含⼀个字节的头信息(⽤来表 示数据类型),以及若⼲整数字节的负荷数据。
H.264标准指出,当数据流是储存在介质上时,在每个NALU 前添加起始码:0x000001 或 0x00000001,⽤来指示⼀个NALU 的起始和终⽌位置:
- 在这样的机制下,在码流中检测起始码,作为⼀个NALU得起始标识,当检测到下⼀个起始码时,当前NALU结束。
- 3字节的0x000001只有⼀种场合下使⽤,就是⼀个完整的帧被编为多个slice(⽚)的时 候,包含这些slice的NALU 使⽤3字节起始码。其余场合都是4字节0x00000001的。
nal_unit_type | NAL 单元和 RBSP 语法结构的内容 | |
---|---|---|
0 | 未指定 | |
1 | ⼀个⾮IDR图像的编码条带 slice_layer_without_partitioning_rbsp | |
2 | 编码条带数据分割块A slice_data_partition_a_layer_rbsp | |
3 | 编码条带数据分割块B slice_data_partition_b_layer_rbsp | |
4 | 编码条带数据分割块C slice_data_partition_c_layer_rbsp | |
5 | IDR图像的编码条带(⽚) slice_layer_without_partitioning_rbsp | |
6 | 辅助增强信息 (SEI) sei_rbsp | |
7 | 序列参数集 seq_parameter_set_rbsp | |
8 | 图像参数集 pic_parameter_set_rbsp | |
9 | 访问单元分隔符 access_unit_delimiter_rbsp | |
10 | 序列结尾 end_of_seq_rbsp | |
11 | 流结尾 end_of_stream_rbsp | |
12 | 填充数据 filler_data_rbsp | |
13 | 序列参数集扩展 seq_parameter_set_extension_rbsp | |
14…18 | 保留 | |
19 | 未分割的辅助编码图像的编码条带 slice_layer_without_partitioning_rbsp | |
20…23 | 保留 | |
24…31 | 未指定 |
6.H264 annexb模式
H264有两种封装:
- ⼀种是annexb模式,传统模式,有startcode,SPS和PPS是在ES中
- ⼀种是mp4模式,⼀般mp4 mkv都是mp4模式,没有startcode,SPS和PPS以及其它信息被封装在container中,每⼀个frame前⾯4个字节是这个frame的⻓度 很多解码器只⽀持annexb这种模式,因此需要将mp4做转换:在ffmpeg中⽤h264_mp4toannexb_filter可以做转换
7.从视频中抽取h264
AVFormatContext *av_format_context =NULL;
int video_index=-1;
AVPacket *pkt = NULL;
int ret=-1;
int file_end = 0; //文件是否读取结束
if(argc<3){
printf("添加必须的输入文件\n");
return -1;
}
FILE * out_fd=fopen(argv[2],"wb"); //声明文件句柄 并打开文件
printf("输入文件:%s,输出文件%s\n",argv[1],argv[2]);
//分配解复用器的内存 使用avformat_close_input 释放
av_format_context =avformat_alloc_context();
if(!av_format_context){
printf("error can not alloc format context\n");
return -1;
}
//根据URL 打开码流 并选择匹配的解复用器
ret=avformat_open_input(&av_format_context,argv[1],NULL,NULL);
if(ret<0){
printf("error avformat_open_input: %s\n", av_get_err(ret));
avformat_close_input(&av_format_context);
return -1;
}
//读取媒体文件部分数据包 获取码流信息
ret=avformat_find_stream_info(av_format_context,NULL);
if(ret<0){
printf("error avformat_find_stream_info: %s\n", av_get_err(ret));
avformat_close_input(&av_format_context);
return -1;
}
// 查找出哪个码流是video/audio/subtitles
video_index=av_find_best_stream(av_format_context,AVMEDIA_TYPE_VIDEO,-1,-1,NULL,0);
if(video_index == -1){
printf("error Do not find a video stream.\n");
avformat_close_input(&av_format_context);
return -1;
}
//分配数据包
pkt=av_packet_alloc();
av_init_packet(pkt);
- avformat_open_input:打开视频文件,解复用
- avformat_find_stream_info:读取媒体文件部分数据包 获取码流信息
- av_find_best_stream:查找出哪个码流是video/audio/subtitles
//获取相应的比特流过滤器
//FLV/MP4/MKV 等结构中,h264需要h264_mp4toannexb处理。添加sps/pps等信息
//FLV封装时,可以把多个NALU放在一个Video TAG 中,结构为4B NALU长度+NALU1+4B NALU长度+NALU2+...
//需要做的处理把4B长度换成00000001或者000001
const AVBitStreamFilter *bsfilter = av_bsf_get_by_name("h264_mp4toannexb");
AVBSFContext *bsf_cts = NULL;
//比特流过滤器上下文分配内存
av_bsf_alloc(bsfilter,&bsf_cts);
//添加解码器属性
avcodec_parameters_copy(bsf_cts->par_in,av_format_context->streams[video_index]->codecpar);
//初始化比特流过滤器
av_bsf_init(bsf_cts);
file_end=0;
while(0 == file_end){
if((ret=av_read_frame(av_format_context,pkt))<0){
//没有更多的数据包了
file_end = 1;
printf("read file end: ret:%d\n", ret);
}
if(ret==0 && pkt->stream_index==video_index){
#if 1
int input_size = pkt->size;
int out_pkt_count= 0;
if(av_bsf_send_packet(bsf_cts,pkt)!=0){ // bitstreamfilter内部去维护内存空间
av_packet_unref(pkt);
continue;
}
av_packet_unref(pkt);
while (av_bsf_receive_packet(bsf_cts,pkt)==0) {
out_pkt_count++;
// printf("fwrite size:%d\n", pkt->size);
size_t size=fwrite(pkt->data,1,pkt->size,out_fd);
if(size!=pkt->size){
printf("fwrite failed-> write:%u, pkt_size:%u\n", size, pkt->size);
}
av_packet_unref(pkt);
}
if(out_pkt_count>=2){
printf("cur pkt(size:%d) only get 1 out pkt, it get %d pkts\n",
input_size, out_pkt_count);
}
#else // TS流可以直接写入
size_t size= fwrite(pkt->data,1,pkt->size,out_fd);
if(size != pkt->size)
{
printf("fwrite failed-> write:%u, pkt_size:%u\n", size, pkt->size);
}
av_packet_unref(pkt);
#endif
}else{
if(ret==0)
av_packet_unref(pkt); //不是视频流直接释放内存
}
}
if(out_fd){
fclose(out_fd);
}
if(bsf_cts){
av_bsf_free(&bsf_cts);
}
if(pkt){
av_packet_free(&pkt);
}
if(av_format_context){
avformat_close_input(&av_format_context);
}
printf("------finish-----------\n");
- av_bsf_get_by_name(“h264_mp4toannexb”):得到比特流过滤器
- av_bsf_send_packet:将数据包发送给比特流过滤器进行处理
- av_bsf_receive_packet:拿到比特流过滤器处理的结果,主要是添加PPS、SPS等信息
- size_t size=fwrite(pkt->data,1,pkt->size,out_fd):将处理之后的数据写入文件