文章目录
- 音频格式
- AAC
- ADIF音频数据交换格式
- ADTS音频数据传输流
- 音频解码
- 音频编码
- 视频格式
- H264
- GOP图像组
- I帧,P帧,B帧
- H264压缩技术
- H264压缩级别
- H264视频级别
- H264码流结构
- SPS
- PPS
- 解码视频
- 编码视频
音频格式
AAC
AAC全称 Advanced Audio Coding,是一种专为声音数据设计的文件压缩格式。出现于1997年,基于MPEG-2的音频编码技术。由Fraunhofer IIS、杜比实验室、AT&T、索尼等公司共同开发,目的是取代MP3格式。与MP3不同,它采用了全新的算法进行编码,更加高效,具有更高的性价比。利用AAC格式,可使人感觉声音质量没有明显降低的前提下,更加小巧。
优点:相较于mp3,AAC格式的音质更佳,文件更小。
不足:AAC属于有损压缩的格式,与时下流行的APE、FLAC等无损格式相比,音质存在本质上的差距。加之传输速度更快的USB3.0和16G以上大容量MP3正在加速普及,也使得AAC头上小巧的光环不复存在。
AAC共有9种规格,以适应不同场合的需要:
- MPEG-2 AAC LC 低复杂度规格(low complexity),比较简单,没有增益控制,但提高了编码效率,在中等码率的编码效率及音质方面,都能找到平衡点
- MPEG-2 AAC Main 主规格
- MPEG-2 AAC SSR 可变采样率规格(scaleable sample rate)
- MPEG-4 AAC LC 低复杂度规格,现在的手机比较常见的MP4文件中的音频部分就包括了该规格音频文件
- MPEG-4 AAC Main 主规格。包含了除增益控制之外的全部功能,其音质最好。
- MPEG-4 AAC SSR 可变采样率规格
- MPEG-4 AAC LTP 长时期预测规格(Long Term Predicition)
- MPEG-4 AAC LD 低延迟规格 (Low Delay)
- MPEG-4 AAC HE 高效率规格(High Efficiency)这种规格适合用于低码率编码,有Nero AAC编码器支持
HE:High Efficiency(高效性)。 HE-AAC v1(又称AACPlusV1,SBR),用容器的方法实现了AAC(LC)+SBR技术。SBR其实代表的是Spectral Band Replication(频段复制)。简要叙述一下,音乐的主要频谱集中在低频段,高频段幅度很小,但很重要,决定了音质。如果对整个频段编码,若是为了保护高频就会造成低频段编码过细以致文件巨大,若是保存了低频的主要成分而失去高频成分就会丧失音质。SBR把频谱切割开来,低频单独编码保存主要成分,高频单独放大编码保存音质,“统筹兼顾”了,在减少文件大小的情况下还保存了音质,完美地化解了这一矛盾。
HEv2:用容器的方法包含了HE-AAC v1和PS技术。PS指parametric stereo(参数立体声)。原来的立体声文件大小是一个声道的两倍。但是两个声道的声音存在某种相似性,根据香农信息熵编码定理,相关性应该被去掉才能减小文件大小。所以PS技术存储了一个声道的全部信息,然后,花很少的字节用参数描述另一个声道和它不同的地方
目前使用最多的是LC和HE(适合低码率)。流行的Nero AAC编码程序只支持LC,HE、HEv2这三种规格,编码后的AAC音频,规格显示都是LC,HE其实就是AAC(LC)+SBR技术,HEv2就是AAC(LC)+SBR+PS技术。
AAC音频格式分为ADIF和ADTS
- ADIF:audio data interchange format,音频数据交换格式。这种格式的特征是可以确定的找到这个音频数据的开始,不需进行在音频数据流中间开始的解码,即它的解码必须在明确定义的开始处进行。故这种格式常用在磁盘文件中。
- ADTS: audio data transport stream,音频数据传输流。这种格式的特征是它是一个有同步字的比特流,解码可以在这个流中任何位置开始,它的特征类似于mp3数据流格式。
简单的说,ADTS可以在任意帧解码,也就是说,它每一帧都有头信息。ADIF只有一个统一的头,所以必须得到所有的数据后解码。且这两种的header格式也是不同的,目前一般编码后的和抽取出的都是ADTS格式的音频流,两者具体的组织结构如下所示:
空白处表示前后帧
有时候当你编码AAC裸流的时候,会遇到写出来的AAC并不能在PC和手机上播放,很大可能就是AAC文件的每一帧里缺少了ADTS头信息文件的包装拼接。秩序奥加入头文件ADTS即可。
ADIF音频数据交换格式
其中adif_header如下表:
raw_data_stream如图所示
byte_alignment为了保持字节对齐用,
可以看到ADIF只有一个header,里面没有关于每帧的信息,因为每帧不是固定大小,故只能按照顺序进行解码,也无法跳播或快进快退。除非自己解码遍历整个文件,建立每帧位置表。
ADTS音频数据传输流
一个AAC原始数据块长度是可变的,对原始帧加上ADTS头进行ADTS的封装,就形成了ADTS帧,AAC音频文件的每一帧由ADTS Header和AAC Audio Data组成。结构体如下:
ADTS头部信息占据了整个文件中的前7或9个字节,分为2部分:
- adts_fixed_header():固定头信息,头信息中的每一帧都相同,其中包括了一个固定的同步标记(syncword),该标记用于确定音频帧的边界位置。
- adts_variable_header():可变头信息,头信息则在帧与帧之间可变
一个完整的头信息就是固定头+可变头。
以下是对头信息中各字段的详细介绍:
- 固定头
- syncword:同步字,12bit,同步字是ADTS文件的标志符,它用于确定音频帧的开始位置和结束位置,通常为0xFFF
- ID:9bit,ID指示使用的MPEG版本。0表示MPEG-4,1表示MPEG-2
- layer:2bit,Layer定义了音频流所属的层级,对于AAC来说,其值为0。
- protection_absent:1bit,指示是否启动CRC错误校验。0表明数据经过CRC校验,否则未经过
- profile:2bit,指示编码所使用的AAC规范类型,
在MPEG-2 中定义了3种
MPEG-4中,profile的值=MPEG-4 Audio Object Type - 1
- sampling_frequency_index:4bit,表示采样率的索引,它告诉解码器当前音频数据的采样率。值的范围是0-15,每一个值表示一个特定的采样率。
- private_bit:1bit,为私有比特,通常被设置为0,没有实际作用。
- channel_configuration:3bit,指示音频的通道数,范围也是0-15
- original_copy:1bit,指示编码数据是否被原始产生,通常为0
- home:1bit,通常被设置为0,没有实际作用
- 可变头
- copyrighted_id_bit:编码时设置为0,解码时忽略
- copyrighted_id_start:编码时设置为0,解码时忽略
- aac_frame_length:ADTS帧长度,它包括ADTS长度和AAC声音数据长度。即aac_frame_length = ( protection_absent == 0 ? 9 : 7 ) + audio_data_length
- adts_buffer_fullness: 固定位0x7FF。表示是码率可变的码流
- number_of_raw_data_blocks_in_frame:ADTS帧中有number_of_raw_data_blocks_in_frame + 1个AAC原始帧。(一个AAC原始帧包含一段时间内1024个采样及相关数据)
AAC ES是AAC音频编码的一种基本数据格式,也是AAC音频数据在流式传输和文件存储中的常见格式之一。
不同于其他容器格式,它不包含额外的元数据或结构信息,仅包含未经任何封装或压缩处理的原始音频数据。这些原始数据可以作为音频文件或流传输的基础,同时也可以用于对AAC音频进行转码、编辑或者重组。
通常由一系列连续的AAC音频帧组成,每个帧以一个特定的标志符开始,该标志符表示这是一个AAC音频帧。在AAC ES中,每个音频帧拥有相同的长度,但并一定包含相同数量的采样点,因为采样率和声道数量可能会发生变化。
另一个关键特征是其比特流顺序,即数字音频数据的组织方式,AAC ES采用大端字节顺序,其中高位字节排在前面,地位字节排在后面。此外,在AAC ES中,音频数据按照从左到右、自上而下的顺序排列,与典型的文本文件不同。
总之,AAC ES是AAC音频编码的一种基本数据格式,它通常由一系列AAC音频帧组成,并且不包含任何附加的元数据或结构信息。AAC ES可以作为音频文件或流传输的基础,同时也可以用于对AAC音频进行转码、编辑或重组。由于其简单性和灵活性,AAC ES受到了广泛的应用,并且成为了数字音频编码领域的标准之一。
音频解码
void Widget::decode()
{
char errbuf[1024]; // 错误信息缓冲区
const char *inputFile = "../source/audio.aac"; // 输入文件
const char *outputFile = "../output/out.pcm"; // 输出文件
AVFormatContext *pInFormatCtx = NULL; // 打开文件上下文
// 打开输入文件
if (avformat_open_input(&pInFormatCtx, inputFile, NULL, NULL) != 0)
{
av_strerror(1, errbuf, sizeof(errbuf));
printf("无法打开输入文件:%s\n", errbuf);
return;
}
// 获取输入文件信息
if (avformat_find_stream_info(pInFormatCtx, NULL) < 0)
{
av_strerror(1, errbuf, sizeof(errbuf));
printf("无法获取输入文件信息:%s\n", errbuf);
return;
}
// 查找音频流
int audioStreamIndex = av_find_best_stream(pInFormatCtx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
if (audioStreamIndex < 0)
{
printf("无法找到音频流\n");
return;
}
// 查找解码器
const AVCodec *pCodec = avcodec_find_decoder(pInFormatCtx->streams[audioStreamIndex]->codecpar->codec_id);
if (!pCodec)
{
printf("无法找到解码器\n");
return;
}
// 创建解码器上下文
AVCodecContext *pCodecCtx = avcodec_alloc_context3(pCodec);
if (!pCodecCtx)
{
printf("无法创建解码器上下文\n");
return;
}
// 复制解码器参数
if (avcodec_parameters_to_context(pCodecCtx, pInFormatCtx->streams[audioStreamIndex]->codecpar) < 0)
{
printf("无法复制解码器参数\n");
return;
}
// 打开解码器
if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0)
{
printf("无法打开解码器\n");
return;
}
// 创建输出文件
FILE *pOutFile = fopen(outputFile, "wb");
if (!pOutFile)
{
printf("无法创建输出文件\n");
return;
}
// 解码数据
AVPacket packet; // 数据包
AVFrame *pFrame = av_frame_alloc(); // 解码后的数据帧
while (av_read_frame(pInFormatCtx, &packet) >= 0)
{
// 解码数据包
int ret = avcodec_send_packet(pCodecCtx, &packet);
if (ret < 0)
{
return; // 解码失败
}
while (ret >= 0)
{
ret = avcodec_receive_frame(pCodecCtx, pFrame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
{
break;
}
else if (ret < 0)
{
av_strerror(ret, errbuf, sizeof(errbuf));
printf("解码数据帧失败:%s\n", errbuf);
break;
}
int fmtByteSize = av_get_bytes_per_sample((AVSampleFormat)pFrame->format); // 每个样本的字节数
for (int i = 0; i < pFrame->nb_samples; i++) // 每个样本
{
for (int ch = 0; ch < pFrame->ch_layout.nb_channels; ch++) // 每个声道
{
fwrite(pFrame->data[ch] + i * fmtByteSize, 1, fmtByteSize, pOutFile); // 写入文件
}
}
av_frame_unref(pFrame); // 释放数据帧
}
av_packet_unref(&packet); // 释放数据包
}
// 编码器刷新,将剩余的数据帧编码为数据包
int ret = avcodec_send_packet(pCodecCtx, NULL);
while (ret >= 0)
{
ret = avcodec_receive_frame(pCodecCtx, pFrame);
if (ret < 0)
{
break;
}
int fmtByteSize = av_get_bytes_per_sample((AVSampleFormat)pFrame->format); // 每个样本的字节数
for (int i = 0; i < pFrame->nb_samples; i++) // 每个样本
{
for (int ch = 0; ch < pFrame->ch_layout.nb_channels; ch++) // 每个声道
{
fwrite(pFrame->data[ch] + i * fmtByteSize, 1, fmtByteSize, pOutFile); // 写入文件
}
}
av_frame_unref(pFrame); // 释放数据帧
}
qDebug() << "解码完成";
// 释放资源
fclose(pOutFile);
av_frame_free(&pFrame);
avcodec_free_context(&pCodecCtx);
avformat_close_input(&pInFormatCtx);
}
音频编码
写入adts头的函数
void Widget::writeADTSHeader(std::ofstream &file, const AVCodecContext *codecContext, const int packetSize)
{
uint8_t adtsHeader[7]{0};
// 写入ADTS头,注意位运算优先级比较低,所以括号不能省略
adtsHeader[0] = 0xFF;
adtsHeader[1] = 0xF1; // 0xF1表示MPEG-4 AAAC 校验位为1
adtsHeader[2] = (codecContext->profile << 6) + (4 << 2) + (codecContext->ch_layout.nb_channels >> 2);
adtsHeader[3] = ((codecContext->ch_layout.nb_channels & 3) << 6) + ((packetSize + 7) >> 11); // 固定头的后4位+可变头的前4位
adtsHeader[4] = ((packetSize + 7) >> 3) & 0xFF;
adtsHeader[5] = (((packetSize + 7) & 0x07) << 5) + 0x1F;
adtsHeader[6] = 0xFC;
file.write((char *)adtsHeader, 7);
}
编码函数
void Widget::encode()
{
char errbuf[1024]; // 错误信息缓冲区
const char *inputFile = "../../source/audio.pcm"; // 输入文件
const char *outputFile = "../../output/out1.aac"; // 输出文件
// 查找AAC编码器
const AVCodec *pCodec = avcodec_find_encoder(AV_CODEC_ID_AAC);
if (!pCodec)
{
printf("无法找到编码器\n");
return;
}
// 创建编码器上下文
AVCodecContext *pCodecCtx = avcodec_alloc_context3(pCodec);
if (!pCodecCtx)
{
printf("无法创建编码器上下文\n");
return;
}
pCodecCtx->sample_fmt = AV_SAMPLE_FMT_FLTP; // 设置采样格式
pCodecCtx->sample_rate = 44100; // 设置采样率
AVChannelLayout ch_layout = AV_CHANNEL_LAYOUT_STEREO; // 设置声道布局
av_channel_layout_copy(&pCodecCtx->ch_layout, &ch_layout);
pCodecCtx->bit_rate = 128'000; // 设置比特率
pCodecCtx->flags = AV_CODEC_FLAG_GLOBAL_HEADER; // 设置全局头标志
pCodecCtx->profile = AV_PROFILE_AAC_HE_V2; // 设置AAC编码器配置文件
// 打开编码器
if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0)
{
printf("无法打开编码器\n");
return;
}
std::ifstream pInFile(inputFile, std::ifstream::binary); // 打开输入文件
std::ofstream pOutFile(outputFile, std::ofstream::binary); // 打开输出文件
if (!pInFile.is_open() || !pOutFile.is_open())
{
return;
}
// 创建数据包
AVPacket *packet = av_packet_alloc();
if (!packet)
{
printf("无法创建数据包\n");
return;
}
// 创建数据帧
AVFrame *pFrame = av_frame_alloc();
if (!pFrame)
{
printf("无法创建数据帧\n");
return;
}
pFrame->sample_rate = pCodecCtx->sample_rate; // 设置采样率
pFrame->format = pCodecCtx->sample_fmt; // 设置采样格式
av_channel_layout_copy(&pFrame->ch_layout, &pCodecCtx->ch_layout); // 设置声道布局
pFrame->nb_samples = pCodecCtx->frame_size; // 设置每帧的样本数
// 分配内存
if (av_frame_get_buffer(pFrame, 0) < 0)
{
printf("无法分配内存\n");
return;
}
// 分配缓冲区
int fmtByteSize = av_get_bytes_per_sample(pCodecCtx->sample_fmt); // 获取每个样本的字节数
uint8_t *pBuffer = (uint8_t *)av_malloc(fmtByteSize * pCodecCtx->frame_size * pCodecCtx->ch_layout.nb_channels); // 分配缓冲区
if (!pBuffer)
{
printf("无法分配缓冲区\n");
return;
}
// 读取数据
while (!pInFile.eof())
{
// 读取数据
for (int i = 0; i < pFrame->nb_samples * pFrame->ch_layout.nb_channels / 2; i++)
{
pInFile.read((char *)pBuffer, fmtByteSize * 2);
if (pInFile.eof())
{
break;
}
for (int ch = 0; ch < pFrame->ch_layout.nb_channels; ch++) // 每个声道
{
memcpy(pFrame->data[ch] + i * fmtByteSize, pBuffer + ch * fmtByteSize, fmtByteSize); // 复制数据到数据帧
}
}
// 编码
int ret = avcodec_send_frame(pCodecCtx, pFrame);
if (ret < 0)
break;
while (ret >= 0)
{
ret = avcodec_receive_packet(pCodecCtx, packet);
if (ret < 0)
break;
writeADTSHeader(pOutFile, pCodecCtx, packet->size); // 写入ADTS头
pOutFile.write((char *)packet->data, packet->size); // 写入数据
av_packet_unref(packet); // 释放数据包
}
// 清空缓冲区
memset(pBuffer, 0, fmtByteSize * pFrame->nb_samples * pFrame->ch_layout.nb_channels);
}
// 清空编码器,防止缓冲区有数据
int ret = avcodec_send_frame(pCodecCtx, nullptr);
while (ret >= 0)
{
ret = avcodec_receive_packet(pCodecCtx, packet);
if (ret < 0)
break;
writeADTSHeader(pOutFile, pCodecCtx, packet->size); // 写入ADTS头
pOutFile.write((char *)packet->data, packet->size); // 写入数据
av_packet_unref(packet); // 释放数据包
}
// 释放资源
av_frame_free(&pFrame);
av_packet_free(&packet);
avcodec_free_context(&pCodecCtx);
pInFile.close();
pOutFile.close();
av_free(pBuffer);
printf("转换完成\n");
}
视频格式
H264
H264从1999年开始,到2003年形成草案,最后在2007年定稿有待核实。H264是MPG-4标准所定义的最新编码格式,同时也是技术含量最高、代表最新技术水平的视频编码格式之一,在ITU的标准称为H.264,在MPEG的标准里是MPEG-4的一个组成部分(MPEG-4 Part10),又叫Advanced Video Codec,因此常常被称为MPG-4 AVC或者直接叫AVC。H264视频格式是经过有损压缩的,但在技术上尽可能做的降低存储体积下获得较好图像质量和低带宽图像快速传输。压缩比大约是1%
GOP图像组
GOP(Group of Pictures)顾名思义,就是一组图片,在实际操作中,就是一组完整的视频帧,怎么叫做完整的视频帧?就是说一个GOP拿出来,必须能够完整地播放、显示。也就是两个I帧之间的间隔。
I帧,P帧,B帧
- I帧(intraframe frame),也叫关键帧,采用帧内压缩技术,将其解压出来就是一张完整的图片。GOP中第一个I帧也被称为IDR帧。
- P帧(forward Predicted frame),向前参考帧,也叫预测帧。压缩时,只参考前面已经处理的帧,采用帧间压缩技术,它只占用I帧一半大小。
- B帧(Bidirectionally predicted frame),双向参考帧,也叫双向预测帧。压缩时既参考前面已经处理的帧,也参考后面的帧,使用帧间压缩技术。它占I帧1/4大小
IDR帧和I帧的区别
一个序列的第一个图像叫做IDR帧(立即刷新图像),IDR帧都是I帧。I帧和IDR帧都是用帧内预测。不用参考任何帧但是之后的P帧和B帧是有可能参考这个I帧之前的帧的。IDR就不允许这样。
其核心作用是,当解码器解码到IDR帧时,立即将参考帧队列清空,将已解码的数据全部输出或抛弃,重新查找参数集,开始一个新的序列。这样,使错误不致传播,从IDR帧开始算新的序列,开始编码。
H264压缩技术
H264首先会把一张图片分成一个个的片,片里面又会分成一个个宏块。
宏块是视频压缩操作的基本单元。无论是帧内压缩还是帧间压缩,它们都以宏块为单位。
以下面的图片为例
划分片
划分宏块
H264默认是用16x16 大小的区域作为一个宏块,也可以划分成8x8大小。划分好宏块后,计算宏块的像素值
划分子块
H264对比较平坦的图像使用16x16大小的宏块。但是为了更高的压缩率,还可以再这个宏块上划分出更小的子块,子块的大小可以是8x16,16x8,8x8,4x8,8x4,4x4非常的灵活
上图红框内的16x16宏块大部分是蓝色背景,而三只鹰的部分图像被划在了该宏块内,为了更好地处理三只鹰部分的图像, 就在这个宏块内又划分出了多个子块。
这样再经过帧内压缩,可以得到更高效的数据。
常见的宏块尺寸如下:
帧内压缩也称为空间压缩,当压缩一帧图像时,仅考虑本帧的数据而不考虑相邻帧之间的冗余信息,这实际上与静态图像压缩类似。帧内一般采用有损压缩算法,由于帧内压缩是编码一个完整的图像,所以可以独立地解码、显示。帧内压缩一般达不到很高的压缩,跟编码JPEG差不多。
帧间压缩,相邻几帧的数据有很大的相关性,或者说前后两帧信息变化很小的特点。也即连续的视频其相邻帧之间具有冗余信息,根据这一特性,压缩相邻帧之间的冗余量就可以进一步提高压缩量,减小压缩比。帧间压缩也称为时间压缩,它通过比较时间轴上不同帧之间的数据进行压缩。帧间压缩一般是无损的,帧差值算法是一种典型的时间压缩法,它通过比较本帧与相邻帧之间的差异,仅记录本帧与其相邻帧的差值,这样可以大大减少数据量。
帧内预测
如图所示,左侧显示的是H264对于帧内预测提供的9种模式
9种帧内预测模式
- 第一种是纵列的即垂直模式,左边以及上边都是已经推测出来的宏块,这个时候就可以将下面4x4的宏块预测出来,对于垂直模式来说,上边A对应下面的一整列都是A,第二个B,对应一列下面也都是B,以此类推。
- 第二种是横向的水平模式,还是左边以及上边都是都是预测出来的数据,预测结果第一行是I,第二行是j,以此类推。
- 第三种是求平均值,在目标的4x4宏块内都是ABCD加上IJKL的求平均值,例如宏块内的小a,它的值是ABCD加IJKL求平均值得出,也就是下面每一个4x4内的像素值都是一样的。需要注意的是:像素a对应的值是(ABCD+IJKL)/2,而不是(A+I)/2。
H264压缩级别
H264 Profile 是对视频压缩特性的描述,Profile越高,说明采用了越高级的压缩特性。
H264视频级别
H264 Level是对视频的描述,Level越高,视频的码率、分辨率、FPS越高。
H264码流结构
H264的两种码流格式,它们分别为:字节流格式和RTP包格式。
- 字节流格式(Annexb):这是在H264官方协议文档中规定的格式,处于文档附录B(Annex-B Byte stream format)中,所以它也成为了大多数编码器,默认的输出格式。它的基本数据单位为NAL单元,也即NALU。为了从字节流中提取出NALU,协议规定,在每个NALU的前面加上起始码:0x000001或0x00000001
- RTP包格式:这种格式并没有在H264中规定,那为什么还要介绍它呢?是因为在H264的官方参考软件JM里,有这种封装格式的实现。在这种格式中,NALU并不需要起始码Start_Code来进行识别,而是在NALU开始的若干字节(1,2,4字节),代表NALU的长度。
码流分层图
接下来我们主要讲字节流格式,H264码流分为2层,NAL层和VCL层
- NAL(Network Abstraction Layer)层,视频数据网络抽象层,是H264码流的基本单元。
- VCL(Video Coding Layer)层,视频数据编码层,VCL结构关系如下图所示,在视频帧序列由图像组成,图像中包含分片,分片里面包含宏块,宏块里面又包含子块
接下来我们来看NALU,NALU由NALU头部和NALU载荷 (Raw Byte Sequence Payload ,RBSP) 组成,其中NALU头部包括禁止位 (forbidden_zero_bit)、参考标识(nal_ref_idc)和NALU类型(nal_uint_type)等信息。NALU载荷则是包含实际视频数据的原始字节序列。
对于一个H264裸流,它就是由一个起始码(StartCode)和一个NALU单元组成
由上图可以知道,NALU单元 = NALU Header + RBSP ,其中RBSP原始字节序列载荷,即在SODB的后面添加了trailing bit,即一个bit 1和若干个bit0,以使字节对齐,SODB(String of Data Bits)原始数据比特流,原始数据比特流长度不一定是8的倍数,故需要补齐,他是由VCL层产生的
在每一个I帧之前,都会存在一个SPS和PPS。
- SPS(Sequence Parameter Set) 序列参数集,作用于一连串连续的视频图像。如seq_parameter_set_id、帧数及POC(picture order count)的约束、参考帧数目、解码图像尺寸和熵编码模式选择标识等
- PPS(Picture Parameter Set)图像参数集,作用于视频序列中的图像。如pic_parameter_set_id、熵编码模式选择标识、片组数目、初始量化参数和去方块滤波系数调整标识等
SPS
SPS结构如下:
- profile_idc:标识当前H264码流的profile。我们知道H264中定义了三种常用的档次profile:
- 基准档次:baseline profile
- 主要档次:main profile
- 扩展档次:extended profile
在H264的SPS中,第一个字节表示profile_idc,根据profile_idc的值可以确定码流符合哪一种档次。判断规律为: - profile_idc = 66 → baseline profile
- profile_idc = 77 → main profile
- profile_idc = 88 → extended profile
在新版的标准中,还包括了High、High 10、High 4:2:2、High 4:4:4、High 10 Intra、High4:2:2 Intra、High 4:4:4 Intra、CAVLC 4:4:4 Intra等,每一种都由不同的profile_idc表示。另外,constraint_set0_flag ~ constraint_set5_flag是在编码的档次方面对码流增加的其他一些额外限制性条件。
- level_idc:标识当前码流的Level。编码的Level定义了某种条件下的最大视频分辨率、最大视频帧率等参数,码流所遵从的Level由level_idc指定。
- seq_parameter_set_id:表示当前的序列参数集的id,通过该id值,图像参数集pps可以引用其代表的sps中的参数。
- log2_max_frame_num_minus4:用于计算MaxFrameNum的值。计算公式为 M a x F r a m e N u m = 2 l o g 2 m a x f r a m e n u m m i n u s 4 + 4 MaxFrameNum = 2^{log2_max_frame_num_minus4 + 4} MaxFrameNum=2log2maxframenumminus4+4。MaxFrameNum是frame_num的上限值,frame_num是图像序号的一种表示方法,在帧间编码中常用作一种参考帧标记的手段。
- pic_order_cnt_type:表示解码picture order count(POC)的方法。POC是另一种计量图像序号的方式,与frame_num有着不同的计算方法。该语法元素的取值为0、1或2
- log2_max_pic_order_cnt_lsb_minus4:用于计算MaxPicOrderCntLsb的值,该值表示POC的上限。计算方法为 M a x P i c O r d e r C n t L s b = 2 l o g 2 m a x p i c o r d e r c n t l s b m i n u s 4 + 4 MaxPicOrderCntLsb = 2^{log2_max_pic_order_cnt_lsb_minus4 + 4} MaxPicOrderCntLsb=2log2maxpicordercntlsbminus4+4。
-
- max_num_ref_frames:用于表示参考帧的最大数目。
-
gaps_in_frame_num_value_allowed_flag:标识位,说明frame_num中是否允许不连续的值。
-
pic_width_in_mbs_minus1:用于计算图像的宽度。单位为宏块个数,因此图像的实际宽度为:
f r a m e w i d t h = 16 × ( p i c w i d t h i n m b s m i n u s 1 + 1 ) frame_width = 16 \times (pic_width_in_mbs_minus1 + 1) framewidth=16×(picwidthinmbsminus1+1)
-
pic_height_in_map_units_minus1:使用PicHeightInMapUnits来度量视频中一帧图像的高度。PicHeightInMapUnits并非图像明确的以像素或宏块为单位的高度,而需要考虑该宏块是帧编码或场编码。PicHeightInMapUnits的计算方式为:
P i c H e i g h t I n M a p U n i t s = p i c h e i g h t i n m a p u n i t s m i n u s 1 + 1 PicHeightInMapUnits = pic_height_in_map_units_minus1 + 1 PicHeightInMapUnits=picheightinmapunitsminus1+1
-
frame_mbs_only_flag:标识位,说明宏块的编码方式。当该标识位为0时,宏块可能为帧编码或场编码;该标识位为1时,所有宏块都采用帧编码。根据该标识位取值不同,PicHeightInMapUnits的含义也不同,为0时表示一场数据按宏块计算的高度,为1时表示一帧数据按宏块计算的高度。
按照宏块计算的图像实际高度FrameHeightInMbs的计算方法为:
F r a m e H e i g h t I n M b s = ( 2 − f r a m e m b s o n l y f l a g ) × P i c H e i g h t I n M a p U n i t s FrameHeightInMbs = ( 2 − frame_mbs_only_flag ) \times PicHeightInMapUnits FrameHeightInMbs=(2−framembsonlyflag)×PicHeightInMapUnits
-
mb_adaptive_frame_field_flag:
-
标识位,说明是否采用了宏块级的帧场自适应编码。当该标识位为0时,不存在帧编码和场编码之间的切换;当标识位为1时,宏块可能在帧编码和场编码模式之间进行选择。
-
direct_8x8_inference_flag:标识位,用于B_Skip、B_Direct模式运动矢量的推导计算。
-
frame_cropping_flag:标识位,说明是否需要对输出的图像帧进行裁剪。
-
vui_parameters_present_flag:标识位,说明SPS中是否存在VUI信息。
PPS
PPS结构如下:
其中的每一个语法元素及其含义如下:
-
pic_parameter_set_id:表示当前PPS的id。某个PPS在码流中会被相应的slice引用,slice引用PPS的方式就是在Slice header中保存PPS的id值。该值的取值范围为[0,255]。
-
seq_parameter_set_id:表示当前PPS所引用的激活的SPS的id。通过这种方式,PPS中也可以取到对应SPS中的参数。该值的取值范围为[0,31]。
-
entropy_coding_mode_flag:熵编码模式标识,该标识位表示码流中熵编码/解码选择的算法。对于部分语法元素,在不同的编码配置下,选择的熵编码方式不同。例如在一个宏块语法元素中,宏块类型mb_type的语法元素描述符为“ue(v)| ae(v)”,在baseline profile等设置下采用指数哥伦布编码,在main profile等设置下采用CABAC编码。
标识位entropy_coding_mode_flag的作用就是控制这种算法选择。当该值为0时,选择左边的算法,通常为指数哥伦布编码或者CAVLC;当该值为1时,选择右边的算法,通常为CABAC。
-
bottom_field_pic_order_in_frame_present_flag:标识位,用于表示另外条带头中的两个语法元素delta_pic_order_cnt_bottom和delta_pic_order_cn是否存在的标识。这两个语法元素表示了某一帧的底场的POC的计算方法。
-
num_slice_groups_minus1:表示某一帧中slice group的个数。当该值为0时,一帧中所有的slice都属于一个slice group。slice group是一帧中宏块的组合方式,定义在协议文档的3.141部分。
-
num_ref_idx_l0_default_active_minus1、num_ref_idx_l0_default_active_minus1表示当Slice Header中的num_ref_idx_active_override_flag标识位为0时,P/SP/Bslice的语法元素num_ref_idx_l0_active_minus1和num_ref_idx_l1_active_minus1的默认值。
-
weighted_pred_flag:标识位,表示在P/SP slice中是否开启加权预测。
-
weighted_bipred_idc:表示在B Slice中加权预测的方法,取值范围为[0,2]。0表示默认加权预测,1表示显式加权预测,2表示隐式加权预测。
-
pic_init_qp_minus26和pic_init_qs_minus26:表示初始的量化参数。实际的量化参数由该参数、slice header中的 s l i c e q p d e l t a ÷ s l i c e q s d e l t a slice_qp_delta \div slice_qs_delta sliceqpdelta÷sliceqsdelta计算得到。
-
chroma_qp_index_offset:用于计算色度分量的量化参数,取值范围为[-12,12]。
-
deblocking_filter_control_present_flag:标识位,用于表示Slice header中是否存在用于去块滤波器控制的信息。当该标志位为1时,slice header中包含去块滤波相应的信息;当该标识位为0时,slice header中没有相应的信息。
-
constrained_intra_pred_flag:若该标识为1,表示I宏块在进行帧内预测时只能使用来自I和SI类型宏块的信息;若该标识位0,表示I宏块可以使用来自Inter类型宏块的信息。
-
redundant_pic_cnt_present_flag:标识位,用于表示Slice header中是否存在redundant_pic_cnt语法元素。当该标志位为1时,slice header中包含redundant_pic_cnt;当该标识位为0时,slice header中没有相应的信息。
解码视频
void Widget::H264Decode()
{
char errbuf[1024];
const char *infile = "../../source/video.h264";
const char *outfile = "../../output/out.yuv";
AVFormatContext *fmt_ctx = nullptr; // 输入文件上下文
// 打开输入文件
if (avformat_open_input(&fmt_ctx, infile, nullptr, nullptr) < 0)
{
av_log(NULL, AV_LOG_ERROR, "无法打开资源文件 %s\n", infile);
return;
}
// 获取资源信息
if (avformat_find_stream_info(fmt_ctx, nullptr) < 0)
{
av_log(NULL, AV_LOG_ERROR, "无法获取资源信息\n");
return;
}
// 查找视频流
int video_stream_index = av_find_best_stream(fmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
if (video_stream_index < 0)
{
av_log(NULL, AV_LOG_ERROR, "无法找到视频流\n");
return;
}
// 查找解码器
const AVCodec *codec = avcodec_find_decoder(fmt_ctx->streams[video_stream_index]->codecpar->codec_id);
if (!codec)
{
av_log(NULL, AV_LOG_ERROR, "无法找到解码器\n");
return;
}
// 创建解码器上下文
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx)
{
av_log(NULL, AV_LOG_ERROR, "无法创建解码器上下文\n");
return;
}
// 填充解码器上下文
if (avcodec_parameters_to_context(codec_ctx, fmt_ctx->streams[video_stream_index]->codecpar) < 0)
{
av_log(NULL, AV_LOG_ERROR, "无法填充解码器上下文\n");
return;
}
// 打开解码器
if (avcodec_open2(codec_ctx, codec, nullptr) < 0)
{
av_log(NULL, AV_LOG_ERROR, "无法打开解码器\n");
return;
}
// 打开输出文件
std::ofstream outfileSream(outfile, std::ios::binary);
if (!outfileSream.is_open())
{
av_log(NULL, AV_LOG_ERROR, "无法打开输出文件\n");
return;
}
// 读取数据包
AVPacket *pkt = av_packet_alloc(); // 分配数据包
AVFrame *frame = av_frame_alloc(); // 分配帧
int ret = 0;
while (av_read_frame(fmt_ctx, pkt) >= 0)
{
// 解码
ret = avcodec_send_packet(codec_ctx, pkt); // 发送数据包到解码器
if (pkt->stream_index != video_stream_index)
continue; // 不是视频流就跳过
if (ret < 0)
{
av_log(NULL, AV_LOG_ERROR, "解码失败\n");
break;
}
while (ret >= 0)
{
ret = avcodec_receive_frame(codec_ctx, frame); // 从解码器接收解码后的帧
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
break;
else if (ret < 0)
{
av_log(NULL, AV_LOG_ERROR, "解码失败\n");
break;
}
// 写入文件
for (int i = 0; i < frame->height; i++)
{
outfileSream.write((char *)frame->data[0] + i * frame->linesize[0], frame->width); // 写入Y分量
}
for (int i = 0; i < frame->height / 2; i++)
{
outfileSream.write((char *)frame->data[1] + i * frame->linesize[1], frame->width / 2); // 写入U分量
}
for (int i = 0; i < frame->height / 2; i++)
{
outfileSream.write((char *)frame->data[2] + i * frame->linesize[2], frame->width / 2); // 写入V分量
}
av_frame_unref(frame); // 清空帧
}
av_packet_unref(pkt); // 清空数据包
}
// 清空编码器
ret = avcodec_send_packet(codec_ctx, nullptr);
while (ret >= 0)
{
ret = avcodec_receive_frame(codec_ctx, frame);
if (ret < 0)
break;
// 写入文件
for (int i = 0; i < frame->height; i++)
{
outfileSream.write((char *)frame->data[0] + i * frame->linesize[0], frame->width); // 写入Y分量
}
for (int i = 0; i < frame->height / 2; i++)
{
outfileSream.write((char *)frame->data[1] + i * frame->linesize[1], frame->width / 2); // 写入U分量
}
for (int i = 0; i < frame->height / 2; i++)
{
outfileSream.write((char *)frame->data[2] + i * frame->linesize[2], frame->width / 2); // 写入V分量
}
}
qDebug() << "解码完成";
// 释放资源
av_frame_free(&frame);
av_packet_free(&pkt);
outfileSream.close();
avcodec_free_context(&codec_ctx);
avformat_close_input(&fmt_ctx);
}
编码视频
void Widget::H264Encode()
{
char errorBuffer[1024];
const char *inputFile = "../../source/video.yuv";
const char *outputFile = "../../output/out.h264";
// 查找H264编码器
const AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_H264);
if (!codec)
{
qDebug() << "找不到编码器";
return;
}
// 创建编码器上下文
AVCodecContext *codecContext = avcodec_alloc_context3(codec);
if (!codecContext)
{
qDebug() << "创建编码器上下文失败";
return;
}
// 设置编码器参数
codecContext->profile = AV_PROFILE_H264_HIGH_444; // 设置编码器压缩级别
codecContext->level = 50; // 设置编码器视频质量级别,50表示5.0
codecContext->width = 1280; // 设置编码器视频宽度
codecContext->height = 720; // 设置编码器视频高度
codecContext->pix_fmt = AV_PIX_FMT_YUV420P; // 设置编码器视频像素格式
codecContext->time_base = AVRational{1, 30}; // 设置编码器时间基准
codecContext->framerate = AVRational{30, 1}; // 设置编码器帧率
codecContext->gop_size = 30; // 设置编码器I帧间隔
codecContext->max_b_frames = 3; // 设置编码器B帧最大数量
codecContext->keyint_min = 3; // 设置编码器最小I帧间隔
codecContext->has_b_frames = 1; // 设置编码器是否支持B帧
codecContext->refs = 3; // 设置编码器参考帧数量
codecContext->bit_rate = 2000000; // 设置编码器比特率
// 打开编码器
CoUninitialize(); // 初始化COM库,头文件<objbase.h>,这里防止报错COM must not be in STA mode
int ret = avcodec_open2(codecContext, codec, nullptr);
if (ret < 0)
{
av_strerror(ret, errorBuffer, sizeof(errorBuffer));
qDebug() << "打开编码器失败:" << errorBuffer;
return;
}
// 打开输入文件
FILE *inputFilePtr = fopen(inputFile, "rb");
if (!inputFilePtr)
{
qDebug() << "Could not open input file";
return;
}
// 打开输出文件
FILE *outputFilePtr = fopen(outputFile, "wb");
if (!outputFilePtr)
{
qDebug() << "Could not open output file";
return;
}
// 创建AVFrame对象
AVFrame *frame = av_frame_alloc();
if (!frame)
{
qDebug() << "Could not allocate video frame";
return;
}
frame->format = codecContext->pix_fmt;
frame->width = codecContext->width;
frame->height = codecContext->height;
// 分配AVFrame对象缓冲区
ret = av_frame_get_buffer(frame, 0);
if (ret < 0)
{
av_strerror(ret, errorBuffer, sizeof(errorBuffer));
qDebug() << "不能为帧分配缓冲区:" << errorBuffer;
return;
}
// 创建AVPacket对象
AVPacket *packet = av_packet_alloc();
if (!packet)
{
qDebug() << "不能分配数据包";
return;
}
// 读取输入文件数据
int ySize = codecContext->width * codecContext->height;
int frameCount = 0;
while (!feof(inputFilePtr))
{
fread(frame->data[0], ySize, 1, inputFilePtr); // Y分量
fread(frame->data[1], ySize / 4, 1, inputFilePtr); // U分量
fread(frame->data[2], ySize / 4, 1, inputFilePtr); // V分量
frame->pts = frameCount; // 设置帧时间戳
// 编码一帧数据
ret = avcodec_send_frame(codecContext, frame);
if (ret < 0)
{
av_strerror(ret, errorBuffer, sizeof(errorBuffer));
qDebug() << "编码失败:" << errorBuffer;
return;
}
while (ret >= 0)
{
ret = avcodec_receive_packet(codecContext, packet);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
break;
else if (ret < 0)
{
av_strerror(ret, errorBuffer, sizeof(errorBuffer));
qDebug() << "编码失败:" << errorBuffer;
return;
}
// 写入输出文件
fwrite(packet->data, packet->size, 1, outputFilePtr);
av_packet_unref(packet);
}
frameCount++; // 帧计数器加1
}
// 清空编码器
ret = avcodec_send_frame(codecContext, nullptr);
while (ret >= 0)
{
ret = avcodec_receive_packet(codecContext, packet);
if (ret < 0)
break;
fwrite(packet->data, packet->size, 1, outputFilePtr); // 写入输出文件
}
// 释放资源
av_packet_free(&packet);
av_frame_free(&frame);
avcodec_free_context(&codecContext);
fclose(inputFilePtr);
fclose(outputFilePtr);
qDebug() << "编码完成!";
}