C++
FFMPEG编码:YUV转H264(AVFrame转AVPacket)
  • By刘立博
  • 2021-01-19 22:30:21
  • 4859人已阅读

程序流程设计

初始化编码器

初始化编码器需要使用以下3个函数:

 

(1)设置编码器 avcodec_find_encoder

需要传入编码器ID,如:AV_CODEC_ID_H264,返回编码器指针

(2)初始化、设置编码器上下文 avcodec_alloc_context3

需要传入编码器指针,获取上下文后,我们需要设置上下文的基础属性,如:宽度(width)、高度(height)、帧数(time_base)、像素格式(pix_fmt)和使用多少个线程进行编码(thread_count)

编码器上下文常用设置:

设置视频宽度为1280
context->width = 1280;

设置视频高度为720
context->height = 720;

设置时间单位为30分之1秒
context->time_base = { 1,30 };

设置像素格式为 YUV420P
context->pix_fmt = AV_PIX_FMT_YUV420P;

设置线程数为4
context->thread_count = 4;

禁用B帧
context->max_b_frames = 0;

设置平均比特率为400KB
context->bit_rate = 400000;

设置CQP恒定质量为18
av_opt_set_int(context->priv_data, "qp", 18,0);

设置恒定速率因子为18
av_opt_set_int(context->priv_data, "crf", 18, 0);

设置约束编码VBV为2M
context->rc_max_rate = 20000000;
context->rc_buffer_size = 20000000 * 2;

 

(3)编码器初始化 avcodec_open2

需要传入编码器指针和编码器上下文

初始化AV FRAME、AV PACKET

AV FRAME用于装载构造出来的YUV帧数据,而AV PACKET用于装载编码器转换后的H264帧数据。

需要使用以下3个函数:

 

(1)初始化AV FRAME空间 av_frame_alloc

同时我们需要设置AV FRAME的宽度(width)、高度(height)和像素格式(pix_fmt)

(2)为AV FRAME分配缓冲区 av_frame_get_buffer

需要传入AV FRAME指针和对齐方式

(3)初始化AV PACKET空间

AV PACKET用于存放转码后的帧数据

H264转码

转码需要使用以下3个函数:

 

(1)将源视频帧发送至编码器 avcodec_send_frame

该函数需要传入编码器上下文和AV FRAME

*frame : 需要将构造出的YUV帧数据放入AV FRAME的data属性,设置AV FRAME的时序(pts)

*avctx : 编码器上下文

(2)获取转换为H264格式的PACKET数据 avcodec_receive_packet

该函数需要传入编码器上下文和用于接收H264帧数据的AV PACKET指针。需要注意的是,每次传入AV PACKET指针后,该函数都会开辟一块新空间用于存放H264数据,所以使用完AV PACKET后,需要将引用计数器减1,避免内存溢出。

(3)AV PACKET引用计数器减1 av_packet_unref

使用完该帧的数据后,需要调用该函数将引用计数器减1

一个栗子:YUV转H264并写入文件

使用4个线程逐帧将YUV编码的AV FRAME帧转换为H264编码的AV PACKET帧同时写入至文件:output.h264

#include <iostream>
#include <fstream>
using namespace std;

extern "C"{
#include <libavcodec/avcodec.h>
}

#pragma comment(lib,"avcodec.lib")
#pragma comment(lib,"avutil.lib")

int main(int argc, char* argv[])
{
	//打开输出文件流
	ofstream outputFile;
	outputFile.open("output.h264", ios::binary);

	//设置编码器
	AVCodec* encoder = avcodec_find_encoder(AV_CODEC_ID_H264);

	//初始化并设置编码器上下文
	AVCodecContext*  context = avcodec_alloc_context3(encoder);
	context->width = 1280; 
	context->height = 720;
	context->time_base = { 1,30 };
	context->pix_fmt = AV_PIX_FMT_YUV420P;
	context->thread_count = 4;

	//编码器初始化
	avcodec_open2(context, encoder, NULL);

	//初始化并设置 AV FRAME
	AVFrame* frame = av_frame_alloc();
	frame->width = context->width;
	frame->height = context->height;
	frame->format = context->pix_fmt;

	//为AV FRAME分配缓冲区
	av_frame_get_buffer(frame, 0);

	//初始化AV PACKET
	AVPacket* packet = av_packet_alloc();

	//构造3000帧YUV数据并将其转换为H264
	for (int i = 0; i < 3000; i++)
	{
		//构造Y
		for (int y = 0; y < frame->height; y++)
		{
			for (int x = 0; x < frame->width; x++)
			{
				frame->data[0][y * frame->linesize[0] + x] = x + y + i * 3;
			}
		}
		//构造UV
		for (int y = 0; y < frame->height / 2; y++)
		{
			for (int x = 0; x < frame->width / 2; x++)
			{
				frame->data[1][y * frame->linesize[1] + x] = 128 + y + i * 2;
				frame->data[2][y * frame->linesize[2] + x] = 64 + x + i * 5;
			}
		}
		frame->pts = i;


		//将生成出来的YUV帧发送至编码器
		int sendResult = avcodec_send_frame(context, frame);

		while (sendResult >= 0)
		{
			//获取转为H264的packet数据
			int receiveResult = avcodec_receive_packet(context, packet);

			//如果暂无处理完成的packet数据,则继续发送其他YUV帧
			if (receiveResult == AVERROR(EAGAIN) || receiveResult == AVERROR_EOF) break;
			
			outputFile.write((char*)packet->data, packet->size);
			//释放packet中的数据
			av_packet_unref(packet);
		}
		
	}

	//释放
	outputFile.close();
	av_packet_free(&packet);
	av_frame_free(&frame);
	avcodec_free_context(&context);
}

运行结果如下: