video_monitor/app/analysis/manager.py
2026-09-04 18:16:14 +08:00

950 lines
39 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""全局分析管理器(单例)
阶段2每路摄像头在独立子进程中运行 CameraPipelineYOLO 推理可选走
主进程 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