实战:用Python+ONVIF库5分钟实现网络摄像头设备发现与PTZ控制

实战:用Python+ONVIF库5分钟实现网络摄像头设备发现与PTZ控制

最近在折腾家里的智能安防系统,想把几个不同品牌的网络摄像头统一管理起来,实现自动巡检和云台控制。本以为要写一堆复杂的网络协议代码,结果发现ONVIF这个标准协议配合Python的onvif-zeep库,竟然能在短短几分钟内搞定设备发现、服务获取甚至PTZ控制。今天我就把整个实战过程拆解给你看,无论你是物联网开发者还是自动化脚本爱好者,这套方案都能让你快速对接市面上大多数支持Profile S协议的摄像头。

1. 环境准备与核心库选择

在开始编写代码之前,我们需要先搭建一个合适的开发环境。ONVIF协议基于SOAP和WSDL,这意味着我们需要处理XML和Web服务。Python社区有几个ONVif客户端库,经过我的实际测试,onvif-zeep是目前最稳定、文档最全的选择。

1.1 安装必要的Python包

打开终端,创建一个新的虚拟环境是个好习惯:

python -m venv onvif_env
source onvif_env/bin/activate  # Linux/macOS
# 或者 onvif_env\Scripts\activate  # Windows

然后安装核心库:

pip install onvif-zeep

注意:onvif-zeep会自动安装其依赖,包括zeep(SOAP客户端)、httpx等。确保你的Python版本在3.7以上。

除了核心库,我还推荐安装几个辅助工具,方便调试和验证:

pip install python-dotenv  # 管理配置文件
pip install rich           # 让终端输出更美观
pip install ipython        # 交互式调试

1.2 理解ONVIF Profile S的关键作用

在动手写代码前,花两分钟理解一下ONVIF的Profile概念至关重要。Profile可以理解为“功能套餐”,它定义了一组设备必须支持的最小功能集合。

Profile 名称 主要应用领域 核心功能
Profile S 视频流系统 视频流获取、PTZ控制、事件处理
Profile T 高级视频流 在Profile S基础上增加智能分析、元数据流
Profile G 边缘存储 本地录像存储与检索
Profile Q 快速安装 简化设备发现与初始配置

我们今天的重点Profile S,是网络摄像头最基础、支持最广泛的Profile。一个符合Profile S标准的摄像头,必须提供:

  • 设备发现(WS-Discovery)
  • 设备管理服务(获取设备信息、网络配置)
  • 媒体服务(获取视频流URI、快照)
  • PTZ服务(云台控制、预置位)

这意味着,只要你的摄像头支持Profile S(市面上90%以上的主流品牌都支持),我们接下来的代码就能直接控制它的云台转动、变焦和对焦。

2. 快速实现网络摄像头设备发现

设备发现是ONVIF交互的第一步。传统方式需要手动输入摄像头的IP地址,但ONVIF提供了WS-Discovery协议,可以让设备自动在网络上“广播”自己的存在。

2.1 使用WS-Discovery探测局域网设备

创建一个名为discover_cameras.py的文件:

#!/usr/bin/env python3
"""
ONVIF设备发现脚本
使用WS-Discovery协议探测局域网内所有支持ONVIF的设备
"""

import socket
import struct
from datetime import datetime
from rich.console import Console
from rich.table import Table

# 创建控制台输出对象
console = Console()

def discover_onvif_devices(timeout=3):
    """
    使用WS-Discovery协议发现ONVIF设备
    
    参数:
        timeout: 探测超时时间(秒)
    
    返回:
        list: 发现的设备信息列表
    """
    # WS-Discovery多播地址和端口
    MULTICAST_GROUP = "239.255.255.250"
    MULTICAST_PORT = 3702
    
    # 创建UDP套接字
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    
    # 设置超时
    sock.settimeout(timeout)
    
    # 绑定到任意地址和端口
    sock.bind(('', MULTICAST_PORT))
    
    # 加入多播组
    mreq = struct.pack("4sl", socket.inet_aton(MULTICAST_GROUP), socket.INADDR_ANY)
    sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
    
    # WS-Discovery探测消息
    probe_message = '''<?xml version="1.0" encoding="UTF-8"?>
    <e:Envelope xmlns:e="http://www.w3.org/2003/05/soap-envelope"
                xmlns:w="http://schemas.xmlsoap.org/ws/2004/08/addressing"
                xmlns:d="http://schemas.xmlsoap.org/ws/2005/04/discovery"
                xmlns:dn="http://www.onvif.org/ver10/network/wsdl">
        <e:Header>
            <w:MessageID>uuid:{message_id}</w:MessageID>
            <w:To e:mustUnderstand="true">urn:schemas-xmlsoap-org:ws:2005:04:discovery</w:To>
            <w:Action e:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</w:Action>
        </e:Header>
        <e:Body>
            <d:Probe>
                <d:Types>dn:NetworkVideoTransmitter</d:Types>
            </d:Probe>
        </e:Body>
    </e:Envelope>'''
    
    # 生成唯一的Message ID
    import uuid
    message_id = str(uuid.uuid4())
    probe_msg = probe_message.format(message_id=message_id)
    
    # 发送探测消息
    sock.sendto(probe_msg.encode(), (MULTICAST_GROUP, MULTICAST_PORT))
    
    devices = []
    console.print(f"[bold green]开始探测局域网ONVIF设备,超时时间{timeout}秒...[/bold green]")
    
    try:
        while True:
            data, addr = sock.recvfrom(4096)
            response = data.decode('utf-8', errors='ignore')
            
            # 解析响应,提取设备信息
            device_info = parse_discovery_response(response, addr[0])
            if device_info:
                devices.append(device_info)
                console.print(f"[green]发现设备: {device_info['manufacturer']} - {device_info['model']} ({addr[0]})[/green]")
                
    except socket.timeout:
        console.print("[yellow]设备探测完成[/yellow]")
    finally:
        sock.close()
    
    return devices

def parse_discovery_response(xml_response, ip_address):
    """解析WS-Discovery响应XML"""
    import re
    
    device_info = {
        'ip': ip_address,
        'manufacturer': '未知',
        'model': '未知',
        'serial_number': '未知',
        'hardware_id': '未知'
    }
    
    # 使用正则表达式提取关键信息(简化版,实际项目建议用XML解析器)
    manufacturer_match = re.search(r'<tt:Manufacturer>(.*?)</tt:Manufacturer>', xml_response)
    model_match = re.search(r'<tt:Model>(.*?)</tt:Model>', xml_response)
    serial_match = re.search(r'<tt:SerialNumber>(.*?)</tt:SerialNumber>', xml_response)
    hardware_match = re.search(r'<tt:HardwareId>(.*?)</tt:HardwareId>', xml_response)
    
    if manufacturer_match:
        device_info['manufacturer'] = manufacturer_match.group(1)
    if model_match:
        device_info['model'] = model_match.group(1)
    if serial_match:
        device_info['serial_number'] = serial_match.group(1)
    if hardware_match:
        device_info['hardware_id'] = hardware_match.group(1)
    
    # 提取服务端点(XAddrs)
    xaddrs_match = re.search(r'<d:XAddrs>(.*?)</d:XAddrs>', xml_response)
    if xaddrs_match:
        device_info['service_url'] = xaddrs_match.group(1).split()[0] if xaddrs_match.group(1) else None
    
    return device_info

def display_devices_table(devices):
    """以表格形式显示发现的设备"""
    if not devices:
        console.print("[red]未发现任何ONVIF设备[/red]")
        return
    
    table = Table(title="发现的ONVIF设备", show_header=True, header_style="bold magenta")
    table.add_column("序号", style="dim", width=6)
    table.add_column("IP地址", style="cyan", width=15)
    table.add_column("制造商", width=20)
    table.add_column("型号", width=20)
    table.add_column("序列号", width=25)
    
    for i, device in enumerate(devices, 1):
        table.add_row(
            str(i),
            device['ip'],
            device['manufacturer'],
            device['model'],
            device['serial_number']
        )
    
    console.print(table)
    console.print(f"[bold]共发现 {len(devices)} 个设备[/bold]")

if __name__ == "__main__":
    # 执行设备发现
    discovered_devices = discover_onvif_devices(timeout=5)
    
    # 显示结果
    display_devices_table(discovered_devices)
    
    # 保存设备信息到文件
    if discovered_devices:
        import json
        with open('discovered_devices.json', 'w') as f:
            json.dump(discovered_devices, f, indent=2, ensure_ascii=False)
        console.print(f"[green]设备信息已保存到 discovered_devices.json[/green]")

运行这个脚本,你会在终端看到一个漂亮的表格,列出所有发现的ONVIF设备:

python discover_cameras.py

2.2 处理常见的设备发现问题

在实际使用中,你可能会遇到设备无法被发现的情况。以下是几个常见问题及解决方案:

问题1:防火墙阻止了多播流量

  • 解决方案:临时关闭防火墙测试,或添加规则允许UDP端口3702的多播流量

问题2:设备不支持WS-Discovery

  • 解决方案:使用手动配置模式,直接指定设备的服务URL

问题3:网络交换机配置限制

  • 解决方案:确保交换机启用了IGMP Snooping,或尝试在同一个VLAN中测试

提示:如果自动发现失败,可以尝试使用nmap扫描摄像头的常用端口(80、443、554、2020),找到设备后再手动配置。

3. 建立ONVIF连接与获取服务能力

成功发现设备后,下一步是建立正式的ONVIF连接。这是最关键的一步,因为我们需要获取设备的服务端点(Endpoints)和能力(Capabilities)。

3.1 创建ONVIF客户端连接

创建一个新的文件camera_client.py

#!/usr/bin/env python3
"""
ONVIF客户端连接与能力获取
"""

from onvif import ONVIFCamera
import ssl
from urllib.parse import urlparse
from rich.console import Console
from rich.panel import Panel

console = Console()

class ONVIFCameraClient:
    def __init__(self, ip, port=80, username='admin', password=''):
        """
        初始化ONVIF相机客户端
        
        参数:
            ip: 相机IP地址
            port: HTTP端口(通常是80或443)
            username: 用户名
            password: 密码
        """
        self.ip = ip
        self.port = port
        self.username = username
        self.password = password
        
        # 构建基础URL
        self.base_url = f"http://{ip}:{port}" if port != 443 else f"https://{ip}"
        
        console.print(f"[bold]正在连接相机 {ip}:{port}...[/bold]")
        
        # 创建ONVIF相机对象
        try:
            # 注意:有些相机需要指定wsdl路径,这里使用库自带的
            self.camera = ONVIFCamera(
                ip, port, username, password,
                wsdl_dir='/usr/local/share/wsdl'  # 可能需要调整路径
            )
            console.print("[green]✓ ONVIF客户端创建成功[/green]")
        except Exception as e:
            console.print(f"[red]✗ 创建客户端失败: {e}[/red]")
            raise
    
    def get_device_information(self):
        """获取设备基本信息"""
        try:
            info = self.camera.devicemgmt.GetDeviceInformation()
            
            device_info = {
                '制造商': info.Manufacturer,
                '型号': info.Model,
   
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值