视频流推送优化

This commit is contained in:
root 2025-07-30 13:47:47 +08:00
parent 7d9f44d124
commit 2da25a0437
5 changed files with 258 additions and 34 deletions

View File

@ -281,6 +281,7 @@ cd backend
# 启动开发服务器
python main.py --mode development --log-level DEBUG
```
python debug_server.py
**前端开发**:
```bash

View File

@ -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}')

View File

@ -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

View File

@ -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);
};

71
test_dynamic_video.py Normal file
View File

@ -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()