refactor: 优化配置管理和异常处理
- 添加YAML配置文件支持 - 改进camera_manager异常处理 - 添加类型提示和URL验证 - 完善依赖注入支持测试 - 新增健康检查API端点
This commit is contained in:
@@ -1,32 +0,0 @@
|
|||||||
# 忽略文件
|
|
||||||
.git
|
|
||||||
.gitignore
|
|
||||||
.env
|
|
||||||
.env.example
|
|
||||||
__pycache__
|
|
||||||
*.pyc
|
|
||||||
*.pyo
|
|
||||||
*.pyd
|
|
||||||
.Python
|
|
||||||
*.so
|
|
||||||
*.egg
|
|
||||||
*.egg-info
|
|
||||||
dist
|
|
||||||
build
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
*.sqlite
|
|
||||||
*.db
|
|
||||||
*.cache
|
|
||||||
.coverage
|
|
||||||
.pytest_cache
|
|
||||||
.mypy_cache
|
|
||||||
.vscode
|
|
||||||
.idea
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
*~
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
node_modules
|
|
||||||
static/node_modules
|
|
||||||
@@ -2,18 +2,51 @@
|
|||||||
摄像头管理器
|
摄像头管理器
|
||||||
处理摄像头配置和URL生成
|
处理摄像头配置和URL生成
|
||||||
"""
|
"""
|
||||||
import requests
|
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from requests.adapters import HTTPAdapter
|
||||||
|
from urllib3.util.retry import Retry
|
||||||
|
|
||||||
from .config import BASE_URL, CAMERA_URL, CAMERAS
|
from .config import BASE_URL, CAMERA_URL, CAMERAS
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class CameraManager:
|
class CameraManager:
|
||||||
def __init__(self):
|
"""摄像头管理器"""
|
||||||
self.base_url = BASE_URL
|
|
||||||
self.camera_url = CAMERA_URL
|
def __init__(self, cameras: Optional[List[Dict[str, Any]]] = None) -> None:
|
||||||
self.session = requests.Session()
|
"""
|
||||||
self.cameras = CAMERAS
|
初始化摄像头管理器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cameras: 摄像头配置列表,如果为None则使用全局配置
|
||||||
|
"""
|
||||||
|
self.base_url: str = BASE_URL
|
||||||
|
self.camera_url: str = CAMERA_URL
|
||||||
|
self.cameras: List[Dict[str, Any]] = cameras if cameras is not None else CAMERAS.copy()
|
||||||
|
|
||||||
|
# 初始化请求会话,配置连接池和重试策略
|
||||||
|
self.session: requests.Session = requests.Session()
|
||||||
|
|
||||||
|
# 配置重试策略
|
||||||
|
retry_strategy = Retry(
|
||||||
|
total=3,
|
||||||
|
backoff_factor=1,
|
||||||
|
status_forcelist=[429, 500, 502, 503, 504],
|
||||||
|
allowed_methods=["HEAD", "GET", "OPTIONS"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# 配置适配器
|
||||||
|
adapter = HTTPAdapter(
|
||||||
|
max_retries=retry_strategy,
|
||||||
|
pool_connections=10,
|
||||||
|
pool_maxsize=10
|
||||||
|
)
|
||||||
|
self.session.mount("http://", adapter)
|
||||||
|
self.session.mount("https://", adapter)
|
||||||
|
|
||||||
# 配置请求头
|
# 配置请求头
|
||||||
self.session.headers.update({
|
self.session.headers.update({
|
||||||
@@ -24,8 +57,20 @@ class CameraManager:
|
|||||||
'Connection': 'keep-alive',
|
'Connection': 'keep-alive',
|
||||||
})
|
})
|
||||||
|
|
||||||
def get_camera_url(self, camera_id, camera_number='mixed'):
|
def get_camera_url(self, camera_id: int, camera_number: str = 'mixed') -> str:
|
||||||
"""根据摄像头ID和编号生成URL"""
|
"""
|
||||||
|
根据摄像头ID和编号生成URL
|
||||||
|
|
||||||
|
Args:
|
||||||
|
camera_id: 摄像头ID
|
||||||
|
camera_number: 摄像头编号,默认为'mixed'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
摄像头URL
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: 摄像头ID不存在
|
||||||
|
"""
|
||||||
camera = next((c for c in self.cameras if c['id'] == camera_id), None)
|
camera = next((c for c in self.cameras if c['id'] == camera_id), None)
|
||||||
if not camera:
|
if not camera:
|
||||||
raise ValueError(f"摄像头ID {camera_id} 不存在")
|
raise ValueError(f"摄像头ID {camera_id} 不存在")
|
||||||
@@ -36,19 +81,73 @@ class CameraManager:
|
|||||||
else:
|
else:
|
||||||
return f"{self.camera_url}?room={room}&camera=camera-{camera_number}"
|
return f"{self.camera_url}?room={room}&camera=camera-{camera_number}"
|
||||||
|
|
||||||
def refresh_camera(self, camera_id):
|
def refresh_camera(self, camera_id: int) -> bool:
|
||||||
"""刷新指定摄像头(模拟操作)"""
|
"""
|
||||||
|
刷新指定摄像头(模拟操作)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
camera_id: 摄像头ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否成功
|
||||||
|
"""
|
||||||
logger.info(f"刷新摄像头 {camera_id}")
|
logger.info(f"刷新摄像头 {camera_id}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_all_cameras(self):
|
def get_all_cameras(self) -> List[Dict[str, Any]]:
|
||||||
"""返回所有摄像头配置"""
|
"""
|
||||||
|
返回所有摄像头配置
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
摄像头配置列表
|
||||||
|
"""
|
||||||
return self.cameras
|
return self.cameras
|
||||||
|
|
||||||
def check_connection(self):
|
def check_connection(self, timeout: int = 5) -> bool:
|
||||||
"""检查连接状态"""
|
"""
|
||||||
|
检查连接状态
|
||||||
|
|
||||||
|
Args:
|
||||||
|
timeout: 超时时间(秒)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
连接是否正常
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
response = self.session.get(self.base_url, timeout=5)
|
response = self.session.get(
|
||||||
|
self.base_url,
|
||||||
|
timeout=timeout,
|
||||||
|
allow_redirects=True
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
return response.status_code == 200
|
return response.status_code == 200
|
||||||
except:
|
except requests.exceptions.Timeout as e:
|
||||||
|
logger.warning(f"连接超时: {e}")
|
||||||
return False
|
return False
|
||||||
|
except requests.exceptions.ConnectionError as e:
|
||||||
|
logger.warning(f"连接错误: {e}")
|
||||||
|
return False
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
logger.warning(f"HTTP错误: {e}")
|
||||||
|
return False
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logger.error(f"请求异常: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_camera_by_id(self, camera_id: int) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
根据ID获取摄像头配置
|
||||||
|
|
||||||
|
Args:
|
||||||
|
camera_id: 摄像头ID
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
摄像头配置,如果不存在返回None
|
||||||
|
"""
|
||||||
|
return next((c for c in self.cameras if c['id'] == camera_id), None)
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
"""
|
||||||
|
关闭会话
|
||||||
|
"""
|
||||||
|
self.session.close()
|
||||||
|
|||||||
212
app/config.py
212
app/config.py
@@ -1,76 +1,170 @@
|
|||||||
"""
|
"""
|
||||||
配置文件
|
配置文件
|
||||||
|
支持YAML配置和.env环境变量
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
import yaml
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
# 加载环境变量
|
# 加载环境变量
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
# 基础URL(必须设置)
|
# 配置文件路径
|
||||||
|
CONFIG_FILE = os.getenv("CONFIG_FILE", "config.yaml")
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_url(url: str, field_name: str) -> None:
|
||||||
|
"""验证URL格式"""
|
||||||
|
if not url:
|
||||||
|
raise ValueError(f"环境变量 {field_name} 未设置")
|
||||||
|
|
||||||
|
parsed = urlparse(url)
|
||||||
|
if not all([parsed.scheme, parsed.netloc]):
|
||||||
|
raise ValueError(f"{field_name} 格式不正确: {url}")
|
||||||
|
|
||||||
|
|
||||||
|
def _load_yaml_config() -> Optional[Dict[str, Any]]:
|
||||||
|
"""加载YAML配置文件"""
|
||||||
|
if os.path.exists(CONFIG_FILE):
|
||||||
|
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
|
||||||
|
return yaml.safe_load(f)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_config_value(
|
||||||
|
yaml_config: Optional[Dict[str, Any]],
|
||||||
|
env_key: str,
|
||||||
|
yaml_path: List[str],
|
||||||
|
default: Any = None,
|
||||||
|
validator: Optional[callable] = None
|
||||||
|
) -> Any:
|
||||||
|
"""获取配置值,优先级:环境变量 > YAML配置 > 默认值"""
|
||||||
|
# 1. 优先从环境变量获取
|
||||||
|
env_value = os.getenv(env_key)
|
||||||
|
if env_value is not None:
|
||||||
|
return env_value
|
||||||
|
|
||||||
|
# 2. 其次从YAML配置获取
|
||||||
|
if yaml_config:
|
||||||
|
value = yaml_config
|
||||||
|
for key in yaml_path:
|
||||||
|
if isinstance(value, dict) and key in value:
|
||||||
|
value = value[key]
|
||||||
|
else:
|
||||||
|
value = None
|
||||||
|
break
|
||||||
|
|
||||||
|
if value is not None:
|
||||||
|
if validator:
|
||||||
|
validator(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
# 3. 使用默认值
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def _get_int_value(yaml_config: Optional[Dict[str, Any]], env_key: str, yaml_path: List[str], default: int) -> int:
|
||||||
|
"""获取整数配置值"""
|
||||||
|
value = _get_config_value(yaml_config, env_key, yaml_path, str(default))
|
||||||
|
try:
|
||||||
|
return int(value)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def _get_bool_value(yaml_config: Optional[Dict[str, Any]], env_key: str, yaml_path: List[str], default: bool) -> bool:
|
||||||
|
"""获取布尔配置值"""
|
||||||
|
value = _get_config_value(yaml_config, env_key, yaml_path, str(default).lower())
|
||||||
|
return value.lower() == "true"
|
||||||
|
|
||||||
|
|
||||||
|
# 加载YAML配置
|
||||||
|
yaml_config = _load_yaml_config()
|
||||||
|
|
||||||
|
# 验证并获取BASE_URL
|
||||||
BASE_URL = os.getenv("BASE_URL")
|
BASE_URL = os.getenv("BASE_URL")
|
||||||
if BASE_URL is None:
|
if BASE_URL is None and yaml_config:
|
||||||
raise ValueError("环境变量 BASE_URL 未设置")
|
BASE_URL = _get_config_value(yaml_config, "BASE_URL", ["camera", "base_url"])
|
||||||
|
_validate_url(BASE_URL, "BASE_URL")
|
||||||
|
|
||||||
CAMERA_URL = f"{BASE_URL}/adaops/blank-layout/camera-view"
|
CAMERA_URL = f"{BASE_URL}/adaops/blank-layout/camera-view"
|
||||||
|
|
||||||
# 摄像头配置(可以从YAML/JSON加载,这里先硬编码)
|
# 摄像头配置
|
||||||
CAMERAS = [
|
def _load_cameras() -> List[Dict[str, Any]]:
|
||||||
{
|
"""加载摄像头配置"""
|
||||||
'id': 1,
|
if yaml_config and "cameras" in yaml_config:
|
||||||
'room': 'cnfzhjyg-igv-251',
|
cameras = yaml_config["cameras"]
|
||||||
'camera': 'mixed',
|
# 如果cameras中没有url字段,自动生成
|
||||||
'name': '1号车',
|
for camera in cameras:
|
||||||
'url': f"{CAMERA_URL}?room=cnfzhjyg-igv-251&camera=mixed"
|
if 'url' not in camera:
|
||||||
},
|
room = camera.get('room', '')
|
||||||
{
|
cam = camera.get('camera', 'mixed')
|
||||||
'id': 2,
|
camera['url'] = f"{CAMERA_URL}?room={room}&camera={cam}"
|
||||||
'room': 'cnfzhjyg-igv-2',
|
return cameras
|
||||||
'camera': 'mixed',
|
|
||||||
'name': '2号车',
|
|
||||||
'url': f"{CAMERA_URL}?room=cnfzhjyg-igv-2&camera=mixed"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': 3,
|
|
||||||
'room': 'cnfzhjyg-igv-3',
|
|
||||||
'camera': 'mixed',
|
|
||||||
'name': '3号车',
|
|
||||||
'url': f"{CAMERA_URL}?room=cnfzhjyg-igv-3&camera=mixed"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': 4,
|
|
||||||
'room': 'cnfzhjyg-igv-5',
|
|
||||||
'camera': 'mixed',
|
|
||||||
'name': '5号车',
|
|
||||||
'url': f"{CAMERA_URL}?room=cnfzhjyg-igv-5&camera=mixed"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': 5,
|
|
||||||
'room': 'cnfzhjyg-igv-6',
|
|
||||||
'camera': 'mixed',
|
|
||||||
'name': '6号车',
|
|
||||||
'url': f"{CAMERA_URL}?room=cnfzhjyg-igv-6&camera=mixed"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': 6,
|
|
||||||
'room': 'cnfzhjyg-igv-7',
|
|
||||||
'camera': 'mixed',
|
|
||||||
'name': '7号车',
|
|
||||||
'url': f"{CAMERA_URL}?room=cnfzhjyg-igv-7&camera=mixed"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
# Flask配置(必须设置)
|
# 硬编码后备配置(不推荐使用)
|
||||||
DEBUG_STR = os.getenv("FLASK_DEBUG")
|
return [
|
||||||
if DEBUG_STR is None:
|
{
|
||||||
raise ValueError("环境变量 FLASK_DEBUG 未设置")
|
'id': 1,
|
||||||
DEBUG = DEBUG_STR.lower() == "true"
|
'room': 'cnfzhjyg-igv-251',
|
||||||
|
'camera': 'mixed',
|
||||||
|
'name': '1号车',
|
||||||
|
'url': f"{CAMERA_URL}?room=cnfzhjyg-igv-251&camera=mixed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 2,
|
||||||
|
'room': 'cnfzhjyg-igv-2',
|
||||||
|
'camera': 'mixed',
|
||||||
|
'name': '2号车',
|
||||||
|
'url': f"{CAMERA_URL}?room=cnfzhjyg-igv-2&camera=mixed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 3,
|
||||||
|
'room': 'cnfzhjyg-igv-3',
|
||||||
|
'camera': 'mixed',
|
||||||
|
'name': '3号车',
|
||||||
|
'url': f"{CAMERA_URL}?room=cnfzhjyg-igv-3&camera=mixed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 4,
|
||||||
|
'room': 'cnfzhjyg-igv-5',
|
||||||
|
'camera': 'mixed',
|
||||||
|
'name': '5号车',
|
||||||
|
'url': f"{CAMERA_URL}?room=cnfzhjyg-igv-5&camera=mixed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 5,
|
||||||
|
'room': 'cnfzhjyg-igv-6',
|
||||||
|
'camera': 'mixed',
|
||||||
|
'name': '6号车',
|
||||||
|
'url': f"{CAMERA_URL}?room=cnfzhjyg-igv-6&camera=mixed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 6,
|
||||||
|
'room': 'cnfzhjyg-igv-7',
|
||||||
|
'camera': 'mixed',
|
||||||
|
'name': '7号车',
|
||||||
|
'url': f"{CAMERA_URL}?room=cnfzhjyg-igv-7&camera=mixed"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
PORT_STR = os.getenv("PORT")
|
CAMERAS: List[Dict[str, Any]] = _load_cameras()
|
||||||
if PORT_STR is None:
|
|
||||||
raise ValueError("环境变量 PORT 未设置")
|
|
||||||
PORT = int(PORT_STR)
|
|
||||||
|
|
||||||
HOST = os.getenv("HOST")
|
# Flask配置
|
||||||
if HOST is None:
|
DEBUG: bool = _get_bool_value(yaml_config, "FLASK_DEBUG", ["app", "debug"], False)
|
||||||
|
PORT: int = _get_int_value(yaml_config, "PORT", ["app", "port"], 5000)
|
||||||
|
HOST: str = _get_config_value(yaml_config, "HOST", ["app", "host"], "0.0.0.0")
|
||||||
|
|
||||||
|
# 验证HOST
|
||||||
|
if not HOST:
|
||||||
raise ValueError("环境变量 HOST 未设置")
|
raise ValueError("环境变量 HOST 未设置")
|
||||||
|
|
||||||
|
# 日志配置
|
||||||
|
LOG_LEVEL: str = _get_config_value(yaml_config, "LOG_LEVEL", ["logging", "level"], "INFO")
|
||||||
|
LOG_FILE: str = _get_config_value(yaml_config, "LOG_FILE", ["logging", "file"], "logs/multi_camera.log")
|
||||||
|
LOG_MAX_BYTES: int = _get_int_value(yaml_config, "LOG_MAX_BYTES", ["logging", "max_bytes"], 10485760)
|
||||||
|
LOG_BACKUP_COUNT: int = _get_int_value(yaml_config, "LOG_BACKUP_COUNT", ["logging", "backup_count"], 10)
|
||||||
|
|||||||
@@ -3,46 +3,91 @@
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from flask import render_template, jsonify, request
|
from typing import Dict, Any, List
|
||||||
|
|
||||||
|
from flask import Flask, render_template, jsonify, request
|
||||||
|
|
||||||
from app.camera_manager import CameraManager
|
from app.camera_manager import CameraManager
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# 创建摄像头管理器实例(全局)
|
# 创建摄像头管理器实例(可通过依赖注入覆盖)
|
||||||
camera_manager = CameraManager()
|
_camera_manager: CameraManager = None
|
||||||
|
|
||||||
def register_main_routes(app):
|
|
||||||
|
def get_camera_manager() -> CameraManager:
|
||||||
|
"""获取摄像头管理器实例(支持依赖注入)"""
|
||||||
|
global _camera_manager
|
||||||
|
if _camera_manager is None:
|
||||||
|
_camera_manager = CameraManager()
|
||||||
|
return _camera_manager
|
||||||
|
|
||||||
|
|
||||||
|
def set_camera_manager(manager: CameraManager) -> None:
|
||||||
|
"""设置摄像头管理器实例(用于测试)"""
|
||||||
|
global _camera_manager
|
||||||
|
_camera_manager = manager
|
||||||
|
|
||||||
|
|
||||||
|
def register_main_routes(app: Flask) -> None:
|
||||||
"""注册主路由到Flask应用"""
|
"""注册主路由到Flask应用"""
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def index():
|
def index() -> str:
|
||||||
"""主页面 - 显示6个摄像头的网格布局"""
|
"""主页面 - 显示6个摄像头的网格布局"""
|
||||||
|
camera_manager = get_camera_manager()
|
||||||
cameras = camera_manager.get_all_cameras()
|
cameras = camera_manager.get_all_cameras()
|
||||||
return render_template('index.html',
|
return render_template(
|
||||||
cameras=cameras,
|
'index.html',
|
||||||
current_time=datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
|
cameras=cameras,
|
||||||
|
current_time=datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
)
|
||||||
|
|
||||||
@app.route('/api/cameras')
|
@app.route('/api/cameras')
|
||||||
def get_cameras():
|
def get_cameras() -> Dict[str, List[Dict[str, Any]]]:
|
||||||
"""获取摄像头列表API"""
|
"""获取摄像头列表API"""
|
||||||
|
camera_manager = get_camera_manager()
|
||||||
cameras = camera_manager.get_all_cameras()
|
cameras = camera_manager.get_all_cameras()
|
||||||
return jsonify(cameras)
|
return jsonify(cameras)
|
||||||
|
|
||||||
@app.route('/api/refresh/<int:camera_id>', methods=['POST'])
|
@app.route('/api/refresh/<int:camera_id>', methods=['POST'])
|
||||||
def refresh_camera(camera_id):
|
def refresh_camera(camera_id: int) -> Dict[str, Any]:
|
||||||
"""刷新指定摄像头"""
|
"""刷新指定摄像头"""
|
||||||
|
camera_manager = get_camera_manager()
|
||||||
success = camera_manager.refresh_camera(camera_id)
|
success = camera_manager.refresh_camera(camera_id)
|
||||||
return jsonify({'success': success, 'camera_id': camera_id})
|
return jsonify({'success': success, 'camera_id': camera_id})
|
||||||
|
|
||||||
@app.route('/api/switch', methods=['POST'])
|
@app.route('/api/switch', methods=['POST'])
|
||||||
def switch_camera():
|
def switch_camera() -> Dict[str, Any]:
|
||||||
"""切换摄像头编号"""
|
"""切换摄像头编号"""
|
||||||
|
camera_manager = get_camera_manager()
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
return jsonify({'success': False, 'error': '无效的请求数据'}), 400
|
||||||
|
|
||||||
camera_id = data.get('camera_id')
|
camera_id = data.get('camera_id')
|
||||||
camera_number = data.get('camera_number', 'mixed')
|
camera_number = data.get('camera_number', 'mixed')
|
||||||
|
|
||||||
|
if camera_id is None:
|
||||||
|
return jsonify({'success': False, 'error': '缺少camera_id参数'}), 400
|
||||||
|
|
||||||
try:
|
try:
|
||||||
url = camera_manager.get_camera_url(camera_id, camera_number)
|
url = camera_manager.get_camera_url(camera_id, camera_number)
|
||||||
return jsonify({'success': True, 'url': url})
|
return jsonify({'success': True, 'url': url})
|
||||||
except Exception as e:
|
except ValueError as e:
|
||||||
return jsonify({'success': False, 'error': str(e)}), 400
|
return jsonify({'success': False, 'error': str(e)}), 400
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"切换摄像头失败: {e}")
|
||||||
|
return jsonify({'success': False, 'error': '内部服务器错误'}), 500
|
||||||
|
|
||||||
|
@app.route('/api/health')
|
||||||
|
def health_check() -> Dict[str, Any]:
|
||||||
|
"""健康检查API"""
|
||||||
|
camera_manager = get_camera_manager()
|
||||||
|
is_connected = camera_manager.check_connection()
|
||||||
|
return jsonify({
|
||||||
|
'status': 'healthy' if is_connected else 'degraded',
|
||||||
|
'connection': is_connected,
|
||||||
|
'timestamp': datetime.now().isoformat()
|
||||||
|
})
|
||||||
|
|||||||
@@ -33,7 +33,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="camera-controls-combined" id="controls-{{ camera.id }}">
|
<div class="camera-controls-combined" id="controls-{{ camera.id }}">
|
||||||
<button class="cam-btn" onclick="refreshCamera({{ camera.id }})">刷新</button>
|
<button class="cam-btn" onclick="refreshCamera({{ camera.id }})">刷新</button>
|
||||||
<button class="cam-btn cam-btn-fullscreen" onclick="toggleFullscreen({{ camera.id }})">全屏</button>
|
<button class="cam-btn cam-btn-fullscreen" id="fullscreen-btn-{{ camera.id }}" onclick="toggleFullscreen({{ camera.id }})">全屏</button>
|
||||||
|
<button class="cam-btn cam-btn-exit-fullscreen" id="exit-fullscreen-btn-{{ camera.id }}" onclick="toggleFullscreen({{ camera.id }})" style="display: none;">退出全屏</button>
|
||||||
<button class="selector-btn active" onclick="switchCameraNumber(event, {{ camera.id }}, 'mixed')">混合</button>
|
<button class="selector-btn active" onclick="switchCameraNumber(event, {{ camera.id }}, 'mixed')">混合</button>
|
||||||
<button class="selector-btn" onclick="switchCameraNumber(event, {{ camera.id }}, 0)">0</button>
|
<button class="selector-btn" onclick="switchCameraNumber(event, {{ camera.id }}, 0)">0</button>
|
||||||
<button class="selector-btn" onclick="switchCameraNumber(event, {{ camera.id }}, 1)">1</button>
|
<button class="selector-btn" onclick="switchCameraNumber(event, {{ camera.id }}, 1)">1</button>
|
||||||
|
|||||||
47
config.yaml
Normal file
47
config.yaml
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# 多摄像头监控系统配置文件
|
||||||
|
|
||||||
|
# 基础配置
|
||||||
|
app:
|
||||||
|
host: "0.0.0.0"
|
||||||
|
port: 5000
|
||||||
|
debug: false
|
||||||
|
|
||||||
|
# 摄像头服务配置
|
||||||
|
camera:
|
||||||
|
base_url: "http://localhost:8080"
|
||||||
|
camera_path: "/adaops/blank-layout/camera-view"
|
||||||
|
|
||||||
|
# 摄像头列表配置
|
||||||
|
cameras:
|
||||||
|
- id: 1
|
||||||
|
room: "cnfzhjyg-igv-251"
|
||||||
|
camera: "mixed"
|
||||||
|
name: "1号车"
|
||||||
|
# url会自动生成: {camera_url}?room={room}&camera={camera}
|
||||||
|
- id: 2
|
||||||
|
room: "cnfzhjyg-igv-2"
|
||||||
|
camera: "mixed"
|
||||||
|
name: "2号车"
|
||||||
|
- id: 3
|
||||||
|
room: "cnfzhjyg-igv-3"
|
||||||
|
camera: "mixed"
|
||||||
|
name: "3号车"
|
||||||
|
- id: 4
|
||||||
|
room: "cnfzhjyg-igv-5"
|
||||||
|
camera: "mixed"
|
||||||
|
name: "5号车"
|
||||||
|
- id: 5
|
||||||
|
room: "cnfzhjyg-igv-6"
|
||||||
|
camera: "mixed"
|
||||||
|
name: "6号车"
|
||||||
|
- id: 6
|
||||||
|
room: "cnfzhjyg-igv-7"
|
||||||
|
camera: "mixed"
|
||||||
|
name: "7号车"
|
||||||
|
|
||||||
|
# 日志配置
|
||||||
|
logging:
|
||||||
|
level: "INFO"
|
||||||
|
file: "logs/multi_camera.log"
|
||||||
|
max_bytes: 10485760
|
||||||
|
backup_count: 10
|
||||||
@@ -1,3 +1,17 @@
|
|||||||
|
# Flask框架
|
||||||
Flask==2.3.3
|
Flask==2.3.3
|
||||||
|
|
||||||
|
# HTTP请求
|
||||||
requests==2.31.0
|
requests==2.31.0
|
||||||
|
|
||||||
|
# 环境变量
|
||||||
python-dotenv==1.0.0
|
python-dotenv==1.0.0
|
||||||
|
|
||||||
|
# YAML配置支持
|
||||||
|
PyYAML>=6.0
|
||||||
|
|
||||||
|
# 生产环境WSGI服务器
|
||||||
|
gunicorn>=21.0.0
|
||||||
|
|
||||||
|
# 异步支持(可选)
|
||||||
|
# gevent>=23.0.0
|
||||||
|
|||||||
@@ -64,6 +64,67 @@ body {
|
|||||||
border-color: #4CAF50;
|
border-color: #4CAF50;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
/* 全屏模式下的样式 */
|
||||||
|
.camera-item:fullscreen {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #000;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.camera-item:fullscreen .camera-frame-container {
|
||||||
|
flex: 1;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
.camera-item:fullscreen .camera-controls-combined {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 25px;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
.camera-item:fullscreen .camera-header {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* WebKit浏览器全屏支持 */
|
||||||
|
.camera-item:-webkit-full-screen {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #000;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.camera-item:-webkit-full-screen .camera-frame-container {
|
||||||
|
flex: 1;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
.camera-item:-webkit-full-screen .camera-controls-combined {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 25px;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
.camera-item:-webkit-full-screen .camera-header {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
.camera-header {
|
.camera-header {
|
||||||
background: #3a3a3a;
|
background: #3a3a3a;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
@@ -124,6 +185,12 @@ body {
|
|||||||
.cam-btn-fullscreen:hover {
|
.cam-btn-fullscreen:hover {
|
||||||
background: #F57C00;
|
background: #F57C00;
|
||||||
}
|
}
|
||||||
|
.cam-btn-exit-fullscreen {
|
||||||
|
background: #f44336;
|
||||||
|
}
|
||||||
|
.cam-btn-exit-fullscreen:hover {
|
||||||
|
background: #d32f2f;
|
||||||
|
}
|
||||||
.camera-controls-combined {
|
.camera-controls-combined {
|
||||||
background: #3a3a3a;
|
background: #3a3a3a;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
|
|||||||
@@ -26,15 +26,16 @@ function refreshAllCameras() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleFullscreen(cameraId) {
|
function toggleFullscreen(cameraId) {
|
||||||
const frame = document.getElementById(`frame-${cameraId}`);
|
const cameraItem = document.getElementById(`camera-${cameraId}`);
|
||||||
|
|
||||||
if (!document.fullscreenElement) {
|
if (!document.fullscreenElement) {
|
||||||
if (frame.requestFullscreen) {
|
// 全屏相机容器而不是iframe
|
||||||
frame.requestFullscreen();
|
if (cameraItem.requestFullscreen) {
|
||||||
} else if (frame.webkitRequestFullscreen) {
|
cameraItem.requestFullscreen();
|
||||||
frame.webkitRequestFullscreen();
|
} else if (cameraItem.webkitRequestFullscreen) {
|
||||||
} else if (frame.msRequestFullscreen) {
|
cameraItem.webkitRequestFullscreen();
|
||||||
frame.msRequestFullscreen();
|
} else if (cameraItem.msRequestFullscreen) {
|
||||||
|
cameraItem.msRequestFullscreen();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (document.exitFullscreen) {
|
if (document.exitFullscreen) {
|
||||||
@@ -47,6 +48,41 @@ function toggleFullscreen(cameraId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 监听全屏变化事件
|
||||||
|
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
||||||
|
document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
|
||||||
|
document.addEventListener('msfullscreenchange', handleFullscreenChange);
|
||||||
|
|
||||||
|
function handleFullscreenChange() {
|
||||||
|
// 获取所有相机
|
||||||
|
for (let i = 1; i <= 6; i++) {
|
||||||
|
const fullscreenBtn = document.getElementById(`fullscreen-btn-${i}`);
|
||||||
|
const exitFullscreenBtn = document.getElementById(`exit-fullscreen-btn-${i}`);
|
||||||
|
|
||||||
|
if (fullscreenBtn && exitFullscreenBtn) {
|
||||||
|
if (document.fullscreenElement) {
|
||||||
|
// 进入全屏时,隐藏全屏按钮,显示退出按钮
|
||||||
|
fullscreenBtn.style.display = 'none';
|
||||||
|
exitFullscreenBtn.style.display = 'inline-block';
|
||||||
|
} else {
|
||||||
|
// 退出全屏时,显示全屏按钮,隐藏退出按钮
|
||||||
|
fullscreenBtn.style.display = 'inline-block';
|
||||||
|
exitFullscreenBtn.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitFullscreen() {
|
||||||
|
if (document.exitFullscreen) {
|
||||||
|
document.exitFullscreen();
|
||||||
|
} else if (document.webkitExitFullscreen) {
|
||||||
|
document.webkitExitFullscreen();
|
||||||
|
} else if (document.msExitFullscreen) {
|
||||||
|
document.msExitFullscreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function switchCameraNumber(event, cameraId, cameraNumber) {
|
function switchCameraNumber(event, cameraId, cameraNumber) {
|
||||||
const frame = document.getElementById(`frame-${cameraId}`);
|
const frame = document.getElementById(`frame-${cameraId}`);
|
||||||
const controls = document.getElementById(`controls-${cameraId}`);
|
const controls = document.getElementById(`controls-${cameraId}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user