FFmpeg视频编码步骤

未编码的视频数据放在AVFrame中, 编码的视频数据放在AVPacket
本文不介绍源码级的代码, 如何使用ffmpeg的API将AVFrame转为AVPacket并保存在文件中. 以H264编码格式为例子

假定有一个400*300尺寸的YUV420P格式的input.yuv文件,需要编码成h264
AVFrame跟AVPacket的数据都是需要资源释放的, 不然会内存泄露, 特别是AVPacket数据空间小, 不容易发现
废话不多说直接上代码

#include <iostream>
#include <fstream>
#include <list>
using namespace std;
extern "C"{
#include <libavcodec/avcodec.h>
#include <libavutil/opt.h>
}
#define PIX_WIDTH 400
#define PIX_HEIGHT 300
#define BITRATE 400000
void printErr(int errID)//ffmpeg的错误信息如何打印
{
	char buf[1024] = { 0 };
	av_strerror(errID, buf, sizeof(buf) - 1);
	cerr << buf << endl;
}
int main(){
	ifstream ifs;
	ifs.open("input.yuv", ios::binary);
	if(!ifs) {
		cout << "input.yuv open failed! " << endl;
		return 0;
	}
	ofstream ofs; 
	ofs.open("output.h264", ios::binary);
	if(!ofs) {
		cout << "output.h264 open failed! " << endl;
		return 0;
	}
/**********AVFrame的初始化*********/	
//1,申请AVFrame空间
	AVFrame *frame = av_frame_alloc();
	if(!frame){
		cout << "frame_alloc failed!" << endl;
		return -1;
	}
//2,设定视频帧的信息
	frame->width = PIX_WIDTH ;
	frame->height = PIX_HEIGHT ;
	frame->format = AV_PIX_FMT_YUV420P;
//3,设置好linesize, 编码格式不一样linesize也会不同
	frame->linesize[0] = PIX_WIDTH ;	   //Y
	frame->linesize[1] = PIX_WIDTH / 2;  //U
	frame->linesize[2] = PIX_WIDTH / 2;  //V
//4,生成AVFrame的数据空间, 使用默认对齐, 默认对齐是32字节对齐
	int ret = av_frame_get_buffer(frame, 0);
	if(ret != 0){//生成失败
		printErr(ret);
		av_frame_free(&frame); //记得释放空间
		return -2;
	}
/**********以上便完成了AVFrame的初始化*********/

/**********编码器的初始化*****************/
//1,找到所需要编码器
	AVCodec *avcodec = avcodec_find_encoder(AV_CODEC_ID_H264);
	if(!avcodec){
		cerr << "avcodec_find_encoder failed!" << endl;
		return -3;
	}
//2,申请编码器上下文 需要手动释放的
	AVCodecContext *c = avcodec_alloc_context3(avcodec);
	if(!c){
		cerr << "avcodec_alloc_context3 failed!" << endl;
		avcodec_free_context(&c);
	}
//3,设定上下文的参数
	c->width = PIX_WIDTH ;
	c->height = PIX_HEIGHT ;
	//帧时间戳的时间单位
	c->time_base = { 1, 25 };
	//时间基数 1/25 很重要,跟帧率有关, 每一帧在什么时间播放都有关系
	//这里的意思就是将一个单位时间设置为 1/25秒
	//在编码时 frame->pts会被赋值, frame->pts * time_base = 播放时间点(秒) 
	c->pix_fmt = AV_PIX_FMT_YUV420P;
	c->thread_count = 16;
	//设置编码的线程数, 可以通过调用系统接口获取CPU核心数
//4,设置所需要的编码编码模式, 这里先记录几种编码模式,用的话是用 恒定速率因子 (CRF)编码
	/***ABR平均速率编码***/
	//c->bit_rate = BITRATE ;
	/***CQP恒定质量编码***/
	//av_opt_set_int(c->priv_data, "qp", 18, 0);//函数内部实现的是赋值
	/***恒定比特率***/
	//c->rc_min_rate = BITRATE ;
	//c->rc_max_rate = BITRATE ;
	//c->rc_buffer_size = BITRATE  * 2;
	//c->bit_rate = BITRATE ;
	//av_opt_set(c->priv_data, "nal-hrd", "cbr", 0);
	/***CRF恒定速率因子 最常用的模式***/
	ret = av_opt_set_int(c->priv_data, "crf", 23, 0);
	if(ret != 0){
		cout << "crf failed!" << endl;
	}
	c->rc_max_rate = BITRATE ; //约束编码 VBV
	c->rc_buffer_size = BITRATE *2;
	/****预设编码器参数***可选设置*/
	//c->max_b_frames = 0; //设置每个GOP中b帧的最大数量
	//ret = av_opt_set(c->priv_data, "preset", "ultrafast", 0);//设置编码速率为最快
	//if(ret !=0){
	//	cout << "preset failed" << endl;
	//}
	//ret = av_opt_set(c->priv_data, "tune", "zerolatency", 0);//设置为0延迟, 也就是没有B帧
	//if(ret != 0){
	//	cout << "tune failed" << endl;
	//}

//5, 打开编码上下文
	ret = avcodec_open2(c, avcodec, nullptr);
	if(ret != 0){
		printErr(ret);
		return -4;
	}
//6, 申请AVPacket 也需要手动释放的 每一帧的编码出来的AVPacket都不一样大
	AVPacket* avpacket = av_packet_alloc();
	if(!avpacket){
		cout << "av_packet_alloc failed!" << endl;
		av_packet_free(&avpacket);
		return -5;
	}

/**********以上便完成了编码器的初始化*****************/

//循环读取input.yuv的数据 进行编码
	for(int i=0; ; ++i){
//1, 读取视频原始帧
		ifs.read((char*)frame->data[0], frame->linesize[0]*PIX_HEIGHT );//Y值
		ifs.read((char*)frame->data[1], frame->linesize[1]*PIX_HEIGHT /2);//U值
		ifs.read((char*)frame->data[2], frame->linesize[2]*PIX_HEIGHT /2);//V值
//2, 设置frame的pts
		frame->pts = i;
//3, 将frame发送到编码线程中进行编码
		ret = avcodec_send_frame(c, frame);
		if(ret !=0){//出错
			break;//退出循环,记得释放空间
		}
//4, 接收压缩帧数据, 一半前几次调用是返回空, 因为编码还未完成
		while(ret >= 0){
		//每次调用都会重新分配avpacket中的空间
			ret = avcodec_receive_packet(c, avpacket);
			if(ret == AVERROR(EAGAIN) || ret == AVERROR_EOF){
				//这是正常的,在还没编码完成时会进入AVERROR(EAGAIN) 
				//在编码结束时会进入AVERROR_EOF
				break;//直接进行下一帧发送
			}else if(ret < 0){//失败
				printErr(ret);
				return -6;
			} 
			//接受编码成功
			ofs.write((char*)avpacket->data, avpacket->size);
			//引用计数减一 不能少否则内存泄露
			av_packet_unref(avpacket);
		}
		if(ifs.eof()){//读到文件的末尾了
			break;
		}
	}
//5,在文件读完,仍然有一些数据还在编码中, 此时要取出这些数据避免丢帧
	list<AVPacket*> list;
	ret = avcodec_send_frame(c, nullptr);//发送nullptr获取缓冲
	while(ret >=0){
		AVPacket *pkt = av_packet_alloc();
		ret = avcodec_receive_packet(c, pkt);
		if(ret != 0){//出错了
			printErr(ret);
			av_packet_free(&pkt);
			break;
		}
		list.push_back(pkt);
	}
	for(auto i : list){
		ofs.write((char*)i->data, i->size);
		av_packet_free(&i);
	}

	ifs.close();
	ofs.close();
	av_frame_free(&frame); //记得释放空间
	avcodec_free_context(&c); //记得释放空间
	av_packet_free(&avpacket);//记得释放空间
}
上一篇:Qt 图片旋转


下一篇:Qt-双缓冲机制