Skip to content

Commit 3c455e9

Browse files
committed
Support RTSP in GUI client
1 parent 320ea3a commit 3c455e9

5 files changed

Lines changed: 221 additions & 124 deletions

File tree

examples/official/ip_camera/gui_client/config.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,16 @@
2424
"version": "1.0.0",
2525
"author": "IP Camera Tools",
2626
"description": "Professional desktop application for viewing IP camera streams"
27-
}
27+
},
28+
"always_on_top": false,
29+
"auto_reconnect": true,
30+
"buffer_size": 1024,
31+
"connection_timeout": 10,
32+
"reconnect_delay": 3,
33+
"save_position": true,
34+
"show_overlay": false,
35+
"start_fullscreen": false,
36+
"theme": 0,
37+
"user_agent": "IP-Camera-GUI-Client/1.0",
38+
"video_fit_mode": 0
2839
}

examples/official/ip_camera/gui_client/dialogs.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ def setup_quick_connect_form(self, layout):
9797
self.name_edit.setPlaceholderText("My IP Camera")
9898
form_layout.addRow("Camera Name:", self.name_edit)
9999

100+
# Protocol selection
101+
self.stream_protocol_combo = QComboBox()
102+
self.stream_protocol_combo.addItems(["HTTP (MJPEG)", "RTSP"])
103+
self.stream_protocol_combo.currentTextChanged.connect(self.on_protocol_changed)
104+
form_layout.addRow("Protocol:", self.stream_protocol_combo)
105+
100106
# Server IP
101107
self.ip_edit = QLineEdit()
102108
self.ip_edit.setValidator(IPValidator())
@@ -111,12 +117,6 @@ def setup_quick_connect_form(self, layout):
111117
self.port_spinbox.valueChanged.connect(self.update_url_preview)
112118
form_layout.addRow("Port:", self.port_spinbox)
113119

114-
# Protocol
115-
self.protocol_combo = QComboBox()
116-
self.protocol_combo.addItems(["http", "https"])
117-
self.protocol_combo.currentTextChanged.connect(self.update_url_preview)
118-
form_layout.addRow("Protocol:", self.protocol_combo)
119-
120120
# Stream path
121121
self.path_edit = QLineEdit()
122122
self.path_edit.setText("/video_feed")
@@ -139,25 +139,48 @@ def setup_quick_connect_form(self, layout):
139139
layout.addLayout(form_layout)
140140

141141
# Update preview initially
142+
self.on_protocol_changed()
143+
self.update_url_preview()
144+
145+
def on_protocol_changed(self):
146+
"""Handle protocol change"""
147+
is_rtsp = "RTSP" in self.stream_protocol_combo.currentText()
148+
149+
if is_rtsp:
150+
self.port_spinbox.setValue(554) # Default RTSP port
151+
self.path_edit.setText("/stream")
152+
else:
153+
self.port_spinbox.setValue(5000) # Default HTTP port
154+
self.path_edit.setText("/video_feed")
155+
142156
self.update_url_preview()
143157

144158

145159
def update_url_preview(self):
146160
"""Update the URL preview"""
147-
protocol = self.protocol_combo.currentText()
148161
ip = self.ip_edit.text() or "0.0.0.0"
149162
port = self.port_spinbox.value()
150163
path = self.path_edit.text() or "/"
151164

152165
if not path.startswith('/'):
153166
path = '/' + path
167+
168+
is_rtsp = "RTSP" in self.stream_protocol_combo.currentText()
169+
170+
if is_rtsp:
171+
url = f"rtsp://{ip}:{port}{path}"
172+
else:
173+
url = f"http://{ip}:{port}{path}"
154174

155-
url = f"{protocol}://{ip}:{port}{path}"
156175
self.url_preview.setText(url)
157176

158177
def get_current_url(self):
159178
"""Get the current URL"""
160179
return self.url_preview.text()
180+
181+
def get_stream_protocol(self):
182+
"""Get the selected streaming protocol"""
183+
return "rtsp" if "RTSP" in self.stream_protocol_combo.currentText() else "http"
161184

162185
def get_current_name(self):
163186
"""Get the current camera name"""

examples/official/ip_camera/gui_client/main.py

Lines changed: 32 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@
1010
from typing import Dict, Any, Optional
1111

1212
from PySide6.QtCore import Qt, QTimer, QSettings, QSize, QPoint, Signal
13-
from PySide6.QtGui import QAction, QKeySequence, QFont
13+
from PySide6.QtGui import QAction, QFont, QPixmap
1414
from PySide6.QtWidgets import (
1515
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
16-
QMenuBar, QStatusBar, QToolBar, QPushButton, QLabel, QSplitter,
16+
QStatusBar, QToolBar, QPushButton, QLabel, QSplitter,
1717
QMessageBox, QFileDialog, QFrame, QSizePolicy
1818
)
1919

@@ -47,7 +47,6 @@ def __init__(self):
4747

4848
# Setup UI
4949
self.setup_ui()
50-
self.setup_menus()
5150
self.setup_toolbar()
5251
self.setup_statusbar()
5352
self.setup_connections()
@@ -102,7 +101,7 @@ def create_control_panel(self):
102101
layout = QVBoxLayout(panel)
103102

104103
# Connection controls - removed duplicate buttons for simplicity
105-
# (Connection controls are available in toolbar and menu)
104+
# (Connection controls are available in toolbar)
106105
conn_layout = QHBoxLayout()
107106
conn_layout.addStretch() # Just add stretch to maintain layout
108107

@@ -127,87 +126,6 @@ def create_control_panel(self):
127126

128127
return panel
129128

130-
def setup_menus(self):
131-
"""Setup application menus"""
132-
menubar = self.menuBar()
133-
134-
# File menu
135-
file_menu = menubar.addMenu("&File")
136-
137-
connect_action = QAction("&Connect to Camera...", self)
138-
connect_action.setShortcut(QKeySequence.Open)
139-
connect_action.setStatusTip("Connect to an IP camera")
140-
connect_action.triggered.connect(self.show_connection_dialog)
141-
file_menu.addAction(connect_action)
142-
143-
disconnect_action = QAction("&Disconnect", self)
144-
disconnect_action.setShortcut(QKeySequence("Ctrl+D"))
145-
disconnect_action.setStatusTip("Disconnect from current camera")
146-
disconnect_action.triggered.connect(self.disconnect_camera)
147-
file_menu.addAction(disconnect_action)
148-
self.disconnect_action = disconnect_action
149-
150-
file_menu.addSeparator()
151-
152-
screenshot_action = QAction("Take &Screenshot", self)
153-
screenshot_action.setShortcut(QKeySequence("Ctrl+S"))
154-
screenshot_action.setStatusTip("Save current frame as image")
155-
screenshot_action.triggered.connect(self.take_screenshot)
156-
file_menu.addAction(screenshot_action)
157-
self.screenshot_action = screenshot_action
158-
159-
file_menu.addSeparator()
160-
161-
exit_action = QAction("E&xit", self)
162-
exit_action.setShortcut(QKeySequence.Quit)
163-
exit_action.setStatusTip("Exit the application")
164-
exit_action.triggered.connect(self.close_application)
165-
file_menu.addAction(exit_action)
166-
167-
# View menu
168-
view_menu = menubar.addMenu("&View")
169-
170-
fullscreen_action = QAction("&Fullscreen", self)
171-
fullscreen_action.setShortcut(QKeySequence("F11"))
172-
fullscreen_action.setStatusTip("Toggle fullscreen mode")
173-
fullscreen_action.triggered.connect(self.toggle_fullscreen)
174-
view_menu.addAction(fullscreen_action)
175-
176-
view_menu.addSeparator()
177-
178-
# Video fit mode submenu
179-
fit_menu = view_menu.addMenu("Video &Fit Mode")
180-
181-
fit_actions = []
182-
fit_modes = ["Keep Aspect Ratio", "Keep Aspect Ratio by Expanding", "Ignore Aspect Ratio"]
183-
for i, mode in enumerate(fit_modes):
184-
action = QAction(mode, self)
185-
action.setCheckable(True)
186-
action.setData(i)
187-
action.triggered.connect(lambda checked, idx=i: self.set_video_fit_mode(idx))
188-
fit_menu.addAction(action)
189-
fit_actions.append(action)
190-
191-
fit_actions[0].setChecked(True) # Default to first mode
192-
self.fit_mode_actions = fit_actions
193-
194-
# Tools menu
195-
tools_menu = menubar.addMenu("&Tools")
196-
197-
settings_action = QAction("&Settings...", self)
198-
settings_action.setShortcut(QKeySequence.Preferences)
199-
settings_action.setStatusTip("Open application settings")
200-
settings_action.triggered.connect(self.show_settings_dialog)
201-
tools_menu.addAction(settings_action)
202-
203-
# Help menu
204-
help_menu = menubar.addMenu("&Help")
205-
206-
about_action = QAction("&About...", self)
207-
about_action.setStatusTip("About this application")
208-
about_action.triggered.connect(self.show_about_dialog)
209-
help_menu.addAction(about_action)
210-
211129
def setup_toolbar(self):
212130
"""Setup application toolbar"""
213131
toolbar = QToolBar("Main Toolbar")
@@ -249,6 +167,20 @@ def setup_toolbar(self):
249167
fullscreen_action.triggered.connect(self.toggle_fullscreen)
250168
toolbar.addAction(fullscreen_action)
251169

170+
toolbar.addSeparator()
171+
172+
# Settings button
173+
settings_action = QAction("Settings", self)
174+
settings_action.setToolTip("Open settings")
175+
settings_action.triggered.connect(self.show_settings_dialog)
176+
toolbar.addAction(settings_action)
177+
178+
# About button
179+
about_action = QAction("About", self)
180+
about_action.setToolTip("About this application")
181+
about_action.triggered.connect(self.show_about_dialog)
182+
toolbar.addAction(about_action)
183+
252184
def setup_statusbar(self):
253185
"""Setup application status bar"""
254186
self.status_bar = QStatusBar()
@@ -281,15 +213,21 @@ def setup_connections(self):
281213

282214
def show_connection_dialog(self):
283215
"""Show connection dialog"""
284-
dialog = ConnectionDialog(self)
285-
dialog.connection_requested.connect(self.connect_to_camera)
286-
dialog.exec()
216+
self.connection_dialog = ConnectionDialog(self)
217+
self.connection_dialog.connection_requested.connect(self.connect_to_camera)
218+
self.connection_dialog.exec()
287219

288220
def connect_to_camera(self, url: str, name: str):
289221
"""Connect to a camera"""
290222
try:
223+
# Get protocol from the dialog
224+
protocol = "http" # Default fallback
225+
if hasattr(self, 'connection_dialog'):
226+
protocol = self.connection_dialog.get_stream_protocol()
227+
291228
self.current_camera_url = url
292229
self.current_camera_name = name
230+
self.current_protocol = protocol
293231

294232
# Apply connection timeout from settings
295233
timeout = self.app_settings.get('connection_timeout', 5) # Shorter default timeout
@@ -298,10 +236,11 @@ def connect_to_camera(self, url: str, name: str):
298236
# Reset error tracking
299237
self._last_error_time = 0
300238

301-
# Connect to stream
302-
self.video_widget.connect_to_stream(url)
239+
# Connect to stream with protocol
240+
self.video_widget.connect_to_stream(url, protocol)
303241

304-
self.status_bar.showMessage(f"Connecting to {name}...", 3000)
242+
protocol_display = "RTSP" if protocol == "rtsp" else "HTTP"
243+
self.status_bar.showMessage(f"Connecting to {name} ({protocol_display})...", 3000)
305244

306245
except Exception as e:
307246
QMessageBox.critical(self, "Connection Error", f"Failed to connect: {str(e)}")
@@ -311,6 +250,8 @@ def disconnect_camera(self):
311250
self.video_widget.disconnect_stream()
312251
self.current_camera_url = ""
313252
self.current_camera_name = ""
253+
self.current_protocol = "http"
254+
self.connection_dialog = None
314255
self.update_ui_state()
315256

316257
def on_connection_changed(self, connected: bool):
@@ -343,10 +284,6 @@ def on_error_occurred(self, error_message: str):
343284

344285
def update_ui_state(self):
345286
"""Update UI state based on connection status"""
346-
# Update menu actions
347-
self.disconnect_action.setEnabled(self.is_connected)
348-
self.screenshot_action.setEnabled(self.is_connected)
349-
350287
# Update toolbar actions
351288
self.toolbar_disconnect_action.setEnabled(self.is_connected)
352289
self.toolbar_screenshot_action.setEnabled(self.is_connected)
@@ -413,13 +350,11 @@ def toggle_fullscreen(self):
413350
"""Toggle fullscreen mode"""
414351
if self.is_fullscreen:
415352
self.showNormal()
416-
self.menuBar().show()
417353
self.statusBar().show()
418354
self.findChild(QToolBar).show()
419355
self.is_fullscreen = False
420356
else:
421357
self.showFullScreen()
422-
self.menuBar().hide()
423358
self.statusBar().hide()
424359
self.findChild(QToolBar).hide()
425360
self.is_fullscreen = True
@@ -429,10 +364,6 @@ def set_video_fit_mode(self, mode: int):
429364
fit_modes = [Qt.KeepAspectRatio, Qt.KeepAspectRatioByExpanding, Qt.IgnoreAspectRatio]
430365
if 0 <= mode < len(fit_modes):
431366
self.video_widget.set_fit_mode(fit_modes[mode])
432-
433-
# Update menu checkmarks
434-
for i, action in enumerate(self.fit_mode_actions):
435-
action.setChecked(i == mode)
436367

437368
def show_settings_dialog(self):
438369
"""Show settings dialog"""

examples/official/ip_camera/gui_client/requirements.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ PySide6==6.7.2
66
# HTTP requests for MJPEG streaming
77
requests==2.31.0
88

9+
# OpenCV for RTSP URL support
10+
opencv-python==4.8.1.78
11+
numpy>=1.21.0,<2.0.0
12+
913
# Additional utilities
1014
urllib3>=1.26.0
1115
certifi>=2023.0.0

0 commit comments

Comments
 (0)