
简介面向C#开发者的FFmpeg集成示例演示借助FFmpeg.AutoGen在.NET环境中调用FFmpeg API完成音视频文件打开、流信息读取、解码器选择与解码等基础操作解决托管代码与原生库之间的互操作难题。包内共148个文件以111个C头文件、16个DLL动态库为主体并包含8个C#源码、3个可执行程序以及配置、工程解决方案等材料整体约30.25MB可直接用于对照学习或二次开发。示例工程覆盖Windows窗体播放器界面、FFmpeg环境路径辅助类、RTMP测试等实用模块配套FFmpeg 3.4版本的头文件与库文件便于开发者快速搭建C#多媒体处理项目。已有2104人学习下载适合需要入门音视频处理或在C#中集成FFmpeg的初中级开发者参考。1. 从进程调用到 API 直调FFmpeg.AutoGen 这个演示项目解决了什么以前我在 C# 里处理视频第一反应就是 Process.Start 拉起一个 ffmpeg.exe 子进程。命令好写但帧级操作完全失控预览要先落盘临时文件批量转码时进程反复创建销毁开销全耗在进程切换上。后来拿到一套 FFmpeg.AutoGen 的演示项目才发现 ffmpeg 原生 API 能在 C# 进程内直接调用AutoGen 是 C API 的 P/Invoke 绑定层C# 通过它直接操纵 AVFormatContext、AVCodecContext把每一帧抓回来自己处理。这套 demo 把视频解码、转码封装、音频重采样、自定义内存流解码全串了起来代码风格是“能跑起来优先”很适合做上位机视频接入和 WPF/WinForm 内嵌预览的开发者。下面从环境搭建讲到内存管理每一步我都实际踩过坑会一并说明。2. 环境搭建与初始化陷阱版本匹配和 RootPath 的加载顺序2.1 为什么选 AutoGen直接绑定与进程内调用的取舍在 C# 里接 FFmpeg 有三条路命令行子进程、C 封装 DLL、AutoGen 这类自动绑定库。命令行最省心但每一帧都要经过“编码→落盘→回读”的路径做实时预览延迟感人手写 C 封装要维护 native 工程跨平台编译头大AutoGen 是直接把 ffmpeg 头文件生成成 C# 结构体和函数签名省掉中间层。这三条路的取舍我用一个表格说明方式帧级控制进程开销开发成本适用场景ffmpeg 命令行无高低一次性转码、切片C 封装 DLL有低高长周期商业项目FFmpeg.AutoGen有低中原型验证、上位机集成、定制播放器我选 AutoGen 的原因很实际第一进程内调用省掉了序列化和临时文件一帧视频从解码到交给 WPF 显示能控制在毫秒级第二AutoGen 是自动生成的FFmpeg 升级后跑一遍生成器就能跟上不用手改签名。代价是要自己管 native 内存稍不注意就泄漏这一点后面专门讲。2.2 NuGet 包选择与 FFmpeg 动态库放置规范AutoGen 的 NuGet 包版本号和 FFmpeg 主版本严格对应用 6.0.x 的包就配 FFmpeg 6.0 的动态库混用大概率在运行时崩在某个结构体读取上。下载 FFmpeg 动态库时注意选与目标进程位数一致的构建C# 工程是 x64 就下 x64 版本AnyCPU 但运行在 x64 系统上也得 x64。我一开始图省事下了 x86 的共享库结果 avformat_open_input 直接返回负错误码查了半天才知道是位数不匹配。动态库有三类文件必须放在 ffmpeg.RootPath 指向的目录avformat-XX.dll、avcodec-XX.dll、avutil-XX.dll。做转码还要 swscale-XX.dll 和 swresample-XX.dll。文件名里带版本号是 FFmpeg 的约定Linux 下对应 libavformat.so.XXAutoGen 按不带版本号的库名去加载通过 RootPath 和 DllImportResolver 才能正确解析。# Windows 下动态库最小集合 C:\ffmpeg\bin\avformat-60.dll C:\ffmpeg\bin\avcodec-60.dll C:\ffmpeg\bin\avutil-58.dll C:\ffmpeg\bin\swscale-7.dll C:\ffmpeg\bin\swresample-4.dll如果拉的是完整版 FFmpeg 发行包bin 目录里还有 avfilter、avdevice、postproc这些不是 AutoGen 初始化必需项。真正让新手翻车的是拷 DLL 时只拷了上表五个文件却漏了 VC 运行库avcodec-60.dll 加载失败会抛 DllNotFoundException 或“无法定位程序输入点”。遇到这个先查系统是否装了对应的 VC Redistributable。2.3 初始化顺序avformat / avcodec / swscale 的最小闭环AutoGen 初始化有个核心原则先配运行时路径再做任何 API 调用。RootPath 设置晚一步所有 P/Invoke 都会在第一次调用时崩掉。看一下项目里最常见的初始化代码using FFmpeg.AutoGen; unsafe { // 必须最先设置AutoGen 内部靠 DllImportResolver 解析 ffmpeg 函数 ffmpeg.RootPath C:\ffmpeg\bin; ffmpeg.av_log_set_level(ffmpeg.AV_LOG_INFO); AVFormatContext* pFmtCtx null; int ret ffmpeg.avformat_open_input(ref pFmtCtx, test.mp4, null, null); if (ret 0) throw new InvalidOperationException($无法打开文件错误码 {ret}); ret ffmpeg.avformat_find_stream_info(pFmtCtx, null); Console.WriteLine($时长{pFmtCtx-duration / 1000000} 秒流数量{pFmtCtx-nb_streams}); }这段代码里 AVFormatContext 是双指针引用传递avformat_open_input 内部会给 pFmtCtx 分配内存用完必须调 avformat_close_input 释放。duration 的单位是 AV_TIME_BASE也就是 1e6 微秒所以除 1000000 才是秒数。avformat_find_stream_info 会读一部分数据去探测流信息这一步不是可选项后续拿 codecpar 的宽高和时间基都依赖它。提示在较老版本的 AutoGen 里还要手动调 ffmpeg.av_register_all()。新版 FFmpeg 已经自动注册所有复用器和编解码器再调用会报符号找不到直接用新版就好。3. 视频解码全流程从 AVPacket 到 Bitmap 的一段完整代码3.1 avformat_open_input 打开文件并定位视频流打开文件后第一件事是遍历 streams 数组找到视频流索引。重点看 codecpar 里的 codec_type、codec_id 和宽高参数。下面这段定位视频流的代码是标准做法int videoIndex -1; for (int i 0; i pFmtCtx-nb_streams; i) { AVCodecParameters* codecParams pFmtCtx-streams[i]-codecpar; if (codecParams-codec_type AVMediaType.AVMEDIA_TYPE_VIDEO) { videoIndex i; Console.WriteLine($视频流 {i}{codecParams-width} x {codecParams-height}编码 {codecParams-codec_id}); break; } } if (videoIndex -1) throw new InvalidOperationException(未找到视频流);AVStream 的 codecpar 在打开输入后就已经填充好不需要自己去解析。codec_id 枚举值区分编码格式AV_CODEC_ID_H264、AV_CODEC_ID_HEVC、AV_CODEC_ID_MPEG4 是常见的几种。拿到 codec_id 后用它去找解码器AVCodec* decoder ffmpeg.avcodec_find_decoder(pFmtCtx-streams[videoIndex]-codecpar-codec_id); if (decoder null) throw new InvalidOperationException(找不到对应解码器); AVCodecContext* codecCtx ffmpeg.avcodec_alloc_context3(decoder); ffmpeg.avcodec_parameters_to_context(codecCtx, pFmtCtx-streams[videoIndex]-codecpar); int openRet ffmpeg.avcodec_open2(codecCtx, decoder, null); if (openRet 0) throw new InvalidOperationException(解码器打开失败);avcodec_alloc_context3 分配出的上下文是空的必须用 avcodec_parameters_to_context 把 codecpar 里的宽高、像素格式、时间基拷进去再 avcodec_open2 才有效。这一步容易漏漏了会导致 avcodec_open2 返回失败或解码出来全是花屏。avcodec_find_decoder 返回的 AVCodec 是只读的不要尝试释放它。3.2 send_packet 与 receive_frame 的状态机新版 FFmpeg 解码用 avcodec_send_packet 喂数据、avcodec_receive_frame 取帧和旧版 avcodec_decode_video2 完全不同。这个 API 是状态机模型同一个 AVPacket 喂进去可能要调多次 receive_frame 才能取完也可能喂进去返回 EAGAIN 表示内部缓冲满了要先往外取帧再继续喂。AVPacket* packet ffmpeg.av_packet_alloc(); AVFrame* frame ffmpeg.av_frame_alloc(); while (ffmpeg.av_read_frame(pFmtCtx, packet) 0) { if (packet-stream_index ! videoIndex) { ffmpeg.av_packet_unref(packet); continue; } int sendRet ffmpeg.avcodec_send_packet(codecCtx, packet); // send 返回 0 不代表有帧产出要循环 receive while (sendRet 0 ffmpeg.avcodec_receive_frame(codecCtx, frame) 0) { // 这里拿到一帧 YUV 原始数据 ProcessFrame(frame, codecCtx); ffmpeg.av_frame_unref(frame); } ffmpeg.av_packet_unref(packet); } // 解码器内部可能还有残留帧需要 flush ffmpeg.avcodec_send_packet(codecCtx, null); while (ffmpeg.avcodec_receive_frame(codecCtx, frame) 0) ProcessFrame(frame, codecCtx);av_packet_alloc 分配的 packet 每次被 av_read_frame 填充后必须有 av_packet_unref 释放内部引用。av_frame_unref 同理一帧数据处理完必须复位。最后那段 avcodec_send_packet(codecCtx, null) 是 flush 操作告诉解码器“数据喂完了”把剩余的 B 帧和延时帧输出。漏掉 flush 会导致视频末尾少几帧肉眼看不出来但如果做逐帧对比会发现丢帧。3.3 swscale 像素格式转换参数详解解码器输出的一般是 YUV420P而 WinForm 的 Bitmap 要 RGB 或 BGR。转换用 sws_getContext 创建 SWS 上下文参数顺序容易搞混前三个是输入宽、高、像素格式后三个是输出目标。目标格式用 BGR24 而不是 RGB24因为 GDI 的 Bitmap 按 BGR 存储用 RGB24 会看到红蓝互换。SwsContext* swsCtx ffmpeg.sws_getContext( codecCtx-width, codecCtx-height, codecCtx-pix_fmt, codecCtx-width, codecCtx-height, AVPixelFormat.AV_PIX_FMT_BGR24, ffmpeg.SWS_BILINEAR, null, null, null);SWS_BILINEAR 是双线性插值速度和画质比较均衡。做缩放到更小尺寸用 SWS_FAST_BILINEAR 更快放大画质要求高用 SWS_LANCZOS。缩放时输出宽高改成目标值即可上下文内部会处理滤波。sws_getContext 返回的上下文复用于所有帧不要每帧创建。3.4 帧转 Bitmap 的正确拷贝姿势AVFrame 的 data 字段在 AutoGen 里按 data0、data1……命名linesize0 对应每行字节数。YUV420P 有三块平面但转换成 BGR24 后只有一块平面按行拷贝即可private Bitmap FrameToBitmap(AVFrame* frame, int width, int height, SwsContext* swsCtx) { byte* rgbBuffer (byte*)ffmpeg.av_malloc((ulong)(width * height * 3)); byte*[] srcData { rgbBuffer, null, null, null }; int[] srcStride { width * 3, 0, 0, 0 }; fixed (byte** dstData srcData) fixed (int* dstStride srcStride) { ffmpeg.sws_scale(swsCtx, frame-data0, frame-linesize0, 0, height, dstData, dstStride); } var bmp new Bitmap(width, height, PixelFormat.Format24bppRgb); var rect new Rectangle(0, 0, width, height); var bmpData bmp.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb); try { byte* src rgbBuffer; byte* dst (byte*)bmpData.Scan0.ToPointer(); // GDI 行数据可能带对齐填充必须按 stride 逐行拷 for (int y 0; y height; y) { Buffer.MemoryCopy(src y * width * 3, dst y * bmpData.Stride, (long)bmpData.Stride, (long)width * 3); } } finally { bmp.UnlockBits(bmpData); ffmpeg.av_free(rgbBuffer); } return bmp; }这里两个关键点第一Bitmap 的 Stride 不一定等于 width×3GDI 为了内存对齐会填充额外字节直接 Copy 整块会错位必须逐行拷贝第二av_malloc 出来的 rgbBuffer 用完必须 av_free否则每帧泄漏 3×宽×高 字节4K 视频跑几分钟就是几百 MB 内存涨上去了。提示如果项目里频繁转帧建议用 ArrayPool 或预分配一块固定 rgbBuffer 复用别每帧都 av_malloc/av_freeGC 压力会小很多。这个优化思路后面第 6 章还会继续讲。4. 转码输出音频重采样和 libx264 封装参数实战4.1 创建输出上下文与编码器绑定解码走上以后转码就是把这个链路反过来调读帧 → 缩放 → 编码 → 写文件。首先创建输出上下文AVFormatContext* outCtx null; ffmpeg.avformat_alloc_output_context2(ref outCtx, null, mp4, output.mp4); AVStream* outStream ffmpeg.avformat_new_stream(outCtx, null);avformat_alloc_output_context2 的第三个参数指定封装格式传 mp4 可以省去手动匹配 muxer。avformat_new_stream 会创建一个空白流接下来要把编码器参数填进去。注意这里和输入流的处理不一样输出流的编解码器参数是从新创建的编码器上下文中拷贝过去的。AVCodec* encoder ffmpeg.avcodec_find_encoder(AVCodecID.AV_CODEC_ID_H264); AVCodecContext* encCtx ffmpeg.avcodec_alloc_context3(encoder); encCtx-width outWidth; encCtx-height outHeight; encCtx-pix_fmt AVPixelFormat.AV_PIX_FMT_YUV420P; encCtx-time_base new AVRational { num 1, den 25 }; encCtx-framerate new AVRational { num 25, den 1 }; encCtx-bit_rate 2_000_000; int ret ffmpeg.avcodec_open2(encCtx, encoder, null); ffmpeg.avcodec_parameters_from_context(outStream-codecpar, encCtx);这里有个编码器特有的大坑time_base 要设成分数的形式 AVRational不是 double。25fps 就是 num1、den25。bit_rate 设的是平均码率2000000 是 2 Mbps1080p 网络视频用这个区间合理本地存档可以调高到 8 Mbps。关键点在最后一行 avcodec_parameters_from_context编码器参数必须在这个时点同步给输出流avformat_write_header 会依赖这些信息去写文件头。4.2 解码帧如何喂给编码器读输入视频帧后不能直接送编码器因为解码输出是 YUV420P编码器输入一般也是 YUV420P但分辨率可能不一致。需要 sws_scale 先把帧缩放到 outWidth/outHeight。别贪图省事在 sws_getContext 创建时输出尺寸写原分辨率那样编码器收到的帧宽高不对写出来的文件播放器会撕裂或报错。// 每帧处理先缩放再送编码 SwsContext* encSws ffmpeg.sws_getContext( decCtx-width, decCtx-height, decCtx-pix_fmt, outWidth, outHeight, AVPixelFormat.AV_PIX_FMT_YUV420P, ffmpeg.SWS_BILINEAR, null, null, null); byte* encBuf (byte*)ffmpeg.av_malloc((ulong)(outWidth * outHeight * 3 / 2)); byte*[] encData { encBuf, encBuf outWidth * outHeight, null, null }; int[] encStride { outWidth, outWidth / 2, 0, 0 }; fixed (byte** dstData encData) fixed (int* dstStride encStride) { ffmpeg.sws_scale(encSws, decFrame-data0, decFrame-linesize0, 0, decCtx-height, dstData, dstStride); }YUV420P 的存储结构是三平面Y 平面大小是 width×heightU 和 V 平面各是 width/2×height/2。所以输出缓冲总大小是 1.5 倍像素数U 平面偏移在 Y 平面之后。手动分配这三块时要按这个偏移关系算否则编码器拿到的是花屏。如果图省事也可以用 sws_scale 的输出数组直接指向编码器内部的 frame buffer但多数人还是习惯自己管一份干净数据避免踩 frame 生命周期问题。4.3 音频重采样 SwrContext 参数配置音频转码如果不做重采样会遇到三处不匹配采样率、通道数、采样格式。比如输入是 44100Hz 双声道 float输出目标 AAC 一般要 48000Hz 双声道 fltp。这一步要用 swr 库初始化参数烦人注意别用错函数SwrContext* swrCtx ffmpeg.swr_alloc(); ffmpeg.av_opt_set_int(swrCtx, in_sample_rate, 44100, 0); ffmpeg.av_opt_set_int(swrCtx, in_channel_layout, (long)AVChannelLayout.AV_CH_LAYOUT_STEREO, 0); ffmpeg.av_opt_set_sample_fmt(swrCtx, in_sample_fmt, AVSampleFormat.AV_SAMPLE_FMT_FLT, 0); ffmpeg.av_opt_set_int(swrCtx, out_sample_rate, 48000, 0); ffmpeg.av_opt_set_int(swrCtx, out_channel_layout, (long)AVChannelLayout.AV_CH_LAYOUT_STEREO, 0); ffmpeg.av_opt_set_sample_fmt(swrCtx, out_sample_fmt, AVSampleFormat.AV_SAMPLE_FMT_FLTP, 0); ffmpeg.swr_init(swrCtx);用 av_opt_set 系列而不是 swr_alloc_set_opts是因为新版 FFmpeg 把 swr_alloc_set_opts 标记为弃用AutoGen 对应生成的函数签名可能不是你熟悉的那个用 av_opt_set 是跨版本更稳的写法。channel_layout 是 64 位位掩码双声道就是 AV_CH_LAYOUT_STEREO。重采样本身是变采样率的过程输入和输出的样本数不同每次 swr_convert 之前要算好输出缓冲能装多少样本int outSamples ffmpeg.swr_get_out_samples(swrCtx, inSamples); byte* outBuffer (byte*)ffmpeg.av_malloc((ulong)(outSamples * 2 * 4)); int converted ffmpeg.swr_convert(swrCtx, outBuffer, outSamples, (byte**)inFrame-data0, inSamples);采样格式 float 是每个样本 4 字节双声道就是 8 字节每帧。转换后 converted 返回实际生成的样本数写文件时要按这个数量写按 outSamples 写会把尾部垃圾数据也写进去音频结尾会有爆音。4.4 收尾顺序flush 与资源释放转码循环结束后编码器内部还堆着若干帧必须 flush。和 decoder 一样avcodec_send_packet 传 null 触发// flush 编码器 ffmpeg.avcodec_send_packet(encCtx, null); AVPacket* outPacket ffmpeg.av_packet_alloc(); while (ffmpeg.avcodec_receive_packet(encCtx, outPacket) 0) { ffmpeg.av_interleaved_write_frame(outCtx, outPacket); ffmpeg.av_packet_unref(outPacket); } // 写文件尾 ffmpeg.av_write_trailer(outCtx); ffmpeg.avio_close(outCtx-pb); ffmpeg.avformat_free_context(outCtx);av_write_trailer 有讲究它要在 avio_close 之前调用因为 trailer 要写索引和时长信息。释放顺序反过来先释放编码器再释放输出上下文ffmpeg.avcodec_free_context(ref encCtx); ffmpeg.av_packet_free(ref outPacket);avcodec_free_context 接收的是指针的引用释放后会置空防止野指针。avio_close 则是把文件句柄关掉如果之前没有手动 flush trailerFFmpeg 在 avio_close 时会尝试帮你补写但那时有些 muxer 的数据会被写得不完整。5. 避坑指南C# 与 FFmpeg 原生层的七个典型翻车现场5.1 DllNotFoundExceptionRootPath 配置的三种根源现象程序在 ffmpeg.avformat_open_input 调用处抛 DllNotFoundException提示找不到 avformat-60.dll。原因有三个一是 RootPath 设置晚了AutoGen 的 DllImportResolver 在第一次调用前就要能定位库文件二是动态库位数不对x86 进程去加载 x64 的 DLL三是依赖链断裂avformat.dll 依赖 avutil.dll少一个就整体加载失败。解决把 RootPath 放在程序入口最开始Main 或 App 构造函数第一行。用 Environment.Is64BitProcess 判断位数日志打印当前 RootPath 指向的目录列出目录下所有 dll 文件名核对版本。我后来写了个启动自检函数扫描目录里 avformat-*.dll 是否存在不存在直接给出明确报错省得用户摸黑排查。5.2 avcodec_send_packet 返回 EAGAIN这不是错误是状态机现象某些视频解码时报 EAGAIN误判为失败直接 break 退出循环导致只解出来前面几帧。原因EAGAIN 是 FFmpeg 解码 API 的正常返回表示解码器内部缓冲满了需要先 avcodec_receive_frame 取出帧才能继续喂。很多从旧版 avcodec_decode_video2 迁移过来的代码还带着“失败就退出”的习惯。解决send_packet 返回 EAGAIN 时继续调 receive_frame 直到返回小于 0然后再回来喂下一个 packet。最简单稳妥的循环长这样外层 av_read_frame内层先调 send 再循环 receive。send 返回 EAGAIN 不退出而是内层 receive 跑完一轮后再 send 一次。参考 3.2 的代码结构就不会踩。5.3 花屏或红蓝互换像素格式匹配问题现象解码后转 Bitmap 显示出来整体偏红或草地变橙色部分行出现错位。原因第一类是 GDI 的 24bpp 位图和 swscale 输出用的 RGB24 还是 BGR24 不一致这是最常见的红蓝互换第二类是 Bitmap 的 Stride 做了字节对齐直接整块拷贝导致每行错位偏移。解决sws_getContext 输出格式传 AV_PIX_FMT_BGR24位图 LockBits 时也用 Format24bppRgb两者就对齐了。拷贝时按行循环目标偏移用 bmpData.Stride源偏移用 width×3别用同一个 stride 值。4K 分辨率下 Stride 一般是 7680width×3 是 7680 没错但 1081 这类奇数宽度就会出现 4 字节填充肉眼可见的竖纹。5.4 转码输出文件时长不对漏了 flush现象转码出来的视频比源文件短了约 0.5 到 1 秒最后一帧卡住不动或直接消失。原因H.264 编码器有 B 帧重排序解码端要等后面的帧才能输出前面的帧。最后一个 packet 送进去后编码器内部还压着几帧没写出来。不 flush 就直接 av_write_trailer这几帧永远丢了。解决编码器收尾必须 avcodec_send_packet(encCtx, null)然后循环 receive_packet 直到返回 AVERROR_EOF。顺序是flush 编码器 → 写一个空帧标记 → av_write_trailer → avio_close。我踩这个坑是拿一个 3 秒测试视频转码输出只有 2.4 秒对比帧数才发现少了 15 帧。5.5 内存泄漏每帧涨几 MB跑几分钟就爆内存现象进程内存持续上升4K 视频解码分析内存能到 2GB 以上。原因AVPacket 和 AVFrame 都带引用计数内部管理着多个缓冲区。只调 av_packet_free 或 av_frame_free忘了在每帧处理完后 av_frame_unref / av_packet_unref引用计数永远不会归零。C# 的 GC 管不到 native 内存这些对象不会被回收。解决掌握一个原则av_read_frame 填的 packet要么手动 unref要么送进解码器后 unref解码器返回的 frame处理完就 unref。每次 av_frame_alloc 对应一次 av_frame_free中间每次接收帧对应一次 unref。用 try/finally 把 unref 包住异常路径也不漏。项目里我习惯把“取帧→处理→释放”封装成一个 using 结构native 资源的释放逻辑和业务逻辑分开。5.6 编码器参数对不上码率控制 crf 在哪里设现象直接给 encCtx-bit_rate 赋值后转码文件码率波动大画质忽好忽坏或者某些播放器报参数不支持。原因libx264 的码率控制模式有多种bit_rate 是平均码率模式crf 是画质优先模式。FFmpeg CLI 里的 -crf 参数映射到 API 上不是 AVCodecContext 的直接字段而是 x264 的私有选项要往 priv_data 里传。解决回退到比特率模式就用 encCtx-bit_rate想用 crf 模式要同时设置全局标志和私有选项encCtx-flags | ffmpeg.AV_CODEC_FLAG_QSCALE; ffmpeg.av_opt_set_int(encCtx-priv_data, crf, 23, 0); ffmpeg.av_opt_set(encCtx-priv_data, preset, medium, 0);crf 取值范围 0 到 5118 到 28 是视觉无损到中等质量的区间。preset 影响编码速度和压缩率的平衡veryfast 编码快但文件大placebo 慢但压得小实际项目一般用 medium 或 slow。C# 里传字符串用 av_opt_set传数字用 av_opt_set_int别搞混。6. 进阶自定义 AVIO 回调从内存 Byte[] 直接解码不落盘6.1 设置 AVIOContext 读取函数指针很多业务场景下视频不是从磁盘文件读的而是从网络流、数据库 BLOB 或接口返回的 Byte[] 里拿到的。默认的 avformat_open_input 只认文件路径或 URL要处理内存数据就得让 FFmpeg 走自定义 IO。核心是给 AVFormatContext 挂一个自己实现的读回调。unsafe { // 假设 data 是完整或分片到达的 MP4 字节流 byte[] videoBytes File.ReadAllBytes(inline.mp4); // 分配内部缓冲FFmpeg 会通过 read 回调来填充它 byte* buffer (byte*)ffmpeg.av_malloc(4096); // 用 GCHandle 把 C# 的 byte[] 钉住避免 GC 移动导致指针失效 GCHandle handle GCHandle.Alloc(videoBytes, GCHandleType.Pinned); void* opaque (void*)handle.AddrOfPinnedObject().ToPointer(); AVIOContext* avioCtx ffmpeg.avio_alloc_context( buffer, 4096, 0, opaque, ReadPacket, null, null); AVFormatContext* fmtCtx ffmpeg.avformat_alloc_context(); fmtCtx-pb avioCtx; fmtCtx-flags | ffmpeg.AVFMT_FLAG_CUSTOM_IO; // 自定义 IO 时url 传占位符即可函数内部走 ReadPacket 回调 int ret ffmpeg.avformat_open_input(ref fmtCtx, memory:, null, null); if (ret 0) ffmpeg.avformat_find_stream_info(fmtCtx, null); }ReadPacket 的签名要匹配 FFmpeg 的函数指针类型四个参数分别是 opaque、目标缓冲区、请求大小返回值是实际读取字节数。0 表示文件尾负数表示读失败。[UnmanagedFunctionPointer(CallingConvention.Cdecl)] private unsafe delegate int ReadPacketDelegate(void* opaque, byte* buf, int bufSize); private static unsafe int ReadPacket(void* opaque, byte* buf, int bufSize) { // opaque 指向 GCHandle需要转回 IntPtr 再取目标 byte[] GCHandle handle GCHandle.FromIntPtr((IntPtr)opaque); byte[] source (byte[])handle.Target; // 这里要维护读取位置简单实现用静态字段或放在自定义结构里 if (_streamPos source.Length) return 0; // EOF int count Math.Min(bufSize, source.Length - _streamPos); Marshal.Copy(source, _streamPos, (IntPtr)buf, count); _streamPos count; return count; }6.2 委托生命周期与缓冲区管理习惯这个设计中最容易被忽视的是委托对象不能被 GC 回收。avio_alloc_context 里传入的 ReadPacket 是托管委托如果没有任何托管引用GC 可能随时把它回收掉再触发读回调时进程直接崩溃。我把这个委托声明为类静态字段持有并且在 AVIOContext 释放前都保持有效。从那以后我每次写 AutoGen 相关代码都强制走一遍相同的自检流程版本号核对、RootPath 日志、位数检测、flush 补帧、unref 兜底。这套流程养成了习惯后来做实时流推送遇到同类问题基本都能在五分钟内定位。FFmpeg.AutoGen 的边界就在那里——入门难在 P/Invoke 和 native 内存模型但只要照着解码闭环和避坑清单走一遍真正做一个能跑的视频工具并没有想象中那么玄学。希望这份拆解对你有所帮助。本文还有配套的精品资源点击获取