RTSP web view issue

I am trying to render an RTSP camera feed in a Flask template.

When I run it locally on Windows, the video works perfectly. However, the problem occurs when I try to run it on my Linux server.

On the server, the video runs for approximately 10 seconds and then freezes. After that, I need to disconnect and reconnect to the stream for it to work again.

Has anyone faced this issue and managed to solve it?

PS: Linux: Debian 12.

threaded_camera.py

import cv2
import time
import logging
import os
from threading import Thread, Event
from urllib.parse import urlparse


logging.basicConfig(level=logging.INFO)

class ThreadedCamera(object):
    def __init__(self, src):
        self.capture = cv2.VideoCapture(src, cv2.CAP_FFMPEG)
        if not self.capture.isOpened():
            print(f"Failed to open stream: {src}")
        self.src = src
        self.hostname = self.extract_hostname(src)
        self.capture.set(cv2.CAP_PROP_BUFFERSIZE, 1)
        self.capture.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
        self.capture.set(cv2.CAP_PROP_FPS, 30)
        self.min_fps = 10
        self.FPS = 1/30
        self.FPS_MS = int(self.FPS * 1000)
        self.status, self.frame = self.capture.read()
        self.frame_count = 0
        self.total_time = 0
        self.origin_info = {
            "width": 0,
            "height": 0,
            "fps": 0,
            "codec": '',
            "latency": False,
            "processing_time": 0
        }
        self.transcoded_info = {
            "width": 640,
            "height": 480,
            "fps": 30,
            "codec": 'mjpg',
            "latency": False,
            "processing_time": 0
        }

        # Start frame retrieval thread
        self.thread = Thread(target=self.update, args=())
        self.thread.daemon = True
        self.thread.start()

    def extract_hostname(self, url):
        parsed_url = urlparse(url)
        return parsed_url.hostname

    def update(self):
        while True:
            if self.capture.isOpened():
                start_time = time.time()
                self.status, self.frame = self.capture.read()
                network_latency = self.check_network_latency(self.hostname)
                end_time = time.time()

                logging.info(f"Status: {self.status}, Start Time: {start_time}, End Time: {end_time}, Duration: {end_time - start_time}")

                self.frame_count += 1
                self.total_time += (end_time - start_time)


                if self.status:
                    # self.frame_count += 1
                    # self.total_time += (end_time - start_time)

                    logging.info(f"Frame Count: {self.frame_count}, Total Time: {self.total_time}")

                    # Update origin info
                    self.origin_info['width'] = self.capture.get(cv2.CAP_PROP_FRAME_WIDTH)
                    self.origin_info['height'] = self.capture.get(cv2.CAP_PROP_FRAME_HEIGHT)
                    self.origin_info['fps'] = self.capture.get(cv2.CAP_PROP_FPS)
                    codec = int(self.capture.get(cv2.CAP_PROP_FOURCC))
                    self.origin_info['codec'] = ''.join([chr((codec >> 8 * i) & 0xFF) for i in range(4)])
                    # self.origin_info['latency'] = self.check_network_latency(self.hostname)
                    self.origin_info['latency'] = network_latency

                if self.frame_count % int(self.capture.get(cv2.CAP_PROP_FPS)) == 0:
                    logging.info(f"Frame Count Reached FPS Interval: {self.frame_count}")
                    avg_processing_time = self.total_time / self.frame_count if self.frame_count > 0 else 0
                    # network_latency = self.check_network_latency(self.hostname)

                    logging.info(f"Average Processing Time: {avg_processing_time}, Network Latency: {network_latency}")

                    self.adjust_stream_settings(avg_processing_time, network_latency)
                    # self.adjust_stream_settings(avg_processing_time, 0 )
                    self.transcoded_info['latency'] = network_latency
                    self.transcoded_info['processing_time'] = round(avg_processing_time * 1000, 4)

                    logging.info(f"Transcoded Info Updated: {self.transcoded_info}")

                    # Reset frame count and total time for next calculation period
                    self.frame_count = 0
                    self.total_time = 0

                time.sleep(self.FPS)

    def check_network_latency(self, hostname):
        # response = os.system(f"ping -c 1 {hostname}")
        # return response == 0
        return 1

    def get_frame(self):
        if self.frame is not None:
            self.frame = self.resize_frame(self.frame, width=640)
            _, buffer = cv2.imencode('.webp', self.frame)
            self.transcoded_info['width'] = self.frame.shape[1]
            self.transcoded_info['height'] = self.frame.shape[0]
            return buffer.tobytes()
        return None

    def stop(self):
        self.stopped.set()
        self.thread.join()
        self.capture.release()

    def resize_frame(self, frame, width):
        height = int(width * (frame.shape[0] / frame.shape[1]))
        return cv2.resize(frame, (width, height))


    def adjust_stream_settings(self, avg_processing_time, network_latency):
        current_fps = self.capture.get(cv2.CAP_PROP_FPS)
        if avg_processing_time > 0.1 or not network_latency:
            new_fps = max(self.min_fps, current_fps - 5)
        else:
            new_fps = current_fps + 5  # Incremento mais suave

        self.capture.set(cv2.CAP_PROP_FPS, new_fps)
        # Ensure minimum FPS
        if self.capture.get(cv2.CAP_PROP_FPS) < self.min_fps:
            self.capture.set(cv2.CAP_PROP_FPS, self.min_fps)
        self.FPS = 1 / self.capture.get(cv2.CAP_PROP_FPS)
        self.FPS_MS = int(self.FPS * 1000)

    def get_origin_stream_info(self):
        logging.info(f"get_origin_stream_info: {self.transcoded_info}")
        return self.origin_info

    def get_transcoded_stream_info(self):
        logging.info(f"get_transcoded_stream_info: {self.transcoded_info}")
        return self.transcoded_info

stream_routes.py

from flask import Blueprint, Response, request, stream_with_context, current_app, jsonify, redirect, url_for
from threaded_camera import ThreadedCamera
import logging

bp = Blueprint('stream', __name__)
cameras = {}

@bp.route('/stream')
def stream():
    try:
        rtsp_url = request.args.get('rtsp_url')
        if not rtsp_url:
            return "RTSP URL not set", 400

        if rtsp_url not in cameras:
            cameras[rtsp_url] = ThreadedCamera(rtsp_url)

        camera = cameras[rtsp_url]

        def generate():
            while True:
                try:
                    frame = camera.get_frame()
                    if frame:
                        yield (b'--frame\r\n'
                               b'Content-Type: image/webp\r\n\r\n' + frame + b'\r\n')
                except Exception as e:
                    current_app.logger.error(f"Error generating frame: {e}")
                    break

        return Response(stream_with_context(generate()), mimetype='multipart/x-mixed-replace; boundary=frame')

    except Exception as e:
        current_app.logger.error(f"Error in /stream route: {e}")
        return "Internal Server Error", 500

@bp.route('/stop')
def stop_stream():
    try:
        rtsp_url = request.args.get('rtsp_url')
        if not rtsp_url:
            return "RTSP URL not set", 400

        if rtsp_url in cameras:
            cameras[rtsp_url].stop()
            del cameras[rtsp_url]

        return redirect(url_for('view_stream'))
    except Exception as e:
        current_app.logger.error(f"Error in /stop route: {e}")
        return redirect(url_for('view_stream'))

@bp.route('/stream_info')
def stream_info():
    try:
        rtsp_url = request.args.get('rtsp_url')
        if not rtsp_url:
            return jsonify(error="RTSP URL not set"), 400

        camera = ThreadedCamera(rtsp_url)
        origin_info = camera.get_origin_stream_info()
        trans_info = camera.get_transcoded_stream_info()

        return jsonify(
            origin=origin_info,
            transcoded=trans_info
        )
    except Exception as e:
        logging.error(f"Error in stream_info: {e}")
        return jsonify(error="Internal Server Error"), 500

app.py

from flask import Flask, request, render_template, redirect, url_for, session
from werkzeug.utils import secure_filename
import os
import json

app = Flask(__name__)

# Configurações de upload
app.secret_key = ''
app.config['UPLOAD_FOLDER'] = ''
app.config['MAX_CONTENT_LENGTH'] = 1024 * 1024 * 1024  # 1GB

ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'mp4', 'mov', 'avi'}

def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route('/')
def home():
    return 'Hello World'

@app.route('/index')
def index():
    return '<h1>Hello World</h1>'

@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        if 'video' not in request.files:
            return 'No file part'
        file = request.files['video']
        if file.filename == '':
            return 'No selected file'
        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)
            file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
            return 'File successfully uploaded'
        else:
            return 'File type not allowed'
    return render_template('upload.html')

@app.route('/view_stream', methods=['GET', 'POST'])
def view_stream():
    if request.method == 'POST':
        session['rtsp_url'] = request.form['rtsp_url']
        return redirect(url_for('view_stream'))
    return render_template('view_stream.html', rtsp_url=session.get('rtsp_url'), stream_info_url=url_for('stream.stream_info'))

@app.route('/set_rtsp', methods=['POST'])
def set_rtsp():
    session['rtsp_url'] = request.form['rtsp_url']
    return redirect(url_for('view_stream'))

@app.route('/stop_stream')
def stop_stream():
    session.pop('rtsp_url', None)
    return redirect(url_for('view_stream'))

# Registrar o Blueprint para as rotas de stream
from stream_routes import bp as stream_bp
app.register_blueprint(stream_bp, url_prefix='/')

if __name__ == '__main__':
    app.run(debug=True)

Guicorn Logs

(ivia_dev) root@srv527791:/var/www/ivia_dev# journalctl -u app.service -f
Dec 09 04:28:11 srv527791 systemd[1]: Started app.service - Gunicorn instance to serve your application.
Dec 09 04:28:11 srv527791 gunicorn[469]: [2024-12-09 04:28:11 +0000] [469] [INFO] Starting gunicorn 23.0.0
Dec 09 04:28:11 srv527791 gunicorn[469]: [2024-12-09 04:28:11 +0000] [469] [INFO] Listening at: unix:/var/www/ivia_dev/app.sock (469)
Dec 09 04:28:11 srv527791 gunicorn[469]: [2024-12-09 04:28:11 +0000] [469] [INFO] Using worker: sync
Dec 09 04:28:11 srv527791 gunicorn[493]: [2024-12-09 04:28:11 +0000] [493] [INFO] Booting worker with pid: 493
Dec 09 04:28:11 srv527791 gunicorn[495]: [2024-12-09 04:28:11 +0000] [495] [INFO] Booting worker with pid: 495
Dec 09 04:28:11 srv527791 gunicorn[500]: [2024-12-09 04:28:11 +0000] [500] [INFO] Booting worker with pid: 500
Dec 09 04:30:32 srv527791 gunicorn[469]: [2024-12-09 04:30:32 +0000] [469] [CRITICAL] WORKER TIMEOUT (pid:493)
Dec 09 04:30:32 srv527791 gunicorn[493]: [2024-12-09 04:30:32 +0000] [493] [ERROR] Error handling request /stream?rtsp_url=https://obrasaovivo.sinfra.mt.gov.br/contorno/index.m3u8
Dec 09 04:30:32 srv527791 gunicorn[493]: Traceback (most recent call last):
Dec 09 04:30:32 srv527791 gunicorn[493]:   File "/var/www/ivia_dev/ivia_dev/lib/python3.11/site-packages/gunicorn/workers/sync.py", line 134, in handle
Dec 09 04:30:32 srv527791 gunicorn[493]:     self.handle_request(listener, req, client, addr)
Dec 09 04:30:32 srv527791 gunicorn[493]:   File "/var/www/ivia_dev/ivia_dev/lib/python3.11/site-packages/gunicorn/workers/sync.py", line 182, in handle_request
Dec 09 04:30:32 srv527791 gunicorn[493]:     for item in respiter:
Dec 09 04:30:32 srv527791 gunicorn[493]:   File "/var/www/ivia_dev/ivia_dev/lib/python3.11/site-packages/werkzeug/wsgi.py", line 256, in __next__
Dec 09 04:30:32 srv527791 gunicorn[493]:     return self._next()
Dec 09 04:30:32 srv527791 gunicorn[493]:            ^^^^^^^^^^^^
Dec 09 04:30:32 srv527791 gunicorn[493]:   File "/var/www/ivia_dev/ivia_dev/lib/python3.11/site-packages/werkzeug/wrappers/response.py", line 32, in _iter_encoded
Dec 09 04:30:32 srv527791 gunicorn[493]:     for item in iterable:
Dec 09 04:30:32 srv527791 gunicorn[493]:   File "/var/www/ivia_dev/ivia_dev/lib/python3.11/site-packages/flask/helpers.py", line 113, in generator
Dec 09 04:30:32 srv527791 gunicorn[493]:     yield from gen
Dec 09 04:30:32 srv527791 gunicorn[493]:   File "/var/www/ivia_dev/stream_routes.py", line 24, in generate
Dec 09 04:30:32 srv527791 gunicorn[493]:     frame = camera.get_frame()
Dec 09 04:30:32 srv527791 gunicorn[493]:             ^^^^^^^^^^^^^^^^^^
Dec 09 04:30:32 srv527791 gunicorn[493]:   File "/var/www/ivia_dev/threaded_camera.py", line 110, in get_frame
Dec 09 04:30:32 srv527791 gunicorn[493]:     _, buffer = cv2.imencode('.webp', self.frame)
Dec 09 04:30:32 srv527791 gunicorn[493]:                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Dec 09 04:30:32 srv527791 gunicorn[493]:   File "/var/www/ivia_dev/ivia_dev/lib/python3.11/site-packages/gunicorn/workers/base.py", line 204, in handle_abort
Dec 09 04:30:32 srv527791 gunicorn[493]:     sys.exit(1)
Dec 09 04:30:32 srv527791 gunicorn[493]: SystemExit: 1
Dec 09 04:30:32 srv527791 gunicorn[493]: [2024-12-09 04:30:32 +0000] [493] [INFO] Worker exiting (pid: 493)
Dec 09 04:30:32 srv527791 gunicorn[586]: [2024-12-09 04:30:32 +0000] [586] [INFO] Booting worker with pid: 586

not an OpenCV issue.

that timeout is purely because of all the web stuff in your project.

crosspost:

1 Like

Hi, thanks for your reply.

yes, by increasing the worker’s timeout I can run it for longer,

but I’m getting the following return, and I’m not sure what it means

WARNING:root:Capture is not open. Trying to reopen.
[rtsp @ 00000272d8352dc0] OPTIONS method failed: 404 not found
WARNING:root:Capture was not opened. Trying to reopen.
[rtsp @ 00000272d8352dc0] method OPTIONS failed: 404 not found
WARNING:root:Capture is not open. Trying to reopen.
[rtsp @ 00000272d8352dc0] method OPTIONS failed: 404 Not Found

This topic was automatically closed after 20 hours. New replies are no longer allowed.