一、H264压缩编码
1.1 H264 中的 I 帧、P帧和 B帧
H264 使用帧内压缩和帧间压缩的方式提高编码压缩率;H264 采用了独特的 I 帧、P 帧和 B 帧策略来实现,连续帧之间的压缩;
1.2 其他概念
GOP(图像组):一个IDR帧到下一个IDR帧之间间隔了多少个帧
IDR:一个序列的第一个图像叫做IDR图像(立即刷新图像),IDR图像都是I帧图像。I帧不用参考任何帧,但之后的P帧和B帧是有可能参考这个I帧之前的帧。IDR帧不允许后面PB参考前面的帧。IDR的核心作用是为了序列出现重大错误后重新同步,不用参考IDR之前的图像的数据来解码
1.3 NLU
视频流至少发从一帧SPS和一帧PPS来告诉客户端视频流信息
H.264原始码流(裸流)是由⼀个接⼀个NALU组成。它的功能分为两层,VCL (视频编码层) 和
NAL (⽹络提取层)。VCL:负责压缩视频;NAL:把VCL压缩的视频封装成NLU帧
1、NLU帧包括三个部分
[StartCode] [NALU Header] [NALU Payload]
StartCode必须是 "00 00 00 01" 或 "00 00 01"
NALU Header是NALU的头部
RBSP是I、P、B帧的数据。
NLU帧的结束也是根据00 00 00 01或00 00 01来判断的
3字节的0x000001只有⼀种场合下使⽤,就是⼀个完整的帧被编为多个slice(⽚)的时
候,包含这些slice的NALU 使⽤3字节起始码。其余场合都是4字节0x00000001的。前面的I帧就是分别存储在两个NALU中,这两个NALU的开头就是00 00 01
2、NLU Header第一个字节
其中:
T为负荷数据类型,占5bit
nal_unit_type:这个NALU单元的类型,1~12由H.264使⽤,24~31由H.264以外的应⽤
使⽤
R为重要性指示位,占2个bit
nal_ref_idc.:取00~11,似乎指示这个NALU的重要性,如00的NALU解码器可以丢弃它⽽不
影响图像的回放,0~3,取值越⼤,表示当前NAL越重要,需要优先受到保护。如果当前
NAL是属于参考帧的⽚,或是序列参数集,或是图像参数集这些重要的单位时,本句法元
素必需⼤于0。
最后的F为禁⽌位,占1bit
forbidden_zero_bit: 在 H.264 规范中规定了这⼀位必须为 0.
3、NLU的类型
0x00 00 00 01 67 …
67:
⼆进制:0110 0111
00111 = 7(⼗进制)
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 |
序列参数集扩展9 seq_parameter_set_extension_rbsp( )
|
14...18
|
保留 |
19
|
未分割的辅助编码图像的编码条带 slice_layer_without_partitioning_rbsp( )
|
20...23
|
保留
|
24...31
|
未指定
|
1.4 H264 annexb模式
H264有两种封装
⼀种是Annex B模式,传统模式,有startcode,SPS和PPS是在ES中
⼀种是mp4模式,⼀般mp4 mkv都是mp4模式,没有startcode,SPS和PPS以及其它信息
被封装在container中,每⼀个frame前⾯4个字节是这个frame的⻓度。很多解码器只⽀持Annex B这种模式,因此需要将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]->codecpar);
av_bsf_init(bsf_ctx);
1. Annex B内部数据,对应的了前面讲解的内容
[4字节起始码] + [SPS NALU数据]
[4字节起始码] + [PPS NALU数据]
[3字节起始码] + [I帧NALU数据]
[3字节起始码] + [P帧NALU数据]
[3字节起始码] + [B帧NALU数据]
...(后续NALU以此类推)
输出的文件是H.264视频文件
2. MP4直接的码流对应下图右边,开头记录的是NALU帧的长度,都是4字节
二、MP4->H.264代码
#include <iostream>
extern "C"{
#include <libavutil/log.h>
#include <libavformat/avio.h>
#include <libavformat/avformat.h>
#include <libavcodec/bsf.h>
}
using namespace std;
static char err_buf[128] = {0};
static char* av_get_err(int errnum)
{
av_strerror(errnum, err_buf, 128);
return err_buf;
}
int main(int argc, char **argv) {
string inputFile = "believe.mp4";
string outputFile = "believe.h264";
FILE * outfp = fopen(outputFile.c_str(),"wb");
printf("in:%s out:%s\n", inputFile.c_str(), outputFile.c_str());
// 创建AVFormatContext上下文
AVFormatContext * ifmt_ctx = avformat_alloc_context();
if (!ifmt_ctx) {
printf("[error] Could not allocate context.\n");
return -1;
}
int ret = avformat_open_input(&ifmt_ctx, inputFile.c_str(), 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
int videoindex = -1;
videoindex = av_find_best_stream(ifmt_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
if(videoindex == -1) {
printf("[error] Didn't find a video stream.\n");
avformat_close_input(&ifmt_ctx);
return -1;
}
// 创建AVPacket包
AVPacket * pkt = av_packet_alloc();
av_init_packet(pkt);
// 1 获取一个名为"h264_mp4toannexb"的比特流过滤器
// 用于将MP4容器中的H.264视频流转换为Annex B格式的H.264流
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);
int 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) {
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);
}
// 注释掉前面代码,可以直接保存mp4流
// 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);
}
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;
}