Всех поздравляю с очередной и долгожданной пятницей! Сегодняшний томный вечер будет полностью посвящён (внезапно) видеонаблюдению и совсем маленькому веб-серверу на python. А если быть более конкретным: это пост про создание простого веб-сервера для просмотра rtsp-потоков с IP-камер. Да-да, теперь мы тут умеем в имитацию видеорегистратора.
Прозаично, конечно, но в просмотре потока с камеры нет ничего сложного. Достаточно войти на веб-интерфейс камеры и спокойно смотреть происходящее перед объективом. Без регистрации и смс.
Проблемы начинаются, когда нужно организовать просмотр нескольких камер несколькими пользователями, а еще желательно,чтобы всё это происходило на одной странице и никто не тратил время на бесполезные логинки.
Впрочем магия rtsp-потока в чистом виде для этого бесполезна. В любом случае нужно организовать хоть какое-то серверное вещание, чтобы нивелировать пресловутое ограничение на количество пользовательских сессий на поток вещания. Грубо говоря, сервер здесь нужен лишь для того чтобы забирать поток и ретранслировать его. Требования для такого сервера просты — кроссплатформенность и монолитность.
Ну, теперь уже давайте про реализацию. Потребуется python и фреймворк flask. Ну, и само собой нужен будет opencv-python для работы с потоками. Установить последний можно так:
pip install opencv-python
Код самого скрипта выглядит так:
from flask import Flask, Response, render_template_string
import cv2
import time
app = Flask(__name__)
# Для добавления потоков
streams = {
'video_feed_1': 'rtsp://user:password@192.168.1.2/stream2',
'video_feed_2': 'rtsp://user:password@192.168.1.3/stream2',
}
# Генерация видеопотока с обработкой ошибок
def generate_video_stream(rtsp_url):
while True:
cap = cv2.VideoCapture(rtsp_url)
if not cap.isOpened():
time.sleep(5)
continue
while True:
success, frame = cap.read()
if not success:
break
_, jpeg = cv2.imencode('.jpg', frame)
yield (b'--frame\r\n'
b'Content-Type: image/jpeg\r\n\r\n' + jpeg.tobytes() + b'\r\n')
cap.release()
time.sleep(5)
@app.route('/<video_feed_id>')
def video_feed(video_feed_id):
rtsp_url = streams.get(video_feed_id)
if rtsp_url:
return Response(generate_video_stream(rtsp_url), mimetype='multipart/x-mixed-replace; boundary=frame')
return "Stream not found!", 404
@app.route('/')
def index():
html = """
<html>
<head>
<title>Камеры</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body
{
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
background-color: #f4f4f4;
}
.video-container
{
display: flex;
flex-wrap: wrap;
justify-content: center;
padding: 20px;
}
.video
{
background: #fff;
border: 1px solid #ccc;
border-radius: 8px;
overflow: hidden;
margin: 10px;
max-width: calc(100% / 2 - 40px);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
h2
{
text-align: center;
padding: 10px;
margin: 0;
background-color: #007bff;
color: white;
}
img {
width: 100%;
height: auto;
display: block;
}
button
{
display: block;
margin: 10px auto;
padding: 10px 20px;
border: none;
border-radius: 5px;
background-color: #007bff;
color: white;
cursor: pointer;
}
button:hover
{
background-color: #0056b3;
}
@media (max-width: 600px)
{
.video {
max-width: 100%;
}
}
.fullscreen
{
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 9999;
background-color: black;
}
.fullscreen img
{
width: 100%;
height: 100%;
object-fit: cover;
}
</style>
<script>
function toggleFullScreen(videoId) {
const videoElement = document.getElementById(videoId);
if (!document.fullscreenElement) {
videoElement.requestFullscreen();
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
}
}
}
</script>
</head>
<body>
<div class="video-container">
<div class="video">
<h2>1 камера</h2>
<img id="video_feed_1" src="{{ url_for('video_feed', video_feed_id='video_feed_1') }}" onclick="toggleFullScreen('video_feed_1')" />
<button onclick="toggleFullScreen('video_feed_1')">Расширить на весь экран</button>
</div>
<div class="video">
<h2>2 камера</h2>
<img id="video_feed_2" src="{{ url_for('video_feed', video_feed_id='video_feed_2') }}" onclick="toggleFullScreen('video_feed_2')" />
<button onclick="toggleFullScreen('video_feed_2')">Расширить на весь экран</button>
</div>
</div>
</body>
</html>
"""
return render_template_string(html)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5600)
Обратите внимание на словарь streams, где хранятся пути к камерам с использованием учетных данных для доступа к потоку. Порт и адрес сервера для доступа к странице можете установить на самой последней строке:
app.run(host='0.0.0.0', port=5600)
Сразу после запуска веб-сервер создаст страницу с просмотром двух камер. При желании количество камер на странице можно увеличить.
У скрипта бывают проблемы с обходом ошибок трансляций, но в целом штуковина работает и даже успела зарекомендовать себя. Если поиграть с оформлением страницы — можно создать страницу трансляций, например, для zabbix.
Настоятельно рекомендую использовать какой-нибудь серьезный веб-сервер (например, такой) для полноценного использования этого скрипта.
Если аппаратная часть сервера и скорость сети позволит — можно пустить в веб и трансляцию основного потока, но мои эксперименты с FullHD не увенчались успехом.
Для ограничения доступа можете прикрутить логинку или же предоставлять доступ на основе доменных групп безопасности, но это уже тема совсем другого поста…