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