Skip to content

Реализовать горячее переключение устройств захвата без остановки записи #48

@chelslava

Description

@chelslava

Описание

При отключении монитора или окна захвата во время записи происходит потеря захвата. Нет возможности переключиться на другое устройство без остановки записи. Пользователь вынужден прерывать запись.

Текущее поведение

Запись идёт → Окно закрыто → Capture lost → Захват прерван → ???
Result: Потеря захвата без альтернативы

Ожидаемое поведение

Запись идёт → Окно закрыто → Детекция → Буфер → Пользователь выбирает источник → Продолжение
Result: Непрерывная запись с переключением источника

Технические требования

1. Архитектура горячего переключения

from abc import ABC, abstractmethod
from typing import Protocol

class CaptureSource(ABC):
    """Абстракция источника захвата."""
    
    @abstractmethod
    def is_available(self) -> bool:
        """Проверяет доступность источника."""
        ...
    
    @abstractmethod
    def get_info(self) -> CaptureSourceInfo:
        """Информация об источнике."""
        ...
    
    @abstractmethod
    def start_capture(self, on_frame: Callable[[np.ndarray], None]) -> None:
        """Начинает захват с источника."""
        ...
    
    @abstractmethod
    def stop_capture(self) -> None:
        """Останавливает захват."""
        ...

@dataclass
class CaptureSourceInfo:
    type: str  # "monitor", "window", "rect"
    id: str
    name: str
    width: int
    height: int

2. Hot-swap менеджер

class CaptureHotSwapManager:
    """Управляет горячим переключением источников захвата."""
    
    def __init__(self, event_bus: EventBus):
        self._event_bus = event_bus
        self._current_source: CaptureSource | None = None
        self._backup_sources: list[CaptureSource] = []
        self._frame_buffer: deque[np.ndarray] = deque(maxlen=900)  # 30 сек
    
    def register_backup_source(self, source: CaptureSource) -> None:
        """Регистрирует резервный источник."""
        self._backup_sources.append(source)
    
    def on_capture_lost(self, reason: str) -> None:
        """Обработчик потери захвата."""
        # 1. Уведомить UI
        self._event_bus.publish(RecordingEvent(
            event_type=RecordingEventType.CAPTURE_SOURCE_LOST,
            payload={"reason": reason, "available_sources": self._get_available_sources()}
        ))
        
        # 2. Попытаться автоматическое переключение
        if self._try_auto_switch():
            return
        
        # 3. Перейти в режим ожидания выбора пользователя
        self._enter_await_mode()
    
    def switch_to_source(self, source_id: str) -> bool:
        """Переключиться на указанный источник."""
        source = self._find_source_by_id(source_id)
        if source is None or not source.is_available():
            return False
        
        # Сохраняем текущий контекст
        current_source = self._current_source
        
        try:
            # Создаём новую сессию
            self._current_source = source
            self._current_source.start_capture(self._on_frame)
            
            # Останавливаем старую
            if current_source:
                current_source.stop_capture()
            
            # Уведомляем о переключении
            self._event_bus.publish(RecordingEvent(
                event_type=RecordingEventType.CAPTURE_SOURCE_SWITCHED,
                payload={"new_source": source.get_info()}
            ))
            
            return True
            
        except Exception as e:
            # Rollback
            self._current_source = current_source
            if current_source:
                current_source.start_capture(self._on_frame)
            return False

3. API endpoints

# Получение доступных источников
GET /api/v1/resources/capture-sources
Response: {
    "sources": [
        {
            "id": "monitor-1",
            "type": "monitor",
            "name": "Primary Monitor",
            "width": 1920,
            "height": 1080,
            "available": true
        },
        {
            "id": "window-Notepad",
            "type": "window",
            "name": "Notepad",
            "width": 800,
            "height": 600,
            "available": true
        }
    ],
    "current": "monitor-1"
}

# Переключение источника
POST /api/v1/recording/switch-source
Body: {"source_id": "monitor-2"}
Response: {"success": true, "new_source": {...}}

4. UI интеграция

class CaptureSourceSelector(QWidget):
    """Виджет выбора источника захвата."""
    
    source_changed = pyqtSignal(str)  # source_id
    
    def update_sources(self, sources: list[CaptureSourceInfo], current: str) -> None:
        """Обновляет список доступных источников."""
        self._sources = sources
        self._current = current
        
        # Обновить combobox
        self.ui.source_combo.clear()
        for source in sources:
            icon = self._get_icon(source.type)
            text = f"{source.name} ({source.width}x{source.height})"
            self.ui.source_combo.addItem(icon, text, source.id)
            
            if source.id == current:
                self.ui.source_combo.setCurrentIndex(
                    self.ui.source_combo.count() - 1
                )
        
        # Показать предупреждение о переключении
        if current not in [s.id for s in sources if s.available]:
            self._show_switch_warning()

5. Обработка событий

# Event Bus
class RecordingEventType(StrEnum):
    CAPTURE_SOURCE_LOST = "capture_source_lost"
    CAPTURE_SOURCE_SWITCHED = "capture_source_switched"
    CAPTURE_SOURCE_AWAITING = "capture_source_awaiting"

Критерии приёмки

  • UI показывает доступные источники в реальном времени
  • Пользователь может переключиться без остановки записи
  • Hot-swap через GUI с подтверждением
  • Hot-swap через API endpoint
  • Автоматическое переключение при доступности резервного источника
  • История переключений в логе
  • Unit-тесты для CaptureHotSwapManager

Файлы для изменения

  • recorder/video_recorder.py — поддержка hot-swap
  • recorder/capture_source.py — абстракция источника
  • core/event_bus.py — новые события
  • api/routes_recording.py — endpoints переключения
  • gui/views/capture_view.py — UI выбора источника
  • tests/unit/ — тесты hot-swap

Приоритет: P2

Тэги: reliability, enhancement

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions