📹
監視カメラビューアをPySimpleGuiでつくる
ネットワークWi-Fiカメラ - Tapo C200を購入しました。
ビューアアプリを作ったので、紹介します。
💡作成するアプリ
以下が概要です。
- 接続情報を入力する
- "Connect"を押すと、接続情報に従い接続
- カメラ画面が表示される
- 矢印ボタンでパンチルトを操作
- "Disconnect"を押すと、切断する
⚙事前準備
アカウントの設定
購入したカメラは、↓となります。
本カメラは、スマホの専用アプリより映像を見たり、パンチルトの操作ができます。
カメラの映像配信には、ユーザ名とパスワードが必要になります。
ユーザ名とパスワードのアカウント設定は、下記の公式サイトのFAQの
ステップ1: Tapoアプリでアカウントを作成
を参照願います。
💻環境
開発環境
- windows10 64bit Home
- Python 3.8.10
- Python 3.10.2
Pythonモジュール
Pythonに依存するモジュールは以下となります。
> python -m venv env
(env) > pip install rtsp
(env) > pip install pysimplegui
(env) > pip install --upgrade onvif_zeep
📝手順
- カメラ映像を受信する
- パンチルト対応を行う
- パンチルト対応を行う(AbsoluteMove)
カメラ映像を受信する
RTSPライブストリームURL
TapoカメラのRTSPライブストリームURLは以下の様になっています。
- 高画質な1080P (1920*1080) ストリームの場合
- rtsp://username:password@IP Address:554/stream1
- 低画質な360P (640*360) ストリームの場合
- rtsp://username:password@IP Address:554/stream2
コード
コードは以下となります。
view.py
import PySimpleGUI as sg
import rtsp
from PIL import Image, ImageTk
sg.theme('Dark Brown')
USER = "username" # <=自分の環境に合わせる
PASS = "password" # <=自分の環境に合わせる
IPADDR = "192.168.11.xx" # <=自分の環境に合わせる
PORT = "554"
STREAM = "stream2"
ONVIF_PORT = "2020"
layout = [
# カメラ接続情報
[ sg.Text('IPADDR: ', size=(12, 1)), sg.InputText(default_text=IPADDR, size=(20, 1),key='-ipaddr-'),
sg.Text('PORT: ', size=(12, 1)), sg.InputText(default_text=PORT, size=(20, 1),key='-port-'),
sg.Text('STREAM: ', size=(12, 1)), sg.InputText(default_text=STREAM, size=(20, 1),key='-stream-')],
[ sg.Text('ONVIF_PORT: ', size=(12, 1)), sg.InputText(default_text=ONVIF_PORT, size=(20, 1),key='-onvif_port-')],
[ sg.Text('USER: ', size=(12, 1)), sg.InputText(default_text=USER, size=(20, 1),key='-user-'),
sg.Text('PASS: ', size=(12, 1)), sg.InputText(default_text=PASS, size=(20, 1),key='-pass-')],
# 画面表示
[ sg.Image(filename='', key='image')],
# 接続, 切断
[ sg.Button('Connect', size=(10, 1), key ='-start-'),
sg.Button('Disconnect', size=(10, 1), key = '-stop-')],
]
is_streaming = False
window = sg.Window('cam viewer', layout, location=(32, 32))
while True:
event, values = window.read(timeout=20)
if event in (None, '-exit-'):
break
elif event == '-start-':
rtsp_url = f"rtsp://{values['-user-']}:{values['-pass-']}@{values['-ipaddr-']}:{values['-port-']}/{values['-stream-']}"
client = rtsp.Client(rtsp_server_uri=rtsp_url, verbose=True)
is_streaming = True
elif event == '-stop-':
is_streaming = False
client.close()
img = Image.new("RGB", (640, 360), color=0)
window['image'].update(data=ImageTk.PhotoImage(img))
if is_streaming:
frame = client.read()
if frame is not None:
window['image'].update(data=ImageTk.PhotoImage(frame))
else:
print("none")
window.close()
実行手順
(env) > python view.py
"Connect"ボタンで映像配信ができます。
パンチルト対応を行う
パンチルト操作
パンチルトは、ONVIF - PTZサービスを利用します。
Pythonモジュールのonvif_zeepを使用します。
コード
UIの部分とカメラ部分をわけて記載します。
- UI部分
ptz_viewer.py
import PySimpleGUI as sg
from camera import CamPtz
from PIL import Image, ImageTk
sg.theme('Dark Brown')
USER = "username" # <=自分の環境に合わせる
PASS = "password" # <=自分の環境に合わせる
IPADDR = "192.168.11.xx" # <=自分の環境に合わせる
PORT = "554"
STREAM = "stream2"
ONVIF_PORT = "2020"
layout = [
# カメラ接続情報
[ sg.Text('IPADDR: ', size=(12, 1)), sg.InputText(default_text=IPADDR, size=(20, 1),key='-ipaddr-'),
sg.Text('PORT: ', size=(12, 1)), sg.InputText(default_text=PORT, size=(20, 1),key='-port-'),
sg.Text('STREAM: ', size=(12, 1)), sg.InputText(default_text=STREAM, size=(20, 1),key='-stream-')],
[ sg.Text('ONVIF_PORT: ', size=(12, 1)), sg.InputText(default_text=ONVIF_PORT, size=(20, 1),key='-onvif_port-')],
[ sg.Text('USER: ', size=(12, 1)), sg.InputText(default_text=USER, size=(20, 1),key='-user-'),
sg.Text('PASS: ', size=(12, 1)), sg.InputText(default_text=PASS, size=(20, 1),key='-pass-')],
# 画面表示
[ sg.Image(filename='', key='image')],
# 接続, 切断
[ sg.Button('Connect', size=(10, 1), key ='-start-'),
sg.Button('Disconnect', size=(10, 1), key = '-stop-')],
# パンチルト制御
[ sg.Button('↖', size=(4, 1), font='Helvetica 14',key ='-pt_topleft-'),
sg.Button('↑', size=(4, 1), font='Helvetica 14',key = '-pt_topcenter-'),
sg.Button('↗', size=(4, 1), font='Helvetica 14', key='-pt_topright-'), ],
[ sg.Button('←', size=(4, 1), font='Helvetica 14',key ='-pt_left-'),
sg.Button('〇', size=(4, 1), font='Helvetica 14',key = '-pt_center-'),
sg.Button('→', size=(4, 1), font='Helvetica 14', key='-pt_right-'), ],
[ sg.Button('↙', size=(4, 1), font='Helvetica 14',key ='-pt_btmleft-'),
sg.Button('↓', size=(4, 1), font='Helvetica 14',key = '-pt_btmcenter-'),
sg.Button('↘', size=(4, 1), font='Helvetica 14', key='-pt_btmright-'), ],
]
is_streaming = False
window = sg.Window('cam viewer', layout, location=(32, 32))
while True:
event, values = window.read(timeout=20)
if event in (None, '-exit-'):
break
elif event == '-start-':
cam_ptz = CamPtz(
user = values['-user-'],
pwd = values['-pass-'],
ipaddr = values['-ipaddr-'],
port = values['-port-'],
stream = values['-stream-'],
onvif_port = values['-onvif_port-']
)
# streaming
cam_ptz.open()
# onvif - move
cam_ptz.setup_ptz()
is_streaming = True
elif event == '-stop-':
is_streaming = False
cam_ptz.close()
img = Image.new("RGB", (640, 360), color=0)
window['image'].update(data=ImageTk.PhotoImage(img))
elif "-pt_" in event:
x, y = 0, 0
x = 1 if "right" in event else x
x = -1 if "left" in event else x
y = 1 if "top" in event else y
y = -1 if "btm" in event else y
print(event, "x:", x, "y:", y)
if is_streaming:
cam_ptz.move(x, y)
if is_streaming:
frame = cam_ptz.read()
if frame is not None:
window['image'].update(data=ImageTk.PhotoImage(frame))
window.close()
- カメラ部分
camera.py
import rtsp
from onvif import ONVIFCamera
import time
class CamStream(object):
def __init__(self, **kwargs):
self.user = kwargs["user"]
self.pwd = kwargs["pwd"]
self.ipaddr = kwargs["ipaddr"]
self.port = kwargs["port"]
self.stream = kwargs["stream"]
self.onvif_port = kwargs["onvif_port"]
self.is_connect = False
def open(self):
rtsp_url = f"rtsp://{self.user}:{self.pwd}@{self.ipaddr}:{self.port}/{self.stream}"
self.client = rtsp.Client(rtsp_server_uri=rtsp_url, verbose=True)
self.is_connect = True
def read(self):
if self.is_connect is False:
return None
return self.client.read()
def close(self):
if self.is_connect:
self.client.close()
self.is_connect = False
return
class CamPtz(CamStream):
def __init__(self, **kwargs):
super().__init__(**kwargs)
pass
self.XMAX = 1
self.XMIN = -1
self.YMAX = 1
self.YMIN = -1
self.moverequest = None
self.ptz = None
self.is_ptz_active = False
def setup_ptz(self):
mycam = ONVIFCamera(self.ipaddr, self.onvif_port, self.user, self.pwd)
# Create media service object
media = mycam.create_media_service()
# Create ptz service object
self.ptz = mycam.create_ptz_service()
# Get target profile
media_profile = media.GetProfiles()[0]
# Get PTZ configuration options for getting continuous move range
request = self.ptz.create_type('GetConfigurationOptions')
request.ConfigurationToken = media_profile.PTZConfiguration.token
ptz_configuration_options = self.ptz.GetConfigurationOptions(request)
self.moverequest = self.ptz.create_type('ContinuousMove')
self.moverequest.ProfileToken = media_profile.token
if self.moverequest.Velocity is None:
self.moverequest.Velocity = self.ptz.GetStatus({'ProfileToken': media_profile.token}).Position
# Get range of pan and tilt
# NOTE: X and Y are velocity vector
self.XMAX = ptz_configuration_options.Spaces.ContinuousPanTiltVelocitySpace[0].XRange.Max
self.XMIN = ptz_configuration_options.Spaces.ContinuousPanTiltVelocitySpace[0].XRange.Min
self.YMAX = ptz_configuration_options.Spaces.ContinuousPanTiltVelocitySpace[0].YRange.Max
self.YMIN = ptz_configuration_options.Spaces.ContinuousPanTiltVelocitySpace[0].YRange.Min
return
def move(self, x, y):
self.moverequest.Velocity.PanTilt.x = x
self.moverequest.Velocity.PanTilt.y = y
if self.is_ptz_active:
self.ptz.Stop({'ProfileToken': self.moverequest.ProfileToken})
active = True
self.ptz.ContinuousMove(self.moverequest)
実行手順
(env) > python ptz_viewer.py
"Connect"ボタンで映像配信ができます。
矢印ボタンでパンチルト操作ができます。
パンチルト対応を行う(AbsoluteMove)
コメントで教えていただいた方法について記載します。
パンチルト操作
パンチルトは、ONVIF - AbsoluteMoveを使ってパンチルトを行います。
コード
UIの部分とカメラ部分をわけて記載します。
- UI部分
ptz_viewer.py
import PySimpleGUI as sg
import rtsp
from PIL import Image, ImageTk
from onvifreq import OnvifRequest
import urllib.request
sg.theme('Dark Brown')
# 接続情報
USER = "username" # <=自分の環境に合わせる
PASS = "password" # <=自分の環境に合わせる
IPADDR = "192.168.11.xx" # <=自分の環境に合わせる
PORT = "554"
STREAM = "stream2"
ONVIF_PORT = "2020"
# パンチルトパラメータ
X_MIN, X_MAX, X_STP = -170, 170, 10
Y_MIN, Y_MAX, Y_STP = -30, 30, 5
cur_x = 0
cur_y = 0
inited_ptz = False
layout = [
# カメラ接続情報
[ sg.Text('IPADDR: ', size=(12, 1)), sg.InputText(default_text=IPADDR, size=(20, 1),key='-ipaddr-'),
sg.Text('PORT: ', size=(12, 1)), sg.InputText(default_text=PORT, size=(20, 1),key='-port-'),
sg.Text('STREAM: ', size=(12, 1)), sg.InputText(default_text=STREAM, size=(20, 1),key='-stream-')],
[ sg.Text('ONVIF_PORT: ', size=(12, 1)), sg.InputText(default_text=ONVIF_PORT, size=(20, 1),key='-onvif_port-')],
[ sg.Text('USER: ', size=(12, 1)), sg.InputText(default_text=USER, size=(20, 1),key='-user-'),
sg.Text('PASS: ', size=(12, 1)), sg.InputText(default_text=PASS, size=(20, 1),key='-pass-')],
# 画面表示
[ sg.Image(filename='', key='image')],
# 接続, 切断
[ sg.Button('Connect', size=(10, 1), key ='-start-'),
sg.Button('Disconnect', size=(10, 1), key = '-stop-')],
# パンチルト制御
[ sg.Button('↖', size=(4, 1), font='Helvetica 14',key ='-pt_topleft-'),
sg.Button('↑', size=(4, 1), font='Helvetica 14',key = '-pt_topcenter-'),
sg.Button('↗', size=(4, 1), font='Helvetica 14', key='-pt_topright-'), ],
[ sg.Button('←', size=(4, 1), font='Helvetica 14',key ='-pt_left-'),
sg.Button('〇', size=(4, 1), font='Helvetica 14',key = '-pt_center-'),
sg.Button('→', size=(4, 1), font='Helvetica 14', key='-pt_right-'), ],
[ sg.Button('↙', size=(4, 1), font='Helvetica 14',key ='-pt_btmleft-'),
sg.Button('↓', size=(4, 1), font='Helvetica 14',key = '-pt_btmcenter-'),
sg.Button('↘', size=(4, 1), font='Helvetica 14', key='-pt_btmright-'), ],
]
is_streaming = False
window = sg.Window('cam viewer', layout, location=(32, 32))
while True:
event, values = window.read(timeout=20)
if event in (None, '-exit-'):
break
elif event == '-start-':
rtsp_url = f"rtsp://{values['-user-']}:{values['-pass-']}@{values['-ipaddr-']}:{values['-port-']}/{values['-stream-']}"
client = rtsp.Client(rtsp_server_uri=rtsp_url, verbose=True)
onvif_request = OnvifRequest(username=values['-user-'], password=values['-pass-'])
url = f"http://{values['-ipaddr-']}:{values['-onvif_port-']}/"
headers = {'Content-Type': 'text/xml; charset=utf-8'}
is_streaming = True
inited_ptz = True
elif event == '-stop-':
is_streaming = False
img = Image.new("RGB", (640, 360), color=0)
window['image'].update(data=ImageTk.PhotoImage(img))
elif "-pt_" in event or inited_ptz:
x, y = cur_x, cur_y
x = (x + X_STP) if "right" in event else x
x = (x - X_STP) if "left" in event else x
y = (y + Y_STP) if "top" in event else y
y = (y - Y_STP) if "btm" in event else y
x = X_MAX if x > X_MAX else x
cur_x = X_MIN if x < X_MIN else x
y = Y_MAX if y > Y_MAX else y
cur_y = Y_MIN if y < Y_MIN else y
print(event, "x:", cur_x, "y:", cur_y)
if is_streaming:
oreq = onvif_request.absolute_move(cur_x, cur_y)
req = urllib.request.Request(url, data=oreq.encode(), method='POST', headers=headers)
try:
with urllib.request.urlopen(req) as response:
pass
# obody = response.read()
# oheaders = response.getheaders()
# ostatus = response.getcode()
# print(oheaders)
# print(ostatus)
# print(obody)
except urllib.error.URLError as e:
print(e.reason)
inited_ptz = False
if is_streaming:
frame = client.read()
if frame is not None:
window['image'].update(data=ImageTk.PhotoImage(frame))
else:
print("none")
window.close()
- パンチルト部分
onvifreq.py
import hashlib
import os
import base64
from datetime import datetime
class OnvifRequest:
def __init__(self, username, password):
self.username = username
self.password = password
def absolute_move(self, x, y):
command = """
<AbsoluteMove xmlns="http://www.onvif.org/ver20/ptz/wsdl">
<ProfileToken>profile_1</ProfileToken>
<Position>
<PanTilt x="{x}" y="{y}" xmlns="http://www.onvif.org/ver10/schema" space="http://www.onvif.org/ver10/tptz/PanTiltSpaces/PositionGenericSpace"></PanTilt>
<Zoom x="0.0" xmlns="http://www.onvif.org/ver10/schema" space="http://www.onvif.org/ver10/tptz/ZoomSpaces/PositionGenericSpace"></Zoom>
</Position>
<Speed>
<PanTilt x="1" y="1" xmlns="http://www.onvif.org/ver10/schema" space="http://www.onvif.org/ver10/tptz/PanTiltSpaces/GenericSpeedSpace"/>
<Zoom x="1.0" xmlns="http://www.onvif.org/ver10/schema" space="http://www.onvif.org/ver10/tptz/ZoomSpaces/ZoomGenericSpeedSpace"/>
</Speed>
</AbsoluteMove>
"""
return self.request(self.username, self.password, command.format(x=x, y=y))
def request(self, username, password, command):
created = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.000Z")
raw_nonce = os.urandom(20)
nonce = base64.b64encode(raw_nonce)
sha1 = hashlib.sha1()
sha1.update(raw_nonce + created.encode("utf8") + password.encode("utf8"))
raw_digest = sha1.digest()
digest = base64.b64encode(raw_digest)
request = """
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope">
<s:Header>
<Security xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" s:mustUnderstand="1">
<UsernameToken>
<Username>{username}</Username>
<Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">{digest}</Password>
<Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">{nonce}</Nonce>
<Created xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">{created}</Created>
</UsernameToken>
</Security>
</s:Header>
<s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
{command}
</s:Body>
</s:Envelope>
"""
return request.format(
username=username,
nonce=nonce.decode("utf8"),
created=created,
digest=digest.decode("utf8"),
command=command,
)
実行手順
(env) > python ptz_viewer.py
"Connect"ボタンで映像配信ができます。
矢印ボタンでパンチルト操作ができます。
さいごに
カメラの映像の取得やパンチルトができるといろいろできそうです。
Discussion
kotaprojさん、こんばんわ!
この記事、すごく参考になりました。ありがとうございました。
ただ、記事の手順どおりに進めると、以下のエラーで手詰まりましたw
zeepかどこかのバグですかねぇ?
何か対処方法をご存じだったら、教えていただけるとありがたいです。。。
Connected to video source rtsp://user:pass@192.168.179.237:554/stream2.
Traceback (most recent call last):
File "C:\Users\futopparagg\env\lib\site-packages\onvif\client.py", line 23, in wrapped
return func(*args, **kwargs)
File "C:\Users\futopparagg\env\lib\site-packages\onvif\client.py", line 153, in wrapped
return call(params, callback)
File "C:\Users\futopparagg\env\lib\site-packages\onvif\client.py", line 140, in call
ret = func(**params)
File "C:\Users\futopparagg\env\lib\site-packages\zeep\proxy.py", line 46, in call
return self._proxy._binding.send(
File "C:\Users\futopparagg\env\lib\site-packages\zeep\wsdl\bindings\soap.py", line 135, in send
return self.process_reply(client, operation_obj, response)
File "C:\Users\futopparagg\env\lib\site-packages\zeep\wsdl\bindings\soap.py", line 229, in process_reply
return self.process_error(doc, operation)
File "C:\Users\futopparagg\env\lib\site-packages\zeep\wsdl\bindings\soap.py", line 391, in process_error
raise Fault(
zeep.exceptions.Fault
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "C:\Users\futopparagg\ptz_viewer.py", line 62, in <module>
cam_ptz.setup_ptz()
File "C:\Users\futopparagg\camera.py", line 65, in setup_ptz
self.moverequest.Velocity = self.ptz.GetStatus({'ProfileToken': media_profile.token}).Position
File "C:\Users\futopparagg\venv\lib\site-packages\onvif\client.py", line 26, in wrapped
raise ONVIFError(err)
onvif.exceptions.ONVIFError: Unknown error:
私の方で、さきほど手順を最初から確認してみましたが、問題なく実施することができました。
機材 or 各モジュールのバージョンの差異なのかもしれません。
特に、機材が異なる場合、onvifといっても方言があるので、その辺のエラーかもです。
参考になるかわかりませんが、細かく条件を記載しました。
機材
環境
kotoprojさん、こんばんわ!
早速返信いただいたのに、応答できず、恐縮です。。。
検証して情報までご提示いただけて、本当にありがとうございます。
いま、細かい確認ができない場所にいるので申し訳ないですが、いただいた情報と突き合わせしてみます。
ちなみに、当方のカメラは、Tapo C210/Aなんですが、ONVIF Device ManagerからだとちゃんとPTが制御できたので、カメラ側の機種の問題ではないかも?とは思っていますが、1つ1つ切り分けてみますね。
まずは、御礼まで。
kotoprojさん、pip以外のバージョンを合わせて実行してみましたが、やはりconnect時に同じエラーでプログラムが停止してしまいました。時間が作れたら、エラーのソースを追いかけてみます。
以下、ご参考まで。
・機材
・Tapo C210/A(Tapoアプリの表示では、型番:C210)
・ハードウェアバージョン:1.0
・ファームウェアバージョン:1.1.12 Build 211028 Rel.19131n(4A50)
→Tapoアプリでアップデートの確認を押しても「最新のファームウェア」と表示されます。
・環境
・Python 3.10.2(python -Vの結果)
・各モジュールのバージョン↓
attrs 21.4.0
cached-property 1.5.2
certifi 2021.10.8
charset-normalizer 2.0.11
idna 3.3
isodate 0.6.1
lxml 4.7.1
MouseInfo 0.1.3
numpy 1.22.2
onvif-zeep 0.2.12
opencv-python 4.5.5.62
Pillow 9.0.1
pip 22.0.3
platformdirs 2.4.1
PyAutoGUI 0.9.53
PyGetWindow 0.0.9
PyMsgBox 1.0.9
pyperclip 1.8.2
PyRect 0.1.4
PyScreeze 0.1.28
PySimpleGUI 4.56.0
pytweening 1.0.4
pytz 2021.3
requests 2.27.1
requests-file 1.5.1
requests-toolbelt 0.9.1
rtsp 1.1.12
setuptools 56.0.0
six 1.16.0
urllib3 1.26.8
zeep 4.1.0
参考まで。
Python3.10.2だったため、こちらの環境でも3.10.2で実施しました。
→問題なく動作することができました。
C200とC210Aの差かもしれませんね。
エラーを見ると、onvif clientのunkownになっているため、
media_profileのところに差異があるのかもしれません。
こちらの環境で、camera.pyのsetup_ptz()にprint文を追加しました。
正常時(C200)の結果が以下です。
'PTZConfiguration'あたりに差異があるのかもです。
あと、onvifのポートが2020でない場合、同様のエラーになる可能性があります。
kotaprojさん、こんばんわ!
返信&情報提供いただいてありがとうございます。
また、お返事に時間がかかってしまって大変恐縮です。
こちらも切り分けに進展がありました。
kotaprojさんに調べてばかりいただいて申し訳なかったので、なんとかC200を入手して、同じプログラムを実行したところ、こちらの環境でも例外が発生せずに接続できて、パン・チルトまで動作することが確認できました。
やはり、ご指摘いただいたように、C200とC210の何らかの差異による例外のようです。
画質向上して上位互換だろうと勝手に思い込んでいたのが裏目にでました。。。
ともあれ両方のハードがあるので、kotaprojさんの情報も参考にして、少し実験を進めてみます。
もし何かしら、差異がわかったら、共有させていただきますね。
本当にいろいろとありがとうございました。(- -)(_ _)ペコリ
kotaprojさん、参考までにC210でmedia_profileをprintした結果を共有させていただきます。
最後のほう(ZoomLimitsの直下のExtension)に単純でない差分が少し見受けられましたが、理由については調査できていません。
media_profile: {
'Name': 'mainStream',
'VideoSourceConfiguration': {
'Name': 'VideoSourceConfig',
'UseCount': 2,
'SourceToken': 'raw_vs1',
'Bounds': {
'x': 0,
'y': 0,
'width': 2304,
'height': 1296
},
'_value_1': None,
'Extension': None,
'token': 'vsconf',
'_attr_1': {
}
},
'AudioSourceConfiguration': {
'Name': 'AudioSourceConfig',
'UseCount': 2,
'SourceToken': 'raw_as1',
'_value_1': None,
'token': 'asconf',
'_attr_1': {
}
},
'VideoEncoderConfiguration': {
'Name': 'VideoEncoder_1',
'UseCount': 1,
'Encoding': 'H264',
'Resolution': {
'Width': 1280,
'Height': 720
},
'Quality': 5.0,
'RateControl': {
'FrameRateLimit': 15,
'EncodingInterval': 1,
'BitrateLimit': 2048
},
'MPEG4': None,
'H264': {
'GovLength': 25,
'H264Profile': 'Main'
},
'Multicast': {
'Address': {
'Type': 'IPv4',
'IPv4Address': '0.0.0.0',
'IPv6Address': None
},
'Port': 0,
'TTL': 0,
'AutoStart': False,
'_value_1': None,
'_attr_1': None
},
'SessionTimeout': datetime.timedelta(seconds=65),
'_value_1': None,
'token': 'main',
'_attr_1': {
}
},
'AudioEncoderConfiguration': {
'Name': 'AudioEncoder_1',
'UseCount': 2,
'Encoding': 'G711',
'Bitrate': 128000,
'SampleRate': 8000,
'Multicast': {
'Address': {
'Type': 'IPv4',
'IPv4Address': '0.0.0.0',
'IPv6Address': None
},
'Port': 0,
'TTL': 0,
'AutoStart': False,
'_value_1': None,
'_attr_1': None
},
'SessionTimeout': datetime.timedelta(seconds=65),
'_value_1': None,
'token': 'microphone',
'_attr_1': {
}
},
'VideoAnalyticsConfiguration': {
'Name': 'VideoAnalyticsName',
'UseCount': 2,
'AnalyticsEngineConfiguration': {
'AnalyticsModule': [
{
'Parameters': {
'SimpleItem': [
{
'Name': 'Sensitivity',
'Value': 'medium'
},
{
'Name': 'Enabled',
'Value': 'on'
}
],
'ElementItem': [
{
'_value_1': <Element {http://www.onvif.org/ver10/schema}CellLayout at 0x1fe51b358c0>,
'Name': 'Layout'
}
],
'Extension': None,
'_attr_1': None
},
'Name': 'MyCellMotionModule',
'Type': 'tt:CellMotionEngine'
},
{
'Parameters': {
'SimpleItem': [
{
'Name': 'Sensitivity',
'Value': 'medium'
},
{
'Name': 'Enabled',
'Value': 'off'
}
],
'ElementItem': [],
'Extension': None,
'_attr_1': None
},
'Name': 'MyTamperDetecModule',
'Type': 'tt:TamperEngine'
}
],
'Extension': None,
'_attr_1': None
},
'RuleEngineConfiguration': {
'Rule': [
{
'Parameters': {
'SimpleItem': [
{
'Name': 'ActiveCells',
'Value': '0P8A8A=='
},
{
'Name': 'MinCount',
'Value': '5'
},
{
'Name': 'AlarmOnDelay',
'Value': '1000'
},
{
'Name': 'AlarmOffDelay',
'Value': '1000'
}
],
'ElementItem': [],
'Extension': None,
'_attr_1': None
},
'Name': 'MyMotionDetectorRule',
'Type': 'tt:CellMotionDetector'
},
{
'Parameters': None,
'Name': 'MyTamperDetectorRule',
'Type': 'tt:TamperDetector'
}
],
'Extension': None,
'_attr_1': None
},
'_value_1': None,
'token': 'VideoAnalyticsToken',
'_attr_1': {
}
},
'PTZConfiguration': {
'Name': 'PTZ',
'UseCount': 2,
'NodeToken': 'PTZNODETOKEN',
'DefaultAbsolutePantTiltPositionSpace': 'http://www.onvif.org/ver10/tptz/PanTiltSpaces/PositionGenericSpace',
'DefaultAbsoluteZoomPositionSpace': None,
'DefaultRelativePanTiltTranslationSpace': 'http://www.onvif.org/ver10/tptz/PanTiltSpaces/TranslationGenericSpace',
'DefaultRelativeZoomTranslationSpace': None,
'DefaultContinuousPanTiltVelocitySpace': 'http://www.onvif.org/ver10/tptz/PanTiltSpaces/VelocityGenericSpace',
'DefaultContinuousZoomVelocitySpace': None,
'DefaultPTZSpeed': {
'PanTilt': {
'x': 0.349999994,
'y': 0.349999994,
'space': 'http://www.onvif.org/ver10/tptz/PanTiltSpaces/GenericSpeedSpace'
},
'Zoom': None
},
'DefaultPTZTimeout': datetime.timedelta(seconds=180),
'PanTiltLimits': {
'Range': {
'URI': 'http://www.onvif.org/ver10/tptz/PanTiltSpaces/PositionGenericSpace',
'XRange': {
'Min': -1.0,
'Max': 1.0
},
'YRange': {
'Min': -1.0,
'Max': 1.0
}
}
},
'ZoomLimits': None,
'Extension': {
'_value_1': [
<Element {http://www.onvif.org/ver10/schema}PTControlDirection at 0x1fe519d7f40>
],
'PTControlDirection': None,
'Extension': None
},
'token': 'PTZTOKEN',
'_attr_1': {
}
},
'MetadataConfiguration': None,
'Extension': None,
'token': 'profile_1',
'fixed': True,
'_attr_1': {
}
}
onvif自体の規格をよくわかっていないのですが、エラーは↓で上がっているようなので、print文をみると何かわかるかもしれませんね。
kotaprojさん、こんばんわ!
だいぶ間が空いてしまい大変恐縮ですが、C210でパンチルトのエラーが発生する件について、以下、わかったことを共有します。
C210の場合、どうしてもonvifパッケージでうまく動かせなかったため、知り合いの方に相談して、以下のようなソースを書くことで、AbsoluteMoveであれば、動かせることがわかりました。
残念ながら、自分の理解が追い付いておらず、どうすれば、ContinuousMoveで動かせるかまでには、至っていません。
ポイントは、PositionとSpeedのところのPatilt内に、xmlnsプロパティを明記することだそうです。
この指定がないと、lang=en は知らんというようなエラーが発生して、失敗するようです。ここも知り合いの方の受け売りなので、自分でもよく咀嚼できておらず、恐縮です。
ちなみに、このソースであれば、C200もC210もパンチルトすることは可能でした。
OnvifRequest(username = 'xxxxx', password = 'xxxxx')のxxxxxの部分と
url = 'http://xxxxxxxxx:2020/'のxxxxxxxxxの部分と
absolute_move(0, 1)のx、y(0と1)の部分を
環境や任意の値に合わせてもらうと、動きます。
同じようにハマってしまった方の参考になれば幸いです。
#!/usr/bin/env python3
import hashlib
import os
import base64
from datetime import datetime
class OnvifRequest:
def init(self, username, password):
self.username = username
self.password = password
################################################################
import urllib.request
url = 'http://xxxxxxxxx:2020/'
headers = {'Content-Type': 'text/xml; charset=utf-8'}
onvif_request = OnvifRequest(username = 'xxxxx', password = 'xxxxx').absolute_move(0, 1)
req = urllib.request.Request(url, data=onvif_request.encode(), method='POST', headers=headers)
try:
with urllib.request.urlopen(req) as response:
body = response.read()
headers = response.getheaders()
status = response.getcode()
print(headers)
print(status)
print(body)
except urllib.error.URLError as e:
print(e.reason)
以上、取り急ぎ、お知らせまで。
AbsoluteMoveの説明、ありがとうございます。
自身での動作確認がてらに、記事を更新させていただきました。
こちらでも正しく制御できました。
詳しい知り合いの方と相談できるのが、うらやましいです。
初めまして。
こちらの記事を参考に、PythonでTapo C210のカメラビューアを開発しています。
とても有益な記事を公開していただき、まずはありがとうございました。
私も記事前半のContinuousMoveを使う方法では太っ腹じじぃさんと同じ症状が出ました。
Tapo C210
ハードウェアバージョン:2.0
ファームウェアバージョン:1.3.6 (2023/08/17時点の最新版)
環境
Windows11
Python 3.8.9(64bit)
onvif-zeep==0.2.12
また、AbsoluteMove版に変更しても、with urllib.request.urlopen(req) as response:部分で、
Bad Requestが返ってしまいます。
確認したところ、onvifreq.pyのabsolute_move()関数に渡すx, yの値が-1~1の間であれば動きました。
その範囲外になるとBad Requestを返すようです。
冒頭のパンチルトパラメータを以下のように書き換えるとPAN/TILTできました。
また、absolute_move()に渡す際に-を付けないと、上下左右が逆転しました。
さらに、RelativeMove、ContinuousMoveも試しました。
AbsoluteMove版のonvifreq.pyに下記関数を追加します。
relative_move()もx, yを-1~0の範囲で指定すると、良い感じに動きました。
こちらは、渡す際の-は不要でした。
relative_move()のx, yの値が範囲外(10や-5)などの場合、一度限界まで移動し、
さらに同じ方向に移動させようとするとBad Requestが返ります。
範囲外の数字は、Tapoアプリにある「垂直パトロール」「水平パトロール」の実現に使えるかもしれません。
私は猫を監視する目的でカメラを天井に設置しており、
Tapoアプリで「動作トラッキング」をONにしています。
猫を追ってカメラが動くので、RelativeMoveの方を採用しました。
その場合、X_MIN, X_MAX, Y_MIN, Y_MAXを使わずにPAN/TILT操作をして、
Bad Requestが返ったら「操作範囲外になったというメッセージを出力する」のがベストでした。
ContinuousMoveは私の状況では利用を想定しておらず、
relative_move()用のx, y値を渡してカメラが動く事だけテストしました。
こちらも、渡す際の-は不要でした。
absolute_move()に渡すx, y値に-が必要と書き込んでしまった件で訂正します。
私は、カメラを天井に逆さに取り付けているため、「画像を反転」をONにしています。
そのため-がないと、移動方向が逆になっていました。
absolute_move()にrelative_move()渡すx, yに-を付けるかは、
以下のようにするのが良いようです。
Tapoアプリで「画像を反転」をONにしている場合、
absolute_move()には-を付ける。
relative_move()には-を付けない。
continuous_move()には-を付けない。
Tapoアプリで「画像を反転」をOFFにしている場合、
absolute_move()には-を付けない。
relative_move()には-を付ける。
continuous_move()には-を付ける。
また、今回の件で私が調べたことなどをこちらにまとめました。
もし記事を修正される予定があれば、必要に応じて引用等してください。tapoC210は製品の仕様上、公式ではiOSもしくはアンドロイドスマフォのみ対応しており、パソコンモニター上で、なんとかライブストリーミングできないか、しかも、既製品に頼ることなくブラウザを自作できないかと思いネットサーフィンをしていたところ、この記事を見つけました。
真似をしてみたいのですが、そもそも前提知識や経験がなく、環境構築から作業が必要です。可能な範囲で謝礼をお支払いしますので、記事の内容のトレースできるところまで、サポートを頂けないか、ご検討をお願いしたいです。
記事をトレースするには、以下の手順になると思われます。
上記でわかるものはどれになりますか
お返事ありがとうございます。
ググりながら見様見真似で、tapoという名前の仮想環境venvの作成し、
サンプルコードの実行まで行いました。
実行したところ、
(tapo) C:\Users\user\tapo>python view.py
[ WARN:0@30.065] global cap_ffmpeg_impl.hpp:453 _opencv_ffmpeg_interrupt_callback Stream timeout triggered after 30059.696000 ms
Connected to video source rtsp://ユーザー名:パスワード@192.168.68.53:554/stream2.
none
none
none
none
none
none
none
none 以下、続く
というエラーが発生し、躓いているところです。
環境構築には問題ないように見えます
下記のいずれかだと思われます
カメラ側の設定 → "高度な設定" → "カメラのアカウント"から実施できます
また、実施後、再起動しないと反映されない現象に遭遇したことがあるので、
アプリから再起動させてみてください。
それで表示されるのではないでしょうか(Python3.12で問題ないことは確認済み)
もし表示されない場合は、vlc media playerを使って、
メニューバー "メディア" -> "ネットワークストリームを開く" にて、
で表示されるか確認してください
表示されないようであれば、やはり認証の問題と思われます。
vlc(サードパーティでの表示)は公式も対応しているとのことなので。
無事に
tapoC210 ハードウェアバージョン2.0 ファームウェアバージョン1.3.11
で上記のサンプルコードを動作することができました。
先日まで実家に帰省しており、スマフォのテザリングで実行していたので、ネットワーク環境(ポートとかですかね?よく分かりませんが)の問題で、カメラを見つけることができなかったようです。
無事にサンプルコードを動作できたので、これをいろいろカスタマイズしていきたいです。
まず、2つやりたいことがあります。
1つ目は、複数のカメラを同時にライブビューしたい。一つのアプリ画面内で複数表示してもよいし、アプリを複数同時起動する、どちらでも構いません。
2つ目は、パソコンでライブビューしている動画を、パソコンに保存する機能をつけたいです。こちらは、最悪、パソコン画面のスクリーンショットを定期的に撮影することでまずは疑似的に、パソコンへの録画でも良いです。
何かしら、アドバイスやご提案頂けないでしょうか。全部、無料で教えてほしいという意図はないです。ご検討よろしくお願いします。
それはよかったです。
先にお伝えしておきますが、本記事の主旨はRTSPクライアントの使い方を理解し、簡単なビューアを作成することにあります。
したがって、カスタマイズやアプリケーションの拡張に関しては、ご自身で調査を進めていただくのが良いかと思います。
アプリケーションの拡張に関する返信はこれを最後にしてください。
以下で、上記の仕様は満足していると思います。
カメラ情報の管理
マルチビューアコード