一.H264编码原理
1 视频为什么需要进行编码压缩
2 为什么压缩的原始数据一般采用YUV格式,而非GRB
历史原因:如果只有Y分量,就是黑白电视机呈现出来的图像,为了兼容电视机
体积减少:UV分量可以减少,但是并不能太影响观看体验,比如YUV420p,就是一组UV只占0.5个Y,因此YUV420p只有YUV444的一半大小。
3 视频压缩原理-数据冗余 了解
4 图像帧的类型 (I帧、P帧和B帧)
![](https://img-blog.csdnimg.cn/direct/d11d9a24cedf41bdb2551ef29fac852a.png)
5 GOP(一组图像,Group of Pictures)和GOP长度
5.1 GOP之Closed GOP和Open GOP
![](https://img-blog.csdnimg.cn/direct/8294240823f54f14944383dc60ba7fb3.png)
5.2 GOP间隔
![](https://img-blog.csdnimg.cn/direct/9580e9f2a8b5496daa5cc38947cc240e.png)
6 H264编码原理 了解
6.1 宏块扫描
![](https://img-blog.csdnimg.cn/direct/17e59d89a96d4e44bc7a16e631aa551f.png)
![](https://img-blog.csdnimg.cn/direct/ab02238e39c24175b9c84ea6d264d4a3.png)
![](https://img-blog.csdnimg.cn/direct/c97efb9ddf814e9484ef81b95ceb6738.png)
6.2 帧内预测
![](https://img-blog.csdnimg.cn/direct/db39003ca07447ac8b5366572ece5a7d.png)
6.3 残缺块
6.4 帧间预测
6.5 DCT 变换和量化
6.5.1 DCT变换
![](https://img-blog.csdnimg.cn/direct/b823587152ef411cac5dbfe2e84ad9df.png)
6.5.2 量化步长
![](https://img-blog.csdnimg.cn/direct/067f4573e78a4c3684fd3d7f3c92b828.png)
6.5.3 量化步长
6.5.4 量化步长表
![](https://img-blog.csdnimg.cn/direct/82bf48b0636d4110a52b05a1d7596f88.png)
6.6 编码原理总结
![](https://img-blog.csdnimg.cn/direct/86216148d7f94b04b83d44e19376422a.png)
二 .H264 视频编码实战
1.FFmpeg流程
2. 目的
从本地读取YUV数据编码为h264格式的数据,然后再存⼊到本地,编码后的数据有带startcode。
3. 函数说明:
4 代码
#include <iostream>
/// 目的:从本地读取YUV数据编码为h264格式的数据,然后再存⼊到本地,编码后的数据有带startcode。
extern "C" {
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavutil/fifo.h"
#include "libavutil/audio_fifo.h"
#include "libswresample/swresample.h"
#include "libavutil/opt.h"
#include <libavutil/avutil.h>
#include<libavutil/error.h>
#include <libavutil/time.h>
#include <libavutil/imgutils.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
}
using namespace std;
#define ERROR_BUF \
char errbuf[1024]; \
av_strerror(ret, errbuf, sizeof (errbuf));
#define CODE(func, code) \
if (ret < 0) { \
ERROR_BUF; \
cout << #func << "error" << errbuf; \
code; \
}
#define END(func) CODE(func, fataError(); return;)
#define RET(func) CODE(func, return ret;)
#define CONTINUE(func) CODE(func, continue;)
#define BREAK(func) CODE(func, break;)
int64_t get_time()
{
return av_gettime_relative() / 1000; // 换算成毫秒
}
static int encode(AVCodecContext *enc_ctx, AVFrame *frame, AVPacket *pkt,
FILE *outfile)
{
int ret;
/* send the frame to the encoder */
if (frame)
printf("Send frame %lld \n", frame->pts);
/* 通过查阅代码,使用x264进行编码时,具体缓存帧是在x264源码进行,
* 不会增加avframe对应buffer的reference*/
ret = avcodec_send_frame(enc_ctx, frame);
if (ret < 0)
{
fprintf(stderr, "Error sending a frame for encoding\n");
return -1;
}
while (ret >= 0)
{
ret = avcodec_receive_packet(enc_ctx, pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
return 0;
} else if (ret < 0) {
fprintf(stderr, "Error encoding audio frame\n");
return -1;
}
if(pkt->flags & AV_PKT_FLAG_KEY)
printf("Write packet flags:%d pts:%lld dts:%lld (size:%5d)\n",
pkt->flags, pkt->pts, pkt->dts, pkt->size);
if(!pkt->flags)
printf("Write packet flags:%d pts:%lld dts:%lld (size:%5d)\n",
pkt->flags, pkt->pts, pkt->dts, pkt->size);
fwrite(pkt->data, 1, pkt->size, outfile);
}
return 0;
}
int main()
{
cout << "Hello World!" << endl;
int ret = 0;
//1.编码器相关
const AVCodec * aacencoder = nullptr;
//2.编码器上下文相关
AVCodecContext * aacencodercontext = nullptr;
//5 . 文件相关
char * in_yuv_file = "D:/AllInformation/qtworkspacenew/08encoder_02_yuvToH264/yuv420p_1280x720.yuv";
FILE * infile = nullptr;
char * out_h264_file = "D:/AllInformation/qtworkspacenew/08encoder_02_yuvToH264/h264.h264";
FILE * outfile = nullptr;
// 6. 分配pkt和frame
AVPacket * pkt = nullptr;
AVFrame * frame = nullptr;
//8.
int frame_bytes_bufsize = 0;
uint8_t *frame_bytes_buf = nullptr;
//9.时间相关
int64_t begin_time;
int64_t end_time;
int64_t all_begin_time;
int64_t all_end_time;
int64_t pts = 0;
//1.编码器相关,找到编码器
aacencoder = avcodec_find_encoder_by_name("libx264");
// aacencoder = avcodec_find_encoder(AV_CODEC_ID_H264);
if(!aacencoder){
ret = -1;
cout << " avcodec_find_encoder_by_name(libx264) error " << endl;
goto yuvtoh264end;
}
// avcodec_find_encoder(AV_CODEC_ID_H264);说明
// ffmpeg -codecs | findstr 264 我们看到 对于encoders 来说,第一个encoder也是libx264,这说明我们的ffmpeg是build 过 h264的,因此通过 avcodec_find_encoder(AV_CODEC_ID_H264) 得到的也是libx264
// DEV.LS h264 H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10
// (decoders: h264 h264_qsv h264_cuvid )
// (encoders: libx264 libx264rgb h264_amf h264_mf h264_nvenc h264_qsv )
//2.编码器上下文相关,找到编码器上下文
aacencodercontext = avcodec_alloc_context3(aacencoder);
if(!aacencodercontext){
ret = -2;
cout << " avcodec_alloc_context3(aacencoder) error " << endl;
goto yuvtoh264end;
}
//3。设置编码器上下文参数,必须要设置的有 宽和高,视频的时间基准timebase,framerate帧率,I帧间隔gop_size,
// 3.1 设置分辨率
aacencodercontext->width = 1280;
aacencodercontext->height = 720;
// 3.2 设置time base ,AVCodecContext中的time_base 是以秒为单位,表示一张图片时间是 1/25 秒
aacencodercontext->time_base = (AVRational){1, 25};
// 3.3 设置framerate,framerate是帧率,表示一秒钟可以播放多少张画面。 framerate 和 time_base是 有关系
aacencodercontext->framerate = (AVRational){25, 1};
// 3.4 设置 I帧的间隔 ,也就是GOP间隔,一般是和 framerate值一样,或者是framerate的整数倍,
// 3.4.1 gop_size越大,GOP 越大,编码的 I 帧就会越少。相比而言,P 帧、B 帧的压缩率更高,
// 因此整个视频的编码效率就会越高。但是 GOP 太大,也会导致 IDR 帧距离太大,点播场景时进行视频的seek 操作就会不方便。
aacencodercontext->gop_size = 25; // I帧间隔
//3.5 指定编码器允许使用的最大B帧数量,应该是一个GOP中可以有最多的B帧数量
aacencodercontext->max_b_frames = 2; // 如果不想包含B帧则设置为0,一般在做直播的时候,max_b_frames这个值需要设置成0
//3.6 指定输入数据的像素格式
aacencodercontext->pix_fmt = AV_PIX_FMT_YUV420P;
//3.7 设置bitrate,指定编码的比特率,即视频数据每秒使用的比特数,通常以bit/s为单位。该参数的大小直接影响到视频的质量和大小,过高的比特率可能会导致视频过大,而过低的比特率可能会导致视频质量下降。
aacencodercontext->bit_rate = 3000000;
// aacencodercontext->rc_max_rate = 3000000; //最大比特率
// aacencodercontext->rc_min_rate = 3000000; //最小比特率
// aacencodercontext->rc_buffer_size = 2000000; //解码器比特流缓冲区大小
//如果开启多线程,那么 thread_count 和 thread_type参数要一起使用,而且thread_type的值需要为 FF_THREAD_FRAME
//注意的是,thread 会有延迟,测试发现,如果 thread_cout设置为4,那么当有4个frame进来的时候,才会实做,因此有延迟,也是因为这个原因,我们在直播的时候,一般不会使用多线程
// aacencodercontext->thread_count = 4; // 开了多线程后也会导致帧输出延迟, 需要缓存thread_count帧后再编程。
// aacencodercontext->thread_type = FF_THREAD_FRAME; // 并 设置为FF_THREAD_FRAME
/* 对于H264 AV_CODEC_FLAG_GLOBAL_HEADER 设置则只包含I帧,此时sps pps需要从codec_ctx->extradata读取
* 不设置AV_CODEC_FLAG_GLOBAL_HEADER,则表示每个I帧都带 sps pps sei
* 因此在存储本地文件的时候,不能带这个值。但是在流媒体的时候不一定。
*/
// aacencodercontext->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; // 存本地文件时不要去设置这个项
//3.7 设置 x264 相关的 参数,那么我们怎么知道x264有哪些参数呢?以及参数的值是多少呢?简单的查,可以用看 libx264.c的 AVOption options,可以看到说明,以及一些默认值。
//如果要查看详细的说明,这时候想到的是 使用ffmpeg -h encoder=libx264查看,发现详细的说明: 看注释是编译 x264,然后运行 x264 --fullhelp 可以看到,意思是,你要下载x264源码并build出来 x264.exe,然后执行 x264 --fullhelp 查看说明
//这块比较复杂,会有一个blog说明,到底怎么去做。todo
//3.7.1 注意,这里设置的x264的 通用相关参数。,设置的参数不管是 fdk自带的aac,还是 libx264,
// 我们通过命令 查看x264的ffmpeg的 ffmpeg -codecs | findstr x264
// DEV.LS h264 H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10
// (decoders: h264 h264_qsv h264_cuvid )
// (encoders: libx264 libx264rgb h264_amf h264_mf h264_nvenc h264_qsv ) 说明当前编码器默认使用的libx264
if (aacencoder->id == AV_CODEC_ID_H264) {
// 相关的参数可以参考libx264.c的 AVOption options
// ultrafast all encode time:2270ms
// medium all encode time:5815ms
// veryslow all encode time:19836ms
ret = av_opt_set(aacencodercontext->priv_data, "preset", "medium", 0);
if(ret != 0) {
printf("av_opt_set preset failed\n");
}
ret = av_opt_set(aacencodercontext->priv_data, "profile", "main", 0); // 默认是high
if(ret != 0) {
printf("av_opt_set profile failed\n");
}
ret = av_opt_set(aacencodercontext->priv_data, "tune","zerolatency",0); // 直播是才使用该设置,zerolatency中文是零延迟的意思
// ret = av_opt_set(aacencodercontext->priv_data, "tune","film",0); // 画质film
if(ret != 0) {
printf("av_opt_set tune failed\n");
}
}
//4. 将编码器 和 编码器上下文 绑定
ret = avcodec_open2(aacencodercontext,aacencoder,NULL);
if(ret<0){
printf("avcodec_open2 failed\n");
goto yuvtoh264end;
}
//打印信息,看是否通过 thread 编码
printf("thread_count: %d, thread_type:%d\n", aacencodercontext->thread_count, aacencodercontext->thread_type);
//5. 打开输入和输出文件
infile = fopen(in_yuv_file, "rb");
if (!infile) {
fprintf(stderr, "Could not open %s\n", in_yuv_file);
goto yuvtoh264end;
}
outfile = fopen(out_h264_file, "wb");
if (!outfile) {
fprintf(stderr, "Could not open %s\n", out_h264_file);
goto yuvtoh264end;
}
// 6. 分配pkt和frame
pkt = av_packet_alloc();
if (!pkt) {
fprintf(stderr, "Could not allocate video frame\n");
goto yuvtoh264end;
}
frame = av_frame_alloc();
if (!frame) {
fprintf(stderr, "Could not allocate video frame\n");
goto yuvtoh264end;
}
// 7. 为frame分配buffer
frame->format = aacencodercontext->pix_fmt;
frame->width = aacencodercontext->width;
frame->height = aacencodercontext->height;
ret = av_frame_get_buffer(frame, 0);
if (ret < 0) {
fprintf(stderr, "Could not allocate the video frame data\n");
goto yuvtoh264end;
}
// 8.计算出每一帧的数据大小-目的是为了分配一帧的大小 像素格式 * 宽 * 高 ,当前值是1382400
frame_bytes_bufsize = av_image_get_buffer_size((enum AVPixelFormat)frame->format, frame->width,
frame->height, 1);
//8.1 av_image_get_buffer_size方法说明 https://blog.csdn.net/e891377/article/details/127034124
printf("frame_bytes %d\n", frame_bytes_bufsize);
// 9. 一帧的空间。
frame_bytes_buf = (uint8_t *)malloc(frame_bytes_bufsize);
if(!frame_bytes_buf) {
printf("frame_bytes_buf malloc failed\n");
return 1;
}
begin_time = get_time();
end_time = begin_time;
all_begin_time = get_time();
all_end_time = all_begin_time;
pts = 0;
printf("start enode\n");
for (;;) {
memset(frame_bytes_buf, 0, frame_bytes_bufsize);
size_t read_bytes = fread(frame_bytes_buf, 1, frame_bytes_bufsize, infile);
if(read_bytes <= 0) {
printf("read file finish\n");
break;
}
/* 确保该frame可写, 如果编码器内部保持了内存参考计数,则需要重新拷贝一个备份
目的是新写入的数据和编码器保存的数据不能产生冲突
*/
int frame_is_writable = 1;
if(av_frame_is_writable(frame) == 0) { // 这里只是用来测试
printf("the frame can't write, buf:%p\n", frame->buf[0]);
if(frame->buf && frame->buf[0]) // 打印referenc-counted,必须保证传入的是有效指针
printf("ref_count1(frame) = %d\n", av_buffer_get_ref_count(frame->buf[0]));
frame_is_writable = 0;
}
ret = av_frame_make_writable(frame);
if(frame_is_writable == 0) { // 这里只是用来测试
printf("av_frame_make_writable, buf:%p\n", frame->buf[0]);
if(frame->buf && frame->buf[0]) // 打印referenc-counted,必须保证传入的是有效指针
printf("ref_count2(frame) = %d\n", av_buffer_get_ref_count(frame->buf[0]));
}
if(ret != 0) {
printf("av_frame_make_writable failed, ret = %d\n", ret);
break;
}
int need_size = av_image_fill_arrays(frame->data, frame->linesize, frame_bytes_buf,
(enum AVPixelFormat)frame->format,
frame->width, frame->height, 1);
//这里是 图片,因此读取的数据和 声音的不一样,每次读取的大小都应该和预设的一样大
if(need_size != frame_bytes_bufsize) {
printf("av_image_fill_arrays failed, need_size:%d, frame_bytes_bufsize:%d\n",
need_size, frame_bytes_bufsize);
break;
}
pts += 40; //pts 是显示时间
// 设置pts
frame->pts = pts; // 使用采样率作为pts的单位,具体换算成秒 pts*1/采样率
begin_time = get_time();
ret = encode(aacencodercontext, frame, pkt, outfile);
end_time = get_time();
printf("encode time:%lldms\n", end_time - begin_time);
if(ret < 0) {
printf("encode failed\n");
break;
}
}
/* 冲刷编码器 */
encode(aacencodercontext, NULL, pkt, outfile);
all_end_time = get_time();
printf("all encode time:%lldms\n", all_end_time - all_begin_time);
yuvtoh264end:
if(!frame_bytes_buf){
free(frame_bytes_buf);
}
if(!infile){
fclose(infile);
}
if(!outfile){
fclose(outfile);
}
avcodec_free_context(&aacencodercontext);
return 0;
}
5 ffmpeg 和 x264的参数对照
ffmpeg 和 x264的参数对照 ,这里查看这个文档的时候不要使用查看模式,不要编辑模式,不然显示不全,可以按住 ctrl + 鼠标上下滑动就可以看清
x264 |
|
ffmpeg |
|
说明 |
命令行 |
字段 |
命令行 |
字段 |
|
qp qp_constant |
cqp |
|
cqp |
固定量化因子。取值范围0到51。 经常取值在20-40之间,越小质量 越好,要求的码率越高。0表示无损压缩 |
max-keyint |
i_keyint_max |
g |
gop_size |
关键帧的最大间隔帧数 |
min-keyint |
i_keyint_min |
|
keyint_min |
关键帧的最小间隔帧数 |
level |
i_level_idc |
|
level |
取值范围10-51。 设置比特流的Level。默认40,即4.0。 用来告诉解码器需要支持的什么级别的 兼容性。只有在你知道自己在做什么的 时候才设置该参数。 |
frameref |
i_frame_reference |
|
refs |
B和P帧向前预测参考的帧数。取值范 围1-16。 该值不影响解码的速度,但是越大解码 所需的内存越大。这个值在一般情况下 越大效果越好,但是超过6以后效果就 不明显了。 |
bframes |
i_bframe |
|
max_b_frames |
最大B帧数. |
b-adapt |
b_bframe_adaptive |
|
b_frame_strategy |
如果为true,则自动决定什么时候需要 插入B帧,最高达到设置的最大B帧数。 如果设置为false,那么最大的B帧数被 使用。 |
b-pyramid |
b_bframe_pyramid |
|
FLAGS2(CODEC_FLAG2_BPYRAMID) |
当设置B帧>=2时候,通过开启这个选 项可以获得质量的略微提高,但是没有 任何的速度损失。 |
|
b_deblocking_filter |
|
FLAGS(CODEC_FLAG_LOOP_FILTER) |
|
deblock |
i_deblocking_filter_alphac0 |
|
deblockalpha |
|
cabac |
b_cabac |
|
coder_type(FF_CODER_TYPE_AC) |
使用CABAC熵编码技术,为引起轻微的 编码和解码的速度损失,但是可以提高 10%-15%的编码质量。 |
qmin |
i_qp_min |
|
qmin |
最小的量化因子。取值范围1-51。建 议在10-30之间。 |
qmax |
i_qp_max |
|
qmax |
最大的量化因子。取值范围1-51。建 议在10-30之间。 |
qpstep qp-step |
i_qp_step |
|
max_qdiff |
最大的在帧与帧之间进行切变的量化 因子的变化量。 |
qcomp |
f_qcompress |
|
|
|
vbv-maxrate |
i_vbv_max_bitrate |
b |
rc_max_rate |
允许的最大码流,x264里面以kbps为 单位,ffmpeg以bps为单位 |
vbv-bufsize |
i_vbv_buffer_size |
bufsize |
rc_buffer_size |
在指定vbv-maxrate的时候必须设置 该字段。 |
vbv-init |
f_vbv_buffer_init |
|
rc_initial_buffer_occupancy |
初始的缓存占用量 |
qcomp |
f_qcompress |
|
qcompress |
量化器压缩比率0-1.越小则比特率 越区域固定,但是越高越使量化器 参数越固定。 |
direct-pred direct |
i_direct_mv_pred |
|
directpred |
B帧里面采用的运动侦测的方式。 时间和空间方式大致PSNR和速度 是一致的。设置为auto质量会好一 些,但是速度会下降一些,设置为0 ,质量和速度都会下降.可以选择 none, auto, temporal, spatial. |
weightb weight-b |
b_weighted_bipred |
|
FLAGS2(CODEC_FLAG2_WPRED) |
当B帧设置>1时使用 |
partitions analyse |
inter |
|
|
X264_ANALYSE_I4x4 X264_ANALYSE_I8x8 X264_ANALYSE_PSUB16x16 X264_ANALYSE_PSUB8x8 X264_ANALYSE_BSUB16x16 |
8x8dct |
b_transform_8x8 |
|
FLAGS(CODEC_FLAG2_8X8DCT) |
|
me |
i_me_method |
|
me_method |
运动侦测的方式 ME_EPZS ME_HEX ME_UMH ME_FULL ME_ESA |
me-range merange |
i_me_range |
|
me_range |
运动侦测的半径 |
subq subme |
i_subpel_refine |
|
me_subpel_quality |
这个参数控制在运动估算过程中质 量和速度的权衡。Subq=5可以压 缩>10%于subq=1。1-7 |
mixed-refs |
b_mixed_references |
|
FLAGS2(CODEC_FLAG2_MIXED_REFS) |
允许8*8,16*8运动块独立地选择 参考帧,如果disable,则所有的宏 块必须参考同一帧。 需要frameref > 1 |
brdo |
b_bframe_rdo |
|
FLAGS2(CODEC_FLAG2_BRDO) |
需要subq>6 |
bime |
b_bidir_me |
|
bidir_refine |
取值范围:true,false.这个值在没 有B帧的时候失效。在双向预测宏块中 双向运动矢量使用。 |
trellis |
i_trellis |
|
trellis |
|
deadzone-intra |
i_luma_deadzone |
|
没有对应值 |
|
deadzone-inter |
i_luma_deadzone |
|
没有对应值 |
|
fast-pskip |
b_fast_pskip |
|
FLAGS(CODEC_FLAG2_FASTPSKIP) |
在P帧内执行早期快速跳跃探测。 这个经常在没有任何损失的前提 下提高了速度。 |
dct-decimate |
b_dct_decimate |
|
没有对应值 |
|
nr |
i_noise_reduction |
|
noise_reduction |
0意味着关闭,对于噪声很大的 内容你需要打开。 范围:0-100000 |
interlaced |
b_interlaced |
|
没有对应值 |
|
global-header |
b_repeat_headers |
|
FLAGS(CODEC_FLAG_GLOBAL_HEADER) |
使得SPS和PPS只在流的开始处 产生一次。有些播放器,如SONY 的PSP需要开启此参数。默认的设 置使得SPS和PPS在每一个IDR帧 开始出都进行重复。 |
aud |
b_aud |
|
FLAGS2(CODEC_FLAG2_AUD) |
|
threads |
i_threads |
|
thread_count |
将帧切分成块,由不同的线程进行 分别编码。0-4。 0 for auto |
rc-eq |
psz_rc_eq |
|
rc_eq |
|
--no-psnr |
b_psnr |
|
FLAGS(CODEC_FLAG_PSNR) |
是否开启PSNR. |
--no-ssim |
b_ssim |
|
没有对应值 |
|
--progress |
b_progress |
|
没有对应值 |
|
--bitrate |
i_bitrate |
b |
bit_rate |
编码输出的比特率,并启用 ABR(Average Birtate 模式(i_rc_method), |
qblur |
f_qblur |
|
qblur |
|
|
f_complexity_blur |
|
complexityblur |
|