From 2da25a0437a19f3011d0ec022ef0a34b5e40179c Mon Sep 17 00:00:00 2001 From: root <13910913995@163.com> Date: Wed, 30 Jul 2025 13:47:47 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A7=86=E9=A2=91=E6=B5=81=E6=8E=A8=E9=80=81?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + backend/app.py | 178 +++++++++++++++++++++++++++----- backend/config.ini | 2 +- frontend_websocket_example.html | 40 +++++-- test_dynamic_video.py | 71 +++++++++++++ 5 files changed, 258 insertions(+), 34 deletions(-) create mode 100644 test_dynamic_video.py diff --git a/README.md b/README.md index b129be2c..f68f5d16 100644 --- a/README.md +++ b/README.md @@ -281,6 +281,7 @@ cd backend # 启动开发服务器 python main.py --mode development --log-level DEBUG ``` +python debug_server.py **前端开发**: ```bash diff --git a/backend/app.py b/backend/app.py index 281aa9ee..f0e4e0f4 100644 --- a/backend/app.py +++ b/backend/app.py @@ -20,6 +20,8 @@ from flask_socketio import SocketIO, emit import cv2 import base64 import configparser +from concurrent.futures import ThreadPoolExecutor +import queue # 添加当前目录到Python路径 sys.path.append(os.path.dirname(os.path.abspath(__file__))) @@ -63,6 +65,46 @@ current_detection = None detection_thread = None rtsp_thread = None rtsp_running = False +# 用于异步编码的线程池和队列 +encoding_executor = ThreadPoolExecutor(max_workers=2) +frame_queue = queue.Queue(maxsize=1) # 只保留最新的一帧 + +def async_encode_frame(frame, frame_count): + """异步编码帧""" + try: + # 优化图像尺寸:平衡质量和性能 + height, width = frame.shape[:2] + if width > 480: # 适度压缩到480宽度,保持更好的图像质量 + scale = 480 / width + new_width = 480 + new_height = int(height * scale) + frame = cv2.resize(frame, (new_width, new_height), interpolation=cv2.INTER_LINEAR) # 使用更好的插值方法 + + # 优化JPEG编码参数:平衡质量和速度 + encode_param = [ + int(cv2.IMWRITE_JPEG_QUALITY), 75, # 提高质量到75,保持较好的图像效果 + int(cv2.IMWRITE_JPEG_OPTIMIZE), 1 # 启用优化以获得更好的压缩效果 + ] + _, buffer = cv2.imencode('.jpg', frame, encode_param) + jpg_as_text = base64.b64encode(buffer).decode('utf-8') + + socketio.emit('rtsp_frame', {'image': jpg_as_text, 'frame_id': frame_count}) + + except Exception as e: + logger.error(f'异步编码帧失败: {e}') + +def frame_encoding_worker(): + """帧编码工作线程""" + while rtsp_running: + try: + # 从队列获取帧 + frame, frame_count = frame_queue.get(timeout=1) + # 提交到线程池进行异步编码 + encoding_executor.submit(async_encode_frame, frame, frame_count) + except queue.Empty: + continue + except Exception as e: + logger.error(f'帧编码工作线程异常: {e}') def init_app(): """初始化应用""" @@ -642,54 +684,140 @@ if __name__ == '__main__': # ==================== WebSocket 实时推送RTSP帧 ==================== +def generate_test_frame(frame_count): + """生成测试帧""" + import numpy as np + width, height = 640, 480 + + # 创建黑色背景 + frame = np.zeros((height, width, 3), dtype=np.uint8) + + # 添加动态元素 + timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3] + + # 添加时间戳 + cv2.putText(frame, timestamp, (10, 90), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 2) + + # 添加帧计数 + cv2.putText(frame, f'TEST Frame: {frame_count}', (10, 120), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 2) + + # 添加移动的圆形 + center_x = int(320 + 200 * np.sin(frame_count * 0.1)) + center_y = int(240 + 100 * np.cos(frame_count * 0.1)) + cv2.circle(frame, (center_x, center_y), 30, (255, 0, 0), -1) + + # 添加变化的矩形 + rect_size = int(50 + 30 * np.sin(frame_count * 0.05)) + cv2.rectangle(frame, (500, 200), (500 + rect_size, 200 + rect_size), (0, 0, 255), -1) + + return frame + def generate_rtsp_frames(): global rtsp_running frame_count = 0 error_count = 0 + use_test_mode = False + last_frame_time = time.time() logger.info(f'开始生成RTSP帧,URL: {rtsp_url}') try: cap = cv2.VideoCapture(rtsp_url) if not cap.isOpened(): - logger.error(f'无法打开RTSP流: {rtsp_url}') - socketio.emit('rtsp_status', {'status': 'error', 'message': f'无法打开RTSP流: {rtsp_url}'}) - return + logger.warning(f'无法打开RTSP流: {rtsp_url},切换到测试模式') + use_test_mode = True + socketio.emit('rtsp_status', {'status': 'started', 'message': '使用测试视频源'}) + else: + # 最激进的实时优化设置 + cap.set(cv2.CAP_PROP_BUFFERSIZE, 0) # 完全禁用缓冲区 + cap.set(cv2.CAP_PROP_FPS, 60) # 提高帧率到60fps + cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('M', 'J', 'P', 'G')) # MJPEG编码 + # 设置更低的分辨率以减少处理时间 + cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) + cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) + logger.info('RTSP流已打开,开始推送帧(激进实时模式)') + socketio.emit('rtsp_status', {'status': 'started', 'message': '使用RTSP视频源(激进实时模式)'}) - logger.info('RTSP流已打开,开始推送帧') rtsp_running = True + # 启动帧编码工作线程 + encoding_thread = threading.Thread(target=frame_encoding_worker) + encoding_thread.daemon = True + encoding_thread.start() + while rtsp_running: - ret, frame = cap.read() - if not ret: - error_count += 1 - logger.warning(f'RTSP读取帧失败(第{error_count}次),尝试重连...') - cap.release() - - if error_count > 5: - logger.error('RTSP连接失败次数过多,停止推流') - socketio.emit('rtsp_status', {'status': 'error', 'message': 'RTSP连接失败次数过多'}) - break + if use_test_mode: + # 使用测试模式生成帧 + frame = generate_test_frame(frame_count) + ret = True + else: + # 使用RTSP流,添加帧跳过机制减少延迟 + ret, frame = cap.read() + if not ret: + error_count += 1 + logger.warning(f'RTSP读取帧失败(第{error_count}次),尝试重连...') + if 'cap' in locals(): + cap.release() - time.sleep(1) - cap = cv2.VideoCapture(rtsp_url) - continue + if error_count > 5: + logger.warning('RTSP连接失败次数过多,切换到测试模式') + use_test_mode = True + socketio.emit('rtsp_status', {'status': 'switched', 'message': '已切换到测试视频源'}) + continue + + # 立即重连,不等待 + cap = cv2.VideoCapture(rtsp_url) + if cap.isOpened(): + # 重连时应用相同的激进实时设置 + cap.set(cv2.CAP_PROP_BUFFERSIZE, 0) + cap.set(cv2.CAP_PROP_FPS, 60) + cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('M', 'J', 'P', 'G')) + cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) + cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) + continue + + error_count = 0 # 重置错误计数 - error_count = 0 # 重置错误计数 + # 最激进的帧跳过策略:大量跳过旧帧,确保绝对实时性 + # 连续读取并丢弃多帧,只保留最新的一帧 + skip_count = 0 + while skip_count < 10: # 最多跳过10帧 + temp_ret, temp_frame = cap.read() + if temp_ret: + frame = temp_frame + skip_count += 1 + else: + break + + # 时间同步检查:如果距离上次处理时间太短,跳过这一帧 + current_time = time.time() + if current_time - last_frame_time < 1/60: # 保持60fps的最大频率 + continue + last_frame_time = current_time + frame_count += 1 try: - _, buffer = cv2.imencode('.jpg', frame) - jpg_as_text = base64.b64encode(buffer).decode('utf-8') - socketio.emit('rtsp_frame', {'image': jpg_as_text}) + # 将帧放入队列进行异步处理 + try: + # 非阻塞方式放入队列,如果队列满了就丢弃旧帧 + frame_queue.put_nowait((frame.copy(), frame_count)) + except queue.Full: + # 队列满了,清空队列并放入新帧 + try: + frame_queue.get_nowait() + except queue.Empty: + pass + frame_queue.put_nowait((frame.copy(), frame_count)) - if frame_count % 150 == 0: # 每150帧(约10秒)记录一次 - logger.info(f'已推送 {frame_count} 帧') + if frame_count % 60 == 0: # 每60帧记录一次 + logger.info(f'已推送 {frame_count} 帧到编码队列,图像尺寸: {frame.shape[:2]}') except Exception as e: - logger.error(f'编码帧失败: {e}') + logger.error(f'帧队列处理失败: {e}') - time.sleep(1/15) # 推送帧率可调 + # 移除sleep以获得最大处理速度,让系统自然限制帧率 + # time.sleep(1/60) # 注释掉sleep以获得最大实时性 except Exception as e: logger.error(f'RTSP推流异常: {e}') diff --git a/backend/config.ini b/backend/config.ini index 92b97002..044e5a40 100644 --- a/backend/config.ini +++ b/backend/config.ini @@ -41,5 +41,5 @@ max_login_attempts = 5 [CAMERA] -rtsp_url = rtsp://admin:password@192.168.1.100:554/Streaming/Channels/101 +rtsp_url = rtsp://admin:JY123456@192.168.1.61:554/Streaming/Channels/101 diff --git a/frontend_websocket_example.html b/frontend_websocket_example.html index ea7558e6..6085a246 100644 --- a/frontend_websocket_example.html +++ b/frontend_websocket_example.html @@ -51,6 +51,12 @@ height: auto; border: 2px solid #ddd; border-radius: 4px; + /* 防止图像缓存 */ + image-rendering: auto; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } .log { background-color: #f8f9fa; @@ -168,7 +174,7 @@ // 监听所有事件(调试用) socket.onAny((eventName, ...args) => { - log(`📨 收到事件: ${eventName}, 数据: ${JSON.stringify(args)}`); + // log(`📨 收到事件: ${eventName}, 数据: ${JSON.stringify(args)}`); }); // 监听RTSP状态事件 @@ -190,6 +196,7 @@ if (frameCount % 30 === 0) { // 每30帧记录一次 log(`🎬 已接收 ${frameCount} 帧`); } + } else { log('⚠️ 收到rtsp_frame事件但无图像数据: ' + JSON.stringify(data)); } @@ -241,8 +248,7 @@ // 设置超时检查 setTimeout(() => { if (frameCount === 0) { - log('⏰ 5秒后仍未收到视频帧,可能存在问题'); - checkBackendStatus(); + log('⏰ 5秒后仍未收到视频帧,可能存在问题'); } }, 5000); @@ -271,9 +277,28 @@ const img = document.getElementById('rtspImage'); const noVideo = document.getElementById('noVideo'); - img.src = 'data:image/jpeg;base64,' + base64Image; - img.style.display = 'block'; - noVideo.style.display = 'none'; + const dataUrl = 'data:image/jpeg;base64,' + base64Image; + + // 创建新的Image对象来预加载,确保图像能正确显示 + const newImg = new Image(); + newImg.onload = function() { + img.src = dataUrl; + img.style.display = 'block'; + noVideo.style.display = 'none'; + + // 强制重绘 + img.style.transform = 'scale(1.001)'; + setTimeout(() => { + img.style.transform = 'scale(1)'; + }, 1); + }; + + newImg.onerror = function() { + console.error('图像加载失败'); + log('❌ 图像加载失败,可能是base64数据损坏'); + }; + + newImg.src = dataUrl; } function hideVideo() { @@ -366,8 +391,7 @@ log('⚠️ 请确保后端服务已启动'); // 自动检查后端状态 - setTimeout(() => { - checkBackendStatus(); + setTimeout(() => { testSocketConnection(); }, 1000); }; diff --git a/test_dynamic_video.py b/test_dynamic_video.py new file mode 100644 index 00000000..f1110280 --- /dev/null +++ b/test_dynamic_video.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +测试动态视频生成脚本 +用于验证RTSP帧是否真的在变化 +""" + +import cv2 +import numpy as np +import time +from datetime import datetime + +def create_test_video_source(): + """ + 创建一个测试视频源,生成动态变化的图像 + """ + # 创建一个640x480的黑色背景 + width, height = 640, 480 + + frame_count = 0 + + while True: + # 创建黑色背景 + frame = np.zeros((height, width, 3), dtype=np.uint8) + + # 添加动态元素 + timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3] + + # 添加时间戳 + cv2.putText(frame, timestamp, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) + + # 添加帧计数 + cv2.putText(frame, f'Frame: {frame_count}', (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) + + # 添加移动的圆形 + center_x = int(320 + 200 * np.sin(frame_count * 0.1)) + center_y = int(240 + 100 * np.cos(frame_count * 0.1)) + cv2.circle(frame, (center_x, center_y), 30, (255, 0, 0), -1) + + # 添加变化的矩形 + rect_size = int(50 + 30 * np.sin(frame_count * 0.05)) + cv2.rectangle(frame, (500, 200), (500 + rect_size, 200 + rect_size), (0, 0, 255), -1) + + # 添加随机噪点 + noise = np.random.randint(0, 50, (height, width, 3), dtype=np.uint8) + frame = cv2.add(frame, noise) + + frame_count += 1 + + yield frame + time.sleep(1/30) # 30 FPS + +def test_rtsp_replacement(): + """ + 测试用动态视频源替换RTSP + """ + print("开始生成测试视频源...") + print("按 'q' 键退出") + + video_source = create_test_video_source() + + for frame in video_source: + cv2.imshow('Test Video Source', frame) + + if cv2.waitKey(1) & 0xFF == ord('q'): + break + + cv2.destroyAllWindows() + +if __name__ == '__main__': + test_rtsp_replacement() \ No newline at end of file