上一篇文章总结了ffmpeg解码,本文继续总结一下编码。
一:编码流程:
ffmpeg函数:
avformat_alloc_output_context2():初始化输出码流的AVFormatContext。
avio_open():打开输出文件。
av_new_stream():创建输出码流的AVStream。
avcodec_find_encoder():查找编码器。
avcodec_open2():打开编码器。
avformat_write_header():写文件头。
avcodec_send_frame() + avcodec_receive_packet():编码一帧视频。即将AVFrame编码为AVPacket(因为ffmpeg的版本更新原因,雷神用的函数是avcodec_encode_video2(),需要进行替换)。
av_write_frame():将编码后的视频码流写入文件。
flush_encoder():输入的像素数据读取完成后调用此函数。用于输出编码器中剩余的AVPacket。
av_write_trailer():写文件尾。
原文链接:https://blog.csdn.net/leixiaohua1020/article/details/39770947
雷霄骅相关文章: https://blog.csdn.net/leixiaohua1020/article/details/39770947
编码部分的主要代码:
因为软件是基于CS模式开发的,客户端这边收到的是服务器转发过来的包含高空抛物信息的视频流,在解码不分已经将数据转成带有抛物轨迹的qt图片了(类型是QImage),所以在保存时需要首先将QImage转成AVFrame类型。
代码:
//初始化
void SaveImage::initRecordParas()
{
m_pFormatCtxOut = avformat_alloc_context();
AVOutputFormat* pOutFormat = av_guess_format(NULL, m_szURL.toStdString().data(), NULL);
pOutFormat->video_codec = AV_CODEC_ID_H264;
m_pFormatCtxOut->oformat = pOutFormat;
avformat_alloc_output_context2(&m_pFormatCtxOut, pOutFormat, NULL, m_szURL.toStdString().data());
//Open output URL
if (avio_open(&m_pFormatCtxOut->pb, m_szURL.toStdString().data(), AVIO_FLAG_READ_WRITE) < 0)
{
printf("Failed to open output file! \n");
return;
}
AVCodec* pCodec = avcodec_find_encoder(pOutFormat->video_codec);
if (!pCodec)
{
printf("Can not find encoder! \n");
return;
}
m_pVideoStream = avformat_new_stream(m_pFormatCtxOut, pCodec);
m_pCodecCtxOut = avcodec_alloc_context3(pCodec);
m_pCodecCtxOut->codec_id = pOutFormat->video_codec;
m_pCodecCtxOut->pix_fmt = AV_PIX_FMT_YUV420P;
m_pCodecCtxOut->width = 1920;//m_nWidth;//1920
m_pCodecCtxOut->height = 1080;// m_nHeight;//1080
m_pCodecCtxOut->bit_rate = 1024 * 12000;// 1024 * 8 * 1100;//1100000; //note: it's bit rate, not byte rate
m_pCodecCtxOut->gop_size = 12;
m_pCodecCtxOut->time_base.num = 1;
m_pCodecCtxOut->time_base.den = 25;//5
m_pCodecCtxOut->qmin = 0;
m_pCodecCtxOut->qmax = 51;
m_pCodecCtxOut->max_b_frames = 1;//3;
AVDictionary* param = 0;
//H.264
if (m_pCodecCtxOut->codec_id == AV_CODEC_ID_H264)
{
av_dict_set(¶m, "preset", "superfast", 0);//superfast
av_dict_set(¶m, "tune", "zerolatency", 0);
}
if (avcodec_open2(m_pCodecCtxOut, pCodec, ¶m) < 0)
{
printf("Failed to open encoder! \n");
return;
}
avcodec_parameters_from_context(m_pVideoStream->codecpar, m_pCodecCtxOut);
m_avFrameOutput = av_frame_alloc();
int picture_size = av_image_get_buffer_size(m_pCodecCtxOut->pix_fmt, m_pCodecCtxOut->width, m_pCodecCtxOut->height, 1);
uint8_t* picture_buf = (uint8_t*)av_malloc(picture_size);
av_image_fill_arrays(m_avFrameOutput->data, m_avFrameOutput->linesize, picture_buf, m_pCodecCtxOut->pix_fmt, m_pCodecCtxOut->width, m_pCodecCtxOut->height, 1);
m_pPacketOut = (AVPacket*)malloc(sizeof(AVPacket));
av_new_packet(m_pPacketOut, picture_size);
int nBytes = av_image_get_buffer_size(AV_PIX_FMT_YUV420P, m_nWidth, m_nHeight, 1);
m_pYUVBuffer = (uint8_t*)av_malloc(nBytes);
}
//存储开始、结束控制
void SaveImage::startFlag()
{
m_pPacketOut = NULL;
m_avFrameOutput = NULL;
m_pVideoStream = NULL;
QString szOutUrl = "C:\\system\\recordVideo\\";
QDateTime dateTime = QDateTime::currentDateTime();
QString szTime = dateTime.toString("yyyy-MM-dd_HH_mm_ss");
m_szURL = szOutUrl + szTime + ".mp4";
initRecordParas();
m_bStartRecord = true;
m_bStopRecord = false;
writeHead();
}
void SaveImage::stopFlag()
{
m_bStartRecord = false;
m_bStopRecord = true;
}
//收到解码后的QImage并转成AVFrame
void SaveImage::recvImage(const QImage& image)
{
if (m_bStartRecord)
{
int nWidth = image.width();
int nHeight = image.height();
AVFrame* pFrameRGB = av_frame_alloc();
int numBytes1 = av_image_get_buffer_size(AV_PIX_FMT_RGB32, nWidth, nHeight, 1);
uint8_t* buffer1 = (uint8_t*)av_malloc(numBytes1);
av_image_fill_arrays(pFrameRGB->data, pFrameRGB->linesize, buffer1, AV_PIX_FMT_RGB32, nWidth, nHeight, 1);
pFrameRGB->data[0] = (uint8_t*)image.bits();
AVFrame* pFrameYUV = av_frame_alloc();
av_image_fill_arrays(pFrameYUV->data, pFrameYUV->linesize, m_pYUVBuffer, AV_PIX_FMT_YUV420P, nWidth, nHeight, 1);
SwsContext* rgb_to_yuv_ctx = sws_getContext(nWidth, nHeight, AV_PIX_FMT_RGB32, nWidth, nHeight, AV_PIX_FMT_YUV420P, SWS_FAST_BILINEAR, NULL, NULL, NULL);
sws_scale(rgb_to_yuv_ctx, (const uint8_t* const*)pFrameRGB->data, pFrameRGB->linesize, 0, nHeight, pFrameYUV->data, pFrameYUV->linesize);
pFrameYUV->height = 1080;
pFrameYUV->width = 1920;
pFrameYUV->format = 12;//这三个值需要自己填写,因为在转为image时,这些信息已经没有了
sws_freeContext(rgb_to_yuv_ctx);
av_frame_free(&pFrameRGB);
av_free(buffer1);
m_pFrameMutex->lock();
m_qFrameBuffer.enqueue(pFrameYUV);
m_pFrameMutex->unlock();
}
}
//Record的线程
void SaveImage::startRecord()
{
while (!m_bStopRecord)
{
//if (m_bStartRecord && m_bReceivedFrame)
if (m_bStartRecord && !m_qFrameBuffer.empty())
{
startVideoRecord();
}
}
while (!m_qFrameBuffer.empty())
{
startVideoRecord();
}
writeTail();
freeRecordParas();
}
//编码的核心函数
void SaveImage::startVideoRecord()
{
//Encode
m_pFrameMutex->lock();
if (m_qFrameBuffer.size() <= 0)
{
return;
m_pFrameMutex->unlock();
}
AVFrame* pFrameTemp = m_qFrameBuffer.dequeue();
m_pFrameMutex->unlock();
pFrameTemp->pts = m_nPts++;
avcodec_send_frame(m_pCodecCtxOut, pFrameTemp);//avFrameOutPut
int ret = avcodec_receive_packet(m_pCodecCtxOut, m_pPacketOut);
if (ret < 0 && ret != -11)
{
qDebug("Failed to encode! \n");
writeTail();
//show a message box, let flag = false, clear para
}
else
{
av_packet_rescale_ts(m_pPacketOut, m_pCodecCtxOut->time_base, m_pVideoStream->time_base);
m_pPacketOut->stream_index = m_pVideoStream->index;
ret = av_write_frame(m_pFormatCtxOut, m_pPacketOut);
av_packet_unref(m_pPacketOut);
av_freep(m_pPacketOut);
}
av_frame_unref(pFrameTemp);
av_frame_free(&pFrameTemp);
}
二:遇到的问题:
在QImage转AVFrame类型时,出现了编码失败的问题,保存下来的视频打不开,且大小固定为200多kb,说明每次保存都只保存了视频格式的头和尾,并没有写入帧数据。逐步排查发现问题出现在类型转换时,转换函数sws_scale,虽然执行成功了,AVFrame中的帧数据也在,但是对比转换后的帧,和刚解码出来的帧发现转换后的帧缺少了一些信息,手动补上了代码中注释的三个信息后,可正常编码。
之前编码265一直失败,找了半天的原因,在设置参数时,不要设置tune参数就可以正常保存了或者不将其设置为"zerolatency",暂时不知具体原因。