如何使用libavcodec将.yuv图像序列编码为.h264的视频码流?
1.实现打开和关闭输入文件和输出文件的操作
//io_data.cpp static FILE* input_file= nullptr; static FILE* output_file= nullptr; int32_t open_input_output_files(const char* input_name,const char* output_name){ if(strlen(input_name)==0||strlen(output_name)==0){ cout<<"Error:empty input or output file name."<<endl; return -1; } close_input_output_files(); input_file=fopen(input_name,"rb");//rb:读取一个二进制文件,该文件必须存在 if(input_file==nullptr){ cerr<<"Error:failed to open input file."<<endl; return -1; } output_file=fopen(output_name,"wb");//wb:打开或新建一个二进制文件,只允许写 if(output_file== nullptr){ cout<<"Error:failed to open output file."<<endl; return -1; } return 0; } void close_input_output_files(){ if(input_file!= nullptr){ fclose(input_file); input_file= nullptr; } if(output_file!= nullptr){ fclose(output_file); output_file= nullptr; } }
2.视频编码器的初始化(在介绍这部分内容之前,先来了解一下几个非常重要的结构体:AVCodec,AVCodecContext,AVPacket以及AVFrame)
AVCodec:
AVCodec类型的结构包含了FFmpeg libavcodec对一个编码器底层实现的封装,其内部定义的部分结构如下:
typedef struct AVCodec{ const char *name;//简要名称 const char *longname;//完整名称 enum AVMediaType type;//媒体类型 enum AVCodecID id; enum AVPixelFormat *pix_fmts;//像素格式,一般为yuv420p const AVProfile *profiles;//编码档次 }
AVCodecContext:
在FFmpeg中,每一个编码器都对应一个上下文结构;在编码开始前,可以通过该结构配置相应的编码参数,比如:编码的profile,图像的宽和高,关键帧间距,码率和帧率等。对于其他编码器(如libx264)的私有参数,AVCodecContext结构可以使用成员priv_data保存编码器的配置信息。该结构的部分定义如下:
typedef struct AVCodecContext{ void *priv_data;//私有参数 int64_t bit_rate;//码率 int width,height; enum AVPixelFormat pix_fmt; int max_b_frames;//最大的b帧数量 }
AVFrame:
在FFmpeg中,未压缩的图像用AVFrame结构来表示。在AVFrame结构中,所包含的最重要的结构即图像数据的缓存区。待编码图像的像素数据保存在AVFrame结构的data指针所指向的内存区。在保存图像像素数据时,存储区的宽度有时会大于图像的宽度,这时可以在每一行像素的末尾填充字节。此时,存储区的宽度可以通过AVFrame的linesize获取。其内部定义的部分结构如下:
typedef struct AVFrame{ #define AV_NUM_DATA_POINTERS 8 uint8_t *data[AV_NUM_DATA_POINTERS];//图像数据缓存区 int linesize[AV_NUM_DATA_POINTERS];//存储区的宽度 int width,height; int format; }
AVPacket:
AVPacket结构用于保存未解码的二进制码流的一个数据包,在该结构中,码流数据保存在data指针指向的内存区中,数据长度为size字节。在从编码器获取到输出的AVPacket结构后,可以通过data指针和size值读取编码后的码流。其内部定义的部分结构如下:
typedef struct AVPacket{ int64_t pts;//显示时间戳 int64_t dts;//解码时间戳 uint8_t *data;//码流数据 int size; int stream_index;//所从属的stream序号 }
编码器初始化的代码如下:
//video_encoder_core.cpp static const AVCodec* codec= nullptr; static AVCodecContext* codec_ctx= nullptr; static AVFrame* frame= nullptr; static AVPacket* pkt= nullptr; int32_t init_video_encoder(const char* codec_name){ if(strlen(codec_name)==0){ cerr<<"Error:empty codec name."<<endl; return -1; } //查找编码器 codec=avcodec_find_encoder_by_name(codec_name); if(!codec){ cerr<<"Error:could not find codec with codec name:"<<string(codec_name)<<endl; return -1; } //创建编码器上下文结构的实例 codec_ctx= avcodec_alloc_context3(codec); if(!codec_ctx){ cerr<<"Error:could not allocate video codec context."<<endl; return -1; } //配置编码参数 codec_ctx->profile=FF_PROFILE_H264_HIGH; codec_ctx->bit_rate=2000000; codec_ctx->width=1920; codec_ctx->height=1080; codec_ctx->gop_size=10;//关键帧间距 codec_ctx->time_base=(AVRational){1,25};//num:分子,den:分母 codec_ctx->framerate=(AVRational){25,1}; codec_ctx->max_b_frames=3; codec_ctx->pix_fmt=AV_PIX_FMT_YUV420P; if(codec->id==AV_CODEC_ID_H264){ av_opt_set(codec_ctx->priv_data,"preset","slow",0); av_opt_set(codec_ctx->priv_data,"tune","zerolatency",0); } //使用指定的codec初始化编码器上下文结构,并分配内存 int32_t result=avcodec_open2(codec_ctx,codec, nullptr); if(result<0){ cerr<<"Error:could not open codec"<<endl; return -1; } pkt=av_packet_alloc(); if(!pkt){ cerr<<"Error:could not allocate AVPacket."<<endl; return -1; } frame=av_frame_alloc(); if(!frame){ cerr<<"Error:could not allocate AVFrame."<<endl; return -1; } frame->width=codec_ctx->width; frame->height=codec_ctx->height; frame->format=codec_ctx->pix_fmt; result= av_frame_get_buffer(frame,0);//给AVFrame结构中的音视频数据分配空间 if(result<0){ cerr<<"Error:could not get AVFrame buffer."<<endl; return -1; } return 0; }
3.编码循环体
在编码循环体中,至少需要实现以下三个功能:
(1)从视频源中循环获取输入图像
(2)将当前帧传入编码器进行编码,获取输出的码流包
(3)输出码流包中的压缩码流到输出文件
读取图像数据和写出码流数据:
//io_data.cpp int32_t read_yuv_to_frame(AVFrame* frame){ int32_t frame_width=frame->width; int32_t frame_height=frame->height; int32_t luma_stride=frame->linesize[0]; int32_t chroma_stride=frame->linesize[1]; int32_t frame_size=frame_width*frame_height*3/2; int32_t read_size=0; if(frame_width==luma_stride){ //如果width等于stride,则说明frame中不存在padding字节,可整体读取 read_size+=fread(frame->data[0],1,frame_width*frame_height,input_file); read_size+=fread(frame->data[1],1,frame_width*frame_height/4,input_file); read_size+=fread(frame->data[2],1,frame_width*frame_height/4,input_file); } else{ //如果width不等于stride,则说明frame中存在padding字节 //对三个分量应该逐行读取 for(size_t i=0;i<frame_height;i++){ read_size+=fread(frame->data[0]+i*luma_stride,1,frame_width,input_file); } for(size_t uv=1;uv<=2;uv++){ for(size_t i=0;i<frame_height/2;i++){ read_size+=fread(frame->data[uv]+i*chroma_stride,1,frame_width/2,input_file); } } } if(read_size!=frame_size){ cerr<<"Error:Read data error,frame_size:"<<frame_size<<",read_size:"<<read_size<<endl; return -1; } return 0; } void write_pkt_to_file(AVPacket* pkt){ fwrite(pkt->data,1,pkt->size,output_file); }
编码一帧图像数据:
//video_encoder_core.cpp static int32_t encode_frame(bool flushing){ int32_t result=0; if(!flushing){ cout<<"Send frame to encoder with pts:"<<frame->pts<<endl; } result=avcodec_send_frame(codec_ctx,flushing? nullptr:frame); if(result<0){ cerr<<"Error:avcodec_send_frame failed."<<endl; return result; } while(result>=0){ result= avcodec_receive_packet(codec_ctx,pkt); if(result==AVERROR(EAGAIN)||result==AVERROR_EOF){//尚未完成对新一帧的编码,要传入后续帧或编码器已完全输出内部缓存的码流 return 1; } else if(result<0){ cerr<<"Error:avcodec_receive_packet failed."<<endl; return result; } if(flushing){ cout<<"Flushing:"; } cout<<"Got encoded package with dts:"<<pkt->dts<<",pts:"<<pkt->pts<<", "<<endl; write_pkt_to_file(pkt); } return 0; }
编码循环体的整体实现:
//video_encoder_core.cpp int32_t encoding(int32_t frame_cnt){ int result=0; for(size_t i=0;i<frame_cnt;i++){ result= av_frame_make_writable(frame);//确保AVFrame是可写的 if(result<0){ cerr<<"Error:could not av_frame_make_writable."<<endl; return result; } result= read_yuv_to_frame(frame); if(result<0){ cerr<<"Error:read_yuv_to_frame failed."<<endl; return result; } frame->pts=i; result= encode_frame(false); if(result<0){ cerr<<"Error:encode_frame failed."<<endl; return result; } } result= encode_frame(true); if(result<0){ cerr<<"Error:flushing failed."<<endl; return result; } return 0; }
关闭编码器:
//video_encoder_core.cpp void destroy_video_encoder(){ avcodec_free_context(&codec_ctx); av_frame_free(&frame); av_packet_free(&pkt); } |
最终main函数的实现如下:
int main(){ const char* input_file_name= "../input.yuv"; const char* output_file_name= "../output.h264"; const char* codec_name= "libx264"; int32_t result= open_input_output_files(input_file_name,output_file_name); if(result<0){ return result; } result=init_video_encoder(codec_name); if(result<0){ return result; } result=encoding(250); if(result<0){ return result; } destroy_video_encoder(); close_input_output_files(); return 0; }
执行完成后会生成码流文件output.h264,使用ffplay可以播放该文件,查看编码结果。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理