ハードウェア
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)でレスポンスも良く、これはちょっと拡張していきたいと思える。

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