#x264 的两种264编码使用方式

1,154 阅读11分钟

使用libx264原生api做264编码

引入x264的库

关于可以的编译,可以翻看我的博客:x264编译

在visual studio 里面引入:

  • 指定头文件 image.png
  • 指定lib库

image.png

image.png

这个图里面,还有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编译

加入头文件

image.png

加入lib库目录

image.png

加入lib库

image.png

把dll文件拷贝到exe目录

image.png

如果运行包少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. 其他: