视频流推送优化
This commit is contained in:
parent
7d9f44d124
commit
2da25a0437
@ -281,6 +281,7 @@ cd backend
|
||||
# 启动开发服务器
|
||||
python main.py --mode development --log-level DEBUG
|
||||
```
|
||||
python debug_server.py
|
||||
|
||||
**前端开发**:
|
||||
```bash
|
||||
|
178
backend/app.py
178
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}')
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
71
test_dynamic_video.py
Normal 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()
|
Loading…
Reference in New Issue
Block a user