这一节是第八章的总收口:发现 → 取信息 → 取配置 → 取流地址,四步全走通。 四步里有两步的耗时差了一个数量级,原因和 ONVIF 无关。
08.02 讲发现、08.03 讲信息、08.05 讲取流,这一节把它们串成一条链。 书上这一节给的是完整案例代码,本笔记用等价的最小实现走同一套步骤, 每步要填的字段完全一致。
一、四步闭环
一个能用的设备接入流程,就是这四步:
| 步 | 动作 | 拿到什么 |
|---|---|---|
| 1 | WS-Discovery Probe | XAddrs |
| 2 | 解析应答 | 设备地址 |
| 3 | GetDeviceInformation |
厂家型号 |
| 4 | 取配置 + 流地址 | RTSP URI |
第一、二步是一件事的两半: Probe 发出去是一瞬间的事,收应答要等—— 所以第 1 步和第 2 步在代码里通常合成一个函数(超时参数就归它管)。 第四步那个「取配置 + 流地址」是两次调用,不是一次——下面会专门讲为什么不能省。 四步的顺序不能换:没有 XAddrs 就不知道往哪发 SOAP,没有 ProfileToken 就取不到流地址。
二、实测耗时:一步占了九成
四步分别计时,差别大得意外:
| 步 | 动作 | 耗时 | 累计 |
|---|---|---|---|
| 1~2 | Probe + 应答 | 0.71 毫秒 | 0.80 |
| 3 | 取设备信息 | 0.94 毫秒 | 1.74 |
| 4 | 配置 + 取流 | 12.79 毫秒 | 14.54 |
第一行那个 0.71 毫秒是「本机自发自收」的结果——真设备上这一行会是几十到几百毫秒,
因为它要走真实网络、还要等设备响应。
第三行那 12.79 毫秒是真实现象,而且原因与 ONVIF 无关:每个请求都带 Connection: close,
所以每次调用都要新建一次 TCP 连接——12.79 毫秒的大头就是三次握手。
这个结论能推广:SOAP over HTTP 的调用耗时里,协议处理往往只占小头,
真正贵的是连接建立。
所以「四步花了 14 毫秒」不能理解成「ONVIF 慢」,它是「四次 TCP 建连」的代价。
三、第三步为什么是两次调用
因为 GetStreamUri 必须带 ProfileToken,而这个 token 只能从 GetProfiles 里拿:
| 做法 | 自己的设备 | 换一台设备 |
|---|---|---|
写死 Profile_1 |
能跑 | 全崩 |
从 GetProfiles 拿 |
能跑 | 能跑 |
第一行那个「能跑」是最坑的:很多人就是这么写通了自己的摄像机,
于是以为 token 就是 Profile_1。
实际上各家命名完全不同:有的叫 Profile_101、有的叫 MainStream、有的干脆是 000。
第二行多了一次往返,但这 12 毫秒换的是「换设备不用改代码」——这笔账在任何工程里都值。
所以「先取 token 再用 token」不是啰嗦,是唯一正确的顺序。
四、流地址要判空
这一步最容易被跳过,后果也最隐蔽:
| 返回 | 含义 | 怎么办 |
|---|---|---|
rtsp://… |
正常 | 直接用 |
| 空串 | 配置有问题 | 报错别传下去 |
第二行那种情况真的会发生:设备有 Profile、但没配编码参数,
GetStreamUri 就返回一个空 URI。
如果直接把空串丢给 FFmpeg,
它报的是「Protocol not found」——看起来像「不支持这个协议」,其实是上游给空了。
这个误判会把人带到完全错误的方向:去查 FFmpeg 版本、查协议支持、查编译选项,
而真正的原因在设备配置。
所以封装的 stream_uri 应该在为空时直接抛异常,
异常信息里写清「设备返回空 URI」——这一句能把排查时间从半小时压到一分钟。
五、三条工程经验
| 经验 | 为什么 |
|---|---|
| Probe 只做一次 | 缓存 XAddrs |
token 从 GetProfiles 拿 |
写死必翻车 |
| 流地址要判空 | 别直接丢给 FFmpeg |
第一条的意思是「别在取流前 Probe」:每次取流都 Probe,等于每次多一轮 UDP,
而 XAddrs 在设备不换 IP 的情况下是稳定的。
但缓存要留失效出口:设备重启后 XAddrs 可能变,所以连不上时应该「清缓存、重新发现」,
而不是死磕那个旧地址。
第二条和第三条前面都说过了,
这里只强调它们的共同点:都是「不要假设」——不要假设 token 叫什么、不要假设地址一定非空。
ONVIF 是个「能力协商」型协议,
它给你 GetCapabilities、GetProfiles、GetVideoEncoderConfigurations 这些查询接口,
就是为了让你别猜。
六、这段代码在真实项目里怎么放
案例代码是为了讲清楚步骤,直接拿去用还差三样东西:
| 要补的 | 补在哪 |
|---|---|
| 失败重试 | 调用方那一层 |
| 日志 | 每次调用的收发 |
| 超时分级 | 发现短、取流长 |
第一行那个「调用方那一层」是有讲究的:重试策略不该进封装层(上一节讲过), 但案例代码里连调用方都没有,所以真实项目里要自己在外面套一层。 第三行那个「超时分级」来自实测:发现是 UDP,等一两秒没应答就该放弃; 而取流地址要走设备内部去准备编码器,可能要几秒。一个超时值套所有步骤,一定会有一头不对。 第二行看着最不重要,其实最省事:ONVIF 的故障现场经常只能靠「当时发了什么、回了什么」判断, 而报文里全是命名空间,人眼根本读不出来。把收发原文按天存盘,是这类项目最值的一笔投入。
七、动手 Demo:完整的设备查找案例
Demo 就是这四步的完整实现,四步都真跑,每步单独计时。 输出里的耗时、XAddrs、流地址全部来自本次运行,端口号每次启动都不一样。
cam = MiniDevice()
port = cam.start() # 本机模拟一台设备
t0 = time.perf_counter()
devs = S.harvest(timeout=1.0) # 1~2 步:Probe + 收应答
t1 = time.perf_counter()
_, info, _ = cam.call_sec(port, "devicemgmt", "GetDeviceInformation")
t2 = time.perf_counter()
_, prof, _ = cam.call_sec(port, "media", "GetProfiles")
_, uri, _ = cam.call_sec(port, "media", "GetStreamUri")
t3 = time.perf_counter()
print("XAddrs →", devs[0].xaddrs)
print("分步 %.2f / %.2f / %.2f 毫秒"
% ((t1 - t0) * 1000, (t2 - t1) * 1000, (t3 - t2) * 1000))
print("四步累计 %.2f 毫秒" % ((t3 - t0) * 1000))
实跑输出:
--------------------------------------------------------------------
完整的设备查找案例
--------------------------------------------------------------------
**这一节把第八章前面所有东西串一遍**:
**发现 → 取信息 → 取配置 → 取流地址**,四步全走通。
**这就是书上 08.08.11 那个「设备查找的案例代码」在做的事**,
**只不过书上用 gSOAP 生成的函数,我用等价的最小实现**——
**步骤、顺序、要填的字段完全一致。**
**四步的实测耗时**:
步 动作 耗时 累计
---------------------------------------------------------------------
1~2 Probe + 收 XAddrs 0.71 毫秒 0.80 毫秒
3 GetDeviceInformation 0.94 毫秒 1.74 毫秒
4 GetProfiles + GetStreamUri 12.79 毫秒 **14.54 毫秒**
**发现出来的设备**:
XAddrs → `http://127.0.0.1:52736/onvif/device_service`
厂家/型号 → ACME/IPC-2000
固件 → 5.6.1,序列号 → SN20240501
通道 Profile_1:1920×1080
流地址 → `rtsp://127.0.0.1:554/onvif/profile1`
**注意第三行那个「GetProfiles + GetStreamUri」是两次调用**:
**因为 `GetStreamUri` 必须带 `ProfileToken`**,
**而这个 token 得先从 `GetProfiles` 里拿**——
**不能猜、不能硬编码。**
**很多人写死 `Profile_1` 能跑通自己的摄像机,**
**换一台 token 叫 `Profile_101` 的设备就全崩。**
**最后是这套流程在工程上的三条经验**:
经验 为什么
------------------------------------------------------------------------------------
**Probe 只做一次**,缓存 XAddrs 每次取流都 Probe,等于每次多一轮 UDP
**token 一定从 `GetProfiles` 拿** 各厂家命名不一样,写死必翻车
**流地址要判空** 返回空串说明配置有问题,**别直接丢给 FFmpeg**
**第三行那种情况真的会发生**:**设备有 Profile 但没配编码参数**,
**`GetStreamUri` 返回一个空 URI**,
**FFmpeg 拿到空串报的是「Protocol not found」**——
**看起来像协议不支持,其实是上游给空了。**
输出里那个端口每次都不一样(这次是 52736),
因为模拟设备每次起在随机空闲端口上——这也是为什么正文不能写死端口号。
四步累计的 14.54 毫秒里,最后一步占了 12.79,而它贵的原因是四次 TCP 建连,
不是 SOAP 解析慢。
这一点在真设备上会更明显:局域网里一次 TCP 建连一两毫秒,四次就是四到八毫秒,
而 SOAP 报文解析通常在百微秒量级。
如果哪天要把这条链压到最快,方向不是「换更快的 XML 库」,
是「复用连接」——也就是把 Connection: close 改成 keep-alive,四次建连变一次。
最后提醒一句 token:Demo 里 Profile_1 能跑,是因为模拟设备就叫这个名字;
真实设备上必须从 GetProfiles 的返回里取。
30 秒实验
- 把四步里的
Connection: close全改成 keep-alive,重新量一次累计耗时。 - 把
GetStreamUri的 token 改成一个不存在的名字,看设备回什么。 - 在
GetProfiles之前插一次GetCapabilities,看能不能省掉后面某些调用。
常见误区
- 以为「ONVIF 慢」是协议本身重。 大头是四次 TCP 建连,
把
Connection: close改成 keep-alive 就能省掉大半。 - 把
Profile_1写死在代码里。 换一台设备就全崩,各家 token 命名完全不同, 必须从GetProfiles取。 - 不判空就把流地址丢给 FFmpeg。 空串报的是「Protocol not found」,看起来像协议不支持, 容易把人带到完全错误的方向。
自测三问(点开对答案)
1. 四步里为什么第三步是两次调用? —— **`GetStreamUri` 必须带 `ProfileToken`**,而这个 token 从 `GetProfiles` 来。 多一次往返,换来「换设备不用改代码」。 2. 14.54 毫秒里那 12.79 是怎么来的? —— **四次 TCP 建连**,因为每个请求都带 `Connection: close`。 SOAP 报文解析在这个量级里只占百微秒。 3. 流地址为空时直接传给 FFmpeg 会看到什么错? —— **「Protocol not found」**。它看起来像协议不支持,其实是上游给了空串,排查方向会被带错。第八章到此结束: 从 WSDL 到能跑的四步闭环,全走通了。下一章进 SIP,也就是 GB/T 28181 用的那套信令。
- ☐ (本章完,下一节 09.01.01 SIP的功能)