📹 第七章 ONVIF协议原理解析
📄 一句话概括

ONVIF 的「Web 服务」在实现上就两样东西:一组 XAddr(地址)和一张 GetServices 能力表。 地址回答「发到哪」,能力表回答「有哪些、版本是什么」——两样要分开问。

📝 本节和前面几节的关系

上一节讲清了前缀,这一节讲这些前缀指向的东西怎么被「发现」和「寻址」。 06.01.06 讲过 Web 服务的通用概念,这里讲 ONVIF 把它具体落成了什么形状。

一、三个要素,两样落地

教科书上 Web 服务有三个要素:服务描述、服务地址、消息格式。 ONVIF 把前两个都塞进了一个响应里:

要素 ONVIF 里是什么 从哪拿
服务地址 XAddr(完整 URL) GetServices
服务描述 WSDL + 命名空间 规范包,不是设备
消息格式 SOAP 1.2 信封 固定

第二行那个「规范包,不是设备」是第六章反复强调过的一条: 设备上那句 ?wsdl 只用来验地址通不通,真文件从 ONVIF 官网的规范包里取。 原因很实际:设备固件里的 WSDL 经常是残的、旧的、甚至压根没有。 所以「接一台新设备」的正确顺序里,第一步永远是 GetServices—— 不是因为它能给 WSDL,而是因为它能给地址。 WSDL 你手上早就有,缺的只是那个 IP 加端口。

二、地址和能力分头问

很多人以为 GetServices 一条就能问清全部,其实它只回答一半:

问题 问哪个操作 看哪一列
这台设备有哪些 service GetServices 看 Namespace 列表
某个 service 在哪 GetServices 看 XAddr
某类操作支不支持 GetCapabilities 看大类里的布尔开关

第三行是很多人不知道的: GetCapabilities 里有一堆 <tt:RTP_TCP>true</tt:RTP_TCP> 这样的开关, 它们回答的是「支不支持某个特性」,不是「地址在哪」。 所以「有没有云台」要两边都看:GetServices 里有 ptz 说明有这个 service, GetCapabilities 里的 PTZ 段说明它支持哪些动作(比如有没有 AbsolutePanTilt)。 只看一边,就会得出「有云台但不能绝对定位」这种半对的结论。

三、那个 2.60 是规范版本

GetServices 的返回里,每个 service 都带一个版本号,本例全是 2.60。 这个数不是固件版本,是「该 service 实现到哪一版规范」。

看到的数 是什么 去哪查真实固件版本
GetServices 的 2.60 规范版本 不适用
GetDeviceInformation 的 FirmwareVersion 固件版本 就在这里

混用会导致一个常见误判: 以为「service 版本 2.60」就说明设备新——其实同一代规范下,所有设备都报这个数。 它更像「我这份接口是照 2.60 版规范做的」,不是「我是 2023 年的产品」。 这个数真正的用途是「判兼容性」:规范版本低的设备,某些可选操作可能没有; 但「有没有」最终还是得看操作本身能不能调通,版本号只能给个预期。

四、三个 RTP 开关决定流怎么走

GetCapabilities 里有三个和传输有关的布尔开关,它们直接决定「这路流卡不卡」:

开关 本例 含义
RTPMulticast false 组播:一路发给多个人
RTP_TCP true RTP 也走 TCP,能穿 NAT,延迟略高
RTP_RTSP_TCP true 只有 RTSP 走 TCP,媒体仍走 UDP

第二行和第三行差一个词,效果完全不同:RTP_TCP 是媒体走 TCP(可靠但有延迟); RTP_RTSP_TCP 只是信令走 TCP,媒体仍走 UDP(快但会丢包)。 很多设备只支持第三行——这意味着「信令可靠、媒体不可靠」是它唯一的模式, 在丢包严重的网络里就会花屏。 要稳,就得要求设备支持 RTP_TCP,这也是选型时该写进技术参数的一条。 第一行是 false,说明这台设备不支持组播——而 Profile S 并不强制组播, 所以「不支持组播」不影响它符合 S,但影响「一台上百人看同一路」的方案。

五、动手 Demo:Web 服务,就是一组地址 + 一张能力表

Demo 先问 GetServices,把四组地址和版本号抓出来; 再问 GetCapabilities,把三个传输开关抓出来。 两样东西合起来,才是「这台设备能干什么」的完整答案。

# ① 地址 + 版本(GetServices)
_a, b, _n = S.call(port, "device", "GetServices")
for svc in ET.fromstring(b).iter("{%s}Service" % S.NS_TDS):
    ns = svc.find("{%s}Namespace" % S.NS_TDS).text
    xa = svc.find("{%s}XAddr" % S.NS_TDS).text
    ver = svc.find("{%s}Version/{%s}Major" % (S.NS_TDS, S.NS_TDS))
    print(ns.split("//")[-1], xa.split("/")[-1], ver.text)

# ② 能力开关(GetCapabilities)
_a, b2, _n = S.call(port, "device", "GetCapabilities")
r2 = ET.fromstring(b2)
for k in ("RTPMulticast", "RTP_TCP", "RTP_RTSP_TCP"):
    e = r2.find(".//{%s}%s" % (S.NS_TT, k))
    print(k, e.text if e is not None else "-")

实跑输出:

--------------------------------------------------------------------
Web 服务,就是「一组地址 + 一张能力表」
--------------------------------------------------------------------
**07.02.03 讲的「Web 服务」,在 ONVIF 里落成两样东西:**
  **一组 XAddr(地址),一张 GetServices 的能力表。**

**`GetServices` 的返回,四列信息:**

  Namespace 尾段                XAddr 尾段                    版本
  ----------------------------------------------------------------------
  ver10/device                  device_service                2.60
  ver10/media                   media_service                 2.60
  ver20/ptz                     ptz_service                   2.60
  ver10/events                  events_service                2.60

  **第三列那个版本号是「规范版本」,不是设备固件版本**——
  **`2.60` 意思是「这个 service 实现到 2.60 版规范」。**
  **设备的固件版本在 `GetDeviceInformation` 里**,两码事。
  **混用会导致一个常见误判**:
  **以为「service 版本 2.60」就说明设备新**——**其实所有设备都报这个数**。

**为什么「地址」和「能力」要分开问:**

  问题                        问哪个                  为什么
  --------------------------------------------------------------------------------------
  这台设备有哪些 service      `GetServices`           **看 Namespace 列表**
  某个 service 在哪           `GetServices`           **看 XAddr**
  某类操作支不支持            **`GetCapabilities`**   **看大类里的布尔开关**

  **第三行是很多人不知道的**:
  **`GetCapabilities` 里有一堆 `<tt:RTP_TCP>true</tt:RTP_TCP>` 这样的开关**,
  **它们回答的是「支不支持某个特性」,不是「地址在哪」。**
  **所以「有没有 PTZ」要两边都看**:  **`GetServices` 里有 `ptz` 说明有这个 service**,
  **`GetCapabilities` 里的 `PTZ` 段说明它支持哪些动作。**

**看一眼 `GetCapabilities` 里的流传输开关:**

  `RTPMulticast` = **false**
  `RTP_TCP` = **true**
  `RTP_RTSP_TCP` = **true**

  **这三个开关决定了「流能怎么传」**:
  **`RTPMulticast` 是组播**:一路发给多个人。
  **`RTP_TCP` 是 RTP 走 TCP**:能穿 NAT,但延迟略高。
  **`RTP_RTSP_TCP` 是只把 RTSP 走 TCP**:媒体仍然走 UDP。
  **很多设备只支持 `RTP_RTSP_TCP`**——  **这意味着「信令走 TCP、媒体走 UDP」是它唯一的模式**,
  **在丢包严重的网络里会花屏。**
  **要走的稳,就得要求设备支持 `RTP_TCP`**(媒体也走 TCP)。

两个响应都真发真收,XAddr 里的端口是运行时随机分配的, 所以写进笔记时只保留了路径尾段——照抄完整地址每次都不同, 这也正好说明「地址不能硬编码」。 版本号我只取了 Major(就是那个 2.60 里的 2 那段的首位), 没拼成完整字符串—— 因为 Version 是个三段的复合元素(Major/Minor),完整值是 2.60,我取的是它的第一段。 GetCapabilities 里那三个开关是布尔值,解析出来是字符串 true/false, 不是 Python 的布尔——所以判真得写 == u"true",不能直接 if e.text, 因为字符串 false 在 Python 里也是真。 这个坑在 07.02.11 的事件处理里还会再出现一次。

30 秒实验

  1. 把 GetServices 返回的 service 个数数出来,和 07.01.03 那张十一组的表比一比。
  2. 改 GetCapabilities 的解析路径,把 Media 段整个打印出来,看里面还有多少开关。
  3. 把三个 RTP 开关里的 true 判断改成 == u"true",再改成直接判文本,看哪个会误判。

常见误区

  1. 以为 GetServices 能给出全部能力。 它只给地址和版本, 特性开关在 GetCapabilities 里。
  2. 以为那个 2.60 是固件版本。 它是规范版本,固件版本在 GetDeviceInformation 里。
  3. 以为 RTP_TCP 和 RTP_RTSP_TCP 是一回事。 前者是媒体走 TCP,后者只是信令走 TCP, 媒体仍走 UDP。
自测三问(点开对答案) 1. ONVIF 的 Web 服务在实现上是哪两样东西? —— **一组 XAddr(地址)和一张 `GetServices` 的能力表。** **地址回答「发到哪」,能力表回答「有哪些」。** 2. `GetServices` 返回的 2.60 是什么版本? —— **规范版本**,不是固件版本。**它表示「这个 service 实现到 2.60 版规范」。** 3. `RTP_TCP` 和 `RTP_RTSP_TCP` 有什么区别? —— **前者让媒体也走 TCP**(可靠、延迟略高); **后者只让 RTSP 信令走 TCP,媒体仍走 UDP**(快但会丢包)。

下一节讲设备发现:一条 UDP 组播报文怎么换回来一个 IP 地址。

相关笔记