Skip to content

ssh_tunnel_manager.gui.components.rtsp_handler

rtsp_handler

SSH Tunnel Manager - RTSP Handler Handles RTSP viewer functionality with proper icons

Classes

RTSPHandler

Handles RTSP viewer functionality.

Source code in src/ssh_tunnel_manager/gui/components/rtsp_handler.py
class RTSPHandler:
    """Handles RTSP viewer functionality."""

    def __init__(self, parent):
        self.parent = parent
        self.config_manager = None
        self.active_tunnels = None
        self.log = None

    def set_managers(self, config_manager, active_tunnels, log_func):
        """Set references to managers and functions."""
        self.config_manager = config_manager
        self.active_tunnels = active_tunnels
        self.log = log_func

    def launch_rtsp(self):
        """Launch RTSP viewer with menu selection."""
        config_name = self.parent.table_widget.get_selected_config_name()
        if not config_name:
            QMessageBox.information(self.parent, "No Selection", "Please select a tunnel first.")
            return

        # Get the tunnel configuration
        config = self.config_manager.get_configuration(config_name)
        if not config:
            self.log(f"Configuration not found: {config_name}")
            return

        # Generate RTSP URL
        if hasattr(config, 'rtsp_url') and config.rtsp_url:
            rtsp_url = config.rtsp_url
        else:
            rtsp_url = f"rtsp://localhost:{config.local_port}/live/0"

        self._show_rtsp_menu(rtsp_url, config_name)

    def launch_rtsp_by_name(self, config_name: str):
        """Launch RTSP viewer for a specific tunnel by name."""
        config = self.config_manager.get_configuration(config_name)
        if not config:
            self.log(f"Configuration not found: {config_name}")
            return

        # Check if tunnel is running
        if config_name not in self.active_tunnels or not self.active_tunnels[config_name].is_running:
            # Ask if user wants to start the tunnel
            reply = QMessageBox.question(
                self.parent, 
                "Tunnel Not Running",
                f"The tunnel '{config_name}' is not running.\n\nWould you like to start it first?",
                QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel
            )

            if reply == QMessageBox.Yes:
                self.parent._start_tunnel_by_name(config_name)
                # Give tunnel a moment to start, then show menu
                QTimer.singleShot(2000, lambda: self._show_rtsp_menu_for_config(config))
            elif reply == QMessageBox.No:
                self._show_rtsp_menu_for_config(config)
            # If Cancel, do nothing
        else:
            self._show_rtsp_menu_for_config(config)

    def _show_rtsp_menu(self, rtsp_url: str, config_name: str, at_button=True):
        """Show RTSP viewer menu."""
        menu = self._create_rtsp_menu()

        # Show menu at RTSP button position or cursor
        if at_button:
            rtsp_button = self.parent.toolbar_manager.buttons.get('rtsp')
            if rtsp_button:
                menu_pos = rtsp_button.mapToGlobal(rtsp_button.rect().bottomLeft())
                action = menu.exec(menu_pos)
            else:
                action = menu.exec()
        else:
            action = menu.exec(QCursor.pos())

        # Handle selection
        if action and hasattr(action, 'text'):
            if 'VLC' in action.text():
                self._play_rtsp_with_vlc(rtsp_url, config_name)
            elif 'OpenCV' in action.text():
                self._play_rtsp_with_opencv(rtsp_url, config_name)

    def _show_rtsp_menu_for_config(self, config: TunnelConfig):
        """Show RTSP viewer menu for the given configuration."""
        # Generate RTSP URL
        if hasattr(config, 'rtsp_url') and config.rtsp_url:
            rtsp_url = config.rtsp_url
        else:
            rtsp_url = f"rtsp://localhost:{config.local_port}/live/0"

        self._show_rtsp_menu(rtsp_url, config.name, at_button=False)

    def _create_rtsp_menu(self) -> QMenu:
        """Create RTSP viewer selection menu with proper icons."""
        menu = QMenu(self.parent)

        # Load icons from assets directory
        assets_dir = Path(__file__).parent.parent / "assets"
        vlc_icon_path = assets_dir / "vlc_icon.png"
        opencv_icon_path = assets_dir / "open_cv_icon.png"

        # VLC option
        vlc_action = menu.addAction("VLC Media Player")
        if vlc_icon_path.exists():
            vlc_pixmap = QPixmap(str(vlc_icon_path))
            if not vlc_pixmap.isNull():
                scaled_pixmap = vlc_pixmap.scaled(16, 16, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
                vlc_action.setIcon(QIcon(scaled_pixmap))

        if not self._is_vlc_available():
            vlc_action.setText("VLC Media Player (Not Available)")
            vlc_action.setEnabled(False)

        # OpenCV option
        opencv_action = menu.addAction("OpenCV Player")
        if opencv_icon_path.exists():
            opencv_pixmap = QPixmap(str(opencv_icon_path))
            if not opencv_pixmap.isNull():
                scaled_pixmap = opencv_pixmap.scaled(16, 16, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
                opencv_action.setIcon(QIcon(scaled_pixmap))

        try:
            import cv2
        except ImportError:
            opencv_action.setText("OpenCV Player (Not Available)")
            opencv_action.setEnabled(False)

        return menu

    def _is_vlc_available(self) -> bool:
        """Check if VLC is available."""
        # Check tools directory first (bundled with application)
        tools_vlc = Path(__file__).parent.parent.parent.parent.parent / "tools" / "vlc" / "vlc.exe"
        if tools_vlc.exists():
            return True

        # Check common VLC installation paths
        common_paths = [
            r"C:\Program Files\VideoLAN\VLC\vlc.exe",
            r"C:\Program Files (x86)\VideoLAN\VLC\vlc.exe",
        ]

        for path in common_paths:
            if Path(path).exists():
                return True

        # Try checking PATH
        try:
            subprocess.run(["vlc", "--version"], capture_output=True, timeout=5, check=True)
            return True
        except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError):
            pass

        return False

    def _play_rtsp_with_vlc(self, rtsp_url: str, config_name: str):
        """Play RTSP stream using VLC."""
        try:
            # Find VLC executable
            vlc_path = None

            # Check tools directory first (bundled with application)
            tools_vlc = Path(__file__).parent.parent.parent.parent.parent / "tools" / "vlc" / "vlc.exe"
            if tools_vlc.exists():
                vlc_path = str(tools_vlc)
            else:
                # Check common VLC installation paths
                common_paths = [
                    r"C:\Program Files\VideoLAN\VLC\vlc.exe",
                    r"C:\Program Files (x86)\VideoLAN\VLC\vlc.exe",
                ]

                for path in common_paths:
                    if Path(path).exists():
                        vlc_path = path
                        break

                if not vlc_path:
                    # Try checking PATH
                    try:
                        subprocess.run(["vlc", "--version"], capture_output=True, timeout=5, check=True)
                        vlc_path = "vlc"  # Available in PATH
                    except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError):
                        pass

            if not vlc_path:
                QMessageBox.warning(self.parent, "VLC Not Available", 
                                   "VLC is not installed or not found. Please install VLC Media Player.")
                return

            # Launch VLC with the RTSP URL
            subprocess.Popen([
                vlc_path,
                rtsp_url,
                "--intf", "qt",  # Use Qt interface
                "--qt-start-minimized",
                "--no-video-title-show"
            ])

            self.log(f"VLC launched for {config_name}: {rtsp_url}")

        except Exception as e:
            self.log(f"Error launching VLC: {e}")
            QMessageBox.critical(self.parent, "VLC Error", f"Error launching VLC:\n{str(e)}")

    def _play_rtsp_with_opencv(self, rtsp_url: str, config_name: str):
        """Play RTSP stream using OpenCV."""
        try:
            import cv2
            import os

            self.log(f"Opening OpenCV stream for {config_name}: {rtsp_url}")

            # Set FFmpeg path for OpenCV if available
            ffmpeg_path = Path("C:\\ffmpeg\\bin\\ffmpeg.exe")
            if ffmpeg_path.exists():
                # Set environment variable for OpenCV to find FFmpeg
                os.environ['OPENCV_FFMPEG_BINARY'] = str(ffmpeg_path)
                self.log(f"Using FFmpeg at: {ffmpeg_path}")

            # Try multiple backend options
            backends_to_try = []
            if ffmpeg_path.exists():
                backends_to_try.append(('FFmpeg', cv2.CAP_FFMPEG))
            backends_to_try.extend([
                ('DirectShow', cv2.CAP_DSHOW),
                ('OpenCV Default', cv2.CAP_ANY)
            ])

            cap = None
            backend_used = None

            for backend_name, backend_id in backends_to_try:
                try:
                    self.log(f"Trying {backend_name} backend...")
                    test_cap = cv2.VideoCapture(rtsp_url, backend_id)

                    # Test if we can actually read a frame
                    if test_cap.isOpened():
                        ret, frame = test_cap.read()
                        if ret and frame is not None:
                            cap = test_cap
                            backend_used = backend_name
                            self.log(f"Successfully connected using {backend_name}")
                            break
                        else:
                            self.log(f"{backend_name} opened but can't read frames")
                            test_cap.release()
                    else:
                        self.log(f"{backend_name} failed to open")
                        test_cap.release()
                except Exception as e:
                    self.log(f"{backend_name} error: {e}")
                    try:
                        test_cap.release()
                    except:
                        pass

            if cap is None:
                error_msg = f"Failed to connect to RTSP stream with any backend.\n\nURL: {rtsp_url}\n\nTroubleshooting:\n1. Check if the tunnel is running\n2. Verify the RTSP server is broadcasting\n3. Try with VLC first to test the stream"
                self.log(f"No working backend found for RTSP stream")
                QMessageBox.warning(self.parent, "Connection Failed", error_msg)
                return

            self.log(f"Stream opened with {backend_used} - Press 'q' in video window to quit")

            # Read and display frames in a separate thread to avoid blocking the main UI
            def play_stream():
                window_name = f"RTSP Stream - {config_name} ({backend_used})"
                cv2.namedWindow(window_name, cv2.WINDOW_NORMAL)
                cv2.resizeWindow(window_name, 800, 600)

                frame_count = 0
                error_count = 0
                max_errors = 10

                while True:
                    ret, frame = cap.read()
                    if not ret:
                        error_count += 1
                        self.log(f"Frame read failed (error {error_count}/{max_errors})")
                        if error_count >= max_errors:
                            self.log(f"Too many read errors, stopping stream")
                            break
                        continue

                    # Reset error count on successful read
                    error_count = 0
                    frame_count += 1

                    # Display the frame (no overlay text as requested in earlier conversation)
                    cv2.imshow(window_name, frame)

                    # Check for 'q' key press to quit
                    if cv2.waitKey(1) & 0xFF == ord('q'):
                        break

                # Clean up
                cap.release()
                cv2.destroyAllWindows()
                self.log(f"Stream closed for {config_name} (showed {frame_count} frames)")

            # Run in a separate thread to avoid blocking the UI
            stream_thread = threading.Thread(target=play_stream, daemon=True)
            stream_thread.start()

        except ImportError:
            QMessageBox.warning(self.parent, "OpenCV Not Available", 
                               "OpenCV is not installed. Install with: pip install opencv-python")
            return
        except Exception as e:
            self.log(f"Error with OpenCV stream: {e}")
            QMessageBox.critical(self.parent, "Stream Error", f"Error playing stream:\n{str(e)}")
Functions
launch_rtsp()

Launch RTSP viewer with menu selection.

Source code in src/ssh_tunnel_manager/gui/components/rtsp_handler.py
def launch_rtsp(self):
    """Launch RTSP viewer with menu selection."""
    config_name = self.parent.table_widget.get_selected_config_name()
    if not config_name:
        QMessageBox.information(self.parent, "No Selection", "Please select a tunnel first.")
        return

    # Get the tunnel configuration
    config = self.config_manager.get_configuration(config_name)
    if not config:
        self.log(f"Configuration not found: {config_name}")
        return

    # Generate RTSP URL
    if hasattr(config, 'rtsp_url') and config.rtsp_url:
        rtsp_url = config.rtsp_url
    else:
        rtsp_url = f"rtsp://localhost:{config.local_port}/live/0"

    self._show_rtsp_menu(rtsp_url, config_name)
launch_rtsp_by_name(config_name)

Launch RTSP viewer for a specific tunnel by name.

Source code in src/ssh_tunnel_manager/gui/components/rtsp_handler.py
def launch_rtsp_by_name(self, config_name: str):
    """Launch RTSP viewer for a specific tunnel by name."""
    config = self.config_manager.get_configuration(config_name)
    if not config:
        self.log(f"Configuration not found: {config_name}")
        return

    # Check if tunnel is running
    if config_name not in self.active_tunnels or not self.active_tunnels[config_name].is_running:
        # Ask if user wants to start the tunnel
        reply = QMessageBox.question(
            self.parent, 
            "Tunnel Not Running",
            f"The tunnel '{config_name}' is not running.\n\nWould you like to start it first?",
            QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel
        )

        if reply == QMessageBox.Yes:
            self.parent._start_tunnel_by_name(config_name)
            # Give tunnel a moment to start, then show menu
            QTimer.singleShot(2000, lambda: self._show_rtsp_menu_for_config(config))
        elif reply == QMessageBox.No:
            self._show_rtsp_menu_for_config(config)
        # If Cancel, do nothing
    else:
        self._show_rtsp_menu_for_config(config)
set_managers(config_manager, active_tunnels, log_func)

Set references to managers and functions.

Source code in src/ssh_tunnel_manager/gui/components/rtsp_handler.py
def set_managers(self, config_manager, active_tunnels, log_func):
    """Set references to managers and functions."""
    self.config_manager = config_manager
    self.active_tunnels = active_tunnels
    self.log = log_func