Описание
При очень длительной записи файлы могут достигать огромных размеров (десятки гигабайт), что затрудняет:
- Хранение и архивирование
- Передачу файлов
- Воспроизведение (медиаплееры могут не открывать)
- Резервное копирование
Текущее поведение
Запись идёт → Файл растёт бесконечно → Размер 50+ GB
Result: Проблемы с хранением и обработкой
Ожидаемое поведение
Запись идёт → Размер близок к лимиту → Новый файл → Нумерация
Result: Файлы разбиты на управляемые части
Технические требования
1. Конфигурация лимитов
from dataclasses import dataclass
from enum import Enum
class SplitMode(Enum):
BY_SIZE = "size" # По размеру файла
BY_TIME = "time" # По времени записи
@dataclass
class FileSplitConfig:
"""Конфигурация разбиения файлов."""
enabled: bool = False
mode: SplitMode = SplitMode.BY_SIZE
# Для BY_SIZE
max_file_size_mb: float = 1024.0 # 1 GB по умолчанию
# Для BY_TIME
max_duration_minutes: int = 60 # 1 час по умолчанию
# Нумерация
padding_width: int = 3 # 001, 002, ...
separator: str = "_"
2. Мониторинг размера файла
class FileSizeMonitor:
"""Мониторинг размера файла для авторазбиения."""
def __init__(self, max_size_bytes: int):
self._max_size = max_size_bytes
self._current_size = 0
self._needs_split = False
def update(self, written_bytes: int) -> None:
"""Обновляет счётчик записанных байт."""
self._current_size += written_bytes
self._needs_split = self._current_size >= self._max_size
@property
def needs_split(self) -> bool:
"""Требуется ли разбиение."""
return self._needs_split
@property
def usage_percent(self) -> float:
"""Процент использования лимита."""
return self._current_size / self._max_size * 100
def reset(self) -> None:
"""Сброс счётчика."""
self._current_size = 0
self._needs_split = False
3. Авторазбиение в FFmpegWriter
class SegmentedFFmpegWriter:
"""FFmpeg writer с автоматическим разбиением."""
def __init__(
self,
base_path: Path,
width: int,
height: int,
fps: int = 30,
split_config: FileSplitConfig | None = None,
**kwargs
):
self._base_path = Path(base_path)
self._split_config = split_config or FileSplitConfig()
self._current_writer: FFmpegVideoWriter | None = None
self._current_segment: int = 1
self._size_monitor: FileSizeMonitor | None = None
if self._split_config.enabled:
self._size_monitor = FileSizeMonitor(
max_size_bytes=self._split_config.max_file_size_mb * 1024 * 1024
)
self._start_new_segment()
def _start_new_segment(self) -> None:
"""Начинает новый сегмент записи."""
# Закрыть текущий
if self._current_writer:
self._current_writer.close()
# Генерация имени файла
filename = self._generate_segment_filename()
segment_path = self._base_path.parent / filename
# Создать новый writer
self._current_writer = FFmpegVideoWriter(
output_path=segment_path,
width=self._width,
height=self._height,
fps=self._fps,
**self._kwargs
)
self._current_writer.open()
# Сбросить монитор
if self._size_monitor:
self._size_monitor.reset()
# Уведомление о начале сегмента
self._on_segment_started(segment_path, self._current_segment)
def _generate_segment_filename(self) -> str:
"""Генерирует имя файла для сегмента."""
stem = self._base_path.stem
suffix = self._base_path.suffix
parent = self._base_path.parent
segment_num = str(self._current_segment).zfill(self._split_config.padding_width)
filename = f"{stem}{self._split_config.separator}{segment_num}{suffix}"
return filename
def write(self, frame: np.ndarray) -> bool:
"""Запись кадра с проверкой лимита."""
if not self._current_writer:
return False
# Проверка необходимости разбиения
if self._size_monitor and self._size_monitor.needs_split:
self._current_segment += 1
self._start_new_segment()
return self._current_writer.write(frame)
result = self._current_writer.write(frame)
# Обновить монитор
if result and self._size_monitor:
# Примерный размер кадра
frame_size = frame.nbytes
self._size_monitor.update(frame_size)
if self._size_monitor.needs_split:
self._current_segment += 1
self._start_new_segment()
return result
def close(self) -> dict[str, bool]:
"""Закрытие всех сегментов."""
results = {}
if self._current_writer:
results["segment_1"] = self._current_writer.close()
self._current_writer = None
return results
def _on_segment_started(self, path: Path, segment: int) -> None:
"""Callback при начале нового сегмента."""
if self._event_bus:
self._event_bus.publish(RecordingEvent(
event_type=RecordingEventType.SEGMENT_STARTED,
payload={
"segment_number": segment,
"file_path": str(path),
"file_size_bytes": 0
}
))
4. API endpoints
# Настройка разбиения
PUT /api/v1/config/video
Body: {
"split_enabled": true,
"split_mode": "size", # or "time"
"max_file_size_mb": 2048,
# или
"max_duration_minutes": 30
}
Response: {"success": true}
# Информация о сегментах
GET /api/v1/recordings/{recording_id}/segments
Response: {
"segments": [
{"index": 1, "file_path": "...", "size_bytes": 2147483648},
{"index": 2, "file_path": "...", "size_bytes": 1073741824}
],
"total_size_bytes": 3221225472
}
5. UI компонент
class FileSizeLimitWidget(QWidget):
"""Виджет настроек лимита размера файла."""
def __init__(self, config: FileSplitConfig):
super().__init__()
self._config = config
self._setup_ui()
def _setup_ui(self) -> None:
layout = QVBoxLayout()
# Чекбокс включения
self._enabled_checkbox = QCheckBox("Автоматически разбивать на части")
layout.addWidget(self._enabled_checkbox)
# Режим
mode_group = QGroupBox("Режим разбиения")
mode_layout = QVBoxLayout()
self._size_mode = QRadioButton("По размеру файла")
self._time_mode = QRadioButton("По времени записи")
mode_layout.addWidget(self._size_mode)
mode_layout.addWidget(self._time_mode)
mode_group.setLayout(mode_layout)
layout.addWidget(mode_group)
# Параметры
params_layout = QHBoxLayout()
self._limit_input = QSpinBox()
self._limit_input.setMinimum(1)
self._limit_input.setMaximum(10000)
self._limit_input.setSuffix(" MB")
self._time_unit_label = QLabel("минут")
self._time_unit_label.setVisible(False)
params_layout.addWidget(self._limit_input)
params_layout.addWidget(self._time_unit_label)
layout.addLayout(params_layout)
self.setLayout(layout)
# Connections
self._size_mode.toggled.connect(self._on_mode_changed)
self._enabled_checkbox.toggled.connect(self._on_enabled_changed)
def _on_mode_changed(self, checked: bool) -> None:
"""Обработчик смены режима."""
if checked:
self._limit_input.setSuffix(" MB")
self._time_unit_label.setVisible(False)
else:
self._limit_input.setSuffix("")
self._time_unit_label.setVisible(True)
Конфигурация
# config.py
class VideoSettingsSchema(BaseModel):
# ... existing fields ...
split_enabled: bool = Field(default=False)
split_mode: Literal["size", "time"] = Field(default="size")
max_file_size_mb: float = Field(default=1024.0, ge=100, le=100000)
max_duration_minutes: int = Field(default=60, ge=1, le=1440)
segment_padding_width: int = Field(default=3, ge=1, le=6)
Критерии приёмки
Файлы для изменения
recorder/segmented_writer.py — новый модуль
recorder/ffmpeg_writer.py — интеграция
api/routes_config.py — endpoints
gui/views/video_view.py — UI
config.py — настройки
tests/unit/ — тесты
Приоритет: P3
Тэги: enhancement, functionality
Описание
При очень длительной записи файлы могут достигать огромных размеров (десятки гигабайт), что затрудняет:
Текущее поведение
Ожидаемое поведение
Технические требования
1. Конфигурация лимитов
2. Мониторинг размера файла
3. Авторазбиение в FFmpegWriter
4. API endpoints
5. UI компонент
Конфигурация
Критерии приёмки
Файлы для изменения
recorder/segmented_writer.py— новый модульrecorder/ffmpeg_writer.py— интеграцияapi/routes_config.py— endpointsgui/views/video_view.py— UIconfig.py— настройкиtests/unit/— тестыПриоритет: P3
Тэги:
enhancement,functionality