ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python实现ONVIF WS-Discovery摄像头自动发现(纯标准库)

Python实现ONVIF WS-Discovery摄像头自动发现(纯标准库) 手头一个网段里挂着几十台摄像头想批量接入平台却记不清每台的IP、端口和ONVIF路径你会怎么做一台台登录Web后台翻太蠢。ONVIF标准其实已经为这种情况留好了答案WS-Discovery。这篇文章不绕弯子直接分享我用Python从零实现ONVIF WS-Discovery设备发现的过程代码完整可运行不依赖任何第三方库。适合要把摄像头自动发现功能嵌进自己监控平台、运维脚本里的开发者也适合想搞懂ONVIF设备发现到底怎么工作的朋友。我在正式写代码之前也试过直接用ONVIF Device Manager这类现成工具确实能扫出设备可一旦要把它集成进系统或者要定时刷新在线状态图形工具就完全接不上。后来干脆自己实现了一套基于UDP多播的发现逻辑稳定跑了大半年才把过程中那些坑整理出来。1. 先搞清楚设备发现到底解决了什么问题1.1 现成工具和第三方库的边界在哪先说结论现成的ONVIF工具基本只适合人工排障不适合做自动化和二次开发。ONVIF Device Manager是很典型的桌面工具它能扫描局域网里的ONVIF设备还能直接改IP、看视频流功能很全。但它的扫描结果不会暴露给外部程序你没法在自己的代码里调用。如果你想实现“每天早上8点自动扫描一次网络把新增摄像头加进平台”这类工具就帮不上忙。第三方Python库里比较常用的是onvif-zeep它能帮我们调用ONVIF的各种服务接口比如获取设备信息、拉RTSP流地址。不过它偏重“发现之后的操作”并不是专门做设备发现的。用onvif-zeep之前你仍然需要先知道设备的IP和端口然后手动创建一个ONVIFCamera对象。那这个最开始的IP从哪来很多时候还得靠手工录入或者用厂家私有协议去查这两条路都挺别扭。我当时的诉求很明确程序启动后自动扫描局域网拿到所有ONVIF设备的XAddr也就是设备服务地址然后我再拿这些地址去做后续的认证和取流。这需要我直接面对WS-Discovery协议本身。好在这个协议并不复杂底层就是一个多播UDP加SOAP XML用Python标准库的socket和xml.etree就能搞定不需要引入任何重量级依赖。1.2 WS-Discovery在摄像头接入流程里的位置ONVIF设备的接入流程本质上分两步先发现再调用。发现这步用的就是WS-Discovery全称Web Services Dynamic Discovery它是一个基于IP多播的分布式服务发现协议。摄像头接入网络后会开始监听固定的多播地址客户端往这个多播地址发一条Probe查询消息符合条件的所有设备都会返回ProbeMatch消息把自己的服务地址报上来。你可能听过有些老工程师说“用ONVIF就绕不开WS-Discovery”这句话不完全准确但方向没错。因为ONVIF规范里规定设备必须支持WS-Discovery来宣告自己的服务客户端也应支持用它来定位设备。哪怕是那些支持UDP广播搜索的老款设备在ONVIF框架下也仍然以WS-Discovery为主要机制。拿到XAddr以后我们才可以用它去请求设备能力、拉取媒体流地址、设置参数等等。所以我一直把WS-Discovery比作“ONVIF的一楼门厅”——你进楼之前总得先找到门在哪。把这个机制摸透后续接入什么牌子的摄像头都会顺手很多。2. WS-Discovery的协议细节搞懂这些代码才写得对2.1 多播地址、端口和TTL为什么这三样不能错WS-Discovery的底层实现是UDP多播不是普通的UDP单播。常用的固定参数如下参数值作用多播组地址239.255.255.250IPv4下的WS-Discovery多播地址端口3702所有WS-Discovery消息都发送到这个端口协议UDP无连接发送Probe后需要自行超时处理消息编码SOAP 1.2 over UDPXML文本UTF-8编码多播地址239.255.255.250是WS-Discovery规范里统一规定的管理地址设备出厂后默认加入这个组。我们发的Probe消息目标就是239.255.255.250:3702。3702端口则是所有ONVIF设备必须监听的端口设备收到多播Probe后会根据情况选择直接向这个多播地址回复或向发送方地址单播回复。TTLTime To Live起初被我忽略过。UDP多播的TTL表示数据包能经过多少跳。大部分场景里客户端和设备在同一个二层网络TTL默认值1就够了。但如果你把发现程序跑在一台有多网卡的服务器上而要发现的摄像头在另一个VLAN并且中间的路由器开启了多播转发那就得把TTL调大。我在代码里设置了2同一个三层网络内足够用又不至于让消息跨太多设备造成无谓广播。还有个容易搞混的点很多人以为绑定了多播地址就能同时收单播。实际上同一台机器如果要同时收多播和单播响应socket绑定需要特别注意。我建议在接收端既加入多播组又把本地端口绑定到3702这样兼顾两种响应方式。后面的代码就是这么写的。2.2 Probe消息的结构与命名空间WS-Discovery的消息是一段SOAP XML。Probe消息看起来长实际骨架很好记Envelope包着Header和BodyHeader里放消息ID和目标动作Body里放查询条件。我用的Probe消息模板如下?xml version1.0 encodingUTF-8? e:Envelope xmlns:ehttp://www.w3.org/2003/05/soap-envelope xmlns:whttp://schemas.xmlsoap.org/ws/2004/08/addressing xmlns:dhttp://schemas.xmlsoap.org/ws/2005/04/discovery xmlns:dnhttp://www.onvif.org/ver10/network/wsdl e:Header w:MessageIDuuid:84ede3de-8dec-11d0-c360-f01234567890/w:MessageID w:To e:mustUnderstandtrueurn:schemas-xmlsoap-org:ws:2005:04:discovery/w:To w:Action e:mustUnderstandtruehttp://schemas.xmlsoap.org/ws/2005/04/discovery/Probe/w:Action /e:Header e:Body d:Probe d:Typesdn:NetworkVideoTransmitter/d:Types /d:Probe /e:Body /e:Envelope这里有几个容易写错的地方。第一MessageID必须是uuid:开头的URI格式不能随便填一串数字。我用Python的uuid.uuid4()动态生成。第二w:Action必须对应Probe动作的URN如果你去网上复制一段老模板很可能是2004年那版地址规范也要注意版本是否一致。第三d:Types里的dn:NetworkVideoTransmitter表示我们想要找的是“网络视频发射端”也就是摄像头或者视频编码器。如果这个字段留空则可能匹配到所有ONVIF设备包括一些纯音频设备或门禁设备。实际测试中我发现部分品牌的设备对Types的处理并不严格有的甚至不理会这个字段不管写什么都回ProbeMatch。所以该字段可以保留但不要指望靠它做精准过滤。2.3 ProbeMatch响应的关键字段XAddrs、Scopes、Types设备返回的ProbeMatch消息长这样?xml version1.0 encodingUTF-8? s:Envelope xmlns:shttp://www.w3.org/2003/05/soap-envelope xmlns:ahttp://schemas.xmlsoap.org/ws/2004/08/addressing xmlns:dhttp://schemas.xmlsoap.org/ws/2005/04/discovery s:Header a:Actionhttp://schemas.xmlsoap.org/ws/2005/04/discovery/ProbeMatches/a:Action a:MessageIDuuid:9aebd2a5-.../a:MessageID a:RelatesTouuid:84ede3de-8dec-11d0-c360-f01234567890/a:RelatesTo /s:Header s:Body d:ProbeMatches d:ProbeMatch a:EndpointReference a:Addressurn:uuid:0c4575f6-.../a:Address /a:EndpointReference d:Typesdn:NetworkVideoTransmitter/d:Types d:Scopesonvif://www.onvif.org/type/video_encoder onvif://www.onvif.org/type/audio_encoder onvif://www.onvif.org/name/YourCamera/d:Scopes d:XAddrshttp://192.168.1.64:8000/onvif/device_service/d:XAddrs d:MetadataVersion1/d:MetadataVersion /d:ProbeMatch /d:ProbeMatches /s:Body /s:Envelope你最需要关注的是d:XAddrs它就是设备ONVIF服务的入口地址。后续获取设备信息、拉流、配参都要从这个地址起步。d:Scopes里一般会携带设备的一些自我描述信息比如设备类型、名称、硬件型号等可以用来做软过滤。a:Address里是设备的EndpointReference每台设备会有自己唯一的UUID但有些设备重启后这个UUID会变所以我做去重时更倾向用XAddr而不是UUID。协议细节先讲到这里。接下来直接进代码。3. 可运行的Python实现从Probe到设备列表3.1 环境与依赖纯标准库就够了代码不依赖第三方包Python 3.6以上都能跑。我本机测试用的是Python 3.10Linux和Windows都验证过。用到四个标准库socket创建UDP socket加入多播组发送和接收数据struct把IP地址转换成二进制格式用于加入多播组uuid生成Probe消息里的MessageIDxml.etree.ElementTree解析ProbeMatch响应的XMLthreading接收线程和主流程配合没有任何第三方依赖意味着你可以直接把这段代码丢进内网环境不用先执行pip install。3.2 拼接并发送Probe多播消息先写一个函数生成Probe消息再写一个函数负责通过多播发送。import socket import uuid MCAST_GRP 239.255.255.250 MCAST_PORT 3702 def build_probe_message(): message_id uuid:%s % uuid.uuid4() return f?xml version1.0 encodingUTF-8? e:Envelope xmlns:ehttp://www.w3.org/2003/05/soap-envelope xmlns:whttp://schemas.xmlsoap.org/ws/2004/08/addressing xmlns:dhttp://schemas.xmlsoap.org/ws/2005/04/discovery xmlns:dnhttp://www.onvif.org/ver10/network/wsdl e:Header w:MessageID{message_id}/w:MessageID w:To e:mustUnderstandtrueurn:schemas-xmlsoap-org:ws:2005:04:discovery/w:To w:Action e:mustUnderstandtruehttp://schemas.xmlsoap.org/ws/2005/04/discovery/Probe/w:Action /e:Header e:Body d:Probe d:Typesdn:NetworkVideoTransmitter/d:Types /d:Probe /e:Body /e:Envelope.encode(utf-8) def send_probe(interface_ipNone): sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) if interface_ip: sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton(interface_ip)) message build_probe_message() sock.sendto(message, (MCAST_GRP, MCAST_PORT)) sock.close()IP_MULTICAST_TTL设置的是多播报文的生存时间值不必设太大。interface_ip参数是可选的当你电脑有多个网卡时可以用它指定从哪个网卡发送。socket.inet_aton这类地址转换函数很容易被忽略但不转的话部分系统会直接报Protocol error。3.3 接收ProbeMatch并解析出设备地址接收端要做的事情创建socket允许地址复用绑定端口加入多播组然后循环接收数据。import socket import struct import xml.etree.ElementTree as ET MCAST_GRP 239.255.255.250 MCAST_PORT 3702 def parse_probe_matches(data): devices [] try: root ET.fromstring(data) except ET.ParseError: return devices ns { s: http://www.w3.org/2003/05/soap-envelope, a: http://schemas.xmlsoap.org/ws/2004/08/addressing, d: http://schemas.xmlsoap.org/ws/2005/04/discovery, } for probe_match in root.iter({http://schemas.xmlsoap.org/ws/2005/04/discovery}ProbeMatch): xaddrs probe_match.findtext({http://schemas.xmlsoap.org/ws/2005/04/discovery}XAddrs) types probe_match.findtext({http://schemas.xmlsoap.org/ws/2005/04/discovery}Types) scopes probe_match.findtext({http://schemas.xmlsoap.org/ws/2005/04/discovery}Scopes) if xaddrs: devices.append({ xaddrs: xaddrs.strip(), types: (types or ).strip(), scopes: (scopes or ).strip(), }) return devices解析XML时最稳的方式不是去匹配前缀而是用完整的{namespace}localname格式。因为不同设备可能会用不同的前缀名例如有些设备响应时把SOAP命名空间前缀写成SOAP-ENV把WS-Discovery前缀写成d或wsd但命名空间URI是不变的。使用完整tag格式可以绕开前缀差异。findtext方法取子元素文本很实用比逐个遍历省事。如果设备返回的XML里有重复的XAddr字段你也可以直接取第一个。接收主循环如下def listen_for_matches(timeout5, interface_ipNone): sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind((, MCAST_PORT)) mreq struct.pack(4sl, socket.inet_aton(MCAST_GRP), socket.INADDR_ANY) if interface_ip: mreq struct.pack(4s4s, socket.inet_aton(MCAST_GRP), socket.inet_aton(interface_ip)) sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) sock.settimeout(timeout) devices {} while True: try: data, addr sock.recvfrom(65535) except socket.timeout: break except OSError: continue for dev in parse_probe_matches(data): key dev[xaddrs] devices[key] dev sock.close() return list(devices.values())这段代码里有个细节我特别提醒sock.bind((, MCAST_PORT))绑定了3702端口。如果不绑定加入多播组后能收到发往多播地址的响应但不少设备会单播回应给发送方端口那样你就收不到。绑定以后单播和多播两种响应都能进同一个socket。SO_REUSEADDR在Linux下主要用于避免TIME_WAIT状态在Windows下则允许另一个程序也绑定同一个端口这能减少“端口被占用”的尴尬。但如果你机器上已经有一个ONVIF Device Manager占用了3702端口依然有冲突可能这个时候建议先关掉其他工具。3.4 主流程多线程配合定时等待因为我们要先启动接收再发送Probe所以最自然的方式是用一个线程专门接收主线程负责发送Probe然后等待一段时间让设备响应。import time import threading def discover_devices(local_ipNone, wait_time5): result_holder {} def worker(): result_holder[devices] listen_for_matches(timeoutwait_time, interface_iplocal_ip) t threading.Thread(targetworker, daemonTrue) t.start() time.sleep(0.5) # 给接收线程一点启动时间 send_probe(interface_iplocal_ip) t.join(timeoutwait_time 1) return result_holder.get(devices, [])这里加0.5秒的启动缓冲很关键否则可能出现接收线程还没加入多播组Probe已经发出去了结果一条响应都没收到的情况。你当然可以把发送动作放到接收线程内部但我喜欢把发送和接收解耦方便后续扩展成周期扫描。3.5 完整代码合并版把前面几段拼起来就是可直接运行的完整版本import socket import struct import time import uuid import threading import xml.etree.ElementTree as ET MCAST_GRP 239.255.255.250 MCAST_PORT 3702 PROBE_ACTION http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe def build_probe_message(): message_id uuid:%s % uuid.uuid4() return f?xml version1.0 encodingUTF-8? e:Envelope xmlns:ehttp://www.w3.org/2003/05/soap-envelope xmlns:whttp://schemas.xmlsoap.org/ws/2004/08/addressing xmlns:dhttp://schemas.xmlsoap.org/ws/2005/04/discovery xmlns:dnhttp://www.onvif.org/ver10/network/wsdl e:Header w:MessageID{message_id}/w:MessageID w:To e:mustUnderstandtrueurn:schemas-xmlsoap-org:ws:2005:04:discovery/w:To w:Action e:mustUnderstandtrue{PROBE_ACTION}/w:Action /e:Header e:Body d:Probe d:Typesdn:NetworkVideoTransmitter/d:Types /d:Probe /e:Body /e:Envelope.encode(utf-8) def send_probe(interface_ipNone): sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) if interface_ip: sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton(interface_ip)) sock.sendto(build_probe_message(), (MCAST_GRP, MCAST_PORT)) sock.close() def parse_probe_matches(data): devices [] try: root ET.fromstring(data) except ET.ParseError: return devices for probe_match in root.iter({http://schemas.xmlsoap.org/ws/2005/04/discovery}ProbeMatch): xaddrs probe_match.findtext({http://schemas.xmlsoap.org/ws/2005/04/discovery}XAddrs) types probe_match.findtext({http://schemas.xmlsoap.org/ws/2005/04/discovery}Types) scopes probe_match.findtext({http://schemas.xmlsoap.org/ws/2005/04/discovery}Scopes) if xaddrs: devices.append({ xaddrs: xaddrs.strip(), types: (types or ).strip(), scopes: (scopes or ).strip(), }) return devices def listen_for_matches(timeout5, interface_ipNone): sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind((, MCAST_PORT)) if interface_ip: mreq struct.pack(4s4s, socket.inet_aton(MCAST_GRP), socket.inet_aton(interface_ip)) else: mreq struct.pack(4sl, socket.inet_aton(MCAST_GRP), socket.INADDR_ANY) sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) sock.settimeout(timeout) devices {} while True: try: data, addr sock.recvfrom(65535) except socket.timeout: break except OSError: continue for dev in parse_probe_matches(data): devices[dev[xaddrs]] dev sock.close() return list(devices.values()) def discover_devices(local_ipNone, wait_time5): result {} def worker(): result[devices] listen_for_matches(timeoutwait_time, interface_iplocal_ip) t threading.Thread(targetworker, daemonTrue) t.start() time.sleep(0.5) send_probe(interface_iplocal_ip) t.join(timeoutwait_time 1) return result.get(devices, []) if __name__ __main__: found discover_devices(wait_time5) print(发现 %d 台ONVIF设备: % len(found)) for d in found: print(d[xaddrs])这段代码在一般局域网环境里可以直接跑出结果。我实测的环境里有海康、大华、雄迈等不同品牌的摄像头能在同一网段内一次性发现大部分设备个别老款设备响应慢需要把等待时间加长到8-10秒。4. 联调时最容易踩的四个坑我把排查过程也放进来4.1 多播发出去了却一条ProbeMatch都没收到这是最常见的坑我自己第一次跑代码的时候也遇到了把send_probe发出去等了5秒控制台干干净净。排查思路要按顺序走。第一步确认发送端已经真正发出了多播包。可以用Wireshark在发送机器上抓包过滤条件填udp.port 3702。如果你看到发送方向有目标地址239.255.255.250的UDP包说明Probe已经离开本机。如果连包都没有检查是不是防火墙拦了socket发送或者你指定的interface_ip不对。第二步确认设备确实在线且和你在同一个广播域。最简单的办法是直接用浏览器访问摄像头Web管理页或者用ping确认网络通。第三步检查接收socket有没有加入多播组并绑定了3702端口。这里有一个容易踩的细节如果你之前跑过一个接收进程退出时没释放端口下一次启动会报Address already in use。在Linux上加了SO_REUSEADDR一般能解决Windows上偶尔还需要把之前的Python进程彻底结束掉。第四步检查Windows防火墙。Windows默认会拦外部发来的UDP入站包多播包也不例外。我建议是在“高级安全Windows防火墙”里新建一条入站规则放行UDP 3702端口或者至少放行当前绑定程序的入站流量。还有一个不太起眼但很实际的点如果你用VMware虚拟机跑这段代码而摄像头在宿主机所在的物理局域网里虚拟机的网络模式必须是“桥接模式”NAT模式下多播包大概率到不了摄像头。4.2 Windows多网卡、Docker和虚拟机网络隔离多网卡是另一个重灾区。我的开发机同时有有线网卡、无线网卡还装了Docker系统里有好几个IP。第一次跑的时候我明明从有线网卡发的Probe接收端却绑定到了无线网卡的地址结果自然是什么都收不到。解决办法很直接在discover_devices里显式传入本机要使用的那块网卡的IP比如discover_devices(local_ip192.168.1.20)。然后在发送端设置IP_MULTICAST_IF在接收端设置IP_ADD_MEMBERSHIP时带上同一个IP。这样发送和接收都会固定在指定网卡上。Docker容器的问题更隐蔽。容器默认的bridge网络是不转发多播包的就算你在容器里绑定了宿主机的IP也可能因为网络隔离而失败。我在Docker里部署这套发现服务时最终用了--network host让容器直接使用宿主机网络栈才能顺利发现设备。如果你跑在Kubernetes里需要额外考虑多播支持和网络插件配置这不是本文重点但方向要清楚。4.3 XML解析取不到字段命名空间和容错问题有次我拿到一段设备返回的ProbeMatch打印出来看非常正常XAddrs就在那但代码里findtext就是取不到。折腾了半天才发现这台设备返回的XML根节点用的是SOAP-ENV前缀ProbeMatch标签用的是wsd前缀而我之前用{http://schemas.xmlsoap.org/ws/2005/04/discovery}ProbeMatch这种完整URI方式写的代码理论上不应该受前缀影响。问题出在哪出在这台设备把d:XAddrs写成了空标签真正的XAddr放在了scopes字段里用的还是http://开头的一段URL。这说明一个现实一些不规范设备对WS-Discovery的实现是残缺的。所以我后来在解析函数里加了兜底逻辑如果XAddrs取不到就去解析Scopes文本从中正则匹配http://开头的地址。虽然Scopes里的URL不一定就是ONVIF服务地址但至少能提供线索。另外不同设备对Scopes字段的分隔符不一样有的用空格有的用换行有的甚至用制表符。解析时先用split()按空白拆分再去重即可。4.4 只发一次Probe就退出大概率会漏设备ONVIF设备收到Probe后不会保证立刻响应。设备启动过程中网络栈已经就绪但ONVIF服务可能还没完全准备好这时候你发的Probe会被内核丢弃。还有的设备为了抑制多播风暴会人为增加响应延迟。所以只发一次、等5秒就退出的方案扫描结果不稳定。我的做法是连续发送三次Probe每次间隔2秒接收线程持续等待。这样既能覆盖设备启动窗口也能应对偶发的丢弃。在实际项目里我甚至把它写成了一个循环每隔60秒扫描一次维护设备在线列表效果比一次性发现稳定得多。5. 拿到XAddr之后接下来的ONVIF验证与扩展5.1 发起GetSystemDateAndTime验证设备可用性WS-Discovery返回的XAddr只是“候选地址”它不一定能直接用来鉴权和取流。为了验证它是否真的可用我通常会立刻向XAddr发起一个GetSystemDateAndTime请求。这个请求在绝大多数设备上不需要认证适合作为连通性探测的探针。请求XML如下?xml version1.0 encodingUTF-8? s:Envelope xmlns:shttp://www.w3.org/2003/05/soap-envelope xmlns:tdshttp://www.onvif.org/ver10/device/wsdl s:Body tds:GetSystemDateAndTime/ /s:Body /s:Envelope用Python标准库的urllib.request就可以发import urllib.request def probe_xaddr(xaddr, timeout3): body ?xml version1.0 encodingUTF-8? s:Envelope xmlns:shttp://www.w3.org/2003/05/soap-envelope xmlns:tdshttp://www.onvif.org/ver10/device/wsdl s:Body tds:GetSystemDateAndTime/ /s:Body /s:Envelope.encode(utf-8) req urllib.request.Request( xaddr, databody, headers{Content-Type: application/soapxml; charsetutf-8}, methodPOST, ) try: with urllib.request.urlopen(req, timeouttimeout) as resp: if resp.status 200: return True except Exception: return False return False返回200并能在响应里看到UTCDateTime之类的内容基本可以确定这台设备的ONVIF服务是通着的。如果返回401或403说明服务在但需要认证不影响它被加入待管理列表。如果超时或者404那这个XAddr很可能是无效的建议从结果里剔除。5.2 从设备列表到RTSP取流和批量配置拿到可用XAddr后下一步就自由了。如果只是看视频可以走RTSP但需要先通过ONVIF的GetProfiles拿到ProfileToken再调用GetStreamUri拿到RTSP地址。这两步用同一个XAddr作为入口逐步加上认证参数即可。我自己的做法是把发现到并验证通过的所有设备存进一个SQLite表记录XAddr、型号、IP、最近在线时间。每次扫描时先发Discovery再对新增的XAddr执行一次GetSystemDateAndTime如果成功就更新最近在线时间失败且连续几次都不成功就标记为离线。这样前端设备列表就一直保持自动刷新基本不用人管。这个思路也可以扩展到批量配置发现一台新摄像头后自动用默认密码尝试连接如果连上就直接把时区、码流参数、录像计划推下去再改一个随机强密码。整个过程对运维人员来说省下的工作量非常可观。5.3 设计上的取舍要不要上第三方库很多刚接触ONVIF的开发者会问既然有onvif-zeep为什么还要自己写WS-Discovery我的回答是两件事可以同时做。WS-Discovery只负责“找到设备在哪”而onvif-zeep负责“找到设备之后怎么操作”。你在系统里完全可以这样设计用本文的轻量发现模块保持在线设备列表等需要操作某一台设备的时候再动态创建ONVIFCamera对象传入XAddr里的IP、端口和凭据。这样一来你既不需要承担重依赖的维护成本又能享受onvif-zeep在协议封装上的便利。如果项目规模很小只有几台固定摄像头那确实不需要这么复杂手动配置IP就行。但一旦设备数量超过50台或者网络里经常有新设备临时接入一套自动发现机制就能体现出价值。它与第三方库并不互斥反而配合得很好。我在实际运维中还发现周期性多播发现对网络的负担几乎可以忽略不计因为Probe消息很小频率又不高。哪怕在设备密集的园区网里每60秒一次扫描也不会造成明显压力。这让我更倾向于把发现机制做成常驻服务而不是一次性脚本。最后分享一个小经验如果你在某个网络环境里始终扫描不到某台设备先别急着怀疑代码用ONVIF Device Manager手动扫一次。如果它同样扫不到那大概率是设备自身的WS-Discovery实现有问题或者它被配置成了不响应发现请求。我遇到过一台老设备必须先在Web端开启“ONVIF发现”开关之后才能被扫到。这属于设备侧行为不是协议层面的问题但排查时很容易被忽略。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进