Это старая версия документа!
Содержание
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
- go2rtc.yaml
#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
- 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
- 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
- index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Видео Плеер</title> <link rel="stylesheet" href="main.css"> <style> /* Стили для разметки плеера */ body { background-color: #202124; /* Цвет фона для темной темы */ color: #ffffff; /* Цвет текста для темной темы */ } </style> </head> <body> <div id="video-player"> <div id="camera-list"> <h2>Камеры</h2> <ul id="camera-buttons"> <!-- Кнопки для камер будут добавлены здесь --> </ul> </div> <div id="video-container"> <h2 id="cam-name">Выберете камеру</h2> <video id="video" controls muted> <!-- Видеофайл будет отображаться здесь --> </video> <div id="speed-buttons"> <button data-speed="1" onclick="changePlaybackSpeed(1)">1x</button> <button data-speed="2" onclick="changePlaybackSpeed(2)">2x</button> <button data-speed="4" onclick="changePlaybackSpeed(4)">4x</button> <button data-speed="8" onclick="changePlaybackSpeed(8)">8x</button> <button data-speed="10" onclick="changePlaybackSpeed(10)">10x</button> </div> </div> <div id="time-folder-list"> <h2>дата и время</h2> <div id="folderTree" class="scrollable"></div> </div> </div> <script src="main.js"></script> </body> </html>
CSS
- main.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
- main.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
- cams.php
<?php echo '{"cameras": '; echo shell_exec("yaml2json /opt/go2rtc/go2rtc.yaml | jq '.streams | keys_unsorted'"); echo '}'; ?>
- files.php
<?php // Function to recursively get all files in a directory function getFiles($dir, &$visitedDirs = []) { $files = glob(rtrim($dir, '/') . '/*'); $result = []; foreach ($files as $file) { if (is_dir($file) && !in_array($file, $visitedDirs)) { $visitedDirs[] = $file; $result = array_merge($result, getFiles($file, $visitedDirs)); } else { $result[] = $file; } } return $result; } // Main function to get files and encode them in JSON format function getAllFilesJSON($dir) { $visitedDirs = []; $files = getFiles($dir, $visitedDirs); $result = []; rsort($files); foreach ($files as $file) { $folder = dirname($file); $filename = basename($file); // if (!array_key_exists($folder, $result)) { // $result[$folder] = []; // } $result[$folder][] = $filename; } return json_encode($result); } //usage $directory = 'video/'.$_GET['camera']; $jsonData = getAllFilesJSON($directory); echo $jsonData; ?>
===== python script.py –config_file '/path/to/config.yaml' –base_dir '/media/disk1/video' –stream_server 'rtsp:10.10.15.103:8554' ===== <code bash> import argparse import time import os import yaml from datetime import datetime import subprocess # Настройка парсера аргументов командной строки parser = argparse.ArgumentParser(description='Запись видеопотоков.') parser.add_argument('–config_file', type=str, help='Путь к файлу конфигурации', required=True) parser.add_argument('–base_dir', type=str, help='Базовая директория для сохранения видео', required=True) parser.add_argument('–stream_server', type=str, help='Адрес сервера потоков', required=True) # Чтение аргументов командной строки args = parser.parse_args() # Функция для записи потоков def record_streams(duration, base_dir, stream_server): with open(args.config_file, '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») 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») try: with open(log_file, 'a') as log: subprocess.run(command, stdout=log, stderr=log, check=True) except subprocess.CalledProcessError as e: print(f«Ошибка при записи потока {stream_name}: {e}») # Основной цикл while True: 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, args.base_dir, args.stream_server) time.sleep(60)
</code>