📹 第八章 ONVIF框架代码案例应用
📄 一句话概括

这一节是第八章的总收口:发现 → 取信息 → 取配置 → 取流地址,四步全走通。 四步里有两步的耗时差了一个数量级,原因和 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 秒实验

  1. 把四步里的 Connection: close 全改成 keep-alive,重新量一次累计耗时。
  2. 把 GetStreamUri 的 token 改成一个不存在的名字,看设备回什么。
  3. 在 GetProfiles 之前插一次 GetCapabilities,看能不能省掉后面某些调用。

常见误区

  1. 以为「ONVIF 慢」是协议本身重。 大头是四次 TCP 建连, 把 Connection: close 改成 keep-alive 就能省掉大半。
  2. 把 Profile_1 写死在代码里。 换一台设备就全崩,各家 token 命名完全不同, 必须从 GetProfiles 取。
  3. 不判空就把流地址丢给 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 用的那套信令。

相关笔记