Python · 26.06.2026

Улыбнитесь, Вас снимают…

Всех поздравляю с очередной и долгожданной пятницей! Сегодняшний томный вечер будет полностью посвящён (внезапно) видеонаблюдению и совсем маленькому веб-серверу на 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 не увенчались успехом.

Для ограничения доступа можете прикрутить логинку или же предоставлять доступ на основе доменных групп безопасности, но это уже тема совсем другого поста…