FEATURED · 精选文章

C#上位机用FFmpeg API拉取RTMP流并实时预览的实践

发布时间 / 2026/9/8 11:41:48
来源 / 创域科博编辑部
栏目 / 资讯中心
C#上位机用FFmpeg API拉取RTMP流并实时预览的实践 简介这是一份面向C#开发者的FFmpeg API调用示例工程解决C#中缺少FFmpeg API例程、难以直接引用原生库的痛点。资源以ffmpeg.autogen封装为基础开发者可将C/C代码中FFmpeg方法名直接加前缀ffmpeg.使用大幅降低迁移成本工程实现了拉取RTMP流并播放的完整流程适合具备一定C#基础和流媒体概念的读者参考。压缩包共145个文件、约25.46MB其中111个h头文件对应FFmpeg接口声明16个DLL动态库提供底层解码能力8个C#源文件与3个可执行程序构成可直接编译运行的解决方案另有config、resx、sln等工程配置文件目录结构清晰。已有9697人浏览学习受到不少开发者关注。通过该示例读者既能得到可复用的C# FFmpeg调用封装也可以学习如何完成RTMP拉流、解码、播放等环节的API组合同时源码中Demo程序、辅助类与配置文件齐全便于在此基础上扩展推流、转码等功能适合作为C#音视频开发入门与进阶的参考。 最近在做一个 C# 上位机项目需要把一路 RTMP 视频流接到 WinForms 界面里实时预览。第一反应是直接Process.Start拉起 ffmpeg 命令行毕竟网上现成命令一搜一堆。但真的做进产品后才发现命令行方案在这种场景下非常难受。最后我换成了 API 方式在 C# 里直接调用 FFmpeg 动态库完成拉流、解码、显示整个过程干净得多。这篇文章就围绕 C# 开发 FFmpeg 例子API 方式来写核心就是如何用 API 拉取 RTMP 流并播放。适合上位机开发者、想在 Windows 程序里嵌入实时视频的同学以及那些刚把 ffmpeg 装好、还不知道从哪里下手的 C# 新手。先说结论整个过程不算复杂FFmpeg 负责把 RTMP 流拉下来解码成视频帧我们再把帧转成 Bitmap 画到控件上。但中间有不少细节顺序错了、参数差了、资源没释放都会让你怀疑人生。下面按我自己的踩坑顺序一步步拆开讲。1. 先想清楚为什么要用 API 方式而不是命令行1.1 命令行方案在实时显示上的瓶颈很多人觉得ffmpeg -i rtmp://xxx -f rawvideo pipe:1也能把数据导出来给 C# 用但这里有个现实问题从 stdout 拿到的是原始 YUV 数据你还要自己解析宽高、像素格式、行对齐然后转成 Bitmap。这等于你把 FFmpeg 内部一部分工作又做了一遍。而且用进程方式管理拉流状态不可控日志要从标准错误里截断线重连要盯着进程退出码非常绕。更难受的是多路并发。一个上位机项目通常不止看一路画面四路、八路是常态。如果每路都起一个 ffmpeg 进程虽然系统也能扛住但进程管理和资源回收都是隐患。某个进程假死、崩溃、权限异常都会直接影响主程序稳定性。API 方式把 FFmpeg 动态库当作普通 DLL 用拉流和解码都在进程内完成拿到的是解码后的AVFrame想转 Bitmap、做分析、叠加信息都很直接。1.2 C# 绑定 FFmpeg 的三种常见路子既然决定用 API接下来的问题是 C# 怎么调用 C 接口。我试过三种方案绑定方式实现原理适合场景主要坑点手写 P/InvokeC# 里自己声明DllImport只用几个函数时FFmpeg 接口太多签名容易写错FFmpeg.AutoGen自动生成整套绑定拉流、解码、转码、显示代码里要开 unsafe指针类型多Xabe.FFmpeg / FFmpegCore封装命令行调用转码、裁剪、合并视频实时预览场景不适用还是走进程我最后用的是FFmpeg.AutoGen。它帮你把 avformat、avcodec、swscale 这些库的 C 接口都生成好了省掉大量手写签名的工作。代价是代码里需要unsafe因为 AVFormatContext、AVFrame 这些全是指针C# 里操作它们必须进 unsafe 上下文。这恰恰也是“C# 开发 FFmpeg API 方式”最原汁原味的体验。1.3 RTMP 拉流到显示的完整链路理清整条链路很重要。RTMP 流从网络进来经过这几步变成界面上的画面RTMP URL -avformat_open_input建立连接 -avformat_find_stream_info拿到流信息 -av_find_best_stream定位视频流 -avcodec_open2打开解码器 - 循环av_read_frame-avcodec_send_packet/avcodec_receive_frame拿到 AVFrame -sws_scale转成 BGR24 - 封装成 Bitmap - UI 显示。每一步都有对应的 API。很多人卡住不是因为某个 API 不会调而是不知道它们的先后逻辑。下面我就按照这个链路逐个讲。2. 环境准备库文件、工程和初始化2.1 选择 FFmpeg 动态库API 方式需要的是 DLL不是命令行工具。你下载 Windows 版 shared 构建后bin目录下会有一堆avcodec-*.dll、avformat-*.dll、avutil-*.dll、swscale-*.dll这些都要带上。RTMP 协议在 libavformat 内部是默认编译进去的不需要额外装 librtmp。有个细节特别容易忽略程序的运行位数必须和 DLL 一致。你下了 x64 的 FFmpegC# 工程也要保证生成 x64如果有 AnyCPU 依赖建议直接在项目属性里强制 x64。我见过好几个朋友折腾半天avformat_open_input返回找不到库最后发现是进程跑到 x86 了。拿到库文件后可以用命令行先验证一下当前 FFmpeg 是否支持 RTMPffmpeg -protocols如果输出列表里有rtmp说明协议没问题。这一步是排除库缺陷的快速办法。2.2 创建 C# 工程并开启 unsafe新建一个 WinForms 项目目标框架我用的是 .NET 6.NET Framework 4.7.2 也完全可以。通过 NuGet 安装FFmpeg.AutoGen后要确认项目允许不安全代码PropertyGroup AllowUnsafeBlockstrue/AllowUnsafeBlocks Platformsx64/Platforms /PropertyGroup然后把 FFmpeg 的 DLL 放到程序运行目录或者在代码里指定路径。用 AutoGen 时初始化一般是这样的ffmpeg.RootPath D:\ffmpeg\bin; // 也可以直接放 exe 目录不用设置 ffmpeg.avformat_network_init(); ffmpeg.av_log_set_level(ffmpeg.AV_LOG_DEBUG);avformat_network_init()必须调用否则 RTMP 网络层没有初始化avformat_open_input大概率失败。这个坑很隐蔽因为报错信息不一定直接告诉你“没有初始化网络”。2.3 日志回调排查问题的第一工具WinForms 程序看不到 FFmpeg 默认的 stderr 日志拉流失败时你会两眼一抹黑。建议启动时设置日志回调把日志引到Debug或者自己的日志文件。av_log_set_callback的回调里format是带占位符的字符串直接用Marshal.PtrToStringAnsi只能拿到格式串本身拿不到完整内容。需要配合vsnprintf做一次格式化或者简化处理只打印 level 和 format再结合具体错误码定位。这一步不需要写多复杂但一定要留一个出口否则遇到Could not connect这类错误你都不知道去哪里看原因。3. 核心 API 流程拆解从打开 RTMP 到显示图像3.1 打开 RTMP 流avformat_open_input 的关键参数拉流的第一步是打开输入。核心代码长这样unsafe { AVFormatContext* formatCtx null; AVDictionary* opts null; // 超时设为 5 秒单位微秒 ffmpeg.av_dict_set(opts, rw_timeout, 5000000, 0); var ret ffmpeg.avformat_open_input(formatCtx, url, null, opts); if (ret 0) { var errBuf new byte[1024]; ffmpeg.av_strerror(ret, errBuf, errBuf.Length); throw new Exception($打开失败: {Encoding.UTF8.GetString(errBuf)}); } ffmpeg.av_dict_free(opts); ffmpeg.avformat_find_stream_info(formatCtx, null); }avformat_open_input的第二个参数是输入格式这里传null让 FFmpeg 自行探测第四个参数是AVDictionary**用来设置协议参数。rw_timeout设置的是读写超时单位是微秒5000000 就是 5 秒。这个参数在实际项目中很重要否则设备掉线时av_read_frame可能会卡很久。打开输入后记得调用avformat_find_stream_info否则codecpar里的宽高、编码格式可能是空的后续解码器初始化会失败。3.2 找视频流并初始化解码器网上很多老代码是遍历formatCtx-streams找 video stream代码啰嗦还容易漏。新版推荐用av_find_best_streamvar videoIndex ffmpeg.av_find_best_stream( formatCtx, AVMediaType.AVMEDIA_TYPE_VIDEO, -1, -1, null, 0); if (videoIndex 0) { throw new Exception(没有视频流); } var codecpar formatCtx-streams[videoIndex]-codecpar; var codec ffmpeg.avcodec_find_decoder(codecpar-codec_id); var codecCtx ffmpeg.avcodec_alloc_context3(codec); ffmpeg.avcodec_parameters_to_context(codecCtx, codecpar); // 低延迟配置做实时预览时很有用 codecCtx-low_delay 1; codecCtx-flags2 | ffmpeg.AV_CODEC_FLAG2_FAST; ffmpeg.avcodec_open2(codecCtx, codec, null);有一点容易被忽略avcodec_parameters_to_context是把流里的codecpar复制到解码上下文里解决了以前extradata丢失导致的解码花屏问题。RTMP 流的视频流绝大多数是 H.264解码用的是 CPU 软解机器性能不够时1080p 会明显掉帧这个后面再说。3.3 循环读包、解码send/receive 模型新版 FFmpeg 推荐的解码方式是生产者消费者模型先用avcodec_send_packet把压缩包交给解码器再循环avcodec_receive_frame取出解码后的帧。一个 packet 可能解出多个 frame所以 receive 要一直收到EAGAIN或错误为止。核心循环var packet ffmpeg.av_packet_alloc(); var frame ffmpeg.av_frame_alloc(); while (_running) { var readRet ffmpeg.av_read_frame(formatCtx, packet); if (readRet 0) { // 流结束或网络异常这里做断线重连处理 break; } if (packet-stream_index videoIndex) { var sendRet ffmpeg.avcodec_send_packet(codecCtx, packet); while (sendRet 0 ffmpeg.avcodec_receive_frame(codecCtx, frame) 0) { ConvertAndInvoke(frame); } } ffmpeg.av_packet_unref(packet); }每次av_read_frame返回的 packet 是带引用计数的用完后必须av_packet_unref否则内存只涨不降。我看到很多新手在这里直接continue或break结果跑几分钟内存就爆炸。关于断流av_read_frame返回负值可能是网络断开、服务器踢人、或者流正常结束。不要尝试在同一个上下文里“恢复”读取最稳妥的做法是重新avformat_close_input再重新avformat_open_input也就是断线重连。重连间隔建议不低于 3 秒否则服务端容易把连接打死。3.4 像素格式转换和 Bitmap 显示解码出来的AVFrame一般是 YUV420PWindows 的 WinForms 不能直接画需要sws_scale转成 BGR24。创建转换上下文var swsCtx ffmpeg.sws_getContext( frame-width, frame-height, (AVPixelFormat)frame-format, frame-width, frame-height, AVPixelFormat.AV_PIX_FMT_BGR24, ffmpeg.SWS_BILINEAR, null, null, null);然后每次拿到 frame 后做转换并写入 Bitmapvar bmp new Bitmap(frame-width, frame-height, PixelFormat.Format24bppRgb); var data bmp.LockBits( new Rectangle(0, 0, frame-width, frame-height), ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb); // FFmpeg.AutoGen 生成的指针数组类型不同版本名称可能不同 var dstData new byte_ptr4(); var dstLinesize new int4(); dstData[0] (byte*)data.Scan0; dstLinesize[0] data.Stride; ffmpeg.sws_scale( swsCtx, frame-data, frame-linesize, 0, frame-height, dstData, dstLinesize); bmp.UnlockBits(data);这里有个经验Bitmap的Stride不一定等于width * 3因为 GDI 会做内存对齐。所以dstLinesize[0]一定要填data.Stride不能自己按width * 3算。否则画面会斜、会绿严重时直接内存越界崩溃。UI 线程还要注意不要在解码线程里直接给 PictureBox 赋值。正确做法是把 Bitmap 抛到 UI 线程同时 dispose 掉上一张图。4. 一个可以直接抄的播放器骨架4.1 定义 RtmpPlayer 类我会把拉流和解码逻辑封装成一个类对外暴露Play、Stop和一个FrameReady事件public unsafe class RtmpPlayer : IDisposable { private AVFormatContext* _formatCtx; private AVCodecContext* _codecCtx; private SwsContext* _swsCtx; private Thread _decodeThread; private volatile bool _running; private string _url; public event ActionBitmap FrameReady; public void Play(string url) { _url url; _running true; _decodeThread new Thread(DecodeLoop) { IsBackground true }; _decodeThread.Start(); } private void DecodeLoop() { // 这里把第 3 节的核心代码按顺序放进来 } public void Stop() { _running false; _decodeThread?.Join(2000); } public void Dispose() { Stop(); // 释放所有 FFmpeg 资源 } }实际使用中我还会在DecodeLoop里加一个Reconnect状态当av_read_frame返回异常时触发重连并重新初始化整个上下文。重连逻辑等于把Play里的初始化代码再执行一遍。4.2 窗体端接入在 WinForms 里订阅事件然后显示到 PictureBoxvar player new RtmpPlayer(); player.FrameReady bmp { pictureBox.BeginInvoke(new Action(() { var old pictureBox.Image; pictureBox.Image bmp; old?.Dispose(); })); }; player.Play(rtmp://127.0.0.1:1935/live/test);如果要测试而没有现成 RTMP 服务器最省事的方法是用 FFmpeg 命令行推一路本地视频到本地服务器。服务器可以用 SRS 或 nginx-rtmp命令行推流示例ffmpeg -re -i test.mp4 -c:v libx264 -preset veryfast -tune zerolatency -c:a aac -f flv rtmp://127.0.0.1:1935/live/test先用命令行拉流验证服务器通不通ffmpeg -i rtmp://127.0.0.1:1935/live/test -t 5 out.mp4如果这一步能成功就说明 C# 代码里的问题如果这一步都失败那别急着调代码先修环境或服务端。4.3 资源释放顺序顺序错了会崩溃FFmpeg 是 C 接口所有上下文都是手动管理内存。释放顺序我踩过坑尤其是先关了解码器再去关输入偶发崩溃。建议固定顺序停止解码线程_running falseJoin等待退出。avcodec_free_context释放解码器上下文。sws_freeContext释放像素转换上下文。avformat_close_input关闭输入并释放AVFormatContext。av_frame_free释放 frameav_packet_free释放 packet。注意 packet 和 frame 要最后释放。如果线程还在用 frame你先把它 free 了另一个线程继续访问就是野指针直接崩溃。5. 踩坑记录RTMP 拉流播放常见问题排查5.1 拉流失败avformat_open_input 返回错误这是新手遇到最多的问题。常见现象和排查方向如下现象原因解决办法提示找不到协议DLL 不完整或版本问题用ffmpeg -protocols检查换完整 shared 构建打开失败地址没问题没调用avformat_network_init启动时先初始化网络Could not connect服务器没开、防火墙拦截先用命令行拉流验证服务器偶发超时RTMP 握手慢rw_timeout调大到 10 秒返回错误码但不知啥意思没看日志开启av_log_set_callback看实时日志我自己的习惯是任何失败先看av_strerror转出来的字符串再看 FFmpeg 日志。这两个信息能覆盖 80% 的问题。5.2 画面花屏、绿屏、有彩色条纹花屏基本是解码上下文参数不对或格式转换参数不对。最常见的原因codecpar没有正确复制到codecCtx导致 extradata 丢失。解决必须调avcodec_parameters_to_context。sws_scale的srcFormat写死成AV_PIX_FMT_YUV420P但实际流可能是 NV12 或 YUV422。解决用(AVPixelFormat)frame-format动态获取。Bitmap 的Stride与dstLinesize[0]不一致导致图像错位。解决dstLinesize[0] data.Stride。5.3 延迟越来越大实时预览的延迟问题几乎是所有播放器都会遇到的。RTMP 本身延迟通常在 1 到 3 秒但如果你解码速度跟不上延迟会越积越多。我的处理经验打开输入后设置formatCtx-flags | ffmpeg.AVFMT_FLAG_NOBUFFER;解码器里设置codecCtx-low_delay 1codecCtx-flags2 | ffmpeg.AV_CODEC_FLAG2_FAST;UI 线程不要做耗时操作FrameReady里只替换 Bitmap不做缩放、裁切、加边框。如果解码速度还是跟不上可以在拿到 frame 后判断frame-pts发现延迟超过 500ms 就调avcodec_flush_buffers清一次解码缓冲。注意别太频繁否则画面会频繁花屏。实际项目里我用“只取最新帧 BeginInvoke”的方式把延迟控制在几百毫秒至少人眼感知不到明显卡顿。5.4 内存持续增长和偶发崩溃内存持续增长90% 是 packet 没释放。检查你的循环里是不是每次av_read_frame之后都调了av_packet_unref。不要每次新建AVPacket用av_packet_alloc分配一次循环里反复 unref。偶发崩溃尤其是运行几分钟后崩溃大概率是 unsafe 指针问题。重点检查sws_scale目标缓冲区是否够大bmp.LockBits返回的Scan0和Stride是否直接用于写入。是否跨线程操作了 FFmpeg 上下文。一个上下文只能在一个线程里读不要在 UI 线程 close然后解码线程还在 read。GDI 对象是否泄漏。每次new Bitmap后下一次事件时要把上一张 Dispose否则句柄暴涨。5.5 音频怎么处理标题里写的是“RTMP 流并播放”实际场景中如果还要声音就要再开一路音频解码线程。FFmpeg 的av_read_frame会把音频包和视频包都返回你要按stream_index分流。音频 AAC 解码成 PCM 后可以用 NAudio 或 SDL2 播放。但很多安防摄像头推的 RTMP 流是纯视频或者只拿画面做监控我一般建议先不做音频把画面稳定跑通再说。最后再分享一个细节如果将来要同时播放多路 RTMP我给的建议是一路一个解码线程但 UI 层不要每路都直接往 PictureBox 上贴图。把每路的最新 Bitmap 缓存起来用一个 Timer 定时统一刷新GDI 的压力会小很多。先写到这断线重连和音频同步的部分我还在整理改天再单独写一篇。本文还有配套的精品资源点击获取
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻