基于FFmpeg C++将YUV文件编码为可播放H.264 MP4的问题
解决YUV420P转MP4(VLC可播放)的FFmpeg C++编码问题
你遇到的输出异常,核心原因是从本地YUV文件读取的帧没有适配FFmpeg编码器的要求,必须对输入帧做**格式填充、时间戳设置、格式转换(按需)**等处理,以下是具体实现步骤和代码:
核心处理要点
- 帧数据正确填充:YUV420P是平面格式,需将读取的Y、U、V分量分别填充到
AVFrame的data[0](Y)、data[1](U)、data[2](V)平面,同时设置对应linesize(Y行字节数为宽度,U/V行字节数为宽度/2) - 时间戳设置:必须给每个帧设置
pts(显示时间戳),否则VLC会出现卡顿、跳帧甚至无法播放,按帧率累加计算即可 - 格式转换(按需):若编码器要求的像素格式与输入YUV420P不一致,需用
sws_scale做格式转换 - 完整帧读取:YUV420P单帧大小固定为
width*height*3/2,读取时必须确保每次读取完整一帧数据
完整C++代码实现
#include <iostream> #include <fstream> #include <string> extern "C" { #include <libavformat/avformat.h> #include <libavcodec/avcodec.h> #include <libswscale/swscale.h> #include <libavutil/imgutils.h> } int main(int argc, char* argv[]) { if (argc != 3) { std::cerr << "用法: " << argv[0] << " <输入YUV路径> <输出MP4路径>" << std::endl; return -1; } const char* input_yuv = argv[1]; const char* output_mp4 = argv[2]; const int width = 1920; const int height = 1080; const int fps = 30; // 初始化FFmpeg组件 av_register_all(); avcodec_register_all(); avformat_network_init(); // 创建输出格式上下文 AVFormatContext* fmt_ctx = nullptr; if (avformat_alloc_output_context2(&fmt_ctx, nullptr, "mp4", output_mp4) < 0) { std::cerr << "创建输出格式上下文失败" << std::endl; return -1; } // 查找H.264编码器 const AVCodec* codec = avcodec_find_encoder(AV_CODEC_ID_H264); if (!codec) { std::cerr << "未找到H.264编码器" << std::endl; return -1; } // 创建视频流并设置编码器参数 AVStream* stream = avformat_new_stream(fmt_ctx, codec); if (!stream) { std::cerr << "创建视频流失败" << std::endl; return -1; } AVCodecContext* codec_ctx = stream->codec; codec_ctx->codec_id = AV_CODEC_ID_H264; codec_ctx->codec_type = AVMEDIA_TYPE_VIDEO; codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P; codec_ctx->width = width; codec_ctx->height = height; codec_ctx->time_base = av_make_q(1, fps); codec_ctx->framerate = av_make_q(fps, 1); codec_ctx->bit_rate = 5000000; // 5Mbps码率 codec_ctx->gop_size = 30; // 每30帧一个关键帧 codec_ctx->max_b_frames = 0; // 关闭B帧,提升VLC兼容性 // 设置全局头部(MP4格式要求) if (fmt_ctx->oformat->flags & AVFMT_GLOBALHEADER) { codec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; } // 打开编码器 if (avcodec_open2(codec_ctx, codec, nullptr) < 0) { std::cerr << "打开编码器失败" << std::endl; return -1; } // 打开输出文件 if (!(fmt_ctx->oformat->flags & AVFMT_NOFILE)) { if (avio_open(&fmt_ctx->pb, output_mp4, AVIO_FLAG_WRITE) < 0) { std::cerr << "打开输出文件失败" << std::endl; return -1; } } // 写入文件头 if (avformat_write_header(fmt_ctx, nullptr) < 0) { std::cerr << "写入文件头失败" << std::endl; return -1; } // 初始化编码帧 AVFrame* frame = av_frame_alloc(); frame->format = codec_ctx->pix_fmt; frame->width = width; frame->height = height; if (av_frame_get_buffer(frame, 32) < 0) { std::cerr << "分配帧缓冲区失败" << std::endl; return -1; } // 初始化格式转换上下文(如果编码器格式不匹配) struct SwsContext* sws_ctx = nullptr; if (codec_ctx->pix_fmt != AV_PIX_FMT_YUV420P) { sws_ctx = sws_getContext(width, height, AV_PIX_FMT_YUV420P, width, height, codec_ctx->pix_fmt, SWS_BILINEAR, nullptr, nullptr, nullptr); } // 读取YUV文件并编码 std::ifstream yuv_file(input_yuv, std::ios::binary); if (!yuv_file.is_open()) { std::cerr << "打开YUV文件失败" << std::endl; return -1; } int frame_count = 0; const int frame_size = width * height * 3 / 2; uint8_t* yuv_buf = new uint8_t[frame_size]; while (yuv_file.read(reinterpret_cast<char*>(yuv_buf), frame_size)) { // 填充YUV数据到AVFrame int y_plane_size = width * height; memcpy(frame->data[0], yuv_buf, y_plane_size); memcpy(frame->data[1], yuv_buf + y_plane_size, y_plane_size / 4); memcpy(frame->data[2], yuv_buf + y_plane_size + y_plane_size / 4, y_plane_size / 4); // 设置帧时间戳 frame->pts = frame_count; frame_count++; // 格式转换(按需) AVFrame* enc_frame = frame; if (sws_ctx) { AVFrame* tmp_frame = av_frame_alloc(); tmp_frame->format = codec_ctx->pix_fmt; tmp_frame->width = width; tmp_frame->height = height; av_frame_get_buffer(tmp_frame, 32); sws_scale(sws_ctx, frame->data, frame->linesize, 0, height, tmp_frame->data, tmp_frame->linesize); tmp_frame->pts = frame->pts; enc_frame = tmp_frame; } // 发送帧到编码器 if (avcodec_send_frame(codec_ctx, enc_frame) < 0) { std::cerr << "发送帧到编码器失败" << std::endl; break; } // 接收编码后的数据包并写入文件 AVPacket pkt; av_init_packet(&pkt); pkt.data = nullptr; pkt.size = 0; while (avcodec_receive_packet(codec_ctx, &pkt) == 0) { av_packet_rescale_ts(&pkt, codec_ctx->time_base, stream->time_base); pkt.stream_index = stream->index; if (av_interleaved_write_frame(fmt_ctx, &pkt) < 0) { std::cerr << "写入数据包失败" << std::endl; break; } av_packet_unref(&pkt); } // 释放临时转换帧 if (sws_ctx && enc_frame != frame) { av_frame_free(&enc_frame); } } // 发送结束信号,处理剩余数据包 avcodec_send_frame(codec_ctx, nullptr); AVPacket pkt; av_init_packet(&pkt); pkt.data = nullptr; pkt.size = 0; while (avcodec_receive_packet(codec_ctx, &pkt) == 0) { av_packet_rescale_ts(&pkt, codec_ctx->time_base, stream->time_base); pkt.stream_index = stream->index; av_interleaved_write_frame(fmt_ctx, &pkt); av_packet_unref(&pkt); } // 写入文件尾 av_write_trailer(fmt_ctx); // 释放资源 delete[] yuv_buf; yuv_file.close(); av_frame_free(&frame); if (sws_ctx) sws_freeContext(sws_ctx); avcodec_close(codec_ctx); if (!(fmt_ctx->oformat->flags & AVFMT_NOFILE)) avio_close(fmt_ctx->pb); avformat_free_context(fmt_ctx); std::cout << "编码完成" << std::endl; return 0; }
额外注意事项
- 编译链接:编译时需链接FFmpeg相关库,示例编译命令:
g++ -o yuv2mp4 yuv2mp4.cpp -lavformat -lavcodec -lavutil -lswscale - VLC兼容性:关闭B帧(
max_b_frames=0)可避免部分旧版本VLC的播放异常;设置全局头部是MP4格式的强制要求 - 资源释放:所有FFmpeg结构体(
AVFrame、AVPacket等)必须调用对应释放函数,避免内存泄漏
内容的提问来源于stack exchange,提问作者devprog
相关产品推荐
相关产品推荐

