参考:零声学院
在⾳视频传输过程中,视频⽂件的传输是⼀个极⼤的问题;⼀段分辨率为19201080,每个像 素点为RGB占⽤3个字节,帧率是25的视频,对于传输带宽的要求是: 1920 * 1080 * 3 * 25 /1024/1024=148.315MB/s,换成bps则意味着视频每秒带宽为 1186.523Mbps,这样的速率对于⽹络存储是不可接受的。因此视频压缩和编码技术应运⽽⽣。
对于视频⽂件来说,视频由单张图⽚帧所组成,⽐如每秒25帧,但是图⽚帧的像素块之间存在 相似性,因此视频帧图像可以进⾏图像压缩;H264采⽤了1616的分块⼤⼩对,视频帧图像 进⾏相似⽐较和压缩编码。如下图所示:
H26使⽤帧内压缩和帧间压缩的⽅式提⾼编码压缩率;H264采⽤了独特的I帧、P帧和B帧策略 来实现,连续帧之间的压缩;
帧的分类 | 中⽂ | 意义 |
---|---|---|
I帧 | 帧内编码帧 intra picture | I 帧通常是每个 GOP(MPEG 所使⽤的⼀种视频压缩技术) 的第⼀个帧,经过适度地压缩,做为随机访问的参考点,可 以当成图象。I帧可以看成是⼀个图像经过压缩后的产物。 ⾃身可以通过视频解压算法解压成⼀张单独的完整的图⽚。 |
P帧 | 前向预测编码帧 predictive-frame | 通过充分将低于图像序列中前⾯已编码帧的时间冗余信息来 压缩传输数据量的编码图像,也叫预测帧。 需要参考其前⾯的⼀个I frame 或者P frame来⽣成⼀张完整 的图⽚。 |
B帧 | 双向预测帧 bi-directional interpolated prediction frame | 既考虑与源图像序列前⾯已编码帧,也顾及源图像序列后⾯已编码帧之间的时间冗余信息来压缩传输数据量的编码图像, 也叫双向预测帧。 则要参考其前⼀个I或者P帧及其后⾯的⼀个P帧来⽣成⼀张完 整的图⽚。 |
压缩率 B > P > I
H264除了实现了对视频的压缩处理之外,为了⽅便⽹络传输,提供了对应的视频编码和分⽚ 策略;类似于⽹络数据封装成IP帧,在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就不允许这样。
⽐如(解码的顺序): IDR1 P4 B2 B3 P7 B5 B6 I10 B8 B9 P13 B11 B12 P16 B14 B15 这⾥的B8可以跨过I10去参考P7
原始图像: IDR1 B2 B3 P4 B5 B6 P7 B8 B9 I10 IDR1 P4 B2 B3 P7 B5 B6 IDR8 P11 B9 B10 P14 B11 B12 这⾥的B9就只能参照IDR8和P11,不可以 参考IDR8前⾯的帧
其核⼼作⽤是,为了解码的重同步,当解码器解码到 IDR 图像时,⽴即将参考帧队列清 空,将已解码的数据全部输出或抛弃,重新查找参数集,开始⼀个新的序列。这样,如果前⼀ 个序列出现重⼤错误,在这⾥可以获得重新同步的机会。IDR图像之后的图像永远不会使⽤ IDR之前的图像的数据来解码。
下⾯是⼀个H264码流的举例(从码流的帧分析可以看出来B帧不能被当做参考帧)
I0 B40 B80 B120 P160
I0 B160
SPS:序列参数集,SPS中保存了⼀组编码视频序列(Coded video sequence)的全局参数。 PPS:图像参数集,对应的是⼀个序列中某⼀幅图像或者某⼏幅图像的参数。 I帧:帧内编码帧,可独⽴解码⽣成完整的图⽚。 P帧: 前向预测编码帧,需要参考其前⾯的⼀个I 或者B 来⽣成⼀张完整的图⽚。 B帧: 双向预测内插编码帧,则要参考其前⼀个I或者P帧及其后⾯的⼀个P帧来⽣成⼀张完整的 图⽚。
发I帧之前,⾄少要发⼀次SPS和PPS。比如当中途改变了分辨率或者一些编码参数等,就需要发送一次SPS和PPS
NALU结构单元的主体结构如下所示;⼀个原始的H.264 NALU单元通常由
[StartCode] [NALU Header] [NALU Payload]
三部分组成,其中 Start Code ⽤于标示这是⼀个NALU 单元的开 始,必须是"00 00 00 01" 或"00 00 01",除此之外基本相当于⼀个NAL header + RBSP;
NALU Hearder,,长度一个字节。
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| F | NRI | TYPE |
F: 1bit,如果是坏帧,则置1,正常帧为0
NRI: 2bit,用来指示该NALU的重要性等级,值越大越重要。例如如果是00则表示这个帧丢了也不影响解码,其他值则表示这个值丢了会影响解码,但是实际上我们不关心这个字段
NALU TYPE: 5bit,指明了NALU的类型,如下:
(对于FFmpeg解复⽤后,MP4⽂件读取出来的H264 packet是不带startcode,但TS⽂件读取出来的H264 packet带了startcode)
这也就是
264常见的帧头数据为:
00 00 00 01 67 (SPS) (67)0110 0111type为后5位0 0111 = 7 对应上表的sps
00 00 00 01 68 (PPS) (68)0110 1000 type为后5位0 1000 = 8 对应上表的pps
00 00 00 01 65 ( IDR 帧) (65)0110 0101 type为后5位0 0101 = 5 对应上表的IDR
00 00 00 01 61 (P帧) (61)0110 0001 type为后5位0 0001 = 1 对应上表的非IDR,P帧
对于一个原始的 H.264 NALU 单元常由[Start Code] [NALU Header] [NALU Payload]三部分组成, 其中 Start Code 用于标示这是一个 NALU 单元的开始, 必须是 “00 00 00 01” 或 “00 00 01”, NALU 头仅一个字节, 其后都是 NALU 单元内容.
I 帧 帧内编码帧,又称 intra picture I 帧通常是每个 GOP(MPEG 所使用的一种视频压缩技术)的第一个帧,经过适度地压缩,做为随机访问的参考点,可以当成图象。I帧可以看成是一个图像经过压缩后的产物
P 帧 前向预测编码帧,又称 predictive-frame 通过充分将低于图像序列中前面已编码帧的时间冗余信息来压缩传输数据量的编码图像,也叫预测帧
B 帧 双向预测帧,又称 bi-directional interpolated prediction frame 既考虑与源图像序列前面已编码帧,也顾及源图像序列后面已编码帧之间的时间冗余信息来压缩传输数据量的编码图像,也叫双向预测帧
I frame: 自身可以通过视频解压算法解压成一张单独的完整的图片;
IDR frame:IDR属于I帧,但是I帧不一定是IDR帧。只有IDR帧,才有SPS和PPS。解码器收到IDR帧时,将reference buffer清空;而收到I帧不会清空reference buffer。也就是说,对某个IDR帧之后的帧,解码器不会参考这个IDR帧之前的任何帧做解码,对某个I帧之后的帧,解码器可能会参考这个I帧之前的帧做解码。从随机存取的视频流中,播放器永远可以从一个IDR帧播放,因为在它之后没有任何帧引用之前的帧。但是,不能在一个没有IDR帧的视频中从任意点开始播放,因为后面的帧总是会引用前面的帧。
核心作用:H.264 引入 IDR 帧是为了解码的重同步,当解码器解码到 IDR 帧时,立即将参考帧队列清空,将已解码的数据全部输出或抛弃,重新查找参数集,开始一个新的序列。这样,如果前一个序列出现重大错误,在这里可以获得重新同步的机会,IDR 帧之后的帧永远不会使用 IDR 之前的图像的数据来解码。
P frame:需要参考其前面的一个 I frame 或者 B frame 来生成一张完整的图片;
B frame: 则要参考其前一个 I 或者 P帧 及其后面的一个 P 帧来生成一张完整的图片;
PTS(Presentation Time Stamp) PTS 主要用于度量解码后的视频帧什么时候被显示出来
DTS(Decode Time Stamp) DTS 主要是标识内存中的 bit 流什么时候开始送入解码器中进行解码
DTS 与 PTS 的不同:DTS 主要用于视频的解码,在解码阶段使用。PTS主要用于视频的同步和输出,在 display 的时候使用。再没有 B frame 的时候输出顺序是一样的。
GOP
GOP 是画面组,一个 GOP 是一组连续的画面。
GOP 一般有两个数字,如 M = 3,N = 12,M 制定 I 帧与 P 帧之间的距离,N 指定两个 I 帧之间的距离。
那么现在的 GOP 结构是:
I BBP BBP BBP BB I
增大图片组能有效的减少编码后的视频体积,但是也会降低视频质量,至于怎么取舍,得看需求了。
一个序列的第一帧叫做 IDR帧(Instantaneous Decoding Refresh,立即解码刷新)。
I 帧和 IDR 帧都是使用帧内预测,本质上是同一个东西,在解码和编码中为了方便,将视频序列中第一个 I 帧和其他 I 帧区分开,所以把第一个 I 帧称作 IDR,这样就方便控制编码和解码流程。
IDR 帧的作用是立刻刷新,使错误不致传播,从 IDR 帧开始,重新算一个新的序列开始编码。
核心作用
H.264 引入 IDR 帧是为了解码的重同步,当解码器解码到 IDR 帧时,立即将参考帧队列清空,将已解码的数据全部输出或抛弃,重新查找参数集,开始一个新的序列。这样,如果前一个序列出现重大错误,在这里可以获得重新同步的机会,IDR 帧之后的帧永远不会使用 IDR 之前的图像的数据来解码。
SPS和PPS
特殊的NALU类型:SPS和PPS
SPS和PPS存储了编解码需要一些图像参数,在H264出现之前的协议中,发现实际网络传输编码好的数据流的时候会出现丢包,而如果丢包数据为图像头等关键信息的时候甚至会导致后续解码失败,为了应对图像头关键信息被丢失的做法是在很多包(也有说法是每一个包)都会携带图像头关键信息(冗余做灾备的思想)。但是,在H264中,为了提高网络传输鲁棒性,重新设计出SPS和PPS。
SPS即Sequence Paramater Set,又称作序列参数集。SPS中保存了一组编码视频序列(Coded video sequence)的全局参数。所谓的编码视频序列即原始视频的一帧一帧的像素数据经过编码之后的结构组成的序列。而每一帧的编码后数据所依赖的参数保存于图像参数集中。一般情况SPS和PPS的NAL Unit通常位于整个码流的起始位置。但在某些特殊情况下,在码流中间也可能出现这两种结构,主要原因可能为:
解码器需要在码流中间开始解码;
编码器在编码的过程中改变了码流的参数(如图像分辨率等);
宽高,有的视频能解出帧率,海思解不出,有的可通过sps和pps能得出帧率
H264有两种封装 ⼀种是annexb模式,传统模式,有startcode,SPS和PPS是在ES中 ⼀种是mp4模式,⼀般mp4 mkv都是mp4模式,没有startcode,SPS和PPS以及其它信息 被封装在container中,每⼀个frame前⾯4个字节是这个frame的⻓度 很多解码器只⽀持annexb这种模式,因此需要将mp4做转换:在ffmpeg中⽤ h264_mp4toannexb_filter可以做转换
实现:
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]->cod ecpar);
av_bsf_init(bsf_ctx);
#include
#include
#include
#include
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 test.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 //MP4文件必须使用下面代码方法
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;
}