"""Сборка команд ffmpeg/ffprobe. Запись — строго копированием (без перекодирования).""" from __future__ import annotations import asyncio import json import os from ..config import Camera def record_args(cam: Camera, out_root: str, segment_seconds: int, input_url: str | None = None) -> list[str]: """ffmpeg для записи основного потока сегментами mp4 копированием. -c copy => CPU почти не используется. -strftime даёт имена сегментов по времени. out_root — корень активного хранилища (внутри пишем подкаталог камеры). input_url — источник (по умолчанию рестрим go2rtc: камера держит одну сессию). """ # Подпапка даты: ///.mp4. # Этот ffmpeg-билд НЕ знает -strftime_mkdir, поэтому каталоги даты (сегодня+завтра) # заранее создаёт recorder_task при старте и индексатор каждые 30с (переживает полночь). out_template = os.path.join(out_root, cam.id, "%Y-%m-%d", "%Y-%m-%d_%H-%M-%S.mp4") return [ "ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "warning", "-rtsp_transport", "tcp", "-timeout", "10000000", # 10s на установку соединения (микросекунды) "-i", input_url or cam.record_url, "-an", # звук не пишем (камеры обычно без него) "-c:v", "copy", "-f", "segment", "-segment_time", str(segment_seconds), "-segment_atclocktime", "1", # резать по часам: кратно segment_time от 00:00 "-segment_clocktime_offset", "0", "-segment_format", "mp4", "-reset_timestamps", "1", "-strftime", "1", out_template, ] async def probe_duration(path: str) -> tuple[float | None, int | None]: """ffprobe: длительность (сек) и размер файла (байт).""" try: size = os.path.getsize(path) except OSError: size = None proc = await asyncio.create_subprocess_exec( "ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", path, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL, ) out, _ = await proc.communicate() duration = None if proc.returncode == 0 and out: try: data = json.loads(out) duration = float(data["format"]["duration"]) except (json.JSONDecodeError, KeyError, ValueError): duration = None return duration, size