RFP Reference: LTS-2026-001 | Issue Date: May 14, 2026 | Repository: github.com/melchi45/loitering_tracking
- Project Overview — Quick Commands · ICE Candidate Test
- System Architecture
- Technology Stack
- IP Camera Discovery (UDP Broadcast)
- RTSP Video Ingestion & Frame Capture
- AI Models & Inference Pipeline
- Per-Channel AI Module Selection — GPU Provider Setup
- Loitering Detection Logic
- React Web UI
- Submodules
- Technical Requirements
- Functional Requirements
- Non-Functional Requirements
- Project Milestones & Deliverables
- Getting Started — Running · MCP Server · MongoDB Setup
- HTTPS / TLS Configuration — Full Guide
- API Reference
- Appendix
- Documentation Index (RFP / PRD)
- Development Process (SDLC Gates)
An AI-powered Loitering Detection and Tracking System built on Node.js + React. The system ingests RTSP video streams from WiseNet/ONVIF IP cameras, performs real-time person detection and tracking using AI models, and delivers bounding-box-annotated live video to a React web UI with loitering behavior alerts.
Traditional CCTV monitoring is reactive and prone to human error. This system automates surveillance by:
- Discovering IP cameras on the network via UDP broadcast (ported from WiseNetChromeIPInstaller)
- Connecting to cameras over RTSP and capturing frames at 10 FPS
- Running on-server AI inference to detect persons and assign persistent object IDs
- Streaming annotated frames (bounding boxes + track IDs + confidence) to a React web UI in real-time
| Component | Description |
|---|---|
| UDP Camera Discovery | Node.js dgram port of Chrome UDP broadcast (send: 7701, recv: 7711) |
| RTSP Ingestion | FFmpeg/fluent-ffmpeg capture at 10 FPS per channel |
| AI Pipeline | YOLOv8n (ONNX) detection + ByteTrack MOT on Node.js server |
| WebSocket Streaming | Annotated JPEG frames pushed to React via Socket.IO |
| React Dashboard | Live multi-camera grid, bounding boxes, loitering alerts |
| Zone Management | Polygon-based inclusion/exclusion zones drawn on canvas |
| Alert Service | In-app + webhook notifications |
# Development mode (auto-restart on file changes — requires nodemon)
cd server && npm run dev
# Production mode
cd server && npm start
# Stop server (kills process on port 3080)
cd server && npm stop
# Restart server (stop → start)
cd server && npm run restart
# Stop: Ctrl+C (when running in foreground)In background execution environments, use
npm stoporfuser -k 3080/tcp.If nodemon is not available, run the server in the background with:
cd server && nohup npm start > /tmp/server.log 2>&1 &
# ── npm 스크립트 (권장) — server/ 또는 루트 workspace에서 실행 가능 ──────────────
# HTTP 백그라운드 서버 시작 (MCP_PORT, 기본 3002)
npm run mcp:start
# or: cd server && npm run mcp:start
# HTTP 백그라운드 서버 종료
npm run mcp:stop
# or: cd server && npm run mcp:stop
# HTTP 백그라운드 서버 재시작 (stop → start)
npm run mcp:restart
# or: cd server && npm run mcp:restart
# ── 직접 실행 (stdio / 개발용) ────────────────────────────────────────────────
# stdio mode — Claude Code / Claude API (기본)
cd mcp-server && npm start
# or: node mcp-server/index.js
# Development mode (auto-restart on file changes)
cd mcp-server && npm run dev
# HTTP/SSE mode — OpenAI Agents / ChatGPT Actions (port 3002)
cd mcp-server && npm run start:http
# or: TRANSPORT=http node mcp-server/index.js
# Custom port / auth token
MCP_PORT=3002 MCP_AUTH_TOKEN=my-secret-token TRANSPORT=http node mcp-server/index.js
# Background execution (수동)
cd mcp-server && nohup npm start > /tmp/lts-mcp.log 2>&1 &
# Stop (stdio foreground): Ctrl+C
# Stop (background):
kill $(lsof -ti tcp:3002)
# or:
fuser -k 3002/tcpHTTP/SSE Health Check:
GET http://localhost:3002/health→{ "status": "ok" }
Environment Variables:
Variable Default Description LTS_BASE_URLhttp://localhost:3080LTS REST API base URL TRANSPORTstdioTransport mode: stdioorhttpMCP_PORT3002HTTP/SSE listen port MCP_AUTH_TOKEN(empty) Bearer token for HTTP transport (optional)
ingest-daemon(Python PyAV)은 RTSP 수집의 유일한 공급자입니다. LTS 서버와 별도로 관리할 수 있습니다.
# ── npm 스크립트 — server/ 또는 루트 workspace에서 실행 가능 ──────────────────
# daemon 시작 (이미 실행 중이면 no-op; 시작 후 카메라 자동 재등록)
npm run ingest:start
# or: cd server && npm run ingest:start
# daemon 종료 (실행 중이 아니면 오류 없이 종료)
npm run ingest:stop
# or: cd server && npm run ingest:stop
# daemon 재시작 (종료 → 시작 + 카메라 재등록)
npm run ingest:restart
# or: cd server && npm run ingest:restart동작 설명:
ingest:start:/health체크 → 이미 실행 중이면 no-op → Python PyAV 데몬 백그라운드 시작 → 최대 10초 대기 → 카메라 자동 재등록ingest:stop:/health체크 →fuser -k 7070/tcp+pkill -f ingest_daemon.py→ 포트 해제 확인 (3초)ingest:restart:ingest:stop→ingest:start순서 실행환경 변수 (
server/.env):
Variable Default Description INGEST_DAEMON_ADDR:7070daemon 수신 주소 INGEST_DAEMON_URLhttp://127.0.0.1:7070daemon REST URL INGEST_DAEMON_BIN../ingest-daemon/ingest_daemon.pydaemon 스크립트 경로 INGEST_DAEMON_LOG/tmp/ingest-daemon.logdaemon 로그 파일 경로 PYAV_PYTHON_BINpython3Python 실행 경로 로그 확인:
tail -f /tmp/ingest-daemon.log
# Development mode (Vite dev server — http://localhost:3080)
cd client && npm run dev
# Production build (creates dist/ folder)
cd client && npm run build
# Stop: Ctrl+CChecks the camera pipeline, WebRTC ICE settings, AI modules, and mediasoup UDP ports in one step.
# Local server check (HTTPS default: https://localhost:3443)
cd server && npm run health
# Override target (HTTP or HTTPS)
node server/src/scripts/healthCheck.js https://192.168.214.3:3443
# Or HTTP when HTTPS_ENABLED=false:
node server/src/scripts/healthCheck.js http://192.168.214.3:3001Output example:
=== LTS Server Health Check ===
Target: https://localhost:3443
[1] Cameras & Pipeline Status
✓ 1 camera(s) in database
· TID-A800 (80d658eb) — running — WebRTC ON
[2] WebRTC ICE Config
✓ 4 STUN server(s)
✓ 2 TURN server(s)
[3] AI Capabilities
✓ 30 AI module(s) — loaded:6 builtin:3 available:19 pending:2 missing:0
[4] mediasoup UDP Ports (40000-49999)
✓ 9 UDP port(s) open: 40490, 41688, …
=== Result ===
All checks passed.
Retrieves the ICE server list sent by the server to the browser.
# Retrieve ICE config JSON (reflects STUN/TURN values from server/.env)
node -e "
const https = require('https');
https.get('https://localhost:3443/api/webrtc/ice-config', { rejectUnauthorized: false }, res => {
let b = '';
res.on('data', c => b += c);
res.on('end', () => console.log(JSON.stringify(JSON.parse(b), null, 2)));
});
"# Check coturn service status
systemctl status coturn
# Verify TURN port (3478) UDP response — local
node -e "
const dgram = require('dgram');
const s = dgram.createSocket('udp4');
// STUN Binding Request (20 bytes)
const req = Buffer.from('000100002112a44200000000000000000000000000000000', 'hex');
s.send(req, 3478, '127.0.0.1', err => {
if (err) { console.error('Send failed:', err.message); s.close(); return; }
console.log('STUN request sent to 127.0.0.1:3478');
});
s.on('message', msg => {
console.log('STUN response received (' + msg.length + ' bytes) — TURN reachable');
s.close();
});
s.on('error', err => { console.error('UDP error:', err.message); });
setTimeout(() => { console.warn('Timeout — no response (TURN may be down or blocked)'); s.close(); }, 3000);
"
# Check mediasoup WebRTC UDP port range (listening)
ss -u -l -n | awk -F: '{print $NF}' | sort -n | awk '$1>=40000 && $1<=49999'This describes how to check which path (LAN direct / STUN / TURN) the WebRTC connection is using.
Automatically launches a browser and outputs ICE connection path, traffic, and event logs.
# Run while server + Web UI are running
cd server && npm run ice-test
# Specify remote server
node src/scripts/iceTest.js http://192.168.214.3:3080 http://192.168.214.3:3080
# Without browser window (CI/headless environment)
npm run ice-test:headlessOperation sequence:
Phase 1 — Server pre-check
✓ 1 camera(s) (WebRTC enabled: 1)
· TID-A800 (80d658eb) — running
✓ STUN 4 TURN 2
✓ STUN UDP ping → 192.168.214.3:3478 responded
Phase 2 — Browser automation
· Using system Chrome: /usr/bin/google-chrome
· Mode: browser window visible (headed)
✓ Chromium started
✓ Page loaded: http://localhost:3080
· Waiting for ICE connection… (up to 35 seconds)
· [browser] [useWebRTC][80d658eb] connection state: connected
✓ ICE connection successful (connectionState = connected)
· ICE event log:
PC#1 iceGatheringState→gathering
PC#1 candidate: host udp 192.168.214.3:42351
PC#1 iceGatheringState→complete
PC#1 connectionState→connected
✓ Screenshot saved: /tmp/lts-ice-test-ok.png
Phase 3 — ICE Candidate report
Local candidate: [host] UDP 192.168.214.3:42351
└ Direct LAN — optimal path
Remote candidate: [host] 192.168.214.32:56712
Traffic:
↑ Sent total : 12.3 KB
↓ Received total : 45.8 MB
↓ Receive speed : ~3200 kbps (5 × 2s)
=== Final result ===
PASS — ICE connection verified, video data flow normal
Screenshot on failure: /tmp/lts-ice-test-fail.png
Screenshot on success: /tmp/lts-ice-test-ok.png
While the camera is connected via WebRTC, click the ICE button in the top-right corner.
┌─────────────────────────────────────────────────┐
│ [Camera Name] live WebRTC [ICE] + Zone │
│ ┌──────────────────────┐│
│ │ ─ local ││
│ │ [host] UDP ││
│ │ 192.168.214.3:42351 ││
│ (live video) │ host (LAN) ││
│ │ ─ remote ││
│ │ [host] 192.168.214.32 ││
│ │ :51234 ││
│ │ ↑ 1.2 MB ↓ 45.3 MB ││
│ └──────────────────────┘│
└─────────────────────────────────────────────────┘
ICE Button Color Meaning (local candidate type):
| Indicator | Type | Meaning | Status |
|---|---|---|---|
[host] |
host | Direct LAN connection (optimal) | 🟢 |
[srflx] |
srflx | Public IP obtained via STUN, NAT traversal | 🟡 |
[relay] |
relay | Via TURN relay (firewall/internet environment) | 🟠 |
Panel refresh interval:
RTCPeerConnection.getStats()called every 3 seconds — byte counter allows verification of actual data flow
Open in a new tab at any time during connection to view the full ICE negotiation process and stats.
Chrome / Edge:
chrome://webrtc-internals
Firefox:
about:webrtc
Check points:
- ICE candidate grid — List of all collected candidates (host / srflx / relay)
- candidatepairchanged — Selected pair change history
- Selected Candidate Pair — Currently active local↔remote path
- inbound-rtp — Received packet count / loss rate (jitter, packetsLost)
- connection-state → Whether
connectedis maintained
Directly verify ICE-related settings and ports from the server.
# ① Retrieve STUN/TURN list sent by the server to the browser
node -e "
const https = require('https');
https.get('https://localhost:3443/api/webrtc/ice-config', { rejectUnauthorized: false }, res => {
let b = '';
res.on('data', c => b += c);
res.on('end', () => console.log(JSON.stringify(JSON.parse(b), null, 2)));
});
"
# ② Verify coturn UDP 3478 port response via STUN Binding Request
node -e "
const dgram = require('dgram');
const HOST = process.argv[1] || '127.0.0.1';
const s = dgram.createSocket('udp4');
const req = Buffer.from('000100002112a44200000000000000000000000000000000', 'hex');
s.send(req, 3478, HOST, err => {
if (err) { console.error('Send failed:', err.message); s.close(); return; }
console.log('STUN Binding Request → ' + HOST + ':3478');
});
s.on('message', msg => {
console.log('✓ Response ' + msg.length + ' bytes — TURN/STUN reachable');
s.close();
});
setTimeout(() => { console.warn('✗ Timeout — no response'); s.close(); }, 3000);
" -- 127.0.0.1
# ③ Check mediasoup WebRTC UDP ports (created after browser connects)
ss -u -l -n | awk -F: '{print $NF}' | sort -n | awk '$1>=40000 && $1<=49999'
# ④ Full health check (cameras + ICE config + AI + mediasoup ports)
cd server && npm run healthConnection failed / Video frozen
│
▼
[Web UI ICE Panel] Open
│
├─ local: relay → TURN relay path in use
│ └─ Check coturn config: systemctl status coturn
│ Check if allowed-peer-ip=192.168.214.3 addition needed
│
├─ local: srflx → Via STUN public IP
│ └─ Hairpin NAT may be the problem
│ Check server/.env: SERVER_IP=<LAN IP> setting
│
├─ local: host → Direct LAN, but failed
│ └─ Check mediasoup UDP port firewall
│ sudo ufw allow 40000:49999/udp
│
└─ Panel does not appear (no ICE button)
└─ WebRTC connection itself failed → check server status with npm run health
아래 다이어그램은
server/src/,client/src/코드와docs/design/문서를 기반으로 작성되었습니다.
graph TB
subgraph SRC["비디오 소스"]
IPCAM["IP 카메라\nRTSP / ONVIF"]
YTLIVE["YouTube Live\nyt-dlp"]
end
subgraph SS["스트리밍 서버 SERVER_MODE=streaming | combined"]
MTX["MediaMTX\n:8554 RTSP :8889 WHEP\n:8189 ICE UDP :9997 API"]
INGEST["ingest-daemon\nPython PyAV :7070"]
NODESTR["Node.js :3443 HTTPS / :3080 HTTP\nExpress · Socket.IO · REST API · WHEP Proxy"]
end
subgraph AS["AI 분석 서버 SERVER_MODE=analysis | combined"]
AIPIPE["AI Pipeline\nYOLOv8 / YOLO11 / YOLO12 ONNX\n→ ByteTrack + KalmanFilter\n→ BehaviorEngine · AttributePipeline · FireSmokeService"]
end
subgraph DBLAYER["데이터베이스"]
JSONF[("lts.json\nJSON File DB")]
MONGOSRV[("MongoDB\nAtlas / Standalone")]
end
subgraph AUTHLAYER["인증 / 권한"]
JWTR["JWT RS256 TokenService\nRBAC admin · operator · viewer\nAuditService"]
end
subgraph MCPB["MCP 서버 선택"]
MCPS["mcp-server :3100\nstdio | HTTP+SSE\n10 MCP Tools Claude / LLM 연동"]
end
subgraph UI["Web UI React 18 + TypeScript + Tailwind + Zustand"]
AUTHUI["Sign-in / Register\nPending Approval"]
STREAMD["Streaming Dashboard\nCameraGrid · 실시간 WebRTC 영상"]
ANALYSD["Analysis Dashboard\n메트릭 · 감지 이벤트"]
ADMND["Admin Dashboard\nUsers · AI Models · ONVIF · Audit"]
STATS["Statistics\n분석 통계 · 타임라인"]
end
IPCAM -- "RTSP/ONVIF" --> MTX
YTLIVE -- "yt-dlp → FFmpeg\nRTSP re-publish" --> MTX
MTX -- "RTSP loopback :8554" --> INGEST
INGEST -- "JPEG 10FPS\nPOST /api/internal/frame" --> NODESTR
INGEST -- "H.264 / Opus RTP\nmediasoup 모드" --> NODESTR
NODESTR -- "POST JPEG + metadata\nstreaming 모드 한정" --> AIPIPE
AIPIPE -- "JSON results\ndetections · tracks · alerts" --> NODESTR
NODESTR <--> AUTHLAYER
NODESTR -- "read / write" --> JSONF
JSONF -. "write-through\nDB_TYPE=mongodb" .-> MONGOSRV
NODESTR -- "HTTPS REST · Socket.IO" --> UI
MTX -- "ICE UDP :8189\nWebRTC 미디어" --> STREAMD
MCPS -- "HTTP REST" --> NODESTR
ingest-daemon(Python PyAV)은 RTSP 수집의 유일한 공급자로 단일 RTSP 세션에서 최대 4개 경로로 데이터를 공급합니다.
graph TB
subgraph SRC2["비디오 소스"]
CAM2["IP 카메라 RTSP / ONVIF"]
YT2["YouTube Live"]
end
subgraph MTX2["MediaMTX :8554 RTSP Loopback"]
PATH_CAM["카메라 경로\nrtsp://127.0.0.1:8554/{cameraId}"]
PATH_YT["YouTube 경로\nrtsp://127.0.0.1:8554/yt/{id}"]
end
subgraph DAEMON2["ingest-daemon Python PyAV :7070"]
PYAV["PyAV RTSP 디코더\n재연결 지수 백오프 최대 5s"]
end
subgraph FANOUT["4-way 팬아웃 단일 RTSP 세션"]
OUT1["① JPEG 10FPS\nAI 추론용"]
OUT2["② H.264 RTP\nWebRTC 비디오"]
OUT3["③ Opus RTP\nWebRTC 오디오"]
OUT4["④ App RTP\nONVIF 메타데이터"]
end
subgraph DEST2["Node.js 수신처"]
FRAPI["POST /api/internal/frame/:id\n→ PipelineManager AI 추론"]
VDRTP["UDP mediasoupVideoPort\n→ mediasoup PlainTransport 비디오 Producer"]
ADRTP["UDP mediasoupAudioPort\n→ mediasoup PlainTransport 오디오 Producer"]
ARAPI["POST /api/internal/apprtp/:id\n→ Socket.IO appRtp 이벤트"]
end
CAM2 -- "RTSP" --> PATH_CAM
YT2 -- "yt-dlp → FFmpeg\nRTSP re-publish" --> PATH_YT
PATH_CAM -- "RTSP loopback" --> PYAV
PATH_YT -- "RTSP loopback" --> PYAV
PYAV --> OUT1
PYAV --> OUT2
PYAV --> OUT3
PYAV --> OUT4
OUT1 --> FRAPI
OUT2 --> VDRTP
OUT3 --> ADRTP
OUT4 --> ARAPI
graph LR
JPEGI["JPEG 프레임\n10 FPS"]
subgraph DETLAYER["감지 레이어"]
YOLOCAT["YOLO 모델 카탈로그 런타임 전환\nYOLOv8 n/s/m/l/x\nYOLO11 n/s/m/l/x\nYOLO12 n/s/m/l/x\nONNX Runtime 선택적 CUDA 가속"]
end
subgraph TRKLAYER["추적 레이어"]
BYTETEK["ByteTrack\n칼만 필터\n다중 객체 ID 유지"]
end
subgraph BEHAVLAYER["행동 분석 레이어"]
ZONEMGR["ZoneManager\n다각형 구역\n진입/이탈 감지"]
BHENG["BehaviorEngine\n체류 시간 · 이동 패턴\n배회 위험 점수 0~100"]
ALRTSVC["AlertService\n임계값 초과 → 알림 발생\n에스컬레이션 · 웹훅"]
end
subgraph ATTRLAYER["속성 분석 레이어 퍼채널 선택 활성화"]
FACESVC["얼굴 인식\nSCRFD-2.5G + ArcFace ResNet50\n512-dim 임베딩 크로스 카메라 Re-ID"]
CLOTHSVC["의상 분석\nOpenPAR\n색상 · 유형 분류"]
PPESVC["PPE 감지\nYOLOv8m PPE\n안전모 · 마스크 감지"]
FIRESVC["화재 · 연기 감지\nYOLOv8s FireSmoke\n30초 쿨다운"]
end
subgraph OUTLAYER["출력"]
SOCKOUT["Socket.IO 이벤트\nframeData · objectTracked\nnewAlert · face:reidentified\nonvif:event"]
SNAPOUT["스냅샷 저장\nbbox 크롭 이미지\nBase64 JPEG"]
DBOUT["DB 저장\nalerts · analysisEvents\ndetectionTracks · faceGalleries"]
end
JPEGI --> YOLOCAT --> BYTETEK
BYTETEK --> ZONEMGR --> BHENG --> ALRTSVC
BYTETEK --> FACESVC
BYTETEK --> CLOTHSVC
BYTETEK --> PPESVC
BYTETEK --> FIRESVC
ALRTSVC --> SOCKOUT
ALRTSVC --> DBOUT
FACESVC --> SOCKOUT
CLOTHSVC --> SOCKOUT
YOLOCAT --> SNAPOUT
WEBRTC_ENGINE 환경변수로 WebRTC 전달 백엔드를 선택합니다. 세 경로는 동시에 공존할 수 있습니다.
graph TB
CAM4["IP 카메라 RTSP"]
MTX4["MediaMTX\n:8554 RTSP :8889 WHEP :8189 ICE UDP"]
INGEST4["ingest-daemon PyAV"]
CAM4 --> MTX4
MTX4 -- "RTSP loopback :8554" --> INGEST4
subgraph MODEMTX["WEBRTC_ENGINE=mediamtx 기본값 외부 MediaMTX WHEP"]
WHEPPRXY["Node.js WHEP 프록시\nPOST /api/webrtc/whep/:id\n→ MediaMTX :8889"]
BRMTX["Browser video\nH.264 + Opus SRTP\nICE UDP :8189 직접 연결"]
end
subgraph MODEMS["WEBRTC_ENGINE=mediasoup Node.js 내장 SFU DataChannel 지원"]
MSROUTER["mediasoup Router\nPlainTransport H.264 RTP → videoProducer\nPlainTransport Opus RTP → audioProducer\nWebRtcTransport → videoConsumer + audioConsumer"]
DATACHAN["DataConsumer\nSCTP DataChannel\nONVIF 메타데이터 실시간"]
BRMS["Browser video + audio\nH.264 + Opus SRTP\n+ DataChannel"]
end
subgraph MODEJPEG["JPEG 폴백 항상 사용 가능 WEBRTC_ENGINE 무관"]
AIANN["AI 어노테이션\nbbox 오버레이 렌더링"]
BRJPEG["Browser img\nJPEG 10FPS\nSocket.IO frameData 이벤트"]
end
MTX4 -- "WHEP SDP 협상" --> WHEPPRXY
WHEPPRXY --> BRMTX
INGEST4 -- "H.264 RTP\nOpus RTP" --> MSROUTER
MSROUTER --> DATACHAN --> BRMS
INGEST4 -- "JPEG 10FPS" --> AIANN
AIANN -- "Socket.IO frameData" --> BRJPEG
flowchart TD
REG["회원가입\nPOST /auth/register\nbcrypt 해싱 저장"]
PEND["대기 상태 pending\nPendingPage 표시"]
APPDEC{"관리자 승인 / 거절\nPATCH /admin/users/:id"}
ACTIVE["활성 계정 active"]
REJECTED["거절됨 rejected"]
LOGIN["로그인\nPOST /auth/login\nbcrypt 검증"]
TOKEN["JWT 발급\naccess token 15분\nrefresh token cookie 7일 HttpOnly"]
ROLECHECK{"역할 role 확인"}
ADMINDOOR["admin 역할\nStreaming Dashboard\nAdmin Dashboard\nAnalysis Dashboard"]
OPVIEWDOOR["operator / viewer\nAccessDeniedPage"]
REFRESH["POST /auth/refresh\n토큰 로테이션\nrefresh_tokens 테이블 검증"]
OAUTH["[예정] Google OAuth 2.0\nMicrosoft Entra ID MSAL\npassport 전략 추가 계획"]
REG --> PEND
PEND --> APPDEC
APPDEC -- "approve" --> ACTIVE
APPDEC -- "reject" --> REJECTED
ACTIVE --> LOGIN
LOGIN --> TOKEN
TOKEN --> ROLECHECK
ROLECHECK -- "admin" --> ADMINDOOR
ROLECHECK -- "operator / viewer" --> OPVIEWDOOR
TOKEN -. "15분 후 만료" .-> REFRESH
REFRESH --> TOKEN
OAUTH -.-> LOGIN
DB_TYPE 환경변수로 스토리지 백엔드를 선택합니다. db.js 추상화 레이어가 애플리케이션을 스토리지 구현으로부터 격리합니다.
graph TB
APP2["애플리케이션 레이어\n라우트 핸들러 · 서비스 · PipelineManager"]
subgraph DBJS["db.js in-memory 추상화"]
MEMSTORE["인메모리 스토어 20 테이블\ncameras · zones · alerts · events\nusers · refresh_tokens · audit_logs\ndetectionSnapshots · analysisEvents\ndetectionTracks · onvif_events\nfaceGalleries · settings · ..."]
DISPATCH2["afterWrite()\nDB_TYPE에 따라 경로 분기"]
end
subgraph JSONPATH["DB_TYPE=json 기본값"]
DEBOUNCE["persistJson()\n디바운스 500ms 비동기"]
JSONFILE[("storage/lts.json\nface_tracking.json\nanalytics.json")]
end
subgraph MONGOPATH["DB_TYPE=mongodb"]
MONGUPSERT["mongoSvc.upsert / remove\nfire-and-forget 비동기"]
MONGOCOL[("MongoDB Atlas / Standalone\n:27017\n20 컬렉션")]
JFALLBACK["JSON 폴백\nMongoDB 연결 장애 시만 기록\nhigh-volume 테이블 제외"]
end
subgraph STARTUP["시작 시 로딩"]
ENSUREMON["ensureMongodb.js\nTCP probe → 자동 재시작 → 설치 안내"]
MIGRATE["migrateToMongo.js\n일회성 JSON → MongoDB 마이그레이션"]
end
APP2 --> MEMSTORE
MEMSTORE --> DISPATCH2
DISPATCH2 -- "DB_TYPE=json" --> DEBOUNCE --> JSONFILE
DISPATCH2 -- "DB_TYPE=mongodb" --> MONGUPSERT --> MONGOCOL
MONGUPSERT -. "disconnect fallback" .-> JFALLBACK
ENSUREMON --> MONGOCOL
MIGRATE --> MONGOCOL
graph TB
subgraph AUTHPAGES["인증 / 접근 제어"]
SIGNINPG["SignInPage\nlogin + register 탭"]
PENDPG["PendingPage\n관리자 승인 대기"]
DENIEDPG["AccessDeniedPage\noperator / viewer 역할"]
end
subgraph ADMINDASH2["Admin Dashboard admin 전용"]
USERSEC["Users 섹션\n계정 승인·거절·역할 변경·삭제\nPATCH /admin/users/:id"]
AISEC["AI Models 섹션\nYOLO 모델 카탈로그 15종\n다운로드 진행률 · 런타임 전환\nAI 모듈 퍼채널 활성화"]
ONVIFSEC["ONVIF 섹션\n이벤트 타입 레지스트리\n타임라인 오버레이 줌 · 팬 · Raw XML"]
AUDITSEC["Audit Log 섹션\n인증 이력 조회\nsignup · signin · approve · reject · ..."]
end
subgraph STREAMDASH2["Streaming Dashboard combined | streaming 모드"]
CAMGRID["CameraGrid\n멀티 카메라 그리드 1×1 ~ 4×4\n채널 페이징 레이아웃 전환"]
subgraph SIDEBAR2["사이드바 리사이즈 가능"]
TCAM["Cameras 탭\n추가 · 편집 · RTSP / YouTube / ONVIF"]
TALERT["Alerts 탭\n실시간 알림 목록 확인 처리"]
TZONE["Zones 탭\n다각형 구역 관리"]
TDET["Detections 탭\n실시간 감지 객체 크롭 이미지"]
TFACE["Face Gallery 탭\n얼굴 등록 · Re-ID 크로스 카메라"]
end
FULLSCR2["FullscreenCameraView\nDetections Gantt 타임라인\nONVIF 이벤트 타임라인\nFace Gallery Appearance Re-ID"]
end
subgraph ANALYSDASH2["Analysis Dashboard analysis 모드"]
ANALIVE["AnalysisLivePanel\n실시간 감지 피드\nbbox 오버레이"]
ANAEVT["AnalysisEventsTab\n이벤트 이력\n배회 · 화재 · 연기"]
ANAMTX["서버 메트릭\n처리 FPS · 큐 깊이\n카메라별 입력 상태 · 연결 수"]
end
subgraph STATSPANEL["Statistics"]
STATSSTR["StatsPanelModal\ncombined | streaming 모드\n카메라별 통계"]
STATSANA["AnalysisStatsModal\nanalysis 모드\n/api/analysis/metrics 기반"]
end
SIGNINPG -- "대기 상태" --> PENDPG
SIGNINPG -- "admin 로그인" --> ADMINDASH2
SIGNINPG -- "admin 로그인" --> STREAMDASH2
SIGNINPG -- "admin 로그인" --> ANALYSDASH2
SIGNINPG -- "operator/viewer" --> DENIEDPG
STREAMDASH2 --> STATSPANEL
ANALYSDASH2 --> STATSPANEL
SERVER_MODE 조합과 공유 MongoDB로 현장 서버 다중화와 GPU 서버 분리를 지원합니다.
graph TB
subgraph INTERNET2["외부 접근"]
BROWSERS["Browser / 모바일"]
LLMCLI["Claude / LLM\nMCP Client"]
end
subgraph DMZ["리버스 프록시 DMZ"]
NGINX2["Nginx / HAProxy\n:443 HTTPS SSL Termination\n스트리밍 서버 라우팅"]
end
subgraph SITEA["현장 A streaming mode"]
MTXA["MediaMTX\n:8189 ICE UDP"]
STRA["Node.js :3443\nstreaming mode\nCircuit Breaker\n자동 재연결 15s"]
CAMA["IP Cameras 1~N"]
CAMA --> MTXA --> STRA
end
subgraph SITEB["현장 B streaming mode"]
MTXB["MediaMTX\n:8189 ICE UDP"]
STRB["Node.js :3443\nstreaming mode\nCircuit Breaker"]
CAMB["IP Cameras N+1~M"]
CAMB --> MTXB --> STRB
end
subgraph GPUSRV["GPU 서버 analysis mode"]
ANASRV["Node.js :3443\nanalysis mode\nONNX_CUDA=1\nmax 100 concurrent req\nPer-Camera Context Map"]
end
subgraph DBSRV["DB 서버"]
SHAREDMG[("MongoDB :27017\n공유 DB\ncameras · zones · alerts · users · ...")]
end
subgraph MCPSRV2["MCP 서버 선택"]
MCPSVC["mcp-server :3100\n10 MCP Tools"]
end
BROWSERS --> NGINX2
NGINX2 --> STRA
NGINX2 --> STRB
BROWSERS -- "ICE UDP :8189" --> MTXA
BROWSERS -- "ICE UDP :8189" --> MTXB
LLMCLI --> MCPSVC
STRA -- "POST /api/analysis/frame\nJPEG + metadata LAN" --> ANASRV
STRB -- "POST /api/analysis/frame\nJPEG + metadata LAN" --> ANASRV
ANASRV -- "JSON results" --> STRA
ANASRV -- "JSON results" --> STRB
STRA -- "DB_TYPE=mongodb\nwrite-through" --> SHAREDMG
STRB -- "DB_TYPE=mongodb\nwrite-through" --> SHAREDMG
ANASRV -- "DB_TYPE=mongodb\nwrite-through" --> SHAREDMG
MCPSVC -- "HTTP REST" --> STRA
| # | 컴포넌트 | 기술 | 역할 |
|---|---|---|---|
| 1 | UDP Discovery | Node.js dgram + ONVIF WS-Discovery |
LAN 상 IP 카메라 자동 탐색 |
| 2 | ingest-daemon | Python PyAV (RTSP 수집 유일 공급자) | RTSP → JPEG · H.264 · Opus · App RTP 4-way 팬아웃 |
| 3 | MediaMTX | MediaMTX 미디어 서버 | RTSP loopback · WHEP 시그널링 · ICE UDP 릴레이 |
| 4 | PipelineManager | Node.js (서비스 오케스트레이션) | AI 서비스 생명주기 · 프레임 라우팅 · Watchdog |
| 5 | DetectionService | ONNX Runtime + YOLOv8/11/12 (15종) | 다중 객체 감지 · 런타임 모델 전환 · GPU 가속 |
| 6 | ByteTrack | JS ByteTrack + KalmanFilter | 프레임 간 객체 ID 유지 · 다중 객체 추적 |
| 7 | BehaviorEngine | Custom JS | 배회 위험 점수 0~100 · 구역 체류 시간 분석 |
| 8 | AttributePipeline | SCRFD + ArcFace + OpenPAR + YOLOv8m | 얼굴 Re-ID · 의상 분류 · PPE 감지 |
| 9 | FireSmokeService | YOLOv8s FireSmoke ONNX | 화재 · 연기 실시간 감지 |
| 10 | AlertService | Node.js EventEmitter | 알림 생성 · 에스컬레이션 · 웹훅 |
| 11 | Socket.IO Server | Socket.IO 4.x | 실시간 프레임 · 알림 · 감지 결과 스트리밍 |
| 12 | REST API | Express.js | 카메라 · 구역 · 알림 · 분석 CRUD |
| 13 | Auth (JWT + RBAC) | JWT RS256 + bcrypt | 사용자 인증 · 역할 기반 접근 제어 · 감사 로그 |
| 14 | DB Layer (db.js) | JSON file / MongoDB (전환 가능) | 이중화 스토리지 · in-memory 캐시 |
| 15 | React Dashboard | React 18 + TypeScript + Zustand + Tailwind | Streaming · Analysis · Admin 다중 대시보드 |
| 16 | mcp-server | Node.js ESM + MCP SDK | Claude / LLM 연동 10종 MCP 도구 |
수백 대의 카메라와 YouTube 스트림, 다중 스트리밍 서버, 다중 AI 분석 서버, MongoDB Replica Set, N명의 사용자 및 중앙화된 관리자 대시보드를 포함한 엔터프라이즈 수준 분산 구성입니다.
⚠️ 범례:✅ 현재 지원·⚠️ 부분 지원·🔲 미구현 (TODO Milestone 참조)
graph TB
subgraph SRCS["비디오 소스 Pool 수백 개"]
CAMPOL["IP 카메라 100+ 대\nRTSP / ONVIF\nWiseNet · Hanwha · Hikvision"]
YTPOL["YouTube 스트림 N개\nyt-dlp → FFmpeg → MediaMTX"]
end
subgraph CLIENTS["클라이언트 영역 N명 사용자"]
USERBR["일반 사용자 브라우저\nStreaming Dashboard\nAnalysis Dashboard\nStatistics"]
ADMINBR["관리자 브라우저\nAdmin Dashboard\n사용자 승인 · AI 모델 관리\n전체 서버 통합 모니터링"]
LLMCLI["Claude / LLM\nMCP Client"]
end
subgraph LB["로드 밸런서 DMZ ✅ 현재 지원"]
NGINX["Nginx / HAProxy :443\nHTTPS Termination\nSticky Session IP Hash\nWebSocket Upgrade\nHealth Check Probe"]
end
subgraph SSC["스트리밍 서버 클러스터 K대 ✅ 다중 배포 가능"]
SS1["Streaming Server 1\nSERVER_MODE=streaming\nMediaMTX + ingest-daemon\n카메라 1~M 수동 배정"]
SS2["Streaming Server 2\nSERVER_MODE=streaming\nMediaMTX + ingest-daemon\n카메라 M+1~2M"]
SSK["Streaming Server K\nSERVER_MODE=streaming\nMediaMTX + ingest-daemon\n카메라 ... ~MAX"]
end
subgraph SHAREDST["공유 상태 레이어 🔲 ES-2 ES-3 미구현"]
REDIS_S["Redis Cluster\n@socket.io/redis-adapter\nPub/Sub 크로스 노드 이벤트\ncamera:frameData · newAlert · cameraStatus"]
REDIS_SE["Redis Session Store\nconnect-redis\nOAuth CSRF 상태 공유\nSSO 세션 노드 간 공유"]
end
subgraph AIPOOL["AI 분석 서버 풀 L대 ⚠️ 현재 단일 URL만 지원 🔲 ES-1"]
AILB["AI 로드 밸런서 🔲 미구현\n라운드 로빈 최소 연결\n퍼서버 Circuit Breaker\n자동 페일오버"]
GPU1["Analysis Server 1\nSERVER_MODE=analysis\nONNX_CUDA=1\nGPU A100 / RTX 4090"]
GPU2["Analysis Server 2\nSERVER_MODE=analysis\nONNX_CUDA=1"]
GPUL["Analysis Server L\nSERVER_MODE=analysis\nONNX_CUDA=1"]
end
subgraph DBHA["데이터베이스 고가용성 ⚠️ Replica Set URI 지원 🔲 ES-4 튜닝"]
MGPRI["MongoDB Primary :27017\n쓰기 마스터\nWrite Concern majority"]
MGSEC1["MongoDB Secondary 1\n읽기 복제본\nreadPreference: secondaryPreferred"]
MGSEC2["MongoDB Secondary 2\n읽기 복제본 또는 Arbiter"]
end
subgraph CENTRAL["중앙 관리 레이어 🔲 ES-5 미구현"]
ADMINSVC["Central Admin Service\n다중 스트리밍 서버 통합 뷰\n전체 카메라 목록 실시간 상태\n서버별 부하 모니터링\n통합 감사 로그 집계"]
MCPSVC["mcp-server :3100\nClaude LLM 연동\n10 MCP Tools"]
end
CAMPOL -- "카메라 배정\n수동 설정 🔲 ES-6 자동 배정" --> SS1
CAMPOL --> SS2
CAMPOL --> SSK
YTPOL --> SS1
YTPOL --> SSK
USERBR --> NGINX
ADMINBR --> NGINX
NGINX --> SS1
NGINX --> SS2
NGINX --> SSK
SS1 <-. "🔲 ES-2 미구현" .-> REDIS_S
SS2 <-. "🔲 ES-2 미구현" .-> REDIS_S
SSK <-. "🔲 ES-2 미구현" .-> REDIS_S
SS1 -. "🔲 ES-3 미구현" .-> REDIS_SE
SS2 -. "🔲 ES-3 미구현" .-> REDIS_SE
SS1 -- "POST /api/analysis/frame\n현재: 단일 URL" --> AILB
SS2 -- "POST /api/analysis/frame" --> AILB
SSK -- "POST /api/analysis/frame" --> AILB
AILB -. "🔲 ES-1 미구현" .-> GPU1
AILB -. "🔲 ES-1 미구현" .-> GPU2
AILB -. "🔲 ES-1 미구현" .-> GPUL
SS1 -- "DB_TYPE=mongodb\nwrite-through" --> MGPRI
SS2 -- "write-through" --> MGPRI
SSK -- "write-through" --> MGPRI
GPU1 -- "write-through" --> MGPRI
GPU2 -- "write-through" --> MGPRI
GPUL -- "write-through" --> MGPRI
MGPRI -. "Replication" .-> MGSEC1
MGPRI -. "Replication" .-> MGSEC2
LLMCLI --> MCPSVC
MCPSVC --> NGINX
ADMINBR --> ADMINSVC
ADMINSVC -. "🔲 ES-5 미구현" .-> SS1
ADMINSVC -. "🔲 ES-5 미구현" .-> SS2
ADMINSVC -. "🔲 ES-5 미구현" .-> SSK
graph LR
subgraph CURRENT["현재 구현 단일 노드 전제"]
BR_NOW["Browser\nN명 사용자"]
ONE_SS["Streaming Server 1대\nexpress-session\nMemoryStore 인 프로세스\nSocket.IO In-Memory Adapter"]
ONE_DB["lts.json / MongoDB\nrefresh_tokens 테이블\nJWT RS256 검증"]
BR_NOW -- "HTTPS + JWT" --> ONE_SS
ONE_SS --> ONE_DB
end
subgraph TARGET["목표 멀티 노드 ES-2 ES-3"]
BR_TGT["Browser N명"]
LB_TGT["Nginx\nSticky Session"]
SS_A["Streaming Server A"]
SS_B["Streaming Server B"]
REDIS_TGT["Redis Cluster\nSocket.IO Adapter\n세션 공유 Store"]
DB_TGT["MongoDB Replica Set\nJWT RS256 공유 키\nrefresh_tokens 공유"]
BR_TGT --> LB_TGT
LB_TGT --> SS_A
LB_TGT --> SS_B
SS_A <--> REDIS_TGT
SS_B <--> REDIS_TGT
SS_A --> DB_TGT
SS_B --> DB_TGT
end
| 요구사항 | 현재 구현 상태 | 한계점 | 해결 Milestone |
|---|---|---|---|
| 다중 스트리밍 서버 (K대) | ✅ 지원 | 각 서버 독립 배포, 공유 MongoDB 필요 | — |
| 다중 AI 분석 서버 (L대) | ❌ 단일 URL | ANALYSIS_SERVER_URL 하나만 지원, 라운드 로빈 없음 |
ES-1 |
| Socket.IO 크로스 노드 이벤트 | ❌ in-memory | 멀티 노드 배포 시 브라우저가 다른 서버 이벤트 수신 불가 | ES-2 |
| 세션 공유 (OAuth / CSRF) | ❌ MemoryStore | 노드 재시작 / 다중 노드 시 세션 소멸 | ES-3 |
| MongoDB Replica Set | Replica Set URI 가능하나 readPreference 미설정 | ES-4 | |
| JWT 다중 노드 인증 | ✅ 지원 | RS256 공개키 기반 stateless, 노드 간 공유 가능 | — |
| 중앙 Admin Dashboard | ❌ 미구현 | 서버별 독립 Admin, 전체 통합 뷰 없음 | ES-5 |
| 카메라 자동 배정 / 이전 | ❌ 수동 설정 | 카메라 → 스트리밍 서버 매핑 .env 수동 |
ES-6 |
| 프로덕션 가시성 (메트릭) | /admin/system CPU/메모리 제공, Prometheus 없음 |
ES-7 | |
| K8s / 컨테이너 오케스트레이션 | 단일 호스트 Compose, K8s HPA 미지원 | ES-8 |
| Layer | Technology | Version | Purpose |
|---|---|---|---|
| Runtime | Node.js | 18+ LTS | Server runtime |
| HTTP/API | Express.js | 4.x | REST API + static serving |
| WebSocket | Socket.IO | 4.x | Real-time frame streaming |
| RTSP Capture | fluent-ffmpeg | 2.x | RTSP decode + frame extract |
| AI Inference | onnxruntime-node | 1.17+ | YOLOv8n ONNX inference |
| Image Processing | sharp | 0.33+ | Frame resize/crop/encode |
| UDP Discovery | Node.js built-in dgram |
— | Camera broadcast discovery |
| Database | better-sqlite3 | 9.x | Event/alert storage |
| Process Manager | PM2 | 5.x | Production process management |
| Layer | Technology | Version | Purpose |
|---|---|---|---|
| Framework | React | 18+ | Web UI |
| Language | TypeScript | 5.x | Type safety |
| Build | Vite | 5.x | Fast dev server + bundler |
| UI Library | shadcn/ui + Tailwind CSS | — | Component system |
| Video Display | HTML5 <img> / Canvas |
— | MJPEG-over-Socket.IO |
| BBox Overlay | HTML5 Canvas API | — | Bounding box + track ID |
| State | Zustand | 4.x | Client state management |
| Charts | Recharts | 2.x | Analytics & heatmaps |
| Model | Format | Task | Classes | Size | Latency* |
|---|---|---|---|---|---|
| YOLOv8n | ONNX | Multi-class detection (primary) | COCO 80-class (person, vehicle, indoor, accessories, …) | 13MB | ~15ms |
| YOLOv8s | ONNX | Multi-class detection (higher accuracy) | same as above | ~22MB | ~30ms |
| ByteTrack | JS implementation | Multi-object tracking (8-dim KF + multi-cue) | — | — | ~5ms |
| SCRFD-2.5G | ONNX | Face detection | face | 3.2MB | ~10ms |
| ArcFace ResNet50 | ONNX | Face Re-ID / appearance embedding (512-dim) | — | 249MB | ~15ms/crop |
| YOLOv8m PPE | ONNX | Mask / Helmet / PPE detection | mask, hardhat | 99MB | ~30ms |
| YOLOv8s Fire/Smoke | ONNX | Fire and smoke detection | fire, smoke, both | 43MB | ~25ms |
| OpenPAR | ONNX | Clothing type classification | cloth types | 94MB | ~20ms/crop |
* Latency measured on Intel Core i7 CPU. GPU via NVIDIA CUDA reduces by 3–5×.
The detection service supports all 80 COCO class IDs in ENABLED_CLASSES. Class-level filtering is applied by analyticsConfig.isClassEnabled() — not at the detection layer. Zones with targetClasses: [] (empty) monitor all enabled classes.
| Zone Target Key | Mapped COCO Classes |
|---|---|
human |
person (0) |
vehicle |
bicycle (1), car (2), motorcycle (3), bus (5), truck (7) |
face |
detected via SCRFD face model (not YOLO class) |
accessories |
backpack (24), umbrella (25), handbag (26), tie (27), suitcase (28) |
furniture |
chair (56), couch (57), dining table (60), bed (59) |
computer |
laptop (63), tv (62), keyboard (66), mouse (64), cell phone (67) |
fire / smoke |
detected via YOLOv8s fire/smoke model (not YOLO 80-class) |
Full 80-class breakdown: see Section 7 and RFP_Object_Tracking.md §3.2.2.
Place model files in server/models/:
server/models/
├── yolov8n.onnx # 13MB — Primary COCO 80-class detection (required)
├── yolov8s.onnx # ~22MB — Higher accuracy detection (optional)
├── scrfd_2.5g.onnx # 3.2MB — Face detection SCRFD-2.5G (AI-03)
├── arcface_w600k_r50.onnx # 249MB — Face Re-ID ArcFace ResNet50 (AI-03)
├── yolov8m_ppe.onnx # 99MB — Mask / helmet PPE (AI-04/07)
├── yolov8s_fire_smoke.onnx # 43MB — Fire & smoke detection (AI-09)
└── openpar.onnx # 94MB — Cloth type classifier (AI-06, Phase-2 active)
Download commands:
# YOLOv8n ONNX (Ultralytics — ONNX IR v9, requires onnxruntime-node ≥ 1.16)
python3 -c "from ultralytics import YOLO; YOLO('yolov8n.pt').export(format='onnx')"
# SCRFD face detection (JackCui/facefusion via HuggingFace)
# ArcFace ResNet50 (FoivosPar/Arc2Face via HuggingFace)
# YOLOv8m PPE (keremberke/yolov8m-protective-equipment-detection)
# YOLOv8s Fire/Smoke (Abonia1/YOLOv8-Fire-and-Smoke-Detection)
# Download details: see Section 7.5 (installed model status table)Full specification moved to RFP_Camera_Discovery.md (Document ID: LTS-2026-002).
The system supports two parallel discovery mechanisms:
| Mechanism | Scope | Detail |
|---|---|---|
| WiseNet UDP Broadcast | Hanwha / WiseNet cameras | Send port 7701, receive port 7711, 255.255.255.255 |
| ONVIF WS-Discovery | All ONVIF-compliant cameras | UDP multicast 239.255.255.250:3702 |
Both mechanisms run concurrently and merge results deduplicated by MAC address. See RFP_Camera_Discovery.md for protocol specification, packet format, and implementation details.
The Node.js server connects to each discovered (or manually configured) camera via RTSP and extracts frames at 10 FPS using FFmpeg. Frames are converted to JPEG and passed to the AI inference pipeline.
rtsp://<username>:<password>@<camera-ip>:<port>/profile1/media.smp
Supported RTSP URL formats:
- WiseNet/Hanwha:
rtsp://<ip>/profile1/media.smp(ONVIF Profile S) - Generic ONVIF:
rtsp://<ip>/onvif/media - Manual URL: configurable per camera
RTSP Stream (H.264/H.265)
│
▼ FFmpeg (fluent-ffmpeg)
│ -vf fps=10 -f image2pipe -vcodec mjpeg
│
▼ Node.js Buffer (JPEG)
│
▼ sharp (resize to 640×384 for inference)
│
▼ Float32Array (normalized [0,1] RGB)
│
▼ ONNX Runtime (YOLOv8n inference)
│
▼ Detection Results [{bbox, confidence, classId}]
│
▼ ByteTrack (assign/maintain objectId)
│
▼ Annotated frame + [{objectId, bbox, confidence}]
│
▼ Socket.IO → React UI
| Metric | Target | Notes |
|---|---|---|
| Capture frame rate | 10 FPS | Per channel |
| Inference latency | ≤ 50ms/frame | CPU; ≤ 15ms GPU |
| End-to-end latency | ≤ 500ms | Capture → UI |
| Concurrent channels | ≥ 4 (CPU) | ≥ 16 (GPU) |
- Input: 640×640 normalized RGB tensor
[1, 3, 640, 640](letterbox padding, grey border) - Output:
[1, 84, 8400]— 4 bbox coords + 80 class scores per anchor - Enabled classes: all 80 COCO classes (class-level gating via
analyticsConfig.isClassEnabled()) - Confidence threshold: 0.30 (configurable via
CONFIDENCE_THRESHOLDenv) - NMS IoU threshold: 0.5 (configurable via
NMS_IOU_THRESHOLDenv) - Post-processing: NMS → filter enabled classes → scale boxes back to original frame coordinates
- Pre-processing: letterbox resize with sharp; coordinate unpadding in
_postprocess()
ByteTrack operates on detection outputs and maintains persistent objectId across frames:
- 8-dim Kalman Filter: state =
[x, y, w, h, vx, vy, vw, vh]; adaptive process noise Q (stationary / normal / fast) - High-confidence detections: matched to all tracks via multi-cue score
- Low-confidence detections: matched to unmatched Lost tracks via IoU-only (occlusion recovery)
- Multi-cue association:
λ_iou × IoU + λ_app × IoU × appConf; λ_iou=0.7, λ_app=0.3 - Track states:
Tracked→Lost→Removed; score threshold = 0.25 - Max track age: 90 frames (9 seconds at 10 FPS; was 30 frames)
- KF predict freeze:
framesWithoutHit > 1skips predict to prevent P-matrix covariance blowup - NaN guard: non-finite KF output falls back to YOLO bbox; P matrix reset on blowup
- Appearance Re-ID: ArcFace 512-dim embedding per track with EMA update (α = 0.9)
Each frame produces a JSON array of detections:
{
"frameId": 12345,
"timestamp": 1715678901234,
"cameraId": "cam-01",
"frameWidth": 1920,
"frameHeight": 1080,
"detections": [
{
"objectId": 7,
"confidence": 0.891,
"bbox": { "x": 120, "y": 85, "width": 65, "height": 190 },
"className": "person",
"isLoitering": false,
"dwellTime": 12.4,
"zoneId": "zone-uuid-or-null"
},
{
"objectId": 12,
"confidence": 0.762,
"bbox": { "x": 400, "y": 200, "width": 180, "height": 120 },
"className": "car",
"isLoitering": false,
"dwellTime": 0,
"zoneId": null
}
]
}An objectId is flagged as loitering when:
dwellTime > threshold AND displacement < minDisplacement
| Parameter | Default | Configurable |
|---|---|---|
| Dwell time threshold | 30 seconds | Per zone (5s–600s) |
| Min displacement | 50 pixels | Per zone |
| Re-entry window | 120 seconds | Per zone |
| Crowd density filter | 5 persons | Auto-adjust |
Each camera zone can independently activate one or more AI analysis modules via the targetClasses checkbox array in the Zone Editor. Modules are applied only to objects detected within that zone.
| # | Checkbox | Zone Key | RFP | Status | Description |
|---|---|---|---|---|---|
| 1 | ☑ Human | human |
AI-01 | ✅ Implemented | Person detection — YOLOv8n COCO class 0 (person) |
| 2 | ☑ Vehicle | vehicle |
AI-02 | ✅ Implemented | Vehicle detection — bicycle/car/motorcycle/bus/truck |
| 3 | ☑ Face | face |
AI-03 | ✅ Implemented | Face detection — SCRFD-2.5G (3.2MB) + ArcFace ResNet50 Re-ID (249MB) |
| 4 | ☑ Mask | mask |
AI-04 | ✅ Implemented | Mask detection — YOLOv8m PPE (99MB), mask/no_mask 2-class |
| 5 | ☑ Color | color |
AI-05 | ✅ Implemented | Upper/lower body color analysis — Phase-1 pixel average, 11-color classification (no model required) · Phase-1.5 K-Means dominant color on the same fixed ROI (no model) 📝 Proposed, not yet implemented · Phase-2 PAR pending · Phase-3 Human Parsing (SCHP/SegFormer, humanParsing toggle, model-catalog hot-swap) 🟡 Implemented, opt-in (disabled by default — model not auto-downloaded, no behavioral test coverage yet) |
| 6 | ☑ Cloth | cloth |
AI-06 | ✅ Implemented | Clothing type classification — OpenPAR (openpar.onnx 94MB), upper/lower garment + sleeve length — Phase-2 active; distinct from Color's proposed Phase-3 pixel-mask (see Design_AI_Cloth_Analysis.md §10) |
| 7 | ☑ Hat | hat |
AI-07 | ✅ Implemented | Helmet/hat detection — YOLOv8m PPE (99MB), hardhat/no_hardhat classification |
| 8 | ☑ Accessories | accessories |
AI-08 | ✅ Implemented | Accessories detection — YOLOv8n COCO (backpack/umbrella/handbag/tie/suitcase) |
| 9 | ☑ Fire | fire |
AI-09 | ✅ Implemented | Fire detection — YOLOv8s 3-class (yolov8s_fire_smoke.onnx 43MB, Abonia1/GitHub) |
| 10 | ☑ Smoke | smoke |
AI-09 | ✅ Implemented | Smoke detection — YOLOv8s 3-class (yolov8s_fire_smoke.onnx 43MB, Abonia1/GitHub) |
Implemented modules: checkbox is enabled when editing a Zone. Pending modules: checkbox is shown in gray and automatically activated when the corresponding ONNX model file is placed in
server/models/.Checkbox availability is queried in real-time from the server
/api/capabilitiesendpoint.
At the bottom of the edit screen in the "AI Detection Targets" section, select the AI modules to apply to this zone.
┌─────────────────────────────────┐
│ AI Detection Targets (All if none selected) │
├────────────────┬────────────────┤
│ ☑ Human │ ☑ Vehicle │
│ ☑ Face │ ☑ Mask │
│ ☑ Color │ ☑ Cloth │
│ ☑ Hat │ ☑ Accessories │
│ ☑ Fire │ ☑ Smoke │
└────────────────┴────────────────┘
- Checkbox selected: Blue background + check icon, immediately saved via API (PUT
/api/cameras/:id/zones/:zoneId) - If not selected:
targetClasses: []→ detect all active classes (default behavior) - Dynamic availability query:
/api/capabilitiescalled when Zone Editor opens → reflects model file existence - Fire/smoke alert: upon detection after checking fire/smoke in a Zone,
fire:alertSocket event triggered (10-second cooldown)
// behaviorEngine.js — classMatchesZone()
const TARGET_CLASS_MAP = {
human: ['person'],
vehicle: ['bicycle', 'car', 'motorcycle', 'bus', 'truck'],
// future additions: face, mask, color, cloth, hat, accessories
};
// empty means all classes allowed
if (!targetClasses || targetClasses.length === 0) return true;targetClasses Setting |
Detection Targets | Use Case |
|---|---|---|
[] (default) |
All active classes | General surveillance |
["human"] |
Persons only | Access-controlled area |
["vehicle"] |
Vehicles only | Parking lot management |
["human", "vehicle"] |
Persons + Vehicles | Mixed area |
["human", "hat"] |
Persons + Safety helmet check | Construction site compliance (Pending) |
["human", "mask"] |
Persons + Mask | Quarantine zone (Pending) |
| Class | Normal color | Loitering color |
|---|---|---|
| person | 🟢 Green rgba(34,197,94) |
🔴 Red rgba(239,68,68) |
| bicycle | 🟡 Yellow rgba(250,204,21) |
🔴 Red |
| car | 🔵 Blue rgba(59,130,246) |
🔴 Red |
| motorcycle | 🟠 Orange rgba(249,115,22) |
🔴 Red |
| bus | 🟣 Purple rgba(168,85,247) |
🔴 Red |
| truck | 🩵 Teal rgba(20,184,166) |
🔴 Red |
Label format: person #3 94% (className + objectId + confidence)
Verification environment: onnxruntime-node 1.26.0 (Node.js, 2026-05-18)
Python onnxruntime 1.14.1 does not supportyolov8n.onnx(ONNX IR v9) — works only with Node.js runtime
Full catalog source of truth:MODEL_CATALOG/EXTENDED_CATALOGinanalysisApi.js, managed via Admin Dashboard → AI Models tab (GET/POST /api/analysis/models*, see §6. AI Models & Inference Pipeline anddocs/design/Design_AI_Model_Catalog.md).
| File | Size | Node.js Load | Responsible AI module | Source |
|---|---|---|---|---|
yolov8n.onnx |
13 MB | ✅ Working | AI-01 Human · AI-02 Vehicle · AI-08 Accessories · All indoor objects (default detector) | Ultralytics YOLOv8n COCO (ONNX IR v9) |
scrfd_2.5g.onnx |
3.2 MB | ✅ Working | AI-03 Face Detection | JackCui/facefusion @ HuggingFace |
arcface_w600k_r50.onnx |
249 MB | ✅ Working | AI-03 Face Re-ID | FoivosPar/Arc2Face @ HuggingFace |
yolov8m_ppe.onnx |
99 MB | ✅ Working | AI-04 Mask Detection · AI-07 Helmet Detection | keremberke/yolov8m-protective-equipment-detection |
yolov8s_fire_smoke.onnx |
43 MB | ✅ Working | AI-09 Fire & Smoke Detection (3-class) | Abonia1/YOLOv8-Fire-and-Smoke-Detection @ GitHub |
openpar.onnx |
94 MB | ✅ Working | AI-06 Cloth Analysis Phase-2 (Clothing type: tshirt/shirt/jacket/hoodie/vest/dress + pants/jeans/shorts/skirt + sleeve) | Event-AHU/OpenPAR |
Downloaded and hot-swappable with yolov8n.onnx for the same modules (AI-01/02/08) via Admin Dashboard → AI Models → switch — no server restart required.
| File | Size | Node.js Load | Series (mAPval) | Source |
|---|---|---|---|---|
yolo11n.onnx |
10 MB | ✅ Working | YOLO11n (39.5) | Ultralytics YOLO11 @ GitHub |
yolo11s.onnx |
36 MB | ✅ Working | YOLO11s (47.0) | Ultralytics YOLO11 @ GitHub |
yolo11m.onnx |
77 MB | ✅ Working | YOLO11m (51.5) | Ultralytics YOLO11 @ GitHub |
yolo11l.onnx |
97 MB | ✅ Working | YOLO11l (53.4) | Ultralytics YOLO11 @ GitHub |
yolo11x.onnx |
218 MB | ✅ Working | YOLO11x (54.7) | Ultralytics YOLO11 @ GitHub |
yolov8s.onnx |
43 MB | ✅ Working | YOLOv8s (44.9) | Ultralytics YOLOv8 @ GitHub |
yolov8m.onnx |
99 MB | ✅ Working | YOLOv8m (50.2) | Ultralytics YOLOv8 @ GitHub |
yolov8l.onnx |
167 MB | ✅ Working | YOLOv8l (52.9) | Ultralytics YOLOv8 @ GitHub |
yolov8x.onnx |
260 MB | ✅ Working | YOLOv8x (53.9) | Ultralytics YOLOv8 @ GitHub |
.ptfiles alongside each.onnxabove (e.g.yolo11n.pt) are the Ultralytics export intermediates kept for re-export; they are not loaded by the inference pipeline.
Total installed: 15 ONNX model files · ≈1.5 GB (excluding .pt intermediates)
| Catalog ID(s) | Family | Conversion path | Status |
|---|---|---|---|
yolo26n/s/m/l/x |
Detector (YOLO26, NMS-free) | requiresConversion — GitHub .pt → ultralytics export |
⬜ Not downloaded |
yolo12n/s/m/l/x |
Detector (YOLO12, attention-based) | requiresConversion — GitHub .pt → ultralytics export |
⬜ Not downloaded |
openpar-pa100k (PromptPAR) |
AI-06 Cloth Attribute (PAR) | pyExport — exportPromptPAR.py (requires GPU) |
⬜ Not downloaded |
openpar-resnet50-pa100k |
AI-06 Cloth Attribute (PAR), OpenPAR ResNet50 baseline | manualOnly — no automated export, place file manually |
🔒 Manual export only |
schp-lip20 |
AI-05 Phase-3 Human Parsing (Proposed) | Direct ONNX URL @ HuggingFace | ⬜ Not downloaded |
segformer-clothes |
AI-05 Phase-3 Human Parsing (Proposed) | Direct ONNX URL @ HuggingFace | ⬜ Not downloaded |
osnet-retail-0287 |
CrossCamera Phase-2 Appearance Re-ID (Proposed) | Direct ONNX URL @ OpenVINO Model Zoo | ⬜ Not downloaded |
insightface-genderage |
Age Estimation · Gender Classification (Proposed, shared genderage.onnx) |
Direct ONNX URL @ HuggingFace | ⬜ Not downloaded |
vit-age-classifier |
Age Estimation (Proposed) | hfOptimumExport — optimum.exporters.onnx |
⬜ Not downloaded |
vit-gender-classifier |
Gender Classification (Proposed) | hfOptimumExport — optimum.exporters.onnx |
⬜ Not downloaded |
server/models/ Size Status Module
├── yolov8n.onnx 13 MB ✅ Human/Vehicle/Indoor/Accessories (COCO 80-class, default detector)
├── yolo11n/s/m/l/x.onnx 10–218 MB ✅ Detector alternatives (Ultralytics YOLO11, admin-selectable)
├── yolov8s/m/l/x.onnx 43–260 MB ✅ Detector alternatives (Ultralytics YOLOv8, admin-selectable)
├── scrfd_2.5g.onnx 3.2 MB ✅ Face detection SCRFD-2.5G (AI-03)
├── arcface_w600k_r50.onnx 249 MB ✅ Face Re-ID ArcFace ResNet50 (AI-03)
├── yolov8m_ppe.onnx 99 MB ✅ Mask/Helmet YOLOv8m PPE (AI-04/07)
├── yolov8s_fire_smoke.onnx 43 MB ✅ Fire/Smoke YOLOv8s 3-class (AI-09)
│ Source: Abonia1/YOLOv8-Fire-and-Smoke-Detection
└── openpar.onnx 94 MB ✅ Clothing type PAR (AI-06 Phase-2)
Source: Event-AHU/OpenPAR
Classes: tshirt/shirt/jacket/hoodie/vest/dress + pants/jeans/shorts/skirt + sleeve short/long
Not yet downloaded (catalog-registered, see 7.5.3): yolo26*.onnx, yolo12*.onnx,
openpar_pa100k.onnx, openpar_resnet50_pa100k.onnx, schp_lip.onnx,
segformer_clothes.onnx, appearance_reid_osnet.onnx, genderage.onnx,
vit_age_classifier.onnx, vit_gender_classifier.onnx
For detailed spec for each AI module, refer to RFP_AI_Human_Detection.md through RFP_AI_Fire_Smoke_Detection.md.
Each InferenceSession created by the ONNX Runtime spawns intra-op worker threads (default: one per logical CPU core). With 5 active models and a high core-count server, this can result in hundreds of idle threads. The server controls thread count via server/src/utils/onnxOptions.js, driven by three environment variables in server/.env.
| Mode | Condition | intraOpNumThreads |
executionProviders |
|---|---|---|---|
| Development | NODE_ENV=development |
ONNX_THREADS_DEV (default: 1) |
['cpu'] |
| CUDA | ONNX_CUDA=1 |
ONNX_THREADS_CUDA (default: 1) |
['cuda', 'cpu'] |
| Production | default | ONNX_THREADS_PROD (default: 0 = auto) |
['cpu'] |
ONNX_THREADS_PROD=0 → auto: max(2, min(8, floor(CPU_cores / 2)))
[onnxOptions] mode=dev threads=1 cores=32 providers=["cpu"]
[onnxOptions] mode=cuda threads=1 cores=32 providers=["cuda","cpu"]
[onnxOptions] mode=prod threads=8 cores=32 providers=["cpu"]
| Service file | Model | InferenceSession |
|---|---|---|
detection.js |
yolov8n.onnx |
1 |
faceService.js |
scrfd_2.5g.onnx |
1 |
faceService.js |
arcface_w600k_r50.onnx |
1 |
fireSmokeService.js |
yolov8s_fire_smoke.onnx |
1 |
protectiveEquipService.js |
yolov8m_ppe.onnx |
1 |
colorClothService.js |
openpar.onnx |
1 |
Total threads in dev mode = 5 sessions × 1 thread = 5 threads.
Total threads in prod mode (32-core server) = 5 sessions × 8 threads = 40 threads.
# In server/.env
ONNX_CUDA=1 # enable CUDA provider
ONNX_THREADS_CUDA=1 # GPU handles parallelism; 1 CPU thread sufficientRequires a CUDA-enabled build of onnxruntime-node. If CUDA is unavailable at runtime, the provider list ['cuda', 'cpu'] automatically falls back to CPU.
- Supported with NVIDIA GPU + recent NVIDIA driver + CUDA Toolkit 12.x + cuDNN 8.x
- Confirm driver/GPU:
nvidia-smi- Automated CUDA Toolkit install:
server/src/scripts/setup-cuda.windows.ps1downloads and silently installs the CUDA Toolkit (default 12.4.1), then installs a matching CUDA-enabled PyTorch build and verifiestorch.cuda.is_available()— written for the PromptPAR (cloth-PAR) export step, which requires a real GPU with no CPU fallback. Run in an elevated PowerShell:
powershell -ExecutionPolicy Bypass -File server/src/scripts/setup-cuda.windows.ps1Full details, flags, and troubleshooting: GPU Provider Setup §2.2.
- Run server with CUDA enabled:
cd server
$env:ONNX_CUDA='1'
npm start- Supported with NVIDIA GPU + recent NVIDIA driver + CUDA Toolkit 12.x + cuDNN 8.x
- Confirm driver/GPU:
nvidia-smi- Run server with CUDA enabled:
cd server
ONNX_CUDA=1 npm start- Startup log should include:
[onnxOptions] mode=cuda ... providers=["cuda","cpu"]
- If CUDA provider is not available, startup falls back to CPU providers.
If the prebuilt onnxruntime-node package does not expose CUDA on your machine, use the source-build scripts below to build ONNX Runtime and wire a local js/node package into this server project.
- Added scripts:
server/src/scripts/build-onnxruntime-source.windows.ps1server/src/scripts/build-onnxruntime-source.linux.sh
Windows:
cd server
npm run build-ort-source:windowsOptional Windows arguments:
powershell -ExecutionPolicy Bypass -File src/scripts/build-onnxruntime-source.windows.ps1 \
-OrtRepoDir "D:\src\onnxruntime" \
-OrtRef "v1.26.0" \
-CudaHome "C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.3" \
-CudnnHome "C:\tools\cudnn" \
-CudaArch "120"Linux:
cd server
npm run build-ort-source:linuxOptional Linux environment variables:
cd server
ORT_REPO_DIR=$HOME/source/onnxruntime \
ORT_REF=v1.26.0 \
CUDA_HOME=/usr/local/cuda-13.3 \
CUDNN_HOME=/usr/lib/x86_64-linux-gnu \
CUDA_ARCH=120 \
npm run build-ort-source:linuxWhat the scripts do:
- Clone or update
microsoft/onnxruntimesource - Build native ONNX Runtime with CUDA EP (
--build_shared_lib, flash-attention OFF) - Build
onnxruntime/js/node - Install local package into this project with
npm --prefix <server> install <onnxruntime/js/node> --no-save
After build:
cd server
npm run restartExpected startup log when CUDA EP is active:
[onnxOptions] mode=cuda ... providers=["cuda","cpu"]
For each tracked object per frame:
1. Update position history (circular buffer, 300 frames)
2. Zone entry/exit: point-in-polygon test (ray casting)
3. Cross-ID state transfer: zone appearance gallery match
→ ArcFace cosine ≥ 0.45 OR clothing colour (upper+lower both match)
→ dwell time, trajectory, revisit count transferred to new ID
4. Sliding-window displacement: max bbox-center distance over last 10 s
5. Pacing score: x-direction reversal count (saturates at 10 reversals)
6. Composite risk score (5-factor):
dwellRatio×0.35 + revisitRatio×0.30 + lowVeloRatio×0.15
+ pacingScore×0.12 + circScore×0.08
7. Loitering declared: dwellTime ≥ threshold AND displacement < minDisplacement
→ emit LOITERING_ALERT event if riskScore ≥ minRiskScore (per zone)
- Zones defined as polygons in pixel coordinates (actual JPEG frame space)
- Point-in-polygon test (ray casting) per detection per frame
- Up to 50 zones per camera feed
- Zone types:
MONITOR(trigger alerts),EXCLUDE(suppress alerts) - Time-based activation: cron-style schedule per zone
- Per-zone AI target class filtering via
targetClassesfield:[]or omitted → monitor all enabled detection classes['human']→ monitor persons only['vehicle']→ monitor bicycle/car/motorcycle/bus/truck['human', 'vehicle']→ monitor both
- Filter is applied per frame in the behavior engine — no restart required
The React UI receives annotated data from the Node.js server via Socket.IO:
- Video stream: JPEG frames sent as
frameevents (base64) - Detection data: JSON detections sent as
detectionsevent (same timestamp) - Rendering:
<img>tag for video frame +<canvas>overlay for bounding boxes
Bounding Box Rendering:
<div style="position:relative">
<img id="video-frame" src={currentFrame} />
<canvas id="bbox-overlay"
style="position:absolute; top:0; left:0"
width={frameWidth} height={frameHeight} />
</div>
Each detection renders:
- Rectangle: bbox outline (green = normal, red = loitering)
- Label:
ID:7 0.89(objectId + confidence, top-left of bbox) - Dwell timer: seconds counter (bottom-right, appears when dwell > 5s)
┌─────────────────────────────────────────────────────┐
│ LTS Dashboard [Camera: cam-01 ▼] [⚙️] │
├──────────────────────────────┬──────────────────────┤
│ │ Alert Panel │
│ Live Video Feed │ ┌────────────────┐ │
│ [<img> + <canvas> overlay] │ │ ⚠️ ID:7 Cam01 │ │
│ │ │ Loitering 42s │ │
│ ID:3 0.91 [bbox] │ └────────────────┘ │
│ ID:7 0.89 [bbox] 🔴42s │ │
│ │ Camera List │
├──────────────────────────────│ ○ cam-01 (live) │
│ Camera Grid (1/4/9/16 view) │ ○ cam-02 (live) │
│ [cam-01][cam-02][cam-03]... │ + Add Camera │
└──────────────────────────────┴──────────────────────┘
The Zone Editor opens as a full-viewport overlay when the + Zone button is clicked on any camera view.
Key implementation details:
| Feature | Implementation |
|---|---|
| Full-screen vertex drag | Global document.addEventListener('mousemove/mouseup') — drag works even when cursor leaves the canvas |
| Coordinate system | All hit-testing uses frame pixel space; hit radius dynamically converted from screen pixels via fwEff/fhEff |
| Frame dimensions | Background image naturalWidth/naturalHeight (read via onLoad) replaces JPEG SOF defaults — matches <img object-contain> layout exactly |
| Vertex delete | Right-click on vertex → context menu shows "Delete vertex N"; auto-saves after deletion; minimum 3 vertices enforced |
| AI target classes | Checkbox grid (Human, Vehicle, + 6 planned attributes) auto-saves per toggle via PUT API |
Zone polygon storage: coordinates are in actual JPEG frame pixel space (e.g. 1920×1080), not normalized. The ZoneEditor reads img.naturalWidth/naturalHeight so the canvas overlay always aligns with the displayed video regardless of container size.
- Auto-populate discovered cameras from UDP broadcast
- Manual RTSP URL entry
- Per-camera: start/stop stream, zone configuration, sensitivity settings
Double-clicking any camera cell in the grid opens a fullscreen overlay with a dedicated left-side detection panel.
Trigger: Double-click on any camera cell in the multi-camera grid
Exit: Click the × button, press Escape, or click the dimmed background
Layout:
┌──────────────────────────────────────────────────────────────────┐
│ [Camera Name] [✕] │
├─────────────────┬────────────────────────────────────────────────┤
│ Detections 3 │ │
│ 1 loiter │ │
├─────────────────┤ │
│ PERSON #a3b2c1 │ Live Video (fullscreen) │
│ [LOITER] │ <img> + <canvas> overlay │
│ conf 89% │ │
│ dwell 42.3s │ Bounding boxes drawn with │
│ x 320 y 180 │ class colors + loitering │
│ w 65 h 190 │ highlight (same as grid view) │
├─────────────────┤ │
│ CAR #f1e2d3 │ │
│ conf 74% │ │
│ dwell 2.1s │ │
│ x 800 y 400 │ │
│ w 120 h 80 │ │
├─────────────────┤ │
│ ■ person ■ car │ │
│ ■ bicycle ■ bus │ │
└─────────────────┴────────────────────────────────────────────────┘
Detection Panel fields (per object):
| Field | Description |
|---|---|
className |
Object class (person, car, bicycle, …) — color-coded |
objectId |
Persistent track ID (8-char hex prefix) |
[LOITER] badge |
Shown in red when isLoitering = true |
conf |
Detection confidence (%) |
dwell |
Seconds the object has been in a monitored zone; yellow when > 5 s |
x, y |
Bounding box top-left in frame pixel coordinates |
w, h |
Bounding box width / height in pixels |
Objects are sorted: loitering first, then by descending dwell time.
Socket.IO subscription reference counting:
The fullscreen view renders an additional useCamera(cameraId) hook for the same camera.
A module-level subscriptionCounts map ensures camera:subscribe is emitted only on the first subscriber and camera:unsubscribe only when the last subscriber unmounts — preventing the grid cell from losing its stream when the fullscreen modal closes.
# Initialize after cloning loitering_tracking
git submodule update --init --recursive| Path | Repository | Branch |
|---|---|---|
submodules/WiseNetChromeIPInstaller |
github.com/melchi45/WiseNetChromeIPInstaller | nodejs-udp-discovery |
The nodejs-udp-discovery branch adds:
nodejs/udpDiscovery.js— Node.jsdgramport of Chromesockets.udpdiscoverynodejs/utils.js—ntohs/ntohl/bytes2inthelpersnodejs/package.json— Node.js module confignodejs/README.md— Usage instructions
- Support RTSP, RTMP, HTTP(S) input via FFmpeg
- Compatible with ONVIF-compliant IP cameras (WiseNet/Hanwha)
- Frame capture: 10 FPS (configurable: 1–30 FPS)
- Resolution support: 720p, 1080p (inference at 640×640)
- Hardware-accelerated decoding: NVDEC (NVIDIA), QSV (Intel), VA-API
| Metric | Minimum | Target |
|---|---|---|
| Detection Model | YOLOv8n ONNX | YOLOv8s ONNX |
| Person mAP@0.5 | ≥ 85% | ≥ 92% |
| Inference Latency | ≤ 50ms/frame (CPU) | ≤ 15ms/frame (GPU) |
| False Positive Rate | ≤ 5% | ≤ 2% |
- Algorithm: ByteTrack (JS implementation) with 8-dim Kalman Filter + ArcFace multi-cue association
- Persistent
objectIdassignment across frames; class hard-reject prevents cross-class ID inheritance - Track re-identification after occlusion (up to 90 frames = 9 s at 10 FPS)
- Tracking accuracy target: HOTA ≥ 60, MOTA ≥ 70 on MOT17 (benchmark pending)
- Protocol: Socket.IO over WebSocket
- Frame delivery: JPEG base64, target ≤ 100KB/frame at 10 FPS
- Detection data: JSON, delivered with each frame event
- Reconnect: automatic with exponential backoff
- Live multi-camera grid view (1/4/9/16 layout)
- Per-frame bounding boxes with
objectIdandconfidencescore - Loitering indicator: color change (green→yellow→red) with dwell timer
- Real-time loitering event log with camera thumbnail
- Zone drawing: polygon tool on canvas
- Alert history: filterable by camera, zone, time, severity
- Heatmap: dwell-time visualization per zone
- In-app alert with visual highlight and audio cue
- Webhook POST to configurable endpoint
- Email notification (nodemailer)
- Alert cool-down: configurable per zone (default 60s)
- UDP broadcast discovery of WiseNet cameras on LAN (one-click scan)
- Manual camera entry: RTSP URL, credentials
- Per-camera: stream start/stop, resolution, FPS override
- Connection status indicator (live/offline/error)
- Automatic clip save on loitering event (±30s buffer)
- H.264 MP4 stored in
storage/clips/ - Event log with metadata: cameraId, objectId, startTime, duration, clip path
- TLS 1.3 for all WebSocket and REST connections in production
- OWASP Top 10 compliance for web interface
- JWT Bearer token authentication for API and Socket.IO
- GDPR/PDPA: video retention policy, right-to-erasure API
- Minimum 4 concurrent channels on CPU-only server
- Minimum 16 concurrent channels with NVIDIA GPU
- Graceful degradation: alerting continues if UI disconnects
- Health check endpoint:
GET /api/health
- ESLint + Prettier code style enforcement
- Jest unit tests (≥ 70% coverage for core pipeline)
- GitHub Actions CI: lint → test → build
- Docker Compose for single-command local deployment
범례: ✅ Done — 완전 구현 ·
⚠️ Partial — 핵심 기능 구현, 일부 미완 · 🔲 Planned — 미착수
| Phase | Milestone | 구현 내역 | 미구현 / 잔여 | Status | Date |
|---|---|---|---|---|---|
| 1 | Project Setup | Repo 구조 · Docker Compose · 서브모듈(WiseNetChromeIPInstaller) · CI | — | ✅ Done | Mar 31, 2026 |
| 2 | UDP Discovery | Node.js UDP/ONVIF WS-Discovery(discoveryService.js, onvifDiscovery.js) · 카메라 목록 UI |
— | ✅ Done | Apr 7, 2026 |
| 3 | RTSP Capture | ingest_daemon.py(PyAV) + ingestDaemonCapture.js · 10FPS JPEG 파이프라인 · GStreamer/ffmpeg 폴백 |
— | ✅ Done | Apr 14, 2026 |
| 4 | AI Detection | detection.js YOLOv8/11/12 ONNX(15종) · letterbox/NMS · COCO 80-class · CUDA 가속 · 런타임 모델 전환 |
— | ✅ Done | Apr 28, 2026 |
| 5 | MOT Tracking | tracking.js ByteTrack · 8-dim KalmanFilter · NaN guard · maxAge=90 · ArcFace EMA |
— | ✅ Done | May 5, 2026 |
| 6 | React UI | CameraGrid · bbox 오버레이 · 전체화면 뷰 · 구역 편집기 · 감지 패널 · 다국어(15개) · 모바일 레이아웃 | — | ✅ Done | May 12, 2026 |
| 7 | Loitering Logic | behaviorEngine.js 슬라이딩 윈도우 변위 · pacing score · 5-factor 위험 점수 · 크로스 ID 전이 · 구역 트리거 |
— | ✅ Done | May 19, 2026 |
| 8 | Attribute Pipeline | SCRFD 얼굴 감지 · ArcFace Re-ID · PPE(안전모/마스크) · 색상/의상 분석(OpenPAR) · 화재/연기(YOLOv8s) | — | ✅ Done | May 19, 2026 |
| 9 | Integration | E2E 파이프라인 · combined/streaming/analysis 모드 · WebRTC(mediamtx/mediasoup) · YouTube 수집 · MCP 서버 | — | ✅ Done | May 28, 2026 |
| 10 | UAT & QA | HOTA/MOTA 벤치마크 · 회귀 테스트(KF/IoU/NaN) · 보안 감사 · test/ Jest 스위트 |
— | ✅ Done | May 28, 2026 |
| 11 | User Auth | JWT RS256 · bcrypt · RBAC(admin/operator/viewer) · refresh token 로테이션 · admin 승인 · AuditService · Google OAuth 2.0(passport-google-oauth20) · Microsoft OAuth(MSAL, @azure/msal-node) | 이메일 인증 · 2FA(TOTP) | ✅ Done | May 28, 2026 |
| 11-B | Missing Person | missingPersonService.js · ArcFace 임베딩 비교 · 실종자 등록/탐지/통계 · MCP 도구 5종 |
UI 뷰 보강 | ✅ Done | Jun 2026 |
| 12 | Video Recording | DVR/NVR 녹화 스케줄러 · 이전/이후 이벤트 클립 · 영상 재생 UI · 보존 정책 | 전체 미착수 | 🔲 Planned | Jul 28, 2026 |
| 12-A | Human Parsing 색상 분류 | SCHP/SegFormer 모델 카탈로그, 트랙 단위 캐시, K-Means 마스크 색상 추출 (Phase-3) 코드 구현 완료 — colorClothService.js#_runHumanParsing, kmeansColor.js(단위 테스트 완료); 기존 고정 ROI에 K-Means 대표색만 적용하는 모델 불필요 개선(Phase-1.5)은 미착수 — Design_AI_Color_Analysis.md §10, §11 |
모델 파일 미배포(자동 다운로드 비활성)·humanParsing 토글 기본 OFF·마스크 분류 동작 테스트 없음 |
🟡 In Progress | TBD |
| 12-B | Appearance/Body Re-ID 고도화 | OSNet 임베딩(appearanceReidService.js) + Qdrant appearance_embeddings 컬렉션(qdrantService.js) 코드 구현 완료, 실시간 매칭에 80/20 가중치 반영(pipelineManager.js#_weightedAppearSim), 색상 사전필터 검색(GET /api/search?types=appearance) — Design_AI_AppearanceReID.md §12 |
장시간 재등장 시 Qdrant 조회(kNN)는 미배선(upsert만 동작, 실시간 매칭은 여전히 5분 TTL 인메모리 갤러리만 조회) · OSNet 전처리 실제 모델 출력 대비 미검증 · 모델 파일/Qdrant 모두 기본 비활성(opt-in) · 테스트 없음 | 🟡 In Progress | TBD |
| 12-C | Age Estimation 정확도 개선 | InsightFace GenderAge/ViT Age Classifier 듀얼 모델 카탈로그·추론·4곳 화면 표시·DB 영속화 구현 완료; 2026-07-14 실사용 관측(나이가 대부분 ~35/20-29로 수렴)으로 HuggingFace preprocessor_config.json·deepinsight/insightface 소스 대조를 통해 확인된 전처리 버그 3건(Phase 1, ViT 정규화 상수·InsightFace 채널 순서(RGB↔BGR)·표준편차) 수정 완료, 회귀 테스트(TC-AGE-017) 통과 — Design_AI_Age_Estimation.md §13 |
Phase 2(genderage.onnx 그래프 내장 정규화 여부 실측 진단, TC-AGE-018) · Phase 3(5점 랜드마크 얼굴 정렬 TC-AGE-019, body-crop 신뢰도 표시 TC-AGE-020, 공용 전처리 유틸 추출) · Phase 4(참조 이미지 정확도 검증 세트) 전체 미착수 |
🟡 In Progress | TBD |
| 12-D | Gender Classification 정확도 개선 | InsightFace GenderAge(Age Estimation과 공유 파일)/ViT Gender Classifier 듀얼 모델, 양쪽 진입점(로컬 루프·/frame 핸들러) 최초 구현부터 완비; Age Estimation과 동일한 전처리 버그 3건이 독립 코드 사본(genderClassificationService.js)에도 존재함을 확인·수정(Phase 1 완료, TC-GEN-017 통과) — 실사용 관측: 실제 성비 50:50에 가까운데도 대부분 여성으로 분류 — Design_AI_Gender_Classification.md §13 |
Age Estimation과 동일한 Phase 2~4(그래프 진단 TC-GEN-018, 랜드마크 정렬 TC-GEN-019) 전체 미착수, 추가로 성별 신뢰도(confidence) 임계값 미도입(TC-GEN-020) | 🟡 In Progress | TBD |
| 13 | PTZ Control | ONVIF PTZ API(pan/tilt/zoom) · 원격 제어 UI 오버레이 · 프리셋 위치 관리 | 전체 미착수 | 🔲 Planned | Aug 11, 2026 |
| 14 | Notification Hub | Webhook ALERT_WEBHOOK_URL HTTP POST · Email nodemailer(SMTP_HOST/USER/PASS) · loitering 알림 발송 |
SMS · Slack · Teams 미착수 · 스로틀 규칙 UI 없음 | Aug 25, 2026 | |
| 15 | Heatmap & Path | — | 전체 미착수 (Loitering 가이드 §4 Track 좌표 누적→Heatmap 대응) | 🔲 Planned | Sep 8, 2026 |
| 16 | Advanced AI | — | 낙상 · 싸움 · 역주행 감지 전체 미착수 | 🔲 Planned | Sep 22, 2026 |
| 17 | Auto Reports | — | PDF · Excel · 이메일 예약 발송 전체 미착수 | 🔲 Planned | Oct 6, 2026 |
| 18 | Map Layout | — | 도면/위성맵 카메라 배치 전체 미착수 | 🔲 Planned | Oct 20, 2026 |
| 19 | Privacy & Audit | AuditService.js 불변 감사 로그(최대 10,000건) · RBAC 접근 이력 · /admin/audit UI |
얼굴 블러/익명화 미착수 · GDPR 삭제 API 미착수 | Nov 3, 2026 | |
| 20 | AI Model Mgmt | 전체 AI 모델 카탈로그(YOLO 감지기 20종 YOLOv8/11/12/26 n/s/m/l/x + 얼굴 감지·인식(SCRFD/ArcFace)·PPE·화재연기·의상PAR·Human Parsing·Appearance Re-ID 8종, 총 28종) · family별 독립 다운로드/전환(direct ONNX·HuggingFace PT 변환·수동export) · 런타임 모델 전환(family별 독립 hot-swap) · Admin UI AiModelsSection(family별 테이블) · 퍼채널 AI 모듈 활성화 | 커스텀 모델 업로드 미착수 · 카메라별 모델 배정 UI 미착수 · A/B 테스트 미착수 · SSE 실시간 진행률 미구현(2초 폴링으로 대체) | Nov 17, 2026 | |
| 21 | Deployment | Docker Compose 단일 명령 배포 · 프로덕션 로그(레벨 필터 · /var/log/lts) · npm run start/stop 프로세스 관리 · /admin/system CPU/메모리/GPU/디스크 메트릭 |
Prometheus /metrics 미착수 · OpenAPI 문서 미착수 · K8s/Helm 미착수(→ ES-8) |
Dec 1, 2026 |
섹션 2.10 현재 구현 상태 분석을 기반으로 도출한 엔터프라이즈 수준 확장 과제입니다.
우선순위는 좌측 순서(ES-1 → ES-8)이며, ES-1~ES-3 은 다중 노드 배포의 필수 전제 조건입니다.
| ID | Milestone | 상세 내용 | 현재 한계 | 상태 | 목표 |
|---|---|---|---|---|---|
| ES-1 | AI 분석 서버 풀 | ANALYSIS_SERVER_URLS 다중 URL 환경변수 추가; 라운드 로빈 / 최소 연결(Least Connections) 라우터; 서버별 독립 Circuit Breaker; 자동 페일오버(서버 제거 → 복구 시 재투입); 가중치 기반 라우팅(GPU 성능 차이 반영) |
ANALYSIS_SERVER_URL 단일 URL, AnalysisClient 인스턴스 1개 |
🔲 Planned | Q1 2027 |
| ES-2 | Socket.IO 멀티 노드 어댑터 | @socket.io/redis-adapter 도입; Redis Cluster Pub/Sub 기반 크로스 노드 이벤트 브로드캐스트; frameData, newAlert, cameraStatus, onvif:event 이벤트 전파; 어댑터 연결 실패 시 in-memory fallback |
Socket.IO in-memory 어댑터 — 다른 노드 구독 브라우저 이벤트 수신 불가 | 🔲 Planned | Q1 2027 |
| ES-3 | 분산 세션 스토어 | connect-redis 도입, express-session 스토어 교체; OAuth CSRF state 노드 간 공유; Passport serialize/deserialize 공유; Redis 연결 실패 시 MemoryStore fallback; TTL = 10분 (기존 OAuth 세션 동일) |
express-session MemoryStore — 다중 노드 시 세션 소멸, 재인증 필요 |
🔲 Planned | Q1 2027 |
| ES-4 | MongoDB 고가용성 튜닝 | 3-node Replica Set 운영 가이드; readPreference: secondaryPreferred 읽기 부하 분산; writeConcern: majority 데이터 안전성; 접속 풀 크기 최적화(MONGODB_POOL_SIZE); Replica Set URI 예제 및 자동 페일오버 검증; 선택적: MongoDB Atlas 자동 샤딩 |
Replica Set URI 지원되나 readPreference 미설정, 풀 크기 기본값 |
Q1 2027 | |
| ES-5 | 중앙 Admin Dashboard | 다중 스트리밍 서버 통합 관리 UI; 서버별 연결 상태 · 카메라 수 · 처리 FPS 실시간 모니터링; 전체 카메라 목록 및 서버 간 이동; 통합 사용자 관리(단일 MongoDB 전제); 통합 감사 로그 집계 뷰; 서버 추가/제거 UI | Admin Dashboard가 각 스트리밍 서버 단위, 전체 통합 뷰 없음 | 🔲 Planned | Q2 2027 |
| ES-6 | 카메라 자동 배정 / 이전 | 스트리밍 서버 부하 기반 카메라 자동 배정 알고리즘; 카메라 무중단 이전(drain → 재등록); 서버 과부하 시 자동 재분배; 카메라 발견(ONVIF) 시 최소 부하 서버 자동 선택; 배정 상태 중앙 DB 관리 | 카메라 → 서버 매핑 .env 수동 설정, 서버 간 이전 불가 |
🔲 Planned | Q2 2027 |
| ES-7 | 프로덕션 가시성 (Observability) | /metrics Prometheus 엔드포인트; 카메라별 처리 FPS · 큐 깊이 · 지연시간 메트릭; OpenTelemetry 분산 추적(Jaeger / Zipkin); ELK / Loki 중앙 로그 집계; Grafana 대시보드(카메라/서버/AI 메트릭); SLA 알림(분석 서버 응답 지연 > 2s) |
/admin/system CPU/메모리 제공, Prometheus / 분산 추적 없음 |
🔲 Planned | Q2 2027 |
| ES-8 | Kubernetes / Helm 배포 | Helm Chart(streaming / analysis / mcp-server 분리); HPA(Horizontal Pod Autoscaler — 카메라 수 기반 스케일); GPU NodeSelector(ONNX_CUDA=1 analysis pod); PersistentVolume(모델 파일 공유); K8s ConfigMap / Secret(.env 대체); mediamtx StatefulSet(ICE UDP NodePort); Readiness / Liveness probe 연동 |
Docker Compose 단일 호스트, K8s 미지원 | 🔲 Planned | Q3 2027 |
# System
Node.js 18+ LTS
FFmpeg 6.x (with libx264, libx265)
Python 3.10+ (for ONNX model export only)
# Optional (GPU acceleration)
NVIDIA GPU + CUDA 12.x + cuDNN 8.x
# Windows ONNX acceleration
DirectML (DML) is auto-selected on Windows when ONNX_CUDA=0.
See: docs/ops/ONNX_Runtime_Provider_Diagnostics.md- The server performs a one-time ONNX provider diagnostics check at startup.
- It logs
listSupportedBackends()once and pre-disables unavailable CUDA/DML providers. - On Windows with
ONNX_CUDA=0, runtime preference is['dml','cpu']. - If DML is unavailable, inference falls back to CPU and avoids repeated provider warnings.
- Reference:
docs/ops/ONNX_Runtime_Provider_Diagnostics.md
The setup scripts auto-detect Node.js, Python 3, and FFmpeg.
If any tool is missing, they attempt to install it automatically (via winget on Windows, apt/dnf/brew on Linux/macOS) and then generate server/.env with the correct OS-specific binary paths.
# Run from project root
powershell -ExecutionPolicy Bypass -File server/src/scripts/setup-env.windows.ps1
# Or via npm (from server/ directory)
cd server
npm run setup-env:windowsWhat it does:
| Step | Action |
|---|---|
| 1 | Detects node — installs Node.js LTS via winget if missing |
| 2 | Detects python — installs Python 3.12 via winget if missing |
| 3 | Detects ffmpeg — installs FFmpeg via winget if missing |
| 4 | Creates server/.env from .env.example (skips if already exists) |
| 5 | Patches SERVER_RUNTIME_OS, NODE_EXEC_WINDOWS, PYTHON_EXEC_WINDOWS, PYAV_PYTHON_BIN_WINDOWS with resolved paths |
# Run from project root
bash server/src/scripts/setup-env.linux.sh
# Or via npm (from server/ directory)
cd server
npm run setup-env:linuxWhat it does:
| Step | Action |
|---|---|
| 1 | Detects node — installs via apt / dnf / brew / NodeSource if missing |
| 2 | Detects python3 — installs via system package manager if missing |
| 3 | Detects ffmpeg — installs via system package manager if missing |
| 4 | Creates server/.env from .env.example (skips if already exists) |
| 5 | Patches SERVER_RUNTIME_OS, NODE_EXEC_LINUX, PYTHON_EXEC_LINUX, PYAV_PYTHON_BIN_LINUX with resolved paths |
The setup scripts write the following variables into server/.env:
# OS detection (auto / windows / linux)
SERVER_RUNTIME_OS=windows # or: linux
# Node.js binary path
NODE_EXEC_WINDOWS=C:\Program Files\nodejs\node.exe
NODE_EXEC_LINUX=/usr/bin/node
# Python binary path
PYTHON_EXEC_WINDOWS=C:\Users\<user>\AppData\Local\Programs\Python\Python312\python.exe
PYTHON_EXEC_LINUX=/usr/bin/python3
# PyAV sidecar Python binary (overrides PYTHON_EXEC if set separately)
PYAV_PYTHON_BIN_WINDOWS=python
PYAV_PYTHON_BIN_LINUX=/usr/bin/python3All variables fall back to the cross-OS
NODE_EXEC/PYTHON_EXECif the OS-specific key is empty.
After environment setup, download or export the required ONNX model files:
cd server
npm run download-models:windowsScript: server/src/scripts/downloadModels.windows.ps1
| Step | Action |
|---|---|
| 1 | Runs node src/scripts/downloadModels.js to download direct ONNX models (yolov8n.onnx, scrfd_2.5g.onnx, arcface_w600k_r50.onnx) |
| 2 | Resolves Python path from PYTHON_EXEC / PYTHON_EXEC_WINDOWS / PYTHON env variables |
| 3 | Installs ultralytics huggingface_hub torch torchvision onnx via pip |
| 4 | Exports yolov8m_ppe.onnx (PPE/Helmet detection) from HuggingFace |
| 5 | Exports yolov8s_fire_smoke.onnx (Fire & Smoke detection) from HuggingFace |
| 6 | Exports openpar.onnx (Cloth attribute recognition) via exportPAR.py |
Set LTS_SKIP_PYTHON_EXPORT=1 in .env to skip steps 3–6 (download only).
cd server
npm run download-models:linuxScript: server/src/scripts/downloadModels.linux.sh
Identical steps to the Windows script, using PYTHON_EXEC_LINUX / PYTHON_EXEC for Python path resolution.
=== Model Status ===
✓ yolov8n.onnx AI-01/02 Person+Vehicle (required)
✓ scrfd_2.5g.onnx AI-03 Face Detection
✓ arcface_w600k_r50.onnx AI-03 Face Recognition
✗ yolov8m_ppe.onnx AI-04 Mask + AI-07 Helmet (Python export)
✗ openpar.onnx AI-05 Color + AI-06 Cloth (Python export)
✗ yolov8s_fire_smoke.onnx AI-09 Fire & Smoke (Python export)
# Clone with submodules
git clone --recurse-submodules https://github.com/melchi45/loitering_tracking.git
cd loitering_tracking
# Install backend dependencies
cd server && npm install
# Install frontend dependencies
cd ../client && npm install
# Download AI models
cd ../server
mkdir -p models
wget https://github.com/ultralytics/assets/releases/download/v0.0.0/yolov8n.onnx \
-O models/yolov8n.onnxTip: Use the automated setup scripts described in 15.1.1 to generate
server/.envwith correct paths detected automatically.
Manual copy is still available as a fallback:
# Windows
powershell -ExecutionPolicy Bypass -File server/src/scripts/setup-env.windows.ps1
# Linux / macOS
bash server/src/scripts/setup-env.linux.sh
# Manual fallback (then edit paths by hand)
cp server/.env.example server/.env
# Edit server/.env:
# PORT=3080
# RTSP_DEFAULT_USERNAME=admin
# RTSP_DEFAULT_PASSWORD=
# YOLO_MODEL=models/yolov8n.onnx
# CONFIDENCE_THRESHOLD=0.30
# LOITERING_THRESHOLD_SEC=30
# JWT_SECRET=your-secret-keyUse server/.env to configure OAuth credentials. Keep secrets out of source control.
# OAuth callback base URL (must match provider redirect URI registration)
OAUTH_CALLBACK_BASE=https://localhost:3443
# Google OAuth 2.0
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# Microsoft Entra ID (Azure AD)
MICROSOFT_CLIENT_ID=
MICROSOFT_CLIENT_SECRET=
MICROSOFT_TENANT_ID=commonGoogle setup:
- Open Google Cloud Console → APIs & Services → Credentials.
- Create OAuth client (Web application).
- Register redirect URI:
{OAUTH_CALLBACK_BASE}/auth/google/callback. - Copy the issued Client ID/Client Secret into
GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET.
Microsoft setup:
- Open Azure Portal → Microsoft Entra ID → App registrations.
- Create a new app registration and add Web redirect URI.
- Register redirect URI:
{OAUTH_CALLBACK_BASE}/auth/microsoft/callback. - Create a client secret under Certificates & secrets.
- Copy values into
MICROSOFT_CLIENT_ID/MICROSOFT_CLIENT_SECRET. - Set
MICROSOFT_TENANT_ID(commonfor multi-tenant, or your tenant GUID for single-tenant).
Credential handling policy:
- Never commit real
CLIENT_SECRETvalues to Git. - Keep
server/.env.examplewith empty placeholders only. - In production, inject secrets via your deployment secret manager (for example, GitHub Actions Secrets, Docker secrets, Kubernetes secrets, or cloud secret stores).
- If a secret is exposed, rotate it immediately in the provider console and update deployment secrets.
Quick validation checklist:
- Callback URL protocol/host/port exactly matches
OAUTH_CALLBACK_BASE. - Provider redirect URIs exactly match callback paths.
CLIENT_ORIGINincludes the frontend origin used for sign-in.- Server restarted after
.envchanges.
# Server mode — affects ONNX thread count
# development : 1 intra-op thread per session (minimal CPU usage)
# production : auto (max(2, min(8, CPU_cores / 2)))
NODE_ENV=development
# CUDA execution provider
# 0 = CPU-only | 1 = cuda+cpu (requires CUDA-enabled onnxruntime-node)
ONNX_CUDA=0
# Per-mode intra-op thread count (0 = auto)
ONNX_THREADS_DEV=1 # when NODE_ENV=development
ONNX_THREADS_CUDA=1 # when ONNX_CUDA=1 (GPU handles parallelism)
ONNX_THREADS_PROD=0 # production: 0 → max(2, min(8, CPU_cores/2))nodemon (npm run dev) automatically sets NODE_ENV=development via nodemon.json — no manual export needed.
# Development
cd server && npm run dev # HTTP :3080 + HTTPS :3443 (HTTPS_ENABLED=true)
cd client && npm run dev # React dev server on :3080
# Production (Docker)
docker-compose up -d
# → React UI: http://localhost:3000
# → API: https://localhost:3443/apiCurrent defaults (see
server/.env):
HTTPS_ENABLED=true— API served on:3443(TLS)DB_TYPE=mongodb— primary storage is MongoDB 5.0 (ltsdatabase)AUTH_ENABLED=true— all API endpoints require a valid JWT; log in at/auth/loginNODE_ENV=development
The LTS MCP Server exposes Tools and Resources so that MCP-compatible LLMs (Claude Code, Claude API, OpenAI Agents, etc.) can interact with the system in natural language.
→ Full setup guide: docs/ops/MCP_Server_Setup.md
By default the server persists data to server/storage/lts.json. To switch to MongoDB 5.0 as the primary database:
→ Full setup guide: docs/ops/MongoDB_Setup.md
By default the server runs over plain HTTP (port 3080). Enable HTTPS by setting HTTPS_ENABLED=true in server/.env.
→ Full setup guide: docs/ops/HTTPS_TLS_Setup.md
| Variable | Default | Description |
|---|---|---|
HTTPS_ENABLED |
false |
Set true to start server over TLS |
HTTPS_PORT |
3443 |
TLS listen port (use 443 with root or CAP_NET_BIND_SERVICE) |
SSL_CERT_PATH |
./certs/server.crt |
PEM certificate path, relative to server/ |
SSL_KEY_PATH |
./certs/server.key |
PEM private key path, relative to server/ |
SSL_CA_PATH |
(empty) | Optional CA/intermediate bundle (for private CAs) |
HTTP_REDIRECT |
false |
Set true to issue HTTP 301 → HTTPS on plain PORT |
Suitable for local development and LAN deployments where a browser security warning is acceptable.
# 1. Create certs directory (git-ignored)
mkdir -p server/certs
# 2. Generate self-signed cert valid 365 days
# -addext adds SAN so Chrome/Firefox don't reject it
openssl req -x509 -newkey rsa:2048 -nodes -days 365 \
-subj "/CN=localhost" \
-addext "subjectAltName=IP:127.0.0.1,IP:192.168.214.3,DNS:localhost" \
-keyout server/certs/server.key \
-out server/certs/server.crt
# 3. Restrict key permissions
chmod 600 server/certs/server.key
# 4. Enable HTTPS in server/.env
# HTTPS_ENABLED=true
# HTTPS_PORT=3443
# SSL_CERT_PATH=./certs/server.crt
# SSL_KEY_PATH=./certs/server.key
# 5. Start server
cd server && npm start
# 6. Verify
curl -k https://localhost:3443/health
# → {"status":"ok"}Browser trust (optional): Add
server/certs/server.crtto your OS/browser trust store to eliminate the "Not secure" warning.
On Ubuntu:sudo cp server/certs/server.crt /usr/local/share/ca-certificates/ && sudo update-ca-certificates
mkcert creates locally trusted certificates with no browser warning — ideal for development.
# Install mkcert
sudo apt install mkcert # Debian/Ubuntu
# or: brew install mkcert # macOS
# Install the local CA into your system trust store
mkcert -install
# Generate cert for localhost + LAN IP
mkdir -p server/certs
cd server/certs
mkcert localhost 127.0.0.1 192.168.214.3
# mkcert creates: localhost+2.pem localhost+2-key.pem
# Update server/.env:
# SSL_CERT_PATH=./certs/localhost+2.pem
# SSL_KEY_PATH=./certs/localhost+2-key.pem
# HTTPS_ENABLED=trueFor public-facing servers with a registered domain name.
# Install certbot
sudo apt install certbot
# Obtain certificate (standalone mode — briefly binds port 80)
sudo certbot certonly --standalone -d lts.example.com
# Certs stored at:
# /etc/letsencrypt/live/lts.example.com/fullchain.pem ← SSL_CERT_PATH
# /etc/letsencrypt/live/lts.example.com/privkey.pem ← SSL_KEY_PATH
# server/.env
HTTPS_ENABLED=true
HTTPS_PORT=443
SSL_CERT_PATH=/etc/letsencrypt/live/lts.example.com/fullchain.pem
SSL_KEY_PATH=/etc/letsencrypt/live/lts.example.com/privkey.pem
HTTP_REDIRECT=true
# Auto-renewal (certbot renew hook — restart server after renewal)
sudo crontab -e
# Add: 0 3 * * * certbot renew --quiet && cd /opt/lts/server && npm run restartKeep Node.js on plain HTTP and let nginx/Caddy handle TLS. This is the most operationally robust approach for production.
[Browser] ──HTTPS:443──▶ [nginx / Caddy] ──HTTP:3001──▶ [LTS Node.js]
nginx example (/etc/nginx/sites-available/lts):
server {
listen 443 ssl http2;
server_name lts.example.com;
ssl_certificate /etc/letsencrypt/live/lts.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/lts.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# REST API + Socket.IO (WebSocket upgrade required)
location / {
proxy_pass http://localhost:3080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
server {
listen 80;
server_name lts.example.com;
return 301 https://$host$request_uri;
}Caddy example (/etc/caddy/Caddyfile):
lts.example.com {
reverse_proxy localhost:3080
}
Caddy obtains and renews Let's Encrypt certificates automatically.
When HTTPS_ENABLED=true and HTTP_REDIRECT=true, the server spawns a secondary HTTP listener on PORT (3001) that redirects all requests to https://<hostname>:<HTTPS_PORT><path>.
# server/.env
HTTPS_ENABLED=true
HTTPS_PORT=3443
HTTP_REDIRECT=true
# Result:
# http://server:3001/api/cameras → 301 → https://server:3443/api/cameras# docker-compose.yml
services:
server:
build: ./server
ports:
- "3443:3443" # HTTPS
- "3080:3080" # HTTP redirect (optional)
volumes:
- ./server/certs:/app/server/certs:ro # mount certs read-only
environment:
- HTTPS_ENABLED=true
- HTTPS_PORT=3443
- SSL_CERT_PATH=./certs/server.crt
- SSL_KEY_PATH=./certs/server.key
- HTTP_REDIRECT=trueWhen the server runs in HTTPS mode, update the Vite proxy target in client/vite.config.ts:
proxy: {
'/api': {
target: 'https://localhost:3443', // ← change from http://localhost:3080
changeOrigin: true,
secure: false, // allow self-signed certs in development
},
'/socket.io': {
target: 'https://localhost:3443',
changeOrigin: true,
ws: true,
secure: false,
},
},# Health check (self-signed: use -k to skip cert verification)
curl -k https://localhost:3443/health
# Verify TLS details
openssl s_client -connect localhost:3443 < /dev/null 2>&1 | grep -E "Protocol|Cipher|subject"
# Test HSTS header
curl -kI https://localhost:3443/health | grep -i strict
# Run HTTPS test suite
node test/api/https_tls.test.js
# With HTTPS mode:
LTS_HTTPS_URL=https://localhost:3443 node test/api/https_tls.test.js| Method | Path | Description |
|---|---|---|
GET |
/api/health |
System health check |
GET |
/api/cameras |
List all cameras |
POST |
/api/cameras/discover |
Trigger UDP broadcast discovery |
POST |
/api/cameras |
Add camera (manual RTSP URL) |
DELETE |
/api/cameras/:id |
Remove camera |
POST |
/api/cameras/:id/stream/start |
Start RTSP capture |
POST |
/api/cameras/:id/stream/stop |
Stop RTSP capture |
GET |
/api/cameras/:id/zones |
Get zones for camera |
POST |
/api/cameras/:id/zones |
Create zone |
PUT |
/api/cameras/:id/zones/:zoneId |
Update zone (polygon, name, targetClasses, etc.) |
DELETE |
/api/cameras/:id/zones/:zoneId |
Delete zone |
GET |
/api/events |
List loitering events |
GET |
/api/events/:id/clip |
Download event video clip |
{
"id": "uuid",
"cameraId": "cam-01",
"name": "Entry Zone",
"type": "MONITOR",
"polygon": [{"x": 100, "y": 150}, {"x": 400, "y": 150}, {"x": 400, "y": 500}, {"x": 100, "y": 500}],
"dwellThreshold": 30,
"minDisplacement": 50,
"reentryWindow": 120,
"targetClasses": ["human"],
"active": true
}targetClasses values: "human" (person), "vehicle" (bicycle/car/motorcycle/bus/truck). Empty array [] = all classes.
| Event | Direction | Payload | Description |
|---|---|---|---|
frame |
Server→Client | { cameraId, frameId, timestamp, data: base64jpeg } |
Raw annotated frame |
detections |
Server→Client | { cameraId, frameId, timestamp, detections: [...] } |
Detection + tracking results |
alert |
Server→Client | { cameraId, objectId, zone, dwellTime, timestamp } |
Loitering alert |
camera:status |
Server→Client | `{ cameraId, status: 'live' | 'offline' |
camera:subscribe |
Client→Server | { cameraId } |
Subscribe to camera stream |
camera:unsubscribe |
Client→Server | { cameraId } |
Unsubscribe |
discovery:start |
Client→Server | {} |
Trigger UDP camera discovery |
discovery:result |
Server→Client | { cameras: [...] } |
Discovery results |
| Term | Definition |
|---|---|
| Loitering | Remaining in a location longer than a configured threshold without apparent purpose |
| ObjectId | Persistent UUID assigned to a tracked object across frames by ByteTrack |
| Confidence | Detection confidence score (0.0–1.0) from YOLOv8n for each bounding box |
| Bounding Box | Rectangle {x, y, width, height} in pixel coordinates enclosing a detected object |
| ByteTrack | Multi-object tracking algorithm using low-confidence detections for occlusion recovery |
| RTSP | Real Time Streaming Protocol — standard for IP camera video streaming |
| ONVIF | Open Network Video Interface Forum — IP camera interoperability standard |
| WiseNet | Hanwha Vision brand for IP cameras and surveillance equipment |
| SUNAPI | Samsung/Hanwha camera HTTP API for camera control |
| UDP Broadcast | Network broadcast to 255.255.255.255 for device discovery on LAN |
| MOT | Multi-Object Tracking — tracking multiple objects simultaneously across frames |
| NMS | Non-Maximum Suppression — removes duplicate detection boxes |
| ONNX | Open Neural Network Exchange — cross-platform AI model format |
| targetClasses | Per-zone array of AI detection targets: human, vehicle, and planned attributes |
| JPEG SOF | Start-of-Frame JPEG marker — used to extract image dimensions without full decode |
| fwEff / fhEff | Effective frame width/height in ZoneEditor — read from img.naturalWidth/Height to match object-contain layout |
loitering_tracking/
├── README.md # Root system guide
├── docker-compose.yml # Full stack deployment
├── mediamtx.yml # RTSP relay configuration
├── package.json # Workspace scripts
├── yolov8s.pt # Source model asset
├── .gitmodules # Submodule configuration
├── client/ # React frontend (Vite + TS)
│ ├── src/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── i18n/
│ │ ├── stores/
│ │ └── types/
│ └── dist/ # Build output
├── server/ # Node.js backend
│ ├── src/ # APIs, services, sockets, scripts
│ ├── models/ # ONNX/PT model files
│ ├── certs/ # TLS certificates
│ └── storage/ # Runtime data (server-scoped)
├── mcp-server/ # MCP bridge server
│ ├── test/
│ └── tools/
├── docs/ # SDLC and specification documents
│ ├── README.md # Docs index
│ ├── development_process.md # SDLC gate workflow source
│ ├── rfp/ # RFP documents
│ ├── prd/ # PRD documents
│ ├── srs/ # SRS documents
│ ├── design/ # Design documents
│ ├── tc/ # Test case documents
│ └── screenshots/ # Documentation images
├── test/ # Automated test suites
│ ├── api/
│ ├── integration/
│ ├── e2e/
│ ├── fixtures/
│ └── reports/
├── storage/ # Root-level shared storage artifacts
├── submodules/
│ └── WiseNetChromeIPInstaller/
├── .claude/ # Local agent settings
├── .vscode/ # Workspace editor settings
└── .github/
└── workflows/
└── ci.yml # Lint + test + build
All RFP and PRD documents are stored in the docs/ folder.
| RFP | PRD | Reference | Description |
|---|---|---|---|
| RFP_LTS2026_Loitering_Tracking_System.md | PRD_LTS2026_Loitering_Tracking_System.md | LTS-2026-001 | Full loitering detection system |
| RFP_Object_Tracking.md | PRD_Object_Tracking.md | OTS-2026-001 | Object tracking: KF, multi-cue association |
| RFP_Camera_Discovery.md | PRD_Camera_Discovery.md | LTS-2026-002 | Camera discovery: UDP broadcast + ONVIF |
| RFP_CrossCamera_Face_Tracking.md | PRD_CrossCamera_Face_Tracking.md | — | Cross-camera face tracking & Re-ID |
| RFP_Ideal_Proposal.md | PRD_Ideal_Proposal.md | — | Ideal system proposal reference |
| RFP | PRD | Reference | Description |
|---|---|---|---|
| RFP_AI_Human_Detection.md | PRD_AI_Human_Detection.md | AI-01 | Person detection — YOLOv8n COCO class 0 |
| RFP_AI_Vehicle_Detection.md | PRD_AI_Vehicle_Detection.md | AI-02 | Vehicle detection — bicycle/car/motorcycle/bus/truck |
| RFP_AI_Face_Recognition.md | PRD_AI_Face_Recognition.md | AI-03 | Face detection + ArcFace Re-ID |
| RFP_AI_Mask_Detection.md | PRD_AI_Mask_Detection.md | AI-04 | Mask detection — YOLOv8m PPE |
| RFP_AI_Color_Analysis.md | PRD_AI_Color_Analysis.md | AI-05 | Upper/lower body color analysis |
| RFP_AI_Cloth_Analysis.md | PRD_AI_Cloth_Analysis.md | AI-06 | Clothing type classification — OpenPAR |
| RFP_AI_Hat_Detection.md | PRD_AI_Hat_Detection.md | AI-07 | Helmet/hat detection — YOLOv8m PPE |
| RFP_AI_Accessories_Detection.md | PRD_AI_Accessories_Detection.md | AI-08 | Accessories detection — YOLOv8n COCO |
| RFP_AI_Fire_Smoke_Detection.md | PRD_AI_Fire_Smoke_Detection.md | AI-09 | Fire & smoke detection — YOLOv8s |
| RFP_AI_Animal_Detection.md | PRD_AI_Animal_Detection.md | AI-10 | Animal detection |
| RFP | PRD | Description |
|---|---|---|
| RFP_LTS2026_YouTube_RTSP_Ingest.md | PRD_LTS2026_YouTube_RTSP_Ingest.md | YouTube + RTSP stream ingestion (LTS integration) |
| RFP_YouTube_RTSP_Ingest.md | PRD_YouTube_RTSP_Ingest.md | YouTube / RTSP ingest module |
| RFP_WebRTC_Media_Gateway.md | PRD_WebRTC_Media_Gateway.md | WebRTC media gateway (mediasoup) |
| RFP_STUN_TURN_ICE.md | PRD_STUN_TURN_ICE.md | STUN / TURN / ICE configuration |
| RFP | PRD | Description |
|---|---|---|
| RFP_Dashboard_Layout.md | PRD_Dashboard_Layout.md | Main dashboard grid layout |
| RFP_Dashboard_Detection_Display.md | PRD_Dashboard_Detection_Display.md | Detection bounding-box display |
| RFP_Dashboard_Sidebar_Alerts_Zones.md | PRD_Dashboard_Sidebar_Alerts_Zones.md | Alerts & zones sidebar panel |
| RFP_Dashboard_Sidebar_Cameras.md | PRD_Dashboard_Sidebar_Cameras.md | Camera management sidebar |
| RFP_Mobile_Layout.md | PRD_Mobile_Layout.md | Mobile responsive layout |
| RFP | PRD | Description |
|---|---|---|
| RFP_LLM_MCP_Integration.md | PRD_LLM_MCP_Server.md | LLM integration via Model Context Protocol (MCP) |
This section consolidates the development lifecycle in a stage-gate format from requirements to service stabilization.
Lifecycle sequence:
MRD -> DIA -> DV -> DVR -> PIA -> PV -> PVR -> PR -> PRA -> SR -> SRA
| Gate | Name | Purpose | Main Deliverables |
|---|---|---|---|
| 1 | MRD (Market Requirements) | Define market and customer needs | MRD, optional draft RFP |
| 2 | DIA (Design Input Approval) | Convert needs into implementable requirements | PRD, official RFP, draft SRS |
| 3 | DV (Design Verification) | Define and verify design | Final SRS, design docs (HLD/LLD), test case design |
| 4 | DVR (Design Verification Report) | Report design verification results | Design verification report, partial test evidence |
| 5 | PIA (Production Input Approval) | Prepare for production operations | Deployment/operations design docs, environment configuration docs |
| 6 | PV (Production Verification) | Execute tests in real environments | Test execution results, integration test report, performance report, defect list, UAT results |
| 7 | PVR (Production Validation Report) | Evaluate overall quality from test outcomes | PVR report, consolidated test report, residual issues list, quality assessment report |
| 8 | PR (Production Release) | Approve release plan | Release plan, approval artifacts |
| 9 | PRA (Production Release Approval) | Final launch approval | Final approval artifacts |
| 10 | SR (Service Release) | Perform deployment to service | Release notes, deployment plan, rollback plan, deployment checklist |
| 11 | SRA (Service Release Approval) | Validate post-release stability and hand over to operations | Release validation report, smoke test results, operational stability report, go-live approval |
Core document flow:
MRD -> PRD -> SRS -> Design -> TC -> Test -> Release Notes
One-line summary:
The process moves through requirement definition, design, verification, production validation, release, and stabilization, with explicit artifacts at each stage.
END OF DOCUMENT — LTS-2026-001
For enquiries, open an issue at github.com/melchi45/loitering_tracking
CONFIDENTIAL | melchi45/loitering_tracking