8e4844111b
- Camera.storage_id: each camera chooses its storage ("" = active/default).
supervisor resolves per-camera (storage_by_id → active fallback); cameras
API (body/payload) + /toggle accept storage_id and repoint the recorder.
- Recordings now go to <root>/<cam>/<YYYY-MM-DD>/<file>.mp4. This ffmpeg build
lacks -strftime_mkdir, so recorder_task pre-creates today+tomorrow dirs at
start and the indexer re-ensures them each tick (survives midnight).
- indexer: scan date subdirs + legacy flat files; sort by basename so the
in-progress segment is detected correctly when both layouts coexist.
- frontend: per-camera "Хранилище" dropdown in the camera table (inline) and
in the add/edit + bulk forms. Move the active-storage summary widget from
the Система tab into the Хранилища tab.
VERSION 0.0.129.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
64 lines
2.9 KiB
Python
64 lines
2.9 KiB
Python
"""Сборка команд 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: камера держит одну сессию).
|
|
"""
|
|
# Подпапка даты: <root>/<cam>/<YYYY-MM-DD>/<YYYY-MM-DD_HH-MM-SS>.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
|