[[https://github.com/AlexxIT/go2rtc/releases/]]
====== go2rtc.service ======
cat << EOF > /etc/systemd/system/go2rtc.service
[Unit]
Description=go2rtc
After=network.target
[Service]
ExecStart=/opt/go2rtc/go2rtc_linux_amd64 -config /opt/go2rtc/go2rtc.yaml
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
#api:
# listen: "127.0.0.1:1984" # localhost
#rtsp:
# listen: "127.0.0.1:8554" # localhost
streams:
cam1: dvrip://user:pass@10.10.15.201?channel=0&subtype=0
cam2: dvrip://user:pass@10.10.15.202?channel=0&subtype=0
====== DVR ======
apt install -y reserialize jq
====== cron ======
cat << EOF > /etc/cron.d/dvr
# record start
*/10 * * * * root /opt/DVR/dvr.sh
EOF
====== cat dvr.sh ======
#!/bin/bash
# Задаем путь к конфиг файлу go2rtc
config_file="/opt/go2rtc/go2rtc.yaml"
streams=$(yaml2json "$config_file" | jq -r '.streams | keys[]')
# Директория для сохранения файлов
base_dir="/media/disk1/video"
# Создаем директории по году, месяцу и дню, если они не существуют
year=$(date +"%Y")
month=$(date +"%m")
day=$(date +"%d")
# Получаем текущее время в формате "час-минуты"
current_time=$(date +"%H-%M")
M=$(date +"%M")
# Длительность записи в секундах (10 минут)
duration=600
# Записываем каждый поток в отдельный файл MP4
for stream_name in $streams; do
# создаем структуру папок
[ ! -d "$base_dir/$stream_name/$year/$month/$day" ] && mkdir -p "$base_dir/$stream_name/$year/$month/$day"
# Создаем выходной файл MP4 с текущим временем в имени
output_file="$base_dir/$stream_name/$year/$month/$day/${current_time}.mp4"
# Команда для захвата видеопотока и записи в файл
ffmpeg -hide_banner -loglevel warning -threads 2 -avoid_negative_ts make_zero -fflags +nobuffer+genpts+discardcorrupt -flags low_delay -rtsp_transport tcp -use_wallclock_as_timestamps 1 -i "rtsp://127.0.0.1:8554/$stream_name" -reset_timestamps 1 -strftime 1 -c:v copy -c:a aac -strict experimental -t "$duration" "$output_file" &> $base_dir/$stream_name'_'$M'.txt' &
done
/opt/DVR/cleanup.sh &> $base_dir'/cleanup.txt' &
====== cat cleanup.sh ======
#!/bin/bash
# Set the target size for camera folders in GB
target_size_gb=90
# Directory containing camera folders
base_dir="/media/disk1/video"
# Найти и удалить пустые папки
find "$base_dir" -type d -empty -delete
# Iterate over camera folders
for camera_dir in "$base_dir"/*/; do
# Get the current size of the camera folder in GB without decimal part
size_gb=$(du -s "$camera_dir" | awk '{printf "%.0f\n", $1 / 1024 / 1024}')
# Calculate how much to delete to reach the target size
space_to_free_gb=$((size_gb - target_size_gb))
# Check if the current size exceeds the target size
if (( size_gb > target_size_gb )); then
echo "Cleaning $camera_dir"
# Delete oldest files until the folder size is reduced to the target size
while (( space_to_free_gb > 0 )); do
# Delete the oldest file in the folder
find "$camera_dir" -type f -printf '%T@ %p\n' | sort -n | head -n 1 | cut -d' ' -f2- | xargs rm
# Update the current size
size_gb=$(du -s "$camera_dir" | awk '{printf "%.0f\n", $1 / 1024 / 1024}')
# Calculate the remaining space to free
space_to_free_gb=$((size_gb - target_size_gb))
done
fi
done
====== плеер ======
===== HTML =====
Видео Плеер
Камеры
Выберете камеру
дата и время
===== CSS =====
#video-player {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
#camera-list {
flex: 1;
padding: 10px;
}
#video-container {
flex: 6;
padding: 10px;
}
/* Стили для кнопок */
button {
background-color: #a9a9a9; /* #4caf50; /* Зеленый цвет кнопок */
color: white;
border: none;
padding: 3px 10px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
cursor: pointer;
border-radius: 8px;
}
button:hover {
background-color: #45a049; /* Темнозеленый цвет при наведении */
}
/* Стили для видеоплеера */
video {
width: 100%;
}
/* Style for folder tree */
#folderTree {
list-style: none;
padding-left: 0;
}
.folder {
cursor: pointer;
}
.collapsed {
display: none;
}
#folderTree {
max-height: 90vh; /* Adjust the height as needed */
overflow-y: auto;
}
#camera-buttons button.active {
background-color: #007bff;
color: #ffffff;
}
#speed-buttons button.active {
background-color: #007bff;
color: #ffffff;
}
/* Стили для активного элемента списка файлов */
#folderTree li ul li.active {
background-color: #007bff; /* Цвет фона активного файла */
cursor: pointer;
}
#folderTree li ul li {
cursor: pointer;
}
\
===== JS =====
// клик по камере
function changeCamera(camera) {
const video = document.getElementById('video');
const hvideo = document.getElementById('cam-name');
hvideo.innerHTML = `${camera}`;
fetch_files(`${camera}`);
}
// Глобальная переменная для хранения текущей скорости воспроизведения
let currentPlaybackSpeed = 1;
// Функция для изменения скорости воспроизведения видео
function changePlaybackSpeed(speed) {
const video = document.getElementById('video');
video.playbackRate = speed;
// Сохраняем текущую скорость в глобальную переменную
currentPlaybackSpeed = speed;
// Удалите класс active у всех кнопок скорости воспроизведения
const speedButtons = document.querySelectorAll('#speed-buttons button');
speedButtons.forEach(button => {
button.classList.remove('active');
});
// Добавьте класс active к текущей кнопке скорости воспроизведения
const currentSpeedButton = document.querySelector(`#speed-buttons button[data-speed="${speed}"]`);
if (currentSpeedButton) {
currentSpeedButton.classList.add('active');
}
}
// Функция для включения нового видео с сохраненной скоростью воспроизведения
function playNewVideo(videoPath) {
const video = document.getElementById('video');
video.src = videoPath;
video.load();
video.playbackRate = currentPlaybackSpeed; // Устанавливаем сохраненную скорость
video.play();
}
// Запрос списка камер с сервера
async function fetchCameraList() {
try {
const response = await fetch('cams.php');
const data = await response.json();
const cameraButtons = document.getElementById('camera-buttons');
// Создание кнопок для каждой камеры
data.cameras.forEach(camera => {
const button = document.createElement('button');
button.textContent = `${camera}`;
button.onclick = () => changeCamera(camera);
const listItem = document.createElement('li');
listItem.appendChild(button);
cameraButtons.appendChild(listItem);
});
} catch (error) {
console.error('Ошибка при получении списка камер:', error);
}
}
// Добавление обработчика событий для элементов списка камер
function addCameraButtonClickHandlers() {
const cameraButtons = document.querySelectorAll('#camera-buttons button');
cameraButtons.forEach(button => {
button.addEventListener('click', () => {
// Удаляем класс active у всех кнопок
cameraButtons.forEach(btn => {
btn.classList.remove('active');
});
// Добавляем класс active к нажатой кнопке
button.classList.add('active');
});
});
}
// Вызов функции добавления обработчиков событий после загрузки списка камер
fetchCameraList().then(() => {
addCameraButtonClickHandlers();
});
// Clear existing file tree
function clearFileTree(parent) {
while (parent.firstChild) {
parent.removeChild(parent.firstChild);
}
}
function generateFileTree(data, parent) {
// Clear existing file tree
clearFileTree(parent);
// Iterate through data and generate file tree
Object.keys(data).forEach(key => {
const folderLi = document.createElement('li');
const folderSpan = document.createElement('span');
folderSpan.textContent = key;
folderSpan.classList.add('folder');
folderLi.appendChild(folderSpan);
const ul = document.createElement('ul');
ul.classList.add('collapsed'); // Add collapsed class by default
data[key].forEach(file => {
const fileLi = document.createElement('li');
const fileTextNode = document.createTextNode(file);
fileLi.appendChild(fileTextNode);
ul.appendChild(fileLi);
});
folderLi.appendChild(ul);
parent.appendChild(folderLi);
// Add click event listener to toggle files visibility
folderSpan.addEventListener('click', () => {
ul.classList.toggle('collapsed');
});
});
}
// Fetch JSON data from
function fetch_files(camera) {
fetch(`files.php?camera=${camera}`)
.then(response => response.json())
.then(data => {
const folderTreeContainer = document.getElementById("folderTree");
// Вызов функции добавления обработчиков событий после генерации дерева файлов
generateFileTree(data, folderTreeContainer);
addFileClickHandlers();
playSecondFileDefault();
// Автоматическое раскрытие первого элемента дерева
const firstFolderSpan = folderTreeContainer.querySelector('.folder');
if (firstFolderSpan) {
const ul = firstFolderSpan.nextElementSibling;
ul.classList.remove('collapsed');
}
})
.catch(error => console.error('Error fetching data:', error));
}
function addFileClickHandlers() {
const fileElements = document.querySelectorAll('#folderTree li ul li');
fileElements.forEach((fileElement, index) => {
fileElement.addEventListener('click', () => {
const camera = document.getElementById('cam-name').innerHTML;
const fileName = fileElement.textContent;
// Удалите класс active у всех элементов списка файлов
fileElements.forEach(file => {
file.classList.remove('active');
});
// Добавьте класс active к проигрываемому файлу
fileElement.classList.add('active');
// Получите путь к видео из дерева файлов
const folderSpan = fileElement.parentElement.parentElement.firstChild;
const folderName = folderSpan.textContent;
const videoPath = `${folderName}/${fileName}`;
// Воспроизведение нового видео с сохраненной скоростью
playNewVideo(videoPath);
});
});
}
const video = document.getElementById('video');
// Добавление обработчика события "ended" к элементу видео
video.addEventListener('ended', () => {
const activeFileElement = document.querySelector('#folderTree li ul li.active');
if (activeFileElement) {
const previousFileElement = activeFileElement.previousElementSibling;
if (previousFileElement) {
// Воспроизведение предыдущего файла
previousFileElement.click();
} else {
// Если предыдущего файла нет, можно воспроизвести последний файл
const lastFileElement = document.querySelector('#folderTree li ul li:last-child');
if (lastFileElement) {
lastFileElement.click();
}
}
}
});
// Функция для воспроизведения второго файла по умолчанию
function playSecondFileDefault() {
const secondFileElement = document.querySelector('#folderTree li ul li:nth-child(2)');
if (secondFileElement) {
secondFileElement.click();
}
}
===== PHP =====
===== python3 dvr.py --config_file 'dvr.yaml' =====
import argparse
import time
import os
import yaml
from datetime import datetime
import subprocess
import schedule
from pathlib import Path
# Настройка парсера аргументов командной строки
parser = argparse.ArgumentParser(description='Запись видеопотоков.')
parser.add_argument('--config_file', type=str, help='Путь к файлу конфигурации', required=True)
# Чтение аргументов командной строки
args = parser.parse_args()
dvr_config_file = args.config_file
def read_dvr_config(config_file):
with open(dvr_config_file, 'r') as file:
return yaml.safe_load(file)
config = read_dvr_config(dvr_config_file)
base_dir = config['base_dir']
stream_server = config['stream_server']
target_size_gb = config['target_size_gb']
go2rtc_config_path = config['go2rtc_config_path']
# Функция очистки папок
def clean_camera_folders(base_dir, target_size_gb):
"""
Очищает папки камер, удаляя самые старые файлы, пока размер папки не уменьшится до целевого размера.
:param base_dir: Путь к каталогу, содержащему папки камер.
:param target_size_gb: Целевой размер папки в гигабайтах.
"""
base_dir = Path(base_dir)
# Удалить пустые папки
for folder in base_dir.iterdir():
if folder.is_dir() and not any(folder.iterdir()):
folder.rmdir()
# Перебор папок камер
for camera_dir in base_dir.iterdir():
if camera_dir.is_dir():
# Получить текущий размер папки камеры в ГБ без десятичной части
size_gb = sum(f.stat().st_size for f in camera_dir.glob('**/*') if f.is_file()) / (1024 ** 3)
# Рассчитать, сколько нужно удалить, чтобы достичь целевого размера
space_to_free_gb = size_gb - target_size_gb
# Проверить, превышает ли текущий размер целевой размер
if size_gb > target_size_gb:
print(f"Cleaning {camera_dir}")
# Удалить самые старые файлы, пока размер папки не уменьшится до целевого размера
while space_to_free_gb > 0:
# Найти самый старый файл в папке
oldest_file = min(camera_dir.glob('**/*'), key=os.path.getmtime)
# Удалить самый старый файл
oldest_file_size = oldest_file.stat().st_size / (1024 ** 3)
try:
oldest_file.unlink()
print(f"Deleted {oldest_file}")
# Обновить текущий размер
space_to_free_gb -= oldest_file_size
except Exception as e:
print(f"Error deleting file {oldest_file}: {e}")
# Функция для записи потоков
def record_streams(duration, base_dir, stream_server):
with open(go2rtc_config_path, 'r') as file:
config_data = yaml.safe_load(file)
streams = config_data['streams'].keys()
now = datetime.now()
year, month, day = now.strftime("%Y"), now.strftime("%m"), now.strftime("%d")
current_time = now.strftime("%H-%M")
M = now.strftime("%M")
processes = []
for stream_name in streams:
directory = os.path.join(base_dir, stream_name, year, month, day)
os.makedirs(directory, exist_ok=True)
output_file = os.path.join(directory, f"{current_time}.mp4")
command = [
'ffmpeg', '-hide_banner', '-loglevel', 'warning', '-threads', '2',
'-avoid_negative_ts', 'make_zero', '-fflags', '+nobuffer+genpts+discardcorrupt',
'-flags', 'low_delay', '-rtsp_transport', 'tcp', '-use_wallclock_as_timestamps', '1',
'-i', f"{stream_server}/{stream_name}", '-reset_timestamps', '1', '-strftime', '1',
'-c:v', 'copy', '-c:a', 'aac', '-strict', 'experimental', '-t', str(duration), output_file
]
#log_file = os.path.join(base_dir, f"{stream_name}_{M}.txt")
# Запуск субпроцесса без ожидания его завершения
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
processes.append(process)
# Возвращаем список запущенных процессов, если нужно контролировать их в будущем
return processes
# Очистка папок до размера 90 Гб
clean_camera_folders(base_dir, target_size_gb)
## Запуск записи при старте.
now = datetime.now()
#if now.minute % 10 != 0:
next_interval = (now.minute // 10 + 1) * 10
remaining_time = (next_interval - now.minute) * 60 - now.second
record_streams(remaining_time, base_dir, stream_server)
duration = 607
# Планируем задачу на каждые 10 минут
schedule.every().hour.at(":00").do(record_streams, duration, base_dir, stream_server)
schedule.every().hour.at(":10").do(record_streams, duration, base_dir, stream_server)
schedule.every().hour.at(":20").do(record_streams, duration, base_dir, stream_server)
schedule.every().hour.at(":30").do(record_streams, duration, base_dir, stream_server)
schedule.every().hour.at(":40").do(record_streams, duration, base_dir, stream_server)
schedule.every().hour.at(":50").do(record_streams, duration, base_dir, stream_server)
# Основной цикл
while True:
schedule.run_pending()
time.sleep(1)
===== dvr.yaml =====
base_dir: '/media/disk1/vt'
stream_server: 'rtsp://10.10.15.103:8554'
target_size_gb: 90
go2rtc_config_path: '/opt/go2rtc/go2rtc.yaml'
===== Docker =====
docker container rm simple_nvr
docker build -t augin/nvr:v1 .
docker run -v /usr/share/hassio/homeassistant/go2rtc.yaml:/config/go2rtc.yaml -v /mnt:/mounts/disk1/video --name simple_nvr augin/nvr:v1