
简介这份资源是面向C#开发者的FFmpeg.AutoGen实战学习示例适合希望在.NET环境中调用FFmpeg完成音视频处理的初中级开发者。压缩包内以CSharpVideoDemo为核心演示了通过NuGet引入绑定后如何打开多媒体文件、查找流信息、调用解码器与编码器、处理AVFrame帧数据以及使用sws_scale做色彩空间转换、借助AVFilterGraph搭建过滤器链并涉及AVFormatContext等对象的内存管理与异步处理思路。资源共174个文件以111个C/C头文件、16个dll动态库、8个cs源码及若干工程配置与资源文件为主整体约55.53MB目录结构便于对照源码逐层理解绑定调用流程。目前已有732人学习下载可作为从API绑定到编解码、封装输出的完整参考帮助读者快速搭建可运行的多媒体处理工程并在此基础上扩展自定义滤镜或流媒体功能。1. FFmpeg.AutoGen 代码例子从托管代码到原生音视频处理的那层窗户纸如果你写过 C# 的音视频处理大概率经历过这个场景想用 FFmpeg 做转码或解码结果发现要么调命令行进程、要么用某个封装库性能和灵活性总差一口气。FFmpeg.AutoGen 解决的就是这个问题——它把 FFmpeg 的 C 接口自动生成成 C# 的 P/Invoke 绑定让你能在 .NET 里直接调avcodec_send_packet、avformat_open_input这些原生函数。标题里的「代码例子.rar」本质上就是一套可运行的绑定调用示例集覆盖解封装、解码、编码、封装这几条主线。这篇文章不依赖那个压缩包的具体内容而是把 FFmpeg.AutoGen 从环境搭建到跑通第一个解码器的完整路径讲清楚适合已经会 C#、想切入音视频底层处理的工程师。2. FFmpeg.AutoGen 的绑定机制与最小可运行环境2.1 自动生成绑定到底生成了什么FFmpeg.AutoGen 的核心工作方式是用 CppSharp 这类工具解析 FFmpeg 的头文件把libavformat、libavcodec、libavutil、libswscale、libswresample里的结构体、枚举、函数签名翻译成 C# 的struct、enum和[DllImport]声明。你拿到的不是手写封装而是近乎一比一的映射。这意味着两件事第一FFmpeg 的 API 怎么用C# 里就怎么用没有额外的抽象层第二C 里的指针操作、内存生命周期管理在 C# 里同样要面对AVFrame*、AVPacket*这些指针不会自动变成托管对象。常见做法是直接引用FFmpeg.AutoGen这个 NuGet 包它里面已经包含了生成好的绑定代码。但绑定代码本身只是声明真正干活的是 FFmpeg 的原生动态库。所以环境搭建的关键不是绑定而是让 .NET 运行时能找到正确版本的avcodec-xx.dll、avformat-xx.dll等文件。2.2 动态库版本匹配与加载路径设置FFmpeg 的 DLL 版本号跟 FFmpeg 主版本绑定比如 FFmpeg 6.x 对应avcodec-60.dllFFmpeg 7.x 对应avcodec-61.dll。绑定包里的函数签名是按某个版本生成的如果你放的原生库版本对不上运行时就会在调用某个函数时抛EntryPointNotFoundException。我一般会先确认绑定包对应的 FFmpeg 版本然后去下同版本的 shared build。# 查看 FFmpeg 动态库导出的函数确认版本匹配 # Windows 下用 dumpbinLinux 下用 nm dumpbin /exports avcodec-60.dll | findstr avcodec_send_packet上面这条命令的作用是验证你手里的avcodec-60.dll确实导出了avcodec_send_packet这个符号。如果找不到说明版本不对或者下的是静态库。参数上没什么可调的关键是findstr后面的函数名要跟你代码里调的一致。加载路径的设置在 .NET Core 及以上版本里推荐用NativeLibrary.SetDllImportResolver来指定。因为 FFmpeg 的 DLL 之间还有依赖关系比如avformat依赖avcodecavcodec依赖avutil如果只把路径加到PATH环境变量里有时候解析顺序会出问题。using System.Reflection; using System.Runtime.InteropServices; // 在程序启动时注册解析器把所有 FFmpeg 原生库指向同一个目录 NativeLibrary.SetDllImportResolver(typeof(FFmpeg.AutoGen.ffmpeg).Assembly, (libraryName, assembly, searchPath) { // 拼接你存放 DLL 的目录 var libPath Path.Combine(AppContext.BaseDirectory, ffmpeg, libraryName .dll); if (File.Exists(libPath)) return NativeLibrary.Load(libPath); return IntPtr.Zero; // 回退到默认解析 });这段代码的逻辑是当 .NET 需要加载任何 FFmpeg 相关的原生库时都先去ffmpeg子目录里找同名 DLL。参数libraryName是运行时传入的库名比如avcodec-60。注意IntPtr.Zero表示交给默认逻辑处理不要直接抛异常否则一些系统库的解析会失败。2.3 第一个能跑的解封装代码环境通了之后最小验证不是解码而是解封装——打开一个媒体文件读出流信息。这一步能跑通说明avformat和avutil的加载没问题。using FFmpeg.AutoGen; // 初始化 FFmpeg 的日志和网络模块如果涉及网络流 ffmpeg.avformat_network_init(); // 分配输入格式上下文 AVFormatContext* fmtCtx null; // 打开输入文件最后一个参数是选项字典这里传 null int ret ffmpeg.avformat_open_input(fmtCtx, test.mp4, null, null); if (ret 0) { // 把错误码转成可读信息 Console.WriteLine($打开失败: {ffmpeg.av_err2str(ret)}); return; } // 读取流信息这一步会填充 fmtCtx-streams ret ffmpeg.avformat_find_stream_info(fmtCtx, null); if (ret 0) { /* 同样处理错误 */ } // 遍历所有流打印编解码器类型 for (int i 0; i fmtCtx-nb_streams; i) { var stream fmtCtx-streams[i]; var codecType stream-codecpar-codec_type; Console.WriteLine($流 {i}: 类型{codecType}, 编码ID{stream-codecpar-codec_id}); } // 释放上下文 ffmpeg.avformat_close_input(fmtCtx);逻辑说明avformat_open_input负责探测容器格式并打开文件avformat_find_stream_info会实际读取一部分数据来确认每个流的编码参数。参数上第三个参数可以指定输入格式比如强制h264一般传null让 FFmpeg 自动探测第四个参数是AVDictionary**用来传超时、缓冲区大小等选项。失败时看av_err2str的输出常见的是Invalid data found或No such file。3. 解码与编码从 AVPacket 到 AVFrame 的完整链路3.1 解码循环的四个关键函数解封装之后拿到的是AVPacket里面是压缩数据。要变成能处理的AVFrame需要经过avcodec_send_packet和avcodec_receive_frame这一对函数。这是 FFmpeg 新解码 API 的核心跟老的avcodec_decode_video2不一样它把发送和接收拆开了一个 packet 可能产生零个或多个 frame。// 假设已经找到了视频流并拿到了对应的 AVCodecContext* codecCtx AVPacket* packet ffmpeg.av_packet_alloc(); AVFrame* frame ffmpeg.av_frame_alloc(); while (ffmpeg.av_read_frame(fmtCtx, packet) 0) { // 只处理视频流 if (packet-stream_index videoStreamIndex) { // 发送压缩数据到解码器 int sendRet ffmpeg.avcodec_send_packet(codecCtx, packet); if (sendRet 0 sendRet ! ffmpeg.AVERROR(ffmpeg.EAGAIN)) { Console.WriteLine($发送失败: {ffmpeg.av_err2str(sendRet)}); break; } // 循环接收解码后的帧一个 packet 可能解出多帧 while (true) { int recvRet ffmpeg.avcodec_receive_frame(codecCtx, frame); if (recvRet ffmpeg.AVERROR(ffmpeg.EAGAIN) || recvRet ffmpeg.AVERROR_EOF) break; if (recvRet 0) { /* 错误处理 */ } // 此时 frame 里是 YUV 数据可以送去做转换或编码 Console.WriteLine($解码出一帧: {frame-width}x{frame-height}, 格式{frame-format}); // 必须调用 unref 来释放帧内部的引用计数 ffmpeg.av_frame_unref(frame); } } // packet 用完也要 unref否则内存泄漏 ffmpeg.av_packet_unref(packet); }逻辑说明avcodec_send_packet把压缩数据喂给解码器返回EAGAIN表示解码器内部缓冲区满了需要先接收帧。avcodec_receive_frame返回EAGAIN表示当前没有更多帧可出需要继续送 packet。参数上codecCtx必须已经用avcodec_open2打开过。这里最容易翻车的地方是忘记av_frame_unref和av_packet_unref跑几分钟内存就涨上去了。3.2 像素格式转换与缩放解码出来的AVFrame通常是 YUV420P如果要显示或保存成图片需要转成 RGB。这一步用libswscale。// 创建转换上下文从 codecCtx 的像素格式转到 RGB24尺寸不变 SwsContext* swsCtx ffmpeg.sws_getContext( codecCtx-width, codecCtx-height, codecCtx-pix_fmt, codecCtx-width, codecCtx-height, AVPixelFormat.AV_PIX_FMT_RGB24, ffmpeg.SWS_BILINEAR, null, null, null); // 分配一个 AVFrame 来存 RGB 数据 AVFrame* rgbFrame ffmpeg.av_frame_alloc(); rgbFrame-format (int)AVPixelFormat.AV_PIX_FMT_RGB24; rgbFrame-width codecCtx-width; rgbFrame-height codecCtx-height; ffmpeg.av_frame_get_buffer(rgbFrame, 0); // 执行转换 ffmpeg.sws_scale(swsCtx, frame-data, frame-linesize, 0, codecCtx-height, rgbFrame-data, rgbFrame-linesize);参数说明sws_getContext的前三个参数是输入尺寸和格式中间三个是输出尺寸和格式SWS_BILINEAR是缩放算法追求质量可以换SWS_BICUBIC。sws_scale的srcSliceY和srcSliceH一般传 0 和高度表示处理整帧。转换完的rgbFrame-data[0]就是连续的 RGB 字节可以直接写文件或送显示。3.3 编码把 AVFrame 压成 AVPacket编码是解码的逆过程用avcodec_send_frame和avcodec_receive_packet。初始化编码器时要设置码率、帧率、GOP 大小这些参数。// 查找 H.264 编码器 AVCodec* codec ffmpeg.avcodec_find_encoder(AVCodecID.AV_CODEC_ID_H264); AVCodecContext* encCtx ffmpeg.avcodec_alloc_context3(codec); // 设置编码参数 encCtx-bit_rate 400000; // 码率 400kbps encCtx-width 640; encCtx-height 480; encCtx-time_base new AVRational { num 1, den 25 }; // 25fps encCtx-framerate new AVRational { num 25, den 1 }; encCtx-gop_size 10; // 每 10 帧一个关键帧 encCtx-pix_fmt AVPixelFormat.AV_PIX_FMT_YUV420P; // 打开编码器 ffmpeg.avcodec_open2(encCtx, codec, null); // 发送一帧 ffmpeg.avcodec_send_frame(encCtx, frame); // 接收编码后的 packet AVPacket* encPacket ffmpeg.av_packet_alloc(); while (ffmpeg.avcodec_receive_packet(encCtx, encPacket) 0) { // encPacket-data 里是 H.264 裸流可以写文件或封装 ffmpeg.av_packet_unref(encPacket); }参数上time_base和framerate要匹配否则时间戳会乱。gop_size影响关键帧间隔直播场景一般设小一点。编码器打开后send_frame传null可以刷新编码器缓冲区把剩余帧都取出来。4. 避坑与排查FFmpeg.AutoGen 里那些让人后悔药的细节4.1 内存泄漏av_frame_unref 和 av_packet_unref 缺一不可现象程序跑几分钟后内存占用持续上升最终 OOM。原因AVFrame和AVPacket内部有引用计数av_frame_alloc只是分配了结构体数据缓冲区是在av_frame_get_buffer或解码时分配的。如果不调用av_frame_unref引用计数不降缓冲区不释放。解决每次avcodec_receive_frame处理完 frame 后立刻av_frame_unref每次av_read_frame处理完 packet 后立刻av_packet_unref。不要指望 GC这些是非托管内存。4.2 时间戳错乱time_base 换算没做对现象编码出来的视频播放速度不对或者音视频不同步。原因AVFrame-pts是基于codecCtx-time_base的而封装时AVStream-time_base可能不一样。直接拿 frame 的 pts 塞给 packet 就会错。解决用av_packet_rescale_ts做转换或者手动按av_q2d换算。常见做法是编码前设好encCtx-time_base编码后把 packet 的 pts 从编码器时基转到流时基。4.3 找不到入口点DLL 版本和绑定不匹配现象运行时抛EntryPointNotFoundException提示某个函数在 DLL 里找不到。原因NuGet 包里的绑定是按某个 FFmpeg 版本生成的你放的原生库是另一个版本函数签名或导出名变了。解决确认绑定包的版本说明下载对应主版本的 shared build。Windows 上可以用dumpbin /exports看导出函数Linux 上用nm -D。如果版本实在对不上可以自己用 CppSharp 重新生成绑定。4.4 解码花屏packet 边界和关键帧问题现象解码出来的画面有绿块或撕裂。原因可能是送进解码器的 packet 不完整或者跳过了关键帧直接送 P 帧。解决确保av_read_frame返回的 packet 完整送入不要手动切割。如果是网络流要处理丢包重传或等待关键帧。另外检查codecCtx-pix_fmt和实际帧格式是否一致不一致时 sws_scale 会出错。4.5 编码器打开失败参数组合不合法现象avcodec_open2返回负数。原因编码器对宽高、像素格式、码率有约束比如 H.264 的宽高必须是偶数AV_PIX_FMT_YUV420P要求宽高是 2 的倍数。解决打开前检查codec-supported_framerates、codec-pix_fmts这些字段把参数调整到支持范围内。也可以先不设bit_rate让编码器用默认值打开成功后再调。5. 用硬件加速和自定义 IO 把方案推到生产级5.1 硬件解码的接入方式软解跑通之后如果 CPU 占用太高就该考虑硬件加速了。FFmpeg 支持 DXVA2、D3D11VA、VAAPI、VideoToolbox 等。在 FFmpeg.AutoGen 里硬件解码的入口是av_hwdevice_ctx_create和av_hwframe_transfer_data。流程是先创建硬件设备上下文把它挂到codecCtx-hw_device_ctx上然后正常解码。解出来的AVFrame格式是AV_PIX_FMT_D3D11之类的硬件格式需要再用av_hwframe_transfer_data拷回系统内存才能做后续处理。// 创建 D3D11VA 设备上下文 AVBufferRef* hwDeviceCtx null; ffmpeg.av_hwdevice_ctx_create(hwDeviceCtx, AVHWDeviceType.AV_HWDEVICE_TYPE_D3D11VA, null, null, 0); codecCtx-hw_device_ctx ffmpeg.av_buffer_ref(hwDeviceCtx); // 解码后如果 frame-format 是硬件格式转回 NV12 或 YUV420P if (frame-format (int)AVPixelFormat.AV_PIX_FMT_D3D11) { AVFrame* swFrame ffmpeg.av_frame_alloc(); ffmpeg.av_hwframe_transfer_data(swFrame, frame, 0); // 后续用 swFrame 做转换或编码 }参数上AVHWDeviceType根据平台选Windows 用 D3D11VALinux 用 VAAPI。注意硬件帧不能直接 sws_scale必须先 transfer。另外hw_device_ctx要记得av_buffer_unref释放。5.2 自定义 AVIOContext 处理内存数据很多时候媒体数据不在文件里而在内存缓冲区或网络流里。这时候需要自定义AVIOContext把读写回调指向自己的函数。FFmpeg.AutoGen 里可以用avio_alloc_context配合AVIOContext的委托。// 定义读回调从 MemoryStream 里读数据 avio_alloc_context_read_packet readPacket (opaque, buf, bufSize) { var stream GCHandle.FromIntPtr(opaque).Target as MemoryStream; byte[] temp new byte[bufSize]; int read stream.Read(temp, 0, bufSize); Marshal.Copy(temp, 0, (IntPtr)buf, read); return read; }; // 分配 AVIOContext缓冲区大小 4096 byte* avioBuffer (byte*)ffmpeg.av_malloc(4096); AVIOContext* avioCtx ffmpeg.avio_alloc_context(avioBuffer, 4096, 0, GCHandle.ToIntPtr(GCHandle.Alloc(memoryStream)), readPacket, null, null); // 把 avioCtx 挂到 fmtCtx-pb 上再调 avformat_open_input fmtCtx-pb avioCtx; ffmpeg.avformat_open_input(fmtCtx, null, null, null);逻辑说明avio_alloc_context的第三个参数write_flag设 0 表示只读。opaque用来传自定义数据这里用GCHandle把MemoryStream钉住。读回调里把数据拷到 FFmpeg 提供的缓冲区返回实际读取字节数。返回 0 表示 EOF。注意avioBuffer要用av_malloc分配不能用托管数组否则 FFmpeg 内部释放会出错。5.3 验证解码正确性的一个笨办法怎么确认解码出来的帧是对的我一般会做两件事第一把解码后的 YUV 数据按帧号存成文件用ffplay直接播 YUV 裸流命令是ffplay -f rawvideo -pixel_format yuv420p -video_size 640x480 frame.yuv。第二对比 FFmpeg 命令行转出来的帧用ffmpeg -i input.mp4 -f rawvideo -pix_fmt yuv420p ref.yuv然后二进制比对。如果对不上先查像素格式再查 linesize 对齐——AVFrame-linesize[0]可能大于宽度拷贝时要按行拷不能直接整块 memcpy。// 按行拷贝 YUV 数据处理 linesize 对齐 for (int y 0; y height; y) { Marshal.Copy((IntPtr)(frame-data[0] y * frame-linesize[0]), yuvBuffer, y * width, width); }这个细节坑了我很久因为很多示例代码直接Marshal.Copy(frame-data[0], ...)拷width * height字节在宽度不是 32 倍数时就会错位。希望帮到你。本文还有配套的精品资源点击获取