2026-08-30 22:22:11 +08:00
|
|
|
|
"""全局分析管理器(单例)
|
|
|
|
|
|
|
|
|
|
|
|
阶段2:每路摄像头在独立子进程中运行 CameraPipeline;YOLO 推理可选走
|
|
|
|
|
|
主进程 InferenceProcessPool(共享 GPU/模型内存)。事件经 Queue → EventBridge 写库。
|
|
|
|
|
|
"""
|
|
|
|
|
|
import json
|
|
|
|
|
|
import logging
|
|
|
|
|
|
import multiprocessing as mp
|
|
|
|
|
|
import threading
|
|
|
|
|
|
import time
|
2026-09-04 18:16:14 +08:00
|
|
|
|
import uuid
|
2026-08-30 22:22:11 +08:00
|
|
|
|
|
|
|
|
|
|
from app.analysis.pipeline import CameraPipeline
|
|
|
|
|
|
from app.analysis.motion import MotionDetector
|
|
|
|
|
|
from app.analysis.worker_pool import DetectorWorkerPool
|
|
|
|
|
|
from app.analysis.process_worker import pipeline_process_main, PipelineProcessHandle
|
|
|
|
|
|
from app.analysis.event_bridge import get_event_bridge
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger("analysis.manager")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _snapshot_storage_paths():
|
|
|
|
|
|
"""主进程解析报警快照目录,注入子进程(子进程不可 import GlobalUtils)。"""
|
|
|
|
|
|
import os
|
|
|
|
|
|
from framework.settings import BASE_DIR
|
|
|
|
|
|
from app.utils.GlobalUtils import g_config
|
|
|
|
|
|
static_dir = os.path.join(str(BASE_DIR), "static")
|
|
|
|
|
|
alarm_dir = getattr(g_config, "storageAlarmDir", "") or os.path.join(static_dir, "storage", "alarm")
|
|
|
|
|
|
return alarm_dir, static_dir
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-04 18:16:14 +08:00
|
|
|
|
def _algorithm_to_spec(a, target_labels=None):
|
2026-08-30 22:22:11 +08:00
|
|
|
|
labels = a.labels
|
|
|
|
|
|
if isinstance(labels, str):
|
|
|
|
|
|
try:
|
|
|
|
|
|
labels = json.loads(labels)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
labels = []
|
|
|
|
|
|
return {
|
|
|
|
|
|
"id": a.id,
|
|
|
|
|
|
"name": a.name,
|
|
|
|
|
|
"inference_engine": a.inference_engine,
|
|
|
|
|
|
"model_file": a.model_file,
|
|
|
|
|
|
"labels": labels,
|
|
|
|
|
|
"input_width": a.input_width,
|
|
|
|
|
|
"input_height": a.input_height,
|
|
|
|
|
|
"conf_threshold": a.conf_threshold,
|
|
|
|
|
|
"iou_threshold": a.iou_threshold,
|
|
|
|
|
|
"algorithm_type": a.algorithm_type,
|
|
|
|
|
|
"task_type": getattr(a, "task_type", "detect"),
|
|
|
|
|
|
"device": getattr(a, "device", "cpu"),
|
2026-09-04 18:16:14 +08:00
|
|
|
|
"target_labels": sorted(set(target_labels or [])),
|
2026-08-30 22:22:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _biz_algo_to_zone_dict(ba):
|
|
|
|
|
|
from app.utils.Credentials import decrypt_credential
|
|
|
|
|
|
labels = ba.target_labels or '[]'
|
|
|
|
|
|
try:
|
|
|
|
|
|
labels_list = json.loads(labels) if isinstance(labels, str) else labels
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
labels_list = []
|
|
|
|
|
|
llm_cfg = None
|
|
|
|
|
|
if ba.llm_id and ba.llm:
|
|
|
|
|
|
llm_cfg = {
|
|
|
|
|
|
"id": ba.llm_id,
|
|
|
|
|
|
"api_url": ba.llm.api_url,
|
|
|
|
|
|
"api_key": decrypt_credential(ba.llm.api_key),
|
|
|
|
|
|
"model_name": ba.llm.model_name,
|
|
|
|
|
|
"timeout": ba.llm.timeout,
|
|
|
|
|
|
"inference_tool": ba.llm.inference_tool or "OpenAI",
|
|
|
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
|
|
|
"id": ba.id,
|
|
|
|
|
|
"name": ba.name or "",
|
|
|
|
|
|
"flow_type": ba.flow_type,
|
|
|
|
|
|
"small_model_id": ba.small_model_id,
|
|
|
|
|
|
"detector_model_id": ba.detector_model_id,
|
|
|
|
|
|
"target_labels": labels_list,
|
|
|
|
|
|
"llm_id": ba.llm_id,
|
|
|
|
|
|
"llm_prompt": ba.llm_prompt or "",
|
|
|
|
|
|
"llm_validate": ba.llm_validate or "",
|
|
|
|
|
|
"post_process": ba.post_process or "AREA",
|
|
|
|
|
|
"ref_angle": float(getattr(ba, "ref_angle", 90.0) or 90.0),
|
|
|
|
|
|
"angle_tolerance": float(getattr(ba, "angle_tolerance", 45.0) or 45.0),
|
|
|
|
|
|
"forward_count_threshold": int(getattr(ba, "forward_count_threshold", 0) or 0),
|
|
|
|
|
|
"reverse_count_threshold": int(getattr(ba, "reverse_count_threshold", 0) or 0),
|
|
|
|
|
|
"llm": llm_cfg,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AnalysisManager(object):
|
|
|
|
|
|
_instance = None
|
|
|
|
|
|
_instance_lock = threading.Lock()
|
|
|
|
|
|
|
|
|
|
|
|
def __new__(cls, *args, **kwargs):
|
|
|
|
|
|
if cls._instance is None:
|
|
|
|
|
|
with cls._instance_lock:
|
|
|
|
|
|
if cls._instance is None:
|
|
|
|
|
|
cls._instance = super(AnalysisManager, cls).__new__(cls)
|
|
|
|
|
|
cls._instance._initialized = False
|
|
|
|
|
|
return cls._instance
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
|
if getattr(self, "_initialized", False):
|
|
|
|
|
|
return
|
|
|
|
|
|
self._initialized = True
|
|
|
|
|
|
self._pipelines = {} # stream_id -> handle dict
|
|
|
|
|
|
self._lock = threading.RLock()
|
|
|
|
|
|
self._worker_pool = DetectorWorkerPool()
|
|
|
|
|
|
self._mp_ctx = mp.get_context("spawn")
|
|
|
|
|
|
self._status_manager = self._mp_ctx.Manager()
|
|
|
|
|
|
self._status_dict = self._status_manager.dict()
|
|
|
|
|
|
self._infer_req_q = self._mp_ctx.Queue(maxsize=128)
|
2026-09-04 18:16:14 +08:00
|
|
|
|
# Never reuse a consumer queue after terminating a camera process:
|
|
|
|
|
|
# multiprocessing.Queue's read lock can remain acquired on Windows.
|
|
|
|
|
|
self._infer_routes = {}
|
|
|
|
|
|
self._infer_routes_lock = threading.Lock()
|
|
|
|
|
|
self._disabled_algos = set()
|
2026-08-30 22:22:11 +08:00
|
|
|
|
self._infer_forwarder_running = True
|
|
|
|
|
|
self._infer_forwarder = threading.Thread(
|
|
|
|
|
|
target=self._inference_forwarder_loop, name="infer-forwarder", daemon=True)
|
|
|
|
|
|
self._infer_forwarder.start()
|
|
|
|
|
|
get_event_bridge()
|
|
|
|
|
|
self._configure_from_settings()
|
|
|
|
|
|
|
|
|
|
|
|
def _use_multiprocess(self):
|
|
|
|
|
|
try:
|
|
|
|
|
|
from app.utils.GlobalUtils import g_config
|
|
|
|
|
|
mode = int(getattr(g_config, "analysisProcessMode", 1))
|
|
|
|
|
|
return mode >= 1
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def _use_shared_inference(self):
|
|
|
|
|
|
try:
|
|
|
|
|
|
from app.utils.GlobalUtils import g_config
|
|
|
|
|
|
return bool(getattr(g_config, "analysisSharedInference", True))
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def set_inference_config(self, shared=None, workers=None):
|
|
|
|
|
|
"""热更新推理配置(不持久化)。
|
|
|
|
|
|
- shared: 切换共享推理开关;切换后需重启所有运行中的 pipeline
|
|
|
|
|
|
- workers: 调整共享推理 worker 数;调整后重启 inference_pool
|
|
|
|
|
|
返回 (ok, msg)
|
|
|
|
|
|
"""
|
|
|
|
|
|
from app.utils.GlobalUtils import g_config
|
|
|
|
|
|
old_shared = self._use_shared_inference()
|
|
|
|
|
|
old_workers = int(getattr(g_config, "analysisInferenceWorkers", 2))
|
|
|
|
|
|
shared_changed = False
|
|
|
|
|
|
workers_changed = False
|
|
|
|
|
|
if shared is not None:
|
|
|
|
|
|
try:
|
|
|
|
|
|
new_shared = bool(int(shared))
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
new_shared = old_shared
|
|
|
|
|
|
if new_shared != old_shared:
|
|
|
|
|
|
g_config.analysisSharedInference = new_shared
|
|
|
|
|
|
shared_changed = True
|
|
|
|
|
|
if workers is not None:
|
|
|
|
|
|
try:
|
|
|
|
|
|
new_workers = max(1, min(32, int(workers)))
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
new_workers = old_workers
|
|
|
|
|
|
if new_workers != old_workers:
|
|
|
|
|
|
g_config.analysisInferenceWorkers = new_workers
|
|
|
|
|
|
workers_changed = True
|
|
|
|
|
|
# 重启推理池(worker 数变了,或从非共享切到共享)
|
|
|
|
|
|
if workers_changed or (shared_changed and self._use_shared_inference()):
|
|
|
|
|
|
try:
|
|
|
|
|
|
from app.analysis.inference_pool import shutdown_inference_pool, get_inference_pool
|
|
|
|
|
|
shutdown_inference_pool()
|
|
|
|
|
|
if self._use_shared_inference():
|
|
|
|
|
|
get_inference_pool() # 会按新 worker 数重建
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning("重启推理池失败: %s" % str(e))
|
|
|
|
|
|
# shared 切换后重启所有运行中的 pipeline,让新模式生效
|
|
|
|
|
|
if shared_changed:
|
|
|
|
|
|
with self._lock:
|
|
|
|
|
|
sids = list(self._pipelines.keys())
|
|
|
|
|
|
for sid in sids:
|
|
|
|
|
|
try:
|
|
|
|
|
|
from app.models import StreamModel as _SM
|
|
|
|
|
|
s = _SM.objects.get(id=sid)
|
|
|
|
|
|
self.stop(sid)
|
|
|
|
|
|
self.start(s)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning("shared 切换重启 pipeline sid=%s 失败: %s" % (sid, str(e)))
|
|
|
|
|
|
if not shared_changed and not workers_changed:
|
|
|
|
|
|
return True, "配置未变化"
|
|
|
|
|
|
return True, "配置已热生效"
|
|
|
|
|
|
|
|
|
|
|
|
def set_algo_instance_enabled(self, algo_id, enabled):
|
|
|
|
|
|
"""设置业务算法的实例化开关(内存,重启丢失)。立即生效,无需重启 pipeline。"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
aid = int(algo_id)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return False, "invalid algorithm_id"
|
|
|
|
|
|
with self._lock:
|
|
|
|
|
|
if enabled:
|
|
|
|
|
|
self._disabled_algos.discard(aid)
|
|
|
|
|
|
else:
|
|
|
|
|
|
self._disabled_algos.add(aid)
|
|
|
|
|
|
return True, "ok"
|
|
|
|
|
|
|
|
|
|
|
|
def is_algo_instance_enabled(self, algo_id):
|
|
|
|
|
|
try:
|
|
|
|
|
|
aid = int(algo_id)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return True
|
|
|
|
|
|
return aid not in self._disabled_algos
|
|
|
|
|
|
|
|
|
|
|
|
def get_disabled_algos(self):
|
|
|
|
|
|
with self._lock:
|
|
|
|
|
|
return set(self._disabled_algos)
|
|
|
|
|
|
|
|
|
|
|
|
def restart_algo_instance(self, algo_id):
|
|
|
|
|
|
"""重启使用指定算法的所有 pipeline(重新加载引擎)。
|
|
|
|
|
|
algo_id 是小模型 AlgorithmModel.id。
|
|
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
aid = int(algo_id)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return False, "invalid algorithm_id"
|
|
|
|
|
|
with self._lock:
|
|
|
|
|
|
sids = []
|
|
|
|
|
|
for sid, item in list(self._pipelines.items()):
|
|
|
|
|
|
algo_ids = item.get("algorithm_ids") or []
|
|
|
|
|
|
if aid in algo_ids:
|
|
|
|
|
|
sids.append(sid)
|
|
|
|
|
|
if not sids:
|
|
|
|
|
|
return True, "没有运行中的 pipeline 使用该算法"
|
|
|
|
|
|
restarted = 0
|
|
|
|
|
|
for sid in sids:
|
|
|
|
|
|
try:
|
|
|
|
|
|
from app.models import StreamModel as _SM
|
|
|
|
|
|
s = _SM.objects.get(id=sid)
|
|
|
|
|
|
self.stop(sid)
|
|
|
|
|
|
self.start(s)
|
|
|
|
|
|
restarted += 1
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning("restart_algo_instance sid=%s 失败: %s" % (sid, str(e)))
|
|
|
|
|
|
return True, "已重启 %d 路 pipeline" % restarted
|
|
|
|
|
|
|
|
|
|
|
|
def restart_inference_pool(self):
|
|
|
|
|
|
"""重启整个推理池(清除所有 worker 子进程内的引擎缓存)。"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
from app.analysis.inference_pool import shutdown_inference_pool, get_inference_pool
|
|
|
|
|
|
shutdown_inference_pool()
|
|
|
|
|
|
if self._use_shared_inference():
|
|
|
|
|
|
get_inference_pool()
|
|
|
|
|
|
return True, "推理池已重启,所有引擎缓存已清除"
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
return False, str(e)
|
|
|
|
|
|
|
|
|
|
|
|
def _inference_forwarder_loop(self):
|
|
|
|
|
|
from app.analysis.inference_pool import get_inference_pool
|
|
|
|
|
|
import queue as _q
|
|
|
|
|
|
while self._infer_forwarder_running:
|
|
|
|
|
|
try:
|
|
|
|
|
|
msg = self._infer_req_q.get(timeout=0.5)
|
|
|
|
|
|
except _q.Empty:
|
|
|
|
|
|
continue
|
|
|
|
|
|
if msg is None:
|
|
|
|
|
|
break
|
|
|
|
|
|
req_id = msg.get("req_id")
|
2026-09-04 18:16:14 +08:00
|
|
|
|
channel = msg.get("response_channel")
|
|
|
|
|
|
with self._infer_routes_lock:
|
|
|
|
|
|
if channel not in self._infer_routes:
|
|
|
|
|
|
continue # The originating pipeline has already stopped.
|
2026-08-30 22:22:11 +08:00
|
|
|
|
try:
|
|
|
|
|
|
jpeg = msg.get("jpeg")
|
|
|
|
|
|
algo = msg.get("algorithm") or {}
|
|
|
|
|
|
# 禁用实例化的算法直接返回空结果,跳过推理
|
|
|
|
|
|
algo_id = algo.get("id", 0)
|
|
|
|
|
|
try:
|
|
|
|
|
|
if algo_id and int(algo_id) in self._disabled_algos:
|
2026-09-04 18:16:14 +08:00
|
|
|
|
self._send_inference_response(channel, {"req_id": req_id, "ok": True, "detections": []})
|
2026-08-30 22:22:11 +08:00
|
|
|
|
continue
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
# 直接透传 JPEG bytes 给推理池,避免主进程 imdecode + imencode 双重编解码,
|
|
|
|
|
|
# 消除主进程 GIL 占用(解码在 worker 子进程内完成)。
|
2026-09-04 18:16:14 +08:00
|
|
|
|
pool = get_inference_pool() # Config changes may replace the pool.
|
|
|
|
|
|
dets = pool.detect_jpeg(jpeg, algo, timeout=30.0, raise_errors=True)
|
|
|
|
|
|
self._send_inference_response(channel, {"req_id": req_id, "ok": True, "detections": dets})
|
2026-08-30 22:22:11 +08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning("推理转发失败: %s", e)
|
|
|
|
|
|
try:
|
2026-09-04 18:16:14 +08:00
|
|
|
|
self._send_inference_response(channel, {"req_id": req_id, "ok": False, "error": str(e)})
|
2026-08-30 22:22:11 +08:00
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
2026-09-04 18:16:14 +08:00
|
|
|
|
def _send_inference_response(self, channel, response):
|
|
|
|
|
|
with self._infer_routes_lock:
|
|
|
|
|
|
response_queue = self._infer_routes.get(channel)
|
|
|
|
|
|
if response_queue is not None:
|
|
|
|
|
|
response_queue.put(response, timeout=1.0)
|
|
|
|
|
|
|
|
|
|
|
|
def _close_inference_channel(self, item):
|
|
|
|
|
|
with self._infer_routes_lock:
|
|
|
|
|
|
response_queue = self._infer_routes.pop(item.get("response_channel"), None)
|
|
|
|
|
|
if response_queue is not None:
|
|
|
|
|
|
response_queue.cancel_join_thread()
|
|
|
|
|
|
response_queue.close()
|
|
|
|
|
|
|
2026-08-30 22:22:11 +08:00
|
|
|
|
def _configure_from_settings(self):
|
|
|
|
|
|
try:
|
|
|
|
|
|
from app.models import AlgorithmModel
|
|
|
|
|
|
default = AlgorithmModel.objects.filter(is_default=1, state=1).first()
|
|
|
|
|
|
if default:
|
|
|
|
|
|
self._default_algorithm = default
|
|
|
|
|
|
self._target_fps = 5
|
|
|
|
|
|
return
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning("AnalysisManager 读取默认 AlgorithmModel 失败: %s" % str(e))
|
|
|
|
|
|
try:
|
|
|
|
|
|
from app.utils.GlobalUtils import g_config
|
|
|
|
|
|
self._target_fps = int(getattr(g_config, "analysisTargetFps", 5))
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
self._target_fps = 5
|
|
|
|
|
|
self._default_algorithm = None
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def build_rtsp_url(stream):
|
|
|
|
|
|
try:
|
|
|
|
|
|
from app.utils.GlobalUtils import g_config
|
|
|
|
|
|
ip = getattr(g_config, "externalHost", "127.0.0.1") or "127.0.0.1"
|
|
|
|
|
|
if ip == "0.0.0.0":
|
|
|
|
|
|
ip = "127.0.0.1"
|
|
|
|
|
|
port = getattr(g_config, "mediaRtspPort", 10554)
|
|
|
|
|
|
app = getattr(stream, "app", "live") or "live"
|
|
|
|
|
|
name = getattr(stream, "name", getattr(stream, "code", "stream")) or "stream"
|
|
|
|
|
|
return "rtsp://%s:%s/%s/%s" % (ip, int(port), app, name)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning("build_rtsp_url 失败: %s" % str(e))
|
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _zone_analyze_fps(interval_sec, detect_frames):
|
|
|
|
|
|
interval = max(0.1, float(interval_sec or 1))
|
|
|
|
|
|
frames = max(1, int(detect_frames or 1))
|
|
|
|
|
|
return float(frames) / interval
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _compute_analyze_fps(stream_id, fallback=None):
|
|
|
|
|
|
"""取该摄像头所有启用布控中最高的算法分析频率(帧/秒)"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
from app.models import ZoneModel
|
|
|
|
|
|
qs = ZoneModel.objects.filter(stream_id=stream_id, state=1)
|
|
|
|
|
|
max_fps = 0.0
|
|
|
|
|
|
for z in qs:
|
|
|
|
|
|
max_fps = max(max_fps, AnalysisManager._zone_analyze_fps(
|
|
|
|
|
|
getattr(z, "detect_interval_sec", 1),
|
|
|
|
|
|
getattr(z, "detect_frames", 1),
|
|
|
|
|
|
))
|
|
|
|
|
|
if max_fps > 0:
|
|
|
|
|
|
return max_fps
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning("_compute_analyze_fps err stream=%s: %s" % (stream_id, str(e)))
|
|
|
|
|
|
if fallback is not None and fallback > 0:
|
|
|
|
|
|
return float(fallback)
|
|
|
|
|
|
return 1.0
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
2026-09-04 18:16:14 +08:00
|
|
|
|
def _load_zones(stream_id, active_only=True, zone_id=None):
|
2026-08-30 22:22:11 +08:00
|
|
|
|
try:
|
|
|
|
|
|
from app.models import ZoneModel
|
2026-09-04 18:16:14 +08:00
|
|
|
|
qs = ZoneModel.objects.filter(stream_id=stream_id)
|
|
|
|
|
|
if active_only:
|
|
|
|
|
|
qs = qs.filter(state=1)
|
|
|
|
|
|
if zone_id is not None:
|
|
|
|
|
|
qs = qs.filter(id=int(zone_id))
|
|
|
|
|
|
qs = qs.prefetch_related(
|
2026-08-30 22:22:11 +08:00
|
|
|
|
'algorithms', 'algorithms__small_model', 'algorithms__detector_model', 'algorithms__llm')
|
|
|
|
|
|
zones = []
|
|
|
|
|
|
for z in qs:
|
|
|
|
|
|
try:
|
|
|
|
|
|
coords = json.loads(z.coordinates)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
coords = []
|
|
|
|
|
|
# LINE_CROSS 警戒线端点(归一化坐标 JSON)
|
|
|
|
|
|
line_a = None
|
|
|
|
|
|
line_b = None
|
|
|
|
|
|
try:
|
|
|
|
|
|
la = getattr(z, "line_a", "") or ""
|
|
|
|
|
|
if la:
|
|
|
|
|
|
line_a = json.loads(la)
|
|
|
|
|
|
lb = getattr(z, "line_b", "") or ""
|
|
|
|
|
|
if lb:
|
|
|
|
|
|
line_b = json.loads(lb)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
line_a = line_b = None
|
|
|
|
|
|
biz_list = []
|
|
|
|
|
|
biz_ids = []
|
|
|
|
|
|
small_ids = set()
|
|
|
|
|
|
for ba in z.algorithms.filter(state=1):
|
|
|
|
|
|
biz_ids.append(ba.id)
|
|
|
|
|
|
biz_list.append(_biz_algo_to_zone_dict(ba))
|
|
|
|
|
|
if int(ba.flow_type or 0) == 4:
|
|
|
|
|
|
if ba.detector_model_id:
|
|
|
|
|
|
small_ids.add(ba.detector_model_id)
|
|
|
|
|
|
elif ba.small_model_id:
|
|
|
|
|
|
small_ids.add(ba.small_model_id)
|
|
|
|
|
|
interval = max(0.1, float(getattr(z, "detect_interval_sec", 1) or 1))
|
|
|
|
|
|
frames = max(1, int(getattr(z, "detect_frames", 1) or 1))
|
|
|
|
|
|
zones.append({
|
|
|
|
|
|
"id": z.id,
|
|
|
|
|
|
"name": z.name,
|
|
|
|
|
|
"coords": coords,
|
|
|
|
|
|
"is_required": z.is_required,
|
|
|
|
|
|
"loiter_threshold": z.loiter_threshold,
|
|
|
|
|
|
"detect_interval_sec": interval,
|
|
|
|
|
|
"detect_frames": frames,
|
2026-09-04 18:16:14 +08:00
|
|
|
|
"alarm_repeat_sec": max(0.0, float(getattr(z, "alarm_repeat_sec", 30) or 0)),
|
|
|
|
|
|
"color": z.color or "#169F85",
|
2026-08-30 22:22:11 +08:00
|
|
|
|
"line_a": line_a,
|
|
|
|
|
|
"line_b": line_b,
|
|
|
|
|
|
"density_threshold": int(getattr(z, "density_threshold", 0) or 0),
|
|
|
|
|
|
"algorithm_ids": biz_ids,
|
|
|
|
|
|
"biz_algorithms": biz_list,
|
|
|
|
|
|
"small_model_ids": sorted(small_ids),
|
|
|
|
|
|
})
|
|
|
|
|
|
return zones
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning("加载 Zone 失败 stream=%s: %s" % (stream_id, str(e)))
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
def _resolve_algorithms_for_stream(self, stream):
|
|
|
|
|
|
try:
|
|
|
|
|
|
from app.models import ZoneModel, AlgorithmModel
|
|
|
|
|
|
algos = []
|
|
|
|
|
|
seen = set()
|
|
|
|
|
|
for z in ZoneModel.objects.filter(stream_id=stream.id, state=1).prefetch_related(
|
|
|
|
|
|
'algorithms__small_model', 'algorithms__detector_model'):
|
|
|
|
|
|
for ba in z.algorithms.filter(state=1):
|
|
|
|
|
|
if int(ba.flow_type or 0) == 4:
|
|
|
|
|
|
det = ba.detector_model
|
|
|
|
|
|
if det and det.state == 1 and det.id not in seen:
|
|
|
|
|
|
seen.add(det.id)
|
|
|
|
|
|
algos.append(det)
|
|
|
|
|
|
continue
|
|
|
|
|
|
sm = ba.small_model
|
|
|
|
|
|
if sm and sm.state == 1 and sm.id not in seen:
|
|
|
|
|
|
seen.add(sm.id)
|
|
|
|
|
|
algos.append(sm)
|
|
|
|
|
|
sa = getattr(stream, "algorithm", None)
|
|
|
|
|
|
if sa is not None and sa.state == 1 and sa.id not in seen:
|
|
|
|
|
|
seen.add(sa.id)
|
|
|
|
|
|
algos.append(sa)
|
|
|
|
|
|
if not algos:
|
|
|
|
|
|
d = AlgorithmModel.objects.filter(is_default=1, state=1).first()
|
|
|
|
|
|
if d:
|
|
|
|
|
|
algos.append(d)
|
|
|
|
|
|
return algos
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning("_resolve_algorithms_for_stream err: %s" % str(e))
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
def _fallback_engine_from_config(self):
|
|
|
|
|
|
try:
|
|
|
|
|
|
from app.utils.GlobalUtils import g_config
|
|
|
|
|
|
model_path = getattr(g_config, "analysisDetectorModel", "") or ""
|
|
|
|
|
|
if not model_path:
|
|
|
|
|
|
return None
|
|
|
|
|
|
labels = getattr(g_config, "analysisDetectorLabels", [])
|
|
|
|
|
|
if isinstance(labels, str):
|
|
|
|
|
|
labels = [x.strip() for x in labels.split(",") if x.strip()]
|
|
|
|
|
|
conf = float(getattr(g_config, "analysisConfThreshold", 0.4))
|
|
|
|
|
|
from app.analysis.engines.onnx_engine import OnnxEngine
|
|
|
|
|
|
eng = OnnxEngine(model_path=model_path, labels=labels, conf_threshold=conf)
|
|
|
|
|
|
if eng.load():
|
|
|
|
|
|
return eng
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning("config fallback engine err: %s" % str(e))
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
2026-09-04 18:16:14 +08:00
|
|
|
|
def _start_process(self, stream, url, zones, algos, detectors_legacy=None,
|
|
|
|
|
|
preview_only=False, analyze_fps_override=None):
|
2026-08-30 22:22:11 +08:00
|
|
|
|
sid = stream.id
|
2026-09-04 18:16:14 +08:00
|
|
|
|
self._status_dict.pop(str(sid), None)
|
2026-08-30 22:22:11 +08:00
|
|
|
|
event_queue = self._mp_ctx.Queue(maxsize=256)
|
|
|
|
|
|
cmd_queue = self._mp_ctx.Queue(maxsize=16)
|
|
|
|
|
|
bridge = get_event_bridge()
|
|
|
|
|
|
bridge.register_queue(event_queue)
|
|
|
|
|
|
|
2026-09-04 18:16:14 +08:00
|
|
|
|
target_by_model = {}
|
|
|
|
|
|
for zone in zones:
|
|
|
|
|
|
for rule in zone.get("biz_algorithms") or []:
|
|
|
|
|
|
model_id = rule.get("detector_model_id") if int(rule.get("flow_type") or 0) == 4 else rule.get("small_model_id")
|
|
|
|
|
|
if model_id:
|
|
|
|
|
|
target_by_model.setdefault(int(model_id), set()).update(rule.get("target_labels") or [])
|
|
|
|
|
|
algo_specs = [_algorithm_to_spec(a, target_by_model.get(int(a.id), set())) for a in algos]
|
|
|
|
|
|
analyze_fps = (float(analyze_fps_override) if analyze_fps_override is not None
|
|
|
|
|
|
else self._compute_analyze_fps(sid, fallback=self._target_fps))
|
2026-08-30 22:22:11 +08:00
|
|
|
|
storage_alarm_dir, static_dir = _snapshot_storage_paths()
|
2026-09-04 18:16:14 +08:00
|
|
|
|
response_channel = uuid.uuid4().hex
|
|
|
|
|
|
infer_resp_q = self._mp_ctx.Queue(maxsize=128)
|
|
|
|
|
|
with self._infer_routes_lock:
|
|
|
|
|
|
self._infer_routes[response_channel] = infer_resp_q
|
2026-08-30 22:22:11 +08:00
|
|
|
|
config = {
|
|
|
|
|
|
"stream_id": sid,
|
|
|
|
|
|
"stream_code": getattr(stream, "code", str(sid)),
|
|
|
|
|
|
"rtsp_url": url,
|
|
|
|
|
|
"target_fps": self._target_fps,
|
|
|
|
|
|
"analyze_fps": analyze_fps,
|
|
|
|
|
|
"zones": zones,
|
|
|
|
|
|
"algorithms": algo_specs,
|
|
|
|
|
|
"use_shared_inference": self._use_shared_inference(),
|
|
|
|
|
|
"storage_alarm_dir": storage_alarm_dir,
|
|
|
|
|
|
"static_dir": static_dir,
|
2026-09-04 18:16:14 +08:00
|
|
|
|
"response_channel": response_channel,
|
|
|
|
|
|
"preview_only": bool(preview_only),
|
2026-08-30 22:22:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
proc = self._mp_ctx.Process(
|
|
|
|
|
|
target=pipeline_process_main,
|
|
|
|
|
|
args=(config, event_queue, cmd_queue, self._status_dict,
|
2026-09-04 18:16:14 +08:00
|
|
|
|
self._infer_req_q, infer_resp_q),
|
2026-08-30 22:22:11 +08:00
|
|
|
|
name="pipeline-%s" % sid,
|
|
|
|
|
|
daemon=True,
|
|
|
|
|
|
)
|
2026-09-04 18:16:14 +08:00
|
|
|
|
try:
|
|
|
|
|
|
proc.start()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
self._close_inference_channel({"response_channel": response_channel})
|
|
|
|
|
|
bridge.unregister_queue(event_queue)
|
|
|
|
|
|
raise
|
2026-08-30 22:22:11 +08:00
|
|
|
|
handle = PipelineProcessHandle(sid, proc, event_queue, cmd_queue, self._status_dict)
|
|
|
|
|
|
self._pipelines[sid] = {
|
|
|
|
|
|
"handle": handle,
|
|
|
|
|
|
"process": proc,
|
|
|
|
|
|
"event_queue": event_queue,
|
|
|
|
|
|
"mode": "process",
|
|
|
|
|
|
"running": True,
|
|
|
|
|
|
"pipeline": None,
|
|
|
|
|
|
"thread": None,
|
|
|
|
|
|
"algorithm_ids": sorted([a.id for a in algos]),
|
2026-09-04 18:16:14 +08:00
|
|
|
|
"preview_only": bool(preview_only),
|
|
|
|
|
|
"response_channel": response_channel,
|
|
|
|
|
|
"analyze_fps": analyze_fps,
|
2026-08-30 22:22:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
return True, "started (process)"
|
|
|
|
|
|
|
2026-09-04 18:16:14 +08:00
|
|
|
|
def _start_thread(self, stream, url, zones, algos, preview_only=False,
|
|
|
|
|
|
analyze_fps_override=None):
|
2026-08-30 22:22:11 +08:00
|
|
|
|
sid = stream.id
|
|
|
|
|
|
detectors = []
|
|
|
|
|
|
algo_names = []
|
|
|
|
|
|
for a in algos:
|
2026-09-04 18:16:14 +08:00
|
|
|
|
labels = set()
|
|
|
|
|
|
for z in zones:
|
|
|
|
|
|
for rule in z.get("biz_algorithms") or []:
|
|
|
|
|
|
model_id = (rule.get("detector_model_id") if int(rule.get("flow_type") or 0) == 4
|
|
|
|
|
|
else rule.get("small_model_id"))
|
|
|
|
|
|
if int(model_id or 0) == int(a.id):
|
|
|
|
|
|
labels.update(rule.get("target_labels") or [])
|
|
|
|
|
|
eng = self._worker_pool.get_detector(_algorithm_to_spec(a, labels))
|
2026-08-30 22:22:11 +08:00
|
|
|
|
if eng:
|
2026-09-04 18:16:14 +08:00
|
|
|
|
detectors.append({"algorithm_id": a.id, "algorithm_name": a.name,
|
|
|
|
|
|
"engine": eng, "target_labels": sorted(labels)})
|
2026-08-30 22:22:11 +08:00
|
|
|
|
algo_names.append(a.name)
|
|
|
|
|
|
if not algos:
|
|
|
|
|
|
eng = self._fallback_engine_from_config()
|
|
|
|
|
|
if eng:
|
|
|
|
|
|
detectors.append({"algorithm_id": 0, "algorithm_name": "config-fallback", "engine": eng})
|
|
|
|
|
|
algo_names.append("config-fallback")
|
|
|
|
|
|
|
|
|
|
|
|
motion = MotionDetector()
|
2026-09-04 18:16:14 +08:00
|
|
|
|
analyze_fps = (float(analyze_fps_override) if analyze_fps_override is not None
|
|
|
|
|
|
else self._compute_analyze_fps(sid, fallback=self._target_fps))
|
2026-08-30 22:22:11 +08:00
|
|
|
|
storage_alarm_dir, static_dir = _snapshot_storage_paths()
|
|
|
|
|
|
pipeline = CameraPipeline(
|
|
|
|
|
|
stream_id=sid,
|
|
|
|
|
|
stream_code=getattr(stream, "code", str(sid)),
|
|
|
|
|
|
rtsp_url=url,
|
|
|
|
|
|
detectors=detectors,
|
|
|
|
|
|
motion=motion,
|
|
|
|
|
|
target_fps=self._target_fps,
|
|
|
|
|
|
analyze_fps=analyze_fps,
|
|
|
|
|
|
on_event=self._on_event,
|
|
|
|
|
|
on_track_snapshot=self._on_track_snapshot,
|
2026-09-04 18:16:14 +08:00
|
|
|
|
on_preview=lambda payload: get_event_bridge()._on_preview(payload),
|
|
|
|
|
|
alarm_enabled=not bool(preview_only),
|
2026-08-30 22:22:11 +08:00
|
|
|
|
zone_polygons=zones,
|
|
|
|
|
|
storage_alarm_dir=storage_alarm_dir,
|
|
|
|
|
|
static_dir=static_dir,
|
|
|
|
|
|
)
|
|
|
|
|
|
pipeline._algorithm_name = ", ".join(algo_names) if algo_names else "motion-only"
|
|
|
|
|
|
t = threading.Thread(target=pipeline.run, name="pipeline-%s" % sid, daemon=True)
|
|
|
|
|
|
self._pipelines[sid] = {
|
|
|
|
|
|
"pipeline": pipeline,
|
|
|
|
|
|
"thread": t,
|
|
|
|
|
|
"running": True,
|
|
|
|
|
|
"mode": "thread",
|
|
|
|
|
|
"algorithm_ids": sorted([a.id for a in algos]),
|
2026-09-04 18:16:14 +08:00
|
|
|
|
"preview_only": bool(preview_only),
|
|
|
|
|
|
"analyze_fps": analyze_fps,
|
2026-08-30 22:22:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
t.start()
|
|
|
|
|
|
return True, "started (thread)"
|
|
|
|
|
|
|
|
|
|
|
|
def start(self, stream):
|
2026-09-04 18:16:14 +08:00
|
|
|
|
from monitor_runtime.licensing import require_license
|
|
|
|
|
|
require_license()
|
2026-08-30 22:22:11 +08:00
|
|
|
|
sid = stream.id
|
|
|
|
|
|
with self._lock:
|
|
|
|
|
|
item = self._pipelines.get(sid)
|
|
|
|
|
|
if item and item.get("running"):
|
|
|
|
|
|
alive = True
|
|
|
|
|
|
if item.get("mode") == "process":
|
|
|
|
|
|
proc = item.get("process")
|
|
|
|
|
|
alive = proc is not None and proc.is_alive()
|
|
|
|
|
|
else:
|
|
|
|
|
|
th = item.get("thread")
|
|
|
|
|
|
alive = th is not None and th.is_alive()
|
|
|
|
|
|
if alive:
|
2026-09-04 18:16:14 +08:00
|
|
|
|
if not item.get("preview_only"):
|
|
|
|
|
|
return True, "already running"
|
|
|
|
|
|
# 正式启动接管由预览创建的临时管线。
|
|
|
|
|
|
self.stop(sid)
|
|
|
|
|
|
item = None
|
2026-08-30 22:22:11 +08:00
|
|
|
|
# 僵尸条目:进程/线程已退出但未清理
|
|
|
|
|
|
try:
|
|
|
|
|
|
if item.get("mode") == "process":
|
2026-09-04 18:16:14 +08:00
|
|
|
|
self._close_inference_channel(item)
|
2026-08-30 22:22:11 +08:00
|
|
|
|
eq = item.get("event_queue")
|
|
|
|
|
|
if eq:
|
|
|
|
|
|
get_event_bridge().unregister_queue(eq)
|
|
|
|
|
|
else:
|
|
|
|
|
|
pipe = item.get("pipeline")
|
|
|
|
|
|
if pipe:
|
|
|
|
|
|
pipe.stop()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
self._pipelines.pop(sid, None)
|
|
|
|
|
|
url = self.build_rtsp_url(stream)
|
|
|
|
|
|
if not url:
|
|
|
|
|
|
return False, "no rtsp url"
|
|
|
|
|
|
algos = self._resolve_algorithms_for_stream(stream)
|
|
|
|
|
|
zones = self._load_zones(sid)
|
|
|
|
|
|
if self._use_multiprocess():
|
|
|
|
|
|
ok, msg = self._start_process(stream, url, zones, algos)
|
|
|
|
|
|
else:
|
|
|
|
|
|
ok, msg = self._start_thread(stream, url, zones, algos)
|
|
|
|
|
|
if ok:
|
|
|
|
|
|
time.sleep(0.35)
|
|
|
|
|
|
if not self.is_running(sid):
|
|
|
|
|
|
self._purge_pipeline(sid)
|
|
|
|
|
|
return False, "analysis subprocess exited (check OpenCV / RTSP / log)"
|
|
|
|
|
|
return ok, msg
|
|
|
|
|
|
|
2026-09-04 18:16:14 +08:00
|
|
|
|
def start_preview(self, zone):
|
|
|
|
|
|
"""确保所选布控有检测管线;停用布控使用不写报警的临时管线。"""
|
|
|
|
|
|
from monitor_runtime.licensing import require_license
|
|
|
|
|
|
require_license()
|
|
|
|
|
|
stream = zone.stream
|
|
|
|
|
|
sid = stream.id
|
|
|
|
|
|
with self._lock:
|
|
|
|
|
|
item = self._pipelines.get(sid)
|
|
|
|
|
|
if self._is_pipeline_alive(item):
|
|
|
|
|
|
if not item.get("preview_only") and "preview_restore_fps" not in item:
|
|
|
|
|
|
base_fps = float(item.get("analyze_fps") or self._compute_analyze_fps(sid, 1))
|
|
|
|
|
|
item["preview_restore_fps"] = base_fps
|
|
|
|
|
|
boosted_fps = max(5.0, base_fps)
|
|
|
|
|
|
if item.get("mode") == "process" and item.get("handle"):
|
|
|
|
|
|
item["handle"].set_analyze_fps(boosted_fps)
|
|
|
|
|
|
elif item.get("pipeline"):
|
|
|
|
|
|
item["pipeline"].set_analyze_fps(boosted_fps)
|
|
|
|
|
|
return True, "preview" if item.get("preview_only") else "formal"
|
|
|
|
|
|
if item:
|
|
|
|
|
|
self._purge_pipeline(sid)
|
|
|
|
|
|
algos = []
|
|
|
|
|
|
seen = set()
|
|
|
|
|
|
for ba in zone.algorithms.filter(state=1).select_related("small_model", "detector_model"):
|
|
|
|
|
|
model = ba.detector_model if int(ba.flow_type or 0) == 4 else ba.small_model
|
|
|
|
|
|
if model and model.state == 1 and model.id not in seen:
|
|
|
|
|
|
seen.add(model.id)
|
|
|
|
|
|
algos.append(model)
|
|
|
|
|
|
if not algos:
|
|
|
|
|
|
return False, "布控未绑定可用的小模型"
|
|
|
|
|
|
zones = self._load_zones(sid, active_only=False, zone_id=zone.id)
|
|
|
|
|
|
url = self.build_rtsp_url(stream)
|
|
|
|
|
|
if not url:
|
|
|
|
|
|
return False, "no rtsp url"
|
|
|
|
|
|
fps = max(5.0, self._zone_analyze_fps(zone.detect_interval_sec, zone.detect_frames))
|
|
|
|
|
|
if self._use_multiprocess():
|
|
|
|
|
|
ok, msg = self._start_process(stream, url, zones, algos,
|
|
|
|
|
|
preview_only=True, analyze_fps_override=fps)
|
|
|
|
|
|
else:
|
|
|
|
|
|
ok, msg = self._start_thread(stream, url, zones, algos,
|
|
|
|
|
|
preview_only=True, analyze_fps_override=fps)
|
|
|
|
|
|
if ok:
|
|
|
|
|
|
time.sleep(0.35)
|
|
|
|
|
|
if not self.is_running(sid):
|
|
|
|
|
|
self._purge_pipeline(sid)
|
|
|
|
|
|
return False, "预览分析子进程启动失败,请检查视频流或模型日志"
|
|
|
|
|
|
return ok, msg
|
|
|
|
|
|
|
|
|
|
|
|
def stop_preview(self, stream_id):
|
|
|
|
|
|
"""只释放预览创建的管线,不影响正式布控。"""
|
|
|
|
|
|
with self._lock:
|
|
|
|
|
|
item = self._pipelines.get(int(stream_id))
|
|
|
|
|
|
if not item:
|
|
|
|
|
|
return False
|
|
|
|
|
|
if not item.get("preview_only"):
|
|
|
|
|
|
restore_fps = item.pop("preview_restore_fps", None)
|
|
|
|
|
|
if restore_fps is not None:
|
|
|
|
|
|
if item.get("mode") == "process" and item.get("handle"):
|
|
|
|
|
|
item["handle"].set_analyze_fps(restore_fps)
|
|
|
|
|
|
elif item.get("pipeline"):
|
|
|
|
|
|
item["pipeline"].set_analyze_fps(restore_fps)
|
|
|
|
|
|
return False
|
|
|
|
|
|
self.stop(int(stream_id))
|
|
|
|
|
|
get_event_bridge().clear_preview(int(stream_id))
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def preview_mode(self, stream_id):
|
|
|
|
|
|
with self._lock:
|
|
|
|
|
|
item = self._pipelines.get(int(stream_id))
|
|
|
|
|
|
if not self._is_pipeline_alive(item):
|
|
|
|
|
|
return "stopped"
|
|
|
|
|
|
return "preview" if item.get("preview_only") else "formal"
|
|
|
|
|
|
|
2026-08-30 22:22:11 +08:00
|
|
|
|
def stop(self, stream_id):
|
|
|
|
|
|
with self._lock:
|
|
|
|
|
|
item = self._pipelines.get(stream_id)
|
|
|
|
|
|
if not item:
|
|
|
|
|
|
return False, "not running"
|
|
|
|
|
|
if item.get("mode") == "process":
|
|
|
|
|
|
handle = item.get("handle")
|
|
|
|
|
|
if handle:
|
|
|
|
|
|
handle.stop()
|
2026-09-04 18:16:14 +08:00
|
|
|
|
self._close_inference_channel(item)
|
2026-08-30 22:22:11 +08:00
|
|
|
|
eq = item.get("event_queue")
|
|
|
|
|
|
if eq:
|
|
|
|
|
|
get_event_bridge().unregister_queue(eq)
|
|
|
|
|
|
else:
|
|
|
|
|
|
item["pipeline"].stop()
|
|
|
|
|
|
item["thread"].join(timeout=3)
|
|
|
|
|
|
item["running"] = False
|
|
|
|
|
|
self._pipelines.pop(stream_id, None)
|
|
|
|
|
|
return True, "stopped"
|
|
|
|
|
|
|
|
|
|
|
|
def is_running(self, stream_id):
|
|
|
|
|
|
with self._lock:
|
|
|
|
|
|
item = self._pipelines.get(stream_id)
|
|
|
|
|
|
return self._is_pipeline_alive(item)
|
|
|
|
|
|
|
|
|
|
|
|
def _is_pipeline_alive(self, item):
|
|
|
|
|
|
if not item or not item.get("running"):
|
|
|
|
|
|
return False
|
|
|
|
|
|
if item.get("mode") == "process":
|
|
|
|
|
|
proc = item.get("process")
|
|
|
|
|
|
return proc is not None and proc.is_alive()
|
|
|
|
|
|
th = item.get("thread")
|
|
|
|
|
|
if th is not None and not th.is_alive():
|
|
|
|
|
|
return False
|
|
|
|
|
|
pipe = item.get("pipeline")
|
|
|
|
|
|
if pipe is not None and not getattr(pipe, "_running", False):
|
|
|
|
|
|
return False
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def _purge_pipeline(self, stream_id):
|
|
|
|
|
|
item = self._pipelines.pop(stream_id, None)
|
|
|
|
|
|
if not item:
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
if item.get("mode") == "process":
|
|
|
|
|
|
eq = item.get("event_queue")
|
|
|
|
|
|
if eq:
|
|
|
|
|
|
get_event_bridge().unregister_queue(eq)
|
|
|
|
|
|
handle = item.get("handle")
|
|
|
|
|
|
if handle:
|
|
|
|
|
|
try:
|
|
|
|
|
|
handle.stop(timeout=1)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
2026-09-04 18:16:14 +08:00
|
|
|
|
self._close_inference_channel(item)
|
2026-08-30 22:22:11 +08:00
|
|
|
|
else:
|
|
|
|
|
|
pipe = item.get("pipeline")
|
|
|
|
|
|
if pipe:
|
|
|
|
|
|
try:
|
|
|
|
|
|
pipe.stop()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
def list_running(self):
|
|
|
|
|
|
with self._lock:
|
|
|
|
|
|
alive = []
|
|
|
|
|
|
for sid, item in list(self._pipelines.items()):
|
2026-09-04 18:16:14 +08:00
|
|
|
|
if self._is_pipeline_alive(item) and not item.get("preview_only"):
|
2026-08-30 22:22:11 +08:00
|
|
|
|
alive.append(sid)
|
2026-09-04 18:16:14 +08:00
|
|
|
|
elif not self._is_pipeline_alive(item):
|
2026-08-30 22:22:11 +08:00
|
|
|
|
self._purge_pipeline(sid)
|
|
|
|
|
|
return alive
|
|
|
|
|
|
|
|
|
|
|
|
def _enrich_pipeline_status(self, stream_id, info):
|
|
|
|
|
|
"""补充流健康状态与摄像头名称(不自动启停分析)。"""
|
|
|
|
|
|
if not info:
|
|
|
|
|
|
return info
|
|
|
|
|
|
try:
|
|
|
|
|
|
from app.models import StreamModel
|
|
|
|
|
|
s = StreamModel.objects.filter(id=stream_id).first()
|
|
|
|
|
|
if s:
|
|
|
|
|
|
info["stream_name"] = s.nickname or s.name or ("#%s" % stream_id)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
health = info.get("stream_health") or "ok"
|
|
|
|
|
|
stalled = float(info.get("stalled_sec") or 0)
|
|
|
|
|
|
fps = float(info.get("analysis_fps") or 0)
|
|
|
|
|
|
if health == "ok" and fps <= 0 and stalled >= 20:
|
|
|
|
|
|
info["stream_health"] = "stalled"
|
|
|
|
|
|
health = "stalled"
|
2026-09-04 18:16:14 +08:00
|
|
|
|
info["healthy"] = health == "ok" and (
|
|
|
|
|
|
info.get("analysis_health") == "running" if "analysis_health" in info else fps > 0.05)
|
2026-08-30 22:22:11 +08:00
|
|
|
|
if not info.get("active_zone_ids") and self.is_running(stream_id):
|
|
|
|
|
|
try:
|
|
|
|
|
|
zones = self._load_zones(stream_id)
|
|
|
|
|
|
info["active_zone_ids"] = sorted([int(z["id"]) for z in zones if z.get("id") is not None])
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
info["active_zone_ids"] = []
|
|
|
|
|
|
return info
|
|
|
|
|
|
|
|
|
|
|
|
def get_pipeline_info(self, stream_id):
|
|
|
|
|
|
with self._lock:
|
|
|
|
|
|
item = self._pipelines.get(stream_id)
|
|
|
|
|
|
if not item:
|
|
|
|
|
|
return None
|
|
|
|
|
|
if item.get("mode") == "process":
|
|
|
|
|
|
handle = item.get("handle")
|
|
|
|
|
|
if handle:
|
|
|
|
|
|
st = handle.status()
|
|
|
|
|
|
if st:
|
|
|
|
|
|
return self._enrich_pipeline_status(stream_id, st)
|
|
|
|
|
|
alive = self.is_running(stream_id)
|
|
|
|
|
|
return self._enrich_pipeline_status(stream_id, {
|
|
|
|
|
|
"stream_id": stream_id,
|
|
|
|
|
|
"running": alive,
|
|
|
|
|
|
"stream_health": "connecting" if alive else "stopped",
|
|
|
|
|
|
"analysis_fps": 0.0,
|
|
|
|
|
|
"stalled_sec": 0,
|
|
|
|
|
|
})
|
|
|
|
|
|
pipe = item.get("pipeline")
|
|
|
|
|
|
if not pipe:
|
|
|
|
|
|
return None
|
|
|
|
|
|
try:
|
|
|
|
|
|
return self._enrich_pipeline_status(stream_id, pipe.status())
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning("get_pipeline_info err: %s" % str(e))
|
|
|
|
|
|
return self._enrich_pipeline_status(stream_id, {
|
|
|
|
|
|
"stream_id": stream_id,
|
|
|
|
|
|
"running": False,
|
|
|
|
|
|
"stream_health": "stalled",
|
|
|
|
|
|
"analysis_fps": 0.0,
|
|
|
|
|
|
"stalled_sec": 0,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
def reload_zones(self, stream_id):
|
|
|
|
|
|
with self._lock:
|
|
|
|
|
|
item = self._pipelines.get(stream_id)
|
|
|
|
|
|
if not item:
|
|
|
|
|
|
return False
|
|
|
|
|
|
zones = self._load_zones(stream_id)
|
|
|
|
|
|
analyze_fps = self._compute_analyze_fps(stream_id, fallback=self._target_fps)
|
|
|
|
|
|
new_small_ids = sorted({sid for z in zones for sid in z.get("small_model_ids", []) if sid})
|
|
|
|
|
|
cur_algo_ids = sorted(item.get("algorithm_ids") or [])
|
|
|
|
|
|
if item.get("mode") == "process":
|
|
|
|
|
|
if new_small_ids != cur_algo_ids:
|
|
|
|
|
|
handle = item.get("handle")
|
|
|
|
|
|
if handle:
|
|
|
|
|
|
handle.stop()
|
|
|
|
|
|
eq = item.get("event_queue")
|
|
|
|
|
|
if eq:
|
|
|
|
|
|
get_event_bridge().unregister_queue(eq)
|
|
|
|
|
|
self._pipelines.pop(stream_id, None)
|
|
|
|
|
|
try:
|
|
|
|
|
|
from app.models import StreamModel as _SM
|
|
|
|
|
|
s = _SM.objects.get(id=stream_id)
|
|
|
|
|
|
self.start(s)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning("reload_zones 重启失败 stream=%s: %s" % (stream_id, str(e)))
|
|
|
|
|
|
else:
|
|
|
|
|
|
handle = item.get("handle")
|
|
|
|
|
|
if handle:
|
|
|
|
|
|
handle.reload_zones(zones, analyze_fps=analyze_fps)
|
|
|
|
|
|
return True
|
|
|
|
|
|
pipe = item.get("pipeline")
|
|
|
|
|
|
if not pipe:
|
|
|
|
|
|
return False
|
|
|
|
|
|
if hasattr(pipe, "set_zone_polygons"):
|
|
|
|
|
|
pipe.set_zone_polygons(zones)
|
|
|
|
|
|
else:
|
|
|
|
|
|
pipe.zone_polygons = zones
|
|
|
|
|
|
pipe.set_analyze_fps(analyze_fps)
|
|
|
|
|
|
pipe.reset_zone_runtime_state()
|
|
|
|
|
|
if new_small_ids != cur_algo_ids:
|
|
|
|
|
|
pipe.stop()
|
|
|
|
|
|
item["running"] = False
|
|
|
|
|
|
try:
|
|
|
|
|
|
item["thread"].join(timeout=3)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
self._pipelines.pop(stream_id, None)
|
|
|
|
|
|
try:
|
|
|
|
|
|
from app.models import StreamModel as _SM
|
|
|
|
|
|
s = _SM.objects.get(id=stream_id)
|
|
|
|
|
|
self.start(s)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning("reload_zones 重启失败 stream=%s: %s" % (stream_id, str(e)))
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
def _on_event(self, event):
|
|
|
|
|
|
try:
|
|
|
|
|
|
from app.services.alarm_service import write_alarm, ALARM_EVENT_TYPES
|
|
|
|
|
|
etype = event.get("type", "")
|
|
|
|
|
|
if etype in ALARM_EVENT_TYPES:
|
|
|
|
|
|
write_alarm(event)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.exception("事件处理失败: %s ev=%s" % (str(e), str(event)[:200]))
|
|
|
|
|
|
|
|
|
|
|
|
def _on_track_snapshot(self, stream_id, frame_index, active, has_motion):
|
|
|
|
|
|
# 已停用:不再写追踪快照
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _severity_for(event):
|
|
|
|
|
|
t = event.get("type")
|
|
|
|
|
|
if t in ("loiter", "cross_camera"):
|
|
|
|
|
|
return 1
|
|
|
|
|
|
if t in ("entered_zone", "left_zone", "object_start"):
|
|
|
|
|
|
return 2
|
|
|
|
|
|
return 3
|
2026-09-04 18:16:14 +08:00
|
|
|
|
|
|
|
|
|
|
def shutdown_analysis():
|
|
|
|
|
|
"""Stop existing resources without constructing a new pool."""
|
|
|
|
|
|
instance = AnalysisManager._instance
|
|
|
|
|
|
if instance and getattr(instance, '_initialized', False):
|
|
|
|
|
|
for sid in list(instance._pipelines):
|
|
|
|
|
|
instance.stop(sid)
|
|
|
|
|
|
instance._infer_forwarder_running = False
|
|
|
|
|
|
instance._infer_forwarder.join(timeout=3)
|
|
|
|
|
|
from app.analysis.inference_pool import shutdown_inference_pool
|
|
|
|
|
|
shutdown_inference_pool()
|
|
|
|
|
|
instance._status_manager.shutdown()
|
|
|
|
|
|
instance._initialized = False
|
|
|
|
|
|
AnalysisManager._instance = None
|