提交我的更新内容

This commit is contained in:
root 2025-08-02 16:52:17 +08:00
parent ffc269331a
commit 85d9ace324
29 changed files with 439 additions and 1610 deletions

41
.gitignore vendored Normal file
View 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

View File

@ -124,8 +124,8 @@ def handle_connect():
### 2. RTSP流问题 ### 2. RTSP流问题
```python ```python
# 在generate_rtsp_frames函数中设置断点 # 在generate_video_frames函数中设置断点
def generate_rtsp_frames(): def generate_video_frames():
print(f'RTSP URL: {rtsp_url}') # 调试输出 print(f'RTSP URL: {rtsp_url}') # 调试输出
# 设置断点检查rtsp_url值 # 设置断点检查rtsp_url值
cap = cv2.VideoCapture(rtsp_url) cap = cv2.VideoCapture(rtsp_url)
@ -177,7 +177,7 @@ logging.getLogger().setLevel(logging.DEBUG)
// 在浏览器控制台中测试WebSocket连接 // 在浏览器控制台中测试WebSocket连接
const socket = io('http://127.0.0.1:5000'); const socket = io('http://127.0.0.1:5000');
socket.on('connect', () => console.log('连接成功')); socket.on('connect', () => console.log('连接成功'));
socket.emit('start_rtsp', {}); socket.emit('start_video', {});
``` ```
## 性能调试 ## 性能调试

View File

@ -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 系统稳定性
---
如有其他问题,请查看项目文档或联系技术支持。

View File

@ -45,7 +45,7 @@ logger = logging.getLogger(__name__)
# 创建Flask应用 # 创建Flask应用
app = Flask(__name__) app = Flask(__name__)
app.config['SECRET_KEY'] = 'body-balance-detection-system-2024' 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支持
CORS(app, origins='*', supports_credentials=True, allow_headers=['Content-Type', 'Authorization'], methods=['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']) 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配置 # 读取RTSP配置
config = configparser.ConfigParser() config = configparser.ConfigParser()
config.read(os.path.join(os.path.dirname(__file__), 'config.ini'), encoding='utf-8') config.read(os.path.join(os.path.dirname(__file__), '..', 'config.ini'), encoding='utf-8')
rtsp_url = config.get('CAMERA', 'rtsp_url', fallback=None) device_index = config.get('CAMERA', 'device_index', fallback=None)
# 全局变量 # 全局变量
db_manager = None db_manager = None
@ -77,11 +77,20 @@ def init_app():
# 创建必要的目录 # 创建必要的目录
os.makedirs('logs', exist_ok=True) os.makedirs('logs', exist_ok=True)
os.makedirs('data', exist_ok=True) os.makedirs('data', exist_ok=True)
os.makedirs('exports', exist_ok=True)
os.makedirs('videos', 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) db_dir = os.path.dirname(db_path)
os.makedirs(db_dir, exist_ok=True) os.makedirs(db_dir, exist_ok=True)
@ -313,16 +322,14 @@ def forgot_password():
user = cursor.fetchone() user = cursor.fetchone()
if user: if user:
# 用户存在且手机号匹配,返回密码 # 用户存在且手机号匹配,返回数据库中存储的实际密码
# 注意:这里返回的是加密后的密码,实际应用中需要解密或重置 actual_password = user['password']
# 为了演示,我们假设有一个简单的解密方法
encrypted_password = user['password'] logger.info(f'用户 {username} 密码查询成功')
# 这里简化处理,实际应该有更安全的密码找回机制
# 由于使用MD5加密无法直接解密所以返回提示信息
return jsonify({ return jsonify({
'success': True, 'success': True,
'password': '1234567', # 演示用固定密码 'password': actual_password, # 返回数据库中存储的实际密码
'message': '密码找回成功' 'message': '密码找回成功'
}) })
else: else:
@ -949,35 +956,35 @@ if __name__ == '__main__':
# ==================== WebSocket 事件处理 ==================== # ==================== WebSocket 事件处理 ====================
@socketio.on('start_rtsp') @socketio.on('start_video')
def handle_start_rtsp(data=None): def handle_start_video(data=None):
logger.info(f'收到start_rtsp事件客户端ID: {request.sid}, 数据: {data}') logger.info(f'收到start_video事件客户端ID: {request.sid}, 数据: {data}')
try: try:
if video_stream_manager: if video_stream_manager:
result = video_stream_manager.start_rtsp_stream() result = video_stream_manager.start_video_stream()
emit('rtsp_status', result) emit('video_status', result)
else: else:
emit('rtsp_status', {'status': 'error', 'message': '视频流管理器未初始化'}) emit('video_status', {'status': 'error', 'message': '视频流管理器未初始化'})
except Exception as e: except Exception as e:
logger.error(f'启动RTSP失败: {e}') logger.error(f'启动视频流失败: {e}')
emit('rtsp_status', {'status': 'error', 'message': f'启动失败: {str(e)}'}) emit('video_status', {'status': 'error', 'message': f'启动失败: {str(e)}'})
@socketio.on('stop_rtsp') @socketio.on('stop_video')
def handle_stop_rtsp(data=None): def handle_stop_video(data=None):
logger.info(f'收到stop_rtsp事件客户端ID: {request.sid}, 数据: {data}') logger.info(f'收到stop_video事件客户端ID: {request.sid}, 数据: {data}')
try: try:
if video_stream_manager: if video_stream_manager:
result = video_stream_manager.stop_rtsp_stream() result = video_stream_manager.stop_video_stream()
emit('rtsp_status', result) emit('video_status', result)
else: else:
emit('rtsp_status', {'status': 'error', 'message': '视频流管理器未初始化'}) emit('video_status', {'status': 'error', 'message': '视频流管理器未初始化'})
except Exception as e: except Exception as e:
logger.error(f'停止RTSP失败: {e}') logger.error(f'停止视频流失败: {e}')
emit('rtsp_status', {'status': 'error', 'message': f'停止失败: {str(e)}'}) emit('video_status', {'status': 'error', 'message': f'停止失败: {str(e)}'})
@socketio.on('connect') @socketio.on('connect')
def handle_connect(): def handle_connect():

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@
import sqlite3 import sqlite3
import json import json
import uuid import uuid
from datetime import datetime from datetime import datetime, timezone, timedelta
from typing import List, Dict, Optional, Any from typing import List, Dict, Optional, Any
import logging import logging
@ -20,6 +20,12 @@ class DatabaseManager:
def __init__(self, db_path: str): def __init__(self, db_path: str):
self.db_path = db_path self.db_path = db_path
self.connection = None 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: def get_connection(self) -> sqlite3.Connection:
"""获取数据库连接""" """获取数据库连接"""
@ -143,10 +149,9 @@ class DatabaseManager:
admin_exists = cursor.fetchone()[0] admin_exists = cursor.fetchone()[0]
if admin_exists == 0: if admin_exists == 0:
import hashlib
admin_id = str(uuid.uuid4()) admin_id = str(uuid.uuid4())
# 默认密码为 admin123使用MD5加密 # 默认密码为 admin123明文存储
admin_password = hashlib.md5('admin123'.encode()).hexdigest() admin_password = 'admin123'
cursor.execute(''' cursor.execute('''
INSERT INTO users (id, name, username, password, is_active, user_type) INSERT INTO users (id, name, username, password, is_active, user_type)
@ -172,12 +177,14 @@ class DatabaseManager:
try: try:
patient_id = str(uuid.uuid4()) patient_id = str(uuid.uuid4())
# 使用中国时区时间
china_time = self.get_china_time()
cursor.execute(''' cursor.execute('''
INSERT INTO patients ( INSERT INTO patients (
id, name, gender, age, birth_date, height, weight, id, name, gender, age, birth_date, height, weight,
phone, shoe_size, medical_history, notes phone, shoe_size, medical_history, notes, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', ( ''', (
patient_id, patient_id,
patient_data.get('name'), patient_data.get('name'),
@ -189,7 +196,9 @@ class DatabaseManager:
patient_data.get('phone'), patient_data.get('phone'),
patient_data.get('shoe_size'), patient_data.get('shoe_size'),
patient_data.get('medical_history'), patient_data.get('medical_history'),
patient_data.get('notes') patient_data.get('notes'),
china_time,
china_time
)) ))
conn.commit() conn.commit()
@ -270,11 +279,14 @@ class DatabaseManager:
cursor = conn.cursor() cursor = conn.cursor()
try: try:
# 使用中国时区时间
china_time = self.get_china_time()
cursor.execute(''' cursor.execute('''
UPDATE patients SET UPDATE patients SET
name = ?, gender = ?, age = ?, birth_date = ?, height = ?, weight = ?, name = ?, gender = ?, age = ?, birth_date = ?, height = ?, weight = ?,
phone = ?, shoe_size = ?, medical_history = ?, notes = ?, phone = ?, shoe_size = ?, medical_history = ?, notes = ?,
updated_at = CURRENT_TIMESTAMP updated_at = ?
WHERE id = ? WHERE id = ?
''', ( ''', (
patient_data.get('name'), patient_data.get('name'),
@ -287,6 +299,7 @@ class DatabaseManager:
patient_data.get('shoe_size'), patient_data.get('shoe_size'),
patient_data.get('medical_history'), patient_data.get('medical_history'),
patient_data.get('notes'), patient_data.get('notes'),
china_time,
patient_id patient_id
)) ))
@ -344,17 +357,22 @@ class DatabaseManager:
try: try:
session_id = str(uuid.uuid4()) session_id = str(uuid.uuid4())
# 使用中国时区时间
china_time = self.get_china_time()
cursor.execute(''' cursor.execute('''
INSERT INTO detection_sessions ( INSERT INTO detection_sessions (
id, patient_id, duration, frequency, settings, status id, patient_id, duration, frequency, settings, status, start_time, created_at
) VALUES (?, ?, ?, ?, ?, ?) ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
''', ( ''', (
session_id, session_id,
patient_id, patient_id,
settings.get('duration', 60), settings.get('duration', 60),
settings.get('frequency', 60), settings.get('frequency', 60),
json.dumps(settings), json.dumps(settings),
'created' 'created',
china_time,
china_time
)) ))
conn.commit() conn.commit()
@ -373,11 +391,13 @@ class DatabaseManager:
try: try:
if status in ['completed', 'stopped', 'error']: if status in ['completed', 'stopped', 'error']:
# 使用中国时区时间
china_time = self.get_china_time()
cursor.execute(''' cursor.execute('''
UPDATE detection_sessions SET UPDATE detection_sessions SET
status = ?, data_points = ?, end_time = CURRENT_TIMESTAMP status = ?, data_points = ?, end_time = ?
WHERE id = ? WHERE id = ?
''', (status, data_points, session_id)) ''', (status, data_points, china_time, session_id))
else: else:
cursor.execute(''' cursor.execute('''
UPDATE detection_sessions SET UPDATE detection_sessions SET
@ -553,12 +573,15 @@ class DatabaseManager:
cursor = conn.cursor() cursor = conn.cursor()
try: try:
# 使用中国时区时间
china_time = self.get_china_time()
# 保存不同类型的数据 # 保存不同类型的数据
for data_type, data_value in data.items(): for data_type, data_value in data.items():
cursor.execute(''' cursor.execute('''
INSERT INTO detection_data (session_id, data_type, data_value) INSERT INTO detection_data (session_id, data_type, data_value, timestamp)
VALUES (?, ?, ?) VALUES (?, ?, ?, ?)
''', (session_id, data_type, json.dumps(data_value))) ''', (session_id, data_type, json.dumps(data_value), china_time))
conn.commit() conn.commit()
@ -679,10 +702,13 @@ class DatabaseManager:
try: try:
value_str = json.dumps(value) if not isinstance(value, str) else value value_str = json.dumps(value) if not isinstance(value, str) else value
# 使用中国时区时间
china_time = self.get_china_time()
cursor.execute(''' cursor.execute('''
INSERT OR REPLACE INTO system_settings (key, value, description, updated_at) INSERT OR REPLACE INTO system_settings (key, value, description, updated_at)
VALUES (?, ?, ?, CURRENT_TIMESTAMP) VALUES (?, ?, ?, ?)
''', (key, value_str, description)) ''', (key, value_str, description, china_time))
conn.commit() conn.commit()
logger.info(f'设置系统设置: {key}') logger.info(f'设置系统设置: {key}')
@ -700,8 +726,6 @@ class DatabaseManager:
cursor = conn.cursor() cursor = conn.cursor()
try: try:
import hashlib
# 检查手机号是否已存在(如果提供了手机号) # 检查手机号是否已存在(如果提供了手机号)
if user_data.get('phone'): if user_data.get('phone'):
cursor.execute('SELECT COUNT(*) FROM users WHERE phone = ?', (user_data['phone'],)) cursor.execute('SELECT COUNT(*) FROM users WHERE phone = ?', (user_data['phone'],))
@ -712,20 +736,25 @@ class DatabaseManager:
} }
user_id = str(uuid.uuid4()) 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(''' cursor.execute('''
INSERT INTO users (id, name, username, password, phone, is_active, user_type) INSERT INTO users (id, name, username, password, phone, is_active, user_type, register_date, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', ( ''', (
user_id, user_id,
user_data['name'], user_data['name'],
user_data['username'], user_data['username'],
password_hash, password,
user_data.get('phone'), # 手机号可选 user_data.get('phone'), # 手机号可选
0, # 新注册用户默认未激活,需要管理员审核 1, # 新注册用户默认激活
'user' 'user',
china_time,
china_time,
china_time
)) ))
conn.commit() conn.commit()
@ -757,14 +786,10 @@ class DatabaseManager:
cursor = conn.cursor() cursor = conn.cursor()
try: try:
import hashlib
password_hash = hashlib.md5(password.encode()).hexdigest()
cursor.execute(''' cursor.execute('''
SELECT * FROM users SELECT * FROM users
WHERE username = ? AND password = ? AND is_active = 1 WHERE username = ? AND password = ? AND is_active = 1
''', (username, password_hash)) ''', (username, password))
row = cursor.fetchone() row = cursor.fetchone()
if row: if row:
@ -863,12 +888,15 @@ class DatabaseManager:
cursor = conn.cursor() cursor = conn.cursor()
try: try:
# 使用中国时区时间
china_time = self.get_china_time()
cursor.execute(''' cursor.execute('''
UPDATE users SET UPDATE users SET
is_active = ?, is_active = ?,
updated_at = CURRENT_TIMESTAMP updated_at = ?
WHERE id = ? WHERE id = ?
''', (1 if approved else 0, user_id)) ''', (1 if approved else 0, china_time, user_id))
conn.commit() conn.commit()
status = '通过' if approved else '拒绝' status = '通过' if approved else '拒绝'
@ -903,10 +931,7 @@ class DatabaseManager:
cursor = conn.cursor() cursor = conn.cursor()
try: try:
# 如果包含密码,需要加密 # 密码明文存储,无需加密处理
if 'password' in user_data:
import hashlib
user_data['password'] = hashlib.md5(user_data['password'].encode()).hexdigest()
# 构建更新语句 # 构建更新语句
fields = [] fields = []
@ -918,7 +943,10 @@ class DatabaseManager:
values.append(value) values.append(value)
if fields: 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) values.append(user_id)
sql = f"UPDATE users SET {', '.join(fields)} WHERE id = ?" sql = f"UPDATE users SET {', '.join(fields)} WHERE id = ?"

View File

@ -492,9 +492,9 @@ class VideoStreamManager:
def __init__(self, socketio=None): def __init__(self, socketio=None):
self.socketio = socketio self.socketio = socketio
self.rtsp_url = None self.device_index = None
self.rtsp_thread = None self.video_thread = None
self.rtsp_running = False self.video_running = False
# 用于异步编码的线程池和队列 # 用于异步编码的线程池和队列
self.encoding_executor = ThreadPoolExecutor(max_workers=2) self.encoding_executor = ThreadPoolExecutor(max_workers=2)
@ -515,13 +515,14 @@ class VideoStreamManager:
"""加载RTSP配置""" """加载RTSP配置"""
try: try:
config = configparser.ConfigParser() 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') config.read(config_path, encoding='utf-8')
self.rtsp_url = config.get('CAMERA', 'rtsp_url', fallback=None) device_index_str = config.get('CAMERA', 'device_index', fallback='0')
logger.info(f'RTSP配置加载完成: {self.rtsp_url}') self.device_index = int(device_index_str) if device_index_str else 0
logger.info(f'视频监控设备配置加载完成,设备号: {self.device_index}')
except Exception as e: except Exception as e:
logger.error(f'加载RTSP配置失败: {e}') logger.error(f'视频监控设备配置失败: {e}')
self.rtsp_url = None self.device_index = None
def get_memory_usage(self): def get_memory_usage(self):
"""获取当前进程内存使用量(字节)""" """获取当前进程内存使用量(字节)"""
@ -585,7 +586,7 @@ class VideoStreamManager:
# 发送数据 # 发送数据
if self.socketio: if self.socketio:
self.socketio.emit('rtsp_frame', { self.socketio.emit('video_frame', {
'image': jpg_as_text, 'image': jpg_as_text,
'frame_id': frame_count, 'frame_id': frame_count,
'timestamp': time.time() 'timestamp': time.time()
@ -603,7 +604,7 @@ class VideoStreamManager:
def frame_encoding_worker(self): def frame_encoding_worker(self):
"""帧编码工作线程""" """帧编码工作线程"""
while self.rtsp_running: while self.video_running:
try: try:
# 从队列获取帧 # 从队列获取帧
frame, frame_count = self.frame_queue.get(timeout=1) frame, frame_count = self.frame_queue.get(timeout=1)
@ -641,22 +642,22 @@ class VideoStreamManager:
return frame return frame
def generate_rtsp_frames(self): def generate_video_frames(self):
"""生成RTSP""" """生成视频监控"""
frame_count = 0 frame_count = 0
error_count = 0 error_count = 0
use_test_mode = False use_test_mode = False
last_frame_time = time.time() last_frame_time = time.time()
logger.info(f'开始生成RTSP帧URL: {self.rtsp_url}') logger.info(f'开始生成视频监控帧,设备号: {self.device_index}')
try: try:
cap = cv2.VideoCapture(self.rtsp_url) cap = cv2.VideoCapture(self.device_index)
if not cap.isOpened(): if not cap.isOpened():
logger.warning(f'无法打开RTSP流: {self.rtsp_url},切换到测试模式') logger.warning(f'无法打开视频监控流: {self.device_index},切换到测试模式')
use_test_mode = True use_test_mode = True
if self.socketio: if self.socketio:
self.socketio.emit('rtsp_status', {'status': 'started', 'message': '使用测试视频源'}) self.socketio.emit('video_status', {'status': 'started', 'message': '使用测试视频源'})
else: else:
# 最激进的实时优化设置 # 最激进的实时优化设置
cap.set(cv2.CAP_PROP_BUFFERSIZE, 0) # 完全禁用缓冲区 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_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
logger.info('RTSP流已打开,开始推送帧(激进实时模式)') logger.info('视频监控流已打开,开始推送帧(激进实时模式)')
if self.socketio: 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 = threading.Thread(target=self.frame_encoding_worker)
encoding_thread.daemon = True encoding_thread.daemon = True
encoding_thread.start() encoding_thread.start()
while self.rtsp_running: while self.video_running:
if use_test_mode: if use_test_mode:
# 使用测试模式生成帧 # 使用测试模式生成帧
frame = self.generate_test_frame(frame_count) frame = self.generate_test_frame(frame_count)
ret = True ret = True
else: else:
# 使用RTSP流,添加帧跳过机制减少延迟 # 使用视频监控流,添加帧跳过机制减少延迟
ret, frame = cap.read() ret, frame = cap.read()
if not ret: if not ret:
error_count += 1 error_count += 1
logger.warning(f'RTSP读取帧失败(第{error_count}次),尝试重连...') logger.warning(f'视频监控读取帧失败(第{error_count}次),尝试重连...')
if 'cap' in locals(): if 'cap' in locals():
cap.release() cap.release()
if error_count > 5: if error_count > 5:
logger.warning('RTSP连接失败次数过多,切换到测试模式') logger.warning('视频监控连接失败次数过多,切换到测试模式')
use_test_mode = True use_test_mode = True
if self.socketio: if self.socketio:
self.socketio.emit('rtsp_status', {'status': 'switched', 'message': '已切换到测试视频源'}) self.socketio.emit('video_status', {'status': 'switched', 'message': '已切换到测试视频源'})
continue continue
# 立即重连,不等待 # 立即重连,不等待
cap = cv2.VideoCapture(self.rtsp_url) cap = cv2.VideoCapture(self.device_index)
if cap.isOpened(): if cap.isOpened():
# 重连时应用相同的激进实时设置 # 重连时应用相同的激进实时设置
cap.set(cv2.CAP_PROP_BUFFERSIZE, 0) cap.set(cv2.CAP_PROP_BUFFERSIZE, 0)
@ -766,67 +767,68 @@ class VideoStreamManager:
logger.error(f'帧队列处理失败: {e}') logger.error(f'帧队列处理失败: {e}')
except Exception as e: except Exception as e:
logger.error(f'RTSP推流异常: {e}') logger.error(f'监控视频推流异常: {e}')
if self.socketio: 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: finally:
if 'cap' in locals(): if 'cap' in locals():
cap.release() cap.release()
self.rtsp_running = False self.video_running = False
logger.info(f'RTSP推流结束,总共推送了 {frame_count}') logger.info(f'视频监控推流结束,总共推送了 {frame_count}')
def start_rtsp_stream(self): def start_video_stream(self):
"""启动RTSP推流""" """启动视频监控推流"""
try: try:
if self.rtsp_thread and self.rtsp_thread.is_alive(): if self.video_thread and self.video_thread.is_alive():
logger.warning('RTSP线程已在运行') logger.warning('视频监控线程已在运行')
return {'status': 'already_running', 'message': 'RTSP已在运行'} return {'status': 'already_running', 'message': '视频监控已在运行'}
if not self.rtsp_url: if not self.device_index:
logger.error('RTSP URL未配置') logger.error('视频监控相机未配置')
return {'status': 'error', 'message': 'RTSP URL未配置'} return {'status': 'error', 'message': '视频监控相机未配置'}
logger.info(f'启动RTSP线程URL: {self.rtsp_url}') logger.info(f'视频启动监控线程,设备号: {self.device_index}')
self.rtsp_thread = threading.Thread(target=self.generate_rtsp_frames) self.video_thread = threading.Thread(target=self.generate_video_frames)
self.rtsp_thread.daemon = True self.video_thread.daemon = True
self.rtsp_thread.start() self.video_thread.start()
self.video_running = True
logger.info('RTSP线程已启动') logger.info('视频监控线程已启动')
return {'status': 'started', 'message': 'RTSP推流已启动'} return {'status': 'started', 'message': '视频监控线程已启动'}
except Exception as e: except Exception as e:
logger.error(f'启动RTSP失败: {e}') logger.error(f'视频监控线程启动失败: {e}')
return {'status': 'error', 'message': f'启动失败: {str(e)}'} return {'status': 'error', 'message': f'视频监控线程启动失败: {str(e)}'}
def stop_rtsp_stream(self): def stop_video_stream(self):
"""停止RTSP推流""" """停止视频监控推流"""
try: try:
self.rtsp_running = False self.video_running = False
logger.info('RTSP推流已停止') logger.info('视频监控推流已停止')
return {'status': 'stopped', 'message': 'RTSP推流已停止'} return {'status': 'stopped', 'message': '视频监控推流已停止'}
except Exception as e: except Exception as e:
logger.error(f'停止RTSP失败: {e}') logger.error(f'停止视频监控推流失败: {e}')
return {'status': 'error', 'message': f'停止失败: {str(e)}'} return {'status': 'error', 'message': f'停止失败: {str(e)}'}
def is_streaming(self): def is_streaming(self):
"""检查是否正在推流""" """检查是否正在推流"""
return self.rtsp_running return self.video_running
def get_stream_status(self): def get_stream_status(self):
"""获取推流状态""" """获取推流状态"""
return { return {
'running': self.rtsp_running, 'running': self.video_running,
'rtsp_url': self.rtsp_url, 'device_index': self.device_index,
'thread_alive': self.rtsp_thread.is_alive() if self.rtsp_thread else False 'thread_alive': self.video_thread.is_alive() if self.video_thread else False
} }
def cleanup(self): def cleanup(self):
"""清理资源""" """清理资源"""
try: try:
self.rtsp_running = False self.video_running = False
if self.rtsp_thread and self.rtsp_thread.is_alive(): if self.video_thread and self.video_thread.is_alive():
self.rtsp_thread.join(timeout=2) self.video_thread.join(timeout=2)
self.encoding_executor.shutdown(wait=False) self.encoding_executor.shutdown(wait=False)

View File

@ -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测试完成!')

View File

@ -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测试完成!')

View File

@ -22,7 +22,7 @@ logger = logging.getLogger(__name__)
class Config: 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_file = Path(config_file)
self.config = configparser.ConfigParser() self.config = configparser.ConfigParser()
self._load_config() self._load_config()

View File

@ -39,3 +39,5 @@ secret_key = 79fcc4983d478c2ee672f3305d5e12c7c84fd1b58a18acb650e9f8125bfa805f
session_timeout = 3600 session_timeout = 3600
max_login_attempts = 5 max_login_attempts = 5
[CAMERA]
device_index = 1

View File

@ -27,7 +27,7 @@
"debug": false, "debug": false,
"cors": { "cors": {
"enabled": true, "enabled": true,
"origins": ["http://localhost:5173", "http://127.0.0.1:5173"] "origins": ["http://192.168.1.38:3000"]
}, },
"ssl": { "ssl": {
"enabled": false, "enabled": false,

3
data/.gitkeep Normal file
View File

@ -0,0 +1,3 @@
# 数据目录
# 此文件用于确保 data 目录在版本控制中被保留
# 实际的数据文件会被 .gitignore 忽略

View File

@ -13,6 +13,7 @@
import os import os
import sys import sys
import logging import logging
import socket
from pathlib import Path from pathlib import Path
# 添加项目路径 # 添加项目路径
@ -45,6 +46,16 @@ def setup_debug_logging():
logger.info('调试日志已启用') logger.info('调试日志已启用')
return logger 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(): def check_debug_environment():
"""检查调试环境""" """检查调试环境"""
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -57,7 +68,7 @@ def check_debug_environment():
# 检查必要文件 # 检查必要文件
required_files = [ required_files = [
'backend/app.py', 'backend/app.py',
'backend/config.ini', 'config.ini',
'backend/requirements.txt' 'backend/requirements.txt'
] ]
@ -86,17 +97,21 @@ def start_debug_server():
logger.info('初始化应用...') logger.info('初始化应用...')
init_app() init_app()
# 获取本机IP地址
local_ip = get_local_ip()
# 启动调试服务器 # 启动调试服务器
logger.info('启动调试服务器...') logger.info('启动调试服务器...')
logger.info('调试模式已启用 - 可以在IDE中设置断点') 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('健康检查: http://127.0.0.1:5000/health')
logger.info('按 Ctrl+C 停止服务器') logger.info('按 Ctrl+C 停止服务器')
# 启动SocketIO服务器支持调试 # 启动SocketIO服务器支持调试和远程访问
socketio.run( socketio.run(
app, app,
host='127.0.0.1', host='0.0.0.0', # 允许所有IP访问
port=5000, port=5000,
debug=True, debug=True,
use_reloader=True, # 启用热重载 use_reloader=True, # 启用热重载

Binary file not shown.

After

Width:  |  Height:  |  Size: 623 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

BIN
document/登录页.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@ -14,6 +14,7 @@
"element-plus": "^2.3.9", "element-plus": "^2.3.9",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"pinia": "^2.1.6", "pinia": "^2.1.6",
"socket.io-client": "^4.7.2",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-echarts": "^6.6.1", "vue-echarts": "^6.6.1",
"vue-router": "^4.2.4" "vue-router": "^4.2.4"
@ -767,6 +768,12 @@
"url": "https://opencollective.com/popperjs" "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": { "node_modules/@types/lodash": {
"version": "4.17.20", "version": "4.17.20",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", "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", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" "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": { "node_modules/delayed-stream": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@ -1092,6 +1116,28 @@
"vue": "^3.2.0" "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": { "node_modules/entities": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
@ -1467,6 +1513,12 @@
"node": ">= 0.6" "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": { "node_modules/nanoid": {
"version": "3.3.11", "version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@ -1621,6 +1673,34 @@
"@parcel/watcher": "^2.4.1" "@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": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@ -1826,6 +1906,35 @@
"vue": "^3.2.0" "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": { "node_modules/zrender": {
"version": "5.6.1", "version": "5.6.1",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz", "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",

View File

@ -14,6 +14,7 @@
"element-plus": "^2.3.9", "element-plus": "^2.3.9",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"pinia": "^2.1.6", "pinia": "^2.1.6",
"socket.io-client": "^4.7.2",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-echarts": "^6.6.1", "vue-echarts": "^6.6.1",
"vue-router": "^4.2.4" "vue-router": "^4.2.4"

View 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

View File

@ -108,13 +108,13 @@ export const deviceAPI = {
}, },
// 校准设备 // 校准设备
calibrateDevice(deviceType) { calibrateDevice() {
return api.post(`/api/devices/${deviceType}/calibrate`) return api.post('/api/devices/calibrate')
}, },
// 测试设备 // 测试设备
testDevice(deviceType) { testDevice() {
return api.post(`/api/devices/${deviceType}/test`) return api.post('/api/devices/test')
} }
} }

View File

@ -428,14 +428,15 @@ function connectWebSocket() {
} }
}) })
// RTSP //
socket.on('rtsp_status', (data) => { socket.on('video_status', (data) => {
console.log('📺 RTSP状态:', data) console.log('📺 视频状态:', data)
}) })
// RTSP //
socket.on('rtsp_frame', (data) => { socket.on('video_frame', (data) => {
frameCount++ frameCount++
console.log(`📺 收到视频帧 #${frameCount}, 数据大小: ${data.image ? data.image.length : 0} 字符`)
displayFrame(data.image) displayFrame(data.image)
}) })
@ -461,16 +462,16 @@ function disconnectWebSocket() {
} }
} }
// RTSP //
function startRtsp() { function startRtsp() {
if (socket && socket.connected) { if (socket && socket.connected) {
console.log('🚀 发送start_rtsp事件') console.log('🚀 发送start_video事件')
socket.emit('start_rtsp', {}, (ack) => { socket.emit('start_video', {}, (ack) => {
if (ack) { if (ack) {
console.log('✅ start_rtsp事件已确认:', ack) console.log('✅ start_video事件已确认:', ack)
} else { } else {
console.log('⚠️ start_rtsp事件无确认响应') console.log('⚠️ start_video事件无确认响应')
} }
}) })
@ -484,23 +485,23 @@ function startRtsp() {
}, 5000) }, 5000)
} else { } else {
console.error('❌ WebSocket未连接无法启动RTSP') console.error('❌ WebSocket未连接无法启动视频流')
} }
} }
// RTSP //
function stopRtsp() { function stopRtsp() {
if (socket && socket.connected) { if (socket && socket.connected) {
console.log('🛑 发送stop_rtsp事件') console.log('🛑 发送stop_video事件')
socket.emit('stop_rtsp', {}, (ack) => { socket.emit('stop_video', {}, (ack) => {
if (ack) { if (ack) {
console.log('✅ stop_rtsp事件已确认:', ack) console.log('✅ stop_video事件已确认:', ack)
} else { } else {
console.log('⚠️ stop_rtsp事件无确认响应') console.log('⚠️ stop_video事件无确认响应')
} }
}) })
} else { } else {
console.error('❌ WebSocket未连接无法停止RTSP') console.error('❌ WebSocket未连接无法停止视频流')
} }
} }
@ -511,7 +512,12 @@ function stopRtsp() {
// //
function displayFrame(base64Image) { function displayFrame(base64Image) {
if (base64Image && base64Image.length > 0) {
rtspImgSrc.value = 'data:image/jpeg;base64,' + base64Image rtspImgSrc.value = 'data:image/jpeg;base64,' + base64Image
console.log(`🖼️ 视频帧已设置到img元素base64长度: ${base64Image.length}`)
} else {
console.warn('⚠️ 收到空的视频帧数据')
}
} }

View File

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

View File

@ -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连接');

View File

@ -12,9 +12,9 @@ try:
sio.connect('http://192.168.1.173:5000', wait_timeout=10) sio.connect('http://192.168.1.173:5000', wait_timeout=10)
print('WebSocket连接成功!') print('WebSocket连接成功!')
# 发送启动RTSP事件 # 发送启动视频流事件
sio.emit('start_rtsp') sio.emit('start_video')
print('已发送start_rtsp事件等待5秒接收数据...') print('已发送start_video事件等待5秒接收数据...')
# 等待并接收事件 # 等待并接收事件
for i in range(5): for i in range(5):
@ -24,16 +24,16 @@ try:
if event: if event:
event_name, data = event event_name, data = event
print(f'收到事件: {event_name}, 数据类型: {type(data)}') 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"])} 字符') print(f'收到图像数据,长度: {len(data["image"])} 字符')
elif event_name == 'rtsp_status': elif event_name == 'video_status':
print(f'RTSP状态: {data}') print(f'视频状态: {data}')
except socketio.exceptions.TimeoutError: except socketio.exceptions.TimeoutError:
print(f'等待事件超时 ({i+1}/5)') print(f'等待事件超时 ({i+1}/5)')
# 发送停止RTSP事件 # 发送停止视频流事件
sio.emit('stop_rtsp') sio.emit('stop_video')
print('已发送stop_rtsp事件') print('已发送stop_video事件')
# 等待停止状态事件 # 等待停止状态事件
try: try: