ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用Python实现ONVIF WS-Discovery:彻底解决摄像头IP漂移问题

用Python实现ONVIF WS-Discovery:彻底解决摄像头IP漂移问题 搞摄像头接入这种事最烦的不是配码流、不是鉴权而是设备IP变了之后你到处找不着它。我之前维护一套视频系统摄像机全走DHCP断电重启一次IP就漂了挨个网段扫端口、翻路由表、查交换机MAC折腾一晚上就为了拿一个新的IP地址。后来接触了ONVIF标准里的WS-Discovery设备发现发现这个机制天生就是解决这种问题的——设备在局域网内广播自己、客户端发一条探测报文就能把摄像头全部捞出来根本不需要知道IP。这篇东西我就把自己用Python实现ONVIF WS-Discovery的过程完整梳理一遍包括协议里的坑、代码怎么拆、抓包怎么验证、收不到响应怎么排查代码是能直接复制跑的完整版本。适合刚接触ONVIF、手里有一堆网络摄像机想统一管理的朋友也适合做安防集成、做NVR协议对接的开发者参考。1. ONVIF与设备发现为什么非要搞这个1.1 摄像头联网不等于能被管理现在市面上的IPC网络摄像机基本都支持ONVIF协议但这只代表它们暴露了一个标准接口不代表你连上网络就能直接调用。ONVIF标准分了很多Profile配置集比如Profile S管流媒体、Profile T管高级编码、Profile G管录像。但无论哪个配置集第一步永远是先找到设备在哪、服务地址是什么。很多人以为发现设备就是扫描一下网段里的IP和端口实际上ONVIF设备默认开的是8000或8899这类端口但IP变了、开了DHCP、或者被手动改过网段你扫描到的端口列表其实啥也说明不了因为你不知道那个端口对应的到底是摄像头的ONVIF服务还是某个NVR的Web页面。WS-Discovery要解决的就是我知道这网里一定有设备但不知道它们在哪个IP这个问题。它不依赖具体IP而是靠UDP组播来广播和回应发现请求设备上线了会主动喊一声客户端需要找设备时也可以主动喊一嗓子。理解这一点后面的代码逻辑就顺了。1.2 WS-Discovery不是扫描IP是设备自己开口说话WS-Discovery全称是Web Services Dynamic Discovery是微软牵头定义的一组基于SOAP/XML的发现协议后来被ONVIF标准直接拿过来当作设备发现的基础机制。它有个中文名经常被翻译成Web服务动态发现核心思路就是客户端往一个约定好的组播地址发一条Probe探测报文网内支持该协议的设备收到后会往客户端返回ProbeMatch探测匹配报文把自己的服务地址、类型、作用域等信息交出来。这里有个关键点这不是TCP握手式的请求响应而是UDP组播。报文发出去之后谁应答、什么时候应答、应答几遍都取决于设备厂商实现。所以代码里不能只发一次就干等得设计超时、重试、多轮接收并且要做好同一台设备应答多次的幂等处理。后面我会详细讲到这几块的代码怎么写。1.3 什么时候你会真正需要它我总结了几类场景你可以对号入座第一设备初始化和批量入网。几十台摄像机第一天通电你一台台去浏览器里输IP、改密码、开ONVIF效率太低了。用WS-Discovery先批量捞出所有设备再按IP段去初始化十分钟搞定。第二设备IP漂移后的重新发现。DHCP环境里IP说变就变但你只要知道设备还在这个网里发一条Probe就能把当前IP拉出来根本不用去路由器后台翻租约记录。第三给第三方系统做一键添加设备。你做一个平台想让用户点按钮就自动发现局域网里的摄像头这比让用户填一堆IP端口密码要友好得多。第四运维巡检。设备离线了、被换掉了、或者固件升级后服务地址变了跑一遍发现流程就知道当前状态不用挨个网段telnet端口。2. 环境准备10分钟把工具箱备齐2.1 Python版本与虚拟环境我的开发环境是Python 3.10实际上3.7以上都够用因为核心逻辑只用标准库不依赖新语法。不建议用Python 2这都什么年代了ONVIF设备返回的XML有时候带中文注释Python 2的字符串处理会让你怀疑人生。项目开始时先建一个虚拟环境把环境隔离清楚养成习惯后面省很多事python3 -m venv venv source venv/bin/activateWindows下的激活命令是venv\Scripts\activateMac/Linux用上面的。激活后命令行前面会多一个(venv)前缀说明已经进入隔离环境了。2.2 选型裸socket还是用自己的库网上搜ONVIF设备发现很多人推荐直接用WSDiscovery这个第三方库pip装一下三四行代码就能跑from wsdiscovery.discovery import ThreadedWSDiscovery as WSDiscovery wsd WSDiscovery() wsd.start() services wsd.searchServices()这个库本身做得不错但它做了很多封装导致你根本不知道底层发生了什么。一旦设备返回的报文格式稍微特殊一点或者你的网络环境比较奇葩多网卡、跨VLAN、防火墙拦组播你就会陷入库能跑但找不着问题的尴尬境地。我的建议是做项目时用标准库socketxml.etree.ElementTree裸写一遍。一是因为WS-Discovery报文本身不复杂二是因为你需要完全掌控超时、多播、套接字选项这些细节调试时能精确知道卡在哪一步。等你完全理解了协议流程再决定要不要用现成库。2.3 依赖安装与编辑器建议核心代码不需要任何第三方依赖标准库就够了。如果你后续要继续调用ONVIF媒体服务比如拿RTSP流地址、设置镜头参数可以装onvif或python-onvif-zeep。编辑器方面我用VS Code配一下Python插件就行。注意VS Code里要选择正确的解释器——按CtrlShiftP输入Python: Select Interpreter选中你刚才建的虚拟环境不然跑起来还是系统的Python依赖会乱。如果你用PyCharm新建项目时直接指定虚拟环境省事。测试时需要一台支持ONVIF的设备。如果手头没有真机可以装一个ONVIF模拟器比如ONVIF Device Manager自带的虚拟设备功能或者用vapix模拟器但说实话模拟器和真机行为还是有区别的真机最容易出问题的点恰恰在于它对Probe报文的响应格式不规范。后面我会讲怎么处理这种不按套路出牌的设备。3. WS-Discovery协议拆解读懂报文才能写出能用的代码3.1 那朵乌云组播地址239.255.255.250与端口3702WS-Discovery的消息走的是UDP组播目标地址是239.255.255.250目标端口是3702。这个地址是全网络范围内约定的任何支持WS-Discovery的设备都会监听这个组播地址。客户端发Probe时也是往这个地址发消息会被局域网内所有加入该组播组的设备收到。这里有一个非常容易踩的坑组播不是广播255.255.255.255设备必须加入组播组才能收到组播报文。ONVIF设备出厂时一般都会自动加入这个组但如果你在交换机上做了IGMP Snooping组播侦听又没有配好组播路由器设备可能收不到跨VLAN的组播报文。后面排查问题这一节我会专门讲。UDP端口3702是固定的但客户端发送时不需要绑定3702可以随便用一个空闲端口。设备收到Probe后会往客户端的源IP和源端口单独回一条UDP单播报文这就是为什么客户端代码里要一直监听同一个socket等着收包。3.2 Probe、ProbeMatch、Hello、Bye四种消息的区别WS-Discovery定义了四类基本的发现消息Probe客户端主动发起的探测请求相当于敲门问有没有ONVIF设备ProbeMatch设备对Probe的应答相当于开门说我在这是我的地址Hello设备上线或加入网络时主动广播的通告相当于我来了请注意我Bye设备下线时主动广播的通告相当于我先走了别找我了写代码重点关注Probe和ProbeMatch。Hello和Bye在高级运维场景里有用可以在线感知设备状态但基础发现用不到。Probe报文里最重要的字段是Types它的值决定了设备要不要应答。对于ONVIF常见的有两种写法dn:NetworkVideoTransmitter表示只找网络视频发射器也就是主要的IP摄像机类别dn:NetworkVideoDisplay表示网络视频显示器比如解码器、大屏控制器如果你Types留空理论上所有支持WS-Discovery的设备都会回应包括打印机、智能家居设备这会让结果很杂。所以标准写法是明确指定dn:NetworkVideoTransmitter。3.3 关键字段Types / Scopes / XAddrsProbeMatch应答报文里三个字段是必须要搞明白的Types设备类别和Probe里请求的一样。比如dn:NetworkVideoTransmitter。Scopes设备的作用域描述本质是一个字符串列表用空格分隔。里面通常会包含设备的MAC地址、型号、硬件信息、固件版本、所在位置等。比如onvif://www.onvif.org/type/video_encoder onvif://www.onvif.org/hardware/DS-2CD2143G0-I onvif://www.onvif.org/name/MyCamera onvif://www.onvif.org/location/Entrance从Scopes里你能抠出很多有价值的信息比如硬件型号、摄像头名称、部署位置。XAddrs这是你最关心的字段。它是一个或多个HTTP地址每个地址都是设备ONVIF服务Device Service的入口地址形如http://192.168.1.100/onvif/device_service。拿到这个地址你就能调用后续的GetCapabilities、GetProfiles、GetStreamUri等ONVIF操作了。还有一个小字段MetadataVersion用来标识设备信息的版本号一般用不上。3.4 一条真实Probe报文逐行解读写完代码后我们需要用Wireshark抓包验证所以先看一条真实的Probe请求长什么样。下面是Wireshark里看到的XML?xml version1.0 encodingutf-8? soap:Envelope xmlns:soaphttp://www.w3.org/2003/05/soap-envelope xmlns:wsahttp://schemas.xmlsoap.org/ws/2004/08/addressing xmlns:dhttp://schemas.xmlsoap.org/ws/2005/04/discovery xmlns:dnhttp://www.onvif.org/ver10/network/wsdl soap:Header wsa:Actionhttp://schemas.xmlsoap.org/ws/2005/04/discovery/Probe/wsa:Action wsa:MessageIDurn:uuid:3e5c6a2a-1e93-4e2f-9fcb-2b1b5c9e8c66/wsa:MessageID wsa:Tourn:schemas-xmlsoap-org:ws:2005:04:discovery/wsa:To /soap:Header soap:Body d:Probe d:Typesdn:NetworkVideoTransmitter/d:Types /d:Probe /soap:Body /soap:Envelope别看这XML长得唬人核心就两部分Header里的Action告诉设备这是一条Probe请求Body里的Types告诉设备我找视频发射器。每个请求的MessageID要唯一通常用UUID生成否则部分严格校验的设备会直接忽略。ProbeMatch报文里还会多出XAddrs和Scopes字段格式类似这样d:ProbeMatches d:ProbeMatch wsa:EndpointReference wsa:Addressuuid:5f2c24e0-.../wsa:Address /wsa:EndpointReference d:Typesdn:NetworkVideoTransmitter/d:Types d:Scopesonvif://www.onvif.org/type/video_encoder onvif://www.onvif.org/hardware/DS-2CD2143G0-I/d:Scopes d:XAddrshttp://192.168.1.100/onvif/device_service/d:XAddrs d:MetadataVersion2/d:MetadataVersion /d:ProbeMatch /d:ProbeMatches注意XAddrs可能出现多个地址用空格分隔里面可能同时存在IPv4和IPv6。解析的时候不要只取第一个要全部拉出来然后选一个能通的用。4. Python代码实现完整可运行版本4.1 主线代码一次性跑通的完整脚本话不多说先上完整代码。这个脚本可以直接保存为onvif_discovery.py然后运行python onvif_discovery.py#!/usr/bin/env python3 # -*- coding: utf-8 -*- ONVIF WS-Discovery 设备发现脚本 通过 UDP 组播发送 Probe 报文监听 ProbeMatch 响应解析设备信息。 纯标准库实现无第三方依赖。 import socket import uuid import time import xml.etree.ElementTree as ET # ---- 常量定义 ---- MULTICAST_GROUP 239.255.255.250 MULTICAST_PORT 3702 DEFAULT_TIMEOUT 5 # 接收响应总超时时间秒 PROBE_INTERVAL 1.5 # 重发 Probe 的间隔秒 # WS-Discovery 命名空间 NS { soap: http://www.w3.org/2003/05/soap-envelope, wsa: http://schemas.xmlsoap.org/ws/2004/08/addressing, d: http://schemas.xmlsoap.org/ws/2005/04/discovery, dn: http://www.onvif.org/ver10/network/wsdl, } def build_probe(): 构造 ONVIF Probe 请求报文。 message_id urn:uuid: str(uuid.uuid4()) return f?xml version1.0 encodingutf-8? soap:Envelope xmlns:soap{NS[soap]} xmlns:wsa{NS[wsa]} xmlns:d{NS[d]} xmlns:dn{NS[dn]} soap:Header wsa:Actionhttp://schemas.xmlsoap.org/ws/2005/04/discovery/Probe/wsa:Action wsa:MessageID{message_id}/wsa:MessageID wsa:Tourn:schemas-xmlsoap-org:ws:2005:04:discovery/wsa:To /soap:Header soap:Body d:Probe d:Typesdn:NetworkVideoTransmitter/d:Types /d:Probe /soap:Body /soap:Envelope.encode(utf-8) def parse_probe_match(packet, source_addr): 解析 ProbeMatch 响应报文。 返回 dict无法解析时返回 None。 try: root ET.fromstring(packet) # 提取 XAddrs服务地址可能有多个空格分隔 xaddrs_elem root.find(.//d:XAddrs, NS) xaddrs_text xaddrs_elem.text.strip() if xaddrs_elem is not None and xaddrs_elem.text else xaddrs_list xaddrs_text.split() if xaddrs_text else [] # 提取 Types types_elem root.find(.//d:Types, NS) types types_elem.text.strip() if types_elem is not None and types_elem.text else # 提取 Scopes并从中解析硬件型号和摄像头名称 scopes_elem root.find(.//d:Scopes, NS) scopes scopes_elem.text.strip() if scopes_elem is not None and scopes_elem.text else scopes_list scopes.split() hardware name for scope in scopes_list: if hardware in scope: hardware scope.rstrip(/).split(/)[-1] elif name in scope: name scope.rstrip(/).split(/)[-1] metadata_version mv_elem root.find(.//d:MetadataVersion, NS) if mv_elem is not None and mv_elem.text: metadata_version mv_elem.text.strip() return { source_ip: source_addr[0], source_port: source_addr[1], xaddrs: xaddrs_list, types: types, scopes: scopes, hardware: hardware, name: name, metadata_version: metadata_version, } except ET.ParseError as e: print(f[!] XML解析失败来自 {source_addr}{e}) return None def discover_devices(timeoutDEFAULT_TIMEOUT, retries2): 核心发现函数 1. 创建 UDP 套接字设置多播选项 2. 多次发送 Probe 报文 3. 循环接收 ProbeMatch 响应 4. 解析并聚合设备信息 devices {} seen set() # 创建 UDP 套接字并设置多播 sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, 1) # 绑定到任意空闲端口 sock.bind((, 0)) local_port sock.getsockname()[1] sock.settimeout(0.5) # 每次 recvfrom 最多等待 0.5 秒 print(f[*] 本机源端口{local_port}) print(f[*] 向 {MULTICAST_GROUP}:{MULTICAST_PORT} 发送 Probe...) start_time time.time() last_probe_time 0 while time.time() - start_time timeout: # 周期性地重发 Probe避免丢包导致没设备响应 if time.time() - last_probe_time PROBE_INTERVAL: try: sock.sendto(build_probe(), (MULTICAST_GROUP, MULTICAST_PORT)) last_probe_time time.time() print(f[] Probe 已发送第 {int((last_probe_time - start_time) // PROBE_INTERVAL) 1} 次) except OSError as e: print(f[!] 发送 Probe 失败{e}) try: data, addr sock.recvfrom(65535) if addr[0] 0.0.0.0: continue # 接收到的可能是任意组播报文尝试解析为 ProbeMatch info parse_probe_match(data, addr) if info and info[xaddrs]: # 以第一个服务地址作为设备的唯一标识去重 key info[xaddrs][0] if key not in seen: seen.add(key) devices[key] info print(f[] 发现设备{key}) except socket.timeout: continue except Exception as e: print(f[!] 接收解析异常{e}) continue sock.close() return list(devices.values()) if __name__ __main__: print( * 50) print(ONVIF WS-Discovery 设备发现) print( * 50) results discover_devices(timeout6) print(\n * 50) print(f[*] 发现完成共 {len(results)} 台设备) print( * 50) for idx, dev in enumerate(results, 1): print(f\n--- 设备 {idx} ---) print(f来源地址 : {dev[source_ip]}:{dev[source_port]}) print(f服务地址 : {, .join(dev[xaddrs])}) print(f设备类型 : {dev[types]}) print(f硬件型号 : {dev[hardware]}) print(f设备名称 : {dev[name]}) print(f元数据版本 : {dev[metadata_version]}) print(f作用域 : {dev[scopes]})运行效果大概是这样 ONVIF WS-Discovery 设备发现 [*] 本机源端口53124 [*] 向 239.255.255.250:3702 发送 Probe... [] Probe 已发送第 1 次 [] 发现设备http://192.168.1.108/onvif/device_service [] Probe 已发送第 2 次 [] 发现设备http://192.168.1.173/onvif/device_service [] 发现设备http://192.168.1.64/onvif/device_service 发现完成共 3 台设备 --- 设备 1 --- 来源地址 : 192.168.1.108:3702 服务地址 : http://192.168.1.108/onvif/device_service 设备类型 : dn:NetworkVideoTransmitter ...三台设备一共用了不到两秒就全部找出来了比扫网段靠谱得多。4.2 代码拆解每一部分在干嘛套接字创建与多播设置sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, 1) sock.bind((, 0))这里用了UDP套接字因为WS-Discovery本质上是UDP协议。SO_REUSEADDR是为了让端口可以快速重用避免脚本反复运行时出现Address already in use的报错。IP_MULTICAST_TTL设为2代表组播报文最多经过2层路由跳转一般局域网内1就够但设2能兼容一些奇葩网络环境。IP_MULTICAST_LOOP设为1代表本机回环允许这样本机如果有虚拟设备也能收到响应。注意bind((, 0))端口填0表示让系统自动分配一个空闲端口。设备收到Probe后会往这个绑定的端口回单播响应所以整个发现过程必须使用同一个socket收发。发送循环的设计我用了一个while time.time() - start_time timeout的大循环里面每隔PROBE_INTERVAL重发一次Probe。为什么要重发因为UDP是不可靠的组播报文可能在交换机里被丢弃设备也可能因为繁忙来不及响应。一次发出去了设备正好在处理别的业务你的Probe就被忽略了。重发2-3次能显著提高发现成功率。接收与解析try: data, addr sock.recvfrom(65535) except socket.timeout: continuesock.settimeout(0.5)让每次接收最多等0.5秒然后回到大循环检查是否超时顺便看看要不要重发Probe。这样整个流程是边发边收而不是发完干等。recvfrom返回的data是字节串直接传给解析函数。数据包最大接收65535字节因为这是UDP报文的理论上限。有时候设备会返回大报文Scopes里带很多信息留足空间是稳妥的。去重策略if key not in seen: seen.add(key) devices[key] infokey用的是XAddrs的第一个地址。为什么要去重因为有的设备会对同一个Probe响应多次或者你的Probe重发两三次设备每次都响应了。不去重的话结果列表会重复到没法看。4.3 解析ProbeMatch重点和坑位解析这块我单独展开因为坑特别多。XML解析用的是标准库的xml.etree.ElementTree最省事。核心就是通过.find()配合XPath路径和命名空间定位元素。注意这里有个大坑WS-Discovery的命名空间前缀并不是固定不变的有的设备会用p:、s:作为SOAP前缀有的设备会把命名空间换个URI。如果你在代码里死写s作为前缀碰到用soap或SOAP-ENV前缀的设备就解析不到了。我代码里的解决办法是不依赖默认前缀而是在find()里显式指定完整命名空间URI。ET.fromstring解析后不管前缀叫什么只要命名空间URI匹配.find(.//d:XAddrs, {d: http://schemas.xmlsoap.org/ws/2005/04/discovery})就能找到。这是最可靠的做法。另一个坑是XAddrs字段里可能有多个地址用空格分隔。有些设备会同时返回IPv4和IPv6地址你取第一个如果是IPv6的而你的网络环境不支持IPv6后续调用就失败了。所以说代码里返回的是xaddrs_list实际使用时应该逐个尝试。还有一点有些设备返回的XML里带了?xml?声明有些没有有些是UTF-8有些是UTF-16。ET.fromstring对UTF-16的XML解码可能会报ParseError。稳妥的做法是先用data.decode(utf-8, errorsreplace)转成字符串再交给解析器。上面的代码直接传字节给ET遇到严格的设备可能翻车建议改成try: xml_str packet.decode(utf-8) except UnicodeDecodeError: xml_str packet.decode(utf-16, errorsreplace) root ET.fromstring(xml_str)这属于实战里踩过坑才写得出来的细节。4.4 实用扩展多网卡、TTL、超时控制生产环境里很多机器不止一块网卡。比如服务器有eth0内网和eth1管理网摄像头接在eth0上但Probe往239.255.255.250一发操作系统可能默认走eth1出去了结果你收不到任何响应。解决方案是给套接字指定出口网卡import subprocess # 查看本机网卡对应的 IP def get_local_ip(interfaceeth0): # 用 socket 的方式更通用 s socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect((8.8.8.8, 80)) # 这里并不会真正发包 ip s.getsockname()[0] s.close() return ip # 绑定到指定网卡 local_ip get_local_ip() # 或者手动指定 192.168.1.10 sock.bind((local_ip, 0))绑定了本地IP之后组播报文就会从该IP所属的网卡发出去。注意IP_MULTICAST_IF选项可以更精确地指定多播出口sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton(192.168.1.10))TTL的控制前面提过了IP_MULTICAST_TTL决定报文能在网络里活多少跳。默认1就够局域网使用但如果你有多个三层交换机互联可能要调大。超时控制这块官方ONVIF建议Probe的超时在3到5秒左右。设太短可能漏掉响应慢的设备设太长用户体验很差。我的经验是timeout5PROBE_INTERVAL1.5重发两轮基本能覆盖99%的设备。有的老设备响应很慢可能要等两三秒才回所以recvfrom的单次等待必须设短配合大循环的总体超时才是正确姿势。5. 常见问题与排查实录5.1 一个问题都没有响应按这个顺序查场景脚本跑起来打印了Probe 已发送但五秒后发现完成共0台设备。这是刚开始用的人遇到最多的问题我按排查优先级给你列一下第一确认网络环境是否允许组播。交换机开了IGMP Snooping且没有组播路由器管理组成员关系组播报文会被交换机丢弃。你可以先用广播式的工具测试或者临时在交换机上关闭IGMP Snooping试验一下。第二确认设备真的支持ONVIF且开启了发现功能。很多海康、大华设备默认支持但部分型号在固件设置里需要手动勾选启用ONVIF或允许发现。登录设备管理页面查一下。第三确认你客户端所在的网段和设备一致。WS-Discovery的组播默认只在二层网络生效跨VLAN需要三层设备转发。你可以临时把电脑挪到摄像机所在VLAN试试。第四防火墙拦截。Windows防火墙默认会拦截UDP入站需要放行3302端口或者允许Python进程访问网络。Linux上则要看iptables/firewalld规则。第五抓包看报文到底发出去没有。这招最直接下面讲。5.2 抓包确认Wireshark 20秒上手在排查网络问题时光靠推理是不行的必须抓包看证据。打开Wireshark在过滤栏里输入udp.port 3702然后点击开始捕获。在另一个终端运行你的发现脚本观察Wireshark列表如果你看到了从你机器发出的、目标地址为239.255.255.250、目标端口为3702的UDP报文说明报文确实发出去了。如果发出的报文都没看见问题出在本地网络栈或防火墙先处理本机。如果发出有、但没有任何设备回包说明组播没到达设备或设备没处理问题出在网络/设备。如果看到响应包回来了但脚本没打印问题出在代码的接收或解析逻辑。还可以在Wireshark里右键点击Probe报文选择Follow UDP Stream直接看完整XML内容检查是否有格式问题。5.3 解析不到XAddrs问题可能不在代码脚本能收到响应、打印了收到来自xx的响应但parse_probe_match返回的信息里XAddrs是空的或者设备信息不全。这种情况往往是设备返回的报文不符合标准格式。有些厂商尤其是贴牌小厂的设备会把XAddrs放在一个你没有预料到的层级或者命名空间写错。解决思路是先打印原始XML人工检查字段位置再调整XPath。我调试时会在解析失败时把原始报文打印出来except ET.ParseError as e: print(f[!] XML解析失败来自 {source_addr}内容) print(packet.decode(utf-8, errorsreplace))看到原始报文后手动看一下XAddrs在什么路径下就能针对性修改解析逻辑了。5.4 联调经验速查表现象可能原因处理方案完全没收到任何响应组播被交换机/防火墙拦截或设备不支持ONVIF检查IGMP、防火墙、设备设置用Wireshark抓包定位收到响应但无XAddrs设备返回报文格式不规范打印原始XML调整XPath解析收到响应但XAddrs是IPv6设备返回多个地址取错了遍历XAddrs列表优先尝试IPv4同一设备出现多次设备重复响应或多个接口用去重逻辑以XAddrs为唯一键某台设备始终发现不到设备处于不同VLAN或已关闭发现检查三层网络登录设备确认ONVIF状态脚本偶尔能发现、偶尔不能UDP丢包或设备响应超时增加重发次数延长总超时运行时报Address already in use端口被占用或上行未释放启用SO_REUSEADDR或换随机端口这些坑我都是实际踩过的尤其是设备重复响应这个最初没去重时三台摄像机打印出八条记录差点以为网络里还有别的设备。6. 从发现到调用一个完整的后续流程6.1 用发现到的XAddrs绑定ONVIF服务发现设备只是第一步最终目标肯定是要拿到码流、调参数。拿到XAddrs之后就可以对接ONVIF Device Service了。以python-onvif-zeep库为例pip install onvif-zeep然后可以用发现到的服务地址创建设备对象from onvif import ONVIFCamera xaddr http://192.168.1.108/onvif/device_service # 从地址中解析出 IP、端口、路径 from urllib.parse import urlparse parts urlparse(xaddr) host parts.hostname port parts.port or 80 path parts.path # 用户名密码是你在设备上创建的 ONVIF 账号 cam ONVIFCamera(host, port, admin, password, wsdl_dirNone) # 获取设备信息 info cam.devicemgmt.GetDeviceInformation() print(info.Manufacturer, info.Model) # 获取媒体配置拿到 RTSP 流地址 media_service cam.create_media_service() profiles media_service.GetProfiles()这里有个关键点ONVIF的账号和密码不一定是设备Web登录的账号很多设备需要在Web里专门创建一个ONVIF用户或者勾选使用管理员账号作为ONVIF账号。不配好鉴权后面GetDeviceInformation会报401 Not Authorized。后面还可以继续调GetStreamUri拿到RTSP地址用OpenCV或FFmpeg拉流一套完整的流程就算打通了。注意GetStreamUri需要传ProfileToken和StreamSetup两个参数不同型号的设备对参数要求不一样多试几种组合就行。6.2 跨网段与交换机配置设备发现的一个额外注意点如果你的设备不在同一个二层网络WS-Discovery默认就失效了。三种解决方案第一种用ONVIF标准里的Remote Discovery通过在已知网段内的一台设备做中继让那台设备帮你转发Probe。这个支持率不高很多设备没实现。第二种手动指定IP列表。既然知道设备可能分布在哪些网段就逐个网段发送Probe但前提是你的机器在这些网段有IP或者三层设备支持组播代理。第三种直接跳过发现靠外部数据源维护设备IP清单。项目里我们通常会用数据库或配置文件记录设备XAddrs定期用WS-Discovery同步验证发现设备离线或换IP时再更新。6.3 生产环境落地时的几个重要建议第一发现操作要异步执行。WS-Discovery需要几秒的窗口期如果放在Web请求的同步链路里前端会卡死。正确做法是放到后端任务队列里发现完成后回调通知。第二做好设备账号密码的集中管理。发现能帮你拿到设备地址但登不上设备一样白搭。建议把ONVIF用户名密码按设备型号或MAC地址维度维护成配置表自动化流程里自动匹配。第三合理处理设备返回的多个XAddrs。我曾经碰到一台设备返回了两个地址一个是内网IP一个是NAT后的公网IP如果不加判断直接选第一个后续调用会在不合适的网络环境中反复超时。可以在发现后追加一轮连通性测试用HTTP OPTIONS请求探一下哪个地址能通再把它作为主服务地址。第四安全方面有个值得注意的细节WS-Discovery是明文组播意味着同一网络里的任何设备都能收到Probe和ProbeMatch报文设备地址会暴露给网内所有人。生产环境中不要把设备发现暴露到不可信网络尽量在独立的管理VLAN里做发现操作。7. 调试神器与效率技巧工欲善其事必先利其器。这一节分享几个我实际用下来非常顺手的工具和技巧能帮你省下一大半排查时间。第一Wireshark的显示过滤器再强调一遍抓ONVIF发现报文就用udp.port 3702。如果只想看Probe请求加一个条件是udp[8:2] 0x014e这一段的含义是UDP报文的长度字段有时候判断不太准直接用完整过滤器更稳udp.dstport 3702 || udp.srcport 3702抓包时如果担心混杂模式没有打开导致收不到组播在Wireshark的Capture Options里勾选Enable promiscuous mode。第二Windows下可以用ONVIF Device Manager这个工具做交叉验证。如果ODM能发现设备、你的脚本发现不了说明是代码或系统配置问题如果ODM也发现不了那就是网络或设备问题。这样能快速缩小排查范围。第三写代码时给脚本加一个-v参数输出详细的报文内容和原始XML。平时静默出错时不至于手忙脚乱加日志。import argparse parser argparse.ArgumentParser(descriptionONVIF WS-Discovery) parser.add_argument(-v, --verbose, actionstore_true, help打印详细调试信息) parser.add_argument(-t, --timeout, typeint, default5, help总超时时间(秒)) args parser.parse_args()然后代码里根据args.verbose决定是否打印原始XML测试时加上-v正式跑就不打了。第四善用抓包对比学习。拿到一台新设备时先抓一次正常流程的包存成pcap文件。以后遇到解析不了的情况拿那个标准包对比一下很快就能发现设备返回的XML哪里不一样。8. 最后的实战心得我做ONVIF设备接入的经验里发现这一环节虽然看起来最简单实际上最坑。协议文档写得清清楚楚但设备厂商的实现千奇百怪光靠文档写代码是不行的一定要抓包、看原始报文、适配各种不规范返回。我个人觉得最值得记住的是两点一是发送流程要设计成边发边收、多次重发不要发一次就干等二是解析报文时不要死绑命名空间前缀和字段层级用命名空间URI来匹配并且做好容错。掌握了这两点基本就能应付市面上九成以上的ONVIF设备。如果你只是需要快速完成一个项目直接复制上面那段完整代码改改用就行。如果你也是做视频平台、设备接入网关这类系统建议还是把协议本身吃透后面遇到不同厂商的设备、不同形态的网络环境时才知道从哪个方向去排查。最后再分享一个实用小技巧发现完设备后先把XAddrs存到本地SQLite或Redis里下次再跑可以做增量比对设备换了IP能立刻知道比每次都重新发现一遍高效很多。
返回列表