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

ONVIF 文档里反复出现五个词:Device、Service、Profile、Configuration、Token。 它们不是并列的五个概念,是一条引用链——最外层是设备,最内层是 Token。

📝 本节和前面几节的关系

07.01 讲完了 ONVIF 的来由、框架、规范和 Profile,那七节是「总体」。 07.02 这十七节是「逐个功能拆开」,第一节先把五个基础概念的关系摆清楚。

一、五个词,一条链

先看这条链的方向:设备提供 service,service 里的操作用 Profile 要流, Profile 引用一串 Configuration,而每一个对象都靠 Token 被指代。

概念 是什么 被谁引用
Device 一台物理设备 (最外层)
Service 设备上的一组接口,各有地址 Device 提供多个
Profile 一套「源 + 编码」的绑定 被 GetStreamUri 引用
Configuration 一组可改的参数 被 Profile 引用
Token 上面每一个对象的身份证号 所有请求都用它指对象

从上往下是「包含」,从下往上是「指代」。 这个区分很重要:包含关系决定「删一个对象会不会连带别的」,指代关系决定「请求里该填什么」。 最后一行的 Token 是链子的扣——没有它,这条链在代码里就断了, 因为没有一个操作是「按名字找对象」的。

二、Token 不是名字

ONVIF 里每个对象都有两样:一个给人看的 Name,一个给代码用的 token。 请求里一律用 token,Name 从来不参与寻址。

字段 例子 能不能改 用途
Name 「大门口主码流」 能,随便改 给人看
token VideoEncoder_1 不能,设备分配 给代码用

第二行那个「不能改」是硬约束:token 由设备在创建对象时分配,客户端只能读、不能指定。 所以「把主码流的 token 改成 main」这件事在 ONVIF 里做不到—— 要改只能删了重建,而重建后的 token 还是设备说了算。 这也是为什么工程代码里到处是「先 Get 再拼请求」:token 是运行期才知道的,编译期写不出来。

三、四层 Token 全在同一个响应里

这是 GetProfiles 最实用的一个性质:它一次返回四层对象,四层的 token 都在里面。

层 token 值 从哪个响应里拿到
Profile Profile_1 GetProfiles
Profile 里的源配置 VSC_1 GetProfiles 的嵌套
物理视频源 VideoSource_1 从父块的 SourceToken 拿
编码配置 VideoEncoder_1 GetProfiles 的嵌套

所以「接一台陌生设备」的最短路径是两条报文:第一条 GetServices(问地址), 第二条 GetProfiles(问 token)。后面所有请求的 token 都从第二条里取。 第三行那个 SourceToken 要特别注意它的来源—— 它不是 Profile 的属性,是 VideoSourceConfiguration 里的一个子元素, 而另外三个是 XML 属性(token=…)。 同一份报文里两种写法混用,这是解析 ONVIF 最容易漏取值的地方。

四、属性还是子元素

ONVIF 的 XML 里,标识一个对象有两种写法,长得完全不同:

<tt:VideoEncoderConfiguration token="VideoEncoder_1">
  <tt:Name>main</tt:Name>
</tt:VideoEncoderConfiguration>

<tt:VideoSourceConfiguration token="VSC_1">
  <tt:SourceToken>VideoSource_1</tt:SourceToken>
</tt:VideoSourceConfiguration>

上面那种是「属性」,读的时候用 elem.get 取; 下面那种是「子元素」,读的时候用 elem.find 取。 两种写法混用不是笔误,是 XSD 定义的: token 是每个元素的必选属性(用来标识自己),SourceToken 是一个引用字段(指向别人)。 所以「自己」用属性,「引用别人」用子元素,规则其实是一致的。 但用代码写起来就不一致了: get 找子元素永远返回 None,不报错、不提示,这个坑我踩过一次, 排查半天才发现是把属性当子元素找了。

五、动手 Demo:五个概念,一条链

Demo 真跑一次 GetProfiles,把这条链上的四层 token 一个个抓出来。 然后拿 Profile 的 token 去要流,验证它真能用——能拿到地址,说明这条链是通的。

# ① 四层 token 都在 GetProfiles 的响应里
_a, b, _n = S.call(port, "media", "GetProfiles")
r = ET.fromstring(b)
pf = r.find(".//{%s}Profiles" % S.NS_TRT)
print(pf.get("token"))                        # Profile_1

vsc = pf.find("{%s}VideoSourceConfiguration" % S.NS_TT)
print(vsc.get("token"))                       # VSC_1(属性)
st = vsc.find("{%s}SourceToken" % S.NS_TT)
print(st.text)                                # VideoSource_1(子元素)

# ② 用 token 要流,验证这条链真能用
inner = ("<trt:GetStreamUri><trt:StreamSetup>"
         "<tt:Stream>RTP-Unicast</tt:Stream>"
         "<tt:Transport><tt:Protocol>RTSP</tt:Protocol>"
         "</tt:Transport></trt:StreamSetup>"
         "<trt:ProfileToken>%s</trt:ProfileToken>"
         "</trt:GetStreamUri>") % pf.get("token")
_a, b2, _n = S.call(port, "media", "GetStreamUri", inner)
print(ET.fromstring(b2).find(".//{%s}Uri" % S.NS_TT).text)

实跑输出:

--------------------------------------------------------------------
五个概念,一条链
--------------------------------------------------------------------
**ONVIF 的文档里反复出现五个词:Device、Service、Profile、Configuration、Token。**
  **它们不是并列关系,是一条引用链。**这一节把这条链跑出来。

**先看这条链:**

  概念            是什么                            被谁引用
  --------------------------------------------------------------------------------
  Device          **一台物理设备**                  (最外层)
  Service         **设备上的一组接口**,各有地址    Device 提供多个
  Profile         **一套「源 + 编码」的绑定**       被 `GetStreamUri` 引用
  Configuration   **一组可改的参数**                被 Profile 引用
  Token           **上面每一个对象的身份证号**      **所有请求都用它指对象**

  **第五行是理解 ONVIF 的关键**:
  **ONVIF 里没有一个操作是「按名字找对象」的,全是「按 Token 找」。**
  **`GetStreamUri` 的参数不是「主码流」,而是 `Profile_1`**——
  **名字只是给人看的,Token 才是给代码用的。**

**真跑一遍,把这条链上的 Token 一个个抓出来:**

  层                          Token 值                从哪个响应里拿到
  --------------------------------------------------------------------------------------
  Profile                     Profile_1               `GetProfiles`
  VideoSourceConfiguration    VSC_1                   `GetProfiles` 的嵌套
  VideoSource                 VideoSource_1           **从父块的 `SourceToken` 拿**
  VideoEncoderConfiguration   VideoEncoder_1          `GetProfiles` 的嵌套

  **四层 Token 全在同一个响应里**,**这就是为什么「一次 `GetProfiles` 就能拿到后面要的一切」。**
  **注意第三行那个 `SourceToken`**:
  **它不是属性,是子元素 `<tt:SourceToken>VideoSource_1</tt:SourceToken>`。**
  **而另外三个是属性(`token="..."`)。**  **同一份报文里两种写法混用**,
  **这是解析 ONVIF 时最容易漏取值的地方**——  **用 `elem.get()` 找子元素永远拿不到。**

**用 Token 去要流,验证它真能用:**

  请求里带 `ProfileToken=Profile_1`,拿回来的地址:
  `rtsp://127.0.0.1:554/onvif/profile1`

  **那个地址里的 `profile1` 就是 Profile 的另一半身份**——
  **RTSP 服务器按这个路径找对应的那套编码配置。**
  **所以「改分辨率」这件事的正确做法不是改 URL,是改 Profile 引用的那个 `Configuration`**
  (07.02.09 讲)。

注意第二个 token 和第三个 token 的取法不一样: vsc.get(u"token") 取属性,find 取子元素。 如果两个都用 get,第三个会返回 None—— 而 None 拼进字符串会变成字符串 None,请求发出去就是 Fault。 最后那段「用 token 要流」是这一节的自检:如果链子在哪儿断了,这一步就会失败, 而不是等到「取到流却播不出来」才发现。这种「每一层都验一下」的写法,比一口气跑到底好排查。 本节的 token 值(Profile_1、VSC_1、VideoSource_1)是模拟设备起的名字, 真设备可能是 Profile_1、PROFILE_000、000,各家不一。 所以代码里绝对不能写死这些字符串——要像 Demo 这样,每次都从响应里读。

30 秒实验

  1. 把 vsc.find 改成 vsc.get,看第三个 token 是不是变成 None,再把这个 None 拼进请求, 看设备回什么。
  2. 打印 pf 的所有子元素标签,数一数一个 Profile 到底套了几块配置。
  3. 把 GetStreamUri 的 ProfileToken 改成 Profile_2(不存在的),看是不是回 Fault。

常见误区

  1. 以为 Name 能当标识用。 请求里只认 token,Name 是给人看的,改它不影响任何调用。
  2. 以为 token 可以由客户端指定。 token 是设备分配的,客户端只能读;要换只能删了重建。
  3. 以为 SourceToken 和 token 一样都是属性。 一个是属性、一个是子元素, 用错取法只会得到 None,不报错。
自测三问(点开对答案) 1. Device、Service、Profile、Configuration、Token 这五个词是什么关系? —— **一条引用链**:设备提供 service,操作用 Profile 指代一套绑定, Profile 引用若干 Configuration,**每个对象靠 Token 被指代。** 2. 为什么 ONVIF 用 token 而不是名字来标识对象? —— 因为 **token 是设备的分配物、稳定且唯一**;名字是给人改的,**改了不该影响寻址。** 3. `SourceToken` 和 `token` 在读法上有什么区别? —— **`token` 是属性,用 `elem.get`;`SourceToken` 是子元素,用 `elem.find`。**用错不报错, 只返回 `None`。

下一节讲缩写:tds、trt、tt 这些前缀到底是谁的缩写,以及为什么它们都以 T 开头。

相关笔记