ONVIFカメラの見つけ方 by Python

ハードウェア

Python が動けばなんでも。
手元では「Raspberry Pi 4B」と「Termux でサーバー化したかなり古いスマホ:Aquos R3」で試してみたところ、ONVIF カメラの検索や PTZ 操作はどちらも特に問題なし。
※Termuxなスマホの場合はプレビュー映像に難あり

必要なもののインストール

Pythonの仮想環境の用意も含めて行おう。

$ python -m venv [venvDirName]
$ cd [venvDirName]
$ source ./bin/activate
(venvDirName)$ ./bin/pip install WSDiscovery
(venvDirName)$ ./bin/pip install onvif-zeep

デバイスの探索

ちょっと適当だけど、ファイル名は wsdicsovery.py みたいにしてみる
シンプルに onvif デバイスのみに絞り込み、そのデバイスの IP アドレスを取得する。

一応、こういうのは Class にしておいて、あとで融通効くようにしておいた方がいいよね。

# wsdicsovery.py

from wsdiscovery.discovery import ThreadedWSDiscovery as WSDiscovery
from wsdiscovery.scope import Scope
from urllib.parse import urlparse
import ipaddress


class OnvifDiscovery:

    def __init__(self):
        self.ip_list = []

    def search(self):
        # WS-Discoveryの初期化と開始
        wsd = WSDiscovery()
        wsd.start()

        # デバイス探索(対象はONVIFデバイス)
        services = wsd.searchServices(timeout=3)

        # 検出されたデバイスのアドレスを取得
        for service in services:
            scopes = [str(s) for s in service.getScopes()]
            scopes_str = ",".join(scopes)
            if 'onvif://www.onvif.org/' in scopes_str:
                for xaddr in service.getXAddrs():
                    parsed = urlparse(xaddr)
                    host = parsed.hostname
                    if not host:
                        continue

                    try:
                        ip = ipaddress.ip_address(host)
                    except ValueError:
                        continue

                    if ip.version == 4:
                        self.ip_list.append(ip)

        wsd.stop()


if __name__ == "__main__":
    od = OnvifDiscovery()
    od.search()

    for ip in od.ip_list:
        print(ip)

このコードを実行すると、同じネットワークにある ONVIF 対応デバイスの IP アドレスがずらずらと表示される。

(venvDirName)$ ./bin/python wsdiscovery.py

IPアドレスからカメラ情報を取得する

こっちも Class にしておいた方が後々便利よね~

# onvif_camera.py

import sys
from onvif import ONVIFCamera


class CameraItem:

    def __init__(self):
        self.device = None
        self.media_service = None
        self.ptz_service = None
        self.imaging_service = None
        self.events_service = None
        self.profile = None
        self.token = None


    def connect(self, ip_address, lid, lpw, port="80"):

        self.device = ONVIFCamera(ip_address, port, lid, lpw)

        # media_service は ONVIF 対応カメラには必ずある
        # 映像を扱うのに media_service がないと始まらないので
        self.media_service = self.device.create_media_service()
        self.profile = self.media_service.GetProfiles()[0]
        self.token = self.profile.token

        # しかし、ptz_Service はないかもしれないし他のサービスも同様にないかも…
        try:
            self.ptz_service = self.device.create_ptz_service()
        except Exception as e:
            print(f"このカメラは PtzService 非対応: {e}")

        try:
            self.imaging_service = self.device.create_imaging_service()
        except Exception as e:
            print(f"このカメラは ImagingService 非対応: {e}")

        try:
            self.events_service = self.device.create_events_service()
        except Exception as e:
            print(f"このカメラは EventsService 非対応: {e}")



if __name__ == "__main__":
    args = sys.argv
    if len(args) < 4:
        print("usage: onvif_camera.py [ip address] [id] [pass]")
    else:
        cam = CameraItem()
        cam.connect(args[1], args[2], args[3])

        print(cam.device.devicemgmt.GetDeviceInformation())
        print("--------------------------------------------")
        print(cam.device.devicemgmt.GetCapabilities({'Category': 'All'}))

てことで、以下のようにして実行すると、いろいろ情報が取れる。
IP アドレスは wsdicsovery.py で探してきた IP で試すといいよ。

(venvDirName)$ ./bin/python onvif_camera.py [ip address] [id] [pw]

Pan Tilt させてみる

上で作ったコードも利用して Pan/Tilt させてみるけど、これだと毎回カメラにログインしに行ってるのでめっちゃ遅い。遅いけど、とりあえず動作確認にはこれでいいかと。

ファイル名は onvif_ptz.py としてみた。

# onvif_ptz.py

import sys
from onvif import ONVIFCamera
from onvif_camera import CameraItem


class CtrlPtz:

    def __init__(self, ptz_service, token):
        self.ptz_service = ptz_service
        self.token = token

    def up(self):
        self.pan_tilt(0, 0.1)

    def down(self):
        self.pan_tilt(0, -0.1)

    def right(self):
        self.pan_tilt(0.1, 0)

    def left(self):
        self.pan_tilt(-0.1, 0)

    def pan_tilt(self, pan_dir, tilt_dir):
        request = self.ptz_service.create_type('ContinuousMove')
        request.ProfileToken = self.token
        request.Velocity = {'PanTilt': {'x': pan_dir, 'y': tilt_dir }}
        self.ptz_service.ContinuousMove(request)

    def pan_tilt_stop(self):
        request = self.ptz_service.create_type('Stop')
        request.ProfileToken = self.token
        request.PanTilt = True
        request.Zoom = False
        self.ptz_service.Stop(request)

    def zoom_in(self):
        self.zoom(1)

    def zoom_out(self):
        self.zoom(-1)

    def zoom(self, direction):
        request = self.ptz_service.create_type('ContinuousMove')
        request.ProfileToken = self.token
        request.Velocity = {'Zoom': {'x': direction}}
        self.ptz_service.ContinuousMove(request)

    def zoom_stop(self):
        request = self.ptz_service.create_type('Stop')
        request.ProfileToken = self.token
        request.PanTilt = False
        request.Zoom = True
        self.ptz_service.Stop(request)


if __name__ == "__main__":
    args = sys.argv
    if len(args) < 5:
        print("usage: onvif_ptz.py [ip address] [id] [pass] [ptz command]")
    else:
        cam = CameraItem()
        cam.connect(args[1], args[2], args[3])


        ptz = CtrlPtz(cam.ptz_service, cam.token)

        match args[4]:
            case "up":
                ptz.up()
            case "down":
                ptz.down()
            case "right":
                ptz.right()
            case "left":
                ptz.left()
            case "ptzStop":
                ptz.pan_tilt_stop()
            case "zoomIn":
                ptz.zoom_in()
            case "zoomOut":
                ptz.zoom_out()
            case "zoomStop":
                ptz.zoom_stop()

使い方は以下のように。

(venvDirName)$ ./bin/python onvif_ptz.py [ip address] [id] [pw] [ptz command]

[ptz command]はコードの通りで

 ”up”
 ”down”
 ”right”
 ”left”
 ”ptzStop”
 ”zoomIn”
 ”zoomOut”
 ”zoomStop”

の8つ。

コマンドラインでの実行は動作確認くらいにしか使えないけどね

ほんと、上記コードはどうすれば動くかが分かるだけで実用には耐えない。

そもそも、PTZ操作はコマンドラインで実行するようなもんじゃないからね。

けれど、基本が分かれば作り込んでいくこともできる。

実際に

・MediaMtx で カメラの映像を取得
・MediaMtx からその映像をブラウザ上に表示
・PTZ 操作を行えるよう WebUI を作る
・WebUI は PTZ だけじゃなく Preset や AUX もボタン等で自由に配置

なんてことをやると、メーカーが異なるカメラでも ONVIF 準拠であれば自分の使いやすい配置にした UI で操作できるし、ログインの自動化、巡回、Pan/Tiltの速度をカメラ毎に設定、などなど、本当に自由にいくらでも整えていくことができる。

確かにメーカーの WebUI はいろいろできて便利だし設定変更なんかの時には絶対に必要なんだけど、重いとかメーカーによって操作感が違うとか、いろんな差異があるから普段使い用には自分で作ってしまった方が割と便利。

しかも、WebRTC だと超低遅延。(メーカーの WebUI 上のプレビューより速いこともままあるw)
実際には0.15~0.20秒くらいの遅延はあるけど、ほぼ感覚的にはリニア。(鋭い人にはわかっちゃうけど…)
直感的(自分専用だから当然w)でレスポンスも良く、これはちょっと拡張していきたいと思える。

この記事にコメントしてみる