Skip to content

Commit ab7f453

Browse files
authored
Implement Reolink PTZ Streamer GUI app (#78)
* stream gui start * initial video stream gui with PTZ -- pan works, zoom broken * stream gui: pan and zoom work, needs refinement * stream gui: some improvements .latency still terrible and key repeat is a problem * stream gui: fix autorepeat messing up commands Includes minor cleanups for publication * video_review_gui: disable SSL warnings The camera uses a self-signed certificate so prevent the annoying spamming.
1 parent ba49720 commit ab7f453

File tree

2 files changed

+178
-0
lines changed

2 files changed

+178
-0
lines changed

Diff for: examples/stream_gui.py

+175
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import sys
2+
import os
3+
from configparser import RawConfigParser
4+
from PyQt6.QtWidgets import QApplication, QWidget, QVBoxLayout, QHBoxLayout, QSlider
5+
from PyQt6.QtMultimedia import QMediaPlayer, QAudioOutput
6+
from PyQt6.QtMultimediaWidgets import QVideoWidget
7+
from PyQt6.QtCore import Qt, QUrl, QTimer
8+
from PyQt6.QtGui import QWheelEvent
9+
from reolinkapi import Camera
10+
from threading import Lock
11+
12+
import urllib3
13+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
14+
15+
def read_config(props_path: str) -> dict:
16+
config = RawConfigParser()
17+
assert os.path.exists(props_path), f"Path does not exist: {props_path}"
18+
config.read(props_path)
19+
return config
20+
21+
class ZoomSlider(QSlider):
22+
def __init__(self, *args, **kwargs):
23+
super().__init__(*args, **kwargs)
24+
25+
def keyPressEvent(self, event):
26+
if event.key() in (Qt.Key.Key_Left, Qt.Key.Key_Right, Qt.Key.Key_Up, Qt.Key.Key_Down):
27+
event.ignore()
28+
else:
29+
super().keyPressEvent(event)
30+
31+
class CameraPlayer(QWidget):
32+
def __init__(self, rtsp_url_wide, rtsp_url_telephoto, camera: Camera):
33+
super().__init__()
34+
self.setWindowTitle("Reolink PTZ Streamer")
35+
self.setGeometry(10, 10, 1900, 600)
36+
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
37+
38+
self.camera = camera
39+
self.zoom_timer = QTimer(self)
40+
self.zoom_timer.timeout.connect(self.stop_zoom)
41+
42+
# Create media players
43+
self.media_player_wide = QMediaPlayer()
44+
self.media_player_telephoto = QMediaPlayer()
45+
46+
# Create video widgets
47+
self.video_widget_wide = QVideoWidget()
48+
self.video_widget_telephoto = QVideoWidget()
49+
self.media_player_wide.setVideoOutput(self.video_widget_wide)
50+
self.media_player_telephoto.setVideoOutput(self.video_widget_telephoto)
51+
self.video_widget_wide.wheelEvent = self.handle_wheel_event
52+
self.video_widget_telephoto.wheelEvent = self.handle_wheel_event
53+
54+
# Create layout
55+
layout = QHBoxLayout()
56+
layout.addWidget(self.video_widget_wide, 2)
57+
layout.addWidget(self.video_widget_telephoto, 2)
58+
self.setLayout(layout)
59+
60+
# Start playing the streams
61+
self.media_player_wide.setSource(QUrl(rtsp_url_wide))
62+
self.media_player_telephoto.setSource(QUrl(rtsp_url_telephoto))
63+
self.media_player_wide.play()
64+
self.media_player_telephoto.play()
65+
66+
67+
def keyPressEvent(self, event):
68+
if event.isAutoRepeat():
69+
return
70+
if event.key() == Qt.Key.Key_Escape:
71+
self.close()
72+
elif event.key() in (Qt.Key.Key_Left, Qt.Key.Key_Right, Qt.Key.Key_Up, Qt.Key.Key_Down):
73+
self.start_move(event.key())
74+
75+
def keyReleaseEvent(self, event):
76+
if event.isAutoRepeat():
77+
return
78+
if event.key() in (Qt.Key.Key_Left, Qt.Key.Key_Right, Qt.Key.Key_Up, Qt.Key.Key_Down):
79+
self.stop_move()
80+
81+
def start_move(self, key):
82+
direction = {
83+
Qt.Key.Key_Left: "left",
84+
Qt.Key.Key_Right: "right",
85+
Qt.Key.Key_Up: "up",
86+
Qt.Key.Key_Down: "down"
87+
}.get(key)
88+
89+
if direction:
90+
self.move_camera(direction)
91+
92+
def stop_move(self):
93+
response = self.camera.stop_ptz()
94+
print("Stop PTZ")
95+
if response[0].get('code') != 0:
96+
self.show_error_message("Failed to stop camera movement", str(response[0]))
97+
98+
def move_camera(self, direction):
99+
speed = 25
100+
if direction == "left":
101+
response = self.camera.move_left(speed)
102+
elif direction == "right":
103+
response = self.camera.move_right(speed)
104+
elif direction == "up":
105+
response = self.camera.move_up(speed)
106+
elif direction == "down":
107+
response = self.camera.move_down(speed)
108+
else:
109+
print(f"Invalid direction: {direction}")
110+
return
111+
112+
if response[0].get('code') == 0:
113+
print(f"Moving camera {direction}")
114+
else:
115+
self.show_error_message(f"Failed to move camera {direction}", str(response[0]))
116+
117+
def handle_wheel_event(self, event: QWheelEvent):
118+
delta = event.angleDelta().y()
119+
if delta > 0:
120+
self.zoom_in()
121+
elif delta < 0:
122+
self.zoom_out()
123+
124+
def zoom_in(self):
125+
self.start_zoom('in')
126+
127+
def zoom_out(self):
128+
self.start_zoom('out')
129+
130+
def start_zoom(self, direction: str):
131+
self.zoom_timer.stop() # Stop any ongoing zoom timer
132+
speed = 60 # You can adjust this value as needed
133+
if direction == 'in':
134+
response = self.camera.start_zooming_in(speed)
135+
else:
136+
response = self.camera.start_zooming_out(speed)
137+
138+
if response[0].get('code') == 0:
139+
print(f"Zooming {direction}")
140+
self.zoom_timer.start(200) # Stop zooming after 200ms
141+
else:
142+
self.show_error_message(f"Failed to start zooming {direction}", str(response[0]))
143+
144+
def stop_zoom(self):
145+
response = self.camera.stop_zooming()
146+
if response[0].get('code') != 0:
147+
self.show_error_message("Failed to stop zooming", str(response[0]))
148+
149+
def show_error_message(self, title, message):
150+
print(f"Error: {title} {message}")
151+
152+
def handle_error(self, error):
153+
print(f"Media player error: {error}")
154+
155+
156+
if __name__ == '__main__':
157+
# Read in your ip, username, & password from the configuration file
158+
config = read_config('camera.cfg')
159+
ip = config.get('camera', 'ip')
160+
un = config.get('camera', 'username')
161+
pw = config.get('camera', 'password')
162+
163+
# Connect to camera
164+
cam = Camera(ip, un, pw, https=True)
165+
166+
rtsp_url_wide = f"rtsp://{un}:{pw}@{ip}/Preview_01_sub"
167+
rtsp_url_telephoto = f"rtsp://{un}:{pw}@{ip}/Preview_02_sub"
168+
169+
# Connect to camera
170+
cam = Camera(ip, un, pw, https=True)
171+
172+
app = QApplication(sys.argv)
173+
player = CameraPlayer(rtsp_url_wide, rtsp_url_telephoto, cam)
174+
player.show()
175+
sys.exit(app.exec())

Diff for: examples/video_review_gui.py

+3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
from PyQt6.QtCore import Qt, QUrl, QTimer, QThread, pyqtSignal, QMutex
1818
from PyQt6.QtGui import QColor, QBrush, QFont
1919

20+
import urllib3
21+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
22+
2023
def path_name_from_camera_path(fname):
2124
# Mp4Record/2024-08-12/RecM13_DST20240812_214255_214348_1F1E828_4DDA4D.mp4
2225
return fname.replace('/', '_')

0 commit comments

Comments
 (0)