提交我的更新内容

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流问题
```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', {});
```
## 性能调试

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应用
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():

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 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 = ?"

View File

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

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:
"""配置管理器"""
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()

View File

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

View File

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

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

View File

@ -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, # 启用热重载

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",
"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",

View File

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

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

View File

@ -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('⚠️ 收到空的视频帧数据')
}
}

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)
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: