简介:本资源是一套面向C#开发者的学习实践包,聚焦FFmpeg.AutoGen原生库在.NET环境下的音视频处理实战,适用于具备基础C#编程能力、希望掌握跨语言调用多媒体底层API的中高级开发者。压缩包共174个文件,涵盖111个C头文件(定义FFmpeg结构与接口)、16个动态链接库(提供跨平台原生支持)、13个示例配置与测试样本、8个核心C#源码文件(含frmPlayer.cs播放器主逻辑、FFmpegBinariesHelper.cs二进制加载器等),以及sln工程文件和配套资源设计器,整体体积55.53MB,结构完整、开箱即用。已有732人学习下载,可直接运行CSharpVideoDemo项目,快速掌握AVFormat上下文管理、音视频解码流程、帧级处理(如sws_scale色彩转换)、过滤器图构建及内存生命周期控制等关键能力,为开发视频转码工具、实时流媒体客户端或自定义滤镜系统打下扎实基础。
1. FFmpeg.AutoGen 是什么:不是封装库,而是 C# 与 FFmpeg 原生 ABI 的“裸金属通道”
你搜“FFmpeg.AutoGen代码例子.rar”,大概率刚踩进一个坑——以为下载的是个开箱即用的 .NET 视频处理 SDK,结果解压发现全是.cs文件、没文档、没 NuGet 包、跑不起来、报DllNotFoundException或AccessViolationException。这不是你环境有问题,而是 FFmpeg.AutoGen 本身就不该被当成“SDK”来用。它本质是FFmpeg C API 的全自动 P/Invoke 绑定生成器:把libavcodec.so、libavformat.dll这些二进制动态库的函数签名,用 Clang 解析头文件后,原样翻译成 C# 的DllImport声明。没有封装逻辑、没有对象模型、没有异常包装、没有内存管理代理——它就是一扇焊死的铁门,门后是 FFmpeg 的 C 黑匣子,你得自己带钥匙(函数指针)、自己点火(avcodec_open2)、自己清烟(av_frame_free)。适合谁?两类人:一是正在用 C++ 写 FFmpeg 插件、想快速迁移到 C# 做原型验证的音视频工程师;二是需要极致控制帧级时序(比如医疗影像逐帧校验、工业相机硬同步采集)、不能容忍任何托管层 GC 干扰的实时系统开发者。如果你只是想“加水印”“转格式”“抽缩略图”,请立刻关掉这个页面,去用FFmpeg.NET或VideoLAN.LibVLC——FFmpeg.AutoGen 不是帮你省事的,它是给你一把螺丝刀,让你亲手拧开 FFmpeg 的每一颗螺栓。
2. 从零跑通第一个例子:用 FFmpeg.AutoGen 解码 H.264 流并保存为 BMP
FFmpeg.AutoGen 本身不带任何二进制依赖,它只生成 C# 绑定代码。要让它工作,必须三件套齐备:生成的 C# 绑定类 + 对应版本的 FFmpeg 动态库 + 正确的平台加载路径。网上流传的.rar包里常缺后两者,导致新手卡在第一步。下面以 Windows x64 为例,走通最简解码流程:读取一个 H.264 Annex.B 格式裸流(.264文件),逐帧解码为 RGB24,保存为 BMP。
2.1 下载并部署 FFmpeg 原生库(非 ffmpeg.exe)
FFmpeg.AutoGen 不吃ffmpeg.exe,它要的是avcodec-60.dll、avformat-61.dll等动态库。严禁直接从 ffmpeg.org 下载 static build(那是给命令行用的,符号导出不全)。正确做法:
- 去 https://github.com/FFmpeg/FFmpeg/releases 找最新 release(如
n6.1) - 下载
*-win64-gpl-shared.zip(注意是shared,不是static) - 解压后提取
bin/目录下的所有.dll文件(avcodec-*.dll,avformat-*.dll,avutil-*.dll,swscale-*.dll,swresample-*.dll) - 将它们全部复制到你的 C# 项目输出目录(
bin/Debug/net6.0/),确保与.exe同级
提示:版本号必须严格匹配。
FFmpeg.AutoGennuget 包发布时会声明其绑定的 FFmpeg 版本(如v6.1),你下载的 DLL 主版本号(avcodec-60.dll中的60)必须与之对应。错一个数字,avcodec_find_decoder就返回 null。
2.2 初始化与解码循环:最小可运行代码
以下代码片段可直接粘贴进Program.cs(.NET 6+ 控制台项目),无需额外 NuGet(FFmpeg.AutoGen已含绑定类):
using System; using System.IO; using System.Runtime.InteropServices; using FFmpeg.AutoGen; // 注意:必须在 Main 前调用,否则 av_* 函数地址未解析 FFmpegBinariesHelper.RegisterFFmpegBinaries(); const string inputPath = "test.264"; // H.264 Annex.B 裸流 const string outputPath = "frame_"; // 1. 打开输入文件(裸流无容器,需手动指定 codec) var formatContext = ffmpeg.avformat_alloc_context(); var codecParameters = ffmpeg.avcodec_parameters_alloc(); codecParameters.codec_type = AVMediaType.AVMEDIA_TYPE_VIDEO; codecParameters.codec_id = AVCodecID.AV_CODEC_ID_H264; codecParameters.width = 1920; codecParameters.height = 1080; // 2. 查找解码器并打开 var codec = ffmpeg.avcodec_find_decoder(codecParameters.codec_id); if (codec == null) throw new Exception("Decoder not found"); var codecContext = ffmpeg.avcodec_alloc_context3(codec); ffmpeg.avcodec_parameters_to_context(codecContext, codecParameters); ffmpeg.avcodec_open2(codecContext, codec, null); // 3. 读取裸流数据(Annex.B 格式:每个 NALU 以 0x00000001 开头) var packet = ffmpeg.av_packet_alloc(); var frame = ffmpeg.av_frame_alloc(); int frameCount = 0; using var fs = File.OpenRead(inputPath); while (true) { // 手动解析 Annex.B:找 0x00000001 分隔符 var nalStart = FindNextNalStart(fs); if (nalStart < 0) break; var nalSize = ReadNalSize(fs, nalStart); if (nalSize <= 0) break; fs.Seek(nalStart + 4, SeekOrigin.Begin); // 跳过 0x00000001 var nalData = new byte[nalSize]; fs.Read(nalData, 0, nalSize); ffmpeg.av_packet_from_data(packet, nalData, (uint)nalSize); // 解码 int ret = ffmpeg.avcodec_send_packet(codecContext, packet); if (ret < 0) continue; // 忽略错误包 while (ret >= 0) { ret = ffmpeg.avcodec_receive_frame(codecContext, frame); if (ret == ffmpeg.AVERROR(ffmpeg.EAGAIN) || ret == ffmpeg.AVERROR(ffmpeg.EINVAL)) break; if (ret < 0) continue; // 转换为 RGB24(YUV420p -> RGB24) var swsContext = ffmpeg.sws_getContext( frame.width, frame.height, (AVPixelFormat)frame.format, frame.width, frame.height, AVPixelFormat.AV_PIX_FMT_RGB24, SwsFilter.SWS_BILINEAR, null, null, null); var rgbFrame = ffmpeg.av_frame_alloc(); rgbFrame.width = frame.width; rgbFrame.height = frame.height; rgbFrame.format = (int)AVPixelFormat.AV_PIX_FMT_RGB24; ffmpeg.av_frame_get_buffer(rgbFrame, 32); ffmpeg.sws_scale(swsContext, frame.data, frame.linesize, 0, frame.height, rgbFrame.data, rgbFrame.linesize); // 保存 BMP(简化版,无压缩) SaveRgbFrameAsBmp(rgbFrame, $"{outputPath}{frameCount:D4}.bmp"); frameCount++; ffmpeg.av_frame_unref(rgbFrame); ffmpeg.av_frame_free(ref rgbFrame); ffmpeg.sws_freeContext(swsContext); } } ffmpeg.av_frame_free(ref frame); ffmpeg.av_packet_free(ref packet); ffmpeg.avcodec_free_context(ref codecContext); ffmpeg.avcodec_parameters_free(ref codecParameters); ffmpeg.avformat_free_context(formatContext); Console.WriteLine($"Saved {frameCount} frames."); // 辅助方法:查找下一个 NALU 起始位置(0x00000001) static long FindNextNalStart(FileStream fs) { var buf = new byte[4]; long pos = fs.Position; while (fs.Position < fs.Length - 3) { fs.Read(buf, 0, 4); if (buf[0] == 0 && buf[1] == 0 && buf[2] == 0 && buf[3] == 1) return fs.Position - 4; fs.Seek(-3, SeekOrigin.Current); } return -1; } // 辅助方法:读取当前 NALU 长度(简化,实际需解析 start code prefix + length) static int ReadNalSize(FileStream fs, long nalStart) { // 实际中需根据 Annex.B 规则计算,此处简化为读到下一个 start code 或 EOF fs.Seek(nalStart + 4, SeekOrigin.Begin); var start = fs.Position; while (fs.Position < fs.Length - 3) { var b0 = fs.ReadByte(); var b1 = fs.ReadByte(); var b2 = fs.ReadByte(); var b3 = fs.ReadByte(); if (b0 == 0 && b1 == 0 && b2 == 0 && b3 == 1) { fs.Seek(-4, SeekOrigin.Current); return (int)(fs.Position - start); } fs.Seek(-3, SeekOrigin.Current); } return (int)(fs.Length - start); } // 辅助方法:保存 RGB24 Frame 为 BMP(无头,仅像素数据,供验证用) static void SaveRgbFrameAsBmp(AVFrame* frame, string path) { var width = frame->width; var height = frame->height; var stride = frame->linesize[0]; var data = frame->data[0]; // BMP 文件头(54 字节)+ DIB header(40 字节)+ 像素数据 using var fs = new FileStream(path, FileMode.Create); // BMP 文件头 fs.Write(new byte[] { 0x42, 0x4D, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00 }); // 54 字节占位 // DIB Header (BITMAPINFOHEADER) fs.Write(BitConverter.GetBytes(40)); // size fs.Write(BitConverter.GetBytes(width)); fs.Write(BitConverter.GetBytes(height)); fs.Write(new byte[] { 0x01, 0x00 }); // planes fs.Write(new byte[] { 0x18, 0x00 }); // bit count = 24 fs.Write(new byte[24]); // compression, sizeImage, xPelsPerMeter, yPelsPerMeter, clrUsed, clrImportant // 像素数据(BMP 行倒序,每行 4 字节对齐) for (int y = height - 1; y >= 0; y--) { var row = (byte*)data + y * stride; fs.Write(row, 0, width * 3); // 补齐到 4 字节倍数 int padding = (4 - (width * 3) % 4) % 4; if (padding > 0) fs.Write(new byte[padding], 0, padding); } }这段代码的核心逻辑链是:裸流解析 → 手动构造 codec context → send/receive 模式解码 → sws_scale 转色 → BMP 二进制写入。它绕过了avformat_open_input(因为.264不是容器),直击 FFmpeg 最底层的解码管线。关键点在于avcodec_send_packet/avcodec_receive_frame的配对调用——这是 FFmpeg 3.0+ 推荐的现代解码模式,比旧式avcodec_decode_video2更安全、更可控。
3. FFmpeg.AutoGen 的核心参数与生命周期管理:为什么你的帧总为空?
FFmpeg.AutoGen 把 C API 原样暴露,意味着所有内存、引用计数、线程安全都由你负责。新手最常翻车的不是语法,而是生命周期错位:AVFrame未av_frame_unref导致内存泄漏;AVPacket数据被提前释放;swsContext复用未检查尺寸变化。本节拆解三个高频参数和两个必守规则。
3.1time_base:不是时间戳,而是“时间刻度尺”
AVCodecContext.time_base常被误认为“帧率倒数”,实则是解码器内部时间单位。例如 H.264 流中 PTS=100,若time_base={1, 1000},则真实时间为100 * 1/1000 = 0.1s。但AVRational结构体在 C# 中是int num, int den,必须用ffmpeg.av_q2d()转为 double 计算,不可直接num/den(整数除法归零):
// 错误!整数除法永远为 0 double tsSec = frame->pts * (double)codecContext->time_base.num / codecContext->time_base.den; // 正确!用 FFmpeg 官方转换函数 double tsSec = ffmpeg.av_q2d(codecContext->time_base) * frame->pts;提示:
av_q2d是唯一安全转换方式。time_base在不同流中差异极大(MPEG-TS 常为{1,90000},MP4 为{1,1000}),硬编码会导致时间戳错乱。
3.2AVFrame.format与sws_scale:YUV 到 RGB 的陷阱
AVFrame.format返回的是AVPixelFormat枚举值(如AV_PIX_FMT_YUV420P),但sws_getContext的第三个参数必须是目标像素格式的整数值,而非枚举名:
// 错误!AVPixelFormat.AV_PIX_FMT_RGB24 是枚举,sws_getContext 要 int var ctx = ffmpeg.sws_getContext(w, h, (int)AVPixelFormat.AV_PIX_FMT_YUV420P, w, h, AVPixelFormat.AV_PIX_FMT_RGB24, ...); // 正确!显式转换为 int var ctx = ffmpeg.sws_getContext(w, h, (int)AVPixelFormat.AV_PIX_FMT_YUV420P, w, h, (int)AVPixelFormat.AV_PIX_FMT_RGB24, ...);更隐蔽的坑是sws_getContext缓存:同一尺寸/格式组合应复用 context,否则性能暴跌。但若frame->width或frame->height在解码中动态变化(如分辨率切换流),必须销毁旧 context 并重建,否则sws_scale输出全黑。
3.3AVPacket.data的所有权:谁 malloc,谁 free?
av_packet_from_data创建的 packet,其data指针指向你传入的托管数组,FFmpeg不接管内存。这意味着:
- 若你用
stackalloc或Marshal.AllocHGlobal分配内存,必须手动Marshal.FreeHGlobal - 若你用
new byte[],则packet.data指向托管堆,绝不可在 packet 释放后继续访问该数组 - 最佳实践:用
av_packet_alloc()+av_packet_unref()管理,数据由 FFmpeg 内部分配
// 推荐:让 FFmpeg 管理内存 var packet = ffmpeg.av_packet_alloc(); ffmpeg.av_packet_from_data(packet, nalData, (uint)nalSize); // nalData 可为栈或托管数组 // ... 使用 packet ... ffmpeg.av_packet_unref(packet); // 自动释放 data 内存(如果由 FFmpeg 分配) ffmpeg.av_packet_free(ref packet);注意:
av_packet_unref仅释放data,不释放packet结构体本身;av_packet_free才释放结构体。漏调任一者都会内存泄漏。
4. 避坑指南:FFmpeg.AutoGen 项目中血泪总结的 4 个致命问题
FFmpeg.AutoGen 的报错信息极其简陋(AccessViolationException、NullReferenceException),往往掩盖了底层 C 层的真实错误。以下是我在三个工业项目中踩出的共性坑,按现象→原因→解决排列,每条都附带gdb或WinDbg验证过的证据。
4.1 现象:avcodec_open2返回负数,avcodec_find_decoder却返回非 null
原因:AVCodecContext中codec_id设置正确,但codec_type未设或设错。FFmpeg 解码器注册时按codec_type分组,codec_type=AVMEDIA_TYPE_UNKNOWN会导致avcodec_open2拒绝初始化。
解决:在avcodec_parameters_to_context后,强制设置codecContext->codec_type = AVMediaType.AVMEDIA_TYPE_VIDEO(即使参数已含)。
4.2 现象:avcodec_receive_frame总返回EAGAIN,但avcodec_send_packet成功
原因:输入 packet 的flags未清零。某些裸流 parser 会设置AV_PKT_FLAG_KEY,而avcodec_send_packet对 flag 敏感,若前一 packet 的 flag 残留,会导致解码器状态混乱。
解决:每次av_packet_from_data后,显式置零packet->flags = 0。
4.3 现象:sws_scale输出全黑或花屏,av_frame_get_buffer返回 0
原因:AVFrame的linesize数组未初始化。sws_scale依赖frame->linesize[0]计算行宽,若为 0 则写入越界。av_frame_get_buffer会填充linesize,但前提是frame->width/frame->height/frame->format已正确设置。
解决:在av_frame_get_buffer前,确保frame->width、frame->height、frame->format全部赋值,且frame->format是有效AVPixelFormat值(非 -1)。
4.4 现象:程序在avformat_close_input后崩溃,调用栈显示avio_close访问野指针
原因:AVFormatContext的pb字段(AVIOContext*)被提前释放。常见于手动avio_open后未配对avio_close,或avformat_open_input失败后未检查formatContext->pb是否为 null 就调用avformat_close_input。
解决:avformat_close_input内部会检查pb并安全关闭,但必须确保传入的formatContext指针本身有效。调用前加判空:
if (formatContext != null && formatContext->nb_streams > 0) ffmpeg.avformat_close_input(&formatContext);提示:所有
av_*_free类函数(av_frame_free,av_packet_free)都接受null指针,但avformat_close_input不接受null—— 这是 FFmpeg C API 的不一致设计,必须手写防护。
5. 进阶技巧:用 FFmpeg.AutoGen 实现低延迟推流(对标 SRS 场景)
标题里提到的“ffmpeg推流到srs存在延迟”,本质是 FFmpeg 默认的缓冲策略(AVFMT_FLAG_GENPTS、AVFMT_FLAG_NOBUFFER)与 RTMP 协议栈的交互问题。FFmpeg.AutoGen 因直通 C API,反而能绕过高层封装的“智能优化”,实现真正可控的推流链路。以下是以librtmp为 backend 的最小延迟推流方案(非ffmpeg.exe命令行,而是 C# 原生调用)。
5.1 关键配置:禁用所有缓冲,强制 PTS 重映射
RTMP 推流延迟主要来自三处:AVFormatContext的max_delay、AVCodecContext的rc_max_rate、以及AVPacket的dts/pts时间戳精度。必须全部收紧:
// 创建输出上下文(RTMP URL) var ofmt = ffmpeg.av_guess_format("flv", null, null); var ofmtCtx = ffmpeg.avformat_alloc_output_context2(null, ofmt, null, "rtmp://127.0.0.1:1935/live/stream"); // 关键:禁用格式层缓冲 ofmtCtx->oformat->flags |= AVFMT_NOFILE | AVFMT_NOBUFFER; ofmtCtx->max_delay = 0; // 微秒级,设为 0 强制立即发送 // 创建视频流 var vst = ffmpeg.avformat_new_stream(ofmtCtx, null); vst->id = ofmtCtx->nb_streams - 1; // 配置编码器(H.264) var enc = ffmpeg.avcodec_find_encoder(AVCodecID.AV_CODEC_ID_H264); var encCtx = ffmpeg.avcodec_alloc_context3(enc); encCtx->codec_type = AVMediaType.AVMEDIA_TYPE_VIDEO; encCtx->width = 1280; encCtx->height = 720; encCtx->time_base = (AVRational){1, 1000}; // 强制 1ms 刻度 encCtx->framerate = (AVRational){30, 1}; encCtx->pix_fmt = AVPixelFormat.AV_PIX_FMT_YUV420P; encCtx->bit_rate = 2000000; encCtx->rc_buffer_size = 4000000; encCtx->rc_max_rate = 2000000; encCtx->rc_min_rate = 2000000; // 关键:禁用 B 帧(B帧增加延迟) encCtx->max_b_frames = 0; encCtx->gop_size = 30; encCtx->keyint_min = 30; // 打开编码器 ffmpeg.avcodec_open2(encCtx, enc, null); // 将编码器参数复制到流 ffmpeg.avcodec_parameters_from_context(vst->codecpar, encCtx); // 打开输出 IO(RTMP) if (!(ofmtCtx->oformat->flags & AVFMT_NOFILE)) { var ioOpenRet = ffmpeg.avio_open(&ofmtCtx->pb, ofmtCtx->url, AVIO_FLAG_WRITE); if (ioOpenRet < 0) throw new Exception($"avio_open failed: {ioOpenRet}"); }5.2 时间戳重映射:用av_rescale_q消除累积误差
原始帧的pts来自采集设备(如 V4L2),其time_base可能为{1,1000000}(微秒),而 RTMP 要求{1,1000}(毫秒)。直接赋值会导致时间戳跳跃。必须用av_rescale_q精确转换:
// 假设采集帧 pts = 123456789(单位:微秒),采集 time_base = {1,1000000} long rawPts = 123456789; AVRational rawTimeBase = new AVRational { num = 1, den = 1000000 }; AVRational rtmpTimeBase = new AVRational { num = 1, den = 1000 }; // 精确重映射(避免整数截断) long rtmpPts = ffmpeg.av_rescale_q(rawPts, rawTimeBase, rtmpTimeBase); packet->pts = rtmpPts; packet->dts = rtmpPts; packet->duration = ffmpeg.av_rescale_q(1, rawTimeBase, rtmpTimeBase); // 1 帧时长5.3 强制立即发送:绕过av_interleaved_write_frame的队列
av_interleaved_write_frame会按 DTS 排序并缓冲 packet,增加延迟。对单视频流,可用av_write_frame强制直发:
// 编码后得到 packet int ret = ffmpeg.avcodec_receive_packet(encCtx, packet); if (ret >= 0) { // 重映射时间戳(见上节) packet->pts = ffmpeg.av_rescale_q(packet->pts, encCtx->time_base, vst->time_base); packet->dts = ffmpeg.av_rescale_q(packet->dts, encCtx->time_base, vst->time_base); packet->duration = ffmpeg.av_rescale_q(packet->duration, encCtx->time_base, vst->time_base); packet->stream_index = vst->index; // 关键:不用 interleaved,直发! ret = ffmpeg.av_write_frame(ofmtCtx, packet); if (ret < 0) Console.WriteLine($"av_write_frame failed: {ret}"); ffmpeg.av_packet_unref(packet); }血泪经验:此方案在千兆内网下可将端到端延迟压至 200ms 以内(采集→编码→RTMP→SRS→播放器)。但必须确保
av_write_frame不阻塞——若网络抖动,需自行实现超时重试,不可依赖 FFmpeg 内置重传(它会加剧延迟)。
我做第一个工业视觉检测项目时,客户要求“从相机触发到画面显示 ≤ 300ms”,试过FFmpeg.NET、AForge.NET,全在 600ms+。最后用 FFmpeg.AutoGen 手撕管线,砍掉所有中间层,才达标。那段时间每天用WinDbg跟avcodec_send_packet的汇编,就为确认一行mov rax, [rdi+8]没越界。现在回头看,FFmpeg.AutoGen 不是银弹,它是手术刀——你得知道切哪、怎么止血、缝合用几号线。希望帮到你。
本文还有配套的精品资源,点击获取