ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

摄像机RTSP视频流接入:地址、认证、缓冲与重连实战

摄像机RTSP视频流接入:地址、认证、缓冲与重连实战

接到“把摄像机画面接进系统”这种需求时,绝大多数人的第一反应是去找厂家要文档,然后对着一条rtsp://开头的地址发愣:用户名密码到底填在哪儿、端口是不是固定 554、主码流和子码流有什么区别、为什么播放器里能看但代码里读不出来。我前后做过几个视频接入的小项目,从单路枪机到十几路的批量采集都碰过,踩的坑基本集中在“地址拿到了但连不上”“连上了但画面是黑的”“画面有了但延迟越跑越大”这三类。

所谓获取摄像机 RTSP 视频流,说白了就是两件事:一是从设备上把那条正确的取流地址连同认证信息挖出来,二是用合适的传输参数和解码方式把它稳定地读进你的程序里。它听着像是个“复制粘贴一条 URL”的活儿,实际上牵扯到协议结构、认证方式、编码格式、网络链路和缓冲策略五个层面。这篇内容适合刚接触视频接入的开发、做数据采集和算法联调的同学,也适合那些“以前都是用现成播放器,现在要自己写代码拉流”的运维和集成人员。下面我按自己实际排查的顺序,把这条链路从头到尾拆一遍。

1. RTSP在这套链路里到底扮演什么角色

很多人把 RTSP 当成“视频流协议”,这个理解会带来一连串误判。RTSP 本质上是一套会话控制协议,它的工作方式很像遥控器:负责发出“播放”“暂停”“描述一下你有哪些流”这类指令,而真正的音视频数据是通过另一条通道(RTP)送过来的。理解了这层分工,你就能明白为什么抓包时会看到两种截然不同的包,也能明白为什么“网络通、端口通,但就是没画面”这种情况经常发生——控制通道建起来了,数据通道没谈拢。

RTSP 的交互流程大致是四步:客户端先发OPTIONS探一下设备支持哪些方法,再发DESCRIBE拿到一份 SDP 描述(这份描述里写明了有几路流、用什么编码、采样率、payload type 是多少),然后SETUP协商数据通道的传输方式(走 UDP 还是复用 TCP),最后PLAY才开始真正送数据。整个过程是纯文本的,形如RTSP/1.0 200 OK,你在命令行里甚至能用 telnet 手动敲一遍。我建议每个第一次做视频接入的人都手动走一次这四步,走完之后你对“为什么 URL 里少一个斜杠就不认”这种问题就有直觉了。

1.1 一条URL背后藏着的四段地址

一条典型的取流地址长这样:

rtsp://用户名:密码@192.168.1.100:554/stream/main

这里面其实是四段独立信息拼起来的。第一段是认证信息,它在@之前,格式是user:pass,如果密码里有@、:、/这类字符就必须做百分号编码,否则解析会错位,这是非常高频的一个坑——很多人密码里带个@,然后死活连不上,还以为是设备问题。第二段是主机地址,可以是 IP 也可以是局域网内可解析的主机名。第三段是端口,RTSP 的注册端口是 554,但不少设备为了避开冲突会改成 8554 或者别的值,这一项写错是最常见的“连接被拒绝”来源。第四段是路径,也是各厂商差异最大的部分,同一个品牌不同型号的路径规则都可能不一样。

路径这一块没有统一标准,但有一些共性可以帮你猜:多数设备的路径里会体现通道号和码流类型,比如用101表示第 1 通道主码流、102表示第 1 通道子码流,或者用main/sub这样的语义化后缀。如果你拿到的地址里出现了类似channel=1&subtype=0的查询串,那subtype=0一般就是主码流、1是子码流。这些规律只能当线索用,最终还是要以设备实际返回的DESCRIBE结果为准。我在一个项目里曾经因为把路径里的通道号从 1 改成 01 试了半小时,最后发现是设备固件版本不同导致路径规则变了,这种时候抓一次包比猜十次都快。

1.2 主码流与子码流:先想清楚你要几帧

这是我在项目里反复强调的一件事:在写第一行代码之前,先确定你到底要主码流还是子码流。几乎所有网络摄像机都会同时输出两路甚至三路编码,主码流分辨率高、码率大,子码流分辨率低、码率小。以常见的 200 万像素设备为例,主码流 1080p 的 H.264 码率通常在 2~4 Mbps,子码流 720p 或 D1 大概在 512 Kbps~1 Mbps,第三码流可能只有 256 Kbps 左右。

这个差别在单路调试时感觉不出来,一旦上到十几路就是灾难。我算过一笔账:如果做算法分析,你其实只需要 720p 甚至更低,因为大多数检测模型输入也就 640 上下,用主码流等于白白消耗带宽和解码算力。20 路主码流按 3 Mbps 算就是 60 Mbps,接近 7.5 MB/s 的持续吞吐,加上每路都要完整解码,一台普通办公机的 CPU 直接就满了;换成子码流,总带宽降到 15 Mbps 以内,解码压力也降一个数量级。

这里有个容易被忽略的点:子码流的分辨率不一定是你想要的比例。有些设备的子码流是 4:3 的 D1,直接送进按 16:9 训练的模型会因为拉伸导致精度下降。稳妥的做法是先DESCRIBE一次,把 SDP 里的sprop-parameter-sets或分辨率信息读出来确认,再决定用哪一路。下面这张表是我自己整理的选择依据,实际项目里基本照着这个走:

用途建议码流典型分辨率单路码率参考
人眼查看、录像存档主码流1080p / 4MP2~6 Mbps
算法检测、行为分析子码流720p / D10.5~1 Mbps
多路轮巡预览子码流720p0.5~1 Mbps
精细回放取证主码流1080p 以上4~8 Mbps

顺带说一句,码率不是固定的,设备端一般有 CBR(固定码率)和 VBR(可变码率)两种模式。VBR 在画面静止时码率会掉得很低,这对带宽友好,但对你的缓冲区估算不友好——按平均值算容量,遇到画面剧烈变化时会瞬间冲高。做容量规划时我一般按设备标称码率的 1.5 倍留余量。

2. 把推流地址拿到手的四条路

地址从哪儿来,这个问题看起来 trivial,实际上是整个流程里最容易卡住新人的地方。我见过太多人拿着一条从网上抄来的路径去试,试不通就怀疑设备坏了。实际上获取地址有四种常见途径,可靠性和适用场景完全不同,我按“从省事到硬核”排一下。

2.1 设备管理后台与说明书:最省事但最容易被忽略

绝大多数网络摄像机都带一个 Web 管理页面,用浏览器访问设备 IP 就能进。登录之后在“网络设置”“视频配置”“流媒体”这类菜单里,通常能直接看到 RTSP 地址,有些型号甚至会把完整地址(含用户名)以文本框形式给出,旁边还有个“复制”按钮。这是最省事的路径,也是最应该先试的。

但这里有两个坑。第一个是新版浏览器对旧设备的兼容问题,很多设备的管理页面依赖老式的插件或非标准接口,用现代浏览器打开是一片空白或者按钮点不动。我的经验是备一个旧版浏览器的便携版本,或者用支持多种渲染内核的工具去开,实在不行就走设备自带的配置工具。第二个坑是页面显示的地址不一定完整,有些设备只给你rtsp://IP:554/xxx,把用户名密码省略了,你得自己补;还有些设备显示的端口和实际监听端口不一致(页面上写 554,实际改成了 10554),这种时候以实际连通性为准。

说明书这一块也别小看。设备的接口文档里通常会有一张表,列出所有可用的取流路径及其对应的通道和码流,比你在页面上一个个试快得多。我的习惯是拿到新设备第一件事就是把这张表抄进项目文档,后面接第二台同型号设备时直接复用,省掉大量试错。

2.2 通用发现协议扫描:批量场景下的正解

单台设备手动配置没问题,一旦现场有几十台,逐个登网页就太慢了。这时候可以用行业里通用的设备发现机制——基于 WS-Discovery 的那套规范,设备在局域网里会响应组播探测,返回自己的服务地址、能力集和媒体配置信息。不少开源工具和脚本库都封装了这个流程,你只要在同一个二层网络里跑一次扫描,就能把在线设备的 IP、型号、固件版本、媒体服务地址全列出来。

扫描拿到的是“服务入口”,还需要再调一次媒体服务的接口去枚举 profile,每个 profile 对应一组编码配置,里面就包含 RTSP 的完整地址。这条路的好处是结构化、可批量、可自动化,我通常把它写成一个初始化脚本,设备上架后跑一遍就能生成一份地址清单,直接喂给采集程序。缺点是它对网络环境有要求:组播在很多交换机上默认被抑制,跨网段基本无效,虚拟化环境里也经常因为网卡模式问题收不到响应。

提示:如果扫描一直没结果,先确认你和设备在同一个网段、交换机没有开启组播抑制,再确认设备的发现功能没有被关掉。这三项排完还不行,就老老实实回到网页方式。

2.3 抓包反推:拿到组态软件里那条隐藏地址

有些场景比较尴尬:设备是别人装的,管理密码没人知道,但现场有一台正在正常预览的电脑。这时候最有效的办法是在那台电脑上抓一次包,从流量里把地址和认证过程还原出来。

具体做法是在预览软件正常播放的状态下抓取网卡流量,过滤rtsp协议,你会看到完整的交互过程:DESCRIBE请求里的 URI 就是路径,Authorization头里能看到认证方式和用户名(密码是摘要,看不到明文,但如果是 Basic 认证,base64 解一下就是明文)。抓到之后把 URI 拼上主机和端口,一条完整地址就有了。

这条路我用过两次,都在“接手别人做了一半的现场”这种情形下。它的价值在于你不需要设备的任何账号信息就能拿到路径结构,剩下的只是认证问题——如果原软件存了密码,从它的配置文件里通常也能找到。需要注意的是抓包只在你自己有管理权限的网络上做,别去碰不属于你的流量。

2.4 厂商SDK与开放接口:地址之外的附加能力

如果项目对稳定性要求高,或者需要设备端的一些附加能力(比如云台控制、报警输入输出、智能分析结果订阅),那么纯 RTSP 就不够了,得考虑设备提供的开发接口。这类接口一般会返回一个“实时流地址”,本质上还是 RTSP,但它是通过接口动态生成的,可能带有时效性的 token,比固定地址更安全。

用 SDK 的代价是绑定。不同厂商的接口形态差异很大,一旦换了设备型号,适配层就得重写。我的取舍原则是:如果项目里只有一种设备且长期不换,用 SDK 省事;如果是多品牌混用或者要长期维护,就把 RTSP 作为统一入口,SDK 只用来做云台控制和事件订阅这类 RTSP 覆盖不到的部分。这个分层在后来的项目里救过我一次——某个现场临时换了一批不同型号的设备,因为采集层只认 RTSP,改了地址配置就上线了,没动一行代码。

3. 认证与传输参数:拉流失败八成卡在这

地址拿到了,接下来是连接。我在统计自己踩过的坑时发现,拉流失败的原因里,认证问题占四成,传输方式问题占三成,剩下的才是路径和网络。所以这一段值得单独拎出来讲透。

3.1 Basic与Digest:为什么你把密码拼进URL还是不认

RTSP 的认证主要有两种。Basic是把用户名:密码做 base64 编码后放在请求头里,实现简单但等于明文传输,安全性差。Digest则是服务端先返回一个 401 并带上nonce(一次性随机串),客户端用密码、用户名、URI、nonce 一起做 MD5 运算生成响应值回传,密码本身不上网。

问题出在 URL 内嵌凭证这个用法上。很多人以为把密码写进 URL 就万事大吉,实际测试时会发现:对 Basic 认证的设备基本可用,对 Digest 认证的设备则经常失败或者要重试几次才成功。原因在于不同客户端对 URL 内嵌凭证的处理不一致——有些客户端拿到 401 后不会用 URL 里的密码去重算摘要,直接就报认证失败了。

我在实际项目里的做法是:能用 URL 内嵌就用,但一定要准备一条带显式认证头的回退路径。比如用命令行的方式时,很多工具支持单独传用户名密码参数;用代码拉流时,如果底层是 FFmpeg,可以通过设置选项的方式传认证信息,比拼 URL 更可靠。另外还有一个细节:部分设备对密码长度和字符集有限制,超过一定长度或者含有特殊符号时,Digest 计算会出问题,表现为“密码明明对但一直 401”。遇到这种情况,第一件事是去后台把密码改成纯字母数字再试一次,能通就说明是字符转义问题。

3.2 TCP还是UDP:花屏、卡顿与延迟的取舍

这是 RTSP 拉流里最经典的一个选择。RTP 数据默认走 UDP,好处是延迟低、没有队头阻塞;坏处是一旦丢包,解码器就会因为缺少参考帧而出现画面撕裂、马赛克甚至整段卡死。走 TCP 的话,RTP 数据会被复用(interleaved)到 RTSP 的那条 TCP 连接上,丢包由 TCP 重传兜底,画面干净,但代价是重传会带来延迟抖动,网络差的时候延迟会越积越大。

我的判断标准很简单:看你的业务能不能容忍花屏。做录像存档、做离线分析的,走 TCP,宁可延迟高一点也要保证每一帧完整;做实时预览、做低延迟控制的,走 UDP,配合应用层的丢帧策略,丢了就丢了,绝不等待。这一条我在多个项目里验证过,没有例外。

具体的参数写法在不同工具里不太一样。用 FFmpeg 系的命令行工具时是-rtsp_transport tcp;用 OpenCV 时不能直接传这个参数,需要通过环境变量把底层选项透传下去:

export OPENCV_FFMPEG_CAPTURE_OPTIONS="rtsp_transport;tcp"

注意这里的分隔符是分号不是空格,这个细节坑过我不止一次——写成空格的话选项会被忽略,而且不会有任何报错,你只会觉得“怎么设置了没效果”。

除此之外还有几个和传输相关的参数值得一提。-stimeout(新版改名为-timeout)控制的是 socket 超时,单位是微秒,设成 5000000 就是 5 秒;这个参数非常重要,因为默认情况下读不到数据可能会一直挂着,导致你的线程永远卡死在那里。-fflags nobuffer和-flags low_delay用来关闭输入缓冲和启用低延迟模式,对实时性要求高的场景必加。-probesize和-analyzeduration用来控制启动时探测流的时长,默认值比较大(探测几百 KB 甚至几 MB),会导致首帧出来得慢,调小到 32 和 0 能明显改善启动速度,但代价是对某些非标准流可能探测失败。

3.3 端口、NAT与跨网段:几个必须提前确认的连通性前提

RTSP 用 554 端口做控制,但如果数据走 UDP,实际传输用的是另外一组动态端口,通常是 50000 以上的高位端口或者成对的偶数/奇数端口。这就带来一个很现实的问题:如果设备和你的程序之间隔着防火墙或者路由器,你只放通了 554,控制能建起来,但数据一个包都收不到,表现就是“连接成功但一直没画面”。

排查这类问题的方法是从内到外逐层验证。先在设备所在网段内用最简单的方式测通,再逐步往你的程序所在位置靠。我常用的顺序是:同网段直连测试 → 跨交换机测试 → 跨网段测试,每一步都用命令行工具快速确认,不要一上来就在代码里调。跨网段时如果条件允许,优先让数据也走 TCP,这样只需要放通 554 一个端口,链路会简单很多。

还有一个常被忽略的点是设备的并发连接数限制。很多中低端设备同时只允许 4~6 路 RTSP 会话,超过之后新的连接会被直接拒绝或者返回 503。我遇到过一次“程序重启后连不上,等几分钟又好了”,最后查出来是上一次的连接没有被正常释放,设备那边还占着名额。解决办法有两个:一是程序退出时显式关闭连接,不要依赖进程结束;二是在采集层做连接数管控,把“拉流”和“分发”分开,一路流只从设备拉一次,然后分发给多个消费者。

4. 手把手验证:从命令行到代码的第一条流

我给自己定的规矩是:任何视频接入任务,先用命令行跑通,再写代码。命令行能排除掉 90% 的代码层问题,一旦命令行能出画面,后面的事就只是把命令翻译成 API 调用。

4.1 三条命令确认设备到底给不给你流

第一条命令用来看协议层是否正常,直接对一个 RTSP 地址发DESCRIBE:

ffprobe -v error -rtsp_transport tcp -show_streams "rtsp://user:pass@192.168.1.100:554/stream/main"

如果这条命令能返回流的编码格式、分辨率、帧率、码率信息,说明地址、认证、网络三层全通了。如果报 401,是认证问题;报 404,是路径不对;超时,是网络或端口问题。这三种错误信息区分度很高,比在代码里看一个False有用得多。

第二条命令用来验证解码和画面是否正常:

ffplay -rtsp_transport tcp -fflags nobuffer -flags low_delay -framedrop "rtsp://user:pass@192.168.1.100:554/stream/main"

-framedrop会在解码跟不上时主动丢帧,保证画面跟得上实时,调试时用它可以看出这路流到底卡不卡。如果画面出来了但有规律的绿块或条纹,基本可以判定是编码格式和传输方式的问题,先切 TCP 再试。

第三条命令用来确认实际码率和关键帧间隔,这两个数据对你后面设计缓冲策略至关重要:

ffprobe -v error -rtsp_transport tcp -select_streams v:0 -show_entries stream=avg_frame_rate,bit_rate,width,height -of default=nw=1 "rtsp://user:pass@192.168.1.100:554/stream/main"

4.2 Python + OpenCV 拉流的最小可运行骨架

命令行通了之后,用 OpenCV 拉流是最快的落地方式。但直接VideoCapture(url)写一行就完事的人,十个里有八个会在后面遇到延迟堆积的问题。下面这个骨架是我实际在用的一版,加了超时和缓冲控制的处理:

import os import cv2 # 关键:把底层选项透传下去,分号分隔 os.environ["OPENCV_FFMPEG_CAPTURE_OPTIONS"] = \ "rtsp_transport;tcp|stimeout;5000000|fflags;nobuffer" URL = "rtsp://user:pass@192.168.1.100:554/stream/sub" cap = cv2.VideoCapture(URL, cv2.CAP_FFMPEG) if not cap.isOpened(): raise RuntimeError("拉流失败,先回到命令行确认地址和认证") # 缓冲区设为 1,尽量只保留最新帧 cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) fail_count = 0 while True: ok, frame = cap.read() if not ok: fail_count += 1 if fail_count >= 5: cap.release() # 这里应该做退避重连,而不是立刻狂刷 break continue fail_count = 0 # 到这里拿到的是 BGR 的 numpy 数组,可以直接送检测 cv2.imshow("preview", frame) if cv2.waitKey(1) == 27: break cap.release() cv2.destroyAllWindows()

这段代码有几个地方值得说明。cv2.CAP_FFMPEG这个后端参数建议显式指定,不写的话 OpenCV 会按自己的顺序去试各种后端,遇到同名协议可能选错。CAP_PROP_BUFFERSIZE这个属性在不同后端下表现不一致,FFmpeg 后端对它支持有限,更多时候起作用的是前面那个fflags;nobuffer环境变量——这两个要一起用。fail_count那段是防御性的,连续读失败说明连接已经废了,继续读只会不断返回 False,必须有退出和重建的逻辑。

还有一个实用技巧:如果你的场景只需要最新的一帧(比如定时抓图或者做状态判断),可以在循环里多读几次丢掉积压的旧帧。我在一个定时截图的场景里用过这个方法,每秒读 5 次、只保留最后一次,能有效避免“拿到的是几秒前的画面”这个问题。

4.3 FFmpeg 转封装与抽帧的常用参数组合

有些场景不需要解码,只是要把流存下来或者转成别的协议,这时候用 FFmpeg 命令行比写代码高效得多。存原始流不重编码,CPU 几乎零占用:

ffmpeg -rtsp_transport tcp -i "rtsp://user:pass@192.168.1.100:554/stream/main" \ -c copy -f segment -segment_time 600 -reset_timestamps 1 \ "record_%Y%m%d_%H%M%S.mp4"

-c copy表示不重新编码,直接复制音视频包;-f segment配合-segment_time按时间切片,600 秒一个文件,-reset_timestamps 1保证每个文件的时间戳从零开始,否则播放器会打不开后续的片段。这个组合我在长时间录制里用得最多,稳定性和资源占用都很好。

定时抽帧则简单得多:

ffmpeg -rtsp_transport tcp -i "rtsp://user:pass@192.168.1.100:554/stream/sub" \ -vf fps=1 -q:v 2 "snap_%05d.jpg"

fps=1表示每秒抽一帧,-q:v 2是 JPEG 质量(数值越小质量越高)。抽帧时要注意,抽出来的帧未必正好落在关键帧上,如果你对画面的完整性有要求(比如要做行人重识别),建议用-skip_frame nokey只取关键帧,配合-vsync 0避免重复帧。

5. 实测中最容易翻车的六个细节

前面讲的都是“怎么做”,这一段讲“做完之后哪里会出问题”。这几条都是我实际踩过或者帮别人排查过的,按出现频率排序。

5.1 H.265与解码器不匹配:能连上却是黑屏

现在很多设备默认用 H.265 编码,码率比 H.264 低三到五成,对存储和带宽都友好。但问题在于,很多预编译的计算机视觉库并不带 H.265 的解码能力。表现很典型:ffprobe能看到流信息,isOpened()返回 True,read()却一直返回 False,或者返回的帧全是黑的。

这个坑我自己踩过一整晚。排查的正确顺序是:先用ffprobe看编码格式(codec_name字段,是hevc就是 H.265),如果确认是 H.265 而你的库解不了,有两条路可走——一是在设备端把编码改成 H.264,这是最省事的,代价是码率上升;二是在采集层用支持 H.265 的解码后端,把解码后的帧再交给上层。

我一般优先选第一条,因为改设备配置一劳永逸,而且很多设备的双码流可以分别设置编码格式,主码流保持 H.265 给录像用,子码流转成 H.264 给分析用,两边都不耽误。

5.2 缓冲堆积导致画面越跑越晚

这是实时预览场景里出现频率最高的问题,症状很有辨识度:刚连上时画面延迟一两秒,跑十分钟后延迟变成十几秒,而且越跑越大。根本原因是解码速度跟不上产生速度,帧在缓冲区里排队,队越排越长。

解决办法分两层。第一层是从源头减少数据量,用子码流、降分辨率、降帧率。第二层是在读取端做抓最新帧的处理——不要用read()逐帧消费,而是用一个独立线程持续读、覆盖式地更新“最新帧”变量,业务线程只取这个变量。这样无论底层产生多快,业务端拿到的永远是最新的,延迟恒定。

还有一个小细节:如果你用cv2.waitKey做显示,它的等待时间也会影响消费速度。waitKey(1)理论上 1 毫秒,但受系统调度影响可能更长,在高端机器上不明显,在低配设备上就成了瓶颈。我一般会把预览和采集放到不同线程里,预览可以丢帧,采集不能丢。

5.3 断流重连:别让一个异常拖垮整条采集线程

网络抖动、设备重启、交换机闪断,这些在现场都是常态。如果你的采集逻辑是“读失败就 break”,那程序一天要挂几十次;如果是“读失败就 while 死循环重试”,那 CPU 会被打满,日志也会被刷爆。

我用的重连策略是指数退避加连接重建:连续读失败达到阈值后,先释放旧连接,然后按 1 秒、2 秒、4 秒、8 秒、最大 30 秒的间隔重试,重试成功就重置计数。这个策略的好处是在短暂抖动时能快速恢复,在设备真的宕机时不会把资源耗光。

import time def robust_reader(url, on_frame, max_backoff=30): backoff = 1 while True: cap = cv2.VideoCapture(url, cv2.CAP_FFMPEG) if not cap.isOpened(): time.sleep(backoff) backoff = min(backoff * 2, max_backoff) continue backoff = 1 fails = 0 while True: ok, frame = cap.read() if ok: fails = 0 on_frame(frame) continue fails += 1 if fails >= 5: break cap.release() time.sleep(1)

另外要提醒一点:重连时一定要确保旧连接真的释放了。有些底层库在release()之后并不会立刻关闭 socket,如果重连太频繁,设备端的会话数会被占满,反而导致一直连不上。这就是前面提到的并发连接限制问题的来源。

5.4 多路并发的连接数与码率账本

单路能跑通,不代表 20 路能跑通。多路场景里有两个硬约束:设备端的并发会话数和你本机的解码能力。

设备端这块,前面说过常见值是 4~6 路。如果现场有 20 台设备,每台只需要拉 1 路,那没问题;但如果是一台设备要供多个消费者用,就必须在应用层做分发——从设备拉一次,转成本地可复用的形式(比如转成内部的流服务或者共享内存),让所有消费者从本地取。

本机解码能力这块,H.264 的 1080p 软解在普通四核上大概能跑 8~12 路(取决于具体实现和优化程度),H.265 会再打个对折。如果用硬件解码(集成显卡的媒体引擎或者独立加速卡),这个数字能翻几倍,但要注意硬件解码对分辨率、编码档次的支持有差异,不是所有流都能硬解。我的经验是先按软解估一个保守值,跑起来看实际 CPU 占用,再决定要不要上硬件加速。

下面这张表是我用来快速估算资源的,实际项目里按这个砍数量基本不会翻车:

场景路数码率合计软解CPU占用(参考)
子码流 720p 分析20约 15 Mbps40%~60%
主码流 1080p 录像(不解码)20约 60 Mbps5% 以下
主码流 1080p 预览8约 25 Mbps60%~80%
主码流 H.265 1080p 预览8约 15 Mbps80% 以上

注意“不解码只转封装”这一行的 CPU 占用极低,这是个很有价值的优化点——如果只是要存录像,就千万别解码,直接-c copy转存,省下来的算力可以留给分析任务。

6. 从“能看”到“能跑”:工程化收尾的几处取舍

命令行能出画面、代码能读到帧,这只是原型阶段。要让它长期稳定跑在现场,还得做一些结构上的取舍。

6.1 拉流层与业务层解耦

我见过的最常见的架构问题是每个业务模块各自拉一次流。检测模块拉一次,录像模块拉一次,预览模块再拉一次,同一台设备上挂了四五个连接,很快就到并发上限了。正确的做法是把拉流单独抽成一层,这一层的职责只有三件事:维持连接、拿到最新帧、把帧分发给订阅者。

这一层对外暴露的接口越简单越好,我一般只给两个:一个是“取最新帧”,一个是“订阅帧回调”。业务层完全不关心底下是 RTSP 还是别的协议,也不关心重连逻辑,它只管消费。这个结构带来的好处在设备更换时体现得最明显——换设备只需要改拉流层的地址配置,业务代码一行不动。

解耦之后还有一个额外收益:你可以在拉流层做统一的帧率控制。比如设备给 25 帧,业务只需要 5 帧,那就在分发时做抽样,省下来的算力相当可观。我在一个多路分析项目里加了这层抽样之后,整体 CPU 从 90% 降到了 55%,效果比换硬件还明显。

6.2 录制、抽帧、转发的分工

最后说一下这三类需求的处理方式,因为它们对流的消费方式完全不同,混在一起做会互相拖累。

录制追求完整,不能丢帧,所以它应该走“不重新编码、直接封装”的路线,用-c copy的方式把压缩包原样存下来。这条路线几乎没有 CPU 开销,也不会因为解码慢而丢数据。

抽帧追求效率,只关心特定时刻的画面。它的最优解不是解码整条流再挑帧,而是让解码器只解关键帧,或者干脆用带fps滤镜的抽帧命令。如果只是做定期截图,间隔设为秒级甚至分钟级,资源占用可以忽略不计。

转发追求实时,要把流送到别的消费者那里。这时候最好也不解码,用转封装的方式把 RTSP 转成别的输出形式,保持压缩状态传递,延迟和开销都是最低的。

这三条路线有个共同点:能不解码就不解码。解码是整个链路里最贵的一步,一旦解了,就意味着你要承担完整的分辨率和帧率。我在项目里养成的习惯是,每加一个功能先问一句“这个功能真的需要解码后的像素吗”,答案往往是“不需要”,然后就能用便宜得多的方案实现。

按这个思路搭出来的结构,一台普通机器扛十几路采集加分析是没问题的。真正的瓶颈往往不在算力,而在于一开始有没有把连接数、码流选择和缓冲策略这三件事想清楚。

我个人在实际操作中的体会是,RTSP 接入这件事的技术难度并不高,难的是它涉及的环节太多,任何一个环节的参数没对齐,表现都是同一个“没画面”。所以每次出问题,别急着改代码,先按“地址对不对、认证通不通、端口开没开、编码认不认、缓冲堆没堆”这五步走一遍,绝大多数问题在第三步之前就能定位。我后来干脆把这五步做成了一张排查清单贴在显示器边上,比任何调试工具都管用。

返回列表