FFmpeg SDK 开发进阶:编码、滤镜与完整转码流程 FFmpeg SDK 开发进阶编码、滤镜与完整转码流程上一篇讲了解码这篇讲编码和滤镜以及一个完整的转码程序怎么写。还有内存管理、错误处理、性能优化的最佳实践——把这些搞清楚你就能在自己的项目里熟练集成 FFmpeg 了。大家好我是黒漂技术佬。上一篇我们把视频解码成原始帧这篇反过来把原始帧编码成压缩视频。再加上滤镜处理串起来就是一个完整的转码程序。这篇会讲编码流程、滤镜的代码用法、完整转码示例、以及实际项目里的内存管理和错误处理。一、编码流程编码和解码是对称的但方向反过来原始帧(AVFrame) → 编码器 → 压缩包(AVPacket) → 写入文件编码步骤创建编码器上下文指定编码参数宽高、码率、像素格式打开编码器循环送 frame 进去 → 拿 packet 出来 → 写入文件flush 编码器把缓冲区里剩下的 packet 都拿出来清理核心代码创建编码器constAVCodec*codecavcodec_find_encoder(AV_CODEC_ID_H264);AVCodecContext*codec_ctxavcodec_alloc_context3(codec);// 设置参数codec_ctx-width1280;codec_ctx-height720;codec_ctx-pix_fmtAV_PIX_FMT_YUV420P;// H.264 必须是 YUVcodec_ctx-time_base(AVRational){1,30};// 时间基1/30秒codec_ctx-framerate(AVRational){30,1};// 30fps// 码率控制选一种codec_ctx-bit_rate2000000;// CBR2Mbps// 或者用 CRFx264 私有参数AVDictionary*optsNULL;av_dict_set(opts,crf,23,0);av_dict_set(opts,preset,medium,0);// 打开编码器avcodec_open2(codec_ctx,codec,opts);编码一帧// 送一帧进去avcodec_send_frame(codec_ctx,frame);// 拿编码后的 packetwhile(avcodec_receive_packet(codec_ctx,pkt)0){// 处理 pkt写入文件等av_packet_unref(pkt);}Flush收尾所有帧送完之后送一个 NULL frame 把缓冲区里剩下的 packet 冲出来avcodec_send_frame(codec_ctx,NULL);// 通知编码器结束了while(avcodec_receive_packet(codec_ctx,pkt)0){// 处理剩余 packetav_packet_unref(pkt);}这一步很重要漏掉的话视频最后几秒会缺失。二、写文件libavformat 输出编码出来的 packet 要写入文件需要创建输出格式上下文。步骤AVFormatContext*out_fmt_ctxNULL;avformat_alloc_output_context2(out_fmt_ctx,NULL,NULL,output.mp4);// 添加视频流AVStream*out_streamavformat_new_stream(out_fmt_ctx,NULL);avcodec_parameters_from_context(out_stream-codecpar,codec_ctx);out_stream-time_basecodec_ctx-time_base;// 打开输出文件avio_open(out_fmt_ctx-pb,output.mp4,AVIO_FLAG_WRITE);// 写文件头avformat_write_header(out_fmt_ctx,NULL);// ... 循环写 packet ...// 写文件尾av_write_trailer(out_fmt_ctx);写一个 packet// 重新计算时间戳编码器时间基 → 流时间基av_packet_rescale_ts(pkt,codec_ctx-time_base,out_stream-time_base);pkt-stream_indexout_stream-index;// 写入av_interleaved_write_frame(out_fmt_ctx,pkt);av_interleaved_write_frame会自动按时间戳排序音视频交错写入比直接 write 更稳。三、滤镜Filter的代码用法命令行里的-vf对应代码里的 libavfilter。滤镜图FilterGraph滤镜是一张有向图输入 → 滤镜1 → 滤镜2 → … → 输出AVFilterGraph*filter_graphavfilter_graph_alloc();创建滤镜链比如缩放 裁剪 加水印三步滤镜。constchar*filter_descrscale720:-1,crop720:720,drawtexttexttest:x10:y10:fontsize24;代码里要手动构建滤镜图主要用avfilter_graph_parse2()解析滤镜描述字符串。完整滤镜初始化代码AVFilterGraph*filter_graphNULL;AVFilterContext*buffersrc_ctxNULL;// 输入滤镜AVFilterContext*buffersink_ctxNULL;// 输出滤镜intinit_filters(AVCodecContext*dec_ctx,constchar*filter_descr){filter_graphavfilter_graph_alloc();// 输入参数charargs[512];snprintf(args,sizeof(args),video_size%dx%d:pix_fmt%d:time_base%d/%d:pixel_aspect%d/%d,dec_ctx-width,dec_ctx-height,dec_ctx-pix_fmt,dec_ctx-time_base.num,dec_ctx-time_base.den,dec_ctx-sample_aspect_ratio.num,dec_ctx-sample_aspect_ratio.den);// 创建 buffer source输入constAVFilter*buffersrcavfilter_get_by_name(buffer);avfilter_graph_create_filter(buffersrc_ctx,buffersrc,in,args,NULL,filter_graph);// 创建 buffer sink输出constAVFilter*buffersinkavfilter_get_by_name(buffersink);avfilter_graph_create_filter(buffersink_ctx,buffersink,out,NULL,NULL,filter_graph);// 设置输出像素格式可选enumAVPixelFormatpix_fmts[]{AV_PIX_FMT_YUV420P,AV_PIX_FMT_NONE};av_opt_set_int_list(buffersink_ctx,pix_fmts,pix_fmts,-1,AV_OPT_SEARCH_CHILDREN);// 解析滤镜描述连接起来AVFilterInOut*outputsavfilter_inout_alloc();AVFilterInOut*inputsavfilter_inout_alloc();outputs-nameav_strdup(in);outputs-filter_ctxbuffersrc_ctx;outputs-pad_idx0;outputs-nextNULL;inputs-nameav_strdup(out);inputs-filter_ctxbuffersink_ctx;inputs-pad_idx0;inputs-nextNULL;avfilter_graph_parse2(filter_graph,filter_descr,inputs,outputs,NULL);avfilter_graph_config(filter_graph,NULL);avfilter_inout_free(inputs);avfilter_inout_free(outputs);return0;}使用滤镜// 把解码后的 frame 送进滤镜av_buffersrc_add_frame(buffersrc_ctx,frame);// 从滤镜拿处理后的 framewhile(av_buffersink_get_frame(buffersink_ctx,filtered_frame)0){// 处理 filtered_frameav_frame_unref(filtered_frame);}和编解码的 send/receive 模型很像。 复杂滤镜多输入多输出比如 overlay 画中画也是同样的思路只是 buffer source 有多个inputs/outputs 配置更复杂一些。四、完整转码程序把解码 → 滤镜 → 编码 → 写文件串起来就是一个简易版的 ffmpeg 命令行。主循环结构while(av_read_frame(in_fmt_ctx,pkt)0){if(pkt-stream_indexvideo_idx){// 1. 送 packet 解码avcodec_send_packet(dec_ctx,pkt);// 2. 拿解码后的 framewhile(avcodec_receive_frame(dec_ctx,frame)0){// 3. 送滤镜处理av_buffersrc_add_frame(buffersrc_ctx,frame);// 4. 拿滤镜输出while(av_buffersink_get_frame(buffersink_ctx,filtered_frame)0){// 5. 送编码器filtered_frame-ptsnext_pts;// 设置 ptsavcodec_send_frame(enc_ctx,filtered_frame);// 6. 拿编码后的 packetwhile(avcodec_receive_packet(enc_ctx,enc_pkt)0){// 7. 写入文件av_packet_rescale_ts(enc_pkt,enc_ctx-time_base,out_stream-time_base);av_interleaved_write_frame(out_fmt_ctx,enc_pkt);av_packet_unref(enc_pkt);}av_frame_unref(filtered_frame);}av_frame_unref(frame);}}av_packet_unref(pkt);}// flush 解码器avcodec_send_packet(dec_ctx,NULL);// ... 同样的 receive 循环// flush 编码器avcodec_send_frame(enc_ctx,NULL);// ... 同样的 receive 循环// 写文件尾av_write_trailer(out_fmt_ctx);四层嵌套循环这就是 FFmpeg 转码的经典结构。五、内存管理最佳实践FFmpeg 全是手动内存管理写不好到处泄漏。记住这几条原则1. 谁分配谁释放av_xxx_alloc()分配的对应av_xxx_free()释放av_packet_alloc()→av_packet_free()av_frame_alloc()→av_frame_free()avcodec_alloc_context3()→avcodec_free_context()2. unref 和 free 的区别av_packet_unref()清空 packet 里的数据释放引用的 bufferpacket 对象还在av_packet_free()连 packet 对象一起释放循环里每次用完 packet 调用av_packet_unref()最后退出时再av_packet_free()。frame 同理av_frame_unref()清数据av_frame_free()释放对象。3. 用 goto cleanup 统一释放C 语言没有异常用 goto 跳到统一的清理位置intret-1;AVFormatContext*fmt_ctxNULL;AVCodecContext*codec_ctxNULL;// ... 各种初始化失败就 goto cleanupret0;// 成功cleanup:if(codec_ctx)avcodec_free_context(codec_ctx);if(fmt_ctx)avformat_close_input(fmt_ctx);returnret;4. 复用 frame 和 packet循环里不要每次都 alloc/free分配一次反复用用完 unref 就行。性能差很多。六、错误处理返回值检查几乎所有 FFmpeg 函数都返回 int负数表示错误。retavcodec_open2(codec_ctx,codec,NULL);if(ret0){charerrbuf[AV_ERROR_MAX_STRING_SIZE];av_strerror(ret,errbuf,sizeof(errbuf));printf(打开编码器失败: %s\n,errbuf);gotocleanup;}av_strerror把错误码转成可读字符串。EAGAIN 不是错误avcodec_receive_frame()返回AVERROR(EAGAIN)表示「现在没有帧再送点数据进来」是正常的不是错误。常见错误码错误含义AVERROR(EAGAIN)需要更多输入AVERROR_EOF已经结束了AVERROR_INVALIDDATA数据损坏AVERROR(ENOMEM)内存不足七、性能优化建议1. 多线程解码编码codec_ctx-thread_count4;// 4 线程codec_ctx-thread_typeFF_THREAD_FRAME;// 帧级多线程默认就是多线程的一般不用手动改但可以确认一下。2. 减少数据拷贝能直接用 YUV 处理就别转 RGB转格式有开销滤镜链尽量合并减少中间拷贝硬件解码/编码下一篇细讲3. 零拷贝某些场景下可以直接用硬件帧的内存不用拷到 CPU。比较高级做嵌入式优化的时候再研究。4. 合理设置缓冲区读文件、网络流的缓冲区大小影响流畅度。八、调试技巧1. 打开日志av_log_set_level(AV_LOG_DEBUG);FFmpeg 会输出详细的调试信息排查问题很有用。2. 打印参数不确定参数对不对直接打印printf(宽: %d, 高: %d, 格式: %d\n,codec_ctx-width,codec_ctx-height,codec_ctx-pix_fmt);3. 先命令行验证代码写出来效果不对先用命令行试一下同样的参数确认是参数问题还是代码问题。命令行能跑通代码就一定能跑通。九、本篇小结编码是解码的逆过程send frame → receive packet → 写文件结束要 flush送 NULL 进去把缓冲区剩余数据拿出来写文件用 avformat创建流 → 写头 → 写 packet → 写尾滤镜用 libavfilterbuffersrc 进、buffersink 出中间是滤镜链完整转码是四层循环读包 → 解码 → 滤镜 → 编码 → 写包内存管理alloc/free 配对循环里用 unref 复用错误处理检查返回值EAGAIN 是正常的调试开日志、先命令行验证参数下一篇讲硬件加速NVIDIA NVENC/NVDEC、Intel QSV、RK3588 MPP——用 GPU/NPU 编解码速度快十倍。我是黒漂技术佬。