一、H264中的profile和level
- H264 Profile:对视频压缩特性的描述,Profile越高,就说明采用了越高级的压缩特性;
- H264 Level:Level是对视频的描述,Level越高,视频的码率、分辨率、fps越高。
二、H264 SPS中的重要参数
分辨率:
帧相关:
- log2_max_frame_num_minus4:用于计算MaxFrameNum的值。计算公式为MaxFrameNum = 2^(log2_max_frame_num_minus4 + 4)。MaxFrameNum是frame_num的上限值,frame_num是图像序号的一种表示方法,在帧间编码中常用作一种参考帧标记的手段。值得注意的是 frame_num 是循环计数的,即当它到达 MaxFrameNum 后又从 0 重新开始新一轮的计数。 解码器必须要有机制检测这种循环, 不然会引起类似千年虫的问题,在图像的顺序上造成混乱。
- pic_order_cnt_type:指明了 poc (picture order count) 的编码方法, poc 标识图像的播放顺序。由于H.264 使用了 B 帧预测,使得图像的解码顺序并不一定等于播放顺序,但它们之间存在一定的映射关系。 poc 可以由 frame-num 通过映射关系计算得来,也可以索性由编码器显式地传送。 H.264 中一共定义了三种 poc 的编码方法,这个句法元素就是用来通知解码器该用哪种方法来计算 poc。
- max_num_ref_frames:指定参考帧队列可能达到的最大长度,解码器依照这个句法元素的值开辟存储区,这个存储区用于存放已解码的参考帧, H.264 规定最多可用 16 个参考帧,本句法元素的值最大为 16。值得注意的是这个长度以帧为单位,如果在场模式下,应该相应地扩展一倍。
帧率的计算:
framerate = (float)(sps->vui.vui_time_scale) / (float)(sps->vui.vui_num_units_in_tick) / 2;
三、H264 PPS与Slice-Header
PPS:
Slice Header:
- 帧类型
- GOP中解码帧序号
- 预测权重
- 滤波
四、H264分析工具
- Electra Stream Eye(付费)
- CodecVisa(付费)
- 雷神开发的工具(免费)
elecard下载地址:
https://www.elecard.com/products/video-analysis
雷神:
https://sourceforge.net/projects/videoeye/files
工具详细的使用说明可以参考雷神写的博客:
https://blog.csdn.net/leixiaohua1020/article/details/34553607
五、视频编码步骤
- 打开编码器
- 将采集的数据转换成YUV420P
- 准备编码数据AVFrame
- H264编码
六、x264参数分类
- 预设值
- 帧相关参数
- 码流的控制
- 编码分析
- 输出
1. 预设值
- Preset
默认:medium,在压缩率和运算时间中平衡的预设值。 - Tune
默认:无,在上一个选项基础上进一步优化输入。如果定义了一个tune值,它将在preset之后,其它选项之前生效。
2. 帧相关参数
- Keyint
默认:250,设置x264输出中最大的IDR帧(也称为关键帧)间距。
IDR帧是视频流的“分隔符”,所有帧都不可以使用越过关键帧的帧作为参考帧。IDR帧是I帧的一种,所以它们也不参照其它帧。这意味着它们可以作为视频的搜索(seek)点。
通过这个设置可以设置IDR帧的最大间隔帧数(亦称最大图像组长度)。较大的值将导致IDR帧减少(会用占用空间更少的P帧和B帧取代),也就同时减弱了参照帧选择的限制。较小的值导致减少搜索一个随机帧所需的平均时间。
建议:默认值(fps的10倍)对大多数视频都很好。如果在为蓝光、广播、直播流或者其它什么专业流编码,也许会需要更小的图像组长度(一般等于fps)。 - min-keyint
默认:auto(keyint/10),过小的keyint范围会导致产生“错误的”IDR帧(比如说,一个闪屏场景)。此选项限制了IDR帧之间的最小距离。 - scenecut
默认:40,设置决策使用I帧、IDR帧的阈值(场景变换检测)。x264会计算每一帧与前一帧的不同程度并得出一个值。如果这个值低于scenecut,那么就算检测到一个“场景变换”。如果此时距离上一帧的距离小于 min-keyint则插入一个I帧,反之则插入一个IDR帧。 - bframes
默认:3,设置x264可使用的B帧的最大连续数量。没有B帧时,一个典型的x264流帧类型是这样的:IPPPPP…PI。如果设置了-bframes 2,那么两个连续的P帧就可以用B帧替换,然后就像这样:IBPBBPBPPPB…PI。
B帧和P帧的区别在于它可以参照它之后的帧,这个特点让它可以显著地提升压缩率。他们的平均品质受 –pbratio选项的控制。 - ref
默认:3,控制DPB (Decoded Picture Buffer)的大小。可以在0-16之间选择。简单地说,就是设置P帧可以选择它之前的多少帧作为参照帧(B帧的值要小1-2,取决于那个B帧能不能作为参照)。最小可以选择值1,只参照自己前面的那帧。
注意H.264标准限制了每个level可以参照的帧的数量。如果选择level4.1,1080p最大选4,720p最大选9。 - no-deblock
默认:无,完全关闭内置去块滤镜。不推荐使用。 - deblock
默认:0:0,调节H.264标准中的内置去块滤镜。这是个性价比很高的选择。 - no-cabac
默认值:无,停用弹性内容的二进制算数编码(CABAC:Context Adaptive Binary Arithmetic Coder)资料流压缩,切换到效率较低的弹性内容的可变长度编码(CAVLC:Context Adaptive Variable Length Coder)系统。大幅降低压缩效率(通常10~20%)和解码的硬件需求。
3. 流控
- QP
默认:无,三种速率控制方法之一。QP关注量化器,比crf码流大且与bitrate/crf互斥。 - Bitrate
默认:无,关注码流,无法控制质量 - Crf
默认值:23.0,关注质量,数越低越好。 - Qmin
默认值:10,定义x264可以使用的最小量化值。量化值越小,输出就越接近输入。到了一定的值,x264的输出看起来会跟输入一样,即使它并不完全相同。 - Qmax
默认值:51,定义x264可以使用的最大量化值。默认值51是H.264规格可供使用的最大量化值,而且品质极低。此默认值有效地停用了qpmax。如果想要限制x264可以输出的最低品质,可以将此值设小一点(通常30~40),但通常并不建议调整此值。 - Qpstep
默认值:4,设定两帧之间量化值的最大变更幅度。
4. 编码分析
- Partitions
默认值:p8x8,b8x8,i8x8,i4x4,H.264视讯在压缩过程中划分为16x16的宏区块。这些区块可以进一步划分为更小的分割,这就是此选项要控制的部分。 - me
默认值:hex,设定全像素(full-pixel)动态估算(motion estimation)的方法。有五个选项:dia(diamond)、hex(hexagon)、umh(uneven multi-hex)、esa(exhaustive)、tesa(transformed exhaustive)。
5. 输出
- SAR 设置输出的宽高比
- FPS 帧率
- leve
详细的参数说明可以参考:
https://blog.csdn.net/dxpqxb/article/details/50755485
http://www.chaneru.com/Roku/HLS/X264_Settings.htm
https://sites.google.com/site/linuxencoding/x264-ffmpeg-mapping
七、测试代码示例
#include <iostream>
using namespace std;
//包含ffmpeg头文件
extern "C"
{
#include "libavutil/avutil.h"
#include "libavdevice/avdevice.h"
#include "libavformat/avformat.h"
#include "libavcodec/avcodec.h"
#include "libswresample/swresample.h"
#include "libavutil/audio_fifo.h"
}
#include <windows.h>
#include <vector>
#include <string>
#include <memory>
using std::vector;
using std::string;
using std::shared_ptr;
#define V_WIDTH 1280
#define V_HEIGHT 720
void encode(AVCodecContext *ctx,
AVFrame *frame,
AVPacket *pkt,
FILE *output)
{
int ret = 0;
if (frame) {
printf("send frame to encoder, pts=%lld", frame->pts);
}
//将数据送到编码器
ret = avcodec_send_frame(ctx, frame);
//如果ret>=0说明数据设置成功
while (ret >= 0) {
//获取编码后的音频数据,如果成功(ret >= 0)需要重复获取,直到失败为止
ret = avcodec_receive_packet(ctx, pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
return;
} else if (ret < 0) {
cout << "Error, encoding audio frame" << endl;
exit(-1);
}
//write file
fwrite(pkt->data, static_cast<size_t>(pkt->size), 1, output);
//刷新缓冲区,使数据写入磁盘
fflush(output);
av_packet_unref(pkt);
}
return;
}
/**
* @brief open audio device
* @param audio device name
* @return succ: AVFormatContext*, fail: nullptr
*/
AVFormatContext *open_dev(void)
{
int ret = 0;
char errors[1024];
AVFormatContext *fmt_ctx = nullptr;
AVDictionary *options = nullptr;
//get format
//录制摄像头
string sDeviceName = "video=HD Camera";
const AVInputFormat *iformat = av_find_input_format("dshow");
//录制桌面
//string sDeviceName = "desktop";
//const AVInputFormat *iformat = av_find_input_format("gdigrab");
//录制摄像头
//string sDeviceName = "0";
//const AVInputFormat *iformat = av_find_input_format("vfwcap");
av_dict_set(&options, "video_size", "1280x720", 0);
av_dict_set(&options, "framerate", "10", 0);
av_dict_set(&options, "pixel_format", "yuyv422", 0);
av_dict_set(&options, "rtbufsize", "30412800", 0);
//open device
if ((ret = avformat_open_input(&fmt_ctx, sDeviceName.data(),
iformat, &options)) < 0) {
av_strerror(ret, errors, 1024);
printf("Failed to open video device, [%d]%s\n", ret, errors);
return nullptr;
}
return fmt_ctx;
}
/**
* @brief creat_frame
* @return succ: AVFrame*, fail: nullptr
*/
AVFrame *creat_frame(int width, int height)
{
AVFrame *frame = nullptr;
int ret = 0;
//音频输入数据(未编码的数据)
frame = av_frame_alloc();
if (! frame) {
cout << "Error, Failed to alloc frame!" << endl;
goto __ERROR;
}
//set parameters
frame->width = width;
frame->height = height;
frame->format = AV_PIX_FMT_YUV420P;
//alloc inner memory
ret = av_frame_get_buffer(frame, 32); //按 32 位对齐
if (ret < 0) {
printf("Error, Failed to alloc buffer for frame!\n");
goto __ERROR;
}
return frame;
__ERROR:
if (frame) {
av_frame_free(&frame);
}
return nullptr;
}
static void open_encoder(int width, int height, AVCodecContext **enc_ctx)
{
const AVCodec *codec = nullptr;
int ret = 0;
codec = avcodec_find_encoder_by_name("libx264");
if (! codec) {
printf("Codec libx264 not found\n");
exit(1);
}
*enc_ctx = avcodec_alloc_context3(codec);
if (! *enc_ctx) {
printf("Could not allocate video codec context!\n");
exit(1);
}
(*enc_ctx)->profile = FF_PROFILE_H264_HIGH_444;
(*enc_ctx)->level = 50; //表示LEVEL是5.0
(*enc_ctx)->width = width;
(*enc_ctx)->height = height;
//GOP
(*enc_ctx)->gop_size = 250;
//最小插入I帧的间隔
(*enc_ctx)->keyint_min = 25; //option
//设置B帧的数量
(*enc_ctx)->max_b_frames = 3; //option
(*enc_ctx)->has_b_frames = 1; //option
//设置参考帧的数量
(*enc_ctx)->refs = 3; //option
//设置输入YUV格式
(*enc_ctx)->pix_fmt = AV_PIX_FMT_YUV420P;
//设置码率
(*enc_ctx)->bit_rate = 600000; //600kbps
//设置帧率
(*enc_ctx)->time_base = (AVRational){1, 25}; //帧与帧之间的间隔是time_base
(*enc_ctx)->framerate = (AVRational){25, 1}; //帧率,每秒 25 帧
ret = avcodec_open2(*enc_ctx, codec, nullptr);
if (ret < 0) {
printf("Could not open codec :%d!\n", ret);
exit(1);
}
}
void rec_video()
{
AVFormatContext *fmt_ctx = nullptr;
AVPacket *pkt = nullptr;
int ret = 0;
int base = 0;
int count = 0;
AVCodecContext *enc_ctx = nullptr;
AVFrame *frame = nullptr;
unsigned char *y = nullptr;
unsigned char *u = nullptr;
unsigned char *v = nullptr;
//set log level
av_log_set_level(AV_LOG_DEBUG);
//create file
FILE *outfile = fopen("D:/Study/ffmpeg/av_base/video.yuv", "wb");
if (! outfile) {
printf("Error, Failed to open file!\n");
goto __ERROR;
}
//register audio device
avdevice_register_all();
//打开设备
fmt_ctx = open_dev();
if (! fmt_ctx) {
printf("Error, Failed to open device!\n");
goto __ERROR;
}
open_encoder(V_WIDTH, V_HEIGHT, &enc_ctx);
//创建 AVFrame
frame = creat_frame(V_WIDTH, V_HEIGHT);
//创建编码后输出的packet
pkt = av_packet_alloc();
if (! pkt) {
printf("Error, Failed to alloc avpacket!\n");
goto __ERROR;
}
//read data from device
while((ret = av_read_frame(fmt_ctx, pkt)) == 0) {
av_log(nullptr, AV_LOG_INFO,
"packet size is %d\n", pkt->size);
// fwrite(pkt->data, 1, static_cast<size_t>(pkt->size), outfile);
// fflush(outfile);
//YUYVYUYVYUYV windows录屏采用的是YUYV422
//YYYYYYYYUUVV YUV420P
y = &frame->data[0][0];
u = &frame->data[1][0];
v = &frame->data[2][0];
for (int i = 0; i < pkt->size; i += 4) {
*y++ = pkt->data[i];
*y++ = pkt->data[i + 2];
if ((i / (V_WIDTH * 2) % 2) == 0) {
*u++ = pkt->data[i + 1];
} else {
*v++ = pkt->data[i + 3];
}
}
// fwrite(frame->data[0], 1, V_WIDTH * V_HEIGHT, outfile);
// fwrite(frame->data[1], 1, V_WIDTH * V_HEIGHT / 4, outfile);
// fwrite(frame->data[2], 1, V_WIDTH * V_HEIGHT / 4, outfile);
// fflush(outfile);
// av_packet_unref(pkt);
frame->pts = base++;
encode(enc_ctx, frame, pkt, outfile);
if ( count++ >= 50)
break;
}
encode(enc_ctx, nullptr, pkt, outfile);
__ERROR:
//close device and release ctx
if (fmt_ctx) {
avformat_close_input(&fmt_ctx);
}
if (pkt) {
av_packet_free(&pkt);
}
//close file
if (outfile) {
fclose(outfile);
}
av_log(nullptr, AV_LOG_DEBUG, "finish!\n");
return;
}
int main()
{
rec_video();
return 0;
}