首页 / 资讯中心 / 文章详情

C#调用FFmpeg解码H.264裸流:从P/Invoke到BMP输出

C#调用FFmpeg解码H.264裸流:从P/Invoke到BMP输出 ★ FEATURED ARTICLE
简介针对在 C# 环境下解码 H.264 视频的常见开发需求这份 demo 项目提供了基于 FFmpeg 开源库的可运行框架适合具有一定 C# 基础、希望向流媒体或视频处理方向扩展的开发者。项目完整演示了如何通过 P/Invoke 与 FFmpeg 动态库交互从引入 NuGet 包或手动添加引用开始到创建并配置解码上下文再到从视频流中提取 H.264 的 NAL 单元解码后输出无压缩的 YUV 原始图像数据并将结果保存为可由播放器直接播放的 .yuv 文件全程可运行、可验证。在实现层面还针对高分辨率视频下图像缓冲区的内存高效分配与释放、基于 PTS 的帧同步、FFmpeg API 错误码捕获与处理以及通过多线程或硬件加速提升解码性能等工程难题提供了具体处理思路帮助使用者避开常见踩坑点。资源以 rar 压缩包方式发布大小约 10.86MB虽然上游未提供文件总数与类型明细但代码组织结构清晰便于按解码流程分段研读。该 demo 已有 2820 人学习/下载不仅可用于理解 H.264 解码原理与 C# 原生调用技术还能在此基础上进一步探索视频转码、帧率调整、实时预览等扩展应用实用价值较高。1. 从文件到像素C# 里解 H.264 的 demo 该怎么下手H.264 是网络摄像头、行车记录仪、无人机图传里最常见的一种视频压缩格式但 C# 生态里你很少能看到一个开箱即用的解码器。很多开发者的第一反应是把解码压缩成命令行进程调用于是问题随之而来进程启动开销、参数转义、输出数据不好回收。我拆过的这个 demo 不一样——它直接加载解码库在 C# 进程内完成 H.264 裸流到 RGB 帧的转换适合“读 .h264 文件 → 逐帧输出位图”这类实际需求。接下来从选型、封装、调用顺序到真实踩坑按落地顺序讲完新手照着改熟手重点看参数边界和排查技巧。2. 解码方案选型FFmpeg 库 vs 系统原生 API vs OpenH2642.1 三条路线的真实差异与选择理由遇到 H.264 解码先别急着写代码选型错了后面全是头疼。C# 本身没有官方解码库能走的路大致有三条系统原生媒体 API、OpenH264 独立解码库、FFmpeg 系列开源解码库。我把它们放在一张表里对照后面再逐个解释。方案额外依赖解码能力裸流兼容集成难度适用场景系统原生 API无额外 DLL有硬件加速但版本差异大要自己管 SPS/PPS高固化了运行环境的单机软件OpenH264单个 DLL纯软解API 简单无拆包能力中封闭场景、对体积敏感FFmpeg 库数个 DLL软解/硬解可选自带 parser 拆包低通用跨平台、直播/文件复用系统原生 media API 听起来最省事因为它不需要任何第三方 DLL 就能在系统上运行而且能借助硬件解码。实际用起来不是这样它的接口是一套 COM 风格的对象模型要经历创建、配置、协商、投递样本、收回样本好几个阶段每个阶段都有状态机约束。最麻烦的是不同系统版本对 H.264 的 Profile 支持不一致同一个 demo 在这台机器上能解出画面换到另一台就可能报“找不到匹配的格式”。真要做底层移植这套方案几乎等于给每个运行环境单独维护一套解码分支。OpenH264 是独立提供的一个 H.264 编解码库API 比 COM 干净得多。但它只做“编解码”本身不帮你从裸流里拆出 NAL 单元。也就是说你仍然需要自己扫描起始码、切分帧、识别 SPS/PPS 参数集再把这些递进解码器。对于单文件还好一旦接实时流丢包、粘包、分片交错拆包逻辑会越写越厚最后相当于自己造了一个简化版 parser。FFmpeg 里的 libavcodec 走的是另一条路它把拆包工具也集成进来了。av_parser_init负责吃原始字节、吐出一个完整的数据包avcodec_find_decoder找解码器avcodec_open2开上下文后面直接avcodec_send_packet、avcodec_receive_frame两段式取帧。这种“解析器 解码器”的组合正好覆盖裸流文件最典型的痛点。我在 demo 里选的就是这一套理由是它省掉了从零开始维护拆包逻辑的时间而且代码路径在文件流和网络流之间几乎不用改。另外明确一点不要用命令行进程包一层 FFmpeg 就当成集成方案。命令行适合手工验证、看诊断信息不适合作为程序内部的实时解码链路。一次进程启动就要几十毫秒频繁调用会积累开销参数需要拼字符串出错时难以定位解码后的像素数据要通过管道或临时文件回收效率和可靠性都差。把 FFmpeg 作为库链接进进程才是能长期维护的路径。2.2 P/Invoke 最小骨架DllImport 声明、结构体布局和调用约定FFmpeg 对外暴露的是 C 函数和 C 结构体C# 要调用它手段很直接[DllImport]。但这里有一个决定成败的细节——调用约定。FFmpeg 的 C 函数按 cdecl 调用约定编译你在 C# 里声明时必须显式写成CallingConvention.Cdecl少写这一个参数轻则拿到错乱数据重则直接崩溃。而且位数也要对编译目标设成 x64进程就是 64 位DLL 也必须匹配 64 位版本。我把 demo 里的核心声明抽出来做成一个独立静态类方便统一管理using System; using System.Runtime.InteropServices; internal static class FFmpegNative { private const string AvCodecDll avcodec-58.dll; private const string AvUtilDll avutil-56.dll; private const string SwScaleDll swscale-5.dll; [DllImport(AvCodecDll, EntryPoint avcodec_find_decoder, CallingConvention CallingConvention.Cdecl)] internal static extern IntPtr AvcodecFindDecoder(int codecId); [DllImport(AvCodecDll, EntryPoint avcodec_alloc_context3, CallingConvention CallingConvention.Cdecl)] internal static extern IntPtr AvcodecAllocContext3(IntPtr codec); [DllImport(AvCodecDll, EntryPoint avcodec_open2, CallingConvention CallingConvention.Cdecl)] internal static extern int AvcodecOpen2( IntPtr avctx, IntPtr codec, IntPtr options); [DllImport(AvUtilDll, EntryPoint av_free, CallingConvention CallingConvention.Cdecl)] internal static extern void AvFree(IntPtr ptr); }这里有个容易踩的认知误区C# 侧的 DllImport函数名不需要跟 C 端完全一样因为真正找入口靠的是EntryPoint字符串。但 DLL 文件名的版本号必须跟实际文件完全一致avcodec-58.dll和avcodec-59.dll是两个 ABI 不同的库不能混用。codecId这个 int 对应 FFmpeg 头文件里的AV_CODEC_ID_H264在常见版本里是 27老版本换过的话就按头文件枚举值来。由于 FFmpeg 的函数返回值大量是裸指针C# 里统一用IntPtr接收。如果你需要访问结构体内部字段比如AVFrame里的data和linesize可以再声明一个布局一致的结构体或者直接按字节偏移量读取。demo 里我采用了后者减少结构体定义维护量但需要在代码里写清楚偏移注释。如果你想省掉手写 P/Invoke 的功夫可以引入一个现成的自动生成包比如 FFmpeg.AutoGen。这类包帮你把函数声明、常量、结构体都生成好了缺点是版本绑定较紧跟随 FFmpeg 大版本升级时要重新拉包。我的建议是为 demo 自写最小声明只暴露你实际用到的十几个函数等业务跑通后再决定要不要换自动生成方案。2.3 demo 工程结构DLL 放置、编译目标与输出配置工程结构方面我习惯把 native 调用和业务逻辑分开避免 Program 里堆上一大段 DllImport。demo 的文件组织大致是这样h264-decode-demo/ Bin/ avcodec-58.dll avutil-56.dll swscale-5.dll Native/ FFmpegNative.cs FrameStructs.cs Pipeline/ H264Decoder.cs FrameConverter.cs Program.cs h264-decode-demo.csprojBin/目录放 FFmpeg 解压包里所有需要的 DLL不能只放一个 avcodec。因为 avcodec 依赖 avutilswscale 又依赖 avutil 和 avcodec少一个文件程序启动时就会报“无法加载 DLL 或其依赖”。在正式部署时宁可把所有相关 DLL 都落在同一个输出目录也不要散在系统路径里。csproj 里要做的两件事一是强制编译目标为 x64二是把 Bin 目录复制到输出目录。我一般这样配置PropertyGroup PlatformTargetx64/PlatformTarget /PropertyGroup ItemGroup None IncludeBin\*.dll CopyToOutputDirectoryPreserveNewest / /ItemGroup如果不写PlatformTarget默认跟随当前系统位数一旦跑在 32 位环境下IntPtr 就只有 4 字节后面的指针运算全错位。CopyToOutputDirectory保证每次编译后 DLL 自动同步到生成目录省掉手动拷贝的麻烦。这套工程骨架跑通后你面对任何视频解码需求只需要替换解码循环那一层底层封装不用再动。3. 解码主流程从 .h264 裸流到 RGB 帧的完整实现3.1 裸流拆包为什么用 parser 而不是自己找起始码拿到一个.h264文件它本质上是一长串字节流没有固定的“帧长度字段”。每条 NAL 单元之间用起始码分隔最常见的是00 00 00 01也有用00 00 01的。初学者最容易自己写一个查找起始码的循环扫到就切片然后直接喂给解码器。这个思路看起来正确但里面藏着几个边界问题一是起始码可能会跨块出现比如上一次读取的缓冲区尾部只看到00 0000 01留在下一次读取二是 SPS、PPS、IDR 帧、非 IDR 帧之间的层级关系需要解析器去识别盲目切分很容易把参数集和图像数据混在一起。FFmpeg 提供了av_parser_init和av_parser_parse2专门解决这类字节流拆包。parser 内部会维护状态跨片段跟踪起始码并且能告诉你当前这个数据包属于哪种 NAL是否包含关键帧信息。demo 里我把 parser 初始化和解码器初始化放在一起private IntPtr _parser; private IntPtr _ctx; private void SetupDecoder() { int h264Id 27; // AV_CODEC_ID_H264 IntPtr codec FFmpegNative.AvcodecFindDecoder(h264Id); _ctx FFmpegNative.AvcodecAllocContext3(codec); int openResult FFmpegNative.AvcodecOpen2(_ctx, codec, IntPtr.Zero); if (openResult ! 0) { throw new InvalidOperationException($avcodec_open2 失败: {openResult}); } _parser FFmpegNative.AvParserInit(h264Id); }这段代码干了两件事先根据 H.264 的编码 ID 找到对应解码器再创建一个解码上下文并把解码器挂进去。codec指针由avcodec_find_decoder返回指向一个静态注册的解码器结构它不允许被手动释放_ctx则是新分配的上下文最后要用avcodec_free_context回收。avcodec_open2的第三个参数是 AVDictionary 选项demo 里传空指针进阶时可以用它来设置低延迟、丢帧策略等。parser 的好处在于你喂给它任意大小的字节块它就返回“当前这批次里能确认的一个完整数据包”。如果不满足一帧的量poutbuf_size会是 0如果解析出了完整数据包会返回一个指向包内存的指针并且告诉你它内部消费了多少字节。我的主循环这样写private void DecodeBytes(byte[] data, int offset, int count) { int consumed FFmpegNative.AvParserParse2( _parser, _ctx, out IntPtr outBuf, out int outSize, data, offset, count, 0, 0, 0); offset consumed; if (outSize 0) { byte[] packetBytes new byte[outSize]; Marshal.Copy(outBuf, packetBytes, 0, outSize); FeedPacket(packetBytes); } }注意consumed和outSize不是同一个东西。consumed表示 parser 从你给的data里吃掉了多少字节可能小于countoutSize表示解析出来的数据包有多大。假如一次调用后consumed count说明输入里还有剩余内容要继续把剩下的字节再喂一次循环直到消费完。我一开始写 demo 时直接把consumed当成outSize用结果解出来全是残帧这个问题后面避坑章节会说。3.2 喂包取帧send_packet 与 receive_frame 的两层缓冲FFmpeg 从 3.x 以后推荐使用两段式接口avcodec_send_packet喂入压缩数据avcodec_receive_frame取回解出的视频帧。它的设计用意是让解码器内部保留一个缓冲队列你塞多少包它都收下能不能立即取回帧则取决于内部状态。这个模型初期让人不太适应因为它不再像老接口那样“调用一次必然得到一帧”。demo 里的FeedPacket方法负责把拆出来的 packet 字节交给解码器然后立刻尝试取帧private void FeedPacket(byte[] packetBytes) { IntPtr avPacket FFmpegNative.AvPacketAlloc(); // 把字节拷贝进 AVPacket 内部 data FFmpegNative.AvPacketFill(avPacket, packetBytes, packetBytes.Length); int sendRet FFmpegNative.AvcodecSendPacket(_ctx, avPacket); if (sendRet ! 0 sendRet ! -11) // -11 是 EAGAIN { Console.WriteLine($send_packet 返回异常: {sendRet}); } while (true) { IntPtr frame FFmpegNative.AvFrameAlloc(); int recvRet FFmpegNative.AvcodecReceiveFrame(_ctx, frame); if (recvRet 0) { OnFrameDecoded(frame); } else { FFmpegNative.AvFrameFree(frame); break; } } FFmpegNative.AvPacketFree(avPacket); }sendRet返回-11表示 EAGAIN意思是解码器内部缓冲区已满需要你先调用receive_frame取走一些帧再继续送包。对 demo 这种离线解码来说每次送一个包后立即取帧一般不会频繁撞到 EAGAIN但对实时流如果你一口气塞了几十个包而不取帧就会出现 EAGAIN这是正常的不是错误。recvRet返回 0 表示成功取出一帧。while(true)循环是因为一个压缩包可能产出多帧也可能暂时产不出帧需要反复调用。等到返回其他负数时说明当前没有可用帧了释放本次 frame 后退出循环。这里一定要记得释放每轮分配的 frame否则循环次数多了内存只增不减后面内存泄漏那条会说。我把解码后的frame交给OnFrameDecoded回调由下一层做像素格式转换。这样文件解码和实时流解码可以共用同一套帧处理逻辑。3.3 像素格式转换YUV420P 到 BGR24 与 BMP 保存解码器输出的原始帧是 YUV 格式。不是所有 YUV 都一样H.264 解码后常见的是AV_PIX_FMT_YUV420P也就是 Y 平面加 U、V 两个半分辨率平面。而 Windows 上最常见的位图 BMP 存储的是 BGR 次序R 和 B 反过来了。直接把 YUV 内存丢给图片控件表现就是偏绿或偏色必须经过 swscale 转换。转换的核心是sws_getContext创建转换上下文然后用sws_scale执行。参数里有输入的宽高、像素格式、输出的宽高、像素格式以及缩放算法。demo 里我保持了同样分辨率只做像素格式变换不用做缩放private byte[] ConvertYuvToBgr(IntPtr frame, int width, int height) { int dstStride width * 3; byte[] dst new byte[height * dstStride]; IntPtr dstBuf Marshal.AllocHGlobal(dst.Length); IntPtr sws FFmpegNative.SwsGetContext( width, height, FFmpegNative.AV_PIX_FMT_YUV420P, width, height, FFmpegNative.AV_PIX_FMT_BGR24, FFmpegNative.SWS_BILINEAR, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); // 从 AVFrame 的 data / linesize 取指针逐平面传入 FFmpegNative.SwsScale(sws, frameDataPtrs, frameLinesizes, 0, height, dstBuf, dstStride); Marshal.Copy(dstBuf, dst, 0, dst.Length); Marshal.FreeHGlobal(dstBuf); FFmpegNative.SwsFreeContext(sws); return dst; }这里有个关键参数容易被忽略dstStride是宽度乘 3 得到的内存间距。BMP 的每行像素正好是width * 3字节因为 24 位色下每个像素占 3 字节。sws_scale要求输出缓冲的 stride 和你给的一致如果你给它一个更大的 stride它会把行末填充成对齐字节如果给它一个更小或未按像素字节数对齐的值转换结果就会错位。拿到 BGR 字节后保存 BMP 还需要补一个文件头。BMP 文件头分 BITMAPFILEHEADER 和 BITMAPINFOHEADER 两部分位数、偏移量、行结束都要按小端写。我建议把封装函数写得越直白越好别用 Bitmap 对象间接转换因为 GDI 构造位图时还会做一次格式猜测不如直接落盘可靠private void SaveBmp(string path, int width, int height, byte[] bgr) { int imageSize bgr.Length; int fileSize 54 imageSize; using var fs new FileStream(path, FileMode.Create); using var bw new BinaryWriter(fs); // BITMAPFILEHEADER bw.Write((byte)B); bw.Write((byte)M); bw.Write(fileSize); bw.Write(0); bw.Write(54); // BITMAPINFOHEADER bw.Write(40); bw.Write(width); bw.Write(height); bw.Write((short)1); bw.Write((short)24); bw.Write(0); bw.Write(imageSize); bw.Write(0); bw.Write(0); bw.Write(0); bw.Write(0); bw.Write(bgr); }BMP 要求每行像素数必须是 4 的倍数否则要补零。因为width * 3在 width 为偶数时基本都自动对齐width 基数时可能多出 padding。如果你的 demo 源视频宽度是 1280、1920 这类常见值width * 3都能被 4 整除不需要额外处理但宽度是 1282、1942 这种奇数场景就得在每行末补(4 - (width * 3) % 4) % 4字节。从 YUV 转 BGR 时应该连 padding 一起生成否则 BMP 文件打开后右下角会出现斜向错位条纹。4. 避坑记录SPS/PPS、B 帧乱序、linesize 对齐与内存泄漏这章记录的是我在实际验证 demo 时踩过的四个具体问题。不只讲现象也把排查路径写出来。按“现象 → 原因 → 解决”的顺序每一条都可以对照自己的工程项目去复现。4.1 解码器输出花屏SPS/PPS 没喂进去现象从某个录像文件切出来一段.h264文件本身能正常播放但用 demo 解码时开头几帧花屏或者完全黑屏几秒后才恢复正常。原因H.264 码流里的 SPS序列参数集和 PPS图像参数集承载着分辨率、帧率、熵编码方式等关键信息。解码器必须在解码 IDR 帧之前知道这些参数。如果你直接从一个非关键帧开始喂数据解码器没有参数集可用自然解不出画面。av_parser_parse2虽然能识别 NAL 类型但它不会替你把 SPS/PPS 重新插入到解码上下文里需要你自己保证这些参数集被尽早送进去。解决先扫描文件开头几个 NAL把 SPS/PPS 单独提取出来在正式送图像帧之前先喂给解码器。代码做法是识别 NAL 头字节SPS 的nal_unit_type是 7PPS 是 8然后交给同一个FeedPacket方法。如果文件本身就是完整录像通常开头的00 00 00 01 67就是 SPS00 00 00 01 68是 PPS。把这两个包先发送后再发 IDR 帧画面立即正常。这里也顺带说另一个相关情况如果解码过程中途出了异常重新开始解码时也要强制重新发送一份 SPS/PPS否则从中间恢复的码流同样会花屏。4.2 画面时序跳变B 帧导致 PTS 乱序现象demo 逐帧保存出来的 BMP 编号看起来不是单调递增的某一帧的 PTS 比前几帧小直接按 PTS 当作播放时间戳时画面出现回跳甚至卡顿。原因H.264 编码允许 B 帧B 帧在解码时依赖后续帧。解码器内部会做重排序把帧的显示顺序调整过来。问题在于重排序发生在receive_frame返回时还是发生在编码阶段取决于具体实现。如果你在拿到压缩包后直接把AVPacket.pts当作这帧的显示时间就会遇到时间戳倒置。AVPacket.pts是编码顺序的展示时间而AVFrame.pts才是解码重排序后的展示时间。解决取帧后一律读AVFrame.pts不要读 packet 的 PTS。如果frame-pts等于AV_NOPTS_VALUE可以退一步用best_effort_timestamp辅助字段。demo 里我处理方式是解码完一帧后立即把帧号递增并把这个序号写到文件名里不依赖 PTS 做索引如果要显示才单独维护 PTS 到播放时钟的映射关系。顺带提醒如果你看到帧的 DTS 比 PTS 大不一定是解码器出错。H.264 本来就可以有 B 帧重排序DTS 小于 PTS 是正常现象不要按 DTS 去排序显示。4.3 右边缘出现绿边linesize 对齐问题现象解码保存的 BMP右侧有一条绿色或暗色竖条宽度大概十几个像素。宽度越大越明显但用播放器打开原文件看不到这个问题。原因FFmpeg 为了提升内存访问效率每帧图像的行字节数并不是严格等于width * channels而是按一定字节对齐后得到的linesize。YUV420P 里Y 平面的 linesize 通常对齐到 32 的倍数U、V 平面对齐到 16 的倍数。你如果直接把整块内存按width * height * 3拷出来会把每行末尾的对齐字节当成了有效像素于是出现右侧条纹。解决转换时不要直接 memcpy 整块。从 YUV 转 BGR 时sws_scale 已经帮你处理了输入 linesize 对齐的问题它会读frame-data和frame-linesize你只要正确传入这两个数组就行。麻烦的是你后续存取 BGR 内存时如果也用统一 stride 覆盖会再次踩到同样的坑。我的习惯是给 BGR 缓冲也定义一个明确的stride width * 3每一行只拷贝 stride 字节即使源 linesize 更大也截断掉多余部分而不是盲目拷贝linesize[0]那么长。如果你发现 BMP 里图像上下颠倒那也正常因为 FFmpeg 解出来的 YUV 是自上而下的而 BMP 存储习惯是自下而上。可以先写完图像数据再补文件头或者把头文件里的高度字段写成负值。4.4 长时间解码内存暴涨忘了 unref现象对一段几十分钟的文件做逐帧抽图前几分钟正常越往后内存占用越高最后程序因为内存不足被系统杀掉。原因FFmpeg 的 AVPacket 和 AVFrame 都采用引用计数管理内存。av_packet_alloc分配了结构体但字段里的数据缓冲是后续由解码器内部填充或复用的avcodec_receive_frame返回的 frame 还持有对解码器内部缓冲的引用。如果你只调用av_packet_free和av_frame_free没有先调用对应的av_packet_unref和av_frame_unref那么内部引用计数可能无法归零缓冲一直被占用。解决每次 send 完 packet 后马上av_packet_unref而不是等到整个解码结束才释放每次 receive 到 frame 并处理完成后也立即av_frame_unref。注意av_frame_free会释放结构体本身但不会替你归还它指向的缓冲区引用。demo 里我统一把它封装进一个 finally 块IntPtr frame FFmpegNative.AvFrameAlloc(); try { int ret FFmpegNative.AvcodecReceiveFrame(_ctx, frame); if (ret 0) { OnFrameDecoded(frame); } } finally { FFmpegNative.AvFrameUnref(frame); FFmpegNative.AvFrameFree(frame); }packet也一样送包后AvPacketUnref然后AvPacketFree。别嫌这两行代码啰嗦漏掉一次就是一次泄漏。在线时长超过一小时的应用里这种小泄漏会直接变成稳定性事故。5. 验证与进阶用帧哈希对拍输出、把 demo 改成实时预览5.1 用帧哈希对拍验证解码链路没有偷换数据demo 能跑出 BMP不代表你的 YUV 转 BGR 参数是正确的。我以前吃过一次亏通道顺序写错RGB 变成 BGR肉眼在深色画面里几乎看不出来直到做自动化检测才发现大量帧的像素值偏差。从那以后我每次调完转换逻辑都会做一次“帧哈希对拍”。具体做法很简单。先让 demo 每保存一帧时同时计算这帧 BGR 数据的 MD5按帧顺序输出到文本再用 FFmpeg 工具对同一份源文件生成 frame hash然后逐行对比前几十帧ffmpeg -i input.h264 -f framemd5 -需要注意这里对比的是解码后的视频帧所以命令里不要加任何滤镜也不要改分辨率。输出里的每行 hash 是按 YUV 还是 RGB 计算取决于你在命令里有没有指定-pix_fmt。我会显式加-pix_fmt bgr24让比对基准和 demo 保持一致。如果两边 hash 不一致优先查三处通道排列是否反了、stride 是否传对、宽高单位是否填错。比对通过后再进实时链路调试时才能确信问题出在网络或缓冲上而不是解码这层有隐藏 bug。5.2 把文件解码改造成实时预览缓冲水位与 PTS 驱动文件解码和实时解码共用了同一套 parser 和解码循环区别只在于数据来源。要改成实时网络收包线程把字节块直接交给DecodeBytes即可原本的while (offset fileBytes.Length)换成while (socket.IsConnected)。有两点要特别处理一是控制取帧频率否则解码速度远高于播放速度画面呈快进状态二是设置缓冲水位避免网络抖动时收到过多数据导致内存堆积。我通常这样设计收到一帧后先不入队而是把时间戳存到一个队列里每次判断当前缓冲帧数和最早一帧的 PTS 差值超过阈值就暂停 decode 循环。这就是所谓的 PTS 驱动比固定让线程 sleep 更抗网络抖动。如果只是纯预览、不做录制水位可以设成 5 帧左右延迟最低。那次我做实时接入时忘了检查 parser 的got_packet边界导致分片数据被错误切分画面卡死又恢复反复横跳查了半天才发现是拆包边界。从那以后我每次做实时接入都强制走一遍“先落一分半文件 → 对拍帧 hash → 再进收发循环”让实时问题和文件解析问题不叠加在一起。这个顺序看起来多花了几分钟但省下的排查时间远不止这些希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站