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 秒实验
- 把
depth改成只对Profiles元素求深度,看数出来是几层。 - 把
ForcePersistence改成false(或删掉),看模拟设备的回读值变不变。 - 改完分辨率后重新
GetProfiles,确认Width真的是 1280。
常见误区
- 以为改分辨率要改 RTSP 地址。 地址是设备的输出,改配置后地址往往不变, 要回读参数才能确认。
- 只传要改的字段去 Set。 没传的可能被重置,必须整块回写。
- 不带
ForcePersistence。 设备可能只改内存不落盘,重启就回去了,而且不报任何错。
自测三问(点开对答案)
1. 一个 Profile 里套了哪四块配置?哪两块是必有的? —— **视频源配置、编码配置、音频源配置、云台配置。** **前两块必有**,后两块可选。 2. `ForcePersistence` 是干什么的?不带会怎样? —— **它要求设备把改动写进持久存储。** **不带的话设备可能只改内存**,重启就回去了。 3. 改了分辨率,RTSP 地址会变吗? —— **通常不变。** 地址里的 `profile1` 指的是**那套配置**,**不是那个分辨率**; 要验改动得回读参数。下一节讲实时流:GetStreamUri 怎么用一个 URL 结束 ONVIF 的全部工作。
- ☐ (本节完,下一节 07.02.10 实时流)