1.h264简介
H.264,也被称为H.264/AVC(Advanced Video Coding),是ITU-T(国际电信联盟电信标准化部门)和ISO/IEC(国际标准化组织/国际电工委员会)联合制定的视频编码标准。这一标准自提出以来,凭借其高效的数据压缩比和广泛的适用性,成为了视频压缩领域的主流标准之一。以下是对H.264的详细解析:
技术背景与特点
- 高效压缩比:H.264能够在同等图像质量下,提供比MPEG-2等旧标准更高的压缩比,数据量仅为MPEG-2的1/2至1/3,大大节省了存储空间和传输带宽。
- 高质量图像:H.264不仅压缩效率高,还能提供连续、流畅的高质量图像,满足用户对视频质量的高要求。
- 广泛应用范围:H.264适用于多种应用场景,包括实时通信、视频存储、流媒体传输等,具有较强的通用性和灵活性。
- 鲁棒性与网络适应性:H.264针对分组交换网络(如Internet)中的分组丢失和无线网络中的比特误码,提供了相应的工具,使得视频数据在这些网络中传输时具有更强的抗误码性能。同时,H.264还增加了网络抽象层(NAL),使得编码器的输出码流能够适配到各种类型的网络中。
关键技术
- 帧内与帧间预测:H.264采用了帧内与帧间预测的混合编码方式,充分利用了视频图像中的空间和时间冗余性,提高了压缩效率。
- 多模式预测:在帧内预测中,H.264提供了多种预测模式(如4×4块大小的9种预测模式和16×16块大小的4种预测模式),以适应不同图像内容的特点。
- 快速运动估值算法:H.264采用了新的快速运动估值算法(如UMHexagonS),提高了运动估值的准确性和效率。
- 多帧参考与亚像素运动估计:H.264支持多帧参考和亚像素精度的运动估计,进一步提高了压缩性能和图像质量。
- 去块效应滤波器:为了消除视频编码中常出现的块效应,H.264使用了去块效应滤波器,使得重建图像更加平滑自然。
编码结构与单元
- 基本单元:在H.264中,一个视频图像编码后的数据被称为一帧。一帧由一个或多个片(slice)组成,而一个片又由一个或多个宏块(MB)组成。宏块是H.264编码的基本单位,通常包含16×16的亮度像素和附加的8×8 Cb和8×8 Cr彩色像素块。
- 帧类型:H.264定义了三种类型的帧:I帧(关键帧)、P帧(预测帧)和B帧(双向预测帧)。I帧是一个完整的图像帧,P帧是参考之前的I帧或P帧生成的,而B帧则是参考前后的图像帧生成的。
应用场景与优势
- 视频传输:在较低带宽下提供高质量的图像传输是H.264的一大应用亮点,特别适用于国内运营商接入网带宽有限的状况。
- 视频监控:H.264的高压缩比和低带宽占用特性使其成为视频监控领域的理想选择,能够在保证图像质量的同时降低存储和传输成本。
- 流媒体服务:H.264广泛应用于各种流媒体服务平台,为用户提供流畅、高清的视频观看体验。
2.h264中的i帧b帧和p帧
在h264编码中,为了提高帧间的压缩效率,采用了ibp帧的措施,用来实现连续的帧之间的压缩。
格式 | 定义与特性: | 作用与影响: |
---|---|---|
I帧 | I帧是视频编码中的关键帧,采用帧内压缩技术,不依赖于其他帧进行编码和解码。 I帧是一个完整的图像帧,包含了完整的图像信息,可以独立解码出完整的画面。 I帧的压缩率相对较低,因为不包含时间上的预测或运动补偿信息,所以其数据量相对较大。 | I帧是视频解码的起点,可以作为随机访问点,支持视频流的快速定位和跳转。 I帧的质量直接影响到后续P帧和B帧的解码质量,因为它是P帧和B帧的参考帧。 在网络传输中,I帧的丢失会导致后续帧无法正确解码,影响视频播放的连续性和质量。 |
p帧 | P帧是前向预测帧,依赖于前面的I帧或P帧进行时间上的预测编码。 P帧不是完整的图像帧,而是包含了与前一帧的差异信息(即预测误差和运动矢量)。 P帧的压缩率较高,因为它去除了图像序列中的时间冗余信息。 | P帧通过参考前一帧来减少数据量,提高压缩效率。 P帧的解码需要依赖前面的已解码帧,因此解码顺序是线性的。 在网络传输中,P帧的丢失可能导致解码错误向后传播,影响后续帧的解码质量。 |
B帧 | B帧是双向预测帧,同时依赖于前面的I帧或P帧和后面的P帧进行编码。 B帧也不是完整的图像帧,而是包含了与前后帧的差异信息(即预测误差和运动矢量)。 B帧的压缩率最高,因为它利用了更多的上下文信息来减少数据量。 | B帧通过双向预测进一步提高压缩效率,减少传输数据量。 B帧的解码需要依赖前后的已解码帧,因此解码顺序与显示顺序不同,增加了解码的复杂性。 在网络传输中,B帧的解码对CPU资源要求较高,过多的B帧可能导致解码延迟增加。但在网络状况良好的情况下,B帧的使用可以有效提高压缩率。 |
3.NALU 结构
上面已经介绍过I,p,b帧了。下面来介绍一下SPS和PPS帧。
SPS:
- Profile与Level:标识了视频流的档次和级别,不同的Profile和Level对应不同的编码功能和视频质量。例如,Baseline Profile支持基本的编解码功能,而High Profile则支持更高级的编码特性。Level则定义了视频的最大分辨率、最大帧率等参数。
- 图像尺寸:包括视频的宽度和高度,这是解码器正确显示视频画面的基础。
- 帧率:虽然SPS中不直接包含帧率的具体值,但可以通过其他参数计算出视频流可能支持的帧率范围。
- 参考帧数:指定了用于帧间预测的参考帧数量,这对于运动估计和补偿等编解码过程至关重要。
- 其他参数:如色度格式、比特深度等,这些参数共同定义了视频流的基本编码特性。
PPS
- 熵编码模式:指定了用于压缩视频数据的熵编码方法,如CAVLC(基于上下文的自适应变长编码)或CABAC(基于上下文的自适应二进制算术编码)。
- 初始量化参数:用于设置视频编码的初始量化步长,量化是视频编码中损失压缩的关键步骤之一。
- 去方块滤波系数:用于控制解码后图像的去方块滤波强度,以减少压缩引入的块效应。
- 其他参数:如片组数目、帧内预测模式等,这些参数共同定义了单个图像或图像序列中几个连续图像的编码特性。
SPS和PPS与NALU的关系
在H.264码流中,SPS和PPS通常位于整个码流的起始位置,封装文件(如MP4)中一般只保存一次,位于文件头部。在整个解码过程中,SPS和PPS会被复用,不会发生变化。然而,对于实时流或需要动态调整编码参数的场景,可能需要在每个关键帧(如I帧)前重新发送SPS和PPS。
在NALU中,SPS和PPS通过NALU头部的nal_unit_type
字段进行标识。具体来说,当nal_unit_type
为7时,表示该NALU包含SPS;当nal_unit_type
为8时,表示该NALU包含PPS。解码器在解析码流时,会根据这些标识来提取和解析SPS和PPS参数集。
综上所述,SPS和PPS在H.264视频编码中扮演着至关重要的角色。它们为解码器提供了正确解析和显示视频序列所需的全局和局部参数信息。
下图的NALU的结构示意图
- 起始码(Start Code)
- 用于标示这是一个NALU单元的开始。在H.264的annexb封装格式中,起始码通常为
0x000001
或0x00000001
。起始码的存在使得NALU单元在码流中可以被清晰地分隔开。 - 注意:在MP4等封装格式中,起始码可能被省略,SPS和PPS等参数集以及其他信息被封装在容器内。
- 用于标示这是一个NALU单元的开始。在H.264的annexb封装格式中,起始码通常为
- NALU头部(NALU Header)
- NALU头部通常包含一个字节的信息,用于指示NALU的类型和其他重要属性。
- 这个字节可以进一步细分为:
forbidden_zero_bit
(禁止位):必须为0,用于错误检测。nal_ref_idc
(重要性指示位):占2bit,表示NALU的重要性级别,用于指示解码器是否应该丢弃该NALU(如果值为00,则解码器可以选择丢弃而不影响图像的回放)。nal_unit_type
(NALU单元类型):占5bit,用于指示NALU的类型,如SPS(序列参数集,值为7)、PPS(图像参数集,值为8)、I帧(IDR帧,一种特殊的I帧,值为5)、P帧(值为1)、B帧(值为0)等。
- NALU载荷(NALU Payload)
- NALU载荷是NALU单元的主体部分,包含了实际的编码数据或参数信息。
- 对于SPS和PPS,NALU载荷中包含了序列或图像的全局参数信息。
- 对于视频帧数据(如I帧、P帧、B帧),NALU载荷中包含了帧的编码数据,这些数据可能进一步被划分为多个片(slice),每个片也可能被封装在一个或多个NALU中。
4.h264的两种封装模式
4.1.Annex B字节流格式
特点与用途:
- 用于实时播放:这是大部分编码器的默认输出格式,非常适合于需要实时传输和播放的场景,如网络直播、视频会议等。
- 包含起始码:每个NALU(网络抽象层单元)前都会加上起始码(Start Code),通常为
0x000001
或0x00000001
。起始码用于标示NALU的开始,便于解码器从码流中分割出独立的NALU单元。 - 随机访问能力:由于起始码的存在,解码器可以从码流的任意位置开始解码,实现随机访问功能。这对于需要快速定位到视频特定位置的场景非常有用。
结构:
- 起始码:如上所述,用于标示NALU的开始。
- NALU头部:紧随起始码之后,包含NALU的类型和其他重要信息。
- NALU载荷:包含实际的编码数据或参数信息。
4.2 AVCC格式
特点与用途:
- 用于存储:这种格式通常被用于可以被随机访问的多媒体数据,如存储在硬盘的文件。MP4、MKV等容器格式常采用AVCC格式来存储H.264编码的视频数据。
- 省略起始码:与Annex B格式不同,AVCC格式省略了起始码,以减少数据冗余和存储空间占用。取而代之的是,在每个NALU前加上一个表示NALU长度的前缀(通常是1、2或4字节)。
- 配置参数集中:解码器配置参数(如SPS和PPS)在一开始就配置好了,并保存在文件头部或容器元数据中,以便解码器在解码前获取这些信息。
结构:
- 长度前缀:表示紧随其后的NALU的长度。
- NALU头部:与Annex B格式相同,包含NALU的类型和其他重要信息。
- NALU载荷:包含实际的编码数据或参数信息。
5.使用ffmpeg解封装
在下面这段代码中,我们从原始容器文件中mp4文件,读取出视频流,然后可用对其进行过滤,从AVCC转换成Annex B字节流格式
#include <stdio.h>
#include <libavutil/log.h>
#include <libavformat/avio.h>
#include <libavformat/avformat.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,
*/
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 1
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;
}