364 lines
15 KiB
Python
364 lines
15 KiB
Python
import cv2
|
||
import json
|
||
import logging
|
||
import asyncio
|
||
import os
|
||
import signal
|
||
import subprocess
|
||
import platform
|
||
from fastapi import WebSocket, WebSocketDisconnect
|
||
from typing import Dict, Optional
|
||
import numpy as np
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
class CameraService:
|
||
def __init__(self, model_service):
|
||
self.model_service = model_service
|
||
self.active_connections: Dict[str, WebSocket] = {}
|
||
self.camera_captures: Dict[str, cv2.VideoCapture] = {}
|
||
self.running = False
|
||
self.camera_configs: Dict[str, Dict] = {}
|
||
self._locks: Dict[str, asyncio.Lock] = {}
|
||
self._stop_events: Dict[str, asyncio.Event] = {}
|
||
|
||
@staticmethod
|
||
def force_release_cameras():
|
||
"""强制释放被占用的摄像头资源
|
||
|
||
在以下场景调用:
|
||
1. 服务启动前 - 清理之前异常退出残留的占用
|
||
2. 服务关闭时 - 确保资源被释放
|
||
3. 信号处理时 - 异常退出前的清理
|
||
"""
|
||
logger.info("强制释放摄像头资源...")
|
||
|
||
# 1. 尝试释放当前进程中的摄像头
|
||
try:
|
||
# 在macOS上,摄像头设备通常是 /dev/video* 或 AVFoundation 设备
|
||
# 尝试打开并立即释放来清理状态
|
||
for i in range(10): # 检查前10个可能的摄像头索引
|
||
try:
|
||
cap = cv2.VideoCapture(i)
|
||
if cap.isOpened():
|
||
cap.release()
|
||
logger.info(f"已释放摄像头索引 {i}")
|
||
except Exception:
|
||
pass
|
||
except Exception as e:
|
||
logger.error(f"释放当前进程摄像头失败: {e}")
|
||
|
||
# 2. 终止占用摄像头的其他Python进程
|
||
current_pid = os.getpid()
|
||
system = platform.system()
|
||
|
||
try:
|
||
if system == "Darwin": # macOS
|
||
# 使用 lsof 查找占用摄像头的进程
|
||
result = subprocess.run(
|
||
['lsof', '-c', 'python'],
|
||
capture_output=True,
|
||
text=True,
|
||
timeout=5
|
||
)
|
||
if result.returncode == 0:
|
||
pids_to_kill = set()
|
||
for line in result.stdout.split('\n'):
|
||
# 查找包含摄像头设备 (/dev/video* 或 V4L 相关) 的行
|
||
if any(x in line for x in ['/dev/video', 'Camera', 'V4L']):
|
||
parts = line.split()
|
||
if len(parts) >= 2:
|
||
try:
|
||
pid = int(parts[1])
|
||
if pid != current_pid:
|
||
pids_to_kill.add(pid)
|
||
except (ValueError, IndexError):
|
||
pass
|
||
|
||
# 终止找到的进程
|
||
for pid in pids_to_kill:
|
||
try:
|
||
logger.info(f"终止占用摄像头的进程: {pid}")
|
||
os.kill(pid, signal.SIGTERM)
|
||
# 给进程一点时间优雅退出
|
||
import time
|
||
time.sleep(0.5)
|
||
# 检查是否还在运行,如果是则强制终止
|
||
try:
|
||
os.kill(pid, 0) # 检查进程是否存在
|
||
logger.warning(f"进程 {pid} 未响应 SIGTERM,使用 SIGKILL")
|
||
os.kill(pid, signal.SIGKILL)
|
||
except ProcessLookupError:
|
||
pass # 进程已退出
|
||
except ProcessLookupError:
|
||
pass # 进程已不存在
|
||
except Exception as e:
|
||
logger.error(f"终止进程 {pid} 失败: {e}")
|
||
|
||
elif system == "Linux":
|
||
# Linux 系统使用 fuser 或 lsof
|
||
for device in ['/dev/video0', '/dev/video1', '/dev/video2']:
|
||
try:
|
||
result = subprocess.run(
|
||
['fuser', '-k', device],
|
||
capture_output=True,
|
||
text=True,
|
||
timeout=5
|
||
)
|
||
if result.returncode == 0:
|
||
logger.info(f"已释放设备 {device}")
|
||
except Exception:
|
||
pass
|
||
|
||
except Exception as e:
|
||
logger.error(f"终止占用进程失败: {e}")
|
||
|
||
# 3. 给系统一点时间完成资源释放
|
||
import time
|
||
time.sleep(0.5)
|
||
logger.info("摄像头资源释放完成")
|
||
|
||
async def handle_connection(self, websocket: WebSocket):
|
||
await websocket.accept()
|
||
connection_id = id(websocket)
|
||
self.active_connections[connection_id] = websocket
|
||
|
||
logger.info(f"新连接: {connection_id}")
|
||
|
||
try:
|
||
while True:
|
||
try:
|
||
data = await websocket.receive_text()
|
||
message = json.loads(data)
|
||
|
||
if message.get('action') == 'start':
|
||
await self.start_camera(connection_id, websocket, message.get('config'))
|
||
elif message.get('action') == 'stop':
|
||
await self.stop_camera(connection_id)
|
||
elif message.get('action') == 'update_config':
|
||
await self.update_config(connection_id, message.get('config', {}))
|
||
except WebSocketDisconnect:
|
||
logger.info(f"WebSocket连接断开: {connection_id}")
|
||
break
|
||
|
||
except WebSocketDisconnect:
|
||
logger.info(f"连接断开: {connection_id}")
|
||
except Exception as e:
|
||
logger.error(f"连接错误: {connection_id}, {e}")
|
||
finally:
|
||
await self.stop_camera(connection_id)
|
||
if connection_id in self.active_connections:
|
||
del self.active_connections[connection_id]
|
||
logger.info(f"连接清理完成: {connection_id}")
|
||
|
||
async def start_camera(self, connection_id: str, websocket: WebSocket, config: Dict = None):
|
||
try:
|
||
camera_source = 0 # 默认本地摄像头
|
||
|
||
if not config:
|
||
config = {
|
||
'model_id': 'fire_detection',
|
||
'confidence': 0.5,
|
||
'iou': 0.45,
|
||
'camera_source': 0 # 默认本地摄像头
|
||
}
|
||
|
||
# 支持多种摄像头源
|
||
if 'camera_source' in config:
|
||
camera_source = config['camera_source']
|
||
|
||
# 如果该连接已有摄像头在运行,先停止它
|
||
if connection_id in self.camera_captures:
|
||
await self.stop_camera(connection_id)
|
||
|
||
# 初始化锁和停止事件
|
||
self._locks[connection_id] = asyncio.Lock()
|
||
self._stop_events[connection_id] = asyncio.Event()
|
||
|
||
# 尝试打开摄像头
|
||
cap = cv2.VideoCapture(camera_source)
|
||
if not cap.isOpened():
|
||
await websocket.send_json({
|
||
'type': 'error',
|
||
'message': f'无法打开摄像头源: {camera_source}'
|
||
})
|
||
return
|
||
|
||
# 只对本地摄像头设置分辨率
|
||
if isinstance(camera_source, int):
|
||
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
|
||
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
|
||
|
||
self.camera_captures[connection_id] = cap
|
||
self.camera_configs[connection_id] = config
|
||
|
||
await websocket.send_json({
|
||
'type': 'camera_started',
|
||
'message': f'摄像头已启动: {camera_source}'
|
||
})
|
||
|
||
await self.process_frames(connection_id, websocket, cap)
|
||
|
||
except Exception as e:
|
||
logger.error(f"启动摄像头失败: {e}")
|
||
await websocket.send_json({
|
||
'type': 'error',
|
||
'message': f'启动摄像头失败: {str(e)}'
|
||
})
|
||
|
||
async def process_frames(self, connection_id: str, websocket: WebSocket, cap: cv2.VideoCapture):
|
||
from .detection_service import DetectionService
|
||
detection_service = DetectionService(self.model_service)
|
||
|
||
try:
|
||
frame_count = 0
|
||
stop_event = self._stop_events.get(connection_id)
|
||
|
||
while connection_id in self.active_connections:
|
||
# 检查是否收到停止信号
|
||
if stop_event and stop_event.is_set():
|
||
logger.info(f"收到停止信号,结束帧处理: {connection_id}")
|
||
break
|
||
|
||
ret, frame = cap.read()
|
||
if not ret:
|
||
await asyncio.sleep(0.1)
|
||
continue
|
||
|
||
config = self.camera_configs.get(connection_id, {
|
||
'model_id': 'fire_detection',
|
||
'confidence': 0.5,
|
||
'iou': 0.45
|
||
})
|
||
|
||
model_id = config.get('model_id', 'fire_detection')
|
||
confidence = config.get('confidence', 0.5)
|
||
iou = config.get('iou', 0.45)
|
||
draw = True
|
||
composite = config.get('composite', False)
|
||
|
||
processed_frame, result = await detection_service.detect_frame(
|
||
frame,
|
||
model_id=model_id,
|
||
confidence=confidence,
|
||
iou=iou,
|
||
draw=draw,
|
||
composite=composite
|
||
)
|
||
|
||
if result['success']:
|
||
frame_count += 1
|
||
|
||
logger.info(f"发送检测结果: {len(result['detections'])} 个目标, {result['stats']}")
|
||
|
||
detection_message = {
|
||
'type': 'detection',
|
||
'detections': result['detections'],
|
||
'stats': result['stats']
|
||
}
|
||
|
||
# 包含行为告警信息
|
||
if 'alerts' in result and result['alerts']:
|
||
detection_message['alerts'] = result['alerts']
|
||
logger.info(f"发送告警: {len(result['alerts'])} 个")
|
||
|
||
if 'behavior_stats' in result:
|
||
detection_message['behavior_stats'] = result['behavior_stats']
|
||
|
||
await websocket.send_json(detection_message)
|
||
|
||
_, buffer = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 80])
|
||
import base64
|
||
original_frame_data = base64.b64encode(buffer).decode('utf-8')
|
||
|
||
_, buffer = cv2.imencode('.jpg', processed_frame, [cv2.IMWRITE_JPEG_QUALITY, 80])
|
||
frame_data = base64.b64encode(buffer).decode('utf-8')
|
||
|
||
try:
|
||
await websocket.send_json({
|
||
'type': 'original_frame',
|
||
'frame': original_frame_data
|
||
})
|
||
|
||
await websocket.send_json({
|
||
'type': 'annotated_frame',
|
||
'frame': frame_data
|
||
})
|
||
except Exception as e:
|
||
logger.error(f"发送帧数据失败: {e}")
|
||
break
|
||
|
||
await asyncio.sleep(0.03)
|
||
|
||
except Exception as e:
|
||
logger.error(f"处理帧错误: {e}")
|
||
finally:
|
||
# 只在摄像头仍存在于字典中时才释放(避免重复释放)
|
||
if connection_id in self.camera_captures:
|
||
cap.release()
|
||
del self.camera_captures[connection_id]
|
||
logger.info(f"帧处理结束,摄像头已释放: {connection_id}")
|
||
|
||
async def stop_camera(self, connection_id: str):
|
||
# 使用锁确保同一时间只有一个协程在操作该连接的摄像头资源
|
||
lock = self._locks.get(connection_id)
|
||
if lock:
|
||
async with lock:
|
||
await self._do_stop_camera(connection_id)
|
||
else:
|
||
await self._do_stop_camera(connection_id)
|
||
|
||
async def _do_stop_camera(self, connection_id: str):
|
||
"""实际执行停止摄像头的操作(内部方法,应在获取锁后调用)"""
|
||
# 设置停止事件,通知帧处理循环退出
|
||
if connection_id in self._stop_events:
|
||
self._stop_events[connection_id].set()
|
||
|
||
if connection_id in self.camera_captures:
|
||
cap = self.camera_captures[connection_id]
|
||
cap.release()
|
||
del self.camera_captures[connection_id]
|
||
|
||
if connection_id in self.active_connections:
|
||
try:
|
||
await self.active_connections[connection_id].send_json({
|
||
'type': 'camera_stopped',
|
||
'message': '摄像头已停止'
|
||
})
|
||
except:
|
||
pass
|
||
|
||
logger.info(f"摄像头已停止: {connection_id}")
|
||
|
||
# 清理锁和事件
|
||
if connection_id in self._locks:
|
||
del self._locks[connection_id]
|
||
if connection_id in self._stop_events:
|
||
del self._stop_events[connection_id]
|
||
|
||
async def update_config(self, connection_id: str, config: Dict):
|
||
if connection_id in self.camera_configs:
|
||
self.camera_configs[connection_id].update(config)
|
||
|
||
model_id = self.camera_configs[connection_id].get('model_id', 'fire_detection')
|
||
confidence = self.camera_configs[connection_id].get('confidence', 0.5)
|
||
iou = self.camera_configs[connection_id].get('iou', 0.45)
|
||
|
||
logger.info(f"配置更新: model_id={model_id}, confidence={confidence}, iou={iou}")
|
||
|
||
if connection_id in self.active_connections:
|
||
try:
|
||
await self.active_connections[connection_id].send_json({
|
||
'type': 'config_updated',
|
||
'message': '配置已更新'
|
||
})
|
||
except:
|
||
pass
|
||
|
||
async def stop(self):
|
||
for connection_id in list(self.camera_captures.keys()):
|
||
await self.stop_camera(connection_id)
|
||
|
||
self.running = False
|
||
logger.info("摄像头服务已停止")
|