diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..24470ad --- /dev/null +++ b/.env.example @@ -0,0 +1,15 @@ +# 环境变量配置示例 +# 复制此文件为 .env 并填写实际值 + +# 基础URL +BASE_URL=http://10.80.0.2:5045 + +# 登录凭据 +USERNAME=hao.wang@westwell-lab.com +PASSWORD=wh707297 + +# Flask配置 +FLASK_DEBUG=False +SECRET_KEY=dev-secret-key +PORT=5002 +HOST=0.0.0.0 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 1b13684..cfa864f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,9 +28,9 @@ RUN mkdir -p /var/log/multi_camera # 暴露端口 EXPOSE 5002 -# 健康检查 +# 健康检查(使用/health端点) HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ - CMD curl -f http://localhost:5002/status || exit 1 + CMD curl -f http://localhost:5002/health || exit 1 # 设置启动命令 -CMD ["python3", "complete_multi_camera_app.py"] \ No newline at end of file +CMD ["python3", "run.py"] \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..801a2d7 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,43 @@ +""" +Flask应用工厂 +""" +import logging +from flask import Flask +from .config import DEBUG, SECRET_KEY, HOST, PORT +from .health import register_health_routes +from .routes.main import register_main_routes + +def create_app(): + """创建Flask应用实例""" + app = Flask(__name__, + static_folder='../static', + template_folder='templates') + + # 配置 + app.config['DEBUG'] = DEBUG + app.config['SECRET_KEY'] = SECRET_KEY + + # 配置日志 + configure_logging(app) + + # 注册路由 + register_health_routes(app) + register_main_routes(app) + + return app + +def configure_logging(app): + """配置日志""" + if not app.debug: + # 在生产环境中,将日志输出到文件 + import logging + from logging.handlers import RotatingFileHandler + + file_handler = RotatingFileHandler('multi_camera.log', maxBytes=10240, backupCount=10) + file_handler.setFormatter(logging.Formatter( + '%(asctime)s - %(levelname)s - %(message)s' + )) + file_handler.setLevel(logging.INFO) + app.logger.addHandler(file_handler) + app.logger.setLevel(logging.INFO) + app.logger.info('多摄像头监控系统启动') \ No newline at end of file diff --git a/app/__pycache__/__init__.cpython-312.pyc b/app/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..ee89c78 Binary files /dev/null and b/app/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/__pycache__/camera_manager.cpython-312.pyc b/app/__pycache__/camera_manager.cpython-312.pyc new file mode 100644 index 0000000..cae822d Binary files /dev/null and b/app/__pycache__/camera_manager.cpython-312.pyc differ diff --git a/app/__pycache__/config.cpython-312.pyc b/app/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000..c15b2ed Binary files /dev/null and b/app/__pycache__/config.cpython-312.pyc differ diff --git a/app/__pycache__/health.cpython-312.pyc b/app/__pycache__/health.cpython-312.pyc new file mode 100644 index 0000000..db3bd95 Binary files /dev/null and b/app/__pycache__/health.cpython-312.pyc differ diff --git a/app/camera_manager.py b/app/camera_manager.py new file mode 100644 index 0000000..442025a --- /dev/null +++ b/app/camera_manager.py @@ -0,0 +1,103 @@ +""" +摄像头管理器 +处理登录、会话管理和摄像头配置 +""" +import requests +import logging +from datetime import datetime +from .config import BASE_URL, LOGIN_API, CAMERA_URL, USERNAME, PASSWORD, CAMERAS + +logger = logging.getLogger(__name__) + +class CameraManager: + def __init__(self): + self.base_url = BASE_URL + self.login_api = LOGIN_API + self.camera_url = CAMERA_URL + self.session = requests.Session() + self.token = None + self.last_login_time = None + self.is_logged_in = False + self.cameras = CAMERAS + + # 配置请求头 + self.session.headers.update({ + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + 'Accept': 'application/json, text/plain, */*', + 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', + 'Content-Type': 'application/json', + 'Connection': 'keep-alive', + }) + + # 不自动登录,按需登录 + # self.auto_login() + + def login(self): + """登录系统""" + logger.info("正在登录系统...") + + login_data = { + 'username': USERNAME, + 'password': PASSWORD, + 'email': USERNAME, + 'user': USERNAME, + 'account': USERNAME + } + + try: + response = self.session.post( + self.login_api, + json=login_data, + timeout=10 + ) + + if response.status_code == 200: + response_data = response.json() + self.token = response_data.get('token') + self.last_login_time = datetime.now() + self.is_logged_in = True + + # 更新认证头 + if self.token: + self.session.headers.update({ + 'Authorization': f'Bearer {self.token}' + }) + + logger.info("登录成功!") + return True + else: + logger.error(f"登录失败,状态码: {response.status_code}") + return False + + except Exception as e: + logger.error(f"登录请求失败: {e}") + return False + + def get_camera_url(self, camera_id, camera_number='mixed'): + """根据摄像头ID和编号生成URL""" + camera = next((c for c in self.cameras if c['id'] == camera_id), None) + if not camera: + raise ValueError(f"摄像头ID {camera_id} 不存在") + + room = camera['room'] + if camera_number == 'mixed': + return f"{self.camera_url}?room={room}&camera=mixed" + else: + return f"{self.camera_url}?room={room}&camera=camera-{camera_number}" + + def refresh_camera(self, camera_id): + """刷新指定摄像头(模拟操作)""" + logger.info(f"刷新摄像头 {camera_id}") + return True + + def get_all_cameras(self): + """返回所有摄像头配置""" + return self.cameras + + def check_connection(self): + """检查连接状态""" + try: + response = self.session.get(self.base_url, timeout=5) + return response.status_code == 200 + except: + return False \ No newline at end of file diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..7bdc6ad --- /dev/null +++ b/app/config.py @@ -0,0 +1,69 @@ +""" +配置文件 +""" +import os +from dotenv import load_dotenv + +# 加载环境变量 +load_dotenv() + +# 基础URL +BASE_URL = os.getenv("BASE_URL", "http://10.80.0.2:5045") +LOGIN_API = f"{BASE_URL}/api/user/login" +CAMERA_URL = f"{BASE_URL}/adaops/blank-layout/camera-view" + +# 认证信息 +USERNAME = os.getenv("USERNAME", "hao.wang@westwell-lab.com") +PASSWORD = os.getenv("PASSWORD", "wh707297") + +# 摄像头配置(可以从YAML/JSON加载,这里先硬编码) +CAMERAS = [ + { + 'id': 1, + '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" + } +] + +# Flask配置 +DEBUG = os.getenv("FLASK_DEBUG", "False").lower() == "true" +SECRET_KEY = os.getenv("SECRET_KEY", "dev-secret-key") +PORT = int(os.getenv("PORT", 5002)) +HOST = os.getenv("HOST", "0.0.0.0") \ No newline at end of file diff --git a/app/health.py b/app/health.py new file mode 100644 index 0000000..47261c1 --- /dev/null +++ b/app/health.py @@ -0,0 +1,63 @@ +""" +健康检查端点 +""" +import psutil +import logging +from datetime import datetime +from flask import jsonify + +logger = logging.getLogger(__name__) + +def get_memory_usage(): + """获取内存使用(MB)""" + process = psutil.Process() + memory_mb = process.memory_info().rss / 1024 / 1024 + return round(memory_mb, 2) + +def get_cpu_usage(): + """获取CPU使用率(%)""" + return psutil.cpu_percent(interval=0.1) + +def register_health_routes(app): + """注册健康检查路由到Flask应用""" + + @app.route('/status') + def status(): + """整体状态检查""" + return jsonify({ + 'status': 'ok', + 'service': 'multi-camera-monitor', + 'timestamp': datetime.now().isoformat() + }) + + @app.route('/memory') + def memory(): + """内存使用""" + return jsonify({ + 'memory_mb': get_memory_usage(), + 'unit': 'MB' + }) + + @app.route('/cpu') + def cpu(): + """CPU使用""" + return jsonify({ + 'cpu_percent': get_cpu_usage(), + 'unit': '%' + }) + + @app.route('/health') + def health(): + """综合健康检查(用于Docker)""" + try: + # 简单检查 + memory = get_memory_usage() + cpu = get_cpu_usage() + return jsonify({ + 'status': 'healthy', + 'memory_mb': memory, + 'cpu_percent': cpu + }), 200 + except Exception as e: + logger.error(f"健康检查失败: {e}") + return jsonify({'status': 'unhealthy', 'error': str(e)}), 500 \ No newline at end of file diff --git a/app/routes/__init__.py b/app/routes/__init__.py new file mode 100644 index 0000000..e2cbf50 --- /dev/null +++ b/app/routes/__init__.py @@ -0,0 +1 @@ +# 路由包 \ No newline at end of file diff --git a/app/routes/__pycache__/__init__.cpython-312.pyc b/app/routes/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..e1a4f5b Binary files /dev/null and b/app/routes/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/routes/__pycache__/main.cpython-312.pyc b/app/routes/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000..0b6bc44 Binary files /dev/null and b/app/routes/__pycache__/main.cpython-312.pyc differ diff --git a/app/routes/main.py b/app/routes/main.py new file mode 100644 index 0000000..2b80184 --- /dev/null +++ b/app/routes/main.py @@ -0,0 +1,53 @@ +""" +主路由 +""" +import logging +from datetime import datetime +from flask import render_template, jsonify, request +from app.camera_manager import CameraManager + +logger = logging.getLogger(__name__) + +# 创建摄像头管理器实例(全局) +camera_manager = CameraManager() + +def register_main_routes(app): + """注册主路由到Flask应用""" + + @app.route('/') + def index(): + """主页面 - 显示6个摄像头的网格布局""" + cameras = camera_manager.get_all_cameras() + return render_template('index.html', + cameras=cameras, + current_time=datetime.now().strftime('%Y-%m-%d %H:%M:%S')) + + @app.route('/api/cameras') + def get_cameras(): + """获取摄像头列表API""" + cameras = camera_manager.get_all_cameras() + return jsonify(cameras) + + @app.route('/api/refresh/', methods=['POST']) + def refresh_camera(camera_id): + """刷新指定摄像头""" + success = camera_manager.refresh_camera(camera_id) + return jsonify({'success': success, 'camera_id': camera_id}) + + @app.route('/api/switch', methods=['POST']) + def switch_camera(): + """切换摄像头编号""" + data = request.get_json() + camera_id = data.get('camera_id') + camera_number = data.get('camera_number', 'mixed') + try: + url = camera_manager.get_camera_url(camera_id, camera_number) + return jsonify({'success': True, 'url': url}) + except Exception as e: + return jsonify({'success': False, 'error': str(e)}), 400 + + @app.route('/api/login', methods=['POST']) + def login(): + """手动登录""" + success = camera_manager.login() + return jsonify({'success': success}) \ No newline at end of file diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..22dfcd3 --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,53 @@ + + + + + + 多摄像头实时监控 - AdaOps + + + +
+ +
+ 时间: {{ current_time }} +
+
+ +
+ {% for camera in cameras %} +
+
+
{{ camera.name }}
+
✅ 在线
+
+
+ +
+
+ + + + + + + + + + + +
+
+ {% endfor %} +
+ + + + \ No newline at end of file diff --git a/multi_camera.log b/multi_camera.log new file mode 100644 index 0000000..03affee --- /dev/null +++ b/multi_camera.log @@ -0,0 +1,2 @@ +2025-12-07 01:58:44,962 - INFO - 多摄像头监控系统启动 +2025-12-07 02:06:10,241 - INFO - 多摄像头监控系统启动 diff --git a/requirements.txt b/requirements.txt index ebc1cd5..f0c6928 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,4 @@ Flask==2.3.3 -requests==2.31.0 \ No newline at end of file +requests==2.31.0 +python-dotenv==1.0.0 +psutil==5.9.6 \ No newline at end of file diff --git a/run.py b/run.py new file mode 100644 index 0000000..c464c8b --- /dev/null +++ b/run.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +""" +多摄像头监控系统启动脚本 +""" +import os +import sys +from app import create_app + +if __name__ == '__main__': + app = create_app() + + from app.config import HOST, PORT, DEBUG + + print("=" * 50) + print("🚀 多摄像头网格布局系统启动") + print(f"📡 访问地址: http://{HOST}:{PORT}") + print(f"🎥 摄像头数量: 6个") + print(f"🔧 调试模式: {DEBUG}") + print("=" * 50) + + app.run(host=HOST, port=PORT, debug=DEBUG) \ No newline at end of file diff --git a/start.sh b/start.sh index 0556c77..0968e62 100755 --- a/start.sh +++ b/start.sh @@ -24,6 +24,18 @@ if [ $? -ne 0 ]; then pip3 install requests==2.31.0 fi +python3 -c "import dotenv" 2>/dev/null +if [ $? -ne 0 ]; then + echo "📥 安装python-dotenv..." + pip3 install python-dotenv==1.0.0 +fi + +python3 -c "import psutil" 2>/dev/null +if [ $? -ne 0 ]; then + echo "📥 安装psutil..." + pip3 install psutil==5.9.6 +fi + # 启动应用 echo "🎬 启动应用..." -python3 complete_multi_camera_app.py \ No newline at end of file +python3 run.py \ No newline at end of file diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..937c64d --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,214 @@ +/* 多摄像头监控系统样式 */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} +body { + font-family: Arial, sans-serif; + background: #1a1a1a; + color: white; + overflow-x: hidden; +} +.controls { + background: #3a3a3a; + padding: 8px 12px; + display: flex; + gap: 8px; + flex-wrap: wrap; + border-bottom: 1px solid #555; + position: sticky; + top: 0; + z-index: 1000; +} +.btn { + padding: 10px 15px; + border: none; + border-radius: 5px; + background: #4CAF50; + color: white; + cursor: pointer; + font-weight: bold; + transition: all 0.3s; +} +.btn:hover { + background: #45a049; + transform: translateY(-2px); +} +.btn-refresh { + background: #2196F3; +} +.btn-refresh:hover { + background: #1976D2; +} +.camera-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0; + padding: 0; + max-width: 100%; + height: calc(100vh - 80px); +} +.camera-item { + background: #2d2d2d; + border-radius: 0; + overflow: hidden; + box-shadow: none; + transition: all 0.3s ease; + border: 1px solid #444; + position: relative; +} +.camera-item:hover { + transform: none; + box-shadow: inset 0 0 0 3px #4CAF50; + border-color: #4CAF50; + z-index: 10; +} +.camera-header { + background: #3a3a3a; + padding: 8px 12px; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid #555; +} +.camera-title { + font-weight: bold; + color: #4CAF50; +} +.camera-status { + font-size: 12px; + padding: 3px 8px; + border-radius: 10px; + background: #4CAF50; +} +.camera-frame-container { + position: relative; + width: 100%; + height: calc(100% - 80px); + background: #000; +} +.camera-frame { + width: 100%; + height: 100%; + border: none; + background: #000; + object-fit: contain; +} +.camera-controls { + display: none; +} +.cam-btn { + padding: 5px 10px; + border: none; + border-radius: 3px; + background: #555; + color: white; + cursor: pointer; + font-size: 12px; + transition: all 0.3s; + min-width: 30px; +} +.cam-btn:hover { + background: #666; + transform: scale(1.1); +} +.cam-btn.active { + background: #4CAF50; + transform: scale(1.1); + box-shadow: 0 0 5px rgba(76, 175, 80, 0.5); +} +.cam-btn-fullscreen { + background: #FF9800; +} +.cam-btn-fullscreen:hover { + background: #F57C00; +} +.camera-controls-combined { + background: #3a3a3a; + padding: 6px; + display: flex; + gap: 3px; + justify-content: center; + align-items: center; + flex-wrap: wrap; + border-top: 1px solid #555; +} +.selector-btn { + padding: 4px 8px; + border: none; + border-radius: 2px; + background: #666; + color: white; + cursor: pointer; + font-size: 11px; + transition: all 0.2s; + min-width: 25px; +} +.selector-btn:hover { + background: #777; + transform: scale(1.05); +} +.selector-btn.active { + background: #2196F3; + transform: scale(1.1); + box-shadow: 0 0 3px rgba(33, 150, 243, 0.5); +} +.info-panel { + position: fixed; + top: 5px; + left: 5px; + background: rgba(45, 45, 45, 0.9); + padding: 5px 8px; + border-radius: 4px; + font-size: 9px; + max-width: 180px; + backdrop-filter: blur(5px); + z-index: 999; + border: 1px solid #555; + box-shadow: 0 2px 8px rgba(0,0,0,0.3); + line-height: 1.2; +} +.status-info { + display: flex; + gap: 15px; + align-items: center; + margin-left: auto; + font-size: 12px; + color: #ccc; +} +.status-item { + display: flex; + align-items: center; + gap: 5px; +} +.status-item strong { + color: #4CAF50; +} +.info-item { + margin: 5px 0; +} +.info-item strong { + color: #4CAF50; +} +@media (max-width: 1200px) { + .camera-grid { + grid-template-columns: repeat(2, 1fr); + } +} +@media (max-width: 768px) { + .camera-grid { + grid-template-columns: 1fr; + padding: 10px; + } + .controls { + flex-direction: column; + } + .info-panel { + position: relative; + top: auto; + left: auto; + max-width: 100%; + margin: 10px; + } +} \ No newline at end of file diff --git a/static/js/app.js b/static/js/app.js new file mode 100644 index 0000000..b5b7ff4 --- /dev/null +++ b/static/js/app.js @@ -0,0 +1,105 @@ +// 多摄像头监控系统前端逻辑 + +function refreshCamera(cameraId) { + const frame = document.getElementById(`frame-${cameraId}`); + const status = document.getElementById(`status-${cameraId}`); + + status.textContent = '🔄 刷新中...'; + + // 添加时间戳避免缓存 + const originalSrc = frame.src.split('?')[0]; + frame.src = originalSrc + `?t=${Date.now()}`; + + setTimeout(() => { + status.textContent = '✅ 在线'; + }, 2000); +} + +function refreshAllCameras() { + document.querySelectorAll('.camera-status').forEach(status => { + status.textContent = '🔄 刷新中...'; + }); + + for (let i = 1; i <= 6; i++) { + refreshCamera(i); + } +} + +function toggleFullscreen(cameraId) { + const frame = document.getElementById(`frame-${cameraId}`); + + if (!document.fullscreenElement) { + if (frame.requestFullscreen) { + frame.requestFullscreen(); + } else if (frame.webkitRequestFullscreen) { + frame.webkitRequestFullscreen(); + } else if (frame.msRequestFullscreen) { + frame.msRequestFullscreen(); + } + } else { + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } else if (document.msExitFullscreen) { + document.msExitFullscreen(); + } + } +} + +function switchCameraNumber(cameraId, cameraNumber) { + const frame = document.getElementById(`frame-${cameraId}`); + const controls = document.getElementById(`controls-${cameraId}`); + const buttons = controls.querySelectorAll('.selector-btn'); + + // 更新按钮状态 + buttons.forEach(btn => { + btn.classList.remove('active'); + }); + event.target.classList.add('active'); + + // 更新摄像头URL + const currentUrl = frame.src; + const baseUrl = currentUrl.split('?')[0]; + const params = new URLSearchParams(currentUrl.split('?')[1]); + const room = params.get('room'); + + // 构建新的URL + let newUrl; + if (cameraNumber === 'mixed') { + newUrl = `${baseUrl}?room=${room}&camera=mixed&t=${Date.now()}`; + } else { + newUrl = `${baseUrl}?room=${room}&camera=camera-${cameraNumber}&t=${Date.now()}`; + } + frame.src = newUrl; + + // 更新状态显示 + const status = document.getElementById(`status-${cameraId}`); + status.textContent = '🔄 切换中...'; + + setTimeout(() => { + status.textContent = '✅ 在线'; + }, 1000); +} + +function updateCurrentTime() { + const now = new Date(); + const timeString = now.toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false + }); + document.getElementById('currentTime').textContent = timeString; +} + +// 每秒更新当前时间 +setInterval(updateCurrentTime, 1000); + +// 页面加载时更新时间 +window.onload = function() { + updateCurrentTime(); +}; \ No newline at end of file