SetVideoEncoderConfiguration 几乎总是返回成功,哪怕设备不支持你设的值。
它会把值悄悄夹到最接近的一档,所以唯一的成功判据是「回读」。
上一节是「读」,这一节是「写」——而 ONVIF 的写操作有一个共同的坑:返回值不代表结果。
这一节讲的四步流程(读→改→写→回读)适用于所有 Set* 操作,不只是分辨率。
一、先看设备支持哪些档
GetVideoEncoderConfigurationOptions 会告诉你「哪些值合法」:
分辨率列表、码率范围、帧率范围、GOP 范围。
| 字段 | 本机设备的值 | 说明 |
|---|---|---|
| 分辨率 | 640×480/1280×720/1920×1080 | 只有三档 |
| 码率范围 | 64 ~ 16384 kbps | 超出会被夹 |
| 帧率范围 | 1 ~ 30 | 同上 |
第一行那个「只有三档」是关键: 设备并不支持「任意分辨率」——你设 3000×2000,它不会报错,只会换成最接近的那一档。 所以「先查选项」这一步不是可选的:不查就写,等于在赌设备会不会夹、夹到哪儿。 第二、三行是范围型的限制,同样的道理: 码率 99999 会被夹到 16384,帧率 99 会被夹到 30——而且都不报错。
二、静默夹边界有多坑
这一节的核心现象:设 3000×2000,返回「成功」,回读是 1920×1080。
| 你以为的 | 实际发生的 | 后果 |
|---|---|---|
| 设成 3000×2000 | 被换成 1920×1080 | 画质没提升也没报错 |
| 设成码率 99999 | 被换成 16384 | 带宽按预期算会错 |
| 设成帧率 99 | 被换成 30 | 流畅度没变 |
三行都是「成功」,三行都被改——而客户看到的只是「我改了啊,怎么没变化」。 更麻烦的是「按预期算会错」这一条:你按 99999 kbps 去算存储容量,结果实际只有 16384, 差了六倍—— 这种错误不会当场暴露,而是在「录像存了三个月就满了」的时候暴露。 所以「静默夹边界」这件事,在方案设计阶段就要防: 所有写操作之后都回读一遍,把实际值记下来,别用请求值去算。
三、四步流程缺一不可
| 步 | 做什么 | 缺了会怎样 |
|---|---|---|
| 1 | GetVideoEncoderConfigurationOptions |
不知道有哪些档,写进去靠赌 |
| 2 | 读现有配置再改 | 漏填的字段被重置成默认值 |
| 3 | SetVideoEncoderConfiguration |
没得写 |
| 4 | 回读确认 | 唯一能发现「被夹」的手段 |
第二步是「最容易被省、代价最隐蔽」的一步:
很多人直接新建一个空结构体,只填分辨率和码率就发过去——
结果 GOP、H.264 profile、质量等级全被重置成默认值。
表现是「分辨率对了,但画质变了、带宽也变了」,而返回值一切正常。
正确做法是「读回来、改一个字段、再写回去」:这样没改的字段保持原值。
这个模式在所有 Set* 操作里都适用,
比如 SetImagingSettings(改曝光)、SetNetworkInterfaces(改 IP)。
四、改完还要注意什么
| 变化 | 会影响什么 | 要不要重启 |
|---|---|---|
| 分辨率 | 流地址通常不变,但客户端要重新协商 | 一般不用 |
| 码率 | 存储与带宽预算 | 不用 |
| GOP | 首帧延迟、seek 精度 | 不用 |
第一行那个「流地址通常不变」是有例外的:有的设备改分辨率之后 Profile 会重建,
于是 ProfileToken 变了——这时候你手里的旧 token 就失效了。
所以改完配置之后,如果还要取流,最好重新 GetProfiles 一次,别用改之前缓存的 token。
第三行那个「GOP 影响首帧延迟」和上一节呼应:GOP 越长,等关键帧越久;
所以「首帧慢」的调优手段之一就是把 GOP 调短。
但 GOP 短了码率会涨——这是个取舍,不是单方面优化。
五、动手 Demo:改分辨率必须回读
Demo 先查支持的档,再设一个不支持的、一个支持的,每次都回读。 重点看第二行:返回「成功」,回读却是 1920×1080。
# ① 先查有哪些档
_a, b, _n = S.call(port, "media", "GetVideoEncoderConfigurationOptions")
avail = re.findall(r"<tt:Width>(\d+)</tt:Width><tt:Height>(\d+)</tt:Height>", b)
# ② 设一个设备不支持的尺寸
write(3000, 2000, 2048, 30)
# ③ 回读——这才是唯一的判据
print(read()) # (1920, 1080, 2048, 30)
实跑输出:
--------------------------------------------------------------------
改分辨率必须回读
--------------------------------------------------------------------
**`SetVideoEncoderConfiguration` 是整个 ONVIF 里最容易误判成功的操作。**
**因为它几乎总是返回成功**——哪怕设备根本不支持你设的值。
**先看设备支持哪些分辨率**(`GetVideoEncoderConfigurationOptions`):
640×480、1280×720、1920×1080
动作 回读分辨率 回读码率 返回
-----------------------------------------------------------------------
读现状 1920×1080 4096 kbps —
**设 3000×2000** 1920×1080 2048 kbps **成功**
**设 1280×720、码率 99999** 1280×720 16384 kbps **成功**
**第二行是这一节的核心**:我把 3000×2000 发过去,
**设备返回「成功」,但回读是 1920×1080。**
**它没报错,而是悄悄换成了最接近的一档。**
**这叫「静默夹边界」**,是安防设备很常见的实现方式。
**后果是:任何只看返回值不看回读的代码,都会以为设置成功了。**
**第三行同理**:码率 99999 被夹到 16384 kbps,帧率 99 被夹到 30。
**都是「成功」但实际被改。**
**所以正确流程一定是四步**:
步 做什么 为什么
--------------------------------------------------------------------------------
1 `GetVideoEncoderConfigurationOptions` **先知道有哪些档**
2 从现有配置改(**不要新建空结构体**) 漏字段会被填默认值
3 `SetVideoEncoderConfiguration` **返回值只能说明「收到了」**
4 **回读** `GetVideoEncoderConfiguration` **唯一的成功判据**
**第二行那个「改现有配置」最容易被省掉**,代价很隐蔽:
**只填了分辨率,其它字段(码率、GOP、H.264 profile)会变成默认值。**
**结果是「分辨率对了,但画质变了、码流也变了」**——
**用户只会说「你没改好」,而你看返回值一切正常。**
第三段那个回读结果是这一节的全部证据:
我设了 3000×2000,回读是 1920×1080——而 write() 的返回值是「成功」。
注意码率 2048 和帧率 30 是被接受的,
只有分辨率被夹——说明「夹边界」是逐字段判断的,不是整条配置一起拒。
第二次设 1280×720、码率 99999 时,分辨率被接受、码率被夹到 16384——同样是逐字段的。
所以「回读之后要逐字段比对」,不能只比一个字段就以为全对了。
30 秒实验
- 把
write()改成只填分辨率、其它字段留空,看回读时 GOP 变成多少。 - 设一个「介于两档之间」的尺寸(比如 1600×1200),看被夹到哪一档。
- 连续设两次不同的分辨率,看第二次的返回值有什么不同。
常见误区
- 看
Set*的返回值判断成功。 它几乎总是返回成功,不支持的值会被静默夹到最接近的一档。 - 新建空结构体只填要改的字段。 没填的字段会被重置成默认值, 症状是「分辨率对了但画质变了」。
- 用请求值去算存储容量。 实际值可能被夹到六分之一, 错误要到「录像存了三个月就满」时才暴露。
自测三问(点开对答案)
1. 设 3000×2000 为什么没报错? —— **设备会静默夹到最接近的支持档位**(这里是 1920×1080),**并且返回成功**。只有回读才能发现。 2. 为什么「读现有配置再改」这一步不能省? —— **不填的字段会被重置成默认值**,结果是「分辨率对了、画质和带宽都变了」。 3. 改完分辨率之后,缓存的 `ProfileToken` 还能用吗? —— **不一定。有的设备改配置会重建 Profile,token 就变了。**要取流就重新 `GetProfiles` 一次。下一节进 08.08:同样的流程换到 Linux 上做,差别只有三处。
- ☐ (本节完,下一节 08.08.01 安装依赖项)