C++ FFmpeg实现将视频帧转换为JPG或PNG图片
在使用FFmpeg处理实时流或本地视频时,经常需要将解码后的画面以JPG或PNG格式保存到本地或上传服务器。这一过程核心在于利用FFmpeg的图像编码器对AVFrame数据进行重编码。本文将详细介绍在C++环境下,如何配置编码器上下文、执行像素格式转换,并最终提取图像数据的完整流程。
结果与目标
我们的目标是将输入的视频帧数据(通常为YUV格式)转换为内存中的JPEG或PNG二进制数据,并可进一步写入文件。整个过程复用FFmpeg的视频编解码API,关键在于正确选择图片编码器并处理像素格式差异。
准备条件
在进行开发前,需确保已集成FFmpeg C API相关头文件,并链接了libavcodec, libswscale, libavutil等库。核心数据结构为AVFrame(包含原始视频帧数据)和目标图片编码器ID(如AV_CODEC_ID_MJPEG对应JPG,AV_CODEC_ID_PNG对应PNG)。
真实操作顺序
第1步:查找图片编码器
首先需要获取目标图片格式的编码器句柄。例如,生成JPG图片需使用MJPEG编码器。
enum AVCodecID codecID = AV_CODEC_ID_MJPEG; // 或 AV_CODEC_ID_PNG
AVCodec* codec = avcodec_find_encoder(codecID);
if (!codec) {
printf("Encoder not found\n");
return;
}
第2步:构造编码器上下文
根据找到的编码器分配并初始化AVCodecContext。需设置分辨率、码率、帧率时间基数等参数。特别注意pix_fmt需设为编码器支持的像素格式,通常取编码器支持列表的第一个格式。
AVCodecContext* ctx = avcodec_alloc_context3(codec);
ctx->bit_rate = 3000000;
ctx->width = frame->width;
ctx->height = frame->height;
ctx->time_base.num = 1;
ctx->time_base.den = 25;
ctx->gop_size = 10;
ctx->max_b_frames = 0;
ctx->thread_count = 1;
ctx->pix_fmt = *codec->pix_fmts; // 选择编码器适配的像素格式
int ret = avcodec_open2(ctx, codec, NULL);
if (ret < 0) {
printf("Failed to open encoder\n");
return;
}
第3步:像素格式转换
若输入帧的像素格式(如AV_PIX_FMT_YUV420P)与编码器所需的像素格式不同,必须使用libswscale进行转换。通常图像编码器需要RGB格式数据。
AVFrame* rgbFrame = av_frame_alloc();
struct SwsContext* swsContext = sws_getContext(
frame->width, frame->height, (enum AVPixelFormat)frame->format,
frame->width, frame->height, ctx->pix_fmt,
SWS_BILINEAR, NULL, NULL, NULL
);
// 计算缓冲区大小并分配内存
int bufferSize = av_image_get_buffer_size(ctx->pix_fmt, frame->width, frame->height, 1);
uint8_t* buffer = (uint8_t*)av_malloc(bufferSize);
// 填充rgbFrame的数据指针
av_image_fill_arrays(rgbFrame->data, rgbFrame->linesize, buffer,
ctx->pix_fmt, frame->width, frame->height, 1);
// 执行转换
sws_scale(swsContext, frame->data, frame->linesize, 0, frame->height,
rgbFrame->data, rgbFrame->linesize);
rgbFrame->format = ctx->pix_fmt;
rgbFrame->width = ctx->width;
rgbFrame->height = ctx->height;
第4步:发送帧进行编码
将转换后的帧发送给编码器。如果无需转换,可直接发送原始帧(前提是格式匹配)。
// 假设已准备好rgbFrame
ret = avcodec_send_frame(ctx, rgbFrame); // 若无转换,可传frame
第5步:获取编码后的数据包
从编码器接收压缩后的数据包(AVPacket),其中包含实际的图片二进制数据。
AVPacket pkt;
av_init_packet(&pkt);
ret = avcodec_receive_packet(ctx, &pkt);
if (ret == 0 && pkt.size > 0) {
// pkt.data 包含图片数据,pkt.size 为大小
// 可将此数据保存文件或上传服务器
}
av_packet_unref(&pkt); // 记得解引用数据包
第6步:资源清理
操作完成后,务必按顺序释放所有申请的资源,防止内存泄漏。
if (swsContext) sws_freeContext(swsContext);
if (rgbFrame) {
av_frame_unref(rgbFrame);
av_frame_free(&rgbFrame);
}
if (buffer) av_free(buffer);
if (ctx) {
avcodec_close(ctx);
avcodec_free_context(&ctx);
}
完整代码封装与使用示例
为了方便复用,可以将上述逻辑封装为一个独立函数。该函数接受AVFrame、编码器ID以及外部提供的输出缓冲区,返回实际写入的图片大小。
核心转换函数
static int frameToImage(AVFrame* frame, enum AVCodecID codecID, uint8_t* outbuf, size_t outbufSize)
{
int ret = 0;
AVPacket pkt;
AVCodec* codec;
AVCodecContext* ctx = NULL;
AVFrame* rgbFrame = NULL;
uint8_t* buffer = NULL;
struct SwsContext* swsContext = NULL;
av_init_packet(&pkt);
codec = avcodec_find_encoder(codecID);
if (!codec || !codec->pix_fmts) {
printf("Encoder not found or unsupported format\n");
goto end;
}
ctx = avcodec_alloc_context3(codec);
ctx->bit_rate = 3000000;
ctx->width = frame->width;
ctx->height = frame->height;
ctx->time_base.num = 1;
ctx->time_base.den = 25;
ctx->gop_size = 10;
ctx->max_b_frames = 0;
ctx->thread_count = 1;
ctx->pix_fmt = *codec->pix_fmts;
if (avcodec_open2(ctx, codec, NULL) < 0) goto end;
// 判断是否需要格式转换
if (frame->format != ctx->pix_fmt) {
rgbFrame = av_frame_alloc();
if (!rgbFrame) goto end;
swsContext = sws_getContext(frame->width, frame->height, (enum AVPixelFormat)frame->format,
frame->width, frame->height, ctx->pix_fmt,
SWS_BILINEAR, NULL, NULL, NULL);
if (!swsContext) goto end;
int bufferSize = av_image_get_buffer_size(ctx->pix_fmt, frame->width, frame->height, 1);
buffer = (uint8_t*)av_malloc(bufferSize);
if (!buffer) goto end;
av_image_fill_arrays(rgbFrame->data, rgbFrame->linesize, buffer,
ctx->pix_fmt, frame->width, frame->height, 1);
if (sws_scale(swsContext, frame->data, frame->linesize, 0, frame->height,
rgbFrame->data, rgbFrame->linesize) < 0) goto end;
rgbFrame->format = ctx->pix_fmt;
rgbFrame->width = ctx->width;
rgbFrame->height = ctx->height;
ret = avcodec_send_frame(ctx, rgbFrame);
} else {
ret = avcodec_send_frame(ctx, frame);
}
if (ret < 0) goto end;
ret = avcodec_receive_packet(ctx, &pkt);
if (ret < 0) goto end;
// 拷贝到外部缓冲区
if (outbuf && pkt.size > 0 && pkt.size <= outbufSize) {
memcpy(outbuf, pkt.data, pkt.size);
}
ret = pkt.size;
end:
if (swsContext) sws_freeContext(swsContext);
if (rgbFrame) { av_frame_unref(rgbFrame); av_frame_free(&rgbFrame); }
if (buffer) av_free(buffer);
av_packet_unref(&pkt);
if (ctx) { avcodec_close(ctx); avcodec_free_context(&ctx); }
return ret;
}
保存为JPG文件示例
结合上述函数,将帧保存为本地JPG文件需要注意缓冲区大小的计算。建议使用BRGA格式估算最大容量,以防溢出。
void saveFrameToJpg(AVFrame* frame, const char* path) {
// 预估缓冲大小,考虑到JPG压缩比波动,适当预留空间
int bufSize = av_image_get_buffer_size(AV_PIX_FMT_BGRA, frame->width, frame->height, 64) * 2;
uint8_t* buf = (uint8_t*)av_malloc(bufSize);
// 执行转换,目标为MJPEG (JPG)
int picSize = frameToImage(frame, AV_CODEC_ID_MJPEG, buf, bufSize);
if (picSize > 0) {
FILE* f = fopen(path, "wb");
if (f) {
fwrite(buf, 1, picSize, f);
fclose(f);
}
}
av_free(buf);
}
自定义构造AVFrame示例
若只有裸数据而无现成AVFrame,可通过辅助函数快速创建:
AVFrame* allocFrame(uint8_t* frameData, int width, int height, AVPixelFormat format) {
AVFrame* frame = av_frame_alloc();
frame->width = width;
frame->height = height;
frame->format = format;
av_image_fill_arrays(frame->data, frame->linesize, frameData,
format, width, height, 64);
return frame;
}
简要总结
以上就是C++利用FFmpeg将视频帧转换为JPG或PNG图片的详细内容。核心逻辑遵循标准的编码流程:查编码器 → 建上下文 → 像素格式转换 → 送帧 → 收包。由于图片数据始终存在于内存中,开发者可根据实际需求灵活选择直接传输或落盘保存。更多关于FFmpeg高级应用技巧的资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













