未编码的视频数据放在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);//记得释放空间
}