使用libx264原生api做264编码
引入x264的库
关于可以的编译,可以翻看我的博客:x264编译
在visual studio 里面引入:
- 指定头文件
- 指定lib库
这个图里面,还有ffmpeg,这个ffmpeg是集成了x264的。所以说,这两个是可以同时存在的
编写编码器封装类
创建头文件 `VideoEncoderX.h'
#include <stdint.h>
#include "x264.h"
#include "VideoEncode.h"
#define WRITE_CAPTURE_264
class VideoEncoderX : public VideoEncoder
{
public:
VideoEncoderX();
int InitEncode(int width, int height, int fps, int bitrate, const char* profile)override;
unsigned int Encode(unsigned char* src_buf, unsigned char* dst_buf)override;
int StopEncode()override;
private:
x264_param_t m_tX264Param;
x264_t* m_pEncoder;
x264_picture_t m_tPicIn;
bool m_pIsSupportSlice;
bool m_bForceKeyFrame;
long m_bLastBitRateTime;
#ifdef WRITE_CAPTURE_264
FILE* h264_out_file = nullptr;
#endif // WRITE_CAPTURE_YUV
};
VideoEncoder是抽象了几个override的方法,没什么大用,不详述
- #include "x264.h" 是要用到的x264的头文件
- x264_param_t 是参数描述编码参数
- x264_picture_t 是用来存储待编码的源数据
- m_pIsSupportSlice是用来控制,编码一帧数据使用几个nalu
- m_bForceKeyFrame 指定强制编码I帧
编码器的初始化方法
int InitEncode(int width, int height, int fps, int bitrate, const char* profile) override;
方法指定了宽,高,编码帧率,码率,编码模式几个参数。下面是详细实现:
int VideoEncoderX::InitEncode(int width, int height, int fps, int bitrate, const char* profile)
{
if (width <= 0 || height <= 0)
return -1;
int nRet = -1;
char strProfileName[16] = { 0 };
uint8_t u8Level_idc = 0;
ParseProfileLevelId(profile, strProfileName, &u8Level_idc);
nRet = x264_param_default_preset(&m_tX264Param, "ultrafast", "zerolatency");
if (nRet != 0)
{
return -1;
}
if (m_pIsSupportSlice)
{
m_tX264Param.i_threads = 4;
m_tX264Param.b_sliced_threads = 1;
m_tX264Param.rc.i_lookahead = 0;
/*************************/
//m_tX264Param.i_threads = 0;
//m_tX264Param.b_sliced_threads = 1;
//m_tX264Param.rc.i_lookahead = 0;
}
else
{
//直播的播放器,一般不止吃slice的编码
m_tX264Param.i_threads = 1;
m_tX264Param.rc.i_lookahead = 0;
}
//m_tX264Param.i_log_level = X264_LOG_DEBUG;
m_tX264Param.i_csp = X264_CSP_I420;
m_tX264Param.i_width = width;
m_tX264Param.i_height = height;
m_tX264Param.i_bframe = 0;
m_tX264Param.i_fps_num = fps;
m_tX264Param.i_fps_den = 1;
m_tX264Param.i_keyint_min = fps * 2;
m_tX264Param.i_keyint_max = fps * 2;
m_tX264Param.b_repeat_headers = 1; // 重复SPS/PPS 放到关键帧前面
m_tX264Param.i_level_idc = u8Level_idc;
// x264动态码率调整只能是在CBR下
int type = 2;
switch (type)
{
case 0:
// CQP 恒定qp,最简单的码率控制方式,每帧图像都按照一个特定的QP来编码,每帧编码后的数据量有多大是未知的。
m_tX264Param.rc.i_rc_method = X264_RC_CQP;
/**
参数qp_constant设置的是P帧的QP。I, B帧的QP根据f_ip_factor, f_pb_factor,计算得到。
**/
m_tX264Param.rc.i_qp_constant = 15;
m_tX264Param.rc.f_ip_factor = 0.1; //值越小,I帧的qp值越大,I帧的大小越小
break;
case 1:
/**********CRF 恒定质量因子***************/
m_tX264Param.rc.i_rc_method = X264_RC_CRF;
m_tX264Param.rc.i_bitrate = bitrate / 1000;
m_tX264Param.b_annexb = 1;
m_tX264Param.rc.i_qp_constant = 15;
//m_tX264Param.rc.i_qp_min = 15;
//m_tX264Param.rc.i_qp_max = 20;
break;
case 2:
/**********CBR 恒定码率***************/
m_tX264Param.rc.i_rc_method = X264_RC_ABR;
m_tX264Param.b_annexb = 1;
m_tX264Param.rc.i_qp_min = 15;
m_tX264Param.rc.i_qp_max = 25;
m_tX264Param.rc.i_bitrate = bitrate / 1000;
m_tX264Param.rc.i_vbv_max_bitrate = bitrate / 1000; //ABR码控模式下,瞬时峰值码率,单位kbps,该值与i_bitrate相等,就是CBR恒定码控模式。
m_tX264Param.rc.i_vbv_buffer_size = bitrate / 1000;
m_tX264Param.rc.f_rate_tolerance = 10; //ABR码控模式下,瞬时码率可以偏离的倍数
m_tX264Param.rc.b_filler = 0; //CBR模式下,码率不够,强行添加填充位,凑码率。
break;
case 3:
/**********ABR 平均码率***************/
m_tX264Param.rc.i_rc_method = X264_RC_ABR;
m_tX264Param.b_vfr_input = 0; //这时用fps而不是timebase, timestamps来计算帧间距离
m_tX264Param.b_annexb = 1;
/************************************************************************/
/* X264_AQ_NONE:不开启AQ模式,帧内宏块全部使用同一QP或者固定的QP表。
X264_AQ_VARIANCE:使用方差动态计算每个宏块的QP。
X264_AQ_AUTOVARIANCE:方差自适应模式,会先遍历一次全部宏块,统计出一些中间参数,之后利用这些参数,对每个宏块计算QP。
X264_AQ_AUTOVARIANCE_BIASED:偏移方差自适应模式,在该模式下BiasStrength即为原始的Strength值。最终每个宏块的 QP*/
/************************************************************************/
//m_tX264Param.rc.i_aq_mode = X264_AQ_VARIANCE;//自适应量化参数。
m_tX264Param.rc.i_qp_min = 15;
m_tX264Param.rc.i_qp_max = 25;
m_tX264Param.rc.i_bitrate = bitrate / 1000;
m_tX264Param.rc.i_vbv_max_bitrate = bitrate * 1.2 / 1000; //ABR码控模式下,瞬时峰值码率,单位kbps,该值与i_bitrate相等,就是CBR恒定码控模式。
m_tX264Param.rc.i_vbv_buffer_size = bitrate / 1000 * 2; //码率控制缓冲区的大小,单位kbit,电影电视剧,场景建议配置3倍i_vbv_max_bitrate。
m_tX264Param.rc.f_rate_tolerance = 1.5; //ABR码控模式下,瞬时码率可以偏离的倍数
//m_tX264Param.rc.f_ip_factor = 0.1; //值越小,I帧的qp值约大,I帧的大小越小
break;
}
nRet = x264_param_apply_profile(&m_tX264Param, strProfileName);
if (nRet != 0)
{
return -1;
}
x264_picture_init(&m_tPicIn);
m_tPicIn.img.i_csp = m_tX264Param.i_csp;
m_tPicIn.img.i_plane = 3;
m_tPicIn.img.i_stride[0] = m_tX264Param.i_width;
m_tPicIn.img.i_stride[1] = m_tX264Param.i_width / 2;
m_tPicIn.img.i_stride[2] = m_tX264Param.i_width / 2;
//nRet = x264_picture_alloc(&m_tPicIn, m_tX264Param.i_csp, m_tX264Param.i_width, m_tX264Param.i_height);
//if(nRet != 0)
//{
// printf("pic alloc failed errno: %d\n", nRet);
// return -1;
//}
m_pEncoder = x264_encoder_open(&m_tX264Param);
if (!m_pEncoder)
{
x264_encoder_close(m_pEncoder);
x264_picture_clean(&m_tPicIn);
m_pEncoder = NULL;
return -1;
}
#ifdef WRITE_CAPTURE_264
if (!h264_out_file) {
h264_out_file = fopen("ouput.264", "wb");
if (h264_out_file == nullptr)
{
}
}
#endif // WRITE_CAPTURE_264
return 0;
}
-
ParseProfileLevelId 是子写的方法。里面将sdp信息里面常用的描述编码器信息解析出来。比如“42801f”这个字符串,每两位代表一个信息。参考信息,前两位代表比如"main,base,hight"之类的。中间两位是 profile-iop,后面两位是编码的level。具体可以参考文档。
-
x264_param_default_preset 是设置默认参数。preset 是编码效率有关。tune 也是和编码效率优化有关,但是它更加关注视频内容,从内容角度优化
-
下面是编码器的几个参数设置,这里解释一下这个几个参数的作用,这是别人解释的,比较详细好理解。
i_threads有三种设置方式。
1. i_threads = 1;
明确告诉编码器,不使用并行编码。zerolatency场景下设置param.rc.i_lookahead=0; 那么编码器来一帧编码一帧,无并行、无延时。如果没有设置i_lookahead=0,编码器会延时40帧(程序缺省值),再开始编码,这是为了做码率控制而统计帧信息。
2. i_threads = N; (N>1)
明确告诉编码器,使用N个并行单元编码。如果param.b_sliced_threads=1那么一帧图像被划分为N个slice(N的取值也有限制,不能大于图像宏块行数),进行slice并行。如果param.b_sliced_threads=0进行[frame](https://so.csdn.net/so/search?q=frame&spm=1001.2101.3001.7020)并行。编码器会延时max(N, param.rc.i_lookahead)帧。zerolatency场景下场景下,param.b_sliced_threads=1; param.rc.i_lookahead=0; N个slice并行、无延时。注意有些厂家的解码器不支持多slice码流,这时不能进行这种设置,而只能采用设置1。
3. i_threads = 0; 或者不设置
不配置并行单元数,由程序根据当前CPU性能决定N值。决定N值后的流程和设置2是一样的。
一般slice并行N值小于frame并行的N值
比较绕,自己单独研究
-
m_tX264Param.i_csp = X264_CSP_I420; 指定了源数据是yuv420
-
m_tX264Param.i_bframe 指定了是否启用B帧
-
m_tX264Param.b_repeat_headers // 重复SPS/PPS 放到关键帧前面
-
接下来的switch设计到几种编码形式,比如恒定qt,恒定质量,恒定码率。平均码率。这里比较复杂,也是我们在编码的时候,调优的主要内容
-
x264_param_apply_profile 应用了这些配置
-
x264_picture_init 初始话了一下,用于存储源yuv的结构体
-
x264_encoder_open 打开解码器
-
最后加一个用于测试写h264文件的
编码方法
unsigned int VideoEncoderX::Encode(unsigned char* src_buf, unsigned char* dst_buf)
{
if (!src_buf || !m_pEncoder)
{
return -1;
}
unsigned int nRet = 0;
x264_nal_t* nal;
int i_nal = 0;
x264_picture_t tPicOut;
int y_size = m_tX264Param.i_width * m_tX264Param.i_height;
m_tPicIn.img.plane[0] = src_buf;
m_tPicIn.img.plane[1] = src_buf + y_size;
m_tPicIn.img.plane[2] = src_buf + y_size * 5 / 4;
if (m_bForceKeyFrame)
{
m_tPicIn.i_type = X264_TYPE_KEYFRAME;
}
else
{
m_tPicIn.i_type = X264_TYPE_AUTO;
}
m_bForceKeyFrame = false;
int i_frame_size = x264_encoder_encode(m_pEncoder, &nal, &i_nal, &m_tPicIn, &tPicOut);
if (i_frame_size)
{
for (int i = 0; i < i_nal; i++)
{
if (dst_buf) {
memcpy(dst_buf + nRet, nal[i].p_payload, nal[i].i_payload);
}
//memcpy(dst_buf, nal[i].p_payload, nal[i].i_payload);
//dst_buf += nal[i].i_payload;
#ifdef WRITE_CAPTURE_264
fwrite(nal[i].p_payload, 1, nal[i].i_payload, h264_out_file);
#endif // WRITE_CAPTURE_264
nRet += nal[i].i_payload;
}
}
if (tPicOut.b_keyframe)
{
//LOGGER_DEBUG << "keyframe:" << tPicOut.b_keyframe << " type: " << tPicOut.i_type << " frame size: " << nRet << " byte";
}
return nRet;
}
- m_tPicIn 根据宽高,指定了平面的指针位置。也就是y,u,v指针的位置,算是alloc吧
- 根据外部参数m_bForceKeyFrame,如果要求强制I帧编码,那么指定 i_type 为X264_TYPE_KEYFRAME。那么这张yuv图。就强制编码成为I帧
- x264_encoder_encode 编码数据的方法
- m_pEncoder 要编初始化的编码器
- nal 用来存放解码后的数据,nal片列表
- i_nal 是nalu slice的个数
- m_tPicIn 编码源数据
- tPicOut 编码后的数据,只用于取信息。
- 然后就是遍历nalu结构,取出里面的编码数据。写文件,或者做其他用
使用ffmeg方式编码264数据
引入带x264的ffmpeg库
关于可以的编译,可以翻看我的博客:x264编译
加入头文件
加入lib库目录
加入lib库
把dll文件拷贝到exe目录
如果运行包少dll,那么你就去mingw64里面去拷贝所有的dll 放在这个目录
创建 VideoEncodeFF.h
#pragma once
#include "VideoEncode.h"
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswresample/swresample.h>
#include <libavutil/avutil.h>
#include <libavutil/opt.h>
}
#define WRITE_CAPTURE_264
class VideoEncodeFF : public VideoEncoder
{
public:
int InitEncode(int width, int height, int fps, int bitrate, const char* profile)override;
unsigned int Encode(unsigned char* src_buf, unsigned char* dst_buf)override;
int StopEncode()override;
private:
AVPacket* pkt;
AVFrame* frame;
AVCodecContext* videoCodecCtx;
#ifdef WRITE_CAPTURE_264
FILE* h264_out_file = nullptr;
#endif // WRITE_CAPTURE_YUV
uint64_t pts = 0;
};
还是根据 VideoEncoder 抽象出来的。但是里面几个类属性不一样了
- AVPacket: 编码出来的包
- AVFrame 编码前的数据帧
- AVCodecContext 编码器上下文
初始化编码器
int VideoEncodeFF::InitEncode(int width, int height, int fps, int bitrate, const char* profile)
{
if (width <= 0 || height <= 0)
return -1;
av_register_all();
AVCodec* codec = NULL;
while ((codec = av_codec_next(codec))) {
if (codec->encode2 && codec->type == AVMediaType::AVMEDIA_TYPE_VIDEO) {
qDebug() << "Codec Name :" << codec->name;
qDebug() << "Type: " << av_get_media_type_string(codec->type);
qDebug() << "Description: " << (codec->long_name ? codec->long_name : codec->name);
qDebug() << "---";
}
}
//codec = avcodec_find_encoder_by_name("libx264");
codec = avcodec_find_encoder(AVCodecID::AV_CODEC_ID_H264);
if (codec == NULL) {
//qDebug() << "cannot find video codec id: " << codec->id;
return -1;
}
qDebug() << "codec long name: " << codec->long_name;
videoCodecCtx = avcodec_alloc_context3(codec);
if (videoCodecCtx == nullptr)
{
qDebug() << "alloc context3 afild: " << codec->long_name;
return -1;
}
pkt = av_packet_alloc();
if (!pkt)
exit(1);
// 设置编码参数
videoCodecCtx->bit_rate = bitrate;
videoCodecCtx->width = width;
videoCodecCtx->height = height;
videoCodecCtx->time_base = { 1,fps };
videoCodecCtx->framerate = { fps,1 };
videoCodecCtx->gop_size = 10;
videoCodecCtx->max_b_frames = 0;
videoCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;
std::string strProfile = profile;
if (strProfile == "main")
{
videoCodecCtx->profile = FF_PROFILE_H264_MAIN;
}
else if (strProfile == "base")
{
videoCodecCtx->profile = FF_PROFILE_H264_BASELINE;
}
else
{
videoCodecCtx->profile = FF_PROFILE_H264_HIGH;
}
int ret = av_opt_set(videoCodecCtx->priv_data, "preset", "ultrafast", 0);
if (ret != 0)
{
qDebug() << "set opt preset error";
}
ret = av_opt_set(videoCodecCtx->priv_data,"tune","zerolatency", 0);
if (ret != 0)
{
qDebug() << "set opt tune error";
}
//x264_param_t* x264_param = (x264_param_t*)videoCodecCtx->priv_data;
//x264_param_default_preset(x264_param, "ultrafast", "zerolatency");
//x264_param_apply_profile(x264_param, "high");
// 打开编码器
ret = avcodec_open2(videoCodecCtx, codec, NULL);
if (ret < 0) {
qDebug() << "avcodec_open2 filed ";
exit(1);
}
frame = av_frame_alloc();
if (!frame) {
fprintf(stderr, "Could not allocate video frame\n");
exit(1);
}
frame->format = videoCodecCtx->pix_fmt;
frame->width = videoCodecCtx->width;
frame->height = videoCodecCtx->height;
ret = av_frame_get_buffer(frame, 32);
if (ret < 0) {
fprintf(stderr, "Could not allocate the video frame data\n");
exit(1);
}
#ifdef WRITE_CAPTURE_264
if (!h264_out_file) {
h264_out_file = fopen("ouput.264", "wb");
if (h264_out_file == nullptr)
{
qDebug() << "Open 264 file failed";
}
}
#endif // WRITE_CAPTURE_264
return 0;
}
- avcodec_find_encoder,avcodec_find_encoder_by_name,一个更具id,一个根据编码器名称找到编码器
- avcodec_alloc_context3 给编码器分配上下文空间
- av_packet_alloc 分配 AVPacket的空间
- 接下来就是设置编码器参数,要简单很多了。
// 设置编码参数
videoCodecCtx->bit_rate = bitrate; //码率
videoCodecCtx->width = width; // 宽
videoCodecCtx->height = height; //高
videoCodecCtx->time_base = { 1,fps }; //时间基数,用于计算pts,dts
videoCodecCtx->framerate = { fps,1 }; // 帧率
videoCodecCtx->gop_size = 10; //gop的时间长度
videoCodecCtx->max_b_frames = 0; //b帧,0:就是不开启b帧
videoCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P; // 编码的源数据,yuv420格式的
-
videoCodecCtx->profile 设置编码的格式,当然和x264源码编译一样,也有level设置
-
私有参数设置,比如下面两个设置就是libx264专有的
int ret = av_opt_set(videoCodecCtx->priv_data, "preset", "ultrafast", 0);
if (ret != 0)
{
qDebug() << "set opt preset error";
}
ret = av_opt_set(videoCodecCtx->priv_data,"tune","zerolatency", 0);
if (ret != 0)
{
qDebug() << "set opt tune error";
}
- avcodec_open2 打开编码器
- av_frame_alloc 分配AVFrame指针
- av_frame_get_buffer 分配AVFrame里面内容的空间 align参数是内存对齐方式,设置0,就是使用当前cpu默认的
编码方法
unsigned int VideoEncodeFF::Encode(unsigned char* src_buf, unsigned char* dst_buf)
{
int ylen = frame->linesize[0] * videoCodecCtx->coded_height;
int ulen = frame->linesize[1] * videoCodecCtx->coded_height /2 ;
int vlen = frame->linesize[2] * videoCodecCtx->coded_height / 2;
memcpy(frame->data[0], src_buf, ylen);
memcpy(frame->data[1], src_buf + ylen, ulen);
memcpy(frame->data[2], src_buf + ylen+ulen, vlen);
frame->pts = pts;
pts++;
int ret = avcodec_send_frame(videoCodecCtx, frame);
if (ret < 0) {
fprintf(stderr, "Error sending a frame for encoding\n");
exit(1);
}
while (ret >= 0) {
ret = avcodec_receive_packet(videoCodecCtx, pkt);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
return 1;
else if (ret < 0) {
fprintf(stderr, "Error during encoding\n");
exit(1);
}
#ifdef WRITE_CAPTURE_264
fwrite(pkt->data, 1, pkt->size, h264_out_file);
#endif // WRITE_CAPTURE_264
if (dst_buf != nullptr)
{
memcpy(pkt->data, dst_buf, pkt->size);
}
av_packet_unref(pkt);
}
return 0;
}
- 首先把数据拷贝到AVFrame里面
- 然后pts计算一下
- avcodec_send_frame avcodec_receive_packet,就是解码过程了
- 然后就可以拷贝编码后的数据了
总结
两种编码方式,都可以实现编码,源码方式比较直接,可以控制的参数相对较多。ffmpeg的api比较通用,写起来简单一下。
7. 其他:
-
仓库: git主页
-
讲解视频地址:
-
联系我:
- 邮箱: gu19860621@163.com
- 微信: p13071210551