"""全局分析管理器(单例) 阶段2:每路摄像头在独立子进程中运行 CameraPipeline;YOLO 推理可选走 主进程 InferenceProcessPool(共享 GPU/模型内存)。事件经 Queue → EventBridge 写库。 """ import json import logging import multiprocessing as mp import threading import time import uuid 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 def _algorithm_to_spec(a, target_labels=None): 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"), "target_labels": sorted(set(target_labels or [])), } 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) # 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() 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") channel = msg.get("response_channel") with self._infer_routes_lock: if channel not in self._infer_routes: continue # The originating pipeline has already stopped. 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: self._send_inference_response(channel, {"req_id": req_id, "ok": True, "detections": []}) continue except Exception: pass # 直接透传 JPEG bytes 给推理池,避免主进程 imdecode + imencode 双重编解码, # 消除主进程 GIL 占用(解码在 worker 子进程内完成)。 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}) except Exception as e: logger.warning("推理转发失败: %s", e) try: self._send_inference_response(channel, {"req_id": req_id, "ok": False, "error": str(e)}) except Exception: pass 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() 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 def _load_zones(stream_id, active_only=True, zone_id=None): try: from app.models import ZoneModel 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( '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, "alarm_repeat_sec": max(0.0, float(getattr(z, "alarm_repeat_sec", 30) or 0)), "color": z.color or "#169F85", "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 def _start_process(self, stream, url, zones, algos, detectors_legacy=None, preview_only=False, analyze_fps_override=None): sid = stream.id self._status_dict.pop(str(sid), None) 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) 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)) storage_alarm_dir, static_dir = _snapshot_storage_paths() 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 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, "response_channel": response_channel, "preview_only": bool(preview_only), } proc = self._mp_ctx.Process( target=pipeline_process_main, args=(config, event_queue, cmd_queue, self._status_dict, self._infer_req_q, infer_resp_q), name="pipeline-%s" % sid, daemon=True, ) try: proc.start() except Exception: self._close_inference_channel({"response_channel": response_channel}) bridge.unregister_queue(event_queue) raise 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]), "preview_only": bool(preview_only), "response_channel": response_channel, "analyze_fps": analyze_fps, } return True, "started (process)" def _start_thread(self, stream, url, zones, algos, preview_only=False, analyze_fps_override=None): sid = stream.id detectors = [] algo_names = [] for a in algos: 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)) if eng: detectors.append({"algorithm_id": a.id, "algorithm_name": a.name, "engine": eng, "target_labels": sorted(labels)}) 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() analyze_fps = (float(analyze_fps_override) if analyze_fps_override is not None else self._compute_analyze_fps(sid, fallback=self._target_fps)) 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, on_preview=lambda payload: get_event_bridge()._on_preview(payload), alarm_enabled=not bool(preview_only), 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]), "preview_only": bool(preview_only), "analyze_fps": analyze_fps, } t.start() return True, "started (thread)" def start(self, stream): from monitor_runtime.licensing import require_license require_license() 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: if not item.get("preview_only"): return True, "already running" # 正式启动接管由预览创建的临时管线。 self.stop(sid) item = None # 僵尸条目:进程/线程已退出但未清理 try: if item.get("mode") == "process": self._close_inference_channel(item) 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 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" 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() self._close_inference_channel(item) 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 self._close_inference_channel(item) 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()): if self._is_pipeline_alive(item) and not item.get("preview_only"): alive.append(sid) elif not self._is_pipeline_alive(item): 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" info["healthy"] = health == "ok" and ( info.get("analysis_health") == "running" if "analysis_health" in info else fps > 0.05) 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 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