本文为像素数据(yuv)编码生成码流数据(h264)的代码实现。
目录
1、编码流程分析
/*编码流程分析:
* 1、注册
* 2、猜测使用什么样子的编码器
* 3、查找需要的编码器,是否存在
* 4、如果存在,打开编码器,进行编码
* 5、打开文件流,新建一个视频流,用来保存我们编码得到的码流数据
* 6、如何给视频流保存数据呢?
* 7、像素数据可以使用视频解码得到的像素数据
* 8、解码——得到yuv像素数据——然后进行编码
* 9、得到了码流数据
* 10、写入对象的文件中
* */
2、FFmpeg相关编码函数
av_register_all():注册所有组件
av_guess_format():已经注册的最合适的输出格式
avcodec_find_encoder():查找一个已经注册的音视频编码器
avcode_open2():打开编码码器
avformat_write_header():把流头信息写入到媒体文件中
av_read_frame():读取一帧压缩数据。
avcodec_send_frame():发送一帧像素数据
avcodec_receive_packet():接受一帧编码数据
av_packet_rescale_ts():时间基转换
av_write_frame():写一帧数据
flush_encoder():将最后一帧写入文件
av_write_trailer():把流尾信息写入文件
av_code_close():关闭流
3、创建一个类专门用来编码
.h文件
#ifndef FCODE_H
#define FCODE_H
#include <QObject>
extern "C"
{
#include <libavcodec/avcodec.h>
#include <libavdevice/avdevice.h>
#include <libavformat/avformat.h>
#include <libavutil/avconfig.h>
#include <libswscale/swscale.h>
#include <libswresample/swresample.h>
#include <libpostproc/postprocess.h>
}
class fCode
{
Q_OBJECT
public:
fCode();
void codecInit(int width, int height);//初始化
void codecFrame(AVFrame *frame);//编码
void writeEndFrame();//写入结束帧
private:
AVOutputFormat * avoutput_format;
AVFormatContext* avformat_context;//封装格式上下文结构体,也是统领全局的结构体,保存了视频文件封装格式相关信息
AVCodecContext* avcodec_context;//编码器上下文结构体,保存了视频(音频)编解码相关信息
AVCodec* avcodec;//每种视频(音频)编解码器(例如H.264解码器)对应一个该结构体
AVStream *newStream;//新建视频流
AVPacket *pkt;//保存一帧码流数据
int pkt_index;//表示帧的顺序
};
#endif // FCODE_H
构造函数
fCode::fCode()
{
//注册组件
av_register_all();
qDebug()<<"————注册组件————";
qDebug()<<"注册成功!";
}
初始化
void fCode::codecInit(int width, int height)
{
//width和height为每一帧图片的宽高
QDate date = QDateTime::currentDateTime().date();
QTime time = QTime::currentTime();
QString file_out = QString("%1_%2_%3_%4_%5.h264").arg(date.year()).arg(date.month()).
arg(date.day()).arg(time.hour()).arg(time.minute());
//猜测 file_out 为输出h264文件的文件名
avoutput_format = av_guess_format(nullptr, file_out.toStdString().c_str(), nullptr);
//判断有没有匹配到格式
if(avoutput_format == nullptr)
{
qDebug()<<"没有匹配到!";
return;
}
//打开编码器之前要做设置输出格式
//用来保存视频相关信息,有输入和输出的相关信息
avformat_context = avformat_alloc_context();
avformat_context->oformat = avoutput_format;
//打开视频流
/*
参数一:AVIOContent:输入输出上下文对象
参数二:文件流的路径
参数三:文件打开的方式,以写入的方式打开
@return >= 0 in case of success, a negative value corresponding to an
*/
int res = avio_open(&avformat_context->pb, file_out.toStdString().c_str(), AVIO_FLAG_WRITE);
if(res < 0)
{
qDebug()<<"avio_open error!";
return;
}
//新建视频流 参数一:保存视频信息的结构体
newStream = avformat_new_stream(avformat_context, nullptr);
if(newStream == nullptr)
{
qDebug()<<"新建视频流失败!";
return;
}
//设置编码器的相关参数
avcodec_context = newStream->codec;//保存视频流对应的AVCodecContext信息
//其他参数设置
avcodec_context->width = width;
avcodec_context->height = height;
//帧率设置
avcodec_context->time_base = {1, 25};//一秒25帧
//码率设置:每一秒存多少比特
avcodec_context->bit_rate = 400000;//4后面5个0
//设置显示的率
avcodec_context->framerate = {25, 1};
//每一组有几张图片 IPB帧
avcodec_context->gop_size = 10;
//量化参数设置(影响视频清晰度) 越小越清晰
avcodec_context->qmax = 51;
avcodec_context->qmin = 10;
//B帧为0
avcodec_context->max_b_frames = 0;
//编码格式设置
avcodec_context->pix_fmt = AV_PIX_FMT_YUV420P;
//流的设置
avcodec_context->codec_type = AVMEDIA_TYPE_VIDEO;
//编码器id的设置
avcodec_context->codec_id = avoutput_format->video_codec;
//查找对应的编码器,打开编码器 @return An encoder if one was found, NULL otherwise.
avcodec = avcodec_find_encoder(avcodec_context->codec_id);
if(avcodec == nullptr)
{
qDebug()<<"没有对应的编码器!";
return;
}
//打开编码器 @return zero on success, a negative value on error
res = avcodec_open2(avcodec_context, avcodec, nullptr);
if(res != 0)
{
qDebug()<<"打开编码器失败!";
return;
}
//进行编码,数据写入文件中
//写入编码的头部信息
res = avformat_write_header(avformat_context, nullptr);
if(res < 0)
{
qDebug()<<"文件头初始化失败!";
return;
}
pkt = av_packet_alloc();//初始化开空间
pkt_index = 0;//初始化为第0帧
qDebug()<<"初始化成功!";
}
开始编码
void fCode::codecFrame(AVFrame *frame)
{
qDebug()<<"————开始编码————";
//编码:发送像素数据到编码器上下文结构体中 @return 0 on success, otherwise negative error code:
int res = avcodec_send_frame(avcodec_context, frame);
if(res < 0)
{
qDebug()<<"发送像素数据失败!";
return;
}
//发送成功
while(res >= 0)
{
frame->pts = pkt_index;//显示时间基,保证视频播放顺序的准确性
pkt_index++;
res = avcodec_receive_packet(avcodec_context, pkt);
if(res < 0)//打包失败
{
qDebug()<<"打包失败!";
return;
}
pkt->stream_index = 0;//0表示视频流
//码流数据写入文件
av_interleaved_write_frame(avformat_context, pkt);
//清空
av_packet_unref(pkt);
}
}
写入尾巴帧
void fCode::writeEndFrame()
{
av_write_trailer(avformat_context);
qDebug()<<"写入尾帧成功!";
}
编码用到的yuv像素数据可以从视频解码那边获取,如下图所示的out_frame
循环读取每一帧并生成.h264、.yuv文件
4、如何获取yuv像素数据
方案一
在解码类内添加一个编码类对象的数据成员,在解码的构造进行一系列的初始化。
//假设解码类有个编码类对象的数据成员 fCode *code;
code = new fCode;
code.codecInit(int width, int height);//宽高自己设定
//然后在读取每一帧的时候将每一帧的yuv像素数据进行编码
code.codecFrame(out_frame);//yuv像素数据编码
//全部像素数据都编码完成以后,最后写入尾巴帧
code.writeEndFrame();//写入结束帧
方案二
采用信号与槽的方式,解码得到一帧yuv像素数据便发送一次信号,在另一个窗口新建槽,然后连接信号与槽。
解码类的.h添加如下信号
signals:
//将解码得到的一帧yuv像素数据进行发送
void sendFrame(AVFrame *frame);
解码一帧便发送一次信号
emit sendFrame(out_frame);
另一个窗口.h添加如下槽
public slots:
//接收信号发送过来的yuv像素数据
void receiveFrame(AVFrame *frame);
信号连接槽
//创建解码对象
decode = new fDecode;
//连接信号与槽
connect(decode, SIGNAL(sendFrame(AVFrame*)), this, SLOT(receiveFrame(AVFrame*)));
槽函数实现
void playWidget::receiveFrame(AVFrame *frame)
{
this->frame = frame;
code->codecFrame(frame);
}
记得写入尾巴帧。
一系列操作之后在当前目录下会生成.h264码流数据文件。