视频流推送优化
This commit is contained in:
parent
7d9f44d124
commit
2da25a0437
@ -281,6 +281,7 @@ cd backend
|
|||||||
# 启动开发服务器
|
# 启动开发服务器
|
||||||
python main.py --mode development --log-level DEBUG
|
python main.py --mode development --log-level DEBUG
|
||||||
```
|
```
|
||||||
|
python debug_server.py
|
||||||
|
|
||||||
**前端开发**:
|
**前端开发**:
|
||||||
```bash
|
```bash
|
||||||
|
178
backend/app.py
178
backend/app.py
@ -20,6 +20,8 @@ from flask_socketio import SocketIO, emit
|
|||||||
import cv2
|
import cv2
|
||||||
import base64
|
import base64
|
||||||
import configparser
|
import configparser
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
import queue
|
||||||
|
|
||||||
# 添加当前目录到Python路径
|
# 添加当前目录到Python路径
|
||||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||||
@ -63,6 +65,46 @@ current_detection = None
|
|||||||
detection_thread = None
|
detection_thread = None
|
||||||
rtsp_thread = None
|
rtsp_thread = None
|
||||||
rtsp_running = False
|
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():
|
def init_app():
|
||||||
"""初始化应用"""
|
"""初始化应用"""
|
||||||
@ -642,54 +684,140 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
# ==================== WebSocket 实时推送RTSP帧 ====================
|
# ==================== 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():
|
def generate_rtsp_frames():
|
||||||
global rtsp_running
|
global rtsp_running
|
||||||
frame_count = 0
|
frame_count = 0
|
||||||
error_count = 0
|
error_count = 0
|
||||||
|
use_test_mode = False
|
||||||
|
last_frame_time = time.time()
|
||||||
|
|
||||||
logger.info(f'开始生成RTSP帧,URL: {rtsp_url}')
|
logger.info(f'开始生成RTSP帧,URL: {rtsp_url}')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cap = cv2.VideoCapture(rtsp_url)
|
cap = cv2.VideoCapture(rtsp_url)
|
||||||
if not cap.isOpened():
|
if not cap.isOpened():
|
||||||
logger.error(f'无法打开RTSP流: {rtsp_url}')
|
logger.warning(f'无法打开RTSP流: {rtsp_url},切换到测试模式')
|
||||||
socketio.emit('rtsp_status', {'status': 'error', 'message': f'无法打开RTSP流: {rtsp_url}'})
|
use_test_mode = True
|
||||||
return
|
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
|
rtsp_running = True
|
||||||
|
|
||||||
|
# 启动帧编码工作线程
|
||||||
|
encoding_thread = threading.Thread(target=frame_encoding_worker)
|
||||||
|
encoding_thread.daemon = True
|
||||||
|
encoding_thread.start()
|
||||||
|
|
||||||
while rtsp_running:
|
while rtsp_running:
|
||||||
ret, frame = cap.read()
|
if use_test_mode:
|
||||||
if not ret:
|
# 使用测试模式生成帧
|
||||||
error_count += 1
|
frame = generate_test_frame(frame_count)
|
||||||
logger.warning(f'RTSP读取帧失败(第{error_count}次),尝试重连...')
|
ret = True
|
||||||
cap.release()
|
else:
|
||||||
|
# 使用RTSP流,添加帧跳过机制减少延迟
|
||||||
if error_count > 5:
|
ret, frame = cap.read()
|
||||||
logger.error('RTSP连接失败次数过多,停止推流')
|
if not ret:
|
||||||
socketio.emit('rtsp_status', {'status': 'error', 'message': 'RTSP连接失败次数过多'})
|
error_count += 1
|
||||||
break
|
logger.warning(f'RTSP读取帧失败(第{error_count}次),尝试重连...')
|
||||||
|
if 'cap' in locals():
|
||||||
|
cap.release()
|
||||||
|
|
||||||
time.sleep(1)
|
if error_count > 5:
|
||||||
cap = cv2.VideoCapture(rtsp_url)
|
logger.warning('RTSP连接失败次数过多,切换到测试模式')
|
||||||
continue
|
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
|
frame_count += 1
|
||||||
|
|
||||||
try:
|
try:
|
||||||
_, buffer = cv2.imencode('.jpg', frame)
|
# 将帧放入队列进行异步处理
|
||||||
jpg_as_text = base64.b64encode(buffer).decode('utf-8')
|
try:
|
||||||
socketio.emit('rtsp_frame', {'image': jpg_as_text})
|
# 非阻塞方式放入队列,如果队列满了就丢弃旧帧
|
||||||
|
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秒)记录一次
|
if frame_count % 60 == 0: # 每60帧记录一次
|
||||||
logger.info(f'已推送 {frame_count} 帧')
|
logger.info(f'已推送 {frame_count} 帧到编码队列,图像尺寸: {frame.shape[:2]}')
|
||||||
|
|
||||||
except Exception as e:
|
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:
|
except Exception as e:
|
||||||
logger.error(f'RTSP推流异常: {e}')
|
logger.error(f'RTSP推流异常: {e}')
|
||||||
|
@ -41,5 +41,5 @@ max_login_attempts = 5
|
|||||||
|
|
||||||
|
|
||||||
[CAMERA]
|
[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;
|
height: auto;
|
||||||
border: 2px solid #ddd;
|
border: 2px solid #ddd;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
/* 防止图像缓存 */
|
||||||
|
image-rendering: auto;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
.log {
|
.log {
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
@ -168,7 +174,7 @@
|
|||||||
|
|
||||||
// 监听所有事件(调试用)
|
// 监听所有事件(调试用)
|
||||||
socket.onAny((eventName, ...args) => {
|
socket.onAny((eventName, ...args) => {
|
||||||
log(`📨 收到事件: ${eventName}, 数据: ${JSON.stringify(args)}`);
|
// log(`📨 收到事件: ${eventName}, 数据: ${JSON.stringify(args)}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听RTSP状态事件
|
// 监听RTSP状态事件
|
||||||
@ -190,6 +196,7 @@
|
|||||||
if (frameCount % 30 === 0) { // 每30帧记录一次
|
if (frameCount % 30 === 0) { // 每30帧记录一次
|
||||||
log(`🎬 已接收 ${frameCount} 帧`);
|
log(`🎬 已接收 ${frameCount} 帧`);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
log('⚠️ 收到rtsp_frame事件但无图像数据: ' + JSON.stringify(data));
|
log('⚠️ 收到rtsp_frame事件但无图像数据: ' + JSON.stringify(data));
|
||||||
}
|
}
|
||||||
@ -241,8 +248,7 @@
|
|||||||
// 设置超时检查
|
// 设置超时检查
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (frameCount === 0) {
|
if (frameCount === 0) {
|
||||||
log('⏰ 5秒后仍未收到视频帧,可能存在问题');
|
log('⏰ 5秒后仍未收到视频帧,可能存在问题');
|
||||||
checkBackendStatus();
|
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|
||||||
@ -271,9 +277,28 @@
|
|||||||
const img = document.getElementById('rtspImage');
|
const img = document.getElementById('rtspImage');
|
||||||
const noVideo = document.getElementById('noVideo');
|
const noVideo = document.getElementById('noVideo');
|
||||||
|
|
||||||
img.src = 'data:image/jpeg;base64,' + base64Image;
|
const dataUrl = 'data:image/jpeg;base64,' + base64Image;
|
||||||
img.style.display = 'block';
|
|
||||||
noVideo.style.display = 'none';
|
// 创建新的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() {
|
function hideVideo() {
|
||||||
@ -366,8 +391,7 @@
|
|||||||
log('⚠️ 请确保后端服务已启动');
|
log('⚠️ 请确保后端服务已启动');
|
||||||
|
|
||||||
// 自动检查后端状态
|
// 自动检查后端状态
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
checkBackendStatus();
|
|
||||||
testSocketConnection();
|
testSocketConnection();
|
||||||
}, 1000);
|
}, 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