H264简介-也叫做 AVC
H.264,在MPEG的标准⾥是MPEG-4的⼀个组成部分–MPEG-4 Part 10,⼜叫Advanced Video Codec,因此常常称为MPEG-4 AVC或直接叫AVC。
原始数据YUV,RGB为什么要压缩-知道就行
在⾳视频传输过程中,视频⽂件的传输是⼀个极⼤的问题;⼀段分辨率为1920*1080,每个像素点为RGB占⽤3个字节,帧率是25的视频,对于传输带宽的要求是:
1920x1080x3x25/1024/1024=148.315MB/s, 这个是每秒的 bytes 数
换成bps则意味着视频每秒带宽为 148.315MB/s x 8 = 1186.523Mbps
1186.523Mbps,这样的速率对于⽹络存储是不可接受的。因此视频压缩和编码技术应运⽽⽣。
H264编码原理
帧内压缩
对于视频⽂件来说,视频由单张图⽚帧所组成,⽐如每秒25帧,但是图⽚帧的像素块之间存在
相似性,因此视频帧图像可以进⾏图像压缩;H264采⽤了16*16的分块⼤⼩对,视频帧图像
进⾏相似⽐较和压缩编码。如下图所示:
帧间压缩
H264采⽤了独特的I帧、P帧和B帧策略 来实现,连续帧之间的压缩;
H264将视频分为连续的帧进⾏传输,在连续的帧之间使⽤I帧、P帧和B帧。
同时对于帧内⽽⾔,将图像分块为⽚、宏块和字块进⾏分⽚传输;通过这个过程实现对视频⽂件的压缩包装。
IDR(Instantaneous Decoding Refresh,即时解码刷新)
⼀个序列的第⼀个图像叫做 IDR 图像(⽴即刷新图像),IDR 图像都是 I 帧图像。
I和IDR帧都使⽤帧内预测。I帧不⽤参考任何帧,但是之后的P帧和B帧是有可能参考这个I帧之
前的帧的。
但是在解码的时候,I 和 IDR 有区别。举例如下:在第一个解码的时候,解码到B8的时候,可以参考I10前面的P7.
在第二个解码的时候,B9 就只能参考 IDR8和 P11,不能参考IDR8之前的帧。
下⾯是⼀个H264码流的举例(从码流的帧分析可以看出来B帧不能被当做参考帧)
在假设条件下分析上图,假设GOP1 的是每秒25帧,也就是一帧画面需要1000/25 = 40ms.
I帧解码的时候时间点在0,那么读取下一帧B要依赖于 P,接着找下一帧是不是P,还不是,在找,直到找到P,也就是说:在 找P的时候已经过去了160ms了,大致如下:
I0 B40 B80 B120 P160
I0 B160
这意味着什么呢?在做实时性要求高的场景时,最好不要使用B帧
H264编码结构- NALU
NAL简介
NAL层即网络抽象层(Network Abstraction Layer),是为了方便在网络上传输的一种抽象层。一般网络上传输的数据包有大小限制,而AVC(H264)的一帧大小远远大于网络传输的字节大小限制。因此要对AVC的数据流进行拆包,将一帧数据拆分为多个包传输。和NAL层相对是VAL层,即视频编码层(Video Coding Layer)
NALU就是经过分组后的一个一个数据包。
发I帧之前,⾄少要发⼀次SPS和PPS。当分辨率变化的时候,要重新发送一次SPS和PPS(类似在视频网站上,我们将分辨率从720p变成1080p的时候)
这个很重要,如果遇到我们显示不了图片或者视频的时候,应该第一个检查的就是 SPS 和PPS是否有正确的发送。
SPS:序列参数集,SPS中保存了⼀组编码视频序列(Coded video sequence)的全局参数。
PPS:图像参数集,对应的是⼀个序列中某⼀幅图像或者某⼏幅图像的参数。
I帧:帧内编码帧,可独⽴解码⽣成完整的图⽚。
P帧: 前向预测编码帧,需要参考其前⾯的⼀个I 或者B 来⽣成⼀张完整的图⽚。
B帧: 双向预测内插编码帧,则要参考其前⼀个I或者P帧及其后⾯的⼀个P帧来⽣成⼀张完整的图⽚。
每个NALU = StartCode + 由一个1字节的NALU头部 + 一个包含控制信息或编码视频数据的字节流组成。
IDR图像的编码条带(⽚) slice_layer_without_partitioning_rbsp( )
6 Supplemental enhancement information (SEI) non-VCL
辅助增强信息 (SEI)sei_rbsp( )
7 Sequence parameter set non-VCL
序列参数集 seq_parameter_set_rbsp( )
8 Picture parameter set non-VCL
图像参数集 pic_parameter_set_rbsp( )
0 Unspecified non-VCL
未指定
1 Coded slice of a non-IDR picture VCL
⼀个⾮IDR图像的编码条带slice_layer_without_partitioning_rbsp()
2 Coded slice data partition A VCL
编码条带数据分割块A slice_data_partition_a_layer_rbsp()
3 Coded slice data partition B VCL
编码条带数据分割块B slice_data_partition_b_layer_rbsp( )
4 Coded slice data partition C VCL
编码条带数据分割块C slice_data_partition_c_layer_rbsp( )
5 Coded slice of an IDR picture VCL
IDR图像的编码条带(⽚) slice_layer_without_partitioning_rbsp( )
6 Supplemental enhancement information (SEI) non-VCL
辅助增强信息 (SEI)sei_rbsp( )
7 Sequence parameter set non-VCL
序列参数集 seq_parameter_set_rbsp( )
8 Picture parameter set non-VCL
图像参数集 pic_parameter_set_rbsp( )
9 Access unit delimiter non-VCL
访问单元分隔符 access_unit_delimiter_rbsp( )
10 End of sequence non-VCL
序列结尾 end_of_seq_rbsp( )
11 End of stream non-VCL
流结尾end_of_stream_rbsp( )
12 Filler data non-VCL
填充数据filler_data_rbsp( )
13 Sequence parameter set extension non-VCL
序列参数集扩展seq_parameter_set_extension_rbsp( )
14 Prefix NAL unit non-VCL
NAL 单元前缀
15 Subset sequence parameter set non-VCL
子集序列参数集
16 Depth parameter set non-VCL
深度参数集
17..18 Reserved non-VCL
保留
19 Coded slice of an auxiliary coded picture without partitioning non-VCL
未分割的辅助编码图像的编码条带slice_layer_without_partitioning_rbsp( )
20 Coded slice extension non-VCL
编码切片扩展
21 Coded slice extension for depth view components non-VCL
深度视图组件的编码切片扩展
22..23 Reserved non-VCL
保留
24..31 Unspecified non-VCL
未定义
H264编码的组织
一个完整的数据包包含多个NALU,不同的NALU该如何组织规范中并没有规定,因此实际实现比较广泛的有两种格式AnnexB和AVCC。
AnnexB
实际上我们前面学习就是以AnnexB 这种模式学习的。AnnexB是一种比较常见的H264码流格式,FFmpeg解封装的H264码流就是这种格式。
AnnexB的格式比较简单:每个NALU单元之前通过分隔符0x00 00 00 01或者0x00 00 01区分不同的NALU单元。
对于非VCL和VCL的单元是不区分的都是存储在NALU的Body中。
由于NALU的Body中的数据是压缩数据可能出现start code,因此规定RBSP中的0x000000、0x000001、0x000002和0x000003是非法的。如果数据中包含类似的二进制序列需要插入一个“模拟预防”字节0x03来实现,使得0x000001变成0x00000301,解码时去除即可。
AVCC
另一种常见的存储H.264流的方法是AVCC格式。
AnnexB和 AVCC的转换
这里开始的部分还是通过 av_read_frame方法 读取数据 到 AVPacket 的时候:
av_read_frame(avformatcontext, avpacket);
然后使用 ffmpeg提供的 av_bsf_send_packet方法,将 avpacket 数据塞入,注意的是:当我们将avpacket 数据塞入的时候,av_bsf_send_packet会自己管理内存,不管av_bsf_send_packet方法成功或者不成功,我们都要调用 av_packet_unref(pkt);将自己的refcount -1 。
int av_bsf_send_packet(AVBSFContext *ctx, AVPacket *pkt);
然后通过 ffmpeg 提供的 av_bsf_receive_packet方法, 将avpacket数据改动,当我们拿出的时候,也要记得调用 av_packet_unref(pkt);将自己的avpacket 的refcount -1。
int av_bsf_receive_packet(AVBSFContext *ctx, AVPacket *pkt);
这时候 avpacket 中的数据,就从 AVCC转换成 AnnexB的了。就可以直接写入到 自己像存储的.h264文件。
那么 AVBSFContext 是怎么来的呢?
参考如下的几步:
// 1 找到 h264_mp4toannexb 的过滤器
const AVBitStreamFilter *bsfilter = av_bsf_get_by_name("h264_mp4toannexb");
AVBSFContext *bsf_ctx = NULL;
// 2 初始化过滤器上下⽂
av_bsf_alloc(bsfilter, &bsf_ctx); //AVBSFContext;
该av_bsf_alloc方法的说明如下:
* Allocate a context for a given bitstream filter. The caller must fill in the
* context parameters as described in the documentation and then call
* av_bsf_init() before sending any data to the filter.
// 3 添加解码器属性
avcodec_parameters_copy(bsf_ctx->par_in, ifmt_ctx->streams[videoindex]->codecpar);
av_bsf_init(bsf_ctx);
注意的是:如果文件是TS流,可以不使用该方法,如果使用,也不会有问题。
但是如果文件是mp4文件,或者flv文件,则要使用该方法,如果不使用,会有问题
整体code如下:
#include <stdio.h>
#include <libavutil/log.h>
#include <libavformat/avio.h>
#include <libavformat/avformat.h>
#include<libavcodec/bsf.h>
static char err_buf[128] = {0};
static char* av_get_err(int errnum)
{
av_strerror(errnum, err_buf, 128);
return err_buf;
}
/*
AvCodecContext->extradata[]中为nalu长度
* codec_extradata:
* 1, 64, 0, 1f, ff, e1, [0, 18], 67, 64, 0, 1f, ac, c8, 60, 78, 1b, 7e,
* 78, 40, 0, 0, fa, 40, 0, 3a, 98, 3, c6, c, 66, 80,
* 1, [0, 5],68, e9, 78, bc, b0, 0,
*/
//ffmpeg -i 2018.mp4 -codec copy -bsf:h264_mp4toannexb -f h264 tmp.h264
//ffmpeg 从mp4上提取H264的nalu h
int main(int argc, char **argv)
{
AVFormatContext *ifmt_ctx = NULL;
int videoindex = -1;
AVPacket *pkt = NULL;
int ret = -1;
int file_end = 0; // 文件是否读取结束
if(argc < 3)
{
printf("usage inputfile outfile\n");
return -1;
}
FILE *outfp=fopen(argv[2],"wb");
printf("in:%s out:%s\n", argv[1], argv[2]);
// 分配解复用器的内存,使用avformat_close_input释放
ifmt_ctx = avformat_alloc_context();
if (!ifmt_ctx)
{
printf("[error] Could not allocate context.\n");
return -1;
}
// 根据url打开码流,并选择匹配的解复用器
ret = avformat_open_input(&ifmt_ctx,argv[1], NULL, NULL);
if(ret != 0)
{
printf("[error]avformat_open_input: %s\n", av_get_err(ret));
return -1;
}
// 读取媒体文件的部分数据包以获取码流信息
ret = avformat_find_stream_info(ifmt_ctx, NULL);
if(ret < 0)
{
printf("[error]avformat_find_stream_info: %s\n", av_get_err(ret));
avformat_close_input(&ifmt_ctx);
return -1;
}
// 查找出哪个码流是video/audio/subtitles
videoindex = -1;
// 推荐的方式
videoindex = av_find_best_stream(ifmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
if(videoindex == -1)
{
printf("Didn't find a video stream.\n");
avformat_close_input(&ifmt_ctx);
return -1;
}
// 分配数据包
pkt = av_packet_alloc();
av_init_packet(pkt);
// 1 获取相应的比特流过滤器
//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_ctx = NULL;
// 2 初始化过滤器上下文
av_bsf_alloc(bsfilter, &bsf_ctx); //AVBSFContext;
// 3 添加解码器属性
avcodec_parameters_copy(bsf_ctx->par_in, ifmt_ctx->streams[videoindex]->codecpar);
av_bsf_init(bsf_ctx);
file_end = 0;
while (0 == file_end)
{
if((ret = av_read_frame(ifmt_ctx, pkt)) < 0)
{
// 没有更多包可读
file_end = 1;
printf("read file end: ret:%d\n", ret);
}
if(ret == 0 && pkt->stream_index == videoindex)
{
#if 0
int input_size = pkt->size;
int out_pkt_count = 0;
if (av_bsf_send_packet(bsf_ctx, pkt) != 0) // bitstreamfilter内部去维护内存空间
{
av_packet_unref(pkt); // 你不用了就把资源释放掉
continue; // 继续送
}
av_packet_unref(pkt); // 释放资源
while(av_bsf_receive_packet(bsf_ctx, pkt) == 0)
{
out_pkt_count++;
// printf("fwrite size:%d\n", pkt->size);
size_t size = fwrite(pkt->data, 1, pkt->size, outfp);
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, outfp);
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(outfp)
fclose(outfp);
if(bsf_ctx)
av_bsf_free(&bsf_ctx);
if(pkt)
av_packet_free(&pkt);
if(ifmt_ctx)
avformat_close_input(&ifmt_ctx);
printf("finish\n");
return 0;
}