提交我的更新内容
This commit is contained in:
parent
ffc269331a
commit
85d9ace324
41
.gitignore
vendored
Normal file
41
.gitignore
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
# 日志文件
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# 患者数据文件
|
||||
data/patients/*/
|
||||
!data/patients/.gitkeep
|
||||
|
||||
# Node.js 依赖
|
||||
frontend/src/renderer/node_modules/
|
||||
|
||||
# Python 缓存文件
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
|
||||
# 数据库文件(如果不需要版本控制)
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# 环境配置文件
|
||||
.env
|
||||
.env.local
|
||||
|
||||
# IDE 文件
|
||||
.vscode/settings.json
|
||||
.idea/
|
||||
|
||||
# 系统文件
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# 构建输出
|
||||
dist/
|
||||
build/
|
||||
|
||||
# 临时文件
|
||||
*.tmp
|
||||
*.temp
|
@ -124,8 +124,8 @@ def handle_connect():
|
||||
|
||||
### 2. RTSP流问题
|
||||
```python
|
||||
# 在generate_rtsp_frames函数中设置断点
|
||||
def generate_rtsp_frames():
|
||||
# 在generate_video_frames函数中设置断点
|
||||
def generate_video_frames():
|
||||
print(f'RTSP URL: {rtsp_url}') # 调试输出
|
||||
# 设置断点检查rtsp_url值
|
||||
cap = cv2.VideoCapture(rtsp_url)
|
||||
@ -177,7 +177,7 @@ logging.getLogger().setLevel(logging.DEBUG)
|
||||
// 在浏览器控制台中测试WebSocket连接
|
||||
const socket = io('http://127.0.0.1:5000');
|
||||
socket.on('connect', () => console.log('连接成功'));
|
||||
socket.emit('start_rtsp', {});
|
||||
socket.emit('start_video', {});
|
||||
```
|
||||
|
||||
## 性能调试
|
||||
|
@ -1,226 +0,0 @@
|
||||
# 内存优化指南
|
||||
|
||||
## 概述
|
||||
|
||||
本指南介绍了身体平衡评估系统中WebSocket视频流的内存优化功能,帮助您解决浏览器内存占用缓慢增长的问题。
|
||||
|
||||
## 问题描述
|
||||
|
||||
在长时间运行WebSocket视频流时,可能会遇到以下问题:
|
||||
- 浏览器内存使用量持续增长
|
||||
- 页面响应速度逐渐变慢
|
||||
- 系统资源占用过高
|
||||
- 可能导致浏览器崩溃
|
||||
|
||||
## 优化方案
|
||||
|
||||
### 1. 前端优化
|
||||
|
||||
#### 1.1 帧率控制
|
||||
- **最大FPS限制**: 默认15fps,可根据需要调整
|
||||
- **跳帧机制**: 每3帧显示1帧,减少处理负担
|
||||
- **自适应帧率**: 根据内存使用情况动态调整
|
||||
|
||||
#### 1.2 内存监控
|
||||
- **实时监控**: 每10秒检查一次内存使用情况
|
||||
- **自动清理**: 内存使用率超过80%时自动垃圾回收
|
||||
- **手动控制**: 提供手动垃圾回收按钮
|
||||
|
||||
#### 1.3 图像缓存优化
|
||||
- **缓存复用**: 重用Image对象,避免频繁创建
|
||||
- **及时清理**: 及时释放不再使用的图像资源
|
||||
- **强制重绘**: 使用requestAnimationFrame优化渲染
|
||||
|
||||
### 2. 后端优化
|
||||
|
||||
#### 2.1 图像压缩
|
||||
- **尺寸压缩**: 最大分辨率320x240
|
||||
- **质量压缩**: JPEG质量设为50%
|
||||
- **编码优化**: 禁用渐进式JPEG,启用优化
|
||||
|
||||
#### 2.2 内存管理
|
||||
- **内存限制**: 后端最大内存使用100MB
|
||||
- **定期检查**: 每50帧检查一次内存使用
|
||||
- **强制清理**: 内存超限时强制垃圾回收
|
||||
|
||||
#### 2.3 帧处理优化
|
||||
- **跳帧处理**: 每3帧处理1帧
|
||||
- **队列管理**: 队列大小限制为1,确保实时性
|
||||
- **及时释放**: 处理完成后立即释放帧内存
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 启动优化版本
|
||||
|
||||
确保使用最新的优化版本:
|
||||
```bash
|
||||
# 启动后端(已包含内存优化)
|
||||
python backend/app.py
|
||||
|
||||
# 打开前端页面
|
||||
# 访问 frontend_websocket_example.html
|
||||
```
|
||||
|
||||
### 2. 监控内存使用
|
||||
|
||||
在前端页面中:
|
||||
1. 点击 **"内存使用情况"** 按钮查看当前内存状态
|
||||
2. 观察以下指标:
|
||||
- 已使用内存
|
||||
- 总分配内存
|
||||
- 内存使用率
|
||||
- 接收帧数和跳帧率
|
||||
|
||||
### 3. 手动优化
|
||||
|
||||
当发现内存使用过高时:
|
||||
1. 点击 **"强制垃圾回收"** 按钮
|
||||
2. 观察内存使用情况的变化
|
||||
3. 如果问题持续,考虑刷新页面
|
||||
|
||||
### 4. 配置调整
|
||||
|
||||
编辑 `memory_config.py` 文件来调整优化参数:
|
||||
|
||||
```python
|
||||
# 前端配置
|
||||
MAX_FPS = 15 # 降低可减少内存使用
|
||||
FRAME_SKIP_THRESHOLD = 2 # 增加可减少处理负担
|
||||
MAX_MEMORY_USAGE_PERCENT = 80 # 调整自动清理阈值
|
||||
|
||||
# 后端配置
|
||||
JPEG_QUALITY = 50 # 降低可减少内存使用
|
||||
MAX_FRAME_WIDTH = 320 # 降低可减少内存使用
|
||||
MAX_BACKEND_MEMORY_MB = 100 # 调整内存限制
|
||||
```
|
||||
|
||||
## 性能调优建议
|
||||
|
||||
### 1. 根据硬件调整
|
||||
|
||||
**低配置设备**:
|
||||
```python
|
||||
MAX_FPS = 10
|
||||
FRAME_SKIP_THRESHOLD = 4
|
||||
JPEG_QUALITY = 40
|
||||
MAX_FRAME_WIDTH = 240
|
||||
```
|
||||
|
||||
**高配置设备**:
|
||||
```python
|
||||
MAX_FPS = 20
|
||||
FRAME_SKIP_THRESHOLD = 1
|
||||
JPEG_QUALITY = 60
|
||||
MAX_FRAME_WIDTH = 480
|
||||
```
|
||||
|
||||
### 2. 根据网络调整
|
||||
|
||||
**慢速网络**:
|
||||
- 降低帧率和质量
|
||||
- 增加跳帧比例
|
||||
- 减小图像尺寸
|
||||
|
||||
**快速网络**:
|
||||
- 可适当提高帧率和质量
|
||||
- 减少跳帧比例
|
||||
|
||||
### 3. 长时间运行优化
|
||||
|
||||
对于需要长时间运行的场景:
|
||||
1. 设置更严格的内存限制
|
||||
2. 增加自动清理频率
|
||||
3. 定期刷新页面(建议每2-4小时)
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 1. 内存仍然增长
|
||||
|
||||
**可能原因**:
|
||||
- 浏览器不支持强制垃圾回收
|
||||
- 其他页面或扩展占用内存
|
||||
- 系统内存不足
|
||||
|
||||
**解决方案**:
|
||||
1. 使用Chrome浏览器并启用 `--enable-precise-memory-info` 标志
|
||||
2. 关闭其他不必要的标签页
|
||||
3. 降低优化参数设置
|
||||
4. 定期刷新页面
|
||||
|
||||
### 2. 视频卡顿
|
||||
|
||||
**可能原因**:
|
||||
- 帧率设置过低
|
||||
- 跳帧比例过高
|
||||
- 网络延迟
|
||||
|
||||
**解决方案**:
|
||||
1. 适当提高MAX_FPS
|
||||
2. 减少FRAME_SKIP_THRESHOLD
|
||||
3. 检查网络连接
|
||||
4. 优化后端处理性能
|
||||
|
||||
### 3. 图像质量差
|
||||
|
||||
**可能原因**:
|
||||
- JPEG质量设置过低
|
||||
- 图像尺寸压缩过度
|
||||
|
||||
**解决方案**:
|
||||
1. 提高JPEG_QUALITY(建议不超过70)
|
||||
2. 增加MAX_FRAME_WIDTH和MAX_FRAME_HEIGHT
|
||||
3. 平衡质量和内存使用
|
||||
|
||||
## 监控和日志
|
||||
|
||||
### 1. 前端监控
|
||||
|
||||
在浏览器控制台中查看:
|
||||
```javascript
|
||||
// 查看内存使用
|
||||
console.log(performance.memory);
|
||||
|
||||
// 查看优化统计
|
||||
console.log('帧数:', frameCount);
|
||||
console.log('跳帧数:', frameSkipCount);
|
||||
```
|
||||
|
||||
### 2. 后端日志
|
||||
|
||||
查看后端日志中的内存相关信息:
|
||||
```
|
||||
[INFO] 内存使用: 45.2MB
|
||||
[WARNING] 内存使用过高: 105.3MB,强制清理
|
||||
[INFO] 垃圾回收完成,内存降至: 38.7MB
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **定期监控**: 每隔一段时间检查内存使用情况
|
||||
2. **合理配置**: 根据实际需求调整优化参数
|
||||
3. **及时清理**: 发现内存异常时及时进行清理
|
||||
4. **版本更新**: 保持使用最新的优化版本
|
||||
5. **硬件匹配**: 根据硬件配置调整参数
|
||||
|
||||
## 技术原理
|
||||
|
||||
### 1. 内存泄漏原因
|
||||
- 频繁创建Image对象
|
||||
- Base64字符串累积
|
||||
- 事件监听器未正确清理
|
||||
- 浏览器垃圾回收不及时
|
||||
|
||||
### 2. 优化原理
|
||||
- 对象复用减少创建开销
|
||||
- 及时释放减少内存占用
|
||||
- 跳帧处理减少处理负担
|
||||
- 强制垃圾回收释放内存
|
||||
|
||||
### 3. 性能平衡
|
||||
- 内存使用 vs 视频质量
|
||||
- 实时性 vs 资源占用
|
||||
- 用户体验 vs 系统稳定性
|
||||
|
||||
---
|
||||
|
||||
如有其他问题,请查看项目文档或联系技术支持。
|
@ -45,7 +45,7 @@ logger = logging.getLogger(__name__)
|
||||
# 创建Flask应用
|
||||
app = Flask(__name__)
|
||||
app.config['SECRET_KEY'] = 'body-balance-detection-system-2024'
|
||||
socketio = SocketIO(app, cors_allowed_origins='*', async_mode='threading')
|
||||
socketio = SocketIO(app, cors_allowed_origins='*', async_mode='threading', logger=False, engineio_logger=False, ping_timeout=60, ping_interval=25)
|
||||
|
||||
# 启用CORS支持
|
||||
CORS(app, origins='*', supports_credentials=True, allow_headers=['Content-Type', 'Authorization'], methods=['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'])
|
||||
@ -55,8 +55,8 @@ app.register_blueprint(detection_bp)
|
||||
|
||||
# 读取RTSP配置
|
||||
config = configparser.ConfigParser()
|
||||
config.read(os.path.join(os.path.dirname(__file__), 'config.ini'), encoding='utf-8')
|
||||
rtsp_url = config.get('CAMERA', 'rtsp_url', fallback=None)
|
||||
config.read(os.path.join(os.path.dirname(__file__), '..', 'config.ini'), encoding='utf-8')
|
||||
device_index = config.get('CAMERA', 'device_index', fallback=None)
|
||||
|
||||
# 全局变量
|
||||
db_manager = None
|
||||
@ -76,12 +76,21 @@ def init_app():
|
||||
try:
|
||||
# 创建必要的目录
|
||||
os.makedirs('logs', exist_ok=True)
|
||||
os.makedirs('data', exist_ok=True)
|
||||
os.makedirs('exports', exist_ok=True)
|
||||
os.makedirs('videos', exist_ok=True)
|
||||
os.makedirs('data', exist_ok=True)
|
||||
|
||||
# 从配置文件读取数据库路径
|
||||
db_path = app_config.get('DATABASE', 'path', 'backend/data/body_balance.db')
|
||||
db_path_config = app_config.get('DATABASE', 'path', 'backend/data/body_balance.db')
|
||||
# 如果是相对路径,基于当前脚本目录解析
|
||||
if not os.path.isabs(db_path_config):
|
||||
# 获取当前脚本所在目录(backend目录)
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
# 如果配置路径以 'backend/' 开头,去掉这个前缀
|
||||
if db_path_config.startswith('backend/'):
|
||||
db_path_config = db_path_config[8:] # 去掉 'backend/' 前缀
|
||||
db_path = os.path.join(current_dir, db_path_config)
|
||||
else:
|
||||
db_path = db_path_config
|
||||
|
||||
# 确保数据库目录存在
|
||||
db_dir = os.path.dirname(db_path)
|
||||
os.makedirs(db_dir, exist_ok=True)
|
||||
@ -313,16 +322,14 @@ def forgot_password():
|
||||
user = cursor.fetchone()
|
||||
|
||||
if user:
|
||||
# 用户存在且手机号匹配,返回密码
|
||||
# 注意:这里返回的是加密后的密码,实际应用中需要解密或重置
|
||||
# 为了演示,我们假设有一个简单的解密方法
|
||||
encrypted_password = user['password']
|
||||
# 用户存在且手机号匹配,返回数据库中存储的实际密码
|
||||
actual_password = user['password']
|
||||
|
||||
logger.info(f'用户 {username} 密码查询成功')
|
||||
|
||||
# 这里简化处理,实际应该有更安全的密码找回机制
|
||||
# 由于使用MD5加密,无法直接解密,所以返回提示信息
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'password': '1234567', # 演示用固定密码
|
||||
'password': actual_password, # 返回数据库中存储的实际密码
|
||||
'message': '密码找回成功'
|
||||
})
|
||||
else:
|
||||
@ -949,35 +956,35 @@ if __name__ == '__main__':
|
||||
|
||||
# ==================== WebSocket 事件处理 ====================
|
||||
|
||||
@socketio.on('start_rtsp')
|
||||
def handle_start_rtsp(data=None):
|
||||
logger.info(f'收到start_rtsp事件,客户端ID: {request.sid}, 数据: {data}')
|
||||
@socketio.on('start_video')
|
||||
def handle_start_video(data=None):
|
||||
logger.info(f'收到start_video事件,客户端ID: {request.sid}, 数据: {data}')
|
||||
|
||||
try:
|
||||
if video_stream_manager:
|
||||
result = video_stream_manager.start_rtsp_stream()
|
||||
emit('rtsp_status', result)
|
||||
result = video_stream_manager.start_video_stream()
|
||||
emit('video_status', result)
|
||||
else:
|
||||
emit('rtsp_status', {'status': 'error', 'message': '视频流管理器未初始化'})
|
||||
emit('video_status', {'status': 'error', 'message': '视频流管理器未初始化'})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'启动RTSP失败: {e}')
|
||||
emit('rtsp_status', {'status': 'error', 'message': f'启动失败: {str(e)}'})
|
||||
logger.error(f'启动视频流失败: {e}')
|
||||
emit('video_status', {'status': 'error', 'message': f'启动失败: {str(e)}'})
|
||||
|
||||
@socketio.on('stop_rtsp')
|
||||
def handle_stop_rtsp(data=None):
|
||||
logger.info(f'收到stop_rtsp事件,客户端ID: {request.sid}, 数据: {data}')
|
||||
@socketio.on('stop_video')
|
||||
def handle_stop_video(data=None):
|
||||
logger.info(f'收到stop_video事件,客户端ID: {request.sid}, 数据: {data}')
|
||||
|
||||
try:
|
||||
if video_stream_manager:
|
||||
result = video_stream_manager.stop_rtsp_stream()
|
||||
emit('rtsp_status', result)
|
||||
result = video_stream_manager.stop_video_stream()
|
||||
emit('video_status', result)
|
||||
else:
|
||||
emit('rtsp_status', {'status': 'error', 'message': '视频流管理器未初始化'})
|
||||
emit('video_status', {'status': 'error', 'message': '视频流管理器未初始化'})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'停止RTSP失败: {e}')
|
||||
emit('rtsp_status', {'status': 'error', 'message': f'停止失败: {str(e)}'})
|
||||
logger.error(f'停止视频流失败: {e}')
|
||||
emit('video_status', {'status': 'error', 'message': f'停止失败: {str(e)}'})
|
||||
|
||||
@socketio.on('connect')
|
||||
def handle_connect():
|
||||
|
@ -1,84 +0,0 @@
|
||||
from database import DatabaseManager
|
||||
import os
|
||||
|
||||
# 检查backend/data目录下的数据库
|
||||
db_path = os.path.join(os.path.dirname(__file__), 'data', 'body_balance.db')
|
||||
db = DatabaseManager(db_path)
|
||||
db.init_database()
|
||||
conn = db.get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 检查患者总数
|
||||
cursor.execute('SELECT COUNT(*) FROM patients')
|
||||
count = cursor.fetchone()[0]
|
||||
print(f'body_balance.db中的患者总数: {count}')
|
||||
|
||||
# 查看前5条患者数据
|
||||
cursor.execute('SELECT * FROM patients LIMIT 5')
|
||||
rows = cursor.fetchall()
|
||||
print('前5条患者数据:')
|
||||
for row in rows:
|
||||
print(dict(row))
|
||||
|
||||
conn.close()
|
||||
|
||||
# 如果没有数据,添加测试数据
|
||||
if count == 0:
|
||||
print('\n数据库中没有患者数据,添加测试数据...')
|
||||
test_patients = [
|
||||
{
|
||||
'name': '张三',
|
||||
'gender': '男',
|
||||
'age': 30,
|
||||
'birth_date': '1994-01-15',
|
||||
'nationality': '汉族',
|
||||
'height': 175.0,
|
||||
'weight': 70.0,
|
||||
'phone': '13800138001',
|
||||
'shoe_size': '42',
|
||||
'medical_history': '无',
|
||||
'notes': '测试患者1'
|
||||
},
|
||||
{
|
||||
'name': '李四',
|
||||
'gender': '女',
|
||||
'age': 25,
|
||||
'birth_date': '1999-03-22',
|
||||
'nationality': '汉族',
|
||||
'height': 165.0,
|
||||
'weight': 55.0,
|
||||
'phone': '13800138002',
|
||||
'shoe_size': '37',
|
||||
'medical_history': '高血压',
|
||||
'notes': '测试患者2'
|
||||
},
|
||||
{
|
||||
'name': '王五',
|
||||
'gender': '男',
|
||||
'age': 35,
|
||||
'birth_date': '1989-07-08',
|
||||
'nationality': '回族',
|
||||
'height': 180.0,
|
||||
'weight': 80.0,
|
||||
'phone': '13800138003',
|
||||
'shoe_size': '44',
|
||||
'medical_history': '糖尿病',
|
||||
'notes': '测试患者3'
|
||||
}
|
||||
]
|
||||
|
||||
for patient in test_patients:
|
||||
patient_id = db.create_patient(patient)
|
||||
print(f'添加患者: {patient["name"]}, ID: {patient_id}')
|
||||
|
||||
print('\n重新测试get_patients方法:')
|
||||
patients = db.get_patients(page=1, size=10, keyword='')
|
||||
print(f'查询结果: {len(patients)}条记录')
|
||||
for p in patients:
|
||||
print(f' - {p["name"]} ({p["gender"]}, {p["age"]}岁)')
|
||||
else:
|
||||
print('\n数据库中已有患者数据,测试get_patients方法:')
|
||||
patients = db.get_patients(page=1, size=10, keyword='')
|
||||
print(f'查询结果: {len(patients)}条记录')
|
||||
for p in patients:
|
||||
print(f' - {p["name"]} ({p["gender"]}, {p["age"]}岁)')
|
@ -1,30 +0,0 @@
|
||||
from database import DatabaseManager
|
||||
import os
|
||||
|
||||
# 使用backend/data目录下的数据库路径
|
||||
db_path = os.path.join(os.path.dirname(__file__), 'data', 'body_balance.db')
|
||||
db = DatabaseManager(db_path)
|
||||
db.init_database()
|
||||
conn = db.get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 检查患者总数
|
||||
cursor.execute('SELECT COUNT(*) FROM patients')
|
||||
count = cursor.fetchone()[0]
|
||||
print(f'患者总数: {count}')
|
||||
|
||||
# 查看前5条患者数据
|
||||
cursor.execute('SELECT * FROM patients LIMIT 5')
|
||||
rows = cursor.fetchall()
|
||||
print('前5条患者数据:')
|
||||
for row in rows:
|
||||
print(dict(row))
|
||||
|
||||
# 检查表结构
|
||||
cursor.execute("PRAGMA table_info(patients)")
|
||||
columns = cursor.fetchall()
|
||||
print('\npatients表结构:')
|
||||
for col in columns:
|
||||
print(dict(col))
|
||||
|
||||
conn.close()
|
@ -1,45 +0,0 @@
|
||||
[APP]
|
||||
name = Body Balance Evaluation System
|
||||
version = 1.0.0
|
||||
debug = false
|
||||
log_level = INFO
|
||||
|
||||
[SERVER]
|
||||
host = 127.0.0.1
|
||||
port = 5000
|
||||
cors_origins = *
|
||||
|
||||
[DATABASE]
|
||||
path = backend/data/body_balance.db
|
||||
backup_interval = 24
|
||||
max_backups = 7
|
||||
|
||||
[DEVICES]
|
||||
camera_index = 0
|
||||
camera_width = 640
|
||||
camera_height = 480
|
||||
camera_fps = 30
|
||||
imu_port = COM3
|
||||
pressure_port = COM4
|
||||
|
||||
[DETECTION]
|
||||
default_duration = 60
|
||||
sampling_rate = 30
|
||||
balance_threshold = 0.2
|
||||
posture_threshold = 5.0
|
||||
|
||||
[DATA_PROCESSING]
|
||||
filter_window = 5
|
||||
outlier_threshold = 2.0
|
||||
chart_dpi = 300
|
||||
export_format = csv
|
||||
|
||||
[SECURITY]
|
||||
secret_key = 026efbf83a2fe101f168780740da86bf1c9260625458e6782738aa9cf18f8e37
|
||||
session_timeout = 3600
|
||||
max_login_attempts = 5
|
||||
|
||||
|
||||
[CAMERA]
|
||||
rtsp_url = rtsp://admin:JY123456@192.168.1.61:554/Streaming/Channels/101
|
||||
|
Binary file not shown.
@ -8,7 +8,7 @@
|
||||
import sqlite3
|
||||
import json
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from typing import List, Dict, Optional, Any
|
||||
import logging
|
||||
|
||||
@ -20,6 +20,12 @@ class DatabaseManager:
|
||||
def __init__(self, db_path: str):
|
||||
self.db_path = db_path
|
||||
self.connection = None
|
||||
# 设置中国上海时区 (UTC+8)
|
||||
self.china_tz = timezone(timedelta(hours=8))
|
||||
|
||||
def get_china_time(self) -> str:
|
||||
"""获取中国时区的当前时间字符串"""
|
||||
return datetime.now(self.china_tz).strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
def get_connection(self) -> sqlite3.Connection:
|
||||
"""获取数据库连接"""
|
||||
@ -143,10 +149,9 @@ class DatabaseManager:
|
||||
admin_exists = cursor.fetchone()[0]
|
||||
|
||||
if admin_exists == 0:
|
||||
import hashlib
|
||||
admin_id = str(uuid.uuid4())
|
||||
# 默认密码为 admin123,使用MD5加密
|
||||
admin_password = hashlib.md5('admin123'.encode()).hexdigest()
|
||||
# 默认密码为 admin123,明文存储
|
||||
admin_password = 'admin123'
|
||||
|
||||
cursor.execute('''
|
||||
INSERT INTO users (id, name, username, password, is_active, user_type)
|
||||
@ -172,12 +177,14 @@ class DatabaseManager:
|
||||
|
||||
try:
|
||||
patient_id = str(uuid.uuid4())
|
||||
# 使用中国时区时间
|
||||
china_time = self.get_china_time()
|
||||
|
||||
cursor.execute('''
|
||||
INSERT INTO patients (
|
||||
id, name, gender, age, birth_date, height, weight,
|
||||
phone, shoe_size, medical_history, notes
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
phone, shoe_size, medical_history, notes, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (
|
||||
patient_id,
|
||||
patient_data.get('name'),
|
||||
@ -189,7 +196,9 @@ class DatabaseManager:
|
||||
patient_data.get('phone'),
|
||||
patient_data.get('shoe_size'),
|
||||
patient_data.get('medical_history'),
|
||||
patient_data.get('notes')
|
||||
patient_data.get('notes'),
|
||||
china_time,
|
||||
china_time
|
||||
))
|
||||
|
||||
conn.commit()
|
||||
@ -270,11 +279,14 @@ class DatabaseManager:
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# 使用中国时区时间
|
||||
china_time = self.get_china_time()
|
||||
|
||||
cursor.execute('''
|
||||
UPDATE patients SET
|
||||
name = ?, gender = ?, age = ?, birth_date = ?, height = ?, weight = ?,
|
||||
phone = ?, shoe_size = ?, medical_history = ?, notes = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
updated_at = ?
|
||||
WHERE id = ?
|
||||
''', (
|
||||
patient_data.get('name'),
|
||||
@ -287,6 +299,7 @@ class DatabaseManager:
|
||||
patient_data.get('shoe_size'),
|
||||
patient_data.get('medical_history'),
|
||||
patient_data.get('notes'),
|
||||
china_time,
|
||||
patient_id
|
||||
))
|
||||
|
||||
@ -344,17 +357,22 @@ class DatabaseManager:
|
||||
try:
|
||||
session_id = str(uuid.uuid4())
|
||||
|
||||
# 使用中国时区时间
|
||||
china_time = self.get_china_time()
|
||||
|
||||
cursor.execute('''
|
||||
INSERT INTO detection_sessions (
|
||||
id, patient_id, duration, frequency, settings, status
|
||||
) VALUES (?, ?, ?, ?, ?, ?)
|
||||
id, patient_id, duration, frequency, settings, status, start_time, created_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (
|
||||
session_id,
|
||||
patient_id,
|
||||
settings.get('duration', 60),
|
||||
settings.get('frequency', 60),
|
||||
json.dumps(settings),
|
||||
'created'
|
||||
'created',
|
||||
china_time,
|
||||
china_time
|
||||
))
|
||||
|
||||
conn.commit()
|
||||
@ -373,11 +391,13 @@ class DatabaseManager:
|
||||
|
||||
try:
|
||||
if status in ['completed', 'stopped', 'error']:
|
||||
# 使用中国时区时间
|
||||
china_time = self.get_china_time()
|
||||
cursor.execute('''
|
||||
UPDATE detection_sessions SET
|
||||
status = ?, data_points = ?, end_time = CURRENT_TIMESTAMP
|
||||
status = ?, data_points = ?, end_time = ?
|
||||
WHERE id = ?
|
||||
''', (status, data_points, session_id))
|
||||
''', (status, data_points, china_time, session_id))
|
||||
else:
|
||||
cursor.execute('''
|
||||
UPDATE detection_sessions SET
|
||||
@ -553,12 +573,15 @@ class DatabaseManager:
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# 使用中国时区时间
|
||||
china_time = self.get_china_time()
|
||||
|
||||
# 保存不同类型的数据
|
||||
for data_type, data_value in data.items():
|
||||
cursor.execute('''
|
||||
INSERT INTO detection_data (session_id, data_type, data_value)
|
||||
VALUES (?, ?, ?)
|
||||
''', (session_id, data_type, json.dumps(data_value)))
|
||||
INSERT INTO detection_data (session_id, data_type, data_value, timestamp)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', (session_id, data_type, json.dumps(data_value), china_time))
|
||||
|
||||
conn.commit()
|
||||
|
||||
@ -679,10 +702,13 @@ class DatabaseManager:
|
||||
try:
|
||||
value_str = json.dumps(value) if not isinstance(value, str) else value
|
||||
|
||||
# 使用中国时区时间
|
||||
china_time = self.get_china_time()
|
||||
|
||||
cursor.execute('''
|
||||
INSERT OR REPLACE INTO system_settings (key, value, description, updated_at)
|
||||
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
|
||||
''', (key, value_str, description))
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', (key, value_str, description, china_time))
|
||||
|
||||
conn.commit()
|
||||
logger.info(f'设置系统设置: {key}')
|
||||
@ -700,8 +726,6 @@ class DatabaseManager:
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
import hashlib
|
||||
|
||||
# 检查手机号是否已存在(如果提供了手机号)
|
||||
if user_data.get('phone'):
|
||||
cursor.execute('SELECT COUNT(*) FROM users WHERE phone = ?', (user_data['phone'],))
|
||||
@ -712,20 +736,25 @@ class DatabaseManager:
|
||||
}
|
||||
|
||||
user_id = str(uuid.uuid4())
|
||||
# 密码MD5加密
|
||||
password_hash = hashlib.md5(user_data['password'].encode()).hexdigest()
|
||||
# 密码明文存储
|
||||
password = user_data['password']
|
||||
# 使用中国时区时间
|
||||
china_time = self.get_china_time()
|
||||
|
||||
cursor.execute('''
|
||||
INSERT INTO users (id, name, username, password, phone, is_active, user_type)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO users (id, name, username, password, phone, is_active, user_type, register_date, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (
|
||||
user_id,
|
||||
user_data['name'],
|
||||
user_data['username'],
|
||||
password_hash,
|
||||
password,
|
||||
user_data.get('phone'), # 手机号可选
|
||||
0, # 新注册用户默认未激活,需要管理员审核
|
||||
'user'
|
||||
1, # 新注册用户默认激活
|
||||
'user',
|
||||
china_time,
|
||||
china_time,
|
||||
china_time
|
||||
))
|
||||
|
||||
conn.commit()
|
||||
@ -757,14 +786,10 @@ class DatabaseManager:
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
import hashlib
|
||||
|
||||
password_hash = hashlib.md5(password.encode()).hexdigest()
|
||||
|
||||
cursor.execute('''
|
||||
SELECT * FROM users
|
||||
WHERE username = ? AND password = ? AND is_active = 1
|
||||
''', (username, password_hash))
|
||||
''', (username, password))
|
||||
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
@ -863,12 +888,15 @@ class DatabaseManager:
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# 使用中国时区时间
|
||||
china_time = self.get_china_time()
|
||||
|
||||
cursor.execute('''
|
||||
UPDATE users SET
|
||||
is_active = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
updated_at = ?
|
||||
WHERE id = ?
|
||||
''', (1 if approved else 0, user_id))
|
||||
''', (1 if approved else 0, china_time, user_id))
|
||||
|
||||
conn.commit()
|
||||
status = '通过' if approved else '拒绝'
|
||||
@ -903,10 +931,7 @@ class DatabaseManager:
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# 如果包含密码,需要加密
|
||||
if 'password' in user_data:
|
||||
import hashlib
|
||||
user_data['password'] = hashlib.md5(user_data['password'].encode()).hexdigest()
|
||||
# 密码明文存储,无需加密处理
|
||||
|
||||
# 构建更新语句
|
||||
fields = []
|
||||
@ -918,7 +943,10 @@ class DatabaseManager:
|
||||
values.append(value)
|
||||
|
||||
if fields:
|
||||
fields.append('updated_at = CURRENT_TIMESTAMP')
|
||||
# 使用中国时区时间
|
||||
china_time = self.get_china_time()
|
||||
fields.append('updated_at = ?')
|
||||
values.append(china_time)
|
||||
values.append(user_id)
|
||||
|
||||
sql = f"UPDATE users SET {', '.join(fields)} WHERE id = ?"
|
||||
|
@ -492,9 +492,9 @@ class VideoStreamManager:
|
||||
|
||||
def __init__(self, socketio=None):
|
||||
self.socketio = socketio
|
||||
self.rtsp_url = None
|
||||
self.rtsp_thread = None
|
||||
self.rtsp_running = False
|
||||
self.device_index = None
|
||||
self.video_thread = None
|
||||
self.video_running = False
|
||||
|
||||
# 用于异步编码的线程池和队列
|
||||
self.encoding_executor = ThreadPoolExecutor(max_workers=2)
|
||||
@ -515,13 +515,14 @@ class VideoStreamManager:
|
||||
"""加载RTSP配置"""
|
||||
try:
|
||||
config = configparser.ConfigParser()
|
||||
config_path = os.path.join(os.path.dirname(__file__), 'config.ini')
|
||||
config_path = os.path.join(os.path.dirname(__file__), '..', 'config.ini')
|
||||
config.read(config_path, encoding='utf-8')
|
||||
self.rtsp_url = config.get('CAMERA', 'rtsp_url', fallback=None)
|
||||
logger.info(f'RTSP配置加载完成: {self.rtsp_url}')
|
||||
device_index_str = config.get('CAMERA', 'device_index', fallback='0')
|
||||
self.device_index = int(device_index_str) if device_index_str else 0
|
||||
logger.info(f'视频监控设备配置加载完成,设备号: {self.device_index}')
|
||||
except Exception as e:
|
||||
logger.error(f'加载RTSP配置失败: {e}')
|
||||
self.rtsp_url = None
|
||||
logger.error(f'视频监控设备配置失败: {e}')
|
||||
self.device_index = None
|
||||
|
||||
def get_memory_usage(self):
|
||||
"""获取当前进程内存使用量(字节)"""
|
||||
@ -585,7 +586,7 @@ class VideoStreamManager:
|
||||
|
||||
# 发送数据
|
||||
if self.socketio:
|
||||
self.socketio.emit('rtsp_frame', {
|
||||
self.socketio.emit('video_frame', {
|
||||
'image': jpg_as_text,
|
||||
'frame_id': frame_count,
|
||||
'timestamp': time.time()
|
||||
@ -603,7 +604,7 @@ class VideoStreamManager:
|
||||
|
||||
def frame_encoding_worker(self):
|
||||
"""帧编码工作线程"""
|
||||
while self.rtsp_running:
|
||||
while self.video_running:
|
||||
try:
|
||||
# 从队列获取帧
|
||||
frame, frame_count = self.frame_queue.get(timeout=1)
|
||||
@ -641,22 +642,22 @@ class VideoStreamManager:
|
||||
|
||||
return frame
|
||||
|
||||
def generate_rtsp_frames(self):
|
||||
"""生成RTSP帧"""
|
||||
def generate_video_frames(self):
|
||||
"""生成视频监控帧"""
|
||||
frame_count = 0
|
||||
error_count = 0
|
||||
use_test_mode = False
|
||||
last_frame_time = time.time()
|
||||
|
||||
logger.info(f'开始生成RTSP帧,URL: {self.rtsp_url}')
|
||||
logger.info(f'开始生成视频监控帧,设备号: {self.device_index}')
|
||||
|
||||
try:
|
||||
cap = cv2.VideoCapture(self.rtsp_url)
|
||||
cap = cv2.VideoCapture(self.device_index)
|
||||
if not cap.isOpened():
|
||||
logger.warning(f'无法打开RTSP流: {self.rtsp_url},切换到测试模式')
|
||||
logger.warning(f'无法打开视频监控流: {self.device_index},切换到测试模式')
|
||||
use_test_mode = True
|
||||
if self.socketio:
|
||||
self.socketio.emit('rtsp_status', {'status': 'started', 'message': '使用测试视频源'})
|
||||
self.socketio.emit('video_status', {'status': 'started', 'message': '使用测试视频源'})
|
||||
else:
|
||||
# 最激进的实时优化设置
|
||||
cap.set(cv2.CAP_PROP_BUFFERSIZE, 0) # 完全禁用缓冲区
|
||||
@ -665,40 +666,40 @@ class VideoStreamManager:
|
||||
# 设置更低的分辨率以减少处理时间
|
||||
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
|
||||
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
|
||||
logger.info('RTSP流已打开,开始推送帧(激进实时模式)')
|
||||
logger.info('视频监控流已打开,开始推送帧(激进实时模式)')
|
||||
if self.socketio:
|
||||
self.socketio.emit('rtsp_status', {'status': 'started', 'message': '使用RTSP视频源(激进实时模式)'})
|
||||
self.socketio.emit('video_status', {'status': 'started', 'message': '使用视频监控视频源(激进实时模式)'})
|
||||
|
||||
self.rtsp_running = True
|
||||
self.video_running = True
|
||||
|
||||
# 启动帧编码工作线程
|
||||
encoding_thread = threading.Thread(target=self.frame_encoding_worker)
|
||||
encoding_thread.daemon = True
|
||||
encoding_thread.start()
|
||||
|
||||
while self.rtsp_running:
|
||||
while self.video_running:
|
||||
if use_test_mode:
|
||||
# 使用测试模式生成帧
|
||||
frame = self.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}次),尝试重连...')
|
||||
logger.warning(f'视频监控读取帧失败(第{error_count}次),尝试重连...')
|
||||
if 'cap' in locals():
|
||||
cap.release()
|
||||
|
||||
if error_count > 5:
|
||||
logger.warning('RTSP连接失败次数过多,切换到测试模式')
|
||||
logger.warning('视频监控连接失败次数过多,切换到测试模式')
|
||||
use_test_mode = True
|
||||
if self.socketio:
|
||||
self.socketio.emit('rtsp_status', {'status': 'switched', 'message': '已切换到测试视频源'})
|
||||
self.socketio.emit('video_status', {'status': 'switched', 'message': '已切换到测试视频源'})
|
||||
continue
|
||||
|
||||
# 立即重连,不等待
|
||||
cap = cv2.VideoCapture(self.rtsp_url)
|
||||
cap = cv2.VideoCapture(self.device_index)
|
||||
if cap.isOpened():
|
||||
# 重连时应用相同的激进实时设置
|
||||
cap.set(cv2.CAP_PROP_BUFFERSIZE, 0)
|
||||
@ -766,67 +767,68 @@ class VideoStreamManager:
|
||||
logger.error(f'帧队列处理失败: {e}')
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'RTSP推流异常: {e}')
|
||||
logger.error(f'监控视频推流异常: {e}')
|
||||
if self.socketio:
|
||||
self.socketio.emit('rtsp_status', {'status': 'error', 'message': f'推流异常: {str(e)}'})
|
||||
self.socketio.emit('video_status', {'status': 'error', 'message': f'推流异常: {str(e)}'})
|
||||
finally:
|
||||
if 'cap' in locals():
|
||||
cap.release()
|
||||
self.rtsp_running = False
|
||||
logger.info(f'RTSP推流结束,总共推送了 {frame_count} 帧')
|
||||
self.video_running = False
|
||||
logger.info(f'视频监控推流结束,总共推送了 {frame_count} 帧')
|
||||
|
||||
def start_rtsp_stream(self):
|
||||
"""启动RTSP推流"""
|
||||
def start_video_stream(self):
|
||||
"""启动视频监控推流"""
|
||||
try:
|
||||
if self.rtsp_thread and self.rtsp_thread.is_alive():
|
||||
logger.warning('RTSP线程已在运行')
|
||||
return {'status': 'already_running', 'message': 'RTSP已在运行'}
|
||||
if self.video_thread and self.video_thread.is_alive():
|
||||
logger.warning('视频监控线程已在运行')
|
||||
return {'status': 'already_running', 'message': '视频监控已在运行'}
|
||||
|
||||
if not self.rtsp_url:
|
||||
logger.error('RTSP URL未配置')
|
||||
return {'status': 'error', 'message': 'RTSP URL未配置'}
|
||||
if not self.device_index:
|
||||
logger.error('视频监控相机未配置')
|
||||
return {'status': 'error', 'message': '视频监控相机未配置'}
|
||||
|
||||
logger.info(f'启动RTSP线程,URL: {self.rtsp_url}')
|
||||
self.rtsp_thread = threading.Thread(target=self.generate_rtsp_frames)
|
||||
self.rtsp_thread.daemon = True
|
||||
self.rtsp_thread.start()
|
||||
logger.info(f'视频启动监控线程,设备号: {self.device_index}')
|
||||
self.video_thread = threading.Thread(target=self.generate_video_frames)
|
||||
self.video_thread.daemon = True
|
||||
self.video_thread.start()
|
||||
self.video_running = True
|
||||
|
||||
logger.info('RTSP线程已启动')
|
||||
return {'status': 'started', 'message': 'RTSP推流已启动'}
|
||||
logger.info('视频监控线程已启动')
|
||||
return {'status': 'started', 'message': '视频监控线程已启动'}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'启动RTSP失败: {e}')
|
||||
return {'status': 'error', 'message': f'启动失败: {str(e)}'}
|
||||
logger.error(f'视频监控线程启动失败: {e}')
|
||||
return {'status': 'error', 'message': f'视频监控线程启动失败: {str(e)}'}
|
||||
|
||||
def stop_rtsp_stream(self):
|
||||
"""停止RTSP推流"""
|
||||
def stop_video_stream(self):
|
||||
"""停止视频监控推流"""
|
||||
try:
|
||||
self.rtsp_running = False
|
||||
logger.info('RTSP推流已停止')
|
||||
return {'status': 'stopped', 'message': 'RTSP推流已停止'}
|
||||
self.video_running = False
|
||||
logger.info('视频监控推流已停止')
|
||||
return {'status': 'stopped', 'message': '视频监控推流已停止'}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'停止RTSP失败: {e}')
|
||||
logger.error(f'停止视频监控推流失败: {e}')
|
||||
return {'status': 'error', 'message': f'停止失败: {str(e)}'}
|
||||
|
||||
def is_streaming(self):
|
||||
"""检查是否正在推流"""
|
||||
return self.rtsp_running
|
||||
return self.video_running
|
||||
|
||||
def get_stream_status(self):
|
||||
"""获取推流状态"""
|
||||
return {
|
||||
'running': self.rtsp_running,
|
||||
'rtsp_url': self.rtsp_url,
|
||||
'thread_alive': self.rtsp_thread.is_alive() if self.rtsp_thread else False
|
||||
'running': self.video_running,
|
||||
'device_index': self.device_index,
|
||||
'thread_alive': self.video_thread.is_alive() if self.video_thread else False
|
||||
}
|
||||
|
||||
def cleanup(self):
|
||||
"""清理资源"""
|
||||
try:
|
||||
self.rtsp_running = False
|
||||
if self.rtsp_thread and self.rtsp_thread.is_alive():
|
||||
self.rtsp_thread.join(timeout=2)
|
||||
self.video_running = False
|
||||
if self.video_thread and self.video_thread.is_alive():
|
||||
self.video_thread.join(timeout=2)
|
||||
|
||||
self.encoding_executor.shutdown(wait=False)
|
||||
|
||||
|
@ -1,60 +0,0 @@
|
||||
from database import DatabaseManager
|
||||
import os
|
||||
|
||||
# 使用backend/data目录下的数据库路径
|
||||
db_path = os.path.join(os.path.dirname(__file__), 'data', 'body_balance.db')
|
||||
db = DatabaseManager(db_path)
|
||||
db.init_database()
|
||||
|
||||
print('测试get_patients方法:')
|
||||
|
||||
# 测试1: 无参数调用
|
||||
print('\n1. 无参数调用 get_patients():')
|
||||
try:
|
||||
patients = db.get_patients()
|
||||
print(f' 结果: {len(patients)}条记录')
|
||||
for i, p in enumerate(patients[:3]): # 只显示前3条
|
||||
print(f' {i+1}. {p["name"]} ({p["gender"]}, {p["age"]}岁)')
|
||||
except Exception as e:
|
||||
print(f' 错误: {e}')
|
||||
|
||||
# 测试2: 带分页参数
|
||||
print('\n2. 带分页参数 get_patients(page=1, size=5):')
|
||||
try:
|
||||
patients = db.get_patients(page=1, size=5)
|
||||
print(f' 结果: {len(patients)}条记录')
|
||||
for i, p in enumerate(patients):
|
||||
print(f' {i+1}. {p["name"]} ({p["gender"]}, {p["age"]}岁)')
|
||||
except Exception as e:
|
||||
print(f' 错误: {e}')
|
||||
|
||||
# 测试3: 带关键字搜索
|
||||
print('\n3. 带关键字搜索 get_patients(keyword="张"):')
|
||||
try:
|
||||
patients = db.get_patients(keyword='张')
|
||||
print(f' 结果: {len(patients)}条记录')
|
||||
for i, p in enumerate(patients):
|
||||
print(f' {i+1}. {p["name"]} ({p["gender"]}, {p["age"]}岁)')
|
||||
except Exception as e:
|
||||
print(f' 错误: {e}')
|
||||
|
||||
# 测试4: 模拟app.py中的调用方式
|
||||
print('\n4. 模拟app.py中的调用方式:')
|
||||
try:
|
||||
page = 1
|
||||
size = 10
|
||||
keyword = ''
|
||||
|
||||
patients = db.get_patients(page, size, keyword)
|
||||
total = db.get_patients_count(keyword)
|
||||
|
||||
print(f' 患者列表: {len(patients)}条记录')
|
||||
print(f' 总数: {total}')
|
||||
print(f' 页码: {page}, 每页: {size}')
|
||||
|
||||
for i, p in enumerate(patients[:3]): # 只显示前3条
|
||||
print(f' {i+1}. {p["name"]} ({p["gender"]}, {p["age"]}岁)')
|
||||
except Exception as e:
|
||||
print(f' 错误: {e}')
|
||||
|
||||
print('\n测试完成!')
|
@ -1,78 +0,0 @@
|
||||
from database import DatabaseManager
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
# 使用backend/data目录下的数据库路径
|
||||
db_path = os.path.join(os.path.dirname(__file__), 'data', 'body_balance.db')
|
||||
db = DatabaseManager(db_path)
|
||||
db.init_database()
|
||||
|
||||
# 添加一些测试患者数据
|
||||
test_patients = [
|
||||
{
|
||||
'name': '张三',
|
||||
'gender': '男',
|
||||
'age': 30,
|
||||
'birth_date': '1994-01-15',
|
||||
'nationality': '汉族',
|
||||
'height': 175.0,
|
||||
'weight': 70.0,
|
||||
'phone': '13800138001',
|
||||
'shoe_size': '42',
|
||||
'medical_history': '无',
|
||||
'notes': '测试患者1'
|
||||
},
|
||||
{
|
||||
'name': '李四',
|
||||
'gender': '女',
|
||||
'age': 25,
|
||||
'birth_date': '1999-03-22',
|
||||
'nationality': '汉族',
|
||||
'height': 165.0,
|
||||
'weight': 55.0,
|
||||
'phone': '13800138002',
|
||||
'shoe_size': '37',
|
||||
'medical_history': '高血压',
|
||||
'notes': '测试患者2'
|
||||
},
|
||||
{
|
||||
'name': '王五',
|
||||
'gender': '男',
|
||||
'age': 35,
|
||||
'birth_date': '1989-07-08',
|
||||
'nationality': '回族',
|
||||
'height': 180.0,
|
||||
'weight': 80.0,
|
||||
'phone': '13800138003',
|
||||
'shoe_size': '44',
|
||||
'medical_history': '糖尿病',
|
||||
'notes': '测试患者3'
|
||||
}
|
||||
]
|
||||
|
||||
print('添加测试患者数据...')
|
||||
for patient in test_patients:
|
||||
patient_id = db.create_patient(patient)
|
||||
print(f'添加患者: {patient["name"]}, ID: {patient_id}')
|
||||
|
||||
print('\n测试get_patients方法:')
|
||||
# 测试无关键字查询
|
||||
patients = db.get_patients(page=1, size=10, keyword='')
|
||||
print(f'无关键字查询结果: {len(patients)}条记录')
|
||||
for p in patients:
|
||||
print(f' - {p["name"]} ({p["gender"]}, {p["age"]}岁)')
|
||||
|
||||
# 测试关键字查询
|
||||
patients = db.get_patients(page=1, size=10, keyword='张')
|
||||
print(f'\n关键字"张"查询结果: {len(patients)}条记录')
|
||||
for p in patients:
|
||||
print(f' - {p["name"]} ({p["gender"]}, {p["age"]}岁)')
|
||||
|
||||
# 测试电话号码查询
|
||||
patients = db.get_patients(page=1, size=10, keyword='13800138002')
|
||||
print(f'\n电话号码查询结果: {len(patients)}条记录')
|
||||
for p in patients:
|
||||
print(f' - {p["name"]} ({p["phone"]})')
|
||||
|
||||
print('\n测试完成!')
|
@ -22,7 +22,7 @@ logger = logging.getLogger(__name__)
|
||||
class Config:
|
||||
"""配置管理器"""
|
||||
|
||||
def __init__(self, config_file: str = 'config.ini'):
|
||||
def __init__(self, config_file: str = '../config.ini'):
|
||||
self.config_file = Path(config_file)
|
||||
self.config = configparser.ConfigParser()
|
||||
self._load_config()
|
||||
|
@ -39,3 +39,5 @@ secret_key = 79fcc4983d478c2ee672f3305d5e12c7c84fd1b58a18acb650e9f8125bfa805f
|
||||
session_timeout = 3600
|
||||
max_login_attempts = 5
|
||||
|
||||
[CAMERA]
|
||||
device_index = 1
|
||||
|
@ -27,7 +27,7 @@
|
||||
"debug": false,
|
||||
"cors": {
|
||||
"enabled": true,
|
||||
"origins": ["http://localhost:5173", "http://127.0.0.1:5173"]
|
||||
"origins": ["http://192.168.1.38:3000"]
|
||||
},
|
||||
"ssl": {
|
||||
"enabled": false,
|
||||
|
3
data/.gitkeep
Normal file
3
data/.gitkeep
Normal file
@ -0,0 +1,3 @@
|
||||
# 数据目录
|
||||
# 此文件用于确保 data 目录在版本控制中被保留
|
||||
# 实际的数据文件会被 .gitignore 忽略
|
@ -13,6 +13,7 @@
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
import socket
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目路径
|
||||
@ -45,6 +46,16 @@ def setup_debug_logging():
|
||||
logger.info('调试日志已启用')
|
||||
return logger
|
||||
|
||||
def get_local_ip():
|
||||
"""获取本机IP地址"""
|
||||
try:
|
||||
# 创建一个UDP socket连接到外部地址来获取本机IP
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
||||
s.connect(("8.8.8.8", 80))
|
||||
return s.getsockname()[0]
|
||||
except Exception:
|
||||
return "127.0.0.1"
|
||||
|
||||
def check_debug_environment():
|
||||
"""检查调试环境"""
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -57,7 +68,7 @@ def check_debug_environment():
|
||||
# 检查必要文件
|
||||
required_files = [
|
||||
'backend/app.py',
|
||||
'backend/config.ini',
|
||||
'config.ini',
|
||||
'backend/requirements.txt'
|
||||
]
|
||||
|
||||
@ -86,17 +97,21 @@ def start_debug_server():
|
||||
logger.info('初始化应用...')
|
||||
init_app()
|
||||
|
||||
# 获取本机IP地址
|
||||
local_ip = get_local_ip()
|
||||
|
||||
# 启动调试服务器
|
||||
logger.info('启动调试服务器...')
|
||||
logger.info('调试模式已启用 - 可以在IDE中设置断点')
|
||||
logger.info('服务器地址: http://127.0.0.1:5000')
|
||||
logger.info('本地访问: http://127.0.0.1:5000')
|
||||
logger.info(f'远程访问: http://{local_ip}:5000')
|
||||
logger.info('健康检查: http://127.0.0.1:5000/health')
|
||||
logger.info('按 Ctrl+C 停止服务器')
|
||||
|
||||
# 启动SocketIO服务器(支持调试)
|
||||
# 启动SocketIO服务器(支持调试和远程访问)
|
||||
socketio.run(
|
||||
app,
|
||||
host='127.0.0.1',
|
||||
host='0.0.0.0', # 允许所有IP访问
|
||||
port=5000,
|
||||
debug=True,
|
||||
use_reloader=True, # 启用热重载
|
||||
|
BIN
document/姿态检测页.png
Normal file
BIN
document/姿态检测页.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 623 KiB |
BIN
document/开始检测中的录屏界面.png
Normal file
BIN
document/开始检测中的录屏界面.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 628 KiB |
BIN
document/登录进入的起始页.png
Normal file
BIN
document/登录进入的起始页.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 135 KiB |
BIN
document/登录页.png
Normal file
BIN
document/登录页.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 63 KiB |
109
frontend/src/renderer/package-lock.json
generated
109
frontend/src/renderer/package-lock.json
generated
@ -14,6 +14,7 @@
|
||||
"element-plus": "^2.3.9",
|
||||
"html2canvas": "^1.4.1",
|
||||
"pinia": "^2.1.6",
|
||||
"socket.io-client": "^4.7.2",
|
||||
"vue": "^3.3.4",
|
||||
"vue-echarts": "^6.6.1",
|
||||
"vue-router": "^4.2.4"
|
||||
@ -767,6 +768,12 @@
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.17.20",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz",
|
||||
@ -1024,6 +1031,23 @@
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
@ -1092,6 +1116,28 @@
|
||||
"vue": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-client": {
|
||||
"version": "6.6.3",
|
||||
"resolved": "https://registry.npmmirror.com/engine.io-client/-/engine.io-client-6.6.3.tgz",
|
||||
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.17.1",
|
||||
"xmlhttprequest-ssl": "~2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-parser": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
@ -1467,6 +1513,12 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
@ -1621,6 +1673,34 @@
|
||||
"@parcel/watcher": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-client": {
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmmirror.com/socket.io-client/-/socket.io-client-4.8.1.tgz",
|
||||
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io-client": "~6.6.1",
|
||||
"socket.io-parser": "~4.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmmirror.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@ -1826,6 +1906,35 @@
|
||||
"vue": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmmirror.com/ws/-/ws-8.17.1.tgz",
|
||||
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xmlhttprequest-ssl": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
||||
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/zrender": {
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
|
||||
|
@ -14,6 +14,7 @@
|
||||
"element-plus": "^2.3.9",
|
||||
"html2canvas": "^1.4.1",
|
||||
"pinia": "^2.1.6",
|
||||
"socket.io-client": "^4.7.2",
|
||||
"vue": "^3.3.4",
|
||||
"vue-echarts": "^6.6.1",
|
||||
"vue-router": "^4.2.4"
|
||||
|
58
frontend/src/renderer/public/logo.png
Normal file
58
frontend/src/renderer/public/logo.png
Normal file
@ -0,0 +1,58 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200">
|
||||
<!-- 背景圆形 -->
|
||||
<circle cx="100" cy="100" r="95" fill="url(#grad1)" />
|
||||
|
||||
<!-- 渐变定义 -->
|
||||
<defs>
|
||||
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#1e5799;stop-opacity:1" />
|
||||
<stop offset="50%" style="stop-color:#2989d8;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#7db9e8;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
<linearGradient id="grad2" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#ffffff;stop-opacity:0.9" />
|
||||
<stop offset="100%" style="stop-color:#f0f0f0;stop-opacity:0.8" />
|
||||
</linearGradient>
|
||||
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur in="SourceAlpha" stdDeviation="5" />
|
||||
<feOffset dx="0" dy="4" result="offsetblur" />
|
||||
<feComponentTransfer>
|
||||
<feFuncA type="linear" slope="0.3" />
|
||||
</feComponentTransfer>
|
||||
<feMerge>
|
||||
<feMergeNode />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- 内部圆形 -->
|
||||
<circle cx="100" cy="100" r="75" fill="url(#grad2)" filter="url(#shadow)" />
|
||||
|
||||
<!-- 平衡图标 -->
|
||||
<g transform="translate(50, 60) scale(0.5)" fill="#1e5799">
|
||||
<!-- 天平支架 -->
|
||||
<rect x="95" y="30" width="10" height="80" rx="5" ry="5" />
|
||||
|
||||
<!-- 天平横杆 -->
|
||||
<rect x="40" y="40" width="120" height="8" rx="4" ry="4" />
|
||||
|
||||
<!-- 左盘 -->
|
||||
<ellipse cx="50" cy="70" rx="30" ry="10" />
|
||||
<path d="M50,70 L80,45 L20,45 Z" fill="#2989d8" opacity="0.7" />
|
||||
|
||||
<!-- 右盘 -->
|
||||
<ellipse cx="150" cy="70" rx="30" ry="10" />
|
||||
<path d="M150,70 L180,45 L120,45 Z" fill="#2989d8" opacity="0.7" />
|
||||
|
||||
<!-- 底座 -->
|
||||
<path d="M80,110 L120,110 L115,130 L85,130 Z" />
|
||||
</g>
|
||||
|
||||
<!-- 装饰元素 -->
|
||||
<circle cx="100" cy="100" r="90" fill="none" stroke="white" stroke-width="2" stroke-opacity="0.3" />
|
||||
<circle cx="100" cy="100" r="85" fill="none" stroke="white" stroke-width="1" stroke-opacity="0.2" />
|
||||
|
||||
<!-- 光晕效果 -->
|
||||
<circle cx="70" cy="70" r="15" fill="white" opacity="0.2" />
|
||||
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
@ -108,13 +108,13 @@ export const deviceAPI = {
|
||||
},
|
||||
|
||||
// 校准设备
|
||||
calibrateDevice(deviceType) {
|
||||
return api.post(`/api/devices/${deviceType}/calibrate`)
|
||||
calibrateDevice() {
|
||||
return api.post('/api/devices/calibrate')
|
||||
},
|
||||
|
||||
// 测试设备
|
||||
testDevice(deviceType) {
|
||||
return api.post(`/api/devices/${deviceType}/test`)
|
||||
testDevice() {
|
||||
return api.post('/api/devices/test')
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -428,14 +428,15 @@ function connectWebSocket() {
|
||||
}
|
||||
})
|
||||
|
||||
// 监听RTSP状态事件
|
||||
socket.on('rtsp_status', (data) => {
|
||||
console.log('📺 RTSP状态:', data)
|
||||
// 监听视频状态事件
|
||||
socket.on('video_status', (data) => {
|
||||
console.log('📺 视频状态:', data)
|
||||
})
|
||||
|
||||
// 监听RTSP帧数据
|
||||
socket.on('rtsp_frame', (data) => {
|
||||
// 监听视频帧数据
|
||||
socket.on('video_frame', (data) => {
|
||||
frameCount++
|
||||
console.log(`📺 收到视频帧 #${frameCount}, 数据大小: ${data.image ? data.image.length : 0} 字符`)
|
||||
displayFrame(data.image)
|
||||
})
|
||||
|
||||
@ -461,16 +462,16 @@ function disconnectWebSocket() {
|
||||
}
|
||||
}
|
||||
|
||||
// 启动RTSP
|
||||
// 启动视频流
|
||||
function startRtsp() {
|
||||
if (socket && socket.connected) {
|
||||
console.log('🚀 发送start_rtsp事件')
|
||||
console.log('🚀 发送start_video事件')
|
||||
|
||||
socket.emit('start_rtsp', {}, (ack) => {
|
||||
socket.emit('start_video', {}, (ack) => {
|
||||
if (ack) {
|
||||
console.log('✅ start_rtsp事件已确认:', ack)
|
||||
console.log('✅ start_video事件已确认:', ack)
|
||||
} else {
|
||||
console.log('⚠️ start_rtsp事件无确认响应')
|
||||
console.log('⚠️ start_video事件无确认响应')
|
||||
}
|
||||
})
|
||||
|
||||
@ -484,23 +485,23 @@ function startRtsp() {
|
||||
}, 5000)
|
||||
|
||||
} else {
|
||||
console.error('❌ WebSocket未连接,无法启动RTSP')
|
||||
console.error('❌ WebSocket未连接,无法启动视频流')
|
||||
}
|
||||
}
|
||||
|
||||
// 停止RTSP
|
||||
// 停止视频流
|
||||
function stopRtsp() {
|
||||
if (socket && socket.connected) {
|
||||
console.log('🛑 发送stop_rtsp事件')
|
||||
socket.emit('stop_rtsp', {}, (ack) => {
|
||||
console.log('🛑 发送stop_video事件')
|
||||
socket.emit('stop_video', {}, (ack) => {
|
||||
if (ack) {
|
||||
console.log('✅ stop_rtsp事件已确认:', ack)
|
||||
console.log('✅ stop_video事件已确认:', ack)
|
||||
} else {
|
||||
console.log('⚠️ stop_rtsp事件无确认响应')
|
||||
console.log('⚠️ stop_video事件无确认响应')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
console.error('❌ WebSocket未连接,无法停止RTSP')
|
||||
console.error('❌ WebSocket未连接,无法停止视频流')
|
||||
}
|
||||
}
|
||||
|
||||
@ -511,7 +512,12 @@ function stopRtsp() {
|
||||
|
||||
// 简单的帧显示函数
|
||||
function displayFrame(base64Image) {
|
||||
rtspImgSrc.value = 'data:image/jpeg;base64,' + base64Image
|
||||
if (base64Image && base64Image.length > 0) {
|
||||
rtspImgSrc.value = 'data:image/jpeg;base64,' + base64Image
|
||||
console.log(`🖼️ 视频帧已设置到img元素,base64长度: ${base64Image.length}`)
|
||||
} else {
|
||||
console.warn('⚠️ 收到空的视频帧数据')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,538 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>前端WebSocket连接示例</title>
|
||||
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
.status {
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.connected { background-color: #d4edda; color: #155724; }
|
||||
.disconnected { background-color: #f8d7da; color: #721c24; }
|
||||
.error { background-color: #fff3cd; color: #856404; }
|
||||
button {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
margin: 5px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover { background-color: #0056b3; }
|
||||
button:disabled {
|
||||
background-color: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
#videoContainer {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
#rtspImage {
|
||||
max-width: 100%;
|
||||
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;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
margin-top: 20px;
|
||||
height: 200px;
|
||||
overflow-y: auto;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>前端WebSocket连接示例</h1>
|
||||
|
||||
<div id="status" class="status disconnected">未连接</div>
|
||||
|
||||
<div>
|
||||
<button id="connectBtn" onclick="connectWebSocket()">连接WebSocket</button>
|
||||
<button id="disconnectBtn" onclick="disconnectWebSocket()" disabled>断开连接</button>
|
||||
<button id="startRtspBtn" onclick="startRtsp()" disabled>启动RTSP</button>
|
||||
<button id="stopRtspBtn" onclick="stopRtsp()" disabled>停止RTSP</button>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 10px;">
|
||||
<button onclick="checkBackendStatus()">检查后端状态</button>
|
||||
<button onclick="testSocketConnection()">测试Socket连接</button>
|
||||
<button onclick="clearLog()">清空日志</button>
|
||||
<button onclick="showDebugInfo()">显示调试信息</button>
|
||||
<button onclick="showMemoryInfo()">内存使用情况</button>
|
||||
<button onclick="forceGarbageCollection()">强制垃圾回收</button>
|
||||
</div>
|
||||
|
||||
<div id="debugInfo" style="display: none; margin-top: 10px; padding: 10px; background-color: #e9ecef; border-radius: 4px;">
|
||||
<h4>调试信息</h4>
|
||||
<div id="debugContent"></div>
|
||||
</div>
|
||||
|
||||
<div id="memoryInfo" style="display: none; margin-top: 10px; padding: 10px; background-color: #fff3cd; border-radius: 4px;">
|
||||
<h4>内存使用情况</h4>
|
||||
<div id="memoryContent"></div>
|
||||
</div>
|
||||
|
||||
<div id="videoContainer">
|
||||
<h3>RTSP视频流</h3>
|
||||
<img id="rtspImage" src="" alt="RTSP视频流" style="display: none;">
|
||||
<div id="noVideo">暂无视频流</div>
|
||||
</div>
|
||||
|
||||
<div class="log" id="logContainer"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let socket = null;
|
||||
let frameCount = 0;
|
||||
let lastFrameTime = 0;
|
||||
let imageCache = null; // 图像缓存,避免重复创建
|
||||
let frameSkipCount = 0; // 跳帧计数
|
||||
|
||||
// 后端服务器地址配置
|
||||
const BACKEND_URL = 'http://localhost:5000'; // 根据实际情况修改
|
||||
// 如果是远程服务器,使用: 'http://192.168.1.173:5000'
|
||||
|
||||
// 内存优化配置
|
||||
const FRAME_SKIP_THRESHOLD = 1; // 每3帧显示1帧
|
||||
const MAX_FPS = 30; // 最大显示帧率
|
||||
const MIN_FRAME_INTERVAL = 1000 / MAX_FPS; // 最小帧间隔(ms)
|
||||
|
||||
function log(message) {
|
||||
const logContainer = document.getElementById('logContainer');
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
logContainer.innerHTML += `[${timestamp}] ${message}<br>`;
|
||||
logContainer.scrollTop = logContainer.scrollHeight;
|
||||
}
|
||||
|
||||
function updateStatus(status, className) {
|
||||
const statusEl = document.getElementById('status');
|
||||
statusEl.textContent = status;
|
||||
statusEl.className = `status ${className}`;
|
||||
}
|
||||
|
||||
function updateButtons(connected, rtspRunning = false) {
|
||||
document.getElementById('connectBtn').disabled = connected;
|
||||
document.getElementById('disconnectBtn').disabled = !connected;
|
||||
document.getElementById('startRtspBtn').disabled = !connected || rtspRunning;
|
||||
document.getElementById('stopRtspBtn').disabled = !connected || !rtspRunning;
|
||||
}
|
||||
|
||||
function connectWebSocket() {
|
||||
try {
|
||||
log('正在连接到 ' + BACKEND_URL);
|
||||
|
||||
// 创建Socket.IO连接
|
||||
socket = io(BACKEND_URL, {
|
||||
transports: ['websocket', 'polling'],
|
||||
timeout: 10000,
|
||||
forceNew: true
|
||||
});
|
||||
|
||||
// 连接成功事件
|
||||
socket.on('connect', () => {
|
||||
log('✅ WebSocket连接成功!Socket ID: ' + socket.id);
|
||||
log('🔍 连接详情: ' + JSON.stringify({
|
||||
connected: socket.connected,
|
||||
id: socket.id,
|
||||
transport: socket.io.engine.transport.name
|
||||
}));
|
||||
updateStatus('已连接', 'connected');
|
||||
updateButtons(true);
|
||||
});
|
||||
|
||||
// 连接失败事件
|
||||
socket.on('connect_error', (error) => {
|
||||
log('❌ 连接失败: ' + error.message);
|
||||
log('🔍 错误详情: ' + JSON.stringify(error));
|
||||
updateStatus('连接失败', 'error');
|
||||
updateButtons(false);
|
||||
});
|
||||
|
||||
// 断开连接事件
|
||||
socket.on('disconnect', (reason) => {
|
||||
log('⚠️ 连接断开: ' + reason);
|
||||
updateStatus('已断开', 'disconnected');
|
||||
updateButtons(false);
|
||||
hideVideo();
|
||||
});
|
||||
|
||||
// 监听所有事件(调试用)
|
||||
socket.onAny((eventName, ...args) => {
|
||||
// log(`📨 收到事件: ${eventName}, 数据: ${JSON.stringify(args)}`);
|
||||
});
|
||||
|
||||
// 监听RTSP状态事件
|
||||
socket.on('rtsp_status', (data) => {
|
||||
log('📺 RTSP状态: ' + JSON.stringify(data));
|
||||
if (data.status === 'started') {
|
||||
updateButtons(true, true);
|
||||
} else if (data.status === 'stopped') {
|
||||
updateButtons(true, false);
|
||||
hideVideo();
|
||||
}
|
||||
});
|
||||
|
||||
// 监听RTSP帧数据
|
||||
socket.on('rtsp_frame', (data) => {
|
||||
if (data.image) {
|
||||
frameCount++;
|
||||
|
||||
// 帧率控制和跳帧优化
|
||||
const currentTime = Date.now();
|
||||
if (currentTime - lastFrameTime < MIN_FRAME_INTERVAL) {
|
||||
frameSkipCount++;
|
||||
return; // 跳过此帧以控制帧率
|
||||
}
|
||||
|
||||
// 每隔几帧显示一次,减少内存压力
|
||||
if (frameCount % (FRAME_SKIP_THRESHOLD + 1) === 0) {
|
||||
displayFrameOptimized(data.image);
|
||||
lastFrameTime = currentTime;
|
||||
}
|
||||
|
||||
if (frameCount % 60 === 0) { // 每60帧记录一次
|
||||
log(`🎬 已接收 ${frameCount} 帧,跳过 ${frameSkipCount} 帧`);
|
||||
}
|
||||
|
||||
} else {
|
||||
log('⚠️ 收到rtsp_frame事件但无图像数据: ' + JSON.stringify(data));
|
||||
}
|
||||
});
|
||||
|
||||
// 监听错误事件
|
||||
socket.on('error', (error) => {
|
||||
log('❌ Socket错误: ' + JSON.stringify(error));
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
log('💥 连接异常: ' + error.message);
|
||||
log('🔍 异常堆栈: ' + error.stack);
|
||||
updateStatus('连接异常', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function disconnectWebSocket() {
|
||||
if (socket) {
|
||||
socket.disconnect();
|
||||
socket = null;
|
||||
log('主动断开连接');
|
||||
updateStatus('已断开', 'disconnected');
|
||||
updateButtons(false);
|
||||
hideVideo();
|
||||
}
|
||||
}
|
||||
|
||||
function startRtsp() {
|
||||
if (socket && socket.connected) {
|
||||
log('🚀 发送start_rtsp事件');
|
||||
log('🔍 Socket状态: ' + JSON.stringify({
|
||||
connected: socket.connected,
|
||||
id: socket.id,
|
||||
transport: socket.io.engine.transport.name
|
||||
}));
|
||||
|
||||
// 发送事件并监听确认
|
||||
socket.emit('start_rtsp', {}, (ack) => {
|
||||
if (ack) {
|
||||
log('✅ start_rtsp事件已确认: ' + JSON.stringify(ack));
|
||||
} else {
|
||||
log('⚠️ start_rtsp事件无确认响应');
|
||||
}
|
||||
});
|
||||
|
||||
frameCount = 0;
|
||||
|
||||
// 设置超时检查
|
||||
setTimeout(() => {
|
||||
if (frameCount === 0) {
|
||||
log('⏰ 5秒后仍未收到视频帧,可能存在问题');
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
} else {
|
||||
log('❌ WebSocket未连接,无法启动RTSP');
|
||||
log('🔍 Socket状态: ' + (socket ? '存在但未连接' : '不存在'));
|
||||
}
|
||||
}
|
||||
|
||||
function stopRtsp() {
|
||||
if (socket && socket.connected) {
|
||||
log('🛑 发送stop_rtsp事件');
|
||||
socket.emit('stop_rtsp', {}, (ack) => {
|
||||
if (ack) {
|
||||
log('✅ stop_rtsp事件已确认: ' + JSON.stringify(ack));
|
||||
} else {
|
||||
log('⚠️ stop_rtsp事件无确认响应');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
log('❌ WebSocket未连接,无法停止RTSP');
|
||||
}
|
||||
}
|
||||
|
||||
// 优化的帧显示函数,减少内存泄漏
|
||||
function displayFrameOptimized(base64Image) {
|
||||
const img = document.getElementById('rtspImage');
|
||||
const noVideo = document.getElementById('noVideo');
|
||||
|
||||
try {
|
||||
// 直接设置图像源,避免创建额外的Image对象
|
||||
const dataUrl = 'data:image/jpeg;base64,' + base64Image;
|
||||
|
||||
// 清理之前的图像缓存
|
||||
if (imageCache) {
|
||||
imageCache.src = '';
|
||||
imageCache = null;
|
||||
}
|
||||
|
||||
// 使用requestAnimationFrame优化渲染
|
||||
requestAnimationFrame(() => {
|
||||
img.src = dataUrl;
|
||||
img.style.display = 'block';
|
||||
noVideo.style.display = 'none';
|
||||
});
|
||||
|
||||
// 定期清理base64数据URL缓存
|
||||
if (frameCount % 100 === 0) {
|
||||
// 强制垃圾回收(如果浏览器支持)
|
||||
if (window.gc) {
|
||||
window.gc();
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('显示帧失败:', error);
|
||||
log('❌ 显示帧失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 保留原始函数作为备用
|
||||
function displayFrame(base64Image) {
|
||||
displayFrameOptimized(base64Image);
|
||||
}
|
||||
|
||||
function hideVideo() {
|
||||
const img = document.getElementById('rtspImage');
|
||||
const noVideo = document.getElementById('noVideo');
|
||||
|
||||
// 清理图像资源
|
||||
img.style.display = 'none';
|
||||
img.src = '';
|
||||
noVideo.style.display = 'block';
|
||||
|
||||
// 清理缓存
|
||||
if (imageCache) {
|
||||
imageCache.src = '';
|
||||
imageCache = null;
|
||||
}
|
||||
|
||||
// 重置计数器
|
||||
frameCount = 0;
|
||||
frameSkipCount = 0;
|
||||
lastFrameTime = 0;
|
||||
|
||||
// 强制垃圾回收
|
||||
if (window.gc) {
|
||||
window.gc();
|
||||
}
|
||||
}
|
||||
|
||||
// 检查后端服务状态
|
||||
function checkBackendStatus() {
|
||||
log('🔍 检查后端服务状态...');
|
||||
|
||||
fetch(BACKEND_URL + '/health')
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
log('✅ 后端HTTP服务正常');
|
||||
return response.text();
|
||||
} else {
|
||||
log('⚠️ 后端HTTP服务响应异常: ' + response.status);
|
||||
}
|
||||
})
|
||||
.then(data => {
|
||||
if (data) {
|
||||
log('📄 后端响应: ' + data);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
log('❌ 无法连接到后端服务: ' + error.message);
|
||||
log('💡 请检查后端服务是否已启动,地址是否正确');
|
||||
});
|
||||
}
|
||||
|
||||
// 测试Socket.IO连接
|
||||
function testSocketConnection() {
|
||||
log('🧪 测试Socket.IO连接...');
|
||||
|
||||
const testSocket = io(BACKEND_URL + '/socket.io/', {
|
||||
transports: ['polling'],
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
testSocket.on('connect', () => {
|
||||
log('✅ Socket.IO测试连接成功');
|
||||
testSocket.disconnect();
|
||||
});
|
||||
|
||||
testSocket.on('connect_error', (error) => {
|
||||
log('❌ Socket.IO测试连接失败: ' + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
// 清空日志
|
||||
function clearLog() {
|
||||
document.getElementById('logContainer').innerHTML = '';
|
||||
log('🧹 日志已清空');
|
||||
}
|
||||
|
||||
// 显示调试信息
|
||||
function showDebugInfo() {
|
||||
const debugInfo = document.getElementById('debugInfo');
|
||||
const debugContent = document.getElementById('debugContent');
|
||||
|
||||
let info = '<strong>当前状态:</strong><br>';
|
||||
info += `Socket对象: ${socket ? '存在' : '不存在'}<br>`;
|
||||
|
||||
if (socket) {
|
||||
info += `连接状态: ${socket.connected ? '已连接' : '未连接'}<br>`;
|
||||
info += `Socket ID: ${socket.id || '无'}<br>`;
|
||||
info += `传输方式: ${socket.io?.engine?.transport?.name || '未知'}<br>`;
|
||||
info += `URL: ${socket.io?.uri || '未知'}<br>`;
|
||||
}
|
||||
|
||||
info += `<br><strong>配置信息:</strong><br>`;
|
||||
info += `后端地址: ${BACKEND_URL}<br>`;
|
||||
info += `接收帧数: ${frameCount}<br>`;
|
||||
info += `页面URL: ${window.location.href}<br>`;
|
||||
info += `用户代理: ${navigator.userAgent}<br>`;
|
||||
|
||||
debugContent.innerHTML = info;
|
||||
debugInfo.style.display = debugInfo.style.display === 'none' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// 显示内存使用情况
|
||||
function showMemoryInfo() {
|
||||
const memoryInfo = document.getElementById('memoryInfo');
|
||||
const memoryContent = document.getElementById('memoryContent');
|
||||
|
||||
let info = '<strong>内存使用情况:</strong><br>';
|
||||
|
||||
// 检查浏览器内存API支持
|
||||
if (performance.memory) {
|
||||
const memory = performance.memory;
|
||||
info += `已使用内存: ${(memory.usedJSHeapSize / 1024 / 1024).toFixed(2)} MB<br>`;
|
||||
info += `总分配内存: ${(memory.totalJSHeapSize / 1024 / 1024).toFixed(2)} MB<br>`;
|
||||
info += `内存限制: ${(memory.jsHeapSizeLimit / 1024 / 1024).toFixed(2)} MB<br>`;
|
||||
info += `内存使用率: ${((memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100).toFixed(2)}%<br>`;
|
||||
} else {
|
||||
info += '浏览器不支持内存API<br>';
|
||||
}
|
||||
|
||||
info += `<br><strong>优化状态:</strong><br>`;
|
||||
info += `接收帧数: ${frameCount}<br>`;
|
||||
info += `跳过帧数: ${frameSkipCount}<br>`;
|
||||
info += `跳帧率: ${frameCount > 0 ? ((frameSkipCount / frameCount) * 100).toFixed(2) : 0}%<br>`;
|
||||
info += `最大FPS限制: ${MAX_FPS}<br>`;
|
||||
info += `帧跳过阈值: 每${FRAME_SKIP_THRESHOLD + 1}帧显示1帧<br>`;
|
||||
|
||||
memoryContent.innerHTML = info;
|
||||
memoryInfo.style.display = memoryInfo.style.display === 'none' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// 强制垃圾回收
|
||||
function forceGarbageCollection() {
|
||||
log('🧹 尝试强制垃圾回收...');
|
||||
|
||||
// 清理可能的内存泄漏
|
||||
if (imageCache) {
|
||||
imageCache.src = '';
|
||||
imageCache = null;
|
||||
}
|
||||
|
||||
// 强制垃圾回收(如果浏览器支持)
|
||||
if (window.gc) {
|
||||
window.gc();
|
||||
log('✅ 强制垃圾回收完成');
|
||||
} else {
|
||||
log('⚠️ 浏览器不支持强制垃圾回收');
|
||||
}
|
||||
|
||||
// 显示内存使用情况
|
||||
setTimeout(() => {
|
||||
showMemoryInfo();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// 定期内存监控
|
||||
function startMemoryMonitoring() {
|
||||
setInterval(() => {
|
||||
if (performance.memory && frameCount > 0) {
|
||||
const memory = performance.memory;
|
||||
const usagePercent = (memory.usedJSHeapSize / memory.jsHeapSizeLimit) * 100;
|
||||
|
||||
// 如果内存使用率超过80%,自动进行垃圾回收
|
||||
if (usagePercent > 80) {
|
||||
log(`⚠️ 内存使用率过高: ${usagePercent.toFixed(2)}%,自动清理...`);
|
||||
forceGarbageCollection();
|
||||
}
|
||||
}
|
||||
}, 10000); // 每10秒检查一次
|
||||
}
|
||||
|
||||
// 页面加载完成后的初始化
|
||||
window.onload = function() {
|
||||
log('📄 页面加载完成,可以开始连接WebSocket');
|
||||
log('🌐 后端地址: ' + BACKEND_URL);
|
||||
log('⚠️ 请确保后端服务已启动');
|
||||
log('🔧 内存优化已启用: 最大FPS=' + MAX_FPS + ', 跳帧阈值=' + (FRAME_SKIP_THRESHOLD + 1));
|
||||
|
||||
// 启动内存监控
|
||||
startMemoryMonitoring();
|
||||
|
||||
// 自动检查后端状态
|
||||
setTimeout(() => {
|
||||
testSocketConnection();
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// 页面关闭时清理连接
|
||||
window.onbeforeunload = function() {
|
||||
if (socket) {
|
||||
socket.disconnect();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -1,382 +0,0 @@
|
||||
/**
|
||||
* 前端WebSocket连接示例 - JavaScript版本
|
||||
* 使用Socket.IO客户端连接Flask-SocketIO后端
|
||||
*/
|
||||
|
||||
// 引入Socket.IO客户端库
|
||||
// 在HTML中添加: <script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
|
||||
// 或者使用npm安装: npm install socket.io-client
|
||||
|
||||
class WebSocketManager {
|
||||
constructor() {
|
||||
this.socket = null;
|
||||
this.isConnected = false;
|
||||
this.isRtspRunning = false;
|
||||
this.frameCount = 0;
|
||||
this.reconnectAttempts = 0;
|
||||
this.maxReconnectAttempts = 5;
|
||||
this.reconnectInterval = 3000;
|
||||
|
||||
// 后端服务器配置
|
||||
this.BACKEND_URL = 'http://localhost:5000'; // 根据实际情况修改
|
||||
// 如果是远程服务器,使用: 'http://192.168.1.173:5000'
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接WebSocket服务器
|
||||
*/
|
||||
connect() {
|
||||
try {
|
||||
console.log(`正在连接到 ${this.BACKEND_URL}`);
|
||||
|
||||
// 创建Socket.IO连接
|
||||
this.socket = io(this.BACKEND_URL, {
|
||||
transports: ['websocket', 'polling'], // 支持WebSocket和轮询
|
||||
timeout: 10000, // 连接超时时间
|
||||
forceNew: true, // 强制创建新连接
|
||||
autoConnect: true // 自动连接
|
||||
});
|
||||
|
||||
this.setupEventListeners();
|
||||
|
||||
} catch (error) {
|
||||
console.error('连接异常:', error);
|
||||
this.onError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置事件监听器
|
||||
*/
|
||||
setupEventListeners() {
|
||||
// 连接成功事件
|
||||
this.socket.on('connect', () => {
|
||||
this.isConnected = true;
|
||||
this.reconnectAttempts = 0;
|
||||
console.log(`WebSocket连接成功!Socket ID: ${this.socket.id}`);
|
||||
this.onConnect();
|
||||
});
|
||||
|
||||
// 连接失败事件
|
||||
this.socket.on('connect_error', (error) => {
|
||||
this.isConnected = false;
|
||||
console.error('连接失败:', error.message);
|
||||
this.onConnectError(error);
|
||||
this.attemptReconnect();
|
||||
});
|
||||
|
||||
// 断开连接事件
|
||||
this.socket.on('disconnect', (reason) => {
|
||||
this.isConnected = false;
|
||||
this.isRtspRunning = false;
|
||||
console.log('连接断开:', reason);
|
||||
this.onDisconnect(reason);
|
||||
|
||||
// 如果不是主动断开,尝试重连
|
||||
if (reason !== 'io client disconnect') {
|
||||
this.attemptReconnect();
|
||||
}
|
||||
});
|
||||
|
||||
// 监听RTSP状态事件
|
||||
this.socket.on('rtsp_status', (data) => {
|
||||
console.log('RTSP状态:', data);
|
||||
this.handleRtspStatus(data);
|
||||
});
|
||||
|
||||
// 监听RTSP帧数据
|
||||
this.socket.on('rtsp_frame', (data) => {
|
||||
if (data.image) {
|
||||
this.frameCount++;
|
||||
this.onRtspFrame(data.image);
|
||||
|
||||
// 每30帧记录一次
|
||||
if (this.frameCount % 30 === 0) {
|
||||
console.log(`已接收 ${this.frameCount} 帧`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开WebSocket连接
|
||||
*/
|
||||
disconnect() {
|
||||
if (this.socket) {
|
||||
this.socket.disconnect();
|
||||
this.socket = null;
|
||||
this.isConnected = false;
|
||||
this.isRtspRunning = false;
|
||||
console.log('主动断开连接');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动RTSP视频流
|
||||
*/
|
||||
startRtsp() {
|
||||
if (this.socket && this.isConnected) {
|
||||
console.log('发送start_rtsp事件');
|
||||
this.socket.emit('start_rtsp');
|
||||
this.frameCount = 0;
|
||||
} else {
|
||||
console.warn('WebSocket未连接,无法启动RTSP');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止RTSP视频流
|
||||
*/
|
||||
stopRtsp() {
|
||||
if (this.socket && this.isConnected) {
|
||||
console.log('发送stop_rtsp事件');
|
||||
this.socket.emit('stop_rtsp');
|
||||
} else {
|
||||
console.warn('WebSocket未连接,无法停止RTSP');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理RTSP状态变化
|
||||
*/
|
||||
handleRtspStatus(data) {
|
||||
switch (data.status) {
|
||||
case 'started':
|
||||
this.isRtspRunning = true;
|
||||
this.onRtspStarted();
|
||||
break;
|
||||
case 'stopped':
|
||||
this.isRtspRunning = false;
|
||||
this.onRtspStopped();
|
||||
break;
|
||||
case 'already_running':
|
||||
console.log('RTSP已在运行中');
|
||||
this.isRtspRunning = true;
|
||||
break;
|
||||
case 'error':
|
||||
console.error('RTSP错误:', data.message);
|
||||
this.onRtspError(data.message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试重连
|
||||
*/
|
||||
attemptReconnect() {
|
||||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
this.reconnectAttempts++;
|
||||
console.log(`尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
|
||||
|
||||
setTimeout(() => {
|
||||
this.connect();
|
||||
}, this.reconnectInterval);
|
||||
} else {
|
||||
console.error('WebSocket重连失败,已达到最大重试次数');
|
||||
this.onReconnectFailed();
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 事件回调函数(可以被重写) ==========
|
||||
|
||||
/**
|
||||
* 连接成功回调
|
||||
*/
|
||||
onConnect() {
|
||||
// 可以在这里添加连接成功后的逻辑
|
||||
console.log('WebSocket连接成功回调');
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接错误回调
|
||||
*/
|
||||
onConnectError(error) {
|
||||
// 可以在这里添加连接错误处理逻辑
|
||||
console.log('WebSocket连接错误回调:', error);
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接回调
|
||||
*/
|
||||
onDisconnect(reason) {
|
||||
// 可以在这里添加断开连接处理逻辑
|
||||
console.log('WebSocket断开连接回调:', reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用错误回调
|
||||
*/
|
||||
onError(error) {
|
||||
// 可以在这里添加错误处理逻辑
|
||||
console.log('WebSocket错误回调:', error);
|
||||
}
|
||||
|
||||
/**
|
||||
* RTSP启动回调
|
||||
*/
|
||||
onRtspStarted() {
|
||||
console.log('RTSP视频流已启动');
|
||||
}
|
||||
|
||||
/**
|
||||
* RTSP停止回调
|
||||
*/
|
||||
onRtspStopped() {
|
||||
console.log('RTSP视频流已停止');
|
||||
}
|
||||
|
||||
/**
|
||||
* RTSP错误回调
|
||||
*/
|
||||
onRtspError(message) {
|
||||
console.error('RTSP错误:', message);
|
||||
}
|
||||
|
||||
/**
|
||||
* RTSP帧数据回调
|
||||
* @param {string} base64Image - Base64编码的图像数据
|
||||
*/
|
||||
onRtspFrame(base64Image) {
|
||||
// 在这里处理接收到的视频帧
|
||||
// 例如:显示在img元素中
|
||||
const imgElement = document.getElementById('rtspImage');
|
||||
if (imgElement) {
|
||||
imgElement.src = 'data:image/jpeg;base64,' + base64Image;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重连失败回调
|
||||
*/
|
||||
onReconnectFailed() {
|
||||
console.error('WebSocket重连失败');
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 使用示例 ==========
|
||||
|
||||
// 创建WebSocket管理器实例
|
||||
const wsManager = new WebSocketManager();
|
||||
|
||||
// 重写回调函数(可选)
|
||||
wsManager.onConnect = function() {
|
||||
console.log('自定义连接成功处理');
|
||||
// 连接成功后可以自动启动RTSP
|
||||
// this.startRtsp();
|
||||
};
|
||||
|
||||
wsManager.onRtspFrame = function(base64Image) {
|
||||
// 自定义帧处理逻辑
|
||||
const imgElement = document.getElementById('videoDisplay');
|
||||
if (imgElement) {
|
||||
imgElement.src = 'data:image/jpeg;base64,' + base64Image;
|
||||
imgElement.style.display = 'block';
|
||||
}
|
||||
};
|
||||
|
||||
// ========== 具体使用方法 ==========
|
||||
|
||||
/**
|
||||
* 1. 连接WebSocket
|
||||
*/
|
||||
function connectWebSocket() {
|
||||
wsManager.connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* 2. 断开WebSocket
|
||||
*/
|
||||
function disconnectWebSocket() {
|
||||
wsManager.disconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* 3. 启动RTSP视频流
|
||||
*/
|
||||
function startRtspStream() {
|
||||
wsManager.startRtsp();
|
||||
}
|
||||
|
||||
/**
|
||||
* 4. 停止RTSP视频流
|
||||
*/
|
||||
function stopRtspStream() {
|
||||
wsManager.stopRtsp();
|
||||
}
|
||||
|
||||
/**
|
||||
* 5. 检查连接状态
|
||||
*/
|
||||
function checkConnectionStatus() {
|
||||
console.log('连接状态:', wsManager.isConnected);
|
||||
console.log('RTSP状态:', wsManager.isRtspRunning);
|
||||
console.log('接收帧数:', wsManager.frameCount);
|
||||
}
|
||||
|
||||
// ========== HTML页面中的使用示例 ==========
|
||||
/*
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>WebSocket RTSP示例</title>
|
||||
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<button onclick="connectWebSocket()">连接</button>
|
||||
<button onclick="disconnectWebSocket()">断开</button>
|
||||
<button onclick="startRtspStream()">启动RTSP</button>
|
||||
<button onclick="stopRtspStream()">停止RTSP</button>
|
||||
<button onclick="checkConnectionStatus()">检查状态</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3>视频显示</h3>
|
||||
<img id="videoDisplay" style="max-width: 100%; display: none;" />
|
||||
</div>
|
||||
|
||||
<script src="javascript_websocket_example.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
*/
|
||||
|
||||
// ========== Node.js环境中的使用示例 ==========
|
||||
/*
|
||||
// 安装依赖: npm install socket.io-client
|
||||
const { io } = require('socket.io-client');
|
||||
|
||||
// 然后使用上面的WebSocketManager类
|
||||
const wsManager = new WebSocketManager();
|
||||
wsManager.connect();
|
||||
|
||||
// 5秒后启动RTSP
|
||||
setTimeout(() => {
|
||||
wsManager.startRtsp();
|
||||
}, 5000);
|
||||
|
||||
// 30秒后停止RTSP并断开连接
|
||||
setTimeout(() => {
|
||||
wsManager.stopRtsp();
|
||||
setTimeout(() => {
|
||||
wsManager.disconnect();
|
||||
}, 2000);
|
||||
}, 30000);
|
||||
*/
|
||||
|
||||
// 导出类(用于模块化)
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = WebSocketManager;
|
||||
}
|
||||
|
||||
// 全局暴露(用于浏览器环境)
|
||||
if (typeof window !== 'undefined') {
|
||||
window.WebSocketManager = WebSocketManager;
|
||||
window.wsManager = wsManager;
|
||||
}
|
||||
|
||||
console.log('WebSocket管理器已加载,可以使用 wsManager 实例或创建新的 WebSocketManager 实例');
|
||||
console.log('使用方法:');
|
||||
console.log('1. wsManager.connect() - 连接WebSocket');
|
||||
console.log('2. wsManager.startRtsp() - 启动RTSP视频流');
|
||||
console.log('3. wsManager.stopRtsp() - 停止RTSP视频流');
|
||||
console.log('4. wsManager.disconnect() - 断开WebSocket连接');
|
@ -12,9 +12,9 @@ try:
|
||||
sio.connect('http://192.168.1.173:5000', wait_timeout=10)
|
||||
print('WebSocket连接成功!')
|
||||
|
||||
# 发送启动RTSP事件
|
||||
sio.emit('start_rtsp')
|
||||
print('已发送start_rtsp事件,等待5秒接收数据...')
|
||||
# 发送启动视频流事件
|
||||
sio.emit('start_video')
|
||||
print('已发送start_video事件,等待5秒接收数据...')
|
||||
|
||||
# 等待并接收事件
|
||||
for i in range(5):
|
||||
@ -24,16 +24,16 @@ try:
|
||||
if event:
|
||||
event_name, data = event
|
||||
print(f'收到事件: {event_name}, 数据类型: {type(data)}')
|
||||
if event_name == 'rtsp_frame' and isinstance(data, dict) and 'image' in data:
|
||||
if event_name == 'video_frame' and isinstance(data, dict) and 'image' in data:
|
||||
print(f'收到图像数据,长度: {len(data["image"])} 字符')
|
||||
elif event_name == 'rtsp_status':
|
||||
print(f'RTSP状态: {data}')
|
||||
elif event_name == 'video_status':
|
||||
print(f'视频状态: {data}')
|
||||
except socketio.exceptions.TimeoutError:
|
||||
print(f'等待事件超时 ({i+1}/5)')
|
||||
|
||||
# 发送停止RTSP事件
|
||||
sio.emit('stop_rtsp')
|
||||
print('已发送stop_rtsp事件')
|
||||
# 发送停止视频流事件
|
||||
sio.emit('stop_video')
|
||||
print('已发送stop_video事件')
|
||||
|
||||
# 等待停止状态事件
|
||||
try:
|
||||
|
Loading…
Reference in New Issue
Block a user