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

GetProfiles 是 ONVIF 里结构最深的响应:一个 Profile 套四块配置,25 个节点、1174 字节。 改参数的正确姿势是「读-改-写」,而且要带 ForcePersistence,否则重启就回去了。

📝 本节和前面几节的关系

上一节讲图像参数,这一节讲「码流参数」——图像是「画得好看」,码流是「传多大」。 这一节也是 media 这组 service 的核心,07.02.10 的「要流」就建立在这里。

一、一个 Profile 套四块

块 管什么 本例的 Token
VideoSourceConfiguration 用哪个镜头、画面裁剪 VSC_1
VideoEncoderConfiguration 编码、分辨率、码率、GOP VideoEncoder_1
AudioSourceConfiguration 音频源(可选) 本例没有
PTZConfiguration 云台配置(可选) 本例没有

前两行是必有的,后两行是可选的。 所以「一个 Profile 就是一路源 + 一套编码的绑定」—— 绑定的目的是「让客户端拿一个 Token 就能要流」,不用逐项指定。 这个设计的好处很容易被忽略: GetStreamUri 只收一个 ProfileToken,不收分辨率、不收码率—— 因为那些都在 Profile 里绑好了。 如果 ONVIF 没有 Profile 这一层,每次要流都得传一整套参数,客户端和设备的耦合会重得多。

二、实测规模:25 个节点描述一路 1080p

指标 数值
Profile 个数 1
响应字节 1174
XML 节点总数 25
最大嵌套层数 7
带 token 属性的节点 3

一个 Profile 用 25 个节点、1174 字节,就为了描述「一路 1080p 的编码」。 这就是为什么「响应总比请求大」:请求只要说「给我 Profile」,响应要把所有参数报一遍。 而且真实设备通常有 2 到 4 个 Profile(主码流、子码流、第三流),字节数还要乘上去。 第四行那个「7 层嵌套」是解析代码最容易写错的地方—— find 的路径少写一层就找不到,多写一层也可能「碰巧」找到同名的别处。 所以稳妥的做法是「用 .// 加完整命名空间」,而不是拼长路径。

三、整块回写,还要 ForcePersistence

SetVideoEncoderConfiguration 要求把整块结构传回去,不是只传要改的字段:

<trt:SetVideoEncoderConfiguration>
  <trt:Configuration token="VideoEncoder_1">
    <tt:Name>main</tt:Name>
    <tt:Encoding>H264</tt:Encoding>
    <tt:Resolution><tt:Width>1920</tt:Width>
      <tt:Height>1080</tt:Height></tt:Resolution>
  </trt:Configuration>
  <trt:ForcePersistence>true</trt:ForcePersistence>
</trt:SetVideoEncoderConfiguration>

只传一半的后果是:没传的字段被当成「清空」,设备可能把分辨率重置成默认值。 最后那个 ForcePersistence=true 是本节第二重要的参数: 不带它,设备可能只改内存不落盘,重启就回去了。 这个行为在规范里是允许的——「改了不写 flash」能延长存储寿命,代价是断电丢失。 所以「改了参数重启就没了」不是 bug,是没带这个参数。

四、改分辨率不是改 URL

很多人第一反应是「改 RTSP 地址里的路径」,但那个路径是设备自己定的,改了就 404。 正确的链路是三层引用:

层次 改什么 结果
第 1 层 改 VideoEncoderConfiguration 的分辨率 编码变了
第 2 层 Profile 引用这个配置 自动跟着变
第 3 层 GetStreamUri 返回的地址 路径可能完全不变

第三行是最反直觉的: 改了分辨率,RTSP 地址往往一模一样—— 因为地址里的 profile1 指的是「那套配置」,不是「那个分辨率」。 所以「地址没变」不代表「改动没生效」, 要验改动,得重新 GetProfiles 看参数,或者连上去看实际分辨率。 理解了这一层,就不会再去做「拼 URL」这种事——URL 是设备的输出,不是你的输入。 这也是 07.02.10 要讲的:ONVIF 的终点是「给你一个地址」,你怎么改都改不到那个地址里去。

五、动手 Demo:媒体配置,Profile 里到底套了几层

Demo 把 GetProfiles 的响应结构数一遍:节点数、嵌套层数、token 数。 然后改一次分辨率,再回读,看「地址变不变」。

_a, b, _n = S.call(port, "media", "GetProfiles")
r = ET.fromstring(b)

# ① 数结构
nodes = list(r.iter())
def depth(e, d=0):
    return max([depth(c, d + 1) for c in e] or [d])
print(len(nodes), depth(r), b.count("token="))

# ② 改分辨率再回读
inner = ("<trt:SetVideoEncoderConfiguration>"
         "<trt:Configuration token=\"VideoEncoder_1\">"
         "<tt:Name>main</tt:Name><tt:Encoding>H264</tt:Encoding>"
         "<tt:Resolution><tt:Width>1280</tt:Width>"
         "<tt:Height>720</tt:Height></tt:Resolution>"
         "</trt:Configuration>"
         "<trt:ForcePersistence>true</trt:ForcePersistence>"
         "</trt:SetVideoEncoderConfiguration>")
_a, b2, _n = S.call(port, "media", "SetVideoEncoderConfiguration", inner)
print("fault:", b2.count("Fault"))

_a, b3, _n = S.call(port, "media", "GetStreamUri", uri_req)
print(ET.fromstring(b3).find(".//{%s}Uri" % S.NS_TT).text)

实跑输出:

--------------------------------------------------------------------
媒体配置,Profile 里到底套了几层
--------------------------------------------------------------------
**`GetProfiles` 是 ONVIF 里结构最深的响应。**
  这一节把它的嵌套层数、重复次数、Token 数全数一遍,**看清「配置」是什么。**

**先看结构:一个 Profile 里套了四个配置块**

  块                            管什么                        本例的 Token
  ------------------------------------------------------------------------------------
  `VideoSourceConfiguration`    **用哪个物理镜头、画面裁剪**  `VSC_1`
  `VideoEncoderConfiguration`   **编码、分辨率、码率、GOP**   `VideoEncoder_1`
  `AudioSourceConfiguration`    音频源(可选)                本例没有
  `PTZConfiguration`            云台配置(可选)              本例没有

  **前两行是必有的,后两行是可选的。**
  **所以「一个 Profile 就是「一路源 + 一套编码」的绑定」**——
  **绑定的目的是「让客户端拿一个 Token 就能要流」,不用逐项指定。**

**实测这份响应的规模:**

  指标                      数值
  ------------------------------------------
  Profile 个数              1
  响应字节                  1174
  XML 节点总数              25
  最大嵌套层数              7
  带 `token` 属性的节点     3

  **一个 Profile 用 25 个节点、1174 字节,就为了描述「一路 1080p 的编码」。**
  **这就是为什么「响应总比请求大」**:  **请求只要说「给我 Profile」,响应要把所有参数报一遍。**
  **而且真实设备通常有 2 到 4 个 Profile**(主码流、子码流、第三流),  **字节数还要乘上去。**

**改参数的正确姿势:整块回写,不是改一个字段**

  **`SetVideoEncoderConfiguration` 要求把整块 `VideoEncoderConfiguration` 传回去**,
  **不是只传要改的那个字段。**
  **只传一半的后果是**:  **没传的字段被当成「清空」,设备可能把分辨率重置成默认值。**

  **正确做法是三步**:
  **① `GetProfiles` 拿到完整块;② 只改目标字段;③ 整块 `Set` 回去。**
  **第三步还要带 `ForcePersistence=true`**,  **否则设备可能只改内存不落盘,重启就回去了。**

  **这个「读-改-写」的模式在 ONVIF 里到处都是**——
  **图像配置、媒体配置、网络配置都是这个套路。**
  **它不优雅,但它有一个好处:不会误删你没注意到的字段。**

depth 那个递归只有三行,但它数的是「整份信封的层数」, 不是「Profile 的层数」——外层还有 Envelope/Body/GetProfilesResponse 三层。 所以 7 这个数里,真正的业务嵌套只有 4 层。 口径不说清,「7 层」这个数就会被误读成「Profile 有 7 层」。 b.count(u"token=") 数的是「带 token 属性的节点」,不是「token 的总数」—— 子元素形式的 SourceToken 不算在内。 这就是上一节讲的「属性 vs 子元素」在这里的实际影响:数出来的 3 比实际的 4 少一个。 第二段改分辨率之后,GetStreamUri 返回的地址没变——这正是本节要说明的那件事: 地址里的 profile1 指的是那套配置,不是那个分辨率。

30 秒实验

  1. 把 depth 改成只对 Profiles 元素求深度,看数出来是几层。
  2. 把 ForcePersistence 改成 false(或删掉),看模拟设备的回读值变不变。
  3. 改完分辨率后重新 GetProfiles,确认 Width 真的是 1280。

常见误区

  1. 以为改分辨率要改 RTSP 地址。 地址是设备的输出,改配置后地址往往不变, 要回读参数才能确认。
  2. 只传要改的字段去 Set。 没传的可能被重置,必须整块回写。
  3. 不带 ForcePersistence。 设备可能只改内存不落盘,重启就回去了,而且不报任何错。
自测三问(点开对答案) 1. 一个 Profile 里套了哪四块配置?哪两块是必有的? —— **视频源配置、编码配置、音频源配置、云台配置。** **前两块必有**,后两块可选。 2. `ForcePersistence` 是干什么的?不带会怎样? —— **它要求设备把改动写进持久存储。** **不带的话设备可能只改内存**,重启就回去了。 3. 改了分辨率,RTSP 地址会变吗? —— **通常不变。** 地址里的 `profile1` 指的是**那套配置**,**不是那个分辨率**; 要验改动得回读参数。

下一节讲实时流:GetStreamUri 怎么用一个 URL 结束 ONVIF 的全部工作。

相关笔记